Files
OpenCode 68a9071cf1 feat: reconcile local master with origin (exa/hot-platform hunters, leaks ledger, latest results)
Local branch had diverged from origin/master (sibling commits on the same base). Rewrote local history linearly on top of origin/master, folding in all local content: exa/hot-platform discovery hunters, leaks ledger, nightly sweep orchestrator, updated .gitignore and skill docs, plus the latest hunt output and vault state. Remote-only files (vault keys, channel scripts) were restored rather than dropped, so the resulting tree is a full union of both sides.
2026-08-07 12:53:47 +08:00

1 line
7.7 MiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"repo": "agentscope-ai/agentteams", "files": {"install/agentteams-install.sh": {"2ea027403398dfa06f3fc86445042d59f4684d71": {"t": 1785674200.821352, "n": 249717, "h": "d0ccc9f2f7452238", "c": "#!/bin/bash\n# agentteams-install.sh - One-click installation for AgentTeams Manager, Worker, and Dashboard\n#\n# Usage:\n# ./agentteams-install.sh # Interactive installation (choose Quick Start or Manual)\n# ./agentteams-install.sh manager # Same as above (explicit)\n# ./agentteams-install.sh worker --name <name> ... # Worker installation\n# ./agentteams-install.sh dashboard # Start/stop agentteams-dashboard (AGENTTEAMS_DASHBOARD=0 to stop)\n# ./agentteams-install.sh uninstall # Stop and remove Manager + all Workers\n#\n# NOTE: agentteams-dashboard is currently only supported via the Bash installer.\n# The PowerShell installer (agentteams-install.ps1) does not yet include dashboard support.\n#\n# Onboarding Modes:\n# Quick Start - Fast installation with all default values (recommended)\n# Manual - Customize each option step by step\n#\n# Environment variables (for automation):\n# AGENTTEAMS_NON_INTERACTIVE Skip all prompts, use defaults (default: 0)\n# AGENTTEAMS_LLM_PROVIDER LLM provider (default: openai-compat for zh non-interactive Token Plan; qwen for en)\n# AGENTTEAMS_DEFAULT_MODEL Default model (default: qwen3.6-plus for zh Token Plan and en non-interactive)\n# AGENTTEAMS_OPENAI_BASE_URL OpenAI-compatible base URL (default for zh non-interactive: Alibaba Token Plan endpoint)\n# AGENTTEAMS_LLM_API_KEY LLM API key (required)\n# AGENTTEAMS_ADMIN_USER Admin username (default: admin)\n# AGENTTEAMS_ADMIN_PASSWORD Admin password (auto-generated if not set, min 8 chars)\n# AGENTTEAMS_MATRIX_DOMAIN Matrix domain (default: matrix-local.agentteams.io:18080)\n# AGENTTEAMS_MOUNT_SOCKET Mount container runtime socket (default: 1)\n# AGENTTEAMS_DATA_DIR Docker volume name for persistent data (default: agentteams-data)\n# AGENTTEAMS_WORKSPACE_DIR Host directory for manager workspace (default: ~/agentteams-manager)\n# AGENTTEAMS_VERSION Image tag (default: latest)\n# AGENTTEAMS_REGISTRY Image registry (default: auto-detected by timezone)\n# AGENTTEAMS_INSTALL_MANAGER_IMAGE Override manager image (e.g., local build)\n# AGENTTEAMS_INSTALL_WORKER_IMAGE Override worker image (e.g., local build)\n# AGENTTEAMS_INSTALL_COPAW_WORKER_IMAGE Override copaw worker image (e.g., local build)\n# AGENTTEAMS_INSTALL_HERMES_WORKER_IMAGE Override hermes worker image (e.g., local build)\n# AGENTTEAMS_NACOS_REGISTRY_URI Default Nacos registry URI for Worker market search/import\n# (default: nacos://market.agentteams.io:80/public)\n# AGENTTEAMS_NACOS_USERNAME Default Nacos username for nacos:// package imports (optional)\n# AGENTTEAMS_NACOS_PASSWORD Default Nacos password for nacos:// package imports (optional)\n# AGENTTEAMS_CMS_TRACES_ENABLED Enable openclaw-cms-plugin traces for Manager AND all Workers (default: false)\n# AGENTTEAMS_CMS_ENDPOINT ARMS OTLP endpoint (required if traces enabled)\n# AGENTTEAMS_CMS_LICENSE_KEY CMS license key (required if traces enabled)\n# AGENTTEAMS_CMS_PROJECT CMS project name (optional)\n# AGENTTEAMS_CMS_WORKSPACE CMS workspace ID (required if traces enabled)\n# AGENTTEAMS_CMS_SERVICE_NAME Manager service name in ARMS (default: agentteams-manager)\n# Workers always report as agentteams-worker-<name> automatically\n# AGENTTEAMS_CMS_METRICS_ENABLED Enable diagnostics-otel metrics for Manager AND all Workers (default: false)\n# AGENTTEAMS_PORT_GATEWAY Host port for Higress gateway (default: 18080)\n# AGENTTEAMS_PORT_CONSOLE Host port for Higress console (default: 18001)\n# AGENTTEAMS_PORT_ELEMENT_WEB Host port for Element Web direct access (default: 18088)\n# AGENTTEAMS_PORT_MANAGER_CONSOLE Host port for Manager console (default: 18888)\n# AGENTTEAMS_WORKER_IDLE_TIMEOUT Worker idle timeout in minutes (default: 720, i.e. 12 hours)\n# AGENTTEAMS_DASHBOARD Install agentteams-dashboard management UI (default: 1)\n# AGENTTEAMS_DASHBOARD_VERSION Dashboard version (default: v1.2.0, independent of AgentTeams version)\n# AGENTTEAMS_PORT_DASHBOARD Dashboard host port (default: 13000)\n# AGENTTEAMS_DASHBOARD_IMAGE Override dashboard image (default: <registry>/agentteams/agentteams-dashboard:<DASHBOARD_VERSION>)\n# AGENTTEAMS_AI_GATEWAY_ADMIN_URL Higress Console URL for shared auth (auto-detected)\n\nset -e\n\nAGENTTEAMS_VERSION=\"${AGENTTEAMS_VERSION:-}\"\nAGENTTEAMS_KNOWN_STABLE_VERSION=\"v1.2.0\" # fallback if GitHub API is unreachable\n\n_normalize_version() {\n local version=\"$1\"\n case \"${version}\" in\n [0-9]*.[0-9]*.[0-9]*) version=\"v${version}\" ;;\n esac\n case \"${version}\" in\n v[0-9]*.[0-9]*.[0-9]*.beta.[0-9]*) version=\"${version/.beta./-beta.}\" ;;\n esac\n printf '%s' \"${version}\"\n}\n\n# Returns 0 (true) if $1 < $2 using semver order; \"latest\" is treated as greatest\n_ver_lt() {\n [ \"$1\" = \"latest\" ] && return 1\n [ \"$2\" = \"latest\" ] && return 0\n [ \"$1\" = \"$2\" ] && return 1\n [ \"$(printf '%s\\n%s' \"$1\" \"$2\" | sort -V | head -1)\" = \"$1\" ]\n}\n\n_use_legacy_image_env() {\n if [ \"$1\" = \"latest\" ]; then\n _ver_lt \"${AGENTTEAMS_KNOWN_STABLE_VERSION}\" \"v1.2.0\"\n else\n _ver_lt \"$1\" \"v1.2.0\"\n fi\n}\n\n_controller_env_prefix() {\n if _use_legacy_image_env \"$1\"; then\n printf '%s%s' 'HIC' 'LAW_'\n else\n printf '%s' 'AGENTTEAMS_'\n fi\n}\n\n_controller_storage_prefix() {\n if _use_legacy_image_env \"$1\"; then\n printf '%s%s' 'hic' 'law/agentteams-storage'\n else\n printf '%s' 'agentteams/agentteams-storage'\n fi\n}\nAGENTTEAMS_NON_INTERACTIVE=\"${AGENTTEAMS_NON_INTERACTIVE:-0}\"\nAGENTTEAMS_MOUNT_SOCKET=\"${AGENTTEAMS_MOUNT_SOCKET:-1}\"\nAGENTTEAMS_DOCKER_PROXY=\"${AGENTTEAMS_DOCKER_PROXY:-1}\"\nSTEP_RESULT=\"\" # Used by state machine to signal \"back\" navigation\n\n# ============================================================\n# Log all output to file\n# ============================================================\n\nAGENTTEAMS_LOG_FILE=\"${HOME}/agentteams-install.log\"\n\nif [ \"${1:-}\" != \"uninstall\" ]; then\n # Redirect all output (stdout and stderr) to both terminal and log file\n exec > >(tee -a \"${AGENTTEAMS_LOG_FILE}\") 2>&1\n\n echo \"\"\n echo \"========================================\"\n echo \"AgentTeams Installation Log\"\n echo \"Started: $(date)\"\n echo \"User: $(whoami)\"\n echo \"System: $(uname -a)\"\n echo \"Log file: ${AGENTTEAMS_LOG_FILE}\"\n echo \"========================================\"\n echo \"\"\nfi\n\n# ============================================================\n# Utility functions (needed early for timezone detection)\n# ============================================================\n\nlog() {\n echo -e \"\\033[36m[AgentTeams]\\033[0m $1\"\n}\n\nerror() {\n echo -e \"\\033[31m[AgentTeams ERROR]\\033[0m $1\" >&2\n}\n\ndie() {\n error \"$1\"\n exit 1\n}\n\n# ============================================================\n# Timezone detection (compatible with Linux and macOS)\n# ============================================================\n\ndetect_timezone() {\n local tz=\"\"\n\n # Try /etc/timezone (Debian/Ubuntu)\n if [ -f /etc/timezone ]; then\n tz=$(cat /etc/timezone 2>/dev/null | tr -d '[:space:]')\n fi\n\n # Try /etc/localtime symlink (macOS and some Linux)\n if [ -z \"${tz}\" ] && [ -L /etc/localtime ]; then\n tz=$(ls -l /etc/localtime 2>/dev/null | sed 's|.*/zoneinfo/||')\n fi\n\n # Try timedatectl (systemd)\n if [ -z \"${tz}\" ]; then\n tz=$(timedatectl show --value -p Timezone 2>/dev/null)\n fi\n\n # If still not detected, warn and prompt user. Diagnostics go to stderr —\n # this function returns its value via stdout to `$(detect_timezone)`, so\n # any echo here would leak into AGENTTEAMS_TIMEZONE and downstream image tags.\n if [ -z \"${tz}\" ]; then\n echo \"\" >&2\n echo -e \"\\033[33m[AgentTeams WARNING]\\033[0m Could not detect timezone automatically.\" >&2\n echo -e \"\\033[33m[AgentTeams]\\033[0m Please enter your timezone (e.g., Asia/Shanghai, America/New_York).\" >&2\n echo \"\" >&2\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n tz=\"Asia/Shanghai\"\n log \"Using default timezone: ${tz}\" >&2\n else\n read -e -p \"Timezone [Asia/Shanghai]: \" tz\n tz=\"${tz:-Asia/Shanghai}\"\n fi\n fi\n\n echo \"${tz}\"\n}\n\n# Detect timezone once at startup (used by registry selection and container TZ)\nAGENTTEAMS_TIMEZONE=\"${AGENTTEAMS_TIMEZONE:-$(detect_timezone)}\"\n\n# ============================================================\n# Language detection based on timezone\n# ============================================================\n\ndetect_language() {\n local tz=\"${AGENTTEAMS_TIMEZONE}\"\n case \"${tz}\" in\n Asia/Shanghai|Asia/Chongqing|Asia/Harbin|Asia/Urumqi|\\\n Asia/Taipei|Asia/Hong_Kong|Asia/Macau)\n echo \"zh\"\n ;;\n *)\n echo \"en\"\n ;;\n esac\n}\n\n# Language priority: env var > existing env file > timezone detection\nif [ -z \"${AGENTTEAMS_LANGUAGE}\" ]; then\n # Check existing env file for saved language preference (upgrade scenario)\n _env_file=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n # Migrate from legacy location (current directory) if needed\n if [ ! -f \"${_env_file}\" ] && [ -f \"./agentteams-manager.env\" ]; then\n mv \"./agentteams-manager.env\" \"${_env_file}\" 2>/dev/null || true\n fi\n if [ -f \"${_env_file}\" ]; then\n _saved_lang=$(grep '^AGENTTEAMS_LANGUAGE=' \"${_env_file}\" 2>/dev/null | cut -d= -f2-)\n if [ -n \"${_saved_lang}\" ]; then\n AGENTTEAMS_LANGUAGE=\"${_saved_lang}\"\n fi\n fi\n # Fall back to timezone-based detection\n if [ -z \"${AGENTTEAMS_LANGUAGE}\" ]; then\n AGENTTEAMS_LANGUAGE=\"$(detect_language)\"\n fi\n unset _env_file _saved_lang\nfi\nexport AGENTTEAMS_LANGUAGE\n\n# ============================================================\n# Centralized message dictionary and msg() function\n# Compatible with bash 3.2+ (macOS default) — uses case instead of declare -A\n# ============================================================\n\n# msg() function: look up message by key, with printf-style argument substitution\n# Falls back to English if the current language translation is missing.\nmsg() {\n local key=\"$1\"\n shift\n local lang=\"${AGENTTEAMS_LANGUAGE:-en}\"\n local text=\"\"\n case \"${key}.${lang}\" in\n # --- Timezone detection messages ---\n \"tz.warning.title.zh\") text=\"无法自动检测时区。\" ;;\n \"tz.warning.title.en\") text=\"Could not detect timezone automatically.\" ;;\n \"tz.warning.prompt.zh\") text=\"请输入您的时区(例如 Asia/Shanghai、America/New_York)。\" ;;\n \"tz.warning.prompt.en\") text=\"Please enter your timezone (e.g., Asia/Shanghai, America/New_York).\" ;;\n \"tz.default.zh\") text=\"使用默认时区: %s\" ;;\n \"tz.default.en\") text=\"Using default timezone: %s\" ;;\n \"tz.input_prompt.zh\") text=\"时区\" ;;\n \"tz.input_prompt.en\") text=\"Timezone\" ;;\n # --- Installation title and info ---\n \"install.title.zh\") text=\"=== AgentTeams Manager 安装 ===\" ;;\n \"install.title.en\") text=\"=== AgentTeams Manager Installation ===\" ;;\n \"install.registry.zh\") text=\"镜像仓库: %s\" ;;\n \"install.registry.en\") text=\"Registry: %s\" ;;\n \"install.dir.zh\") text=\"安装目录: %s\" ;;\n \"install.dir.en\") text=\"Installation directory: %s\" ;;\n \"install.dir_hint.zh\") text=\" (env 文件 'agentteams-manager.env' 将保存到 HOME 目录。)\" ;;\n \"install.dir_hint.en\") text=\" (The env file 'agentteams-manager.env' will be saved to your HOME directory.)\" ;;\n \"install.dir_hint2.zh\") text=\" (请从您希望管理此安装的目录运行此脚本。)\" ;;\n \"install.dir_hint2.en\") text=\" (Run this script from the directory where you want to manage this installation.)\" ;;\n # --- Onboarding mode ---\n \"install.mode.title.zh\") text=\"--- Onboarding 模式 ---\" ;;\n \"install.mode.title.en\") text=\"--- Onboarding Mode ---\" ;;\n \"install.mode.choose.zh\") text=\"选择安装模式:\" ;;\n \"install.mode.choose.en\") text=\"Choose your installation mode:\" ;;\n \"install.mode.quickstart.zh\") text=\" 1) 快速开始 - 使用阿里云通义 Token 套餐快速安装(推荐)\" ;;\n \"install.mode.quickstart.en\") text=\" 1) Quick Start - Fast installation with Qwen Cloud (recommended)\" ;;\n \"install.mode.manual.zh\") text=\" 2) 手动配置 - 选择 LLM 提供商并自定义选项\" ;;\n \"install.mode.manual.en\") text=\" 2) Manual - Choose LLM provider and customize options\" ;;\n \"install.mode.prompt.zh\") text=\"请选择 [1/2]\" ;;\n \"install.mode.prompt.en\") text=\"Enter choice [1/2]\" ;;\n \"install.mode.quickstart_selected.zh\") text=\"已选择快速开始模式 - 使用阿里云通义 Token 套餐\" ;;\n \"install.mode.quickstart_selected.en\") text=\"Quick Start mode selected - using Qwen Cloud\" ;;\n \"install.mode.manual_selected.zh\") text=\"已选择手动配置模式 - 您将选择 LLM 提供商并自定义选项\" ;;\n \"install.mode.manual_selected.en\") text=\"Manual mode selected - you will choose LLM provider and customize options\" ;;\n \"install.mode.invalid.zh\") text=\"无效选择,默认使用快速开始模式\" ;;\n \"install.mode.invalid.en\") text=\"Invalid choice, defaulting to Quick Start mode\" ;;\n # --- Upgrade mode sub-menu ---\n \"upgrade.mode.prompt.zh\") text=\"请选择升级方式:\" ;;\n \"upgrade.mode.prompt.en\") text=\"Select upgrade mode:\" ;;\n \"upgrade.mode.keep_all.zh\") text=\" 1) 保留所有参数,一键升级(推荐)\" ;;\n \"upgrade.mode.keep_all.en\") text=\" 1) Keep all parameters, quick upgrade (recommended)\" ;;\n \"upgrade.mode.confirm_each.zh\") text=\" 2) 逐个确认参数(可修改)\" ;;\n \"upgrade.mode.confirm_each.en\") text=\" 2) Confirm each parameter (can modify)\" ;;\n \"upgrade.mode.back.zh\") text=\" 3) 返回\" ;;\n \"upgrade.mode.back.en\") text=\" 3) Back\" ;;\n # --- Version selection ---\n \"install.version.title.zh\") text=\"--- 版本选择 ---\" ;;\n \"install.version.title.en\") text=\"--- Version Selection ---\" ;;\n \"install.version.choose.zh\") text=\"选择要安装的版本:\" ;;\n \"install.version.choose.en\") text=\"Choose the version to install:\" ;;\n \"install.version.option_latest.zh\") text=\" 1) latest - 最新版(默认)\" ;;\n \"install.version.option_latest.en\") text=\" 1) latest - Latest build (default)\" ;;\n \"install.version.option_stable.zh\") text=\" 2) %s - 最新稳定版\" ;;\n \"install.version.option_stable.en\") text=\" 2) %s - Latest stable release\" ;;\n \"install.version.fetching.zh\") text=\"正在查询最新稳定版本...\" ;;\n \"install.version.fetching.en\") text=\"Fetching latest stable release...\" ;;\n \"install.version.fetch_failed.zh\") text=\"无法查询 GitHub,使用内置版本 %s\" ;;\n \"install.version.fetch_failed.en\") text=\"Could not reach GitHub, using built-in version %s\" ;;\n \"install.version.option_custom.zh\") text=\" 3) 自定义 - 手动输入版本号(如 v1.0.5)\" ;;\n \"install.version.option_custom.en\") text=\" 3) Custom - Enter a specific version (e.g. v1.0.5)\" ;;\n \"install.version.prompt.zh\") text=\"请选择 [1/2/3]\" ;;\n \"install.version.prompt.en\") text=\"Enter choice [1/2/3]\" ;;\n \"install.version.custom_prompt.zh\") text=\"请输入版本号\" ;;\n \"install.version.custom_prompt.en\") text=\"Enter version tag\" ;;\n \"install.version.selected_latest.zh\") text=\"已选择最新版 (latest)\" ;;\n \"install.version.selected_latest.en\") text=\"Selected latest version\" ;;\n \"install.version.selected_stable.zh\") text=\"已选择最新稳定版 (%s)\" ;;\n \"install.version.selected_stable.en\") text=\"Selected latest stable version (%s)\" ;;\n \"install.version.selected_custom.zh\") text=\"已选择自定义版本 (%s)\" ;;\n \"install.version.selected_custom.en\") text=\"Selected custom version (%s)\" ;;\n \"install.version.invalid.zh\") text=\"无效选择,使用最新稳定版 (%s)\" ;;\n \"install.version.invalid.en\") text=\"Invalid choice, defaulting to latest stable version (%s)\" ;;\n # --- Existing installation detected ---\n \"install.existing.detected.zh\") text=\"检测到已有 Manager 安装(env 文件: %s)\" ;;\n \"install.existing.detected.en\") text=\"Existing Manager installation detected (env file: %s)\" ;;\n \"install.existing.choose.zh\") text=\"选择操作:\" ;;\n \"install.existing.choose.en\") text=\"Choose an action:\" ;;\n \"install.existing.upgrade.zh\") text=\" 1) 就地升级(保留数据、工作空间、env 文件)\" ;;\n \"install.existing.upgrade.en\") text=\" 1) In-place upgrade (keep data, workspace, env file)\" ;;\n \"install.existing.reinstall.zh\") text=\" 2) 全新重装(删除所有数据,重新开始)\" ;;\n \"install.existing.reinstall.en\") text=\" 2) Clean reinstall (remove all data, start fresh)\" ;;\n \"install.existing.cancel.zh\") text=\" 3) 取消\" ;;\n \"install.existing.cancel.en\") text=\" 3) Cancel\" ;;\n \"install.existing.prompt.zh\") text=\"请选择 [1/2/3]\" ;;\n \"install.existing.prompt.en\") text=\"Enter choice [1/2/3]\" ;;\n \"install.existing.upgrade_noninteractive.zh\") text=\"非交互模式: 执行就地升级...\" ;;\n \"install.existing.upgrade_noninteractive.en\") text=\"Non-interactive mode: performing in-place upgrade...\" ;;\n \"install.existing.upgrading.zh\") text=\"执行就地升级...\" ;;\n \"install.existing.upgrading.en\") text=\"Performing in-place upgrade...\" ;;\n \"install.existing.warn_manager_stop.zh\") text=\"⚠️ Manager 容器将被停止并重新创建。\" ;;\n \"install.existing.warn_manager_stop.en\") text=\"⚠️ Manager container will be stopped and recreated.\" ;;\n \"install.existing.warn_worker_recreate.zh\") text=\"⚠️ Worker 容器也将被重新创建(以更新 Manager IP)。\" ;;\n \"install.existing.warn_worker_recreate.en\") text=\"⚠️ Worker containers will also be recreated (to update Manager IP in hosts).\" ;;\n \"install.existing.continue_prompt.zh\") text=\"继续?[y/N]\" ;;\n \"install.existing.continue_prompt.en\") text=\"Continue? [y/N]\" ;;\n \"install.existing.cancelled.zh\") text=\"安装已取消。\" ;;\n \"install.existing.cancelled.en\") text=\"Installation cancelled.\" ;;\n \"install.existing.stopping_manager.zh\") text=\"停止并移除现有 manager 容器...\" ;;\n \"install.existing.stopping_manager.en\") text=\"Stopping and removing existing manager container...\" ;;\n \"install.existing.stopping_workers.zh\") text=\"停止并移除现有 worker 容器...\" ;;\n \"install.existing.stopping_workers.en\") text=\"Stopping and removing existing worker containers...\" ;;\n \"install.existing.removed.zh\") text=\" 已移除: %s\" ;;\n \"install.existing.removed.en\") text=\" Removed: %s\" ;;\n # --- Clean reinstall messages ---\n \"install.reinstall.performing.zh\") text=\"执行全新重装...\" ;;\n \"install.reinstall.performing.en\") text=\"Performing clean reinstall...\" ;;\n \"install.reinstall.warn_stop.zh\") text=\"⚠️ 以下运行中的容器将被停止:\" ;;\n \"install.reinstall.warn_stop.en\") text=\"⚠️ The following running containers will be stopped:\" ;;\n \"install.reinstall.warn_delete.zh\") text=\"⚠️ 警告: 以下内容将被删除:\" ;;\n \"install.reinstall.warn_delete.en\") text=\"⚠️ WARNING: This will DELETE the following:\" ;;\n \"install.reinstall.warn_volume.zh\") text=\" - Docker 卷: agentteams-data\" ;;\n \"install.reinstall.warn_volume.en\") text=\" - Docker volume: agentteams-data\" ;;\n \"install.reinstall.warn_env.zh\") text=\" - Env 文件: %s\" ;;\n \"install.reinstall.warn_env.en\") text=\" - Env file: %s\" ;;\n \"install.reinstall.warn_workspace.zh\") text=\" - Manager 工作空间: %s\" ;;\n \"install.reinstall.warn_workspace.en\") text=\" - Manager workspace: %s\" ;;\n \"install.reinstall.warn_workers.zh\") text=\" - 所有 worker 容器\" ;;\n \"install.reinstall.warn_workers.en\") text=\" - All worker containers\" ;;\n \"install.reinstall.warn_proxy.zh\") text=\" - Docker API 代理容器: agentteams-controller\" ;;\n \"install.reinstall.warn_proxy.en\") text=\" - Docker API proxy container: agentteams-controller\" ;;\n \"install.reinstall.warn_network.zh\") text=\" - Docker 网络: agentteams-net\" ;;\n \"install.reinstall.warn_network.en\") text=\" - Docker network: agentteams-net\" ;;\n \"install.reinstall.confirm_type.zh\") text=\"请输入工作空间路径以确认删除(或按 Ctrl+C 取消):\" ;;\n \"install.reinstall.confirm_type.en\") text=\"To confirm deletion, please type the workspace path:\" ;;\n \"install.reinstall.confirm_path.zh\") text=\"输入路径以确认(或按 Ctrl+C 取消)\" ;;\n \"install.reinstall.confirm_path.en\") text=\"Type the path to confirm (or press Ctrl+C to cancel)\" ;;\n \"install.reinstall.path_mismatch.zh\") text=\"路径不匹配。中止重装。输入: '%s',期望: '%s'\" ;;\n \"install.reinstall.path_mismatch.en\") text=\"Path mismatch. Aborting reinstall. Input: '%s', Expected: '%s'\" ;;\n \"install.reinstall.confirmed.zh\") text=\"已确认。正在清理...\" ;;\n \"install.reinstall.confirmed.en\") text=\"Confirmed. Cleaning up...\" ;;\n \"install.reinstall.removed_worker.zh\") text=\" 已移除 worker: %s\" ;;\n \"install.reinstall.removed_worker.en\") text=\" Removed worker: %s\" ;;\n \"install.reinstall.removing_volume.zh\") text=\"正在移除 Docker 卷: agentteams-data\" ;;\n \"install.reinstall.removing_volume.en\") text=\"Removing Docker volume: agentteams-data\" ;;\n \"install.reinstall.warn_volume_fail.zh\") text=\" 警告: 无法移除卷(可能有引用)\" ;;\n \"install.reinstall.warn_volume_fail.en\") text=\" Warning: Could not remove volume (may have references)\" ;;\n \"install.reinstall.removing_proxy.zh\") text=\"正在移除 Docker API 代理容器: agentteams-controller\" ;;\n \"install.reinstall.removing_proxy.en\") text=\"Removing Docker API proxy container: agentteams-controller\" ;;\n \"install.reinstall.removing_network.zh\") text=\"正在移除 Docker 网络: agentteams-net\" ;;\n \"install.reinstall.removing_network.en\") text=\"Removing Docker network: agentteams-net\" ;;\n \"install.reinstall.removing_workspace.zh\") text=\"正在移除工作空间目录: %s\" ;;\n \"install.reinstall.removing_workspace.en\") text=\"Removing workspace directory: %s\" ;;\n \"install.reinstall.removing_env.zh\") text=\"正在移除 env 文件: %s\" ;;\n \"install.reinstall.removing_env.en\") text=\"Removing env file: %s\" ;;\n \"install.reinstall.cleanup_done.zh\") text=\"清理完成。开始全新安装...\" ;;\n \"install.reinstall.cleanup_done.en\") text=\"Cleanup complete. Starting fresh installation...\" ;;\n \"install.reinstall.failed_rm_workspace.zh\") text=\"无法移除工作空间目录\" ;;\n \"install.reinstall.failed_rm_workspace.en\") text=\"Failed to remove workspace directory\" ;;\n # --- Orphan volume detection ---\n \"install.orphan_volume.detected.zh\") text=\"⚠️ 检测到残留数据卷 '%s',但未找到对应的 env 配置文件。\" ;;\n \"install.orphan_volume.detected.en\") text=\"⚠️ Found leftover data volume '%s' but no matching env config file.\" ;;\n \"install.orphan_volume.warn.zh\") text=\"这可能是之前安装的残留数据,会导致新安装出现异常(如密码冲突、服务启动失败)。\" ;;\n \"install.orphan_volume.warn.en\") text=\"This is likely leftover data from a previous installation and may cause issues (e.g., credential conflicts, service startup failures).\" ;;\n \"install.orphan_volume.choose.zh\") text=\"选择操作:\" ;;\n \"install.orphan_volume.choose.en\") text=\"Choose an action:\" ;;\n \"install.orphan_volume.clean.zh\") text=\" 1) 清理残留数据卷后继续安装(推荐)\" ;;\n \"install.orphan_volume.clean.en\") text=\" 1) Remove leftover volume and continue installation (recommended)\" ;;\n \"install.orphan_volume.keep.zh\") text=\" 2) 保留数据卷继续安装(可能出现异常)\" ;;\n \"install.orphan_volume.keep.en\") text=\" 2) Keep the volume and continue installation (may cause issues)\" ;;\n \"install.orphan_volume.prompt.zh\") text=\"请选择 [1/2]\" ;;\n \"install.orphan_volume.prompt.en\") text=\"Enter choice [1/2]\" ;;\n \"install.orphan_volume.cleaning.zh\") text=\"正在清理残留数据卷...\" ;;\n \"install.orphan_volume.cleaning.en\") text=\"Removing leftover data volume...\" ;;\n \"install.orphan_volume.cleaned.zh\") text=\"残留数据卷已清理。继续全新安装...\" ;;\n \"install.orphan_volume.cleaned.en\") text=\"Leftover volume removed. Continuing with fresh installation...\" ;;\n \"install.orphan_volume.keeping.zh\") text=\"保留数据卷,继续安装。如遇异常请选择全新重装。\" ;;\n \"install.orphan_volume.keeping.en\") text=\"Keeping existing volume. If you encounter issues, consider a clean reinstall.\" ;;\n \"install.orphan_volume.clean_noninteractive.zh\") text=\"非交互模式: 自动清理残留数据卷...\" ;;\n \"install.orphan_volume.clean_noninteractive.en\") text=\"Non-interactive mode: automatically removing leftover volume...\" ;;\n # --- Loading existing config ---\n \"install.loading_config.zh\") text=\"从 %s 加载已有配置(shell 环境变量优先)...\" ;;\n \"install.loading_config.en\") text=\"Loading existing config from %s (shell env vars take priority)...\" ;;\n # --- LLM Configuration ---\n \"llm.title.zh\") text=\"--- LLM 配置 ---\" ;;\n \"llm.title.en\") text=\"--- LLM Configuration ---\" ;;\n \"llm.provider.label.zh\") text=\" 提供商: %s\" ;;\n \"llm.provider.label.en\") text=\" Provider: %s\" ;;\n \"llm.model.label.zh\") text=\" 模型: %s\" ;;\n \"llm.model.label.en\") text=\" Model: %s\" ;;\n \"llm.provider.qwen.zh\") text=\" 提供商: qwen(阿里云百炼)\" ;;\n \"llm.provider.qwen.en\") text=\" Provider: qwen (Alibaba Cloud Bailian)\" ;;\n \"llm.provider.qwen_default.zh\") text=\" 提供商: %s(默认)\" ;;\n \"llm.provider.qwen_default.en\") text=\" Provider: %s (default)\" ;;\n \"llm.model.default.zh\") text=\" 模型: %s(默认)\" ;;\n \"llm.model.default.en\") text=\" Model: %s (default)\" ;;\n \"llm.apikey_hint_bailian.zh\") text=\" 💡 获取阿里云百炼(DashScope)API Key:\" ;;\n \"llm.apikey_hint_bailian.en\") text=\" 💡 Get your Alibaba Cloud Bailian (DashScope) API Key:\" ;;\n \"llm.apikey_url_bailian.zh\") text=\" https://www.aliyun.com/product/bailian\" ;;\n \"llm.apikey_url_bailian.en\") text=\" https://www.aliyun.com/product/bailian\" ;;\n \"llm.apikey_hint_qwencloud.zh\") text=\" 💡 从 Qwen Cloud(国际站)获取 DASHSCOPE_API_KEY:\" ;;\n \"llm.apikey_hint_qwencloud.en\") text=\" 💡 Get your DASHSCOPE_API_KEY for Qwen Cloud (international) from:\" ;;\n \"llm.apikey_url_qwencloud.zh\") text=\" https://home.qwencloud.com/api-keys (文档: https://docs.qwencloud.com/)\" ;;\n \"llm.apikey_url_qwencloud.en\") text=\" https://home.qwencloud.com/api-keys | Docs: https://docs.qwencloud.com/\" ;;\n \"llm.apikey_hint_tokenplan.zh\") text=\" 💡 获取 DashScope API Key 或开通通义 Token 套餐,请参考:\" ;;\n \"llm.apikey_hint_tokenplan.en\") text=\" 💡 Get your DashScope or Token Plan API key (Alibaba Model Studio):\" ;;\n \"llm.apikey_url_tokenplan.zh\") text=\" https://help.aliyun.com/zh/model-studio/token-plan-quickstart\" ;;\n \"llm.apikey_url_tokenplan.en\") text=\" https://common-buy.aliyun.com/token-plan/ | https://help.aliyun.com/zh/model-studio/token-plan-quickstart\" ;;\n \"llm.apikey_hint_codingplan.zh\") text=\" 💡 获取 DashScope API Key(Coding 套餐 / coding.dashscope 接口):\" ;;\n \"llm.apikey_hint_codingplan.en\") text=\" 💡 Get your DashScope API key for Coding Plan (coding.dashscope endpoint):\" ;;\n \"llm.apikey_url_codingplan.zh\") text=\" https://help.aliyun.com/zh/model-studio/get-api-key\" ;;\n \"llm.apikey_url_codingplan.en\") text=\" https://help.aliyun.com/zh/model-studio/get-api-key\" ;;\n \"llm.apikey_prompt.zh\") text=\"LLM API Key\" ;;\n \"llm.apikey_prompt.en\") text=\"LLM API Key\" ;;\n \"llm.providers_title.zh\") text=\"可用 LLM 提供商:\" ;;\n \"llm.providers_title.en\") text=\"Available LLM Providers:\" ;;\n \"llm.provider.alibaba.zh\") text=\" 1) 阿里云通义 Token 套餐 - 推荐中国用户使用\" ;;\n \"llm.provider.alibaba.en\") text=\" 1) Qwen Cloud - International (OpenAI-compatible API, recommended)\" ;;\n \"llm.provider.openai_compat.zh\") text=\" 2) OpenAI 兼容 API - 自定义 Base URL(OpenAI、DeepSeek 等)\" ;;\n \"llm.provider.openai_compat.en\") text=\" 2) OpenAI-compatible API - Custom Base URL (OpenAI, DeepSeek, etc.)\" ;;\n \"llm.provider.select.zh\") text=\"选择提供商 [1/2]\" ;;\n \"llm.provider.select.en\") text=\"Select provider [1/2]\" ;;\n \"llm.alibaba.models_title.zh\") text=\"选择阿里云模型接入方式:\" ;;\n \"llm.alibaba.models_title.en\") text=\"Select Alibaba Cloud model access:\" ;;\n \"llm.alibaba.model.tokenplan.zh\") text=\" 1) 阿里云通义 Token 套餐 - 兼容模式(推荐)\" ;;\n \"llm.alibaba.model.tokenplan.en\") text=\" 1) Alibaba Cloud Token Plan - compatible-mode (recommended)\" ;;\n \"llm.alibaba.model.bailian.zh\") text=\" 2) 阿里云百炼 - DashScope 通用兼容接口\" ;;\n \"llm.alibaba.model.bailian.en\") text=\" 2) Alibaba Cloud Bailian - DashScope compatible mode\" ;;\n \"llm.alibaba.model.codingplan_legacy.zh\") text=\" 3) 阿里云 Coding 套餐 - 旧版端点(兼容保留)\" ;;\n \"llm.alibaba.model.codingplan_legacy.en\") text=\" 3) Alibaba Cloud Coding Plan - legacy endpoint (backward compatible)\" ;;\n \"llm.alibaba.model.select.zh\") text=\"选择接入方式 [1/2/3]\" ;;\n \"llm.alibaba.model.select.en\") text=\"Select access option [1/2/3]\" ;;\n \"llm.alibaba.model.invalid.zh\") text=\"无效选择: %s(请输入 1、2 或 3)\" ;;\n \"llm.alibaba.model.invalid.en\") text=\"Invalid choice: %s (please enter 1, 2, or 3)\" ;;\n \"llm.codingplan.models_title.zh\") text=\"选择通义 Token 套餐默认模型:\" ;;\n \"llm.codingplan.models_title.en\") text=\"Select Qwen Cloud default model:\" ;;\n \"llm.codingplan.model.qwen36plus.zh\") text=\" 1) qwen3.6-plus - 千问 3.6(推荐)\" ;;\n \"llm.codingplan.model.qwen36plus.en\") text=\" 1) qwen3.6-plus - Qwen 3.6 (recommended)\" ;;\n \"llm.codingplan.model.glm5.zh\") text=\" 2) glm-5 - 智谱 GLM-5(编程推荐)\" ;;\n \"llm.codingplan.model.glm5.en\") text=\" 2) glm-5 - Zhipu GLM-5 (recommended for coding)\" ;;\n \"llm.codingplan.model.kimi.zh\") text=\" 3) kimi-k2.5 - Moonshot Kimi K2.5\" ;;\n \"llm.codingplan.model.kimi.en\") text=\" 3) kimi-k2.5 - Moonshot Kimi K2.5\" ;;\n \"llm.codingplan.model.minimax.zh\") text=\" 4) MiniMax-M2.5 - MiniMax M2.5\" ;;\n \"llm.codingplan.model.minimax.en\") text=\" 4) MiniMax-M2.5 - MiniMax M2.5\" ;;\n \"llm.codingplan.model.select.zh\") text=\"选择模型 [1/2/3/4]\" ;;\n \"llm.codingplan.model.select.en\") text=\"Select model [1/2/3/4]\" ;;\n \"llm.provider.selected_tokenplan.zh\") text=\" 提供商: 阿里云通义 Token 套餐(兼容模式)\" ;;\n \"llm.provider.selected_tokenplan.en\") text=\" Provider: Alibaba Cloud Token Plan (compatible mode)\" ;;\n \"llm.provider.selected_codingplan.zh\") text=\" 提供商: 阿里云通义 Token 套餐(alibaba-cloud)\" ;;\n \"llm.provider.selected_codingplan.en\") text=\" Provider: Qwen Cloud (international) (alibaba-cloud)\" ;;\n \"llm.provider.selected_codingplan_legacy.zh\") text=\" 提供商: 阿里云 Coding 套餐(coding.dashscope)\" ;;\n \"llm.provider.selected_codingplan_legacy.en\") text=\" Provider: Alibaba Cloud Coding Plan (coding.dashscope)\" ;;\n \"llm.provider.selected_qwen.zh\") text=\" 提供商: 阿里云百炼\" ;;\n \"llm.provider.selected_qwen.en\") text=\" Provider: Alibaba Cloud Bailian\" ;;\n \"llm.provider.selected_openai.zh\") text=\" 提供商: %s(OpenAI 兼容)\" ;;\n \"llm.provider.selected_openai.en\") text=\" Provider: %s (OpenAI-compatible)\" ;;\n \"llm.provider.invalid.zh\") text=\"无效选择: %s(请输入 1 或 2)\" ;;\n \"llm.provider.invalid.en\") text=\"Invalid choice: %s (please enter 1 or 2)\" ;;\n \"llm.qwen.model_prompt.zh\") text=\"默认模型 ID\" ;;\n \"llm.qwen.model_prompt.en\") text=\"Default Model ID\" ;;\n \"llm.openai.base_url_prompt.zh\") text=\"Base URL\" ;;\n \"llm.openai.base_url_prompt.en\") text=\"Base URL\" ;;\n \"llm.openai.model_prompt.zh\") text=\"默认模型 ID\" ;;\n \"llm.openai.model_prompt.en\") text=\"Default Model ID\" ;;\n \"llm.openai.base_url_label.zh\") text=\" Base URL: %s\" ;;\n \"llm.openai.base_url_label.en\") text=\" Base URL: %s\" ;;\n # --- Custom model parameters ---\n \"llm.custom_model.detected.zh\") text=\" ⚠️ 模型 '%s' 不在内置模型列表中,请配置模型参数:\" ;;\n \"llm.custom_model.detected.en\") text=\" ⚠️ Model '%s' is not in the built-in model list. Please configure model parameters:\" ;;\n \"llm.custom_model.context_prompt.zh\") text=\"最大上下文长度(token 数)[150000]\" ;;\n \"llm.custom_model.context_prompt.en\") text=\"Max context window (tokens) [150000]\" ;;\n \"llm.custom_model.max_tokens_prompt.zh\") text=\"最大输出长度(token 数)[128000]\" ;;\n \"llm.custom_model.max_tokens_prompt.en\") text=\"Max output tokens [128000]\" ;;\n \"llm.custom_model.reasoning_prompt.zh\") text=\"是否支持推理/思考模式?[Y/n]\" ;;\n \"llm.custom_model.reasoning_prompt.en\") text=\"Does it support reasoning/thinking mode? [Y/n]\" ;;\n \"llm.custom_model.vision_prompt.zh\") text=\"是否支持图片输入?[y/N]\" ;;\n \"llm.custom_model.vision_prompt.en\") text=\"Does it support image input? [y/N]\" ;;\n \"llm.custom_model.summary.zh\") text=\" 自定义模型参数: 上下文=%s, 最大输出=%s, 推理=%s, 图片=%s\" ;;\n \"llm.custom_model.summary.en\") text=\" Custom model params: context=%s, maxTokens=%s, reasoning=%s, vision=%s\" ;;\n # --- Admin Credentials ---\n \"admin.title.zh\") text=\"--- 管理员凭据 ---\" ;;\n \"admin.title.en\") text=\"--- Admin Credentials ---\" ;;\n \"admin.username_prompt.zh\") text=\"管理员用户名\" ;;\n \"admin.username_prompt.en\") text=\"Admin Username\" ;;\n \"admin.password_prompt.zh\") text=\"管理员密码(留空自动生成,最少 8 位)\" ;;\n \"admin.password_prompt.en\") text=\"Admin Password (leave empty to auto-generate, min 8 chars)\" ;;\n \"admin.password_generated.zh\") text=\" 已自动生成管理员密码\" ;;\n \"admin.password_generated.en\") text=\" Auto-generated admin password\" ;;\n \"admin.password_too_short.zh\") text=\"管理员密码至少需要 8 个字符(MinIO 要求)。当前长度: %s\" ;;\n \"admin.password_too_short.en\") text=\"Admin password must be at least 8 characters (MinIO requirement). Current length: %s\" ;;\n # --- Port Configuration ---\n \"port.title.zh\") text=\"--- 端口配置(按回车使用默认值)---\" ;;\n \"port.title.en\") text=\"--- Port Configuration (press Enter for defaults) ---\" ;;\n \"port.gateway_prompt.zh\") text=\"网关主机端口(容器内 8080)\" ;;\n \"port.gateway_prompt.en\") text=\"Host port for gateway (8080 inside container)\" ;;\n \"port.console_prompt.zh\") text=\"Higress 控制台主机端口(容器内 8001)\" ;;\n \"port.console_prompt.en\") text=\"Host port for Higress console (8001 inside container)\" ;;\n \"port.element_prompt.zh\") text=\"Element Web 直接访问主机端口(容器内 8088)\" ;;\n \"port.element_prompt.en\") text=\"Host port for Element Web direct access (8088 inside container)\" ;;\n \"port.manager_console_prompt.zh\") text=\"Manager 控制台主机端口(容器内 18888)\" ;;\n \"port.manager_console_prompt.en\") text=\"Host port for Manager console (18888 inside container)\" ;;\n \"port.copaw_app_prompt.zh\") text=\"CoPaw App API 主机端口(容器内 18799)\" ;;\n \"port.copaw_app_prompt.en\") text=\"Host port for CoPaw App API (18799 inside container)\" ;;\n # --- Local-only binding ---\n \"port.local_only.title.zh\") text=\"--- 网络访问模式 ---\" ;;\n \"port.local_only.title.en\") text=\"--- Network Access Mode ---\" ;;\n \"port.local_only.prompt.zh\") text=\"是否仅允许本机访问(端口绑定到 127.0.0.1)?\" ;;\n \"port.local_only.prompt.en\") text=\"Bind ports to localhost only (127.0.0.1)?\" ;;\n \"port.local_only.hint_yes.zh\") text=\" 仅本机使用,无需开放外部端口(推荐)\" ;;\n \"port.local_only.hint_yes.en\") text=\" Local use only, no external port exposure (recommended)\" ;;\n \"port.local_only.hint_no.zh\") text=\" 允许外部访问(局域网 / 公网)\" ;;\n \"port.local_only.hint_no.en\") text=\" Allow external access (LAN / public network)\" ;;\n \"port.local_only.choice.zh\") text=\"请选择 [1/2]\" ;;\n \"port.local_only.choice.en\") text=\"Enter choice [1/2]\" ;;\n \"port.local_only.selected_local.zh\") text=\"端口已绑定到 127.0.0.1(仅本机访问)\" ;;\n \"port.local_only.selected_local.en\") text=\"Ports bound to 127.0.0.1 (localhost only)\" ;;\n \"port.local_only.selected_external.zh\") text=\"端口已绑定到所有网络接口(0.0.0.0)\" ;;\n \"port.local_only.selected_external.en\") text=\"Ports bound to all interfaces (0.0.0.0)\" ;;\n \"port.local_only.https_hint.zh\") text=\"⚠️ 建议在 Higress 控制台配置 TLS 证书并启用 HTTPS,避免明文传输。\" ;;\n \"port.local_only.https_hint.en\") text=\"⚠️ It is recommended to configure TLS certificates and enable HTTPS in the Higress Console to avoid plaintext transmission.\" ;;\n \"port.local_only.https_docs.zh\") text=\"\" ;;\n \"port.local_only.https_docs.en\") text=\"\" ;;\n # --- Domain Configuration ---\n \"domain.title.zh\") text=\"--- 域名配置(按回车使用默认值)---\" ;;\n \"domain.title.en\") text=\"--- Domain Configuration (press Enter for defaults) ---\" ;;\n \"domain.hint.zh\") text=\"提示: 自定义域名前必须事先做好 DNS 解析。单机 ECS 部署时无需修改 aigw、fs 等域名;Element Web 和 Matrix Server 也可通过 IP 直接访问。\" ;;\n \"domain.hint.en\") text=\"Hint: Configure DNS resolution before customizing domains. For single ECS deployment, no need to change aigw, fs, etc.; Element Web and Matrix Server can also be accessed directly via IP.\" ;;\n \"domain.matrix_prompt.zh\") text=\"Matrix 域名\" ;;\n \"domain.matrix_prompt.en\") text=\"Matrix Domain\" ;;\n \"domain.element_prompt.zh\") text=\"Element Web 域名\" ;;\n \"domain.element_prompt.en\") text=\"Element Web Domain\" ;;\n \"domain.gateway_prompt.zh\") text=\"AI 网关域名\" ;;\n \"domain.gateway_prompt.en\") text=\"AI Gateway Domain\" ;;\n \"domain.fs_prompt.zh\") text=\"文件系统域名\" ;;\n \"domain.fs_prompt.en\") text=\"File System Domain\" ;;\n \"domain.console_prompt.zh\") text=\"Manager 控制台域名\" ;;\n \"domain.console_prompt.en\") text=\"Manager Console Domain\" ;;\n # --- GitHub Integration ---\n \"github.title.zh\") text=\"--- GitHub 集成(可选,按回车跳过)---\" ;;\n \"github.title.en\") text=\"--- GitHub Integration (optional, press Enter to skip) ---\" ;;\n \"github.token_prompt.zh\") text=\"GitHub 个人访问令牌(可选)\" ;;\n \"github.token_prompt.en\") text=\"GitHub Personal Access Token (optional)\" ;;\n # --- Skills Registry ---\n \"skills.title.zh\") text=\"--- Skills 注册中心(可选,按回车使用默认 nacos://market.agentteams.io:80/public)---\" ;;\n \"skills.title.en\") text=\"--- Skills Registry (optional, press Enter for default nacos://market.agentteams.io:80/public) ---\" ;;\n \"skills.url_prompt.zh\") text=\"Skills 注册中心 URL(留空使用默认 nacos://market.agentteams.io:80/public)\" ;;\n \"skills.url_prompt.en\") text=\"Skills Registry URL (leave empty for default nacos://market.agentteams.io:80/public)\" ;;\n # --- Data Persistence ---\n \"data.title.zh\") text=\"--- 数据持久化 ---\" ;;\n \"data.title.en\") text=\"--- Data Persistence ---\" ;;\n \"data.volume_prompt.zh\") text=\"Docker 卷名称 [agentteams-data]\" ;;\n \"data.volume_prompt.en\") text=\"Docker volume name for persistent data [agentteams-data]\" ;;\n \"data.volume_using.zh\") text=\" 使用 Docker 卷: %s\" ;;\n \"data.volume_using.en\") text=\" Using Docker volume: %s\" ;;\n # --- Manager Workspace ---\n \"workspace.title.zh\") text=\"--- Manager 工作空间 ---\" ;;\n \"workspace.title.en\") text=\"--- Manager Workspace ---\" ;;\n \"workspace.dir_prompt.zh\") text=\"Manager 工作空间目录 [%s]\" ;;\n \"workspace.dir_prompt.en\") text=\"Manager workspace directory [%s]\" ;;\n \"workspace.dir_label.zh\") text=\" Manager 工作空间: %s\" ;;\n \"workspace.dir_label.en\") text=\" Manager workspace: %s\" ;;\n # --- Host directory sharing ---\n \"host_share.prompt.zh\") text=\"与 Agent 共享的主机目录(默认: %s)\" ;;\n \"host_share.prompt.en\") text=\"Host directory to share with agents (default: %s)\" ;;\n \"host_share.sharing.zh\") text=\"共享主机目录: %s -> 容器内 /host-share\" ;;\n \"host_share.sharing.en\") text=\"Sharing host directory: %s -> /host-share in container\" ;;\n \"host_share.not_exist.zh\") text=\"警告: 主机目录 %s 不存在,跳过验证继续使用\" ;;\n \"host_share.not_exist.en\") text=\"WARNING: Host directory %s does not exist, using without validation\" ;;\n # --- Default worker runtime ---\n \"worker_runtime.title.zh\") text=\"--- 默认 Worker 运行时 ---\" ;;\n \"worker_runtime.title.en\") text=\"--- Default Worker Runtime ---\" ;;\n \"worker_runtime.openclaw.zh\") text=\"OpenClaw\" ;;\n \"worker_runtime.openclaw.en\") text=\"OpenClaw\" ;;\n \"worker_runtime.copaw.zh\") text=\"CoPaw\" ;;\n \"worker_runtime.copaw.en\") text=\"CoPaw\" ;;\n \"worker_runtime.hermes.zh\") text=\"Hermes\" ;;\n \"worker_runtime.hermes.en\") text=\"Hermes\" ;;\n \"worker_runtime.choice.zh\") text=\"请选择 [1/2/3]\" ;;\n \"worker_runtime.choice.en\") text=\"Enter choice [1/2/3]\" ;;\n \"worker_runtime.selected.zh\") text=\"默认 Worker 运行时: %s\" ;;\n \"worker_runtime.selected.en\") text=\"Default Worker runtime: %s\" ;;\n \"worker_runtime.title_short.zh\") text=\"默认 Worker 运行时\" ;;\n \"worker_runtime.title_short.en\") text=\"Default Worker Runtime\" ;;\n \"manager_runtime.title.zh\") text=\"--- Manager 运行时 ---\" ;;\n \"manager_runtime.title.en\") text=\"--- Manager Runtime ---\" ;;\n \"manager_runtime.openclaw.zh\") text=\"OpenClaw\" ;;\n \"manager_runtime.openclaw.en\") text=\"OpenClaw\" ;;\n \"manager_runtime.copaw.zh\") text=\"CoPaw\" ;;\n \"manager_runtime.copaw.en\") text=\"CoPaw\" ;;\n \"manager_runtime.choice.zh\") text=\"请选择 [1/2]\" ;;\n \"manager_runtime.choice.en\") text=\"Enter choice [1/2]\" ;;\n \"manager_runtime.selected.zh\") text=\"Manager 运行时: %s\" ;;\n \"manager_runtime.selected.en\") text=\"Manager runtime: %s\" ;;\n \"manager_runtime.title_short.zh\") text=\"Manager 运行时\" ;;\n \"manager_runtime.title_short.en\") text=\"Manager Runtime\" ;;\n # --- Secrets and config ---\n \"install.generating_secrets.zh\") text=\"正在生成密钥...\" ;;\n \"install.generating_secrets.en\") text=\"Generating secrets...\" ;;\n \"install.config_saved.zh\") text=\"配置已保存到 %s\" ;;\n \"install.config_saved.en\") text=\"Configuration saved to %s\" ;;\n # --- Container runtime socket ---\n \"install.socket_detected.zh\") text=\"容器运行时 socket: %s(已启用直接创建 Worker)\" ;;\n \"install.socket_detected.en\") text=\"Container runtime socket: %s (direct Worker creation enabled)\" ;;\n \"install.socket_not_found.zh\") text=\"未找到容器运行时 socket(Manager 无法直接创建 Worker 容器,需要你手动执行 docker 命令创建)\" ;;\n \"install.socket_not_found.en\") text=\"No container runtime socket found (Manager cannot create Worker containers directly, you will need to create them manually using docker commands)\" ;;\n \"install.socket_confirm.title.zh\") text=\"⚠️ 未检测到容器运行时 Socket\" ;;\n \"install.socket_confirm.title.en\") text=\"⚠️ Container Runtime Socket Not Detected\" ;;\n \"install.socket_confirm.message.zh\") text=\"未找到 Docker/Podman socket,Manager 将无法自动创建 Worker 容器。\\n你需要手动执行 docker run 命令来创建 Worker。\\n\\n是否继续安装?\" ;;\n \"install.socket_confirm.message.en\") text=\"Docker/Podman socket not found. Manager will not be able to create Worker containers automatically.\\nYou will need to manually run docker commands to create Workers.\\n\\nContinue installation?\" ;;\n \"install.socket_confirm.prompt.zh\") text=\"继续安装? [y/N]: \" ;;\n \"install.socket_confirm.prompt.en\") text=\"Continue? [y/N]: \" ;;\n \"install.socket_confirm.cancelled.zh\") text=\"安装已取消。如需启用 Worker 自动创建,请确保 Docker/Podman 正在运行,然后重新运行安装脚本。\" ;;\n \"install.socket_confirm.cancelled.en\") text=\"Installation cancelled. To enable automatic Worker creation, ensure Docker/Podman is running and re-run the installer.\" ;;\n # --- Container management ---\n \"install.removing_existing.zh\") text=\"正在移除现有 agentteams-manager 容器...\" ;;\n \"install.removing_existing.en\") text=\"Removing existing agentteams-manager container...\" ;;\n # --- Matrix E2EE ---\n \"matrix_e2ee.title.zh\") text=\"--- Matrix 端到端加密(E2EE)---\" ;;\n \"matrix_e2ee.title.en\") text=\"--- Matrix End-to-End Encryption (E2EE) ---\" ;;\n \"matrix_e2ee.desc.zh\") text=\"E2EE 会对 Manager 与 Worker 之间的 Matrix 消息进行端到端加密。\\n 启用后,即使 Matrix 服务器被入侵,消息内容也无法被窃取。\\n 但 E2EE 会增加首次握手耗时,且要求所有 Agent 都支持 matrix-sdk-crypto。\\n 如果不确定,建议保持禁用。\\n ⚠ 注意:禁用 E2EE 后,请勿在 Element 上创建默认启用加密的 Private 房间,\\n 否则 Agent 将无法读取该房间中的加密消息。请改用 Public 房间或关闭房间加密。\" ;;\n \"matrix_e2ee.desc.en\") text=\"E2EE encrypts Matrix messages between Manager and Workers end-to-end.\\n When enabled, message content stays private even if the Matrix server is compromised.\\n However, E2EE adds overhead to the initial handshake and requires all Agents\\n to support matrix-sdk-crypto. If unsure, keep it disabled.\\n ⚠ Note: When E2EE is disabled, do NOT create Private rooms in Element (which\\n enable encryption by default) — Agents cannot read encrypted messages without\\n E2EE support. Use Public rooms or turn off room encryption instead.\" ;;\n \"matrix_e2ee.enable.zh\") text=\"启用 E2EE\" ;;\n \"matrix_e2ee.enable.en\") text=\"Enable E2EE\" ;;\n \"matrix_e2ee.disable.zh\") text=\"禁用 E2EE(推荐)\" ;;\n \"matrix_e2ee.disable.en\") text=\"Disable E2EE (recommended)\" ;;\n \"matrix_e2ee.choice.zh\") text=\"请选择 [1/2]\" ;;\n \"matrix_e2ee.choice.en\") text=\"Enter choice [1/2]\" ;;\n \"matrix_e2ee.selected_enabled.zh\") text=\"Matrix E2EE: 已启用\" ;;\n \"matrix_e2ee.selected_enabled.en\") text=\"Matrix E2EE: enabled\" ;;\n \"matrix_e2ee.selected_disabled.zh\") text=\"Matrix E2EE: 已禁用(默认)\" ;;\n \"matrix_e2ee.selected_disabled.en\") text=\"Matrix E2EE: disabled (default)\" ;;\n \"matrix_e2ee.title_short.zh\") text=\"Matrix E2EE\" ;;\n \"matrix_e2ee.title_short.en\") text=\"Matrix E2EE\" ;;\n \"matrix_e2ee.val_enabled.zh\") text=\"已启用\" ;;\n \"matrix_e2ee.val_enabled.en\") text=\"enabled\" ;;\n \"matrix_e2ee.val_disabled.zh\") text=\"已禁用\" ;;\n \"matrix_e2ee.val_disabled.en\") text=\"disabled\" ;;\n # --- Docker API proxy ---\n \"docker_proxy.title.zh\") text=\"--- Docker API 安全代理 ---\" ;;\n \"docker_proxy.title.en\") text=\"--- Docker API Security Proxy ---\" ;;\n \"docker_proxy.desc.zh\") text=\"Docker API 代理可防止 AI Agent 通过 Docker API 越狱访问宿主机。\\n 启用后,Manager 不再直接持有 Docker socket,所有容器操作经过安全校验。\" ;;\n \"docker_proxy.desc.en\") text=\"Docker API proxy prevents AI Agents from escaping via Docker API to access the host.\\n When enabled, Manager no longer has direct Docker socket access; all container operations go through security validation.\" ;;\n \"docker_proxy.enable.zh\") text=\"启用(推荐)\" ;;\n \"docker_proxy.enable.en\") text=\"Enable (recommended)\" ;;\n \"docker_proxy.disable.zh\") text=\"禁用(直接挂载 Docker socket)\" ;;\n \"docker_proxy.disable.en\") text=\"Disable (mount Docker socket directly)\" ;;\n \"docker_proxy.choice.zh\") text=\"请选择 [1/2]\" ;;\n \"docker_proxy.choice.en\") text=\"Enter choice [1/2]\" ;;\n \"docker_proxy.selected_enabled.zh\") text=\"Docker API 代理: 已启用\" ;;\n \"docker_proxy.selected_enabled.en\") text=\"Docker API proxy: enabled\" ;;\n \"docker_proxy.selected_disabled.zh\") text=\"Docker API 代理: 已禁用\" ;;\n \"docker_proxy.selected_disabled.en\") text=\"Docker API proxy: disabled\" ;;\n \"docker_proxy.title_short.zh\") text=\"Docker API 代理\" ;;\n \"docker_proxy.title_short.en\") text=\"Docker API Proxy\" ;;\n \"docker_proxy.val_enabled.zh\") text=\"已启用\" ;;\n \"docker_proxy.val_enabled.en\") text=\"enabled\" ;;\n \"docker_proxy.val_disabled.zh\") text=\"已禁用\" ;;\n \"docker_proxy.val_disabled.en\") text=\"disabled\" ;;\n \"docker_proxy.registries_desc.zh\") text=\"默认放行的镜像来源:本地镜像、localhost、Higress 仓库(所有 region)。\\n 如需放行其他镜像仓库,请输入逗号分隔的地址前缀。\\n 示例: ghcr.io/myorg,registry.example.com/team\" ;;\n \"docker_proxy.registries_desc.en\") text=\"Default allowed image sources: local images, localhost, Higress registries (all regions).\\n To allow additional image sources, enter comma-separated address prefixes.\\n Example: ghcr.io/myorg,registry.example.com/team\" ;;\n \"docker_proxy.registries_prompt.zh\") text=\"额外放行的镜像来源(按回车跳过)\" ;;\n \"docker_proxy.registries_prompt.en\") text=\"Additional allowed image sources (press Enter to skip)\" ;;\n \"docker_proxy.registries_label.zh\") text=\"额外放行的镜像来源\" ;;\n \"docker_proxy.registries_label.en\") text=\"Additional allowed image sources\" ;;\n # --- Podman Autostart ---\n \"podman.autostart.title.zh\") text=\"--- Podman 驻留与开机自启 ---\" ;;\n \"podman.autostart.title.en\") text=\"--- Podman Linger & Autostart ---\" ;;\n \"podman.autostart.desc.zh\") text=\"配置为 systemd 服务,支持开机自动启动及后台驻留。\" ;;\n \"podman.autostart.desc.en\") text=\"Configure as a systemd service for automatic startup on boot.\" ;;\n \"podman.autostart.enable.zh\") text=\"启用(推荐)\" ;;\n \"podman.autostart.enable.en\") text=\"Enable (recommended)\" ;;\n \"podman.autostart.disable.zh\") text=\"不启用(容器随当前会话关闭而停止)\" ;;\n \"podman.autostart.disable.en\") text=\"Disable (containers stop when session ends)\" ;;\n \"podman.autostart.choice.zh\") text=\"请选择 [1/2]\" ;;\n \"podman.autostart.choice.en\") text=\"Enter choice [1/2]\" ;;\n \"podman.autostart.selected_enabled.zh\") text=\"Podman 开机自启: 已启用\" ;;\n \"podman.autostart.selected_enabled.en\") text=\"Podman Autostart: enabled\" ;;\n \"podman.autostart.selected_disabled.zh\") text=\"Podman 开机自启: 已禁用\" ;;\n \"podman.autostart.selected_disabled.en\") text=\"Podman Autostart: disabled\" ;;\n \"podman.autostart.title_short.zh\") text=\"Podman 开机自启\" ;;\n \"podman.autostart.title_short.en\") text=\"Podman Autostart\" ;;\n \"podman.autostart.val_enabled.zh\") text=\"已启用\" ;;\n \"podman.autostart.val_enabled.en\") text=\"enabled\" ;;\n \"podman.autostart.val_disabled.zh\") text=\"已禁用\" ;;\n \"podman.autostart.val_disabled.en\") text=\"disabled\" ;;\n \"install.podman.autostart_title.zh\") text=\"正在配置 Podman 容器开机自启...\" ;;\n \"install.podman.autostart_title.en\") text=\"Configuring Podman container autostart...\" ;;\n \"install.podman.linger_enable.zh\") text=\"尝试启用 systemd linger (如有提示请在此输入 sudo 密码)...\" ;;\n \"install.podman.linger_enable.en\") text=\"Attempting to enable systemd linger (enter sudo password if prompted)...\" ;;\n \"install.podman.linger_warn.zh\") text=\"警告: 无法自动启用 linger。驻留可能失败,请手动执行: sudo loginctl enable-linger \\$(whoami)\" ;;\n \"install.podman.linger_warn.en\") text=\"WARNING: Failed to auto-enable linger. Please run manually: sudo loginctl enable-linger \\$(whoami)\" ;;\n \"install.podman.root_setup.zh\") text=\"检测到 root 用户。正在配置系统级 agentteams-podman-restart 服务...\" ;;\n \"install.podman.root_setup.en\") text=\"Root user detected. Configuring system-wide agentteams-podman-restart service...\" ;;\n \"install.podman.root_success.zh\") text=\"✅ 系统级 agentteams-podman-restart 服务已成功启用并运行。\" ;;\n \"install.podman.root_success.en\") text=\"✅ System-wide agentteams-podman-restart service successfully enabled and running.\" ;;\n \"install.podman.root_fail.zh\") text=\"⚠️ 系统级 agentteams-podman-restart 服务启用失败,请稍后手动检查。\" ;;\n \"install.podman.root_fail.en\") text=\"⚠️ Failed to enable system-wide agentteams-podman-restart service. Please check manually later.\" ;;\n \"install.podman.user_setup.zh\") text=\"检测到普通用户 (%s)。正在配置用户级 agentteams-podman-restart 服务...\" ;;\n \"install.podman.user_setup.en\") text=\"Non-root user (%s) detected. Configuring user-level agentteams-podman-restart service...\" ;;\n \"install.podman.user_success.zh\") text=\"✅ 用户级 agentteams-podman-restart 服务已成功启用并运行。\" ;;\n \"install.podman.user_success.en\") text=\"✅ User-level agentteams-podman-restart service successfully enabled and running.\" ;;\n \"install.podman.user_fail.zh\") text=\"⚠️ 用户级 agentteams-podman-restart 服务启用失败。\\n提示:这通常是因为缺少 XDG_RUNTIME_DIR 或 dbus 没有运行。您可以尝试手动执行: systemctl --user enable --now agentteams-podman-restart.service\" ;;\n \"install.podman.user_fail.en\") text=\"⚠️ Failed to enable user-level agentteams-podman-restart service.\\nHint: This is usually due to missing XDG_RUNTIME_DIR or dbus not running. Try manually: systemctl --user enable --now agentteams-podman-restart.service\" ;;\n \"install.podman.success.zh\") text=\"Podman 开机自启配置完成。\" ;;\n \"install.podman.success.en\") text=\"Podman autostart successfully configured.\" ;;\n # --- Worker idle timeout ---\n \"idle_timeout.prompt.zh\") text=\"Worker 空闲自动停止超时(分钟)[720]\" ;;\n \"idle_timeout.prompt.en\") text=\"Worker idle auto-stop timeout in minutes [720]\" ;;\n \"idle_timeout.selected.zh\") text=\"Worker 空闲超时: %s 分钟\" ;;\n \"idle_timeout.selected.en\") text=\"Worker idle timeout: %s minutes\" ;;\n \"idle_timeout.label.zh\") text=\"Worker 空闲超时(分钟)\" ;;\n \"idle_timeout.label.en\") text=\"Worker idle timeout (min)\" ;;\n # --- YOLO mode ---\n \"install.yolo.zh\") text=\"YOLO 模式已启用(自主决策,无交互提示)\" ;;\n \"install.yolo.en\") text=\"YOLO mode enabled (autonomous decisions, no interactive prompts)\" ;;\n # --- Image pulling ---\n \"install.image.exists.zh\") text=\"Manager 镜像已存在: %s\" ;;\n \"install.image.exists.en\") text=\"Manager image already exists locally: %s\" ;;\n \"install.image.pulling_manager.zh\") text=\"正在拉取 Manager 镜像: %s\" ;;\n \"install.image.pulling_manager.en\") text=\"Pulling Manager image: %s\" ;;\n \"install.image.worker_exists.zh\") text=\"Worker 镜像已存在: %s\" ;;\n \"install.image.worker_exists.en\") text=\"Worker image already exists locally: %s\" ;;\n \"install.image.pulling_worker.zh\") text=\"正在拉取 Worker 镜像: %s\" ;;\n \"install.image.pulling_worker.en\") text=\"Pulling Worker image: %s\" ;;\n # --- Starting container ---\n \"install.starting_manager.zh\") text=\"正在启动 Manager 容器...\" ;;\n \"install.starting_manager.en\") text=\"Starting Manager container...\" ;;\n # --- Wait for Manager ready ---\n \"install.wait_ready.zh\") text=\"等待 Manager Agent 就绪(超时: %ss)...\" ;;\n \"install.wait_ready.en\") text=\"Waiting for Manager agent to be ready (timeout: %ss)...\" ;;\n \"install.wait_ready.ok.zh\") text=\"Manager Agent 已就绪!\" ;;\n \"install.wait_ready.ok.en\") text=\"Manager agent is ready!\" ;;\n \"install.wait_ready.waiting.zh\") text=\"等待中... (%ds/%ds)\" ;;\n \"install.wait_ready.waiting.en\") text=\"Waiting... (%ds/%ds)\" ;;\n \"install.wait_ready.timeout.zh\") text=\"Manager Agent 在 %ss 内未就绪。请检查: docker logs %s\" ;;\n \"install.wait_ready.timeout.en\") text=\"Manager agent did not become ready within %ss. Check: docker logs %s\" ;;\n # --- Wait for Matrix ready ---\n \"install.wait_matrix.zh\") text=\"等待 Matrix 服务就绪(超时: %ss)...\" ;;\n \"install.wait_matrix.en\") text=\"Waiting for Matrix server to be ready (timeout: %ss)...\" ;;\n \"install.wait_matrix.ok.zh\") text=\"Matrix 服务已就绪!\" ;;\n \"install.wait_matrix.ok.en\") text=\"Matrix server is ready!\" ;;\n \"install.wait_matrix.waiting.zh\") text=\"等待 Matrix 中... (%ds/%ds)\" ;;\n \"install.wait_matrix.waiting.en\") text=\"Waiting for Matrix... (%ds/%ds)\" ;;\n \"install.wait_matrix.timeout.zh\") text=\"Matrix 服务在 %ss 内未就绪。请检查: docker logs %s\" ;;\n \"install.wait_matrix.timeout.en\") text=\"Matrix server did not become ready within %ss. Check: docker logs %s\" ;;\n # --- OpenAI-compatible connectivity test ---\n \"llm.openai.test.testing.zh\") text=\"正在测试 API 联通性...\" ;;\n \"llm.openai.test.testing.en\") text=\"Testing API connectivity...\" ;;\n \"llm.openai.test.ok.zh\") text=\"✅ API 联通性测试通过\" ;;\n \"llm.openai.test.ok.en\") text=\"✅ API connectivity test passed\" ;;\n \"llm.openai.test.fail.zh\") text=\"⚠️ API 联通性测试失败(HTTP %s)。响应内容:\\n%s\\n请根据以上错误信息联系您的模型服务商解决。\" ;;\n \"llm.openai.test.fail.en\") text=\"⚠️ API connectivity test failed (HTTP %s). Response body:\\n%s\\nPlease contact your model provider to resolve the issue.\" ;;\n \"llm.openai.test.fail.tokenplan.zh\") text=\"⚠️ 提示: 请确认 API Key 有效且已开通通义 Token 套餐。文档: https://help.aliyun.com/zh/model-studio/token-plan-quickstart\" ;;\n \"llm.openai.test.fail.tokenplan.en\") text=\"⚠️ Hint: Verify your Token Plan API key and compatible-mode access. Docs: https://help.aliyun.com/zh/model-studio/token-plan-quickstart\" ;;\n \"llm.openai.test.fail.codingplan.zh\") text=\"⚠️ 提示: 请确认 API Key 有效且已开通通义 Token 套餐。文档: https://help.aliyun.com/zh/model-studio/token-plan-quickstart\" ;;\n \"llm.openai.test.fail.codingplan.en\") text=\"⚠️ Hint: Verify your DASHSCOPE_API_KEY for Qwen Cloud. API keys: https://home.qwencloud.com/api-keys Docs: https://docs.qwencloud.com/\" ;;\n \"llm.openai.test.fail.codingplan_legacy.zh\") text=\"⚠️ 提示: 请确认 API Key 有效且 Coding 套餐接口可用。文档: https://help.aliyun.com/zh/model-studio/get-api-key\" ;;\n \"llm.openai.test.fail.codingplan_legacy.en\") text=\"⚠️ Hint: Verify your DashScope API key and Coding Plan access. Docs: https://help.aliyun.com/zh/model-studio/get-api-key\" ;;\n \"llm.openai.test.no_curl.zh\") text=\"⚠️ 未找到 curl,跳过 API 联通性测试\" ;;\n \"llm.openai.test.no_curl.en\") text=\"⚠️ curl not found, skipping API connectivity test\" ;;\n \"llm.openai.test.confirm.zh\") text=\"是否仍要继续安装?[y/N/b] \" ;;\n \"llm.openai.test.confirm.en\") text=\"Continue with installation anyway? [y/N/b] \" ;;\n \"llm.embedding.title.zh\") text=\"📦 记忆搜索配置\" ;;\n \"llm.embedding.title.en\") text=\"📦 Memory Search Configuration\" ;;\n \"llm.embedding.hint.zh\") text=\" Embedding 模型可提升记忆搜索质量(语义匹配)。不启用也可正常使用记忆功能(关键词匹配)。\" ;;\n \"llm.embedding.hint.en\") text=\" Embedding model improves memory search quality (semantic matching). Memory still works without it (keyword matching).\" ;;\n \"llm.embedding.option.default.zh\") text=\" 1) text-embedding-v4(推荐)\" ;;\n \"llm.embedding.option.default.en\") text=\" 1) text-embedding-v4 (Recommended)\" ;;\n \"llm.embedding.option.custom.zh\") text=\" 2) 自定义 Embedding 模型\" ;;\n \"llm.embedding.option.custom.en\") text=\" 2) Custom embedding model\" ;;\n \"llm.embedding.option.disable.zh\") text=\" 3) 不启用\" ;;\n \"llm.embedding.option.disable.en\") text=\" 3) Do not enable\" ;;\n \"llm.embedding.select.zh\") text=\"选择\" ;;\n \"llm.embedding.select.en\") text=\"Select\" ;;\n \"llm.embedding.custom_prompt.zh\") text=\" Embedding 模型名称\" ;;\n \"llm.embedding.custom_prompt.en\") text=\" Embedding model name\" ;;\n \"llm.embedding.test.testing.zh\") text=\"正在测试 Embedding API 联通性...\" ;;\n \"llm.embedding.test.testing.en\") text=\"Testing Embedding API connectivity...\" ;;\n \"llm.embedding.test.ok.zh\") text=\"✅ Embedding API 联通性测试通过\" ;;\n \"llm.embedding.test.ok.en\") text=\"✅ Embedding API connectivity test passed\" ;;\n \"llm.embedding.test.fail.zh\") text=\"⚠️ Embedding API 测试失败(HTTP %s)。响应: %s\" ;;\n \"llm.embedding.test.fail.en\") text=\"⚠️ Embedding API test failed (HTTP %s). Response: %s\" ;;\n \"llm.embedding.auto_disabled.zh\") text=\"⚠️ Embedding 已自动禁用,记忆搜索将使用关键词匹配。您可以稍后在 agentteams-manager.env 中设置 AGENTTEAMS_EMBEDDING_MODEL 启用。\" ;;\n \"llm.embedding.auto_disabled.en\") text=\"⚠️ Embedding auto-disabled. Memory search will use keyword matching. You can enable it later in agentteams-manager.env by setting AGENTTEAMS_EMBEDDING_MODEL.\" ;;\n \"llm.embedding.disabled.zh\") text=\"ℹ️ Embedding 已禁用,记忆搜索将使用关键词匹配。\" ;;\n \"llm.embedding.disabled.en\") text=\"ℹ️ Embedding disabled. Memory search will use keyword matching.\" ;;\n \"llm.openai.test.aborted.zh\") text=\"安装已中止。\" ;;\n \"llm.openai.test.aborted.en\") text=\"Installation aborted.\" ;;\n \"nav.back_hint.zh\") text=\"(输入 b 返回上一步)\" ;;\n \"nav.back_hint.en\") text=\"(enter b to go back)\" ;;\n # --- OpenAI-compatible provider creation ---\n \"install.openai_compat.missing.zh\") text=\"警告: OpenAI Base URL 或 API Key 未设置,跳过提供商创建\" ;;\n \"install.openai_compat.missing.en\") text=\"WARNING: OpenAI Base URL or API Key not set, skipping provider creation\" ;;\n \"install.openai_compat.creating.zh\") text=\"正在创建 OpenAI 兼容提供商...\" ;;\n \"install.openai_compat.creating.en\") text=\"Creating OpenAI-compatible provider...\" ;;\n \"install.openai_compat.domain.zh\") text=\" 域名: %s\" ;;\n \"install.openai_compat.domain.en\") text=\" Domain: %s\" ;;\n \"install.openai_compat.port.zh\") text=\" 端口: %s\" ;;\n \"install.openai_compat.port.en\") text=\" Port: %s\" ;;\n \"install.openai_compat.protocol.zh\") text=\" 协议: %s\" ;;\n \"install.openai_compat.protocol.en\") text=\" Protocol: %s\" ;;\n \"install.openai_compat.service_fail.zh\") text=\"警告: 创建 DNS 服务源失败(可能已存在)\" ;;\n \"install.openai_compat.service_fail.en\") text=\"WARNING: Failed to create DNS service source (may already exist)\" ;;\n \"install.openai_compat.provider_fail.zh\") text=\"警告: 创建 AI 提供商失败(可能已存在)\" ;;\n \"install.openai_compat.provider_fail.en\") text=\"WARNING: Failed to create AI provider (may already exist)\" ;;\n \"install.openai_compat.success.zh\") text=\"OpenAI 兼容提供商创建成功\" ;;\n \"install.openai_compat.success.en\") text=\"OpenAI-compatible provider created successfully\" ;;\n # --- Welcome message ---\n \"install.welcome_msg.soul_configured.zh\") text=\"Soul 已配置(找到 soul-configured 标记),跳过 onboarding 消息\" ;;\n \"install.welcome_msg.soul_configured.en\") text=\"Soul already configured (soul-configured marker found), skipping onboarding message\" ;;\n \"install.welcome_msg.logging_in.zh\") text=\"正在以 %s 身份登录以发送欢迎消息...\" ;;\n \"install.welcome_msg.logging_in.en\") text=\"Logging in as %s to send welcome message...\" ;;\n \"install.welcome_msg.login_failed.zh\") text=\"警告: 以 %s 身份登录失败,跳过欢迎消息\" ;;\n \"install.welcome_msg.login_failed.en\") text=\"WARNING: Failed to login as %s, skipping welcome message\" ;;\n \"install.welcome_msg.finding_room.zh\") text=\"正在查找与 Manager 的 DM 房间...\" ;;\n \"install.welcome_msg.finding_room.en\") text=\"Finding DM room with Manager...\" ;;\n \"install.welcome_msg.creating_room.zh\") text=\"正在创建与 Manager 的 DM 房间...\" ;;\n \"install.welcome_msg.creating_room.en\") text=\"Creating DM room with Manager...\" ;;\n \"install.welcome_msg.no_room.zh\") text=\"警告: 无法找到或创建与 Manager 的 DM 房间\" ;;\n \"install.welcome_msg.no_room.en\") text=\"WARNING: Could not find or create DM room with Manager\" ;;\n \"install.welcome_msg.waiting_join.zh\") text=\"等待 Manager 加入房间...\" ;;\n \"install.welcome_msg.waiting_join.en\") text=\"Waiting for Manager to join the room...\" ;;\n \"install.welcome_msg.sending.zh\") text=\"正在向 Manager 发送欢迎消息...\" ;;\n \"install.welcome_msg.sending.en\") text=\"Sending welcome message to Manager...\" ;;\n \"install.welcome_msg.send_failed.zh\") text=\"警告: 发送欢迎消息失败\" ;;\n \"install.welcome_msg.send_failed.en\") text=\"WARNING: Failed to send welcome message\" ;;\n \"install.welcome_msg.sent.zh\") text=\"欢迎消息已发送给 Manager\" ;;\n \"install.welcome_msg.sent.en\") text=\"Welcome message sent to Manager\" ;;\n \"install.welcome_msg.waiting.zh\") text=\"等待 Manager 发送欢迎消息(Higress 路由授权 + LLM 探活,约 45-90s)...\" ;;\n \"install.welcome_msg.waiting.en\") text=\"Waiting for Manager to send the welcome message (Higress route auth + LLM probe, ~45-90s)...\" ;;\n \"install.welcome_msg.confirmed.zh\") text=\"Manager 已确认发送欢迎消息(status.welcomeSent=true,用时 %ss)\" ;;\n \"install.welcome_msg.confirmed.en\") text=\"Manager confirmed welcome message sent (status.welcomeSent=true, %ss elapsed)\" ;;\n \"install.welcome_msg.timeout.zh\") text=\"警告: 在 %ss 内未观察到 Manager 发送欢迎消息(status.welcomeSent=true)。安装仍然成功,所有服务已就绪——可继续按下方提示登录 Element Web。\" ;;\n \"install.welcome_msg.timeout.en\") text=\"WARNING: Did not observe the Manager sending its welcome message (status.welcomeSent=true) within %ss. Installation is still successful, all services are up — continue with the Element Web instructions below.\" ;;\n \"install.welcome_msg.timeout_hint.zh\") text=\"手动触发 onboarding: 登录 Element Web → 打开与 Manager 的 DM 房间 → 发送任意一句话(例如 \\\"hi\\\"),Manager 会接管对话并开始引导。\" ;;\n \"install.welcome_msg.timeout_hint.en\") text=\"Manual onboarding: log in to Element Web → open the DM with the Manager → send any message (e.g. \\\"hi\\\") and the Manager will take over and start the guided setup.\" ;;\n \"install.welcome_msg.timeout_inspect.zh\") text=\"排查命令: docker exec agentteams-controller agt get managers default\" ;;\n \"install.welcome_msg.timeout_inspect.en\") text=\"Inspect status: docker exec agentteams-controller agt get managers default\" ;;\n \"install.welcome_msg.poll_unavailable.zh\") text=\"提示: agentteams-manager 内未找到 AgentTeams CLI,跳过 welcome 等待(旧镜像?)\" ;;\n \"install.welcome_msg.poll_unavailable.en\") text=\"Note: AgentTeams CLI not found inside agentteams-manager; skipping welcome wait (old image?)\" ;;\n # --- Dashboard wizard ---\n \"dash.prompt.zh\") text=\"是否安装 agentteams-dashboard 管理面板?\" ;;\n \"dash.prompt.en\") text=\"Install agentteams-dashboard management UI?\" ;;\n \"dash.version_prompt.zh\") text=\"Dashboard 版本\" ;;\n \"dash.version_prompt.en\") text=\"Dashboard version\" ;;\n \"dash.port_prompt.zh\") text=\"Dashboard 端口号\" ;;\n \"dash.port_prompt.en\") text=\"Dashboard port\" ;;\n \"dash.image_prompt.zh\") text=\"Dashboard 镜像\" ;;\n \"dash.image_prompt.en\") text=\"Dashboard image\" ;;\n \"dash.gateway_prompt.zh\") text=\"Higress Console URL (用于共享登录)\" ;;\n \"dash.gateway_prompt.en\") text=\"Higress Console URL (for shared login)\" ;;\n \"dash.skip.zh\") text=\"跳过 Dashboard 安装\" ;;\n \"dash.skip.en\") text=\"Skipping Dashboard installation\" ;;\n \"dash.auto_url.zh\") text=\"自动检测到 Higress Console URL\" ;;\n \"dash.auto_url.en\") text=\"Auto-detected Higress Console URL\" ;;\n \"dash.auto_url_fail.zh\") text=\"无法自动检测 Higress Console URL,请手动输入\" ;;\n \"dash.auto_url_fail.en\") text=\"Cannot auto-detect Higress Console URL, please enter manually\" ;;\n \"dash.summary.zh\") text=\"Dashboard 配置: 端口=%s 镜像=%s\" ;;\n \"dash.summary.en\") text=\"Dashboard config: port=%s image=%s\" ;;\n # --- Dashboard success ---\n \"success.dashboard.zh\") text=\" agentteams-dashboard 管理面板: http://localhost:%s/\" ;;\n \"success.dashboard.en\") text=\" agentteams-dashboard: http://localhost:%s/\" ;;\n # --- Final output panel ---\n \"success.title.zh\") text=\"=== AgentTeams Manager 已启动!===\" ;;\n \"success.title.en\") text=\"=== AgentTeams Manager Started! ===\" ;;\n \"success.domains_configured.zh\") text=\"以下域名已配置解析到 127.0.0.1:\" ;;\n \"success.domains_configured.en\") text=\"The following domains are configured to resolve to 127.0.0.1:\" ;;\n \"success.open_url.zh\") text=\" ★ 在浏览器中打开以下 URL 开始使用: ★\" ;;\n \"success.open_url.en\") text=\" ★ Open the following URL in your browser to start: ★\" ;;\n \"success.login_with.zh\") text=\" 登录信息:\" ;;\n \"success.login_with.en\") text=\" Login with:\" ;;\n \"success.username.zh\") text=\" 用户名: %s\" ;;\n \"success.username.en\") text=\" Username: %s\" ;;\n \"success.password.zh\") text=\" 密码: %s\" ;;\n \"success.password.en\") text=\" Password: %s\" ;;\n \"success.after_login.zh\") text=\" 登录后,开始与 Manager 聊天!\" ;;\n \"success.after_login.en\") text=\" After login, start chatting with the Manager!\" ;;\n \"success.tell_it.zh\") text=\" 告诉它: \\\"创建一个名为 alice 的前端开发 Worker\\\"\" ;;\n \"success.tell_it.en\") text=\" Tell it: \\\"Create a Worker named alice for frontend dev\\\"\" ;;\n \"success.manager_auto.zh\") text=\" Manager 会自动处理一切。\" ;;\n \"success.manager_auto.en\") text=\" The Manager will handle everything automatically.\" ;;\n \"success.mobile_title.zh\") text=\" 📱 移动端访问(FluffyChat / Element Mobile):\" ;;\n \"success.mobile_title.en\") text=\" 📱 Mobile access (FluffyChat / Element Mobile):\" ;;\n \"success.mobile_step1.zh\") text=\" 1. 在手机上下载 FluffyChat 或 Element\" ;;\n \"success.mobile_step1.en\") text=\" 1. Download FluffyChat or Element on your phone\" ;;\n \"success.mobile_step2.zh\") text=\" 2. 设置 homeserver 为: %s\" ;;\n \"success.mobile_step2.en\") text=\" 2. Set homeserver to: %s\" ;;\n \"success.mobile_step2_noip.zh\") text=\" 2. 设置 homeserver 为: http://<本机局域网IP>:%s\" ;;\n \"success.mobile_step2_noip.en\") text=\" 2. Set homeserver to: http://<this-machine-LAN-IP>:%s\" ;;\n \"success.mobile_noip_hint.zh\") text=\" (无法自动检测局域网 IP — 请使用 ifconfig / ip addr 查看)\" ;;\n \"success.mobile_noip_hint.en\") text=\" (Could not detect LAN IP automatically — check with: ifconfig / ip addr)\" ;;\n \"success.mobile_step3.zh\") text=\" 3. 登录信息:\" ;;\n \"success.mobile_step3.en\") text=\" 3. Login with:\" ;;\n \"success.mobile_username.zh\") text=\" 用户名: %s\" ;;\n \"success.mobile_username.en\") text=\" Username: %s\" ;;\n \"success.mobile_password.zh\") text=\" 密码: %s\" ;;\n \"success.mobile_password.en\") text=\" Password: %s\" ;;\n # --- Other consoles and tips ---\n \"success.other_consoles.zh\") text=\"--- 其他控制台 ---\" ;;\n \"success.other_consoles.en\") text=\"--- Other Consoles ---\" ;;\n \"success.higress_console.zh\") text=\" Higress 控制台: http://localhost:%s(用户名: %s / 密码: %s)\" ;;\n \"success.higress_console.en\") text=\" Higress Console: http://localhost:%s (Username: %s / Password: %s)\" ;;\n \"success.manager_console.zh\") text=\" Manager 控制台(本地): http://localhost:%s(无需登录)\" ;;\n \"success.manager_console.en\") text=\" Manager Console (local): http://localhost:%s (no login required)\" ;;\n \"success.manager_console_gateway.zh\") text=\" Manager 控制台(网关): http://console-local.agentteams.io(用户名: %s / 密码: %s)\" ;;\n \"success.manager_console_gateway.en\") text=\" Manager Console (gateway): http://console-local.agentteams.io (Username: %s / Password: %s)\" ;;\n \"success.copaw_console.zh\") text=\" CoPaw App API: http://localhost:%s(无需登录)\" ;;\n \"success.copaw_console.en\") text=\" CoPaw App API: http://localhost:%s (no login required)\" ;;\n \"success.switch_llm.title.zh\") text=\"--- 切换 LLM 提供商 ---\" ;;\n \"success.switch_llm.title.en\") text=\"--- Switch LLM Providers ---\" ;;\n \"success.switch_llm.hint.zh\") text=\" 您可以通过 Higress 控制台切换到其他 LLM 提供商(OpenAI、Anthropic 等)。\" ;;\n \"success.switch_llm.hint.en\") text=\" You can switch to other LLM providers (OpenAI, Anthropic, etc.) via Higress Console.\" ;;\n \"success.switch_llm.docs.zh\") text=\" 详细说明请参阅:\" ;;\n \"success.switch_llm.docs.en\") text=\" For detailed instructions, see:\" ;;\n \"success.switch_llm.url.zh\") text=\" https://higress.ai/en/docs/ai/scene-guide/multi-proxy#console-configuration\" ;;\n \"success.switch_llm.url.en\") text=\" https://higress.ai/en/docs/ai/scene-guide/multi-proxy#console-configuration\" ;;\n \"success.tip.zh\") text=\"提示: 您也可以在聊天中让 Manager 为您配置 LLM 提供商。\" ;;\n \"success.tip.en\") text=\"Tip: You can also ask the Manager to configure LLM providers for you in the chat.\" ;;\n \"success.config_file.zh\") text=\"配置文件: %s\" ;;\n \"success.config_file.en\") text=\"Configuration file: %s\" ;;\n \"success.data_volume.zh\") text=\"数据卷: %s\" ;;\n \"success.data_volume.en\") text=\"Data volume: %s\" ;;\n \"success.workspace.zh\") text=\"Manager 工作空间: %s\" ;;\n \"success.workspace.en\") text=\"Manager workspace: %s\" ;;\n # --- Worker installation ---\n \"worker.resetting.zh\") text=\"正在重置 Worker: %s...\" ;;\n \"worker.resetting.en\") text=\"Resetting Worker: %s...\" ;;\n \"worker.exists.zh\") text=\"容器 '%s' 已存在。使用 --reset 重新创建。\" ;;\n \"worker.exists.en\") text=\"Container '%s' already exists. Use --reset to recreate.\" ;;\n \"worker.starting.zh\") text=\"正在启动 Worker: %s...\" ;;\n \"worker.starting.en\") text=\"Starting Worker: %s...\" ;;\n \"worker.skills_url.zh\") text=\" Skills API URL: %s\" ;;\n \"worker.skills_url.en\") text=\" Skills API URL: %s\" ;;\n \"worker.started.zh\") text=\"=== Worker %s 已启动!===\" ;;\n \"worker.started.en\") text=\"=== Worker %s Started! ===\" ;;\n \"worker.container.zh\") text=\"容器: %s\" ;;\n \"worker.container.en\") text=\"Container: %s\" ;;\n \"worker.view_logs.zh\") text=\"查看日志: docker logs -f %s\" ;;\n \"worker.view_logs.en\") text=\"View logs: docker logs -f %s\" ;;\n # --- Prompt function messages ---\n \"prompt.preset.zh\") text=\" %s = (已通过环境变量预设)\" ;;\n \"prompt.preset.en\") text=\" %s = (pre-set via env)\" ;;\n \"prompt.upgrade_keep.zh\") text=\" %s = %s(当前值,回车保留 / 输入新值覆盖)\" ;;\n \"prompt.upgrade_keep.en\") text=\" %s = %s (current value, press Enter to keep / type new value to change)\" ;;\n \"prompt.upgrade_keep_secret.zh\") text=\" %s = %s(当前值,回车保留 / 输入新值覆盖)\" ;;\n \"prompt.upgrade_keep_secret.en\") text=\" %s = %s (current value, press Enter to keep / type new value to change)\" ;;\n \"prompt.upgrade_empty.zh\") text=\" %s = (未设置,回车跳过 / 输入新值设置)\" ;;\n \"prompt.upgrade_empty.en\") text=\" %s = (not set, press Enter to skip / type new value to set)\" ;;\n \"prompt.default.zh\") text=\" %s = %s(默认)\" ;;\n \"prompt.default.en\") text=\" %s = %s (default)\" ;;\n \"prompt.required.zh\") text=\"%s 是必需的(在非交互模式下通过环境变量设置)\" ;;\n \"prompt.required.en\") text=\"%s is required (set via environment variable in non-interactive mode)\" ;;\n \"prompt.required_empty.zh\") text=\"%s 是必需的\" ;;\n \"prompt.required_empty.en\") text=\"%s is required\" ;;\n # --- Language switch prompt (bilingual by design) ---\n \"lang.detected.zh\") text=\"检测到语言 / Detected language: 中文\" ;;\n \"lang.detected.en\") text=\"检测到语言 / Detected language: English\" ;;\n \"lang.switch_title.zh\") text=\"切换语言 / Switch language:\" ;;\n \"lang.switch_title.en\") text=\"切换语言 / Switch language:\" ;;\n \"lang.option_zh.zh\") text=\" 1) 中文\" ;;\n \"lang.option_zh.en\") text=\" 1) 中文\" ;;\n \"lang.option_en.zh\") text=\" 2) English\" ;;\n \"lang.option_en.en\") text=\" 2) English\" ;;\n \"lang.prompt.zh\") text=\"请选择 / Enter choice\" ;;\n \"lang.prompt.en\") text=\"请选择 / Enter choice\" ;;\n # --- Uninstall messages ---\n \"uninstall.title.zh\") text=\"正在卸载 AgentTeams...\" ;;\n \"uninstall.title.en\") text=\"Uninstalling AgentTeams...\" ;;\n \"uninstall.stopping_manager.zh\") text=\"正在停止并移除 agentteams-manager...\" ;;\n \"uninstall.stopping_manager.en\") text=\"Stopping and removing agentteams-manager...\" ;;\n \"uninstall.stopping_workers.zh\") text=\"正在停止并移除 Worker 容器...\" ;;\n \"uninstall.stopping_workers.en\") text=\"Stopping and removing worker containers...\" ;;\n \"uninstall.removed.zh\") text=\" 已移除: %s\" ;;\n \"uninstall.removed.en\") text=\" Removed: %s\" ;;\n \"uninstall.removing_volume.zh\") text=\"正在移除 Docker 卷: %s\" ;;\n \"uninstall.removing_volume.en\") text=\"Removing Docker volume: %s\" ;;\n \"uninstall.removing_env.zh\") text=\"正在移除 env 文件: %s\" ;;\n \"uninstall.removing_env.en\") text=\"Removing env file: %s\" ;;\n \"uninstall.removing_proxy.zh\") text=\"正在停止并移除 Docker API 代理容器: agentteams-docker-proxy\" ;;\n \"uninstall.removing_proxy.en\") text=\"Stopping and removing Docker API proxy container: agentteams-docker-proxy\" ;;\n \"uninstall.stopping_controller.zh\") text=\"正在停止并移除 agentteams-controller (内嵌 Tuwunel/MinIO/Higress)...\" ;;\n \"uninstall.stopping_controller.en\") text=\"Stopping and removing agentteams-controller (embedded Tuwunel/MinIO/Higress)...\" ;;\n \"uninstall.removing_network.zh\") text=\"正在移除 Docker 网络: agentteams-net\" ;;\n \"uninstall.removing_network.en\") text=\"Removing Docker network: agentteams-net\" ;;\n \"uninstall.removing_workspace.zh\") text=\"正在移除工作空间目录: %s\" ;;\n \"uninstall.removing_workspace.en\") text=\"Removing workspace directory: %s\" ;;\n \"uninstall.removing_workspace_elevated.zh\") text=\" 工作空间包含 root 文件,通过容器清理...\" ;;\n \"uninstall.removing_workspace_elevated.en\") text=\" Workspace contains root-owned files, cleaning via container...\" ;;\n \"uninstall.removing_log.zh\") text=\"正在移除日志文件: %s\" ;;\n \"uninstall.removing_log.en\") text=\"Removing log file: %s\" ;;\n \"uninstall.done.zh\") text=\"AgentTeams 已卸载。\" ;;\n \"uninstall.done.en\") text=\"AgentTeams has been uninstalled.\" ;;\n # --- Error messages ---\n \"error.name_required.zh\") text=\"--name 是必需的\" ;;\n \"error.name_required.en\") text=\"--name is required\" ;;\n \"error.fs_required.zh\") text=\"--fs 是必需的\" ;;\n \"error.fs_required.en\") text=\"--fs is required\" ;;\n \"error.fs_key_required.zh\") text=\"--fs-key 是必需的\" ;;\n \"error.fs_key_required.en\") text=\"--fs-key is required\" ;;\n \"error.fs_secret_required.zh\") text=\"--fs-secret 是必需的\" ;;\n \"error.fs_secret_required.en\") text=\"--fs-secret is required\" ;;\n \"error.unknown_option.zh\") text=\"未知选项: %s\" ;;\n \"error.unknown_option.en\") text=\"Unknown option: %s\" ;;\n \"error.docker_not_found.zh\") text=\"未找到 docker 或 podman 命令。请先安装 Docker Desktop 或 Podman Desktop:\\n Docker Desktop: https://www.docker.com/products/docker-desktop/\\n Podman Desktop: https://podman-desktop.io/\" ;;\n \"error.docker_not_found.en\") text=\"docker or podman command not found. Please install Docker Desktop or Podman Desktop first:\\n Docker Desktop: https://www.docker.com/products/docker-desktop/\\n Podman Desktop: https://podman-desktop.io/\" ;;\n \"error.docker_not_running.zh\") text=\"Docker 未运行。请先启动 Docker Desktop 或 Podman Desktop。\" ;;\n \"error.docker_not_running.en\") text=\"Docker is not running. Please start Docker Desktop or Podman Desktop first.\" ;;\n # --- Fallback: try English for unknown lang ---\n *)\n case \"${key}.en\" in\n \"tz.warning.title.en\") text=\"Could not detect timezone automatically.\" ;;\n \"install.title.en\") text=\"=== AgentTeams Manager Installation ===\" ;;\n *) text=\"${key}\" ;;\n esac\n ;;\n esac\n if [ $# -gt 0 ]; then\n # shellcheck disable=SC2059\n printf \"${text}\\n\" \"$@\"\n else\n echo \"${text}\"\n fi\n}\n\n# ============================================================\n# Registry selection based on timezone\n# ============================================================\n\ndetect_registry() {\n local tz=\"${AGENTTEAMS_TIMEZONE}\"\n\n case \"${tz}\" in\n America/*)\n echo \"higress-registry.us-west-1.cr.aliyuncs.com\"\n ;;\n Asia/Singapore|Asia/Bangkok|Asia/Jakarta|Asia/Makassar|Asia/Jayapura|\\\n Asia/Kuala_Lumpur|Asia/Ho_Chi_Minh|Asia/Manila|Asia/Yangon|\\\n Asia/Vientiane|Asia/Phnom_Penh|Asia/Pontianak|Asia/Ujung_Pandang)\n echo \"higress-registry.ap-southeast-7.cr.aliyuncs.com\"\n ;;\n *)\n echo \"higress-registry.cn-hangzhou.cr.aliyuncs.com\"\n ;;\n esac\n}\n\nAGENTTEAMS_REGISTRY=\"${AGENTTEAMS_REGISTRY:-$(detect_registry)}\"\n# Backward compatibility: accept old env var names from previous versions\nAGENTTEAMS_INSTALL_CONTROLLER_IMAGE=\"${AGENTTEAMS_INSTALL_CONTROLLER_IMAGE:-${AGENTTEAMS_INSTALL_DOCKER_PROXY_IMAGE:-}}\"\n# Image variables are resolved after version selection in step_version().\n# These placeholders allow early code paths to reference them without errors.\nMANAGER_IMAGE=\"${AGENTTEAMS_INSTALL_MANAGER_IMAGE:-}\"\nMANAGER_COPAW_IMAGE=\"${AGENTTEAMS_INSTALL_MANAGER_COPAW_IMAGE:-}\"\nWORKER_IMAGE=\"${AGENTTEAMS_INSTALL_WORKER_IMAGE:-}\"\nCOPAW_WORKER_IMAGE=\"${AGENTTEAMS_INSTALL_COPAW_WORKER_IMAGE:-}\"\nQWENPAW_WORKER_IMAGE=\"${AGENTTEAMS_INSTALL_QWENPAW_WORKER_IMAGE:-}\"\nHERMES_WORKER_IMAGE=\"${AGENTTEAMS_INSTALL_HERMES_WORKER_IMAGE:-}\"\nCONTROLLER_IMAGE=\"${AGENTTEAMS_INSTALL_CONTROLLER_IMAGE:-}\"\n\nresolve_image_tags() {\n AGENTTEAMS_VERSION=\"$(_normalize_version \"${AGENTTEAMS_VERSION}\")\"\n MANAGER_IMAGE=\"${AGENTTEAMS_INSTALL_MANAGER_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-manager:${AGENTTEAMS_VERSION}}\"\n MANAGER_COPAW_IMAGE=\"${AGENTTEAMS_INSTALL_MANAGER_COPAW_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-manager-copaw:${AGENTTEAMS_VERSION}}\"\n WORKER_IMAGE=\"${AGENTTEAMS_INSTALL_WORKER_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-worker:${AGENTTEAMS_VERSION}}\"\n COPAW_WORKER_IMAGE=\"${AGENTTEAMS_INSTALL_COPAW_WORKER_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-copaw-worker:${AGENTTEAMS_VERSION}}\"\n QWENPAW_WORKER_IMAGE=\"${AGENTTEAMS_INSTALL_QWENPAW_WORKER_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-qwenpaw-worker:${AGENTTEAMS_VERSION}}\"\n HERMES_WORKER_IMAGE=\"${AGENTTEAMS_INSTALL_HERMES_WORKER_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-hermes-worker:${AGENTTEAMS_VERSION}}\"\n EMBEDDED_IMAGE=\"${AGENTTEAMS_INSTALL_EMBEDDED_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-embedded:${AGENTTEAMS_VERSION}}\"\n # CoPaw Worker introduced in v1.0.4; Hermes Worker introduced in v1.1.0\n if [ -z \"${AGENTTEAMS_INSTALL_COPAW_WORKER_IMAGE:-}\" ] && _ver_lt \"${AGENTTEAMS_VERSION}\" \"v1.0.4\"; then\n COPAW_WORKER_IMAGE=\"\"\n fi\n if [ -z \"${AGENTTEAMS_INSTALL_HERMES_WORKER_IMAGE:-}\" ] && _ver_lt \"${AGENTTEAMS_VERSION}\" \"v1.1.0\"; then\n HERMES_WORKER_IMAGE=\"\"\n fi\n}\n\n# Resolve the embedded controller image. Embedded mode is the only supported\n# architecture since PR #616 (manager image no longer bundles Higress/Tuwunel/MinIO).\n# If the embedded image is unavailable for the requested version, fail fast with an\n# actionable error rather than silently falling back to the legacy single-container\n# path — that path is permanently broken with the slim manager image and would just\n# leave the user with a manager container looping on \"Waiting for Higress Gateway\".\n# Sets EMBEDDED_IMAGE and AGENTTEAMS_USE_EMBEDDED.\nresolve_embedded_image() {\n AGENTTEAMS_USE_EMBEDDED=1\n\n # If the user explicitly overrode the image (e.g. `make install-embedded` passes\n # a locally-built tag), respect it as-is without any registry probe.\n if [ -n \"${AGENTTEAMS_INSTALL_EMBEDDED_IMAGE:-}\" ]; then\n EMBEDDED_IMAGE=\"${AGENTTEAMS_INSTALL_EMBEDDED_IMAGE}\"\n return 0\n fi\n\n local _versioned=\"${AGENTTEAMS_REGISTRY}/agentteams/agentteams-embedded:${AGENTTEAMS_VERSION}\"\n local _latest=\"${AGENTTEAMS_REGISTRY}/agentteams/agentteams-embedded:latest\"\n\n # Skip probe when AGENTTEAMS_VERSION is \"latest\" — no point trying the same tag twice.\n if [ \"${AGENTTEAMS_VERSION}\" = \"latest\" ]; then\n EMBEDDED_IMAGE=\"${_latest}\"\n return 0\n fi\n\n if ${DOCKER_CMD} pull \"${_versioned}\" >/dev/null 2>&1; then\n EMBEDDED_IMAGE=\"${_versioned}\"\n return 0\n fi\n\n # Versions before v1.1.0 predate agentteams-embedded entirely — their manager image\n # bundled all infrastructure. Falling back to agentteams-embedded:latest would\n # silently swap in the v1.1.0 architecture (embedded kube-apiserver) which\n # crashes under QEMU on Apple Silicon. Auto-activate legacy mode instead.\n if _ver_lt \"${AGENTTEAMS_VERSION}\" \"v1.1.0\"; then\n log \"INFO: ${AGENTTEAMS_VERSION} predates agentteams-embedded; switching to legacy all-in-one manager architecture.\"\n log \"WARNING: Legacy all-in-one mode requires AGENTTEAMS_VERSION <= v1.0.9 (older bundled manager image).\"\n log \"WARNING: Newer slim manager images will hang on 'Waiting for Higress Gateway'.\"\n AGENTTEAMS_USE_EMBEDDED=0\n return 0\n fi\n\n if ${DOCKER_CMD} pull \"${_latest}\" >/dev/null 2>&1; then\n log \"embedded ${AGENTTEAMS_VERSION} not found, using latest\"\n EMBEDDED_IMAGE=\"${_latest}\"\n return 0\n fi\n\n # Explicit escape hatch — still honoured for edge cases.\n if [ \"${AGENTTEAMS_FORCE_LEGACY:-0}\" = \"1\" ]; then\n log \"WARNING: AGENTTEAMS_FORCE_LEGACY=1 — using legacy all-in-one manager architecture.\"\n log \"WARNING: This requires AGENTTEAMS_VERSION <= v1.0.9 (older bundled manager image).\"\n log \"WARNING: Newer slim manager images will hang on 'Waiting for Higress Gateway'.\"\n AGENTTEAMS_USE_EMBEDDED=0\n return 0\n fi\n\n error \"Embedded controller image is not available in the registry:\"\n error \" - tried: ${_versioned}\"\n error \" - tried: ${_latest}\"\n error \"\"\n error \"Embedded mode is the only supported architecture since PR #616.\"\n error \"How to resolve:\"\n error \" 1) Pin to a AGENTTEAMS_VERSION whose embedded image has been published, or\"\n error \" wait for the release pipeline to publish it.\"\n error \" 2) For a local build, run: make install-embedded\"\n error \" (builds and uses the local embedded image without touching the registry).\"\n error \" 3) Override with a custom image: AGENTTEAMS_INSTALL_EMBEDDED_IMAGE=... ./agentteams-install.sh\"\n exit 1\n}\n\n# ============================================================\n# Known models list — used to detect custom models during install\n# ============================================================\nKNOWN_MODELS=\"gpt-5.4 gpt-5.3-codex gpt-5-mini gpt-5-nano claude-opus-4-6 claude-sonnet-4-6 claude-haiku-4-5 qwen3.6-plus qwen3.5-plus deepseek-chat deepseek-reasoner kimi-k2.5 glm-5 MiniMax-M2.7 MiniMax-M2.7-highspeed MiniMax-M2.5\"\n\nis_known_model() {\n local model=\"$1\"\n for m in ${KNOWN_MODELS}; do\n [ \"${m}\" = \"${model}\" ] && return 0\n done\n return 1\n}\n\n# Prompt user for custom model parameters when model is not in the known list.\n# Sets: AGENTTEAMS_MODEL_CONTEXT_WINDOW, AGENTTEAMS_MODEL_MAX_TOKENS, AGENTTEAMS_MODEL_REASONING, AGENTTEAMS_MODEL_VISION\nprompt_custom_model_params() {\n local model=\"$1\"\n if is_known_model \"${model}\"; then\n # Clear any stale custom params for known models\n AGENTTEAMS_MODEL_CONTEXT_WINDOW=\"\"\n AGENTTEAMS_MODEL_MAX_TOKENS=\"\"\n AGENTTEAMS_MODEL_REASONING=\"\"\n AGENTTEAMS_MODEL_VISION=\"\"\n return\n fi\n echo \"\"\n log \"$(msg llm.custom_model.detected \"${model}\")\"\n echo \"\"\n read -e -p \" $(msg llm.custom_model.context_prompt): \" AGENTTEAMS_MODEL_CONTEXT_WINDOW\n if [ \"${AGENTTEAMS_MODEL_CONTEXT_WINDOW}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n AGENTTEAMS_MODEL_CONTEXT_WINDOW=\"${AGENTTEAMS_MODEL_CONTEXT_WINDOW:-150000}\"\n read -e -p \" $(msg llm.custom_model.max_tokens_prompt): \" AGENTTEAMS_MODEL_MAX_TOKENS\n if [ \"${AGENTTEAMS_MODEL_MAX_TOKENS}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n AGENTTEAMS_MODEL_MAX_TOKENS=\"${AGENTTEAMS_MODEL_MAX_TOKENS:-128000}\"\n read -e -p \" $(msg llm.custom_model.reasoning_prompt): \" _reasoning\n if [ \"${_reasoning}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n case \"${_reasoning}\" in\n n|N|no|NO) AGENTTEAMS_MODEL_REASONING=\"false\" ;;\n *) AGENTTEAMS_MODEL_REASONING=\"true\" ;;\n esac\n read -e -p \" $(msg llm.custom_model.vision_prompt): \" _vision\n if [ \"${_vision}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n case \"${_vision}\" in\n y|Y|yes|YES) AGENTTEAMS_MODEL_VISION=\"true\" ;;\n *) AGENTTEAMS_MODEL_VISION=\"false\" ;;\n esac\n log \"$(msg llm.custom_model.summary \"${AGENTTEAMS_MODEL_CONTEXT_WINDOW}\" \"${AGENTTEAMS_MODEL_MAX_TOKENS}\" \"${AGENTTEAMS_MODEL_REASONING}\" \"${AGENTTEAMS_MODEL_VISION}\")\"\n}\n\n# ============================================================\n# Wait for Manager agent to be ready\n# Uses `openclaw gateway health` inside the container to confirm the gateway is running\n# ============================================================\n\nwait_manager_ready() {\n local timeout=\"${AGENTTEAMS_READY_TIMEOUT:-300}\"\n local elapsed=0\n local container=\"${1:-agentteams-manager}\"\n\n log \"$(msg install.wait_ready \"${timeout}\")\"\n\n # Wait for Manager agent to be healthy inside the container\n local runtime=\"${AGENTTEAMS_MANAGER_RUNTIME:-copaw}\"\n while [ \"${elapsed}\" -lt \"${timeout}\" ]; do\n case \"${runtime}\" in\n copaw)\n if ${DOCKER_CMD} exec \"${container}\" curl -sf http://127.0.0.1:18799/api/agents 2>/dev/null | grep -q '\"agents\"'; then\n log \"$(msg install.wait_ready.ok)\"\n return 0\n fi\n ;;\n *)\n if ${DOCKER_CMD} exec \"${container}\" openclaw gateway health --json 2>/dev/null | grep -q '\"ok\"' 2>/dev/null; then\n log \"$(msg install.wait_ready.ok)\"\n return 0\n fi\n ;;\n esac\n sleep 5\n elapsed=$((elapsed + 5))\n printf \"\\r\\033[36m[AgentTeams]\\033[0m $(msg install.wait_ready.waiting \"${elapsed}\" \"${timeout}\")\"\n done\n\n echo \"\"\n die \"$(msg install.wait_ready.timeout \"${timeout}\" \"${container}\")\"\n}\n\nwait_matrix_ready() {\n local timeout=\"${AGENTTEAMS_READY_TIMEOUT:-300}\"\n local elapsed=0\n local container=\"${1:-agentteams-manager}\"\n\n log \"$(msg install.wait_matrix \"${timeout}\")\"\n\n while [ \"${elapsed}\" -lt \"${timeout}\" ]; do\n if ${DOCKER_CMD} exec \"${container}\" curl -sf http://127.0.0.1:6167/_tuwunel/server_version >/dev/null 2>&1; then\n log \"$(msg install.wait_matrix.ok)\"\n return 0\n fi\n sleep 5\n elapsed=$((elapsed + 5))\n printf \"\\r\\033[36m[AgentTeams]\\033[0m $(msg install.wait_matrix.waiting \"${elapsed}\" \"${timeout}\")\"\n done\n\n echo \"\"\n die \"$(msg install.wait_matrix.timeout \"${timeout}\" \"${container}\")\"\n}\n\n# Read KEY=value from /data/agentteams-secrets.env on a Docker volume (manager container not required).\n# Requires EMBEDDED_IMAGE (resolved earlier in install_manager). Uses ${DOCKER_CMD}.\nagentteams_read_secret_from_data_volume() {\n local _vol=\"$1\" _key=\"$2\"\n if [ -z \"${_vol}\" ] || [ -z \"${_key}\" ] || [ -z \"${EMBEDDED_IMAGE:-}\" ]; then\n echo \"\"\n return 0\n fi\n ${DOCKER_CMD} run --rm --entrypoint sh \\\n -v \"${_vol}:/data:ro\" \\\n \"${EMBEDDED_IMAGE}\" -c \"grep \\\"^${_key}=\\\" /data/agentteams-secrets.env 2>/dev/null | cut -d= -f2- | head -1 | tr -d '\\r'\" 2>/dev/null\n}\n\n# Read KEY=value from /data/worker-creds/<worker>.env on a Docker volume.\nagentteams_read_worker_creds_value_from_volume() {\n local _vol=\"$1\" _worker=\"$2\" _key=\"$3\"\n if [ -z \"${_vol}\" ] || [ -z \"${_worker}\" ] || [ -z \"${_key}\" ] || [ -z \"${EMBEDDED_IMAGE:-}\" ]; then\n echo \"\"\n return 0\n fi\n ${DOCKER_CMD} run --rm --entrypoint sh \\\n -v \"${_vol}:/data:ro\" \\\n \"${EMBEDDED_IMAGE}\" -c \"grep \\\"^${_key}=\\\" \\\"/data/worker-creds/${_worker}.env\\\" 2>/dev/null | cut -d= -f2- | head -1 | tr -d \\\"\\\\r\\\"\" 2>/dev/null\n}\n\n# Read admin_dm_room_id from host workspace state.json (fallback when Matrix API is unavailable).\nagentteams_read_admin_dm_room_from_workspace() {\n local _ws=\"$1\"\n local _f=\"${_ws}/state.json\"\n if [ ! -f \"${_f}\" ] || ! command -v jq >/dev/null 2>&1; then\n echo \"\"\n return 0\n fi\n jq -r '.admin_dm_room_id // empty | select(. != \"null\")' \"${_f}\" 2>/dev/null\n}\n\n# Read secret input with masked echo (shows * per keystroke, supports backspace)\n# Usage: read_secret \"prompt text: \"; value=\"${_RS_RESULT}\"\nread_secret() {\n local _rs_prompt=\"$1\"\n _RS_RESULT=\"\"\n local _rs_char=\"\"\n\n printf \"%s\" \"${_rs_prompt}\"\n\n while IFS= read -r -s -n 1 _rs_char; do\n if [[ -z \"${_rs_char}\" ]]; then\n break\n elif [[ \"${_rs_char}\" == $'\\177' ]] || [[ \"${_rs_char}\" == $'\\b' ]]; then\n if [ -n \"${_RS_RESULT}\" ]; then\n _RS_RESULT=\"${_RS_RESULT%?}\"\n printf \"\\b \\b\"\n fi\n else\n _RS_RESULT=\"${_RS_RESULT}${_rs_char}\"\n printf \"*\"\n fi\n done\n echo\n}\n\n# Load current parameter values from env file for upgrade mode display\nload_current_params_from_env() {\n local env_file=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n if [ -f \"${env_file}\" ]; then\n [ -z \"${AGENTTEAMS_LLM_PROVIDER:+x}\" ] && AGENTTEAMS_LLM_PROVIDER=\"$(grep '^AGENTTEAMS_LLM_PROVIDER=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_OPENAI_BASE_URL:+x}\" ] && AGENTTEAMS_OPENAI_BASE_URL=\"$(grep '^AGENTTEAMS_OPENAI_BASE_URL=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_DEFAULT_MODEL:+x}\" ] && AGENTTEAMS_DEFAULT_MODEL=\"$(grep '^AGENTTEAMS_DEFAULT_MODEL=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_EMBEDDING_MODEL:+x}\" ] && AGENTTEAMS_EMBEDDING_MODEL=\"$(grep '^AGENTTEAMS_EMBEDDING_MODEL=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_WORKSPACE_DIR:+x}\" ] && AGENTTEAMS_WORKSPACE_DIR=\"$(grep '^AGENTTEAMS_WORKSPACE_DIR=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_HOST_SHARE_DIR:+x}\" ] && AGENTTEAMS_HOST_SHARE_DIR=\"$(grep '^AGENTTEAMS_HOST_SHARE_DIR=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_PODMAN_AUTOSTART:+x}\" ] && AGENTTEAMS_PODMAN_AUTOSTART=\"$(grep '^AGENTTEAMS_PODMAN_AUTOSTART=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_DASHBOARD:+x}\" ] && AGENTTEAMS_DASHBOARD=\"$(grep '^AGENTTEAMS_DASHBOARD=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_DASHBOARD_VERSION:+x}\" ] && AGENTTEAMS_DASHBOARD_VERSION=\"$(grep '^AGENTTEAMS_DASHBOARD_VERSION=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_PORT_DASHBOARD:+x}\" ] && AGENTTEAMS_PORT_DASHBOARD=\"$(grep '^AGENTTEAMS_PORT_DASHBOARD=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_DASHBOARD_IMAGE:+x}\" ] && AGENTTEAMS_DASHBOARD_IMAGE=\"$(grep '^AGENTTEAMS_DASHBOARD_IMAGE=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL:+x}\" ] && AGENTTEAMS_AI_GATEWAY_ADMIN_URL=\"$(grep '^AGENTTEAMS_AI_GATEWAY_ADMIN_URL=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n fi\n}\n\n# In non-interactive mode, uses default or errors if required and no default.\n# Usage: prompt VAR_NAME \"Prompt text\" \"default\" [true=secret]\nprompt() {\n local var_name=\"$1\"\n local prompt_text=\"$2\"\n local default_value=\"$3\"\n local is_secret=\"${4:-false}\"\n\n # If the variable is already set in the environment, use it silently\n # In upgrade mode, show current value and let user change it\n eval \"local current_value=\\\"\\${${var_name}}\\\"\"\n if [ -n \"${current_value}\" ]; then\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_NON_INTERACTIVE}\" != \"1\" ]; then\n # Show masked value for secrets, full value otherwise\n local display_value=\"${current_value}\"\n if [ \"${is_secret}\" = \"true\" ]; then\n local len=${#current_value}\n if [ \"${len}\" -le 8 ]; then\n display_value=\"****\"\n else\n display_value=\"${current_value:0:4}****${current_value: -4}\"\n fi\n fi\n log \"$(msg prompt.upgrade_keep \"${prompt_text}\" \"${display_value}\")\"\n local new_value=\"\"\n if [ \"${is_secret}\" = \"true\" ]; then\n read_secret \"${prompt_text}: \"\n new_value=\"${_RS_RESULT}\"\n else\n read -e -p \"${prompt_text}: \" new_value\n if [ \"${new_value}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n fi\n if [ -n \"${new_value}\" ]; then\n eval \"export ${var_name}='${new_value}'\"\n fi\n return\n fi\n log \"$(msg prompt.preset \"${prompt_text}\")\"\n return\n fi\n\n # Non-interactive or quickstart: use default or error\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] || [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ]; then\n if [ -n \"${default_value}\" ]; then\n eval \"export ${var_name}='${default_value}'\"\n log \"$(msg prompt.default \"${prompt_text}\" \"${default_value}\")\"\n return\n elif [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n # Only hard-error in fully non-interactive mode, not quickstart\n die \"$(msg prompt.required \"${prompt_text}\")\"\n fi\n # quickstart + no default: fall through to interactive prompt below\n fi\n\n if [ -n \"${default_value}\" ]; then\n prompt_text=\"${prompt_text} [${default_value}]\"\n fi\n\n local value=\"\"\n if [ \"${is_secret}\" = \"true\" ]; then\n read_secret \"${prompt_text}: \"\n value=\"${_RS_RESULT}\"\n else\n read -e -p \"${prompt_text}: \" value\n if [ \"${value}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n fi\n\n value=\"${value:-${default_value}}\"\n if [ -z \"${value}\" ]; then\n die \"$(msg prompt.required_empty \"${prompt_text}\")\"\n fi\n\n eval \"export ${var_name}='${value}'\"\n}\n\n# Prompt for an optional value (empty string is acceptable)\n# Skips prompt if variable is already defined in environment (even if empty)\n# In upgrade mode, shows current value and lets user change it.\n# In non-interactive mode, defaults to empty string.\nprompt_optional() {\n local var_name=\"$1\"\n local prompt_text=\"$2\"\n local is_secret=\"${3:-false}\"\n\n # Check if variable is defined (even if set to empty string)\n eval \"local _chk=\\\"\\${${var_name}+x}\\\"\"\n if [ -n \"${_chk}\" ]; then\n # In upgrade mode, show current value and let user change it\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_NON_INTERACTIVE}\" != \"1\" ]; then\n eval \"local current_value=\\\"\\${${var_name}}\\\"\"\n local display_value=\"${current_value}\"\n if [ \"${is_secret}\" = \"true\" ] && [ -n \"${current_value}\" ]; then\n local len=${#current_value}\n if [ \"${len}\" -le 8 ]; then\n display_value=\"****\"\n else\n display_value=\"${current_value:0:4}****${current_value: -4}\"\n fi\n fi\n if [ -n \"${current_value}\" ]; then\n if [ \"${is_secret}\" = \"true\" ]; then\n log \"$(msg prompt.upgrade_keep_secret \"${prompt_text}\" \"${display_value}\")\"\n else\n log \"$(msg prompt.upgrade_keep \"${prompt_text}\" \"${display_value}\")\"\n fi\n else\n log \"$(msg prompt.upgrade_empty \"${prompt_text}\")\"\n fi\n local new_value=\"\"\n if [ \"${is_secret}\" = \"true\" ]; then\n read_secret \"${prompt_text}: \"\n new_value=\"${_RS_RESULT}\"\n else\n read -e -p \"${prompt_text}: \" new_value\n if [ \"${new_value}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n fi\n if [ -n \"${new_value}\" ]; then\n eval \"export ${var_name}='${new_value}'\"\n fi\n return\n fi\n log \"$(msg prompt.preset \"${prompt_text}\")\"\n return\n fi\n\n # Non-interactive or quickstart: skip, leave unset\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] || [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ]; then\n eval \"export ${var_name}=''\"\n return\n fi\n\n local value=\"\"\n if [ \"${is_secret}\" = \"true\" ]; then\n read_secret \"${prompt_text}: \"\n value=\"${_RS_RESULT}\"\n else\n read -e -p \"${prompt_text}: \" value\n if [ \"${value}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n fi\n\n eval \"export ${var_name}='${value}'\"\n}\n\ngenerate_key() {\n openssl rand -hex 32\n}\n\n# ============================================================\n# Detect container runtime socket on the host\n#\n# Dependency: Requires DOCKER_CMD to reflect the TRUE underlying runtime.\n# (Crucial for unmasking 'podman' disguised as 'docker'.\n# Must be strictly resolved prior, e.g., via check_container_runtime)\n# ============================================================\ndetect_socket() {\n local socket_path\n\n # 1. Respect explicitly defined DOCKER_HOST environment variable\n if [ -n \"${DOCKER_HOST}\" ]; then\n socket_path=$(echo \"${DOCKER_HOST}\" | sed 's|^unix://||')\n if [ -S \"${socket_path}\" ]; then\n echo \"${socket_path}\"\n return 0\n fi\n fi\n\n # 2. Route based on the resolved container runtime\n case \"${DOCKER_CMD}\" in\n docker)\n # Docker path handling\n socket_path=$(docker context ls --format '{{if .Current}}{{.DockerEndpoint}}{{end}}' 2>/dev/null | grep . | sed 's|^unix://||')\n if [ -n \"${socket_path}\" ] && [ -S \"${socket_path}\" ]; then\n echo \"${socket_path}\"\n return 0\n fi\n\n # Rootless Docker\n if [ -n \"${XDG_RUNTIME_DIR:-}\" ] && [ -S \"${XDG_RUNTIME_DIR}/docker.sock\" ]; then\n echo \"${XDG_RUNTIME_DIR}/docker.sock\"\n return 0\n fi\n\n # Rootless Docker (macOS fallback)\n if [ -S \"${HOME}/.docker/run/docker.sock\" ]; then\n echo \"${HOME}/.docker/run/docker.sock\"\n return 0\n fi\n\n # Root Docker\n if [ -S \"/var/run/docker.sock\" ]; then\n echo \"/var/run/docker.sock\"\n return 0\n fi\n ;;\n podman)\n # Podman path handling\n if [ \"$(id -u)\" -eq 0 ]; then\n socket_path=\"/run/podman/podman.sock\"\n else\n socket_path=\"${XDG_RUNTIME_DIR}/podman/podman.sock\"\n fi\n if [ -S \"${socket_path}\" ]; then\n echo \"${socket_path}\"\n return 0\n fi\n ;;\n esac\n\n # Return empty if no socket is found\n echo \"\"\n return 0\n}\n\n# Ensure Podman API socket is active (State mutation)\nensure_podman_socket() {\n local socket_path\n\n # Only applicable if runtime is podman and systemctl is available\n if [ \"${DOCKER_CMD:-}\" != \"podman\" ] || ! command -v systemctl >/dev/null 2>&1; then\n return 0\n fi\n\n echo \"Ensuring Podman API socket is active...\" >&2\n\n # Enable and start the socket based on user privileges\n if [ \"$(id -u)\" -eq 0 ]; then\n socket_path=\"/run/podman/podman.sock\"\n systemctl enable --now podman.socket >/dev/null 2>&1 || true\n else\n socket_path=\"${XDG_RUNTIME_DIR}/podman/podman.sock\"\n systemctl --user enable --now podman.socket >/dev/null 2>&1 || true\n fi\n\n # Give systemd a brief moment to assert the socket for root or rootless user\n local _i\n for _i in 1 2 3; do\n [ -S \"${socket_path}\" ] && break\n sleep 1\n done\n}\n\n# Detect local LAN IP address (cross-platform: macOS and Linux)\ndetect_lan_ip() {\n local ip=\"\"\n\n # macOS: try common Wi-Fi / Ethernet interfaces\n if command -v ipconfig >/dev/null 2>&1; then\n for iface in en0 en1 en2 en3 en4; do\n ip=$(ipconfig getifaddr \"${iface}\" 2>/dev/null)\n if [ -n \"${ip}\" ]; then\n echo \"${ip}\"\n return 0\n fi\n done\n fi\n\n # Linux: ip route — most reliable\n if command -v ip >/dev/null 2>&1; then\n ip=$(ip route get 1.1.1.1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i==\"src\"){print $(i+1); exit}}')\n if [ -n \"${ip}\" ]; then\n echo \"${ip}\"\n return 0\n fi\n fi\n\n # Linux fallback: hostname -I (space-separated list, take first non-loopback)\n if command -v hostname >/dev/null 2>&1; then\n ip=$(hostname -I 2>/dev/null | tr ' ' '\\n' | grep -v '^127\\.' | grep -v '^::' | head -1)\n if [ -n \"${ip}\" ]; then\n echo \"${ip}\"\n return 0\n fi\n fi\n\n # Last resort: ifconfig\n if command -v ifconfig >/dev/null 2>&1; then\n ip=$(ifconfig 2>/dev/null | awk '/inet /{if($2!~/^127\\./){print $2; exit}}')\n # Strip \"addr:\" prefix that some ifconfig versions add\n ip=\"${ip#addr:}\"\n if [ -n \"${ip}\" ]; then\n echo \"${ip}\"\n return 0\n fi\n fi\n\n echo \"\"\n}\n\n\n# ============================================================\n# Step-back navigation helpers\n# ============================================================\n\n# should_skip_step: returns 0 (skip) when the step is irrelevant in current mode\nshould_skip_step() {\n local step_fn=\"$1\"\n case \"${step_fn}\" in\n step_lang|step_mode)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n ;;\n step_version)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n ;;\n step_existing)\n local _env=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n [ ! -f \"${_env}\" ] && return 0\n ;;\n # Keep-All upgrade mode: skip all config steps (step_volume/step_workspace handled separately)\n step_llm|step_admin|step_network|step_ports|step_domains|step_github|step_skills|step_runtime|step_manager_runtime|step_dashboard|step_e2ee|step_docker_proxy|step_idle|step_hostshare)\n [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ] && return 0\n ;;\n step_volume|step_workspace)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ] && return 0\n [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ] && return 0\n ;;\n step_e2ee|step_idle)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE}\" != \"1\" ] && return 0\n ;;\n step_docker_proxy)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE}\" != \"1\" ] && return 0\n # Embedded mode handles docker access natively — skip this step\n [ \"${AGENTTEAMS_USE_EMBEDDED:-}\" = \"1\" ] && return 0\n ;;\n step_manager_runtime)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n ;;\n step_hostshare)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ] && return 0\n ;;\n step_podman_autostart)\n # Only relevant for Podman with systemd\n [ \"${DOCKER_CMD}\" != \"podman\" ] && return 0\n ! command -v systemctl >/dev/null 2>&1 && return 0\n\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE}\" != \"1\" ] && return 0\n [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ] && return 0\n ;;\n esac\n return 1\n}\n\n# clear_step_vars: unset variables set by a step so it will re-prompt on re-entry\nclear_step_vars() {\n local step_fn=\"$1\"\n case \"${step_fn}\" in\n step_mode) unset AGENTTEAMS_QUICKSTART ;;\n step_version) unset AGENTTEAMS_VERSION ;;\n step_existing) unset AGENTTEAMS_UPGRADE UPGRADE_EXISTING_WORKERS ;;\n step_llm)\n unset AGENTTEAMS_LLM_PROVIDER AGENTTEAMS_DEFAULT_MODEL AGENTTEAMS_OPENAI_BASE_URL\n unset AGENTTEAMS_LLM_API_KEY AGENTTEAMS_MODEL_CONTEXT_WINDOW AGENTTEAMS_MODEL_MAX_TOKENS\n unset AGENTTEAMS_MODEL_REASONING AGENTTEAMS_MODEL_VISION\n ;;\n step_admin) unset AGENTTEAMS_ADMIN_USER AGENTTEAMS_ADMIN_PASSWORD ;;\n step_network) unset AGENTTEAMS_LOCAL_ONLY ;;\n step_ports)\n unset AGENTTEAMS_PORT_GATEWAY AGENTTEAMS_PORT_CONSOLE\n unset AGENTTEAMS_PORT_ELEMENT_WEB AGENTTEAMS_PORT_MANAGER_CONSOLE\n ;;\n step_domains)\n unset AGENTTEAMS_MATRIX_DOMAIN AGENTTEAMS_MATRIX_CLIENT_DOMAIN\n unset AGENTTEAMS_AI_GATEWAY_DOMAIN AGENTTEAMS_FS_DOMAIN AGENTTEAMS_CONSOLE_DOMAIN\n ;;\n step_github) unset AGENTTEAMS_GITHUB_TOKEN ;;\n step_skills) unset AGENTTEAMS_SKILLS_API_URL ;;\n step_volume) unset AGENTTEAMS_DATA_DIR ;;\n step_workspace) unset AGENTTEAMS_WORKSPACE_DIR ;;\n step_dashboard) unset AGENTTEAMS_DASHBOARD AGENTTEAMS_DASHBOARD_VERSION AGENTTEAMS_PORT_DASHBOARD AGENTTEAMS_DASHBOARD_IMAGE AGENTTEAMS_AI_GATEWAY_ADMIN_URL ;;\n step_runtime) unset AGENTTEAMS_DEFAULT_WORKER_RUNTIME ;;\n step_manager_runtime) unset AGENTTEAMS_MANAGER_RUNTIME ;;\n step_e2ee) unset AGENTTEAMS_MATRIX_E2EE ;;\n step_docker_proxy) unset AGENTTEAMS_DOCKER_PROXY; unset AGENTTEAMS_PROXY_ALLOWED_REGISTRIES ;;\n step_idle) unset AGENTTEAMS_WORKER_IDLE_TIMEOUT ;;\n step_hostshare) unset AGENTTEAMS_HOST_SHARE_DIR ;;\n step_podman_autostart) unset AGENTTEAMS_PODMAN_AUTOSTART ;;\n esac\n}\n\n# ============================================================\n# Individual step functions\n# ============================================================\n\nstep_lang() {\n local lang_default_choice=\"2\"\n [ \"${AGENTTEAMS_LANGUAGE}\" = \"zh\" ] && lang_default_choice=\"1\"\n log \"$(msg lang.detected)\"\n log \"$(msg lang.switch_title)\"\n echo \"$(msg lang.option_zh)\"\n echo \"$(msg lang.option_en)\"\n echo \"\"\n local LANG_CHOICE\n read -e -p \"$(msg lang.prompt) [${lang_default_choice}]: \" LANG_CHOICE\n LANG_CHOICE=\"${LANG_CHOICE:-${lang_default_choice}}\"\n if [ \"${LANG_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n case \"${LANG_CHOICE}\" in\n 1) AGENTTEAMS_LANGUAGE=\"zh\" ;;\n 2) AGENTTEAMS_LANGUAGE=\"en\" ;;\n esac\n export AGENTTEAMS_LANGUAGE\n log \"\"\n}\n\nstep_mode() {\n log \"$(msg install.mode.title)\"\n echo \"\"\n echo \"$(msg install.mode.choose)\"\n echo \"$(msg install.mode.quickstart)\"\n echo \"$(msg install.mode.manual)\"\n echo \"\"\n local ONBOARDING_CHOICE\n read -e -p \"$(msg install.mode.prompt): \" ONBOARDING_CHOICE\n ONBOARDING_CHOICE=\"${ONBOARDING_CHOICE:-1}\"\n if [ \"${ONBOARDING_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n case \"${ONBOARDING_CHOICE}\" in\n 1|quick|quickstart)\n log \"$(msg install.mode.quickstart_selected)\"\n AGENTTEAMS_QUICKSTART=1\n ;;\n 2|manual)\n log \"$(msg install.mode.manual_selected)\"\n ;;\n *)\n log \"$(msg install.mode.invalid)\"\n AGENTTEAMS_QUICKSTART=1\n ;;\n esac\n log \"\"\n}\n\nstep_version() {\n # Skip if version already provided via env var\n if [ -n \"${AGENTTEAMS_VERSION}\" ]; then\n resolve_image_tags\n return 0\n fi\n # Try to fetch the latest stable release from GitHub\n log \"$(msg install.version.fetching)\"\n local _fetched\n _fetched=$(curl -sf --max-time 5 \\\n -H \"Accept: application/vnd.github+json\" \\\n \"https://api.github.com/repos/agentscope-ai/AgentTeams/releases/latest\" \\\n 2>/dev/null | grep '\"tag_name\"' | head -1 | sed 's/.*\"tag_name\": *\"\\([^\"]*\\)\".*/\\1/')\n if [ -n \"${_fetched}\" ]; then\n AGENTTEAMS_KNOWN_STABLE_VERSION=\"${_fetched}\"\n else\n log \"$(msg install.version.fetch_failed \"${AGENTTEAMS_KNOWN_STABLE_VERSION}\")\"\n fi\n log \"$(msg install.version.title)\"\n echo \"\"\n echo \"$(msg install.version.choose)\"\n echo \"$(msg install.version.option_latest)\"\n printf \"%s\\n\" \"$(msg install.version.option_stable \"${AGENTTEAMS_KNOWN_STABLE_VERSION}\")\"\n echo \"$(msg install.version.option_custom)\"\n echo \"\"\n local VERSION_CHOICE\n read -e -p \"$(msg install.version.prompt) [1]: \" VERSION_CHOICE\n VERSION_CHOICE=\"${VERSION_CHOICE:-1}\"\n if [ \"${VERSION_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n case \"${VERSION_CHOICE}\" in\n 1|latest)\n AGENTTEAMS_VERSION=\"latest\"\n log \"$(msg install.version.selected_latest)\"\n ;;\n 2|stable)\n AGENTTEAMS_VERSION=\"${AGENTTEAMS_KNOWN_STABLE_VERSION}\"\n log \"$(msg install.version.selected_stable \"${AGENTTEAMS_VERSION}\")\"\n ;;\n 3|custom)\n local CUSTOM_VERSION\n read -e -p \"$(msg install.version.custom_prompt): \" CUSTOM_VERSION\n AGENTTEAMS_VERSION=\"${CUSTOM_VERSION:-${AGENTTEAMS_KNOWN_STABLE_VERSION}}\"\n log \"$(msg install.version.selected_custom \"${AGENTTEAMS_VERSION}\")\"\n ;;\n *)\n AGENTTEAMS_VERSION=\"${AGENTTEAMS_KNOWN_STABLE_VERSION}\"\n log \"$(msg install.version.invalid \"${AGENTTEAMS_VERSION}\")\"\n ;;\n esac\n log \"\"\n resolve_image_tags\n}\n\nstep_existing() {\n local existing_env=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n log \"$(msg install.existing.detected \"${existing_env}\")\"\n local running_manager=\"\" running_workers=\"\" existing_workers=\"\"\n if ${DOCKER_CMD} ps --format '{{.Names}}' | grep -q \"^agentteams-manager$\"; then\n running_manager=\"agentteams-manager\"\n fi\n running_workers=$(${DOCKER_CMD} ps --format '{{.Names}}' | grep \"^agentteams-worker-\" || true)\n existing_workers=$(${DOCKER_CMD} ps -a --format '{{.Names}}' | grep \"^agentteams-worker-\" || true)\n local UPGRADE_CHOICE\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n log \"$(msg install.existing.upgrade_noninteractive)\"\n UPGRADE_CHOICE=\"upgrade\"\n else\n echo \"\"\n echo \"$(msg install.existing.choose)\"\n echo \"$(msg install.existing.upgrade)\"\n echo \"$(msg install.existing.reinstall)\"\n echo \"$(msg install.existing.cancel)\"\n echo \"\"\n read -e -p \"$(msg install.existing.prompt): \" UPGRADE_CHOICE\n UPGRADE_CHOICE=\"${UPGRADE_CHOICE:-1}\"\n if [ \"${UPGRADE_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n fi\n case \"${UPGRADE_CHOICE}\" in\n 1|upgrade)\n AGENTTEAMS_UPGRADE=1\n log \"$(msg install.existing.upgrading)\"\n # Show upgrade mode sub-menu (unless already set via env var)\n if [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL:-0}\" != \"1\" ]; then\n echo \"\"\n echo \"$(msg upgrade.mode.prompt)\"\n echo \"$(msg upgrade.mode.keep_all)\"\n echo \"$(msg upgrade.mode.confirm_each)\"\n echo \"$(msg upgrade.mode.back)\"\n echo \"\"\n local UPGRADE_MODE_CHOICE\n read -e -p \"$(msg install.existing.prompt): \" UPGRADE_MODE_CHOICE\n UPGRADE_MODE_CHOICE=\"${UPGRADE_MODE_CHOICE:-1}\"\n case \"${UPGRADE_MODE_CHOICE}\" in\n 1|keep)\n AGENTTEAMS_UPGRADE_KEEP_ALL=1\n ;;\n 2|confirm)\n AGENTTEAMS_UPGRADE_KEEP_ALL=0\n ;;\n 3|b)\n STEP_RESULT=\"back\"\n return 0\n ;;\n *)\n AGENTTEAMS_UPGRADE_KEEP_ALL=0\n ;;\n esac\n fi\n # Load current parameters for both Keep-All and confirm-each modes\n load_current_params_from_env\n if [ -n \"${running_manager}\" ] || [ -n \"${running_workers}\" ]; then\n echo \"\"\n echo -e \"\\033[33m$(msg install.existing.warn_manager_stop)\\033[0m\"\n if [ -n \"${existing_workers}\" ]; then\n echo -e \"\\033[33m$(msg install.existing.warn_worker_recreate)\\033[0m\"\n fi\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" != \"1\" ]; then\n echo \"\"\n local CONFIRM_STOP\n read -e -p \"$(msg install.existing.continue_prompt): \" CONFIRM_STOP\n if [ \"${CONFIRM_STOP}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n if [ \"${CONFIRM_STOP}\" != \"y\" ] && [ \"${CONFIRM_STOP}\" != \"Y\" ]; then\n log \"$(msg install.existing.cancelled)\"\n exit 0\n fi\n fi\n fi\n UPGRADE_EXISTING_WORKERS=\"${existing_workers}\"\n ;;\n 2|reinstall)\n log \"$(msg install.reinstall.performing)\"\n local existing_workspace=\"\"\n if [ -f \"${existing_env}\" ]; then\n existing_workspace=$(grep '^AGENTTEAMS_WORKSPACE_DIR=' \"${existing_env}\" 2>/dev/null | cut -d= -f2-)\n fi\n [ -z \"${existing_workspace}\" ] && existing_workspace=\"${HOME}/agentteams-manager\"\n echo \"\"\n echo -e \"\\033[33m$(msg install.reinstall.warn_stop)\\033[0m\"\n [ -n \"${running_manager}\" ] && echo -e \"\\033[33m - ${running_manager} (manager)\\033[0m\"\n for w in ${running_workers}; do\n echo -e \"\\033[33m - ${w} (worker)\\033[0m\"\n done\n echo \"\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_delete)\\033[0m\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_volume)\\033[0m\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_env \"${existing_env}\")\\033[0m\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_workspace \"${existing_workspace}\")\\033[0m\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_workers)\\033[0m\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_proxy)\\033[0m\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_network)\\033[0m\"\n echo \"\"\n echo -e \"\\033[31m$(msg install.reinstall.confirm_type)\\033[0m\"\n echo -e \"\\033[31m ${existing_workspace}\\033[0m\"\n echo \"\"\n local CONFIRM_PATH\n read -e -p \"$(msg install.reinstall.confirm_path): \" CONFIRM_PATH\n if [ \"${CONFIRM_PATH}\" != \"${existing_workspace}\" ]; then\n die \"$(msg install.reinstall.path_mismatch \"${CONFIRM_PATH}\" \"${existing_workspace}\")\"\n fi\n log \"$(msg install.reinstall.confirmed)\"\n ${DOCKER_CMD} stop agentteams-manager 2>/dev/null || true\n ${DOCKER_CMD} rm agentteams-manager 2>/dev/null || true\n for w in $(${DOCKER_CMD} ps -a --format '{{.Names}}' | grep \"^agentteams-worker-\" || true); do\n ${DOCKER_CMD} stop \"${w}\" 2>/dev/null || true\n ${DOCKER_CMD} rm \"${w}\" 2>/dev/null || true\n log \"$(msg install.reinstall.removed_worker \"${w}\")\"\n done\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' | grep -q \"^agentteams-controller$\"; then\n log \"$(msg install.reinstall.removing_proxy)\"\n ${DOCKER_CMD} stop agentteams-controller 2>/dev/null || true\n ${DOCKER_CMD} rm agentteams-controller 2>/dev/null || true\n fi\n if ${DOCKER_CMD} network ls --format '{{.Name}}' | grep -q \"^agentteams-net$\"; then\n log \"$(msg install.reinstall.removing_network)\"\n ${DOCKER_CMD} network rm agentteams-net 2>/dev/null || true\n fi\n if ${DOCKER_CMD} volume ls -q | grep -q \"^agentteams-data$\"; then\n log \"$(msg install.reinstall.removing_volume)\"\n ${DOCKER_CMD} volume rm agentteams-data 2>/dev/null || log \"$(msg install.reinstall.warn_volume_fail)\"\n fi\n if [ -d \"${existing_workspace}\" ]; then\n log \"$(msg install.reinstall.removing_workspace \"${existing_workspace}\")\"\n rm -rf \"${existing_workspace}\" || die \"$(msg install.reinstall.failed_rm_workspace)\"\n fi\n if [ -f \"${existing_env}\" ]; then\n log \"$(msg install.reinstall.removing_env \"${existing_env}\")\"\n rm -f \"${existing_env}\"\n fi\n log \"$(msg install.reinstall.cleanup_done)\"\n unset AGENTTEAMS_WORKSPACE_DIR\n return 0\n ;;\n 3|cancel|*)\n log \"$(msg install.existing.cancelled)\"\n exit 0\n ;;\n esac\n # Load existing env file as fallback (shell env vars take priority)\n if [ -f \"${existing_env}\" ]; then\n log \"$(msg install.loading_config \"${existing_env}\")\"\n while IFS='=' read -r key value; do\n case \"${key}\" in \\#*|\"\") continue ;; esac\n value=\"${value%%#*}\"\n value=\"${value#\"${value%%[![:space:]]*}\"}\"\n value=\"${value%\"${value##*[![:space:]]}\"}\"\n eval \"_existing_val=\\\"\\${${key}+x}\\\"\"\n if [ -z \"${_existing_val}\" ]; then export \"${key}=${value}\"; fi\n done < \"${existing_env}\"\n fi\n}\n\nstep_llm() {\n log \"$(msg llm.title)\"\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n if [ \"${AGENTTEAMS_LANGUAGE}\" = \"zh\" ]; then\n AGENTTEAMS_LLM_PROVIDER=\"${AGENTTEAMS_LLM_PROVIDER:-openai-compat}\"\n AGENTTEAMS_DEFAULT_MODEL=\"${AGENTTEAMS_DEFAULT_MODEL:-qwen3.6-plus}\"\n AGENTTEAMS_OPENAI_BASE_URL=\"${AGENTTEAMS_OPENAI_BASE_URL:-https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1}\"\n log \"$(msg llm.provider.label \"${AGENTTEAMS_LLM_PROVIDER}\")\"\n log \"$(msg llm.openai.base_url_label \"${AGENTTEAMS_OPENAI_BASE_URL}\")\"\n else\n AGENTTEAMS_LLM_PROVIDER=\"${AGENTTEAMS_LLM_PROVIDER:-qwen}\"\n AGENTTEAMS_DEFAULT_MODEL=\"${AGENTTEAMS_DEFAULT_MODEL:-qwen3.6-plus}\"\n AGENTTEAMS_OPENAI_BASE_URL=\"${AGENTTEAMS_OPENAI_BASE_URL:-}\"\n log \"$(msg llm.provider.qwen_default \"${AGENTTEAMS_LLM_PROVIDER}\")\"\n fi\n log \"$(msg llm.model.default \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n prompt AGENTTEAMS_LLM_API_KEY \"$(msg llm.apikey_prompt)\" \"\" \"true\"\n AGENTTEAMS_EMBEDDING_MODEL=\"${AGENTTEAMS_EMBEDDING_MODEL-text-embedding-v4}\"\n export AGENTTEAMS_LLM_PROVIDER AGENTTEAMS_DEFAULT_MODEL AGENTTEAMS_EMBEDDING_MODEL\n [ -n \"${AGENTTEAMS_OPENAI_BASE_URL+x}\" ] && export AGENTTEAMS_OPENAI_BASE_URL\n return 0\n fi\n\n # Upgrade Keep-All mode: skip provider selection, use loaded values\n local SKIP_TO_EMBEDDING=0\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ] && [ -n \"${AGENTTEAMS_LLM_PROVIDER}\" ]; then\n log \"$(msg llm.provider.label \"${AGENTTEAMS_LLM_PROVIDER}\")\"\n log \"$(msg llm.openai.base_url_label \"${AGENTTEAMS_OPENAI_BASE_URL}\")\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n PROVIDER_CHOICE=\"${AGENTTEAMS_LLM_PROVIDER}\"\n SKIP_TO_EMBEDDING=1\n else\n echo \"\"\n echo \"$(msg llm.providers_title)\"\n echo \"$(msg llm.provider.alibaba)\"\n echo \"$(msg llm.provider.openai_compat)\"\n echo \"\"\n local PROVIDER_CHOICE\n # If upgrade mode with loaded provider, show current as default\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_LLM_PROVIDER}\" ]; then\n local _prov_display\n case \"${AGENTTEAMS_LLM_PROVIDER}\" in\n openai-compat) _prov_display=\"2\" ;;\n *) _prov_display=\"1\" ;;\n esac\n read -e -p \"$(msg llm.provider.select) [${_prov_display}]: \" PROVIDER_CHOICE\n PROVIDER_CHOICE=\"${PROVIDER_CHOICE:-${_prov_display}}\"\n elif [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ]; then\n read -e -p \"$(msg llm.provider.select) [1]: \" PROVIDER_CHOICE\n PROVIDER_CHOICE=\"${PROVIDER_CHOICE:-1}\"\n else\n read -e -p \"$(msg llm.provider.select): \" PROVIDER_CHOICE\n PROVIDER_CHOICE=\"${PROVIDER_CHOICE:-1}\"\n fi\n if [ \"${PROVIDER_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n fi\n local ALIBABA_MODEL_CHOICE=\"\"\n local ALIBABA_ACCESS=\"\"\n case \"${PROVIDER_CHOICE}\" in\n 1|alibaba-cloud|openai-compat)\n if [ \"${AGENTTEAMS_LANGUAGE}\" = \"en\" ]; then\n AGENTTEAMS_LLM_PROVIDER=\"openai-compat\"\n AGENTTEAMS_OPENAI_BASE_URL=\"https://dashscope-intl.aliyuncs.com/compatible-mode/v1\"\n ALIBABA_ACCESS=\"tokenplan\"\n echo \"\"\n echo \"$(msg llm.codingplan.models_title)\"\n echo \"$(msg llm.codingplan.model.qwen36plus)\"\n echo \"$(msg llm.codingplan.model.glm5)\"\n echo \"$(msg llm.codingplan.model.kimi)\"\n echo \"$(msg llm.codingplan.model.minimax)\"\n echo \"\"\n local CODINGPLAN_MODEL_CHOICE\n # If upgrade with loaded model, show current as default\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_DEFAULT_MODEL}\" ]; then\n local _model_default\n case \"${AGENTTEAMS_DEFAULT_MODEL}\" in\n qwen3.6-plus) _model_default=\"1\" ;;\n glm-5) _model_default=\"2\" ;;\n kimi-k2.5) _model_default=\"3\" ;;\n MiniMax-M2.5) _model_default=\"4\" ;;\n *) _model_default=\"1\" ;;\n esac\n read -e -p \"$(msg llm.codingplan.model.select) [${_model_default}]: \" CODINGPLAN_MODEL_CHOICE\n CODINGPLAN_MODEL_CHOICE=\"${CODINGPLAN_MODEL_CHOICE:-${_model_default}}\"\n elif [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ]; then\n read -e -p \"$(msg llm.codingplan.model.select) [1]: \" CODINGPLAN_MODEL_CHOICE\n CODINGPLAN_MODEL_CHOICE=\"${CODINGPLAN_MODEL_CHOICE:-1}\"\n else\n read -e -p \"$(msg llm.codingplan.model.select): \" CODINGPLAN_MODEL_CHOICE\n CODINGPLAN_MODEL_CHOICE=\"${CODINGPLAN_MODEL_CHOICE:-1}\"\n fi\n if [ \"${CODINGPLAN_MODEL_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n case \"${CODINGPLAN_MODEL_CHOICE}\" in\n 1|qwen3.6-plus) AGENTTEAMS_DEFAULT_MODEL=\"qwen3.6-plus\" ;;\n 2|glm-5) AGENTTEAMS_DEFAULT_MODEL=\"glm-5\" ;;\n 3|kimi-k2.5) AGENTTEAMS_DEFAULT_MODEL=\"kimi-k2.5\" ;;\n 4|MiniMax-M2.5) AGENTTEAMS_DEFAULT_MODEL=\"MiniMax-M2.5\" ;;\n *) AGENTTEAMS_DEFAULT_MODEL=\"qwen3.6-plus\" ;;\n esac\n log \"$(msg llm.provider.selected_codingplan)\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n else\n echo \"\"\n echo \"$(msg llm.alibaba.models_title)\"\n echo \"$(msg llm.alibaba.model.tokenplan)\"\n echo \"$(msg llm.alibaba.model.bailian)\"\n echo \"$(msg llm.alibaba.model.codingplan_legacy)\"\n echo \"\"\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_OPENAI_BASE_URL}\" ]; then\n # Determine alibaba access type from loaded URL\n local _access_default=\"1\"\n if echo \"${AGENTTEAMS_OPENAI_BASE_URL}\" | grep -q \"token-plan\"; then\n _access_default=\"1\"\n elif echo \"${AGENTTEAMS_OPENAI_BASE_URL}\" | grep -q \"dashscope\"; then\n if echo \"${AGENTTEAMS_OPENAI_BASE_URL}\" | grep -q \"intl\"; then\n _access_default=\"3\"\n else\n _access_default=\"2\"\n fi\n fi\n read -e -p \"$(msg llm.alibaba.model.select) [${_access_default}]: \" ALIBABA_MODEL_CHOICE\n ALIBABA_MODEL_CHOICE=\"${ALIBABA_MODEL_CHOICE:-${_access_default}}\"\n elif [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ]; then\n read -e -p \"$(msg llm.alibaba.model.select) [1]: \" ALIBABA_MODEL_CHOICE\n ALIBABA_MODEL_CHOICE=\"${ALIBABA_MODEL_CHOICE:-1}\"\n else\n read -e -p \"$(msg llm.alibaba.model.select): \" ALIBABA_MODEL_CHOICE\n ALIBABA_MODEL_CHOICE=\"${ALIBABA_MODEL_CHOICE:-1}\"\n fi\n if [ \"${ALIBABA_MODEL_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n case \"${ALIBABA_MODEL_CHOICE}\" in\n 1|token-plan|tokenplan)\n ALIBABA_ACCESS=\"tokenplan\"\n AGENTTEAMS_LLM_PROVIDER=\"openai-compat\"\n AGENTTEAMS_OPENAI_BASE_URL=\"https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1\"\n echo \"\"\n echo \"$(msg llm.codingplan.models_title)\"\n echo \"$(msg llm.codingplan.model.qwen36plus)\"\n echo \"$(msg llm.codingplan.model.glm5)\"\n echo \"$(msg llm.codingplan.model.kimi)\"\n echo \"$(msg llm.codingplan.model.minimax)\"\n echo \"\"\n local CODINGPLAN_MODEL_CHOICE\n # If upgrade with loaded model, show current as default\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_DEFAULT_MODEL}\" ]; then\n local _model_default\n case \"${AGENTTEAMS_DEFAULT_MODEL}\" in\n qwen3.6-plus) _model_default=\"1\" ;;\n glm-5) _model_default=\"2\" ;;\n kimi-k2.5) _model_default=\"3\" ;;\n MiniMax-M2.5) _model_default=\"4\" ;;\n *) _model_default=\"1\" ;;\n esac\n read -e -p \"$(msg llm.codingplan.model.select) [${_model_default}]: \" CODINGPLAN_MODEL_CHOICE\n CODINGPLAN_MODEL_CHOICE=\"${CODINGPLAN_MODEL_CHOICE:-${_model_default}}\"\n elif [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ]; then\n read -e -p \"$(msg llm.codingplan.model.select) [1]: \" CODINGPLAN_MODEL_CHOICE\n CODINGPLAN_MODEL_CHOICE=\"${CODINGPLAN_MODEL_CHOICE:-1}\"\n else\n read -e -p \"$(msg llm.codingplan.model.select): \" CODINGPLAN_MODEL_CHOICE\n CODINGPLAN_MODEL_CHOICE=\"${CODINGPLAN_MODEL_CHOICE:-1}\"\n fi\n if [ \"${CODINGPLAN_MODEL_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n case \"${CODINGPLAN_MODEL_CHOICE}\" in\n 1|qwen3.6-plus) AGENTTEAMS_DEFAULT_MODEL=\"qwen3.6-plus\" ;;\n 2|glm-5) AGENTTEAMS_DEFAULT_MODEL=\"glm-5\" ;;\n 3|kimi-k2.5) AGENTTEAMS_DEFAULT_MODEL=\"kimi-k2.5\" ;;\n 4|MiniMax-M2.5) AGENTTEAMS_DEFAULT_MODEL=\"MiniMax-M2.5\" ;;\n *) AGENTTEAMS_DEFAULT_MODEL=\"qwen3.6-plus\" ;;\n esac\n log \"$(msg llm.provider.selected_tokenplan)\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n ;;\n 2|qwen|bailian)\n ALIBABA_ACCESS=\"bailian\"\n AGENTTEAMS_LLM_PROVIDER=\"qwen\"\n AGENTTEAMS_OPENAI_BASE_URL=\"\"\n echo \"\"\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_DEFAULT_MODEL}\" ]; then\n read -e -p \"$(msg llm.qwen.model_prompt) [${AGENTTEAMS_DEFAULT_MODEL}]: \" AGENTTEAMS_DEFAULT_MODEL\n else\n read -e -p \"$(msg llm.qwen.model_prompt) [qwen3.6-plus]: \" AGENTTEAMS_DEFAULT_MODEL\n fi\n if [ \"${AGENTTEAMS_DEFAULT_MODEL}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_DEFAULT_MODEL=\"${AGENTTEAMS_DEFAULT_MODEL:-qwen3.6-plus}\"\n log \"$(msg llm.provider.selected_qwen)\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n prompt_custom_model_params \"${AGENTTEAMS_DEFAULT_MODEL}\" || return 0\n ;;\n 3|coding-plan|codingplan)\n ALIBABA_ACCESS=\"codingplan_legacy\"\n AGENTTEAMS_LLM_PROVIDER=\"openai-compat\"\n AGENTTEAMS_OPENAI_BASE_URL=\"https://coding.dashscope.aliyuncs.com/v1\"\n echo \"\"\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_DEFAULT_MODEL}\" ]; then\n read -e -p \"$(msg llm.qwen.model_prompt) [${AGENTTEAMS_DEFAULT_MODEL}]: \" AGENTTEAMS_DEFAULT_MODEL\n else\n read -e -p \"$(msg llm.qwen.model_prompt): \" AGENTTEAMS_DEFAULT_MODEL\n fi\n if [ \"${AGENTTEAMS_DEFAULT_MODEL}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_DEFAULT_MODEL=\"${AGENTTEAMS_DEFAULT_MODEL:-qwen3.6-plus}\"\n log \"$(msg llm.provider.selected_codingplan_legacy)\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n prompt_custom_model_params \"${AGENTTEAMS_DEFAULT_MODEL}\" || return 0\n ;;\n *)\n die \"$(msg llm.alibaba.model.invalid \"${ALIBABA_MODEL_CHOICE}\")\"\n ;;\n esac\n fi\n if [ \"${ALIBABA_ACCESS}\" = \"bailian\" ]; then\n log \"$(msg llm.apikey_hint_bailian)\"\n log \"$(msg llm.apikey_url_bailian)\"\n elif [ \"${AGENTTEAMS_LANGUAGE}\" = \"en\" ]; then\n log \"$(msg llm.apikey_hint_qwencloud)\"\n log \"$(msg llm.apikey_url_qwencloud)\"\n elif [ \"${ALIBABA_ACCESS}\" = \"codingplan_legacy\" ]; then\n log \"$(msg llm.apikey_hint_codingplan)\"\n log \"$(msg llm.apikey_url_codingplan)\"\n else\n log \"$(msg llm.apikey_hint_tokenplan)\"\n log \"$(msg llm.apikey_url_tokenplan)\"\n fi\n log \"\"\n prompt AGENTTEAMS_LLM_API_KEY \"$(msg llm.apikey_prompt)\" \"\" \"true\" || return 0\n if [ \"${ALIBABA_ACCESS}\" = \"bailian\" ]; then\n test_llm_connectivity \"https://dashscope.aliyuncs.com/compatible-mode/v1\" \"${AGENTTEAMS_LLM_API_KEY}\" \"${AGENTTEAMS_DEFAULT_MODEL}\" || return 0\n elif [ \"${ALIBABA_ACCESS}\" = \"codingplan_legacy\" ]; then\n test_llm_connectivity \"https://coding.dashscope.aliyuncs.com/v1\" \"${AGENTTEAMS_LLM_API_KEY}\" \"${AGENTTEAMS_DEFAULT_MODEL}\" \"$(msg llm.openai.test.fail.codingplan_legacy)\" || return 0\n elif [ \"${AGENTTEAMS_LANGUAGE}\" = \"en\" ]; then\n test_llm_connectivity \"${AGENTTEAMS_OPENAI_BASE_URL}\" \"${AGENTTEAMS_LLM_API_KEY}\" \"${AGENTTEAMS_DEFAULT_MODEL}\" \"$(msg llm.openai.test.fail.codingplan)\" || return 0\n else\n test_llm_connectivity \"${AGENTTEAMS_OPENAI_BASE_URL}\" \"${AGENTTEAMS_LLM_API_KEY}\" \"${AGENTTEAMS_DEFAULT_MODEL}\" \"$(msg llm.openai.test.fail.tokenplan)\" || return 0\n fi\n ;;\n 2|openai-compat)\n AGENTTEAMS_LLM_PROVIDER=\"openai-compat\"\n log \"$(msg llm.provider.selected_openai \"${AGENTTEAMS_LLM_PROVIDER}\")\"\n echo \"\"\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg llm.openai.base_url_prompt)\" \"${AGENTTEAMS_OPENAI_BASE_URL}\")\"\n else\n _current=\"${AGENTTEAMS_OPENAI_BASE_URL}\"\n if [ -n \"${_current}\" ]; then\n read -e -p \"$(msg llm.openai.base_url_prompt) [${_current}]: \" AGENTTEAMS_OPENAI_BASE_URL\n else\n read -e -p \"$(msg llm.openai.base_url_prompt) [https://api.openai.com/v1]: \" AGENTTEAMS_OPENAI_BASE_URL\n fi\n if [ \"${AGENTTEAMS_OPENAI_BASE_URL}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_OPENAI_BASE_URL=\"${AGENTTEAMS_OPENAI_BASE_URL:-${_current}}\"\n [ -z \"${AGENTTEAMS_OPENAI_BASE_URL}\" ] && AGENTTEAMS_OPENAI_BASE_URL=\"https://api.openai.com/v1\"\n fi\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg llm.openai.model_prompt)\" \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n else\n _current=\"${AGENTTEAMS_DEFAULT_MODEL}\"\n if [ -n \"${_current}\" ]; then\n read -e -p \"$(msg llm.openai.model_prompt) [${_current}]: \" AGENTTEAMS_DEFAULT_MODEL\n else\n read -e -p \"$(msg llm.openai.model_prompt) [gpt-5.4]: \" AGENTTEAMS_DEFAULT_MODEL\n fi\n if [ \"${AGENTTEAMS_DEFAULT_MODEL}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_DEFAULT_MODEL=\"${AGENTTEAMS_DEFAULT_MODEL:-${_current}}\"\n [ -z \"${AGENTTEAMS_DEFAULT_MODEL}\" ] && AGENTTEAMS_DEFAULT_MODEL=\"gpt-5.4\"\n fi\n log \"$(msg llm.openai.base_url_label \"${AGENTTEAMS_OPENAI_BASE_URL}\")\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n prompt_custom_model_params \"${AGENTTEAMS_DEFAULT_MODEL}\" || return 0\n log \"\"\n prompt AGENTTEAMS_LLM_API_KEY \"$(msg llm.apikey_prompt)\" \"\" \"true\" || return 0\n test_llm_connectivity \"${AGENTTEAMS_OPENAI_BASE_URL}\" \"${AGENTTEAMS_LLM_API_KEY}\" \"${AGENTTEAMS_DEFAULT_MODEL}\" || return 0\n ;;\n *)\n die \"$(msg llm.provider.invalid \"${PROVIDER_CHOICE}\")\"\n ;;\n esac\n\n # Skip to embedding if Keep-All mode handled LLM params\n if [ \"${SKIP_TO_EMBEDDING}\" = \"1\" ]; then\n if [ -n \"${AGENTTEAMS_EMBEDDING_MODEL}\" ]; then\n log \"$(msg llm.embedding.title)\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_EMBEDDING_MODEL}\")\"\n log \"\"\n fi\n return 0\n fi\n\n # --- Embedding model (optional, auto-tested) ---\n echo \"\"\n log \"$(msg llm.embedding.title)\"\n log \"$(msg llm.embedding.hint)\"\n echo \"\"\n echo \"$(msg llm.embedding.option.default)\"\n echo \"$(msg llm.embedding.option.custom)\"\n echo \"$(msg llm.embedding.option.disable)\"\n echo \"\"\n local EMB_CHOICE\n read -e -p \"$(msg llm.embedding.select) [1]: \" EMB_CHOICE\n EMB_CHOICE=\"${EMB_CHOICE:-1}\"\n if [ \"${EMB_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n\n case \"${EMB_CHOICE}\" in\n 1)\n AGENTTEAMS_EMBEDDING_MODEL=\"text-embedding-v4\"\n ;;\n 2)\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg llm.embedding.custom_prompt)\" \"${AGENTTEAMS_EMBEDDING_MODEL}\")\"\n else\n _current=\"${AGENTTEAMS_EMBEDDING_MODEL}\"\n if [ -n \"${_current}\" ]; then\n read -e -p \"$(msg llm.embedding.custom_prompt) [${_current}]: \" AGENTTEAMS_EMBEDDING_MODEL\n else\n read -e -p \"$(msg llm.embedding.custom_prompt): \" AGENTTEAMS_EMBEDDING_MODEL\n fi\n if [ \"${AGENTTEAMS_EMBEDDING_MODEL}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_EMBEDDING_MODEL=\"${AGENTTEAMS_EMBEDDING_MODEL:-${_current}}\"\n fi\n ;;\n 3)\n AGENTTEAMS_EMBEDDING_MODEL=\"\"\n log \"$(msg llm.embedding.disabled)\"\n ;;\n *)\n AGENTTEAMS_EMBEDDING_MODEL=\"text-embedding-v4\"\n ;;\n esac\n\n if [ -n \"${AGENTTEAMS_EMBEDDING_MODEL}\" ]; then\n # Qwen provider uses dashscope directly; others use OPENAI_BASE_URL\n local EMB_BASE_URL=\"${AGENTTEAMS_OPENAI_BASE_URL}\"\n if [ \"${AGENTTEAMS_LLM_PROVIDER}\" = \"qwen\" ]; then\n EMB_BASE_URL=\"https://dashscope.aliyuncs.com/compatible-mode/v1\"\n fi\n if ! test_embedding_connectivity \"${EMB_BASE_URL}\" \"${AGENTTEAMS_LLM_API_KEY}\" \"${AGENTTEAMS_EMBEDDING_MODEL}\"; then\n AGENTTEAMS_EMBEDDING_MODEL=\"\"\n log \"$(msg llm.embedding.auto_disabled)\"\n fi\n fi\n\n export AGENTTEAMS_LLM_PROVIDER AGENTTEAMS_DEFAULT_MODEL\n [ -n \"${AGENTTEAMS_OPENAI_BASE_URL+x}\" ] && export AGENTTEAMS_OPENAI_BASE_URL\n log \"\"\n}\n\nstep_admin() {\n log \"$(msg admin.title)\"\n prompt AGENTTEAMS_ADMIN_USER \"$(msg admin.username_prompt)\" \"admin\" || return 0\n AGENTTEAMS_ADMIN_USER=\"$(printf '%s' \"${AGENTTEAMS_ADMIN_USER}\" | tr '[:upper:]' '[:lower:]')\"\n\n # Pre-set via env var: validate; in non-interactive mode fail fast,\n # in interactive mode warn and fall through to the retry prompt.\n if [ -n \"${AGENTTEAMS_ADMIN_PASSWORD:-}\" ]; then\n log \" $(msg prompt.preset \"$(msg admin.password_prompt)\")\"\n if [ ${#AGENTTEAMS_ADMIN_PASSWORD} -ge 8 ]; then\n log \"\"\n return 0\n fi\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n die \"$(msg admin.password_too_short \"${#AGENTTEAMS_ADMIN_PASSWORD}\")\"\n fi\n error \"$(msg admin.password_too_short \"${#AGENTTEAMS_ADMIN_PASSWORD}\")\"\n unset AGENTTEAMS_ADMIN_PASSWORD\n fi\n\n while true; do\n unset AGENTTEAMS_ADMIN_PASSWORD\n prompt_optional AGENTTEAMS_ADMIN_PASSWORD \"$(msg admin.password_prompt)\" \"true\" || return 0\n if [ -z \"${AGENTTEAMS_ADMIN_PASSWORD}\" ]; then\n AGENTTEAMS_ADMIN_PASSWORD=\"admin$(openssl rand -hex 6)\"\n log \"$(msg admin.password_generated)\"\n break\n fi\n if [ ${#AGENTTEAMS_ADMIN_PASSWORD} -ge 8 ]; then\n break\n fi\n error \"$(msg admin.password_too_short \"${#AGENTTEAMS_ADMIN_PASSWORD}\")\"\n done\n log \"\"\n}\n\nstep_network() {\n log \"$(msg port.local_only.title)\"\n echo \"\"\n echo \" 1) $(msg port.local_only.hint_yes)\"\n echo \" 2) $(msg port.local_only.hint_no)\"\n echo \"\"\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_LOCAL_ONLY=\"${AGENTTEAMS_LOCAL_ONLY:-1}\"\n elif [ -z \"${AGENTTEAMS_LOCAL_ONLY+x}\" ]; then\n local _local_choice\n read -e -p \"$(msg port.local_only.choice): \" _local_choice\n if [ \"${_local_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _local_choice=\"${_local_choice:-1}\"\n case \"${_local_choice}\" in\n 2|n|N|no|NO) AGENTTEAMS_LOCAL_ONLY=\"0\" ;;\n *) AGENTTEAMS_LOCAL_ONLY=\"1\" ;;\n esac\n fi\n export AGENTTEAMS_LOCAL_ONLY\n if [ \"${AGENTTEAMS_LOCAL_ONLY}\" = \"1\" ]; then\n log \"$(msg port.local_only.selected_local)\"\n else\n log \"$(msg port.local_only.selected_external)\"\n echo \"\"\n echo -e \"\\033[33m$(msg port.local_only.https_hint)\\033[0m\"\n fi\n}\n\nstep_ports() {\n log \"$(msg port.title)\"\n prompt AGENTTEAMS_PORT_GATEWAY \"$(msg port.gateway_prompt)\" \"18080\" || return 0\n prompt AGENTTEAMS_PORT_CONSOLE \"$(msg port.console_prompt)\" \"18001\" || return 0\n prompt AGENTTEAMS_PORT_ELEMENT_WEB \"$(msg port.element_prompt)\" \"18088\" || return 0\n prompt AGENTTEAMS_PORT_MANAGER_CONSOLE \"$(msg port.manager_console_prompt)\" \"18888\" || return 0\n log \"\"\n}\n\nstep_domains() {\n log \"$(msg domain.title)\"\n log \"$(msg domain.hint)\"\n prompt AGENTTEAMS_MATRIX_DOMAIN \"$(msg domain.matrix_prompt)\" \"matrix-local.agentteams.io:${AGENTTEAMS_PORT_GATEWAY}\" || return 0\n prompt AGENTTEAMS_MATRIX_CLIENT_DOMAIN \"$(msg domain.element_prompt)\" \"matrix-client-local.agentteams.io\" || return 0\n prompt AGENTTEAMS_AI_GATEWAY_DOMAIN \"$(msg domain.gateway_prompt)\" \"aigw-local.agentteams.io\" || return 0\n prompt AGENTTEAMS_FS_DOMAIN \"$(msg domain.fs_prompt)\" \"fs-local.agentteams.io\" || return 0\n if [ \"${AGENTTEAMS_MANAGER_RUNTIME}\" != \"copaw\" ]; then\n prompt AGENTTEAMS_CONSOLE_DOMAIN \"$(msg domain.console_prompt)\" \"console-local.agentteams.io\" || return 0\n fi\n log \"\"\n}\n\nstep_github() {\n log \"$(msg github.title)\"\n prompt_optional AGENTTEAMS_GITHUB_TOKEN \"$(msg github.token_prompt)\" \"true\" || return 0\n}\n\nstep_skills() {\n log \"\"\n log \"$(msg skills.title)\"\n prompt_optional AGENTTEAMS_SKILLS_API_URL \"$(msg skills.url_prompt)\" || return 0\n log \"\"\n}\n\nstep_volume() {\n log \"$(msg data.title)\"\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_DATA_DIR=\"${AGENTTEAMS_DATA_DIR:-agentteams-data}\"\n log \" $(msg data.volume_using \"${AGENTTEAMS_DATA_DIR}\") (non-interactive, skipped)\"\n export AGENTTEAMS_DATA_DIR\n return 0\n fi\n # ─────────────────────────────────────────────────────────────────\n if [ -z \"${AGENTTEAMS_DATA_DIR+x}\" ]; then\n local _input\n read -e -p \"$(msg data.volume_prompt): \" _input\n if [ \"${_input}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_DATA_DIR=\"${_input:-agentteams-data}\"\n export AGENTTEAMS_DATA_DIR\n fi\n AGENTTEAMS_DATA_DIR=\"${AGENTTEAMS_DATA_DIR:-agentteams-data}\"\n log \"$(msg data.volume_using \"${AGENTTEAMS_DATA_DIR}\")\"\n}\n\nstep_workspace() {\n log \"$(msg workspace.title)\"\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_WORKSPACE_DIR=\"${AGENTTEAMS_WORKSPACE_DIR:-${HOME}/agentteams-manager}\"\n AGENTTEAMS_WORKSPACE_DIR=\"$(cd \"${AGENTTEAMS_WORKSPACE_DIR}\" 2>/dev/null && pwd || echo \"${AGENTTEAMS_WORKSPACE_DIR}\")\"\n mkdir -p \"${AGENTTEAMS_WORKSPACE_DIR}\"\n log \" $(msg workspace.dir_label \"${AGENTTEAMS_WORKSPACE_DIR}\") (non-interactive, skipped)\"\n export AGENTTEAMS_WORKSPACE_DIR\n return 0\n fi\n # ─────────────────────────────────────────────────────────────────\n local _input\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg workspace.dir_prompt \"${HOME}/agentteams-manager\")\" \"${AGENTTEAMS_WORKSPACE_DIR}\")\"\n _input=\"${AGENTTEAMS_WORKSPACE_DIR}\"\n else\n local _current=\"${AGENTTEAMS_WORKSPACE_DIR}\"\n if [ -n \"${_current}\" ]; then\n read -e -p \"$(msg workspace.dir_prompt \"${HOME}/agentteams-manager\") [${_current}]: \" _input\n else\n read -e -p \"$(msg workspace.dir_prompt \"${HOME}/agentteams-manager\"): \" _input\n fi\n if [ \"${_input}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _input=\"${_input:-${_current}}\"\n fi\n AGENTTEAMS_WORKSPACE_DIR=\"${_input:-${HOME}/agentteams-manager}\"\n export AGENTTEAMS_WORKSPACE_DIR\n AGENTTEAMS_WORKSPACE_DIR=\"$(cd \"${AGENTTEAMS_WORKSPACE_DIR}\" 2>/dev/null && pwd || echo \"${AGENTTEAMS_WORKSPACE_DIR}\")\"\n mkdir -p \"${AGENTTEAMS_WORKSPACE_DIR}\"\n log \"$(msg workspace.dir_label \"${AGENTTEAMS_WORKSPACE_DIR}\")\"\n}\n\nstep_dashboard() {\n AGENTTEAMS_DASHBOARD=\"${AGENTTEAMS_DASHBOARD:-1}\"\n AGENTTEAMS_DASHBOARD_VERSION=\"${AGENTTEAMS_DASHBOARD_VERSION:-v1.2.0}\"\n AGENTTEAMS_PORT_DASHBOARD=\"${AGENTTEAMS_PORT_DASHBOARD:-13000}\"\n AGENTTEAMS_AI_GATEWAY_ADMIN_URL=\"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL:-}\"\n\n # Compute the default image for the current version.\n # We track whether the current image matches the derived default so that\n # changing the version also updates the image (unless the user has\n # explicitly overridden it to a non-default value).\n _dashboard_default_image() {\n echo \"${AGENTTEAMS_REGISTRY}/agentteams/agentteams-dashboard:${1:-${AGENTTEAMS_DASHBOARD_VERSION}}\"\n }\n local _init_default\n _init_default=$(_dashboard_default_image)\n if [ -z \"${AGENTTEAMS_DASHBOARD_IMAGE:-}\" ]; then\n AGENTTEAMS_DASHBOARD_IMAGE=\"${_init_default}\"\n fi\n\n # Recompute default image when the version changes AND the current image\n # matches the default for the *previous* (env-file) version.\n # This handles non-interactive upgrades correctly:\n # - env file loads old default image + old version\n # - CLI passes new version\n # - image should follow (because it was auto-derived before)\n # But preserves truly custom images (same repo but custom tag like :canary)\n # because they won't match the old-version default exactly.\n local _old_saved_version=\"\"\n local _env_file=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n if [ -f \"${_env_file}\" ]; then\n _old_saved_version=\"$(grep '^AGENTTEAMS_DASHBOARD_VERSION=' \"${_env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n fi\n if [ -n \"${_old_saved_version}\" ] && \\\n [ \"${_old_saved_version}\" != \"${AGENTTEAMS_DASHBOARD_VERSION}\" ] && \\\n [ \"${AGENTTEAMS_DASHBOARD_IMAGE}\" = \"$(_dashboard_default_image \"${_old_saved_version}\")\" ]; then\n AGENTTEAMS_DASHBOARD_IMAGE=$(_dashboard_default_image)\n fi\n\n # Quick Start: accept all defaults without prompting. The dashboard is\n # installed by default (AGENTTEAMS_DASHBOARD=1); set AGENTTEAMS_DASHBOARD=0\n # to opt out.\n if [ \"${AGENTTEAMS_QUICKSTART:-0}\" = \"1\" ]; then\n if [ \"${AGENTTEAMS_DASHBOARD}\" = \"1\" ]; then\n log \"$(msg dash.summary \"${AGENTTEAMS_PORT_DASHBOARD}\" \"${AGENTTEAMS_DASHBOARD_IMAGE}\") (quick start)\"\n else\n log \"$(msg dash.skip) (quick start)\"\n fi\n export AGENTTEAMS_DASHBOARD AGENTTEAMS_DASHBOARD_VERSION AGENTTEAMS_PORT_DASHBOARD AGENTTEAMS_DASHBOARD_IMAGE AGENTTEAMS_AI_GATEWAY_ADMIN_URL\n return 0\n fi\n\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_DASHBOARD=\"${AGENTTEAMS_DASHBOARD:-1}\"\n if [ \"${AGENTTEAMS_DASHBOARD}\" = \"1\" ]; then\n log \"$(msg dash.summary \"${AGENTTEAMS_PORT_DASHBOARD}\" \"${AGENTTEAMS_DASHBOARD_IMAGE}\") (non-interactive)\"\n else\n log \"$(msg dash.skip) (non-interactive)\"\n fi\n export AGENTTEAMS_DASHBOARD AGENTTEAMS_DASHBOARD_VERSION AGENTTEAMS_PORT_DASHBOARD AGENTTEAMS_DASHBOARD_IMAGE AGENTTEAMS_AI_GATEWAY_ADMIN_URL\n return 0\n fi\n\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ]; then\n log \"$(msg prompt.upgrade_keep \"\" \"${AGENTTEAMS_DASHBOARD}\") dashboard=${AGENTTEAMS_DASHBOARD} version=${AGENTTEAMS_DASHBOARD_VERSION} port=${AGENTTEAMS_PORT_DASHBOARD}\"\n export AGENTTEAMS_DASHBOARD AGENTTEAMS_DASHBOARD_VERSION AGENTTEAMS_PORT_DASHBOARD AGENTTEAMS_DASHBOARD_IMAGE AGENTTEAMS_AI_GATEWAY_ADMIN_URL\n return 0\n fi\n\n local _input\n echo \"\"\n read -p \"$(msg dash.prompt) [Y/n]: \" _input\n case \"${_input:-}\" in\n [nN0]) AGENTTEAMS_DASHBOARD=0 ;;\n *) AGENTTEAMS_DASHBOARD=1 ;;\n esac\n if [ \"${_input}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n\n if [ \"${AGENTTEAMS_DASHBOARD}\" != \"1\" ]; then\n log \"$(msg dash.skip)\"\n export AGENTTEAMS_DASHBOARD AGENTTEAMS_DASHBOARD_VERSION AGENTTEAMS_PORT_DASHBOARD AGENTTEAMS_DASHBOARD_IMAGE AGENTTEAMS_AI_GATEWAY_ADMIN_URL\n return 0\n fi\n\n # Dashboard version\n local _current_version=\"${AGENTTEAMS_DASHBOARD_VERSION}\"\n local _old_version=\"${AGENTTEAMS_DASHBOARD_VERSION}\"\n local _old_default_image\n _old_default_image=$(_dashboard_default_image \"${_old_version}\")\n read -p \"$(msg dash.version_prompt) [${_current_version}]: \" _input\n AGENTTEAMS_DASHBOARD_VERSION=\"${_input:-${_current_version}}\"\n\n # Dashboard port\n local _current_port=\"${AGENTTEAMS_PORT_DASHBOARD}\"\n read -p \"$(msg dash.port_prompt) [${_current_port}]: \" _input\n AGENTTEAMS_PORT_DASHBOARD=\"${_input:-${_current_port}}\"\n\n # Dashboard image: if the current image matches the default derived from\n # the OLD version, recompute it from the NEW version. This handles both\n # fresh installs and upgrades where the previous image was the default.\n # Only a real user override (image != default for old version) is preserved.\n if [ \"${AGENTTEAMS_DASHBOARD_VERSION}\" != \"${_old_version}\" ] && \\\n [ \"${AGENTTEAMS_DASHBOARD_IMAGE}\" = \"${_old_default_image}\" ]; then\n AGENTTEAMS_DASHBOARD_IMAGE=$(_dashboard_default_image)\n fi\n local _current_image=\"${AGENTTEAMS_DASHBOARD_IMAGE}\"\n read -p \"$(msg dash.image_prompt) [${_current_image}]: \" _input\n if [ -n \"${_input}\" ]; then\n AGENTTEAMS_DASHBOARD_IMAGE=\"${_input}\"\n fi\n\n # Higress Console URL (explicit config takes priority; auto-detect as fallback)\n local _higress_url=\"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\"\n if [ -z \"${_higress_url}\" ]; then\n if ${DOCKER_CMD} ps --format '{{.Names}}' | grep -q \"^agentteams-controller$\"; then\n if ${DOCKER_CMD} exec agentteams-controller curl -sf --max-time 2 http://127.0.0.1:8001/ >/dev/null 2>&1; then\n _higress_url=\"http://agentteams-controller:8001\"\n log \"$(msg dash.auto_url): ${_higress_url}\"\n else\n log \"$(msg dash.auto_url_fail)\"\n fi\n fi\n fi\n read -p \"$(msg dash.gateway_prompt) [${_higress_url:-<auto>}]: \" _input\n AGENTTEAMS_AI_GATEWAY_ADMIN_URL=\"${_input:-${_higress_url}}\"\n\n # Normalize URL: add http:// prefix if missing\n if [ -n \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\" ]; then\n case \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\" in\n http://*|https://*) ;;\n *) AGENTTEAMS_AI_GATEWAY_ADMIN_URL=\"http://${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\" ;;\n esac\n fi\n\n # Verify the Higress Console URL is reachable (best-effort warning only)\n if [ -n \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\" ]; then\n local _gw_reachable=0\n # If the URL points to the controller container, test from inside the container\n if echo \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\" | grep -qE \"(agentteams-controller|hiclaw-controller|127\\.0\\.0\\.1|localhost)\"; then\n if ${DOCKER_CMD} ps --format '{{.Names}}' | grep -q \"^agentteams-controller$\"; then\n if ${DOCKER_CMD} exec agentteams-controller curl -sf --max-time 3 \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}/\" >/dev/null 2>&1; then\n _gw_reachable=1\n fi\n fi\n else\n # External URL — probe from inside the controller (same network as\n # the Dashboard container) when possible; fall back to the host.\n if ${DOCKER_CMD} ps --format '{{.Names}}' | grep -q \"^agentteams-controller$\"; then\n if ${DOCKER_CMD} exec agentteams-controller curl -sf --max-time 3 \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}/\" >/dev/null 2>&1; then\n _gw_reachable=1\n fi\n elif curl -sf --max-time 3 \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}/\" >/dev/null 2>&1; then\n _gw_reachable=1\n fi\n fi\n if [ \"${_gw_reachable}\" = \"0\" ]; then\n log \"WARNING: Higress Console URL may not be reachable: ${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\"\n log \" Dashboard will still work, but shared login via Higress Console may fail.\"\n log \" To fix: verify the URL is correct and the Higress Console service is running.\"\n fi\n fi\n\n log \"$(msg dash.summary \"${AGENTTEAMS_PORT_DASHBOARD}\" \"${AGENTTEAMS_DASHBOARD_IMAGE}\")\"\n export AGENTTEAMS_DASHBOARD AGENTTEAMS_DASHBOARD_VERSION AGENTTEAMS_PORT_DASHBOARD AGENTTEAMS_DASHBOARD_IMAGE AGENTTEAMS_AI_GATEWAY_ADMIN_URL\n}\n\nstep_runtime() {\n log \"$(msg worker_runtime.title)\"\n echo \"\"\n echo \" 1) $(msg worker_runtime.copaw)\"\n echo \" 2) $(msg worker_runtime.openclaw)\"\n if ! _ver_lt \"${AGENTTEAMS_VERSION}\" \"v1.1.0\"; then\n echo \" 3) $(msg worker_runtime.hermes)\"\n fi\n echo \"\"\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"${AGENTTEAMS_DEFAULT_WORKER_RUNTIME:-copaw}\"\n elif [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_DEFAULT_WORKER_RUNTIME}\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg worker_runtime.title_short)\" \"${AGENTTEAMS_DEFAULT_WORKER_RUNTIME}\")\"\n local _runtime_choice\n read -e -p \"$(msg worker_runtime.choice): \" _runtime_choice\n if [ \"${_runtime_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n if [ -n \"${_runtime_choice}\" ]; then\n case \"${_runtime_choice}\" in\n 2) AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"openclaw\" ;;\n 3) if ! _ver_lt \"${AGENTTEAMS_VERSION}\" \"v1.1.0\"; then\n AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"hermes\"\n else\n AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"copaw\"\n fi ;;\n *) AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"copaw\" ;;\n esac\n fi\n elif [ -z \"${AGENTTEAMS_DEFAULT_WORKER_RUNTIME+x}\" ]; then\n local _runtime_choice\n read -e -p \"$(msg worker_runtime.choice): \" _runtime_choice\n if [ \"${_runtime_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _runtime_choice=\"${_runtime_choice:-1}\"\n case \"${_runtime_choice}\" in\n 2) AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"openclaw\" ;;\n 3) if ! _ver_lt \"${AGENTTEAMS_VERSION}\" \"v1.1.0\"; then\n AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"hermes\"\n else\n AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"copaw\"\n fi ;;\n *) AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"copaw\" ;;\n esac\n fi\n export AGENTTEAMS_DEFAULT_WORKER_RUNTIME\n log \"$(msg worker_runtime.selected \"${AGENTTEAMS_DEFAULT_WORKER_RUNTIME}\")\"\n}\n\nstep_manager_runtime() {\n log \"$(msg manager_runtime.title)\"\n echo \"\"\n echo \" 1) $(msg manager_runtime.copaw)\"\n echo \" 2) $(msg manager_runtime.openclaw)\"\n echo \"\"\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_MANAGER_RUNTIME=\"${AGENTTEAMS_MANAGER_RUNTIME:-copaw}\"\n elif [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_MANAGER_RUNTIME}\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg manager_runtime.title_short)\" \"${AGENTTEAMS_MANAGER_RUNTIME}\")\"\n local _runtime_choice\n read -e -p \"$(msg manager_runtime.choice): \" _runtime_choice\n if [ \"${_runtime_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n if [ -n \"${_runtime_choice}\" ]; then\n case \"${_runtime_choice}\" in\n 2) AGENTTEAMS_MANAGER_RUNTIME=\"openclaw\" ;;\n *) AGENTTEAMS_MANAGER_RUNTIME=\"copaw\" ;;\n esac\n fi\n elif [ -z \"${AGENTTEAMS_MANAGER_RUNTIME+x}\" ]; then\n local _runtime_choice\n read -e -p \"$(msg manager_runtime.choice): \" _runtime_choice\n if [ \"${_runtime_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _runtime_choice=\"${_runtime_choice:-1}\"\n case \"${_runtime_choice}\" in\n 2) AGENTTEAMS_MANAGER_RUNTIME=\"openclaw\" ;;\n *) AGENTTEAMS_MANAGER_RUNTIME=\"copaw\" ;;\n esac\n fi\n export AGENTTEAMS_MANAGER_RUNTIME\n log \"$(msg manager_runtime.selected \"${AGENTTEAMS_MANAGER_RUNTIME}\")\"\n}\n\nstep_e2ee() {\n log \"\"\n log \"$(msg matrix_e2ee.title)\"\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_MATRIX_E2EE=\"${AGENTTEAMS_MATRIX_E2EE:-0}\"\n log \" $(msg matrix_e2ee.title_short) = ${AGENTTEAMS_MATRIX_E2EE} (non-interactive, skipped)\"\n export AGENTTEAMS_MATRIX_E2EE\n return 0\n fi\n # ─────────────────────────────────────────────────────────────────\n echo \"\"\n echo -e \" $(msg matrix_e2ee.desc)\"\n echo \"\"\n echo \" 1) $(msg matrix_e2ee.disable)\"\n echo \" 2) $(msg matrix_e2ee.enable)\"\n echo \"\"\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_MATRIX_E2EE}\" ]; then\n local _e2ee_display; if [ \"${AGENTTEAMS_MATRIX_E2EE}\" = \"1\" ]; then _e2ee_display=\"$(msg matrix_e2ee.val_enabled)\"; else _e2ee_display=\"$(msg matrix_e2ee.val_disabled)\"; fi\n log \"$(msg prompt.upgrade_keep \"$(msg matrix_e2ee.title_short)\" \"${_e2ee_display}\")\"\n local _e2ee_choice\n read -e -p \"$(msg matrix_e2ee.choice): \" _e2ee_choice\n if [ \"${_e2ee_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n if [ -n \"${_e2ee_choice}\" ]; then\n case \"${_e2ee_choice}\" in\n 2) AGENTTEAMS_MATRIX_E2EE=\"1\" ;;\n *) AGENTTEAMS_MATRIX_E2EE=\"0\" ;;\n esac\n fi\n elif [ -z \"${AGENTTEAMS_MATRIX_E2EE+x}\" ]; then\n local _e2ee_choice\n read -e -p \"$(msg matrix_e2ee.choice): \" _e2ee_choice\n if [ \"${_e2ee_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _e2ee_choice=\"${_e2ee_choice:-1}\"\n case \"${_e2ee_choice}\" in\n 2) AGENTTEAMS_MATRIX_E2EE=\"1\" ;;\n *) AGENTTEAMS_MATRIX_E2EE=\"0\" ;;\n esac\n fi\n AGENTTEAMS_MATRIX_E2EE=\"${AGENTTEAMS_MATRIX_E2EE:-0}\"\n export AGENTTEAMS_MATRIX_E2EE\n if [ \"${AGENTTEAMS_MATRIX_E2EE}\" = \"1\" ]; then\n log \"$(msg matrix_e2ee.selected_enabled)\"\n else\n log \"$(msg matrix_e2ee.selected_disabled)\"\n fi\n}\n\nstep_docker_proxy() {\n # Only relevant when socket mounting is enabled\n if [ \"${AGENTTEAMS_MOUNT_SOCKET}\" != \"1\" ]; then\n AGENTTEAMS_DOCKER_PROXY=\"0\"\n return 0\n fi\n\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_DOCKER_PROXY=\"${AGENTTEAMS_DOCKER_PROXY:-0}\"\n log \" $(msg docker_proxy.title_short) = ${AGENTTEAMS_DOCKER_PROXY} (non-interactive, skipped)\"\n export AGENTTEAMS_DOCKER_PROXY\n return 0\n fi\n # ─────────────────────────────────────────────────────────────────\n\n echo \"\"\n echo -e \" \\033[1m$(msg docker_proxy.title)\\033[0m\"\n echo \"\"\n echo -e \" $(msg docker_proxy.desc)\"\n echo \"\"\n echo \" 1) $(msg docker_proxy.enable)\"\n echo \" 2) $(msg docker_proxy.disable)\"\n echo \"\"\n\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_DOCKER_PROXY}\" ]; then\n local _proxy_display; if [ \"${AGENTTEAMS_DOCKER_PROXY}\" = \"1\" ]; then _proxy_display=\"$(msg docker_proxy.val_enabled)\"; else _proxy_display=\"$(msg docker_proxy.val_disabled)\"; fi\n log \"$(msg prompt.upgrade_keep \"$(msg docker_proxy.title_short)\" \"${_proxy_display}\")\"\n local _choice\n read -e -p \"$(msg docker_proxy.choice): \" _choice\n if [ \"${_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n if [ -n \"${_choice}\" ]; then\n case \"${_choice}\" in\n 2) AGENTTEAMS_DOCKER_PROXY=\"0\" ;;\n *) AGENTTEAMS_DOCKER_PROXY=\"1\" ;;\n esac\n fi\n elif [ -z \"${AGENTTEAMS_DOCKER_PROXY+x}\" ]; then\n local _choice\n read -e -p \"$(msg docker_proxy.choice): \" _choice\n if [ \"${_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _choice=\"${_choice:-1}\"\n case \"${_choice}\" in\n 2) AGENTTEAMS_DOCKER_PROXY=\"0\" ;;\n *) AGENTTEAMS_DOCKER_PROXY=\"1\" ;;\n esac\n fi\n AGENTTEAMS_DOCKER_PROXY=\"${AGENTTEAMS_DOCKER_PROXY:-1}\"\n export AGENTTEAMS_DOCKER_PROXY\n if [ \"${AGENTTEAMS_DOCKER_PROXY}\" = \"1\" ]; then\n log \"$(msg docker_proxy.selected_enabled)\"\n\n # Prompt for additional allowed image sources\n echo \"\"\n echo -e \" $(msg docker_proxy.registries_desc)\"\n echo \"\"\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_PROXY_ALLOWED_REGISTRIES}\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg docker_proxy.registries_label)\" \"${AGENTTEAMS_PROXY_ALLOWED_REGISTRIES}\")\"\n local _reg_input\n read -e -p \"$(msg docker_proxy.registries_prompt): \" _reg_input\n if [ \"${_reg_input}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n [ -n \"${_reg_input}\" ] && AGENTTEAMS_PROXY_ALLOWED_REGISTRIES=\"${_reg_input}\"\n elif [ -z \"${AGENTTEAMS_PROXY_ALLOWED_REGISTRIES+x}\" ]; then\n local _reg_input\n read -e -p \"$(msg docker_proxy.registries_prompt): \" _reg_input\n if [ \"${_reg_input}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_PROXY_ALLOWED_REGISTRIES=\"${_reg_input:-}\"\n fi\n export AGENTTEAMS_PROXY_ALLOWED_REGISTRIES\n else\n log \"$(msg docker_proxy.selected_disabled)\"\n fi\n}\n\nstep_idle() {\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_WORKER_IDLE_TIMEOUT=\"${AGENTTEAMS_WORKER_IDLE_TIMEOUT:-720}\"\n log \" $(msg idle_timeout.label) = ${AGENTTEAMS_WORKER_IDLE_TIMEOUT} (non-interactive, skipped)\"\n export AGENTTEAMS_WORKER_IDLE_TIMEOUT\n return 0\n fi\n # ─────────────────────────────────────────────────────────────────\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_WORKER_IDLE_TIMEOUT}\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg idle_timeout.label)\" \"${AGENTTEAMS_WORKER_IDLE_TIMEOUT}\")\"\n local _idle_timeout\n read -e -p \"$(msg idle_timeout.prompt): \" _idle_timeout\n if [ \"${_idle_timeout}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n [ -n \"${_idle_timeout}\" ] && AGENTTEAMS_WORKER_IDLE_TIMEOUT=\"${_idle_timeout}\"\n elif [ -z \"${AGENTTEAMS_WORKER_IDLE_TIMEOUT+x}\" ]; then\n local _idle_timeout\n read -e -p \"$(msg idle_timeout.prompt): \" _idle_timeout\n if [ \"${_idle_timeout}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_WORKER_IDLE_TIMEOUT=\"${_idle_timeout:-720}\"\n fi\n AGENTTEAMS_WORKER_IDLE_TIMEOUT=\"${AGENTTEAMS_WORKER_IDLE_TIMEOUT:-720}\"\n export AGENTTEAMS_WORKER_IDLE_TIMEOUT\n log \"$(msg idle_timeout.selected \"${AGENTTEAMS_WORKER_IDLE_TIMEOUT}\")\"\n}\n\nstep_hostshare() {\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_HOST_SHARE_DIR=\"${AGENTTEAMS_HOST_SHARE_DIR:-${HOME}}\"\n log \" $(msg host_share.label) = ${AGENTTEAMS_HOST_SHARE_DIR} (non-interactive, skipped)\"\n export AGENTTEAMS_HOST_SHARE_DIR\n return 0\n fi\n # ─────────────────────────────────────────────────────────────────\n local _current=\"${AGENTTEAMS_HOST_SHARE_DIR}\"\n local _share_dir\n if [ -n \"${_current}\" ]; then\n read -e -p \"$(msg host_share.prompt \"$HOME\") [${_current}]: \" _share_dir\n else\n read -e -p \"$(msg host_share.prompt \"$HOME\"): \" _share_dir\n fi\n if [ \"${_share_dir}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_HOST_SHARE_DIR=\"${_share_dir:-${_current:-$HOME}}\"\n export AGENTTEAMS_HOST_SHARE_DIR\n}\n\nstep_podman_autostart() {\n echo \"\"\n log \"$(msg podman.autostart.title)\"\n echo \"\"\n echo -e \" $(msg podman.autostart.desc)\"\n echo \"\"\n echo \" 1) $(msg podman.autostart.enable)\"\n echo \" 2) $(msg podman.autostart.disable)\"\n echo \"\"\n\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_PODMAN_AUTOSTART}\" ]; then\n local _disp; if [ \"${AGENTTEAMS_PODMAN_AUTOSTART}\" = \"1\" ]; then _disp=\"$(msg podman.autostart.val_enabled)\"; else _disp=\"$(msg podman.autostart.val_disabled)\"; fi\n log \"$(msg prompt.upgrade_keep \"$(msg podman.autostart.title_short)\" \"${_disp}\")\"\n local _choice\n read -e -p \"$(msg podman.autostart.choice): \" _choice\n if [ \"${_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n if [ -n \"${_choice}\" ]; then\n case \"${_choice}\" in\n 2) AGENTTEAMS_PODMAN_AUTOSTART=\"0\" ;;\n *) AGENTTEAMS_PODMAN_AUTOSTART=\"1\" ;;\n esac\n fi\n elif [ -z \"${AGENTTEAMS_PODMAN_AUTOSTART+x}\" ]; then\n local _choice\n read -e -p \"$(msg podman.autostart.choice) [1]: \" _choice\n if [ \"${_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _choice=\"${_choice:-1}\"\n case \"${_choice}\" in\n 2) AGENTTEAMS_PODMAN_AUTOSTART=\"0\" ;;\n *) AGENTTEAMS_PODMAN_AUTOSTART=\"1\" ;;\n esac\n fi\n AGENTTEAMS_PODMAN_AUTOSTART=\"${AGENTTEAMS_PODMAN_AUTOSTART:-1}\"\n export AGENTTEAMS_PODMAN_AUTOSTART\n if [ \"${AGENTTEAMS_PODMAN_AUTOSTART}\" = \"1\" ]; then\n log \"$(msg podman.autostart.selected_enabled)\"\n else\n log \"$(msg podman.autostart.selected_disabled)\"\n fi\n}\n\n# ============================================================\n# Setup dedicated Podman autostart service for AgentTeams\n# ============================================================\nsetup_podman_autostart() {\n # Only execute if using podman and autostart is enabled\n if [ \"${DOCKER_CMD:-docker}\" != \"podman\" ] || [ \"${AGENTTEAMS_PODMAN_AUTOSTART:-0}\" != \"1\" ]; then\n return 0\n fi\n\n log \"$(msg install.podman.autostart_title)\"\n\n local current_user\n current_user=\"$(whoami)\"\n\n # Define the dedicated AgentTeams service content\n # Strict scoping: Only manage containers matching '--filter name=agentteams'\n local _service_content=\"[Unit]\nDescription=AgentTeams Dedicated Podman Autostart Service\nDocumentation=https://github.com/agentscope-ai/AgentTeams\nWants=network-online.target\nAfter=network-online.target\n\n[Service]\nType=oneshot\nRemainAfterExit=true\nEnvironment=LOGGING=\\\"--log-level=info\\\"\nExecStart=/usr/bin/podman \\$LOGGING start --filter name=agentteams --filter restart-policy=always --filter restart-policy=unless-stopped\nExecStop=/usr/bin/podman \\$LOGGING stop --filter name=agentteams --filter restart-policy=always --filter restart-policy=unless-stopped\n\n[Install]\nWantedBy=default.target\"\n\n local _autostart_ok=0\n\n if [ \"${current_user}\" = \"root\" ]; then\n # Rootful Mode (System-wide dedicated service)\n log \"$(msg install.podman.root_setup)\"\n\n local _sys_dir=\"/etc/systemd/system\"\n mkdir -p \"${_sys_dir}\"\n echo \"${_service_content}\" > \"${_sys_dir}/agentteams-podman-restart.service\"\n\n systemctl daemon-reload >/dev/null 2>&1 || true\n\n if systemctl enable --now agentteams-podman-restart.service >/dev/null 2>&1; then\n log \"$(msg install.podman.root_success)\"\n _autostart_ok=1\n else\n log \"$(msg install.podman.root_fail)\"\n fi\n else\n # Rootless Mode (User-level dedicated service)\n log \"$(msg install.podman.user_setup \"${current_user}\")\"\n\n local _linger_ok=0\n\n # 1. Enable Linger for background execution (Graceful escalation strategy)\n if loginctl show-user \"${current_user}\" --property=Linger 2>/dev/null | grep -q \"Linger=yes\"; then\n _linger_ok=1\n else\n log \"$(msg install.podman.linger_enable)\"\n\n # Step A: Try to enable linger natively without sudo\n if loginctl enable-linger \"${current_user}\" >/dev/null 2>&1; then\n log \"Successfully enabled systemd linger natively.\"\n _linger_ok=1\n else\n # Step B: Fallback to sudo\n log \"Native linger enablement failed. Distro security policy requires privileges.\"\n if command -v sudo >/dev/null 2>&1; then\n log \"Attempting to enable systemd linger via sudo (enter password if prompted)...\"\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n sudo -n loginctl enable-linger \"${current_user}\" 2>/dev/null && _linger_ok=1 || log \"$(msg install.podman.linger_warn)\"\n else\n sudo loginctl enable-linger \"${current_user}\" && _linger_ok=1 || log \"$(msg install.podman.linger_warn)\"\n fi\n else\n log \"$(msg install.podman.linger_warn)\"\n fi\n fi\n fi\n\n # 2. Write dedicated service file to user directory\n local _user_dir=\"${HOME}/.config/systemd/user\"\n mkdir -p \"${_user_dir}\"\n echo \"${_service_content}\" > \"${_user_dir}/agentteams-podman-restart.service\"\n\n systemctl --user daemon-reload >/dev/null 2>&1 || true\n\n if systemctl --user enable --now agentteams-podman-restart.service >/dev/null 2>&1; then\n log \"$(msg install.podman.user_success)\"\n # Crucial: Only consider autostart fully successful if systemd linger is also enabled\n if [ \"${_linger_ok}\" = \"1\" ]; then\n _autostart_ok=1\n else\n log \"⚠️ Service enabled, but since systemd linger is not active, containers will not auto-recover on system reboot.\"\n fi\n else\n log \"$(msg install.podman.user_fail)\"\n fi\n fi\n\n # Only print final success banner if the service AND linger were actually enabled\n if [ \"${_autostart_ok}\" = \"1\" ]; then\n log \"$(msg install.podman.success)\"\n fi\n}\n\n# ============================================================\n# Dashboard startup (called after controller is ready)\n# ============================================================\n\n_start_dashboard() {\n local CTRL_CONTAINER=\"agentteams-controller\"\n local DASHBOARD_CONTAINER=\"agentteams-dashboard\"\n\n if [ \"${AGENTTEAMS_DASHBOARD:-0}\" != \"1\" ]; then\n # Stop and remove the existing dashboard container if present.\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -q \"^${DASHBOARD_CONTAINER}$\"; then\n log \"Stopping and removing ${DASHBOARD_CONTAINER}...\"\n ${DOCKER_CMD} stop \"${DASHBOARD_CONTAINER}\" >/dev/null 2>&1 || true\n ${DOCKER_CMD} rm -f \"${DASHBOARD_CONTAINER}\" >/dev/null 2>&1 || true\n log \"Dashboard stopped.\"\n fi\n return 0\n fi\n\n # Dashboard requires the embedded architecture (controller container).\n # In legacy all-in-one mode the controller API is not available.\n if [ \"${AGENTTEAMS_USE_EMBEDDED:-0}\" != \"1\" ]; then\n log \"Skipping Dashboard: requires embedded architecture (agentteams-controller), not available in legacy mode.\"\n log \"Use Quick Start (embedded) mode to enable agentteams-dashboard.\"\n return 0\n fi\n\n AGENTTEAMS_PORT_DASHBOARD=\"${AGENTTEAMS_PORT_DASHBOARD:-13000}\"\n AGENTTEAMS_DASHBOARD_VERSION=\"${AGENTTEAMS_DASHBOARD_VERSION:-v1.2.0}\"\n AGENTTEAMS_DASHBOARD_IMAGE=\"${AGENTTEAMS_DASHBOARD_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-dashboard:${AGENTTEAMS_DASHBOARD_VERSION}}\"\n\n log \"\"\n log \"Starting agentteams-dashboard...\"\n\n local BIND_ADDR=\"127.0.0.1\"\n [ \"${AGENTTEAMS_LOCAL_ONLY:-1}\" = \"0\" ] && BIND_ADDR=\"0.0.0.0\"\n\n # Remove existing container\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' | grep -qx \"${DASHBOARD_CONTAINER}\"; then\n ${DOCKER_CMD} rm -f \"${DASHBOARD_CONTAINER}\" >/dev/null 2>&1 || true\n fi\n\n # Pull or use local image\n if ! ${DOCKER_CMD} image inspect \"${AGENTTEAMS_DASHBOARD_IMAGE}\" >/dev/null 2>&1; then\n log \"Pulling dashboard image: ${AGENTTEAMS_DASHBOARD_IMAGE}\"\n ${DOCKER_CMD} pull \"${AGENTTEAMS_DASHBOARD_IMAGE}\" 2>/dev/null || {\n log \"WARNING: Could not pull dashboard image. Skipping.\"\n return 0\n }\n fi\n\n # Build env args from controller container (MinIO/LLM/auth).\n local env_args=()\n env_args+=(-e AGENTTEAMS_CONTROLLER_URL=\"http://${CTRL_CONTAINER}:8090\")\n env_args+=(-e NEXT_PUBLIC_MATRIX_API_URL=\"http://${CTRL_CONTAINER}:6167\")\n env_args+=(-e MATRIX_HOMESERVER_ALLOWLIST=\"${CTRL_CONTAINER},matrix-local.agentteams.io,matrix.org\")\n\n if ${DOCKER_CMD} ps --format '{{.Names}}' | grep -qx \"${CTRL_CONTAINER}\"; then\n local env_out\n env_out=$(${DOCKER_CMD} inspect \"${CTRL_CONTAINER}\" --format='{{range .Config.Env}}{{.}}{{\"\\n\"}}{{end}}')\n\n _env() { echo \"${env_out}\" | sed -n \"s|^${1}=||p\" | head -n1; }\n\n local fs_endpoint; fs_endpoint=$(_env AGENTTEAMS_FS_ENDPOINT)\n [ -z \"${fs_endpoint}\" ] && fs_endpoint=$(_env AGENTTEAMS_MINIO_ENDPOINT)\n if [ -n \"${fs_endpoint}\" ]; then\n fs_endpoint=$(echo \"${fs_endpoint}\" | sed -e \"s|127\\.0\\.0\\.1|${CTRL_CONTAINER}|\" -e \"s|localhost|${CTRL_CONTAINER}|\")\n env_args+=(-e AGENTTEAMS_FS_ENDPOINT=\"${fs_endpoint}\")\n else\n env_args+=(-e AGENTTEAMS_FS_ENDPOINT=\"http://${CTRL_CONTAINER}:9000\")\n fi\n\n local fs_bucket; fs_bucket=$(_env AGENTTEAMS_FS_BUCKET)\n [ -z \"${fs_bucket}\" ] && fs_bucket=$(_env AGENTTEAMS_MINIO_BUCKET)\n [ -n \"${fs_bucket}\" ] && env_args+=(-e AGENTTEAMS_FS_BUCKET=\"${fs_bucket}\")\n\n local fs_user; fs_user=$(_env AGENTTEAMS_FS_ACCESS_KEY)\n [ -z \"${fs_user}\" ] && fs_user=$(_env AGENTTEAMS_MINIO_USER)\n [ -n \"${fs_user}\" ] && env_args+=(-e AGENTTEAMS_FS_ACCESS_KEY=\"${fs_user}\")\n\n local fs_pass; fs_pass=$(_env AGENTTEAMS_FS_SECRET_KEY)\n [ -z \"${fs_pass}\" ] && fs_pass=$(_env AGENTTEAMS_MINIO_PASSWORD)\n [ -n \"${fs_pass}\" ] && env_args+=(-e AGENTTEAMS_FS_SECRET_KEY=\"${fs_pass}\")\n\n for k in AGENTTEAMS_LLM_PROVIDER AGENTTEAMS_LLM_API_KEY AGENTTEAMS_OPENAI_BASE_URL AGENTTEAMS_DEFAULT_MODEL; do\n local v; v=$(_env \"${k}\")\n [ -n \"${v}\" ] && env_args+=(-e \"${k}=${v}\")\n done\n\n # The controller writes the CLI SA token to /var/run/agentteams/cli-token.\n # The token is generated asynchronously during the first reconcile loop,\n # so it may not be available immediately after the container starts.\n # We poll for up to 30s to handle both fresh installs (token not yet minted)\n # and older images that don't support cli-token at all.\n # For backward compatibility, also try the legacy /var/run/hiclaw/cli-token\n # path used by older HiClaw images.\n local auth_token=\"${AGENTTEAMS_AUTH_TOKEN:-}\"\n if [ -z \"${auth_token}\" ]; then\n local _token_wait=0\n local _token_max_wait=30\n while [ ${_token_wait} -lt ${_token_max_wait} ]; do\n auth_token=$(${DOCKER_CMD} exec \"${CTRL_CONTAINER}\" sh -c 'cat /var/run/agentteams/cli-token 2>/dev/null || cat /var/run/hiclaw/cli-token 2>/dev/null' | tr -d '\\n' || true)\n if [ -n \"${auth_token}\" ]; then\n break\n fi\n sleep 2\n _token_wait=$((_token_wait + 2))\n done\n fi\n if [ -n \"${auth_token}\" ]; then\n env_args+=(-e AGENTTEAMS_AUTH_TOKEN=\"${auth_token}\")\n else\n log \"WARNING: could not read controller SA token (timed out after ${_token_max_wait:-30}s)\"\n log \" This can happen with older AgentTeams/HiClaw images or if the controller is still starting.\"\n log \" Dashboard will still work but some API calls may fail until you log in via Higress Console.\"\n log \" To fix: upgrade AgentTeams to v1.2.0-beta.1+ or set AGENTTEAMS_AUTH_TOKEN manually.\"\n fi\n\n local admin_user; admin_user=$(_env AGENTTEAMS_ADMIN_USER)\n local admin_pass; admin_pass=$(_env AGENTTEAMS_ADMIN_PASSWORD)\n [ -n \"${admin_user}\" ] && env_args+=(-e AGENTTEAMS_ADMIN_USER=\"${admin_user}\")\n [ -n \"${admin_pass}\" ] && env_args+=(-e AGENTTEAMS_ADMIN_PASSWORD=\"${admin_pass}\")\n\n # Higress Console URL: explicit config takes priority, auto-detect as fallback.\n local _resolved_gw_url=\"\"\n if [ -n \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\" ]; then\n # Normalize URL: add http:// prefix if missing\n _resolved_gw_url=\"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\"\n case \"${_resolved_gw_url}\" in\n http://*|https://*) ;;\n *) _resolved_gw_url=\"http://${_resolved_gw_url}\" ;;\n esac\n env_args+=(-e AGENTTEAMS_AI_GATEWAY_ADMIN_URL=\"${_resolved_gw_url}\")\n elif ${DOCKER_CMD} exec \"${CTRL_CONTAINER}\" wget -q -O- --timeout=2 http://127.0.0.1:8001/ >/dev/null 2>&1; then\n _resolved_gw_url=\"http://${CTRL_CONTAINER}:8001\"\n env_args+=(-e AGENTTEAMS_AI_GATEWAY_ADMIN_URL=\"${_resolved_gw_url}\")\n fi\n\n # Verify Higress Console URL reachability (best-effort warning)\n if [ -n \"${_resolved_gw_url}\" ]; then\n if ! ${DOCKER_CMD} exec \"${CTRL_CONTAINER}\" curl -sf --max-time 3 \"${_resolved_gw_url}/\" >/dev/null 2>&1; then\n log \"WARNING: Higress Console URL may not be reachable: ${_resolved_gw_url}\"\n log \" Dashboard will still work, but shared login via Higress Console may fail.\"\n log \" To fix: verify the URL is correct and the Higress Console service is running.\"\n fi\n fi\n else\n log \"WARNING: ${CTRL_CONTAINER} not running; launching Dashboard without Controller/MinIO/LLM env.\"\n if [ -n \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\" ]; then\n local _gw_norm=\"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\"\n case \"${_gw_norm}\" in\n http://*|https://*) ;;\n *) _gw_norm=\"http://${_gw_norm}\" ;;\n esac\n env_args+=(-e AGENTTEAMS_AI_GATEWAY_ADMIN_URL=\"${_gw_norm}\")\n fi\n fi\n\n # Create persistent data volume (for future use)\n ${DOCKER_CMD} volume create agentteams-dashboard-data >/dev/null 2>&1 || true\n\n ${DOCKER_CMD} run -d \\\n --name \"${DASHBOARD_CONTAINER}\" \\\n --restart unless-stopped \\\n --network \"${AGENTTEAMS_NETWORK:-agentteams-net}\" \\\n --network-alias dashboard.agentteams.io \\\n -p \"${BIND_ADDR}:${AGENTTEAMS_PORT_DASHBOARD}:3000\" \\\n \"${env_args[@]}\" \\\n -v agentteams-dashboard-data:/app/db \\\n \"${AGENTTEAMS_DASHBOARD_IMAGE}\"\n\n # Wait for dashboard to be ready\n local max_wait=60\n local waited=0\n while [ $waited -lt $max_wait ]; do\n if curl -sf \"http://127.0.0.1:${AGENTTEAMS_PORT_DASHBOARD}/\" >/dev/null 2>&1; then\n log \"Dashboard ready on port ${AGENTTEAMS_PORT_DASHBOARD}\"\n return 0\n fi\n sleep 2\n waited=$((waited + 2))\n done\n log \"WARNING: Dashboard startup timed out after ${max_wait}s\"\n ${DOCKER_CMD} logs \"${DASHBOARD_CONTAINER}\" 2>&1 | tail -20\n return 0\n}\n\n# ============================================================\n# Manager Installation (Interactive)\n# ============================================================\n\ninstall_manager() {\n log \"$(msg install.title)\"\n log \"$(msg install.registry \"${AGENTTEAMS_REGISTRY}\")\"\n log \"\"\n log \"$(msg install.dir \"$(pwd)\")\"\n log \"$(msg install.dir_hint)\"\n log \"$(msg install.dir_hint2)\"\n log \"\"\n\n # Non-interactive fallback: resolve version immediately so image tags are available\n # before the step state machine runs. Interactive mode lets step_version handle it.\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_VERSION=\"${AGENTTEAMS_VERSION:-${AGENTTEAMS_KNOWN_STABLE_VERSION}}\"\n resolve_image_tags\n fi\n\n # Migrate legacy env file location before checks\n local existing_env=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n if [ ! -f \"${existing_env}\" ] && [ -f \"./agentteams-manager.env\" ]; then\n log \"Migrating agentteams-manager.env from current directory to ${existing_env}...\"\n mv \"./agentteams-manager.env\" \"${existing_env}\"\n fi\n\n # Orphan volume detection (only when no env file — step_existing handles the env-file case)\n if [ ! -f \"${existing_env}\" ]; then\n local data_vol=\"${AGENTTEAMS_DATA_DIR:-agentteams-data}\"\n if ${DOCKER_CMD} volume ls -q | grep -q \"^${data_vol}$\"; then\n echo \"\"\n log \"$(msg install.orphan_volume.detected \"${data_vol}\")\"\n log \"$(msg install.orphan_volume.warn)\"\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n log \"$(msg install.orphan_volume.clean_noninteractive)\"\n ${DOCKER_CMD} stop agentteams-manager 2>/dev/null || true\n ${DOCKER_CMD} rm agentteams-manager 2>/dev/null || true\n for w in $(${DOCKER_CMD} ps -a --format '{{.Names}}' | grep \"^agentteams-worker-\" || true); do\n ${DOCKER_CMD} stop \"${w}\" 2>/dev/null || true\n ${DOCKER_CMD} rm \"${w}\" 2>/dev/null || true\n done\n log \"$(msg install.orphan_volume.cleaning)\"\n ${DOCKER_CMD} volume rm \"${data_vol}\" 2>/dev/null || true\n log \"$(msg install.orphan_volume.cleaned)\"\n else\n echo \"\"\n echo \"$(msg install.orphan_volume.choose)\"\n echo \"$(msg install.orphan_volume.clean)\"\n echo \"$(msg install.orphan_volume.keep)\"\n echo \"\"\n local ORPHAN_CHOICE\n read -e -p \"$(msg install.orphan_volume.prompt): \" ORPHAN_CHOICE\n ORPHAN_CHOICE=\"${ORPHAN_CHOICE:-1}\"\n case \"${ORPHAN_CHOICE}\" in\n 1|clean)\n ${DOCKER_CMD} stop agentteams-manager 2>/dev/null || true\n ${DOCKER_CMD} rm agentteams-manager 2>/dev/null || true\n for w in $(${DOCKER_CMD} ps -a --format '{{.Names}}' | grep \"^agentteams-worker-\" || true); do\n ${DOCKER_CMD} stop \"${w}\" 2>/dev/null || true\n ${DOCKER_CMD} rm \"${w}\" 2>/dev/null || true\n done\n log \"$(msg install.orphan_volume.cleaning)\"\n ${DOCKER_CMD} volume rm \"${data_vol}\" 2>/dev/null || true\n log \"$(msg install.orphan_volume.cleaned)\"\n ;;\n 2|keep)\n log \"$(msg install.orphan_volume.keeping)\"\n ;;\n esac\n fi\n fi\n fi\n\n # ── State machine ─────────────────────────────────────────────────────────\n local _STEPS=( step_lang step_mode step_version step_existing step_llm step_manager_runtime step_runtime step_admin step_network\n step_ports step_domains step_github step_skills step_volume\n step_workspace step_dashboard step_e2ee step_docker_proxy step_idle step_hostshare step_podman_autostart )\n local _STEP_HISTORY=()\n local _step_idx=0\n while [ \"${_step_idx}\" -lt \"${#_STEPS[@]}\" ]; do\n local _step_fn=\"${_STEPS[$_step_idx]}\"\n if should_skip_step \"${_step_fn}\"; then\n _step_idx=$((_step_idx + 1))\n continue\n fi\n if [ \"${#_STEP_HISTORY[@]}\" -gt 0 ]; then\n log \"$(msg nav.back_hint)\"\n fi\n STEP_RESULT=\"\"\n \"${_step_fn}\"\n if [ \"${STEP_RESULT}\" = \"back\" ]; then\n if [ \"${#_STEP_HISTORY[@]}\" -gt 0 ]; then\n local _last=$(( ${#_STEP_HISTORY[@]} - 1 ))\n _step_idx=\"${_STEP_HISTORY[$_last]}\"\n _STEP_HISTORY=(\"${_STEP_HISTORY[@]:0:${_last}}\")\n clear_step_vars \"${_STEPS[$_step_idx]}\"\n fi\n # else: first step, ignore 'b'\n else\n _STEP_HISTORY+=(\"${_step_idx}\")\n _step_idx=$((_step_idx + 1))\n fi\n done\n # ── End state machine ──────────────────────────────────────────────────────\n\n # Post-machine defaults for any steps that were skipped\n AGENTTEAMS_DATA_DIR=\"${AGENTTEAMS_DATA_DIR:-agentteams-data}\"\n if [ -z \"${AGENTTEAMS_WORKSPACE_DIR+x}\" ] || [ -z \"${AGENTTEAMS_WORKSPACE_DIR}\" ]; then\n AGENTTEAMS_WORKSPACE_DIR=\"${HOME}/agentteams-manager\"\n export AGENTTEAMS_WORKSPACE_DIR\n fi\n AGENTTEAMS_WORKSPACE_DIR=\"$(cd \"${AGENTTEAMS_WORKSPACE_DIR}\" 2>/dev/null && pwd || echo \"${AGENTTEAMS_WORKSPACE_DIR}\")\"\n mkdir -p \"${AGENTTEAMS_WORKSPACE_DIR}\"\n AGENTTEAMS_MANAGER_RUNTIME=\"${AGENTTEAMS_MANAGER_RUNTIME:-copaw}\"\n export AGENTTEAMS_MANAGER_RUNTIME\n AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"${AGENTTEAMS_DEFAULT_WORKER_RUNTIME:-copaw}\"\n AGENTTEAMS_MATRIX_E2EE=\"${AGENTTEAMS_MATRIX_E2EE:-0}\"\n export AGENTTEAMS_MATRIX_E2EE\n AGENTTEAMS_WORKER_IDLE_TIMEOUT=\"${AGENTTEAMS_WORKER_IDLE_TIMEOUT:-720}\"\n export AGENTTEAMS_WORKER_IDLE_TIMEOUT\n AGENTTEAMS_HOST_SHARE_DIR=\"${AGENTTEAMS_HOST_SHARE_DIR:-$HOME}\"\n export AGENTTEAMS_HOST_SHARE_DIR\n\n log \"\"\n\n # Generate secrets (only if not already set)\n log \"$(msg install.generating_secrets)\"\n AGENTTEAMS_MANAGER_PASSWORD=\"${AGENTTEAMS_MANAGER_PASSWORD:-$(generate_key)}\"\n AGENTTEAMS_REGISTRATION_TOKEN=\"${AGENTTEAMS_REGISTRATION_TOKEN:-$(generate_key)}\"\n AGENTTEAMS_MINIO_USER=\"${AGENTTEAMS_MINIO_USER:-${AGENTTEAMS_ADMIN_USER}}\"\n AGENTTEAMS_MINIO_PASSWORD=\"${AGENTTEAMS_MINIO_PASSWORD:-${AGENTTEAMS_ADMIN_PASSWORD}}\"\n AGENTTEAMS_MANAGER_GATEWAY_KEY=\"${AGENTTEAMS_MANAGER_GATEWAY_KEY:-$(generate_key)}\"\n\n # Matrix AppService tokens — generate once during install/upgrade if not provided.\n # Persisted to env file so they survive controller restarts.\n AGENTTEAMS_MATRIX_APPSERVICE_ENABLED=\"${AGENTTEAMS_MATRIX_APPSERVICE_ENABLED:-true}\"\n export AGENTTEAMS_MATRIX_APPSERVICE_ENABLED\n if [ \"${AGENTTEAMS_MATRIX_APPSERVICE_ENABLED}\" != \"false\" ] && [ \"${AGENTTEAMS_MATRIX_APPSERVICE_ENABLED}\" != \"0\" ]; then\n if [ -z \"${AGENTTEAMS_MATRIX_APPSERVICE_AS_TOKEN}\" ]; then\n AGENTTEAMS_MATRIX_APPSERVICE_AS_TOKEN=\"$(openssl rand -hex 32)\"\n log \" Auto-generated AppService as_token (saved to env file)\"\n fi\n if [ -z \"${AGENTTEAMS_MATRIX_APPSERVICE_HS_TOKEN}\" ]; then\n AGENTTEAMS_MATRIX_APPSERVICE_HS_TOKEN=\"$(openssl rand -hex 32)\"\n log \" Auto-generated AppService hs_token (saved to env file)\"\n fi\n export AGENTTEAMS_MATRIX_APPSERVICE_AS_TOKEN\n export AGENTTEAMS_MATRIX_APPSERVICE_HS_TOKEN\n fi\n\n # Detect Apple Silicon (M1/M2/M3/M4) - need JVM fix for Higress Console\n # See: https://github.com/agentscope-ai/AgentTeams/issues/249\n if [ -z \"${JVM_ARGS:-}\" ] && [ \"$(uname -m)\" = \"arm64\" ] && [ \"$(uname -s)\" = \"Darwin\" ]; then\n log \"Apple Silicon detected - setting JVM_ARGS to fix Higress Console SIGILL issue\"\n JVM_ARGS=\"-XX:+UnlockDiagnosticVMOptions -XX:-UseAESCTRIntrinsics -XX:UseSVE=0\"\n fi\n\n # Write .env file\n ENV_FILE=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n cat > \"${ENV_FILE}\" << EOF\n# AgentTeams Manager Configuration\n# Generated by agentteams-install.sh on $(date)\n\n# Language\nAGENTTEAMS_LANGUAGE=${AGENTTEAMS_LANGUAGE}\n\n# LLM\nAGENTTEAMS_LLM_PROVIDER=${AGENTTEAMS_LLM_PROVIDER}\nAGENTTEAMS_DEFAULT_MODEL=${AGENTTEAMS_DEFAULT_MODEL}\nAGENTTEAMS_LLM_API_KEY=${AGENTTEAMS_LLM_API_KEY}\nAGENTTEAMS_OPENAI_BASE_URL=${AGENTTEAMS_OPENAI_BASE_URL:-}\nAGENTTEAMS_MODEL_CONTEXT_WINDOW=${AGENTTEAMS_MODEL_CONTEXT_WINDOW:-}\nAGENTTEAMS_MODEL_MAX_TOKENS=${AGENTTEAMS_MODEL_MAX_TOKENS:-}\nAGENTTEAMS_MODEL_REASONING=${AGENTTEAMS_MODEL_REASONING:-}\nAGENTTEAMS_MODEL_VISION=${AGENTTEAMS_MODEL_VISION:-}\n\n# Embedding model (empty = disabled, default: text-embedding-v4)\nAGENTTEAMS_EMBEDDING_MODEL=${AGENTTEAMS_EMBEDDING_MODEL}\n\n# Admin\nAGENTTEAMS_ADMIN_USER=${AGENTTEAMS_ADMIN_USER}\nAGENTTEAMS_ADMIN_PASSWORD=${AGENTTEAMS_ADMIN_PASSWORD}\n\n# Ports\nAGENTTEAMS_LOCAL_ONLY=${AGENTTEAMS_LOCAL_ONLY}\nAGENTTEAMS_PORT_GATEWAY=${AGENTTEAMS_PORT_GATEWAY}\nAGENTTEAMS_PORT_CONSOLE=${AGENTTEAMS_PORT_CONSOLE}\nAGENTTEAMS_PORT_ELEMENT_WEB=${AGENTTEAMS_PORT_ELEMENT_WEB}\nAGENTTEAMS_PORT_MANAGER_CONSOLE=${AGENTTEAMS_PORT_MANAGER_CONSOLE:-18888}\n\n# Manager runtime (openclaw | copaw)\nAGENTTEAMS_MANAGER_RUNTIME=${AGENTTEAMS_MANAGER_RUNTIME:-copaw}\n\n# Matrix\nAGENTTEAMS_MATRIX_DOMAIN=${AGENTTEAMS_MATRIX_DOMAIN}\nAGENTTEAMS_MATRIX_CLIENT_DOMAIN=${AGENTTEAMS_MATRIX_CLIENT_DOMAIN}\n\n# Gateway\nAGENTTEAMS_AI_GATEWAY_DOMAIN=${AGENTTEAMS_AI_GATEWAY_DOMAIN}\nAGENTTEAMS_MANAGER_GATEWAY_KEY=${AGENTTEAMS_MANAGER_GATEWAY_KEY}\n\n# File System\nAGENTTEAMS_FS_DOMAIN=${AGENTTEAMS_FS_DOMAIN}\nAGENTTEAMS_CONSOLE_DOMAIN=${AGENTTEAMS_CONSOLE_DOMAIN}\nAGENTTEAMS_MINIO_USER=${AGENTTEAMS_MINIO_USER}\nAGENTTEAMS_MINIO_PASSWORD=${AGENTTEAMS_MINIO_PASSWORD}\n\n# Internal\nAGENTTEAMS_MANAGER_PASSWORD=${AGENTTEAMS_MANAGER_PASSWORD}\nAGENTTEAMS_REGISTRATION_TOKEN=${AGENTTEAMS_REGISTRATION_TOKEN}\n\n# GitHub (optional)\nAGENTTEAMS_GITHUB_TOKEN=${AGENTTEAMS_GITHUB_TOKEN:-}\n\n# Nacos package import defaults\nAGENTTEAMS_NACOS_REGISTRY_URI=${AGENTTEAMS_NACOS_REGISTRY_URI:-nacos://market.agentteams.io:80/public}\nAGENTTEAMS_NACOS_USERNAME=${AGENTTEAMS_NACOS_USERNAME:-}\nAGENTTEAMS_NACOS_PASSWORD=${AGENTTEAMS_NACOS_PASSWORD:-}\nAGENTTEAMS_NACOS_TOKEN=${AGENTTEAMS_NACOS_TOKEN:-}\n\n# Skills Registry (optional, default: nacos://market.agentteams.io:80/public)\nAGENTTEAMS_SKILLS_API_URL=${AGENTTEAMS_SKILLS_API_URL:-nacos://market.agentteams.io:80/public}\n\n# OpenClaw CMS plugin (optional)\nAGENTTEAMS_CMS_TRACES_ENABLED=${AGENTTEAMS_CMS_TRACES_ENABLED:-false}\nAGENTTEAMS_CMS_ENDPOINT=${AGENTTEAMS_CMS_ENDPOINT:-}\nAGENTTEAMS_CMS_LICENSE_KEY=${AGENTTEAMS_CMS_LICENSE_KEY:-}\nAGENTTEAMS_CMS_PROJECT=${AGENTTEAMS_CMS_PROJECT:-}\nAGENTTEAMS_CMS_WORKSPACE=${AGENTTEAMS_CMS_WORKSPACE:-}\nAGENTTEAMS_CMS_SERVICE_NAME=${AGENTTEAMS_CMS_SERVICE_NAME:-agentteams-manager}\nAGENTTEAMS_CMS_METRICS_ENABLED=${AGENTTEAMS_CMS_METRICS_ENABLED:-false}\n\n# Worker images (for direct container creation)\nAGENTTEAMS_WORKER_IMAGE=${WORKER_IMAGE}\nAGENTTEAMS_COPAW_WORKER_IMAGE=${COPAW_WORKER_IMAGE}\nAGENTTEAMS_QWENPAW_WORKER_IMAGE=${QWENPAW_WORKER_IMAGE}\nAGENTTEAMS_HERMES_WORKER_IMAGE=${HERMES_WORKER_IMAGE}\n\n# Default Worker runtime (openclaw | copaw | qwenpaw | hermes)\nAGENTTEAMS_DEFAULT_WORKER_RUNTIME=${AGENTTEAMS_DEFAULT_WORKER_RUNTIME:-copaw}\n\n# Matrix E2EE (0=disabled, 1=enabled; default: 0)\nAGENTTEAMS_MATRIX_E2EE=${AGENTTEAMS_MATRIX_E2EE:-0}\n\n# Matrix AppService\nAGENTTEAMS_MATRIX_APPSERVICE_ENABLED=${AGENTTEAMS_MATRIX_APPSERVICE_ENABLED:-true}\nAGENTTEAMS_MATRIX_APPSERVICE_AS_TOKEN=${AGENTTEAMS_MATRIX_APPSERVICE_AS_TOKEN:-}\nAGENTTEAMS_MATRIX_APPSERVICE_HS_TOKEN=${AGENTTEAMS_MATRIX_APPSERVICE_HS_TOKEN:-}\n\n# Docker API proxy (0=disabled, 1=enabled; default: 1)\nAGENTTEAMS_DOCKER_PROXY=${AGENTTEAMS_DOCKER_PROXY:-1}\n\n# Docker API proxy: additional allowed image sources (comma-separated)\nAGENTTEAMS_PROXY_ALLOWED_REGISTRIES=${AGENTTEAMS_PROXY_ALLOWED_REGISTRIES:-}\n\n# Worker idle timeout in minutes (default: 720 = 12 hours)\nAGENTTEAMS_WORKER_IDLE_TIMEOUT=${AGENTTEAMS_WORKER_IDLE_TIMEOUT:-720}\n\n# Podman autostart via systemd (0=disabled, 1=enabled)\nAGENTTEAMS_PODMAN_AUTOSTART=${AGENTTEAMS_PODMAN_AUTOSTART:-0}\n\n# JVM Args for Higress Console (fixes SIGILL on Apple Silicon)\nJVM_ARGS=${JVM_ARGS:-}\n\n# Higress WASM plugin image registry (auto-selected by timezone)\nHIGRESS_ADMIN_WASM_PLUGIN_IMAGE_REGISTRY=${AGENTTEAMS_REGISTRY}\n\n# Data persistence\nAGENTTEAMS_DATA_DIR=${AGENTTEAMS_DATA_DIR:-agentteams-data}\n# Manager workspace (skills, memory, state — host-editable)\nAGENTTEAMS_WORKSPACE_DIR=${AGENTTEAMS_WORKSPACE_DIR:-}\n# Host directory sharing\nAGENTTEAMS_HOST_SHARE_DIR=${AGENTTEAMS_HOST_SHARE_DIR:-}\n\n# agentteams-dashboard (management UI)\nAGENTTEAMS_DASHBOARD=${AGENTTEAMS_DASHBOARD:-1}\nAGENTTEAMS_DASHBOARD_VERSION=${AGENTTEAMS_DASHBOARD_VERSION:-v1.2.0}\nAGENTTEAMS_PORT_DASHBOARD=${AGENTTEAMS_PORT_DASHBOARD:-13000}\nAGENTTEAMS_DASHBOARD_IMAGE=${AGENTTEAMS_DASHBOARD_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-dashboard:${AGENTTEAMS_DASHBOARD_VERSION}}\nAGENTTEAMS_AI_GATEWAY_ADMIN_URL=${AGENTTEAMS_AI_GATEWAY_ADMIN_URL:-}\nEOF\n\n chmod 600 \"${ENV_FILE}\"\n log \"$(msg install.config_saved \"${ENV_FILE}\")\"\n\n # Detect container runtime socket\n SOCKET_MOUNT_ARGS=\"\"\n if [ \"${AGENTTEAMS_MOUNT_SOCKET}\" = \"1\" ]; then\n # Actively ensure the API socket is enabled and active before detection.\n if [ \"${DOCKER_CMD:-}\" = \"podman\" ]; then\n ensure_podman_socket\n fi\n CONTAINER_SOCK=$(detect_socket)\n if [ -n \"${CONTAINER_SOCK}\" ]; then\n log \"$(msg install.socket_detected \"${CONTAINER_SOCK}\")\"\n SOCKET_MOUNT_ARGS=\"-v ${CONTAINER_SOCK}:/var/run/docker.sock --security-opt label=disable\"\n else\n log \"$(msg install.socket_not_found)\"\n # Interactive confirmation when socket not found\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" != \"1\" ]; then\n echo \"\"\n echo -e \"\\033[33m$(msg install.socket_confirm.title)\\033[0m\"\n echo \"\"\n echo -e \"$(msg install.socket_confirm.message)\"\n echo \"\"\n read -p \"$(msg install.socket_confirm.prompt)\" SOCKET_CONFIRM\n if [ \"${SOCKET_CONFIRM}\" != \"y\" ] && [ \"${SOCKET_CONFIRM}\" != \"Y\" ]; then\n log \"$(msg install.socket_confirm.cancelled)\"\n exit 0\n fi\n fi\n fi\n fi\n\n # Create the data volume if it doesn't already exist (reuse on reinstall)\n if ! ${DOCKER_CMD} volume ls -q | grep -q \"^${AGENTTEAMS_DATA_DIR}$\"; then\n ${DOCKER_CMD} volume create \"${AGENTTEAMS_DATA_DIR}\" > /dev/null\n fi\n\n # Data mount: Docker volume\n DATA_MOUNT_ARGS=\"-v ${AGENTTEAMS_DATA_DIR}:/data\"\n\n # Manager workspace mount (always a host directory, defaulting to ~/agentteams-manager)\n WORKSPACE_MOUNT_ARGS=\"-v ${AGENTTEAMS_WORKSPACE_DIR}:/root/manager-workspace\"\n\n # Pass host timezone to container so date/time commands reflect local time\n TZ_ARGS=\"-e TZ=${AGENTTEAMS_TIMEZONE}\"\n\n # Host directory mount\n if [ -d \"${AGENTTEAMS_HOST_SHARE_DIR}\" ]; then\n HOST_SHARE_MOUNT_ARGS=\"-v ${AGENTTEAMS_HOST_SHARE_DIR}:/host-share\"\n log \"$(msg host_share.sharing \"${AGENTTEAMS_HOST_SHARE_DIR}\")\"\n else\n log \"$(msg host_share.not_exist \"${AGENTTEAMS_HOST_SHARE_DIR}\")\"\n HOST_SHARE_MOUNT_ARGS=\"-v ${AGENTTEAMS_HOST_SHARE_DIR}:/host-share\"\n fi\n\n # YOLO mode: pass through if set in environment (enables autonomous decisions)\n YOLO_ARGS=\"\"\n if [ \"${AGENTTEAMS_YOLO:-}\" = \"1\" ]; then\n YOLO_ARGS=\"-e AGENTTEAMS_YOLO=1\"\n log \"$(msg install.yolo)\"\n fi\n\n # Matrix-plugin debug tracing: pass through if AGENTTEAMS_MATRIX_DEBUG=1.\n # The container entrypoints translate this to OPENCLAW_MATRIX_DEBUG=1\n # so the openclaw matrix plugin emits structured INFO-level lifecycle\n # traces (sync state transitions, room.invite/join, message handler\n # arrival + filter outcomes). Used to diagnose worker/manager hangs.\n MATRIX_DEBUG_ARGS=\"\"\n if [ \"${AGENTTEAMS_MATRIX_DEBUG:-}\" = \"1\" ]; then\n MATRIX_DEBUG_ARGS=\"-e AGENTTEAMS_MATRIX_DEBUG=1\"\n fi\n\n # E2EE is already in the env file; but also pass explicitly in case env file is not the source\n # (AGENTTEAMS_MATRIX_E2EE is already written to ENV_FILE above via --env-file)\n\n # Pull images (manager based on runtime config; all worker runtimes always pulled)\n _is_local_image() {\n case \"$1\" in\n agentteams/*) return 0 ;;\n *) return 1 ;;\n esac\n }\n\n _local_image_exists() {\n local _img=\"$1\"\n [ -z \"${_img}\" ] && return 1\n _is_local_image \"${_img}\" || return 1\n ${DOCKER_CMD} image inspect \"${_img}\" >/dev/null 2>&1\n }\n\n # Helper: pull or skip a single image\n # Args: $1=image $2=exists_msg_key $3=pulling_msg_key\n _pull_image() {\n local _img=\"$1\" _exists_key=\"$2\" _pull_key=\"$3\"\n [ -z \"${_img}\" ] && return 0\n if _local_image_exists \"${_img}\"; then\n log \"$(msg \"${_exists_key}\" \"${_img}\")\"\n return 0\n fi\n log \"$(msg \"${_pull_key}\" \"${_img}\")\"\n local _attempt=1\n while [ $_attempt -le 3 ]; do\n if ${DOCKER_CMD} pull \"${_img}\"; then\n return 0\n fi\n if [ $_attempt -lt 3 ]; then\n log \"Pull failed (attempt ${_attempt}/3), retrying in 5s...\"\n sleep 5\n fi\n _attempt=$((_attempt + 1))\n done\n die \"Failed to pull ${_img} after 3 attempts\"\n return 1\n }\n\n # Embedded controller image (resolve versioned tag, fallback to latest)\n resolve_embedded_image\n if [ \"${AGENTTEAMS_USE_EMBEDDED}\" = \"1\" ]; then\n _local_image_exists \"${EMBEDDED_IMAGE}\" || true\n fi\n\n # Manager image is always required (select based on runtime)\n if [ \"${AGENTTEAMS_MANAGER_RUNTIME}\" = \"copaw\" ]; then\n _pull_image \"${MANAGER_COPAW_IMAGE}\" \"install.image.exists\" \"install.image.pulling_manager\"\n else\n _pull_image \"${MANAGER_IMAGE}\" \"install.image.exists\" \"install.image.pulling_manager\"\n fi\n\n # Pull all worker runtime images (workers may use any runtime regardless of the default)\n _pull_image \"${WORKER_IMAGE}\" \"install.image.worker_exists\" \"install.image.pulling_worker\"\n _pull_image \"${COPAW_WORKER_IMAGE}\" \"install.image.worker_exists\" \"install.image.pulling_worker\"\n # Temporarily disabled until the QwenPaw worker image is published.\n # _pull_image \"${QWENPAW_WORKER_IMAGE}\" \"install.image.worker_exists\" \"install.image.pulling_worker\"\n _pull_image \"${HERMES_WORKER_IMAGE}\" \"install.image.worker_exists\" \"install.image.pulling_worker\"\n\n # --- Pre-upgrade: extract Matrix passwords from running old containers ---\n # Only needed when upgrading FROM old architecture (v1.0.9) TO embedded.\n # For new-arch-to-new-arch upgrades, credential files already exist with\n # correct room IDs — we must NOT overwrite them.\n _creds_tmp=\"\"\n if [ \"${AGENTTEAMS_UPGRADE:-0}\" = \"1\" ] && [ \"${AGENTTEAMS_USE_EMBEDDED}\" = \"1\" ]; then\n # Detect if upgrading from old arch: old arch has no agentteams-controller container\n # (or has it as a docker-proxy only, not embedded). Check if the existing\n # agentteams-controller is an embedded image (has supervisord) or just a proxy.\n _is_old_arch=0\n if ! ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -q \"^agentteams-controller$\"; then\n _is_old_arch=1\n elif ${DOCKER_CMD} ps -a --format '{{.Names}} {{.Image}}' 2>/dev/null | grep \"^agentteams-controller \" | grep -qv \"embedded\"; then\n _is_old_arch=1\n fi\n\n if [ \"${_is_old_arch}\" = \"1\" ]; then\n _creds_tmp=$(mktemp -d)\n\n # docker exec for Matrix/minio paths only works while agentteams-manager is running.\n _mgr_creds_tempstart=0\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -q '^agentteams-manager$'; then\n if ! ${DOCKER_CMD} ps --format '{{.Names}}' 2>/dev/null | grep -q '^agentteams-manager$'; then\n log \"agentteams-manager is stopped; starting it temporarily to extract Matrix credentials for upgrade...\"\n ${DOCKER_CMD} start agentteams-manager 2>/dev/null || true\n wait_matrix_ready \"agentteams-manager\"\n _mgr_creds_tempstart=1\n fi\n fi\n\n # Manager password (container Config.Env, then secrets file inside running manager, then data volume)\n _mgr_pw=$(${DOCKER_CMD} inspect agentteams-manager --format '{{range .Config.Env}}{{println .}}{{end}}' 2>/dev/null | grep '^AGENTTEAMS_MANAGER_PASSWORD=' | cut -d= -f2-)\n if [ -z \"${_mgr_pw}\" ] && ${DOCKER_CMD} ps --format '{{.Names}}' 2>/dev/null | grep -q '^agentteams-manager$'; then\n _mgr_pw=$(${DOCKER_CMD} exec agentteams-manager bash -c 'source /data/agentteams-secrets.env 2>/dev/null && echo \"${AGENTTEAMS_MANAGER_PASSWORD}\"' 2>/dev/null)\n fi\n if [ -z \"${_mgr_pw}\" ] && ${DOCKER_CMD} volume ls -q 2>/dev/null | grep -q \"^${AGENTTEAMS_DATA_DIR}$\"; then\n _mgr_pw=$(agentteams_read_secret_from_data_volume \"${AGENTTEAMS_DATA_DIR}\" AGENTTEAMS_MANAGER_PASSWORD)\n fi\n\n # Manager admin DM room ID: login as admin, find DM room with @manager\n _mgr_room=\"\"\n if [ -n \"${_mgr_pw}\" ]; then\n _admin_pw=$(grep AGENTTEAMS_ADMIN_PASSWORD \"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\" 2>/dev/null | cut -d= -f2-)\n _admin_user=$(grep AGENTTEAMS_ADMIN_USER \"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\" 2>/dev/null | cut -d= -f2-)\n _admin_user=\"${_admin_user:-admin}\"\n _matrix_domain=$(grep AGENTTEAMS_MATRIX_DOMAIN \"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\" 2>/dev/null | cut -d= -f2-)\n if [ -n \"${_admin_pw}\" ] && ${DOCKER_CMD} ps --format '{{.Names}}' 2>/dev/null | grep -q '^agentteams-manager$'; then\n _admin_token=$(${DOCKER_CMD} exec agentteams-manager curl -sf -X POST http://127.0.0.1:6167/_matrix/client/v3/login \\\n -H \"Content-Type: application/json\" \\\n -d '{\"type\":\"m.login.password\",\"identifier\":{\"type\":\"m.id.user\",\"user\":\"'\"${_admin_user}\"'\"},\"password\":\"'\"${_admin_pw}\"'\"}' 2>/dev/null | python3 -c \"import sys,json; print(json.load(sys.stdin).get('access_token',''))\" 2>/dev/null || true)\n if [ -n \"${_admin_token}\" ]; then\n _mgr_room=$(${DOCKER_CMD} exec agentteams-manager curl -sf -X GET \\\n -H \"Authorization: Bearer ${_admin_token}\" \\\n \"http://127.0.0.1:6167/_matrix/client/v3/joined_rooms\" 2>/dev/null | python3 -c \"\nimport sys,json,subprocess\nrooms = json.load(sys.stdin).get('joined_rooms',[])\nfor room_id in rooms:\n enc = room_id.replace('!','%21')\n members = json.loads(subprocess.check_output([\n 'docker','exec','agentteams-manager','curl','-sf','-X','GET',\n '-H','Authorization: Bearer ${_admin_token}',\n 'http://127.0.0.1:6167/_matrix/client/v3/rooms/'+enc+'/members'\n ]).decode()).get('chunk',[])\n member_ids = [m['state_key'] for m in members]\n if any('manager' in m and 'admin' not in m.split(':')[0] for m in member_ids):\n if len(member_ids) <= 3:\n print(room_id)\n break\n\" 2>/dev/null || true)\n fi\n fi\n if [ -z \"${_mgr_room}\" ]; then\n _mgr_room=$(agentteams_read_admin_dm_room_from_workspace \"${AGENTTEAMS_WORKSPACE_DIR}\")\n fi\n cat > \"${_creds_tmp}/default.env\" <<CREDEOF\nWORKER_PASSWORD=\"${_mgr_pw}\"\nWORKER_MINIO_PASSWORD=\"$(openssl rand -hex 24)\"\nWORKER_GATEWAY_KEY=\"${AGENTTEAMS_MANAGER_GATEWAY_KEY}\"\nWORKER_ROOM_ID=\"${_mgr_room}\"\nCREDEOF\n log \"Extracted Manager Matrix password${_mgr_room:+ and room ID}\"\n fi\n\n # Worker passwords and room IDs from workers-registry.json\n if [ -f \"${AGENTTEAMS_WORKSPACE_DIR}/workers-registry.json\" ]; then\n _worker_names=$(python3 -c \"import json; d=json.load(open('${AGENTTEAMS_WORKSPACE_DIR}/workers-registry.json')); print(' '.join(d.get('workers',{}).keys()))\" 2>/dev/null || true)\n for _wname in ${_worker_names}; do\n _wpw=\"\"\n if ${DOCKER_CMD} ps --format '{{.Names}}' 2>/dev/null | grep -q '^agentteams-manager$'; then\n _wpw=$(${DOCKER_CMD} exec agentteams-manager cat \"/root/agentteams-fs/agents/${_wname}/credentials/matrix/password\" 2>/dev/null || true)\n fi\n if [ -z \"${_wpw}\" ] && ${DOCKER_CMD} volume ls -q 2>/dev/null | grep -q \"^${AGENTTEAMS_DATA_DIR}$\"; then\n _wpw=$(agentteams_read_worker_creds_value_from_volume \"${AGENTTEAMS_DATA_DIR}\" \"${_wname}\" WORKER_PASSWORD)\n fi\n _wroom=$(python3 -c \"import json; d=json.load(open('${AGENTTEAMS_WORKSPACE_DIR}/workers-registry.json')); print(d.get('workers',{}).get('${_wname}',{}).get('room_id',''))\" 2>/dev/null || true)\n if [ -z \"${_wroom}\" ] && ${DOCKER_CMD} volume ls -q 2>/dev/null | grep -q \"^${AGENTTEAMS_DATA_DIR}$\"; then\n _wroom=$(agentteams_read_worker_creds_value_from_volume \"${AGENTTEAMS_DATA_DIR}\" \"${_wname}\" WORKER_ROOM_ID)\n fi\n if [ -n \"${_wpw}\" ]; then\n cat > \"${_creds_tmp}/${_wname}.env\" <<CREDEOF\nWORKER_PASSWORD=\"${_wpw}\"\nWORKER_MINIO_PASSWORD=\"$(openssl rand -hex 24)\"\nWORKER_GATEWAY_KEY=\"$(openssl rand -hex 32)\"\nWORKER_ROOM_ID=\"${_wroom}\"\nCREDEOF\n log \"Extracted ${_wname} Matrix password${_wroom:+ and room ID}\"\n fi\n done\n fi\n\n if [ \"${_mgr_creds_tempstart}\" = \"1\" ]; then\n log \"Stopping agentteams-manager after credential extraction (upgrade will recreate containers)...\"\n ${DOCKER_CMD} stop agentteams-manager 2>/dev/null || true\n fi\n fi # _is_old_arch\n fi\n\n # --- Stop and remove existing containers ---\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' | grep -q \"^agentteams-controller$\"; then\n ${DOCKER_CMD} stop agentteams-controller 2>/dev/null || true\n ${DOCKER_CMD} rm agentteams-controller 2>/dev/null || true\n fi\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' | grep -q \"^agentteams-manager$\"; then\n log \"$(msg install.removing_existing)\"\n ${DOCKER_CMD} stop agentteams-manager 2>/dev/null || true\n ${DOCKER_CMD} rm agentteams-manager 2>/dev/null || true\n fi\n\n # Stop and remove worker containers (controller will recreate via CR reconciliation)\n if [ -n \"${UPGRADE_EXISTING_WORKERS:-}\" ]; then\n log \"$(msg install.existing.stopping_workers)\"\n for w in ${UPGRADE_EXISTING_WORKERS}; do\n ${DOCKER_CMD} stop \"${w}\" 2>/dev/null || true\n ${DOCKER_CMD} rm \"${w}\" 2>/dev/null || true\n log \"$(msg install.existing.removed \"${w}\")\"\n done\n fi\n\n # Clean up legacy containers from v1.0.x and earlier\n for _legacy_name in agentteams-docker-proxy; do\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -qx \"${_legacy_name}\"; then\n log \"Removing legacy container: ${_legacy_name}\"\n ${DOCKER_CMD} stop \"${_legacy_name}\" 2>/dev/null || true\n ${DOCKER_CMD} rm -f \"${_legacy_name}\" 2>/dev/null || true\n fi\n done\n\n # --- Upgrade: inject extracted credentials into data volume ---\n # Only needed for old-arch upgrades (credential files were extracted above).\n if [ -n \"${_creds_tmp}\" ] && [ -d \"${_creds_tmp}\" ] && [ -n \"$(ls -A \"${_creds_tmp}\" 2>/dev/null)\" ]; then\n local _cleanup_ctr=\"agentteams-upgrade-cleanup\"\n ${DOCKER_CMD} rm -f \"${_cleanup_ctr}\" 2>/dev/null || true\n ${DOCKER_CMD} run --rm --name \"${_cleanup_ctr}\" \\\n --entrypoint sh \\\n -v \"${AGENTTEAMS_DATA_DIR}:/data\" \\\n -v \"${_creds_tmp}:/creds:ro\" \\\n \"${EMBEDDED_IMAGE}\" -c '\n rm -rf /data/worker-creds\n mkdir -p /data/worker-creds\n cp /creds/*.env /data/worker-creds/ 2>/dev/null || true\n chmod 600 /data/worker-creds/*.env 2>/dev/null || true\n ' 2>/dev/null && log \"Injected credentials for upgrade\" || log \"Warning: credential injection failed, continuing\"\n rm -rf \"${_creds_tmp}\"\n fi\n\n # --- Start containers ---\n log \"$(msg install.starting_manager)\"\n\n # Build port binding args\n if [ \"${AGENTTEAMS_LOCAL_ONLY:-1}\" = \"1\" ]; then\n _port_prefix=\"127.0.0.1:\"\n else\n _port_prefix=\"\"\n fi\n\n # Ensure agentteams-net Docker network exists\n ${DOCKER_CMD} network inspect agentteams-net >/dev/null 2>&1 || ${DOCKER_CMD} network create agentteams-net\n\n if [ \"${AGENTTEAMS_USE_EMBEDDED}\" != \"1\" ] && [ \"${AGENTTEAMS_UPGRADE:-0}\" = \"1\" ]; then\n # Check if current installation is embedded — downgrade to legacy is not supported\n if ${DOCKER_CMD} ps -a --format '{{.Names}} {{.Image}}' 2>/dev/null | grep \"^agentteams-controller \" | grep -q \"embedded\"; then\n error \"Downgrade from embedded architecture to legacy version (${AGENTTEAMS_VERSION}) is not supported.\"\n error \"Please use 'make uninstall-embedded' first, then do a clean install of the target version.\"\n exit 1\n fi\n fi\n\n if [ \"${AGENTTEAMS_USE_EMBEDDED}\" = \"1\" ]; then\n # ============================================================\n # New architecture: embedded controller + auto-created manager\n # ============================================================\n\n # Internal port: 8080 (Higress gateway inside the container).\n local _internal_gw_port=8080\n local _matrix_domain=\"${AGENTTEAMS_MATRIX_DOMAIN:-matrix-local.agentteams.io:${AGENTTEAMS_PORT_GATEWAY}}\"\n local _aigw_domain=\"${AGENTTEAMS_AI_GATEWAY_DOMAIN:-aigw-local.agentteams.io}\"\n # Ensure internal gateway port is present (container-internal traffic uses 8080)\n case \"${_aigw_domain}\" in *:*) ;; *) _aigw_domain=\"${_aigw_domain}:${_internal_gw_port}\" ;; esac\n local _fs_domain=\"${AGENTTEAMS_FS_DOMAIN:-fs-local.agentteams.io}\"\n case \"${_fs_domain}\" in *:*) ;; *) _fs_domain=\"${_fs_domain}:${_internal_gw_port}\" ;; esac\n\n # Controller env args\n local _ctrl_env_prefix\n _ctrl_env_prefix=\"$(_controller_env_prefix \"${AGENTTEAMS_VERSION}\")\"\n local _storage_prefix\n _storage_prefix=\"$(_controller_storage_prefix \"${AGENTTEAMS_VERSION}\")\"\n local _ctrl_env_args=(\n -e \"${_ctrl_env_prefix}ADMIN_USER=${AGENTTEAMS_ADMIN_USER}\"\n -e \"${_ctrl_env_prefix}ADMIN_PASSWORD=${AGENTTEAMS_ADMIN_PASSWORD}\"\n -e \"${_ctrl_env_prefix}MANAGER_PASSWORD=${AGENTTEAMS_MANAGER_PASSWORD}\"\n -e \"${_ctrl_env_prefix}REGISTRATION_TOKEN=${AGENTTEAMS_REGISTRATION_TOKEN}\"\n -e \"${_ctrl_env_prefix}MINIO_USER=${AGENTTEAMS_MINIO_USER}\"\n -e \"${_ctrl_env_prefix}MINIO_PASSWORD=${AGENTTEAMS_MINIO_PASSWORD}\"\n -e \"${_ctrl_env_prefix}LLM_PROVIDER=${AGENTTEAMS_LLM_PROVIDER}\"\n -e \"${_ctrl_env_prefix}LLM_API_KEY=${AGENTTEAMS_LLM_API_KEY}\"\n -e \"${_ctrl_env_prefix}DEFAULT_MODEL=${AGENTTEAMS_DEFAULT_MODEL}\"\n -e \"${_ctrl_env_prefix}MANAGER_GATEWAY_KEY=${AGENTTEAMS_MANAGER_GATEWAY_KEY}\"\n -e \"${_ctrl_env_prefix}MANAGER_RUNTIME=${AGENTTEAMS_MANAGER_RUNTIME:-copaw}\"\n -e \"${_ctrl_env_prefix}MANAGER_IMAGE=$([ \"${AGENTTEAMS_MANAGER_RUNTIME}\" = \"copaw\" ] && echo \"${MANAGER_COPAW_IMAGE}\" || echo \"${MANAGER_IMAGE}\")\"\n -e \"${_ctrl_env_prefix}DEFAULT_WORKER_RUNTIME=${AGENTTEAMS_DEFAULT_WORKER_RUNTIME:-copaw}\"\n -e \"${_ctrl_env_prefix}WORKER_IMAGE=${WORKER_IMAGE}\"\n -e \"${_ctrl_env_prefix}COPAW_WORKER_IMAGE=${COPAW_WORKER_IMAGE}\"\n -e \"${_ctrl_env_prefix}QWENPAW_WORKER_IMAGE=${QWENPAW_WORKER_IMAGE}\"\n -e \"${_ctrl_env_prefix}HERMES_WORKER_IMAGE=${HERMES_WORKER_IMAGE}\"\n -e \"${_ctrl_env_prefix}MATRIX_DOMAIN=${_matrix_domain}\"\n -e \"${_ctrl_env_prefix}ELEMENT_HOMESERVER_URL=http://127.0.0.1:${AGENTTEAMS_PORT_GATEWAY}\"\n -e \"${_ctrl_env_prefix}MATRIX_URL=http://127.0.0.1:6167\"\n -e \"${_ctrl_env_prefix}MATRIX_E2EE=${AGENTTEAMS_MATRIX_E2EE:-0}\"\n -e \"${_ctrl_env_prefix}MINIO_ENDPOINT=http://127.0.0.1:9000\"\n -e \"${_ctrl_env_prefix}MINIO_BUCKET=agentteams-storage\"\n -e \"${_ctrl_env_prefix}STORAGE_PREFIX=${_storage_prefix}\"\n -e \"${_ctrl_env_prefix}FS_ENDPOINT=http://127.0.0.1:9000\"\n -e \"${_ctrl_env_prefix}AI_GATEWAY_URL=http://${_aigw_domain}\"\n -e \"${_ctrl_env_prefix}CONTROLLER_URL=http://agentteams-controller:8090\"\n -e \"${_ctrl_env_prefix}DOCKER_NETWORK=agentteams-net\"\n -e \"${_ctrl_env_prefix}WORKSPACE_DIR=${AGENTTEAMS_WORKSPACE_DIR}\"\n -e \"${_ctrl_env_prefix}HOST_SHARE_DIR=${AGENTTEAMS_HOST_SHARE_DIR}\"\n -e \"${_ctrl_env_prefix}MANAGER_ENABLED=true\"\n -e \"${_ctrl_env_prefix}PORT_MANAGER_CONSOLE=${AGENTTEAMS_PORT_MANAGER_CONSOLE:-18888}\"\n )\n if _use_legacy_image_env \"${AGENTTEAMS_VERSION}\"; then\n _ctrl_env_args+=(\n -e \"${_ctrl_env_prefix}FS_BUCKET=agentteams-storage\"\n -e \"${_ctrl_env_prefix}RESOURCE_PREFIX=agentteams-\"\n )\n else\n _ctrl_env_args+=(\n -e \"${_ctrl_env_prefix}MATRIX_APPSERVICE_ENABLED=${AGENTTEAMS_MATRIX_APPSERVICE_ENABLED:-true}\"\n -e \"${_ctrl_env_prefix}MATRIX_APPSERVICE_AS_TOKEN=${AGENTTEAMS_MATRIX_APPSERVICE_AS_TOKEN:-}\"\n -e \"${_ctrl_env_prefix}MATRIX_APPSERVICE_HS_TOKEN=${AGENTTEAMS_MATRIX_APPSERVICE_HS_TOKEN:-}\"\n )\n fi\n\n # Timezone\n if [ -n \"${AGENTTEAMS_TIMEZONE:-}\" ]; then\n _ctrl_env_args+=(-e \"TZ=${AGENTTEAMS_TIMEZONE}\")\n fi\n\n # Yolo mode\n if [ \"${AGENTTEAMS_YOLO:-}\" = \"1\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}YOLO=1\")\n fi\n\n # Matrix-plugin debug tracing — propagated to every manager + worker\n # the controller spawns, then translated to OPENCLAW_MATRIX_DEBUG=1\n # by the container entrypoints. Use this to diagnose\n # \"worker did not join\" / \"manager replied empty\" hangs.\n if [ \"${AGENTTEAMS_MATRIX_DEBUG:-}\" = \"1\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}MATRIX_DEBUG=1\")\n fi\n\n # Optional: GitHub token\n if [ -n \"${AGENTTEAMS_GITHUB_TOKEN:-}\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}GITHUB_TOKEN=${AGENTTEAMS_GITHUB_TOKEN}\")\n fi\n\n # Optional: embedding model\n if [ -n \"${AGENTTEAMS_EMBEDDING_MODEL:-}\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}EMBEDDING_MODEL=${AGENTTEAMS_EMBEDDING_MODEL}\")\n fi\n\n # Optional: custom model capability overrides (context window, max\n # tokens, vision, reasoning). These are read by the Controller's\n # config.go LoadConfig to annotate custom models not in the built-in\n # presets table.\n if [ -n \"${AGENTTEAMS_MODEL_CONTEXT_WINDOW:-}\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}MODEL_CONTEXT_WINDOW=${AGENTTEAMS_MODEL_CONTEXT_WINDOW}\")\n fi\n if [ -n \"${AGENTTEAMS_MODEL_MAX_TOKENS:-}\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}MODEL_MAX_TOKENS=${AGENTTEAMS_MODEL_MAX_TOKENS}\")\n fi\n if [ -n \"${AGENTTEAMS_MODEL_VISION:-}\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}MODEL_VISION=${AGENTTEAMS_MODEL_VISION}\")\n fi\n if [ -n \"${AGENTTEAMS_MODEL_REASONING:-}\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}MODEL_REASONING=${AGENTTEAMS_MODEL_REASONING}\")\n fi\n\n # Optional: OpenAI-compatible base URL\n if [ -n \"${AGENTTEAMS_OPENAI_BASE_URL:-}\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}OPENAI_BASE_URL=${AGENTTEAMS_OPENAI_BASE_URL}\")\n fi\n # Optional: language\n if [ -n \"${AGENTTEAMS_LANGUAGE:-}\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}LANGUAGE=${AGENTTEAMS_LANGUAGE}\")\n fi\n\n # Optional: CMS/ARMS observability. In embedded mode the controller\n # spawns the Manager and Workers, so it must receive these settings.\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}CMS_TRACES_ENABLED=${AGENTTEAMS_CMS_TRACES_ENABLED:-false}\")\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}CMS_SERVICE_NAME=${AGENTTEAMS_CMS_SERVICE_NAME:-agentteams-manager}\")\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}CMS_METRICS_ENABLED=${AGENTTEAMS_CMS_METRICS_ENABLED:-false}\")\n if [ -n \"${AGENTTEAMS_CMS_ENDPOINT:-}\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}CMS_ENDPOINT=${AGENTTEAMS_CMS_ENDPOINT}\")\n fi\n if [ -n \"${AGENTTEAMS_CMS_LICENSE_KEY:-}\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}CMS_LICENSE_KEY=${AGENTTEAMS_CMS_LICENSE_KEY}\")\n fi\n if [ -n \"${AGENTTEAMS_CMS_PROJECT:-}\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}CMS_PROJECT=${AGENTTEAMS_CMS_PROJECT}\")\n fi\n if [ -n \"${AGENTTEAMS_CMS_WORKSPACE:-}\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}CMS_WORKSPACE=${AGENTTEAMS_CMS_WORKSPACE}\")\n fi\n\n # shellcheck disable=SC2086\n ${DOCKER_CMD} run -d \\\n --name agentteams-controller \\\n --network agentteams-net \\\n --network-alias matrix-local.agentteams.io \\\n --network-alias aigw-local.agentteams.io \\\n --network-alias fs-local.agentteams.io \\\n \"${_ctrl_env_args[@]}\" \\\n -v \"${CONTAINER_SOCK}:/var/run/docker.sock\" \\\n --security-opt label=disable \\\n -v \"${AGENTTEAMS_DATA_DIR}:/data\" \\\n -v \"${AGENTTEAMS_WORKSPACE_DIR}:/root/agentteams-fs/agents/manager\" \\\n -p \"${_port_prefix}${AGENTTEAMS_PORT_GATEWAY}:8080\" \\\n -p \"${_port_prefix}${AGENTTEAMS_PORT_CONSOLE}:8001\" \\\n -p \"${_port_prefix}${AGENTTEAMS_PORT_ELEMENT_WEB:-18088}:8088\" \\\n --restart unless-stopped \\\n \"${EMBEDDED_IMAGE}\"\n\n log \"Embedded controller started: agentteams-controller\"\n\n # Wait for infrastructure inside the controller container\n _wait_for_url() {\n local url=\"$1\" ctr=\"$2\" max_wait=\"${3:-120}\" desc=\"${4:-service}\"\n local elapsed=0\n log \"Waiting for ${desc}...\"\n while [ $elapsed -lt $max_wait ]; do\n if ${DOCKER_CMD} exec \"${ctr}\" curl -sf \"${url}\" >/dev/null 2>&1; then\n log \"${desc} is ready (${elapsed}s)\"\n return 0\n fi\n sleep 2\n elapsed=$((elapsed + 2))\n done\n log \"ERROR: ${desc} not ready after ${max_wait}s\"\n return 1\n }\n\n _wait_for_url \"http://127.0.0.1:6167/_tuwunel/server_version\" agentteams-controller 120 \"Tuwunel (Matrix)\" || exit 1\n _wait_for_url \"http://127.0.0.1:9000/minio/health/live\" agentteams-controller 60 \"MinIO\" || exit 1\n _wait_for_url \"http://127.0.0.1:8080/status\" agentteams-controller 120 \"Higress Gateway\" || exit 1\n\n # Wait for controller to create Manager Agent container\n log \"Waiting for Manager Agent container...\"\n local _mgr_wait=0\n local _mgr_max=300\n while [ $_mgr_wait -lt $_mgr_max ]; do\n if ${DOCKER_CMD} ps --format '{{.Names}}' 2>/dev/null | grep -q \"^agentteams-manager$\"; then\n log \"Manager Agent container detected (${_mgr_wait}s)\"\n break\n fi\n sleep 3\n _mgr_wait=$((_mgr_wait + 3))\n done\n if [ $_mgr_wait -ge $_mgr_max ]; then\n log \"ERROR: Manager Agent container not created after ${_mgr_max}s\"\n log \"Controller logs:\"\n ${DOCKER_CMD} exec agentteams-controller tail -30 /var/log/agentteams/agentteams-controller-error.log 2>/dev/null || true\n exit 1\n fi\n\n # Wait for Manager Agent to be running\n log \"Waiting for Manager Agent to start...\"\n local _agent_wait=0\n while [ $_agent_wait -lt 120 ]; do\n local _state\n _state=$(${DOCKER_CMD} inspect --format '{{.State.Status}}' agentteams-manager 2>/dev/null || echo \"missing\")\n if [ \"${_state}\" = \"running\" ]; then\n log \"Manager Agent is running\"\n break\n fi\n sleep 2\n _agent_wait=$((_agent_wait + 2))\n done\n\n # Enable yolo mode in agent if requested\n if [ \"${AGENTTEAMS_YOLO:-}\" = \"1\" ]; then\n ${DOCKER_CMD} exec agentteams-manager touch /root/manager-workspace/yolo-mode 2>/dev/null || true\n fi\n\n # Wait for the controller to send the first-boot welcome message.\n # The controller gates this on (a) Manager joining the DM room and\n # (b) Higress WASM key-auth propagation actually clearing /v1/chat/completions\n # for the Manager's gateway key — typically ~45-90s on a fresh install.\n # We poll Manager CR Status.WelcomeSent via the in-container agt CLI,\n # exec'd inside agentteams-controller (the source-of-truth container — its\n # bundled CLI binary is always in lockstep with whatever controller\n # binary is currently serving the HTTP API, since they're the same\n # `go build` output. The agentteams-manager container's CLI may lag the\n # controller across image upgrades and silently drop the welcomeSent\n # field, leaving this loop hung). The controller container mints a\n # long-lived admin SA token at startup and writes it to\n # AGENTTEAMS_AUTH_TOKEN_FILE=/var/run/agentteams/cli-token (set as a Dockerfile\n # ENV default), so a bare `docker exec agentteams-controller agt …`\n # auto-discovers both the endpoint and the token. There is a brief\n # window after container start before bootstrapAdminCLIToken completes\n # where the file may be empty / absent — the loop's silent retry\n # handles that the same way it handles the manager-not-yet-running\n # case below.\n if ${DOCKER_CMD} exec agentteams-controller sh -c 'command -v agt' >/dev/null 2>&1; then\n log \"$(msg install.welcome_msg.waiting)\"\n local _welcome_wait=0\n local _welcome_max=\"${AGENTTEAMS_WELCOME_TIMEOUT:-300}\"\n local _welcome_done=0\n while [ $_welcome_wait -lt $_welcome_max ]; do\n # `tr -d` strips whitespace/CR so the grep stays robust to\n # any future change in go-json field ordering or formatting.\n local _wjson\n _wjson=$(${DOCKER_CMD} exec agentteams-controller \\\n agt get managers default -o json 2>/dev/null || true)\n if [ -n \"${_wjson}\" ] && printf '%s' \"${_wjson}\" | tr -d ' \\r\\n' | grep -q '\"welcomeSent\":true'; then\n log \"$(msg install.welcome_msg.confirmed \"${_welcome_wait}\")\"\n _welcome_done=1\n break\n fi\n sleep 3\n _welcome_wait=$((_welcome_wait + 3))\n done\n if [ $_welcome_done -ne 1 ]; then\n # Non-fatal: install is still good. Keep going to the success\n # banner so the admin can use Element Web to nudge Manager into\n # onboarding manually (one DM message is enough).\n log \"$(msg install.welcome_msg.timeout \"${_welcome_max}\")\"\n log \"$(msg install.welcome_msg.timeout_hint)\"\n log \"$(msg install.welcome_msg.timeout_inspect)\"\n fi\n else\n log \"$(msg install.welcome_msg.poll_unavailable)\"\n fi\n\n else\n # ============================================================\n # Legacy architecture: all-in-one manager container\n # ============================================================\n\n NETWORK_ARGS=\"--network agentteams-net\"\n NETWORK_ALIAS_ARGS=\"--network-alias matrix-local.agentteams.io --network-alias aigw-local.agentteams.io --network-alias fs-local.agentteams.io\"\n for _domain in \"${AGENTTEAMS_MATRIX_CLIENT_DOMAIN:-}\" \"${AGENTTEAMS_CONSOLE_DOMAIN:-}\"; do\n if [ -n \"${_domain}\" ] && [[ \"${_domain}\" == *-local.agentteams.io ]]; then\n NETWORK_ALIAS_ARGS=\"${NETWORK_ALIAS_ARGS} --network-alias ${_domain}\"\n fi\n done\n\n # Start Docker API proxy if enabled (security layer between Manager and Docker daemon)\n PROXY_ARGS=\"\"\n if [ \"${AGENTTEAMS_DOCKER_PROXY:-1}\" = \"1\" ] && [ -n \"${CONTAINER_SOCK:-}\" ]; then\n local _proxy_image=\"${AGENTTEAMS_REGISTRY}/higress/agentteams-docker-proxy:${AGENTTEAMS_VERSION}\"\n # Try versioned tag, fallback to latest\n if ! ${DOCKER_CMD} image inspect \"${_proxy_image}\" >/dev/null 2>&1; then\n ${DOCKER_CMD} pull \"${_proxy_image}\" 2>/dev/null || {\n _proxy_image=\"${AGENTTEAMS_REGISTRY}/higress/agentteams-docker-proxy:latest\"\n ${DOCKER_CMD} pull \"${_proxy_image}\" 2>/dev/null || true\n }\n fi\n if ${DOCKER_CMD} image inspect \"${_proxy_image}\" >/dev/null 2>&1; then\n log \"Starting Docker API proxy...\"\n ${DOCKER_CMD} run -d \\\n --name agentteams-docker-proxy \\\n --network agentteams-net \\\n -v \"${CONTAINER_SOCK}:/var/run/docker.sock\" \\\n --security-opt label=disable \\\n -e AGENTTEAMS_WORKER_IMAGE=\"${WORKER_IMAGE}\" \\\n -e AGENTTEAMS_COPAW_WORKER_IMAGE=\"${COPAW_WORKER_IMAGE}\" \\\n -e AGENTTEAMS_QWENPAW_WORKER_IMAGE=\"${QWENPAW_WORKER_IMAGE}\" \\\n -e AGENTTEAMS_HERMES_WORKER_IMAGE=\"${HERMES_WORKER_IMAGE}\" \\\n ${AGENTTEAMS_PROXY_ALLOWED_REGISTRIES:+-e AGENTTEAMS_PROXY_ALLOWED_REGISTRIES=\"${AGENTTEAMS_PROXY_ALLOWED_REGISTRIES}\"} \\\n --restart unless-stopped \\\n \"${_proxy_image}\"\n PROXY_ARGS=\"-e AGENTTEAMS_CONTROLLER_URL=http://agentteams-docker-proxy:2375 -e AGENTTEAMS_CONTAINER_API=http://agentteams-docker-proxy:2375\"\n SOCKET_MOUNT_ARGS=\"\"\n fi\n fi\n\n # Pass host timezone to container\n TZ_ARGS=\"\"\n if [ -n \"${AGENTTEAMS_TIMEZONE:-}\" ]; then\n TZ_ARGS=\"-e TZ=${AGENTTEAMS_TIMEZONE}\"\n fi\n\n YOLO_ARGS=\"\"\n if [ \"${AGENTTEAMS_YOLO:-}\" = \"1\" ]; then\n YOLO_ARGS=\"-e AGENTTEAMS_YOLO=1\"\n fi\n\n MATRIX_DEBUG_ARGS=\"\"\n if [ \"${AGENTTEAMS_MATRIX_DEBUG:-}\" = \"1\" ]; then\n MATRIX_DEBUG_ARGS=\"-e AGENTTEAMS_MATRIX_DEBUG=1\"\n fi\n\n # shellcheck disable=SC2086\n ${DOCKER_CMD} run -d \\\n --name agentteams-manager \\\n --env-file \"${ENV_FILE}\" \\\n -e HOME=/root/manager-workspace \\\n -w /root/manager-workspace \\\n -e HOST_ORIGINAL_HOME=\"${AGENTTEAMS_HOST_SHARE_DIR}\" \\\n -e AGENTTEAMS_MANAGER_RUNTIME=\"${AGENTTEAMS_MANAGER_RUNTIME:-copaw}\" \\\n ${JVM_ARGS:+-e JVM_ARGS=\"${JVM_ARGS}\"} \\\n ${YOLO_ARGS} \\\n ${MATRIX_DEBUG_ARGS} \\\n ${TZ_ARGS} \\\n ${SOCKET_MOUNT_ARGS} \\\n ${NETWORK_ARGS} \\\n ${NETWORK_ALIAS_ARGS} \\\n ${PROXY_ARGS} \\\n -p \"${_port_prefix}${AGENTTEAMS_PORT_GATEWAY}:8080\" \\\n -p \"${_port_prefix}${AGENTTEAMS_PORT_CONSOLE}:8001\" \\\n -p \"${_port_prefix}${AGENTTEAMS_PORT_ELEMENT_WEB:-18088}:8088\" \\\n -p \"127.0.0.1:${AGENTTEAMS_PORT_MANAGER_CONSOLE:-18888}:18888\" \\\n ${DATA_MOUNT_ARGS} \\\n ${WORKSPACE_MOUNT_ARGS} \\\n ${HOST_SHARE_MOUNT_ARGS} \\\n --restart unless-stopped \\\n \"$([ \"${AGENTTEAMS_MANAGER_RUNTIME}\" = \"copaw\" ] && echo \"${MANAGER_COPAW_IMAGE}\" || echo \"${MANAGER_IMAGE}\")\"\n\n # Wait for Manager agent to be ready\n wait_manager_ready \"agentteams-manager\"\n\n # Wait for Matrix server to be ready\n wait_matrix_ready \"agentteams-manager\"\n\n # Post-install verification (non-fatal: warnings only)\n local _verify_script\n _verify_script=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)/agentteams-verify.sh\"\n if [ -f \"${_verify_script}\" ]; then\n bash \"${_verify_script}\" \"agentteams-manager\" || {\n log \"WARNING: Some post-install checks failed. Re-run: bash install/agentteams-verify.sh\"\n }\n fi\n fi\n unset _port_prefix\n\n # ── Start Dashboard (optional) ────────────────────────────────────\n _start_dashboard\n\n # Apply Podman autostart if selected and environment matches\n if [ \"${DOCKER_CMD}\" = \"podman\" ] && [ \"${AGENTTEAMS_PODMAN_AUTOSTART:-0}\" = \"1\" ]; then\n setup_podman_autostart\n fi\n\n log \"\"\n log \"$(msg success.title)\"\n log \"\"\n log \"$(msg success.domains_configured)\"\n log \" ${AGENTTEAMS_MATRIX_DOMAIN%%:*} ${AGENTTEAMS_MATRIX_CLIENT_DOMAIN} ${AGENTTEAMS_AI_GATEWAY_DOMAIN} ${AGENTTEAMS_FS_DOMAIN} ${AGENTTEAMS_CONSOLE_DOMAIN}\"\n log \"\"\n local lan_ip\n lan_ip=$(detect_lan_ip)\n echo -e \"\\033[33m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\033[0m\"\n echo -e \"\\033[33m $(msg success.open_url)\\033[0m\"\n echo -e \"\\033[33m \\033[0m\"\n echo -e \"\\033[1;36m http://127.0.0.1:${AGENTTEAMS_PORT_ELEMENT_WEB:-18088}/#/login\\033[0m\"\n echo -e \"\\033[33m \\033[0m\"\n echo -e \"\\033[33m $(msg success.login_with)\\033[0m\"\n echo -e \"\\033[33m $(msg success.username \"${AGENTTEAMS_ADMIN_USER}\")\\033[0m\"\n echo -e \"\\033[33m $(msg success.password \"${AGENTTEAMS_ADMIN_PASSWORD}\")\\033[0m\"\n echo -e \"\\033[33m \\033[0m\"\n echo -e \"\\033[33m $(msg success.after_login)\\033[0m\"\n echo -e \"\\033[33m $(msg success.tell_it)\\033[0m\"\n echo -e \"\\033[33m $(msg success.manager_auto)\\033[0m\"\n echo -e \"\\033[33m \\033[0m\"\n echo -e \"\\033[33m ───────────────────────────────────────────────────────────────────────────── \\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_title)\\033[0m\"\n echo -e \"\\033[33m \\033[0m\"\n if [ -n \"${lan_ip}\" ]; then\n echo -e \"\\033[33m $(msg success.mobile_step1)\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_step2 \"http://${lan_ip}:${AGENTTEAMS_PORT_GATEWAY}\")\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_step3)\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_username \"${AGENTTEAMS_ADMIN_USER}\")\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_password \"${AGENTTEAMS_ADMIN_PASSWORD}\")\\033[0m\"\n else\n echo -e \"\\033[33m $(msg success.mobile_step1)\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_step2_noip \"${AGENTTEAMS_PORT_GATEWAY}\")\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_noip_hint)\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_step3)\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_username \"${AGENTTEAMS_ADMIN_USER}\")\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_password \"${AGENTTEAMS_ADMIN_PASSWORD}\")\\033[0m\"\n fi\n echo -e \"\\033[33m \\033[0m\"\n echo -e \"\\033[33m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\033[0m\"\n log \"\"\n log \"$(msg success.other_consoles)\"\n log \"$(msg success.higress_console \"${AGENTTEAMS_PORT_CONSOLE}\" \"${AGENTTEAMS_ADMIN_USER}\" \"${AGENTTEAMS_ADMIN_PASSWORD}\")\"\n if [ \"${AGENTTEAMS_DASHBOARD:-0}\" = \"1\" ]; then\n log \"$(msg success.dashboard \"${AGENTTEAMS_PORT_DASHBOARD:-13000}\")\"\n fi\n if [ \"${AGENTTEAMS_USE_EMBEDDED}\" != \"1\" ]; then\n log \"$(msg success.manager_console \"${AGENTTEAMS_PORT_MANAGER_CONSOLE:-18888}\")\"\n log \"$(msg success.manager_console_gateway \"${AGENTTEAMS_ADMIN_USER}\" \"${AGENTTEAMS_ADMIN_PASSWORD}\")\"\n fi\n log \"\"\n log \"$(msg success.switch_llm.title)\"\n log \"$(msg success.switch_llm.hint)\"\n log \"$(msg success.switch_llm.docs)\"\n log \"$(msg success.switch_llm.url)\"\n log \"\"\n log \"$(msg success.tip)\"\n log \"\"\n if [ \"${AGENTTEAMS_LOCAL_ONLY:-1}\" != \"1\" ]; then\n echo -e \"\\033[33m$(msg port.local_only.https_hint)\\033[0m\"\n log \"\"\n fi\n log \"$(msg success.config_file \"${ENV_FILE}\")\"\n log \"$(msg success.data_volume \"${AGENTTEAMS_DATA_DIR}\")\"\n log \"$(msg success.workspace \"${AGENTTEAMS_WORKSPACE_DIR}\")\"\n}\n\n# ============================================================\n# Worker Installation (One-Click)\n# ============================================================\n\ninstall_worker() {\n local WORKER_NAME=\"\"\n local FS=\"\"\n local FS_KEY=\"\"\n local FS_SECRET=\"\"\n local RESET=false\n local SKILLS_API_URL=\"\"\n\n # Parse arguments\n while [ $# -gt 0 ]; do\n case $1 in\n --name) WORKER_NAME=\"$2\"; shift 2 ;;\n --fs) FS=\"$2\"; shift 2 ;;\n --fs-key) FS_KEY=\"$2\"; shift 2 ;;\n --fs-secret) FS_SECRET=\"$2\"; shift 2 ;;\n --skills-api-url) SKILLS_API_URL=\"$2\"; shift 2 ;;\n --reset) RESET=true; shift ;;\n *) die \"$(msg error.unknown_option \"$1\")\" ;;\n esac\n done\n\n # Validate required params\n [ -z \"${WORKER_NAME}\" ] && die \"$(msg error.name_required)\"\n [ -z \"${FS}\" ] && die \"$(msg error.fs_required)\"\n [ -z \"${FS_KEY}\" ] && die \"$(msg error.fs_key_required)\"\n [ -z \"${FS_SECRET}\" ] && die \"$(msg error.fs_secret_required)\"\n\n local CONTAINER_NAME=\"agentteams-worker-${WORKER_NAME}\"\n\n # Handle reset\n if [ \"${RESET}\" = true ]; then\n log \"$(msg worker.resetting \"${WORKER_NAME}\")\"\n ${DOCKER_CMD} stop \"${CONTAINER_NAME}\" 2>/dev/null || true\n ${DOCKER_CMD} rm \"${CONTAINER_NAME}\" 2>/dev/null || true\n fi\n\n # Check for existing container\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' | grep -q \"^${CONTAINER_NAME}$\"; then\n die \"$(msg worker.exists \"${CONTAINER_NAME}\")\"\n fi\n\n log \"$(msg worker.starting \"${WORKER_NAME}\")\"\n\n # Build docker run args\n local DOCKER_ENV=\"\"\n DOCKER_ENV=\"${DOCKER_ENV} -e HOME=/root/agentteams-fs/agents/${WORKER_NAME}\"\n DOCKER_ENV=\"${DOCKER_ENV} -w /root/agentteams-fs/agents/${WORKER_NAME}\"\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_WORKER_NAME=${WORKER_NAME}\"\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_FS_ENDPOINT=${FS}\"\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_FS_ACCESS_KEY=${FS_KEY}\"\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_FS_SECRET_KEY=${FS_SECRET}\"\n\n if [ -z \"${SKILLS_API_URL}\" ]; then\n if [ -n \"${AGENTTEAMS_SKILLS_API_URL:-}\" ]; then\n SKILLS_API_URL=\"${AGENTTEAMS_SKILLS_API_URL}\"\n else\n SKILLS_API_URL=\"nacos://market.agentteams.io:80/public\"\n fi\n fi\n\n # Add SKILLS_API_URL if specified\n DOCKER_ENV=\"${DOCKER_ENV} -e SKILLS_API_URL=${SKILLS_API_URL}\"\n log \"$(msg worker.skills_url \"${SKILLS_API_URL}\")\"\n if [ -n \"${AGENTTEAMS_NACOS_USERNAME:-}\" ]; then\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_NACOS_USERNAME=${AGENTTEAMS_NACOS_USERNAME}\"\n fi\n if [ -n \"${AGENTTEAMS_NACOS_PASSWORD:-}\" ]; then\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_NACOS_PASSWORD=${AGENTTEAMS_NACOS_PASSWORD}\"\n fi\n if [ -n \"${AGENTTEAMS_NACOS_TOKEN:-}\" ]; then\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_NACOS_TOKEN=${AGENTTEAMS_NACOS_TOKEN}\"\n fi\n\n # shellcheck disable=SC2086\n ${DOCKER_CMD} run -d \\\n --name \"${CONTAINER_NAME}\" \\\n ${DOCKER_ENV} \\\n --restart unless-stopped \\\n \"${WORKER_IMAGE}\"\n\n log \"\"\n log \"$(msg worker.started \"${WORKER_NAME}\")\"\n log \"$(msg worker.container \"${CONTAINER_NAME}\")\"\n log \"$(msg worker.view_logs \"${CONTAINER_NAME}\")\"\n}\n\n# ============================================================\n# Main\n# ============================================================\n\n# ============================================================\n# LLM API connectivity test\n# ============================================================\n\ntest_llm_connectivity() {\n local base_url=\"$1\"\n local api_key=\"$2\"\n local model=\"$3\"\n local hint=\"${4:-}\" # optional: extra hint shown on failure\n if ! command -v curl >/dev/null 2>&1; then\n echo -e \"\\033[33m$(msg llm.openai.test.no_curl)\\033[0m\"\n return\n fi\n log \"$(msg llm.openai.test.testing)\"\n local _body _http_code _tmpfile\n _tmpfile=$(mktemp)\n _http_code=$(curl -s -o \"${_tmpfile}\" -w \"%{http_code}\" \\\n -X POST \"${base_url%/}/chat/completions\" \\\n -H \"Authorization: Bearer ${api_key}\" \\\n -H \"Content-Type: application/json\" \\\n -H \"User-Agent: AgentTeams/${AGENTTEAMS_VERSION:-latest}\" \\\n --max-time 30 \\\n -d \"{\\\"model\\\":\\\"${model}\\\",\\\"messages\\\":[{\\\"role\\\":\\\"user\\\",\\\"content\\\":\\\"hi\\\"}]}\" \\\n 2>/dev/null)\n _body=$(cat \"${_tmpfile}\")\n rm -f \"${_tmpfile}\"\n if [ \"${_http_code}\" = \"200\" ] || [ \"${_http_code}\" = \"201\" ]; then\n log \"$(msg llm.openai.test.ok)\"\n else\n echo -e \"\\033[33m$(msg llm.openai.test.fail \"${_http_code}\" \"${_body}\")\\033[0m\"\n if [ -n \"${hint}\" ]; then\n echo -e \"\\033[33m${hint}\\033[0m\"\n fi\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" != \"1\" ]; then\n local _confirm\n read -e -p \"$(msg llm.openai.test.confirm)\" _confirm\n if [ \"${_confirm}\" = \"b\" ]; then\n STEP_RESULT=\"back\"\n return 1\n fi\n if [ \"${_confirm}\" != \"y\" ] && [ \"${_confirm}\" != \"Y\" ]; then\n log \"$(msg llm.openai.test.aborted)\"\n exit 1\n fi\n fi\n fi\n}\n\ntest_embedding_connectivity() {\n local base_url=\"$1\"\n local api_key=\"$2\"\n local model=\"$3\"\n if ! command -v curl >/dev/null 2>&1; then\n return 0\n fi\n log \"$(msg llm.embedding.test.testing)\"\n local _body _http_code _tmpfile\n _tmpfile=$(mktemp)\n _http_code=$(curl -s -o \"${_tmpfile}\" -w \"%{http_code}\" \\\n -X POST \"${base_url%/}/embeddings\" \\\n -H \"Authorization: Bearer ${api_key}\" \\\n -H \"Content-Type: application/json\" \\\n -H \"User-Agent: AgentTeams/${AGENTTEAMS_VERSION:-latest}\" \\\n --max-time 30 \\\n -d \"{\\\"model\\\":\\\"${model}\\\",\\\"input\\\":\\\"test\\\"}\" \\\n 2>/dev/null)\n _body=$(cat \"${_tmpfile}\")\n rm -f \"${_tmpfile}\"\n if [ \"${_http_code}\" = \"200\" ] || [ \"${_http_code}\" = \"201\" ]; then\n log \"$(msg llm.embedding.test.ok)\"\n return 0\n else\n echo -e \"\\033[33m$(msg llm.embedding.test.fail \"${_http_code}\" \"${_body}\")\\033[0m\"\n return 1\n fi\n}\n\n# ============================================================\n# Uninstall\n# ============================================================\n\nuninstall_agentteams() {\n log \"$(msg uninstall.title)\"\n\n # Safely disable and remove the dedicated AgentTeams Podman autostart service\n if [ \"${DOCKER_CMD}\" = \"podman\" ] && command -v systemctl >/dev/null 2>&1; then\n local _systemctl_cmd=\"systemctl\"\n local _service_dir=\"/etc/systemd/system\"\n\n if [ \"$(id -u)\" != \"0\" ]; then\n _systemctl_cmd=\"systemctl --user\"\n _service_dir=\"${HOME}/.config/systemd/user\"\n fi\n\n if [ -f \"${_service_dir}/agentteams-podman-restart.service\" ]; then\n log \"Disabling and removing dedicated AgentTeams Podman autostart service...\"\n ${_systemctl_cmd} disable --now agentteams-podman-restart.service 2>/dev/null || true\n rm -f \"${_service_dir}/agentteams-podman-restart.service\"\n ${_systemctl_cmd} daemon-reload 2>/dev/null || true\n fi\n # Note: Native podman-restart.service is strictly untouched per maintainer review.\n fi\n\n # Capture manager image before removing (needed for workspace cleanup on Linux)\n local manager_image=\"\"\n manager_image=$(${DOCKER_CMD} inspect agentteams-manager --format '{{.Config.Image}}' 2>/dev/null || true)\n\n # Stop and remove manager\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -q \"^agentteams-manager$\"; then\n log \"$(msg uninstall.stopping_manager)\"\n ${DOCKER_CMD} stop agentteams-manager >/dev/null 2>&1 || true\n ${DOCKER_CMD} rm agentteams-manager >/dev/null 2>&1 || true\n fi\n\n # Stop and remove workers\n local workers\n workers=$(${DOCKER_CMD} ps -a --filter \"name=agentteams-worker-\" --format '{{.Names}}' 2>/dev/null || true)\n if [ -n \"${workers}\" ]; then\n log \"$(msg uninstall.stopping_workers)\"\n echo \"${workers}\" | while read -r w; do\n ${DOCKER_CMD} rm -f \"${w}\" >/dev/null 2>&1 || true\n log \"$(msg uninstall.removed \"${w}\")\"\n done\n fi\n\n # Stop and remove the dashboard container (installed via step_dashboard)\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -q \"^agentteams-dashboard$\"; then\n log \"Stopping and removing agentteams-dashboard...\"\n ${DOCKER_CMD} stop agentteams-dashboard >/dev/null 2>&1 || true\n ${DOCKER_CMD} rm agentteams-dashboard >/dev/null 2>&1 || true\n fi\n if ${DOCKER_CMD} volume inspect agentteams-dashboard-data >/dev/null 2>&1; then\n log \"Removing dashboard data volume: agentteams-dashboard-data\"\n ${DOCKER_CMD} volume rm agentteams-dashboard-data >/dev/null 2>&1 || true\n fi\n\n # Stop and remove docker-proxy (legacy ≤ v1.0.x; current arch uses\n # agentteams-controller for the same role)\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -q \"^agentteams-docker-proxy$\"; then\n log \"$(msg uninstall.removing_proxy)\"\n ${DOCKER_CMD} stop agentteams-docker-proxy >/dev/null 2>&1 || true\n ${DOCKER_CMD} rm agentteams-docker-proxy >/dev/null 2>&1 || true\n fi\n\n # Stop and remove the embedded controller container. MUST happen\n # before the `docker volume rm agentteams-data` step below — in embedded\n # mode agentteams-controller mounts agentteams-data at /data (Tuwunel DB,\n # MinIO state, Higress state, all the room messages), and `volume\n # rm` against an in-use volume fails silently because of the trailing\n # `|| true`. Skipping this used to leave room/message history behind\n # across \"uninstall + reinstall\" cycles. See PR #692.\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -q \"^agentteams-controller$\"; then\n log \"$(msg uninstall.stopping_controller)\"\n ${DOCKER_CMD} stop agentteams-controller >/dev/null 2>&1 || true\n ${DOCKER_CMD} rm agentteams-controller >/dev/null 2>&1 || true\n fi\n\n # Read env file for data/workspace info before removing\n local env_file=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n [ ! -f \"${env_file}\" ] && [ -f \"./agentteams-manager.env\" ] && env_file=\"./agentteams-manager.env\"\n\n local data_dir=\"\" workspace_dir=\"\"\n if [ -f \"${env_file}\" ]; then\n data_dir=$(grep '^AGENTTEAMS_DATA_DIR=' \"${env_file}\" 2>/dev/null | cut -d= -f2- || true)\n workspace_dir=$(grep '^AGENTTEAMS_WORKSPACE_DIR=' \"${env_file}\" 2>/dev/null | cut -d= -f2- || true)\n fi\n\n # Remove Docker volume\n local vol=\"${data_dir:-agentteams-data}\"\n if ${DOCKER_CMD} volume inspect \"${vol}\" >/dev/null 2>&1; then\n log \"$(msg uninstall.removing_volume \"${vol}\")\"\n ${DOCKER_CMD} volume rm \"${vol}\" >/dev/null 2>&1 || true\n fi\n\n # Remove Docker network\n if ${DOCKER_CMD} network ls --format '{{.Name}}' 2>/dev/null | grep -q \"^agentteams-net$\"; then\n log \"$(msg uninstall.removing_network)\"\n ${DOCKER_CMD} network rm agentteams-net >/dev/null 2>&1 || true\n fi\n\n # Remove workspace directory\n if [ -n \"${workspace_dir}\" ] && [ -d \"${workspace_dir}\" ]; then\n log \"$(msg uninstall.removing_workspace \"${workspace_dir}\")\"\n rm -rf \"${workspace_dir}\" 2>/dev/null || true\n if [ -d \"${workspace_dir}\" ]; then\n log \"$(msg uninstall.removing_workspace_elevated)\"\n local _parent _base\n _parent=$(dirname \"${workspace_dir}\")\n _base=$(basename \"${workspace_dir}\")\n local _rm_image=\"${manager_image:-busybox}\"\n ${DOCKER_CMD} run --rm --entrypoint sh \\\n -v \"${_parent}:/host-parent\" \\\n \"${_rm_image}\" \\\n -c \"rm -rf /host-parent/${_base}\" 2>/dev/null || true\n fi\n fi\n\n # Remove env file\n if [ -f \"${env_file}\" ]; then\n log \"$(msg uninstall.removing_env \"${env_file}\")\"\n rm -f \"${env_file}\"\n fi\n\n # Remove install log\n if [ -f \"${HOME}/agentteams-install.log\" ]; then\n log \"$(msg uninstall.removing_log \"${HOME}/agentteams-install.log\")\"\n rm -f \"${HOME}/agentteams-install.log\"\n fi\n\n echo \"\"\n log \"$(msg uninstall.done)\"\n}\n\n# ============================================================\n# Check container runtime (docker or podman) and environment\n# ============================================================\ncheck_container_runtime() {\n # Ensure the rootless environment baseline (XDG_RUNTIME_DIR) is ready before engine detection.\n if [ \"$(id -u)\" -ne 0 ] && [ -z \"${XDG_RUNTIME_DIR:-}\" ]; then\n export XDG_RUNTIME_DIR=\"/run/user/$(id -u)\"\n fi\n\n if command -v docker >/dev/null 2>&1; then\n DOCKER_CMD=\"docker\"\n # Check for podman disguised as docker (alias or podman-docker package)\n if docker --version 2>/dev/null | grep -qi \"podman\"; then\n DOCKER_CMD=\"podman\"\n fi\n elif command -v podman >/dev/null 2>&1; then\n DOCKER_CMD=\"podman\"\n else\n echo -e \"\\033[31m[AgentTeams ERROR]\\033[0m $(msg error.docker_not_found)\" >&2\n exit 1\n fi\n\n # Unified health check (Validates runtime responsiveness)\n if ! ${DOCKER_CMD} ps >/dev/null 2>&1; then\n echo -e \"\\033[31m[AgentTeams ERROR]\\033[0m $(msg error.docker_not_running)\" >&2\n exit 1\n fi\n}\n\ncheck_container_runtime\n\ncase \"${1:-}\" in\n manager|\"\")\n install_manager\n ;;\n worker)\n shift\n install_worker \"$@\"\n ;;\n dashboard)\n check_container_runtime\n load_current_params_from_env\n AGENTTEAMS_DASHBOARD=\"${AGENTTEAMS_DASHBOARD:-1}\"\n AGENTTEAMS_DASHBOARD_VERSION=\"${AGENTTEAMS_DASHBOARD_VERSION:-v1.2.0}\"\n AGENTTEAMS_PORT_DASHBOARD=\"${AGENTTEAMS_PORT_DASHBOARD:-13000}\"\n AGENTTEAMS_DASHBOARD_IMAGE=\"${AGENTTEAMS_DASHBOARD_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-dashboard:${AGENTTEAMS_DASHBOARD_VERSION}}\"\n AGENTTEAMS_USE_EMBEDDED=1\n _start_dashboard\n ;;\n uninstall)\n uninstall_agentteams\n ;;\n *)\n echo \"Usage: $0 [manager|worker [options]|dashboard|uninstall]\"\n echo \"\"\n echo \"Commands:\"\n echo \" manager Interactive Manager installation (default)\"\n echo \" Choose Quick Start (all defaults) or Manual mode\"\n echo \" worker Worker installation (requires --name and connection params)\"\n echo \" dashboard Start/stop agentteams-dashboard (use with AGENTTEAMS_DASHBOARD=0 to stop)\"\n echo \" uninstall Stop and remove Manager + all Worker containers\"\n echo \"\"\n echo \"Quick Start (fastest):\"\n echo \" $0\"\n echo \" # Then select '1' for Quick Start mode\"\n echo \"\"\n echo \"Non-interactive (for automation):\"\n echo \" AGENTTEAMS_NON_INTERACTIVE=1 AGENTTEAMS_LLM_API_KEY=sk-xxx $0\"\n echo \"\"\n echo \"Worker Options:\"\n echo \" --name <name> Worker name (required)\"\n echo \" --fs <url> MinIO endpoint URL (required)\"\n echo \" --fs-key <key> MinIO access key (required)\"\n echo \" --fs-secret <secret> MinIO secret key (required)\"\n echo \" --reset Remove existing Worker container before creating\"\n exit 1\n ;;\nesac\n"}, "124f06d13fd6bb4c2054128dcff26bcd9f9c8dbf": {"t": 1785746533.9792135, "n": 249717, "h": "d0ccc9f2f7452238", "c": "#!/bin/bash\n# agentteams-install.sh - One-click installation for AgentTeams Manager, Worker, and Dashboard\n#\n# Usage:\n# ./agentteams-install.sh # Interactive installation (choose Quick Start or Manual)\n# ./agentteams-install.sh manager # Same as above (explicit)\n# ./agentteams-install.sh worker --name <name> ... # Worker installation\n# ./agentteams-install.sh dashboard # Start/stop agentteams-dashboard (AGENTTEAMS_DASHBOARD=0 to stop)\n# ./agentteams-install.sh uninstall # Stop and remove Manager + all Workers\n#\n# NOTE: agentteams-dashboard is currently only supported via the Bash installer.\n# The PowerShell installer (agentteams-install.ps1) does not yet include dashboard support.\n#\n# Onboarding Modes:\n# Quick Start - Fast installation with all default values (recommended)\n# Manual - Customize each option step by step\n#\n# Environment variables (for automation):\n# AGENTTEAMS_NON_INTERACTIVE Skip all prompts, use defaults (default: 0)\n# AGENTTEAMS_LLM_PROVIDER LLM provider (default: openai-compat for zh non-interactive Token Plan; qwen for en)\n# AGENTTEAMS_DEFAULT_MODEL Default model (default: qwen3.6-plus for zh Token Plan and en non-interactive)\n# AGENTTEAMS_OPENAI_BASE_URL OpenAI-compatible base URL (default for zh non-interactive: Alibaba Token Plan endpoint)\n# AGENTTEAMS_LLM_API_KEY LLM API key (required)\n# AGENTTEAMS_ADMIN_USER Admin username (default: admin)\n# AGENTTEAMS_ADMIN_PASSWORD Admin password (auto-generated if not set, min 8 chars)\n# AGENTTEAMS_MATRIX_DOMAIN Matrix domain (default: matrix-local.agentteams.io:18080)\n# AGENTTEAMS_MOUNT_SOCKET Mount container runtime socket (default: 1)\n# AGENTTEAMS_DATA_DIR Docker volume name for persistent data (default: agentteams-data)\n# AGENTTEAMS_WORKSPACE_DIR Host directory for manager workspace (default: ~/agentteams-manager)\n# AGENTTEAMS_VERSION Image tag (default: latest)\n# AGENTTEAMS_REGISTRY Image registry (default: auto-detected by timezone)\n# AGENTTEAMS_INSTALL_MANAGER_IMAGE Override manager image (e.g., local build)\n# AGENTTEAMS_INSTALL_WORKER_IMAGE Override worker image (e.g., local build)\n# AGENTTEAMS_INSTALL_COPAW_WORKER_IMAGE Override copaw worker image (e.g., local build)\n# AGENTTEAMS_INSTALL_HERMES_WORKER_IMAGE Override hermes worker image (e.g., local build)\n# AGENTTEAMS_NACOS_REGISTRY_URI Default Nacos registry URI for Worker market search/import\n# (default: nacos://market.agentteams.io:80/public)\n# AGENTTEAMS_NACOS_USERNAME Default Nacos username for nacos:// package imports (optional)\n# AGENTTEAMS_NACOS_PASSWORD Default Nacos password for nacos:// package imports (optional)\n# AGENTTEAMS_CMS_TRACES_ENABLED Enable openclaw-cms-plugin traces for Manager AND all Workers (default: false)\n# AGENTTEAMS_CMS_ENDPOINT ARMS OTLP endpoint (required if traces enabled)\n# AGENTTEAMS_CMS_LICENSE_KEY CMS license key (required if traces enabled)\n# AGENTTEAMS_CMS_PROJECT CMS project name (optional)\n# AGENTTEAMS_CMS_WORKSPACE CMS workspace ID (required if traces enabled)\n# AGENTTEAMS_CMS_SERVICE_NAME Manager service name in ARMS (default: agentteams-manager)\n# Workers always report as agentteams-worker-<name> automatically\n# AGENTTEAMS_CMS_METRICS_ENABLED Enable diagnostics-otel metrics for Manager AND all Workers (default: false)\n# AGENTTEAMS_PORT_GATEWAY Host port for Higress gateway (default: 18080)\n# AGENTTEAMS_PORT_CONSOLE Host port for Higress console (default: 18001)\n# AGENTTEAMS_PORT_ELEMENT_WEB Host port for Element Web direct access (default: 18088)\n# AGENTTEAMS_PORT_MANAGER_CONSOLE Host port for Manager console (default: 18888)\n# AGENTTEAMS_WORKER_IDLE_TIMEOUT Worker idle timeout in minutes (default: 720, i.e. 12 hours)\n# AGENTTEAMS_DASHBOARD Install agentteams-dashboard management UI (default: 1)\n# AGENTTEAMS_DASHBOARD_VERSION Dashboard version (default: v1.2.0, independent of AgentTeams version)\n# AGENTTEAMS_PORT_DASHBOARD Dashboard host port (default: 13000)\n# AGENTTEAMS_DASHBOARD_IMAGE Override dashboard image (default: <registry>/agentteams/agentteams-dashboard:<DASHBOARD_VERSION>)\n# AGENTTEAMS_AI_GATEWAY_ADMIN_URL Higress Console URL for shared auth (auto-detected)\n\nset -e\n\nAGENTTEAMS_VERSION=\"${AGENTTEAMS_VERSION:-}\"\nAGENTTEAMS_KNOWN_STABLE_VERSION=\"v1.2.0\" # fallback if GitHub API is unreachable\n\n_normalize_version() {\n local version=\"$1\"\n case \"${version}\" in\n [0-9]*.[0-9]*.[0-9]*) version=\"v${version}\" ;;\n esac\n case \"${version}\" in\n v[0-9]*.[0-9]*.[0-9]*.beta.[0-9]*) version=\"${version/.beta./-beta.}\" ;;\n esac\n printf '%s' \"${version}\"\n}\n\n# Returns 0 (true) if $1 < $2 using semver order; \"latest\" is treated as greatest\n_ver_lt() {\n [ \"$1\" = \"latest\" ] && return 1\n [ \"$2\" = \"latest\" ] && return 0\n [ \"$1\" = \"$2\" ] && return 1\n [ \"$(printf '%s\\n%s' \"$1\" \"$2\" | sort -V | head -1)\" = \"$1\" ]\n}\n\n_use_legacy_image_env() {\n if [ \"$1\" = \"latest\" ]; then\n _ver_lt \"${AGENTTEAMS_KNOWN_STABLE_VERSION}\" \"v1.2.0\"\n else\n _ver_lt \"$1\" \"v1.2.0\"\n fi\n}\n\n_controller_env_prefix() {\n if _use_legacy_image_env \"$1\"; then\n printf '%s%s' 'HIC' 'LAW_'\n else\n printf '%s' 'AGENTTEAMS_'\n fi\n}\n\n_controller_storage_prefix() {\n if _use_legacy_image_env \"$1\"; then\n printf '%s%s' 'hic' 'law/agentteams-storage'\n else\n printf '%s' 'agentteams/agentteams-storage'\n fi\n}\nAGENTTEAMS_NON_INTERACTIVE=\"${AGENTTEAMS_NON_INTERACTIVE:-0}\"\nAGENTTEAMS_MOUNT_SOCKET=\"${AGENTTEAMS_MOUNT_SOCKET:-1}\"\nAGENTTEAMS_DOCKER_PROXY=\"${AGENTTEAMS_DOCKER_PROXY:-1}\"\nSTEP_RESULT=\"\" # Used by state machine to signal \"back\" navigation\n\n# ============================================================\n# Log all output to file\n# ============================================================\n\nAGENTTEAMS_LOG_FILE=\"${HOME}/agentteams-install.log\"\n\nif [ \"${1:-}\" != \"uninstall\" ]; then\n # Redirect all output (stdout and stderr) to both terminal and log file\n exec > >(tee -a \"${AGENTTEAMS_LOG_FILE}\") 2>&1\n\n echo \"\"\n echo \"========================================\"\n echo \"AgentTeams Installation Log\"\n echo \"Started: $(date)\"\n echo \"User: $(whoami)\"\n echo \"System: $(uname -a)\"\n echo \"Log file: ${AGENTTEAMS_LOG_FILE}\"\n echo \"========================================\"\n echo \"\"\nfi\n\n# ============================================================\n# Utility functions (needed early for timezone detection)\n# ============================================================\n\nlog() {\n echo -e \"\\033[36m[AgentTeams]\\033[0m $1\"\n}\n\nerror() {\n echo -e \"\\033[31m[AgentTeams ERROR]\\033[0m $1\" >&2\n}\n\ndie() {\n error \"$1\"\n exit 1\n}\n\n# ============================================================\n# Timezone detection (compatible with Linux and macOS)\n# ============================================================\n\ndetect_timezone() {\n local tz=\"\"\n\n # Try /etc/timezone (Debian/Ubuntu)\n if [ -f /etc/timezone ]; then\n tz=$(cat /etc/timezone 2>/dev/null | tr -d '[:space:]')\n fi\n\n # Try /etc/localtime symlink (macOS and some Linux)\n if [ -z \"${tz}\" ] && [ -L /etc/localtime ]; then\n tz=$(ls -l /etc/localtime 2>/dev/null | sed 's|.*/zoneinfo/||')\n fi\n\n # Try timedatectl (systemd)\n if [ -z \"${tz}\" ]; then\n tz=$(timedatectl show --value -p Timezone 2>/dev/null)\n fi\n\n # If still not detected, warn and prompt user. Diagnostics go to stderr —\n # this function returns its value via stdout to `$(detect_timezone)`, so\n # any echo here would leak into AGENTTEAMS_TIMEZONE and downstream image tags.\n if [ -z \"${tz}\" ]; then\n echo \"\" >&2\n echo -e \"\\033[33m[AgentTeams WARNING]\\033[0m Could not detect timezone automatically.\" >&2\n echo -e \"\\033[33m[AgentTeams]\\033[0m Please enter your timezone (e.g., Asia/Shanghai, America/New_York).\" >&2\n echo \"\" >&2\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n tz=\"Asia/Shanghai\"\n log \"Using default timezone: ${tz}\" >&2\n else\n read -e -p \"Timezone [Asia/Shanghai]: \" tz\n tz=\"${tz:-Asia/Shanghai}\"\n fi\n fi\n\n echo \"${tz}\"\n}\n\n# Detect timezone once at startup (used by registry selection and container TZ)\nAGENTTEAMS_TIMEZONE=\"${AGENTTEAMS_TIMEZONE:-$(detect_timezone)}\"\n\n# ============================================================\n# Language detection based on timezone\n# ============================================================\n\ndetect_language() {\n local tz=\"${AGENTTEAMS_TIMEZONE}\"\n case \"${tz}\" in\n Asia/Shanghai|Asia/Chongqing|Asia/Harbin|Asia/Urumqi|\\\n Asia/Taipei|Asia/Hong_Kong|Asia/Macau)\n echo \"zh\"\n ;;\n *)\n echo \"en\"\n ;;\n esac\n}\n\n# Language priority: env var > existing env file > timezone detection\nif [ -z \"${AGENTTEAMS_LANGUAGE}\" ]; then\n # Check existing env file for saved language preference (upgrade scenario)\n _env_file=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n # Migrate from legacy location (current directory) if needed\n if [ ! -f \"${_env_file}\" ] && [ -f \"./agentteams-manager.env\" ]; then\n mv \"./agentteams-manager.env\" \"${_env_file}\" 2>/dev/null || true\n fi\n if [ -f \"${_env_file}\" ]; then\n _saved_lang=$(grep '^AGENTTEAMS_LANGUAGE=' \"${_env_file}\" 2>/dev/null | cut -d= -f2-)\n if [ -n \"${_saved_lang}\" ]; then\n AGENTTEAMS_LANGUAGE=\"${_saved_lang}\"\n fi\n fi\n # Fall back to timezone-based detection\n if [ -z \"${AGENTTEAMS_LANGUAGE}\" ]; then\n AGENTTEAMS_LANGUAGE=\"$(detect_language)\"\n fi\n unset _env_file _saved_lang\nfi\nexport AGENTTEAMS_LANGUAGE\n\n# ============================================================\n# Centralized message dictionary and msg() function\n# Compatible with bash 3.2+ (macOS default) — uses case instead of declare -A\n# ============================================================\n\n# msg() function: look up message by key, with printf-style argument substitution\n# Falls back to English if the current language translation is missing.\nmsg() {\n local key=\"$1\"\n shift\n local lang=\"${AGENTTEAMS_LANGUAGE:-en}\"\n local text=\"\"\n case \"${key}.${lang}\" in\n # --- Timezone detection messages ---\n \"tz.warning.title.zh\") text=\"无法自动检测时区。\" ;;\n \"tz.warning.title.en\") text=\"Could not detect timezone automatically.\" ;;\n \"tz.warning.prompt.zh\") text=\"请输入您的时区(例如 Asia/Shanghai、America/New_York)。\" ;;\n \"tz.warning.prompt.en\") text=\"Please enter your timezone (e.g., Asia/Shanghai, America/New_York).\" ;;\n \"tz.default.zh\") text=\"使用默认时区: %s\" ;;\n \"tz.default.en\") text=\"Using default timezone: %s\" ;;\n \"tz.input_prompt.zh\") text=\"时区\" ;;\n \"tz.input_prompt.en\") text=\"Timezone\" ;;\n # --- Installation title and info ---\n \"install.title.zh\") text=\"=== AgentTeams Manager 安装 ===\" ;;\n \"install.title.en\") text=\"=== AgentTeams Manager Installation ===\" ;;\n \"install.registry.zh\") text=\"镜像仓库: %s\" ;;\n \"install.registry.en\") text=\"Registry: %s\" ;;\n \"install.dir.zh\") text=\"安装目录: %s\" ;;\n \"install.dir.en\") text=\"Installation directory: %s\" ;;\n \"install.dir_hint.zh\") text=\" (env 文件 'agentteams-manager.env' 将保存到 HOME 目录。)\" ;;\n \"install.dir_hint.en\") text=\" (The env file 'agentteams-manager.env' will be saved to your HOME directory.)\" ;;\n \"install.dir_hint2.zh\") text=\" (请从您希望管理此安装的目录运行此脚本。)\" ;;\n \"install.dir_hint2.en\") text=\" (Run this script from the directory where you want to manage this installation.)\" ;;\n # --- Onboarding mode ---\n \"install.mode.title.zh\") text=\"--- Onboarding 模式 ---\" ;;\n \"install.mode.title.en\") text=\"--- Onboarding Mode ---\" ;;\n \"install.mode.choose.zh\") text=\"选择安装模式:\" ;;\n \"install.mode.choose.en\") text=\"Choose your installation mode:\" ;;\n \"install.mode.quickstart.zh\") text=\" 1) 快速开始 - 使用阿里云通义 Token 套餐快速安装(推荐)\" ;;\n \"install.mode.quickstart.en\") text=\" 1) Quick Start - Fast installation with Qwen Cloud (recommended)\" ;;\n \"install.mode.manual.zh\") text=\" 2) 手动配置 - 选择 LLM 提供商并自定义选项\" ;;\n \"install.mode.manual.en\") text=\" 2) Manual - Choose LLM provider and customize options\" ;;\n \"install.mode.prompt.zh\") text=\"请选择 [1/2]\" ;;\n \"install.mode.prompt.en\") text=\"Enter choice [1/2]\" ;;\n \"install.mode.quickstart_selected.zh\") text=\"已选择快速开始模式 - 使用阿里云通义 Token 套餐\" ;;\n \"install.mode.quickstart_selected.en\") text=\"Quick Start mode selected - using Qwen Cloud\" ;;\n \"install.mode.manual_selected.zh\") text=\"已选择手动配置模式 - 您将选择 LLM 提供商并自定义选项\" ;;\n \"install.mode.manual_selected.en\") text=\"Manual mode selected - you will choose LLM provider and customize options\" ;;\n \"install.mode.invalid.zh\") text=\"无效选择,默认使用快速开始模式\" ;;\n \"install.mode.invalid.en\") text=\"Invalid choice, defaulting to Quick Start mode\" ;;\n # --- Upgrade mode sub-menu ---\n \"upgrade.mode.prompt.zh\") text=\"请选择升级方式:\" ;;\n \"upgrade.mode.prompt.en\") text=\"Select upgrade mode:\" ;;\n \"upgrade.mode.keep_all.zh\") text=\" 1) 保留所有参数,一键升级(推荐)\" ;;\n \"upgrade.mode.keep_all.en\") text=\" 1) Keep all parameters, quick upgrade (recommended)\" ;;\n \"upgrade.mode.confirm_each.zh\") text=\" 2) 逐个确认参数(可修改)\" ;;\n \"upgrade.mode.confirm_each.en\") text=\" 2) Confirm each parameter (can modify)\" ;;\n \"upgrade.mode.back.zh\") text=\" 3) 返回\" ;;\n \"upgrade.mode.back.en\") text=\" 3) Back\" ;;\n # --- Version selection ---\n \"install.version.title.zh\") text=\"--- 版本选择 ---\" ;;\n \"install.version.title.en\") text=\"--- Version Selection ---\" ;;\n \"install.version.choose.zh\") text=\"选择要安装的版本:\" ;;\n \"install.version.choose.en\") text=\"Choose the version to install:\" ;;\n \"install.version.option_latest.zh\") text=\" 1) latest - 最新版(默认)\" ;;\n \"install.version.option_latest.en\") text=\" 1) latest - Latest build (default)\" ;;\n \"install.version.option_stable.zh\") text=\" 2) %s - 最新稳定版\" ;;\n \"install.version.option_stable.en\") text=\" 2) %s - Latest stable release\" ;;\n \"install.version.fetching.zh\") text=\"正在查询最新稳定版本...\" ;;\n \"install.version.fetching.en\") text=\"Fetching latest stable release...\" ;;\n \"install.version.fetch_failed.zh\") text=\"无法查询 GitHub,使用内置版本 %s\" ;;\n \"install.version.fetch_failed.en\") text=\"Could not reach GitHub, using built-in version %s\" ;;\n \"install.version.option_custom.zh\") text=\" 3) 自定义 - 手动输入版本号(如 v1.0.5)\" ;;\n \"install.version.option_custom.en\") text=\" 3) Custom - Enter a specific version (e.g. v1.0.5)\" ;;\n \"install.version.prompt.zh\") text=\"请选择 [1/2/3]\" ;;\n \"install.version.prompt.en\") text=\"Enter choice [1/2/3]\" ;;\n \"install.version.custom_prompt.zh\") text=\"请输入版本号\" ;;\n \"install.version.custom_prompt.en\") text=\"Enter version tag\" ;;\n \"install.version.selected_latest.zh\") text=\"已选择最新版 (latest)\" ;;\n \"install.version.selected_latest.en\") text=\"Selected latest version\" ;;\n \"install.version.selected_stable.zh\") text=\"已选择最新稳定版 (%s)\" ;;\n \"install.version.selected_stable.en\") text=\"Selected latest stable version (%s)\" ;;\n \"install.version.selected_custom.zh\") text=\"已选择自定义版本 (%s)\" ;;\n \"install.version.selected_custom.en\") text=\"Selected custom version (%s)\" ;;\n \"install.version.invalid.zh\") text=\"无效选择,使用最新稳定版 (%s)\" ;;\n \"install.version.invalid.en\") text=\"Invalid choice, defaulting to latest stable version (%s)\" ;;\n # --- Existing installation detected ---\n \"install.existing.detected.zh\") text=\"检测到已有 Manager 安装(env 文件: %s)\" ;;\n \"install.existing.detected.en\") text=\"Existing Manager installation detected (env file: %s)\" ;;\n \"install.existing.choose.zh\") text=\"选择操作:\" ;;\n \"install.existing.choose.en\") text=\"Choose an action:\" ;;\n \"install.existing.upgrade.zh\") text=\" 1) 就地升级(保留数据、工作空间、env 文件)\" ;;\n \"install.existing.upgrade.en\") text=\" 1) In-place upgrade (keep data, workspace, env file)\" ;;\n \"install.existing.reinstall.zh\") text=\" 2) 全新重装(删除所有数据,重新开始)\" ;;\n \"install.existing.reinstall.en\") text=\" 2) Clean reinstall (remove all data, start fresh)\" ;;\n \"install.existing.cancel.zh\") text=\" 3) 取消\" ;;\n \"install.existing.cancel.en\") text=\" 3) Cancel\" ;;\n \"install.existing.prompt.zh\") text=\"请选择 [1/2/3]\" ;;\n \"install.existing.prompt.en\") text=\"Enter choice [1/2/3]\" ;;\n \"install.existing.upgrade_noninteractive.zh\") text=\"非交互模式: 执行就地升级...\" ;;\n \"install.existing.upgrade_noninteractive.en\") text=\"Non-interactive mode: performing in-place upgrade...\" ;;\n \"install.existing.upgrading.zh\") text=\"执行就地升级...\" ;;\n \"install.existing.upgrading.en\") text=\"Performing in-place upgrade...\" ;;\n \"install.existing.warn_manager_stop.zh\") text=\"⚠️ Manager 容器将被停止并重新创建。\" ;;\n \"install.existing.warn_manager_stop.en\") text=\"⚠️ Manager container will be stopped and recreated.\" ;;\n \"install.existing.warn_worker_recreate.zh\") text=\"⚠️ Worker 容器也将被重新创建(以更新 Manager IP)。\" ;;\n \"install.existing.warn_worker_recreate.en\") text=\"⚠️ Worker containers will also be recreated (to update Manager IP in hosts).\" ;;\n \"install.existing.continue_prompt.zh\") text=\"继续?[y/N]\" ;;\n \"install.existing.continue_prompt.en\") text=\"Continue? [y/N]\" ;;\n \"install.existing.cancelled.zh\") text=\"安装已取消。\" ;;\n \"install.existing.cancelled.en\") text=\"Installation cancelled.\" ;;\n \"install.existing.stopping_manager.zh\") text=\"停止并移除现有 manager 容器...\" ;;\n \"install.existing.stopping_manager.en\") text=\"Stopping and removing existing manager container...\" ;;\n \"install.existing.stopping_workers.zh\") text=\"停止并移除现有 worker 容器...\" ;;\n \"install.existing.stopping_workers.en\") text=\"Stopping and removing existing worker containers...\" ;;\n \"install.existing.removed.zh\") text=\" 已移除: %s\" ;;\n \"install.existing.removed.en\") text=\" Removed: %s\" ;;\n # --- Clean reinstall messages ---\n \"install.reinstall.performing.zh\") text=\"执行全新重装...\" ;;\n \"install.reinstall.performing.en\") text=\"Performing clean reinstall...\" ;;\n \"install.reinstall.warn_stop.zh\") text=\"⚠️ 以下运行中的容器将被停止:\" ;;\n \"install.reinstall.warn_stop.en\") text=\"⚠️ The following running containers will be stopped:\" ;;\n \"install.reinstall.warn_delete.zh\") text=\"⚠️ 警告: 以下内容将被删除:\" ;;\n \"install.reinstall.warn_delete.en\") text=\"⚠️ WARNING: This will DELETE the following:\" ;;\n \"install.reinstall.warn_volume.zh\") text=\" - Docker 卷: agentteams-data\" ;;\n \"install.reinstall.warn_volume.en\") text=\" - Docker volume: agentteams-data\" ;;\n \"install.reinstall.warn_env.zh\") text=\" - Env 文件: %s\" ;;\n \"install.reinstall.warn_env.en\") text=\" - Env file: %s\" ;;\n \"install.reinstall.warn_workspace.zh\") text=\" - Manager 工作空间: %s\" ;;\n \"install.reinstall.warn_workspace.en\") text=\" - Manager workspace: %s\" ;;\n \"install.reinstall.warn_workers.zh\") text=\" - 所有 worker 容器\" ;;\n \"install.reinstall.warn_workers.en\") text=\" - All worker containers\" ;;\n \"install.reinstall.warn_proxy.zh\") text=\" - Docker API 代理容器: agentteams-controller\" ;;\n \"install.reinstall.warn_proxy.en\") text=\" - Docker API proxy container: agentteams-controller\" ;;\n \"install.reinstall.warn_network.zh\") text=\" - Docker 网络: agentteams-net\" ;;\n \"install.reinstall.warn_network.en\") text=\" - Docker network: agentteams-net\" ;;\n \"install.reinstall.confirm_type.zh\") text=\"请输入工作空间路径以确认删除(或按 Ctrl+C 取消):\" ;;\n \"install.reinstall.confirm_type.en\") text=\"To confirm deletion, please type the workspace path:\" ;;\n \"install.reinstall.confirm_path.zh\") text=\"输入路径以确认(或按 Ctrl+C 取消)\" ;;\n \"install.reinstall.confirm_path.en\") text=\"Type the path to confirm (or press Ctrl+C to cancel)\" ;;\n \"install.reinstall.path_mismatch.zh\") text=\"路径不匹配。中止重装。输入: '%s',期望: '%s'\" ;;\n \"install.reinstall.path_mismatch.en\") text=\"Path mismatch. Aborting reinstall. Input: '%s', Expected: '%s'\" ;;\n \"install.reinstall.confirmed.zh\") text=\"已确认。正在清理...\" ;;\n \"install.reinstall.confirmed.en\") text=\"Confirmed. Cleaning up...\" ;;\n \"install.reinstall.removed_worker.zh\") text=\" 已移除 worker: %s\" ;;\n \"install.reinstall.removed_worker.en\") text=\" Removed worker: %s\" ;;\n \"install.reinstall.removing_volume.zh\") text=\"正在移除 Docker 卷: agentteams-data\" ;;\n \"install.reinstall.removing_volume.en\") text=\"Removing Docker volume: agentteams-data\" ;;\n \"install.reinstall.warn_volume_fail.zh\") text=\" 警告: 无法移除卷(可能有引用)\" ;;\n \"install.reinstall.warn_volume_fail.en\") text=\" Warning: Could not remove volume (may have references)\" ;;\n \"install.reinstall.removing_proxy.zh\") text=\"正在移除 Docker API 代理容器: agentteams-controller\" ;;\n \"install.reinstall.removing_proxy.en\") text=\"Removing Docker API proxy container: agentteams-controller\" ;;\n \"install.reinstall.removing_network.zh\") text=\"正在移除 Docker 网络: agentteams-net\" ;;\n \"install.reinstall.removing_network.en\") text=\"Removing Docker network: agentteams-net\" ;;\n \"install.reinstall.removing_workspace.zh\") text=\"正在移除工作空间目录: %s\" ;;\n \"install.reinstall.removing_workspace.en\") text=\"Removing workspace directory: %s\" ;;\n \"install.reinstall.removing_env.zh\") text=\"正在移除 env 文件: %s\" ;;\n \"install.reinstall.removing_env.en\") text=\"Removing env file: %s\" ;;\n \"install.reinstall.cleanup_done.zh\") text=\"清理完成。开始全新安装...\" ;;\n \"install.reinstall.cleanup_done.en\") text=\"Cleanup complete. Starting fresh installation...\" ;;\n \"install.reinstall.failed_rm_workspace.zh\") text=\"无法移除工作空间目录\" ;;\n \"install.reinstall.failed_rm_workspace.en\") text=\"Failed to remove workspace directory\" ;;\n # --- Orphan volume detection ---\n \"install.orphan_volume.detected.zh\") text=\"⚠️ 检测到残留数据卷 '%s',但未找到对应的 env 配置文件。\" ;;\n \"install.orphan_volume.detected.en\") text=\"⚠️ Found leftover data volume '%s' but no matching env config file.\" ;;\n \"install.orphan_volume.warn.zh\") text=\"这可能是之前安装的残留数据,会导致新安装出现异常(如密码冲突、服务启动失败)。\" ;;\n \"install.orphan_volume.warn.en\") text=\"This is likely leftover data from a previous installation and may cause issues (e.g., credential conflicts, service startup failures).\" ;;\n \"install.orphan_volume.choose.zh\") text=\"选择操作:\" ;;\n \"install.orphan_volume.choose.en\") text=\"Choose an action:\" ;;\n \"install.orphan_volume.clean.zh\") text=\" 1) 清理残留数据卷后继续安装(推荐)\" ;;\n \"install.orphan_volume.clean.en\") text=\" 1) Remove leftover volume and continue installation (recommended)\" ;;\n \"install.orphan_volume.keep.zh\") text=\" 2) 保留数据卷继续安装(可能出现异常)\" ;;\n \"install.orphan_volume.keep.en\") text=\" 2) Keep the volume and continue installation (may cause issues)\" ;;\n \"install.orphan_volume.prompt.zh\") text=\"请选择 [1/2]\" ;;\n \"install.orphan_volume.prompt.en\") text=\"Enter choice [1/2]\" ;;\n \"install.orphan_volume.cleaning.zh\") text=\"正在清理残留数据卷...\" ;;\n \"install.orphan_volume.cleaning.en\") text=\"Removing leftover data volume...\" ;;\n \"install.orphan_volume.cleaned.zh\") text=\"残留数据卷已清理。继续全新安装...\" ;;\n \"install.orphan_volume.cleaned.en\") text=\"Leftover volume removed. Continuing with fresh installation...\" ;;\n \"install.orphan_volume.keeping.zh\") text=\"保留数据卷,继续安装。如遇异常请选择全新重装。\" ;;\n \"install.orphan_volume.keeping.en\") text=\"Keeping existing volume. If you encounter issues, consider a clean reinstall.\" ;;\n \"install.orphan_volume.clean_noninteractive.zh\") text=\"非交互模式: 自动清理残留数据卷...\" ;;\n \"install.orphan_volume.clean_noninteractive.en\") text=\"Non-interactive mode: automatically removing leftover volume...\" ;;\n # --- Loading existing config ---\n \"install.loading_config.zh\") text=\"从 %s 加载已有配置(shell 环境变量优先)...\" ;;\n \"install.loading_config.en\") text=\"Loading existing config from %s (shell env vars take priority)...\" ;;\n # --- LLM Configuration ---\n \"llm.title.zh\") text=\"--- LLM 配置 ---\" ;;\n \"llm.title.en\") text=\"--- LLM Configuration ---\" ;;\n \"llm.provider.label.zh\") text=\" 提供商: %s\" ;;\n \"llm.provider.label.en\") text=\" Provider: %s\" ;;\n \"llm.model.label.zh\") text=\" 模型: %s\" ;;\n \"llm.model.label.en\") text=\" Model: %s\" ;;\n \"llm.provider.qwen.zh\") text=\" 提供商: qwen(阿里云百炼)\" ;;\n \"llm.provider.qwen.en\") text=\" Provider: qwen (Alibaba Cloud Bailian)\" ;;\n \"llm.provider.qwen_default.zh\") text=\" 提供商: %s(默认)\" ;;\n \"llm.provider.qwen_default.en\") text=\" Provider: %s (default)\" ;;\n \"llm.model.default.zh\") text=\" 模型: %s(默认)\" ;;\n \"llm.model.default.en\") text=\" Model: %s (default)\" ;;\n \"llm.apikey_hint_bailian.zh\") text=\" 💡 获取阿里云百炼(DashScope)API Key:\" ;;\n \"llm.apikey_hint_bailian.en\") text=\" 💡 Get your Alibaba Cloud Bailian (DashScope) API Key:\" ;;\n \"llm.apikey_url_bailian.zh\") text=\" https://www.aliyun.com/product/bailian\" ;;\n \"llm.apikey_url_bailian.en\") text=\" https://www.aliyun.com/product/bailian\" ;;\n \"llm.apikey_hint_qwencloud.zh\") text=\" 💡 从 Qwen Cloud(国际站)获取 DASHSCOPE_API_KEY:\" ;;\n \"llm.apikey_hint_qwencloud.en\") text=\" 💡 Get your DASHSCOPE_API_KEY for Qwen Cloud (international) from:\" ;;\n \"llm.apikey_url_qwencloud.zh\") text=\" https://home.qwencloud.com/api-keys (文档: https://docs.qwencloud.com/)\" ;;\n \"llm.apikey_url_qwencloud.en\") text=\" https://home.qwencloud.com/api-keys | Docs: https://docs.qwencloud.com/\" ;;\n \"llm.apikey_hint_tokenplan.zh\") text=\" 💡 获取 DashScope API Key 或开通通义 Token 套餐,请参考:\" ;;\n \"llm.apikey_hint_tokenplan.en\") text=\" 💡 Get your DashScope or Token Plan API key (Alibaba Model Studio):\" ;;\n \"llm.apikey_url_tokenplan.zh\") text=\" https://help.aliyun.com/zh/model-studio/token-plan-quickstart\" ;;\n \"llm.apikey_url_tokenplan.en\") text=\" https://common-buy.aliyun.com/token-plan/ | https://help.aliyun.com/zh/model-studio/token-plan-quickstart\" ;;\n \"llm.apikey_hint_codingplan.zh\") text=\" 💡 获取 DashScope API Key(Coding 套餐 / coding.dashscope 接口):\" ;;\n \"llm.apikey_hint_codingplan.en\") text=\" 💡 Get your DashScope API key for Coding Plan (coding.dashscope endpoint):\" ;;\n \"llm.apikey_url_codingplan.zh\") text=\" https://help.aliyun.com/zh/model-studio/get-api-key\" ;;\n \"llm.apikey_url_codingplan.en\") text=\" https://help.aliyun.com/zh/model-studio/get-api-key\" ;;\n \"llm.apikey_prompt.zh\") text=\"LLM API Key\" ;;\n \"llm.apikey_prompt.en\") text=\"LLM API Key\" ;;\n \"llm.providers_title.zh\") text=\"可用 LLM 提供商:\" ;;\n \"llm.providers_title.en\") text=\"Available LLM Providers:\" ;;\n \"llm.provider.alibaba.zh\") text=\" 1) 阿里云通义 Token 套餐 - 推荐中国用户使用\" ;;\n \"llm.provider.alibaba.en\") text=\" 1) Qwen Cloud - International (OpenAI-compatible API, recommended)\" ;;\n \"llm.provider.openai_compat.zh\") text=\" 2) OpenAI 兼容 API - 自定义 Base URL(OpenAI、DeepSeek 等)\" ;;\n \"llm.provider.openai_compat.en\") text=\" 2) OpenAI-compatible API - Custom Base URL (OpenAI, DeepSeek, etc.)\" ;;\n \"llm.provider.select.zh\") text=\"选择提供商 [1/2]\" ;;\n \"llm.provider.select.en\") text=\"Select provider [1/2]\" ;;\n \"llm.alibaba.models_title.zh\") text=\"选择阿里云模型接入方式:\" ;;\n \"llm.alibaba.models_title.en\") text=\"Select Alibaba Cloud model access:\" ;;\n \"llm.alibaba.model.tokenplan.zh\") text=\" 1) 阿里云通义 Token 套餐 - 兼容模式(推荐)\" ;;\n \"llm.alibaba.model.tokenplan.en\") text=\" 1) Alibaba Cloud Token Plan - compatible-mode (recommended)\" ;;\n \"llm.alibaba.model.bailian.zh\") text=\" 2) 阿里云百炼 - DashScope 通用兼容接口\" ;;\n \"llm.alibaba.model.bailian.en\") text=\" 2) Alibaba Cloud Bailian - DashScope compatible mode\" ;;\n \"llm.alibaba.model.codingplan_legacy.zh\") text=\" 3) 阿里云 Coding 套餐 - 旧版端点(兼容保留)\" ;;\n \"llm.alibaba.model.codingplan_legacy.en\") text=\" 3) Alibaba Cloud Coding Plan - legacy endpoint (backward compatible)\" ;;\n \"llm.alibaba.model.select.zh\") text=\"选择接入方式 [1/2/3]\" ;;\n \"llm.alibaba.model.select.en\") text=\"Select access option [1/2/3]\" ;;\n \"llm.alibaba.model.invalid.zh\") text=\"无效选择: %s(请输入 1、2 或 3)\" ;;\n \"llm.alibaba.model.invalid.en\") text=\"Invalid choice: %s (please enter 1, 2, or 3)\" ;;\n \"llm.codingplan.models_title.zh\") text=\"选择通义 Token 套餐默认模型:\" ;;\n \"llm.codingplan.models_title.en\") text=\"Select Qwen Cloud default model:\" ;;\n \"llm.codingplan.model.qwen36plus.zh\") text=\" 1) qwen3.6-plus - 千问 3.6(推荐)\" ;;\n \"llm.codingplan.model.qwen36plus.en\") text=\" 1) qwen3.6-plus - Qwen 3.6 (recommended)\" ;;\n \"llm.codingplan.model.glm5.zh\") text=\" 2) glm-5 - 智谱 GLM-5(编程推荐)\" ;;\n \"llm.codingplan.model.glm5.en\") text=\" 2) glm-5 - Zhipu GLM-5 (recommended for coding)\" ;;\n \"llm.codingplan.model.kimi.zh\") text=\" 3) kimi-k2.5 - Moonshot Kimi K2.5\" ;;\n \"llm.codingplan.model.kimi.en\") text=\" 3) kimi-k2.5 - Moonshot Kimi K2.5\" ;;\n \"llm.codingplan.model.minimax.zh\") text=\" 4) MiniMax-M2.5 - MiniMax M2.5\" ;;\n \"llm.codingplan.model.minimax.en\") text=\" 4) MiniMax-M2.5 - MiniMax M2.5\" ;;\n \"llm.codingplan.model.select.zh\") text=\"选择模型 [1/2/3/4]\" ;;\n \"llm.codingplan.model.select.en\") text=\"Select model [1/2/3/4]\" ;;\n \"llm.provider.selected_tokenplan.zh\") text=\" 提供商: 阿里云通义 Token 套餐(兼容模式)\" ;;\n \"llm.provider.selected_tokenplan.en\") text=\" Provider: Alibaba Cloud Token Plan (compatible mode)\" ;;\n \"llm.provider.selected_codingplan.zh\") text=\" 提供商: 阿里云通义 Token 套餐(alibaba-cloud)\" ;;\n \"llm.provider.selected_codingplan.en\") text=\" Provider: Qwen Cloud (international) (alibaba-cloud)\" ;;\n \"llm.provider.selected_codingplan_legacy.zh\") text=\" 提供商: 阿里云 Coding 套餐(coding.dashscope)\" ;;\n \"llm.provider.selected_codingplan_legacy.en\") text=\" Provider: Alibaba Cloud Coding Plan (coding.dashscope)\" ;;\n \"llm.provider.selected_qwen.zh\") text=\" 提供商: 阿里云百炼\" ;;\n \"llm.provider.selected_qwen.en\") text=\" Provider: Alibaba Cloud Bailian\" ;;\n \"llm.provider.selected_openai.zh\") text=\" 提供商: %s(OpenAI 兼容)\" ;;\n \"llm.provider.selected_openai.en\") text=\" Provider: %s (OpenAI-compatible)\" ;;\n \"llm.provider.invalid.zh\") text=\"无效选择: %s(请输入 1 或 2)\" ;;\n \"llm.provider.invalid.en\") text=\"Invalid choice: %s (please enter 1 or 2)\" ;;\n \"llm.qwen.model_prompt.zh\") text=\"默认模型 ID\" ;;\n \"llm.qwen.model_prompt.en\") text=\"Default Model ID\" ;;\n \"llm.openai.base_url_prompt.zh\") text=\"Base URL\" ;;\n \"llm.openai.base_url_prompt.en\") text=\"Base URL\" ;;\n \"llm.openai.model_prompt.zh\") text=\"默认模型 ID\" ;;\n \"llm.openai.model_prompt.en\") text=\"Default Model ID\" ;;\n \"llm.openai.base_url_label.zh\") text=\" Base URL: %s\" ;;\n \"llm.openai.base_url_label.en\") text=\" Base URL: %s\" ;;\n # --- Custom model parameters ---\n \"llm.custom_model.detected.zh\") text=\" ⚠️ 模型 '%s' 不在内置模型列表中,请配置模型参数:\" ;;\n \"llm.custom_model.detected.en\") text=\" ⚠️ Model '%s' is not in the built-in model list. Please configure model parameters:\" ;;\n \"llm.custom_model.context_prompt.zh\") text=\"最大上下文长度(token 数)[150000]\" ;;\n \"llm.custom_model.context_prompt.en\") text=\"Max context window (tokens) [150000]\" ;;\n \"llm.custom_model.max_tokens_prompt.zh\") text=\"最大输出长度(token 数)[128000]\" ;;\n \"llm.custom_model.max_tokens_prompt.en\") text=\"Max output tokens [128000]\" ;;\n \"llm.custom_model.reasoning_prompt.zh\") text=\"是否支持推理/思考模式?[Y/n]\" ;;\n \"llm.custom_model.reasoning_prompt.en\") text=\"Does it support reasoning/thinking mode? [Y/n]\" ;;\n \"llm.custom_model.vision_prompt.zh\") text=\"是否支持图片输入?[y/N]\" ;;\n \"llm.custom_model.vision_prompt.en\") text=\"Does it support image input? [y/N]\" ;;\n \"llm.custom_model.summary.zh\") text=\" 自定义模型参数: 上下文=%s, 最大输出=%s, 推理=%s, 图片=%s\" ;;\n \"llm.custom_model.summary.en\") text=\" Custom model params: context=%s, maxTokens=%s, reasoning=%s, vision=%s\" ;;\n # --- Admin Credentials ---\n \"admin.title.zh\") text=\"--- 管理员凭据 ---\" ;;\n \"admin.title.en\") text=\"--- Admin Credentials ---\" ;;\n \"admin.username_prompt.zh\") text=\"管理员用户名\" ;;\n \"admin.username_prompt.en\") text=\"Admin Username\" ;;\n \"admin.password_prompt.zh\") text=\"管理员密码(留空自动生成,最少 8 位)\" ;;\n \"admin.password_prompt.en\") text=\"Admin Password (leave empty to auto-generate, min 8 chars)\" ;;\n \"admin.password_generated.zh\") text=\" 已自动生成管理员密码\" ;;\n \"admin.password_generated.en\") text=\" Auto-generated admin password\" ;;\n \"admin.password_too_short.zh\") text=\"管理员密码至少需要 8 个字符(MinIO 要求)。当前长度: %s\" ;;\n \"admin.password_too_short.en\") text=\"Admin password must be at least 8 characters (MinIO requirement). Current length: %s\" ;;\n # --- Port Configuration ---\n \"port.title.zh\") text=\"--- 端口配置(按回车使用默认值)---\" ;;\n \"port.title.en\") text=\"--- Port Configuration (press Enter for defaults) ---\" ;;\n \"port.gateway_prompt.zh\") text=\"网关主机端口(容器内 8080)\" ;;\n \"port.gateway_prompt.en\") text=\"Host port for gateway (8080 inside container)\" ;;\n \"port.console_prompt.zh\") text=\"Higress 控制台主机端口(容器内 8001)\" ;;\n \"port.console_prompt.en\") text=\"Host port for Higress console (8001 inside container)\" ;;\n \"port.element_prompt.zh\") text=\"Element Web 直接访问主机端口(容器内 8088)\" ;;\n \"port.element_prompt.en\") text=\"Host port for Element Web direct access (8088 inside container)\" ;;\n \"port.manager_console_prompt.zh\") text=\"Manager 控制台主机端口(容器内 18888)\" ;;\n \"port.manager_console_prompt.en\") text=\"Host port for Manager console (18888 inside container)\" ;;\n \"port.copaw_app_prompt.zh\") text=\"CoPaw App API 主机端口(容器内 18799)\" ;;\n \"port.copaw_app_prompt.en\") text=\"Host port for CoPaw App API (18799 inside container)\" ;;\n # --- Local-only binding ---\n \"port.local_only.title.zh\") text=\"--- 网络访问模式 ---\" ;;\n \"port.local_only.title.en\") text=\"--- Network Access Mode ---\" ;;\n \"port.local_only.prompt.zh\") text=\"是否仅允许本机访问(端口绑定到 127.0.0.1)?\" ;;\n \"port.local_only.prompt.en\") text=\"Bind ports to localhost only (127.0.0.1)?\" ;;\n \"port.local_only.hint_yes.zh\") text=\" 仅本机使用,无需开放外部端口(推荐)\" ;;\n \"port.local_only.hint_yes.en\") text=\" Local use only, no external port exposure (recommended)\" ;;\n \"port.local_only.hint_no.zh\") text=\" 允许外部访问(局域网 / 公网)\" ;;\n \"port.local_only.hint_no.en\") text=\" Allow external access (LAN / public network)\" ;;\n \"port.local_only.choice.zh\") text=\"请选择 [1/2]\" ;;\n \"port.local_only.choice.en\") text=\"Enter choice [1/2]\" ;;\n \"port.local_only.selected_local.zh\") text=\"端口已绑定到 127.0.0.1(仅本机访问)\" ;;\n \"port.local_only.selected_local.en\") text=\"Ports bound to 127.0.0.1 (localhost only)\" ;;\n \"port.local_only.selected_external.zh\") text=\"端口已绑定到所有网络接口(0.0.0.0)\" ;;\n \"port.local_only.selected_external.en\") text=\"Ports bound to all interfaces (0.0.0.0)\" ;;\n \"port.local_only.https_hint.zh\") text=\"⚠️ 建议在 Higress 控制台配置 TLS 证书并启用 HTTPS,避免明文传输。\" ;;\n \"port.local_only.https_hint.en\") text=\"⚠️ It is recommended to configure TLS certificates and enable HTTPS in the Higress Console to avoid plaintext transmission.\" ;;\n \"port.local_only.https_docs.zh\") text=\"\" ;;\n \"port.local_only.https_docs.en\") text=\"\" ;;\n # --- Domain Configuration ---\n \"domain.title.zh\") text=\"--- 域名配置(按回车使用默认值)---\" ;;\n \"domain.title.en\") text=\"--- Domain Configuration (press Enter for defaults) ---\" ;;\n \"domain.hint.zh\") text=\"提示: 自定义域名前必须事先做好 DNS 解析。单机 ECS 部署时无需修改 aigw、fs 等域名;Element Web 和 Matrix Server 也可通过 IP 直接访问。\" ;;\n \"domain.hint.en\") text=\"Hint: Configure DNS resolution before customizing domains. For single ECS deployment, no need to change aigw, fs, etc.; Element Web and Matrix Server can also be accessed directly via IP.\" ;;\n \"domain.matrix_prompt.zh\") text=\"Matrix 域名\" ;;\n \"domain.matrix_prompt.en\") text=\"Matrix Domain\" ;;\n \"domain.element_prompt.zh\") text=\"Element Web 域名\" ;;\n \"domain.element_prompt.en\") text=\"Element Web Domain\" ;;\n \"domain.gateway_prompt.zh\") text=\"AI 网关域名\" ;;\n \"domain.gateway_prompt.en\") text=\"AI Gateway Domain\" ;;\n \"domain.fs_prompt.zh\") text=\"文件系统域名\" ;;\n \"domain.fs_prompt.en\") text=\"File System Domain\" ;;\n \"domain.console_prompt.zh\") text=\"Manager 控制台域名\" ;;\n \"domain.console_prompt.en\") text=\"Manager Console Domain\" ;;\n # --- GitHub Integration ---\n \"github.title.zh\") text=\"--- GitHub 集成(可选,按回车跳过)---\" ;;\n \"github.title.en\") text=\"--- GitHub Integration (optional, press Enter to skip) ---\" ;;\n \"github.token_prompt.zh\") text=\"GitHub 个人访问令牌(可选)\" ;;\n \"github.token_prompt.en\") text=\"GitHub Personal Access Token (optional)\" ;;\n # --- Skills Registry ---\n \"skills.title.zh\") text=\"--- Skills 注册中心(可选,按回车使用默认 nacos://market.agentteams.io:80/public)---\" ;;\n \"skills.title.en\") text=\"--- Skills Registry (optional, press Enter for default nacos://market.agentteams.io:80/public) ---\" ;;\n \"skills.url_prompt.zh\") text=\"Skills 注册中心 URL(留空使用默认 nacos://market.agentteams.io:80/public)\" ;;\n \"skills.url_prompt.en\") text=\"Skills Registry URL (leave empty for default nacos://market.agentteams.io:80/public)\" ;;\n # --- Data Persistence ---\n \"data.title.zh\") text=\"--- 数据持久化 ---\" ;;\n \"data.title.en\") text=\"--- Data Persistence ---\" ;;\n \"data.volume_prompt.zh\") text=\"Docker 卷名称 [agentteams-data]\" ;;\n \"data.volume_prompt.en\") text=\"Docker volume name for persistent data [agentteams-data]\" ;;\n \"data.volume_using.zh\") text=\" 使用 Docker 卷: %s\" ;;\n \"data.volume_using.en\") text=\" Using Docker volume: %s\" ;;\n # --- Manager Workspace ---\n \"workspace.title.zh\") text=\"--- Manager 工作空间 ---\" ;;\n \"workspace.title.en\") text=\"--- Manager Workspace ---\" ;;\n \"workspace.dir_prompt.zh\") text=\"Manager 工作空间目录 [%s]\" ;;\n \"workspace.dir_prompt.en\") text=\"Manager workspace directory [%s]\" ;;\n \"workspace.dir_label.zh\") text=\" Manager 工作空间: %s\" ;;\n \"workspace.dir_label.en\") text=\" Manager workspace: %s\" ;;\n # --- Host directory sharing ---\n \"host_share.prompt.zh\") text=\"与 Agent 共享的主机目录(默认: %s)\" ;;\n \"host_share.prompt.en\") text=\"Host directory to share with agents (default: %s)\" ;;\n \"host_share.sharing.zh\") text=\"共享主机目录: %s -> 容器内 /host-share\" ;;\n \"host_share.sharing.en\") text=\"Sharing host directory: %s -> /host-share in container\" ;;\n \"host_share.not_exist.zh\") text=\"警告: 主机目录 %s 不存在,跳过验证继续使用\" ;;\n \"host_share.not_exist.en\") text=\"WARNING: Host directory %s does not exist, using without validation\" ;;\n # --- Default worker runtime ---\n \"worker_runtime.title.zh\") text=\"--- 默认 Worker 运行时 ---\" ;;\n \"worker_runtime.title.en\") text=\"--- Default Worker Runtime ---\" ;;\n \"worker_runtime.openclaw.zh\") text=\"OpenClaw\" ;;\n \"worker_runtime.openclaw.en\") text=\"OpenClaw\" ;;\n \"worker_runtime.copaw.zh\") text=\"CoPaw\" ;;\n \"worker_runtime.copaw.en\") text=\"CoPaw\" ;;\n \"worker_runtime.hermes.zh\") text=\"Hermes\" ;;\n \"worker_runtime.hermes.en\") text=\"Hermes\" ;;\n \"worker_runtime.choice.zh\") text=\"请选择 [1/2/3]\" ;;\n \"worker_runtime.choice.en\") text=\"Enter choice [1/2/3]\" ;;\n \"worker_runtime.selected.zh\") text=\"默认 Worker 运行时: %s\" ;;\n \"worker_runtime.selected.en\") text=\"Default Worker runtime: %s\" ;;\n \"worker_runtime.title_short.zh\") text=\"默认 Worker 运行时\" ;;\n \"worker_runtime.title_short.en\") text=\"Default Worker Runtime\" ;;\n \"manager_runtime.title.zh\") text=\"--- Manager 运行时 ---\" ;;\n \"manager_runtime.title.en\") text=\"--- Manager Runtime ---\" ;;\n \"manager_runtime.openclaw.zh\") text=\"OpenClaw\" ;;\n \"manager_runtime.openclaw.en\") text=\"OpenClaw\" ;;\n \"manager_runtime.copaw.zh\") text=\"CoPaw\" ;;\n \"manager_runtime.copaw.en\") text=\"CoPaw\" ;;\n \"manager_runtime.choice.zh\") text=\"请选择 [1/2]\" ;;\n \"manager_runtime.choice.en\") text=\"Enter choice [1/2]\" ;;\n \"manager_runtime.selected.zh\") text=\"Manager 运行时: %s\" ;;\n \"manager_runtime.selected.en\") text=\"Manager runtime: %s\" ;;\n \"manager_runtime.title_short.zh\") text=\"Manager 运行时\" ;;\n \"manager_runtime.title_short.en\") text=\"Manager Runtime\" ;;\n # --- Secrets and config ---\n \"install.generating_secrets.zh\") text=\"正在生成密钥...\" ;;\n \"install.generating_secrets.en\") text=\"Generating secrets...\" ;;\n \"install.config_saved.zh\") text=\"配置已保存到 %s\" ;;\n \"install.config_saved.en\") text=\"Configuration saved to %s\" ;;\n # --- Container runtime socket ---\n \"install.socket_detected.zh\") text=\"容器运行时 socket: %s(已启用直接创建 Worker)\" ;;\n \"install.socket_detected.en\") text=\"Container runtime socket: %s (direct Worker creation enabled)\" ;;\n \"install.socket_not_found.zh\") text=\"未找到容器运行时 socket(Manager 无法直接创建 Worker 容器,需要你手动执行 docker 命令创建)\" ;;\n \"install.socket_not_found.en\") text=\"No container runtime socket found (Manager cannot create Worker containers directly, you will need to create them manually using docker commands)\" ;;\n \"install.socket_confirm.title.zh\") text=\"⚠️ 未检测到容器运行时 Socket\" ;;\n \"install.socket_confirm.title.en\") text=\"⚠️ Container Runtime Socket Not Detected\" ;;\n \"install.socket_confirm.message.zh\") text=\"未找到 Docker/Podman socket,Manager 将无法自动创建 Worker 容器。\\n你需要手动执行 docker run 命令来创建 Worker。\\n\\n是否继续安装?\" ;;\n \"install.socket_confirm.message.en\") text=\"Docker/Podman socket not found. Manager will not be able to create Worker containers automatically.\\nYou will need to manually run docker commands to create Workers.\\n\\nContinue installation?\" ;;\n \"install.socket_confirm.prompt.zh\") text=\"继续安装? [y/N]: \" ;;\n \"install.socket_confirm.prompt.en\") text=\"Continue? [y/N]: \" ;;\n \"install.socket_confirm.cancelled.zh\") text=\"安装已取消。如需启用 Worker 自动创建,请确保 Docker/Podman 正在运行,然后重新运行安装脚本。\" ;;\n \"install.socket_confirm.cancelled.en\") text=\"Installation cancelled. To enable automatic Worker creation, ensure Docker/Podman is running and re-run the installer.\" ;;\n # --- Container management ---\n \"install.removing_existing.zh\") text=\"正在移除现有 agentteams-manager 容器...\" ;;\n \"install.removing_existing.en\") text=\"Removing existing agentteams-manager container...\" ;;\n # --- Matrix E2EE ---\n \"matrix_e2ee.title.zh\") text=\"--- Matrix 端到端加密(E2EE)---\" ;;\n \"matrix_e2ee.title.en\") text=\"--- Matrix End-to-End Encryption (E2EE) ---\" ;;\n \"matrix_e2ee.desc.zh\") text=\"E2EE 会对 Manager 与 Worker 之间的 Matrix 消息进行端到端加密。\\n 启用后,即使 Matrix 服务器被入侵,消息内容也无法被窃取。\\n 但 E2EE 会增加首次握手耗时,且要求所有 Agent 都支持 matrix-sdk-crypto。\\n 如果不确定,建议保持禁用。\\n ⚠ 注意:禁用 E2EE 后,请勿在 Element 上创建默认启用加密的 Private 房间,\\n 否则 Agent 将无法读取该房间中的加密消息。请改用 Public 房间或关闭房间加密。\" ;;\n \"matrix_e2ee.desc.en\") text=\"E2EE encrypts Matrix messages between Manager and Workers end-to-end.\\n When enabled, message content stays private even if the Matrix server is compromised.\\n However, E2EE adds overhead to the initial handshake and requires all Agents\\n to support matrix-sdk-crypto. If unsure, keep it disabled.\\n ⚠ Note: When E2EE is disabled, do NOT create Private rooms in Element (which\\n enable encryption by default) — Agents cannot read encrypted messages without\\n E2EE support. Use Public rooms or turn off room encryption instead.\" ;;\n \"matrix_e2ee.enable.zh\") text=\"启用 E2EE\" ;;\n \"matrix_e2ee.enable.en\") text=\"Enable E2EE\" ;;\n \"matrix_e2ee.disable.zh\") text=\"禁用 E2EE(推荐)\" ;;\n \"matrix_e2ee.disable.en\") text=\"Disable E2EE (recommended)\" ;;\n \"matrix_e2ee.choice.zh\") text=\"请选择 [1/2]\" ;;\n \"matrix_e2ee.choice.en\") text=\"Enter choice [1/2]\" ;;\n \"matrix_e2ee.selected_enabled.zh\") text=\"Matrix E2EE: 已启用\" ;;\n \"matrix_e2ee.selected_enabled.en\") text=\"Matrix E2EE: enabled\" ;;\n \"matrix_e2ee.selected_disabled.zh\") text=\"Matrix E2EE: 已禁用(默认)\" ;;\n \"matrix_e2ee.selected_disabled.en\") text=\"Matrix E2EE: disabled (default)\" ;;\n \"matrix_e2ee.title_short.zh\") text=\"Matrix E2EE\" ;;\n \"matrix_e2ee.title_short.en\") text=\"Matrix E2EE\" ;;\n \"matrix_e2ee.val_enabled.zh\") text=\"已启用\" ;;\n \"matrix_e2ee.val_enabled.en\") text=\"enabled\" ;;\n \"matrix_e2ee.val_disabled.zh\") text=\"已禁用\" ;;\n \"matrix_e2ee.val_disabled.en\") text=\"disabled\" ;;\n # --- Docker API proxy ---\n \"docker_proxy.title.zh\") text=\"--- Docker API 安全代理 ---\" ;;\n \"docker_proxy.title.en\") text=\"--- Docker API Security Proxy ---\" ;;\n \"docker_proxy.desc.zh\") text=\"Docker API 代理可防止 AI Agent 通过 Docker API 越狱访问宿主机。\\n 启用后,Manager 不再直接持有 Docker socket,所有容器操作经过安全校验。\" ;;\n \"docker_proxy.desc.en\") text=\"Docker API proxy prevents AI Agents from escaping via Docker API to access the host.\\n When enabled, Manager no longer has direct Docker socket access; all container operations go through security validation.\" ;;\n \"docker_proxy.enable.zh\") text=\"启用(推荐)\" ;;\n \"docker_proxy.enable.en\") text=\"Enable (recommended)\" ;;\n \"docker_proxy.disable.zh\") text=\"禁用(直接挂载 Docker socket)\" ;;\n \"docker_proxy.disable.en\") text=\"Disable (mount Docker socket directly)\" ;;\n \"docker_proxy.choice.zh\") text=\"请选择 [1/2]\" ;;\n \"docker_proxy.choice.en\") text=\"Enter choice [1/2]\" ;;\n \"docker_proxy.selected_enabled.zh\") text=\"Docker API 代理: 已启用\" ;;\n \"docker_proxy.selected_enabled.en\") text=\"Docker API proxy: enabled\" ;;\n \"docker_proxy.selected_disabled.zh\") text=\"Docker API 代理: 已禁用\" ;;\n \"docker_proxy.selected_disabled.en\") text=\"Docker API proxy: disabled\" ;;\n \"docker_proxy.title_short.zh\") text=\"Docker API 代理\" ;;\n \"docker_proxy.title_short.en\") text=\"Docker API Proxy\" ;;\n \"docker_proxy.val_enabled.zh\") text=\"已启用\" ;;\n \"docker_proxy.val_enabled.en\") text=\"enabled\" ;;\n \"docker_proxy.val_disabled.zh\") text=\"已禁用\" ;;\n \"docker_proxy.val_disabled.en\") text=\"disabled\" ;;\n \"docker_proxy.registries_desc.zh\") text=\"默认放行的镜像来源:本地镜像、localhost、Higress 仓库(所有 region)。\\n 如需放行其他镜像仓库,请输入逗号分隔的地址前缀。\\n 示例: ghcr.io/myorg,registry.example.com/team\" ;;\n \"docker_proxy.registries_desc.en\") text=\"Default allowed image sources: local images, localhost, Higress registries (all regions).\\n To allow additional image sources, enter comma-separated address prefixes.\\n Example: ghcr.io/myorg,registry.example.com/team\" ;;\n \"docker_proxy.registries_prompt.zh\") text=\"额外放行的镜像来源(按回车跳过)\" ;;\n \"docker_proxy.registries_prompt.en\") text=\"Additional allowed image sources (press Enter to skip)\" ;;\n \"docker_proxy.registries_label.zh\") text=\"额外放行的镜像来源\" ;;\n \"docker_proxy.registries_label.en\") text=\"Additional allowed image sources\" ;;\n # --- Podman Autostart ---\n \"podman.autostart.title.zh\") text=\"--- Podman 驻留与开机自启 ---\" ;;\n \"podman.autostart.title.en\") text=\"--- Podman Linger & Autostart ---\" ;;\n \"podman.autostart.desc.zh\") text=\"配置为 systemd 服务,支持开机自动启动及后台驻留。\" ;;\n \"podman.autostart.desc.en\") text=\"Configure as a systemd service for automatic startup on boot.\" ;;\n \"podman.autostart.enable.zh\") text=\"启用(推荐)\" ;;\n \"podman.autostart.enable.en\") text=\"Enable (recommended)\" ;;\n \"podman.autostart.disable.zh\") text=\"不启用(容器随当前会话关闭而停止)\" ;;\n \"podman.autostart.disable.en\") text=\"Disable (containers stop when session ends)\" ;;\n \"podman.autostart.choice.zh\") text=\"请选择 [1/2]\" ;;\n \"podman.autostart.choice.en\") text=\"Enter choice [1/2]\" ;;\n \"podman.autostart.selected_enabled.zh\") text=\"Podman 开机自启: 已启用\" ;;\n \"podman.autostart.selected_enabled.en\") text=\"Podman Autostart: enabled\" ;;\n \"podman.autostart.selected_disabled.zh\") text=\"Podman 开机自启: 已禁用\" ;;\n \"podman.autostart.selected_disabled.en\") text=\"Podman Autostart: disabled\" ;;\n \"podman.autostart.title_short.zh\") text=\"Podman 开机自启\" ;;\n \"podman.autostart.title_short.en\") text=\"Podman Autostart\" ;;\n \"podman.autostart.val_enabled.zh\") text=\"已启用\" ;;\n \"podman.autostart.val_enabled.en\") text=\"enabled\" ;;\n \"podman.autostart.val_disabled.zh\") text=\"已禁用\" ;;\n \"podman.autostart.val_disabled.en\") text=\"disabled\" ;;\n \"install.podman.autostart_title.zh\") text=\"正在配置 Podman 容器开机自启...\" ;;\n \"install.podman.autostart_title.en\") text=\"Configuring Podman container autostart...\" ;;\n \"install.podman.linger_enable.zh\") text=\"尝试启用 systemd linger (如有提示请在此输入 sudo 密码)...\" ;;\n \"install.podman.linger_enable.en\") text=\"Attempting to enable systemd linger (enter sudo password if prompted)...\" ;;\n \"install.podman.linger_warn.zh\") text=\"警告: 无法自动启用 linger。驻留可能失败,请手动执行: sudo loginctl enable-linger \\$(whoami)\" ;;\n \"install.podman.linger_warn.en\") text=\"WARNING: Failed to auto-enable linger. Please run manually: sudo loginctl enable-linger \\$(whoami)\" ;;\n \"install.podman.root_setup.zh\") text=\"检测到 root 用户。正在配置系统级 agentteams-podman-restart 服务...\" ;;\n \"install.podman.root_setup.en\") text=\"Root user detected. Configuring system-wide agentteams-podman-restart service...\" ;;\n \"install.podman.root_success.zh\") text=\"✅ 系统级 agentteams-podman-restart 服务已成功启用并运行。\" ;;\n \"install.podman.root_success.en\") text=\"✅ System-wide agentteams-podman-restart service successfully enabled and running.\" ;;\n \"install.podman.root_fail.zh\") text=\"⚠️ 系统级 agentteams-podman-restart 服务启用失败,请稍后手动检查。\" ;;\n \"install.podman.root_fail.en\") text=\"⚠️ Failed to enable system-wide agentteams-podman-restart service. Please check manually later.\" ;;\n \"install.podman.user_setup.zh\") text=\"检测到普通用户 (%s)。正在配置用户级 agentteams-podman-restart 服务...\" ;;\n \"install.podman.user_setup.en\") text=\"Non-root user (%s) detected. Configuring user-level agentteams-podman-restart service...\" ;;\n \"install.podman.user_success.zh\") text=\"✅ 用户级 agentteams-podman-restart 服务已成功启用并运行。\" ;;\n \"install.podman.user_success.en\") text=\"✅ User-level agentteams-podman-restart service successfully enabled and running.\" ;;\n \"install.podman.user_fail.zh\") text=\"⚠️ 用户级 agentteams-podman-restart 服务启用失败。\\n提示:这通常是因为缺少 XDG_RUNTIME_DIR 或 dbus 没有运行。您可以尝试手动执行: systemctl --user enable --now agentteams-podman-restart.service\" ;;\n \"install.podman.user_fail.en\") text=\"⚠️ Failed to enable user-level agentteams-podman-restart service.\\nHint: This is usually due to missing XDG_RUNTIME_DIR or dbus not running. Try manually: systemctl --user enable --now agentteams-podman-restart.service\" ;;\n \"install.podman.success.zh\") text=\"Podman 开机自启配置完成。\" ;;\n \"install.podman.success.en\") text=\"Podman autostart successfully configured.\" ;;\n # --- Worker idle timeout ---\n \"idle_timeout.prompt.zh\") text=\"Worker 空闲自动停止超时(分钟)[720]\" ;;\n \"idle_timeout.prompt.en\") text=\"Worker idle auto-stop timeout in minutes [720]\" ;;\n \"idle_timeout.selected.zh\") text=\"Worker 空闲超时: %s 分钟\" ;;\n \"idle_timeout.selected.en\") text=\"Worker idle timeout: %s minutes\" ;;\n \"idle_timeout.label.zh\") text=\"Worker 空闲超时(分钟)\" ;;\n \"idle_timeout.label.en\") text=\"Worker idle timeout (min)\" ;;\n # --- YOLO mode ---\n \"install.yolo.zh\") text=\"YOLO 模式已启用(自主决策,无交互提示)\" ;;\n \"install.yolo.en\") text=\"YOLO mode enabled (autonomous decisions, no interactive prompts)\" ;;\n # --- Image pulling ---\n \"install.image.exists.zh\") text=\"Manager 镜像已存在: %s\" ;;\n \"install.image.exists.en\") text=\"Manager image already exists locally: %s\" ;;\n \"install.image.pulling_manager.zh\") text=\"正在拉取 Manager 镜像: %s\" ;;\n \"install.image.pulling_manager.en\") text=\"Pulling Manager image: %s\" ;;\n \"install.image.worker_exists.zh\") text=\"Worker 镜像已存在: %s\" ;;\n \"install.image.worker_exists.en\") text=\"Worker image already exists locally: %s\" ;;\n \"install.image.pulling_worker.zh\") text=\"正在拉取 Worker 镜像: %s\" ;;\n \"install.image.pulling_worker.en\") text=\"Pulling Worker image: %s\" ;;\n # --- Starting container ---\n \"install.starting_manager.zh\") text=\"正在启动 Manager 容器...\" ;;\n \"install.starting_manager.en\") text=\"Starting Manager container...\" ;;\n # --- Wait for Manager ready ---\n \"install.wait_ready.zh\") text=\"等待 Manager Agent 就绪(超时: %ss)...\" ;;\n \"install.wait_ready.en\") text=\"Waiting for Manager agent to be ready (timeout: %ss)...\" ;;\n \"install.wait_ready.ok.zh\") text=\"Manager Agent 已就绪!\" ;;\n \"install.wait_ready.ok.en\") text=\"Manager agent is ready!\" ;;\n \"install.wait_ready.waiting.zh\") text=\"等待中... (%ds/%ds)\" ;;\n \"install.wait_ready.waiting.en\") text=\"Waiting... (%ds/%ds)\" ;;\n \"install.wait_ready.timeout.zh\") text=\"Manager Agent 在 %ss 内未就绪。请检查: docker logs %s\" ;;\n \"install.wait_ready.timeout.en\") text=\"Manager agent did not become ready within %ss. Check: docker logs %s\" ;;\n # --- Wait for Matrix ready ---\n \"install.wait_matrix.zh\") text=\"等待 Matrix 服务就绪(超时: %ss)...\" ;;\n \"install.wait_matrix.en\") text=\"Waiting for Matrix server to be ready (timeout: %ss)...\" ;;\n \"install.wait_matrix.ok.zh\") text=\"Matrix 服务已就绪!\" ;;\n \"install.wait_matrix.ok.en\") text=\"Matrix server is ready!\" ;;\n \"install.wait_matrix.waiting.zh\") text=\"等待 Matrix 中... (%ds/%ds)\" ;;\n \"install.wait_matrix.waiting.en\") text=\"Waiting for Matrix... (%ds/%ds)\" ;;\n \"install.wait_matrix.timeout.zh\") text=\"Matrix 服务在 %ss 内未就绪。请检查: docker logs %s\" ;;\n \"install.wait_matrix.timeout.en\") text=\"Matrix server did not become ready within %ss. Check: docker logs %s\" ;;\n # --- OpenAI-compatible connectivity test ---\n \"llm.openai.test.testing.zh\") text=\"正在测试 API 联通性...\" ;;\n \"llm.openai.test.testing.en\") text=\"Testing API connectivity...\" ;;\n \"llm.openai.test.ok.zh\") text=\"✅ API 联通性测试通过\" ;;\n \"llm.openai.test.ok.en\") text=\"✅ API connectivity test passed\" ;;\n \"llm.openai.test.fail.zh\") text=\"⚠️ API 联通性测试失败(HTTP %s)。响应内容:\\n%s\\n请根据以上错误信息联系您的模型服务商解决。\" ;;\n \"llm.openai.test.fail.en\") text=\"⚠️ API connectivity test failed (HTTP %s). Response body:\\n%s\\nPlease contact your model provider to resolve the issue.\" ;;\n \"llm.openai.test.fail.tokenplan.zh\") text=\"⚠️ 提示: 请确认 API Key 有效且已开通通义 Token 套餐。文档: https://help.aliyun.com/zh/model-studio/token-plan-quickstart\" ;;\n \"llm.openai.test.fail.tokenplan.en\") text=\"⚠️ Hint: Verify your Token Plan API key and compatible-mode access. Docs: https://help.aliyun.com/zh/model-studio/token-plan-quickstart\" ;;\n \"llm.openai.test.fail.codingplan.zh\") text=\"⚠️ 提示: 请确认 API Key 有效且已开通通义 Token 套餐。文档: https://help.aliyun.com/zh/model-studio/token-plan-quickstart\" ;;\n \"llm.openai.test.fail.codingplan.en\") text=\"⚠️ Hint: Verify your DASHSCOPE_API_KEY for Qwen Cloud. API keys: https://home.qwencloud.com/api-keys Docs: https://docs.qwencloud.com/\" ;;\n \"llm.openai.test.fail.codingplan_legacy.zh\") text=\"⚠️ 提示: 请确认 API Key 有效且 Coding 套餐接口可用。文档: https://help.aliyun.com/zh/model-studio/get-api-key\" ;;\n \"llm.openai.test.fail.codingplan_legacy.en\") text=\"⚠️ Hint: Verify your DashScope API key and Coding Plan access. Docs: https://help.aliyun.com/zh/model-studio/get-api-key\" ;;\n \"llm.openai.test.no_curl.zh\") text=\"⚠️ 未找到 curl,跳过 API 联通性测试\" ;;\n \"llm.openai.test.no_curl.en\") text=\"⚠️ curl not found, skipping API connectivity test\" ;;\n \"llm.openai.test.confirm.zh\") text=\"是否仍要继续安装?[y/N/b] \" ;;\n \"llm.openai.test.confirm.en\") text=\"Continue with installation anyway? [y/N/b] \" ;;\n \"llm.embedding.title.zh\") text=\"📦 记忆搜索配置\" ;;\n \"llm.embedding.title.en\") text=\"📦 Memory Search Configuration\" ;;\n \"llm.embedding.hint.zh\") text=\" Embedding 模型可提升记忆搜索质量(语义匹配)。不启用也可正常使用记忆功能(关键词匹配)。\" ;;\n \"llm.embedding.hint.en\") text=\" Embedding model improves memory search quality (semantic matching). Memory still works without it (keyword matching).\" ;;\n \"llm.embedding.option.default.zh\") text=\" 1) text-embedding-v4(推荐)\" ;;\n \"llm.embedding.option.default.en\") text=\" 1) text-embedding-v4 (Recommended)\" ;;\n \"llm.embedding.option.custom.zh\") text=\" 2) 自定义 Embedding 模型\" ;;\n \"llm.embedding.option.custom.en\") text=\" 2) Custom embedding model\" ;;\n \"llm.embedding.option.disable.zh\") text=\" 3) 不启用\" ;;\n \"llm.embedding.option.disable.en\") text=\" 3) Do not enable\" ;;\n \"llm.embedding.select.zh\") text=\"选择\" ;;\n \"llm.embedding.select.en\") text=\"Select\" ;;\n \"llm.embedding.custom_prompt.zh\") text=\" Embedding 模型名称\" ;;\n \"llm.embedding.custom_prompt.en\") text=\" Embedding model name\" ;;\n \"llm.embedding.test.testing.zh\") text=\"正在测试 Embedding API 联通性...\" ;;\n \"llm.embedding.test.testing.en\") text=\"Testing Embedding API connectivity...\" ;;\n \"llm.embedding.test.ok.zh\") text=\"✅ Embedding API 联通性测试通过\" ;;\n \"llm.embedding.test.ok.en\") text=\"✅ Embedding API connectivity test passed\" ;;\n \"llm.embedding.test.fail.zh\") text=\"⚠️ Embedding API 测试失败(HTTP %s)。响应: %s\" ;;\n \"llm.embedding.test.fail.en\") text=\"⚠️ Embedding API test failed (HTTP %s). Response: %s\" ;;\n \"llm.embedding.auto_disabled.zh\") text=\"⚠️ Embedding 已自动禁用,记忆搜索将使用关键词匹配。您可以稍后在 agentteams-manager.env 中设置 AGENTTEAMS_EMBEDDING_MODEL 启用。\" ;;\n \"llm.embedding.auto_disabled.en\") text=\"⚠️ Embedding auto-disabled. Memory search will use keyword matching. You can enable it later in agentteams-manager.env by setting AGENTTEAMS_EMBEDDING_MODEL.\" ;;\n \"llm.embedding.disabled.zh\") text=\"ℹ️ Embedding 已禁用,记忆搜索将使用关键词匹配。\" ;;\n \"llm.embedding.disabled.en\") text=\"ℹ️ Embedding disabled. Memory search will use keyword matching.\" ;;\n \"llm.openai.test.aborted.zh\") text=\"安装已中止。\" ;;\n \"llm.openai.test.aborted.en\") text=\"Installation aborted.\" ;;\n \"nav.back_hint.zh\") text=\"(输入 b 返回上一步)\" ;;\n \"nav.back_hint.en\") text=\"(enter b to go back)\" ;;\n # --- OpenAI-compatible provider creation ---\n \"install.openai_compat.missing.zh\") text=\"警告: OpenAI Base URL 或 API Key 未设置,跳过提供商创建\" ;;\n \"install.openai_compat.missing.en\") text=\"WARNING: OpenAI Base URL or API Key not set, skipping provider creation\" ;;\n \"install.openai_compat.creating.zh\") text=\"正在创建 OpenAI 兼容提供商...\" ;;\n \"install.openai_compat.creating.en\") text=\"Creating OpenAI-compatible provider...\" ;;\n \"install.openai_compat.domain.zh\") text=\" 域名: %s\" ;;\n \"install.openai_compat.domain.en\") text=\" Domain: %s\" ;;\n \"install.openai_compat.port.zh\") text=\" 端口: %s\" ;;\n \"install.openai_compat.port.en\") text=\" Port: %s\" ;;\n \"install.openai_compat.protocol.zh\") text=\" 协议: %s\" ;;\n \"install.openai_compat.protocol.en\") text=\" Protocol: %s\" ;;\n \"install.openai_compat.service_fail.zh\") text=\"警告: 创建 DNS 服务源失败(可能已存在)\" ;;\n \"install.openai_compat.service_fail.en\") text=\"WARNING: Failed to create DNS service source (may already exist)\" ;;\n \"install.openai_compat.provider_fail.zh\") text=\"警告: 创建 AI 提供商失败(可能已存在)\" ;;\n \"install.openai_compat.provider_fail.en\") text=\"WARNING: Failed to create AI provider (may already exist)\" ;;\n \"install.openai_compat.success.zh\") text=\"OpenAI 兼容提供商创建成功\" ;;\n \"install.openai_compat.success.en\") text=\"OpenAI-compatible provider created successfully\" ;;\n # --- Welcome message ---\n \"install.welcome_msg.soul_configured.zh\") text=\"Soul 已配置(找到 soul-configured 标记),跳过 onboarding 消息\" ;;\n \"install.welcome_msg.soul_configured.en\") text=\"Soul already configured (soul-configured marker found), skipping onboarding message\" ;;\n \"install.welcome_msg.logging_in.zh\") text=\"正在以 %s 身份登录以发送欢迎消息...\" ;;\n \"install.welcome_msg.logging_in.en\") text=\"Logging in as %s to send welcome message...\" ;;\n \"install.welcome_msg.login_failed.zh\") text=\"警告: 以 %s 身份登录失败,跳过欢迎消息\" ;;\n \"install.welcome_msg.login_failed.en\") text=\"WARNING: Failed to login as %s, skipping welcome message\" ;;\n \"install.welcome_msg.finding_room.zh\") text=\"正在查找与 Manager 的 DM 房间...\" ;;\n \"install.welcome_msg.finding_room.en\") text=\"Finding DM room with Manager...\" ;;\n \"install.welcome_msg.creating_room.zh\") text=\"正在创建与 Manager 的 DM 房间...\" ;;\n \"install.welcome_msg.creating_room.en\") text=\"Creating DM room with Manager...\" ;;\n \"install.welcome_msg.no_room.zh\") text=\"警告: 无法找到或创建与 Manager 的 DM 房间\" ;;\n \"install.welcome_msg.no_room.en\") text=\"WARNING: Could not find or create DM room with Manager\" ;;\n \"install.welcome_msg.waiting_join.zh\") text=\"等待 Manager 加入房间...\" ;;\n \"install.welcome_msg.waiting_join.en\") text=\"Waiting for Manager to join the room...\" ;;\n \"install.welcome_msg.sending.zh\") text=\"正在向 Manager 发送欢迎消息...\" ;;\n \"install.welcome_msg.sending.en\") text=\"Sending welcome message to Manager...\" ;;\n \"install.welcome_msg.send_failed.zh\") text=\"警告: 发送欢迎消息失败\" ;;\n \"install.welcome_msg.send_failed.en\") text=\"WARNING: Failed to send welcome message\" ;;\n \"install.welcome_msg.sent.zh\") text=\"欢迎消息已发送给 Manager\" ;;\n \"install.welcome_msg.sent.en\") text=\"Welcome message sent to Manager\" ;;\n \"install.welcome_msg.waiting.zh\") text=\"等待 Manager 发送欢迎消息(Higress 路由授权 + LLM 探活,约 45-90s)...\" ;;\n \"install.welcome_msg.waiting.en\") text=\"Waiting for Manager to send the welcome message (Higress route auth + LLM probe, ~45-90s)...\" ;;\n \"install.welcome_msg.confirmed.zh\") text=\"Manager 已确认发送欢迎消息(status.welcomeSent=true,用时 %ss)\" ;;\n \"install.welcome_msg.confirmed.en\") text=\"Manager confirmed welcome message sent (status.welcomeSent=true, %ss elapsed)\" ;;\n \"install.welcome_msg.timeout.zh\") text=\"警告: 在 %ss 内未观察到 Manager 发送欢迎消息(status.welcomeSent=true)。安装仍然成功,所有服务已就绪——可继续按下方提示登录 Element Web。\" ;;\n \"install.welcome_msg.timeout.en\") text=\"WARNING: Did not observe the Manager sending its welcome message (status.welcomeSent=true) within %ss. Installation is still successful, all services are up — continue with the Element Web instructions below.\" ;;\n \"install.welcome_msg.timeout_hint.zh\") text=\"手动触发 onboarding: 登录 Element Web → 打开与 Manager 的 DM 房间 → 发送任意一句话(例如 \\\"hi\\\"),Manager 会接管对话并开始引导。\" ;;\n \"install.welcome_msg.timeout_hint.en\") text=\"Manual onboarding: log in to Element Web → open the DM with the Manager → send any message (e.g. \\\"hi\\\") and the Manager will take over and start the guided setup.\" ;;\n \"install.welcome_msg.timeout_inspect.zh\") text=\"排查命令: docker exec agentteams-controller agt get managers default\" ;;\n \"install.welcome_msg.timeout_inspect.en\") text=\"Inspect status: docker exec agentteams-controller agt get managers default\" ;;\n \"install.welcome_msg.poll_unavailable.zh\") text=\"提示: agentteams-manager 内未找到 AgentTeams CLI,跳过 welcome 等待(旧镜像?)\" ;;\n \"install.welcome_msg.poll_unavailable.en\") text=\"Note: AgentTeams CLI not found inside agentteams-manager; skipping welcome wait (old image?)\" ;;\n # --- Dashboard wizard ---\n \"dash.prompt.zh\") text=\"是否安装 agentteams-dashboard 管理面板?\" ;;\n \"dash.prompt.en\") text=\"Install agentteams-dashboard management UI?\" ;;\n \"dash.version_prompt.zh\") text=\"Dashboard 版本\" ;;\n \"dash.version_prompt.en\") text=\"Dashboard version\" ;;\n \"dash.port_prompt.zh\") text=\"Dashboard 端口号\" ;;\n \"dash.port_prompt.en\") text=\"Dashboard port\" ;;\n \"dash.image_prompt.zh\") text=\"Dashboard 镜像\" ;;\n \"dash.image_prompt.en\") text=\"Dashboard image\" ;;\n \"dash.gateway_prompt.zh\") text=\"Higress Console URL (用于共享登录)\" ;;\n \"dash.gateway_prompt.en\") text=\"Higress Console URL (for shared login)\" ;;\n \"dash.skip.zh\") text=\"跳过 Dashboard 安装\" ;;\n \"dash.skip.en\") text=\"Skipping Dashboard installation\" ;;\n \"dash.auto_url.zh\") text=\"自动检测到 Higress Console URL\" ;;\n \"dash.auto_url.en\") text=\"Auto-detected Higress Console URL\" ;;\n \"dash.auto_url_fail.zh\") text=\"无法自动检测 Higress Console URL,请手动输入\" ;;\n \"dash.auto_url_fail.en\") text=\"Cannot auto-detect Higress Console URL, please enter manually\" ;;\n \"dash.summary.zh\") text=\"Dashboard 配置: 端口=%s 镜像=%s\" ;;\n \"dash.summary.en\") text=\"Dashboard config: port=%s image=%s\" ;;\n # --- Dashboard success ---\n \"success.dashboard.zh\") text=\" agentteams-dashboard 管理面板: http://localhost:%s/\" ;;\n \"success.dashboard.en\") text=\" agentteams-dashboard: http://localhost:%s/\" ;;\n # --- Final output panel ---\n \"success.title.zh\") text=\"=== AgentTeams Manager 已启动!===\" ;;\n \"success.title.en\") text=\"=== AgentTeams Manager Started! ===\" ;;\n \"success.domains_configured.zh\") text=\"以下域名已配置解析到 127.0.0.1:\" ;;\n \"success.domains_configured.en\") text=\"The following domains are configured to resolve to 127.0.0.1:\" ;;\n \"success.open_url.zh\") text=\" ★ 在浏览器中打开以下 URL 开始使用: ★\" ;;\n \"success.open_url.en\") text=\" ★ Open the following URL in your browser to start: ★\" ;;\n \"success.login_with.zh\") text=\" 登录信息:\" ;;\n \"success.login_with.en\") text=\" Login with:\" ;;\n \"success.username.zh\") text=\" 用户名: %s\" ;;\n \"success.username.en\") text=\" Username: %s\" ;;\n \"success.password.zh\") text=\" 密码: %s\" ;;\n \"success.password.en\") text=\" Password: %s\" ;;\n \"success.after_login.zh\") text=\" 登录后,开始与 Manager 聊天!\" ;;\n \"success.after_login.en\") text=\" After login, start chatting with the Manager!\" ;;\n \"success.tell_it.zh\") text=\" 告诉它: \\\"创建一个名为 alice 的前端开发 Worker\\\"\" ;;\n \"success.tell_it.en\") text=\" Tell it: \\\"Create a Worker named alice for frontend dev\\\"\" ;;\n \"success.manager_auto.zh\") text=\" Manager 会自动处理一切。\" ;;\n \"success.manager_auto.en\") text=\" The Manager will handle everything automatically.\" ;;\n \"success.mobile_title.zh\") text=\" 📱 移动端访问(FluffyChat / Element Mobile):\" ;;\n \"success.mobile_title.en\") text=\" 📱 Mobile access (FluffyChat / Element Mobile):\" ;;\n \"success.mobile_step1.zh\") text=\" 1. 在手机上下载 FluffyChat 或 Element\" ;;\n \"success.mobile_step1.en\") text=\" 1. Download FluffyChat or Element on your phone\" ;;\n \"success.mobile_step2.zh\") text=\" 2. 设置 homeserver 为: %s\" ;;\n \"success.mobile_step2.en\") text=\" 2. Set homeserver to: %s\" ;;\n \"success.mobile_step2_noip.zh\") text=\" 2. 设置 homeserver 为: http://<本机局域网IP>:%s\" ;;\n \"success.mobile_step2_noip.en\") text=\" 2. Set homeserver to: http://<this-machine-LAN-IP>:%s\" ;;\n \"success.mobile_noip_hint.zh\") text=\" (无法自动检测局域网 IP — 请使用 ifconfig / ip addr 查看)\" ;;\n \"success.mobile_noip_hint.en\") text=\" (Could not detect LAN IP automatically — check with: ifconfig / ip addr)\" ;;\n \"success.mobile_step3.zh\") text=\" 3. 登录信息:\" ;;\n \"success.mobile_step3.en\") text=\" 3. Login with:\" ;;\n \"success.mobile_username.zh\") text=\" 用户名: %s\" ;;\n \"success.mobile_username.en\") text=\" Username: %s\" ;;\n \"success.mobile_password.zh\") text=\" 密码: %s\" ;;\n \"success.mobile_password.en\") text=\" Password: %s\" ;;\n # --- Other consoles and tips ---\n \"success.other_consoles.zh\") text=\"--- 其他控制台 ---\" ;;\n \"success.other_consoles.en\") text=\"--- Other Consoles ---\" ;;\n \"success.higress_console.zh\") text=\" Higress 控制台: http://localhost:%s(用户名: %s / 密码: %s)\" ;;\n \"success.higress_console.en\") text=\" Higress Console: http://localhost:%s (Username: %s / Password: %s)\" ;;\n \"success.manager_console.zh\") text=\" Manager 控制台(本地): http://localhost:%s(无需登录)\" ;;\n \"success.manager_console.en\") text=\" Manager Console (local): http://localhost:%s (no login required)\" ;;\n \"success.manager_console_gateway.zh\") text=\" Manager 控制台(网关): http://console-local.agentteams.io(用户名: %s / 密码: %s)\" ;;\n \"success.manager_console_gateway.en\") text=\" Manager Console (gateway): http://console-local.agentteams.io (Username: %s / Password: %s)\" ;;\n \"success.copaw_console.zh\") text=\" CoPaw App API: http://localhost:%s(无需登录)\" ;;\n \"success.copaw_console.en\") text=\" CoPaw App API: http://localhost:%s (no login required)\" ;;\n \"success.switch_llm.title.zh\") text=\"--- 切换 LLM 提供商 ---\" ;;\n \"success.switch_llm.title.en\") text=\"--- Switch LLM Providers ---\" ;;\n \"success.switch_llm.hint.zh\") text=\" 您可以通过 Higress 控制台切换到其他 LLM 提供商(OpenAI、Anthropic 等)。\" ;;\n \"success.switch_llm.hint.en\") text=\" You can switch to other LLM providers (OpenAI, Anthropic, etc.) via Higress Console.\" ;;\n \"success.switch_llm.docs.zh\") text=\" 详细说明请参阅:\" ;;\n \"success.switch_llm.docs.en\") text=\" For detailed instructions, see:\" ;;\n \"success.switch_llm.url.zh\") text=\" https://higress.ai/en/docs/ai/scene-guide/multi-proxy#console-configuration\" ;;\n \"success.switch_llm.url.en\") text=\" https://higress.ai/en/docs/ai/scene-guide/multi-proxy#console-configuration\" ;;\n \"success.tip.zh\") text=\"提示: 您也可以在聊天中让 Manager 为您配置 LLM 提供商。\" ;;\n \"success.tip.en\") text=\"Tip: You can also ask the Manager to configure LLM providers for you in the chat.\" ;;\n \"success.config_file.zh\") text=\"配置文件: %s\" ;;\n \"success.config_file.en\") text=\"Configuration file: %s\" ;;\n \"success.data_volume.zh\") text=\"数据卷: %s\" ;;\n \"success.data_volume.en\") text=\"Data volume: %s\" ;;\n \"success.workspace.zh\") text=\"Manager 工作空间: %s\" ;;\n \"success.workspace.en\") text=\"Manager workspace: %s\" ;;\n # --- Worker installation ---\n \"worker.resetting.zh\") text=\"正在重置 Worker: %s...\" ;;\n \"worker.resetting.en\") text=\"Resetting Worker: %s...\" ;;\n \"worker.exists.zh\") text=\"容器 '%s' 已存在。使用 --reset 重新创建。\" ;;\n \"worker.exists.en\") text=\"Container '%s' already exists. Use --reset to recreate.\" ;;\n \"worker.starting.zh\") text=\"正在启动 Worker: %s...\" ;;\n \"worker.starting.en\") text=\"Starting Worker: %s...\" ;;\n \"worker.skills_url.zh\") text=\" Skills API URL: %s\" ;;\n \"worker.skills_url.en\") text=\" Skills API URL: %s\" ;;\n \"worker.started.zh\") text=\"=== Worker %s 已启动!===\" ;;\n \"worker.started.en\") text=\"=== Worker %s Started! ===\" ;;\n \"worker.container.zh\") text=\"容器: %s\" ;;\n \"worker.container.en\") text=\"Container: %s\" ;;\n \"worker.view_logs.zh\") text=\"查看日志: docker logs -f %s\" ;;\n \"worker.view_logs.en\") text=\"View logs: docker logs -f %s\" ;;\n # --- Prompt function messages ---\n \"prompt.preset.zh\") text=\" %s = (已通过环境变量预设)\" ;;\n \"prompt.preset.en\") text=\" %s = (pre-set via env)\" ;;\n \"prompt.upgrade_keep.zh\") text=\" %s = %s(当前值,回车保留 / 输入新值覆盖)\" ;;\n \"prompt.upgrade_keep.en\") text=\" %s = %s (current value, press Enter to keep / type new value to change)\" ;;\n \"prompt.upgrade_keep_secret.zh\") text=\" %s = %s(当前值,回车保留 / 输入新值覆盖)\" ;;\n \"prompt.upgrade_keep_secret.en\") text=\" %s = %s (current value, press Enter to keep / type new value to change)\" ;;\n \"prompt.upgrade_empty.zh\") text=\" %s = (未设置,回车跳过 / 输入新值设置)\" ;;\n \"prompt.upgrade_empty.en\") text=\" %s = (not set, press Enter to skip / type new value to set)\" ;;\n \"prompt.default.zh\") text=\" %s = %s(默认)\" ;;\n \"prompt.default.en\") text=\" %s = %s (default)\" ;;\n \"prompt.required.zh\") text=\"%s 是必需的(在非交互模式下通过环境变量设置)\" ;;\n \"prompt.required.en\") text=\"%s is required (set via environment variable in non-interactive mode)\" ;;\n \"prompt.required_empty.zh\") text=\"%s 是必需的\" ;;\n \"prompt.required_empty.en\") text=\"%s is required\" ;;\n # --- Language switch prompt (bilingual by design) ---\n \"lang.detected.zh\") text=\"检测到语言 / Detected language: 中文\" ;;\n \"lang.detected.en\") text=\"检测到语言 / Detected language: English\" ;;\n \"lang.switch_title.zh\") text=\"切换语言 / Switch language:\" ;;\n \"lang.switch_title.en\") text=\"切换语言 / Switch language:\" ;;\n \"lang.option_zh.zh\") text=\" 1) 中文\" ;;\n \"lang.option_zh.en\") text=\" 1) 中文\" ;;\n \"lang.option_en.zh\") text=\" 2) English\" ;;\n \"lang.option_en.en\") text=\" 2) English\" ;;\n \"lang.prompt.zh\") text=\"请选择 / Enter choice\" ;;\n \"lang.prompt.en\") text=\"请选择 / Enter choice\" ;;\n # --- Uninstall messages ---\n \"uninstall.title.zh\") text=\"正在卸载 AgentTeams...\" ;;\n \"uninstall.title.en\") text=\"Uninstalling AgentTeams...\" ;;\n \"uninstall.stopping_manager.zh\") text=\"正在停止并移除 agentteams-manager...\" ;;\n \"uninstall.stopping_manager.en\") text=\"Stopping and removing agentteams-manager...\" ;;\n \"uninstall.stopping_workers.zh\") text=\"正在停止并移除 Worker 容器...\" ;;\n \"uninstall.stopping_workers.en\") text=\"Stopping and removing worker containers...\" ;;\n \"uninstall.removed.zh\") text=\" 已移除: %s\" ;;\n \"uninstall.removed.en\") text=\" Removed: %s\" ;;\n \"uninstall.removing_volume.zh\") text=\"正在移除 Docker 卷: %s\" ;;\n \"uninstall.removing_volume.en\") text=\"Removing Docker volume: %s\" ;;\n \"uninstall.removing_env.zh\") text=\"正在移除 env 文件: %s\" ;;\n \"uninstall.removing_env.en\") text=\"Removing env file: %s\" ;;\n \"uninstall.removing_proxy.zh\") text=\"正在停止并移除 Docker API 代理容器: agentteams-docker-proxy\" ;;\n \"uninstall.removing_proxy.en\") text=\"Stopping and removing Docker API proxy container: agentteams-docker-proxy\" ;;\n \"uninstall.stopping_controller.zh\") text=\"正在停止并移除 agentteams-controller (内嵌 Tuwunel/MinIO/Higress)...\" ;;\n \"uninstall.stopping_controller.en\") text=\"Stopping and removing agentteams-controller (embedded Tuwunel/MinIO/Higress)...\" ;;\n \"uninstall.removing_network.zh\") text=\"正在移除 Docker 网络: agentteams-net\" ;;\n \"uninstall.removing_network.en\") text=\"Removing Docker network: agentteams-net\" ;;\n \"uninstall.removing_workspace.zh\") text=\"正在移除工作空间目录: %s\" ;;\n \"uninstall.removing_workspace.en\") text=\"Removing workspace directory: %s\" ;;\n \"uninstall.removing_workspace_elevated.zh\") text=\" 工作空间包含 root 文件,通过容器清理...\" ;;\n \"uninstall.removing_workspace_elevated.en\") text=\" Workspace contains root-owned files, cleaning via container...\" ;;\n \"uninstall.removing_log.zh\") text=\"正在移除日志文件: %s\" ;;\n \"uninstall.removing_log.en\") text=\"Removing log file: %s\" ;;\n \"uninstall.done.zh\") text=\"AgentTeams 已卸载。\" ;;\n \"uninstall.done.en\") text=\"AgentTeams has been uninstalled.\" ;;\n # --- Error messages ---\n \"error.name_required.zh\") text=\"--name 是必需的\" ;;\n \"error.name_required.en\") text=\"--name is required\" ;;\n \"error.fs_required.zh\") text=\"--fs 是必需的\" ;;\n \"error.fs_required.en\") text=\"--fs is required\" ;;\n \"error.fs_key_required.zh\") text=\"--fs-key 是必需的\" ;;\n \"error.fs_key_required.en\") text=\"--fs-key is required\" ;;\n \"error.fs_secret_required.zh\") text=\"--fs-secret 是必需的\" ;;\n \"error.fs_secret_required.en\") text=\"--fs-secret is required\" ;;\n \"error.unknown_option.zh\") text=\"未知选项: %s\" ;;\n \"error.unknown_option.en\") text=\"Unknown option: %s\" ;;\n \"error.docker_not_found.zh\") text=\"未找到 docker 或 podman 命令。请先安装 Docker Desktop 或 Podman Desktop:\\n Docker Desktop: https://www.docker.com/products/docker-desktop/\\n Podman Desktop: https://podman-desktop.io/\" ;;\n \"error.docker_not_found.en\") text=\"docker or podman command not found. Please install Docker Desktop or Podman Desktop first:\\n Docker Desktop: https://www.docker.com/products/docker-desktop/\\n Podman Desktop: https://podman-desktop.io/\" ;;\n \"error.docker_not_running.zh\") text=\"Docker 未运行。请先启动 Docker Desktop 或 Podman Desktop。\" ;;\n \"error.docker_not_running.en\") text=\"Docker is not running. Please start Docker Desktop or Podman Desktop first.\" ;;\n # --- Fallback: try English for unknown lang ---\n *)\n case \"${key}.en\" in\n \"tz.warning.title.en\") text=\"Could not detect timezone automatically.\" ;;\n \"install.title.en\") text=\"=== AgentTeams Manager Installation ===\" ;;\n *) text=\"${key}\" ;;\n esac\n ;;\n esac\n if [ $# -gt 0 ]; then\n # shellcheck disable=SC2059\n printf \"${text}\\n\" \"$@\"\n else\n echo \"${text}\"\n fi\n}\n\n# ============================================================\n# Registry selection based on timezone\n# ============================================================\n\ndetect_registry() {\n local tz=\"${AGENTTEAMS_TIMEZONE}\"\n\n case \"${tz}\" in\n America/*)\n echo \"higress-registry.us-west-1.cr.aliyuncs.com\"\n ;;\n Asia/Singapore|Asia/Bangkok|Asia/Jakarta|Asia/Makassar|Asia/Jayapura|\\\n Asia/Kuala_Lumpur|Asia/Ho_Chi_Minh|Asia/Manila|Asia/Yangon|\\\n Asia/Vientiane|Asia/Phnom_Penh|Asia/Pontianak|Asia/Ujung_Pandang)\n echo \"higress-registry.ap-southeast-7.cr.aliyuncs.com\"\n ;;\n *)\n echo \"higress-registry.cn-hangzhou.cr.aliyuncs.com\"\n ;;\n esac\n}\n\nAGENTTEAMS_REGISTRY=\"${AGENTTEAMS_REGISTRY:-$(detect_registry)}\"\n# Backward compatibility: accept old env var names from previous versions\nAGENTTEAMS_INSTALL_CONTROLLER_IMAGE=\"${AGENTTEAMS_INSTALL_CONTROLLER_IMAGE:-${AGENTTEAMS_INSTALL_DOCKER_PROXY_IMAGE:-}}\"\n# Image variables are resolved after version selection in step_version().\n# These placeholders allow early code paths to reference them without errors.\nMANAGER_IMAGE=\"${AGENTTEAMS_INSTALL_MANAGER_IMAGE:-}\"\nMANAGER_COPAW_IMAGE=\"${AGENTTEAMS_INSTALL_MANAGER_COPAW_IMAGE:-}\"\nWORKER_IMAGE=\"${AGENTTEAMS_INSTALL_WORKER_IMAGE:-}\"\nCOPAW_WORKER_IMAGE=\"${AGENTTEAMS_INSTALL_COPAW_WORKER_IMAGE:-}\"\nQWENPAW_WORKER_IMAGE=\"${AGENTTEAMS_INSTALL_QWENPAW_WORKER_IMAGE:-}\"\nHERMES_WORKER_IMAGE=\"${AGENTTEAMS_INSTALL_HERMES_WORKER_IMAGE:-}\"\nCONTROLLER_IMAGE=\"${AGENTTEAMS_INSTALL_CONTROLLER_IMAGE:-}\"\n\nresolve_image_tags() {\n AGENTTEAMS_VERSION=\"$(_normalize_version \"${AGENTTEAMS_VERSION}\")\"\n MANAGER_IMAGE=\"${AGENTTEAMS_INSTALL_MANAGER_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-manager:${AGENTTEAMS_VERSION}}\"\n MANAGER_COPAW_IMAGE=\"${AGENTTEAMS_INSTALL_MANAGER_COPAW_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-manager-copaw:${AGENTTEAMS_VERSION}}\"\n WORKER_IMAGE=\"${AGENTTEAMS_INSTALL_WORKER_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-worker:${AGENTTEAMS_VERSION}}\"\n COPAW_WORKER_IMAGE=\"${AGENTTEAMS_INSTALL_COPAW_WORKER_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-copaw-worker:${AGENTTEAMS_VERSION}}\"\n QWENPAW_WORKER_IMAGE=\"${AGENTTEAMS_INSTALL_QWENPAW_WORKER_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-qwenpaw-worker:${AGENTTEAMS_VERSION}}\"\n HERMES_WORKER_IMAGE=\"${AGENTTEAMS_INSTALL_HERMES_WORKER_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-hermes-worker:${AGENTTEAMS_VERSION}}\"\n EMBEDDED_IMAGE=\"${AGENTTEAMS_INSTALL_EMBEDDED_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-embedded:${AGENTTEAMS_VERSION}}\"\n # CoPaw Worker introduced in v1.0.4; Hermes Worker introduced in v1.1.0\n if [ -z \"${AGENTTEAMS_INSTALL_COPAW_WORKER_IMAGE:-}\" ] && _ver_lt \"${AGENTTEAMS_VERSION}\" \"v1.0.4\"; then\n COPAW_WORKER_IMAGE=\"\"\n fi\n if [ -z \"${AGENTTEAMS_INSTALL_HERMES_WORKER_IMAGE:-}\" ] && _ver_lt \"${AGENTTEAMS_VERSION}\" \"v1.1.0\"; then\n HERMES_WORKER_IMAGE=\"\"\n fi\n}\n\n# Resolve the embedded controller image. Embedded mode is the only supported\n# architecture since PR #616 (manager image no longer bundles Higress/Tuwunel/MinIO).\n# If the embedded image is unavailable for the requested version, fail fast with an\n# actionable error rather than silently falling back to the legacy single-container\n# path — that path is permanently broken with the slim manager image and would just\n# leave the user with a manager container looping on \"Waiting for Higress Gateway\".\n# Sets EMBEDDED_IMAGE and AGENTTEAMS_USE_EMBEDDED.\nresolve_embedded_image() {\n AGENTTEAMS_USE_EMBEDDED=1\n\n # If the user explicitly overrode the image (e.g. `make install-embedded` passes\n # a locally-built tag), respect it as-is without any registry probe.\n if [ -n \"${AGENTTEAMS_INSTALL_EMBEDDED_IMAGE:-}\" ]; then\n EMBEDDED_IMAGE=\"${AGENTTEAMS_INSTALL_EMBEDDED_IMAGE}\"\n return 0\n fi\n\n local _versioned=\"${AGENTTEAMS_REGISTRY}/agentteams/agentteams-embedded:${AGENTTEAMS_VERSION}\"\n local _latest=\"${AGENTTEAMS_REGISTRY}/agentteams/agentteams-embedded:latest\"\n\n # Skip probe when AGENTTEAMS_VERSION is \"latest\" — no point trying the same tag twice.\n if [ \"${AGENTTEAMS_VERSION}\" = \"latest\" ]; then\n EMBEDDED_IMAGE=\"${_latest}\"\n return 0\n fi\n\n if ${DOCKER_CMD} pull \"${_versioned}\" >/dev/null 2>&1; then\n EMBEDDED_IMAGE=\"${_versioned}\"\n return 0\n fi\n\n # Versions before v1.1.0 predate agentteams-embedded entirely — their manager image\n # bundled all infrastructure. Falling back to agentteams-embedded:latest would\n # silently swap in the v1.1.0 architecture (embedded kube-apiserver) which\n # crashes under QEMU on Apple Silicon. Auto-activate legacy mode instead.\n if _ver_lt \"${AGENTTEAMS_VERSION}\" \"v1.1.0\"; then\n log \"INFO: ${AGENTTEAMS_VERSION} predates agentteams-embedded; switching to legacy all-in-one manager architecture.\"\n log \"WARNING: Legacy all-in-one mode requires AGENTTEAMS_VERSION <= v1.0.9 (older bundled manager image).\"\n log \"WARNING: Newer slim manager images will hang on 'Waiting for Higress Gateway'.\"\n AGENTTEAMS_USE_EMBEDDED=0\n return 0\n fi\n\n if ${DOCKER_CMD} pull \"${_latest}\" >/dev/null 2>&1; then\n log \"embedded ${AGENTTEAMS_VERSION} not found, using latest\"\n EMBEDDED_IMAGE=\"${_latest}\"\n return 0\n fi\n\n # Explicit escape hatch — still honoured for edge cases.\n if [ \"${AGENTTEAMS_FORCE_LEGACY:-0}\" = \"1\" ]; then\n log \"WARNING: AGENTTEAMS_FORCE_LEGACY=1 — using legacy all-in-one manager architecture.\"\n log \"WARNING: This requires AGENTTEAMS_VERSION <= v1.0.9 (older bundled manager image).\"\n log \"WARNING: Newer slim manager images will hang on 'Waiting for Higress Gateway'.\"\n AGENTTEAMS_USE_EMBEDDED=0\n return 0\n fi\n\n error \"Embedded controller image is not available in the registry:\"\n error \" - tried: ${_versioned}\"\n error \" - tried: ${_latest}\"\n error \"\"\n error \"Embedded mode is the only supported architecture since PR #616.\"\n error \"How to resolve:\"\n error \" 1) Pin to a AGENTTEAMS_VERSION whose embedded image has been published, or\"\n error \" wait for the release pipeline to publish it.\"\n error \" 2) For a local build, run: make install-embedded\"\n error \" (builds and uses the local embedded image without touching the registry).\"\n error \" 3) Override with a custom image: AGENTTEAMS_INSTALL_EMBEDDED_IMAGE=... ./agentteams-install.sh\"\n exit 1\n}\n\n# ============================================================\n# Known models list — used to detect custom models during install\n# ============================================================\nKNOWN_MODELS=\"gpt-5.4 gpt-5.3-codex gpt-5-mini gpt-5-nano claude-opus-4-6 claude-sonnet-4-6 claude-haiku-4-5 qwen3.6-plus qwen3.5-plus deepseek-chat deepseek-reasoner kimi-k2.5 glm-5 MiniMax-M2.7 MiniMax-M2.7-highspeed MiniMax-M2.5\"\n\nis_known_model() {\n local model=\"$1\"\n for m in ${KNOWN_MODELS}; do\n [ \"${m}\" = \"${model}\" ] && return 0\n done\n return 1\n}\n\n# Prompt user for custom model parameters when model is not in the known list.\n# Sets: AGENTTEAMS_MODEL_CONTEXT_WINDOW, AGENTTEAMS_MODEL_MAX_TOKENS, AGENTTEAMS_MODEL_REASONING, AGENTTEAMS_MODEL_VISION\nprompt_custom_model_params() {\n local model=\"$1\"\n if is_known_model \"${model}\"; then\n # Clear any stale custom params for known models\n AGENTTEAMS_MODEL_CONTEXT_WINDOW=\"\"\n AGENTTEAMS_MODEL_MAX_TOKENS=\"\"\n AGENTTEAMS_MODEL_REASONING=\"\"\n AGENTTEAMS_MODEL_VISION=\"\"\n return\n fi\n echo \"\"\n log \"$(msg llm.custom_model.detected \"${model}\")\"\n echo \"\"\n read -e -p \" $(msg llm.custom_model.context_prompt): \" AGENTTEAMS_MODEL_CONTEXT_WINDOW\n if [ \"${AGENTTEAMS_MODEL_CONTEXT_WINDOW}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n AGENTTEAMS_MODEL_CONTEXT_WINDOW=\"${AGENTTEAMS_MODEL_CONTEXT_WINDOW:-150000}\"\n read -e -p \" $(msg llm.custom_model.max_tokens_prompt): \" AGENTTEAMS_MODEL_MAX_TOKENS\n if [ \"${AGENTTEAMS_MODEL_MAX_TOKENS}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n AGENTTEAMS_MODEL_MAX_TOKENS=\"${AGENTTEAMS_MODEL_MAX_TOKENS:-128000}\"\n read -e -p \" $(msg llm.custom_model.reasoning_prompt): \" _reasoning\n if [ \"${_reasoning}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n case \"${_reasoning}\" in\n n|N|no|NO) AGENTTEAMS_MODEL_REASONING=\"false\" ;;\n *) AGENTTEAMS_MODEL_REASONING=\"true\" ;;\n esac\n read -e -p \" $(msg llm.custom_model.vision_prompt): \" _vision\n if [ \"${_vision}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n case \"${_vision}\" in\n y|Y|yes|YES) AGENTTEAMS_MODEL_VISION=\"true\" ;;\n *) AGENTTEAMS_MODEL_VISION=\"false\" ;;\n esac\n log \"$(msg llm.custom_model.summary \"${AGENTTEAMS_MODEL_CONTEXT_WINDOW}\" \"${AGENTTEAMS_MODEL_MAX_TOKENS}\" \"${AGENTTEAMS_MODEL_REASONING}\" \"${AGENTTEAMS_MODEL_VISION}\")\"\n}\n\n# ============================================================\n# Wait for Manager agent to be ready\n# Uses `openclaw gateway health` inside the container to confirm the gateway is running\n# ============================================================\n\nwait_manager_ready() {\n local timeout=\"${AGENTTEAMS_READY_TIMEOUT:-300}\"\n local elapsed=0\n local container=\"${1:-agentteams-manager}\"\n\n log \"$(msg install.wait_ready \"${timeout}\")\"\n\n # Wait for Manager agent to be healthy inside the container\n local runtime=\"${AGENTTEAMS_MANAGER_RUNTIME:-copaw}\"\n while [ \"${elapsed}\" -lt \"${timeout}\" ]; do\n case \"${runtime}\" in\n copaw)\n if ${DOCKER_CMD} exec \"${container}\" curl -sf http://127.0.0.1:18799/api/agents 2>/dev/null | grep -q '\"agents\"'; then\n log \"$(msg install.wait_ready.ok)\"\n return 0\n fi\n ;;\n *)\n if ${DOCKER_CMD} exec \"${container}\" openclaw gateway health --json 2>/dev/null | grep -q '\"ok\"' 2>/dev/null; then\n log \"$(msg install.wait_ready.ok)\"\n return 0\n fi\n ;;\n esac\n sleep 5\n elapsed=$((elapsed + 5))\n printf \"\\r\\033[36m[AgentTeams]\\033[0m $(msg install.wait_ready.waiting \"${elapsed}\" \"${timeout}\")\"\n done\n\n echo \"\"\n die \"$(msg install.wait_ready.timeout \"${timeout}\" \"${container}\")\"\n}\n\nwait_matrix_ready() {\n local timeout=\"${AGENTTEAMS_READY_TIMEOUT:-300}\"\n local elapsed=0\n local container=\"${1:-agentteams-manager}\"\n\n log \"$(msg install.wait_matrix \"${timeout}\")\"\n\n while [ \"${elapsed}\" -lt \"${timeout}\" ]; do\n if ${DOCKER_CMD} exec \"${container}\" curl -sf http://127.0.0.1:6167/_tuwunel/server_version >/dev/null 2>&1; then\n log \"$(msg install.wait_matrix.ok)\"\n return 0\n fi\n sleep 5\n elapsed=$((elapsed + 5))\n printf \"\\r\\033[36m[AgentTeams]\\033[0m $(msg install.wait_matrix.waiting \"${elapsed}\" \"${timeout}\")\"\n done\n\n echo \"\"\n die \"$(msg install.wait_matrix.timeout \"${timeout}\" \"${container}\")\"\n}\n\n# Read KEY=value from /data/agentteams-secrets.env on a Docker volume (manager container not required).\n# Requires EMBEDDED_IMAGE (resolved earlier in install_manager). Uses ${DOCKER_CMD}.\nagentteams_read_secret_from_data_volume() {\n local _vol=\"$1\" _key=\"$2\"\n if [ -z \"${_vol}\" ] || [ -z \"${_key}\" ] || [ -z \"${EMBEDDED_IMAGE:-}\" ]; then\n echo \"\"\n return 0\n fi\n ${DOCKER_CMD} run --rm --entrypoint sh \\\n -v \"${_vol}:/data:ro\" \\\n \"${EMBEDDED_IMAGE}\" -c \"grep \\\"^${_key}=\\\" /data/agentteams-secrets.env 2>/dev/null | cut -d= -f2- | head -1 | tr -d '\\r'\" 2>/dev/null\n}\n\n# Read KEY=value from /data/worker-creds/<worker>.env on a Docker volume.\nagentteams_read_worker_creds_value_from_volume() {\n local _vol=\"$1\" _worker=\"$2\" _key=\"$3\"\n if [ -z \"${_vol}\" ] || [ -z \"${_worker}\" ] || [ -z \"${_key}\" ] || [ -z \"${EMBEDDED_IMAGE:-}\" ]; then\n echo \"\"\n return 0\n fi\n ${DOCKER_CMD} run --rm --entrypoint sh \\\n -v \"${_vol}:/data:ro\" \\\n \"${EMBEDDED_IMAGE}\" -c \"grep \\\"^${_key}=\\\" \\\"/data/worker-creds/${_worker}.env\\\" 2>/dev/null | cut -d= -f2- | head -1 | tr -d \\\"\\\\r\\\"\" 2>/dev/null\n}\n\n# Read admin_dm_room_id from host workspace state.json (fallback when Matrix API is unavailable).\nagentteams_read_admin_dm_room_from_workspace() {\n local _ws=\"$1\"\n local _f=\"${_ws}/state.json\"\n if [ ! -f \"${_f}\" ] || ! command -v jq >/dev/null 2>&1; then\n echo \"\"\n return 0\n fi\n jq -r '.admin_dm_room_id // empty | select(. != \"null\")' \"${_f}\" 2>/dev/null\n}\n\n# Read secret input with masked echo (shows * per keystroke, supports backspace)\n# Usage: read_secret \"prompt text: \"; value=\"${_RS_RESULT}\"\nread_secret() {\n local _rs_prompt=\"$1\"\n _RS_RESULT=\"\"\n local _rs_char=\"\"\n\n printf \"%s\" \"${_rs_prompt}\"\n\n while IFS= read -r -s -n 1 _rs_char; do\n if [[ -z \"${_rs_char}\" ]]; then\n break\n elif [[ \"${_rs_char}\" == $'\\177' ]] || [[ \"${_rs_char}\" == $'\\b' ]]; then\n if [ -n \"${_RS_RESULT}\" ]; then\n _RS_RESULT=\"${_RS_RESULT%?}\"\n printf \"\\b \\b\"\n fi\n else\n _RS_RESULT=\"${_RS_RESULT}${_rs_char}\"\n printf \"*\"\n fi\n done\n echo\n}\n\n# Load current parameter values from env file for upgrade mode display\nload_current_params_from_env() {\n local env_file=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n if [ -f \"${env_file}\" ]; then\n [ -z \"${AGENTTEAMS_LLM_PROVIDER:+x}\" ] && AGENTTEAMS_LLM_PROVIDER=\"$(grep '^AGENTTEAMS_LLM_PROVIDER=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_OPENAI_BASE_URL:+x}\" ] && AGENTTEAMS_OPENAI_BASE_URL=\"$(grep '^AGENTTEAMS_OPENAI_BASE_URL=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_DEFAULT_MODEL:+x}\" ] && AGENTTEAMS_DEFAULT_MODEL=\"$(grep '^AGENTTEAMS_DEFAULT_MODEL=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_EMBEDDING_MODEL:+x}\" ] && AGENTTEAMS_EMBEDDING_MODEL=\"$(grep '^AGENTTEAMS_EMBEDDING_MODEL=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_WORKSPACE_DIR:+x}\" ] && AGENTTEAMS_WORKSPACE_DIR=\"$(grep '^AGENTTEAMS_WORKSPACE_DIR=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_HOST_SHARE_DIR:+x}\" ] && AGENTTEAMS_HOST_SHARE_DIR=\"$(grep '^AGENTTEAMS_HOST_SHARE_DIR=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_PODMAN_AUTOSTART:+x}\" ] && AGENTTEAMS_PODMAN_AUTOSTART=\"$(grep '^AGENTTEAMS_PODMAN_AUTOSTART=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_DASHBOARD:+x}\" ] && AGENTTEAMS_DASHBOARD=\"$(grep '^AGENTTEAMS_DASHBOARD=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_DASHBOARD_VERSION:+x}\" ] && AGENTTEAMS_DASHBOARD_VERSION=\"$(grep '^AGENTTEAMS_DASHBOARD_VERSION=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_PORT_DASHBOARD:+x}\" ] && AGENTTEAMS_PORT_DASHBOARD=\"$(grep '^AGENTTEAMS_PORT_DASHBOARD=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_DASHBOARD_IMAGE:+x}\" ] && AGENTTEAMS_DASHBOARD_IMAGE=\"$(grep '^AGENTTEAMS_DASHBOARD_IMAGE=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL:+x}\" ] && AGENTTEAMS_AI_GATEWAY_ADMIN_URL=\"$(grep '^AGENTTEAMS_AI_GATEWAY_ADMIN_URL=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n fi\n}\n\n# In non-interactive mode, uses default or errors if required and no default.\n# Usage: prompt VAR_NAME \"Prompt text\" \"default\" [true=secret]\nprompt() {\n local var_name=\"$1\"\n local prompt_text=\"$2\"\n local default_value=\"$3\"\n local is_secret=\"${4:-false}\"\n\n # If the variable is already set in the environment, use it silently\n # In upgrade mode, show current value and let user change it\n eval \"local current_value=\\\"\\${${var_name}}\\\"\"\n if [ -n \"${current_value}\" ]; then\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_NON_INTERACTIVE}\" != \"1\" ]; then\n # Show masked value for secrets, full value otherwise\n local display_value=\"${current_value}\"\n if [ \"${is_secret}\" = \"true\" ]; then\n local len=${#current_value}\n if [ \"${len}\" -le 8 ]; then\n display_value=\"****\"\n else\n display_value=\"${current_value:0:4}****${current_value: -4}\"\n fi\n fi\n log \"$(msg prompt.upgrade_keep \"${prompt_text}\" \"${display_value}\")\"\n local new_value=\"\"\n if [ \"${is_secret}\" = \"true\" ]; then\n read_secret \"${prompt_text}: \"\n new_value=\"${_RS_RESULT}\"\n else\n read -e -p \"${prompt_text}: \" new_value\n if [ \"${new_value}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n fi\n if [ -n \"${new_value}\" ]; then\n eval \"export ${var_name}='${new_value}'\"\n fi\n return\n fi\n log \"$(msg prompt.preset \"${prompt_text}\")\"\n return\n fi\n\n # Non-interactive or quickstart: use default or error\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] || [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ]; then\n if [ -n \"${default_value}\" ]; then\n eval \"export ${var_name}='${default_value}'\"\n log \"$(msg prompt.default \"${prompt_text}\" \"${default_value}\")\"\n return\n elif [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n # Only hard-error in fully non-interactive mode, not quickstart\n die \"$(msg prompt.required \"${prompt_text}\")\"\n fi\n # quickstart + no default: fall through to interactive prompt below\n fi\n\n if [ -n \"${default_value}\" ]; then\n prompt_text=\"${prompt_text} [${default_value}]\"\n fi\n\n local value=\"\"\n if [ \"${is_secret}\" = \"true\" ]; then\n read_secret \"${prompt_text}: \"\n value=\"${_RS_RESULT}\"\n else\n read -e -p \"${prompt_text}: \" value\n if [ \"${value}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n fi\n\n value=\"${value:-${default_value}}\"\n if [ -z \"${value}\" ]; then\n die \"$(msg prompt.required_empty \"${prompt_text}\")\"\n fi\n\n eval \"export ${var_name}='${value}'\"\n}\n\n# Prompt for an optional value (empty string is acceptable)\n# Skips prompt if variable is already defined in environment (even if empty)\n# In upgrade mode, shows current value and lets user change it.\n# In non-interactive mode, defaults to empty string.\nprompt_optional() {\n local var_name=\"$1\"\n local prompt_text=\"$2\"\n local is_secret=\"${3:-false}\"\n\n # Check if variable is defined (even if set to empty string)\n eval \"local _chk=\\\"\\${${var_name}+x}\\\"\"\n if [ -n \"${_chk}\" ]; then\n # In upgrade mode, show current value and let user change it\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_NON_INTERACTIVE}\" != \"1\" ]; then\n eval \"local current_value=\\\"\\${${var_name}}\\\"\"\n local display_value=\"${current_value}\"\n if [ \"${is_secret}\" = \"true\" ] && [ -n \"${current_value}\" ]; then\n local len=${#current_value}\n if [ \"${len}\" -le 8 ]; then\n display_value=\"****\"\n else\n display_value=\"${current_value:0:4}****${current_value: -4}\"\n fi\n fi\n if [ -n \"${current_value}\" ]; then\n if [ \"${is_secret}\" = \"true\" ]; then\n log \"$(msg prompt.upgrade_keep_secret \"${prompt_text}\" \"${display_value}\")\"\n else\n log \"$(msg prompt.upgrade_keep \"${prompt_text}\" \"${display_value}\")\"\n fi\n else\n log \"$(msg prompt.upgrade_empty \"${prompt_text}\")\"\n fi\n local new_value=\"\"\n if [ \"${is_secret}\" = \"true\" ]; then\n read_secret \"${prompt_text}: \"\n new_value=\"${_RS_RESULT}\"\n else\n read -e -p \"${prompt_text}: \" new_value\n if [ \"${new_value}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n fi\n if [ -n \"${new_value}\" ]; then\n eval \"export ${var_name}='${new_value}'\"\n fi\n return\n fi\n log \"$(msg prompt.preset \"${prompt_text}\")\"\n return\n fi\n\n # Non-interactive or quickstart: skip, leave unset\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] || [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ]; then\n eval \"export ${var_name}=''\"\n return\n fi\n\n local value=\"\"\n if [ \"${is_secret}\" = \"true\" ]; then\n read_secret \"${prompt_text}: \"\n value=\"${_RS_RESULT}\"\n else\n read -e -p \"${prompt_text}: \" value\n if [ \"${value}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n fi\n\n eval \"export ${var_name}='${value}'\"\n}\n\ngenerate_key() {\n openssl rand -hex 32\n}\n\n# ============================================================\n# Detect container runtime socket on the host\n#\n# Dependency: Requires DOCKER_CMD to reflect the TRUE underlying runtime.\n# (Crucial for unmasking 'podman' disguised as 'docker'.\n# Must be strictly resolved prior, e.g., via check_container_runtime)\n# ============================================================\ndetect_socket() {\n local socket_path\n\n # 1. Respect explicitly defined DOCKER_HOST environment variable\n if [ -n \"${DOCKER_HOST}\" ]; then\n socket_path=$(echo \"${DOCKER_HOST}\" | sed 's|^unix://||')\n if [ -S \"${socket_path}\" ]; then\n echo \"${socket_path}\"\n return 0\n fi\n fi\n\n # 2. Route based on the resolved container runtime\n case \"${DOCKER_CMD}\" in\n docker)\n # Docker path handling\n socket_path=$(docker context ls --format '{{if .Current}}{{.DockerEndpoint}}{{end}}' 2>/dev/null | grep . | sed 's|^unix://||')\n if [ -n \"${socket_path}\" ] && [ -S \"${socket_path}\" ]; then\n echo \"${socket_path}\"\n return 0\n fi\n\n # Rootless Docker\n if [ -n \"${XDG_RUNTIME_DIR:-}\" ] && [ -S \"${XDG_RUNTIME_DIR}/docker.sock\" ]; then\n echo \"${XDG_RUNTIME_DIR}/docker.sock\"\n return 0\n fi\n\n # Rootless Docker (macOS fallback)\n if [ -S \"${HOME}/.docker/run/docker.sock\" ]; then\n echo \"${HOME}/.docker/run/docker.sock\"\n return 0\n fi\n\n # Root Docker\n if [ -S \"/var/run/docker.sock\" ]; then\n echo \"/var/run/docker.sock\"\n return 0\n fi\n ;;\n podman)\n # Podman path handling\n if [ \"$(id -u)\" -eq 0 ]; then\n socket_path=\"/run/podman/podman.sock\"\n else\n socket_path=\"${XDG_RUNTIME_DIR}/podman/podman.sock\"\n fi\n if [ -S \"${socket_path}\" ]; then\n echo \"${socket_path}\"\n return 0\n fi\n ;;\n esac\n\n # Return empty if no socket is found\n echo \"\"\n return 0\n}\n\n# Ensure Podman API socket is active (State mutation)\nensure_podman_socket() {\n local socket_path\n\n # Only applicable if runtime is podman and systemctl is available\n if [ \"${DOCKER_CMD:-}\" != \"podman\" ] || ! command -v systemctl >/dev/null 2>&1; then\n return 0\n fi\n\n echo \"Ensuring Podman API socket is active...\" >&2\n\n # Enable and start the socket based on user privileges\n if [ \"$(id -u)\" -eq 0 ]; then\n socket_path=\"/run/podman/podman.sock\"\n systemctl enable --now podman.socket >/dev/null 2>&1 || true\n else\n socket_path=\"${XDG_RUNTIME_DIR}/podman/podman.sock\"\n systemctl --user enable --now podman.socket >/dev/null 2>&1 || true\n fi\n\n # Give systemd a brief moment to assert the socket for root or rootless user\n local _i\n for _i in 1 2 3; do\n [ -S \"${socket_path}\" ] && break\n sleep 1\n done\n}\n\n# Detect local LAN IP address (cross-platform: macOS and Linux)\ndetect_lan_ip() {\n local ip=\"\"\n\n # macOS: try common Wi-Fi / Ethernet interfaces\n if command -v ipconfig >/dev/null 2>&1; then\n for iface in en0 en1 en2 en3 en4; do\n ip=$(ipconfig getifaddr \"${iface}\" 2>/dev/null)\n if [ -n \"${ip}\" ]; then\n echo \"${ip}\"\n return 0\n fi\n done\n fi\n\n # Linux: ip route — most reliable\n if command -v ip >/dev/null 2>&1; then\n ip=$(ip route get 1.1.1.1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i==\"src\"){print $(i+1); exit}}')\n if [ -n \"${ip}\" ]; then\n echo \"${ip}\"\n return 0\n fi\n fi\n\n # Linux fallback: hostname -I (space-separated list, take first non-loopback)\n if command -v hostname >/dev/null 2>&1; then\n ip=$(hostname -I 2>/dev/null | tr ' ' '\\n' | grep -v '^127\\.' | grep -v '^::' | head -1)\n if [ -n \"${ip}\" ]; then\n echo \"${ip}\"\n return 0\n fi\n fi\n\n # Last resort: ifconfig\n if command -v ifconfig >/dev/null 2>&1; then\n ip=$(ifconfig 2>/dev/null | awk '/inet /{if($2!~/^127\\./){print $2; exit}}')\n # Strip \"addr:\" prefix that some ifconfig versions add\n ip=\"${ip#addr:}\"\n if [ -n \"${ip}\" ]; then\n echo \"${ip}\"\n return 0\n fi\n fi\n\n echo \"\"\n}\n\n\n# ============================================================\n# Step-back navigation helpers\n# ============================================================\n\n# should_skip_step: returns 0 (skip) when the step is irrelevant in current mode\nshould_skip_step() {\n local step_fn=\"$1\"\n case \"${step_fn}\" in\n step_lang|step_mode)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n ;;\n step_version)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n ;;\n step_existing)\n local _env=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n [ ! -f \"${_env}\" ] && return 0\n ;;\n # Keep-All upgrade mode: skip all config steps (step_volume/step_workspace handled separately)\n step_llm|step_admin|step_network|step_ports|step_domains|step_github|step_skills|step_runtime|step_manager_runtime|step_dashboard|step_e2ee|step_docker_proxy|step_idle|step_hostshare)\n [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ] && return 0\n ;;\n step_volume|step_workspace)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ] && return 0\n [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ] && return 0\n ;;\n step_e2ee|step_idle)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE}\" != \"1\" ] && return 0\n ;;\n step_docker_proxy)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE}\" != \"1\" ] && return 0\n # Embedded mode handles docker access natively — skip this step\n [ \"${AGENTTEAMS_USE_EMBEDDED:-}\" = \"1\" ] && return 0\n ;;\n step_manager_runtime)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n ;;\n step_hostshare)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ] && return 0\n ;;\n step_podman_autostart)\n # Only relevant for Podman with systemd\n [ \"${DOCKER_CMD}\" != \"podman\" ] && return 0\n ! command -v systemctl >/dev/null 2>&1 && return 0\n\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE}\" != \"1\" ] && return 0\n [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ] && return 0\n ;;\n esac\n return 1\n}\n\n# clear_step_vars: unset variables set by a step so it will re-prompt on re-entry\nclear_step_vars() {\n local step_fn=\"$1\"\n case \"${step_fn}\" in\n step_mode) unset AGENTTEAMS_QUICKSTART ;;\n step_version) unset AGENTTEAMS_VERSION ;;\n step_existing) unset AGENTTEAMS_UPGRADE UPGRADE_EXISTING_WORKERS ;;\n step_llm)\n unset AGENTTEAMS_LLM_PROVIDER AGENTTEAMS_DEFAULT_MODEL AGENTTEAMS_OPENAI_BASE_URL\n unset AGENTTEAMS_LLM_API_KEY AGENTTEAMS_MODEL_CONTEXT_WINDOW AGENTTEAMS_MODEL_MAX_TOKENS\n unset AGENTTEAMS_MODEL_REASONING AGENTTEAMS_MODEL_VISION\n ;;\n step_admin) unset AGENTTEAMS_ADMIN_USER AGENTTEAMS_ADMIN_PASSWORD ;;\n step_network) unset AGENTTEAMS_LOCAL_ONLY ;;\n step_ports)\n unset AGENTTEAMS_PORT_GATEWAY AGENTTEAMS_PORT_CONSOLE\n unset AGENTTEAMS_PORT_ELEMENT_WEB AGENTTEAMS_PORT_MANAGER_CONSOLE\n ;;\n step_domains)\n unset AGENTTEAMS_MATRIX_DOMAIN AGENTTEAMS_MATRIX_CLIENT_DOMAIN\n unset AGENTTEAMS_AI_GATEWAY_DOMAIN AGENTTEAMS_FS_DOMAIN AGENTTEAMS_CONSOLE_DOMAIN\n ;;\n step_github) unset AGENTTEAMS_GITHUB_TOKEN ;;\n step_skills) unset AGENTTEAMS_SKILLS_API_URL ;;\n step_volume) unset AGENTTEAMS_DATA_DIR ;;\n step_workspace) unset AGENTTEAMS_WORKSPACE_DIR ;;\n step_dashboard) unset AGENTTEAMS_DASHBOARD AGENTTEAMS_DASHBOARD_VERSION AGENTTEAMS_PORT_DASHBOARD AGENTTEAMS_DASHBOARD_IMAGE AGENTTEAMS_AI_GATEWAY_ADMIN_URL ;;\n step_runtime) unset AGENTTEAMS_DEFAULT_WORKER_RUNTIME ;;\n step_manager_runtime) unset AGENTTEAMS_MANAGER_RUNTIME ;;\n step_e2ee) unset AGENTTEAMS_MATRIX_E2EE ;;\n step_docker_proxy) unset AGENTTEAMS_DOCKER_PROXY; unset AGENTTEAMS_PROXY_ALLOWED_REGISTRIES ;;\n step_idle) unset AGENTTEAMS_WORKER_IDLE_TIMEOUT ;;\n step_hostshare) unset AGENTTEAMS_HOST_SHARE_DIR ;;\n step_podman_autostart) unset AGENTTEAMS_PODMAN_AUTOSTART ;;\n esac\n}\n\n# ============================================================\n# Individual step functions\n# ============================================================\n\nstep_lang() {\n local lang_default_choice=\"2\"\n [ \"${AGENTTEAMS_LANGUAGE}\" = \"zh\" ] && lang_default_choice=\"1\"\n log \"$(msg lang.detected)\"\n log \"$(msg lang.switch_title)\"\n echo \"$(msg lang.option_zh)\"\n echo \"$(msg lang.option_en)\"\n echo \"\"\n local LANG_CHOICE\n read -e -p \"$(msg lang.prompt) [${lang_default_choice}]: \" LANG_CHOICE\n LANG_CHOICE=\"${LANG_CHOICE:-${lang_default_choice}}\"\n if [ \"${LANG_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n case \"${LANG_CHOICE}\" in\n 1) AGENTTEAMS_LANGUAGE=\"zh\" ;;\n 2) AGENTTEAMS_LANGUAGE=\"en\" ;;\n esac\n export AGENTTEAMS_LANGUAGE\n log \"\"\n}\n\nstep_mode() {\n log \"$(msg install.mode.title)\"\n echo \"\"\n echo \"$(msg install.mode.choose)\"\n echo \"$(msg install.mode.quickstart)\"\n echo \"$(msg install.mode.manual)\"\n echo \"\"\n local ONBOARDING_CHOICE\n read -e -p \"$(msg install.mode.prompt): \" ONBOARDING_CHOICE\n ONBOARDING_CHOICE=\"${ONBOARDING_CHOICE:-1}\"\n if [ \"${ONBOARDING_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n case \"${ONBOARDING_CHOICE}\" in\n 1|quick|quickstart)\n log \"$(msg install.mode.quickstart_selected)\"\n AGENTTEAMS_QUICKSTART=1\n ;;\n 2|manual)\n log \"$(msg install.mode.manual_selected)\"\n ;;\n *)\n log \"$(msg install.mode.invalid)\"\n AGENTTEAMS_QUICKSTART=1\n ;;\n esac\n log \"\"\n}\n\nstep_version() {\n # Skip if version already provided via env var\n if [ -n \"${AGENTTEAMS_VERSION}\" ]; then\n resolve_image_tags\n return 0\n fi\n # Try to fetch the latest stable release from GitHub\n log \"$(msg install.version.fetching)\"\n local _fetched\n _fetched=$(curl -sf --max-time 5 \\\n -H \"Accept: application/vnd.github+json\" \\\n \"https://api.github.com/repos/agentscope-ai/AgentTeams/releases/latest\" \\\n 2>/dev/null | grep '\"tag_name\"' | head -1 | sed 's/.*\"tag_name\": *\"\\([^\"]*\\)\".*/\\1/')\n if [ -n \"${_fetched}\" ]; then\n AGENTTEAMS_KNOWN_STABLE_VERSION=\"${_fetched}\"\n else\n log \"$(msg install.version.fetch_failed \"${AGENTTEAMS_KNOWN_STABLE_VERSION}\")\"\n fi\n log \"$(msg install.version.title)\"\n echo \"\"\n echo \"$(msg install.version.choose)\"\n echo \"$(msg install.version.option_latest)\"\n printf \"%s\\n\" \"$(msg install.version.option_stable \"${AGENTTEAMS_KNOWN_STABLE_VERSION}\")\"\n echo \"$(msg install.version.option_custom)\"\n echo \"\"\n local VERSION_CHOICE\n read -e -p \"$(msg install.version.prompt) [1]: \" VERSION_CHOICE\n VERSION_CHOICE=\"${VERSION_CHOICE:-1}\"\n if [ \"${VERSION_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n case \"${VERSION_CHOICE}\" in\n 1|latest)\n AGENTTEAMS_VERSION=\"latest\"\n log \"$(msg install.version.selected_latest)\"\n ;;\n 2|stable)\n AGENTTEAMS_VERSION=\"${AGENTTEAMS_KNOWN_STABLE_VERSION}\"\n log \"$(msg install.version.selected_stable \"${AGENTTEAMS_VERSION}\")\"\n ;;\n 3|custom)\n local CUSTOM_VERSION\n read -e -p \"$(msg install.version.custom_prompt): \" CUSTOM_VERSION\n AGENTTEAMS_VERSION=\"${CUSTOM_VERSION:-${AGENTTEAMS_KNOWN_STABLE_VERSION}}\"\n log \"$(msg install.version.selected_custom \"${AGENTTEAMS_VERSION}\")\"\n ;;\n *)\n AGENTTEAMS_VERSION=\"${AGENTTEAMS_KNOWN_STABLE_VERSION}\"\n log \"$(msg install.version.invalid \"${AGENTTEAMS_VERSION}\")\"\n ;;\n esac\n log \"\"\n resolve_image_tags\n}\n\nstep_existing() {\n local existing_env=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n log \"$(msg install.existing.detected \"${existing_env}\")\"\n local running_manager=\"\" running_workers=\"\" existing_workers=\"\"\n if ${DOCKER_CMD} ps --format '{{.Names}}' | grep -q \"^agentteams-manager$\"; then\n running_manager=\"agentteams-manager\"\n fi\n running_workers=$(${DOCKER_CMD} ps --format '{{.Names}}' | grep \"^agentteams-worker-\" || true)\n existing_workers=$(${DOCKER_CMD} ps -a --format '{{.Names}}' | grep \"^agentteams-worker-\" || true)\n local UPGRADE_CHOICE\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n log \"$(msg install.existing.upgrade_noninteractive)\"\n UPGRADE_CHOICE=\"upgrade\"\n else\n echo \"\"\n echo \"$(msg install.existing.choose)\"\n echo \"$(msg install.existing.upgrade)\"\n echo \"$(msg install.existing.reinstall)\"\n echo \"$(msg install.existing.cancel)\"\n echo \"\"\n read -e -p \"$(msg install.existing.prompt): \" UPGRADE_CHOICE\n UPGRADE_CHOICE=\"${UPGRADE_CHOICE:-1}\"\n if [ \"${UPGRADE_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n fi\n case \"${UPGRADE_CHOICE}\" in\n 1|upgrade)\n AGENTTEAMS_UPGRADE=1\n log \"$(msg install.existing.upgrading)\"\n # Show upgrade mode sub-menu (unless already set via env var)\n if [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL:-0}\" != \"1\" ]; then\n echo \"\"\n echo \"$(msg upgrade.mode.prompt)\"\n echo \"$(msg upgrade.mode.keep_all)\"\n echo \"$(msg upgrade.mode.confirm_each)\"\n echo \"$(msg upgrade.mode.back)\"\n echo \"\"\n local UPGRADE_MODE_CHOICE\n read -e -p \"$(msg install.existing.prompt): \" UPGRADE_MODE_CHOICE\n UPGRADE_MODE_CHOICE=\"${UPGRADE_MODE_CHOICE:-1}\"\n case \"${UPGRADE_MODE_CHOICE}\" in\n 1|keep)\n AGENTTEAMS_UPGRADE_KEEP_ALL=1\n ;;\n 2|confirm)\n AGENTTEAMS_UPGRADE_KEEP_ALL=0\n ;;\n 3|b)\n STEP_RESULT=\"back\"\n return 0\n ;;\n *)\n AGENTTEAMS_UPGRADE_KEEP_ALL=0\n ;;\n esac\n fi\n # Load current parameters for both Keep-All and confirm-each modes\n load_current_params_from_env\n if [ -n \"${running_manager}\" ] || [ -n \"${running_workers}\" ]; then\n echo \"\"\n echo -e \"\\033[33m$(msg install.existing.warn_manager_stop)\\033[0m\"\n if [ -n \"${existing_workers}\" ]; then\n echo -e \"\\033[33m$(msg install.existing.warn_worker_recreate)\\033[0m\"\n fi\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" != \"1\" ]; then\n echo \"\"\n local CONFIRM_STOP\n read -e -p \"$(msg install.existing.continue_prompt): \" CONFIRM_STOP\n if [ \"${CONFIRM_STOP}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n if [ \"${CONFIRM_STOP}\" != \"y\" ] && [ \"${CONFIRM_STOP}\" != \"Y\" ]; then\n log \"$(msg install.existing.cancelled)\"\n exit 0\n fi\n fi\n fi\n UPGRADE_EXISTING_WORKERS=\"${existing_workers}\"\n ;;\n 2|reinstall)\n log \"$(msg install.reinstall.performing)\"\n local existing_workspace=\"\"\n if [ -f \"${existing_env}\" ]; then\n existing_workspace=$(grep '^AGENTTEAMS_WORKSPACE_DIR=' \"${existing_env}\" 2>/dev/null | cut -d= -f2-)\n fi\n [ -z \"${existing_workspace}\" ] && existing_workspace=\"${HOME}/agentteams-manager\"\n echo \"\"\n echo -e \"\\033[33m$(msg install.reinstall.warn_stop)\\033[0m\"\n [ -n \"${running_manager}\" ] && echo -e \"\\033[33m - ${running_manager} (manager)\\033[0m\"\n for w in ${running_workers}; do\n echo -e \"\\033[33m - ${w} (worker)\\033[0m\"\n done\n echo \"\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_delete)\\033[0m\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_volume)\\033[0m\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_env \"${existing_env}\")\\033[0m\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_workspace \"${existing_workspace}\")\\033[0m\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_workers)\\033[0m\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_proxy)\\033[0m\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_network)\\033[0m\"\n echo \"\"\n echo -e \"\\033[31m$(msg install.reinstall.confirm_type)\\033[0m\"\n echo -e \"\\033[31m ${existing_workspace}\\033[0m\"\n echo \"\"\n local CONFIRM_PATH\n read -e -p \"$(msg install.reinstall.confirm_path): \" CONFIRM_PATH\n if [ \"${CONFIRM_PATH}\" != \"${existing_workspace}\" ]; then\n die \"$(msg install.reinstall.path_mismatch \"${CONFIRM_PATH}\" \"${existing_workspace}\")\"\n fi\n log \"$(msg install.reinstall.confirmed)\"\n ${DOCKER_CMD} stop agentteams-manager 2>/dev/null || true\n ${DOCKER_CMD} rm agentteams-manager 2>/dev/null || true\n for w in $(${DOCKER_CMD} ps -a --format '{{.Names}}' | grep \"^agentteams-worker-\" || true); do\n ${DOCKER_CMD} stop \"${w}\" 2>/dev/null || true\n ${DOCKER_CMD} rm \"${w}\" 2>/dev/null || true\n log \"$(msg install.reinstall.removed_worker \"${w}\")\"\n done\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' | grep -q \"^agentteams-controller$\"; then\n log \"$(msg install.reinstall.removing_proxy)\"\n ${DOCKER_CMD} stop agentteams-controller 2>/dev/null || true\n ${DOCKER_CMD} rm agentteams-controller 2>/dev/null || true\n fi\n if ${DOCKER_CMD} network ls --format '{{.Name}}' | grep -q \"^agentteams-net$\"; then\n log \"$(msg install.reinstall.removing_network)\"\n ${DOCKER_CMD} network rm agentteams-net 2>/dev/null || true\n fi\n if ${DOCKER_CMD} volume ls -q | grep -q \"^agentteams-data$\"; then\n log \"$(msg install.reinstall.removing_volume)\"\n ${DOCKER_CMD} volume rm agentteams-data 2>/dev/null || log \"$(msg install.reinstall.warn_volume_fail)\"\n fi\n if [ -d \"${existing_workspace}\" ]; then\n log \"$(msg install.reinstall.removing_workspace \"${existing_workspace}\")\"\n rm -rf \"${existing_workspace}\" || die \"$(msg install.reinstall.failed_rm_workspace)\"\n fi\n if [ -f \"${existing_env}\" ]; then\n log \"$(msg install.reinstall.removing_env \"${existing_env}\")\"\n rm -f \"${existing_env}\"\n fi\n log \"$(msg install.reinstall.cleanup_done)\"\n unset AGENTTEAMS_WORKSPACE_DIR\n return 0\n ;;\n 3|cancel|*)\n log \"$(msg install.existing.cancelled)\"\n exit 0\n ;;\n esac\n # Load existing env file as fallback (shell env vars take priority)\n if [ -f \"${existing_env}\" ]; then\n log \"$(msg install.loading_config \"${existing_env}\")\"\n while IFS='=' read -r key value; do\n case \"${key}\" in \\#*|\"\") continue ;; esac\n value=\"${value%%#*}\"\n value=\"${value#\"${value%%[![:space:]]*}\"}\"\n value=\"${value%\"${value##*[![:space:]]}\"}\"\n eval \"_existing_val=\\\"\\${${key}+x}\\\"\"\n if [ -z \"${_existing_val}\" ]; then export \"${key}=${value}\"; fi\n done < \"${existing_env}\"\n fi\n}\n\nstep_llm() {\n log \"$(msg llm.title)\"\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n if [ \"${AGENTTEAMS_LANGUAGE}\" = \"zh\" ]; then\n AGENTTEAMS_LLM_PROVIDER=\"${AGENTTEAMS_LLM_PROVIDER:-openai-compat}\"\n AGENTTEAMS_DEFAULT_MODEL=\"${AGENTTEAMS_DEFAULT_MODEL:-qwen3.6-plus}\"\n AGENTTEAMS_OPENAI_BASE_URL=\"${AGENTTEAMS_OPENAI_BASE_URL:-https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1}\"\n log \"$(msg llm.provider.label \"${AGENTTEAMS_LLM_PROVIDER}\")\"\n log \"$(msg llm.openai.base_url_label \"${AGENTTEAMS_OPENAI_BASE_URL}\")\"\n else\n AGENTTEAMS_LLM_PROVIDER=\"${AGENTTEAMS_LLM_PROVIDER:-qwen}\"\n AGENTTEAMS_DEFAULT_MODEL=\"${AGENTTEAMS_DEFAULT_MODEL:-qwen3.6-plus}\"\n AGENTTEAMS_OPENAI_BASE_URL=\"${AGENTTEAMS_OPENAI_BASE_URL:-}\"\n log \"$(msg llm.provider.qwen_default \"${AGENTTEAMS_LLM_PROVIDER}\")\"\n fi\n log \"$(msg llm.model.default \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n prompt AGENTTEAMS_LLM_API_KEY \"$(msg llm.apikey_prompt)\" \"\" \"true\"\n AGENTTEAMS_EMBEDDING_MODEL=\"${AGENTTEAMS_EMBEDDING_MODEL-text-embedding-v4}\"\n export AGENTTEAMS_LLM_PROVIDER AGENTTEAMS_DEFAULT_MODEL AGENTTEAMS_EMBEDDING_MODEL\n [ -n \"${AGENTTEAMS_OPENAI_BASE_URL+x}\" ] && export AGENTTEAMS_OPENAI_BASE_URL\n return 0\n fi\n\n # Upgrade Keep-All mode: skip provider selection, use loaded values\n local SKIP_TO_EMBEDDING=0\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ] && [ -n \"${AGENTTEAMS_LLM_PROVIDER}\" ]; then\n log \"$(msg llm.provider.label \"${AGENTTEAMS_LLM_PROVIDER}\")\"\n log \"$(msg llm.openai.base_url_label \"${AGENTTEAMS_OPENAI_BASE_URL}\")\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n PROVIDER_CHOICE=\"${AGENTTEAMS_LLM_PROVIDER}\"\n SKIP_TO_EMBEDDING=1\n else\n echo \"\"\n echo \"$(msg llm.providers_title)\"\n echo \"$(msg llm.provider.alibaba)\"\n echo \"$(msg llm.provider.openai_compat)\"\n echo \"\"\n local PROVIDER_CHOICE\n # If upgrade mode with loaded provider, show current as default\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_LLM_PROVIDER}\" ]; then\n local _prov_display\n case \"${AGENTTEAMS_LLM_PROVIDER}\" in\n openai-compat) _prov_display=\"2\" ;;\n *) _prov_display=\"1\" ;;\n esac\n read -e -p \"$(msg llm.provider.select) [${_prov_display}]: \" PROVIDER_CHOICE\n PROVIDER_CHOICE=\"${PROVIDER_CHOICE:-${_prov_display}}\"\n elif [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ]; then\n read -e -p \"$(msg llm.provider.select) [1]: \" PROVIDER_CHOICE\n PROVIDER_CHOICE=\"${PROVIDER_CHOICE:-1}\"\n else\n read -e -p \"$(msg llm.provider.select): \" PROVIDER_CHOICE\n PROVIDER_CHOICE=\"${PROVIDER_CHOICE:-1}\"\n fi\n if [ \"${PROVIDER_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n fi\n local ALIBABA_MODEL_CHOICE=\"\"\n local ALIBABA_ACCESS=\"\"\n case \"${PROVIDER_CHOICE}\" in\n 1|alibaba-cloud|openai-compat)\n if [ \"${AGENTTEAMS_LANGUAGE}\" = \"en\" ]; then\n AGENTTEAMS_LLM_PROVIDER=\"openai-compat\"\n AGENTTEAMS_OPENAI_BASE_URL=\"https://dashscope-intl.aliyuncs.com/compatible-mode/v1\"\n ALIBABA_ACCESS=\"tokenplan\"\n echo \"\"\n echo \"$(msg llm.codingplan.models_title)\"\n echo \"$(msg llm.codingplan.model.qwen36plus)\"\n echo \"$(msg llm.codingplan.model.glm5)\"\n echo \"$(msg llm.codingplan.model.kimi)\"\n echo \"$(msg llm.codingplan.model.minimax)\"\n echo \"\"\n local CODINGPLAN_MODEL_CHOICE\n # If upgrade with loaded model, show current as default\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_DEFAULT_MODEL}\" ]; then\n local _model_default\n case \"${AGENTTEAMS_DEFAULT_MODEL}\" in\n qwen3.6-plus) _model_default=\"1\" ;;\n glm-5) _model_default=\"2\" ;;\n kimi-k2.5) _model_default=\"3\" ;;\n MiniMax-M2.5) _model_default=\"4\" ;;\n *) _model_default=\"1\" ;;\n esac\n read -e -p \"$(msg llm.codingplan.model.select) [${_model_default}]: \" CODINGPLAN_MODEL_CHOICE\n CODINGPLAN_MODEL_CHOICE=\"${CODINGPLAN_MODEL_CHOICE:-${_model_default}}\"\n elif [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ]; then\n read -e -p \"$(msg llm.codingplan.model.select) [1]: \" CODINGPLAN_MODEL_CHOICE\n CODINGPLAN_MODEL_CHOICE=\"${CODINGPLAN_MODEL_CHOICE:-1}\"\n else\n read -e -p \"$(msg llm.codingplan.model.select): \" CODINGPLAN_MODEL_CHOICE\n CODINGPLAN_MODEL_CHOICE=\"${CODINGPLAN_MODEL_CHOICE:-1}\"\n fi\n if [ \"${CODINGPLAN_MODEL_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n case \"${CODINGPLAN_MODEL_CHOICE}\" in\n 1|qwen3.6-plus) AGENTTEAMS_DEFAULT_MODEL=\"qwen3.6-plus\" ;;\n 2|glm-5) AGENTTEAMS_DEFAULT_MODEL=\"glm-5\" ;;\n 3|kimi-k2.5) AGENTTEAMS_DEFAULT_MODEL=\"kimi-k2.5\" ;;\n 4|MiniMax-M2.5) AGENTTEAMS_DEFAULT_MODEL=\"MiniMax-M2.5\" ;;\n *) AGENTTEAMS_DEFAULT_MODEL=\"qwen3.6-plus\" ;;\n esac\n log \"$(msg llm.provider.selected_codingplan)\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n else\n echo \"\"\n echo \"$(msg llm.alibaba.models_title)\"\n echo \"$(msg llm.alibaba.model.tokenplan)\"\n echo \"$(msg llm.alibaba.model.bailian)\"\n echo \"$(msg llm.alibaba.model.codingplan_legacy)\"\n echo \"\"\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_OPENAI_BASE_URL}\" ]; then\n # Determine alibaba access type from loaded URL\n local _access_default=\"1\"\n if echo \"${AGENTTEAMS_OPENAI_BASE_URL}\" | grep -q \"token-plan\"; then\n _access_default=\"1\"\n elif echo \"${AGENTTEAMS_OPENAI_BASE_URL}\" | grep -q \"dashscope\"; then\n if echo \"${AGENTTEAMS_OPENAI_BASE_URL}\" | grep -q \"intl\"; then\n _access_default=\"3\"\n else\n _access_default=\"2\"\n fi\n fi\n read -e -p \"$(msg llm.alibaba.model.select) [${_access_default}]: \" ALIBABA_MODEL_CHOICE\n ALIBABA_MODEL_CHOICE=\"${ALIBABA_MODEL_CHOICE:-${_access_default}}\"\n elif [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ]; then\n read -e -p \"$(msg llm.alibaba.model.select) [1]: \" ALIBABA_MODEL_CHOICE\n ALIBABA_MODEL_CHOICE=\"${ALIBABA_MODEL_CHOICE:-1}\"\n else\n read -e -p \"$(msg llm.alibaba.model.select): \" ALIBABA_MODEL_CHOICE\n ALIBABA_MODEL_CHOICE=\"${ALIBABA_MODEL_CHOICE:-1}\"\n fi\n if [ \"${ALIBABA_MODEL_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n case \"${ALIBABA_MODEL_CHOICE}\" in\n 1|token-plan|tokenplan)\n ALIBABA_ACCESS=\"tokenplan\"\n AGENTTEAMS_LLM_PROVIDER=\"openai-compat\"\n AGENTTEAMS_OPENAI_BASE_URL=\"https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1\"\n echo \"\"\n echo \"$(msg llm.codingplan.models_title)\"\n echo \"$(msg llm.codingplan.model.qwen36plus)\"\n echo \"$(msg llm.codingplan.model.glm5)\"\n echo \"$(msg llm.codingplan.model.kimi)\"\n echo \"$(msg llm.codingplan.model.minimax)\"\n echo \"\"\n local CODINGPLAN_MODEL_CHOICE\n # If upgrade with loaded model, show current as default\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_DEFAULT_MODEL}\" ]; then\n local _model_default\n case \"${AGENTTEAMS_DEFAULT_MODEL}\" in\n qwen3.6-plus) _model_default=\"1\" ;;\n glm-5) _model_default=\"2\" ;;\n kimi-k2.5) _model_default=\"3\" ;;\n MiniMax-M2.5) _model_default=\"4\" ;;\n *) _model_default=\"1\" ;;\n esac\n read -e -p \"$(msg llm.codingplan.model.select) [${_model_default}]: \" CODINGPLAN_MODEL_CHOICE\n CODINGPLAN_MODEL_CHOICE=\"${CODINGPLAN_MODEL_CHOICE:-${_model_default}}\"\n elif [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ]; then\n read -e -p \"$(msg llm.codingplan.model.select) [1]: \" CODINGPLAN_MODEL_CHOICE\n CODINGPLAN_MODEL_CHOICE=\"${CODINGPLAN_MODEL_CHOICE:-1}\"\n else\n read -e -p \"$(msg llm.codingplan.model.select): \" CODINGPLAN_MODEL_CHOICE\n CODINGPLAN_MODEL_CHOICE=\"${CODINGPLAN_MODEL_CHOICE:-1}\"\n fi\n if [ \"${CODINGPLAN_MODEL_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n case \"${CODINGPLAN_MODEL_CHOICE}\" in\n 1|qwen3.6-plus) AGENTTEAMS_DEFAULT_MODEL=\"qwen3.6-plus\" ;;\n 2|glm-5) AGENTTEAMS_DEFAULT_MODEL=\"glm-5\" ;;\n 3|kimi-k2.5) AGENTTEAMS_DEFAULT_MODEL=\"kimi-k2.5\" ;;\n 4|MiniMax-M2.5) AGENTTEAMS_DEFAULT_MODEL=\"MiniMax-M2.5\" ;;\n *) AGENTTEAMS_DEFAULT_MODEL=\"qwen3.6-plus\" ;;\n esac\n log \"$(msg llm.provider.selected_tokenplan)\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n ;;\n 2|qwen|bailian)\n ALIBABA_ACCESS=\"bailian\"\n AGENTTEAMS_LLM_PROVIDER=\"qwen\"\n AGENTTEAMS_OPENAI_BASE_URL=\"\"\n echo \"\"\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_DEFAULT_MODEL}\" ]; then\n read -e -p \"$(msg llm.qwen.model_prompt) [${AGENTTEAMS_DEFAULT_MODEL}]: \" AGENTTEAMS_DEFAULT_MODEL\n else\n read -e -p \"$(msg llm.qwen.model_prompt) [qwen3.6-plus]: \" AGENTTEAMS_DEFAULT_MODEL\n fi\n if [ \"${AGENTTEAMS_DEFAULT_MODEL}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_DEFAULT_MODEL=\"${AGENTTEAMS_DEFAULT_MODEL:-qwen3.6-plus}\"\n log \"$(msg llm.provider.selected_qwen)\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n prompt_custom_model_params \"${AGENTTEAMS_DEFAULT_MODEL}\" || return 0\n ;;\n 3|coding-plan|codingplan)\n ALIBABA_ACCESS=\"codingplan_legacy\"\n AGENTTEAMS_LLM_PROVIDER=\"openai-compat\"\n AGENTTEAMS_OPENAI_BASE_URL=\"https://coding.dashscope.aliyuncs.com/v1\"\n echo \"\"\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_DEFAULT_MODEL}\" ]; then\n read -e -p \"$(msg llm.qwen.model_prompt) [${AGENTTEAMS_DEFAULT_MODEL}]: \" AGENTTEAMS_DEFAULT_MODEL\n else\n read -e -p \"$(msg llm.qwen.model_prompt): \" AGENTTEAMS_DEFAULT_MODEL\n fi\n if [ \"${AGENTTEAMS_DEFAULT_MODEL}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_DEFAULT_MODEL=\"${AGENTTEAMS_DEFAULT_MODEL:-qwen3.6-plus}\"\n log \"$(msg llm.provider.selected_codingplan_legacy)\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n prompt_custom_model_params \"${AGENTTEAMS_DEFAULT_MODEL}\" || return 0\n ;;\n *)\n die \"$(msg llm.alibaba.model.invalid \"${ALIBABA_MODEL_CHOICE}\")\"\n ;;\n esac\n fi\n if [ \"${ALIBABA_ACCESS}\" = \"bailian\" ]; then\n log \"$(msg llm.apikey_hint_bailian)\"\n log \"$(msg llm.apikey_url_bailian)\"\n elif [ \"${AGENTTEAMS_LANGUAGE}\" = \"en\" ]; then\n log \"$(msg llm.apikey_hint_qwencloud)\"\n log \"$(msg llm.apikey_url_qwencloud)\"\n elif [ \"${ALIBABA_ACCESS}\" = \"codingplan_legacy\" ]; then\n log \"$(msg llm.apikey_hint_codingplan)\"\n log \"$(msg llm.apikey_url_codingplan)\"\n else\n log \"$(msg llm.apikey_hint_tokenplan)\"\n log \"$(msg llm.apikey_url_tokenplan)\"\n fi\n log \"\"\n prompt AGENTTEAMS_LLM_API_KEY \"$(msg llm.apikey_prompt)\" \"\" \"true\" || return 0\n if [ \"${ALIBABA_ACCESS}\" = \"bailian\" ]; then\n test_llm_connectivity \"https://dashscope.aliyuncs.com/compatible-mode/v1\" \"${AGENTTEAMS_LLM_API_KEY}\" \"${AGENTTEAMS_DEFAULT_MODEL}\" || return 0\n elif [ \"${ALIBABA_ACCESS}\" = \"codingplan_legacy\" ]; then\n test_llm_connectivity \"https://coding.dashscope.aliyuncs.com/v1\" \"${AGENTTEAMS_LLM_API_KEY}\" \"${AGENTTEAMS_DEFAULT_MODEL}\" \"$(msg llm.openai.test.fail.codingplan_legacy)\" || return 0\n elif [ \"${AGENTTEAMS_LANGUAGE}\" = \"en\" ]; then\n test_llm_connectivity \"${AGENTTEAMS_OPENAI_BASE_URL}\" \"${AGENTTEAMS_LLM_API_KEY}\" \"${AGENTTEAMS_DEFAULT_MODEL}\" \"$(msg llm.openai.test.fail.codingplan)\" || return 0\n else\n test_llm_connectivity \"${AGENTTEAMS_OPENAI_BASE_URL}\" \"${AGENTTEAMS_LLM_API_KEY}\" \"${AGENTTEAMS_DEFAULT_MODEL}\" \"$(msg llm.openai.test.fail.tokenplan)\" || return 0\n fi\n ;;\n 2|openai-compat)\n AGENTTEAMS_LLM_PROVIDER=\"openai-compat\"\n log \"$(msg llm.provider.selected_openai \"${AGENTTEAMS_LLM_PROVIDER}\")\"\n echo \"\"\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg llm.openai.base_url_prompt)\" \"${AGENTTEAMS_OPENAI_BASE_URL}\")\"\n else\n _current=\"${AGENTTEAMS_OPENAI_BASE_URL}\"\n if [ -n \"${_current}\" ]; then\n read -e -p \"$(msg llm.openai.base_url_prompt) [${_current}]: \" AGENTTEAMS_OPENAI_BASE_URL\n else\n read -e -p \"$(msg llm.openai.base_url_prompt) [https://api.openai.com/v1]: \" AGENTTEAMS_OPENAI_BASE_URL\n fi\n if [ \"${AGENTTEAMS_OPENAI_BASE_URL}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_OPENAI_BASE_URL=\"${AGENTTEAMS_OPENAI_BASE_URL:-${_current}}\"\n [ -z \"${AGENTTEAMS_OPENAI_BASE_URL}\" ] && AGENTTEAMS_OPENAI_BASE_URL=\"https://api.openai.com/v1\"\n fi\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg llm.openai.model_prompt)\" \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n else\n _current=\"${AGENTTEAMS_DEFAULT_MODEL}\"\n if [ -n \"${_current}\" ]; then\n read -e -p \"$(msg llm.openai.model_prompt) [${_current}]: \" AGENTTEAMS_DEFAULT_MODEL\n else\n read -e -p \"$(msg llm.openai.model_prompt) [gpt-5.4]: \" AGENTTEAMS_DEFAULT_MODEL\n fi\n if [ \"${AGENTTEAMS_DEFAULT_MODEL}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_DEFAULT_MODEL=\"${AGENTTEAMS_DEFAULT_MODEL:-${_current}}\"\n [ -z \"${AGENTTEAMS_DEFAULT_MODEL}\" ] && AGENTTEAMS_DEFAULT_MODEL=\"gpt-5.4\"\n fi\n log \"$(msg llm.openai.base_url_label \"${AGENTTEAMS_OPENAI_BASE_URL}\")\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n prompt_custom_model_params \"${AGENTTEAMS_DEFAULT_MODEL}\" || return 0\n log \"\"\n prompt AGENTTEAMS_LLM_API_KEY \"$(msg llm.apikey_prompt)\" \"\" \"true\" || return 0\n test_llm_connectivity \"${AGENTTEAMS_OPENAI_BASE_URL}\" \"${AGENTTEAMS_LLM_API_KEY}\" \"${AGENTTEAMS_DEFAULT_MODEL}\" || return 0\n ;;\n *)\n die \"$(msg llm.provider.invalid \"${PROVIDER_CHOICE}\")\"\n ;;\n esac\n\n # Skip to embedding if Keep-All mode handled LLM params\n if [ \"${SKIP_TO_EMBEDDING}\" = \"1\" ]; then\n if [ -n \"${AGENTTEAMS_EMBEDDING_MODEL}\" ]; then\n log \"$(msg llm.embedding.title)\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_EMBEDDING_MODEL}\")\"\n log \"\"\n fi\n return 0\n fi\n\n # --- Embedding model (optional, auto-tested) ---\n echo \"\"\n log \"$(msg llm.embedding.title)\"\n log \"$(msg llm.embedding.hint)\"\n echo \"\"\n echo \"$(msg llm.embedding.option.default)\"\n echo \"$(msg llm.embedding.option.custom)\"\n echo \"$(msg llm.embedding.option.disable)\"\n echo \"\"\n local EMB_CHOICE\n read -e -p \"$(msg llm.embedding.select) [1]: \" EMB_CHOICE\n EMB_CHOICE=\"${EMB_CHOICE:-1}\"\n if [ \"${EMB_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n\n case \"${EMB_CHOICE}\" in\n 1)\n AGENTTEAMS_EMBEDDING_MODEL=\"text-embedding-v4\"\n ;;\n 2)\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg llm.embedding.custom_prompt)\" \"${AGENTTEAMS_EMBEDDING_MODEL}\")\"\n else\n _current=\"${AGENTTEAMS_EMBEDDING_MODEL}\"\n if [ -n \"${_current}\" ]; then\n read -e -p \"$(msg llm.embedding.custom_prompt) [${_current}]: \" AGENTTEAMS_EMBEDDING_MODEL\n else\n read -e -p \"$(msg llm.embedding.custom_prompt): \" AGENTTEAMS_EMBEDDING_MODEL\n fi\n if [ \"${AGENTTEAMS_EMBEDDING_MODEL}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_EMBEDDING_MODEL=\"${AGENTTEAMS_EMBEDDING_MODEL:-${_current}}\"\n fi\n ;;\n 3)\n AGENTTEAMS_EMBEDDING_MODEL=\"\"\n log \"$(msg llm.embedding.disabled)\"\n ;;\n *)\n AGENTTEAMS_EMBEDDING_MODEL=\"text-embedding-v4\"\n ;;\n esac\n\n if [ -n \"${AGENTTEAMS_EMBEDDING_MODEL}\" ]; then\n # Qwen provider uses dashscope directly; others use OPENAI_BASE_URL\n local EMB_BASE_URL=\"${AGENTTEAMS_OPENAI_BASE_URL}\"\n if [ \"${AGENTTEAMS_LLM_PROVIDER}\" = \"qwen\" ]; then\n EMB_BASE_URL=\"https://dashscope.aliyuncs.com/compatible-mode/v1\"\n fi\n if ! test_embedding_connectivity \"${EMB_BASE_URL}\" \"${AGENTTEAMS_LLM_API_KEY}\" \"${AGENTTEAMS_EMBEDDING_MODEL}\"; then\n AGENTTEAMS_EMBEDDING_MODEL=\"\"\n log \"$(msg llm.embedding.auto_disabled)\"\n fi\n fi\n\n export AGENTTEAMS_LLM_PROVIDER AGENTTEAMS_DEFAULT_MODEL\n [ -n \"${AGENTTEAMS_OPENAI_BASE_URL+x}\" ] && export AGENTTEAMS_OPENAI_BASE_URL\n log \"\"\n}\n\nstep_admin() {\n log \"$(msg admin.title)\"\n prompt AGENTTEAMS_ADMIN_USER \"$(msg admin.username_prompt)\" \"admin\" || return 0\n AGENTTEAMS_ADMIN_USER=\"$(printf '%s' \"${AGENTTEAMS_ADMIN_USER}\" | tr '[:upper:]' '[:lower:]')\"\n\n # Pre-set via env var: validate; in non-interactive mode fail fast,\n # in interactive mode warn and fall through to the retry prompt.\n if [ -n \"${AGENTTEAMS_ADMIN_PASSWORD:-}\" ]; then\n log \" $(msg prompt.preset \"$(msg admin.password_prompt)\")\"\n if [ ${#AGENTTEAMS_ADMIN_PASSWORD} -ge 8 ]; then\n log \"\"\n return 0\n fi\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n die \"$(msg admin.password_too_short \"${#AGENTTEAMS_ADMIN_PASSWORD}\")\"\n fi\n error \"$(msg admin.password_too_short \"${#AGENTTEAMS_ADMIN_PASSWORD}\")\"\n unset AGENTTEAMS_ADMIN_PASSWORD\n fi\n\n while true; do\n unset AGENTTEAMS_ADMIN_PASSWORD\n prompt_optional AGENTTEAMS_ADMIN_PASSWORD \"$(msg admin.password_prompt)\" \"true\" || return 0\n if [ -z \"${AGENTTEAMS_ADMIN_PASSWORD}\" ]; then\n AGENTTEAMS_ADMIN_PASSWORD=\"admin$(openssl rand -hex 6)\"\n log \"$(msg admin.password_generated)\"\n break\n fi\n if [ ${#AGENTTEAMS_ADMIN_PASSWORD} -ge 8 ]; then\n break\n fi\n error \"$(msg admin.password_too_short \"${#AGENTTEAMS_ADMIN_PASSWORD}\")\"\n done\n log \"\"\n}\n\nstep_network() {\n log \"$(msg port.local_only.title)\"\n echo \"\"\n echo \" 1) $(msg port.local_only.hint_yes)\"\n echo \" 2) $(msg port.local_only.hint_no)\"\n echo \"\"\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_LOCAL_ONLY=\"${AGENTTEAMS_LOCAL_ONLY:-1}\"\n elif [ -z \"${AGENTTEAMS_LOCAL_ONLY+x}\" ]; then\n local _local_choice\n read -e -p \"$(msg port.local_only.choice): \" _local_choice\n if [ \"${_local_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _local_choice=\"${_local_choice:-1}\"\n case \"${_local_choice}\" in\n 2|n|N|no|NO) AGENTTEAMS_LOCAL_ONLY=\"0\" ;;\n *) AGENTTEAMS_LOCAL_ONLY=\"1\" ;;\n esac\n fi\n export AGENTTEAMS_LOCAL_ONLY\n if [ \"${AGENTTEAMS_LOCAL_ONLY}\" = \"1\" ]; then\n log \"$(msg port.local_only.selected_local)\"\n else\n log \"$(msg port.local_only.selected_external)\"\n echo \"\"\n echo -e \"\\033[33m$(msg port.local_only.https_hint)\\033[0m\"\n fi\n}\n\nstep_ports() {\n log \"$(msg port.title)\"\n prompt AGENTTEAMS_PORT_GATEWAY \"$(msg port.gateway_prompt)\" \"18080\" || return 0\n prompt AGENTTEAMS_PORT_CONSOLE \"$(msg port.console_prompt)\" \"18001\" || return 0\n prompt AGENTTEAMS_PORT_ELEMENT_WEB \"$(msg port.element_prompt)\" \"18088\" || return 0\n prompt AGENTTEAMS_PORT_MANAGER_CONSOLE \"$(msg port.manager_console_prompt)\" \"18888\" || return 0\n log \"\"\n}\n\nstep_domains() {\n log \"$(msg domain.title)\"\n log \"$(msg domain.hint)\"\n prompt AGENTTEAMS_MATRIX_DOMAIN \"$(msg domain.matrix_prompt)\" \"matrix-local.agentteams.io:${AGENTTEAMS_PORT_GATEWAY}\" || return 0\n prompt AGENTTEAMS_MATRIX_CLIENT_DOMAIN \"$(msg domain.element_prompt)\" \"matrix-client-local.agentteams.io\" || return 0\n prompt AGENTTEAMS_AI_GATEWAY_DOMAIN \"$(msg domain.gateway_prompt)\" \"aigw-local.agentteams.io\" || return 0\n prompt AGENTTEAMS_FS_DOMAIN \"$(msg domain.fs_prompt)\" \"fs-local.agentteams.io\" || return 0\n if [ \"${AGENTTEAMS_MANAGER_RUNTIME}\" != \"copaw\" ]; then\n prompt AGENTTEAMS_CONSOLE_DOMAIN \"$(msg domain.console_prompt)\" \"console-local.agentteams.io\" || return 0\n fi\n log \"\"\n}\n\nstep_github() {\n log \"$(msg github.title)\"\n prompt_optional AGENTTEAMS_GITHUB_TOKEN \"$(msg github.token_prompt)\" \"true\" || return 0\n}\n\nstep_skills() {\n log \"\"\n log \"$(msg skills.title)\"\n prompt_optional AGENTTEAMS_SKILLS_API_URL \"$(msg skills.url_prompt)\" || return 0\n log \"\"\n}\n\nstep_volume() {\n log \"$(msg data.title)\"\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_DATA_DIR=\"${AGENTTEAMS_DATA_DIR:-agentteams-data}\"\n log \" $(msg data.volume_using \"${AGENTTEAMS_DATA_DIR}\") (non-interactive, skipped)\"\n export AGENTTEAMS_DATA_DIR\n return 0\n fi\n # ─────────────────────────────────────────────────────────────────\n if [ -z \"${AGENTTEAMS_DATA_DIR+x}\" ]; then\n local _input\n read -e -p \"$(msg data.volume_prompt): \" _input\n if [ \"${_input}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_DATA_DIR=\"${_input:-agentteams-data}\"\n export AGENTTEAMS_DATA_DIR\n fi\n AGENTTEAMS_DATA_DIR=\"${AGENTTEAMS_DATA_DIR:-agentteams-data}\"\n log \"$(msg data.volume_using \"${AGENTTEAMS_DATA_DIR}\")\"\n}\n\nstep_workspace() {\n log \"$(msg workspace.title)\"\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_WORKSPACE_DIR=\"${AGENTTEAMS_WORKSPACE_DIR:-${HOME}/agentteams-manager}\"\n AGENTTEAMS_WORKSPACE_DIR=\"$(cd \"${AGENTTEAMS_WORKSPACE_DIR}\" 2>/dev/null && pwd || echo \"${AGENTTEAMS_WORKSPACE_DIR}\")\"\n mkdir -p \"${AGENTTEAMS_WORKSPACE_DIR}\"\n log \" $(msg workspace.dir_label \"${AGENTTEAMS_WORKSPACE_DIR}\") (non-interactive, skipped)\"\n export AGENTTEAMS_WORKSPACE_DIR\n return 0\n fi\n # ─────────────────────────────────────────────────────────────────\n local _input\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg workspace.dir_prompt \"${HOME}/agentteams-manager\")\" \"${AGENTTEAMS_WORKSPACE_DIR}\")\"\n _input=\"${AGENTTEAMS_WORKSPACE_DIR}\"\n else\n local _current=\"${AGENTTEAMS_WORKSPACE_DIR}\"\n if [ -n \"${_current}\" ]; then\n read -e -p \"$(msg workspace.dir_prompt \"${HOME}/agentteams-manager\") [${_current}]: \" _input\n else\n read -e -p \"$(msg workspace.dir_prompt \"${HOME}/agentteams-manager\"): \" _input\n fi\n if [ \"${_input}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _input=\"${_input:-${_current}}\"\n fi\n AGENTTEAMS_WORKSPACE_DIR=\"${_input:-${HOME}/agentteams-manager}\"\n export AGENTTEAMS_WORKSPACE_DIR\n AGENTTEAMS_WORKSPACE_DIR=\"$(cd \"${AGENTTEAMS_WORKSPACE_DIR}\" 2>/dev/null && pwd || echo \"${AGENTTEAMS_WORKSPACE_DIR}\")\"\n mkdir -p \"${AGENTTEAMS_WORKSPACE_DIR}\"\n log \"$(msg workspace.dir_label \"${AGENTTEAMS_WORKSPACE_DIR}\")\"\n}\n\nstep_dashboard() {\n AGENTTEAMS_DASHBOARD=\"${AGENTTEAMS_DASHBOARD:-1}\"\n AGENTTEAMS_DASHBOARD_VERSION=\"${AGENTTEAMS_DASHBOARD_VERSION:-v1.2.0}\"\n AGENTTEAMS_PORT_DASHBOARD=\"${AGENTTEAMS_PORT_DASHBOARD:-13000}\"\n AGENTTEAMS_AI_GATEWAY_ADMIN_URL=\"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL:-}\"\n\n # Compute the default image for the current version.\n # We track whether the current image matches the derived default so that\n # changing the version also updates the image (unless the user has\n # explicitly overridden it to a non-default value).\n _dashboard_default_image() {\n echo \"${AGENTTEAMS_REGISTRY}/agentteams/agentteams-dashboard:${1:-${AGENTTEAMS_DASHBOARD_VERSION}}\"\n }\n local _init_default\n _init_default=$(_dashboard_default_image)\n if [ -z \"${AGENTTEAMS_DASHBOARD_IMAGE:-}\" ]; then\n AGENTTEAMS_DASHBOARD_IMAGE=\"${_init_default}\"\n fi\n\n # Recompute default image when the version changes AND the current image\n # matches the default for the *previous* (env-file) version.\n # This handles non-interactive upgrades correctly:\n # - env file loads old default image + old version\n # - CLI passes new version\n # - image should follow (because it was auto-derived before)\n # But preserves truly custom images (same repo but custom tag like :canary)\n # because they won't match the old-version default exactly.\n local _old_saved_version=\"\"\n local _env_file=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n if [ -f \"${_env_file}\" ]; then\n _old_saved_version=\"$(grep '^AGENTTEAMS_DASHBOARD_VERSION=' \"${_env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n fi\n if [ -n \"${_old_saved_version}\" ] && \\\n [ \"${_old_saved_version}\" != \"${AGENTTEAMS_DASHBOARD_VERSION}\" ] && \\\n [ \"${AGENTTEAMS_DASHBOARD_IMAGE}\" = \"$(_dashboard_default_image \"${_old_saved_version}\")\" ]; then\n AGENTTEAMS_DASHBOARD_IMAGE=$(_dashboard_default_image)\n fi\n\n # Quick Start: accept all defaults without prompting. The dashboard is\n # installed by default (AGENTTEAMS_DASHBOARD=1); set AGENTTEAMS_DASHBOARD=0\n # to opt out.\n if [ \"${AGENTTEAMS_QUICKSTART:-0}\" = \"1\" ]; then\n if [ \"${AGENTTEAMS_DASHBOARD}\" = \"1\" ]; then\n log \"$(msg dash.summary \"${AGENTTEAMS_PORT_DASHBOARD}\" \"${AGENTTEAMS_DASHBOARD_IMAGE}\") (quick start)\"\n else\n log \"$(msg dash.skip) (quick start)\"\n fi\n export AGENTTEAMS_DASHBOARD AGENTTEAMS_DASHBOARD_VERSION AGENTTEAMS_PORT_DASHBOARD AGENTTEAMS_DASHBOARD_IMAGE AGENTTEAMS_AI_GATEWAY_ADMIN_URL\n return 0\n fi\n\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_DASHBOARD=\"${AGENTTEAMS_DASHBOARD:-1}\"\n if [ \"${AGENTTEAMS_DASHBOARD}\" = \"1\" ]; then\n log \"$(msg dash.summary \"${AGENTTEAMS_PORT_DASHBOARD}\" \"${AGENTTEAMS_DASHBOARD_IMAGE}\") (non-interactive)\"\n else\n log \"$(msg dash.skip) (non-interactive)\"\n fi\n export AGENTTEAMS_DASHBOARD AGENTTEAMS_DASHBOARD_VERSION AGENTTEAMS_PORT_DASHBOARD AGENTTEAMS_DASHBOARD_IMAGE AGENTTEAMS_AI_GATEWAY_ADMIN_URL\n return 0\n fi\n\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ]; then\n log \"$(msg prompt.upgrade_keep \"\" \"${AGENTTEAMS_DASHBOARD}\") dashboard=${AGENTTEAMS_DASHBOARD} version=${AGENTTEAMS_DASHBOARD_VERSION} port=${AGENTTEAMS_PORT_DASHBOARD}\"\n export AGENTTEAMS_DASHBOARD AGENTTEAMS_DASHBOARD_VERSION AGENTTEAMS_PORT_DASHBOARD AGENTTEAMS_DASHBOARD_IMAGE AGENTTEAMS_AI_GATEWAY_ADMIN_URL\n return 0\n fi\n\n local _input\n echo \"\"\n read -p \"$(msg dash.prompt) [Y/n]: \" _input\n case \"${_input:-}\" in\n [nN0]) AGENTTEAMS_DASHBOARD=0 ;;\n *) AGENTTEAMS_DASHBOARD=1 ;;\n esac\n if [ \"${_input}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n\n if [ \"${AGENTTEAMS_DASHBOARD}\" != \"1\" ]; then\n log \"$(msg dash.skip)\"\n export AGENTTEAMS_DASHBOARD AGENTTEAMS_DASHBOARD_VERSION AGENTTEAMS_PORT_DASHBOARD AGENTTEAMS_DASHBOARD_IMAGE AGENTTEAMS_AI_GATEWAY_ADMIN_URL\n return 0\n fi\n\n # Dashboard version\n local _current_version=\"${AGENTTEAMS_DASHBOARD_VERSION}\"\n local _old_version=\"${AGENTTEAMS_DASHBOARD_VERSION}\"\n local _old_default_image\n _old_default_image=$(_dashboard_default_image \"${_old_version}\")\n read -p \"$(msg dash.version_prompt) [${_current_version}]: \" _input\n AGENTTEAMS_DASHBOARD_VERSION=\"${_input:-${_current_version}}\"\n\n # Dashboard port\n local _current_port=\"${AGENTTEAMS_PORT_DASHBOARD}\"\n read -p \"$(msg dash.port_prompt) [${_current_port}]: \" _input\n AGENTTEAMS_PORT_DASHBOARD=\"${_input:-${_current_port}}\"\n\n # Dashboard image: if the current image matches the default derived from\n # the OLD version, recompute it from the NEW version. This handles both\n # fresh installs and upgrades where the previous image was the default.\n # Only a real user override (image != default for old version) is preserved.\n if [ \"${AGENTTEAMS_DASHBOARD_VERSION}\" != \"${_old_version}\" ] && \\\n [ \"${AGENTTEAMS_DASHBOARD_IMAGE}\" = \"${_old_default_image}\" ]; then\n AGENTTEAMS_DASHBOARD_IMAGE=$(_dashboard_default_image)\n fi\n local _current_image=\"${AGENTTEAMS_DASHBOARD_IMAGE}\"\n read -p \"$(msg dash.image_prompt) [${_current_image}]: \" _input\n if [ -n \"${_input}\" ]; then\n AGENTTEAMS_DASHBOARD_IMAGE=\"${_input}\"\n fi\n\n # Higress Console URL (explicit config takes priority; auto-detect as fallback)\n local _higress_url=\"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\"\n if [ -z \"${_higress_url}\" ]; then\n if ${DOCKER_CMD} ps --format '{{.Names}}' | grep -q \"^agentteams-controller$\"; then\n if ${DOCKER_CMD} exec agentteams-controller curl -sf --max-time 2 http://127.0.0.1:8001/ >/dev/null 2>&1; then\n _higress_url=\"http://agentteams-controller:8001\"\n log \"$(msg dash.auto_url): ${_higress_url}\"\n else\n log \"$(msg dash.auto_url_fail)\"\n fi\n fi\n fi\n read -p \"$(msg dash.gateway_prompt) [${_higress_url:-<auto>}]: \" _input\n AGENTTEAMS_AI_GATEWAY_ADMIN_URL=\"${_input:-${_higress_url}}\"\n\n # Normalize URL: add http:// prefix if missing\n if [ -n \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\" ]; then\n case \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\" in\n http://*|https://*) ;;\n *) AGENTTEAMS_AI_GATEWAY_ADMIN_URL=\"http://${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\" ;;\n esac\n fi\n\n # Verify the Higress Console URL is reachable (best-effort warning only)\n if [ -n \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\" ]; then\n local _gw_reachable=0\n # If the URL points to the controller container, test from inside the container\n if echo \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\" | grep -qE \"(agentteams-controller|hiclaw-controller|127\\.0\\.0\\.1|localhost)\"; then\n if ${DOCKER_CMD} ps --format '{{.Names}}' | grep -q \"^agentteams-controller$\"; then\n if ${DOCKER_CMD} exec agentteams-controller curl -sf --max-time 3 \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}/\" >/dev/null 2>&1; then\n _gw_reachable=1\n fi\n fi\n else\n # External URL — probe from inside the controller (same network as\n # the Dashboard container) when possible; fall back to the host.\n if ${DOCKER_CMD} ps --format '{{.Names}}' | grep -q \"^agentteams-controller$\"; then\n if ${DOCKER_CMD} exec agentteams-controller curl -sf --max-time 3 \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}/\" >/dev/null 2>&1; then\n _gw_reachable=1\n fi\n elif curl -sf --max-time 3 \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}/\" >/dev/null 2>&1; then\n _gw_reachable=1\n fi\n fi\n if [ \"${_gw_reachable}\" = \"0\" ]; then\n log \"WARNING: Higress Console URL may not be reachable: ${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\"\n log \" Dashboard will still work, but shared login via Higress Console may fail.\"\n log \" To fix: verify the URL is correct and the Higress Console service is running.\"\n fi\n fi\n\n log \"$(msg dash.summary \"${AGENTTEAMS_PORT_DASHBOARD}\" \"${AGENTTEAMS_DASHBOARD_IMAGE}\")\"\n export AGENTTEAMS_DASHBOARD AGENTTEAMS_DASHBOARD_VERSION AGENTTEAMS_PORT_DASHBOARD AGENTTEAMS_DASHBOARD_IMAGE AGENTTEAMS_AI_GATEWAY_ADMIN_URL\n}\n\nstep_runtime() {\n log \"$(msg worker_runtime.title)\"\n echo \"\"\n echo \" 1) $(msg worker_runtime.copaw)\"\n echo \" 2) $(msg worker_runtime.openclaw)\"\n if ! _ver_lt \"${AGENTTEAMS_VERSION}\" \"v1.1.0\"; then\n echo \" 3) $(msg worker_runtime.hermes)\"\n fi\n echo \"\"\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"${AGENTTEAMS_DEFAULT_WORKER_RUNTIME:-copaw}\"\n elif [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_DEFAULT_WORKER_RUNTIME}\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg worker_runtime.title_short)\" \"${AGENTTEAMS_DEFAULT_WORKER_RUNTIME}\")\"\n local _runtime_choice\n read -e -p \"$(msg worker_runtime.choice): \" _runtime_choice\n if [ \"${_runtime_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n if [ -n \"${_runtime_choice}\" ]; then\n case \"${_runtime_choice}\" in\n 2) AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"openclaw\" ;;\n 3) if ! _ver_lt \"${AGENTTEAMS_VERSION}\" \"v1.1.0\"; then\n AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"hermes\"\n else\n AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"copaw\"\n fi ;;\n *) AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"copaw\" ;;\n esac\n fi\n elif [ -z \"${AGENTTEAMS_DEFAULT_WORKER_RUNTIME+x}\" ]; then\n local _runtime_choice\n read -e -p \"$(msg worker_runtime.choice): \" _runtime_choice\n if [ \"${_runtime_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _runtime_choice=\"${_runtime_choice:-1}\"\n case \"${_runtime_choice}\" in\n 2) AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"openclaw\" ;;\n 3) if ! _ver_lt \"${AGENTTEAMS_VERSION}\" \"v1.1.0\"; then\n AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"hermes\"\n else\n AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"copaw\"\n fi ;;\n *) AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"copaw\" ;;\n esac\n fi\n export AGENTTEAMS_DEFAULT_WORKER_RUNTIME\n log \"$(msg worker_runtime.selected \"${AGENTTEAMS_DEFAULT_WORKER_RUNTIME}\")\"\n}\n\nstep_manager_runtime() {\n log \"$(msg manager_runtime.title)\"\n echo \"\"\n echo \" 1) $(msg manager_runtime.copaw)\"\n echo \" 2) $(msg manager_runtime.openclaw)\"\n echo \"\"\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_MANAGER_RUNTIME=\"${AGENTTEAMS_MANAGER_RUNTIME:-copaw}\"\n elif [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_MANAGER_RUNTIME}\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg manager_runtime.title_short)\" \"${AGENTTEAMS_MANAGER_RUNTIME}\")\"\n local _runtime_choice\n read -e -p \"$(msg manager_runtime.choice): \" _runtime_choice\n if [ \"${_runtime_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n if [ -n \"${_runtime_choice}\" ]; then\n case \"${_runtime_choice}\" in\n 2) AGENTTEAMS_MANAGER_RUNTIME=\"openclaw\" ;;\n *) AGENTTEAMS_MANAGER_RUNTIME=\"copaw\" ;;\n esac\n fi\n elif [ -z \"${AGENTTEAMS_MANAGER_RUNTIME+x}\" ]; then\n local _runtime_choice\n read -e -p \"$(msg manager_runtime.choice): \" _runtime_choice\n if [ \"${_runtime_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _runtime_choice=\"${_runtime_choice:-1}\"\n case \"${_runtime_choice}\" in\n 2) AGENTTEAMS_MANAGER_RUNTIME=\"openclaw\" ;;\n *) AGENTTEAMS_MANAGER_RUNTIME=\"copaw\" ;;\n esac\n fi\n export AGENTTEAMS_MANAGER_RUNTIME\n log \"$(msg manager_runtime.selected \"${AGENTTEAMS_MANAGER_RUNTIME}\")\"\n}\n\nstep_e2ee() {\n log \"\"\n log \"$(msg matrix_e2ee.title)\"\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_MATRIX_E2EE=\"${AGENTTEAMS_MATRIX_E2EE:-0}\"\n log \" $(msg matrix_e2ee.title_short) = ${AGENTTEAMS_MATRIX_E2EE} (non-interactive, skipped)\"\n export AGENTTEAMS_MATRIX_E2EE\n return 0\n fi\n # ─────────────────────────────────────────────────────────────────\n echo \"\"\n echo -e \" $(msg matrix_e2ee.desc)\"\n echo \"\"\n echo \" 1) $(msg matrix_e2ee.disable)\"\n echo \" 2) $(msg matrix_e2ee.enable)\"\n echo \"\"\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_MATRIX_E2EE}\" ]; then\n local _e2ee_display; if [ \"${AGENTTEAMS_MATRIX_E2EE}\" = \"1\" ]; then _e2ee_display=\"$(msg matrix_e2ee.val_enabled)\"; else _e2ee_display=\"$(msg matrix_e2ee.val_disabled)\"; fi\n log \"$(msg prompt.upgrade_keep \"$(msg matrix_e2ee.title_short)\" \"${_e2ee_display}\")\"\n local _e2ee_choice\n read -e -p \"$(msg matrix_e2ee.choice): \" _e2ee_choice\n if [ \"${_e2ee_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n if [ -n \"${_e2ee_choice}\" ]; then\n case \"${_e2ee_choice}\" in\n 2) AGENTTEAMS_MATRIX_E2EE=\"1\" ;;\n *) AGENTTEAMS_MATRIX_E2EE=\"0\" ;;\n esac\n fi\n elif [ -z \"${AGENTTEAMS_MATRIX_E2EE+x}\" ]; then\n local _e2ee_choice\n read -e -p \"$(msg matrix_e2ee.choice): \" _e2ee_choice\n if [ \"${_e2ee_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _e2ee_choice=\"${_e2ee_choice:-1}\"\n case \"${_e2ee_choice}\" in\n 2) AGENTTEAMS_MATRIX_E2EE=\"1\" ;;\n *) AGENTTEAMS_MATRIX_E2EE=\"0\" ;;\n esac\n fi\n AGENTTEAMS_MATRIX_E2EE=\"${AGENTTEAMS_MATRIX_E2EE:-0}\"\n export AGENTTEAMS_MATRIX_E2EE\n if [ \"${AGENTTEAMS_MATRIX_E2EE}\" = \"1\" ]; then\n log \"$(msg matrix_e2ee.selected_enabled)\"\n else\n log \"$(msg matrix_e2ee.selected_disabled)\"\n fi\n}\n\nstep_docker_proxy() {\n # Only relevant when socket mounting is enabled\n if [ \"${AGENTTEAMS_MOUNT_SOCKET}\" != \"1\" ]; then\n AGENTTEAMS_DOCKER_PROXY=\"0\"\n return 0\n fi\n\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_DOCKER_PROXY=\"${AGENTTEAMS_DOCKER_PROXY:-0}\"\n log \" $(msg docker_proxy.title_short) = ${AGENTTEAMS_DOCKER_PROXY} (non-interactive, skipped)\"\n export AGENTTEAMS_DOCKER_PROXY\n return 0\n fi\n # ─────────────────────────────────────────────────────────────────\n\n echo \"\"\n echo -e \" \\033[1m$(msg docker_proxy.title)\\033[0m\"\n echo \"\"\n echo -e \" $(msg docker_proxy.desc)\"\n echo \"\"\n echo \" 1) $(msg docker_proxy.enable)\"\n echo \" 2) $(msg docker_proxy.disable)\"\n echo \"\"\n\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_DOCKER_PROXY}\" ]; then\n local _proxy_display; if [ \"${AGENTTEAMS_DOCKER_PROXY}\" = \"1\" ]; then _proxy_display=\"$(msg docker_proxy.val_enabled)\"; else _proxy_display=\"$(msg docker_proxy.val_disabled)\"; fi\n log \"$(msg prompt.upgrade_keep \"$(msg docker_proxy.title_short)\" \"${_proxy_display}\")\"\n local _choice\n read -e -p \"$(msg docker_proxy.choice): \" _choice\n if [ \"${_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n if [ -n \"${_choice}\" ]; then\n case \"${_choice}\" in\n 2) AGENTTEAMS_DOCKER_PROXY=\"0\" ;;\n *) AGENTTEAMS_DOCKER_PROXY=\"1\" ;;\n esac\n fi\n elif [ -z \"${AGENTTEAMS_DOCKER_PROXY+x}\" ]; then\n local _choice\n read -e -p \"$(msg docker_proxy.choice): \" _choice\n if [ \"${_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _choice=\"${_choice:-1}\"\n case \"${_choice}\" in\n 2) AGENTTEAMS_DOCKER_PROXY=\"0\" ;;\n *) AGENTTEAMS_DOCKER_PROXY=\"1\" ;;\n esac\n fi\n AGENTTEAMS_DOCKER_PROXY=\"${AGENTTEAMS_DOCKER_PROXY:-1}\"\n export AGENTTEAMS_DOCKER_PROXY\n if [ \"${AGENTTEAMS_DOCKER_PROXY}\" = \"1\" ]; then\n log \"$(msg docker_proxy.selected_enabled)\"\n\n # Prompt for additional allowed image sources\n echo \"\"\n echo -e \" $(msg docker_proxy.registries_desc)\"\n echo \"\"\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_PROXY_ALLOWED_REGISTRIES}\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg docker_proxy.registries_label)\" \"${AGENTTEAMS_PROXY_ALLOWED_REGISTRIES}\")\"\n local _reg_input\n read -e -p \"$(msg docker_proxy.registries_prompt): \" _reg_input\n if [ \"${_reg_input}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n [ -n \"${_reg_input}\" ] && AGENTTEAMS_PROXY_ALLOWED_REGISTRIES=\"${_reg_input}\"\n elif [ -z \"${AGENTTEAMS_PROXY_ALLOWED_REGISTRIES+x}\" ]; then\n local _reg_input\n read -e -p \"$(msg docker_proxy.registries_prompt): \" _reg_input\n if [ \"${_reg_input}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_PROXY_ALLOWED_REGISTRIES=\"${_reg_input:-}\"\n fi\n export AGENTTEAMS_PROXY_ALLOWED_REGISTRIES\n else\n log \"$(msg docker_proxy.selected_disabled)\"\n fi\n}\n\nstep_idle() {\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_WORKER_IDLE_TIMEOUT=\"${AGENTTEAMS_WORKER_IDLE_TIMEOUT:-720}\"\n log \" $(msg idle_timeout.label) = ${AGENTTEAMS_WORKER_IDLE_TIMEOUT} (non-interactive, skipped)\"\n export AGENTTEAMS_WORKER_IDLE_TIMEOUT\n return 0\n fi\n # ─────────────────────────────────────────────────────────────────\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_WORKER_IDLE_TIMEOUT}\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg idle_timeout.label)\" \"${AGENTTEAMS_WORKER_IDLE_TIMEOUT}\")\"\n local _idle_timeout\n read -e -p \"$(msg idle_timeout.prompt): \" _idle_timeout\n if [ \"${_idle_timeout}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n [ -n \"${_idle_timeout}\" ] && AGENTTEAMS_WORKER_IDLE_TIMEOUT=\"${_idle_timeout}\"\n elif [ -z \"${AGENTTEAMS_WORKER_IDLE_TIMEOUT+x}\" ]; then\n local _idle_timeout\n read -e -p \"$(msg idle_timeout.prompt): \" _idle_timeout\n if [ \"${_idle_timeout}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_WORKER_IDLE_TIMEOUT=\"${_idle_timeout:-720}\"\n fi\n AGENTTEAMS_WORKER_IDLE_TIMEOUT=\"${AGENTTEAMS_WORKER_IDLE_TIMEOUT:-720}\"\n export AGENTTEAMS_WORKER_IDLE_TIMEOUT\n log \"$(msg idle_timeout.selected \"${AGENTTEAMS_WORKER_IDLE_TIMEOUT}\")\"\n}\n\nstep_hostshare() {\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_HOST_SHARE_DIR=\"${AGENTTEAMS_HOST_SHARE_DIR:-${HOME}}\"\n log \" $(msg host_share.label) = ${AGENTTEAMS_HOST_SHARE_DIR} (non-interactive, skipped)\"\n export AGENTTEAMS_HOST_SHARE_DIR\n return 0\n fi\n # ─────────────────────────────────────────────────────────────────\n local _current=\"${AGENTTEAMS_HOST_SHARE_DIR}\"\n local _share_dir\n if [ -n \"${_current}\" ]; then\n read -e -p \"$(msg host_share.prompt \"$HOME\") [${_current}]: \" _share_dir\n else\n read -e -p \"$(msg host_share.prompt \"$HOME\"): \" _share_dir\n fi\n if [ \"${_share_dir}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_HOST_SHARE_DIR=\"${_share_dir:-${_current:-$HOME}}\"\n export AGENTTEAMS_HOST_SHARE_DIR\n}\n\nstep_podman_autostart() {\n echo \"\"\n log \"$(msg podman.autostart.title)\"\n echo \"\"\n echo -e \" $(msg podman.autostart.desc)\"\n echo \"\"\n echo \" 1) $(msg podman.autostart.enable)\"\n echo \" 2) $(msg podman.autostart.disable)\"\n echo \"\"\n\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_PODMAN_AUTOSTART}\" ]; then\n local _disp; if [ \"${AGENTTEAMS_PODMAN_AUTOSTART}\" = \"1\" ]; then _disp=\"$(msg podman.autostart.val_enabled)\"; else _disp=\"$(msg podman.autostart.val_disabled)\"; fi\n log \"$(msg prompt.upgrade_keep \"$(msg podman.autostart.title_short)\" \"${_disp}\")\"\n local _choice\n read -e -p \"$(msg podman.autostart.choice): \" _choice\n if [ \"${_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n if [ -n \"${_choice}\" ]; then\n case \"${_choice}\" in\n 2) AGENTTEAMS_PODMAN_AUTOSTART=\"0\" ;;\n *) AGENTTEAMS_PODMAN_AUTOSTART=\"1\" ;;\n esac\n fi\n elif [ -z \"${AGENTTEAMS_PODMAN_AUTOSTART+x}\" ]; then\n local _choice\n read -e -p \"$(msg podman.autostart.choice) [1]: \" _choice\n if [ \"${_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _choice=\"${_choice:-1}\"\n case \"${_choice}\" in\n 2) AGENTTEAMS_PODMAN_AUTOSTART=\"0\" ;;\n *) AGENTTEAMS_PODMAN_AUTOSTART=\"1\" ;;\n esac\n fi\n AGENTTEAMS_PODMAN_AUTOSTART=\"${AGENTTEAMS_PODMAN_AUTOSTART:-1}\"\n export AGENTTEAMS_PODMAN_AUTOSTART\n if [ \"${AGENTTEAMS_PODMAN_AUTOSTART}\" = \"1\" ]; then\n log \"$(msg podman.autostart.selected_enabled)\"\n else\n log \"$(msg podman.autostart.selected_disabled)\"\n fi\n}\n\n# ============================================================\n# Setup dedicated Podman autostart service for AgentTeams\n# ============================================================\nsetup_podman_autostart() {\n # Only execute if using podman and autostart is enabled\n if [ \"${DOCKER_CMD:-docker}\" != \"podman\" ] || [ \"${AGENTTEAMS_PODMAN_AUTOSTART:-0}\" != \"1\" ]; then\n return 0\n fi\n\n log \"$(msg install.podman.autostart_title)\"\n\n local current_user\n current_user=\"$(whoami)\"\n\n # Define the dedicated AgentTeams service content\n # Strict scoping: Only manage containers matching '--filter name=agentteams'\n local _service_content=\"[Unit]\nDescription=AgentTeams Dedicated Podman Autostart Service\nDocumentation=https://github.com/agentscope-ai/AgentTeams\nWants=network-online.target\nAfter=network-online.target\n\n[Service]\nType=oneshot\nRemainAfterExit=true\nEnvironment=LOGGING=\\\"--log-level=info\\\"\nExecStart=/usr/bin/podman \\$LOGGING start --filter name=agentteams --filter restart-policy=always --filter restart-policy=unless-stopped\nExecStop=/usr/bin/podman \\$LOGGING stop --filter name=agentteams --filter restart-policy=always --filter restart-policy=unless-stopped\n\n[Install]\nWantedBy=default.target\"\n\n local _autostart_ok=0\n\n if [ \"${current_user}\" = \"root\" ]; then\n # Rootful Mode (System-wide dedicated service)\n log \"$(msg install.podman.root_setup)\"\n\n local _sys_dir=\"/etc/systemd/system\"\n mkdir -p \"${_sys_dir}\"\n echo \"${_service_content}\" > \"${_sys_dir}/agentteams-podman-restart.service\"\n\n systemctl daemon-reload >/dev/null 2>&1 || true\n\n if systemctl enable --now agentteams-podman-restart.service >/dev/null 2>&1; then\n log \"$(msg install.podman.root_success)\"\n _autostart_ok=1\n else\n log \"$(msg install.podman.root_fail)\"\n fi\n else\n # Rootless Mode (User-level dedicated service)\n log \"$(msg install.podman.user_setup \"${current_user}\")\"\n\n local _linger_ok=0\n\n # 1. Enable Linger for background execution (Graceful escalation strategy)\n if loginctl show-user \"${current_user}\" --property=Linger 2>/dev/null | grep -q \"Linger=yes\"; then\n _linger_ok=1\n else\n log \"$(msg install.podman.linger_enable)\"\n\n # Step A: Try to enable linger natively without sudo\n if loginctl enable-linger \"${current_user}\" >/dev/null 2>&1; then\n log \"Successfully enabled systemd linger natively.\"\n _linger_ok=1\n else\n # Step B: Fallback to sudo\n log \"Native linger enablement failed. Distro security policy requires privileges.\"\n if command -v sudo >/dev/null 2>&1; then\n log \"Attempting to enable systemd linger via sudo (enter password if prompted)...\"\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n sudo -n loginctl enable-linger \"${current_user}\" 2>/dev/null && _linger_ok=1 || log \"$(msg install.podman.linger_warn)\"\n else\n sudo loginctl enable-linger \"${current_user}\" && _linger_ok=1 || log \"$(msg install.podman.linger_warn)\"\n fi\n else\n log \"$(msg install.podman.linger_warn)\"\n fi\n fi\n fi\n\n # 2. Write dedicated service file to user directory\n local _user_dir=\"${HOME}/.config/systemd/user\"\n mkdir -p \"${_user_dir}\"\n echo \"${_service_content}\" > \"${_user_dir}/agentteams-podman-restart.service\"\n\n systemctl --user daemon-reload >/dev/null 2>&1 || true\n\n if systemctl --user enable --now agentteams-podman-restart.service >/dev/null 2>&1; then\n log \"$(msg install.podman.user_success)\"\n # Crucial: Only consider autostart fully successful if systemd linger is also enabled\n if [ \"${_linger_ok}\" = \"1\" ]; then\n _autostart_ok=1\n else\n log \"⚠️ Service enabled, but since systemd linger is not active, containers will not auto-recover on system reboot.\"\n fi\n else\n log \"$(msg install.podman.user_fail)\"\n fi\n fi\n\n # Only print final success banner if the service AND linger were actually enabled\n if [ \"${_autostart_ok}\" = \"1\" ]; then\n log \"$(msg install.podman.success)\"\n fi\n}\n\n# ============================================================\n# Dashboard startup (called after controller is ready)\n# ============================================================\n\n_start_dashboard() {\n local CTRL_CONTAINER=\"agentteams-controller\"\n local DASHBOARD_CONTAINER=\"agentteams-dashboard\"\n\n if [ \"${AGENTTEAMS_DASHBOARD:-0}\" != \"1\" ]; then\n # Stop and remove the existing dashboard container if present.\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -q \"^${DASHBOARD_CONTAINER}$\"; then\n log \"Stopping and removing ${DASHBOARD_CONTAINER}...\"\n ${DOCKER_CMD} stop \"${DASHBOARD_CONTAINER}\" >/dev/null 2>&1 || true\n ${DOCKER_CMD} rm -f \"${DASHBOARD_CONTAINER}\" >/dev/null 2>&1 || true\n log \"Dashboard stopped.\"\n fi\n return 0\n fi\n\n # Dashboard requires the embedded architecture (controller container).\n # In legacy all-in-one mode the controller API is not available.\n if [ \"${AGENTTEAMS_USE_EMBEDDED:-0}\" != \"1\" ]; then\n log \"Skipping Dashboard: requires embedded architecture (agentteams-controller), not available in legacy mode.\"\n log \"Use Quick Start (embedded) mode to enable agentteams-dashboard.\"\n return 0\n fi\n\n AGENTTEAMS_PORT_DASHBOARD=\"${AGENTTEAMS_PORT_DASHBOARD:-13000}\"\n AGENTTEAMS_DASHBOARD_VERSION=\"${AGENTTEAMS_DASHBOARD_VERSION:-v1.2.0}\"\n AGENTTEAMS_DASHBOARD_IMAGE=\"${AGENTTEAMS_DASHBOARD_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-dashboard:${AGENTTEAMS_DASHBOARD_VERSION}}\"\n\n log \"\"\n log \"Starting agentteams-dashboard...\"\n\n local BIND_ADDR=\"127.0.0.1\"\n [ \"${AGENTTEAMS_LOCAL_ONLY:-1}\" = \"0\" ] && BIND_ADDR=\"0.0.0.0\"\n\n # Remove existing container\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' | grep -qx \"${DASHBOARD_CONTAINER}\"; then\n ${DOCKER_CMD} rm -f \"${DASHBOARD_CONTAINER}\" >/dev/null 2>&1 || true\n fi\n\n # Pull or use local image\n if ! ${DOCKER_CMD} image inspect \"${AGENTTEAMS_DASHBOARD_IMAGE}\" >/dev/null 2>&1; then\n log \"Pulling dashboard image: ${AGENTTEAMS_DASHBOARD_IMAGE}\"\n ${DOCKER_CMD} pull \"${AGENTTEAMS_DASHBOARD_IMAGE}\" 2>/dev/null || {\n log \"WARNING: Could not pull dashboard image. Skipping.\"\n return 0\n }\n fi\n\n # Build env args from controller container (MinIO/LLM/auth).\n local env_args=()\n env_args+=(-e AGENTTEAMS_CONTROLLER_URL=\"http://${CTRL_CONTAINER}:8090\")\n env_args+=(-e NEXT_PUBLIC_MATRIX_API_URL=\"http://${CTRL_CONTAINER}:6167\")\n env_args+=(-e MATRIX_HOMESERVER_ALLOWLIST=\"${CTRL_CONTAINER},matrix-local.agentteams.io,matrix.org\")\n\n if ${DOCKER_CMD} ps --format '{{.Names}}' | grep -qx \"${CTRL_CONTAINER}\"; then\n local env_out\n env_out=$(${DOCKER_CMD} inspect \"${CTRL_CONTAINER}\" --format='{{range .Config.Env}}{{.}}{{\"\\n\"}}{{end}}')\n\n _env() { echo \"${env_out}\" | sed -n \"s|^${1}=||p\" | head -n1; }\n\n local fs_endpoint; fs_endpoint=$(_env AGENTTEAMS_FS_ENDPOINT)\n [ -z \"${fs_endpoint}\" ] && fs_endpoint=$(_env AGENTTEAMS_MINIO_ENDPOINT)\n if [ -n \"${fs_endpoint}\" ]; then\n fs_endpoint=$(echo \"${fs_endpoint}\" | sed -e \"s|127\\.0\\.0\\.1|${CTRL_CONTAINER}|\" -e \"s|localhost|${CTRL_CONTAINER}|\")\n env_args+=(-e AGENTTEAMS_FS_ENDPOINT=\"${fs_endpoint}\")\n else\n env_args+=(-e AGENTTEAMS_FS_ENDPOINT=\"http://${CTRL_CONTAINER}:9000\")\n fi\n\n local fs_bucket; fs_bucket=$(_env AGENTTEAMS_FS_BUCKET)\n [ -z \"${fs_bucket}\" ] && fs_bucket=$(_env AGENTTEAMS_MINIO_BUCKET)\n [ -n \"${fs_bucket}\" ] && env_args+=(-e AGENTTEAMS_FS_BUCKET=\"${fs_bucket}\")\n\n local fs_user; fs_user=$(_env AGENTTEAMS_FS_ACCESS_KEY)\n [ -z \"${fs_user}\" ] && fs_user=$(_env AGENTTEAMS_MINIO_USER)\n [ -n \"${fs_user}\" ] && env_args+=(-e AGENTTEAMS_FS_ACCESS_KEY=\"${fs_user}\")\n\n local fs_pass; fs_pass=$(_env AGENTTEAMS_FS_SECRET_KEY)\n [ -z \"${fs_pass}\" ] && fs_pass=$(_env AGENTTEAMS_MINIO_PASSWORD)\n [ -n \"${fs_pass}\" ] && env_args+=(-e AGENTTEAMS_FS_SECRET_KEY=\"${fs_pass}\")\n\n for k in AGENTTEAMS_LLM_PROVIDER AGENTTEAMS_LLM_API_KEY AGENTTEAMS_OPENAI_BASE_URL AGENTTEAMS_DEFAULT_MODEL; do\n local v; v=$(_env \"${k}\")\n [ -n \"${v}\" ] && env_args+=(-e \"${k}=${v}\")\n done\n\n # The controller writes the CLI SA token to /var/run/agentteams/cli-token.\n # The token is generated asynchronously during the first reconcile loop,\n # so it may not be available immediately after the container starts.\n # We poll for up to 30s to handle both fresh installs (token not yet minted)\n # and older images that don't support cli-token at all.\n # For backward compatibility, also try the legacy /var/run/hiclaw/cli-token\n # path used by older HiClaw images.\n local auth_token=\"${AGENTTEAMS_AUTH_TOKEN:-}\"\n if [ -z \"${auth_token}\" ]; then\n local _token_wait=0\n local _token_max_wait=30\n while [ ${_token_wait} -lt ${_token_max_wait} ]; do\n auth_token=$(${DOCKER_CMD} exec \"${CTRL_CONTAINER}\" sh -c 'cat /var/run/agentteams/cli-token 2>/dev/null || cat /var/run/hiclaw/cli-token 2>/dev/null' | tr -d '\\n' || true)\n if [ -n \"${auth_token}\" ]; then\n break\n fi\n sleep 2\n _token_wait=$((_token_wait + 2))\n done\n fi\n if [ -n \"${auth_token}\" ]; then\n env_args+=(-e AGENTTEAMS_AUTH_TOKEN=\"${auth_token}\")\n else\n log \"WARNING: could not read controller SA token (timed out after ${_token_max_wait:-30}s)\"\n log \" This can happen with older AgentTeams/HiClaw images or if the controller is still starting.\"\n log \" Dashboard will still work but some API calls may fail until you log in via Higress Console.\"\n log \" To fix: upgrade AgentTeams to v1.2.0-beta.1+ or set AGENTTEAMS_AUTH_TOKEN manually.\"\n fi\n\n local admin_user; admin_user=$(_env AGENTTEAMS_ADMIN_USER)\n local admin_pass; admin_pass=$(_env AGENTTEAMS_ADMIN_PASSWORD)\n [ -n \"${admin_user}\" ] && env_args+=(-e AGENTTEAMS_ADMIN_USER=\"${admin_user}\")\n [ -n \"${admin_pass}\" ] && env_args+=(-e AGENTTEAMS_ADMIN_PASSWORD=\"${admin_pass}\")\n\n # Higress Console URL: explicit config takes priority, auto-detect as fallback.\n local _resolved_gw_url=\"\"\n if [ -n \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\" ]; then\n # Normalize URL: add http:// prefix if missing\n _resolved_gw_url=\"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\"\n case \"${_resolved_gw_url}\" in\n http://*|https://*) ;;\n *) _resolved_gw_url=\"http://${_resolved_gw_url}\" ;;\n esac\n env_args+=(-e AGENTTEAMS_AI_GATEWAY_ADMIN_URL=\"${_resolved_gw_url}\")\n elif ${DOCKER_CMD} exec \"${CTRL_CONTAINER}\" wget -q -O- --timeout=2 http://127.0.0.1:8001/ >/dev/null 2>&1; then\n _resolved_gw_url=\"http://${CTRL_CONTAINER}:8001\"\n env_args+=(-e AGENTTEAMS_AI_GATEWAY_ADMIN_URL=\"${_resolved_gw_url}\")\n fi\n\n # Verify Higress Console URL reachability (best-effort warning)\n if [ -n \"${_resolved_gw_url}\" ]; then\n if ! ${DOCKER_CMD} exec \"${CTRL_CONTAINER}\" curl -sf --max-time 3 \"${_resolved_gw_url}/\" >/dev/null 2>&1; then\n log \"WARNING: Higress Console URL may not be reachable: ${_resolved_gw_url}\"\n log \" Dashboard will still work, but shared login via Higress Console may fail.\"\n log \" To fix: verify the URL is correct and the Higress Console service is running.\"\n fi\n fi\n else\n log \"WARNING: ${CTRL_CONTAINER} not running; launching Dashboard without Controller/MinIO/LLM env.\"\n if [ -n \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\" ]; then\n local _gw_norm=\"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\"\n case \"${_gw_norm}\" in\n http://*|https://*) ;;\n *) _gw_norm=\"http://${_gw_norm}\" ;;\n esac\n env_args+=(-e AGENTTEAMS_AI_GATEWAY_ADMIN_URL=\"${_gw_norm}\")\n fi\n fi\n\n # Create persistent data volume (for future use)\n ${DOCKER_CMD} volume create agentteams-dashboard-data >/dev/null 2>&1 || true\n\n ${DOCKER_CMD} run -d \\\n --name \"${DASHBOARD_CONTAINER}\" \\\n --restart unless-stopped \\\n --network \"${AGENTTEAMS_NETWORK:-agentteams-net}\" \\\n --network-alias dashboard.agentteams.io \\\n -p \"${BIND_ADDR}:${AGENTTEAMS_PORT_DASHBOARD}:3000\" \\\n \"${env_args[@]}\" \\\n -v agentteams-dashboard-data:/app/db \\\n \"${AGENTTEAMS_DASHBOARD_IMAGE}\"\n\n # Wait for dashboard to be ready\n local max_wait=60\n local waited=0\n while [ $waited -lt $max_wait ]; do\n if curl -sf \"http://127.0.0.1:${AGENTTEAMS_PORT_DASHBOARD}/\" >/dev/null 2>&1; then\n log \"Dashboard ready on port ${AGENTTEAMS_PORT_DASHBOARD}\"\n return 0\n fi\n sleep 2\n waited=$((waited + 2))\n done\n log \"WARNING: Dashboard startup timed out after ${max_wait}s\"\n ${DOCKER_CMD} logs \"${DASHBOARD_CONTAINER}\" 2>&1 | tail -20\n return 0\n}\n\n# ============================================================\n# Manager Installation (Interactive)\n# ============================================================\n\ninstall_manager() {\n log \"$(msg install.title)\"\n log \"$(msg install.registry \"${AGENTTEAMS_REGISTRY}\")\"\n log \"\"\n log \"$(msg install.dir \"$(pwd)\")\"\n log \"$(msg install.dir_hint)\"\n log \"$(msg install.dir_hint2)\"\n log \"\"\n\n # Non-interactive fallback: resolve version immediately so image tags are available\n # before the step state machine runs. Interactive mode lets step_version handle it.\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_VERSION=\"${AGENTTEAMS_VERSION:-${AGENTTEAMS_KNOWN_STABLE_VERSION}}\"\n resolve_image_tags\n fi\n\n # Migrate legacy env file location before checks\n local existing_env=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n if [ ! -f \"${existing_env}\" ] && [ -f \"./agentteams-manager.env\" ]; then\n log \"Migrating agentteams-manager.env from current directory to ${existing_env}...\"\n mv \"./agentteams-manager.env\" \"${existing_env}\"\n fi\n\n # Orphan volume detection (only when no env file — step_existing handles the env-file case)\n if [ ! -f \"${existing_env}\" ]; then\n local data_vol=\"${AGENTTEAMS_DATA_DIR:-agentteams-data}\"\n if ${DOCKER_CMD} volume ls -q | grep -q \"^${data_vol}$\"; then\n echo \"\"\n log \"$(msg install.orphan_volume.detected \"${data_vol}\")\"\n log \"$(msg install.orphan_volume.warn)\"\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n log \"$(msg install.orphan_volume.clean_noninteractive)\"\n ${DOCKER_CMD} stop agentteams-manager 2>/dev/null || true\n ${DOCKER_CMD} rm agentteams-manager 2>/dev/null || true\n for w in $(${DOCKER_CMD} ps -a --format '{{.Names}}' | grep \"^agentteams-worker-\" || true); do\n ${DOCKER_CMD} stop \"${w}\" 2>/dev/null || true\n ${DOCKER_CMD} rm \"${w}\" 2>/dev/null || true\n done\n log \"$(msg install.orphan_volume.cleaning)\"\n ${DOCKER_CMD} volume rm \"${data_vol}\" 2>/dev/null || true\n log \"$(msg install.orphan_volume.cleaned)\"\n else\n echo \"\"\n echo \"$(msg install.orphan_volume.choose)\"\n echo \"$(msg install.orphan_volume.clean)\"\n echo \"$(msg install.orphan_volume.keep)\"\n echo \"\"\n local ORPHAN_CHOICE\n read -e -p \"$(msg install.orphan_volume.prompt): \" ORPHAN_CHOICE\n ORPHAN_CHOICE=\"${ORPHAN_CHOICE:-1}\"\n case \"${ORPHAN_CHOICE}\" in\n 1|clean)\n ${DOCKER_CMD} stop agentteams-manager 2>/dev/null || true\n ${DOCKER_CMD} rm agentteams-manager 2>/dev/null || true\n for w in $(${DOCKER_CMD} ps -a --format '{{.Names}}' | grep \"^agentteams-worker-\" || true); do\n ${DOCKER_CMD} stop \"${w}\" 2>/dev/null || true\n ${DOCKER_CMD} rm \"${w}\" 2>/dev/null || true\n done\n log \"$(msg install.orphan_volume.cleaning)\"\n ${DOCKER_CMD} volume rm \"${data_vol}\" 2>/dev/null || true\n log \"$(msg install.orphan_volume.cleaned)\"\n ;;\n 2|keep)\n log \"$(msg install.orphan_volume.keeping)\"\n ;;\n esac\n fi\n fi\n fi\n\n # ── State machine ─────────────────────────────────────────────────────────\n local _STEPS=( step_lang step_mode step_version step_existing step_llm step_manager_runtime step_runtime step_admin step_network\n step_ports step_domains step_github step_skills step_volume\n step_workspace step_dashboard step_e2ee step_docker_proxy step_idle step_hostshare step_podman_autostart )\n local _STEP_HISTORY=()\n local _step_idx=0\n while [ \"${_step_idx}\" -lt \"${#_STEPS[@]}\" ]; do\n local _step_fn=\"${_STEPS[$_step_idx]}\"\n if should_skip_step \"${_step_fn}\"; then\n _step_idx=$((_step_idx + 1))\n continue\n fi\n if [ \"${#_STEP_HISTORY[@]}\" -gt 0 ]; then\n log \"$(msg nav.back_hint)\"\n fi\n STEP_RESULT=\"\"\n \"${_step_fn}\"\n if [ \"${STEP_RESULT}\" = \"back\" ]; then\n if [ \"${#_STEP_HISTORY[@]}\" -gt 0 ]; then\n local _last=$(( ${#_STEP_HISTORY[@]} - 1 ))\n _step_idx=\"${_STEP_HISTORY[$_last]}\"\n _STEP_HISTORY=(\"${_STEP_HISTORY[@]:0:${_last}}\")\n clear_step_vars \"${_STEPS[$_step_idx]}\"\n fi\n # else: first step, ignore 'b'\n else\n _STEP_HISTORY+=(\"${_step_idx}\")\n _step_idx=$((_step_idx + 1))\n fi\n done\n # ── End state machine ──────────────────────────────────────────────────────\n\n # Post-machine defaults for any steps that were skipped\n AGENTTEAMS_DATA_DIR=\"${AGENTTEAMS_DATA_DIR:-agentteams-data}\"\n if [ -z \"${AGENTTEAMS_WORKSPACE_DIR+x}\" ] || [ -z \"${AGENTTEAMS_WORKSPACE_DIR}\" ]; then\n AGENTTEAMS_WORKSPACE_DIR=\"${HOME}/agentteams-manager\"\n export AGENTTEAMS_WORKSPACE_DIR\n fi\n AGENTTEAMS_WORKSPACE_DIR=\"$(cd \"${AGENTTEAMS_WORKSPACE_DIR}\" 2>/dev/null && pwd || echo \"${AGENTTEAMS_WORKSPACE_DIR}\")\"\n mkdir -p \"${AGENTTEAMS_WORKSPACE_DIR}\"\n AGENTTEAMS_MANAGER_RUNTIME=\"${AGENTTEAMS_MANAGER_RUNTIME:-copaw}\"\n export AGENTTEAMS_MANAGER_RUNTIME\n AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"${AGENTTEAMS_DEFAULT_WORKER_RUNTIME:-copaw}\"\n AGENTTEAMS_MATRIX_E2EE=\"${AGENTTEAMS_MATRIX_E2EE:-0}\"\n export AGENTTEAMS_MATRIX_E2EE\n AGENTTEAMS_WORKER_IDLE_TIMEOUT=\"${AGENTTEAMS_WORKER_IDLE_TIMEOUT:-720}\"\n export AGENTTEAMS_WORKER_IDLE_TIMEOUT\n AGENTTEAMS_HOST_SHARE_DIR=\"${AGENTTEAMS_HOST_SHARE_DIR:-$HOME}\"\n export AGENTTEAMS_HOST_SHARE_DIR\n\n log \"\"\n\n # Generate secrets (only if not already set)\n log \"$(msg install.generating_secrets)\"\n AGENTTEAMS_MANAGER_PASSWORD=\"${AGENTTEAMS_MANAGER_PASSWORD:-$(generate_key)}\"\n AGENTTEAMS_REGISTRATION_TOKEN=\"${AGENTTEAMS_REGISTRATION_TOKEN:-$(generate_key)}\"\n AGENTTEAMS_MINIO_USER=\"${AGENTTEAMS_MINIO_USER:-${AGENTTEAMS_ADMIN_USER}}\"\n AGENTTEAMS_MINIO_PASSWORD=\"${AGENTTEAMS_MINIO_PASSWORD:-${AGENTTEAMS_ADMIN_PASSWORD}}\"\n AGENTTEAMS_MANAGER_GATEWAY_KEY=\"${AGENTTEAMS_MANAGER_GATEWAY_KEY:-$(generate_key)}\"\n\n # Matrix AppService tokens — generate once during install/upgrade if not provided.\n # Persisted to env file so they survive controller restarts.\n AGENTTEAMS_MATRIX_APPSERVICE_ENABLED=\"${AGENTTEAMS_MATRIX_APPSERVICE_ENABLED:-true}\"\n export AGENTTEAMS_MATRIX_APPSERVICE_ENABLED\n if [ \"${AGENTTEAMS_MATRIX_APPSERVICE_ENABLED}\" != \"false\" ] && [ \"${AGENTTEAMS_MATRIX_APPSERVICE_ENABLED}\" != \"0\" ]; then\n if [ -z \"${AGENTTEAMS_MATRIX_APPSERVICE_AS_TOKEN}\" ]; then\n AGENTTEAMS_MATRIX_APPSERVICE_AS_TOKEN=\"$(openssl rand -hex 32)\"\n log \" Auto-generated AppService as_token (saved to env file)\"\n fi\n if [ -z \"${AGENTTEAMS_MATRIX_APPSERVICE_HS_TOKEN}\" ]; then\n AGENTTEAMS_MATRIX_APPSERVICE_HS_TOKEN=\"$(openssl rand -hex 32)\"\n log \" Auto-generated AppService hs_token (saved to env file)\"\n fi\n export AGENTTEAMS_MATRIX_APPSERVICE_AS_TOKEN\n export AGENTTEAMS_MATRIX_APPSERVICE_HS_TOKEN\n fi\n\n # Detect Apple Silicon (M1/M2/M3/M4) - need JVM fix for Higress Console\n # See: https://github.com/agentscope-ai/AgentTeams/issues/249\n if [ -z \"${JVM_ARGS:-}\" ] && [ \"$(uname -m)\" = \"arm64\" ] && [ \"$(uname -s)\" = \"Darwin\" ]; then\n log \"Apple Silicon detected - setting JVM_ARGS to fix Higress Console SIGILL issue\"\n JVM_ARGS=\"-XX:+UnlockDiagnosticVMOptions -XX:-UseAESCTRIntrinsics -XX:UseSVE=0\"\n fi\n\n # Write .env file\n ENV_FILE=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n cat > \"${ENV_FILE}\" << EOF\n# AgentTeams Manager Configuration\n# Generated by agentteams-install.sh on $(date)\n\n# Language\nAGENTTEAMS_LANGUAGE=${AGENTTEAMS_LANGUAGE}\n\n# LLM\nAGENTTEAMS_LLM_PROVIDER=${AGENTTEAMS_LLM_PROVIDER}\nAGENTTEAMS_DEFAULT_MODEL=${AGENTTEAMS_DEFAULT_MODEL}\nAGENTTEAMS_LLM_API_KEY=${AGENTTEAMS_LLM_API_KEY}\nAGENTTEAMS_OPENAI_BASE_URL=${AGENTTEAMS_OPENAI_BASE_URL:-}\nAGENTTEAMS_MODEL_CONTEXT_WINDOW=${AGENTTEAMS_MODEL_CONTEXT_WINDOW:-}\nAGENTTEAMS_MODEL_MAX_TOKENS=${AGENTTEAMS_MODEL_MAX_TOKENS:-}\nAGENTTEAMS_MODEL_REASONING=${AGENTTEAMS_MODEL_REASONING:-}\nAGENTTEAMS_MODEL_VISION=${AGENTTEAMS_MODEL_VISION:-}\n\n# Embedding model (empty = disabled, default: text-embedding-v4)\nAGENTTEAMS_EMBEDDING_MODEL=${AGENTTEAMS_EMBEDDING_MODEL}\n\n# Admin\nAGENTTEAMS_ADMIN_USER=${AGENTTEAMS_ADMIN_USER}\nAGENTTEAMS_ADMIN_PASSWORD=${AGENTTEAMS_ADMIN_PASSWORD}\n\n# Ports\nAGENTTEAMS_LOCAL_ONLY=${AGENTTEAMS_LOCAL_ONLY}\nAGENTTEAMS_PORT_GATEWAY=${AGENTTEAMS_PORT_GATEWAY}\nAGENTTEAMS_PORT_CONSOLE=${AGENTTEAMS_PORT_CONSOLE}\nAGENTTEAMS_PORT_ELEMENT_WEB=${AGENTTEAMS_PORT_ELEMENT_WEB}\nAGENTTEAMS_PORT_MANAGER_CONSOLE=${AGENTTEAMS_PORT_MANAGER_CONSOLE:-18888}\n\n# Manager runtime (openclaw | copaw)\nAGENTTEAMS_MANAGER_RUNTIME=${AGENTTEAMS_MANAGER_RUNTIME:-copaw}\n\n# Matrix\nAGENTTEAMS_MATRIX_DOMAIN=${AGENTTEAMS_MATRIX_DOMAIN}\nAGENTTEAMS_MATRIX_CLIENT_DOMAIN=${AGENTTEAMS_MATRIX_CLIENT_DOMAIN}\n\n# Gateway\nAGENTTEAMS_AI_GATEWAY_DOMAIN=${AGENTTEAMS_AI_GATEWAY_DOMAIN}\nAGENTTEAMS_MANAGER_GATEWAY_KEY=${AGENTTEAMS_MANAGER_GATEWAY_KEY}\n\n# File System\nAGENTTEAMS_FS_DOMAIN=${AGENTTEAMS_FS_DOMAIN}\nAGENTTEAMS_CONSOLE_DOMAIN=${AGENTTEAMS_CONSOLE_DOMAIN}\nAGENTTEAMS_MINIO_USER=${AGENTTEAMS_MINIO_USER}\nAGENTTEAMS_MINIO_PASSWORD=${AGENTTEAMS_MINIO_PASSWORD}\n\n# Internal\nAGENTTEAMS_MANAGER_PASSWORD=${AGENTTEAMS_MANAGER_PASSWORD}\nAGENTTEAMS_REGISTRATION_TOKEN=${AGENTTEAMS_REGISTRATION_TOKEN}\n\n# GitHub (optional)\nAGENTTEAMS_GITHUB_TOKEN=${AGENTTEAMS_GITHUB_TOKEN:-}\n\n# Nacos package import defaults\nAGENTTEAMS_NACOS_REGISTRY_URI=${AGENTTEAMS_NACOS_REGISTRY_URI:-nacos://market.agentteams.io:80/public}\nAGENTTEAMS_NACOS_USERNAME=${AGENTTEAMS_NACOS_USERNAME:-}\nAGENTTEAMS_NACOS_PASSWORD=${AGENTTEAMS_NACOS_PASSWORD:-}\nAGENTTEAMS_NACOS_TOKEN=${AGENTTEAMS_NACOS_TOKEN:-}\n\n# Skills Registry (optional, default: nacos://market.agentteams.io:80/public)\nAGENTTEAMS_SKILLS_API_URL=${AGENTTEAMS_SKILLS_API_URL:-nacos://market.agentteams.io:80/public}\n\n# OpenClaw CMS plugin (optional)\nAGENTTEAMS_CMS_TRACES_ENABLED=${AGENTTEAMS_CMS_TRACES_ENABLED:-false}\nAGENTTEAMS_CMS_ENDPOINT=${AGENTTEAMS_CMS_ENDPOINT:-}\nAGENTTEAMS_CMS_LICENSE_KEY=${AGENTTEAMS_CMS_LICENSE_KEY:-}\nAGENTTEAMS_CMS_PROJECT=${AGENTTEAMS_CMS_PROJECT:-}\nAGENTTEAMS_CMS_WORKSPACE=${AGENTTEAMS_CMS_WORKSPACE:-}\nAGENTTEAMS_CMS_SERVICE_NAME=${AGENTTEAMS_CMS_SERVICE_NAME:-agentteams-manager}\nAGENTTEAMS_CMS_METRICS_ENABLED=${AGENTTEAMS_CMS_METRICS_ENABLED:-false}\n\n# Worker images (for direct container creation)\nAGENTTEAMS_WORKER_IMAGE=${WORKER_IMAGE}\nAGENTTEAMS_COPAW_WORKER_IMAGE=${COPAW_WORKER_IMAGE}\nAGENTTEAMS_QWENPAW_WORKER_IMAGE=${QWENPAW_WORKER_IMAGE}\nAGENTTEAMS_HERMES_WORKER_IMAGE=${HERMES_WORKER_IMAGE}\n\n# Default Worker runtime (openclaw | copaw | qwenpaw | hermes)\nAGENTTEAMS_DEFAULT_WORKER_RUNTIME=${AGENTTEAMS_DEFAULT_WORKER_RUNTIME:-copaw}\n\n# Matrix E2EE (0=disabled, 1=enabled; default: 0)\nAGENTTEAMS_MATRIX_E2EE=${AGENTTEAMS_MATRIX_E2EE:-0}\n\n# Matrix AppService\nAGENTTEAMS_MATRIX_APPSERVICE_ENABLED=${AGENTTEAMS_MATRIX_APPSERVICE_ENABLED:-true}\nAGENTTEAMS_MATRIX_APPSERVICE_AS_TOKEN=${AGENTTEAMS_MATRIX_APPSERVICE_AS_TOKEN:-}\nAGENTTEAMS_MATRIX_APPSERVICE_HS_TOKEN=${AGENTTEAMS_MATRIX_APPSERVICE_HS_TOKEN:-}\n\n# Docker API proxy (0=disabled, 1=enabled; default: 1)\nAGENTTEAMS_DOCKER_PROXY=${AGENTTEAMS_DOCKER_PROXY:-1}\n\n# Docker API proxy: additional allowed image sources (comma-separated)\nAGENTTEAMS_PROXY_ALLOWED_REGISTRIES=${AGENTTEAMS_PROXY_ALLOWED_REGISTRIES:-}\n\n# Worker idle timeout in minutes (default: 720 = 12 hours)\nAGENTTEAMS_WORKER_IDLE_TIMEOUT=${AGENTTEAMS_WORKER_IDLE_TIMEOUT:-720}\n\n# Podman autostart via systemd (0=disabled, 1=enabled)\nAGENTTEAMS_PODMAN_AUTOSTART=${AGENTTEAMS_PODMAN_AUTOSTART:-0}\n\n# JVM Args for Higress Console (fixes SIGILL on Apple Silicon)\nJVM_ARGS=${JVM_ARGS:-}\n\n# Higress WASM plugin image registry (auto-selected by timezone)\nHIGRESS_ADMIN_WASM_PLUGIN_IMAGE_REGISTRY=${AGENTTEAMS_REGISTRY}\n\n# Data persistence\nAGENTTEAMS_DATA_DIR=${AGENTTEAMS_DATA_DIR:-agentteams-data}\n# Manager workspace (skills, memory, state — host-editable)\nAGENTTEAMS_WORKSPACE_DIR=${AGENTTEAMS_WORKSPACE_DIR:-}\n# Host directory sharing\nAGENTTEAMS_HOST_SHARE_DIR=${AGENTTEAMS_HOST_SHARE_DIR:-}\n\n# agentteams-dashboard (management UI)\nAGENTTEAMS_DASHBOARD=${AGENTTEAMS_DASHBOARD:-1}\nAGENTTEAMS_DASHBOARD_VERSION=${AGENTTEAMS_DASHBOARD_VERSION:-v1.2.0}\nAGENTTEAMS_PORT_DASHBOARD=${AGENTTEAMS_PORT_DASHBOARD:-13000}\nAGENTTEAMS_DASHBOARD_IMAGE=${AGENTTEAMS_DASHBOARD_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-dashboard:${AGENTTEAMS_DASHBOARD_VERSION}}\nAGENTTEAMS_AI_GATEWAY_ADMIN_URL=${AGENTTEAMS_AI_GATEWAY_ADMIN_URL:-}\nEOF\n\n chmod 600 \"${ENV_FILE}\"\n log \"$(msg install.config_saved \"${ENV_FILE}\")\"\n\n # Detect container runtime socket\n SOCKET_MOUNT_ARGS=\"\"\n if [ \"${AGENTTEAMS_MOUNT_SOCKET}\" = \"1\" ]; then\n # Actively ensure the API socket is enabled and active before detection.\n if [ \"${DOCKER_CMD:-}\" = \"podman\" ]; then\n ensure_podman_socket\n fi\n CONTAINER_SOCK=$(detect_socket)\n if [ -n \"${CONTAINER_SOCK}\" ]; then\n log \"$(msg install.socket_detected \"${CONTAINER_SOCK}\")\"\n SOCKET_MOUNT_ARGS=\"-v ${CONTAINER_SOCK}:/var/run/docker.sock --security-opt label=disable\"\n else\n log \"$(msg install.socket_not_found)\"\n # Interactive confirmation when socket not found\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" != \"1\" ]; then\n echo \"\"\n echo -e \"\\033[33m$(msg install.socket_confirm.title)\\033[0m\"\n echo \"\"\n echo -e \"$(msg install.socket_confirm.message)\"\n echo \"\"\n read -p \"$(msg install.socket_confirm.prompt)\" SOCKET_CONFIRM\n if [ \"${SOCKET_CONFIRM}\" != \"y\" ] && [ \"${SOCKET_CONFIRM}\" != \"Y\" ]; then\n log \"$(msg install.socket_confirm.cancelled)\"\n exit 0\n fi\n fi\n fi\n fi\n\n # Create the data volume if it doesn't already exist (reuse on reinstall)\n if ! ${DOCKER_CMD} volume ls -q | grep -q \"^${AGENTTEAMS_DATA_DIR}$\"; then\n ${DOCKER_CMD} volume create \"${AGENTTEAMS_DATA_DIR}\" > /dev/null\n fi\n\n # Data mount: Docker volume\n DATA_MOUNT_ARGS=\"-v ${AGENTTEAMS_DATA_DIR}:/data\"\n\n # Manager workspace mount (always a host directory, defaulting to ~/agentteams-manager)\n WORKSPACE_MOUNT_ARGS=\"-v ${AGENTTEAMS_WORKSPACE_DIR}:/root/manager-workspace\"\n\n # Pass host timezone to container so date/time commands reflect local time\n TZ_ARGS=\"-e TZ=${AGENTTEAMS_TIMEZONE}\"\n\n # Host directory mount\n if [ -d \"${AGENTTEAMS_HOST_SHARE_DIR}\" ]; then\n HOST_SHARE_MOUNT_ARGS=\"-v ${AGENTTEAMS_HOST_SHARE_DIR}:/host-share\"\n log \"$(msg host_share.sharing \"${AGENTTEAMS_HOST_SHARE_DIR}\")\"\n else\n log \"$(msg host_share.not_exist \"${AGENTTEAMS_HOST_SHARE_DIR}\")\"\n HOST_SHARE_MOUNT_ARGS=\"-v ${AGENTTEAMS_HOST_SHARE_DIR}:/host-share\"\n fi\n\n # YOLO mode: pass through if set in environment (enables autonomous decisions)\n YOLO_ARGS=\"\"\n if [ \"${AGENTTEAMS_YOLO:-}\" = \"1\" ]; then\n YOLO_ARGS=\"-e AGENTTEAMS_YOLO=1\"\n log \"$(msg install.yolo)\"\n fi\n\n # Matrix-plugin debug tracing: pass through if AGENTTEAMS_MATRIX_DEBUG=1.\n # The container entrypoints translate this to OPENCLAW_MATRIX_DEBUG=1\n # so the openclaw matrix plugin emits structured INFO-level lifecycle\n # traces (sync state transitions, room.invite/join, message handler\n # arrival + filter outcomes). Used to diagnose worker/manager hangs.\n MATRIX_DEBUG_ARGS=\"\"\n if [ \"${AGENTTEAMS_MATRIX_DEBUG:-}\" = \"1\" ]; then\n MATRIX_DEBUG_ARGS=\"-e AGENTTEAMS_MATRIX_DEBUG=1\"\n fi\n\n # E2EE is already in the env file; but also pass explicitly in case env file is not the source\n # (AGENTTEAMS_MATRIX_E2EE is already written to ENV_FILE above via --env-file)\n\n # Pull images (manager based on runtime config; all worker runtimes always pulled)\n _is_local_image() {\n case \"$1\" in\n agentteams/*) return 0 ;;\n *) return 1 ;;\n esac\n }\n\n _local_image_exists() {\n local _img=\"$1\"\n [ -z \"${_img}\" ] && return 1\n _is_local_image \"${_img}\" || return 1\n ${DOCKER_CMD} image inspect \"${_img}\" >/dev/null 2>&1\n }\n\n # Helper: pull or skip a single image\n # Args: $1=image $2=exists_msg_key $3=pulling_msg_key\n _pull_image() {\n local _img=\"$1\" _exists_key=\"$2\" _pull_key=\"$3\"\n [ -z \"${_img}\" ] && return 0\n if _local_image_exists \"${_img}\"; then\n log \"$(msg \"${_exists_key}\" \"${_img}\")\"\n return 0\n fi\n log \"$(msg \"${_pull_key}\" \"${_img}\")\"\n local _attempt=1\n while [ $_attempt -le 3 ]; do\n if ${DOCKER_CMD} pull \"${_img}\"; then\n return 0\n fi\n if [ $_attempt -lt 3 ]; then\n log \"Pull failed (attempt ${_attempt}/3), retrying in 5s...\"\n sleep 5\n fi\n _attempt=$((_attempt + 1))\n done\n die \"Failed to pull ${_img} after 3 attempts\"\n return 1\n }\n\n # Embedded controller image (resolve versioned tag, fallback to latest)\n resolve_embedded_image\n if [ \"${AGENTTEAMS_USE_EMBEDDED}\" = \"1\" ]; then\n _local_image_exists \"${EMBEDDED_IMAGE}\" || true\n fi\n\n # Manager image is always required (select based on runtime)\n if [ \"${AGENTTEAMS_MANAGER_RUNTIME}\" = \"copaw\" ]; then\n _pull_image \"${MANAGER_COPAW_IMAGE}\" \"install.image.exists\" \"install.image.pulling_manager\"\n else\n _pull_image \"${MANAGER_IMAGE}\" \"install.image.exists\" \"install.image.pulling_manager\"\n fi\n\n # Pull all worker runtime images (workers may use any runtime regardless of the default)\n _pull_image \"${WORKER_IMAGE}\" \"install.image.worker_exists\" \"install.image.pulling_worker\"\n _pull_image \"${COPAW_WORKER_IMAGE}\" \"install.image.worker_exists\" \"install.image.pulling_worker\"\n # Temporarily disabled until the QwenPaw worker image is published.\n # _pull_image \"${QWENPAW_WORKER_IMAGE}\" \"install.image.worker_exists\" \"install.image.pulling_worker\"\n _pull_image \"${HERMES_WORKER_IMAGE}\" \"install.image.worker_exists\" \"install.image.pulling_worker\"\n\n # --- Pre-upgrade: extract Matrix passwords from running old containers ---\n # Only needed when upgrading FROM old architecture (v1.0.9) TO embedded.\n # For new-arch-to-new-arch upgrades, credential files already exist with\n # correct room IDs — we must NOT overwrite them.\n _creds_tmp=\"\"\n if [ \"${AGENTTEAMS_UPGRADE:-0}\" = \"1\" ] && [ \"${AGENTTEAMS_USE_EMBEDDED}\" = \"1\" ]; then\n # Detect if upgrading from old arch: old arch has no agentteams-controller container\n # (or has it as a docker-proxy only, not embedded). Check if the existing\n # agentteams-controller is an embedded image (has supervisord) or just a proxy.\n _is_old_arch=0\n if ! ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -q \"^agentteams-controller$\"; then\n _is_old_arch=1\n elif ${DOCKER_CMD} ps -a --format '{{.Names}} {{.Image}}' 2>/dev/null | grep \"^agentteams-controller \" | grep -qv \"embedded\"; then\n _is_old_arch=1\n fi\n\n if [ \"${_is_old_arch}\" = \"1\" ]; then\n _creds_tmp=$(mktemp -d)\n\n # docker exec for Matrix/minio paths only works while agentteams-manager is running.\n _mgr_creds_tempstart=0\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -q '^agentteams-manager$'; then\n if ! ${DOCKER_CMD} ps --format '{{.Names}}' 2>/dev/null | grep -q '^agentteams-manager$'; then\n log \"agentteams-manager is stopped; starting it temporarily to extract Matrix credentials for upgrade...\"\n ${DOCKER_CMD} start agentteams-manager 2>/dev/null || true\n wait_matrix_ready \"agentteams-manager\"\n _mgr_creds_tempstart=1\n fi\n fi\n\n # Manager password (container Config.Env, then secrets file inside running manager, then data volume)\n _mgr_pw=$(${DOCKER_CMD} inspect agentteams-manager --format '{{range .Config.Env}}{{println .}}{{end}}' 2>/dev/null | grep '^AGENTTEAMS_MANAGER_PASSWORD=' | cut -d= -f2-)\n if [ -z \"${_mgr_pw}\" ] && ${DOCKER_CMD} ps --format '{{.Names}}' 2>/dev/null | grep -q '^agentteams-manager$'; then\n _mgr_pw=$(${DOCKER_CMD} exec agentteams-manager bash -c 'source /data/agentteams-secrets.env 2>/dev/null && echo \"${AGENTTEAMS_MANAGER_PASSWORD}\"' 2>/dev/null)\n fi\n if [ -z \"${_mgr_pw}\" ] && ${DOCKER_CMD} volume ls -q 2>/dev/null | grep -q \"^${AGENTTEAMS_DATA_DIR}$\"; then\n _mgr_pw=$(agentteams_read_secret_from_data_volume \"${AGENTTEAMS_DATA_DIR}\" AGENTTEAMS_MANAGER_PASSWORD)\n fi\n\n # Manager admin DM room ID: login as admin, find DM room with @manager\n _mgr_room=\"\"\n if [ -n \"${_mgr_pw}\" ]; then\n _admin_pw=$(grep AGENTTEAMS_ADMIN_PASSWORD \"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\" 2>/dev/null | cut -d= -f2-)\n _admin_user=$(grep AGENTTEAMS_ADMIN_USER \"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\" 2>/dev/null | cut -d= -f2-)\n _admin_user=\"${_admin_user:-admin}\"\n _matrix_domain=$(grep AGENTTEAMS_MATRIX_DOMAIN \"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\" 2>/dev/null | cut -d= -f2-)\n if [ -n \"${_admin_pw}\" ] && ${DOCKER_CMD} ps --format '{{.Names}}' 2>/dev/null | grep -q '^agentteams-manager$'; then\n _admin_token=$(${DOCKER_CMD} exec agentteams-manager curl -sf -X POST http://127.0.0.1:6167/_matrix/client/v3/login \\\n -H \"Content-Type: application/json\" \\\n -d '{\"type\":\"m.login.password\",\"identifier\":{\"type\":\"m.id.user\",\"user\":\"'\"${_admin_user}\"'\"},\"password\":\"'\"${_admin_pw}\"'\"}' 2>/dev/null | python3 -c \"import sys,json; print(json.load(sys.stdin).get('access_token',''))\" 2>/dev/null || true)\n if [ -n \"${_admin_token}\" ]; then\n _mgr_room=$(${DOCKER_CMD} exec agentteams-manager curl -sf -X GET \\\n -H \"Authorization: Bearer ${_admin_token}\" \\\n \"http://127.0.0.1:6167/_matrix/client/v3/joined_rooms\" 2>/dev/null | python3 -c \"\nimport sys,json,subprocess\nrooms = json.load(sys.stdin).get('joined_rooms',[])\nfor room_id in rooms:\n enc = room_id.replace('!','%21')\n members = json.loads(subprocess.check_output([\n 'docker','exec','agentteams-manager','curl','-sf','-X','GET',\n '-H','Authorization: Bearer ${_admin_token}',\n 'http://127.0.0.1:6167/_matrix/client/v3/rooms/'+enc+'/members'\n ]).decode()).get('chunk',[])\n member_ids = [m['state_key'] for m in members]\n if any('manager' in m and 'admin' not in m.split(':')[0] for m in member_ids):\n if len(member_ids) <= 3:\n print(room_id)\n break\n\" 2>/dev/null || true)\n fi\n fi\n if [ -z \"${_mgr_room}\" ]; then\n _mgr_room=$(agentteams_read_admin_dm_room_from_workspace \"${AGENTTEAMS_WORKSPACE_DIR}\")\n fi\n cat > \"${_creds_tmp}/default.env\" <<CREDEOF\nWORKER_PASSWORD=\"${_mgr_pw}\"\nWORKER_MINIO_PASSWORD=\"$(openssl rand -hex 24)\"\nWORKER_GATEWAY_KEY=\"${AGENTTEAMS_MANAGER_GATEWAY_KEY}\"\nWORKER_ROOM_ID=\"${_mgr_room}\"\nCREDEOF\n log \"Extracted Manager Matrix password${_mgr_room:+ and room ID}\"\n fi\n\n # Worker passwords and room IDs from workers-registry.json\n if [ -f \"${AGENTTEAMS_WORKSPACE_DIR}/workers-registry.json\" ]; then\n _worker_names=$(python3 -c \"import json; d=json.load(open('${AGENTTEAMS_WORKSPACE_DIR}/workers-registry.json')); print(' '.join(d.get('workers',{}).keys()))\" 2>/dev/null || true)\n for _wname in ${_worker_names}; do\n _wpw=\"\"\n if ${DOCKER_CMD} ps --format '{{.Names}}' 2>/dev/null | grep -q '^agentteams-manager$'; then\n _wpw=$(${DOCKER_CMD} exec agentteams-manager cat \"/root/agentteams-fs/agents/${_wname}/credentials/matrix/password\" 2>/dev/null || true)\n fi\n if [ -z \"${_wpw}\" ] && ${DOCKER_CMD} volume ls -q 2>/dev/null | grep -q \"^${AGENTTEAMS_DATA_DIR}$\"; then\n _wpw=$(agentteams_read_worker_creds_value_from_volume \"${AGENTTEAMS_DATA_DIR}\" \"${_wname}\" WORKER_PASSWORD)\n fi\n _wroom=$(python3 -c \"import json; d=json.load(open('${AGENTTEAMS_WORKSPACE_DIR}/workers-registry.json')); print(d.get('workers',{}).get('${_wname}',{}).get('room_id',''))\" 2>/dev/null || true)\n if [ -z \"${_wroom}\" ] && ${DOCKER_CMD} volume ls -q 2>/dev/null | grep -q \"^${AGENTTEAMS_DATA_DIR}$\"; then\n _wroom=$(agentteams_read_worker_creds_value_from_volume \"${AGENTTEAMS_DATA_DIR}\" \"${_wname}\" WORKER_ROOM_ID)\n fi\n if [ -n \"${_wpw}\" ]; then\n cat > \"${_creds_tmp}/${_wname}.env\" <<CREDEOF\nWORKER_PASSWORD=\"${_wpw}\"\nWORKER_MINIO_PASSWORD=\"$(openssl rand -hex 24)\"\nWORKER_GATEWAY_KEY=\"$(openssl rand -hex 32)\"\nWORKER_ROOM_ID=\"${_wroom}\"\nCREDEOF\n log \"Extracted ${_wname} Matrix password${_wroom:+ and room ID}\"\n fi\n done\n fi\n\n if [ \"${_mgr_creds_tempstart}\" = \"1\" ]; then\n log \"Stopping agentteams-manager after credential extraction (upgrade will recreate containers)...\"\n ${DOCKER_CMD} stop agentteams-manager 2>/dev/null || true\n fi\n fi # _is_old_arch\n fi\n\n # --- Stop and remove existing containers ---\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' | grep -q \"^agentteams-controller$\"; then\n ${DOCKER_CMD} stop agentteams-controller 2>/dev/null || true\n ${DOCKER_CMD} rm agentteams-controller 2>/dev/null || true\n fi\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' | grep -q \"^agentteams-manager$\"; then\n log \"$(msg install.removing_existing)\"\n ${DOCKER_CMD} stop agentteams-manager 2>/dev/null || true\n ${DOCKER_CMD} rm agentteams-manager 2>/dev/null || true\n fi\n\n # Stop and remove worker containers (controller will recreate via CR reconciliation)\n if [ -n \"${UPGRADE_EXISTING_WORKERS:-}\" ]; then\n log \"$(msg install.existing.stopping_workers)\"\n for w in ${UPGRADE_EXISTING_WORKERS}; do\n ${DOCKER_CMD} stop \"${w}\" 2>/dev/null || true\n ${DOCKER_CMD} rm \"${w}\" 2>/dev/null || true\n log \"$(msg install.existing.removed \"${w}\")\"\n done\n fi\n\n # Clean up legacy containers from v1.0.x and earlier\n for _legacy_name in agentteams-docker-proxy; do\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -qx \"${_legacy_name}\"; then\n log \"Removing legacy container: ${_legacy_name}\"\n ${DOCKER_CMD} stop \"${_legacy_name}\" 2>/dev/null || true\n ${DOCKER_CMD} rm -f \"${_legacy_name}\" 2>/dev/null || true\n fi\n done\n\n # --- Upgrade: inject extracted credentials into data volume ---\n # Only needed for old-arch upgrades (credential files were extracted above).\n if [ -n \"${_creds_tmp}\" ] && [ -d \"${_creds_tmp}\" ] && [ -n \"$(ls -A \"${_creds_tmp}\" 2>/dev/null)\" ]; then\n local _cleanup_ctr=\"agentteams-upgrade-cleanup\"\n ${DOCKER_CMD} rm -f \"${_cleanup_ctr}\" 2>/dev/null || true\n ${DOCKER_CMD} run --rm --name \"${_cleanup_ctr}\" \\\n --entrypoint sh \\\n -v \"${AGENTTEAMS_DATA_DIR}:/data\" \\\n -v \"${_creds_tmp}:/creds:ro\" \\\n \"${EMBEDDED_IMAGE}\" -c '\n rm -rf /data/worker-creds\n mkdir -p /data/worker-creds\n cp /creds/*.env /data/worker-creds/ 2>/dev/null || true\n chmod 600 /data/worker-creds/*.env 2>/dev/null || true\n ' 2>/dev/null && log \"Injected credentials for upgrade\" || log \"Warning: credential injection failed, continuing\"\n rm -rf \"${_creds_tmp}\"\n fi\n\n # --- Start containers ---\n log \"$(msg install.starting_manager)\"\n\n # Build port binding args\n if [ \"${AGENTTEAMS_LOCAL_ONLY:-1}\" = \"1\" ]; then\n _port_prefix=\"127.0.0.1:\"\n else\n _port_prefix=\"\"\n fi\n\n # Ensure agentteams-net Docker network exists\n ${DOCKER_CMD} network inspect agentteams-net >/dev/null 2>&1 || ${DOCKER_CMD} network create agentteams-net\n\n if [ \"${AGENTTEAMS_USE_EMBEDDED}\" != \"1\" ] && [ \"${AGENTTEAMS_UPGRADE:-0}\" = \"1\" ]; then\n # Check if current installation is embedded — downgrade to legacy is not supported\n if ${DOCKER_CMD} ps -a --format '{{.Names}} {{.Image}}' 2>/dev/null | grep \"^agentteams-controller \" | grep -q \"embedded\"; then\n error \"Downgrade from embedded architecture to legacy version (${AGENTTEAMS_VERSION}) is not supported.\"\n error \"Please use 'make uninstall-embedded' first, then do a clean install of the target version.\"\n exit 1\n fi\n fi\n\n if [ \"${AGENTTEAMS_USE_EMBEDDED}\" = \"1\" ]; then\n # ============================================================\n # New architecture: embedded controller + auto-created manager\n # ============================================================\n\n # Internal port: 8080 (Higress gateway inside the container).\n local _internal_gw_port=8080\n local _matrix_domain=\"${AGENTTEAMS_MATRIX_DOMAIN:-matrix-local.agentteams.io:${AGENTTEAMS_PORT_GATEWAY}}\"\n local _aigw_domain=\"${AGENTTEAMS_AI_GATEWAY_DOMAIN:-aigw-local.agentteams.io}\"\n # Ensure internal gateway port is present (container-internal traffic uses 8080)\n case \"${_aigw_domain}\" in *:*) ;; *) _aigw_domain=\"${_aigw_domain}:${_internal_gw_port}\" ;; esac\n local _fs_domain=\"${AGENTTEAMS_FS_DOMAIN:-fs-local.agentteams.io}\"\n case \"${_fs_domain}\" in *:*) ;; *) _fs_domain=\"${_fs_domain}:${_internal_gw_port}\" ;; esac\n\n # Controller env args\n local _ctrl_env_prefix\n _ctrl_env_prefix=\"$(_controller_env_prefix \"${AGENTTEAMS_VERSION}\")\"\n local _storage_prefix\n _storage_prefix=\"$(_controller_storage_prefix \"${AGENTTEAMS_VERSION}\")\"\n local _ctrl_env_args=(\n -e \"${_ctrl_env_prefix}ADMIN_USER=${AGENTTEAMS_ADMIN_USER}\"\n -e \"${_ctrl_env_prefix}ADMIN_PASSWORD=${AGENTTEAMS_ADMIN_PASSWORD}\"\n -e \"${_ctrl_env_prefix}MANAGER_PASSWORD=${AGENTTEAMS_MANAGER_PASSWORD}\"\n -e \"${_ctrl_env_prefix}REGISTRATION_TOKEN=${AGENTTEAMS_REGISTRATION_TOKEN}\"\n -e \"${_ctrl_env_prefix}MINIO_USER=${AGENTTEAMS_MINIO_USER}\"\n -e \"${_ctrl_env_prefix}MINIO_PASSWORD=${AGENTTEAMS_MINIO_PASSWORD}\"\n -e \"${_ctrl_env_prefix}LLM_PROVIDER=${AGENTTEAMS_LLM_PROVIDER}\"\n -e \"${_ctrl_env_prefix}LLM_API_KEY=${AGENTTEAMS_LLM_API_KEY}\"\n -e \"${_ctrl_env_prefix}DEFAULT_MODEL=${AGENTTEAMS_DEFAULT_MODEL}\"\n -e \"${_ctrl_env_prefix}MANAGER_GATEWAY_KEY=${AGENTTEAMS_MANAGER_GATEWAY_KEY}\"\n -e \"${_ctrl_env_prefix}MANAGER_RUNTIME=${AGENTTEAMS_MANAGER_RUNTIME:-copaw}\"\n -e \"${_ctrl_env_prefix}MANAGER_IMAGE=$([ \"${AGENTTEAMS_MANAGER_RUNTIME}\" = \"copaw\" ] && echo \"${MANAGER_COPAW_IMAGE}\" || echo \"${MANAGER_IMAGE}\")\"\n -e \"${_ctrl_env_prefix}DEFAULT_WORKER_RUNTIME=${AGENTTEAMS_DEFAULT_WORKER_RUNTIME:-copaw}\"\n -e \"${_ctrl_env_prefix}WORKER_IMAGE=${WORKER_IMAGE}\"\n -e \"${_ctrl_env_prefix}COPAW_WORKER_IMAGE=${COPAW_WORKER_IMAGE}\"\n -e \"${_ctrl_env_prefix}QWENPAW_WORKER_IMAGE=${QWENPAW_WORKER_IMAGE}\"\n -e \"${_ctrl_env_prefix}HERMES_WORKER_IMAGE=${HERMES_WORKER_IMAGE}\"\n -e \"${_ctrl_env_prefix}MATRIX_DOMAIN=${_matrix_domain}\"\n -e \"${_ctrl_env_prefix}ELEMENT_HOMESERVER_URL=http://127.0.0.1:${AGENTTEAMS_PORT_GATEWAY}\"\n -e \"${_ctrl_env_prefix}MATRIX_URL=http://127.0.0.1:6167\"\n -e \"${_ctrl_env_prefix}MATRIX_E2EE=${AGENTTEAMS_MATRIX_E2EE:-0}\"\n -e \"${_ctrl_env_prefix}MINIO_ENDPOINT=http://127.0.0.1:9000\"\n -e \"${_ctrl_env_prefix}MINIO_BUCKET=agentteams-storage\"\n -e \"${_ctrl_env_prefix}STORAGE_PREFIX=${_storage_prefix}\"\n -e \"${_ctrl_env_prefix}FS_ENDPOINT=http://127.0.0.1:9000\"\n -e \"${_ctrl_env_prefix}AI_GATEWAY_URL=http://${_aigw_domain}\"\n -e \"${_ctrl_env_prefix}CONTROLLER_URL=http://agentteams-controller:8090\"\n -e \"${_ctrl_env_prefix}DOCKER_NETWORK=agentteams-net\"\n -e \"${_ctrl_env_prefix}WORKSPACE_DIR=${AGENTTEAMS_WORKSPACE_DIR}\"\n -e \"${_ctrl_env_prefix}HOST_SHARE_DIR=${AGENTTEAMS_HOST_SHARE_DIR}\"\n -e \"${_ctrl_env_prefix}MANAGER_ENABLED=true\"\n -e \"${_ctrl_env_prefix}PORT_MANAGER_CONSOLE=${AGENTTEAMS_PORT_MANAGER_CONSOLE:-18888}\"\n )\n if _use_legacy_image_env \"${AGENTTEAMS_VERSION}\"; then\n _ctrl_env_args+=(\n -e \"${_ctrl_env_prefix}FS_BUCKET=agentteams-storage\"\n -e \"${_ctrl_env_prefix}RESOURCE_PREFIX=agentteams-\"\n )\n else\n _ctrl_env_args+=(\n -e \"${_ctrl_env_prefix}MATRIX_APPSERVICE_ENABLED=${AGENTTEAMS_MATRIX_APPSERVICE_ENABLED:-true}\"\n -e \"${_ctrl_env_prefix}MATRIX_APPSERVICE_AS_TOKEN=${AGENTTEAMS_MATRIX_APPSERVICE_AS_TOKEN:-}\"\n -e \"${_ctrl_env_prefix}MATRIX_APPSERVICE_HS_TOKEN=${AGENTTEAMS_MATRIX_APPSERVICE_HS_TOKEN:-}\"\n )\n fi\n\n # Timezone\n if [ -n \"${AGENTTEAMS_TIMEZONE:-}\" ]; then\n _ctrl_env_args+=(-e \"TZ=${AGENTTEAMS_TIMEZONE}\")\n fi\n\n # Yolo mode\n if [ \"${AGENTTEAMS_YOLO:-}\" = \"1\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}YOLO=1\")\n fi\n\n # Matrix-plugin debug tracing — propagated to every manager + worker\n # the controller spawns, then translated to OPENCLAW_MATRIX_DEBUG=1\n # by the container entrypoints. Use this to diagnose\n # \"worker did not join\" / \"manager replied empty\" hangs.\n if [ \"${AGENTTEAMS_MATRIX_DEBUG:-}\" = \"1\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}MATRIX_DEBUG=1\")\n fi\n\n # Optional: GitHub token\n if [ -n \"${AGENTTEAMS_GITHUB_TOKEN:-}\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}GITHUB_TOKEN=${AGENTTEAMS_GITHUB_TOKEN}\")\n fi\n\n # Optional: embedding model\n if [ -n \"${AGENTTEAMS_EMBEDDING_MODEL:-}\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}EMBEDDING_MODEL=${AGENTTEAMS_EMBEDDING_MODEL}\")\n fi\n\n # Optional: custom model capability overrides (context window, max\n # tokens, vision, reasoning). These are read by the Controller's\n # config.go LoadConfig to annotate custom models not in the built-in\n # presets table.\n if [ -n \"${AGENTTEAMS_MODEL_CONTEXT_WINDOW:-}\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}MODEL_CONTEXT_WINDOW=${AGENTTEAMS_MODEL_CONTEXT_WINDOW}\")\n fi\n if [ -n \"${AGENTTEAMS_MODEL_MAX_TOKENS:-}\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}MODEL_MAX_TOKENS=${AGENTTEAMS_MODEL_MAX_TOKENS}\")\n fi\n if [ -n \"${AGENTTEAMS_MODEL_VISION:-}\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}MODEL_VISION=${AGENTTEAMS_MODEL_VISION}\")\n fi\n if [ -n \"${AGENTTEAMS_MODEL_REASONING:-}\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}MODEL_REASONING=${AGENTTEAMS_MODEL_REASONING}\")\n fi\n\n # Optional: OpenAI-compatible base URL\n if [ -n \"${AGENTTEAMS_OPENAI_BASE_URL:-}\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}OPENAI_BASE_URL=${AGENTTEAMS_OPENAI_BASE_URL}\")\n fi\n # Optional: language\n if [ -n \"${AGENTTEAMS_LANGUAGE:-}\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}LANGUAGE=${AGENTTEAMS_LANGUAGE}\")\n fi\n\n # Optional: CMS/ARMS observability. In embedded mode the controller\n # spawns the Manager and Workers, so it must receive these settings.\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}CMS_TRACES_ENABLED=${AGENTTEAMS_CMS_TRACES_ENABLED:-false}\")\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}CMS_SERVICE_NAME=${AGENTTEAMS_CMS_SERVICE_NAME:-agentteams-manager}\")\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}CMS_METRICS_ENABLED=${AGENTTEAMS_CMS_METRICS_ENABLED:-false}\")\n if [ -n \"${AGENTTEAMS_CMS_ENDPOINT:-}\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}CMS_ENDPOINT=${AGENTTEAMS_CMS_ENDPOINT}\")\n fi\n if [ -n \"${AGENTTEAMS_CMS_LICENSE_KEY:-}\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}CMS_LICENSE_KEY=${AGENTTEAMS_CMS_LICENSE_KEY}\")\n fi\n if [ -n \"${AGENTTEAMS_CMS_PROJECT:-}\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}CMS_PROJECT=${AGENTTEAMS_CMS_PROJECT}\")\n fi\n if [ -n \"${AGENTTEAMS_CMS_WORKSPACE:-}\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}CMS_WORKSPACE=${AGENTTEAMS_CMS_WORKSPACE}\")\n fi\n\n # shellcheck disable=SC2086\n ${DOCKER_CMD} run -d \\\n --name agentteams-controller \\\n --network agentteams-net \\\n --network-alias matrix-local.agentteams.io \\\n --network-alias aigw-local.agentteams.io \\\n --network-alias fs-local.agentteams.io \\\n \"${_ctrl_env_args[@]}\" \\\n -v \"${CONTAINER_SOCK}:/var/run/docker.sock\" \\\n --security-opt label=disable \\\n -v \"${AGENTTEAMS_DATA_DIR}:/data\" \\\n -v \"${AGENTTEAMS_WORKSPACE_DIR}:/root/agentteams-fs/agents/manager\" \\\n -p \"${_port_prefix}${AGENTTEAMS_PORT_GATEWAY}:8080\" \\\n -p \"${_port_prefix}${AGENTTEAMS_PORT_CONSOLE}:8001\" \\\n -p \"${_port_prefix}${AGENTTEAMS_PORT_ELEMENT_WEB:-18088}:8088\" \\\n --restart unless-stopped \\\n \"${EMBEDDED_IMAGE}\"\n\n log \"Embedded controller started: agentteams-controller\"\n\n # Wait for infrastructure inside the controller container\n _wait_for_url() {\n local url=\"$1\" ctr=\"$2\" max_wait=\"${3:-120}\" desc=\"${4:-service}\"\n local elapsed=0\n log \"Waiting for ${desc}...\"\n while [ $elapsed -lt $max_wait ]; do\n if ${DOCKER_CMD} exec \"${ctr}\" curl -sf \"${url}\" >/dev/null 2>&1; then\n log \"${desc} is ready (${elapsed}s)\"\n return 0\n fi\n sleep 2\n elapsed=$((elapsed + 2))\n done\n log \"ERROR: ${desc} not ready after ${max_wait}s\"\n return 1\n }\n\n _wait_for_url \"http://127.0.0.1:6167/_tuwunel/server_version\" agentteams-controller 120 \"Tuwunel (Matrix)\" || exit 1\n _wait_for_url \"http://127.0.0.1:9000/minio/health/live\" agentteams-controller 60 \"MinIO\" || exit 1\n _wait_for_url \"http://127.0.0.1:8080/status\" agentteams-controller 120 \"Higress Gateway\" || exit 1\n\n # Wait for controller to create Manager Agent container\n log \"Waiting for Manager Agent container...\"\n local _mgr_wait=0\n local _mgr_max=300\n while [ $_mgr_wait -lt $_mgr_max ]; do\n if ${DOCKER_CMD} ps --format '{{.Names}}' 2>/dev/null | grep -q \"^agentteams-manager$\"; then\n log \"Manager Agent container detected (${_mgr_wait}s)\"\n break\n fi\n sleep 3\n _mgr_wait=$((_mgr_wait + 3))\n done\n if [ $_mgr_wait -ge $_mgr_max ]; then\n log \"ERROR: Manager Agent container not created after ${_mgr_max}s\"\n log \"Controller logs:\"\n ${DOCKER_CMD} exec agentteams-controller tail -30 /var/log/agentteams/agentteams-controller-error.log 2>/dev/null || true\n exit 1\n fi\n\n # Wait for Manager Agent to be running\n log \"Waiting for Manager Agent to start...\"\n local _agent_wait=0\n while [ $_agent_wait -lt 120 ]; do\n local _state\n _state=$(${DOCKER_CMD} inspect --format '{{.State.Status}}' agentteams-manager 2>/dev/null || echo \"missing\")\n if [ \"${_state}\" = \"running\" ]; then\n log \"Manager Agent is running\"\n break\n fi\n sleep 2\n _agent_wait=$((_agent_wait + 2))\n done\n\n # Enable yolo mode in agent if requested\n if [ \"${AGENTTEAMS_YOLO:-}\" = \"1\" ]; then\n ${DOCKER_CMD} exec agentteams-manager touch /root/manager-workspace/yolo-mode 2>/dev/null || true\n fi\n\n # Wait for the controller to send the first-boot welcome message.\n # The controller gates this on (a) Manager joining the DM room and\n # (b) Higress WASM key-auth propagation actually clearing /v1/chat/completions\n # for the Manager's gateway key — typically ~45-90s on a fresh install.\n # We poll Manager CR Status.WelcomeSent via the in-container agt CLI,\n # exec'd inside agentteams-controller (the source-of-truth container — its\n # bundled CLI binary is always in lockstep with whatever controller\n # binary is currently serving the HTTP API, since they're the same\n # `go build` output. The agentteams-manager container's CLI may lag the\n # controller across image upgrades and silently drop the welcomeSent\n # field, leaving this loop hung). The controller container mints a\n # long-lived admin SA token at startup and writes it to\n # AGENTTEAMS_AUTH_TOKEN_FILE=/var/run/agentteams/cli-token (set as a Dockerfile\n # ENV default), so a bare `docker exec agentteams-controller agt …`\n # auto-discovers both the endpoint and the token. There is a brief\n # window after container start before bootstrapAdminCLIToken completes\n # where the file may be empty / absent — the loop's silent retry\n # handles that the same way it handles the manager-not-yet-running\n # case below.\n if ${DOCKER_CMD} exec agentteams-controller sh -c 'command -v agt' >/dev/null 2>&1; then\n log \"$(msg install.welcome_msg.waiting)\"\n local _welcome_wait=0\n local _welcome_max=\"${AGENTTEAMS_WELCOME_TIMEOUT:-300}\"\n local _welcome_done=0\n while [ $_welcome_wait -lt $_welcome_max ]; do\n # `tr -d` strips whitespace/CR so the grep stays robust to\n # any future change in go-json field ordering or formatting.\n local _wjson\n _wjson=$(${DOCKER_CMD} exec agentteams-controller \\\n agt get managers default -o json 2>/dev/null || true)\n if [ -n \"${_wjson}\" ] && printf '%s' \"${_wjson}\" | tr -d ' \\r\\n' | grep -q '\"welcomeSent\":true'; then\n log \"$(msg install.welcome_msg.confirmed \"${_welcome_wait}\")\"\n _welcome_done=1\n break\n fi\n sleep 3\n _welcome_wait=$((_welcome_wait + 3))\n done\n if [ $_welcome_done -ne 1 ]; then\n # Non-fatal: install is still good. Keep going to the success\n # banner so the admin can use Element Web to nudge Manager into\n # onboarding manually (one DM message is enough).\n log \"$(msg install.welcome_msg.timeout \"${_welcome_max}\")\"\n log \"$(msg install.welcome_msg.timeout_hint)\"\n log \"$(msg install.welcome_msg.timeout_inspect)\"\n fi\n else\n log \"$(msg install.welcome_msg.poll_unavailable)\"\n fi\n\n else\n # ============================================================\n # Legacy architecture: all-in-one manager container\n # ============================================================\n\n NETWORK_ARGS=\"--network agentteams-net\"\n NETWORK_ALIAS_ARGS=\"--network-alias matrix-local.agentteams.io --network-alias aigw-local.agentteams.io --network-alias fs-local.agentteams.io\"\n for _domain in \"${AGENTTEAMS_MATRIX_CLIENT_DOMAIN:-}\" \"${AGENTTEAMS_CONSOLE_DOMAIN:-}\"; do\n if [ -n \"${_domain}\" ] && [[ \"${_domain}\" == *-local.agentteams.io ]]; then\n NETWORK_ALIAS_ARGS=\"${NETWORK_ALIAS_ARGS} --network-alias ${_domain}\"\n fi\n done\n\n # Start Docker API proxy if enabled (security layer between Manager and Docker daemon)\n PROXY_ARGS=\"\"\n if [ \"${AGENTTEAMS_DOCKER_PROXY:-1}\" = \"1\" ] && [ -n \"${CONTAINER_SOCK:-}\" ]; then\n local _proxy_image=\"${AGENTTEAMS_REGISTRY}/higress/agentteams-docker-proxy:${AGENTTEAMS_VERSION}\"\n # Try versioned tag, fallback to latest\n if ! ${DOCKER_CMD} image inspect \"${_proxy_image}\" >/dev/null 2>&1; then\n ${DOCKER_CMD} pull \"${_proxy_image}\" 2>/dev/null || {\n _proxy_image=\"${AGENTTEAMS_REGISTRY}/higress/agentteams-docker-proxy:latest\"\n ${DOCKER_CMD} pull \"${_proxy_image}\" 2>/dev/null || true\n }\n fi\n if ${DOCKER_CMD} image inspect \"${_proxy_image}\" >/dev/null 2>&1; then\n log \"Starting Docker API proxy...\"\n ${DOCKER_CMD} run -d \\\n --name agentteams-docker-proxy \\\n --network agentteams-net \\\n -v \"${CONTAINER_SOCK}:/var/run/docker.sock\" \\\n --security-opt label=disable \\\n -e AGENTTEAMS_WORKER_IMAGE=\"${WORKER_IMAGE}\" \\\n -e AGENTTEAMS_COPAW_WORKER_IMAGE=\"${COPAW_WORKER_IMAGE}\" \\\n -e AGENTTEAMS_QWENPAW_WORKER_IMAGE=\"${QWENPAW_WORKER_IMAGE}\" \\\n -e AGENTTEAMS_HERMES_WORKER_IMAGE=\"${HERMES_WORKER_IMAGE}\" \\\n ${AGENTTEAMS_PROXY_ALLOWED_REGISTRIES:+-e AGENTTEAMS_PROXY_ALLOWED_REGISTRIES=\"${AGENTTEAMS_PROXY_ALLOWED_REGISTRIES}\"} \\\n --restart unless-stopped \\\n \"${_proxy_image}\"\n PROXY_ARGS=\"-e AGENTTEAMS_CONTROLLER_URL=http://agentteams-docker-proxy:2375 -e AGENTTEAMS_CONTAINER_API=http://agentteams-docker-proxy:2375\"\n SOCKET_MOUNT_ARGS=\"\"\n fi\n fi\n\n # Pass host timezone to container\n TZ_ARGS=\"\"\n if [ -n \"${AGENTTEAMS_TIMEZONE:-}\" ]; then\n TZ_ARGS=\"-e TZ=${AGENTTEAMS_TIMEZONE}\"\n fi\n\n YOLO_ARGS=\"\"\n if [ \"${AGENTTEAMS_YOLO:-}\" = \"1\" ]; then\n YOLO_ARGS=\"-e AGENTTEAMS_YOLO=1\"\n fi\n\n MATRIX_DEBUG_ARGS=\"\"\n if [ \"${AGENTTEAMS_MATRIX_DEBUG:-}\" = \"1\" ]; then\n MATRIX_DEBUG_ARGS=\"-e AGENTTEAMS_MATRIX_DEBUG=1\"\n fi\n\n # shellcheck disable=SC2086\n ${DOCKER_CMD} run -d \\\n --name agentteams-manager \\\n --env-file \"${ENV_FILE}\" \\\n -e HOME=/root/manager-workspace \\\n -w /root/manager-workspace \\\n -e HOST_ORIGINAL_HOME=\"${AGENTTEAMS_HOST_SHARE_DIR}\" \\\n -e AGENTTEAMS_MANAGER_RUNTIME=\"${AGENTTEAMS_MANAGER_RUNTIME:-copaw}\" \\\n ${JVM_ARGS:+-e JVM_ARGS=\"${JVM_ARGS}\"} \\\n ${YOLO_ARGS} \\\n ${MATRIX_DEBUG_ARGS} \\\n ${TZ_ARGS} \\\n ${SOCKET_MOUNT_ARGS} \\\n ${NETWORK_ARGS} \\\n ${NETWORK_ALIAS_ARGS} \\\n ${PROXY_ARGS} \\\n -p \"${_port_prefix}${AGENTTEAMS_PORT_GATEWAY}:8080\" \\\n -p \"${_port_prefix}${AGENTTEAMS_PORT_CONSOLE}:8001\" \\\n -p \"${_port_prefix}${AGENTTEAMS_PORT_ELEMENT_WEB:-18088}:8088\" \\\n -p \"127.0.0.1:${AGENTTEAMS_PORT_MANAGER_CONSOLE:-18888}:18888\" \\\n ${DATA_MOUNT_ARGS} \\\n ${WORKSPACE_MOUNT_ARGS} \\\n ${HOST_SHARE_MOUNT_ARGS} \\\n --restart unless-stopped \\\n \"$([ \"${AGENTTEAMS_MANAGER_RUNTIME}\" = \"copaw\" ] && echo \"${MANAGER_COPAW_IMAGE}\" || echo \"${MANAGER_IMAGE}\")\"\n\n # Wait for Manager agent to be ready\n wait_manager_ready \"agentteams-manager\"\n\n # Wait for Matrix server to be ready\n wait_matrix_ready \"agentteams-manager\"\n\n # Post-install verification (non-fatal: warnings only)\n local _verify_script\n _verify_script=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)/agentteams-verify.sh\"\n if [ -f \"${_verify_script}\" ]; then\n bash \"${_verify_script}\" \"agentteams-manager\" || {\n log \"WARNING: Some post-install checks failed. Re-run: bash install/agentteams-verify.sh\"\n }\n fi\n fi\n unset _port_prefix\n\n # ── Start Dashboard (optional) ────────────────────────────────────\n _start_dashboard\n\n # Apply Podman autostart if selected and environment matches\n if [ \"${DOCKER_CMD}\" = \"podman\" ] && [ \"${AGENTTEAMS_PODMAN_AUTOSTART:-0}\" = \"1\" ]; then\n setup_podman_autostart\n fi\n\n log \"\"\n log \"$(msg success.title)\"\n log \"\"\n log \"$(msg success.domains_configured)\"\n log \" ${AGENTTEAMS_MATRIX_DOMAIN%%:*} ${AGENTTEAMS_MATRIX_CLIENT_DOMAIN} ${AGENTTEAMS_AI_GATEWAY_DOMAIN} ${AGENTTEAMS_FS_DOMAIN} ${AGENTTEAMS_CONSOLE_DOMAIN}\"\n log \"\"\n local lan_ip\n lan_ip=$(detect_lan_ip)\n echo -e \"\\033[33m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\033[0m\"\n echo -e \"\\033[33m $(msg success.open_url)\\033[0m\"\n echo -e \"\\033[33m \\033[0m\"\n echo -e \"\\033[1;36m http://127.0.0.1:${AGENTTEAMS_PORT_ELEMENT_WEB:-18088}/#/login\\033[0m\"\n echo -e \"\\033[33m \\033[0m\"\n echo -e \"\\033[33m $(msg success.login_with)\\033[0m\"\n echo -e \"\\033[33m $(msg success.username \"${AGENTTEAMS_ADMIN_USER}\")\\033[0m\"\n echo -e \"\\033[33m $(msg success.password \"${AGENTTEAMS_ADMIN_PASSWORD}\")\\033[0m\"\n echo -e \"\\033[33m \\033[0m\"\n echo -e \"\\033[33m $(msg success.after_login)\\033[0m\"\n echo -e \"\\033[33m $(msg success.tell_it)\\033[0m\"\n echo -e \"\\033[33m $(msg success.manager_auto)\\033[0m\"\n echo -e \"\\033[33m \\033[0m\"\n echo -e \"\\033[33m ───────────────────────────────────────────────────────────────────────────── \\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_title)\\033[0m\"\n echo -e \"\\033[33m \\033[0m\"\n if [ -n \"${lan_ip}\" ]; then\n echo -e \"\\033[33m $(msg success.mobile_step1)\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_step2 \"http://${lan_ip}:${AGENTTEAMS_PORT_GATEWAY}\")\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_step3)\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_username \"${AGENTTEAMS_ADMIN_USER}\")\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_password \"${AGENTTEAMS_ADMIN_PASSWORD}\")\\033[0m\"\n else\n echo -e \"\\033[33m $(msg success.mobile_step1)\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_step2_noip \"${AGENTTEAMS_PORT_GATEWAY}\")\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_noip_hint)\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_step3)\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_username \"${AGENTTEAMS_ADMIN_USER}\")\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_password \"${AGENTTEAMS_ADMIN_PASSWORD}\")\\033[0m\"\n fi\n echo -e \"\\033[33m \\033[0m\"\n echo -e \"\\033[33m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\033[0m\"\n log \"\"\n log \"$(msg success.other_consoles)\"\n log \"$(msg success.higress_console \"${AGENTTEAMS_PORT_CONSOLE}\" \"${AGENTTEAMS_ADMIN_USER}\" \"${AGENTTEAMS_ADMIN_PASSWORD}\")\"\n if [ \"${AGENTTEAMS_DASHBOARD:-0}\" = \"1\" ]; then\n log \"$(msg success.dashboard \"${AGENTTEAMS_PORT_DASHBOARD:-13000}\")\"\n fi\n if [ \"${AGENTTEAMS_USE_EMBEDDED}\" != \"1\" ]; then\n log \"$(msg success.manager_console \"${AGENTTEAMS_PORT_MANAGER_CONSOLE:-18888}\")\"\n log \"$(msg success.manager_console_gateway \"${AGENTTEAMS_ADMIN_USER}\" \"${AGENTTEAMS_ADMIN_PASSWORD}\")\"\n fi\n log \"\"\n log \"$(msg success.switch_llm.title)\"\n log \"$(msg success.switch_llm.hint)\"\n log \"$(msg success.switch_llm.docs)\"\n log \"$(msg success.switch_llm.url)\"\n log \"\"\n log \"$(msg success.tip)\"\n log \"\"\n if [ \"${AGENTTEAMS_LOCAL_ONLY:-1}\" != \"1\" ]; then\n echo -e \"\\033[33m$(msg port.local_only.https_hint)\\033[0m\"\n log \"\"\n fi\n log \"$(msg success.config_file \"${ENV_FILE}\")\"\n log \"$(msg success.data_volume \"${AGENTTEAMS_DATA_DIR}\")\"\n log \"$(msg success.workspace \"${AGENTTEAMS_WORKSPACE_DIR}\")\"\n}\n\n# ============================================================\n# Worker Installation (One-Click)\n# ============================================================\n\ninstall_worker() {\n local WORKER_NAME=\"\"\n local FS=\"\"\n local FS_KEY=\"\"\n local FS_SECRET=\"\"\n local RESET=false\n local SKILLS_API_URL=\"\"\n\n # Parse arguments\n while [ $# -gt 0 ]; do\n case $1 in\n --name) WORKER_NAME=\"$2\"; shift 2 ;;\n --fs) FS=\"$2\"; shift 2 ;;\n --fs-key) FS_KEY=\"$2\"; shift 2 ;;\n --fs-secret) FS_SECRET=\"$2\"; shift 2 ;;\n --skills-api-url) SKILLS_API_URL=\"$2\"; shift 2 ;;\n --reset) RESET=true; shift ;;\n *) die \"$(msg error.unknown_option \"$1\")\" ;;\n esac\n done\n\n # Validate required params\n [ -z \"${WORKER_NAME}\" ] && die \"$(msg error.name_required)\"\n [ -z \"${FS}\" ] && die \"$(msg error.fs_required)\"\n [ -z \"${FS_KEY}\" ] && die \"$(msg error.fs_key_required)\"\n [ -z \"${FS_SECRET}\" ] && die \"$(msg error.fs_secret_required)\"\n\n local CONTAINER_NAME=\"agentteams-worker-${WORKER_NAME}\"\n\n # Handle reset\n if [ \"${RESET}\" = true ]; then\n log \"$(msg worker.resetting \"${WORKER_NAME}\")\"\n ${DOCKER_CMD} stop \"${CONTAINER_NAME}\" 2>/dev/null || true\n ${DOCKER_CMD} rm \"${CONTAINER_NAME}\" 2>/dev/null || true\n fi\n\n # Check for existing container\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' | grep -q \"^${CONTAINER_NAME}$\"; then\n die \"$(msg worker.exists \"${CONTAINER_NAME}\")\"\n fi\n\n log \"$(msg worker.starting \"${WORKER_NAME}\")\"\n\n # Build docker run args\n local DOCKER_ENV=\"\"\n DOCKER_ENV=\"${DOCKER_ENV} -e HOME=/root/agentteams-fs/agents/${WORKER_NAME}\"\n DOCKER_ENV=\"${DOCKER_ENV} -w /root/agentteams-fs/agents/${WORKER_NAME}\"\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_WORKER_NAME=${WORKER_NAME}\"\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_FS_ENDPOINT=${FS}\"\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_FS_ACCESS_KEY=${FS_KEY}\"\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_FS_SECRET_KEY=${FS_SECRET}\"\n\n if [ -z \"${SKILLS_API_URL}\" ]; then\n if [ -n \"${AGENTTEAMS_SKILLS_API_URL:-}\" ]; then\n SKILLS_API_URL=\"${AGENTTEAMS_SKILLS_API_URL}\"\n else\n SKILLS_API_URL=\"nacos://market.agentteams.io:80/public\"\n fi\n fi\n\n # Add SKILLS_API_URL if specified\n DOCKER_ENV=\"${DOCKER_ENV} -e SKILLS_API_URL=${SKILLS_API_URL}\"\n log \"$(msg worker.skills_url \"${SKILLS_API_URL}\")\"\n if [ -n \"${AGENTTEAMS_NACOS_USERNAME:-}\" ]; then\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_NACOS_USERNAME=${AGENTTEAMS_NACOS_USERNAME}\"\n fi\n if [ -n \"${AGENTTEAMS_NACOS_PASSWORD:-}\" ]; then\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_NACOS_PASSWORD=${AGENTTEAMS_NACOS_PASSWORD}\"\n fi\n if [ -n \"${AGENTTEAMS_NACOS_TOKEN:-}\" ]; then\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_NACOS_TOKEN=${AGENTTEAMS_NACOS_TOKEN}\"\n fi\n\n # shellcheck disable=SC2086\n ${DOCKER_CMD} run -d \\\n --name \"${CONTAINER_NAME}\" \\\n ${DOCKER_ENV} \\\n --restart unless-stopped \\\n \"${WORKER_IMAGE}\"\n\n log \"\"\n log \"$(msg worker.started \"${WORKER_NAME}\")\"\n log \"$(msg worker.container \"${CONTAINER_NAME}\")\"\n log \"$(msg worker.view_logs \"${CONTAINER_NAME}\")\"\n}\n\n# ============================================================\n# Main\n# ============================================================\n\n# ============================================================\n# LLM API connectivity test\n# ============================================================\n\ntest_llm_connectivity() {\n local base_url=\"$1\"\n local api_key=\"$2\"\n local model=\"$3\"\n local hint=\"${4:-}\" # optional: extra hint shown on failure\n if ! command -v curl >/dev/null 2>&1; then\n echo -e \"\\033[33m$(msg llm.openai.test.no_curl)\\033[0m\"\n return\n fi\n log \"$(msg llm.openai.test.testing)\"\n local _body _http_code _tmpfile\n _tmpfile=$(mktemp)\n _http_code=$(curl -s -o \"${_tmpfile}\" -w \"%{http_code}\" \\\n -X POST \"${base_url%/}/chat/completions\" \\\n -H \"Authorization: Bearer ${api_key}\" \\\n -H \"Content-Type: application/json\" \\\n -H \"User-Agent: AgentTeams/${AGENTTEAMS_VERSION:-latest}\" \\\n --max-time 30 \\\n -d \"{\\\"model\\\":\\\"${model}\\\",\\\"messages\\\":[{\\\"role\\\":\\\"user\\\",\\\"content\\\":\\\"hi\\\"}]}\" \\\n 2>/dev/null)\n _body=$(cat \"${_tmpfile}\")\n rm -f \"${_tmpfile}\"\n if [ \"${_http_code}\" = \"200\" ] || [ \"${_http_code}\" = \"201\" ]; then\n log \"$(msg llm.openai.test.ok)\"\n else\n echo -e \"\\033[33m$(msg llm.openai.test.fail \"${_http_code}\" \"${_body}\")\\033[0m\"\n if [ -n \"${hint}\" ]; then\n echo -e \"\\033[33m${hint}\\033[0m\"\n fi\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" != \"1\" ]; then\n local _confirm\n read -e -p \"$(msg llm.openai.test.confirm)\" _confirm\n if [ \"${_confirm}\" = \"b\" ]; then\n STEP_RESULT=\"back\"\n return 1\n fi\n if [ \"${_confirm}\" != \"y\" ] && [ \"${_confirm}\" != \"Y\" ]; then\n log \"$(msg llm.openai.test.aborted)\"\n exit 1\n fi\n fi\n fi\n}\n\ntest_embedding_connectivity() {\n local base_url=\"$1\"\n local api_key=\"$2\"\n local model=\"$3\"\n if ! command -v curl >/dev/null 2>&1; then\n return 0\n fi\n log \"$(msg llm.embedding.test.testing)\"\n local _body _http_code _tmpfile\n _tmpfile=$(mktemp)\n _http_code=$(curl -s -o \"${_tmpfile}\" -w \"%{http_code}\" \\\n -X POST \"${base_url%/}/embeddings\" \\\n -H \"Authorization: Bearer ${api_key}\" \\\n -H \"Content-Type: application/json\" \\\n -H \"User-Agent: AgentTeams/${AGENTTEAMS_VERSION:-latest}\" \\\n --max-time 30 \\\n -d \"{\\\"model\\\":\\\"${model}\\\",\\\"input\\\":\\\"test\\\"}\" \\\n 2>/dev/null)\n _body=$(cat \"${_tmpfile}\")\n rm -f \"${_tmpfile}\"\n if [ \"${_http_code}\" = \"200\" ] || [ \"${_http_code}\" = \"201\" ]; then\n log \"$(msg llm.embedding.test.ok)\"\n return 0\n else\n echo -e \"\\033[33m$(msg llm.embedding.test.fail \"${_http_code}\" \"${_body}\")\\033[0m\"\n return 1\n fi\n}\n\n# ============================================================\n# Uninstall\n# ============================================================\n\nuninstall_agentteams() {\n log \"$(msg uninstall.title)\"\n\n # Safely disable and remove the dedicated AgentTeams Podman autostart service\n if [ \"${DOCKER_CMD}\" = \"podman\" ] && command -v systemctl >/dev/null 2>&1; then\n local _systemctl_cmd=\"systemctl\"\n local _service_dir=\"/etc/systemd/system\"\n\n if [ \"$(id -u)\" != \"0\" ]; then\n _systemctl_cmd=\"systemctl --user\"\n _service_dir=\"${HOME}/.config/systemd/user\"\n fi\n\n if [ -f \"${_service_dir}/agentteams-podman-restart.service\" ]; then\n log \"Disabling and removing dedicated AgentTeams Podman autostart service...\"\n ${_systemctl_cmd} disable --now agentteams-podman-restart.service 2>/dev/null || true\n rm -f \"${_service_dir}/agentteams-podman-restart.service\"\n ${_systemctl_cmd} daemon-reload 2>/dev/null || true\n fi\n # Note: Native podman-restart.service is strictly untouched per maintainer review.\n fi\n\n # Capture manager image before removing (needed for workspace cleanup on Linux)\n local manager_image=\"\"\n manager_image=$(${DOCKER_CMD} inspect agentteams-manager --format '{{.Config.Image}}' 2>/dev/null || true)\n\n # Stop and remove manager\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -q \"^agentteams-manager$\"; then\n log \"$(msg uninstall.stopping_manager)\"\n ${DOCKER_CMD} stop agentteams-manager >/dev/null 2>&1 || true\n ${DOCKER_CMD} rm agentteams-manager >/dev/null 2>&1 || true\n fi\n\n # Stop and remove workers\n local workers\n workers=$(${DOCKER_CMD} ps -a --filter \"name=agentteams-worker-\" --format '{{.Names}}' 2>/dev/null || true)\n if [ -n \"${workers}\" ]; then\n log \"$(msg uninstall.stopping_workers)\"\n echo \"${workers}\" | while read -r w; do\n ${DOCKER_CMD} rm -f \"${w}\" >/dev/null 2>&1 || true\n log \"$(msg uninstall.removed \"${w}\")\"\n done\n fi\n\n # Stop and remove the dashboard container (installed via step_dashboard)\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -q \"^agentteams-dashboard$\"; then\n log \"Stopping and removing agentteams-dashboard...\"\n ${DOCKER_CMD} stop agentteams-dashboard >/dev/null 2>&1 || true\n ${DOCKER_CMD} rm agentteams-dashboard >/dev/null 2>&1 || true\n fi\n if ${DOCKER_CMD} volume inspect agentteams-dashboard-data >/dev/null 2>&1; then\n log \"Removing dashboard data volume: agentteams-dashboard-data\"\n ${DOCKER_CMD} volume rm agentteams-dashboard-data >/dev/null 2>&1 || true\n fi\n\n # Stop and remove docker-proxy (legacy ≤ v1.0.x; current arch uses\n # agentteams-controller for the same role)\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -q \"^agentteams-docker-proxy$\"; then\n log \"$(msg uninstall.removing_proxy)\"\n ${DOCKER_CMD} stop agentteams-docker-proxy >/dev/null 2>&1 || true\n ${DOCKER_CMD} rm agentteams-docker-proxy >/dev/null 2>&1 || true\n fi\n\n # Stop and remove the embedded controller container. MUST happen\n # before the `docker volume rm agentteams-data` step below — in embedded\n # mode agentteams-controller mounts agentteams-data at /data (Tuwunel DB,\n # MinIO state, Higress state, all the room messages), and `volume\n # rm` against an in-use volume fails silently because of the trailing\n # `|| true`. Skipping this used to leave room/message history behind\n # across \"uninstall + reinstall\" cycles. See PR #692.\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -q \"^agentteams-controller$\"; then\n log \"$(msg uninstall.stopping_controller)\"\n ${DOCKER_CMD} stop agentteams-controller >/dev/null 2>&1 || true\n ${DOCKER_CMD} rm agentteams-controller >/dev/null 2>&1 || true\n fi\n\n # Read env file for data/workspace info before removing\n local env_file=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n [ ! -f \"${env_file}\" ] && [ -f \"./agentteams-manager.env\" ] && env_file=\"./agentteams-manager.env\"\n\n local data_dir=\"\" workspace_dir=\"\"\n if [ -f \"${env_file}\" ]; then\n data_dir=$(grep '^AGENTTEAMS_DATA_DIR=' \"${env_file}\" 2>/dev/null | cut -d= -f2- || true)\n workspace_dir=$(grep '^AGENTTEAMS_WORKSPACE_DIR=' \"${env_file}\" 2>/dev/null | cut -d= -f2- || true)\n fi\n\n # Remove Docker volume\n local vol=\"${data_dir:-agentteams-data}\"\n if ${DOCKER_CMD} volume inspect \"${vol}\" >/dev/null 2>&1; then\n log \"$(msg uninstall.removing_volume \"${vol}\")\"\n ${DOCKER_CMD} volume rm \"${vol}\" >/dev/null 2>&1 || true\n fi\n\n # Remove Docker network\n if ${DOCKER_CMD} network ls --format '{{.Name}}' 2>/dev/null | grep -q \"^agentteams-net$\"; then\n log \"$(msg uninstall.removing_network)\"\n ${DOCKER_CMD} network rm agentteams-net >/dev/null 2>&1 || true\n fi\n\n # Remove workspace directory\n if [ -n \"${workspace_dir}\" ] && [ -d \"${workspace_dir}\" ]; then\n log \"$(msg uninstall.removing_workspace \"${workspace_dir}\")\"\n rm -rf \"${workspace_dir}\" 2>/dev/null || true\n if [ -d \"${workspace_dir}\" ]; then\n log \"$(msg uninstall.removing_workspace_elevated)\"\n local _parent _base\n _parent=$(dirname \"${workspace_dir}\")\n _base=$(basename \"${workspace_dir}\")\n local _rm_image=\"${manager_image:-busybox}\"\n ${DOCKER_CMD} run --rm --entrypoint sh \\\n -v \"${_parent}:/host-parent\" \\\n \"${_rm_image}\" \\\n -c \"rm -rf /host-parent/${_base}\" 2>/dev/null || true\n fi\n fi\n\n # Remove env file\n if [ -f \"${env_file}\" ]; then\n log \"$(msg uninstall.removing_env \"${env_file}\")\"\n rm -f \"${env_file}\"\n fi\n\n # Remove install log\n if [ -f \"${HOME}/agentteams-install.log\" ]; then\n log \"$(msg uninstall.removing_log \"${HOME}/agentteams-install.log\")\"\n rm -f \"${HOME}/agentteams-install.log\"\n fi\n\n echo \"\"\n log \"$(msg uninstall.done)\"\n}\n\n# ============================================================\n# Check container runtime (docker or podman) and environment\n# ============================================================\ncheck_container_runtime() {\n # Ensure the rootless environment baseline (XDG_RUNTIME_DIR) is ready before engine detection.\n if [ \"$(id -u)\" -ne 0 ] && [ -z \"${XDG_RUNTIME_DIR:-}\" ]; then\n export XDG_RUNTIME_DIR=\"/run/user/$(id -u)\"\n fi\n\n if command -v docker >/dev/null 2>&1; then\n DOCKER_CMD=\"docker\"\n # Check for podman disguised as docker (alias or podman-docker package)\n if docker --version 2>/dev/null | grep -qi \"podman\"; then\n DOCKER_CMD=\"podman\"\n fi\n elif command -v podman >/dev/null 2>&1; then\n DOCKER_CMD=\"podman\"\n else\n echo -e \"\\033[31m[AgentTeams ERROR]\\033[0m $(msg error.docker_not_found)\" >&2\n exit 1\n fi\n\n # Unified health check (Validates runtime responsiveness)\n if ! ${DOCKER_CMD} ps >/dev/null 2>&1; then\n echo -e \"\\033[31m[AgentTeams ERROR]\\033[0m $(msg error.docker_not_running)\" >&2\n exit 1\n fi\n}\n\ncheck_container_runtime\n\ncase \"${1:-}\" in\n manager|\"\")\n install_manager\n ;;\n worker)\n shift\n install_worker \"$@\"\n ;;\n dashboard)\n check_container_runtime\n load_current_params_from_env\n AGENTTEAMS_DASHBOARD=\"${AGENTTEAMS_DASHBOARD:-1}\"\n AGENTTEAMS_DASHBOARD_VERSION=\"${AGENTTEAMS_DASHBOARD_VERSION:-v1.2.0}\"\n AGENTTEAMS_PORT_DASHBOARD=\"${AGENTTEAMS_PORT_DASHBOARD:-13000}\"\n AGENTTEAMS_DASHBOARD_IMAGE=\"${AGENTTEAMS_DASHBOARD_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-dashboard:${AGENTTEAMS_DASHBOARD_VERSION}}\"\n AGENTTEAMS_USE_EMBEDDED=1\n _start_dashboard\n ;;\n uninstall)\n uninstall_agentteams\n ;;\n *)\n echo \"Usage: $0 [manager|worker [options]|dashboard|uninstall]\"\n echo \"\"\n echo \"Commands:\"\n echo \" manager Interactive Manager installation (default)\"\n echo \" Choose Quick Start (all defaults) or Manual mode\"\n echo \" worker Worker installation (requires --name and connection params)\"\n echo \" dashboard Start/stop agentteams-dashboard (use with AGENTTEAMS_DASHBOARD=0 to stop)\"\n echo \" uninstall Stop and remove Manager + all Worker containers\"\n echo \"\"\n echo \"Quick Start (fastest):\"\n echo \" $0\"\n echo \" # Then select '1' for Quick Start mode\"\n echo \"\"\n echo \"Non-interactive (for automation):\"\n echo \" AGENTTEAMS_NON_INTERACTIVE=1 AGENTTEAMS_LLM_API_KEY=sk-xxx $0\"\n echo \"\"\n echo \"Worker Options:\"\n echo \" --name <name> Worker name (required)\"\n echo \" --fs <url> MinIO endpoint URL (required)\"\n echo \" --fs-key <key> MinIO access key (required)\"\n echo \" --fs-secret <secret> MinIO secret key (required)\"\n echo \" --reset Remove existing Worker container before creating\"\n exit 1\n ;;\nesac\n"}, "99131d6ed7087d307469f9b359d10cc1ec8c78e2": {"t": 1785764734.0776632, "n": 248781, "h": "ed80ee68e8132683", "c": "#!/bin/bash\n# agentteams-install.sh - One-click installation for AgentTeams Manager, Worker, and Dashboard\n#\n# Usage:\n# ./agentteams-install.sh # Interactive installation (choose Quick Start or Manual)\n# ./agentteams-install.sh manager # Same as above (explicit)\n# ./agentteams-install.sh worker --name <name> ... # Worker installation\n# ./agentteams-install.sh dashboard # Start/stop agentteams-dashboard (AGENTTEAMS_DASHBOARD=0 to stop)\n# ./agentteams-install.sh uninstall # Stop and remove Manager + all Workers\n#\n# NOTE: agentteams-dashboard is currently only supported via the Bash installer.\n# The PowerShell installer (agentteams-install.ps1) does not yet include dashboard support.\n#\n# Onboarding Modes:\n# Quick Start - Fast installation with all default values (recommended)\n# Manual - Customize each option step by step\n#\n# Environment variables (for automation):\n# AGENTTEAMS_NON_INTERACTIVE Skip all prompts, use defaults (default: 0)\n# AGENTTEAMS_LLM_PROVIDER LLM provider (default: openai-compat for zh non-interactive Token Plan; qwen for en)\n# AGENTTEAMS_DEFAULT_MODEL Default model (default: qwen3.6-plus for zh Token Plan and en non-interactive)\n# AGENTTEAMS_OPENAI_BASE_URL OpenAI-compatible base URL (default for zh non-interactive: Alibaba Token Plan endpoint)\n# AGENTTEAMS_LLM_API_KEY LLM API key (required)\n# AGENTTEAMS_ADMIN_USER Admin username (default: admin)\n# AGENTTEAMS_ADMIN_PASSWORD Admin password (auto-generated if not set, min 8 chars)\n# AGENTTEAMS_MATRIX_DOMAIN Matrix domain (default: matrix-local.agentteams.io:18080)\n# AGENTTEAMS_MOUNT_SOCKET Mount container runtime socket (default: 1)\n# AGENTTEAMS_DATA_DIR Docker volume name for persistent data (default: agentteams-data)\n# AGENTTEAMS_WORKSPACE_DIR Host directory for manager workspace (default: ~/agentteams-manager)\n# AGENTTEAMS_VERSION Image tag (default: latest)\n# AGENTTEAMS_REGISTRY Image registry (default: auto-detected by timezone)\n# AGENTTEAMS_INSTALL_MANAGER_IMAGE Override manager image (e.g., local build)\n# AGENTTEAMS_INSTALL_WORKER_IMAGE Override worker image (e.g., local build)\n# AGENTTEAMS_INSTALL_COPAW_WORKER_IMAGE Override copaw worker image (e.g., local build)\n# AGENTTEAMS_INSTALL_HERMES_WORKER_IMAGE Override hermes worker image (e.g., local build)\n# AGENTTEAMS_NACOS_REGISTRY_URI Default Nacos registry URI for Worker market search/import\n# (default: nacos://market.agentteams.io:80/public)\n# AGENTTEAMS_NACOS_USERNAME Default Nacos username for nacos:// package imports (optional)\n# AGENTTEAMS_NACOS_PASSWORD Default Nacos password for nacos:// package imports (optional)\n# AGENTTEAMS_CMS_TRACES_ENABLED Enable openclaw-cms-plugin traces for Manager AND all Workers (default: false)\n# AGENTTEAMS_CMS_ENDPOINT ARMS OTLP endpoint (required if traces enabled)\n# AGENTTEAMS_CMS_LICENSE_KEY CMS license key (required if traces enabled)\n# AGENTTEAMS_CMS_PROJECT CMS project name (optional)\n# AGENTTEAMS_CMS_WORKSPACE CMS workspace ID (required if traces enabled)\n# AGENTTEAMS_CMS_SERVICE_NAME Manager service name in ARMS (default: agentteams-manager)\n# Workers always report as agentteams-worker-<name> automatically\n# AGENTTEAMS_CMS_METRICS_ENABLED Enable diagnostics-otel metrics for Manager AND all Workers (default: false)\n# AGENTTEAMS_PORT_GATEWAY Host port for Higress gateway (default: 18080)\n# AGENTTEAMS_PORT_CONSOLE Host port for Higress console (default: 18001)\n# AGENTTEAMS_PORT_ELEMENT_WEB Host port for Element Web direct access (default: 18088)\n# AGENTTEAMS_PORT_MANAGER_CONSOLE Host port for Manager console (default: 18888)\n# AGENTTEAMS_WORKER_IDLE_TIMEOUT Worker idle timeout in minutes (default: 720, i.e. 12 hours)\n# AGENTTEAMS_DASHBOARD Install agentteams-dashboard management UI (default: 1)\n# AGENTTEAMS_DASHBOARD_VERSION Dashboard version (default: v1.2.0, independent of AgentTeams version)\n# AGENTTEAMS_PORT_DASHBOARD Dashboard host port (default: 13000)\n# AGENTTEAMS_DASHBOARD_IMAGE Override dashboard image (default: <registry>/agentteams/agentteams-dashboard:<DASHBOARD_VERSION>)\n# AGENTTEAMS_AI_GATEWAY_ADMIN_URL Higress Console URL for shared auth (auto-detected)\n\nset -e\n\nAGENTTEAMS_VERSION=\"${AGENTTEAMS_VERSION:-}\"\nAGENTTEAMS_KNOWN_STABLE_VERSION=\"v1.2.0\" # fallback if GitHub API is unreachable\n\n_normalize_version() {\n local version=\"$1\"\n case \"${version}\" in\n [0-9]*.[0-9]*.[0-9]*) version=\"v${version}\" ;;\n esac\n case \"${version}\" in\n v[0-9]*.[0-9]*.[0-9]*.beta.[0-9]*) version=\"${version/.beta./-beta.}\" ;;\n esac\n printf '%s' \"${version}\"\n}\n\n# Returns 0 (true) if $1 < $2 using semver order; \"latest\" is treated as greatest\n_ver_lt() {\n [ \"$1\" = \"latest\" ] && return 1\n [ \"$2\" = \"latest\" ] && return 0\n [ \"$1\" = \"$2\" ] && return 1\n [ \"$(printf '%s\\n%s' \"$1\" \"$2\" | sort -V | head -1)\" = \"$1\" ]\n}\n\n_use_legacy_image_env() {\n if [ \"$1\" = \"latest\" ]; then\n _ver_lt \"${AGENTTEAMS_KNOWN_STABLE_VERSION}\" \"v1.2.0\"\n else\n _ver_lt \"$1\" \"v1.2.0\"\n fi\n}\n\n_controller_env_prefix() {\n if _use_legacy_image_env \"$1\"; then\n printf '%s%s' 'HIC' 'LAW_'\n else\n printf '%s' 'AGENTTEAMS_'\n fi\n}\n\n_controller_storage_prefix() {\n if _use_legacy_image_env \"$1\"; then\n printf '%s%s' 'hic' 'law/agentteams-storage'\n else\n printf '%s' 'agentteams/agentteams-storage'\n fi\n}\nAGENTTEAMS_NON_INTERACTIVE=\"${AGENTTEAMS_NON_INTERACTIVE:-0}\"\nAGENTTEAMS_MOUNT_SOCKET=\"${AGENTTEAMS_MOUNT_SOCKET:-1}\"\nAGENTTEAMS_DOCKER_PROXY=\"${AGENTTEAMS_DOCKER_PROXY:-1}\"\nSTEP_RESULT=\"\" # Used by state machine to signal \"back\" navigation\n\n# ============================================================\n# Log all output to file\n# ============================================================\n\nAGENTTEAMS_LOG_FILE=\"${HOME}/agentteams-install.log\"\n\nif [ \"${1:-}\" != \"uninstall\" ]; then\n # Redirect all output (stdout and stderr) to both terminal and log file\n exec > >(tee -a \"${AGENTTEAMS_LOG_FILE}\") 2>&1\n\n echo \"\"\n echo \"========================================\"\n echo \"AgentTeams Installation Log\"\n echo \"Started: $(date)\"\n echo \"User: $(whoami)\"\n echo \"System: $(uname -a)\"\n echo \"Log file: ${AGENTTEAMS_LOG_FILE}\"\n echo \"========================================\"\n echo \"\"\nfi\n\n# ============================================================\n# Utility functions (needed early for timezone detection)\n# ============================================================\n\nlog() {\n echo -e \"\\033[36m[AgentTeams]\\033[0m $1\"\n}\n\nerror() {\n echo -e \"\\033[31m[AgentTeams ERROR]\\033[0m $1\" >&2\n}\n\ndie() {\n error \"$1\"\n exit 1\n}\n\n# ============================================================\n# Timezone detection (compatible with Linux and macOS)\n# ============================================================\n\ndetect_timezone() {\n local tz=\"\"\n\n # Try /etc/timezone (Debian/Ubuntu)\n if [ -f /etc/timezone ]; then\n tz=$(cat /etc/timezone 2>/dev/null | tr -d '[:space:]')\n fi\n\n # Try /etc/localtime symlink (macOS and some Linux)\n if [ -z \"${tz}\" ] && [ -L /etc/localtime ]; then\n tz=$(ls -l /etc/localtime 2>/dev/null | sed 's|.*/zoneinfo/||')\n fi\n\n # Try timedatectl (systemd)\n if [ -z \"${tz}\" ]; then\n tz=$(timedatectl show --value -p Timezone 2>/dev/null)\n fi\n\n # If still not detected, warn and prompt user. Diagnostics go to stderr —\n # this function returns its value via stdout to `$(detect_timezone)`, so\n # any echo here would leak into AGENTTEAMS_TIMEZONE and downstream image tags.\n if [ -z \"${tz}\" ]; then\n echo \"\" >&2\n echo -e \"\\033[33m[AgentTeams WARNING]\\033[0m Could not detect timezone automatically.\" >&2\n echo -e \"\\033[33m[AgentTeams]\\033[0m Please enter your timezone (e.g., Asia/Shanghai, America/New_York).\" >&2\n echo \"\" >&2\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n tz=\"Asia/Shanghai\"\n log \"Using default timezone: ${tz}\" >&2\n else\n read -e -p \"Timezone [Asia/Shanghai]: \" tz\n tz=\"${tz:-Asia/Shanghai}\"\n fi\n fi\n\n echo \"${tz}\"\n}\n\n# Detect timezone once at startup (used by registry selection and container TZ)\nAGENTTEAMS_TIMEZONE=\"${AGENTTEAMS_TIMEZONE:-$(detect_timezone)}\"\n\n# ============================================================\n# Language detection based on timezone\n# ============================================================\n\ndetect_language() {\n local tz=\"${AGENTTEAMS_TIMEZONE}\"\n case \"${tz}\" in\n Asia/Shanghai|Asia/Chongqing|Asia/Harbin|Asia/Urumqi|\\\n Asia/Taipei|Asia/Hong_Kong|Asia/Macau)\n echo \"zh\"\n ;;\n *)\n echo \"en\"\n ;;\n esac\n}\n\n# Language priority: env var > existing env file > timezone detection\nif [ -z \"${AGENTTEAMS_LANGUAGE}\" ]; then\n # Check existing env file for saved language preference (upgrade scenario)\n _env_file=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n # Migrate from legacy location (current directory) if needed\n if [ ! -f \"${_env_file}\" ] && [ -f \"./agentteams-manager.env\" ]; then\n mv \"./agentteams-manager.env\" \"${_env_file}\" 2>/dev/null || true\n fi\n if [ -f \"${_env_file}\" ]; then\n _saved_lang=$(grep '^AGENTTEAMS_LANGUAGE=' \"${_env_file}\" 2>/dev/null | cut -d= -f2-)\n if [ -n \"${_saved_lang}\" ]; then\n AGENTTEAMS_LANGUAGE=\"${_saved_lang}\"\n fi\n fi\n # Fall back to timezone-based detection\n if [ -z \"${AGENTTEAMS_LANGUAGE}\" ]; then\n AGENTTEAMS_LANGUAGE=\"$(detect_language)\"\n fi\n unset _env_file _saved_lang\nfi\nexport AGENTTEAMS_LANGUAGE\n\n# ============================================================\n# Centralized message dictionary and msg() function\n# Compatible with bash 3.2+ (macOS default) — uses case instead of declare -A\n# ============================================================\n\n# msg() function: look up message by key, with printf-style argument substitution\n# Falls back to English if the current language translation is missing.\nmsg() {\n local key=\"$1\"\n shift\n local lang=\"${AGENTTEAMS_LANGUAGE:-en}\"\n local text=\"\"\n case \"${key}.${lang}\" in\n # --- Timezone detection messages ---\n \"tz.warning.title.zh\") text=\"无法自动检测时区。\" ;;\n \"tz.warning.title.en\") text=\"Could not detect timezone automatically.\" ;;\n \"tz.warning.prompt.zh\") text=\"请输入您的时区(例如 Asia/Shanghai、America/New_York)。\" ;;\n \"tz.warning.prompt.en\") text=\"Please enter your timezone (e.g., Asia/Shanghai, America/New_York).\" ;;\n \"tz.default.zh\") text=\"使用默认时区: %s\" ;;\n \"tz.default.en\") text=\"Using default timezone: %s\" ;;\n \"tz.input_prompt.zh\") text=\"时区\" ;;\n \"tz.input_prompt.en\") text=\"Timezone\" ;;\n # --- Installation title and info ---\n \"install.title.zh\") text=\"=== AgentTeams Manager 安装 ===\" ;;\n \"install.title.en\") text=\"=== AgentTeams Manager Installation ===\" ;;\n \"install.registry.zh\") text=\"镜像仓库: %s\" ;;\n \"install.registry.en\") text=\"Registry: %s\" ;;\n \"install.dir.zh\") text=\"安装目录: %s\" ;;\n \"install.dir.en\") text=\"Installation directory: %s\" ;;\n \"install.dir_hint.zh\") text=\" (env 文件 'agentteams-manager.env' 将保存到 HOME 目录。)\" ;;\n \"install.dir_hint.en\") text=\" (The env file 'agentteams-manager.env' will be saved to your HOME directory.)\" ;;\n \"install.dir_hint2.zh\") text=\" (请从您希望管理此安装的目录运行此脚本。)\" ;;\n \"install.dir_hint2.en\") text=\" (Run this script from the directory where you want to manage this installation.)\" ;;\n # --- Onboarding mode ---\n \"install.mode.title.zh\") text=\"--- Onboarding 模式 ---\" ;;\n \"install.mode.title.en\") text=\"--- Onboarding Mode ---\" ;;\n \"install.mode.choose.zh\") text=\"选择安装模式:\" ;;\n \"install.mode.choose.en\") text=\"Choose your installation mode:\" ;;\n \"install.mode.quickstart.zh\") text=\" 1) 快速开始 - 使用阿里云通义 Token 套餐快速安装(推荐)\" ;;\n \"install.mode.quickstart.en\") text=\" 1) Quick Start - Fast installation with Qwen Cloud (recommended)\" ;;\n \"install.mode.manual.zh\") text=\" 2) 手动配置 - 选择 LLM 提供商并自定义选项\" ;;\n \"install.mode.manual.en\") text=\" 2) Manual - Choose LLM provider and customize options\" ;;\n \"install.mode.prompt.zh\") text=\"请选择 [1/2]\" ;;\n \"install.mode.prompt.en\") text=\"Enter choice [1/2]\" ;;\n \"install.mode.quickstart_selected.zh\") text=\"已选择快速开始模式 - 使用阿里云通义 Token 套餐\" ;;\n \"install.mode.quickstart_selected.en\") text=\"Quick Start mode selected - using Qwen Cloud\" ;;\n \"install.mode.manual_selected.zh\") text=\"已选择手动配置模式 - 您将选择 LLM 提供商并自定义选项\" ;;\n \"install.mode.manual_selected.en\") text=\"Manual mode selected - you will choose LLM provider and customize options\" ;;\n \"install.mode.invalid.zh\") text=\"无效选择,默认使用快速开始模式\" ;;\n \"install.mode.invalid.en\") text=\"Invalid choice, defaulting to Quick Start mode\" ;;\n # --- Upgrade mode sub-menu ---\n \"upgrade.mode.prompt.zh\") text=\"请选择升级方式:\" ;;\n \"upgrade.mode.prompt.en\") text=\"Select upgrade mode:\" ;;\n \"upgrade.mode.keep_all.zh\") text=\" 1) 保留所有参数,一键升级(推荐)\" ;;\n \"upgrade.mode.keep_all.en\") text=\" 1) Keep all parameters, quick upgrade (recommended)\" ;;\n \"upgrade.mode.confirm_each.zh\") text=\" 2) 逐个确认参数(可修改)\" ;;\n \"upgrade.mode.confirm_each.en\") text=\" 2) Confirm each parameter (can modify)\" ;;\n \"upgrade.mode.back.zh\") text=\" 3) 返回\" ;;\n \"upgrade.mode.back.en\") text=\" 3) Back\" ;;\n # --- Version selection ---\n \"install.version.title.zh\") text=\"--- 版本选择 ---\" ;;\n \"install.version.title.en\") text=\"--- Version Selection ---\" ;;\n \"install.version.choose.zh\") text=\"选择要安装的版本:\" ;;\n \"install.version.choose.en\") text=\"Choose the version to install:\" ;;\n \"install.version.option_latest.zh\") text=\" 1) latest - 最新版(默认)\" ;;\n \"install.version.option_latest.en\") text=\" 1) latest - Latest build (default)\" ;;\n \"install.version.option_stable.zh\") text=\" 2) %s - 最新稳定版\" ;;\n \"install.version.option_stable.en\") text=\" 2) %s - Latest stable release\" ;;\n \"install.version.fetching.zh\") text=\"正在查询最新稳定版本...\" ;;\n \"install.version.fetching.en\") text=\"Fetching latest stable release...\" ;;\n \"install.version.fetch_failed.zh\") text=\"无法查询 GitHub,使用内置版本 %s\" ;;\n \"install.version.fetch_failed.en\") text=\"Could not reach GitHub, using built-in version %s\" ;;\n \"install.version.option_custom.zh\") text=\" 3) 自定义 - 手动输入版本号(如 v1.0.5)\" ;;\n \"install.version.option_custom.en\") text=\" 3) Custom - Enter a specific version (e.g. v1.0.5)\" ;;\n \"install.version.prompt.zh\") text=\"请选择 [1/2/3]\" ;;\n \"install.version.prompt.en\") text=\"Enter choice [1/2/3]\" ;;\n \"install.version.custom_prompt.zh\") text=\"请输入版本号\" ;;\n \"install.version.custom_prompt.en\") text=\"Enter version tag\" ;;\n \"install.version.selected_latest.zh\") text=\"已选择最新版 (latest)\" ;;\n \"install.version.selected_latest.en\") text=\"Selected latest version\" ;;\n \"install.version.selected_stable.zh\") text=\"已选择最新稳定版 (%s)\" ;;\n \"install.version.selected_stable.en\") text=\"Selected latest stable version (%s)\" ;;\n \"install.version.selected_custom.zh\") text=\"已选择自定义版本 (%s)\" ;;\n \"install.version.selected_custom.en\") text=\"Selected custom version (%s)\" ;;\n \"install.version.invalid.zh\") text=\"无效选择,使用最新稳定版 (%s)\" ;;\n \"install.version.invalid.en\") text=\"Invalid choice, defaulting to latest stable version (%s)\" ;;\n # --- Existing installation detected ---\n \"install.existing.detected.zh\") text=\"检测到已有 Manager 安装(env 文件: %s)\" ;;\n \"install.existing.detected.en\") text=\"Existing Manager installation detected (env file: %s)\" ;;\n \"install.existing.choose.zh\") text=\"选择操作:\" ;;\n \"install.existing.choose.en\") text=\"Choose an action:\" ;;\n \"install.existing.upgrade.zh\") text=\" 1) 就地升级(保留数据、工作空间、env 文件)\" ;;\n \"install.existing.upgrade.en\") text=\" 1) In-place upgrade (keep data, workspace, env file)\" ;;\n \"install.existing.reinstall.zh\") text=\" 2) 全新重装(删除所有数据,重新开始)\" ;;\n \"install.existing.reinstall.en\") text=\" 2) Clean reinstall (remove all data, start fresh)\" ;;\n \"install.existing.cancel.zh\") text=\" 3) 取消\" ;;\n \"install.existing.cancel.en\") text=\" 3) Cancel\" ;;\n \"install.existing.prompt.zh\") text=\"请选择 [1/2/3]\" ;;\n \"install.existing.prompt.en\") text=\"Enter choice [1/2/3]\" ;;\n \"install.existing.upgrade_noninteractive.zh\") text=\"非交互模式: 执行就地升级...\" ;;\n \"install.existing.upgrade_noninteractive.en\") text=\"Non-interactive mode: performing in-place upgrade...\" ;;\n \"install.existing.upgrading.zh\") text=\"执行就地升级...\" ;;\n \"install.existing.upgrading.en\") text=\"Performing in-place upgrade...\" ;;\n \"install.existing.warn_manager_stop.zh\") text=\"⚠️ Manager 容器将被停止并重新创建。\" ;;\n \"install.existing.warn_manager_stop.en\") text=\"⚠️ Manager container will be stopped and recreated.\" ;;\n \"install.existing.warn_worker_recreate.zh\") text=\"⚠️ Worker 容器也将被重新创建(以更新 Manager IP)。\" ;;\n \"install.existing.warn_worker_recreate.en\") text=\"⚠️ Worker containers will also be recreated (to update Manager IP in hosts).\" ;;\n \"install.existing.continue_prompt.zh\") text=\"继续?[y/N]\" ;;\n \"install.existing.continue_prompt.en\") text=\"Continue? [y/N]\" ;;\n \"install.existing.cancelled.zh\") text=\"安装已取消。\" ;;\n \"install.existing.cancelled.en\") text=\"Installation cancelled.\" ;;\n \"install.existing.stopping_manager.zh\") text=\"停止并移除现有 manager 容器...\" ;;\n \"install.existing.stopping_manager.en\") text=\"Stopping and removing existing manager container...\" ;;\n \"install.existing.stopping_workers.zh\") text=\"停止并移除现有 worker 容器...\" ;;\n \"install.existing.stopping_workers.en\") text=\"Stopping and removing existing worker containers...\" ;;\n \"install.existing.removed.zh\") text=\" 已移除: %s\" ;;\n \"install.existing.removed.en\") text=\" Removed: %s\" ;;\n # --- Clean reinstall messages ---\n \"install.reinstall.performing.zh\") text=\"执行全新重装...\" ;;\n \"install.reinstall.performing.en\") text=\"Performing clean reinstall...\" ;;\n \"install.reinstall.warn_stop.zh\") text=\"⚠️ 以下运行中的容器将被停止:\" ;;\n \"install.reinstall.warn_stop.en\") text=\"⚠️ The following running containers will be stopped:\" ;;\n \"install.reinstall.warn_delete.zh\") text=\"⚠️ 警告: 以下内容将被删除:\" ;;\n \"install.reinstall.warn_delete.en\") text=\"⚠️ WARNING: This will DELETE the following:\" ;;\n \"install.reinstall.warn_volume.zh\") text=\" - Docker 卷: agentteams-data\" ;;\n \"install.reinstall.warn_volume.en\") text=\" - Docker volume: agentteams-data\" ;;\n \"install.reinstall.warn_env.zh\") text=\" - Env 文件: %s\" ;;\n \"install.reinstall.warn_env.en\") text=\" - Env file: %s\" ;;\n \"install.reinstall.warn_workspace.zh\") text=\" - Manager 工作空间: %s\" ;;\n \"install.reinstall.warn_workspace.en\") text=\" - Manager workspace: %s\" ;;\n \"install.reinstall.warn_workers.zh\") text=\" - 所有 worker 容器\" ;;\n \"install.reinstall.warn_workers.en\") text=\" - All worker containers\" ;;\n \"install.reinstall.warn_proxy.zh\") text=\" - Docker API 代理容器: agentteams-controller\" ;;\n \"install.reinstall.warn_proxy.en\") text=\" - Docker API proxy container: agentteams-controller\" ;;\n \"install.reinstall.warn_network.zh\") text=\" - Docker 网络: agentteams-net\" ;;\n \"install.reinstall.warn_network.en\") text=\" - Docker network: agentteams-net\" ;;\n \"install.reinstall.confirm_type.zh\") text=\"请输入工作空间路径以确认删除(或按 Ctrl+C 取消):\" ;;\n \"install.reinstall.confirm_type.en\") text=\"To confirm deletion, please type the workspace path:\" ;;\n \"install.reinstall.confirm_path.zh\") text=\"输入路径以确认(或按 Ctrl+C 取消)\" ;;\n \"install.reinstall.confirm_path.en\") text=\"Type the path to confirm (or press Ctrl+C to cancel)\" ;;\n \"install.reinstall.path_mismatch.zh\") text=\"路径不匹配。中止重装。输入: '%s',期望: '%s'\" ;;\n \"install.reinstall.path_mismatch.en\") text=\"Path mismatch. Aborting reinstall. Input: '%s', Expected: '%s'\" ;;\n \"install.reinstall.confirmed.zh\") text=\"已确认。正在清理...\" ;;\n \"install.reinstall.confirmed.en\") text=\"Confirmed. Cleaning up...\" ;;\n \"install.reinstall.removed_worker.zh\") text=\" 已移除 worker: %s\" ;;\n \"install.reinstall.removed_worker.en\") text=\" Removed worker: %s\" ;;\n \"install.reinstall.removing_volume.zh\") text=\"正在移除 Docker 卷: agentteams-data\" ;;\n \"install.reinstall.removing_volume.en\") text=\"Removing Docker volume: agentteams-data\" ;;\n \"install.reinstall.warn_volume_fail.zh\") text=\" 警告: 无法移除卷(可能有引用)\" ;;\n \"install.reinstall.warn_volume_fail.en\") text=\" Warning: Could not remove volume (may have references)\" ;;\n \"install.reinstall.removing_proxy.zh\") text=\"正在移除 Docker API 代理容器: agentteams-controller\" ;;\n \"install.reinstall.removing_proxy.en\") text=\"Removing Docker API proxy container: agentteams-controller\" ;;\n \"install.reinstall.removing_network.zh\") text=\"正在移除 Docker 网络: agentteams-net\" ;;\n \"install.reinstall.removing_network.en\") text=\"Removing Docker network: agentteams-net\" ;;\n \"install.reinstall.removing_workspace.zh\") text=\"正在移除工作空间目录: %s\" ;;\n \"install.reinstall.removing_workspace.en\") text=\"Removing workspace directory: %s\" ;;\n \"install.reinstall.removing_env.zh\") text=\"正在移除 env 文件: %s\" ;;\n \"install.reinstall.removing_env.en\") text=\"Removing env file: %s\" ;;\n \"install.reinstall.cleanup_done.zh\") text=\"清理完成。开始全新安装...\" ;;\n \"install.reinstall.cleanup_done.en\") text=\"Cleanup complete. Starting fresh installation...\" ;;\n \"install.reinstall.failed_rm_workspace.zh\") text=\"无法移除工作空间目录\" ;;\n \"install.reinstall.failed_rm_workspace.en\") text=\"Failed to remove workspace directory\" ;;\n # --- Orphan volume detection ---\n \"install.orphan_volume.detected.zh\") text=\"⚠️ 检测到残留数据卷 '%s',但未找到对应的 env 配置文件。\" ;;\n \"install.orphan_volume.detected.en\") text=\"⚠️ Found leftover data volume '%s' but no matching env config file.\" ;;\n \"install.orphan_volume.warn.zh\") text=\"这可能是之前安装的残留数据,会导致新安装出现异常(如密码冲突、服务启动失败)。\" ;;\n \"install.orphan_volume.warn.en\") text=\"This is likely leftover data from a previous installation and may cause issues (e.g., credential conflicts, service startup failures).\" ;;\n \"install.orphan_volume.choose.zh\") text=\"选择操作:\" ;;\n \"install.orphan_volume.choose.en\") text=\"Choose an action:\" ;;\n \"install.orphan_volume.clean.zh\") text=\" 1) 清理残留数据卷后继续安装(推荐)\" ;;\n \"install.orphan_volume.clean.en\") text=\" 1) Remove leftover volume and continue installation (recommended)\" ;;\n \"install.orphan_volume.keep.zh\") text=\" 2) 保留数据卷继续安装(可能出现异常)\" ;;\n \"install.orphan_volume.keep.en\") text=\" 2) Keep the volume and continue installation (may cause issues)\" ;;\n \"install.orphan_volume.prompt.zh\") text=\"请选择 [1/2]\" ;;\n \"install.orphan_volume.prompt.en\") text=\"Enter choice [1/2]\" ;;\n \"install.orphan_volume.cleaning.zh\") text=\"正在清理残留数据卷...\" ;;\n \"install.orphan_volume.cleaning.en\") text=\"Removing leftover data volume...\" ;;\n \"install.orphan_volume.cleaned.zh\") text=\"残留数据卷已清理。继续全新安装...\" ;;\n \"install.orphan_volume.cleaned.en\") text=\"Leftover volume removed. Continuing with fresh installation...\" ;;\n \"install.orphan_volume.keeping.zh\") text=\"保留数据卷,继续安装。如遇异常请选择全新重装。\" ;;\n \"install.orphan_volume.keeping.en\") text=\"Keeping existing volume. If you encounter issues, consider a clean reinstall.\" ;;\n \"install.orphan_volume.clean_noninteractive.zh\") text=\"非交互模式: 自动清理残留数据卷...\" ;;\n \"install.orphan_volume.clean_noninteractive.en\") text=\"Non-interactive mode: automatically removing leftover volume...\" ;;\n # --- Loading existing config ---\n \"install.loading_config.zh\") text=\"从 %s 加载已有配置(shell 环境变量优先)...\" ;;\n \"install.loading_config.en\") text=\"Loading existing config from %s (shell env vars take priority)...\" ;;\n # --- LLM Configuration ---\n \"llm.title.zh\") text=\"--- LLM 配置 ---\" ;;\n \"llm.title.en\") text=\"--- LLM Configuration ---\" ;;\n \"llm.provider.label.zh\") text=\" 提供商: %s\" ;;\n \"llm.provider.label.en\") text=\" Provider: %s\" ;;\n \"llm.model.label.zh\") text=\" 模型: %s\" ;;\n \"llm.model.label.en\") text=\" Model: %s\" ;;\n \"llm.provider.qwen.zh\") text=\" 提供商: qwen(阿里云百炼)\" ;;\n \"llm.provider.qwen.en\") text=\" Provider: qwen (Alibaba Cloud Bailian)\" ;;\n \"llm.provider.qwen_default.zh\") text=\" 提供商: %s(默认)\" ;;\n \"llm.provider.qwen_default.en\") text=\" Provider: %s (default)\" ;;\n \"llm.model.default.zh\") text=\" 模型: %s(默认)\" ;;\n \"llm.model.default.en\") text=\" Model: %s (default)\" ;;\n \"llm.apikey_hint_bailian.zh\") text=\" 💡 获取阿里云百炼(DashScope)API Key:\" ;;\n \"llm.apikey_hint_bailian.en\") text=\" 💡 Get your Alibaba Cloud Bailian (DashScope) API Key:\" ;;\n \"llm.apikey_url_bailian.zh\") text=\" https://www.aliyun.com/product/bailian\" ;;\n \"llm.apikey_url_bailian.en\") text=\" https://www.aliyun.com/product/bailian\" ;;\n \"llm.apikey_hint_qwencloud.zh\") text=\" 💡 从 Qwen Cloud(国际站)获取 DASHSCOPE_API_KEY:\" ;;\n \"llm.apikey_hint_qwencloud.en\") text=\" 💡 Get your DASHSCOPE_API_KEY for Qwen Cloud (international) from:\" ;;\n \"llm.apikey_url_qwencloud.zh\") text=\" https://home.qwencloud.com/api-keys (文档: https://docs.qwencloud.com/)\" ;;\n \"llm.apikey_url_qwencloud.en\") text=\" https://home.qwencloud.com/api-keys | Docs: https://docs.qwencloud.com/\" ;;\n \"llm.apikey_hint_tokenplan.zh\") text=\" 💡 获取 DashScope API Key 或开通通义 Token 套餐,请参考:\" ;;\n \"llm.apikey_hint_tokenplan.en\") text=\" 💡 Get your DashScope or Token Plan API key (Alibaba Model Studio):\" ;;\n \"llm.apikey_url_tokenplan.zh\") text=\" https://help.aliyun.com/zh/model-studio/token-plan-quickstart\" ;;\n \"llm.apikey_url_tokenplan.en\") text=\" https://common-buy.aliyun.com/token-plan/ | https://help.aliyun.com/zh/model-studio/token-plan-quickstart\" ;;\n \"llm.apikey_hint_codingplan.zh\") text=\" 💡 获取 DashScope API Key(Coding 套餐 / coding.dashscope 接口):\" ;;\n \"llm.apikey_hint_codingplan.en\") text=\" 💡 Get your DashScope API key for Coding Plan (coding.dashscope endpoint):\" ;;\n \"llm.apikey_url_codingplan.zh\") text=\" https://help.aliyun.com/zh/model-studio/get-api-key\" ;;\n \"llm.apikey_url_codingplan.en\") text=\" https://help.aliyun.com/zh/model-studio/get-api-key\" ;;\n \"llm.apikey_prompt.zh\") text=\"LLM API Key\" ;;\n \"llm.apikey_prompt.en\") text=\"LLM API Key\" ;;\n \"llm.providers_title.zh\") text=\"可用 LLM 提供商:\" ;;\n \"llm.providers_title.en\") text=\"Available LLM Providers:\" ;;\n \"llm.provider.alibaba.zh\") text=\" 1) 阿里云通义 Token 套餐 - 推荐中国用户使用\" ;;\n \"llm.provider.alibaba.en\") text=\" 1) Qwen Cloud - International (OpenAI-compatible API, recommended)\" ;;\n \"llm.provider.openai_compat.zh\") text=\" 2) OpenAI 兼容 API - 自定义 Base URL(OpenAI、DeepSeek 等)\" ;;\n \"llm.provider.openai_compat.en\") text=\" 2) OpenAI-compatible API - Custom Base URL (OpenAI, DeepSeek, etc.)\" ;;\n \"llm.provider.select.zh\") text=\"选择提供商 [1/2]\" ;;\n \"llm.provider.select.en\") text=\"Select provider [1/2]\" ;;\n \"llm.alibaba.models_title.zh\") text=\"选择阿里云模型接入方式:\" ;;\n \"llm.alibaba.models_title.en\") text=\"Select Alibaba Cloud model access:\" ;;\n \"llm.alibaba.model.tokenplan.zh\") text=\" 1) 阿里云通义 Token 套餐 - 兼容模式(推荐)\" ;;\n \"llm.alibaba.model.tokenplan.en\") text=\" 1) Alibaba Cloud Token Plan - compatible-mode (recommended)\" ;;\n \"llm.alibaba.model.bailian.zh\") text=\" 2) 阿里云百炼 - DashScope 通用兼容接口\" ;;\n \"llm.alibaba.model.bailian.en\") text=\" 2) Alibaba Cloud Bailian - DashScope compatible mode\" ;;\n \"llm.alibaba.model.codingplan_legacy.zh\") text=\" 3) 阿里云 Coding 套餐 - 旧版端点(兼容保留)\" ;;\n \"llm.alibaba.model.codingplan_legacy.en\") text=\" 3) Alibaba Cloud Coding Plan - legacy endpoint (backward compatible)\" ;;\n \"llm.alibaba.model.select.zh\") text=\"选择接入方式 [1/2/3]\" ;;\n \"llm.alibaba.model.select.en\") text=\"Select access option [1/2/3]\" ;;\n \"llm.alibaba.model.invalid.zh\") text=\"无效选择: %s(请输入 1、2 或 3)\" ;;\n \"llm.alibaba.model.invalid.en\") text=\"Invalid choice: %s (please enter 1, 2, or 3)\" ;;\n \"llm.codingplan.models_title.zh\") text=\"选择通义 Token 套餐默认模型:\" ;;\n \"llm.codingplan.models_title.en\") text=\"Select Qwen Cloud default model:\" ;;\n \"llm.codingplan.model.qwen36plus.zh\") text=\" 1) qwen3.6-plus - 千问 3.6(推荐)\" ;;\n \"llm.codingplan.model.qwen36plus.en\") text=\" 1) qwen3.6-plus - Qwen 3.6 (recommended)\" ;;\n \"llm.codingplan.model.glm5.zh\") text=\" 2) glm-5 - 智谱 GLM-5(编程推荐)\" ;;\n \"llm.codingplan.model.glm5.en\") text=\" 2) glm-5 - Zhipu GLM-5 (recommended for coding)\" ;;\n \"llm.codingplan.model.kimi.zh\") text=\" 3) kimi-k2.5 - Moonshot Kimi K2.5\" ;;\n \"llm.codingplan.model.kimi.en\") text=\" 3) kimi-k2.5 - Moonshot Kimi K2.5\" ;;\n \"llm.codingplan.model.minimax.zh\") text=\" 4) MiniMax-M2.5 - MiniMax M2.5\" ;;\n \"llm.codingplan.model.minimax.en\") text=\" 4) MiniMax-M2.5 - MiniMax M2.5\" ;;\n \"llm.codingplan.model.select.zh\") text=\"选择模型 [1/2/3/4]\" ;;\n \"llm.codingplan.model.select.en\") text=\"Select model [1/2/3/4]\" ;;\n \"llm.provider.selected_tokenplan.zh\") text=\" 提供商: 阿里云通义 Token 套餐(兼容模式)\" ;;\n \"llm.provider.selected_tokenplan.en\") text=\" Provider: Alibaba Cloud Token Plan (compatible mode)\" ;;\n \"llm.provider.selected_codingplan.zh\") text=\" 提供商: 阿里云通义 Token 套餐(alibaba-cloud)\" ;;\n \"llm.provider.selected_codingplan.en\") text=\" Provider: Qwen Cloud (international) (alibaba-cloud)\" ;;\n \"llm.provider.selected_codingplan_legacy.zh\") text=\" 提供商: 阿里云 Coding 套餐(coding.dashscope)\" ;;\n \"llm.provider.selected_codingplan_legacy.en\") text=\" Provider: Alibaba Cloud Coding Plan (coding.dashscope)\" ;;\n \"llm.provider.selected_qwen.zh\") text=\" 提供商: 阿里云百炼\" ;;\n \"llm.provider.selected_qwen.en\") text=\" Provider: Alibaba Cloud Bailian\" ;;\n \"llm.provider.selected_openai.zh\") text=\" 提供商: %s(OpenAI 兼容)\" ;;\n \"llm.provider.selected_openai.en\") text=\" Provider: %s (OpenAI-compatible)\" ;;\n \"llm.provider.invalid.zh\") text=\"无效选择: %s(请输入 1 或 2)\" ;;\n \"llm.provider.invalid.en\") text=\"Invalid choice: %s (please enter 1 or 2)\" ;;\n \"llm.qwen.model_prompt.zh\") text=\"默认模型 ID\" ;;\n \"llm.qwen.model_prompt.en\") text=\"Default Model ID\" ;;\n \"llm.openai.base_url_prompt.zh\") text=\"Base URL\" ;;\n \"llm.openai.base_url_prompt.en\") text=\"Base URL\" ;;\n \"llm.openai.model_prompt.zh\") text=\"默认模型 ID\" ;;\n \"llm.openai.model_prompt.en\") text=\"Default Model ID\" ;;\n \"llm.openai.base_url_label.zh\") text=\" Base URL: %s\" ;;\n \"llm.openai.base_url_label.en\") text=\" Base URL: %s\" ;;\n # --- Custom model parameters ---\n \"llm.custom_model.detected.zh\") text=\" ⚠️ 模型 '%s' 不在内置模型列表中,请配置模型参数:\" ;;\n \"llm.custom_model.detected.en\") text=\" ⚠️ Model '%s' is not in the built-in model list. Please configure model parameters:\" ;;\n \"llm.custom_model.context_prompt.zh\") text=\"最大上下文长度(token 数)[150000]\" ;;\n \"llm.custom_model.context_prompt.en\") text=\"Max context window (tokens) [150000]\" ;;\n \"llm.custom_model.max_tokens_prompt.zh\") text=\"最大输出长度(token 数)[128000]\" ;;\n \"llm.custom_model.max_tokens_prompt.en\") text=\"Max output tokens [128000]\" ;;\n \"llm.custom_model.reasoning_prompt.zh\") text=\"是否支持推理/思考模式?[Y/n]\" ;;\n \"llm.custom_model.reasoning_prompt.en\") text=\"Does it support reasoning/thinking mode? [Y/n]\" ;;\n \"llm.custom_model.vision_prompt.zh\") text=\"是否支持图片输入?[y/N]\" ;;\n \"llm.custom_model.vision_prompt.en\") text=\"Does it support image input? [y/N]\" ;;\n \"llm.custom_model.summary.zh\") text=\" 自定义模型参数: 上下文=%s, 最大输出=%s, 推理=%s, 图片=%s\" ;;\n \"llm.custom_model.summary.en\") text=\" Custom model params: context=%s, maxTokens=%s, reasoning=%s, vision=%s\" ;;\n # --- Admin Credentials ---\n \"admin.title.zh\") text=\"--- 管理员凭据 ---\" ;;\n \"admin.title.en\") text=\"--- Admin Credentials ---\" ;;\n \"admin.username_prompt.zh\") text=\"管理员用户名\" ;;\n \"admin.username_prompt.en\") text=\"Admin Username\" ;;\n \"admin.password_prompt.zh\") text=\"管理员密码(留空自动生成,最少 8 位)\" ;;\n \"admin.password_prompt.en\") text=\"Admin Password (leave empty to auto-generate, min 8 chars)\" ;;\n \"admin.password_generated.zh\") text=\" 已自动生成管理员密码\" ;;\n \"admin.password_generated.en\") text=\" Auto-generated admin password\" ;;\n \"admin.password_too_short.zh\") text=\"管理员密码至少需要 8 个字符(MinIO 要求)。当前长度: %s\" ;;\n \"admin.password_too_short.en\") text=\"Admin password must be at least 8 characters (MinIO requirement). Current length: %s\" ;;\n # --- Port Configuration ---\n \"port.title.zh\") text=\"--- 端口配置(按回车使用默认值)---\" ;;\n \"port.title.en\") text=\"--- Port Configuration (press Enter for defaults) ---\" ;;\n \"port.gateway_prompt.zh\") text=\"网关主机端口(容器内 8080)\" ;;\n \"port.gateway_prompt.en\") text=\"Host port for gateway (8080 inside container)\" ;;\n \"port.console_prompt.zh\") text=\"Higress 控制台主机端口(容器内 8001)\" ;;\n \"port.console_prompt.en\") text=\"Host port for Higress console (8001 inside container)\" ;;\n \"port.element_prompt.zh\") text=\"Element Web 直接访问主机端口(容器内 8088)\" ;;\n \"port.element_prompt.en\") text=\"Host port for Element Web direct access (8088 inside container)\" ;;\n \"port.manager_console_prompt.zh\") text=\"Manager 控制台主机端口(容器内 18888)\" ;;\n \"port.manager_console_prompt.en\") text=\"Host port for Manager console (18888 inside container)\" ;;\n \"port.copaw_app_prompt.zh\") text=\"CoPaw App API 主机端口(容器内 18799)\" ;;\n \"port.copaw_app_prompt.en\") text=\"Host port for CoPaw App API (18799 inside container)\" ;;\n # --- Local-only binding ---\n \"port.local_only.title.zh\") text=\"--- 网络访问模式 ---\" ;;\n \"port.local_only.title.en\") text=\"--- Network Access Mode ---\" ;;\n \"port.local_only.prompt.zh\") text=\"是否仅允许本机访问(端口绑定到 127.0.0.1)?\" ;;\n \"port.local_only.prompt.en\") text=\"Bind ports to localhost only (127.0.0.1)?\" ;;\n \"port.local_only.hint_yes.zh\") text=\" 仅本机使用,无需开放外部端口(推荐)\" ;;\n \"port.local_only.hint_yes.en\") text=\" Local use only, no external port exposure (recommended)\" ;;\n \"port.local_only.hint_no.zh\") text=\" 允许外部访问(局域网 / 公网)\" ;;\n \"port.local_only.hint_no.en\") text=\" Allow external access (LAN / public network)\" ;;\n \"port.local_only.choice.zh\") text=\"请选择 [1/2]\" ;;\n \"port.local_only.choice.en\") text=\"Enter choice [1/2]\" ;;\n \"port.local_only.selected_local.zh\") text=\"端口已绑定到 127.0.0.1(仅本机访问)\" ;;\n \"port.local_only.selected_local.en\") text=\"Ports bound to 127.0.0.1 (localhost only)\" ;;\n \"port.local_only.selected_external.zh\") text=\"端口已绑定到所有网络接口(0.0.0.0)\" ;;\n \"port.local_only.selected_external.en\") text=\"Ports bound to all interfaces (0.0.0.0)\" ;;\n \"port.local_only.https_hint.zh\") text=\"⚠️ 建议在 Higress 控制台配置 TLS 证书并启用 HTTPS,避免明文传输。\" ;;\n \"port.local_only.https_hint.en\") text=\"⚠️ It is recommended to configure TLS certificates and enable HTTPS in the Higress Console to avoid plaintext transmission.\" ;;\n \"port.local_only.https_docs.zh\") text=\"\" ;;\n \"port.local_only.https_docs.en\") text=\"\" ;;\n # --- Domain Configuration ---\n \"domain.title.zh\") text=\"--- 域名配置(按回车使用默认值)---\" ;;\n \"domain.title.en\") text=\"--- Domain Configuration (press Enter for defaults) ---\" ;;\n \"domain.hint.zh\") text=\"提示: 自定义域名前必须事先做好 DNS 解析。单机 ECS 部署时无需修改 aigw、fs 等域名;Element Web 和 Matrix Server 也可通过 IP 直接访问。\" ;;\n \"domain.hint.en\") text=\"Hint: Configure DNS resolution before customizing domains. For single ECS deployment, no need to change aigw, fs, etc.; Element Web and Matrix Server can also be accessed directly via IP.\" ;;\n \"domain.matrix_prompt.zh\") text=\"Matrix 域名\" ;;\n \"domain.matrix_prompt.en\") text=\"Matrix Domain\" ;;\n \"domain.element_prompt.zh\") text=\"Element Web 域名\" ;;\n \"domain.element_prompt.en\") text=\"Element Web Domain\" ;;\n \"domain.gateway_prompt.zh\") text=\"AI 网关域名\" ;;\n \"domain.gateway_prompt.en\") text=\"AI Gateway Domain\" ;;\n \"domain.fs_prompt.zh\") text=\"文件系统域名\" ;;\n \"domain.fs_prompt.en\") text=\"File System Domain\" ;;\n \"domain.console_prompt.zh\") text=\"Manager 控制台域名\" ;;\n \"domain.console_prompt.en\") text=\"Manager Console Domain\" ;;\n # --- GitHub Integration ---\n \"github.title.zh\") text=\"--- GitHub 集成(可选,按回车跳过)---\" ;;\n \"github.title.en\") text=\"--- GitHub Integration (optional, press Enter to skip) ---\" ;;\n \"github.token_prompt.zh\") text=\"GitHub 个人访问令牌(可选)\" ;;\n \"github.token_prompt.en\") text=\"GitHub Personal Access Token (optional)\" ;;\n # --- Skills Registry ---\n \"skills.title.zh\") text=\"--- Skills 注册中心(可选,按回车使用默认 nacos://market.agentteams.io:80/public)---\" ;;\n \"skills.title.en\") text=\"--- Skills Registry (optional, press Enter for default nacos://market.agentteams.io:80/public) ---\" ;;\n \"skills.url_prompt.zh\") text=\"Skills 注册中心 URL(留空使用默认 nacos://market.agentteams.io:80/public)\" ;;\n \"skills.url_prompt.en\") text=\"Skills Registry URL (leave empty for default nacos://market.agentteams.io:80/public)\" ;;\n # --- Data Persistence ---\n \"data.title.zh\") text=\"--- 数据持久化 ---\" ;;\n \"data.title.en\") text=\"--- Data Persistence ---\" ;;\n \"data.volume_prompt.zh\") text=\"Docker 卷名称 [agentteams-data]\" ;;\n \"data.volume_prompt.en\") text=\"Docker volume name for persistent data [agentteams-data]\" ;;\n \"data.volume_using.zh\") text=\" 使用 Docker 卷: %s\" ;;\n \"data.volume_using.en\") text=\" Using Docker volume: %s\" ;;\n # --- Manager Workspace ---\n \"workspace.title.zh\") text=\"--- Manager 工作空间 ---\" ;;\n \"workspace.title.en\") text=\"--- Manager Workspace ---\" ;;\n \"workspace.dir_prompt.zh\") text=\"Manager 工作空间目录 [%s]\" ;;\n \"workspace.dir_prompt.en\") text=\"Manager workspace directory [%s]\" ;;\n \"workspace.dir_label.zh\") text=\" Manager 工作空间: %s\" ;;\n \"workspace.dir_label.en\") text=\" Manager workspace: %s\" ;;\n # --- Host directory sharing ---\n \"host_share.prompt.zh\") text=\"与 Agent 共享的主机目录(默认: %s)\" ;;\n \"host_share.prompt.en\") text=\"Host directory to share with agents (default: %s)\" ;;\n \"host_share.sharing.zh\") text=\"共享主机目录: %s -> 容器内 /host-share\" ;;\n \"host_share.sharing.en\") text=\"Sharing host directory: %s -> /host-share in container\" ;;\n \"host_share.not_exist.zh\") text=\"警告: 主机目录 %s 不存在,跳过验证继续使用\" ;;\n \"host_share.not_exist.en\") text=\"WARNING: Host directory %s does not exist, using without validation\" ;;\n # --- Default worker runtime ---\n \"worker_runtime.title.zh\") text=\"--- 默认 Worker 运行时 ---\" ;;\n \"worker_runtime.title.en\") text=\"--- Default Worker Runtime ---\" ;;\n \"worker_runtime.openclaw.zh\") text=\"OpenClaw\" ;;\n \"worker_runtime.openclaw.en\") text=\"OpenClaw\" ;;\n \"worker_runtime.copaw.zh\") text=\"CoPaw\" ;;\n \"worker_runtime.copaw.en\") text=\"CoPaw\" ;;\n \"worker_runtime.hermes.zh\") text=\"Hermes\" ;;\n \"worker_runtime.hermes.en\") text=\"Hermes\" ;;\n \"worker_runtime.choice.zh\") text=\"请选择 [1/2/3]\" ;;\n \"worker_runtime.choice.en\") text=\"Enter choice [1/2/3]\" ;;\n \"worker_runtime.selected.zh\") text=\"默认 Worker 运行时: %s\" ;;\n \"worker_runtime.selected.en\") text=\"Default Worker runtime: %s\" ;;\n \"worker_runtime.title_short.zh\") text=\"默认 Worker 运行时\" ;;\n \"worker_runtime.title_short.en\") text=\"Default Worker Runtime\" ;;\n \"manager_runtime.title.zh\") text=\"--- Manager 运行时 ---\" ;;\n \"manager_runtime.title.en\") text=\"--- Manager Runtime ---\" ;;\n \"manager_runtime.openclaw.zh\") text=\"OpenClaw\" ;;\n \"manager_runtime.openclaw.en\") text=\"OpenClaw\" ;;\n \"manager_runtime.copaw.zh\") text=\"CoPaw\" ;;\n \"manager_runtime.copaw.en\") text=\"CoPaw\" ;;\n \"manager_runtime.choice.zh\") text=\"请选择 [1/2]\" ;;\n \"manager_runtime.choice.en\") text=\"Enter choice [1/2]\" ;;\n \"manager_runtime.selected.zh\") text=\"Manager 运行时: %s\" ;;\n \"manager_runtime.selected.en\") text=\"Manager runtime: %s\" ;;\n \"manager_runtime.title_short.zh\") text=\"Manager 运行时\" ;;\n \"manager_runtime.title_short.en\") text=\"Manager Runtime\" ;;\n # --- Secrets and config ---\n \"install.generating_secrets.zh\") text=\"正在生成密钥...\" ;;\n \"install.generating_secrets.en\") text=\"Generating secrets...\" ;;\n \"install.config_saved.zh\") text=\"配置已保存到 %s\" ;;\n \"install.config_saved.en\") text=\"Configuration saved to %s\" ;;\n # --- Container runtime socket ---\n \"install.socket_detected.zh\") text=\"容器运行时 socket: %s(已启用直接创建 Worker)\" ;;\n \"install.socket_detected.en\") text=\"Container runtime socket: %s (direct Worker creation enabled)\" ;;\n \"install.socket_not_found.zh\") text=\"未找到容器运行时 socket(Manager 无法直接创建 Worker 容器,需要你手动执行 docker 命令创建)\" ;;\n \"install.socket_not_found.en\") text=\"No container runtime socket found (Manager cannot create Worker containers directly, you will need to create them manually using docker commands)\" ;;\n \"install.socket_confirm.title.zh\") text=\"⚠️ 未检测到容器运行时 Socket\" ;;\n \"install.socket_confirm.title.en\") text=\"⚠️ Container Runtime Socket Not Detected\" ;;\n \"install.socket_confirm.message.zh\") text=\"未找到 Docker/Podman socket,Manager 将无法自动创建 Worker 容器。\\n你需要手动执行 docker run 命令来创建 Worker。\\n\\n是否继续安装?\" ;;\n \"install.socket_confirm.message.en\") text=\"Docker/Podman socket not found. Manager will not be able to create Worker containers automatically.\\nYou will need to manually run docker commands to create Workers.\\n\\nContinue installation?\" ;;\n \"install.socket_confirm.prompt.zh\") text=\"继续安装? [y/N]: \" ;;\n \"install.socket_confirm.prompt.en\") text=\"Continue? [y/N]: \" ;;\n \"install.socket_confirm.cancelled.zh\") text=\"安装已取消。如需启用 Worker 自动创建,请确保 Docker/Podman 正在运行,然后重新运行安装脚本。\" ;;\n \"install.socket_confirm.cancelled.en\") text=\"Installation cancelled. To enable automatic Worker creation, ensure Docker/Podman is running and re-run the installer.\" ;;\n # --- Container management ---\n \"install.removing_existing.zh\") text=\"正在移除现有 agentteams-manager 容器...\" ;;\n \"install.removing_existing.en\") text=\"Removing existing agentteams-manager container...\" ;;\n # --- Matrix E2EE ---\n \"matrix_e2ee.title.zh\") text=\"--- Matrix 端到端加密(E2EE)---\" ;;\n \"matrix_e2ee.title.en\") text=\"--- Matrix End-to-End Encryption (E2EE) ---\" ;;\n \"matrix_e2ee.desc.zh\") text=\"E2EE 会对 Manager 与 Worker 之间的 Matrix 消息进行端到端加密。\\n 启用后,即使 Matrix 服务器被入侵,消息内容也无法被窃取。\\n 但 E2EE 会增加首次握手耗时,且要求所有 Agent 都支持 matrix-sdk-crypto。\\n 如果不确定,建议保持禁用。\\n ⚠ 注意:禁用 E2EE 后,请勿在 Element 上创建默认启用加密的 Private 房间,\\n 否则 Agent 将无法读取该房间中的加密消息。请改用 Public 房间或关闭房间加密。\" ;;\n \"matrix_e2ee.desc.en\") text=\"E2EE encrypts Matrix messages between Manager and Workers end-to-end.\\n When enabled, message content stays private even if the Matrix server is compromised.\\n However, E2EE adds overhead to the initial handshake and requires all Agents\\n to support matrix-sdk-crypto. If unsure, keep it disabled.\\n ⚠ Note: When E2EE is disabled, do NOT create Private rooms in Element (which\\n enable encryption by default) — Agents cannot read encrypted messages without\\n E2EE support. Use Public rooms or turn off room encryption instead.\" ;;\n \"matrix_e2ee.enable.zh\") text=\"启用 E2EE\" ;;\n \"matrix_e2ee.enable.en\") text=\"Enable E2EE\" ;;\n \"matrix_e2ee.disable.zh\") text=\"禁用 E2EE(推荐)\" ;;\n \"matrix_e2ee.disable.en\") text=\"Disable E2EE (recommended)\" ;;\n \"matrix_e2ee.choice.zh\") text=\"请选择 [1/2]\" ;;\n \"matrix_e2ee.choice.en\") text=\"Enter choice [1/2]\" ;;\n \"matrix_e2ee.selected_enabled.zh\") text=\"Matrix E2EE: 已启用\" ;;\n \"matrix_e2ee.selected_enabled.en\") text=\"Matrix E2EE: enabled\" ;;\n \"matrix_e2ee.selected_disabled.zh\") text=\"Matrix E2EE: 已禁用(默认)\" ;;\n \"matrix_e2ee.selected_disabled.en\") text=\"Matrix E2EE: disabled (default)\" ;;\n \"matrix_e2ee.title_short.zh\") text=\"Matrix E2EE\" ;;\n \"matrix_e2ee.title_short.en\") text=\"Matrix E2EE\" ;;\n \"matrix_e2ee.val_enabled.zh\") text=\"已启用\" ;;\n \"matrix_e2ee.val_enabled.en\") text=\"enabled\" ;;\n \"matrix_e2ee.val_disabled.zh\") text=\"已禁用\" ;;\n \"matrix_e2ee.val_disabled.en\") text=\"disabled\" ;;\n # --- Docker API proxy ---\n \"docker_proxy.title.zh\") text=\"--- Docker API 安全代理 ---\" ;;\n \"docker_proxy.title.en\") text=\"--- Docker API Security Proxy ---\" ;;\n \"docker_proxy.desc.zh\") text=\"Docker API 代理可防止 AI Agent 通过 Docker API 越狱访问宿主机。\\n 启用后,Manager 不再直接持有 Docker socket,所有容器操作经过安全校验。\" ;;\n \"docker_proxy.desc.en\") text=\"Docker API proxy prevents AI Agents from escaping via Docker API to access the host.\\n When enabled, Manager no longer has direct Docker socket access; all container operations go through security validation.\" ;;\n \"docker_proxy.enable.zh\") text=\"启用(推荐)\" ;;\n \"docker_proxy.enable.en\") text=\"Enable (recommended)\" ;;\n \"docker_proxy.disable.zh\") text=\"禁用(直接挂载 Docker socket)\" ;;\n \"docker_proxy.disable.en\") text=\"Disable (mount Docker socket directly)\" ;;\n \"docker_proxy.choice.zh\") text=\"请选择 [1/2]\" ;;\n \"docker_proxy.choice.en\") text=\"Enter choice [1/2]\" ;;\n \"docker_proxy.selected_enabled.zh\") text=\"Docker API 代理: 已启用\" ;;\n \"docker_proxy.selected_enabled.en\") text=\"Docker API proxy: enabled\" ;;\n \"docker_proxy.selected_disabled.zh\") text=\"Docker API 代理: 已禁用\" ;;\n \"docker_proxy.selected_disabled.en\") text=\"Docker API proxy: disabled\" ;;\n \"docker_proxy.title_short.zh\") text=\"Docker API 代理\" ;;\n \"docker_proxy.title_short.en\") text=\"Docker API Proxy\" ;;\n \"docker_proxy.val_enabled.zh\") text=\"已启用\" ;;\n \"docker_proxy.val_enabled.en\") text=\"enabled\" ;;\n \"docker_proxy.val_disabled.zh\") text=\"已禁用\" ;;\n \"docker_proxy.val_disabled.en\") text=\"disabled\" ;;\n \"docker_proxy.registries_desc.zh\") text=\"默认放行的镜像来源:本地镜像、localhost、Higress 仓库(所有 region)。\\n 如需放行其他镜像仓库,请输入逗号分隔的地址前缀。\\n 示例: ghcr.io/myorg,registry.example.com/team\" ;;\n \"docker_proxy.registries_desc.en\") text=\"Default allowed image sources: local images, localhost, Higress registries (all regions).\\n To allow additional image sources, enter comma-separated address prefixes.\\n Example: ghcr.io/myorg,registry.example.com/team\" ;;\n \"docker_proxy.registries_prompt.zh\") text=\"额外放行的镜像来源(按回车跳过)\" ;;\n \"docker_proxy.registries_prompt.en\") text=\"Additional allowed image sources (press Enter to skip)\" ;;\n \"docker_proxy.registries_label.zh\") text=\"额外放行的镜像来源\" ;;\n \"docker_proxy.registries_label.en\") text=\"Additional allowed image sources\" ;;\n # --- Podman Autostart ---\n \"podman.autostart.title.zh\") text=\"--- Podman 驻留与开机自启 ---\" ;;\n \"podman.autostart.title.en\") text=\"--- Podman Linger & Autostart ---\" ;;\n \"podman.autostart.desc.zh\") text=\"配置为 systemd 服务,支持开机自动启动及后台驻留。\" ;;\n \"podman.autostart.desc.en\") text=\"Configure as a systemd service for automatic startup on boot.\" ;;\n \"podman.autostart.enable.zh\") text=\"启用(推荐)\" ;;\n \"podman.autostart.enable.en\") text=\"Enable (recommended)\" ;;\n \"podman.autostart.disable.zh\") text=\"不启用(容器随当前会话关闭而停止)\" ;;\n \"podman.autostart.disable.en\") text=\"Disable (containers stop when session ends)\" ;;\n \"podman.autostart.choice.zh\") text=\"请选择 [1/2]\" ;;\n \"podman.autostart.choice.en\") text=\"Enter choice [1/2]\" ;;\n \"podman.autostart.selected_enabled.zh\") text=\"Podman 开机自启: 已启用\" ;;\n \"podman.autostart.selected_enabled.en\") text=\"Podman Autostart: enabled\" ;;\n \"podman.autostart.selected_disabled.zh\") text=\"Podman 开机自启: 已禁用\" ;;\n \"podman.autostart.selected_disabled.en\") text=\"Podman Autostart: disabled\" ;;\n \"podman.autostart.title_short.zh\") text=\"Podman 开机自启\" ;;\n \"podman.autostart.title_short.en\") text=\"Podman Autostart\" ;;\n \"podman.autostart.val_enabled.zh\") text=\"已启用\" ;;\n \"podman.autostart.val_enabled.en\") text=\"enabled\" ;;\n \"podman.autostart.val_disabled.zh\") text=\"已禁用\" ;;\n \"podman.autostart.val_disabled.en\") text=\"disabled\" ;;\n \"install.podman.autostart_title.zh\") text=\"正在配置 Podman 容器开机自启...\" ;;\n \"install.podman.autostart_title.en\") text=\"Configuring Podman container autostart...\" ;;\n \"install.podman.linger_enable.zh\") text=\"尝试启用 systemd linger (如有提示请在此输入 sudo 密码)...\" ;;\n \"install.podman.linger_enable.en\") text=\"Attempting to enable systemd linger (enter sudo password if prompted)...\" ;;\n \"install.podman.linger_warn.zh\") text=\"警告: 无法自动启用 linger。驻留可能失败,请手动执行: sudo loginctl enable-linger \\$(whoami)\" ;;\n \"install.podman.linger_warn.en\") text=\"WARNING: Failed to auto-enable linger. Please run manually: sudo loginctl enable-linger \\$(whoami)\" ;;\n \"install.podman.root_setup.zh\") text=\"检测到 root 用户。正在配置系统级 agentteams-podman-restart 服务...\" ;;\n \"install.podman.root_setup.en\") text=\"Root user detected. Configuring system-wide agentteams-podman-restart service...\" ;;\n \"install.podman.root_success.zh\") text=\"✅ 系统级 agentteams-podman-restart 服务已成功启用并运行。\" ;;\n \"install.podman.root_success.en\") text=\"✅ System-wide agentteams-podman-restart service successfully enabled and running.\" ;;\n \"install.podman.root_fail.zh\") text=\"⚠️ 系统级 agentteams-podman-restart 服务启用失败,请稍后手动检查。\" ;;\n \"install.podman.root_fail.en\") text=\"⚠️ Failed to enable system-wide agentteams-podman-restart service. Please check manually later.\" ;;\n \"install.podman.user_setup.zh\") text=\"检测到普通用户 (%s)。正在配置用户级 agentteams-podman-restart 服务...\" ;;\n \"install.podman.user_setup.en\") text=\"Non-root user (%s) detected. Configuring user-level agentteams-podman-restart service...\" ;;\n \"install.podman.user_success.zh\") text=\"✅ 用户级 agentteams-podman-restart 服务已成功启用并运行。\" ;;\n \"install.podman.user_success.en\") text=\"✅ User-level agentteams-podman-restart service successfully enabled and running.\" ;;\n \"install.podman.user_fail.zh\") text=\"⚠️ 用户级 agentteams-podman-restart 服务启用失败。\\n提示:这通常是因为缺少 XDG_RUNTIME_DIR 或 dbus 没有运行。您可以尝试手动执行: systemctl --user enable --now agentteams-podman-restart.service\" ;;\n \"install.podman.user_fail.en\") text=\"⚠️ Failed to enable user-level agentteams-podman-restart service.\\nHint: This is usually due to missing XDG_RUNTIME_DIR or dbus not running. Try manually: systemctl --user enable --now agentteams-podman-restart.service\" ;;\n \"install.podman.success.zh\") text=\"Podman 开机自启配置完成。\" ;;\n \"install.podman.success.en\") text=\"Podman autostart successfully configured.\" ;;\n # --- Worker idle timeout ---\n \"idle_timeout.prompt.zh\") text=\"Worker 空闲自动停止超时(分钟)[720]\" ;;\n \"idle_timeout.prompt.en\") text=\"Worker idle auto-stop timeout in minutes [720]\" ;;\n \"idle_timeout.selected.zh\") text=\"Worker 空闲超时: %s 分钟\" ;;\n \"idle_timeout.selected.en\") text=\"Worker idle timeout: %s minutes\" ;;\n \"idle_timeout.label.zh\") text=\"Worker 空闲超时(分钟)\" ;;\n \"idle_timeout.label.en\") text=\"Worker idle timeout (min)\" ;;\n # --- YOLO mode ---\n \"install.yolo.zh\") text=\"YOLO 模式已启用(自主决策,无交互提示)\" ;;\n \"install.yolo.en\") text=\"YOLO mode enabled (autonomous decisions, no interactive prompts)\" ;;\n # --- Image pulling ---\n \"install.image.exists.zh\") text=\"Manager 镜像已存在: %s\" ;;\n \"install.image.exists.en\") text=\"Manager image already exists locally: %s\" ;;\n \"install.image.pulling_manager.zh\") text=\"正在拉取 Manager 镜像: %s\" ;;\n \"install.image.pulling_manager.en\") text=\"Pulling Manager image: %s\" ;;\n \"install.image.worker_exists.zh\") text=\"Worker 镜像已存在: %s\" ;;\n \"install.image.worker_exists.en\") text=\"Worker image already exists locally: %s\" ;;\n \"install.image.pulling_worker.zh\") text=\"正在拉取 Worker 镜像: %s\" ;;\n \"install.image.pulling_worker.en\") text=\"Pulling Worker image: %s\" ;;\n # --- Starting container ---\n \"install.starting_manager.zh\") text=\"正在启动 Manager 容器...\" ;;\n \"install.starting_manager.en\") text=\"Starting Manager container...\" ;;\n # --- Wait for Manager ready ---\n \"install.wait_ready.zh\") text=\"等待 Manager Agent 就绪(超时: %ss)...\" ;;\n \"install.wait_ready.en\") text=\"Waiting for Manager agent to be ready (timeout: %ss)...\" ;;\n \"install.wait_ready.ok.zh\") text=\"Manager Agent 已就绪!\" ;;\n \"install.wait_ready.ok.en\") text=\"Manager agent is ready!\" ;;\n \"install.wait_ready.waiting.zh\") text=\"等待中... (%ds/%ds)\" ;;\n \"install.wait_ready.waiting.en\") text=\"Waiting... (%ds/%ds)\" ;;\n \"install.wait_ready.timeout.zh\") text=\"Manager Agent 在 %ss 内未就绪。请检查: docker logs %s\" ;;\n \"install.wait_ready.timeout.en\") text=\"Manager agent did not become ready within %ss. Check: docker logs %s\" ;;\n # --- Wait for Matrix ready ---\n \"install.wait_matrix.zh\") text=\"等待 Matrix 服务就绪(超时: %ss)...\" ;;\n \"install.wait_matrix.en\") text=\"Waiting for Matrix server to be ready (timeout: %ss)...\" ;;\n \"install.wait_matrix.ok.zh\") text=\"Matrix 服务已就绪!\" ;;\n \"install.wait_matrix.ok.en\") text=\"Matrix server is ready!\" ;;\n \"install.wait_matrix.waiting.zh\") text=\"等待 Matrix 中... (%ds/%ds)\" ;;\n \"install.wait_matrix.waiting.en\") text=\"Waiting for Matrix... (%ds/%ds)\" ;;\n \"install.wait_matrix.timeout.zh\") text=\"Matrix 服务在 %ss 内未就绪。请检查: docker logs %s\" ;;\n \"install.wait_matrix.timeout.en\") text=\"Matrix server did not become ready within %ss. Check: docker logs %s\" ;;\n # --- OpenAI-compatible connectivity test ---\n \"llm.openai.test.testing.zh\") text=\"正在测试 API 联通性...\" ;;\n \"llm.openai.test.testing.en\") text=\"Testing API connectivity...\" ;;\n \"llm.openai.test.ok.zh\") text=\"✅ API 联通性测试通过\" ;;\n \"llm.openai.test.ok.en\") text=\"✅ API connectivity test passed\" ;;\n \"llm.openai.test.fail.zh\") text=\"⚠️ API 联通性测试失败(HTTP %s)。响应内容:\\n%s\\n请根据以上错误信息联系您的模型服务商解决。\" ;;\n \"llm.openai.test.fail.en\") text=\"⚠️ API connectivity test failed (HTTP %s). Response body:\\n%s\\nPlease contact your model provider to resolve the issue.\" ;;\n \"llm.openai.test.fail.tokenplan.zh\") text=\"⚠️ 提示: 请确认 API Key 有效且已开通通义 Token 套餐。文档: https://help.aliyun.com/zh/model-studio/token-plan-quickstart\" ;;\n \"llm.openai.test.fail.tokenplan.en\") text=\"⚠️ Hint: Verify your Token Plan API key and compatible-mode access. Docs: https://help.aliyun.com/zh/model-studio/token-plan-quickstart\" ;;\n \"llm.openai.test.fail.codingplan.zh\") text=\"⚠️ 提示: 请确认 API Key 有效且已开通通义 Token 套餐。文档: https://help.aliyun.com/zh/model-studio/token-plan-quickstart\" ;;\n \"llm.openai.test.fail.codingplan.en\") text=\"⚠️ Hint: Verify your DASHSCOPE_API_KEY for Qwen Cloud. API keys: https://home.qwencloud.com/api-keys Docs: https://docs.qwencloud.com/\" ;;\n \"llm.openai.test.fail.codingplan_legacy.zh\") text=\"⚠️ 提示: 请确认 API Key 有效且 Coding 套餐接口可用。文档: https://help.aliyun.com/zh/model-studio/get-api-key\" ;;\n \"llm.openai.test.fail.codingplan_legacy.en\") text=\"⚠️ Hint: Verify your DashScope API key and Coding Plan access. Docs: https://help.aliyun.com/zh/model-studio/get-api-key\" ;;\n \"llm.openai.test.no_curl.zh\") text=\"⚠️ 未找到 curl,跳过 API 联通性测试\" ;;\n \"llm.openai.test.no_curl.en\") text=\"⚠️ curl not found, skipping API connectivity test\" ;;\n \"llm.openai.test.confirm.zh\") text=\"是否仍要继续安装?[y/N/b] \" ;;\n \"llm.openai.test.confirm.en\") text=\"Continue with installation anyway? [y/N/b] \" ;;\n \"llm.embedding.title.zh\") text=\"📦 记忆搜索配置\" ;;\n \"llm.embedding.title.en\") text=\"📦 Memory Search Configuration\" ;;\n \"llm.embedding.hint.zh\") text=\" Embedding 模型可提升记忆搜索质量(语义匹配)。不启用也可正常使用记忆功能(关键词匹配)。\" ;;\n \"llm.embedding.hint.en\") text=\" Embedding model improves memory search quality (semantic matching). Memory still works without it (keyword matching).\" ;;\n \"llm.embedding.option.default.zh\") text=\" 1) text-embedding-v4(推荐)\" ;;\n \"llm.embedding.option.default.en\") text=\" 1) text-embedding-v4 (Recommended)\" ;;\n \"llm.embedding.option.custom.zh\") text=\" 2) 自定义 Embedding 模型\" ;;\n \"llm.embedding.option.custom.en\") text=\" 2) Custom embedding model\" ;;\n \"llm.embedding.option.disable.zh\") text=\" 3) 不启用\" ;;\n \"llm.embedding.option.disable.en\") text=\" 3) Do not enable\" ;;\n \"llm.embedding.select.zh\") text=\"选择\" ;;\n \"llm.embedding.select.en\") text=\"Select\" ;;\n \"llm.embedding.custom_prompt.zh\") text=\" Embedding 模型名称\" ;;\n \"llm.embedding.custom_prompt.en\") text=\" Embedding model name\" ;;\n \"llm.embedding.test.testing.zh\") text=\"正在测试 Embedding API 联通性...\" ;;\n \"llm.embedding.test.testing.en\") text=\"Testing Embedding API connectivity...\" ;;\n \"llm.embedding.test.ok.zh\") text=\"✅ Embedding API 联通性测试通过\" ;;\n \"llm.embedding.test.ok.en\") text=\"✅ Embedding API connectivity test passed\" ;;\n \"llm.embedding.test.fail.zh\") text=\"⚠️ Embedding API 测试失败(HTTP %s)。响应: %s\" ;;\n \"llm.embedding.test.fail.en\") text=\"⚠️ Embedding API test failed (HTTP %s). Response: %s\" ;;\n \"llm.embedding.auto_disabled.zh\") text=\"⚠️ Embedding 已自动禁用,记忆搜索将使用关键词匹配。您可以稍后在 agentteams-manager.env 中设置 AGENTTEAMS_EMBEDDING_MODEL 启用。\" ;;\n \"llm.embedding.auto_disabled.en\") text=\"⚠️ Embedding auto-disabled. Memory search will use keyword matching. You can enable it later in agentteams-manager.env by setting AGENTTEAMS_EMBEDDING_MODEL.\" ;;\n \"llm.embedding.disabled.zh\") text=\"ℹ️ Embedding 已禁用,记忆搜索将使用关键词匹配。\" ;;\n \"llm.embedding.disabled.en\") text=\"ℹ️ Embedding disabled. Memory search will use keyword matching.\" ;;\n \"llm.openai.test.aborted.zh\") text=\"安装已中止。\" ;;\n \"llm.openai.test.aborted.en\") text=\"Installation aborted.\" ;;\n \"nav.back_hint.zh\") text=\"(输入 b 返回上一步)\" ;;\n \"nav.back_hint.en\") text=\"(enter b to go back)\" ;;\n # --- OpenAI-compatible provider creation ---\n \"install.openai_compat.missing.zh\") text=\"警告: OpenAI Base URL 或 API Key 未设置,跳过提供商创建\" ;;\n \"install.openai_compat.missing.en\") text=\"WARNING: OpenAI Base URL or API Key not set, skipping provider creation\" ;;\n \"install.openai_compat.creating.zh\") text=\"正在创建 OpenAI 兼容提供商...\" ;;\n \"install.openai_compat.creating.en\") text=\"Creating OpenAI-compatible provider...\" ;;\n \"install.openai_compat.domain.zh\") text=\" 域名: %s\" ;;\n \"install.openai_compat.domain.en\") text=\" Domain: %s\" ;;\n \"install.openai_compat.port.zh\") text=\" 端口: %s\" ;;\n \"install.openai_compat.port.en\") text=\" Port: %s\" ;;\n \"install.openai_compat.protocol.zh\") text=\" 协议: %s\" ;;\n \"install.openai_compat.protocol.en\") text=\" Protocol: %s\" ;;\n \"install.openai_compat.service_fail.zh\") text=\"警告: 创建 DNS 服务源失败(可能已存在)\" ;;\n \"install.openai_compat.service_fail.en\") text=\"WARNING: Failed to create DNS service source (may already exist)\" ;;\n \"install.openai_compat.provider_fail.zh\") text=\"警告: 创建 AI 提供商失败(可能已存在)\" ;;\n \"install.openai_compat.provider_fail.en\") text=\"WARNING: Failed to create AI provider (may already exist)\" ;;\n \"install.openai_compat.success.zh\") text=\"OpenAI 兼容提供商创建成功\" ;;\n \"install.openai_compat.success.en\") text=\"OpenAI-compatible provider created successfully\" ;;\n # --- Welcome message ---\n \"install.welcome_msg.soul_configured.zh\") text=\"Soul 已配置(找到 soul-configured 标记),跳过 onboarding 消息\" ;;\n \"install.welcome_msg.soul_configured.en\") text=\"Soul already configured (soul-configured marker found), skipping onboarding message\" ;;\n \"install.welcome_msg.logging_in.zh\") text=\"正在以 %s 身份登录以发送欢迎消息...\" ;;\n \"install.welcome_msg.logging_in.en\") text=\"Logging in as %s to send welcome message...\" ;;\n \"install.welcome_msg.login_failed.zh\") text=\"警告: 以 %s 身份登录失败,跳过欢迎消息\" ;;\n \"install.welcome_msg.login_failed.en\") text=\"WARNING: Failed to login as %s, skipping welcome message\" ;;\n \"install.welcome_msg.finding_room.zh\") text=\"正在查找与 Manager 的 DM 房间...\" ;;\n \"install.welcome_msg.finding_room.en\") text=\"Finding DM room with Manager...\" ;;\n \"install.welcome_msg.creating_room.zh\") text=\"正在创建与 Manager 的 DM 房间...\" ;;\n \"install.welcome_msg.creating_room.en\") text=\"Creating DM room with Manager...\" ;;\n \"install.welcome_msg.no_room.zh\") text=\"警告: 无法找到或创建与 Manager 的 DM 房间\" ;;\n \"install.welcome_msg.no_room.en\") text=\"WARNING: Could not find or create DM room with Manager\" ;;\n \"install.welcome_msg.waiting_join.zh\") text=\"等待 Manager 加入房间...\" ;;\n \"install.welcome_msg.waiting_join.en\") text=\"Waiting for Manager to join the room...\" ;;\n \"install.welcome_msg.sending.zh\") text=\"正在向 Manager 发送欢迎消息...\" ;;\n \"install.welcome_msg.sending.en\") text=\"Sending welcome message to Manager...\" ;;\n \"install.welcome_msg.send_failed.zh\") text=\"警告: 发送欢迎消息失败\" ;;\n \"install.welcome_msg.send_failed.en\") text=\"WARNING: Failed to send welcome message\" ;;\n \"install.welcome_msg.sent.zh\") text=\"欢迎消息已发送给 Manager\" ;;\n \"install.welcome_msg.sent.en\") text=\"Welcome message sent to Manager\" ;;\n \"install.welcome_msg.waiting.zh\") text=\"等待 Manager 发送欢迎消息(Higress 路由授权 + LLM 探活,约 45-90s)...\" ;;\n \"install.welcome_msg.waiting.en\") text=\"Waiting for Manager to send the welcome message (Higress route auth + LLM probe, ~45-90s)...\" ;;\n \"install.welcome_msg.confirmed.zh\") text=\"Manager 已确认发送欢迎消息(status.welcomeSent=true,用时 %ss)\" ;;\n \"install.welcome_msg.confirmed.en\") text=\"Manager confirmed welcome message sent (status.welcomeSent=true, %ss elapsed)\" ;;\n \"install.welcome_msg.timeout.zh\") text=\"警告: 在 %ss 内未观察到 Manager 发送欢迎消息(status.welcomeSent=true)。安装仍然成功,所有服务已就绪——可继续按下方提示登录 Element Web。\" ;;\n \"install.welcome_msg.timeout.en\") text=\"WARNING: Did not observe the Manager sending its welcome message (status.welcomeSent=true) within %ss. Installation is still successful, all services are up — continue with the Element Web instructions below.\" ;;\n \"install.welcome_msg.timeout_hint.zh\") text=\"手动触发 onboarding: 登录 Element Web → 打开与 Manager 的 DM 房间 → 发送任意一句话(例如 \\\"hi\\\"),Manager 会接管对话并开始引导。\" ;;\n \"install.welcome_msg.timeout_hint.en\") text=\"Manual onboarding: log in to Element Web → open the DM with the Manager → send any message (e.g. \\\"hi\\\") and the Manager will take over and start the guided setup.\" ;;\n \"install.welcome_msg.timeout_inspect.zh\") text=\"排查命令: docker exec agentteams-controller agt get managers default\" ;;\n \"install.welcome_msg.timeout_inspect.en\") text=\"Inspect status: docker exec agentteams-controller agt get managers default\" ;;\n \"install.welcome_msg.poll_unavailable.zh\") text=\"提示: agentteams-manager 内未找到 AgentTeams CLI,跳过 welcome 等待(旧镜像?)\" ;;\n \"install.welcome_msg.poll_unavailable.en\") text=\"Note: AgentTeams CLI not found inside agentteams-manager; skipping welcome wait (old image?)\" ;;\n # --- Dashboard wizard ---\n \"dash.prompt.zh\") text=\"是否安装 agentteams-dashboard 管理面板?\" ;;\n \"dash.prompt.en\") text=\"Install agentteams-dashboard management UI?\" ;;\n \"dash.version_prompt.zh\") text=\"Dashboard 版本\" ;;\n \"dash.version_prompt.en\") text=\"Dashboard version\" ;;\n \"dash.port_prompt.zh\") text=\"Dashboard 端口号\" ;;\n \"dash.port_prompt.en\") text=\"Dashboard port\" ;;\n \"dash.image_prompt.zh\") text=\"Dashboard 镜像\" ;;\n \"dash.image_prompt.en\") text=\"Dashboard image\" ;;\n \"dash.gateway_prompt.zh\") text=\"Higress Console URL (用于共享登录)\" ;;\n \"dash.gateway_prompt.en\") text=\"Higress Console URL (for shared login)\" ;;\n \"dash.skip.zh\") text=\"跳过 Dashboard 安装\" ;;\n \"dash.skip.en\") text=\"Skipping Dashboard installation\" ;;\n \"dash.auto_url.zh\") text=\"自动检测到 Higress Console URL\" ;;\n \"dash.auto_url.en\") text=\"Auto-detected Higress Console URL\" ;;\n \"dash.auto_url_fail.zh\") text=\"无法自动检测 Higress Console URL,请手动输入\" ;;\n \"dash.auto_url_fail.en\") text=\"Cannot auto-detect Higress Console URL, please enter manually\" ;;\n \"dash.summary.zh\") text=\"Dashboard 配置: 端口=%s 镜像=%s\" ;;\n \"dash.summary.en\") text=\"Dashboard config: port=%s image=%s\" ;;\n # --- Dashboard success ---\n \"success.dashboard.zh\") text=\" agentteams-dashboard 管理面板: http://localhost:%s/\" ;;\n \"success.dashboard.en\") text=\" agentteams-dashboard: http://localhost:%s/\" ;;\n # --- Final output panel ---\n \"success.title.zh\") text=\"=== AgentTeams Manager 已启动!===\" ;;\n \"success.title.en\") text=\"=== AgentTeams Manager Started! ===\" ;;\n \"success.domains_configured.zh\") text=\"以下域名已配置解析到 127.0.0.1:\" ;;\n \"success.domains_configured.en\") text=\"The following domains are configured to resolve to 127.0.0.1:\" ;;\n \"success.open_url.zh\") text=\" ★ 在浏览器中打开以下 URL 开始使用: ★\" ;;\n \"success.open_url.en\") text=\" ★ Open the following URL in your browser to start: ★\" ;;\n \"success.login_with.zh\") text=\" 登录信息:\" ;;\n \"success.login_with.en\") text=\" Login with:\" ;;\n \"success.username.zh\") text=\" 用户名: %s\" ;;\n \"success.username.en\") text=\" Username: %s\" ;;\n \"success.password.zh\") text=\" 密码: %s\" ;;\n \"success.password.en\") text=\" Password: %s\" ;;\n \"success.after_login.zh\") text=\" 登录后,开始与 Manager 聊天!\" ;;\n \"success.after_login.en\") text=\" After login, start chatting with the Manager!\" ;;\n \"success.tell_it.zh\") text=\" 告诉它: \\\"创建一个名为 alice 的前端开发 Worker\\\"\" ;;\n \"success.tell_it.en\") text=\" Tell it: \\\"Create a Worker named alice for frontend dev\\\"\" ;;\n \"success.manager_auto.zh\") text=\" Manager 会自动处理一切。\" ;;\n \"success.manager_auto.en\") text=\" The Manager will handle everything automatically.\" ;;\n \"success.mobile_title.zh\") text=\" 📱 移动端访问(FluffyChat / Element Mobile):\" ;;\n \"success.mobile_title.en\") text=\" 📱 Mobile access (FluffyChat / Element Mobile):\" ;;\n \"success.mobile_step1.zh\") text=\" 1. 在手机上下载 FluffyChat 或 Element\" ;;\n \"success.mobile_step1.en\") text=\" 1. Download FluffyChat or Element on your phone\" ;;\n \"success.mobile_step2.zh\") text=\" 2. 设置 homeserver 为: %s\" ;;\n \"success.mobile_step2.en\") text=\" 2. Set homeserver to: %s\" ;;\n \"success.mobile_step2_noip.zh\") text=\" 2. 设置 homeserver 为: http://<本机局域网IP>:%s\" ;;\n \"success.mobile_step2_noip.en\") text=\" 2. Set homeserver to: http://<this-machine-LAN-IP>:%s\" ;;\n \"success.mobile_noip_hint.zh\") text=\" (无法自动检测局域网 IP — 请使用 ifconfig / ip addr 查看)\" ;;\n \"success.mobile_noip_hint.en\") text=\" (Could not detect LAN IP automatically — check with: ifconfig / ip addr)\" ;;\n \"success.mobile_step3.zh\") text=\" 3. 登录信息:\" ;;\n \"success.mobile_step3.en\") text=\" 3. Login with:\" ;;\n \"success.mobile_username.zh\") text=\" 用户名: %s\" ;;\n \"success.mobile_username.en\") text=\" Username: %s\" ;;\n \"success.mobile_password.zh\") text=\" 密码: %s\" ;;\n \"success.mobile_password.en\") text=\" Password: %s\" ;;\n # --- Other consoles and tips ---\n \"success.other_consoles.zh\") text=\"--- 其他控制台 ---\" ;;\n \"success.other_consoles.en\") text=\"--- Other Consoles ---\" ;;\n \"success.higress_console.zh\") text=\" Higress 控制台: http://localhost:%s(用户名: %s / 密码: %s)\" ;;\n \"success.higress_console.en\") text=\" Higress Console: http://localhost:%s (Username: %s / Password: %s)\" ;;\n \"success.manager_console.zh\") text=\" Manager 控制台(本地): http://localhost:%s(无需登录)\" ;;\n \"success.manager_console.en\") text=\" Manager Console (local): http://localhost:%s (no login required)\" ;;\n \"success.manager_console_gateway.zh\") text=\" Manager 控制台(网关): http://console-local.agentteams.io(用户名: %s / 密码: %s)\" ;;\n \"success.manager_console_gateway.en\") text=\" Manager Console (gateway): http://console-local.agentteams.io (Username: %s / Password: %s)\" ;;\n \"success.copaw_console.zh\") text=\" CoPaw App API: http://localhost:%s(无需登录)\" ;;\n \"success.copaw_console.en\") text=\" CoPaw App API: http://localhost:%s (no login required)\" ;;\n \"success.switch_llm.title.zh\") text=\"--- 切换 LLM 提供商 ---\" ;;\n \"success.switch_llm.title.en\") text=\"--- Switch LLM Providers ---\" ;;\n \"success.switch_llm.hint.zh\") text=\" 您可以通过 Higress 控制台切换到其他 LLM 提供商(OpenAI、Anthropic 等)。\" ;;\n \"success.switch_llm.hint.en\") text=\" You can switch to other LLM providers (OpenAI, Anthropic, etc.) via Higress Console.\" ;;\n \"success.switch_llm.docs.zh\") text=\" 详细说明请参阅:\" ;;\n \"success.switch_llm.docs.en\") text=\" For detailed instructions, see:\" ;;\n \"success.switch_llm.url.zh\") text=\" https://higress.ai/en/docs/ai/scene-guide/multi-proxy#console-configuration\" ;;\n \"success.switch_llm.url.en\") text=\" https://higress.ai/en/docs/ai/scene-guide/multi-proxy#console-configuration\" ;;\n \"success.tip.zh\") text=\"提示: 您也可以在聊天中让 Manager 为您配置 LLM 提供商。\" ;;\n \"success.tip.en\") text=\"Tip: You can also ask the Manager to configure LLM providers for you in the chat.\" ;;\n \"success.config_file.zh\") text=\"配置文件: %s\" ;;\n \"success.config_file.en\") text=\"Configuration file: %s\" ;;\n \"success.data_volume.zh\") text=\"数据卷: %s\" ;;\n \"success.data_volume.en\") text=\"Data volume: %s\" ;;\n \"success.workspace.zh\") text=\"Manager 工作空间: %s\" ;;\n \"success.workspace.en\") text=\"Manager workspace: %s\" ;;\n # --- Worker installation ---\n \"worker.resetting.zh\") text=\"正在重置 Worker: %s...\" ;;\n \"worker.resetting.en\") text=\"Resetting Worker: %s...\" ;;\n \"worker.exists.zh\") text=\"容器 '%s' 已存在。使用 --reset 重新创建。\" ;;\n \"worker.exists.en\") text=\"Container '%s' already exists. Use --reset to recreate.\" ;;\n \"worker.starting.zh\") text=\"正在启动 Worker: %s...\" ;;\n \"worker.starting.en\") text=\"Starting Worker: %s...\" ;;\n \"worker.skills_url.zh\") text=\" Skills API URL: %s\" ;;\n \"worker.skills_url.en\") text=\" Skills API URL: %s\" ;;\n \"worker.started.zh\") text=\"=== Worker %s 已启动!===\" ;;\n \"worker.started.en\") text=\"=== Worker %s Started! ===\" ;;\n \"worker.container.zh\") text=\"容器: %s\" ;;\n \"worker.container.en\") text=\"Container: %s\" ;;\n \"worker.view_logs.zh\") text=\"查看日志: docker logs -f %s\" ;;\n \"worker.view_logs.en\") text=\"View logs: docker logs -f %s\" ;;\n # --- Prompt function messages ---\n \"prompt.preset.zh\") text=\" %s = (已通过环境变量预设)\" ;;\n \"prompt.preset.en\") text=\" %s = (pre-set via env)\" ;;\n \"prompt.upgrade_keep.zh\") text=\" %s = %s(当前值,回车保留 / 输入新值覆盖)\" ;;\n \"prompt.upgrade_keep.en\") text=\" %s = %s (current value, press Enter to keep / type new value to change)\" ;;\n \"prompt.upgrade_keep_secret.zh\") text=\" %s = %s(当前值,回车保留 / 输入新值覆盖)\" ;;\n \"prompt.upgrade_keep_secret.en\") text=\" %s = %s (current value, press Enter to keep / type new value to change)\" ;;\n \"prompt.upgrade_empty.zh\") text=\" %s = (未设置,回车跳过 / 输入新值设置)\" ;;\n \"prompt.upgrade_empty.en\") text=\" %s = (not set, press Enter to skip / type new value to set)\" ;;\n \"prompt.default.zh\") text=\" %s = %s(默认)\" ;;\n \"prompt.default.en\") text=\" %s = %s (default)\" ;;\n \"prompt.required.zh\") text=\"%s 是必需的(在非交互模式下通过环境变量设置)\" ;;\n \"prompt.required.en\") text=\"%s is required (set via environment variable in non-interactive mode)\" ;;\n \"prompt.required_empty.zh\") text=\"%s 是必需的\" ;;\n \"prompt.required_empty.en\") text=\"%s is required\" ;;\n # --- Language switch prompt (bilingual by design) ---\n \"lang.detected.zh\") text=\"检测到语言 / Detected language: 中文\" ;;\n \"lang.detected.en\") text=\"检测到语言 / Detected language: English\" ;;\n \"lang.switch_title.zh\") text=\"切换语言 / Switch language:\" ;;\n \"lang.switch_title.en\") text=\"切换语言 / Switch language:\" ;;\n \"lang.option_zh.zh\") text=\" 1) 中文\" ;;\n \"lang.option_zh.en\") text=\" 1) 中文\" ;;\n \"lang.option_en.zh\") text=\" 2) English\" ;;\n \"lang.option_en.en\") text=\" 2) English\" ;;\n \"lang.prompt.zh\") text=\"请选择 / Enter choice\" ;;\n \"lang.prompt.en\") text=\"请选择 / Enter choice\" ;;\n # --- Uninstall messages ---\n \"uninstall.title.zh\") text=\"正在卸载 AgentTeams...\" ;;\n \"uninstall.title.en\") text=\"Uninstalling AgentTeams...\" ;;\n \"uninstall.stopping_manager.zh\") text=\"正在停止并移除 agentteams-manager...\" ;;\n \"uninstall.stopping_manager.en\") text=\"Stopping and removing agentteams-manager...\" ;;\n \"uninstall.stopping_workers.zh\") text=\"正在停止并移除 Worker 容器...\" ;;\n \"uninstall.stopping_workers.en\") text=\"Stopping and removing worker containers...\" ;;\n \"uninstall.removed.zh\") text=\" 已移除: %s\" ;;\n \"uninstall.removed.en\") text=\" Removed: %s\" ;;\n \"uninstall.removing_volume.zh\") text=\"正在移除 Docker 卷: %s\" ;;\n \"uninstall.removing_volume.en\") text=\"Removing Docker volume: %s\" ;;\n \"uninstall.removing_env.zh\") text=\"正在移除 env 文件: %s\" ;;\n \"uninstall.removing_env.en\") text=\"Removing env file: %s\" ;;\n \"uninstall.removing_proxy.zh\") text=\"正在停止并移除 Docker API 代理容器: agentteams-docker-proxy\" ;;\n \"uninstall.removing_proxy.en\") text=\"Stopping and removing Docker API proxy container: agentteams-docker-proxy\" ;;\n \"uninstall.stopping_controller.zh\") text=\"正在停止并移除 agentteams-controller (内嵌 Tuwunel/MinIO/Higress)...\" ;;\n \"uninstall.stopping_controller.en\") text=\"Stopping and removing agentteams-controller (embedded Tuwunel/MinIO/Higress)...\" ;;\n \"uninstall.removing_network.zh\") text=\"正在移除 Docker 网络: agentteams-net\" ;;\n \"uninstall.removing_network.en\") text=\"Removing Docker network: agentteams-net\" ;;\n \"uninstall.removing_workspace.zh\") text=\"正在移除工作空间目录: %s\" ;;\n \"uninstall.removing_workspace.en\") text=\"Removing workspace directory: %s\" ;;\n \"uninstall.removing_workspace_elevated.zh\") text=\" 工作空间包含 root 文件,通过容器清理...\" ;;\n \"uninstall.removing_workspace_elevated.en\") text=\" Workspace contains root-owned files, cleaning via container...\" ;;\n \"uninstall.removing_log.zh\") text=\"正在移除日志文件: %s\" ;;\n \"uninstall.removing_log.en\") text=\"Removing log file: %s\" ;;\n \"uninstall.done.zh\") text=\"AgentTeams 已卸载。\" ;;\n \"uninstall.done.en\") text=\"AgentTeams has been uninstalled.\" ;;\n # --- Error messages ---\n \"error.name_required.zh\") text=\"--name 是必需的\" ;;\n \"error.name_required.en\") text=\"--name is required\" ;;\n \"error.fs_required.zh\") text=\"--fs 是必需的\" ;;\n \"error.fs_required.en\") text=\"--fs is required\" ;;\n \"error.fs_key_required.zh\") text=\"--fs-key 是必需的\" ;;\n \"error.fs_key_required.en\") text=\"--fs-key is required\" ;;\n \"error.fs_secret_required.zh\") text=\"--fs-secret 是必需的\" ;;\n \"error.fs_secret_required.en\") text=\"--fs-secret is required\" ;;\n \"error.unknown_option.zh\") text=\"未知选项: %s\" ;;\n \"error.unknown_option.en\") text=\"Unknown option: %s\" ;;\n \"error.docker_not_found.zh\") text=\"未找到 docker 或 podman 命令。请先安装 Docker Desktop 或 Podman Desktop:\\n Docker Desktop: https://www.docker.com/products/docker-desktop/\\n Podman Desktop: https://podman-desktop.io/\" ;;\n \"error.docker_not_found.en\") text=\"docker or podman command not found. Please install Docker Desktop or Podman Desktop first:\\n Docker Desktop: https://www.docker.com/products/docker-desktop/\\n Podman Desktop: https://podman-desktop.io/\" ;;\n \"error.docker_not_running.zh\") text=\"Docker 未运行。请先启动 Docker Desktop 或 Podman Desktop。\" ;;\n \"error.docker_not_running.en\") text=\"Docker is not running. Please start Docker Desktop or Podman Desktop first.\" ;;\n # --- Fallback: try English for unknown lang ---\n *)\n case \"${key}.en\" in\n \"tz.warning.title.en\") text=\"Could not detect timezone automatically.\" ;;\n \"install.title.en\") text=\"=== AgentTeams Manager Installation ===\" ;;\n *) text=\"${key}\" ;;\n esac\n ;;\n esac\n if [ $# -gt 0 ]; then\n # shellcheck disable=SC2059\n printf \"${text}\\n\" \"$@\"\n else\n echo \"${text}\"\n fi\n}\n\n# ============================================================\n# Registry selection based on timezone\n# ============================================================\n\ndetect_registry() {\n local tz=\"${AGENTTEAMS_TIMEZONE}\"\n\n case \"${tz}\" in\n America/*)\n echo \"higress-registry.us-west-1.cr.aliyuncs.com\"\n ;;\n Asia/Singapore|Asia/Bangkok|Asia/Jakarta|Asia/Makassar|Asia/Jayapura|\\\n Asia/Kuala_Lumpur|Asia/Ho_Chi_Minh|Asia/Manila|Asia/Yangon|\\\n Asia/Vientiane|Asia/Phnom_Penh|Asia/Pontianak|Asia/Ujung_Pandang)\n echo \"higress-registry.ap-southeast-7.cr.aliyuncs.com\"\n ;;\n *)\n echo \"higress-registry.cn-hangzhou.cr.aliyuncs.com\"\n ;;\n esac\n}\n\nAGENTTEAMS_REGISTRY=\"${AGENTTEAMS_REGISTRY:-$(detect_registry)}\"\n# Backward compatibility: accept old env var names from previous versions\nAGENTTEAMS_INSTALL_CONTROLLER_IMAGE=\"${AGENTTEAMS_INSTALL_CONTROLLER_IMAGE:-${AGENTTEAMS_INSTALL_DOCKER_PROXY_IMAGE:-}}\"\n# Image variables are resolved after version selection in step_version().\n# These placeholders allow early code paths to reference them without errors.\nMANAGER_IMAGE=\"${AGENTTEAMS_INSTALL_MANAGER_IMAGE:-}\"\nMANAGER_COPAW_IMAGE=\"${AGENTTEAMS_INSTALL_MANAGER_COPAW_IMAGE:-}\"\nWORKER_IMAGE=\"${AGENTTEAMS_INSTALL_WORKER_IMAGE:-}\"\nCOPAW_WORKER_IMAGE=\"${AGENTTEAMS_INSTALL_COPAW_WORKER_IMAGE:-}\"\nQWENPAW_WORKER_IMAGE=\"${AGENTTEAMS_INSTALL_QWENPAW_WORKER_IMAGE:-}\"\nHERMES_WORKER_IMAGE=\"${AGENTTEAMS_INSTALL_HERMES_WORKER_IMAGE:-}\"\nCONTROLLER_IMAGE=\"${AGENTTEAMS_INSTALL_CONTROLLER_IMAGE:-}\"\n\nresolve_image_tags() {\n AGENTTEAMS_VERSION=\"$(_normalize_version \"${AGENTTEAMS_VERSION}\")\"\n MANAGER_IMAGE=\"${AGENTTEAMS_INSTALL_MANAGER_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-manager:${AGENTTEAMS_VERSION}}\"\n MANAGER_COPAW_IMAGE=\"${AGENTTEAMS_INSTALL_MANAGER_COPAW_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-manager-copaw:${AGENTTEAMS_VERSION}}\"\n WORKER_IMAGE=\"${AGENTTEAMS_INSTALL_WORKER_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-worker:${AGENTTEAMS_VERSION}}\"\n COPAW_WORKER_IMAGE=\"${AGENTTEAMS_INSTALL_COPAW_WORKER_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-copaw-worker:${AGENTTEAMS_VERSION}}\"\n QWENPAW_WORKER_IMAGE=\"${AGENTTEAMS_INSTALL_QWENPAW_WORKER_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-qwenpaw-worker:${AGENTTEAMS_VERSION}}\"\n HERMES_WORKER_IMAGE=\"${AGENTTEAMS_INSTALL_HERMES_WORKER_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-hermes-worker:${AGENTTEAMS_VERSION}}\"\n EMBEDDED_IMAGE=\"${AGENTTEAMS_INSTALL_EMBEDDED_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-embedded:${AGENTTEAMS_VERSION}}\"\n # CoPaw Worker introduced in v1.0.4; Hermes Worker introduced in v1.1.0\n if [ -z \"${AGENTTEAMS_INSTALL_COPAW_WORKER_IMAGE:-}\" ] && _ver_lt \"${AGENTTEAMS_VERSION}\" \"v1.0.4\"; then\n COPAW_WORKER_IMAGE=\"\"\n fi\n if [ -z \"${AGENTTEAMS_INSTALL_HERMES_WORKER_IMAGE:-}\" ] && _ver_lt \"${AGENTTEAMS_VERSION}\" \"v1.1.0\"; then\n HERMES_WORKER_IMAGE=\"\"\n fi\n}\n\n# Resolve the embedded controller image. Embedded mode is the only supported\n# architecture since PR #616 (manager image no longer bundles Higress/Tuwunel/MinIO).\n# If the embedded image is unavailable for the requested version, fail fast with an\n# actionable error rather than silently falling back to the legacy single-container\n# path — that path is permanently broken with the slim manager image and would just\n# leave the user with a manager container looping on \"Waiting for Higress Gateway\".\n# Sets EMBEDDED_IMAGE and AGENTTEAMS_USE_EMBEDDED.\nresolve_embedded_image() {\n AGENTTEAMS_USE_EMBEDDED=1\n\n # If the user explicitly overrode the image (e.g. `make install-embedded` passes\n # a locally-built tag), respect it as-is without any registry probe.\n if [ -n \"${AGENTTEAMS_INSTALL_EMBEDDED_IMAGE:-}\" ]; then\n EMBEDDED_IMAGE=\"${AGENTTEAMS_INSTALL_EMBEDDED_IMAGE}\"\n return 0\n fi\n\n local _versioned=\"${AGENTTEAMS_REGISTRY}/agentteams/agentteams-embedded:${AGENTTEAMS_VERSION}\"\n local _latest=\"${AGENTTEAMS_REGISTRY}/agentteams/agentteams-embedded:latest\"\n\n # Skip probe when AGENTTEAMS_VERSION is \"latest\" — no point trying the same tag twice.\n if [ \"${AGENTTEAMS_VERSION}\" = \"latest\" ]; then\n EMBEDDED_IMAGE=\"${_latest}\"\n return 0\n fi\n\n if ${DOCKER_CMD} pull \"${_versioned}\" >/dev/null 2>&1; then\n EMBEDDED_IMAGE=\"${_versioned}\"\n return 0\n fi\n\n # Versions before v1.1.0 predate agentteams-embedded entirely — their manager image\n # bundled all infrastructure. Falling back to agentteams-embedded:latest would\n # silently swap in the v1.1.0 architecture (embedded kube-apiserver) which\n # crashes under QEMU on Apple Silicon. Auto-activate legacy mode instead.\n if _ver_lt \"${AGENTTEAMS_VERSION}\" \"v1.1.0\"; then\n log \"INFO: ${AGENTTEAMS_VERSION} predates agentteams-embedded; switching to legacy all-in-one manager architecture.\"\n log \"WARNING: Legacy all-in-one mode requires AGENTTEAMS_VERSION <= v1.0.9 (older bundled manager image).\"\n log \"WARNING: Newer slim manager images will hang on 'Waiting for Higress Gateway'.\"\n AGENTTEAMS_USE_EMBEDDED=0\n return 0\n fi\n\n if ${DOCKER_CMD} pull \"${_latest}\" >/dev/null 2>&1; then\n log \"embedded ${AGENTTEAMS_VERSION} not found, using latest\"\n EMBEDDED_IMAGE=\"${_latest}\"\n return 0\n fi\n\n # Explicit escape hatch — still honoured for edge cases.\n if [ \"${AGENTTEAMS_FORCE_LEGACY:-0}\" = \"1\" ]; then\n log \"WARNING: AGENTTEAMS_FORCE_LEGACY=1 — using legacy all-in-one manager architecture.\"\n log \"WARNING: This requires AGENTTEAMS_VERSION <= v1.0.9 (older bundled manager image).\"\n log \"WARNING: Newer slim manager images will hang on 'Waiting for Higress Gateway'.\"\n AGENTTEAMS_USE_EMBEDDED=0\n return 0\n fi\n\n error \"Embedded controller image is not available in the registry:\"\n error \" - tried: ${_versioned}\"\n error \" - tried: ${_latest}\"\n error \"\"\n error \"Embedded mode is the only supported architecture since PR #616.\"\n error \"How to resolve:\"\n error \" 1) Pin to a AGENTTEAMS_VERSION whose embedded image has been published, or\"\n error \" wait for the release pipeline to publish it.\"\n error \" 2) For a local build, run: make install-embedded\"\n error \" (builds and uses the local embedded image without touching the registry).\"\n error \" 3) Override with a custom image: AGENTTEAMS_INSTALL_EMBEDDED_IMAGE=... ./agentteams-install.sh\"\n exit 1\n}\n\n# ============================================================\n# Known models list — used to detect custom models during install\n# ============================================================\nKNOWN_MODELS=\"gpt-5.4 gpt-5.3-codex gpt-5-mini gpt-5-nano claude-opus-4-6 claude-sonnet-4-6 claude-haiku-4-5 qwen3.6-plus qwen3.5-plus deepseek-chat deepseek-reasoner kimi-k2.5 glm-5 MiniMax-M2.7 MiniMax-M2.7-highspeed MiniMax-M2.5\"\n\nis_known_model() {\n local model=\"$1\"\n for m in ${KNOWN_MODELS}; do\n [ \"${m}\" = \"${model}\" ] && return 0\n done\n return 1\n}\n\n# Prompt user for custom model parameters when model is not in the known list.\n# Sets: AGENTTEAMS_MODEL_CONTEXT_WINDOW, AGENTTEAMS_MODEL_MAX_TOKENS, AGENTTEAMS_MODEL_REASONING, AGENTTEAMS_MODEL_VISION\nprompt_custom_model_params() {\n local model=\"$1\"\n if is_known_model \"${model}\"; then\n # Clear any stale custom params for known models\n AGENTTEAMS_MODEL_CONTEXT_WINDOW=\"\"\n AGENTTEAMS_MODEL_MAX_TOKENS=\"\"\n AGENTTEAMS_MODEL_REASONING=\"\"\n AGENTTEAMS_MODEL_VISION=\"\"\n return\n fi\n echo \"\"\n log \"$(msg llm.custom_model.detected \"${model}\")\"\n echo \"\"\n read -e -p \" $(msg llm.custom_model.context_prompt): \" AGENTTEAMS_MODEL_CONTEXT_WINDOW\n if [ \"${AGENTTEAMS_MODEL_CONTEXT_WINDOW}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n AGENTTEAMS_MODEL_CONTEXT_WINDOW=\"${AGENTTEAMS_MODEL_CONTEXT_WINDOW:-150000}\"\n read -e -p \" $(msg llm.custom_model.max_tokens_prompt): \" AGENTTEAMS_MODEL_MAX_TOKENS\n if [ \"${AGENTTEAMS_MODEL_MAX_TOKENS}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n AGENTTEAMS_MODEL_MAX_TOKENS=\"${AGENTTEAMS_MODEL_MAX_TOKENS:-128000}\"\n read -e -p \" $(msg llm.custom_model.reasoning_prompt): \" _reasoning\n if [ \"${_reasoning}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n case \"${_reasoning}\" in\n n|N|no|NO) AGENTTEAMS_MODEL_REASONING=\"false\" ;;\n *) AGENTTEAMS_MODEL_REASONING=\"true\" ;;\n esac\n read -e -p \" $(msg llm.custom_model.vision_prompt): \" _vision\n if [ \"${_vision}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n case \"${_vision}\" in\n y|Y|yes|YES) AGENTTEAMS_MODEL_VISION=\"true\" ;;\n *) AGENTTEAMS_MODEL_VISION=\"false\" ;;\n esac\n log \"$(msg llm.custom_model.summary \"${AGENTTEAMS_MODEL_CONTEXT_WINDOW}\" \"${AGENTTEAMS_MODEL_MAX_TOKENS}\" \"${AGENTTEAMS_MODEL_REASONING}\" \"${AGENTTEAMS_MODEL_VISION}\")\"\n}\n\n# ============================================================\n# Wait for Manager agent to be ready\n# Uses `openclaw gateway health` inside the container to confirm the gateway is running\n# ============================================================\n\nwait_manager_ready() {\n local timeout=\"${AGENTTEAMS_READY_TIMEOUT:-300}\"\n local elapsed=0\n local container=\"${1:-agentteams-manager}\"\n\n log \"$(msg install.wait_ready \"${timeout}\")\"\n\n # Wait for Manager agent to be healthy inside the container\n local runtime=\"${AGENTTEAMS_MANAGER_RUNTIME:-copaw}\"\n while [ \"${elapsed}\" -lt \"${timeout}\" ]; do\n case \"${runtime}\" in\n copaw)\n if ${DOCKER_CMD} exec \"${container}\" curl -sf http://127.0.0.1:18799/api/agents 2>/dev/null | grep -q '\"agents\"'; then\n log \"$(msg install.wait_ready.ok)\"\n return 0\n fi\n ;;\n *)\n if ${DOCKER_CMD} exec \"${container}\" openclaw gateway health --json 2>/dev/null | grep -q '\"ok\"' 2>/dev/null; then\n log \"$(msg install.wait_ready.ok)\"\n return 0\n fi\n ;;\n esac\n sleep 5\n elapsed=$((elapsed + 5))\n printf \"\\r\\033[36m[AgentTeams]\\033[0m $(msg install.wait_ready.waiting \"${elapsed}\" \"${timeout}\")\"\n done\n\n echo \"\"\n die \"$(msg install.wait_ready.timeout \"${timeout}\" \"${container}\")\"\n}\n\nwait_matrix_ready() {\n local timeout=\"${AGENTTEAMS_READY_TIMEOUT:-300}\"\n local elapsed=0\n local container=\"${1:-agentteams-manager}\"\n\n log \"$(msg install.wait_matrix \"${timeout}\")\"\n\n while [ \"${elapsed}\" -lt \"${timeout}\" ]; do\n if ${DOCKER_CMD} exec \"${container}\" curl -sf http://127.0.0.1:6167/_tuwunel/server_version >/dev/null 2>&1; then\n log \"$(msg install.wait_matrix.ok)\"\n return 0\n fi\n sleep 5\n elapsed=$((elapsed + 5))\n printf \"\\r\\033[36m[AgentTeams]\\033[0m $(msg install.wait_matrix.waiting \"${elapsed}\" \"${timeout}\")\"\n done\n\n echo \"\"\n die \"$(msg install.wait_matrix.timeout \"${timeout}\" \"${container}\")\"\n}\n\n# Read KEY=value from /data/agentteams-secrets.env on a Docker volume (manager container not required).\n# Requires EMBEDDED_IMAGE (resolved earlier in install_manager). Uses ${DOCKER_CMD}.\nagentteams_read_secret_from_data_volume() {\n local _vol=\"$1\" _key=\"$2\"\n if [ -z \"${_vol}\" ] || [ -z \"${_key}\" ] || [ -z \"${EMBEDDED_IMAGE:-}\" ]; then\n echo \"\"\n return 0\n fi\n ${DOCKER_CMD} run --rm --entrypoint sh \\\n -v \"${_vol}:/data:ro\" \\\n \"${EMBEDDED_IMAGE}\" -c \"grep \\\"^${_key}=\\\" /data/agentteams-secrets.env 2>/dev/null | cut -d= -f2- | head -1 | tr -d '\\r'\" 2>/dev/null\n}\n\n# Read KEY=value from /data/worker-creds/<worker>.env on a Docker volume.\nagentteams_read_worker_creds_value_from_volume() {\n local _vol=\"$1\" _worker=\"$2\" _key=\"$3\"\n if [ -z \"${_vol}\" ] || [ -z \"${_worker}\" ] || [ -z \"${_key}\" ] || [ -z \"${EMBEDDED_IMAGE:-}\" ]; then\n echo \"\"\n return 0\n fi\n ${DOCKER_CMD} run --rm --entrypoint sh \\\n -v \"${_vol}:/data:ro\" \\\n \"${EMBEDDED_IMAGE}\" -c \"grep \\\"^${_key}=\\\" \\\"/data/worker-creds/${_worker}.env\\\" 2>/dev/null | cut -d= -f2- | head -1 | tr -d \\\"\\\\r\\\"\" 2>/dev/null\n}\n\n# Read admin_dm_room_id from host workspace state.json (fallback when Matrix API is unavailable).\nagentteams_read_admin_dm_room_from_workspace() {\n local _ws=\"$1\"\n local _f=\"${_ws}/state.json\"\n if [ ! -f \"${_f}\" ] || ! command -v jq >/dev/null 2>&1; then\n echo \"\"\n return 0\n fi\n jq -r '.admin_dm_room_id // empty | select(. != \"null\")' \"${_f}\" 2>/dev/null\n}\n\n# Read secret input with masked echo (shows * per keystroke, supports backspace)\n# Usage: read_secret \"prompt text: \"; value=\"${_RS_RESULT}\"\nread_secret() {\n local _rs_prompt=\"$1\"\n _RS_RESULT=\"\"\n local _rs_char=\"\"\n\n printf \"%s\" \"${_rs_prompt}\"\n\n while IFS= read -r -s -n 1 _rs_char; do\n if [[ -z \"${_rs_char}\" ]]; then\n break\n elif [[ \"${_rs_char}\" == $'\\177' ]] || [[ \"${_rs_char}\" == $'\\b' ]]; then\n if [ -n \"${_RS_RESULT}\" ]; then\n _RS_RESULT=\"${_RS_RESULT%?}\"\n printf \"\\b \\b\"\n fi\n else\n _RS_RESULT=\"${_RS_RESULT}${_rs_char}\"\n printf \"*\"\n fi\n done\n echo\n}\n\n# Load current parameter values from env file for upgrade mode display\nload_current_params_from_env() {\n local env_file=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n if [ -f \"${env_file}\" ]; then\n [ -z \"${AGENTTEAMS_LLM_PROVIDER:+x}\" ] && AGENTTEAMS_LLM_PROVIDER=\"$(grep '^AGENTTEAMS_LLM_PROVIDER=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_OPENAI_BASE_URL:+x}\" ] && AGENTTEAMS_OPENAI_BASE_URL=\"$(grep '^AGENTTEAMS_OPENAI_BASE_URL=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_DEFAULT_MODEL:+x}\" ] && AGENTTEAMS_DEFAULT_MODEL=\"$(grep '^AGENTTEAMS_DEFAULT_MODEL=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_EMBEDDING_MODEL:+x}\" ] && AGENTTEAMS_EMBEDDING_MODEL=\"$(grep '^AGENTTEAMS_EMBEDDING_MODEL=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_WORKSPACE_DIR:+x}\" ] && AGENTTEAMS_WORKSPACE_DIR=\"$(grep '^AGENTTEAMS_WORKSPACE_DIR=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_HOST_SHARE_DIR:+x}\" ] && AGENTTEAMS_HOST_SHARE_DIR=\"$(grep '^AGENTTEAMS_HOST_SHARE_DIR=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_PODMAN_AUTOSTART:+x}\" ] && AGENTTEAMS_PODMAN_AUTOSTART=\"$(grep '^AGENTTEAMS_PODMAN_AUTOSTART=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_DASHBOARD:+x}\" ] && AGENTTEAMS_DASHBOARD=\"$(grep '^AGENTTEAMS_DASHBOARD=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_DASHBOARD_VERSION:+x}\" ] && AGENTTEAMS_DASHBOARD_VERSION=\"$(grep '^AGENTTEAMS_DASHBOARD_VERSION=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_PORT_DASHBOARD:+x}\" ] && AGENTTEAMS_PORT_DASHBOARD=\"$(grep '^AGENTTEAMS_PORT_DASHBOARD=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_DASHBOARD_IMAGE:+x}\" ] && AGENTTEAMS_DASHBOARD_IMAGE=\"$(grep '^AGENTTEAMS_DASHBOARD_IMAGE=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL:+x}\" ] && AGENTTEAMS_AI_GATEWAY_ADMIN_URL=\"$(grep '^AGENTTEAMS_AI_GATEWAY_ADMIN_URL=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n fi\n}\n\n# In non-interactive mode, uses default or errors if required and no default.\n# Usage: prompt VAR_NAME \"Prompt text\" \"default\" [true=secret]\nprompt() {\n local var_name=\"$1\"\n local prompt_text=\"$2\"\n local default_value=\"$3\"\n local is_secret=\"${4:-false}\"\n\n # If the variable is already set in the environment, use it silently\n # In upgrade mode, show current value and let user change it\n eval \"local current_value=\\\"\\${${var_name}}\\\"\"\n if [ -n \"${current_value}\" ]; then\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_NON_INTERACTIVE}\" != \"1\" ]; then\n # Show masked value for secrets, full value otherwise\n local display_value=\"${current_value}\"\n if [ \"${is_secret}\" = \"true\" ]; then\n local len=${#current_value}\n if [ \"${len}\" -le 8 ]; then\n display_value=\"****\"\n else\n display_value=\"${current_value:0:4}****${current_value: -4}\"\n fi\n fi\n log \"$(msg prompt.upgrade_keep \"${prompt_text}\" \"${display_value}\")\"\n local new_value=\"\"\n if [ \"${is_secret}\" = \"true\" ]; then\n read_secret \"${prompt_text}: \"\n new_value=\"${_RS_RESULT}\"\n else\n read -e -p \"${prompt_text}: \" new_value\n if [ \"${new_value}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n fi\n if [ -n \"${new_value}\" ]; then\n eval \"export ${var_name}='${new_value}'\"\n fi\n return\n fi\n log \"$(msg prompt.preset \"${prompt_text}\")\"\n return\n fi\n\n # Non-interactive or quickstart: use default or error\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] || [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ]; then\n if [ -n \"${default_value}\" ]; then\n eval \"export ${var_name}='${default_value}'\"\n log \"$(msg prompt.default \"${prompt_text}\" \"${default_value}\")\"\n return\n elif [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n # Only hard-error in fully non-interactive mode, not quickstart\n die \"$(msg prompt.required \"${prompt_text}\")\"\n fi\n # quickstart + no default: fall through to interactive prompt below\n fi\n\n if [ -n \"${default_value}\" ]; then\n prompt_text=\"${prompt_text} [${default_value}]\"\n fi\n\n local value=\"\"\n if [ \"${is_secret}\" = \"true\" ]; then\n read_secret \"${prompt_text}: \"\n value=\"${_RS_RESULT}\"\n else\n read -e -p \"${prompt_text}: \" value\n if [ \"${value}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n fi\n\n value=\"${value:-${default_value}}\"\n if [ -z \"${value}\" ]; then\n die \"$(msg prompt.required_empty \"${prompt_text}\")\"\n fi\n\n eval \"export ${var_name}='${value}'\"\n}\n\n# Prompt for an optional value (empty string is acceptable)\n# Skips prompt if variable is already defined in environment (even if empty)\n# In upgrade mode, shows current value and lets user change it.\n# In non-interactive mode, defaults to empty string.\nprompt_optional() {\n local var_name=\"$1\"\n local prompt_text=\"$2\"\n local is_secret=\"${3:-false}\"\n\n # Check if variable is defined (even if set to empty string)\n eval \"local _chk=\\\"\\${${var_name}+x}\\\"\"\n if [ -n \"${_chk}\" ]; then\n # In upgrade mode, show current value and let user change it\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_NON_INTERACTIVE}\" != \"1\" ]; then\n eval \"local current_value=\\\"\\${${var_name}}\\\"\"\n local display_value=\"${current_value}\"\n if [ \"${is_secret}\" = \"true\" ] && [ -n \"${current_value}\" ]; then\n local len=${#current_value}\n if [ \"${len}\" -le 8 ]; then\n display_value=\"****\"\n else\n display_value=\"${current_value:0:4}****${current_value: -4}\"\n fi\n fi\n if [ -n \"${current_value}\" ]; then\n if [ \"${is_secret}\" = \"true\" ]; then\n log \"$(msg prompt.upgrade_keep_secret \"${prompt_text}\" \"${display_value}\")\"\n else\n log \"$(msg prompt.upgrade_keep \"${prompt_text}\" \"${display_value}\")\"\n fi\n else\n log \"$(msg prompt.upgrade_empty \"${prompt_text}\")\"\n fi\n local new_value=\"\"\n if [ \"${is_secret}\" = \"true\" ]; then\n read_secret \"${prompt_text}: \"\n new_value=\"${_RS_RESULT}\"\n else\n read -e -p \"${prompt_text}: \" new_value\n if [ \"${new_value}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n fi\n if [ -n \"${new_value}\" ]; then\n eval \"export ${var_name}='${new_value}'\"\n fi\n return\n fi\n log \"$(msg prompt.preset \"${prompt_text}\")\"\n return\n fi\n\n # Non-interactive or quickstart: skip, leave unset\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] || [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ]; then\n eval \"export ${var_name}=''\"\n return\n fi\n\n local value=\"\"\n if [ \"${is_secret}\" = \"true\" ]; then\n read_secret \"${prompt_text}: \"\n value=\"${_RS_RESULT}\"\n else\n read -e -p \"${prompt_text}: \" value\n if [ \"${value}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n fi\n\n eval \"export ${var_name}='${value}'\"\n}\n\ngenerate_key() {\n openssl rand -hex 32\n}\n\n# ============================================================\n# Detect container runtime socket on the host\n#\n# Dependency: Requires DOCKER_CMD to reflect the TRUE underlying runtime.\n# (Crucial for unmasking 'podman' disguised as 'docker'.\n# Must be strictly resolved prior, e.g., via check_container_runtime)\n# ============================================================\ndetect_socket() {\n local socket_path\n\n # 1. Respect explicitly defined DOCKER_HOST environment variable\n if [ -n \"${DOCKER_HOST}\" ]; then\n socket_path=$(echo \"${DOCKER_HOST}\" | sed 's|^unix://||')\n if [ -S \"${socket_path}\" ]; then\n echo \"${socket_path}\"\n return 0\n fi\n fi\n\n # 2. Route based on the resolved container runtime\n case \"${DOCKER_CMD}\" in\n docker)\n # Docker path handling\n socket_path=$(docker context ls --format '{{if .Current}}{{.DockerEndpoint}}{{end}}' 2>/dev/null | grep . | sed 's|^unix://||')\n if [ -n \"${socket_path}\" ] && [ -S \"${socket_path}\" ]; then\n echo \"${socket_path}\"\n return 0\n fi\n\n # Rootless Docker\n if [ -n \"${XDG_RUNTIME_DIR:-}\" ] && [ -S \"${XDG_RUNTIME_DIR}/docker.sock\" ]; then\n echo \"${XDG_RUNTIME_DIR}/docker.sock\"\n return 0\n fi\n\n # Rootless Docker (macOS fallback)\n if [ -S \"${HOME}/.docker/run/docker.sock\" ]; then\n echo \"${HOME}/.docker/run/docker.sock\"\n return 0\n fi\n\n # Root Docker\n if [ -S \"/var/run/docker.sock\" ]; then\n echo \"/var/run/docker.sock\"\n return 0\n fi\n ;;\n podman)\n # Podman path handling\n if [ \"$(id -u)\" -eq 0 ]; then\n socket_path=\"/run/podman/podman.sock\"\n else\n socket_path=\"${XDG_RUNTIME_DIR}/podman/podman.sock\"\n fi\n if [ -S \"${socket_path}\" ]; then\n echo \"${socket_path}\"\n return 0\n fi\n ;;\n esac\n\n # Return empty if no socket is found\n echo \"\"\n return 0\n}\n\n# Ensure Podman API socket is active (State mutation)\nensure_podman_socket() {\n local socket_path\n\n # Only applicable if runtime is podman and systemctl is available\n if [ \"${DOCKER_CMD:-}\" != \"podman\" ] || ! command -v systemctl >/dev/null 2>&1; then\n return 0\n fi\n\n echo \"Ensuring Podman API socket is active...\" >&2\n\n # Enable and start the socket based on user privileges\n if [ \"$(id -u)\" -eq 0 ]; then\n socket_path=\"/run/podman/podman.sock\"\n systemctl enable --now podman.socket >/dev/null 2>&1 || true\n else\n socket_path=\"${XDG_RUNTIME_DIR}/podman/podman.sock\"\n systemctl --user enable --now podman.socket >/dev/null 2>&1 || true\n fi\n\n # Give systemd a brief moment to assert the socket for root or rootless user\n local _i\n for _i in 1 2 3; do\n [ -S \"${socket_path}\" ] && break\n sleep 1\n done\n}\n\n# Detect local LAN IP address (cross-platform: macOS and Linux)\ndetect_lan_ip() {\n local ip=\"\"\n\n # macOS: try common Wi-Fi / Ethernet interfaces\n if command -v ipconfig >/dev/null 2>&1; then\n for iface in en0 en1 en2 en3 en4; do\n ip=$(ipconfig getifaddr \"${iface}\" 2>/dev/null)\n if [ -n \"${ip}\" ]; then\n echo \"${ip}\"\n return 0\n fi\n done\n fi\n\n # Linux: ip route — most reliable\n if command -v ip >/dev/null 2>&1; then\n ip=$(ip route get 1.1.1.1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i==\"src\"){print $(i+1); exit}}')\n if [ -n \"${ip}\" ]; then\n echo \"${ip}\"\n return 0\n fi\n fi\n\n # Linux fallback: hostname -I (space-separated list, take first non-loopback)\n if command -v hostname >/dev/null 2>&1; then\n ip=$(hostname -I 2>/dev/null | tr ' ' '\\n' | grep -v '^127\\.' | grep -v '^::' | head -1)\n if [ -n \"${ip}\" ]; then\n echo \"${ip}\"\n return 0\n fi\n fi\n\n # Last resort: ifconfig\n if command -v ifconfig >/dev/null 2>&1; then\n ip=$(ifconfig 2>/dev/null | awk '/inet /{if($2!~/^127\\./){print $2; exit}}')\n # Strip \"addr:\" prefix that some ifconfig versions add\n ip=\"${ip#addr:}\"\n if [ -n \"${ip}\" ]; then\n echo \"${ip}\"\n return 0\n fi\n fi\n\n echo \"\"\n}\n\n\n# ============================================================\n# Step-back navigation helpers\n# ============================================================\n\n# should_skip_step: returns 0 (skip) when the step is irrelevant in current mode\nshould_skip_step() {\n local step_fn=\"$1\"\n case \"${step_fn}\" in\n step_lang|step_mode)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n ;;\n step_version)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n ;;\n step_existing)\n local _env=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n [ ! -f \"${_env}\" ] && return 0\n ;;\n # Keep-All upgrade mode: skip all config steps (step_volume/step_workspace handled separately)\n step_llm|step_admin|step_network|step_ports|step_domains|step_github|step_skills|step_runtime|step_manager_runtime|step_dashboard|step_e2ee|step_docker_proxy|step_idle|step_hostshare)\n [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ] && return 0\n ;;\n step_volume|step_workspace)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ] && return 0\n [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ] && return 0\n ;;\n step_e2ee|step_idle)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE}\" != \"1\" ] && return 0\n ;;\n step_docker_proxy)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE}\" != \"1\" ] && return 0\n # Embedded mode handles docker access natively — skip this step\n [ \"${AGENTTEAMS_USE_EMBEDDED:-}\" = \"1\" ] && return 0\n ;;\n step_manager_runtime)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n ;;\n step_hostshare)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ] && return 0\n ;;\n step_podman_autostart)\n # Only relevant for Podman with systemd\n [ \"${DOCKER_CMD}\" != \"podman\" ] && return 0\n ! command -v systemctl >/dev/null 2>&1 && return 0\n\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE}\" != \"1\" ] && return 0\n [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ] && return 0\n ;;\n esac\n return 1\n}\n\n# clear_step_vars: unset variables set by a step so it will re-prompt on re-entry\nclear_step_vars() {\n local step_fn=\"$1\"\n case \"${step_fn}\" in\n step_mode) unset AGENTTEAMS_QUICKSTART ;;\n step_version) unset AGENTTEAMS_VERSION ;;\n step_existing) unset AGENTTEAMS_UPGRADE UPGRADE_EXISTING_WORKERS ;;\n step_llm)\n unset AGENTTEAMS_LLM_PROVIDER AGENTTEAMS_DEFAULT_MODEL AGENTTEAMS_OPENAI_BASE_URL\n unset AGENTTEAMS_LLM_API_KEY AGENTTEAMS_MODEL_CONTEXT_WINDOW AGENTTEAMS_MODEL_MAX_TOKENS\n unset AGENTTEAMS_MODEL_REASONING AGENTTEAMS_MODEL_VISION\n ;;\n step_admin) unset AGENTTEAMS_ADMIN_USER AGENTTEAMS_ADMIN_PASSWORD ;;\n step_network) unset AGENTTEAMS_LOCAL_ONLY ;;\n step_ports)\n unset AGENTTEAMS_PORT_GATEWAY AGENTTEAMS_PORT_CONSOLE\n unset AGENTTEAMS_PORT_ELEMENT_WEB AGENTTEAMS_PORT_MANAGER_CONSOLE\n ;;\n step_domains)\n unset AGENTTEAMS_MATRIX_DOMAIN AGENTTEAMS_MATRIX_CLIENT_DOMAIN\n unset AGENTTEAMS_AI_GATEWAY_DOMAIN AGENTTEAMS_FS_DOMAIN AGENTTEAMS_CONSOLE_DOMAIN\n ;;\n step_github) unset AGENTTEAMS_GITHUB_TOKEN ;;\n step_skills) unset AGENTTEAMS_SKILLS_API_URL ;;\n step_volume) unset AGENTTEAMS_DATA_DIR ;;\n step_workspace) unset AGENTTEAMS_WORKSPACE_DIR ;;\n step_dashboard) unset AGENTTEAMS_DASHBOARD AGENTTEAMS_DASHBOARD_VERSION AGENTTEAMS_PORT_DASHBOARD AGENTTEAMS_DASHBOARD_IMAGE AGENTTEAMS_AI_GATEWAY_ADMIN_URL ;;\n step_runtime) unset AGENTTEAMS_DEFAULT_WORKER_RUNTIME ;;\n step_manager_runtime) unset AGENTTEAMS_MANAGER_RUNTIME ;;\n step_e2ee) unset AGENTTEAMS_MATRIX_E2EE ;;\n step_docker_proxy) unset AGENTTEAMS_DOCKER_PROXY; unset AGENTTEAMS_PROXY_ALLOWED_REGISTRIES ;;\n step_idle) unset AGENTTEAMS_WORKER_IDLE_TIMEOUT ;;\n step_hostshare) unset AGENTTEAMS_HOST_SHARE_DIR ;;\n step_podman_autostart) unset AGENTTEAMS_PODMAN_AUTOSTART ;;\n esac\n}\n\n# ============================================================\n# Individual step functions\n# ============================================================\n\nstep_lang() {\n local lang_default_choice=\"2\"\n [ \"${AGENTTEAMS_LANGUAGE}\" = \"zh\" ] && lang_default_choice=\"1\"\n log \"$(msg lang.detected)\"\n log \"$(msg lang.switch_title)\"\n echo \"$(msg lang.option_zh)\"\n echo \"$(msg lang.option_en)\"\n echo \"\"\n local LANG_CHOICE\n read -e -p \"$(msg lang.prompt) [${lang_default_choice}]: \" LANG_CHOICE\n LANG_CHOICE=\"${LANG_CHOICE:-${lang_default_choice}}\"\n if [ \"${LANG_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n case \"${LANG_CHOICE}\" in\n 1) AGENTTEAMS_LANGUAGE=\"zh\" ;;\n 2) AGENTTEAMS_LANGUAGE=\"en\" ;;\n esac\n export AGENTTEAMS_LANGUAGE\n log \"\"\n}\n\nstep_mode() {\n log \"$(msg install.mode.title)\"\n echo \"\"\n echo \"$(msg install.mode.choose)\"\n echo \"$(msg install.mode.quickstart)\"\n echo \"$(msg install.mode.manual)\"\n echo \"\"\n local ONBOARDING_CHOICE\n read -e -p \"$(msg install.mode.prompt): \" ONBOARDING_CHOICE\n ONBOARDING_CHOICE=\"${ONBOARDING_CHOICE:-1}\"\n if [ \"${ONBOARDING_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n case \"${ONBOARDING_CHOICE}\" in\n 1|quick|quickstart)\n log \"$(msg install.mode.quickstart_selected)\"\n AGENTTEAMS_QUICKSTART=1\n ;;\n 2|manual)\n log \"$(msg install.mode.manual_selected)\"\n ;;\n *)\n log \"$(msg install.mode.invalid)\"\n AGENTTEAMS_QUICKSTART=1\n ;;\n esac\n log \"\"\n}\n\nstep_version() {\n # Skip if version already provided via env var\n if [ -n \"${AGENTTEAMS_VERSION}\" ]; then\n resolve_image_tags\n return 0\n fi\n # Try to fetch the latest stable release from GitHub\n log \"$(msg install.version.fetching)\"\n local _fetched\n _fetched=$(curl -sf --max-time 5 \\\n -H \"Accept: application/vnd.github+json\" \\\n \"https://api.github.com/repos/agentscope-ai/AgentTeams/releases/latest\" \\\n 2>/dev/null | grep '\"tag_name\"' | head -1 | sed 's/.*\"tag_name\": *\"\\([^\"]*\\)\".*/\\1/')\n if [ -n \"${_fetched}\" ]; then\n AGENTTEAMS_KNOWN_STABLE_VERSION=\"${_fetched}\"\n else\n log \"$(msg install.version.fetch_failed \"${AGENTTEAMS_KNOWN_STABLE_VERSION}\")\"\n fi\n log \"$(msg install.version.title)\"\n echo \"\"\n echo \"$(msg install.version.choose)\"\n echo \"$(msg install.version.option_latest)\"\n printf \"%s\\n\" \"$(msg install.version.option_stable \"${AGENTTEAMS_KNOWN_STABLE_VERSION}\")\"\n echo \"$(msg install.version.option_custom)\"\n echo \"\"\n local VERSION_CHOICE\n read -e -p \"$(msg install.version.prompt) [1]: \" VERSION_CHOICE\n VERSION_CHOICE=\"${VERSION_CHOICE:-1}\"\n if [ \"${VERSION_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n case \"${VERSION_CHOICE}\" in\n 1|latest)\n AGENTTEAMS_VERSION=\"latest\"\n log \"$(msg install.version.selected_latest)\"\n ;;\n 2|stable)\n AGENTTEAMS_VERSION=\"${AGENTTEAMS_KNOWN_STABLE_VERSION}\"\n log \"$(msg install.version.selected_stable \"${AGENTTEAMS_VERSION}\")\"\n ;;\n 3|custom)\n local CUSTOM_VERSION\n read -e -p \"$(msg install.version.custom_prompt): \" CUSTOM_VERSION\n AGENTTEAMS_VERSION=\"${CUSTOM_VERSION:-${AGENTTEAMS_KNOWN_STABLE_VERSION}}\"\n log \"$(msg install.version.selected_custom \"${AGENTTEAMS_VERSION}\")\"\n ;;\n *)\n AGENTTEAMS_VERSION=\"${AGENTTEAMS_KNOWN_STABLE_VERSION}\"\n log \"$(msg install.version.invalid \"${AGENTTEAMS_VERSION}\")\"\n ;;\n esac\n log \"\"\n resolve_image_tags\n}\n\nstep_existing() {\n local existing_env=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n log \"$(msg install.existing.detected \"${existing_env}\")\"\n local running_manager=\"\" running_workers=\"\" existing_workers=\"\"\n if ${DOCKER_CMD} ps --format '{{.Names}}' | grep -q \"^agentteams-manager$\"; then\n running_manager=\"agentteams-manager\"\n fi\n running_workers=$(${DOCKER_CMD} ps --format '{{.Names}}' | grep \"^agentteams-worker-\" || true)\n existing_workers=$(${DOCKER_CMD} ps -a --format '{{.Names}}' | grep \"^agentteams-worker-\" || true)\n local UPGRADE_CHOICE\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n log \"$(msg install.existing.upgrade_noninteractive)\"\n UPGRADE_CHOICE=\"upgrade\"\n else\n echo \"\"\n echo \"$(msg install.existing.choose)\"\n echo \"$(msg install.existing.upgrade)\"\n echo \"$(msg install.existing.reinstall)\"\n echo \"$(msg install.existing.cancel)\"\n echo \"\"\n read -e -p \"$(msg install.existing.prompt): \" UPGRADE_CHOICE\n UPGRADE_CHOICE=\"${UPGRADE_CHOICE:-1}\"\n if [ \"${UPGRADE_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n fi\n case \"${UPGRADE_CHOICE}\" in\n 1|upgrade)\n AGENTTEAMS_UPGRADE=1\n log \"$(msg install.existing.upgrading)\"\n # Show upgrade mode sub-menu (unless already set via env var)\n if [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL:-0}\" != \"1\" ]; then\n echo \"\"\n echo \"$(msg upgrade.mode.prompt)\"\n echo \"$(msg upgrade.mode.keep_all)\"\n echo \"$(msg upgrade.mode.confirm_each)\"\n echo \"$(msg upgrade.mode.back)\"\n echo \"\"\n local UPGRADE_MODE_CHOICE\n read -e -p \"$(msg install.existing.prompt): \" UPGRADE_MODE_CHOICE\n UPGRADE_MODE_CHOICE=\"${UPGRADE_MODE_CHOICE:-1}\"\n case \"${UPGRADE_MODE_CHOICE}\" in\n 1|keep)\n AGENTTEAMS_UPGRADE_KEEP_ALL=1\n ;;\n 2|confirm)\n AGENTTEAMS_UPGRADE_KEEP_ALL=0\n ;;\n 3|b)\n STEP_RESULT=\"back\"\n return 0\n ;;\n *)\n AGENTTEAMS_UPGRADE_KEEP_ALL=0\n ;;\n esac\n fi\n # Load current parameters for both Keep-All and confirm-each modes\n load_current_params_from_env\n if [ -n \"${running_manager}\" ] || [ -n \"${running_workers}\" ]; then\n echo \"\"\n echo -e \"\\033[33m$(msg install.existing.warn_manager_stop)\\033[0m\"\n if [ -n \"${existing_workers}\" ]; then\n echo -e \"\\033[33m$(msg install.existing.warn_worker_recreate)\\033[0m\"\n fi\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" != \"1\" ]; then\n echo \"\"\n local CONFIRM_STOP\n read -e -p \"$(msg install.existing.continue_prompt): \" CONFIRM_STOP\n if [ \"${CONFIRM_STOP}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n if [ \"${CONFIRM_STOP}\" != \"y\" ] && [ \"${CONFIRM_STOP}\" != \"Y\" ]; then\n log \"$(msg install.existing.cancelled)\"\n exit 0\n fi\n fi\n fi\n UPGRADE_EXISTING_WORKERS=\"${existing_workers}\"\n ;;\n 2|reinstall)\n log \"$(msg install.reinstall.performing)\"\n local existing_workspace=\"\"\n if [ -f \"${existing_env}\" ]; then\n existing_workspace=$(grep '^AGENTTEAMS_WORKSPACE_DIR=' \"${existing_env}\" 2>/dev/null | cut -d= -f2-)\n fi\n [ -z \"${existing_workspace}\" ] && existing_workspace=\"${HOME}/agentteams-manager\"\n echo \"\"\n echo -e \"\\033[33m$(msg install.reinstall.warn_stop)\\033[0m\"\n [ -n \"${running_manager}\" ] && echo -e \"\\033[33m - ${running_manager} (manager)\\033[0m\"\n for w in ${running_workers}; do\n echo -e \"\\033[33m - ${w} (worker)\\033[0m\"\n done\n echo \"\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_delete)\\033[0m\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_volume)\\033[0m\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_env \"${existing_env}\")\\033[0m\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_workspace \"${existing_workspace}\")\\033[0m\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_workers)\\033[0m\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_proxy)\\033[0m\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_network)\\033[0m\"\n echo \"\"\n echo -e \"\\033[31m$(msg install.reinstall.confirm_type)\\033[0m\"\n echo -e \"\\033[31m ${existing_workspace}\\033[0m\"\n echo \"\"\n local CONFIRM_PATH\n read -e -p \"$(msg install.reinstall.confirm_path): \" CONFIRM_PATH\n if [ \"${CONFIRM_PATH}\" != \"${existing_workspace}\" ]; then\n die \"$(msg install.reinstall.path_mismatch \"${CONFIRM_PATH}\" \"${existing_workspace}\")\"\n fi\n log \"$(msg install.reinstall.confirmed)\"\n ${DOCKER_CMD} stop agentteams-manager 2>/dev/null || true\n ${DOCKER_CMD} rm agentteams-manager 2>/dev/null || true\n for w in $(${DOCKER_CMD} ps -a --format '{{.Names}}' | grep \"^agentteams-worker-\" || true); do\n ${DOCKER_CMD} stop \"${w}\" 2>/dev/null || true\n ${DOCKER_CMD} rm \"${w}\" 2>/dev/null || true\n log \"$(msg install.reinstall.removed_worker \"${w}\")\"\n done\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' | grep -q \"^agentteams-controller$\"; then\n log \"$(msg install.reinstall.removing_proxy)\"\n ${DOCKER_CMD} stop agentteams-controller 2>/dev/null || true\n ${DOCKER_CMD} rm agentteams-controller 2>/dev/null || true\n fi\n if ${DOCKER_CMD} network ls --format '{{.Name}}' | grep -q \"^agentteams-net$\"; then\n log \"$(msg install.reinstall.removing_network)\"\n ${DOCKER_CMD} network rm agentteams-net 2>/dev/null || true\n fi\n if ${DOCKER_CMD} volume ls -q | grep -q \"^agentteams-data$\"; then\n log \"$(msg install.reinstall.removing_volume)\"\n ${DOCKER_CMD} volume rm agentteams-data 2>/dev/null || log \"$(msg install.reinstall.warn_volume_fail)\"\n fi\n if [ -d \"${existing_workspace}\" ]; then\n log \"$(msg install.reinstall.removing_workspace \"${existing_workspace}\")\"\n rm -rf \"${existing_workspace}\" || die \"$(msg install.reinstall.failed_rm_workspace)\"\n fi\n if [ -f \"${existing_env}\" ]; then\n log \"$(msg install.reinstall.removing_env \"${existing_env}\")\"\n rm -f \"${existing_env}\"\n fi\n log \"$(msg install.reinstall.cleanup_done)\"\n unset AGENTTEAMS_WORKSPACE_DIR\n return 0\n ;;\n 3|cancel|*)\n log \"$(msg install.existing.cancelled)\"\n exit 0\n ;;\n esac\n # Load existing env file as fallback (shell env vars take priority)\n if [ -f \"${existing_env}\" ]; then\n log \"$(msg install.loading_config \"${existing_env}\")\"\n while IFS='=' read -r key value; do\n case \"${key}\" in \\#*|\"\") continue ;; esac\n value=\"${value%%#*}\"\n value=\"${value#\"${value%%[![:space:]]*}\"}\"\n value=\"${value%\"${value##*[![:space:]]}\"}\"\n eval \"_existing_val=\\\"\\${${key}+x}\\\"\"\n if [ -z \"${_existing_val}\" ]; then export \"${key}=${value}\"; fi\n done < \"${existing_env}\"\n fi\n}\n\nstep_llm() {\n log \"$(msg llm.title)\"\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n if [ \"${AGENTTEAMS_LANGUAGE}\" = \"zh\" ]; then\n AGENTTEAMS_LLM_PROVIDER=\"${AGENTTEAMS_LLM_PROVIDER:-openai-compat}\"\n AGENTTEAMS_DEFAULT_MODEL=\"${AGENTTEAMS_DEFAULT_MODEL:-qwen3.6-plus}\"\n AGENTTEAMS_OPENAI_BASE_URL=\"${AGENTTEAMS_OPENAI_BASE_URL:-https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1}\"\n log \"$(msg llm.provider.label \"${AGENTTEAMS_LLM_PROVIDER}\")\"\n log \"$(msg llm.openai.base_url_label \"${AGENTTEAMS_OPENAI_BASE_URL}\")\"\n else\n AGENTTEAMS_LLM_PROVIDER=\"${AGENTTEAMS_LLM_PROVIDER:-qwen}\"\n AGENTTEAMS_DEFAULT_MODEL=\"${AGENTTEAMS_DEFAULT_MODEL:-qwen3.6-plus}\"\n AGENTTEAMS_OPENAI_BASE_URL=\"${AGENTTEAMS_OPENAI_BASE_URL:-}\"\n log \"$(msg llm.provider.qwen_default \"${AGENTTEAMS_LLM_PROVIDER}\")\"\n fi\n log \"$(msg llm.model.default \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n prompt AGENTTEAMS_LLM_API_KEY \"$(msg llm.apikey_prompt)\" \"\" \"true\"\n AGENTTEAMS_EMBEDDING_MODEL=\"${AGENTTEAMS_EMBEDDING_MODEL-text-embedding-v4}\"\n export AGENTTEAMS_LLM_PROVIDER AGENTTEAMS_DEFAULT_MODEL AGENTTEAMS_EMBEDDING_MODEL\n [ -n \"${AGENTTEAMS_OPENAI_BASE_URL+x}\" ] && export AGENTTEAMS_OPENAI_BASE_URL\n return 0\n fi\n\n # Upgrade Keep-All mode: skip provider selection, use loaded values\n local SKIP_TO_EMBEDDING=0\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ] && [ -n \"${AGENTTEAMS_LLM_PROVIDER}\" ]; then\n log \"$(msg llm.provider.label \"${AGENTTEAMS_LLM_PROVIDER}\")\"\n log \"$(msg llm.openai.base_url_label \"${AGENTTEAMS_OPENAI_BASE_URL}\")\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n PROVIDER_CHOICE=\"${AGENTTEAMS_LLM_PROVIDER}\"\n SKIP_TO_EMBEDDING=1\n else\n echo \"\"\n echo \"$(msg llm.providers_title)\"\n echo \"$(msg llm.provider.alibaba)\"\n echo \"$(msg llm.provider.openai_compat)\"\n echo \"\"\n local PROVIDER_CHOICE\n # If upgrade mode with loaded provider, show current as default\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_LLM_PROVIDER}\" ]; then\n local _prov_display\n case \"${AGENTTEAMS_LLM_PROVIDER}\" in\n openai-compat) _prov_display=\"2\" ;;\n *) _prov_display=\"1\" ;;\n esac\n read -e -p \"$(msg llm.provider.select) [${_prov_display}]: \" PROVIDER_CHOICE\n PROVIDER_CHOICE=\"${PROVIDER_CHOICE:-${_prov_display}}\"\n elif [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ]; then\n read -e -p \"$(msg llm.provider.select) [1]: \" PROVIDER_CHOICE\n PROVIDER_CHOICE=\"${PROVIDER_CHOICE:-1}\"\n else\n read -e -p \"$(msg llm.provider.select): \" PROVIDER_CHOICE\n PROVIDER_CHOICE=\"${PROVIDER_CHOICE:-1}\"\n fi\n if [ \"${PROVIDER_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n fi\n local ALIBABA_MODEL_CHOICE=\"\"\n local ALIBABA_ACCESS=\"\"\n case \"${PROVIDER_CHOICE}\" in\n 1|alibaba-cloud|openai-compat)\n if [ \"${AGENTTEAMS_LANGUAGE}\" = \"en\" ]; then\n AGENTTEAMS_LLM_PROVIDER=\"openai-compat\"\n AGENTTEAMS_OPENAI_BASE_URL=\"https://dashscope-intl.aliyuncs.com/compatible-mode/v1\"\n ALIBABA_ACCESS=\"tokenplan\"\n echo \"\"\n echo \"$(msg llm.codingplan.models_title)\"\n echo \"$(msg llm.codingplan.model.qwen36plus)\"\n echo \"$(msg llm.codingplan.model.glm5)\"\n echo \"$(msg llm.codingplan.model.kimi)\"\n echo \"$(msg llm.codingplan.model.minimax)\"\n echo \"\"\n local CODINGPLAN_MODEL_CHOICE\n # If upgrade with loaded model, show current as default\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_DEFAULT_MODEL}\" ]; then\n local _model_default\n case \"${AGENTTEAMS_DEFAULT_MODEL}\" in\n qwen3.6-plus) _model_default=\"1\" ;;\n glm-5) _model_default=\"2\" ;;\n kimi-k2.5) _model_default=\"3\" ;;\n MiniMax-M2.5) _model_default=\"4\" ;;\n *) _model_default=\"1\" ;;\n esac\n read -e -p \"$(msg llm.codingplan.model.select) [${_model_default}]: \" CODINGPLAN_MODEL_CHOICE\n CODINGPLAN_MODEL_CHOICE=\"${CODINGPLAN_MODEL_CHOICE:-${_model_default}}\"\n elif [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ]; then\n read -e -p \"$(msg llm.codingplan.model.select) [1]: \" CODINGPLAN_MODEL_CHOICE\n CODINGPLAN_MODEL_CHOICE=\"${CODINGPLAN_MODEL_CHOICE:-1}\"\n else\n read -e -p \"$(msg llm.codingplan.model.select): \" CODINGPLAN_MODEL_CHOICE\n CODINGPLAN_MODEL_CHOICE=\"${CODINGPLAN_MODEL_CHOICE:-1}\"\n fi\n if [ \"${CODINGPLAN_MODEL_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n case \"${CODINGPLAN_MODEL_CHOICE}\" in\n 1|qwen3.6-plus) AGENTTEAMS_DEFAULT_MODEL=\"qwen3.6-plus\" ;;\n 2|glm-5) AGENTTEAMS_DEFAULT_MODEL=\"glm-5\" ;;\n 3|kimi-k2.5) AGENTTEAMS_DEFAULT_MODEL=\"kimi-k2.5\" ;;\n 4|MiniMax-M2.5) AGENTTEAMS_DEFAULT_MODEL=\"MiniMax-M2.5\" ;;\n *) AGENTTEAMS_DEFAULT_MODEL=\"qwen3.6-plus\" ;;\n esac\n log \"$(msg llm.provider.selected_codingplan)\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n else\n echo \"\"\n echo \"$(msg llm.alibaba.models_title)\"\n echo \"$(msg llm.alibaba.model.tokenplan)\"\n echo \"$(msg llm.alibaba.model.bailian)\"\n echo \"$(msg llm.alibaba.model.codingplan_legacy)\"\n echo \"\"\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_OPENAI_BASE_URL}\" ]; then\n # Determine alibaba access type from loaded URL\n local _access_default=\"1\"\n if echo \"${AGENTTEAMS_OPENAI_BASE_URL}\" | grep -q \"token-plan\"; then\n _access_default=\"1\"\n elif echo \"${AGENTTEAMS_OPENAI_BASE_URL}\" | grep -q \"dashscope\"; then\n if echo \"${AGENTTEAMS_OPENAI_BASE_URL}\" | grep -q \"intl\"; then\n _access_default=\"3\"\n else\n _access_default=\"2\"\n fi\n fi\n read -e -p \"$(msg llm.alibaba.model.select) [${_access_default}]: \" ALIBABA_MODEL_CHOICE\n ALIBABA_MODEL_CHOICE=\"${ALIBABA_MODEL_CHOICE:-${_access_default}}\"\n elif [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ]; then\n read -e -p \"$(msg llm.alibaba.model.select) [1]: \" ALIBABA_MODEL_CHOICE\n ALIBABA_MODEL_CHOICE=\"${ALIBABA_MODEL_CHOICE:-1}\"\n else\n read -e -p \"$(msg llm.alibaba.model.select): \" ALIBABA_MODEL_CHOICE\n ALIBABA_MODEL_CHOICE=\"${ALIBABA_MODEL_CHOICE:-1}\"\n fi\n if [ \"${ALIBABA_MODEL_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n case \"${ALIBABA_MODEL_CHOICE}\" in\n 1|token-plan|tokenplan)\n ALIBABA_ACCESS=\"tokenplan\"\n AGENTTEAMS_LLM_PROVIDER=\"openai-compat\"\n AGENTTEAMS_OPENAI_BASE_URL=\"https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1\"\n echo \"\"\n echo \"$(msg llm.codingplan.models_title)\"\n echo \"$(msg llm.codingplan.model.qwen36plus)\"\n echo \"$(msg llm.codingplan.model.glm5)\"\n echo \"$(msg llm.codingplan.model.kimi)\"\n echo \"$(msg llm.codingplan.model.minimax)\"\n echo \"\"\n local CODINGPLAN_MODEL_CHOICE\n # If upgrade with loaded model, show current as default\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_DEFAULT_MODEL}\" ]; then\n local _model_default\n case \"${AGENTTEAMS_DEFAULT_MODEL}\" in\n qwen3.6-plus) _model_default=\"1\" ;;\n glm-5) _model_default=\"2\" ;;\n kimi-k2.5) _model_default=\"3\" ;;\n MiniMax-M2.5) _model_default=\"4\" ;;\n *) _model_default=\"1\" ;;\n esac\n read -e -p \"$(msg llm.codingplan.model.select) [${_model_default}]: \" CODINGPLAN_MODEL_CHOICE\n CODINGPLAN_MODEL_CHOICE=\"${CODINGPLAN_MODEL_CHOICE:-${_model_default}}\"\n elif [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ]; then\n read -e -p \"$(msg llm.codingplan.model.select) [1]: \" CODINGPLAN_MODEL_CHOICE\n CODINGPLAN_MODEL_CHOICE=\"${CODINGPLAN_MODEL_CHOICE:-1}\"\n else\n read -e -p \"$(msg llm.codingplan.model.select): \" CODINGPLAN_MODEL_CHOICE\n CODINGPLAN_MODEL_CHOICE=\"${CODINGPLAN_MODEL_CHOICE:-1}\"\n fi\n if [ \"${CODINGPLAN_MODEL_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n case \"${CODINGPLAN_MODEL_CHOICE}\" in\n 1|qwen3.6-plus) AGENTTEAMS_DEFAULT_MODEL=\"qwen3.6-plus\" ;;\n 2|glm-5) AGENTTEAMS_DEFAULT_MODEL=\"glm-5\" ;;\n 3|kimi-k2.5) AGENTTEAMS_DEFAULT_MODEL=\"kimi-k2.5\" ;;\n 4|MiniMax-M2.5) AGENTTEAMS_DEFAULT_MODEL=\"MiniMax-M2.5\" ;;\n *) AGENTTEAMS_DEFAULT_MODEL=\"qwen3.6-plus\" ;;\n esac\n log \"$(msg llm.provider.selected_tokenplan)\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n ;;\n 2|qwen|bailian)\n ALIBABA_ACCESS=\"bailian\"\n AGENTTEAMS_LLM_PROVIDER=\"qwen\"\n AGENTTEAMS_OPENAI_BASE_URL=\"\"\n echo \"\"\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_DEFAULT_MODEL}\" ]; then\n read -e -p \"$(msg llm.qwen.model_prompt) [${AGENTTEAMS_DEFAULT_MODEL}]: \" AGENTTEAMS_DEFAULT_MODEL\n else\n read -e -p \"$(msg llm.qwen.model_prompt) [qwen3.6-plus]: \" AGENTTEAMS_DEFAULT_MODEL\n fi\n if [ \"${AGENTTEAMS_DEFAULT_MODEL}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_DEFAULT_MODEL=\"${AGENTTEAMS_DEFAULT_MODEL:-qwen3.6-plus}\"\n log \"$(msg llm.provider.selected_qwen)\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n prompt_custom_model_params \"${AGENTTEAMS_DEFAULT_MODEL}\" || return 0\n ;;\n 3|coding-plan|codingplan)\n ALIBABA_ACCESS=\"codingplan_legacy\"\n AGENTTEAMS_LLM_PROVIDER=\"openai-compat\"\n AGENTTEAMS_OPENAI_BASE_URL=\"https://coding.dashscope.aliyuncs.com/v1\"\n echo \"\"\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_DEFAULT_MODEL}\" ]; then\n read -e -p \"$(msg llm.qwen.model_prompt) [${AGENTTEAMS_DEFAULT_MODEL}]: \" AGENTTEAMS_DEFAULT_MODEL\n else\n read -e -p \"$(msg llm.qwen.model_prompt): \" AGENTTEAMS_DEFAULT_MODEL\n fi\n if [ \"${AGENTTEAMS_DEFAULT_MODEL}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_DEFAULT_MODEL=\"${AGENTTEAMS_DEFAULT_MODEL:-qwen3.6-plus}\"\n log \"$(msg llm.provider.selected_codingplan_legacy)\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n prompt_custom_model_params \"${AGENTTEAMS_DEFAULT_MODEL}\" || return 0\n ;;\n *)\n die \"$(msg llm.alibaba.model.invalid \"${ALIBABA_MODEL_CHOICE}\")\"\n ;;\n esac\n fi\n if [ \"${ALIBABA_ACCESS}\" = \"bailian\" ]; then\n log \"$(msg llm.apikey_hint_bailian)\"\n log \"$(msg llm.apikey_url_bailian)\"\n elif [ \"${AGENTTEAMS_LANGUAGE}\" = \"en\" ]; then\n log \"$(msg llm.apikey_hint_qwencloud)\"\n log \"$(msg llm.apikey_url_qwencloud)\"\n elif [ \"${ALIBABA_ACCESS}\" = \"codingplan_legacy\" ]; then\n log \"$(msg llm.apikey_hint_codingplan)\"\n log \"$(msg llm.apikey_url_codingplan)\"\n else\n log \"$(msg llm.apikey_hint_tokenplan)\"\n log \"$(msg llm.apikey_url_tokenplan)\"\n fi\n log \"\"\n prompt AGENTTEAMS_LLM_API_KEY \"$(msg llm.apikey_prompt)\" \"\" \"true\" || return 0\n if [ \"${ALIBABA_ACCESS}\" = \"bailian\" ]; then\n test_llm_connectivity \"https://dashscope.aliyuncs.com/compatible-mode/v1\" \"${AGENTTEAMS_LLM_API_KEY}\" \"${AGENTTEAMS_DEFAULT_MODEL}\" || return 0\n elif [ \"${ALIBABA_ACCESS}\" = \"codingplan_legacy\" ]; then\n test_llm_connectivity \"https://coding.dashscope.aliyuncs.com/v1\" \"${AGENTTEAMS_LLM_API_KEY}\" \"${AGENTTEAMS_DEFAULT_MODEL}\" \"$(msg llm.openai.test.fail.codingplan_legacy)\" || return 0\n elif [ \"${AGENTTEAMS_LANGUAGE}\" = \"en\" ]; then\n test_llm_connectivity \"${AGENTTEAMS_OPENAI_BASE_URL}\" \"${AGENTTEAMS_LLM_API_KEY}\" \"${AGENTTEAMS_DEFAULT_MODEL}\" \"$(msg llm.openai.test.fail.codingplan)\" || return 0\n else\n test_llm_connectivity \"${AGENTTEAMS_OPENAI_BASE_URL}\" \"${AGENTTEAMS_LLM_API_KEY}\" \"${AGENTTEAMS_DEFAULT_MODEL}\" \"$(msg llm.openai.test.fail.tokenplan)\" || return 0\n fi\n ;;\n 2|openai-compat)\n AGENTTEAMS_LLM_PROVIDER=\"openai-compat\"\n log \"$(msg llm.provider.selected_openai \"${AGENTTEAMS_LLM_PROVIDER}\")\"\n echo \"\"\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg llm.openai.base_url_prompt)\" \"${AGENTTEAMS_OPENAI_BASE_URL}\")\"\n else\n _current=\"${AGENTTEAMS_OPENAI_BASE_URL}\"\n if [ -n \"${_current}\" ]; then\n read -e -p \"$(msg llm.openai.base_url_prompt) [${_current}]: \" AGENTTEAMS_OPENAI_BASE_URL\n else\n read -e -p \"$(msg llm.openai.base_url_prompt) [https://api.openai.com/v1]: \" AGENTTEAMS_OPENAI_BASE_URL\n fi\n if [ \"${AGENTTEAMS_OPENAI_BASE_URL}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_OPENAI_BASE_URL=\"${AGENTTEAMS_OPENAI_BASE_URL:-${_current}}\"\n [ -z \"${AGENTTEAMS_OPENAI_BASE_URL}\" ] && AGENTTEAMS_OPENAI_BASE_URL=\"https://api.openai.com/v1\"\n fi\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg llm.openai.model_prompt)\" \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n else\n _current=\"${AGENTTEAMS_DEFAULT_MODEL}\"\n if [ -n \"${_current}\" ]; then\n read -e -p \"$(msg llm.openai.model_prompt) [${_current}]: \" AGENTTEAMS_DEFAULT_MODEL\n else\n read -e -p \"$(msg llm.openai.model_prompt) [gpt-5.4]: \" AGENTTEAMS_DEFAULT_MODEL\n fi\n if [ \"${AGENTTEAMS_DEFAULT_MODEL}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_DEFAULT_MODEL=\"${AGENTTEAMS_DEFAULT_MODEL:-${_current}}\"\n [ -z \"${AGENTTEAMS_DEFAULT_MODEL}\" ] && AGENTTEAMS_DEFAULT_MODEL=\"gpt-5.4\"\n fi\n log \"$(msg llm.openai.base_url_label \"${AGENTTEAMS_OPENAI_BASE_URL}\")\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n prompt_custom_model_params \"${AGENTTEAMS_DEFAULT_MODEL}\" || return 0\n log \"\"\n prompt AGENTTEAMS_LLM_API_KEY \"$(msg llm.apikey_prompt)\" \"\" \"true\" || return 0\n test_llm_connectivity \"${AGENTTEAMS_OPENAI_BASE_URL}\" \"${AGENTTEAMS_LLM_API_KEY}\" \"${AGENTTEAMS_DEFAULT_MODEL}\" || return 0\n ;;\n *)\n die \"$(msg llm.provider.invalid \"${PROVIDER_CHOICE}\")\"\n ;;\n esac\n\n # Skip to embedding if Keep-All mode handled LLM params\n if [ \"${SKIP_TO_EMBEDDING}\" = \"1\" ]; then\n if [ -n \"${AGENTTEAMS_EMBEDDING_MODEL}\" ]; then\n log \"$(msg llm.embedding.title)\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_EMBEDDING_MODEL}\")\"\n log \"\"\n fi\n return 0\n fi\n\n # --- Embedding model (optional, auto-tested) ---\n echo \"\"\n log \"$(msg llm.embedding.title)\"\n log \"$(msg llm.embedding.hint)\"\n echo \"\"\n echo \"$(msg llm.embedding.option.default)\"\n echo \"$(msg llm.embedding.option.custom)\"\n echo \"$(msg llm.embedding.option.disable)\"\n echo \"\"\n local EMB_CHOICE\n read -e -p \"$(msg llm.embedding.select) [1]: \" EMB_CHOICE\n EMB_CHOICE=\"${EMB_CHOICE:-1}\"\n if [ \"${EMB_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n\n case \"${EMB_CHOICE}\" in\n 1)\n AGENTTEAMS_EMBEDDING_MODEL=\"text-embedding-v4\"\n ;;\n 2)\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg llm.embedding.custom_prompt)\" \"${AGENTTEAMS_EMBEDDING_MODEL}\")\"\n else\n _current=\"${AGENTTEAMS_EMBEDDING_MODEL}\"\n if [ -n \"${_current}\" ]; then\n read -e -p \"$(msg llm.embedding.custom_prompt) [${_current}]: \" AGENTTEAMS_EMBEDDING_MODEL\n else\n read -e -p \"$(msg llm.embedding.custom_prompt): \" AGENTTEAMS_EMBEDDING_MODEL\n fi\n if [ \"${AGENTTEAMS_EMBEDDING_MODEL}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_EMBEDDING_MODEL=\"${AGENTTEAMS_EMBEDDING_MODEL:-${_current}}\"\n fi\n ;;\n 3)\n AGENTTEAMS_EMBEDDING_MODEL=\"\"\n log \"$(msg llm.embedding.disabled)\"\n ;;\n *)\n AGENTTEAMS_EMBEDDING_MODEL=\"text-embedding-v4\"\n ;;\n esac\n\n if [ -n \"${AGENTTEAMS_EMBEDDING_MODEL}\" ]; then\n # Qwen provider uses dashscope directly; others use OPENAI_BASE_URL\n local EMB_BASE_URL=\"${AGENTTEAMS_OPENAI_BASE_URL}\"\n if [ \"${AGENTTEAMS_LLM_PROVIDER}\" = \"qwen\" ]; then\n EMB_BASE_URL=\"https://dashscope.aliyuncs.com/compatible-mode/v1\"\n fi\n if ! test_embedding_connectivity \"${EMB_BASE_URL}\" \"${AGENTTEAMS_LLM_API_KEY}\" \"${AGENTTEAMS_EMBEDDING_MODEL}\"; then\n AGENTTEAMS_EMBEDDING_MODEL=\"\"\n log \"$(msg llm.embedding.auto_disabled)\"\n fi\n fi\n\n export AGENTTEAMS_LLM_PROVIDER AGENTTEAMS_DEFAULT_MODEL\n [ -n \"${AGENTTEAMS_OPENAI_BASE_URL+x}\" ] && export AGENTTEAMS_OPENAI_BASE_URL\n log \"\"\n}\n\nstep_admin() {\n log \"$(msg admin.title)\"\n prompt AGENTTEAMS_ADMIN_USER \"$(msg admin.username_prompt)\" \"admin\" || return 0\n AGENTTEAMS_ADMIN_USER=\"$(printf '%s' \"${AGENTTEAMS_ADMIN_USER}\" | tr '[:upper:]' '[:lower:]')\"\n\n # Pre-set via env var: validate; in non-interactive mode fail fast,\n # in interactive mode warn and fall through to the retry prompt.\n if [ -n \"${AGENTTEAMS_ADMIN_PASSWORD:-}\" ]; then\n log \" $(msg prompt.preset \"$(msg admin.password_prompt)\")\"\n if [ ${#AGENTTEAMS_ADMIN_PASSWORD} -ge 8 ]; then\n log \"\"\n return 0\n fi\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n die \"$(msg admin.password_too_short \"${#AGENTTEAMS_ADMIN_PASSWORD}\")\"\n fi\n error \"$(msg admin.password_too_short \"${#AGENTTEAMS_ADMIN_PASSWORD}\")\"\n unset AGENTTEAMS_ADMIN_PASSWORD\n fi\n\n while true; do\n unset AGENTTEAMS_ADMIN_PASSWORD\n prompt_optional AGENTTEAMS_ADMIN_PASSWORD \"$(msg admin.password_prompt)\" \"true\" || return 0\n if [ -z \"${AGENTTEAMS_ADMIN_PASSWORD}\" ]; then\n AGENTTEAMS_ADMIN_PASSWORD=\"admin$(openssl rand -hex 6)\"\n log \"$(msg admin.password_generated)\"\n break\n fi\n if [ ${#AGENTTEAMS_ADMIN_PASSWORD} -ge 8 ]; then\n break\n fi\n error \"$(msg admin.password_too_short \"${#AGENTTEAMS_ADMIN_PASSWORD}\")\"\n done\n log \"\"\n}\n\nstep_network() {\n log \"$(msg port.local_only.title)\"\n echo \"\"\n echo \" 1) $(msg port.local_only.hint_yes)\"\n echo \" 2) $(msg port.local_only.hint_no)\"\n echo \"\"\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_LOCAL_ONLY=\"${AGENTTEAMS_LOCAL_ONLY:-1}\"\n elif [ -z \"${AGENTTEAMS_LOCAL_ONLY+x}\" ]; then\n local _local_choice\n read -e -p \"$(msg port.local_only.choice): \" _local_choice\n if [ \"${_local_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _local_choice=\"${_local_choice:-1}\"\n case \"${_local_choice}\" in\n 2|n|N|no|NO) AGENTTEAMS_LOCAL_ONLY=\"0\" ;;\n *) AGENTTEAMS_LOCAL_ONLY=\"1\" ;;\n esac\n fi\n export AGENTTEAMS_LOCAL_ONLY\n if [ \"${AGENTTEAMS_LOCAL_ONLY}\" = \"1\" ]; then\n log \"$(msg port.local_only.selected_local)\"\n else\n log \"$(msg port.local_only.selected_external)\"\n echo \"\"\n echo -e \"\\033[33m$(msg port.local_only.https_hint)\\033[0m\"\n fi\n}\n\nstep_ports() {\n log \"$(msg port.title)\"\n prompt AGENTTEAMS_PORT_GATEWAY \"$(msg port.gateway_prompt)\" \"18080\" || return 0\n prompt AGENTTEAMS_PORT_CONSOLE \"$(msg port.console_prompt)\" \"18001\" || return 0\n prompt AGENTTEAMS_PORT_ELEMENT_WEB \"$(msg port.element_prompt)\" \"18088\" || return 0\n prompt AGENTTEAMS_PORT_MANAGER_CONSOLE \"$(msg port.manager_console_prompt)\" \"18888\" || return 0\n log \"\"\n}\n\nstep_domains() {\n log \"$(msg domain.title)\"\n log \"$(msg domain.hint)\"\n prompt AGENTTEAMS_MATRIX_DOMAIN \"$(msg domain.matrix_prompt)\" \"matrix-local.agentteams.io:${AGENTTEAMS_PORT_GATEWAY}\" || return 0\n prompt AGENTTEAMS_MATRIX_CLIENT_DOMAIN \"$(msg domain.element_prompt)\" \"matrix-client-local.agentteams.io\" || return 0\n prompt AGENTTEAMS_AI_GATEWAY_DOMAIN \"$(msg domain.gateway_prompt)\" \"aigw-local.agentteams.io\" || return 0\n prompt AGENTTEAMS_FS_DOMAIN \"$(msg domain.fs_prompt)\" \"fs-local.agentteams.io\" || return 0\n if [ \"${AGENTTEAMS_MANAGER_RUNTIME}\" != \"copaw\" ]; then\n prompt AGENTTEAMS_CONSOLE_DOMAIN \"$(msg domain.console_prompt)\" \"console-local.agentteams.io\" || return 0\n fi\n log \"\"\n}\n\nstep_github() {\n log \"$(msg github.title)\"\n prompt_optional AGENTTEAMS_GITHUB_TOKEN \"$(msg github.token_prompt)\" \"true\" || return 0\n}\n\nstep_skills() {\n log \"\"\n log \"$(msg skills.title)\"\n prompt_optional AGENTTEAMS_SKILLS_API_URL \"$(msg skills.url_prompt)\" || return 0\n log \"\"\n}\n\nstep_volume() {\n log \"$(msg data.title)\"\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_DATA_DIR=\"${AGENTTEAMS_DATA_DIR:-agentteams-data}\"\n log \" $(msg data.volume_using \"${AGENTTEAMS_DATA_DIR}\") (non-interactive, skipped)\"\n export AGENTTEAMS_DATA_DIR\n return 0\n fi\n # ─────────────────────────────────────────────────────────────────\n if [ -z \"${AGENTTEAMS_DATA_DIR+x}\" ]; then\n local _input\n read -e -p \"$(msg data.volume_prompt): \" _input\n if [ \"${_input}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_DATA_DIR=\"${_input:-agentteams-data}\"\n export AGENTTEAMS_DATA_DIR\n fi\n AGENTTEAMS_DATA_DIR=\"${AGENTTEAMS_DATA_DIR:-agentteams-data}\"\n log \"$(msg data.volume_using \"${AGENTTEAMS_DATA_DIR}\")\"\n}\n\nstep_workspace() {\n log \"$(msg workspace.title)\"\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_WORKSPACE_DIR=\"${AGENTTEAMS_WORKSPACE_DIR:-${HOME}/agentteams-manager}\"\n AGENTTEAMS_WORKSPACE_DIR=\"$(cd \"${AGENTTEAMS_WORKSPACE_DIR}\" 2>/dev/null && pwd || echo \"${AGENTTEAMS_WORKSPACE_DIR}\")\"\n mkdir -p \"${AGENTTEAMS_WORKSPACE_DIR}\"\n log \" $(msg workspace.dir_label \"${AGENTTEAMS_WORKSPACE_DIR}\") (non-interactive, skipped)\"\n export AGENTTEAMS_WORKSPACE_DIR\n return 0\n fi\n # ─────────────────────────────────────────────────────────────────\n local _input\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg workspace.dir_prompt \"${HOME}/agentteams-manager\")\" \"${AGENTTEAMS_WORKSPACE_DIR}\")\"\n _input=\"${AGENTTEAMS_WORKSPACE_DIR}\"\n else\n local _current=\"${AGENTTEAMS_WORKSPACE_DIR}\"\n if [ -n \"${_current}\" ]; then\n read -e -p \"$(msg workspace.dir_prompt \"${HOME}/agentteams-manager\") [${_current}]: \" _input\n else\n read -e -p \"$(msg workspace.dir_prompt \"${HOME}/agentteams-manager\"): \" _input\n fi\n if [ \"${_input}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _input=\"${_input:-${_current}}\"\n fi\n AGENTTEAMS_WORKSPACE_DIR=\"${_input:-${HOME}/agentteams-manager}\"\n export AGENTTEAMS_WORKSPACE_DIR\n AGENTTEAMS_WORKSPACE_DIR=\"$(cd \"${AGENTTEAMS_WORKSPACE_DIR}\" 2>/dev/null && pwd || echo \"${AGENTTEAMS_WORKSPACE_DIR}\")\"\n mkdir -p \"${AGENTTEAMS_WORKSPACE_DIR}\"\n log \"$(msg workspace.dir_label \"${AGENTTEAMS_WORKSPACE_DIR}\")\"\n}\n\nstep_dashboard() {\n AGENTTEAMS_DASHBOARD=\"${AGENTTEAMS_DASHBOARD:-1}\"\n AGENTTEAMS_DASHBOARD_VERSION=\"${AGENTTEAMS_DASHBOARD_VERSION:-v1.2.0}\"\n AGENTTEAMS_PORT_DASHBOARD=\"${AGENTTEAMS_PORT_DASHBOARD:-13000}\"\n AGENTTEAMS_AI_GATEWAY_ADMIN_URL=\"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL:-}\"\n\n # Compute the default image for the current version.\n # We track whether the current image matches the derived default so that\n # changing the version also updates the image (unless the user has\n # explicitly overridden it to a non-default value).\n _dashboard_default_image() {\n echo \"${AGENTTEAMS_REGISTRY}/agentteams/agentteams-dashboard:${1:-${AGENTTEAMS_DASHBOARD_VERSION}}\"\n }\n local _init_default\n _init_default=$(_dashboard_default_image)\n if [ -z \"${AGENTTEAMS_DASHBOARD_IMAGE:-}\" ]; then\n AGENTTEAMS_DASHBOARD_IMAGE=\"${_init_default}\"\n fi\n\n # Recompute default image when the version changes AND the current image\n # matches the default for the *previous* (env-file) version.\n # This handles non-interactive upgrades correctly:\n # - env file loads old default image + old version\n # - CLI passes new version\n # - image should follow (because it was auto-derived before)\n # But preserves truly custom images (same repo but custom tag like :canary)\n # because they won't match the old-version default exactly.\n local _old_saved_version=\"\"\n local _env_file=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n if [ -f \"${_env_file}\" ]; then\n _old_saved_version=\"$(grep '^AGENTTEAMS_DASHBOARD_VERSION=' \"${_env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n fi\n if [ -n \"${_old_saved_version}\" ] && \\\n [ \"${_old_saved_version}\" != \"${AGENTTEAMS_DASHBOARD_VERSION}\" ] && \\\n [ \"${AGENTTEAMS_DASHBOARD_IMAGE}\" = \"$(_dashboard_default_image \"${_old_saved_version}\")\" ]; then\n AGENTTEAMS_DASHBOARD_IMAGE=$(_dashboard_default_image)\n fi\n\n # Quick Start: accept all defaults without prompting. The dashboard is\n # installed by default (AGENTTEAMS_DASHBOARD=1); set AGENTTEAMS_DASHBOARD=0\n # to opt out.\n if [ \"${AGENTTEAMS_QUICKSTART:-0}\" = \"1\" ]; then\n if [ \"${AGENTTEAMS_DASHBOARD}\" = \"1\" ]; then\n log \"$(msg dash.summary \"${AGENTTEAMS_PORT_DASHBOARD}\" \"${AGENTTEAMS_DASHBOARD_IMAGE}\") (quick start)\"\n else\n log \"$(msg dash.skip) (quick start)\"\n fi\n export AGENTTEAMS_DASHBOARD AGENTTEAMS_DASHBOARD_VERSION AGENTTEAMS_PORT_DASHBOARD AGENTTEAMS_DASHBOARD_IMAGE AGENTTEAMS_AI_GATEWAY_ADMIN_URL\n return 0\n fi\n\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_DASHBOARD=\"${AGENTTEAMS_DASHBOARD:-1}\"\n if [ \"${AGENTTEAMS_DASHBOARD}\" = \"1\" ]; then\n log \"$(msg dash.summary \"${AGENTTEAMS_PORT_DASHBOARD}\" \"${AGENTTEAMS_DASHBOARD_IMAGE}\") (non-interactive)\"\n else\n log \"$(msg dash.skip) (non-interactive)\"\n fi\n export AGENTTEAMS_DASHBOARD AGENTTEAMS_DASHBOARD_VERSION AGENTTEAMS_PORT_DASHBOARD AGENTTEAMS_DASHBOARD_IMAGE AGENTTEAMS_AI_GATEWAY_ADMIN_URL\n return 0\n fi\n\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ]; then\n log \"$(msg prompt.upgrade_keep \"\" \"${AGENTTEAMS_DASHBOARD}\") dashboard=${AGENTTEAMS_DASHBOARD} version=${AGENTTEAMS_DASHBOARD_VERSION} port=${AGENTTEAMS_PORT_DASHBOARD}\"\n export AGENTTEAMS_DASHBOARD AGENTTEAMS_DASHBOARD_VERSION AGENTTEAMS_PORT_DASHBOARD AGENTTEAMS_DASHBOARD_IMAGE AGENTTEAMS_AI_GATEWAY_ADMIN_URL\n return 0\n fi\n\n local _input\n echo \"\"\n read -p \"$(msg dash.prompt) [Y/n]: \" _input\n case \"${_input:-}\" in\n [nN0]) AGENTTEAMS_DASHBOARD=0 ;;\n *) AGENTTEAMS_DASHBOARD=1 ;;\n esac\n if [ \"${_input}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n\n if [ \"${AGENTTEAMS_DASHBOARD}\" != \"1\" ]; then\n log \"$(msg dash.skip)\"\n export AGENTTEAMS_DASHBOARD AGENTTEAMS_DASHBOARD_VERSION AGENTTEAMS_PORT_DASHBOARD AGENTTEAMS_DASHBOARD_IMAGE AGENTTEAMS_AI_GATEWAY_ADMIN_URL\n return 0\n fi\n\n # Dashboard version\n local _current_version=\"${AGENTTEAMS_DASHBOARD_VERSION}\"\n local _old_version=\"${AGENTTEAMS_DASHBOARD_VERSION}\"\n local _old_default_image\n _old_default_image=$(_dashboard_default_image \"${_old_version}\")\n read -p \"$(msg dash.version_prompt) [${_current_version}]: \" _input\n AGENTTEAMS_DASHBOARD_VERSION=\"${_input:-${_current_version}}\"\n\n # Dashboard port\n local _current_port=\"${AGENTTEAMS_PORT_DASHBOARD}\"\n read -p \"$(msg dash.port_prompt) [${_current_port}]: \" _input\n AGENTTEAMS_PORT_DASHBOARD=\"${_input:-${_current_port}}\"\n\n # Dashboard image: if the current image matches the default derived from\n # the OLD version, recompute it from the NEW version. This handles both\n # fresh installs and upgrades where the previous image was the default.\n # Only a real user override (image != default for old version) is preserved.\n if [ \"${AGENTTEAMS_DASHBOARD_VERSION}\" != \"${_old_version}\" ] && \\\n [ \"${AGENTTEAMS_DASHBOARD_IMAGE}\" = \"${_old_default_image}\" ]; then\n AGENTTEAMS_DASHBOARD_IMAGE=$(_dashboard_default_image)\n fi\n local _current_image=\"${AGENTTEAMS_DASHBOARD_IMAGE}\"\n read -p \"$(msg dash.image_prompt) [${_current_image}]: \" _input\n if [ -n \"${_input}\" ]; then\n AGENTTEAMS_DASHBOARD_IMAGE=\"${_input}\"\n fi\n\n # Higress Console URL (explicit config takes priority; auto-detect as fallback)\n local _higress_url=\"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\"\n if [ -z \"${_higress_url}\" ]; then\n if ${DOCKER_CMD} ps --format '{{.Names}}' | grep -q \"^agentteams-controller$\"; then\n if ${DOCKER_CMD} exec agentteams-controller curl -sf --max-time 2 http://127.0.0.1:8001/ >/dev/null 2>&1; then\n _higress_url=\"http://agentteams-controller:8001\"\n log \"$(msg dash.auto_url): ${_higress_url}\"\n else\n log \"$(msg dash.auto_url_fail)\"\n fi\n fi\n fi\n read -p \"$(msg dash.gateway_prompt) [${_higress_url:-<auto>}]: \" _input\n AGENTTEAMS_AI_GATEWAY_ADMIN_URL=\"${_input:-${_higress_url}}\"\n\n # Normalize URL: add http:// prefix if missing\n if [ -n \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\" ]; then\n case \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\" in\n http://*|https://*) ;;\n *) AGENTTEAMS_AI_GATEWAY_ADMIN_URL=\"http://${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\" ;;\n esac\n fi\n\n # Verify the Higress Console URL is reachable (best-effort warning only)\n if [ -n \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\" ]; then\n local _gw_reachable=0\n # If the URL points to the controller container, test from inside the container\n if echo \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\" | grep -qE \"(agentteams-controller|hiclaw-controller|127\\.0\\.0\\.1|localhost)\"; then\n if ${DOCKER_CMD} ps --format '{{.Names}}' | grep -q \"^agentteams-controller$\"; then\n if ${DOCKER_CMD} exec agentteams-controller curl -sf --max-time 3 \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}/\" >/dev/null 2>&1; then\n _gw_reachable=1\n fi\n fi\n else\n # External URL — probe from inside the controller (same network as\n # the Dashboard container) when possible; fall back to the host.\n if ${DOCKER_CMD} ps --format '{{.Names}}' | grep -q \"^agentteams-controller$\"; then\n if ${DOCKER_CMD} exec agentteams-controller curl -sf --max-time 3 \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}/\" >/dev/null 2>&1; then\n _gw_reachable=1\n fi\n elif curl -sf --max-time 3 \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}/\" >/dev/null 2>&1; then\n _gw_reachable=1\n fi\n fi\n if [ \"${_gw_reachable}\" = \"0\" ]; then\n log \"WARNING: Higress Console URL may not be reachable: ${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\"\n log \" Dashboard will still work, but shared login via Higress Console may fail.\"\n log \" To fix: verify the URL is correct and the Higress Console service is running.\"\n fi\n fi\n\n log \"$(msg dash.summary \"${AGENTTEAMS_PORT_DASHBOARD}\" \"${AGENTTEAMS_DASHBOARD_IMAGE}\")\"\n export AGENTTEAMS_DASHBOARD AGENTTEAMS_DASHBOARD_VERSION AGENTTEAMS_PORT_DASHBOARD AGENTTEAMS_DASHBOARD_IMAGE AGENTTEAMS_AI_GATEWAY_ADMIN_URL\n}\n\nstep_runtime() {\n log \"$(msg worker_runtime.title)\"\n echo \"\"\n echo \" 1) $(msg worker_runtime.copaw)\"\n echo \" 2) $(msg worker_runtime.openclaw)\"\n if ! _ver_lt \"${AGENTTEAMS_VERSION}\" \"v1.1.0\"; then\n echo \" 3) $(msg worker_runtime.hermes)\"\n fi\n echo \"\"\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"${AGENTTEAMS_DEFAULT_WORKER_RUNTIME:-copaw}\"\n elif [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_DEFAULT_WORKER_RUNTIME}\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg worker_runtime.title_short)\" \"${AGENTTEAMS_DEFAULT_WORKER_RUNTIME}\")\"\n local _runtime_choice\n read -e -p \"$(msg worker_runtime.choice): \" _runtime_choice\n if [ \"${_runtime_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n if [ -n \"${_runtime_choice}\" ]; then\n case \"${_runtime_choice}\" in\n 2) AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"openclaw\" ;;\n 3) if ! _ver_lt \"${AGENTTEAMS_VERSION}\" \"v1.1.0\"; then\n AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"hermes\"\n else\n AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"copaw\"\n fi ;;\n *) AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"copaw\" ;;\n esac\n fi\n elif [ -z \"${AGENTTEAMS_DEFAULT_WORKER_RUNTIME+x}\" ]; then\n local _runtime_choice\n read -e -p \"$(msg worker_runtime.choice): \" _runtime_choice\n if [ \"${_runtime_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _runtime_choice=\"${_runtime_choice:-1}\"\n case \"${_runtime_choice}\" in\n 2) AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"openclaw\" ;;\n 3) if ! _ver_lt \"${AGENTTEAMS_VERSION}\" \"v1.1.0\"; then\n AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"hermes\"\n else\n AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"copaw\"\n fi ;;\n *) AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"copaw\" ;;\n esac\n fi\n export AGENTTEAMS_DEFAULT_WORKER_RUNTIME\n log \"$(msg worker_runtime.selected \"${AGENTTEAMS_DEFAULT_WORKER_RUNTIME}\")\"\n}\n\nstep_manager_runtime() {\n log \"$(msg manager_runtime.title)\"\n echo \"\"\n echo \" 1) $(msg manager_runtime.copaw)\"\n echo \" 2) $(msg manager_runtime.openclaw)\"\n echo \"\"\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_MANAGER_RUNTIME=\"${AGENTTEAMS_MANAGER_RUNTIME:-copaw}\"\n elif [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_MANAGER_RUNTIME}\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg manager_runtime.title_short)\" \"${AGENTTEAMS_MANAGER_RUNTIME}\")\"\n local _runtime_choice\n read -e -p \"$(msg manager_runtime.choice): \" _runtime_choice\n if [ \"${_runtime_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n if [ -n \"${_runtime_choice}\" ]; then\n case \"${_runtime_choice}\" in\n 2) AGENTTEAMS_MANAGER_RUNTIME=\"openclaw\" ;;\n *) AGENTTEAMS_MANAGER_RUNTIME=\"copaw\" ;;\n esac\n fi\n elif [ -z \"${AGENTTEAMS_MANAGER_RUNTIME+x}\" ]; then\n local _runtime_choice\n read -e -p \"$(msg manager_runtime.choice): \" _runtime_choice\n if [ \"${_runtime_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _runtime_choice=\"${_runtime_choice:-1}\"\n case \"${_runtime_choice}\" in\n 2) AGENTTEAMS_MANAGER_RUNTIME=\"openclaw\" ;;\n *) AGENTTEAMS_MANAGER_RUNTIME=\"copaw\" ;;\n esac\n fi\n export AGENTTEAMS_MANAGER_RUNTIME\n log \"$(msg manager_runtime.selected \"${AGENTTEAMS_MANAGER_RUNTIME}\")\"\n}\n\nstep_e2ee() {\n log \"\"\n log \"$(msg matrix_e2ee.title)\"\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_MATRIX_E2EE=\"${AGENTTEAMS_MATRIX_E2EE:-0}\"\n log \" $(msg matrix_e2ee.title_short) = ${AGENTTEAMS_MATRIX_E2EE} (non-interactive, skipped)\"\n export AGENTTEAMS_MATRIX_E2EE\n return 0\n fi\n # ─────────────────────────────────────────────────────────────────\n echo \"\"\n echo -e \" $(msg matrix_e2ee.desc)\"\n echo \"\"\n echo \" 1) $(msg matrix_e2ee.disable)\"\n echo \" 2) $(msg matrix_e2ee.enable)\"\n echo \"\"\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_MATRIX_E2EE}\" ]; then\n local _e2ee_display; if [ \"${AGENTTEAMS_MATRIX_E2EE}\" = \"1\" ]; then _e2ee_display=\"$(msg matrix_e2ee.val_enabled)\"; else _e2ee_display=\"$(msg matrix_e2ee.val_disabled)\"; fi\n log \"$(msg prompt.upgrade_keep \"$(msg matrix_e2ee.title_short)\" \"${_e2ee_display}\")\"\n local _e2ee_choice\n read -e -p \"$(msg matrix_e2ee.choice): \" _e2ee_choice\n if [ \"${_e2ee_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n if [ -n \"${_e2ee_choice}\" ]; then\n case \"${_e2ee_choice}\" in\n 2) AGENTTEAMS_MATRIX_E2EE=\"1\" ;;\n *) AGENTTEAMS_MATRIX_E2EE=\"0\" ;;\n esac\n fi\n elif [ -z \"${AGENTTEAMS_MATRIX_E2EE+x}\" ]; then\n local _e2ee_choice\n read -e -p \"$(msg matrix_e2ee.choice): \" _e2ee_choice\n if [ \"${_e2ee_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _e2ee_choice=\"${_e2ee_choice:-1}\"\n case \"${_e2ee_choice}\" in\n 2) AGENTTEAMS_MATRIX_E2EE=\"1\" ;;\n *) AGENTTEAMS_MATRIX_E2EE=\"0\" ;;\n esac\n fi\n AGENTTEAMS_MATRIX_E2EE=\"${AGENTTEAMS_MATRIX_E2EE:-0}\"\n export AGENTTEAMS_MATRIX_E2EE\n if [ \"${AGENTTEAMS_MATRIX_E2EE}\" = \"1\" ]; then\n log \"$(msg matrix_e2ee.selected_enabled)\"\n else\n log \"$(msg matrix_e2ee.selected_disabled)\"\n fi\n}\n\nstep_docker_proxy() {\n # Only relevant when socket mounting is enabled\n if [ \"${AGENTTEAMS_MOUNT_SOCKET}\" != \"1\" ]; then\n AGENTTEAMS_DOCKER_PROXY=\"0\"\n return 0\n fi\n\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_DOCKER_PROXY=\"${AGENTTEAMS_DOCKER_PROXY:-0}\"\n log \" $(msg docker_proxy.title_short) = ${AGENTTEAMS_DOCKER_PROXY} (non-interactive, skipped)\"\n export AGENTTEAMS_DOCKER_PROXY\n return 0\n fi\n # ─────────────────────────────────────────────────────────────────\n\n echo \"\"\n echo -e \" \\033[1m$(msg docker_proxy.title)\\033[0m\"\n echo \"\"\n echo -e \" $(msg docker_proxy.desc)\"\n echo \"\"\n echo \" 1) $(msg docker_proxy.enable)\"\n echo \" 2) $(msg docker_proxy.disable)\"\n echo \"\"\n\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_DOCKER_PROXY}\" ]; then\n local _proxy_display; if [ \"${AGENTTEAMS_DOCKER_PROXY}\" = \"1\" ]; then _proxy_display=\"$(msg docker_proxy.val_enabled)\"; else _proxy_display=\"$(msg docker_proxy.val_disabled)\"; fi\n log \"$(msg prompt.upgrade_keep \"$(msg docker_proxy.title_short)\" \"${_proxy_display}\")\"\n local _choice\n read -e -p \"$(msg docker_proxy.choice): \" _choice\n if [ \"${_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n if [ -n \"${_choice}\" ]; then\n case \"${_choice}\" in\n 2) AGENTTEAMS_DOCKER_PROXY=\"0\" ;;\n *) AGENTTEAMS_DOCKER_PROXY=\"1\" ;;\n esac\n fi\n elif [ -z \"${AGENTTEAMS_DOCKER_PROXY+x}\" ]; then\n local _choice\n read -e -p \"$(msg docker_proxy.choice): \" _choice\n if [ \"${_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _choice=\"${_choice:-1}\"\n case \"${_choice}\" in\n 2) AGENTTEAMS_DOCKER_PROXY=\"0\" ;;\n *) AGENTTEAMS_DOCKER_PROXY=\"1\" ;;\n esac\n fi\n AGENTTEAMS_DOCKER_PROXY=\"${AGENTTEAMS_DOCKER_PROXY:-1}\"\n export AGENTTEAMS_DOCKER_PROXY\n if [ \"${AGENTTEAMS_DOCKER_PROXY}\" = \"1\" ]; then\n log \"$(msg docker_proxy.selected_enabled)\"\n\n # Prompt for additional allowed image sources\n echo \"\"\n echo -e \" $(msg docker_proxy.registries_desc)\"\n echo \"\"\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_PROXY_ALLOWED_REGISTRIES}\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg docker_proxy.registries_label)\" \"${AGENTTEAMS_PROXY_ALLOWED_REGISTRIES}\")\"\n local _reg_input\n read -e -p \"$(msg docker_proxy.registries_prompt): \" _reg_input\n if [ \"${_reg_input}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n [ -n \"${_reg_input}\" ] && AGENTTEAMS_PROXY_ALLOWED_REGISTRIES=\"${_reg_input}\"\n elif [ -z \"${AGENTTEAMS_PROXY_ALLOWED_REGISTRIES+x}\" ]; then\n local _reg_input\n read -e -p \"$(msg docker_proxy.registries_prompt): \" _reg_input\n if [ \"${_reg_input}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_PROXY_ALLOWED_REGISTRIES=\"${_reg_input:-}\"\n fi\n export AGENTTEAMS_PROXY_ALLOWED_REGISTRIES\n else\n log \"$(msg docker_proxy.selected_disabled)\"\n fi\n}\n\nstep_idle() {\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_WORKER_IDLE_TIMEOUT=\"${AGENTTEAMS_WORKER_IDLE_TIMEOUT:-720}\"\n log \" $(msg idle_timeout.label) = ${AGENTTEAMS_WORKER_IDLE_TIMEOUT} (non-interactive, skipped)\"\n export AGENTTEAMS_WORKER_IDLE_TIMEOUT\n return 0\n fi\n # ─────────────────────────────────────────────────────────────────\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_WORKER_IDLE_TIMEOUT}\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg idle_timeout.label)\" \"${AGENTTEAMS_WORKER_IDLE_TIMEOUT}\")\"\n local _idle_timeout\n read -e -p \"$(msg idle_timeout.prompt): \" _idle_timeout\n if [ \"${_idle_timeout}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n [ -n \"${_idle_timeout}\" ] && AGENTTEAMS_WORKER_IDLE_TIMEOUT=\"${_idle_timeout}\"\n elif [ -z \"${AGENTTEAMS_WORKER_IDLE_TIMEOUT+x}\" ]; then\n local _idle_timeout\n read -e -p \"$(msg idle_timeout.prompt): \" _idle_timeout\n if [ \"${_idle_timeout}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_WORKER_IDLE_TIMEOUT=\"${_idle_timeout:-720}\"\n fi\n AGENTTEAMS_WORKER_IDLE_TIMEOUT=\"${AGENTTEAMS_WORKER_IDLE_TIMEOUT:-720}\"\n export AGENTTEAMS_WORKER_IDLE_TIMEOUT\n log \"$(msg idle_timeout.selected \"${AGENTTEAMS_WORKER_IDLE_TIMEOUT}\")\"\n}\n\nstep_hostshare() {\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_HOST_SHARE_DIR=\"${AGENTTEAMS_HOST_SHARE_DIR:-${HOME}}\"\n log \" $(msg host_share.label) = ${AGENTTEAMS_HOST_SHARE_DIR} (non-interactive, skipped)\"\n export AGENTTEAMS_HOST_SHARE_DIR\n return 0\n fi\n # ─────────────────────────────────────────────────────────────────\n local _current=\"${AGENTTEAMS_HOST_SHARE_DIR}\"\n local _share_dir\n if [ -n \"${_current}\" ]; then\n read -e -p \"$(msg host_share.prompt \"$HOME\") [${_current}]: \" _share_dir\n else\n read -e -p \"$(msg host_share.prompt \"$HOME\"): \" _share_dir\n fi\n if [ \"${_share_dir}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_HOST_SHARE_DIR=\"${_share_dir:-${_current:-$HOME}}\"\n export AGENTTEAMS_HOST_SHARE_DIR\n}\n\nstep_podman_autostart() {\n echo \"\"\n log \"$(msg podman.autostart.title)\"\n echo \"\"\n echo -e \" $(msg podman.autostart.desc)\"\n echo \"\"\n echo \" 1) $(msg podman.autostart.enable)\"\n echo \" 2) $(msg podman.autostart.disable)\"\n echo \"\"\n\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_PODMAN_AUTOSTART}\" ]; then\n local _disp; if [ \"${AGENTTEAMS_PODMAN_AUTOSTART}\" = \"1\" ]; then _disp=\"$(msg podman.autostart.val_enabled)\"; else _disp=\"$(msg podman.autostart.val_disabled)\"; fi\n log \"$(msg prompt.upgrade_keep \"$(msg podman.autostart.title_short)\" \"${_disp}\")\"\n local _choice\n read -e -p \"$(msg podman.autostart.choice): \" _choice\n if [ \"${_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n if [ -n \"${_choice}\" ]; then\n case \"${_choice}\" in\n 2) AGENTTEAMS_PODMAN_AUTOSTART=\"0\" ;;\n *) AGENTTEAMS_PODMAN_AUTOSTART=\"1\" ;;\n esac\n fi\n elif [ -z \"${AGENTTEAMS_PODMAN_AUTOSTART+x}\" ]; then\n local _choice\n read -e -p \"$(msg podman.autostart.choice) [1]: \" _choice\n if [ \"${_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _choice=\"${_choice:-1}\"\n case \"${_choice}\" in\n 2) AGENTTEAMS_PODMAN_AUTOSTART=\"0\" ;;\n *) AGENTTEAMS_PODMAN_AUTOSTART=\"1\" ;;\n esac\n fi\n AGENTTEAMS_PODMAN_AUTOSTART=\"${AGENTTEAMS_PODMAN_AUTOSTART:-1}\"\n export AGENTTEAMS_PODMAN_AUTOSTART\n if [ \"${AGENTTEAMS_PODMAN_AUTOSTART}\" = \"1\" ]; then\n log \"$(msg podman.autostart.selected_enabled)\"\n else\n log \"$(msg podman.autostart.selected_disabled)\"\n fi\n}\n\n# ============================================================\n# Setup dedicated Podman autostart service for AgentTeams\n# ============================================================\nsetup_podman_autostart() {\n # Only execute if using podman and autostart is enabled\n if [ \"${DOCKER_CMD:-docker}\" != \"podman\" ] || [ \"${AGENTTEAMS_PODMAN_AUTOSTART:-0}\" != \"1\" ]; then\n return 0\n fi\n\n log \"$(msg install.podman.autostart_title)\"\n\n local current_user\n current_user=\"$(whoami)\"\n\n # Define the dedicated AgentTeams service content\n # Strict scoping: Only manage containers matching '--filter name=agentteams'\n local _service_content=\"[Unit]\nDescription=AgentTeams Dedicated Podman Autostart Service\nDocumentation=https://github.com/agentscope-ai/AgentTeams\nWants=network-online.target\nAfter=network-online.target\n\n[Service]\nType=oneshot\nRemainAfterExit=true\nEnvironment=LOGGING=\\\"--log-level=info\\\"\nExecStart=/usr/bin/podman \\$LOGGING start --filter name=agentteams --filter restart-policy=always --filter restart-policy=unless-stopped\nExecStop=/usr/bin/podman \\$LOGGING stop --filter name=agentteams --filter restart-policy=always --filter restart-policy=unless-stopped\n\n[Install]\nWantedBy=default.target\"\n\n local _autostart_ok=0\n\n if [ \"${current_user}\" = \"root\" ]; then\n # Rootful Mode (System-wide dedicated service)\n log \"$(msg install.podman.root_setup)\"\n\n local _sys_dir=\"/etc/systemd/system\"\n mkdir -p \"${_sys_dir}\"\n echo \"${_service_content}\" > \"${_sys_dir}/agentteams-podman-restart.service\"\n\n systemctl daemon-reload >/dev/null 2>&1 || true\n\n if systemctl enable --now agentteams-podman-restart.service >/dev/null 2>&1; then\n log \"$(msg install.podman.root_success)\"\n _autostart_ok=1\n else\n log \"$(msg install.podman.root_fail)\"\n fi\n else\n # Rootless Mode (User-level dedicated service)\n log \"$(msg install.podman.user_setup \"${current_user}\")\"\n\n local _linger_ok=0\n\n # 1. Enable Linger for background execution (Graceful escalation strategy)\n if loginctl show-user \"${current_user}\" --property=Linger 2>/dev/null | grep -q \"Linger=yes\"; then\n _linger_ok=1\n else\n log \"$(msg install.podman.linger_enable)\"\n\n # Step A: Try to enable linger natively without sudo\n if loginctl enable-linger \"${current_user}\" >/dev/null 2>&1; then\n log \"Successfully enabled systemd linger natively.\"\n _linger_ok=1\n else\n # Step B: Fallback to sudo\n log \"Native linger enablement failed. Distro security policy requires privileges.\"\n if command -v sudo >/dev/null 2>&1; then\n log \"Attempting to enable systemd linger via sudo (enter password if prompted)...\"\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n sudo -n loginctl enable-linger \"${current_user}\" 2>/dev/null && _linger_ok=1 || log \"$(msg install.podman.linger_warn)\"\n else\n sudo loginctl enable-linger \"${current_user}\" && _linger_ok=1 || log \"$(msg install.podman.linger_warn)\"\n fi\n else\n log \"$(msg install.podman.linger_warn)\"\n fi\n fi\n fi\n\n # 2. Write dedicated service file to user directory\n local _user_dir=\"${HOME}/.config/systemd/user\"\n mkdir -p \"${_user_dir}\"\n echo \"${_service_content}\" > \"${_user_dir}/agentteams-podman-restart.service\"\n\n systemctl --user daemon-reload >/dev/null 2>&1 || true\n\n if systemctl --user enable --now agentteams-podman-restart.service >/dev/null 2>&1; then\n log \"$(msg install.podman.user_success)\"\n # Crucial: Only consider autostart fully successful if systemd linger is also enabled\n if [ \"${_linger_ok}\" = \"1\" ]; then\n _autostart_ok=1\n else\n log \"⚠️ Service enabled, but since systemd linger is not active, containers will not auto-recover on system reboot.\"\n fi\n else\n log \"$(msg install.podman.user_fail)\"\n fi\n fi\n\n # Only print final success banner if the service AND linger were actually enabled\n if [ \"${_autostart_ok}\" = \"1\" ]; then\n log \"$(msg install.podman.success)\"\n fi\n}\n\n# ============================================================\n# Dashboard startup (called after controller is ready)\n# ============================================================\n\n_start_dashboard() {\n local CTRL_CONTAINER=\"agentteams-controller\"\n local DASHBOARD_CONTAINER=\"agentteams-dashboard\"\n\n if [ \"${AGENTTEAMS_DASHBOARD:-0}\" != \"1\" ]; then\n # Stop and remove the existing dashboard container if present.\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -q \"^${DASHBOARD_CONTAINER}$\"; then\n log \"Stopping and removing ${DASHBOARD_CONTAINER}...\"\n ${DOCKER_CMD} stop \"${DASHBOARD_CONTAINER}\" >/dev/null 2>&1 || true\n ${DOCKER_CMD} rm -f \"${DASHBOARD_CONTAINER}\" >/dev/null 2>&1 || true\n log \"Dashboard stopped.\"\n fi\n return 0\n fi\n\n # Dashboard requires the embedded architecture (controller container).\n # In legacy all-in-one mode the controller API is not available.\n if [ \"${AGENTTEAMS_USE_EMBEDDED:-0}\" != \"1\" ]; then\n log \"Skipping Dashboard: requires embedded architecture (agentteams-controller), not available in legacy mode.\"\n log \"Use Quick Start (embedded) mode to enable agentteams-dashboard.\"\n return 0\n fi\n\n AGENTTEAMS_PORT_DASHBOARD=\"${AGENTTEAMS_PORT_DASHBOARD:-13000}\"\n AGENTTEAMS_DASHBOARD_VERSION=\"${AGENTTEAMS_DASHBOARD_VERSION:-v1.2.0}\"\n AGENTTEAMS_DASHBOARD_IMAGE=\"${AGENTTEAMS_DASHBOARD_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-dashboard:${AGENTTEAMS_DASHBOARD_VERSION}}\"\n\n log \"\"\n log \"Starting agentteams-dashboard...\"\n\n local BIND_ADDR=\"127.0.0.1\"\n [ \"${AGENTTEAMS_LOCAL_ONLY:-1}\" = \"0\" ] && BIND_ADDR=\"0.0.0.0\"\n\n # Remove existing container\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' | grep -qx \"${DASHBOARD_CONTAINER}\"; then\n ${DOCKER_CMD} rm -f \"${DASHBOARD_CONTAINER}\" >/dev/null 2>&1 || true\n fi\n\n # Pull or use local image\n if ! ${DOCKER_CMD} image inspect \"${AGENTTEAMS_DASHBOARD_IMAGE}\" >/dev/null 2>&1; then\n log \"Pulling dashboard image: ${AGENTTEAMS_DASHBOARD_IMAGE}\"\n ${DOCKER_CMD} pull \"${AGENTTEAMS_DASHBOARD_IMAGE}\" 2>/dev/null || {\n log \"WARNING: Could not pull dashboard image. Skipping.\"\n return 0\n }\n fi\n\n # Build env args from controller container (MinIO/LLM/auth).\n local env_args=()\n env_args+=(-e AGENTTEAMS_CONTROLLER_URL=\"http://${CTRL_CONTAINER}:8090\")\n env_args+=(-e NEXT_PUBLIC_MATRIX_API_URL=\"http://${CTRL_CONTAINER}:6167\")\n env_args+=(-e MATRIX_HOMESERVER_ALLOWLIST=\"${CTRL_CONTAINER},matrix-local.agentteams.io,matrix.org\")\n\n if ${DOCKER_CMD} ps --format '{{.Names}}' | grep -qx \"${CTRL_CONTAINER}\"; then\n local env_out\n env_out=$(${DOCKER_CMD} inspect \"${CTRL_CONTAINER}\" --format='{{range .Config.Env}}{{.}}{{\"\\n\"}}{{end}}')\n\n _env() { echo \"${env_out}\" | sed -n \"s|^${1}=||p\" | head -n1; }\n\n local fs_endpoint; fs_endpoint=$(_env AGENTTEAMS_FS_ENDPOINT)\n [ -z \"${fs_endpoint}\" ] && fs_endpoint=$(_env AGENTTEAMS_MINIO_ENDPOINT)\n if [ -n \"${fs_endpoint}\" ]; then\n fs_endpoint=$(echo \"${fs_endpoint}\" | sed -e \"s|127\\.0\\.0\\.1|${CTRL_CONTAINER}|\" -e \"s|localhost|${CTRL_CONTAINER}|\")\n env_args+=(-e AGENTTEAMS_FS_ENDPOINT=\"${fs_endpoint}\")\n else\n env_args+=(-e AGENTTEAMS_FS_ENDPOINT=\"http://${CTRL_CONTAINER}:9000\")\n fi\n\n local fs_bucket; fs_bucket=$(_env AGENTTEAMS_FS_BUCKET)\n [ -z \"${fs_bucket}\" ] && fs_bucket=$(_env AGENTTEAMS_MINIO_BUCKET)\n [ -n \"${fs_bucket}\" ] && env_args+=(-e AGENTTEAMS_FS_BUCKET=\"${fs_bucket}\")\n\n local fs_user; fs_user=$(_env AGENTTEAMS_FS_ACCESS_KEY)\n [ -z \"${fs_user}\" ] && fs_user=$(_env AGENTTEAMS_MINIO_USER)\n [ -n \"${fs_user}\" ] && env_args+=(-e AGENTTEAMS_FS_ACCESS_KEY=\"${fs_user}\")\n\n local fs_pass; fs_pass=$(_env AGENTTEAMS_FS_SECRET_KEY)\n [ -z \"${fs_pass}\" ] && fs_pass=$(_env AGENTTEAMS_MINIO_PASSWORD)\n [ -n \"${fs_pass}\" ] && env_args+=(-e AGENTTEAMS_FS_SECRET_KEY=\"${fs_pass}\")\n\n for k in AGENTTEAMS_LLM_PROVIDER AGENTTEAMS_LLM_API_KEY AGENTTEAMS_OPENAI_BASE_URL AGENTTEAMS_DEFAULT_MODEL; do\n local v; v=$(_env \"${k}\")\n [ -n \"${v}\" ] && env_args+=(-e \"${k}=${v}\")\n done\n\n # The controller writes the CLI SA token to /var/run/agentteams/cli-token.\n # The token is generated asynchronously during the first reconcile loop,\n # so it may not be available immediately after the container starts.\n # We poll for up to 30s to handle both fresh installs (token not yet minted)\n # and older images that don't support cli-token at all.\n # For backward compatibility, also try the legacy /var/run/hiclaw/cli-token\n # path used by older HiClaw images.\n local auth_token=\"${AGENTTEAMS_AUTH_TOKEN:-}\"\n if [ -z \"${auth_token}\" ]; then\n local _token_wait=0\n local _token_max_wait=30\n while [ ${_token_wait} -lt ${_token_max_wait} ]; do\n auth_token=$(${DOCKER_CMD} exec \"${CTRL_CONTAINER}\" sh -c 'cat /var/run/agentteams/cli-token 2>/dev/null || cat /var/run/hiclaw/cli-token 2>/dev/null' | tr -d '\\n' || true)\n if [ -n \"${auth_token}\" ]; then\n break\n fi\n sleep 2\n _token_wait=$((_token_wait + 2))\n done\n fi\n if [ -n \"${auth_token}\" ]; then\n env_args+=(-e AGENTTEAMS_AUTH_TOKEN=\"${auth_token}\")\n else\n log \"WARNING: could not read controller SA token (timed out after ${_token_max_wait:-30}s)\"\n log \" This can happen with older AgentTeams/HiClaw images or if the controller is still starting.\"\n log \" Dashboard will still work but some API calls may fail until you log in via Higress Console.\"\n log \" To fix: upgrade AgentTeams to v1.2.0-beta.1+ or set AGENTTEAMS_AUTH_TOKEN manually.\"\n fi\n\n local admin_user; admin_user=$(_env AGENTTEAMS_ADMIN_USER)\n local admin_pass; admin_pass=$(_env AGENTTEAMS_ADMIN_PASSWORD)\n [ -n \"${admin_user}\" ] && env_args+=(-e AGENTTEAMS_ADMIN_USER=\"${admin_user}\")\n [ -n \"${admin_pass}\" ] && env_args+=(-e AGENTTEAMS_ADMIN_PASSWORD=\"${admin_pass}\")\n\n # Higress Console URL: explicit config takes priority, auto-detect as fallback.\n local _resolved_gw_url=\"\"\n if [ -n \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\" ]; then\n # Normalize URL: add http:// prefix if missing\n _resolved_gw_url=\"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\"\n case \"${_resolved_gw_url}\" in\n http://*|https://*) ;;\n *) _resolved_gw_url=\"http://${_resolved_gw_url}\" ;;\n esac\n env_args+=(-e AGENTTEAMS_AI_GATEWAY_ADMIN_URL=\"${_resolved_gw_url}\")\n elif ${DOCKER_CMD} exec \"${CTRL_CONTAINER}\" wget -q -O- --timeout=2 http://127.0.0.1:8001/ >/dev/null 2>&1; then\n _resolved_gw_url=\"http://${CTRL_CONTAINER}:8001\"\n env_args+=(-e AGENTTEAMS_AI_GATEWAY_ADMIN_URL=\"${_resolved_gw_url}\")\n fi\n\n # Verify Higress Console URL reachability (best-effort warning)\n if [ -n \"${_resolved_gw_url}\" ]; then\n if ! ${DOCKER_CMD} exec \"${CTRL_CONTAINER}\" curl -sf --max-time 3 \"${_resolved_gw_url}/\" >/dev/null 2>&1; then\n log \"WARNING: Higress Console URL may not be reachable: ${_resolved_gw_url}\"\n log \" Dashboard will still work, but shared login via Higress Console may fail.\"\n log \" To fix: verify the URL is correct and the Higress Console service is running.\"\n fi\n fi\n else\n log \"WARNING: ${CTRL_CONTAINER} not running; launching Dashboard without Controller/MinIO/LLM env.\"\n if [ -n \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\" ]; then\n local _gw_norm=\"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\"\n case \"${_gw_norm}\" in\n http://*|https://*) ;;\n *) _gw_norm=\"http://${_gw_norm}\" ;;\n esac\n env_args+=(-e AGENTTEAMS_AI_GATEWAY_ADMIN_URL=\"${_gw_norm}\")\n fi\n fi\n\n # Create persistent data volume (for future use)\n ${DOCKER_CMD} volume create agentteams-dashboard-data >/dev/null 2>&1 || true\n\n ${DOCKER_CMD} run -d \\\n --name \"${DASHBOARD_CONTAINER}\" \\\n --restart unless-stopped \\\n --network \"${AGENTTEAMS_NETWORK:-agentteams-net}\" \\\n --network-alias dashboard.agentteams.io \\\n -p \"${BIND_ADDR}:${AGENTTEAMS_PORT_DASHBOARD}:3000\" \\\n \"${env_args[@]}\" \\\n -v agentteams-dashboard-data:/app/db \\\n \"${AGENTTEAMS_DASHBOARD_IMAGE}\"\n\n # Wait for dashboard to be ready\n local max_wait=60\n local waited=0\n while [ $waited -lt $max_wait ]; do\n if curl -sf \"http://127.0.0.1:${AGENTTEAMS_PORT_DASHBOARD}/\" >/dev/null 2>&1; then\n log \"Dashboard ready on port ${AGENTTEAMS_PORT_DASHBOARD}\"\n return 0\n fi\n sleep 2\n waited=$((waited + 2))\n done\n log \"WARNING: Dashboard startup timed out after ${max_wait}s\"\n ${DOCKER_CMD} logs \"${DASHBOARD_CONTAINER}\" 2>&1 | tail -20\n return 0\n}\n\n# ============================================================\n# Manager Installation (Interactive)\n# ============================================================\n\ninstall_manager() {\n log \"$(msg install.title)\"\n log \"$(msg install.registry \"${AGENTTEAMS_REGISTRY}\")\"\n log \"\"\n log \"$(msg install.dir \"$(pwd)\")\"\n log \"$(msg install.dir_hint)\"\n log \"$(msg install.dir_hint2)\"\n log \"\"\n\n # Non-interactive fallback: resolve version immediately so image tags are available\n # before the step state machine runs. Interactive mode lets step_version handle it.\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_VERSION=\"${AGENTTEAMS_VERSION:-${AGENTTEAMS_KNOWN_STABLE_VERSION}}\"\n resolve_image_tags\n fi\n\n # Migrate legacy env file location before checks\n local existing_env=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n if [ ! -f \"${existing_env}\" ] && [ -f \"./agentteams-manager.env\" ]; then\n log \"Migrating agentteams-manager.env from current directory to ${existing_env}...\"\n mv \"./agentteams-manager.env\" \"${existing_env}\"\n fi\n\n # Orphan volume detection (only when no env file — step_existing handles the env-file case)\n if [ ! -f \"${existing_env}\" ]; then\n local data_vol=\"${AGENTTEAMS_DATA_DIR:-agentteams-data}\"\n if ${DOCKER_CMD} volume ls -q | grep -q \"^${data_vol}$\"; then\n echo \"\"\n log \"$(msg install.orphan_volume.detected \"${data_vol}\")\"\n log \"$(msg install.orphan_volume.warn)\"\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n log \"$(msg install.orphan_volume.clean_noninteractive)\"\n ${DOCKER_CMD} stop agentteams-manager 2>/dev/null || true\n ${DOCKER_CMD} rm agentteams-manager 2>/dev/null || true\n for w in $(${DOCKER_CMD} ps -a --format '{{.Names}}' | grep \"^agentteams-worker-\" || true); do\n ${DOCKER_CMD} stop \"${w}\" 2>/dev/null || true\n ${DOCKER_CMD} rm \"${w}\" 2>/dev/null || true\n done\n log \"$(msg install.orphan_volume.cleaning)\"\n ${DOCKER_CMD} volume rm \"${data_vol}\" 2>/dev/null || true\n log \"$(msg install.orphan_volume.cleaned)\"\n else\n echo \"\"\n echo \"$(msg install.orphan_volume.choose)\"\n echo \"$(msg install.orphan_volume.clean)\"\n echo \"$(msg install.orphan_volume.keep)\"\n echo \"\"\n local ORPHAN_CHOICE\n read -e -p \"$(msg install.orphan_volume.prompt): \" ORPHAN_CHOICE\n ORPHAN_CHOICE=\"${ORPHAN_CHOICE:-1}\"\n case \"${ORPHAN_CHOICE}\" in\n 1|clean)\n ${DOCKER_CMD} stop agentteams-manager 2>/dev/null || true\n ${DOCKER_CMD} rm agentteams-manager 2>/dev/null || true\n for w in $(${DOCKER_CMD} ps -a --format '{{.Names}}' | grep \"^agentteams-worker-\" || true); do\n ${DOCKER_CMD} stop \"${w}\" 2>/dev/null || true\n ${DOCKER_CMD} rm \"${w}\" 2>/dev/null || true\n done\n log \"$(msg install.orphan_volume.cleaning)\"\n ${DOCKER_CMD} volume rm \"${data_vol}\" 2>/dev/null || true\n log \"$(msg install.orphan_volume.cleaned)\"\n ;;\n 2|keep)\n log \"$(msg install.orphan_volume.keeping)\"\n ;;\n esac\n fi\n fi\n fi\n\n # ── State machine ─────────────────────────────────────────────────────────\n local _STEPS=( step_lang step_mode step_version step_existing step_llm step_manager_runtime step_runtime step_admin step_network\n step_ports step_domains step_github step_skills step_volume\n step_workspace step_dashboard step_e2ee step_docker_proxy step_idle step_hostshare step_podman_autostart )\n local _STEP_HISTORY=()\n local _step_idx=0\n while [ \"${_step_idx}\" -lt \"${#_STEPS[@]}\" ]; do\n local _step_fn=\"${_STEPS[$_step_idx]}\"\n if should_skip_step \"${_step_fn}\"; then\n _step_idx=$((_step_idx + 1))\n continue\n fi\n if [ \"${#_STEP_HISTORY[@]}\" -gt 0 ]; then\n log \"$(msg nav.back_hint)\"\n fi\n STEP_RESULT=\"\"\n \"${_step_fn}\"\n if [ \"${STEP_RESULT}\" = \"back\" ]; then\n if [ \"${#_STEP_HISTORY[@]}\" -gt 0 ]; then\n local _last=$(( ${#_STEP_HISTORY[@]} - 1 ))\n _step_idx=\"${_STEP_HISTORY[$_last]}\"\n _STEP_HISTORY=(\"${_STEP_HISTORY[@]:0:${_last}}\")\n clear_step_vars \"${_STEPS[$_step_idx]}\"\n fi\n # else: first step, ignore 'b'\n else\n _STEP_HISTORY+=(\"${_step_idx}\")\n _step_idx=$((_step_idx + 1))\n fi\n done\n # ── End state machine ──────────────────────────────────────────────────────\n\n # Post-machine defaults for any steps that were skipped\n AGENTTEAMS_DATA_DIR=\"${AGENTTEAMS_DATA_DIR:-agentteams-data}\"\n if [ -z \"${AGENTTEAMS_WORKSPACE_DIR+x}\" ] || [ -z \"${AGENTTEAMS_WORKSPACE_DIR}\" ]; then\n AGENTTEAMS_WORKSPACE_DIR=\"${HOME}/agentteams-manager\"\n export AGENTTEAMS_WORKSPACE_DIR\n fi\n AGENTTEAMS_WORKSPACE_DIR=\"$(cd \"${AGENTTEAMS_WORKSPACE_DIR}\" 2>/dev/null && pwd || echo \"${AGENTTEAMS_WORKSPACE_DIR}\")\"\n mkdir -p \"${AGENTTEAMS_WORKSPACE_DIR}\"\n AGENTTEAMS_MANAGER_RUNTIME=\"${AGENTTEAMS_MANAGER_RUNTIME:-copaw}\"\n export AGENTTEAMS_MANAGER_RUNTIME\n AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"${AGENTTEAMS_DEFAULT_WORKER_RUNTIME:-copaw}\"\n AGENTTEAMS_MATRIX_E2EE=\"${AGENTTEAMS_MATRIX_E2EE:-0}\"\n export AGENTTEAMS_MATRIX_E2EE\n AGENTTEAMS_WORKER_IDLE_TIMEOUT=\"${AGENTTEAMS_WORKER_IDLE_TIMEOUT:-720}\"\n export AGENTTEAMS_WORKER_IDLE_TIMEOUT\n AGENTTEAMS_HOST_SHARE_DIR=\"${AGENTTEAMS_HOST_SHARE_DIR:-$HOME}\"\n export AGENTTEAMS_HOST_SHARE_DIR\n\n log \"\"\n\n # Generate secrets (only if not already set)\n log \"$(msg install.generating_secrets)\"\n AGENTTEAMS_MANAGER_PASSWORD=\"${AGENTTEAMS_MANAGER_PASSWORD:-$(generate_key)}\"\n AGENTTEAMS_REGISTRATION_TOKEN=\"${AGENTTEAMS_REGISTRATION_TOKEN:-$(generate_key)}\"\n AGENTTEAMS_MINIO_USER=\"${AGENTTEAMS_MINIO_USER:-${AGENTTEAMS_ADMIN_USER}}\"\n AGENTTEAMS_MINIO_PASSWORD=\"${AGENTTEAMS_MINIO_PASSWORD:-${AGENTTEAMS_ADMIN_PASSWORD}}\"\n AGENTTEAMS_MANAGER_GATEWAY_KEY=\"${AGENTTEAMS_MANAGER_GATEWAY_KEY:-$(generate_key)}\"\n\n # Matrix AppService tokens — generate once during install/upgrade if not provided.\n # Persisted to env file so they survive controller restarts.\n AGENTTEAMS_MATRIX_APPSERVICE_ENABLED=\"${AGENTTEAMS_MATRIX_APPSERVICE_ENABLED:-true}\"\n export AGENTTEAMS_MATRIX_APPSERVICE_ENABLED\n if [ \"${AGENTTEAMS_MATRIX_APPSERVICE_ENABLED}\" != \"false\" ] && [ \"${AGENTTEAMS_MATRIX_APPSERVICE_ENABLED}\" != \"0\" ]; then\n if [ -z \"${AGENTTEAMS_MATRIX_APPSERVICE_AS_TOKEN}\" ]; then\n AGENTTEAMS_MATRIX_APPSERVICE_AS_TOKEN=\"$(openssl rand -hex 32)\"\n log \" Auto-generated AppService as_token (saved to env file)\"\n fi\n if [ -z \"${AGENTTEAMS_MATRIX_APPSERVICE_HS_TOKEN}\" ]; then\n AGENTTEAMS_MATRIX_APPSERVICE_HS_TOKEN=\"$(openssl rand -hex 32)\"\n log \" Auto-generated AppService hs_token (saved to env file)\"\n fi\n export AGENTTEAMS_MATRIX_APPSERVICE_AS_TOKEN\n export AGENTTEAMS_MATRIX_APPSERVICE_HS_TOKEN\n fi\n\n # Detect Apple Silicon (M1/M2/M3/M4) - need JVM fix for Higress Console\n # See: https://github.com/agentscope-ai/AgentTeams/issues/249\n if [ -z \"${JVM_ARGS:-}\" ] && [ \"$(uname -m)\" = \"arm64\" ] && [ \"$(uname -s)\" = \"Darwin\" ]; then\n log \"Apple Silicon detected - setting JVM_ARGS to fix Higress Console SIGILL issue\"\n JVM_ARGS=\"-XX:+UnlockDiagnosticVMOptions -XX:-UseAESCTRIntrinsics -XX:UseSVE=0\"\n fi\n\n # Write .env file\n ENV_FILE=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n cat > \"${ENV_FILE}\" << EOF\n# AgentTeams Manager Configuration\n# Generated by agentteams-install.sh on $(date)\n\n# Language\nAGENTTEAMS_LANGUAGE=${AGENTTEAMS_LANGUAGE}\n\n# LLM\nAGENTTEAMS_LLM_PROVIDER=${AGENTTEAMS_LLM_PROVIDER}\nAGENTTEAMS_DEFAULT_MODEL=${AGENTTEAMS_DEFAULT_MODEL}\nAGENTTEAMS_LLM_API_KEY=${AGENTTEAMS_LLM_API_KEY}\nAGENTTEAMS_OPENAI_BASE_URL=${AGENTTEAMS_OPENAI_BASE_URL:-}\nAGENTTEAMS_MODEL_CONTEXT_WINDOW=${AGENTTEAMS_MODEL_CONTEXT_WINDOW:-}\nAGENTTEAMS_MODEL_MAX_TOKENS=${AGENTTEAMS_MODEL_MAX_TOKENS:-}\nAGENTTEAMS_MODEL_REASONING=${AGENTTEAMS_MODEL_REASONING:-}\nAGENTTEAMS_MODEL_VISION=${AGENTTEAMS_MODEL_VISION:-}\n\n# Embedding model (empty = disabled, default: text-embedding-v4)\nAGENTTEAMS_EMBEDDING_MODEL=${AGENTTEAMS_EMBEDDING_MODEL}\n\n# Admin\nAGENTTEAMS_ADMIN_USER=${AGENTTEAMS_ADMIN_USER}\nAGENTTEAMS_ADMIN_PASSWORD=${AGENTTEAMS_ADMIN_PASSWORD}\n\n# Ports\nAGENTTEAMS_LOCAL_ONLY=${AGENTTEAMS_LOCAL_ONLY}\nAGENTTEAMS_PORT_GATEWAY=${AGENTTEAMS_PORT_GATEWAY}\nAGENTTEAMS_PORT_CONSOLE=${AGENTTEAMS_PORT_CONSOLE}\nAGENTTEAMS_PORT_ELEMENT_WEB=${AGENTTEAMS_PORT_ELEMENT_WEB}\nAGENTTEAMS_PORT_MANAGER_CONSOLE=${AGENTTEAMS_PORT_MANAGER_CONSOLE:-18888}\n\n# Manager runtime (openclaw | copaw)\nAGENTTEAMS_MANAGER_RUNTIME=${AGENTTEAMS_MANAGER_RUNTIME:-copaw}\n\n# Matrix\nAGENTTEAMS_MATRIX_DOMAIN=${AGENTTEAMS_MATRIX_DOMAIN}\nAGENTTEAMS_MATRIX_CLIENT_DOMAIN=${AGENTTEAMS_MATRIX_CLIENT_DOMAIN}\n\n# Gateway\nAGENTTEAMS_AI_GATEWAY_DOMAIN=${AGENTTEAMS_AI_GATEWAY_DOMAIN}\nAGENTTEAMS_MANAGER_GATEWAY_KEY=${AGENTTEAMS_MANAGER_GATEWAY_KEY}\n\n# File System\nAGENTTEAMS_FS_DOMAIN=${AGENTTEAMS_FS_DOMAIN}\nAGENTTEAMS_CONSOLE_DOMAIN=${AGENTTEAMS_CONSOLE_DOMAIN}\nAGENTTEAMS_MINIO_USER=${AGENTTEAMS_MINIO_USER}\nAGENTTEAMS_MINIO_PASSWORD=${AGENTTEAMS_MINIO_PASSWORD}\n\n# Internal\nAGENTTEAMS_MANAGER_PASSWORD=${AGENTTEAMS_MANAGER_PASSWORD}\nAGENTTEAMS_REGISTRATION_TOKEN=${AGENTTEAMS_REGISTRATION_TOKEN}\n\n# GitHub (optional)\nAGENTTEAMS_GITHUB_TOKEN=${AGENTTEAMS_GITHUB_TOKEN:-}\n\n# Nacos package import defaults\nAGENTTEAMS_NACOS_REGISTRY_URI=${AGENTTEAMS_NACOS_REGISTRY_URI:-nacos://market.agentteams.io:80/public}\nAGENTTEAMS_NACOS_USERNAME=${AGENTTEAMS_NACOS_USERNAME:-}\nAGENTTEAMS_NACOS_PASSWORD=${AGENTTEAMS_NACOS_PASSWORD:-}\nAGENTTEAMS_NACOS_TOKEN=${AGENTTEAMS_NACOS_TOKEN:-}\n\n# Skills Registry (optional, default: nacos://market.agentteams.io:80/public)\nAGENTTEAMS_SKILLS_API_URL=${AGENTTEAMS_SKILLS_API_URL:-nacos://market.agentteams.io:80/public}\n\n# OpenClaw CMS plugin (optional)\nAGENTTEAMS_CMS_TRACES_ENABLED=${AGENTTEAMS_CMS_TRACES_ENABLED:-false}\nAGENTTEAMS_CMS_ENDPOINT=${AGENTTEAMS_CMS_ENDPOINT:-}\nAGENTTEAMS_CMS_LICENSE_KEY=${AGENTTEAMS_CMS_LICENSE_KEY:-}\nAGENTTEAMS_CMS_PROJECT=${AGENTTEAMS_CMS_PROJECT:-}\nAGENTTEAMS_CMS_WORKSPACE=${AGENTTEAMS_CMS_WORKSPACE:-}\nAGENTTEAMS_CMS_SERVICE_NAME=${AGENTTEAMS_CMS_SERVICE_NAME:-agentteams-manager}\nAGENTTEAMS_CMS_METRICS_ENABLED=${AGENTTEAMS_CMS_METRICS_ENABLED:-false}\n\n# Worker images (for direct container creation)\nAGENTTEAMS_WORKER_IMAGE=${WORKER_IMAGE}\nAGENTTEAMS_COPAW_WORKER_IMAGE=${COPAW_WORKER_IMAGE}\nAGENTTEAMS_QWENPAW_WORKER_IMAGE=${QWENPAW_WORKER_IMAGE}\nAGENTTEAMS_HERMES_WORKER_IMAGE=${HERMES_WORKER_IMAGE}\n\n# Default Worker runtime (openclaw | copaw | qwenpaw | hermes)\nAGENTTEAMS_DEFAULT_WORKER_RUNTIME=${AGENTTEAMS_DEFAULT_WORKER_RUNTIME:-copaw}\n\n# Matrix E2EE (0=disabled, 1=enabled; default: 0)\nAGENTTEAMS_MATRIX_E2EE=${AGENTTEAMS_MATRIX_E2EE:-0}\n\n# Matrix AppService\nAGENTTEAMS_MATRIX_APPSERVICE_ENABLED=${AGENTTEAMS_MATRIX_APPSERVICE_ENABLED:-true}\nAGENTTEAMS_MATRIX_APPSERVICE_AS_TOKEN=${AGENTTEAMS_MATRIX_APPSERVICE_AS_TOKEN:-}\nAGENTTEAMS_MATRIX_APPSERVICE_HS_TOKEN=${AGENTTEAMS_MATRIX_APPSERVICE_HS_TOKEN:-}\n\n# Docker API proxy (0=disabled, 1=enabled; default: 1)\nAGENTTEAMS_DOCKER_PROXY=${AGENTTEAMS_DOCKER_PROXY:-1}\n\n# Docker API proxy: additional allowed image sources (comma-separated)\nAGENTTEAMS_PROXY_ALLOWED_REGISTRIES=${AGENTTEAMS_PROXY_ALLOWED_REGISTRIES:-}\n\n# Worker idle timeout in minutes (default: 720 = 12 hours)\nAGENTTEAMS_WORKER_IDLE_TIMEOUT=${AGENTTEAMS_WORKER_IDLE_TIMEOUT:-720}\n\n# Podman autostart via systemd (0=disabled, 1=enabled)\nAGENTTEAMS_PODMAN_AUTOSTART=${AGENTTEAMS_PODMAN_AUTOSTART:-0}\n\n# JVM Args for Higress Console (fixes SIGILL on Apple Silicon)\nJVM_ARGS=${JVM_ARGS:-}\n\n# Higress WASM plugin image registry (auto-selected by timezone)\nHIGRESS_ADMIN_WASM_PLUGIN_IMAGE_REGISTRY=${AGENTTEAMS_REGISTRY}\n\n# Data persistence\nAGENTTEAMS_DATA_DIR=${AGENTTEAMS_DATA_DIR:-agentteams-data}\n# Manager workspace (skills, memory, state — host-editable)\nAGENTTEAMS_WORKSPACE_DIR=${AGENTTEAMS_WORKSPACE_DIR:-}\n# Host directory sharing\nAGENTTEAMS_HOST_SHARE_DIR=${AGENTTEAMS_HOST_SHARE_DIR:-}\n\n# agentteams-dashboard (management UI)\nAGENTTEAMS_DASHBOARD=${AGENTTEAMS_DASHBOARD:-1}\nAGENTTEAMS_DASHBOARD_VERSION=${AGENTTEAMS_DASHBOARD_VERSION:-v1.2.0}\nAGENTTEAMS_PORT_DASHBOARD=${AGENTTEAMS_PORT_DASHBOARD:-13000}\nAGENTTEAMS_DASHBOARD_IMAGE=${AGENTTEAMS_DASHBOARD_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-dashboard:${AGENTTEAMS_DASHBOARD_VERSION}}\nAGENTTEAMS_AI_GATEWAY_ADMIN_URL=${AGENTTEAMS_AI_GATEWAY_ADMIN_URL:-}\nEOF\n\n chmod 600 \"${ENV_FILE}\"\n log \"$(msg install.config_saved \"${ENV_FILE}\")\"\n\n # Detect container runtime socket\n SOCKET_MOUNT_ARGS=\"\"\n if [ \"${AGENTTEAMS_MOUNT_SOCKET}\" = \"1\" ]; then\n # Actively ensure the API socket is enabled and active before detection.\n if [ \"${DOCKER_CMD:-}\" = \"podman\" ]; then\n ensure_podman_socket\n fi\n CONTAINER_SOCK=$(detect_socket)\n if [ -n \"${CONTAINER_SOCK}\" ]; then\n log \"$(msg install.socket_detected \"${CONTAINER_SOCK}\")\"\n SOCKET_MOUNT_ARGS=\"-v ${CONTAINER_SOCK}:/var/run/docker.sock --security-opt label=disable\"\n else\n log \"$(msg install.socket_not_found)\"\n # Interactive confirmation when socket not found\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" != \"1\" ]; then\n echo \"\"\n echo -e \"\\033[33m$(msg install.socket_confirm.title)\\033[0m\"\n echo \"\"\n echo -e \"$(msg install.socket_confirm.message)\"\n echo \"\"\n read -p \"$(msg install.socket_confirm.prompt)\" SOCKET_CONFIRM\n if [ \"${SOCKET_CONFIRM}\" != \"y\" ] && [ \"${SOCKET_CONFIRM}\" != \"Y\" ]; then\n log \"$(msg install.socket_confirm.cancelled)\"\n exit 0\n fi\n fi\n fi\n fi\n\n # Create the data volume if it doesn't already exist (reuse on reinstall)\n if ! ${DOCKER_CMD} volume ls -q | grep -q \"^${AGENTTEAMS_DATA_DIR}$\"; then\n ${DOCKER_CMD} volume create \"${AGENTTEAMS_DATA_DIR}\" > /dev/null\n fi\n\n # Data mount: Docker volume\n DATA_MOUNT_ARGS=\"-v ${AGENTTEAMS_DATA_DIR}:/data\"\n\n # Manager workspace mount (always a host directory, defaulting to ~/agentteams-manager)\n WORKSPACE_MOUNT_ARGS=\"-v ${AGENTTEAMS_WORKSPACE_DIR}:/root/manager-workspace\"\n\n # Pass host timezone to container so date/time commands reflect local time\n TZ_ARGS=\"-e TZ=${AGENTTEAMS_TIMEZONE}\"\n\n # Host directory mount\n if [ -d \"${AGENTTEAMS_HOST_SHARE_DIR}\" ]; then\n HOST_SHARE_MOUNT_ARGS=\"-v ${AGENTTEAMS_HOST_SHARE_DIR}:/host-share\"\n log \"$(msg host_share.sharing \"${AGENTTEAMS_HOST_SHARE_DIR}\")\"\n else\n log \"$(msg host_share.not_exist \"${AGENTTEAMS_HOST_SHARE_DIR}\")\"\n HOST_SHARE_MOUNT_ARGS=\"-v ${AGENTTEAMS_HOST_SHARE_DIR}:/host-share\"\n fi\n\n # YOLO mode: pass through if set in environment (enables autonomous decisions)\n YOLO_ARGS=\"\"\n if [ \"${AGENTTEAMS_YOLO:-}\" = \"1\" ]; then\n YOLO_ARGS=\"-e AGENTTEAMS_YOLO=1\"\n log \"$(msg install.yolo)\"\n fi\n\n # Matrix-plugin debug tracing: pass through if AGENTTEAMS_MATRIX_DEBUG=1.\n # The container entrypoints translate this to OPENCLAW_MATRIX_DEBUG=1\n # so the openclaw matrix plugin emits structured INFO-level lifecycle\n # traces (sync state transitions, room.invite/join, message handler\n # arrival + filter outcomes). Used to diagnose worker/manager hangs.\n MATRIX_DEBUG_ARGS=\"\"\n if [ \"${AGENTTEAMS_MATRIX_DEBUG:-}\" = \"1\" ]; then\n MATRIX_DEBUG_ARGS=\"-e AGENTTEAMS_MATRIX_DEBUG=1\"\n fi\n\n # E2EE is already in the env file; but also pass explicitly in case env file is not the source\n # (AGENTTEAMS_MATRIX_E2EE is already written to ENV_FILE above via --env-file)\n\n # Pull images (manager based on runtime config; all worker runtimes always pulled)\n _is_local_image() {\n case \"$1\" in\n agentteams/*) return 0 ;;\n *) return 1 ;;\n esac\n }\n\n _local_image_exists() {\n local _img=\"$1\"\n [ -z \"${_img}\" ] && return 1\n _is_local_image \"${_img}\" || return 1\n ${DOCKER_CMD} image inspect \"${_img}\" >/dev/null 2>&1\n }\n\n # Helper: pull or skip a single image\n # Args: $1=image $2=exists_msg_key $3=pulling_msg_key\n _pull_image() {\n local _img=\"$1\" _exists_key=\"$2\" _pull_key=\"$3\"\n [ -z \"${_img}\" ] && return 0\n if _local_image_exists \"${_img}\"; then\n log \"$(msg \"${_exists_key}\" \"${_img}\")\"\n return 0\n fi\n log \"$(msg \"${_pull_key}\" \"${_img}\")\"\n local _attempt=1\n while [ $_attempt -le 3 ]; do\n if ${DOCKER_CMD} pull \"${_img}\"; then\n return 0\n fi\n if [ $_attempt -lt 3 ]; then\n log \"Pull failed (attempt ${_attempt}/3), retrying in 5s...\"\n sleep 5\n fi\n _attempt=$((_attempt + 1))\n done\n die \"Failed to pull ${_img} after 3 attempts\"\n return 1\n }\n\n # Embedded controller image (resolve versioned tag, fallback to latest)\n resolve_embedded_image\n if [ \"${AGENTTEAMS_USE_EMBEDDED}\" = \"1\" ]; then\n _local_image_exists \"${EMBEDDED_IMAGE}\" || true\n fi\n\n # Manager image is always required (select based on runtime)\n if [ \"${AGENTTEAMS_MANAGER_RUNTIME}\" = \"copaw\" ]; then\n _pull_image \"${MANAGER_COPAW_IMAGE}\" \"install.image.exists\" \"install.image.pulling_manager\"\n else\n _pull_image \"${MANAGER_IMAGE}\" \"install.image.exists\" \"install.image.pulling_manager\"\n fi\n\n # Pull all worker runtime images (workers may use any runtime regardless of the default)\n _pull_image \"${WORKER_IMAGE}\" \"install.image.worker_exists\" \"install.image.pulling_worker\"\n _pull_image \"${COPAW_WORKER_IMAGE}\" \"install.image.worker_exists\" \"install.image.pulling_worker\"\n # Temporarily disabled until the QwenPaw worker image is published.\n # _pull_image \"${QWENPAW_WORKER_IMAGE}\" \"install.image.worker_exists\" \"install.image.pulling_worker\"\n _pull_image \"${HERMES_WORKER_IMAGE}\" \"install.image.worker_exists\" \"install.image.pulling_worker\"\n\n # --- Pre-upgrade: extract Matrix passwords from running old containers ---\n # Only needed when upgrading FROM old architecture (v1.0.9) TO embedded.\n # For new-arch-to-new-arch upgrades, credential files already exist with\n # correct room IDs — we must NOT overwrite them.\n _creds_tmp=\"\"\n if [ \"${AGENTTEAMS_UPGRADE:-0}\" = \"1\" ] && [ \"${AGENTTEAMS_USE_EMBEDDED}\" = \"1\" ]; then\n # Detect if upgrading from old arch: old arch has no agentteams-controller container\n # (or has it as a docker-proxy only, not embedded). Check if the existing\n # agentteams-controller is an embedded image (has supervisord) or just a proxy.\n _is_old_arch=0\n if ! ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -q \"^agentteams-controller$\"; then\n _is_old_arch=1\n elif ${DOCKER_CMD} ps -a --format '{{.Names}} {{.Image}}' 2>/dev/null | grep \"^agentteams-controller \" | grep -qv \"embedded\"; then\n _is_old_arch=1\n fi\n\n if [ \"${_is_old_arch}\" = \"1\" ]; then\n _creds_tmp=$(mktemp -d)\n\n # docker exec for Matrix/minio paths only works while agentteams-manager is running.\n _mgr_creds_tempstart=0\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -q '^agentteams-manager$'; then\n if ! ${DOCKER_CMD} ps --format '{{.Names}}' 2>/dev/null | grep -q '^agentteams-manager$'; then\n log \"agentteams-manager is stopped; starting it temporarily to extract Matrix credentials for upgrade...\"\n ${DOCKER_CMD} start agentteams-manager 2>/dev/null || true\n wait_matrix_ready \"agentteams-manager\"\n _mgr_creds_tempstart=1\n fi\n fi\n\n # Manager password (container Config.Env, then secrets file inside running manager, then data volume)\n _mgr_pw=$(${DOCKER_CMD} inspect agentteams-manager --format '{{range .Config.Env}}{{println .}}{{end}}' 2>/dev/null | grep '^AGENTTEAMS_MANAGER_PASSWORD=' | cut -d= -f2-)\n if [ -z \"${_mgr_pw}\" ] && ${DOCKER_CMD} ps --format '{{.Names}}' 2>/dev/null | grep -q '^agentteams-manager$'; then\n _mgr_pw=$(${DOCKER_CMD} exec agentteams-manager bash -c 'source /data/agentteams-secrets.env 2>/dev/null && echo \"${AGENTTEAMS_MANAGER_PASSWORD}\"' 2>/dev/null)\n fi\n if [ -z \"${_mgr_pw}\" ] && ${DOCKER_CMD} volume ls -q 2>/dev/null | grep -q \"^${AGENTTEAMS_DATA_DIR}$\"; then\n _mgr_pw=$(agentteams_read_secret_from_data_volume \"${AGENTTEAMS_DATA_DIR}\" AGENTTEAMS_MANAGER_PASSWORD)\n fi\n\n # Manager admin DM room ID: login as admin, find DM room with @manager\n _mgr_room=\"\"\n if [ -n \"${_mgr_pw}\" ]; then\n _admin_pw=$(grep AGENTTEAMS_ADMIN_PASSWORD \"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\" 2>/dev/null | cut -d= -f2-)\n _admin_user=$(grep AGENTTEAMS_ADMIN_USER \"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\" 2>/dev/null | cut -d= -f2-)\n _admin_user=\"${_admin_user:-admin}\"\n _matrix_domain=$(grep AGENTTEAMS_MATRIX_DOMAIN \"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\" 2>/dev/null | cut -d= -f2-)\n if [ -n \"${_admin_pw}\" ] && ${DOCKER_CMD} ps --format '{{.Names}}' 2>/dev/null | grep -q '^agentteams-manager$'; then\n _admin_token=$(${DOCKER_CMD} exec agentteams-manager curl -sf -X POST http://127.0.0.1:6167/_matrix/client/v3/login \\\n -H \"Content-Type: application/json\" \\\n -d '{\"type\":\"m.login.password\",\"identifier\":{\"type\":\"m.id.user\",\"user\":\"'\"${_admin_user}\"'\"},\"password\":\"'\"${_admin_pw}\"'\"}' 2>/dev/null | python3 -c \"import sys,json; print(json.load(sys.stdin).get('access_token',''))\" 2>/dev/null || true)\n if [ -n \"${_admin_token}\" ]; then\n _mgr_room=$(${DOCKER_CMD} exec agentteams-manager curl -sf -X GET \\\n -H \"Authorization: Bearer ${_admin_token}\" \\\n \"http://127.0.0.1:6167/_matrix/client/v3/joined_rooms\" 2>/dev/null | python3 -c \"\nimport sys,json,subprocess\nrooms = json.load(sys.stdin).get('joined_rooms',[])\nfor room_id in rooms:\n enc = room_id.replace('!','%21')\n members = json.loads(subprocess.check_output([\n 'docker','exec','agentteams-manager','curl','-sf','-X','GET',\n '-H','Authorization: Bearer ${_admin_token}',\n 'http://127.0.0.1:6167/_matrix/client/v3/rooms/'+enc+'/members'\n ]).decode()).get('chunk',[])\n member_ids = [m['state_key'] for m in members]\n if any('manager' in m and 'admin' not in m.split(':')[0] for m in member_ids):\n if len(member_ids) <= 3:\n print(room_id)\n break\n\" 2>/dev/null || true)\n fi\n fi\n if [ -z \"${_mgr_room}\" ]; then\n _mgr_room=$(agentteams_read_admin_dm_room_from_workspace \"${AGENTTEAMS_WORKSPACE_DIR}\")\n fi\n cat > \"${_creds_tmp}/default.env\" <<CREDEOF\nWORKER_PASSWORD=\"${_mgr_pw}\"\nWORKER_MINIO_PASSWORD=\"$(openssl rand -hex 24)\"\nWORKER_GATEWAY_KEY=\"${AGENTTEAMS_MANAGER_GATEWAY_KEY}\"\nWORKER_ROOM_ID=\"${_mgr_room}\"\nCREDEOF\n log \"Extracted Manager Matrix password${_mgr_room:+ and room ID}\"\n fi\n\n # Worker passwords and room IDs from workers-registry.json\n if [ -f \"${AGENTTEAMS_WORKSPACE_DIR}/workers-registry.json\" ]; then\n _worker_names=$(python3 -c \"import json; d=json.load(open('${AGENTTEAMS_WORKSPACE_DIR}/workers-registry.json')); print(' '.join(d.get('workers',{}).keys()))\" 2>/dev/null || true)\n for _wname in ${_worker_names}; do\n _wpw=\"\"\n if ${DOCKER_CMD} ps --format '{{.Names}}' 2>/dev/null | grep -q '^agentteams-manager$'; then\n _wpw=$(${DOCKER_CMD} exec agentteams-manager cat \"/root/agentteams-fs/agents/${_wname}/credentials/matrix/password\" 2>/dev/null || true)\n fi\n if [ -z \"${_wpw}\" ] && ${DOCKER_CMD} volume ls -q 2>/dev/null | grep -q \"^${AGENTTEAMS_DATA_DIR}$\"; then\n _wpw=$(agentteams_read_worker_creds_value_from_volume \"${AGENTTEAMS_DATA_DIR}\" \"${_wname}\" WORKER_PASSWORD)\n fi\n _wroom=$(python3 -c \"import json; d=json.load(open('${AGENTTEAMS_WORKSPACE_DIR}/workers-registry.json')); print(d.get('workers',{}).get('${_wname}',{}).get('room_id',''))\" 2>/dev/null || true)\n if [ -z \"${_wroom}\" ] && ${DOCKER_CMD} volume ls -q 2>/dev/null | grep -q \"^${AGENTTEAMS_DATA_DIR}$\"; then\n _wroom=$(agentteams_read_worker_creds_value_from_volume \"${AGENTTEAMS_DATA_DIR}\" \"${_wname}\" WORKER_ROOM_ID)\n fi\n if [ -n \"${_wpw}\" ]; then\n cat > \"${_creds_tmp}/${_wname}.env\" <<CREDEOF\nWORKER_PASSWORD=\"${_wpw}\"\nWORKER_MINIO_PASSWORD=\"$(openssl rand -hex 24)\"\nWORKER_GATEWAY_KEY=\"$(openssl rand -hex 32)\"\nWORKER_ROOM_ID=\"${_wroom}\"\nCREDEOF\n log \"Extracted ${_wname} Matrix password${_wroom:+ and room ID}\"\n fi\n done\n fi\n\n if [ \"${_mgr_creds_tempstart}\" = \"1\" ]; then\n log \"Stopping agentteams-manager after credential extraction (upgrade will recreate containers)...\"\n ${DOCKER_CMD} stop agentteams-manager 2>/dev/null || true\n fi\n fi # _is_old_arch\n fi\n\n # --- Stop and remove existing containers ---\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' | grep -q \"^agentteams-controller$\"; then\n ${DOCKER_CMD} stop agentteams-controller 2>/dev/null || true\n ${DOCKER_CMD} rm agentteams-controller 2>/dev/null || true\n fi\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' | grep -q \"^agentteams-manager$\"; then\n log \"$(msg install.removing_existing)\"\n ${DOCKER_CMD} stop agentteams-manager 2>/dev/null || true\n ${DOCKER_CMD} rm agentteams-manager 2>/dev/null || true\n fi\n\n # Stop and remove worker containers (controller will recreate via CR reconciliation)\n if [ -n \"${UPGRADE_EXISTING_WORKERS:-}\" ]; then\n log \"$(msg install.existing.stopping_workers)\"\n for w in ${UPGRADE_EXISTING_WORKERS}; do\n ${DOCKER_CMD} stop \"${w}\" 2>/dev/null || true\n ${DOCKER_CMD} rm \"${w}\" 2>/dev/null || true\n log \"$(msg install.existing.removed \"${w}\")\"\n done\n fi\n\n # Clean up legacy containers from v1.0.x and earlier\n for _legacy_name in agentteams-docker-proxy; do\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -qx \"${_legacy_name}\"; then\n log \"Removing legacy container: ${_legacy_name}\"\n ${DOCKER_CMD} stop \"${_legacy_name}\" 2>/dev/null || true\n ${DOCKER_CMD} rm -f \"${_legacy_name}\" 2>/dev/null || true\n fi\n done\n\n # --- Upgrade: inject extracted credentials into data volume ---\n # Only needed for old-arch upgrades (credential files were extracted above).\n if [ -n \"${_creds_tmp}\" ] && [ -d \"${_creds_tmp}\" ] && [ -n \"$(ls -A \"${_creds_tmp}\" 2>/dev/null)\" ]; then\n local _cleanup_ctr=\"agentteams-upgrade-cleanup\"\n ${DOCKER_CMD} rm -f \"${_cleanup_ctr}\" 2>/dev/null || true\n ${DOCKER_CMD} run --rm --name \"${_cleanup_ctr}\" \\\n --entrypoint sh \\\n -v \"${AGENTTEAMS_DATA_DIR}:/data\" \\\n -v \"${_creds_tmp}:/creds:ro\" \\\n \"${EMBEDDED_IMAGE}\" -c '\n rm -rf /data/worker-creds\n mkdir -p /data/worker-creds\n cp /creds/*.env /data/worker-creds/ 2>/dev/null || true\n chmod 600 /data/worker-creds/*.env 2>/dev/null || true\n ' 2>/dev/null && log \"Injected credentials for upgrade\" || log \"Warning: credential injection failed, continuing\"\n rm -rf \"${_creds_tmp}\"\n fi\n\n # --- Start containers ---\n log \"$(msg install.starting_manager)\"\n\n # Build port binding args\n if [ \"${AGENTTEAMS_LOCAL_ONLY:-1}\" = \"1\" ]; then\n _port_prefix=\"127.0.0.1:\"\n else\n _port_prefix=\"\"\n fi\n\n # Ensure agentteams-net Docker network exists\n ${DOCKER_CMD} network inspect agentteams-net >/dev/null 2>&1 || ${DOCKER_CMD} network create agentteams-net\n\n if [ \"${AGENTTEAMS_USE_EMBEDDED}\" != \"1\" ] && [ \"${AGENTTEAMS_UPGRADE:-0}\" = \"1\" ]; then\n # Check if current installation is embedded — downgrade to legacy is not supported\n if ${DOCKER_CMD} ps -a --format '{{.Names}} {{.Image}}' 2>/dev/null | grep \"^agentteams-controller \" | grep -q \"embedded\"; then\n error \"Downgrade from embedded architecture to legacy version (${AGENTTEAMS_VERSION}) is not supported.\"\n error \"Please use 'make uninstall-embedded' first, then do a clean install of the target version.\"\n exit 1\n fi\n fi\n\n if [ \"${AGENTTEAMS_USE_EMBEDDED}\" = \"1\" ]; then\n # ============================================================\n # New architecture: embedded controller + auto-created manager\n # ============================================================\n\n # Internal port: 8080 (Higress gateway inside the container).\n local _internal_gw_port=8080\n local _matrix_domain=\"${AGENTTEAMS_MATRIX_DOMAIN:-matrix-local.agentteams.io:${AGENTTEAMS_PORT_GATEWAY}}\"\n local _aigw_domain=\"${AGENTTEAMS_AI_GATEWAY_DOMAIN:-aigw-local.agentteams.io}\"\n # Ensure internal gateway port is present (container-internal traffic uses 8080)\n case \"${_aigw_domain}\" in *:*) ;; *) _aigw_domain=\"${_aigw_domain}:${_internal_gw_port}\" ;; esac\n local _fs_domain=\"${AGENTTEAMS_FS_DOMAIN:-fs-local.agentteams.io}\"\n case \"${_fs_domain}\" in *:*) ;; *) _fs_domain=\"${_fs_domain}:${_internal_gw_port}\" ;; esac\n\n # Controller env args\n local _ctrl_env_prefix\n _ctrl_env_prefix=\"$(_controller_env_prefix \"${AGENTTEAMS_VERSION}\")\"\n local _storage_prefix\n _storage_prefix=\"$(_controller_storage_prefix \"${AGENTTEAMS_VERSION}\")\"\n local _ctrl_env_args=(\n -e \"${_ctrl_env_prefix}ADMIN_USER=${AGENTTEAMS_ADMIN_USER}\"\n -e \"${_ctrl_env_prefix}ADMIN_PASSWORD=${AGENTTEAMS_ADMIN_PASSWORD}\"\n -e \"${_ctrl_env_prefix}MANAGER_PASSWORD=${AGENTTEAMS_MANAGER_PASSWORD}\"\n -e \"${_ctrl_env_prefix}REGISTRATION_TOKEN=${AGENTTEAMS_REGISTRATION_TOKEN}\"\n -e \"${_ctrl_env_prefix}MINIO_USER=${AGENTTEAMS_MINIO_USER}\"\n -e \"${_ctrl_env_prefix}MINIO_PASSWORD=${AGENTTEAMS_MINIO_PASSWORD}\"\n -e \"${_ctrl_env_prefix}LLM_PROVIDER=${AGENTTEAMS_LLM_PROVIDER}\"\n -e \"${_ctrl_env_prefix}LLM_API_KEY=${AGENTTEAMS_LLM_API_KEY}\"\n -e \"${_ctrl_env_prefix}DEFAULT_MODEL=${AGENTTEAMS_DEFAULT_MODEL}\"\n -e \"${_ctrl_env_prefix}MANAGER_GATEWAY_KEY=${AGENTTEAMS_MANAGER_GATEWAY_KEY}\"\n -e \"${_ctrl_env_prefix}MANAGER_RUNTIME=${AGENTTEAMS_MANAGER_RUNTIME:-copaw}\"\n -e \"${_ctrl_env_prefix}MANAGER_IMAGE=$([ \"${AGENTTEAMS_MANAGER_RUNTIME}\" = \"copaw\" ] && echo \"${MANAGER_COPAW_IMAGE}\" || echo \"${MANAGER_IMAGE}\")\"\n -e \"${_ctrl_env_prefix}DEFAULT_WORKER_RUNTIME=${AGENTTEAMS_DEFAULT_WORKER_RUNTIME:-copaw}\"\n -e \"${_ctrl_env_prefix}WORKER_IMAGE=${WORKER_IMAGE}\"\n -e \"${_ctrl_env_prefix}COPAW_WORKER_IMAGE=${COPAW_WORKER_IMAGE}\"\n -e \"${_ctrl_env_prefix}QWENPAW_WORKER_IMAGE=${QWENPAW_WORKER_IMAGE}\"\n -e \"${_ctrl_env_prefix}HERMES_WORKER_IMAGE=${HERMES_WORKER_IMAGE}\"\n -e \"${_ctrl_env_prefix}MATRIX_DOMAIN=${_matrix_domain}\"\n -e \"${_ctrl_env_prefix}ELEMENT_HOMESERVER_URL=http://127.0.0.1:${AGENTTEAMS_PORT_GATEWAY}\"\n -e \"${_ctrl_env_prefix}MATRIX_URL=http://127.0.0.1:6167\"\n -e \"${_ctrl_env_prefix}MATRIX_E2EE=${AGENTTEAMS_MATRIX_E2EE:-0}\"\n -e \"${_ctrl_env_prefix}MINIO_ENDPOINT=http://127.0.0.1:9000\"\n -e \"${_ctrl_env_prefix}MINIO_BUCKET=agentteams-storage\"\n -e \"${_ctrl_env_prefix}STORAGE_PREFIX=${_storage_prefix}\"\n -e \"${_ctrl_env_prefix}FS_ENDPOINT=http://127.0.0.1:9000\"\n -e \"${_ctrl_env_prefix}AI_GATEWAY_URL=http://${_aigw_domain}\"\n -e \"${_ctrl_env_prefix}CONTROLLER_URL=http://agentteams-controller:8090\"\n -e \"${_ctrl_env_prefix}DOCKER_NETWORK=agentteams-net\"\n -e \"${_ctrl_env_prefix}WORKSPACE_DIR=${AGENTTEAMS_WORKSPACE_DIR}\"\n -e \"${_ctrl_env_prefix}HOST_SHARE_DIR=${AGENTTEAMS_HOST_SHARE_DIR}\"\n -e \"${_ctrl_env_prefix}MANAGER_ENABLED=true\"\n -e \"${_ctrl_env_prefix}PORT_MANAGER_CONSOLE=${AGENTTEAMS_PORT_MANAGER_CONSOLE:-18888}\"\n )\n if _use_legacy_image_env \"${AGENTTEAMS_VERSION}\"; then\n _ctrl_env_args+=(\n -e \"${_ctrl_env_prefix}FS_BUCKET=agentteams-storage\"\n -e \"${_ctrl_env_prefix}RESOURCE_PREFIX=agentteams-\"\n )\n else\n _ctrl_env_args+=(\n -e \"${_ctrl_env_prefix}MATRIX_APPSERVICE_ENABLED=${AGENTTEAMS_MATRIX_APPSERVICE_ENABLED:-true}\"\n -e \"${_ctrl_env_prefix}MATRIX_APPSERVICE_AS_TOKEN=${AGENTTEAMS_MATRIX_APPSERVICE_AS_TOKEN:-}\"\n -e \"${_ctrl_env_prefix}MATRIX_APPSERVICE_HS_TOKEN=${AGENTTEAMS_MATRIX_APPSERVICE_HS_TOKEN:-}\"\n )\n fi\n\n # Timezone\n if [ -n \"${AGENTTEAMS_TIMEZONE:-}\" ]; then\n _ctrl_env_args+=(-e \"TZ=${AGENTTEAMS_TIMEZONE}\")\n fi\n\n # Yolo mode\n if [ \"${AGENTTEAMS_YOLO:-}\" = \"1\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}YOLO=1\")\n fi\n\n # Matrix-plugin debug tracing — propagated to every manager + worker\n # the controller spawns, then translated to OPENCLAW_MATRIX_DEBUG=1\n # by the container entrypoints. Use this to diagnose\n # \"worker did not join\" / \"manager replied empty\" hangs.\n if [ \"${AGENTTEAMS_MATRIX_DEBUG:-}\" = \"1\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}MATRIX_DEBUG=1\")\n fi\n\n # Optional: GitHub token\n if [ -n \"${AGENTTEAMS_GITHUB_TOKEN:-}\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}GITHUB_TOKEN=${AGENTTEAMS_GITHUB_TOKEN}\")\n fi\n\n # Optional: embedding model\n if [ -n \"${AGENTTEAMS_EMBEDDING_MODEL:-}\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}EMBEDDING_MODEL=${AGENTTEAMS_EMBEDDING_MODEL}\")\n fi\n\n # Optional: OpenAI-compatible base URL\n if [ -n \"${AGENTTEAMS_OPENAI_BASE_URL:-}\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}OPENAI_BASE_URL=${AGENTTEAMS_OPENAI_BASE_URL}\")\n fi\n # Optional: language\n if [ -n \"${AGENTTEAMS_LANGUAGE:-}\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}LANGUAGE=${AGENTTEAMS_LANGUAGE}\")\n fi\n\n # Optional: CMS/ARMS observability. In embedded mode the controller\n # spawns the Manager and Workers, so it must receive these settings.\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}CMS_TRACES_ENABLED=${AGENTTEAMS_CMS_TRACES_ENABLED:-false}\")\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}CMS_SERVICE_NAME=${AGENTTEAMS_CMS_SERVICE_NAME:-agentteams-manager}\")\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}CMS_METRICS_ENABLED=${AGENTTEAMS_CMS_METRICS_ENABLED:-false}\")\n if [ -n \"${AGENTTEAMS_CMS_ENDPOINT:-}\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}CMS_ENDPOINT=${AGENTTEAMS_CMS_ENDPOINT}\")\n fi\n if [ -n \"${AGENTTEAMS_CMS_LICENSE_KEY:-}\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}CMS_LICENSE_KEY=${AGENTTEAMS_CMS_LICENSE_KEY}\")\n fi\n if [ -n \"${AGENTTEAMS_CMS_PROJECT:-}\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}CMS_PROJECT=${AGENTTEAMS_CMS_PROJECT}\")\n fi\n if [ -n \"${AGENTTEAMS_CMS_WORKSPACE:-}\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}CMS_WORKSPACE=${AGENTTEAMS_CMS_WORKSPACE}\")\n fi\n\n # shellcheck disable=SC2086\n ${DOCKER_CMD} run -d \\\n --name agentteams-controller \\\n --network agentteams-net \\\n --network-alias matrix-local.agentteams.io \\\n --network-alias aigw-local.agentteams.io \\\n --network-alias fs-local.agentteams.io \\\n \"${_ctrl_env_args[@]}\" \\\n -v \"${CONTAINER_SOCK}:/var/run/docker.sock\" \\\n --security-opt label=disable \\\n -v \"${AGENTTEAMS_DATA_DIR}:/data\" \\\n -v \"${AGENTTEAMS_WORKSPACE_DIR}:/root/agentteams-fs/agents/manager\" \\\n -p \"${_port_prefix}${AGENTTEAMS_PORT_GATEWAY}:8080\" \\\n -p \"${_port_prefix}${AGENTTEAMS_PORT_CONSOLE}:8001\" \\\n -p \"${_port_prefix}${AGENTTEAMS_PORT_ELEMENT_WEB:-18088}:8088\" \\\n --restart unless-stopped \\\n \"${EMBEDDED_IMAGE}\"\n\n log \"Embedded controller started: agentteams-controller\"\n\n # Wait for infrastructure inside the controller container\n _wait_for_url() {\n local url=\"$1\" ctr=\"$2\" max_wait=\"${3:-120}\" desc=\"${4:-service}\"\n local elapsed=0\n log \"Waiting for ${desc}...\"\n while [ $elapsed -lt $max_wait ]; do\n if ${DOCKER_CMD} exec \"${ctr}\" curl -sf \"${url}\" >/dev/null 2>&1; then\n log \"${desc} is ready (${elapsed}s)\"\n return 0\n fi\n sleep 2\n elapsed=$((elapsed + 2))\n done\n log \"ERROR: ${desc} not ready after ${max_wait}s\"\n return 1\n }\n\n _wait_for_url \"http://127.0.0.1:6167/_tuwunel/server_version\" agentteams-controller 120 \"Tuwunel (Matrix)\" || exit 1\n _wait_for_url \"http://127.0.0.1:9000/minio/health/live\" agentteams-controller 60 \"MinIO\" || exit 1\n _wait_for_url \"http://127.0.0.1:8080/status\" agentteams-controller 120 \"Higress Gateway\" || exit 1\n\n # Wait for controller to create Manager Agent container\n log \"Waiting for Manager Agent container...\"\n local _mgr_wait=0\n local _mgr_max=300\n while [ $_mgr_wait -lt $_mgr_max ]; do\n if ${DOCKER_CMD} ps --format '{{.Names}}' 2>/dev/null | grep -q \"^agentteams-manager$\"; then\n log \"Manager Agent container detected (${_mgr_wait}s)\"\n break\n fi\n sleep 3\n _mgr_wait=$((_mgr_wait + 3))\n done\n if [ $_mgr_wait -ge $_mgr_max ]; then\n log \"ERROR: Manager Agent container not created after ${_mgr_max}s\"\n log \"Controller logs:\"\n ${DOCKER_CMD} exec agentteams-controller tail -30 /var/log/agentteams/agentteams-controller-error.log 2>/dev/null || true\n exit 1\n fi\n\n # Wait for Manager Agent to be running\n log \"Waiting for Manager Agent to start...\"\n local _agent_wait=0\n while [ $_agent_wait -lt 120 ]; do\n local _state\n _state=$(${DOCKER_CMD} inspect --format '{{.State.Status}}' agentteams-manager 2>/dev/null || echo \"missing\")\n if [ \"${_state}\" = \"running\" ]; then\n log \"Manager Agent is running\"\n break\n fi\n sleep 2\n _agent_wait=$((_agent_wait + 2))\n done\n\n # Enable yolo mode in agent if requested\n if [ \"${AGENTTEAMS_YOLO:-}\" = \"1\" ]; then\n ${DOCKER_CMD} exec agentteams-manager touch /root/manager-workspace/yolo-mode 2>/dev/null || true\n fi\n\n # Wait for the controller to send the first-boot welcome message.\n # The controller gates this on (a) Manager joining the DM room and\n # (b) Higress WASM key-auth propagation actually clearing /v1/chat/completions\n # for the Manager's gateway key — typically ~45-90s on a fresh install.\n # We poll Manager CR Status.WelcomeSent via the in-container agt CLI,\n # exec'd inside agentteams-controller (the source-of-truth container — its\n # bundled CLI binary is always in lockstep with whatever controller\n # binary is currently serving the HTTP API, since they're the same\n # `go build` output. The agentteams-manager container's CLI may lag the\n # controller across image upgrades and silently drop the welcomeSent\n # field, leaving this loop hung). The controller container mints a\n # long-lived admin SA token at startup and writes it to\n # AGENTTEAMS_AUTH_TOKEN_FILE=/var/run/agentteams/cli-token (set as a Dockerfile\n # ENV default), so a bare `docker exec agentteams-controller agt …`\n # auto-discovers both the endpoint and the token. There is a brief\n # window after container start before bootstrapAdminCLIToken completes\n # where the file may be empty / absent — the loop's silent retry\n # handles that the same way it handles the manager-not-yet-running\n # case below.\n if ${DOCKER_CMD} exec agentteams-controller sh -c 'command -v agt' >/dev/null 2>&1; then\n log \"$(msg install.welcome_msg.waiting)\"\n local _welcome_wait=0\n local _welcome_max=\"${AGENTTEAMS_WELCOME_TIMEOUT:-300}\"\n local _welcome_done=0\n while [ $_welcome_wait -lt $_welcome_max ]; do\n # `tr -d` strips whitespace/CR so the grep stays robust to\n # any future change in go-json field ordering or formatting.\n local _wjson\n _wjson=$(${DOCKER_CMD} exec agentteams-controller \\\n agt get managers default -o json 2>/dev/null || true)\n if [ -n \"${_wjson}\" ] && printf '%s' \"${_wjson}\" | tr -d ' \\r\\n' | grep -q '\"welcomeSent\":true'; then\n log \"$(msg install.welcome_msg.confirmed \"${_welcome_wait}\")\"\n _welcome_done=1\n break\n fi\n sleep 3\n _welcome_wait=$((_welcome_wait + 3))\n done\n if [ $_welcome_done -ne 1 ]; then\n # Non-fatal: install is still good. Keep going to the success\n # banner so the admin can use Element Web to nudge Manager into\n # onboarding manually (one DM message is enough).\n log \"$(msg install.welcome_msg.timeout \"${_welcome_max}\")\"\n log \"$(msg install.welcome_msg.timeout_hint)\"\n log \"$(msg install.welcome_msg.timeout_inspect)\"\n fi\n else\n log \"$(msg install.welcome_msg.poll_unavailable)\"\n fi\n\n else\n # ============================================================\n # Legacy architecture: all-in-one manager container\n # ============================================================\n\n NETWORK_ARGS=\"--network agentteams-net\"\n NETWORK_ALIAS_ARGS=\"--network-alias matrix-local.agentteams.io --network-alias aigw-local.agentteams.io --network-alias fs-local.agentteams.io\"\n for _domain in \"${AGENTTEAMS_MATRIX_CLIENT_DOMAIN:-}\" \"${AGENTTEAMS_CONSOLE_DOMAIN:-}\"; do\n if [ -n \"${_domain}\" ] && [[ \"${_domain}\" == *-local.agentteams.io ]]; then\n NETWORK_ALIAS_ARGS=\"${NETWORK_ALIAS_ARGS} --network-alias ${_domain}\"\n fi\n done\n\n # Start Docker API proxy if enabled (security layer between Manager and Docker daemon)\n PROXY_ARGS=\"\"\n if [ \"${AGENTTEAMS_DOCKER_PROXY:-1}\" = \"1\" ] && [ -n \"${CONTAINER_SOCK:-}\" ]; then\n local _proxy_image=\"${AGENTTEAMS_REGISTRY}/higress/agentteams-docker-proxy:${AGENTTEAMS_VERSION}\"\n # Try versioned tag, fallback to latest\n if ! ${DOCKER_CMD} image inspect \"${_proxy_image}\" >/dev/null 2>&1; then\n ${DOCKER_CMD} pull \"${_proxy_image}\" 2>/dev/null || {\n _proxy_image=\"${AGENTTEAMS_REGISTRY}/higress/agentteams-docker-proxy:latest\"\n ${DOCKER_CMD} pull \"${_proxy_image}\" 2>/dev/null || true\n }\n fi\n if ${DOCKER_CMD} image inspect \"${_proxy_image}\" >/dev/null 2>&1; then\n log \"Starting Docker API proxy...\"\n ${DOCKER_CMD} run -d \\\n --name agentteams-docker-proxy \\\n --network agentteams-net \\\n -v \"${CONTAINER_SOCK}:/var/run/docker.sock\" \\\n --security-opt label=disable \\\n -e AGENTTEAMS_WORKER_IMAGE=\"${WORKER_IMAGE}\" \\\n -e AGENTTEAMS_COPAW_WORKER_IMAGE=\"${COPAW_WORKER_IMAGE}\" \\\n -e AGENTTEAMS_QWENPAW_WORKER_IMAGE=\"${QWENPAW_WORKER_IMAGE}\" \\\n -e AGENTTEAMS_HERMES_WORKER_IMAGE=\"${HERMES_WORKER_IMAGE}\" \\\n ${AGENTTEAMS_PROXY_ALLOWED_REGISTRIES:+-e AGENTTEAMS_PROXY_ALLOWED_REGISTRIES=\"${AGENTTEAMS_PROXY_ALLOWED_REGISTRIES}\"} \\\n --restart unless-stopped \\\n \"${_proxy_image}\"\n PROXY_ARGS=\"-e AGENTTEAMS_CONTROLLER_URL=http://agentteams-docker-proxy:2375 -e AGENTTEAMS_CONTAINER_API=http://agentteams-docker-proxy:2375\"\n SOCKET_MOUNT_ARGS=\"\"\n fi\n fi\n\n # Pass host timezone to container\n TZ_ARGS=\"\"\n if [ -n \"${AGENTTEAMS_TIMEZONE:-}\" ]; then\n TZ_ARGS=\"-e TZ=${AGENTTEAMS_TIMEZONE}\"\n fi\n\n YOLO_ARGS=\"\"\n if [ \"${AGENTTEAMS_YOLO:-}\" = \"1\" ]; then\n YOLO_ARGS=\"-e AGENTTEAMS_YOLO=1\"\n fi\n\n MATRIX_DEBUG_ARGS=\"\"\n if [ \"${AGENTTEAMS_MATRIX_DEBUG:-}\" = \"1\" ]; then\n MATRIX_DEBUG_ARGS=\"-e AGENTTEAMS_MATRIX_DEBUG=1\"\n fi\n\n # shellcheck disable=SC2086\n ${DOCKER_CMD} run -d \\\n --name agentteams-manager \\\n --env-file \"${ENV_FILE}\" \\\n -e HOME=/root/manager-workspace \\\n -w /root/manager-workspace \\\n -e HOST_ORIGINAL_HOME=\"${AGENTTEAMS_HOST_SHARE_DIR}\" \\\n -e AGENTTEAMS_MANAGER_RUNTIME=\"${AGENTTEAMS_MANAGER_RUNTIME:-copaw}\" \\\n ${JVM_ARGS:+-e JVM_ARGS=\"${JVM_ARGS}\"} \\\n ${YOLO_ARGS} \\\n ${MATRIX_DEBUG_ARGS} \\\n ${TZ_ARGS} \\\n ${SOCKET_MOUNT_ARGS} \\\n ${NETWORK_ARGS} \\\n ${NETWORK_ALIAS_ARGS} \\\n ${PROXY_ARGS} \\\n -p \"${_port_prefix}${AGENTTEAMS_PORT_GATEWAY}:8080\" \\\n -p \"${_port_prefix}${AGENTTEAMS_PORT_CONSOLE}:8001\" \\\n -p \"${_port_prefix}${AGENTTEAMS_PORT_ELEMENT_WEB:-18088}:8088\" \\\n -p \"127.0.0.1:${AGENTTEAMS_PORT_MANAGER_CONSOLE:-18888}:18888\" \\\n ${DATA_MOUNT_ARGS} \\\n ${WORKSPACE_MOUNT_ARGS} \\\n ${HOST_SHARE_MOUNT_ARGS} \\\n --restart unless-stopped \\\n \"$([ \"${AGENTTEAMS_MANAGER_RUNTIME}\" = \"copaw\" ] && echo \"${MANAGER_COPAW_IMAGE}\" || echo \"${MANAGER_IMAGE}\")\"\n\n # Wait for Manager agent to be ready\n wait_manager_ready \"agentteams-manager\"\n\n # Wait for Matrix server to be ready\n wait_matrix_ready \"agentteams-manager\"\n\n # Post-install verification (non-fatal: warnings only)\n local _verify_script\n _verify_script=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)/agentteams-verify.sh\"\n if [ -f \"${_verify_script}\" ]; then\n bash \"${_verify_script}\" \"agentteams-manager\" || {\n log \"WARNING: Some post-install checks failed. Re-run: bash install/agentteams-verify.sh\"\n }\n fi\n fi\n unset _port_prefix\n\n # ── Start Dashboard (optional) ────────────────────────────────────\n _start_dashboard\n\n # Apply Podman autostart if selected and environment matches\n if [ \"${DOCKER_CMD}\" = \"podman\" ] && [ \"${AGENTTEAMS_PODMAN_AUTOSTART:-0}\" = \"1\" ]; then\n setup_podman_autostart\n fi\n\n log \"\"\n log \"$(msg success.title)\"\n log \"\"\n log \"$(msg success.domains_configured)\"\n log \" ${AGENTTEAMS_MATRIX_DOMAIN%%:*} ${AGENTTEAMS_MATRIX_CLIENT_DOMAIN} ${AGENTTEAMS_AI_GATEWAY_DOMAIN} ${AGENTTEAMS_FS_DOMAIN} ${AGENTTEAMS_CONSOLE_DOMAIN}\"\n log \"\"\n local lan_ip\n lan_ip=$(detect_lan_ip)\n echo -e \"\\033[33m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\033[0m\"\n echo -e \"\\033[33m $(msg success.open_url)\\033[0m\"\n echo -e \"\\033[33m \\033[0m\"\n echo -e \"\\033[1;36m http://127.0.0.1:${AGENTTEAMS_PORT_ELEMENT_WEB:-18088}/#/login\\033[0m\"\n echo -e \"\\033[33m \\033[0m\"\n echo -e \"\\033[33m $(msg success.login_with)\\033[0m\"\n echo -e \"\\033[33m $(msg success.username \"${AGENTTEAMS_ADMIN_USER}\")\\033[0m\"\n echo -e \"\\033[33m $(msg success.password \"${AGENTTEAMS_ADMIN_PASSWORD}\")\\033[0m\"\n echo -e \"\\033[33m \\033[0m\"\n echo -e \"\\033[33m $(msg success.after_login)\\033[0m\"\n echo -e \"\\033[33m $(msg success.tell_it)\\033[0m\"\n echo -e \"\\033[33m $(msg success.manager_auto)\\033[0m\"\n echo -e \"\\033[33m \\033[0m\"\n echo -e \"\\033[33m ───────────────────────────────────────────────────────────────────────────── \\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_title)\\033[0m\"\n echo -e \"\\033[33m \\033[0m\"\n if [ -n \"${lan_ip}\" ]; then\n echo -e \"\\033[33m $(msg success.mobile_step1)\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_step2 \"http://${lan_ip}:${AGENTTEAMS_PORT_GATEWAY}\")\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_step3)\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_username \"${AGENTTEAMS_ADMIN_USER}\")\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_password \"${AGENTTEAMS_ADMIN_PASSWORD}\")\\033[0m\"\n else\n echo -e \"\\033[33m $(msg success.mobile_step1)\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_step2_noip \"${AGENTTEAMS_PORT_GATEWAY}\")\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_noip_hint)\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_step3)\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_username \"${AGENTTEAMS_ADMIN_USER}\")\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_password \"${AGENTTEAMS_ADMIN_PASSWORD}\")\\033[0m\"\n fi\n echo -e \"\\033[33m \\033[0m\"\n echo -e \"\\033[33m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\033[0m\"\n log \"\"\n log \"$(msg success.other_consoles)\"\n log \"$(msg success.higress_console \"${AGENTTEAMS_PORT_CONSOLE}\" \"${AGENTTEAMS_ADMIN_USER}\" \"${AGENTTEAMS_ADMIN_PASSWORD}\")\"\n if [ \"${AGENTTEAMS_DASHBOARD:-0}\" = \"1\" ]; then\n log \"$(msg success.dashboard \"${AGENTTEAMS_PORT_DASHBOARD:-13000}\")\"\n fi\n if [ \"${AGENTTEAMS_USE_EMBEDDED}\" != \"1\" ]; then\n log \"$(msg success.manager_console \"${AGENTTEAMS_PORT_MANAGER_CONSOLE:-18888}\")\"\n log \"$(msg success.manager_console_gateway \"${AGENTTEAMS_ADMIN_USER}\" \"${AGENTTEAMS_ADMIN_PASSWORD}\")\"\n fi\n log \"\"\n log \"$(msg success.switch_llm.title)\"\n log \"$(msg success.switch_llm.hint)\"\n log \"$(msg success.switch_llm.docs)\"\n log \"$(msg success.switch_llm.url)\"\n log \"\"\n log \"$(msg success.tip)\"\n log \"\"\n if [ \"${AGENTTEAMS_LOCAL_ONLY:-1}\" != \"1\" ]; then\n echo -e \"\\033[33m$(msg port.local_only.https_hint)\\033[0m\"\n log \"\"\n fi\n log \"$(msg success.config_file \"${ENV_FILE}\")\"\n log \"$(msg success.data_volume \"${AGENTTEAMS_DATA_DIR}\")\"\n log \"$(msg success.workspace \"${AGENTTEAMS_WORKSPACE_DIR}\")\"\n}\n\n# ============================================================\n# Worker Installation (One-Click)\n# ============================================================\n\ninstall_worker() {\n local WORKER_NAME=\"\"\n local FS=\"\"\n local FS_KEY=\"\"\n local FS_SECRET=\"\"\n local RESET=false\n local SKILLS_API_URL=\"\"\n\n # Parse arguments\n while [ $# -gt 0 ]; do\n case $1 in\n --name) WORKER_NAME=\"$2\"; shift 2 ;;\n --fs) FS=\"$2\"; shift 2 ;;\n --fs-key) FS_KEY=\"$2\"; shift 2 ;;\n --fs-secret) FS_SECRET=\"$2\"; shift 2 ;;\n --skills-api-url) SKILLS_API_URL=\"$2\"; shift 2 ;;\n --reset) RESET=true; shift ;;\n *) die \"$(msg error.unknown_option \"$1\")\" ;;\n esac\n done\n\n # Validate required params\n [ -z \"${WORKER_NAME}\" ] && die \"$(msg error.name_required)\"\n [ -z \"${FS}\" ] && die \"$(msg error.fs_required)\"\n [ -z \"${FS_KEY}\" ] && die \"$(msg error.fs_key_required)\"\n [ -z \"${FS_SECRET}\" ] && die \"$(msg error.fs_secret_required)\"\n\n local CONTAINER_NAME=\"agentteams-worker-${WORKER_NAME}\"\n\n # Handle reset\n if [ \"${RESET}\" = true ]; then\n log \"$(msg worker.resetting \"${WORKER_NAME}\")\"\n ${DOCKER_CMD} stop \"${CONTAINER_NAME}\" 2>/dev/null || true\n ${DOCKER_CMD} rm \"${CONTAINER_NAME}\" 2>/dev/null || true\n fi\n\n # Check for existing container\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' | grep -q \"^${CONTAINER_NAME}$\"; then\n die \"$(msg worker.exists \"${CONTAINER_NAME}\")\"\n fi\n\n log \"$(msg worker.starting \"${WORKER_NAME}\")\"\n\n # Build docker run args\n local DOCKER_ENV=\"\"\n DOCKER_ENV=\"${DOCKER_ENV} -e HOME=/root/agentteams-fs/agents/${WORKER_NAME}\"\n DOCKER_ENV=\"${DOCKER_ENV} -w /root/agentteams-fs/agents/${WORKER_NAME}\"\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_WORKER_NAME=${WORKER_NAME}\"\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_FS_ENDPOINT=${FS}\"\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_FS_ACCESS_KEY=${FS_KEY}\"\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_FS_SECRET_KEY=${FS_SECRET}\"\n\n if [ -z \"${SKILLS_API_URL}\" ]; then\n if [ -n \"${AGENTTEAMS_SKILLS_API_URL:-}\" ]; then\n SKILLS_API_URL=\"${AGENTTEAMS_SKILLS_API_URL}\"\n else\n SKILLS_API_URL=\"nacos://market.agentteams.io:80/public\"\n fi\n fi\n\n # Add SKILLS_API_URL if specified\n DOCKER_ENV=\"${DOCKER_ENV} -e SKILLS_API_URL=${SKILLS_API_URL}\"\n log \"$(msg worker.skills_url \"${SKILLS_API_URL}\")\"\n if [ -n \"${AGENTTEAMS_NACOS_USERNAME:-}\" ]; then\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_NACOS_USERNAME=${AGENTTEAMS_NACOS_USERNAME}\"\n fi\n if [ -n \"${AGENTTEAMS_NACOS_PASSWORD:-}\" ]; then\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_NACOS_PASSWORD=${AGENTTEAMS_NACOS_PASSWORD}\"\n fi\n if [ -n \"${AGENTTEAMS_NACOS_TOKEN:-}\" ]; then\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_NACOS_TOKEN=${AGENTTEAMS_NACOS_TOKEN}\"\n fi\n\n # shellcheck disable=SC2086\n ${DOCKER_CMD} run -d \\\n --name \"${CONTAINER_NAME}\" \\\n ${DOCKER_ENV} \\\n --restart unless-stopped \\\n \"${WORKER_IMAGE}\"\n\n log \"\"\n log \"$(msg worker.started \"${WORKER_NAME}\")\"\n log \"$(msg worker.container \"${CONTAINER_NAME}\")\"\n log \"$(msg worker.view_logs \"${CONTAINER_NAME}\")\"\n}\n\n# ============================================================\n# Main\n# ============================================================\n\n# ============================================================\n# LLM API connectivity test\n# ============================================================\n\ntest_llm_connectivity() {\n local base_url=\"$1\"\n local api_key=\"$2\"\n local model=\"$3\"\n local hint=\"${4:-}\" # optional: extra hint shown on failure\n if ! command -v curl >/dev/null 2>&1; then\n echo -e \"\\033[33m$(msg llm.openai.test.no_curl)\\033[0m\"\n return\n fi\n log \"$(msg llm.openai.test.testing)\"\n local _body _http_code _tmpfile\n _tmpfile=$(mktemp)\n _http_code=$(curl -s -o \"${_tmpfile}\" -w \"%{http_code}\" \\\n -X POST \"${base_url%/}/chat/completions\" \\\n -H \"Authorization: Bearer ${api_key}\" \\\n -H \"Content-Type: application/json\" \\\n -H \"User-Agent: AgentTeams/${AGENTTEAMS_VERSION:-latest}\" \\\n --max-time 30 \\\n -d \"{\\\"model\\\":\\\"${model}\\\",\\\"messages\\\":[{\\\"role\\\":\\\"user\\\",\\\"content\\\":\\\"hi\\\"}]}\" \\\n 2>/dev/null)\n _body=$(cat \"${_tmpfile}\")\n rm -f \"${_tmpfile}\"\n if [ \"${_http_code}\" = \"200\" ] || [ \"${_http_code}\" = \"201\" ]; then\n log \"$(msg llm.openai.test.ok)\"\n else\n echo -e \"\\033[33m$(msg llm.openai.test.fail \"${_http_code}\" \"${_body}\")\\033[0m\"\n if [ -n \"${hint}\" ]; then\n echo -e \"\\033[33m${hint}\\033[0m\"\n fi\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" != \"1\" ]; then\n local _confirm\n read -e -p \"$(msg llm.openai.test.confirm)\" _confirm\n if [ \"${_confirm}\" = \"b\" ]; then\n STEP_RESULT=\"back\"\n return 1\n fi\n if [ \"${_confirm}\" != \"y\" ] && [ \"${_confirm}\" != \"Y\" ]; then\n log \"$(msg llm.openai.test.aborted)\"\n exit 1\n fi\n fi\n fi\n}\n\ntest_embedding_connectivity() {\n local base_url=\"$1\"\n local api_key=\"$2\"\n local model=\"$3\"\n if ! command -v curl >/dev/null 2>&1; then\n return 0\n fi\n log \"$(msg llm.embedding.test.testing)\"\n local _body _http_code _tmpfile\n _tmpfile=$(mktemp)\n _http_code=$(curl -s -o \"${_tmpfile}\" -w \"%{http_code}\" \\\n -X POST \"${base_url%/}/embeddings\" \\\n -H \"Authorization: Bearer ${api_key}\" \\\n -H \"Content-Type: application/json\" \\\n -H \"User-Agent: AgentTeams/${AGENTTEAMS_VERSION:-latest}\" \\\n --max-time 30 \\\n -d \"{\\\"model\\\":\\\"${model}\\\",\\\"input\\\":\\\"test\\\"}\" \\\n 2>/dev/null)\n _body=$(cat \"${_tmpfile}\")\n rm -f \"${_tmpfile}\"\n if [ \"${_http_code}\" = \"200\" ] || [ \"${_http_code}\" = \"201\" ]; then\n log \"$(msg llm.embedding.test.ok)\"\n return 0\n else\n echo -e \"\\033[33m$(msg llm.embedding.test.fail \"${_http_code}\" \"${_body}\")\\033[0m\"\n return 1\n fi\n}\n\n# ============================================================\n# Uninstall\n# ============================================================\n\nuninstall_agentteams() {\n log \"$(msg uninstall.title)\"\n\n # Safely disable and remove the dedicated AgentTeams Podman autostart service\n if [ \"${DOCKER_CMD}\" = \"podman\" ] && command -v systemctl >/dev/null 2>&1; then\n local _systemctl_cmd=\"systemctl\"\n local _service_dir=\"/etc/systemd/system\"\n\n if [ \"$(id -u)\" != \"0\" ]; then\n _systemctl_cmd=\"systemctl --user\"\n _service_dir=\"${HOME}/.config/systemd/user\"\n fi\n\n if [ -f \"${_service_dir}/agentteams-podman-restart.service\" ]; then\n log \"Disabling and removing dedicated AgentTeams Podman autostart service...\"\n ${_systemctl_cmd} disable --now agentteams-podman-restart.service 2>/dev/null || true\n rm -f \"${_service_dir}/agentteams-podman-restart.service\"\n ${_systemctl_cmd} daemon-reload 2>/dev/null || true\n fi\n # Note: Native podman-restart.service is strictly untouched per maintainer review.\n fi\n\n # Capture manager image before removing (needed for workspace cleanup on Linux)\n local manager_image=\"\"\n manager_image=$(${DOCKER_CMD} inspect agentteams-manager --format '{{.Config.Image}}' 2>/dev/null || true)\n\n # Stop and remove manager\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -q \"^agentteams-manager$\"; then\n log \"$(msg uninstall.stopping_manager)\"\n ${DOCKER_CMD} stop agentteams-manager >/dev/null 2>&1 || true\n ${DOCKER_CMD} rm agentteams-manager >/dev/null 2>&1 || true\n fi\n\n # Stop and remove workers\n local workers\n workers=$(${DOCKER_CMD} ps -a --filter \"name=agentteams-worker-\" --format '{{.Names}}' 2>/dev/null || true)\n if [ -n \"${workers}\" ]; then\n log \"$(msg uninstall.stopping_workers)\"\n echo \"${workers}\" | while read -r w; do\n ${DOCKER_CMD} rm -f \"${w}\" >/dev/null 2>&1 || true\n log \"$(msg uninstall.removed \"${w}\")\"\n done\n fi\n\n # Stop and remove the dashboard container (installed via step_dashboard)\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -q \"^agentteams-dashboard$\"; then\n log \"Stopping and removing agentteams-dashboard...\"\n ${DOCKER_CMD} stop agentteams-dashboard >/dev/null 2>&1 || true\n ${DOCKER_CMD} rm agentteams-dashboard >/dev/null 2>&1 || true\n fi\n if ${DOCKER_CMD} volume inspect agentteams-dashboard-data >/dev/null 2>&1; then\n log \"Removing dashboard data volume: agentteams-dashboard-data\"\n ${DOCKER_CMD} volume rm agentteams-dashboard-data >/dev/null 2>&1 || true\n fi\n\n # Stop and remove docker-proxy (legacy ≤ v1.0.x; current arch uses\n # agentteams-controller for the same role)\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -q \"^agentteams-docker-proxy$\"; then\n log \"$(msg uninstall.removing_proxy)\"\n ${DOCKER_CMD} stop agentteams-docker-proxy >/dev/null 2>&1 || true\n ${DOCKER_CMD} rm agentteams-docker-proxy >/dev/null 2>&1 || true\n fi\n\n # Stop and remove the embedded controller container. MUST happen\n # before the `docker volume rm agentteams-data` step below — in embedded\n # mode agentteams-controller mounts agentteams-data at /data (Tuwunel DB,\n # MinIO state, Higress state, all the room messages), and `volume\n # rm` against an in-use volume fails silently because of the trailing\n # `|| true`. Skipping this used to leave room/message history behind\n # across \"uninstall + reinstall\" cycles. See PR #692.\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -q \"^agentteams-controller$\"; then\n log \"$(msg uninstall.stopping_controller)\"\n ${DOCKER_CMD} stop agentteams-controller >/dev/null 2>&1 || true\n ${DOCKER_CMD} rm agentteams-controller >/dev/null 2>&1 || true\n fi\n\n # Read env file for data/workspace info before removing\n local env_file=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n [ ! -f \"${env_file}\" ] && [ -f \"./agentteams-manager.env\" ] && env_file=\"./agentteams-manager.env\"\n\n local data_dir=\"\" workspace_dir=\"\"\n if [ -f \"${env_file}\" ]; then\n data_dir=$(grep '^AGENTTEAMS_DATA_DIR=' \"${env_file}\" 2>/dev/null | cut -d= -f2- || true)\n workspace_dir=$(grep '^AGENTTEAMS_WORKSPACE_DIR=' \"${env_file}\" 2>/dev/null | cut -d= -f2- || true)\n fi\n\n # Remove Docker volume\n local vol=\"${data_dir:-agentteams-data}\"\n if ${DOCKER_CMD} volume inspect \"${vol}\" >/dev/null 2>&1; then\n log \"$(msg uninstall.removing_volume \"${vol}\")\"\n ${DOCKER_CMD} volume rm \"${vol}\" >/dev/null 2>&1 || true\n fi\n\n # Remove Docker network\n if ${DOCKER_CMD} network ls --format '{{.Name}}' 2>/dev/null | grep -q \"^agentteams-net$\"; then\n log \"$(msg uninstall.removing_network)\"\n ${DOCKER_CMD} network rm agentteams-net >/dev/null 2>&1 || true\n fi\n\n # Remove workspace directory\n if [ -n \"${workspace_dir}\" ] && [ -d \"${workspace_dir}\" ]; then\n log \"$(msg uninstall.removing_workspace \"${workspace_dir}\")\"\n rm -rf \"${workspace_dir}\" 2>/dev/null || true\n if [ -d \"${workspace_dir}\" ]; then\n log \"$(msg uninstall.removing_workspace_elevated)\"\n local _parent _base\n _parent=$(dirname \"${workspace_dir}\")\n _base=$(basename \"${workspace_dir}\")\n local _rm_image=\"${manager_image:-busybox}\"\n ${DOCKER_CMD} run --rm --entrypoint sh \\\n -v \"${_parent}:/host-parent\" \\\n \"${_rm_image}\" \\\n -c \"rm -rf /host-parent/${_base}\" 2>/dev/null || true\n fi\n fi\n\n # Remove env file\n if [ -f \"${env_file}\" ]; then\n log \"$(msg uninstall.removing_env \"${env_file}\")\"\n rm -f \"${env_file}\"\n fi\n\n # Remove install log\n if [ -f \"${HOME}/agentteams-install.log\" ]; then\n log \"$(msg uninstall.removing_log \"${HOME}/agentteams-install.log\")\"\n rm -f \"${HOME}/agentteams-install.log\"\n fi\n\n echo \"\"\n log \"$(msg uninstall.done)\"\n}\n\n# ============================================================\n# Check container runtime (docker or podman) and environment\n# ============================================================\ncheck_container_runtime() {\n # Ensure the rootless environment baseline (XDG_RUNTIME_DIR) is ready before engine detection.\n if [ \"$(id -u)\" -ne 0 ] && [ -z \"${XDG_RUNTIME_DIR:-}\" ]; then\n export XDG_RUNTIME_DIR=\"/run/user/$(id -u)\"\n fi\n\n if command -v docker >/dev/null 2>&1; then\n DOCKER_CMD=\"docker\"\n # Check for podman disguised as docker (alias or podman-docker package)\n if docker --version 2>/dev/null | grep -qi \"podman\"; then\n DOCKER_CMD=\"podman\"\n fi\n elif command -v podman >/dev/null 2>&1; then\n DOCKER_CMD=\"podman\"\n else\n echo -e \"\\033[31m[AgentTeams ERROR]\\033[0m $(msg error.docker_not_found)\" >&2\n exit 1\n fi\n\n # Unified health check (Validates runtime responsiveness)\n if ! ${DOCKER_CMD} ps >/dev/null 2>&1; then\n echo -e \"\\033[31m[AgentTeams ERROR]\\033[0m $(msg error.docker_not_running)\" >&2\n exit 1\n fi\n}\n\ncheck_container_runtime\n\ncase \"${1:-}\" in\n manager|\"\")\n install_manager\n ;;\n worker)\n shift\n install_worker \"$@\"\n ;;\n dashboard)\n check_container_runtime\n load_current_params_from_env\n AGENTTEAMS_DASHBOARD=\"${AGENTTEAMS_DASHBOARD:-1}\"\n AGENTTEAMS_DASHBOARD_VERSION=\"${AGENTTEAMS_DASHBOARD_VERSION:-v1.2.0}\"\n AGENTTEAMS_PORT_DASHBOARD=\"${AGENTTEAMS_PORT_DASHBOARD:-13000}\"\n AGENTTEAMS_DASHBOARD_IMAGE=\"${AGENTTEAMS_DASHBOARD_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-dashboard:${AGENTTEAMS_DASHBOARD_VERSION}}\"\n AGENTTEAMS_USE_EMBEDDED=1\n _start_dashboard\n ;;\n uninstall)\n uninstall_agentteams\n ;;\n *)\n echo \"Usage: $0 [manager|worker [options]|dashboard|uninstall]\"\n echo \"\"\n echo \"Commands:\"\n echo \" manager Interactive Manager installation (default)\"\n echo \" Choose Quick Start (all defaults) or Manual mode\"\n echo \" worker Worker installation (requires --name and connection params)\"\n echo \" dashboard Start/stop agentteams-dashboard (use with AGENTTEAMS_DASHBOARD=0 to stop)\"\n echo \" uninstall Stop and remove Manager + all Worker containers\"\n echo \"\"\n echo \"Quick Start (fastest):\"\n echo \" $0\"\n echo \" # Then select '1' for Quick Start mode\"\n echo \"\"\n echo \"Non-interactive (for automation):\"\n echo \" AGENTTEAMS_NON_INTERACTIVE=1 AGENTTEAMS_LLM_API_KEY=sk-xxx $0\"\n echo \"\"\n echo \"Worker Options:\"\n echo \" --name <name> Worker name (required)\"\n echo \" --fs <url> MinIO endpoint URL (required)\"\n echo \" --fs-key <key> MinIO access key (required)\"\n echo \" --fs-secret <secret> MinIO secret key (required)\"\n echo \" --reset Remove existing Worker container before creating\"\n exit 1\n ;;\nesac\n"}, "ce3a4770fb177d56db1ca445f92ae9030b9633b7": {"t": 1785764735.9213252, "n": 248816, "h": "d284fb8d68dae1fd", "c": "#!/bin/bash\n# agentteams-install.sh - One-click installation for AgentTeams Manager, Worker, and Dashboard\n#\n# Usage:\n# ./agentteams-install.sh # Interactive installation (choose Quick Start or Manual)\n# ./agentteams-install.sh manager # Same as above (explicit)\n# ./agentteams-install.sh worker --name <name> ... # Worker installation\n# ./agentteams-install.sh dashboard # Start/stop agentteams-dashboard (AGENTTEAMS_DASHBOARD=0 to stop)\n# ./agentteams-install.sh uninstall # Stop and remove Manager + all Workers\n#\n# NOTE: agentteams-dashboard is currently only supported via the Bash installer.\n# The PowerShell installer (agentteams-install.ps1) does not yet include dashboard support.\n#\n# Onboarding Modes:\n# Quick Start - Fast installation with all default values (recommended)\n# Manual - Customize each option step by step\n#\n# Environment variables (for automation):\n# AGENTTEAMS_NON_INTERACTIVE Skip all prompts, use defaults (default: 0)\n# AGENTTEAMS_LLM_PROVIDER LLM provider (default: openai-compat for zh non-interactive Token Plan; qwen for en)\n# AGENTTEAMS_DEFAULT_MODEL Default model (default: qwen3.6-plus for zh Token Plan and en non-interactive)\n# AGENTTEAMS_OPENAI_BASE_URL OpenAI-compatible base URL (default for zh non-interactive: Alibaba Token Plan endpoint)\n# AGENTTEAMS_LLM_API_KEY LLM API key (required)\n# AGENTTEAMS_ADMIN_USER Admin username (default: admin)\n# AGENTTEAMS_ADMIN_PASSWORD Admin password (auto-generated if not set, min 8 chars)\n# AGENTTEAMS_MATRIX_DOMAIN Matrix domain (default: matrix-local.agentteams.io:18080)\n# AGENTTEAMS_MOUNT_SOCKET Mount container runtime socket (default: 1)\n# AGENTTEAMS_DATA_DIR Docker volume name for persistent data (default: agentteams-data)\n# AGENTTEAMS_WORKSPACE_DIR Host directory for manager workspace (default: ~/agentteams-manager)\n# AGENTTEAMS_VERSION Image tag (default: latest)\n# AGENTTEAMS_REGISTRY Image registry (default: auto-detected by timezone)\n# AGENTTEAMS_INSTALL_MANAGER_IMAGE Override manager image (e.g., local build)\n# AGENTTEAMS_INSTALL_WORKER_IMAGE Override worker image (e.g., local build)\n# AGENTTEAMS_INSTALL_COPAW_WORKER_IMAGE Override copaw worker image (e.g., local build)\n# AGENTTEAMS_INSTALL_HERMES_WORKER_IMAGE Override hermes worker image (e.g., local build)\n# AGENTTEAMS_NACOS_REGISTRY_URI Default Nacos registry URI for Worker market search/import\n# (default: nacos://market.agentteams.io:80/public)\n# AGENTTEAMS_NACOS_USERNAME Default Nacos username for nacos:// package imports (optional)\n# AGENTTEAMS_NACOS_PASSWORD Default Nacos password for nacos:// package imports (optional)\n# AGENTTEAMS_CMS_TRACES_ENABLED Enable openclaw-cms-plugin traces for Manager AND all Workers (default: false)\n# AGENTTEAMS_CMS_ENDPOINT ARMS OTLP endpoint (required if traces enabled)\n# AGENTTEAMS_CMS_LICENSE_KEY CMS license key (required if traces enabled)\n# AGENTTEAMS_CMS_PROJECT CMS project name (optional)\n# AGENTTEAMS_CMS_WORKSPACE CMS workspace ID (required if traces enabled)\n# AGENTTEAMS_CMS_SERVICE_NAME Manager service name in ARMS (default: agentteams-manager)\n# Workers always report as agentteams-worker-<name> automatically\n# AGENTTEAMS_CMS_METRICS_ENABLED Enable diagnostics-otel metrics for Manager AND all Workers (default: false)\n# AGENTTEAMS_PORT_GATEWAY Host port for Higress gateway (default: 18080)\n# AGENTTEAMS_PORT_CONSOLE Host port for Higress console (default: 18001)\n# AGENTTEAMS_PORT_ELEMENT_WEB Host port for Element Web direct access (default: 18088)\n# AGENTTEAMS_PORT_MANAGER_CONSOLE Host port for Manager console (default: 18888)\n# AGENTTEAMS_WORKER_IDLE_TIMEOUT Worker idle timeout in minutes (default: 720, i.e. 12 hours)\n# AGENTTEAMS_DASHBOARD Install agentteams-dashboard management UI (default: 1)\n# AGENTTEAMS_DASHBOARD_VERSION Dashboard version (default: v1.2.0-beta.2, independent of AgentTeams version)\n# AGENTTEAMS_PORT_DASHBOARD Dashboard host port (default: 13000)\n# AGENTTEAMS_DASHBOARD_IMAGE Override dashboard image (default: <registry>/agentteams/agentteams-dashboard:<DASHBOARD_VERSION>)\n# AGENTTEAMS_AI_GATEWAY_ADMIN_URL Higress Console URL for shared auth (auto-detected)\n\nset -e\n\nAGENTTEAMS_VERSION=\"${AGENTTEAMS_VERSION:-}\"\nAGENTTEAMS_KNOWN_STABLE_VERSION=\"v1.2.0\" # fallback if GitHub API is unreachable\n\n_normalize_version() {\n local version=\"$1\"\n case \"${version}\" in\n [0-9]*.[0-9]*.[0-9]*) version=\"v${version}\" ;;\n esac\n case \"${version}\" in\n v[0-9]*.[0-9]*.[0-9]*.beta.[0-9]*) version=\"${version/.beta./-beta.}\" ;;\n esac\n printf '%s' \"${version}\"\n}\n\n# Returns 0 (true) if $1 < $2 using semver order; \"latest\" is treated as greatest\n_ver_lt() {\n [ \"$1\" = \"latest\" ] && return 1\n [ \"$2\" = \"latest\" ] && return 0\n [ \"$1\" = \"$2\" ] && return 1\n [ \"$(printf '%s\\n%s' \"$1\" \"$2\" | sort -V | head -1)\" = \"$1\" ]\n}\n\n_use_legacy_image_env() {\n if [ \"$1\" = \"latest\" ]; then\n _ver_lt \"${AGENTTEAMS_KNOWN_STABLE_VERSION}\" \"v1.2.0\"\n else\n _ver_lt \"$1\" \"v1.2.0\"\n fi\n}\n\n_controller_env_prefix() {\n if _use_legacy_image_env \"$1\"; then\n printf '%s%s' 'HIC' 'LAW_'\n else\n printf '%s' 'AGENTTEAMS_'\n fi\n}\n\n_controller_storage_prefix() {\n if _use_legacy_image_env \"$1\"; then\n printf '%s%s' 'hic' 'law/agentteams-storage'\n else\n printf '%s' 'agentteams/agentteams-storage'\n fi\n}\nAGENTTEAMS_NON_INTERACTIVE=\"${AGENTTEAMS_NON_INTERACTIVE:-0}\"\nAGENTTEAMS_MOUNT_SOCKET=\"${AGENTTEAMS_MOUNT_SOCKET:-1}\"\nAGENTTEAMS_DOCKER_PROXY=\"${AGENTTEAMS_DOCKER_PROXY:-1}\"\nSTEP_RESULT=\"\" # Used by state machine to signal \"back\" navigation\n\n# ============================================================\n# Log all output to file\n# ============================================================\n\nAGENTTEAMS_LOG_FILE=\"${HOME}/agentteams-install.log\"\n\nif [ \"${1:-}\" != \"uninstall\" ]; then\n # Redirect all output (stdout and stderr) to both terminal and log file\n exec > >(tee -a \"${AGENTTEAMS_LOG_FILE}\") 2>&1\n\n echo \"\"\n echo \"========================================\"\n echo \"AgentTeams Installation Log\"\n echo \"Started: $(date)\"\n echo \"User: $(whoami)\"\n echo \"System: $(uname -a)\"\n echo \"Log file: ${AGENTTEAMS_LOG_FILE}\"\n echo \"========================================\"\n echo \"\"\nfi\n\n# ============================================================\n# Utility functions (needed early for timezone detection)\n# ============================================================\n\nlog() {\n echo -e \"\\033[36m[AgentTeams]\\033[0m $1\"\n}\n\nerror() {\n echo -e \"\\033[31m[AgentTeams ERROR]\\033[0m $1\" >&2\n}\n\ndie() {\n error \"$1\"\n exit 1\n}\n\n# ============================================================\n# Timezone detection (compatible with Linux and macOS)\n# ============================================================\n\ndetect_timezone() {\n local tz=\"\"\n\n # Try /etc/timezone (Debian/Ubuntu)\n if [ -f /etc/timezone ]; then\n tz=$(cat /etc/timezone 2>/dev/null | tr -d '[:space:]')\n fi\n\n # Try /etc/localtime symlink (macOS and some Linux)\n if [ -z \"${tz}\" ] && [ -L /etc/localtime ]; then\n tz=$(ls -l /etc/localtime 2>/dev/null | sed 's|.*/zoneinfo/||')\n fi\n\n # Try timedatectl (systemd)\n if [ -z \"${tz}\" ]; then\n tz=$(timedatectl show --value -p Timezone 2>/dev/null)\n fi\n\n # If still not detected, warn and prompt user. Diagnostics go to stderr —\n # this function returns its value via stdout to `$(detect_timezone)`, so\n # any echo here would leak into AGENTTEAMS_TIMEZONE and downstream image tags.\n if [ -z \"${tz}\" ]; then\n echo \"\" >&2\n echo -e \"\\033[33m[AgentTeams WARNING]\\033[0m Could not detect timezone automatically.\" >&2\n echo -e \"\\033[33m[AgentTeams]\\033[0m Please enter your timezone (e.g., Asia/Shanghai, America/New_York).\" >&2\n echo \"\" >&2\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n tz=\"Asia/Shanghai\"\n log \"Using default timezone: ${tz}\" >&2\n else\n read -e -p \"Timezone [Asia/Shanghai]: \" tz\n tz=\"${tz:-Asia/Shanghai}\"\n fi\n fi\n\n echo \"${tz}\"\n}\n\n# Detect timezone once at startup (used by registry selection and container TZ)\nAGENTTEAMS_TIMEZONE=\"${AGENTTEAMS_TIMEZONE:-$(detect_timezone)}\"\n\n# ============================================================\n# Language detection based on timezone\n# ============================================================\n\ndetect_language() {\n local tz=\"${AGENTTEAMS_TIMEZONE}\"\n case \"${tz}\" in\n Asia/Shanghai|Asia/Chongqing|Asia/Harbin|Asia/Urumqi|\\\n Asia/Taipei|Asia/Hong_Kong|Asia/Macau)\n echo \"zh\"\n ;;\n *)\n echo \"en\"\n ;;\n esac\n}\n\n# Language priority: env var > existing env file > timezone detection\nif [ -z \"${AGENTTEAMS_LANGUAGE}\" ]; then\n # Check existing env file for saved language preference (upgrade scenario)\n _env_file=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n # Migrate from legacy location (current directory) if needed\n if [ ! -f \"${_env_file}\" ] && [ -f \"./agentteams-manager.env\" ]; then\n mv \"./agentteams-manager.env\" \"${_env_file}\" 2>/dev/null || true\n fi\n if [ -f \"${_env_file}\" ]; then\n _saved_lang=$(grep '^AGENTTEAMS_LANGUAGE=' \"${_env_file}\" 2>/dev/null | cut -d= -f2-)\n if [ -n \"${_saved_lang}\" ]; then\n AGENTTEAMS_LANGUAGE=\"${_saved_lang}\"\n fi\n fi\n # Fall back to timezone-based detection\n if [ -z \"${AGENTTEAMS_LANGUAGE}\" ]; then\n AGENTTEAMS_LANGUAGE=\"$(detect_language)\"\n fi\n unset _env_file _saved_lang\nfi\nexport AGENTTEAMS_LANGUAGE\n\n# ============================================================\n# Centralized message dictionary and msg() function\n# Compatible with bash 3.2+ (macOS default) — uses case instead of declare -A\n# ============================================================\n\n# msg() function: look up message by key, with printf-style argument substitution\n# Falls back to English if the current language translation is missing.\nmsg() {\n local key=\"$1\"\n shift\n local lang=\"${AGENTTEAMS_LANGUAGE:-en}\"\n local text=\"\"\n case \"${key}.${lang}\" in\n # --- Timezone detection messages ---\n \"tz.warning.title.zh\") text=\"无法自动检测时区。\" ;;\n \"tz.warning.title.en\") text=\"Could not detect timezone automatically.\" ;;\n \"tz.warning.prompt.zh\") text=\"请输入您的时区(例如 Asia/Shanghai、America/New_York)。\" ;;\n \"tz.warning.prompt.en\") text=\"Please enter your timezone (e.g., Asia/Shanghai, America/New_York).\" ;;\n \"tz.default.zh\") text=\"使用默认时区: %s\" ;;\n \"tz.default.en\") text=\"Using default timezone: %s\" ;;\n \"tz.input_prompt.zh\") text=\"时区\" ;;\n \"tz.input_prompt.en\") text=\"Timezone\" ;;\n # --- Installation title and info ---\n \"install.title.zh\") text=\"=== AgentTeams Manager 安装 ===\" ;;\n \"install.title.en\") text=\"=== AgentTeams Manager Installation ===\" ;;\n \"install.registry.zh\") text=\"镜像仓库: %s\" ;;\n \"install.registry.en\") text=\"Registry: %s\" ;;\n \"install.dir.zh\") text=\"安装目录: %s\" ;;\n \"install.dir.en\") text=\"Installation directory: %s\" ;;\n \"install.dir_hint.zh\") text=\" (env 文件 'agentteams-manager.env' 将保存到 HOME 目录。)\" ;;\n \"install.dir_hint.en\") text=\" (The env file 'agentteams-manager.env' will be saved to your HOME directory.)\" ;;\n \"install.dir_hint2.zh\") text=\" (请从您希望管理此安装的目录运行此脚本。)\" ;;\n \"install.dir_hint2.en\") text=\" (Run this script from the directory where you want to manage this installation.)\" ;;\n # --- Onboarding mode ---\n \"install.mode.title.zh\") text=\"--- Onboarding 模式 ---\" ;;\n \"install.mode.title.en\") text=\"--- Onboarding Mode ---\" ;;\n \"install.mode.choose.zh\") text=\"选择安装模式:\" ;;\n \"install.mode.choose.en\") text=\"Choose your installation mode:\" ;;\n \"install.mode.quickstart.zh\") text=\" 1) 快速开始 - 使用阿里云通义 Token 套餐快速安装(推荐)\" ;;\n \"install.mode.quickstart.en\") text=\" 1) Quick Start - Fast installation with Qwen Cloud (recommended)\" ;;\n \"install.mode.manual.zh\") text=\" 2) 手动配置 - 选择 LLM 提供商并自定义选项\" ;;\n \"install.mode.manual.en\") text=\" 2) Manual - Choose LLM provider and customize options\" ;;\n \"install.mode.prompt.zh\") text=\"请选择 [1/2]\" ;;\n \"install.mode.prompt.en\") text=\"Enter choice [1/2]\" ;;\n \"install.mode.quickstart_selected.zh\") text=\"已选择快速开始模式 - 使用阿里云通义 Token 套餐\" ;;\n \"install.mode.quickstart_selected.en\") text=\"Quick Start mode selected - using Qwen Cloud\" ;;\n \"install.mode.manual_selected.zh\") text=\"已选择手动配置模式 - 您将选择 LLM 提供商并自定义选项\" ;;\n \"install.mode.manual_selected.en\") text=\"Manual mode selected - you will choose LLM provider and customize options\" ;;\n \"install.mode.invalid.zh\") text=\"无效选择,默认使用快速开始模式\" ;;\n \"install.mode.invalid.en\") text=\"Invalid choice, defaulting to Quick Start mode\" ;;\n # --- Upgrade mode sub-menu ---\n \"upgrade.mode.prompt.zh\") text=\"请选择升级方式:\" ;;\n \"upgrade.mode.prompt.en\") text=\"Select upgrade mode:\" ;;\n \"upgrade.mode.keep_all.zh\") text=\" 1) 保留所有参数,一键升级(推荐)\" ;;\n \"upgrade.mode.keep_all.en\") text=\" 1) Keep all parameters, quick upgrade (recommended)\" ;;\n \"upgrade.mode.confirm_each.zh\") text=\" 2) 逐个确认参数(可修改)\" ;;\n \"upgrade.mode.confirm_each.en\") text=\" 2) Confirm each parameter (can modify)\" ;;\n \"upgrade.mode.back.zh\") text=\" 3) 返回\" ;;\n \"upgrade.mode.back.en\") text=\" 3) Back\" ;;\n # --- Version selection ---\n \"install.version.title.zh\") text=\"--- 版本选择 ---\" ;;\n \"install.version.title.en\") text=\"--- Version Selection ---\" ;;\n \"install.version.choose.zh\") text=\"选择要安装的版本:\" ;;\n \"install.version.choose.en\") text=\"Choose the version to install:\" ;;\n \"install.version.option_latest.zh\") text=\" 1) latest - 最新版(默认)\" ;;\n \"install.version.option_latest.en\") text=\" 1) latest - Latest build (default)\" ;;\n \"install.version.option_stable.zh\") text=\" 2) %s - 最新稳定版\" ;;\n \"install.version.option_stable.en\") text=\" 2) %s - Latest stable release\" ;;\n \"install.version.fetching.zh\") text=\"正在查询最新稳定版本...\" ;;\n \"install.version.fetching.en\") text=\"Fetching latest stable release...\" ;;\n \"install.version.fetch_failed.zh\") text=\"无法查询 GitHub,使用内置版本 %s\" ;;\n \"install.version.fetch_failed.en\") text=\"Could not reach GitHub, using built-in version %s\" ;;\n \"install.version.option_custom.zh\") text=\" 3) 自定义 - 手动输入版本号(如 v1.0.5)\" ;;\n \"install.version.option_custom.en\") text=\" 3) Custom - Enter a specific version (e.g. v1.0.5)\" ;;\n \"install.version.prompt.zh\") text=\"请选择 [1/2/3]\" ;;\n \"install.version.prompt.en\") text=\"Enter choice [1/2/3]\" ;;\n \"install.version.custom_prompt.zh\") text=\"请输入版本号\" ;;\n \"install.version.custom_prompt.en\") text=\"Enter version tag\" ;;\n \"install.version.selected_latest.zh\") text=\"已选择最新版 (latest)\" ;;\n \"install.version.selected_latest.en\") text=\"Selected latest version\" ;;\n \"install.version.selected_stable.zh\") text=\"已选择最新稳定版 (%s)\" ;;\n \"install.version.selected_stable.en\") text=\"Selected latest stable version (%s)\" ;;\n \"install.version.selected_custom.zh\") text=\"已选择自定义版本 (%s)\" ;;\n \"install.version.selected_custom.en\") text=\"Selected custom version (%s)\" ;;\n \"install.version.invalid.zh\") text=\"无效选择,使用最新稳定版 (%s)\" ;;\n \"install.version.invalid.en\") text=\"Invalid choice, defaulting to latest stable version (%s)\" ;;\n # --- Existing installation detected ---\n \"install.existing.detected.zh\") text=\"检测到已有 Manager 安装(env 文件: %s)\" ;;\n \"install.existing.detected.en\") text=\"Existing Manager installation detected (env file: %s)\" ;;\n \"install.existing.choose.zh\") text=\"选择操作:\" ;;\n \"install.existing.choose.en\") text=\"Choose an action:\" ;;\n \"install.existing.upgrade.zh\") text=\" 1) 就地升级(保留数据、工作空间、env 文件)\" ;;\n \"install.existing.upgrade.en\") text=\" 1) In-place upgrade (keep data, workspace, env file)\" ;;\n \"install.existing.reinstall.zh\") text=\" 2) 全新重装(删除所有数据,重新开始)\" ;;\n \"install.existing.reinstall.en\") text=\" 2) Clean reinstall (remove all data, start fresh)\" ;;\n \"install.existing.cancel.zh\") text=\" 3) 取消\" ;;\n \"install.existing.cancel.en\") text=\" 3) Cancel\" ;;\n \"install.existing.prompt.zh\") text=\"请选择 [1/2/3]\" ;;\n \"install.existing.prompt.en\") text=\"Enter choice [1/2/3]\" ;;\n \"install.existing.upgrade_noninteractive.zh\") text=\"非交互模式: 执行就地升级...\" ;;\n \"install.existing.upgrade_noninteractive.en\") text=\"Non-interactive mode: performing in-place upgrade...\" ;;\n \"install.existing.upgrading.zh\") text=\"执行就地升级...\" ;;\n \"install.existing.upgrading.en\") text=\"Performing in-place upgrade...\" ;;\n \"install.existing.warn_manager_stop.zh\") text=\"⚠️ Manager 容器将被停止并重新创建。\" ;;\n \"install.existing.warn_manager_stop.en\") text=\"⚠️ Manager container will be stopped and recreated.\" ;;\n \"install.existing.warn_worker_recreate.zh\") text=\"⚠️ Worker 容器也将被重新创建(以更新 Manager IP)。\" ;;\n \"install.existing.warn_worker_recreate.en\") text=\"⚠️ Worker containers will also be recreated (to update Manager IP in hosts).\" ;;\n \"install.existing.continue_prompt.zh\") text=\"继续?[y/N]\" ;;\n \"install.existing.continue_prompt.en\") text=\"Continue? [y/N]\" ;;\n \"install.existing.cancelled.zh\") text=\"安装已取消。\" ;;\n \"install.existing.cancelled.en\") text=\"Installation cancelled.\" ;;\n \"install.existing.stopping_manager.zh\") text=\"停止并移除现有 manager 容器...\" ;;\n \"install.existing.stopping_manager.en\") text=\"Stopping and removing existing manager container...\" ;;\n \"install.existing.stopping_workers.zh\") text=\"停止并移除现有 worker 容器...\" ;;\n \"install.existing.stopping_workers.en\") text=\"Stopping and removing existing worker containers...\" ;;\n \"install.existing.removed.zh\") text=\" 已移除: %s\" ;;\n \"install.existing.removed.en\") text=\" Removed: %s\" ;;\n # --- Clean reinstall messages ---\n \"install.reinstall.performing.zh\") text=\"执行全新重装...\" ;;\n \"install.reinstall.performing.en\") text=\"Performing clean reinstall...\" ;;\n \"install.reinstall.warn_stop.zh\") text=\"⚠️ 以下运行中的容器将被停止:\" ;;\n \"install.reinstall.warn_stop.en\") text=\"⚠️ The following running containers will be stopped:\" ;;\n \"install.reinstall.warn_delete.zh\") text=\"⚠️ 警告: 以下内容将被删除:\" ;;\n \"install.reinstall.warn_delete.en\") text=\"⚠️ WARNING: This will DELETE the following:\" ;;\n \"install.reinstall.warn_volume.zh\") text=\" - Docker 卷: agentteams-data\" ;;\n \"install.reinstall.warn_volume.en\") text=\" - Docker volume: agentteams-data\" ;;\n \"install.reinstall.warn_env.zh\") text=\" - Env 文件: %s\" ;;\n \"install.reinstall.warn_env.en\") text=\" - Env file: %s\" ;;\n \"install.reinstall.warn_workspace.zh\") text=\" - Manager 工作空间: %s\" ;;\n \"install.reinstall.warn_workspace.en\") text=\" - Manager workspace: %s\" ;;\n \"install.reinstall.warn_workers.zh\") text=\" - 所有 worker 容器\" ;;\n \"install.reinstall.warn_workers.en\") text=\" - All worker containers\" ;;\n \"install.reinstall.warn_proxy.zh\") text=\" - Docker API 代理容器: agentteams-controller\" ;;\n \"install.reinstall.warn_proxy.en\") text=\" - Docker API proxy container: agentteams-controller\" ;;\n \"install.reinstall.warn_network.zh\") text=\" - Docker 网络: agentteams-net\" ;;\n \"install.reinstall.warn_network.en\") text=\" - Docker network: agentteams-net\" ;;\n \"install.reinstall.confirm_type.zh\") text=\"请输入工作空间路径以确认删除(或按 Ctrl+C 取消):\" ;;\n \"install.reinstall.confirm_type.en\") text=\"To confirm deletion, please type the workspace path:\" ;;\n \"install.reinstall.confirm_path.zh\") text=\"输入路径以确认(或按 Ctrl+C 取消)\" ;;\n \"install.reinstall.confirm_path.en\") text=\"Type the path to confirm (or press Ctrl+C to cancel)\" ;;\n \"install.reinstall.path_mismatch.zh\") text=\"路径不匹配。中止重装。输入: '%s',期望: '%s'\" ;;\n \"install.reinstall.path_mismatch.en\") text=\"Path mismatch. Aborting reinstall. Input: '%s', Expected: '%s'\" ;;\n \"install.reinstall.confirmed.zh\") text=\"已确认。正在清理...\" ;;\n \"install.reinstall.confirmed.en\") text=\"Confirmed. Cleaning up...\" ;;\n \"install.reinstall.removed_worker.zh\") text=\" 已移除 worker: %s\" ;;\n \"install.reinstall.removed_worker.en\") text=\" Removed worker: %s\" ;;\n \"install.reinstall.removing_volume.zh\") text=\"正在移除 Docker 卷: agentteams-data\" ;;\n \"install.reinstall.removing_volume.en\") text=\"Removing Docker volume: agentteams-data\" ;;\n \"install.reinstall.warn_volume_fail.zh\") text=\" 警告: 无法移除卷(可能有引用)\" ;;\n \"install.reinstall.warn_volume_fail.en\") text=\" Warning: Could not remove volume (may have references)\" ;;\n \"install.reinstall.removing_proxy.zh\") text=\"正在移除 Docker API 代理容器: agentteams-controller\" ;;\n \"install.reinstall.removing_proxy.en\") text=\"Removing Docker API proxy container: agentteams-controller\" ;;\n \"install.reinstall.removing_network.zh\") text=\"正在移除 Docker 网络: agentteams-net\" ;;\n \"install.reinstall.removing_network.en\") text=\"Removing Docker network: agentteams-net\" ;;\n \"install.reinstall.removing_workspace.zh\") text=\"正在移除工作空间目录: %s\" ;;\n \"install.reinstall.removing_workspace.en\") text=\"Removing workspace directory: %s\" ;;\n \"install.reinstall.removing_env.zh\") text=\"正在移除 env 文件: %s\" ;;\n \"install.reinstall.removing_env.en\") text=\"Removing env file: %s\" ;;\n \"install.reinstall.cleanup_done.zh\") text=\"清理完成。开始全新安装...\" ;;\n \"install.reinstall.cleanup_done.en\") text=\"Cleanup complete. Starting fresh installation...\" ;;\n \"install.reinstall.failed_rm_workspace.zh\") text=\"无法移除工作空间目录\" ;;\n \"install.reinstall.failed_rm_workspace.en\") text=\"Failed to remove workspace directory\" ;;\n # --- Orphan volume detection ---\n \"install.orphan_volume.detected.zh\") text=\"⚠️ 检测到残留数据卷 '%s',但未找到对应的 env 配置文件。\" ;;\n \"install.orphan_volume.detected.en\") text=\"⚠️ Found leftover data volume '%s' but no matching env config file.\" ;;\n \"install.orphan_volume.warn.zh\") text=\"这可能是之前安装的残留数据,会导致新安装出现异常(如密码冲突、服务启动失败)。\" ;;\n \"install.orphan_volume.warn.en\") text=\"This is likely leftover data from a previous installation and may cause issues (e.g., credential conflicts, service startup failures).\" ;;\n \"install.orphan_volume.choose.zh\") text=\"选择操作:\" ;;\n \"install.orphan_volume.choose.en\") text=\"Choose an action:\" ;;\n \"install.orphan_volume.clean.zh\") text=\" 1) 清理残留数据卷后继续安装(推荐)\" ;;\n \"install.orphan_volume.clean.en\") text=\" 1) Remove leftover volume and continue installation (recommended)\" ;;\n \"install.orphan_volume.keep.zh\") text=\" 2) 保留数据卷继续安装(可能出现异常)\" ;;\n \"install.orphan_volume.keep.en\") text=\" 2) Keep the volume and continue installation (may cause issues)\" ;;\n \"install.orphan_volume.prompt.zh\") text=\"请选择 [1/2]\" ;;\n \"install.orphan_volume.prompt.en\") text=\"Enter choice [1/2]\" ;;\n \"install.orphan_volume.cleaning.zh\") text=\"正在清理残留数据卷...\" ;;\n \"install.orphan_volume.cleaning.en\") text=\"Removing leftover data volume...\" ;;\n \"install.orphan_volume.cleaned.zh\") text=\"残留数据卷已清理。继续全新安装...\" ;;\n \"install.orphan_volume.cleaned.en\") text=\"Leftover volume removed. Continuing with fresh installation...\" ;;\n \"install.orphan_volume.keeping.zh\") text=\"保留数据卷,继续安装。如遇异常请选择全新重装。\" ;;\n \"install.orphan_volume.keeping.en\") text=\"Keeping existing volume. If you encounter issues, consider a clean reinstall.\" ;;\n \"install.orphan_volume.clean_noninteractive.zh\") text=\"非交互模式: 自动清理残留数据卷...\" ;;\n \"install.orphan_volume.clean_noninteractive.en\") text=\"Non-interactive mode: automatically removing leftover volume...\" ;;\n # --- Loading existing config ---\n \"install.loading_config.zh\") text=\"从 %s 加载已有配置(shell 环境变量优先)...\" ;;\n \"install.loading_config.en\") text=\"Loading existing config from %s (shell env vars take priority)...\" ;;\n # --- LLM Configuration ---\n \"llm.title.zh\") text=\"--- LLM 配置 ---\" ;;\n \"llm.title.en\") text=\"--- LLM Configuration ---\" ;;\n \"llm.provider.label.zh\") text=\" 提供商: %s\" ;;\n \"llm.provider.label.en\") text=\" Provider: %s\" ;;\n \"llm.model.label.zh\") text=\" 模型: %s\" ;;\n \"llm.model.label.en\") text=\" Model: %s\" ;;\n \"llm.provider.qwen.zh\") text=\" 提供商: qwen(阿里云百炼)\" ;;\n \"llm.provider.qwen.en\") text=\" Provider: qwen (Alibaba Cloud Bailian)\" ;;\n \"llm.provider.qwen_default.zh\") text=\" 提供商: %s(默认)\" ;;\n \"llm.provider.qwen_default.en\") text=\" Provider: %s (default)\" ;;\n \"llm.model.default.zh\") text=\" 模型: %s(默认)\" ;;\n \"llm.model.default.en\") text=\" Model: %s (default)\" ;;\n \"llm.apikey_hint_bailian.zh\") text=\" 💡 获取阿里云百炼(DashScope)API Key:\" ;;\n \"llm.apikey_hint_bailian.en\") text=\" 💡 Get your Alibaba Cloud Bailian (DashScope) API Key:\" ;;\n \"llm.apikey_url_bailian.zh\") text=\" https://www.aliyun.com/product/bailian\" ;;\n \"llm.apikey_url_bailian.en\") text=\" https://www.aliyun.com/product/bailian\" ;;\n \"llm.apikey_hint_qwencloud.zh\") text=\" 💡 从 Qwen Cloud(国际站)获取 DASHSCOPE_API_KEY:\" ;;\n \"llm.apikey_hint_qwencloud.en\") text=\" 💡 Get your DASHSCOPE_API_KEY for Qwen Cloud (international) from:\" ;;\n \"llm.apikey_url_qwencloud.zh\") text=\" https://home.qwencloud.com/api-keys (文档: https://docs.qwencloud.com/)\" ;;\n \"llm.apikey_url_qwencloud.en\") text=\" https://home.qwencloud.com/api-keys | Docs: https://docs.qwencloud.com/\" ;;\n \"llm.apikey_hint_tokenplan.zh\") text=\" 💡 获取 DashScope API Key 或开通通义 Token 套餐,请参考:\" ;;\n \"llm.apikey_hint_tokenplan.en\") text=\" 💡 Get your DashScope or Token Plan API key (Alibaba Model Studio):\" ;;\n \"llm.apikey_url_tokenplan.zh\") text=\" https://help.aliyun.com/zh/model-studio/token-plan-quickstart\" ;;\n \"llm.apikey_url_tokenplan.en\") text=\" https://common-buy.aliyun.com/token-plan/ | https://help.aliyun.com/zh/model-studio/token-plan-quickstart\" ;;\n \"llm.apikey_hint_codingplan.zh\") text=\" 💡 获取 DashScope API Key(Coding 套餐 / coding.dashscope 接口):\" ;;\n \"llm.apikey_hint_codingplan.en\") text=\" 💡 Get your DashScope API key for Coding Plan (coding.dashscope endpoint):\" ;;\n \"llm.apikey_url_codingplan.zh\") text=\" https://help.aliyun.com/zh/model-studio/get-api-key\" ;;\n \"llm.apikey_url_codingplan.en\") text=\" https://help.aliyun.com/zh/model-studio/get-api-key\" ;;\n \"llm.apikey_prompt.zh\") text=\"LLM API Key\" ;;\n \"llm.apikey_prompt.en\") text=\"LLM API Key\" ;;\n \"llm.providers_title.zh\") text=\"可用 LLM 提供商:\" ;;\n \"llm.providers_title.en\") text=\"Available LLM Providers:\" ;;\n \"llm.provider.alibaba.zh\") text=\" 1) 阿里云通义 Token 套餐 - 推荐中国用户使用\" ;;\n \"llm.provider.alibaba.en\") text=\" 1) Qwen Cloud - International (OpenAI-compatible API, recommended)\" ;;\n \"llm.provider.openai_compat.zh\") text=\" 2) OpenAI 兼容 API - 自定义 Base URL(OpenAI、DeepSeek 等)\" ;;\n \"llm.provider.openai_compat.en\") text=\" 2) OpenAI-compatible API - Custom Base URL (OpenAI, DeepSeek, etc.)\" ;;\n \"llm.provider.select.zh\") text=\"选择提供商 [1/2]\" ;;\n \"llm.provider.select.en\") text=\"Select provider [1/2]\" ;;\n \"llm.alibaba.models_title.zh\") text=\"选择阿里云模型接入方式:\" ;;\n \"llm.alibaba.models_title.en\") text=\"Select Alibaba Cloud model access:\" ;;\n \"llm.alibaba.model.tokenplan.zh\") text=\" 1) 阿里云通义 Token 套餐 - 兼容模式(推荐)\" ;;\n \"llm.alibaba.model.tokenplan.en\") text=\" 1) Alibaba Cloud Token Plan - compatible-mode (recommended)\" ;;\n \"llm.alibaba.model.bailian.zh\") text=\" 2) 阿里云百炼 - DashScope 通用兼容接口\" ;;\n \"llm.alibaba.model.bailian.en\") text=\" 2) Alibaba Cloud Bailian - DashScope compatible mode\" ;;\n \"llm.alibaba.model.codingplan_legacy.zh\") text=\" 3) 阿里云 Coding 套餐 - 旧版端点(兼容保留)\" ;;\n \"llm.alibaba.model.codingplan_legacy.en\") text=\" 3) Alibaba Cloud Coding Plan - legacy endpoint (backward compatible)\" ;;\n \"llm.alibaba.model.select.zh\") text=\"选择接入方式 [1/2/3]\" ;;\n \"llm.alibaba.model.select.en\") text=\"Select access option [1/2/3]\" ;;\n \"llm.alibaba.model.invalid.zh\") text=\"无效选择: %s(请输入 1、2 或 3)\" ;;\n \"llm.alibaba.model.invalid.en\") text=\"Invalid choice: %s (please enter 1, 2, or 3)\" ;;\n \"llm.codingplan.models_title.zh\") text=\"选择通义 Token 套餐默认模型:\" ;;\n \"llm.codingplan.models_title.en\") text=\"Select Qwen Cloud default model:\" ;;\n \"llm.codingplan.model.qwen36plus.zh\") text=\" 1) qwen3.6-plus - 千问 3.6(推荐)\" ;;\n \"llm.codingplan.model.qwen36plus.en\") text=\" 1) qwen3.6-plus - Qwen 3.6 (recommended)\" ;;\n \"llm.codingplan.model.glm5.zh\") text=\" 2) glm-5 - 智谱 GLM-5(编程推荐)\" ;;\n \"llm.codingplan.model.glm5.en\") text=\" 2) glm-5 - Zhipu GLM-5 (recommended for coding)\" ;;\n \"llm.codingplan.model.kimi.zh\") text=\" 3) kimi-k2.5 - Moonshot Kimi K2.5\" ;;\n \"llm.codingplan.model.kimi.en\") text=\" 3) kimi-k2.5 - Moonshot Kimi K2.5\" ;;\n \"llm.codingplan.model.minimax.zh\") text=\" 4) MiniMax-M2.5 - MiniMax M2.5\" ;;\n \"llm.codingplan.model.minimax.en\") text=\" 4) MiniMax-M2.5 - MiniMax M2.5\" ;;\n \"llm.codingplan.model.select.zh\") text=\"选择模型 [1/2/3/4]\" ;;\n \"llm.codingplan.model.select.en\") text=\"Select model [1/2/3/4]\" ;;\n \"llm.provider.selected_tokenplan.zh\") text=\" 提供商: 阿里云通义 Token 套餐(兼容模式)\" ;;\n \"llm.provider.selected_tokenplan.en\") text=\" Provider: Alibaba Cloud Token Plan (compatible mode)\" ;;\n \"llm.provider.selected_codingplan.zh\") text=\" 提供商: 阿里云通义 Token 套餐(alibaba-cloud)\" ;;\n \"llm.provider.selected_codingplan.en\") text=\" Provider: Qwen Cloud (international) (alibaba-cloud)\" ;;\n \"llm.provider.selected_codingplan_legacy.zh\") text=\" 提供商: 阿里云 Coding 套餐(coding.dashscope)\" ;;\n \"llm.provider.selected_codingplan_legacy.en\") text=\" Provider: Alibaba Cloud Coding Plan (coding.dashscope)\" ;;\n \"llm.provider.selected_qwen.zh\") text=\" 提供商: 阿里云百炼\" ;;\n \"llm.provider.selected_qwen.en\") text=\" Provider: Alibaba Cloud Bailian\" ;;\n \"llm.provider.selected_openai.zh\") text=\" 提供商: %s(OpenAI 兼容)\" ;;\n \"llm.provider.selected_openai.en\") text=\" Provider: %s (OpenAI-compatible)\" ;;\n \"llm.provider.invalid.zh\") text=\"无效选择: %s(请输入 1 或 2)\" ;;\n \"llm.provider.invalid.en\") text=\"Invalid choice: %s (please enter 1 or 2)\" ;;\n \"llm.qwen.model_prompt.zh\") text=\"默认模型 ID\" ;;\n \"llm.qwen.model_prompt.en\") text=\"Default Model ID\" ;;\n \"llm.openai.base_url_prompt.zh\") text=\"Base URL\" ;;\n \"llm.openai.base_url_prompt.en\") text=\"Base URL\" ;;\n \"llm.openai.model_prompt.zh\") text=\"默认模型 ID\" ;;\n \"llm.openai.model_prompt.en\") text=\"Default Model ID\" ;;\n \"llm.openai.base_url_label.zh\") text=\" Base URL: %s\" ;;\n \"llm.openai.base_url_label.en\") text=\" Base URL: %s\" ;;\n # --- Custom model parameters ---\n \"llm.custom_model.detected.zh\") text=\" ⚠️ 模型 '%s' 不在内置模型列表中,请配置模型参数:\" ;;\n \"llm.custom_model.detected.en\") text=\" ⚠️ Model '%s' is not in the built-in model list. Please configure model parameters:\" ;;\n \"llm.custom_model.context_prompt.zh\") text=\"最大上下文长度(token 数)[150000]\" ;;\n \"llm.custom_model.context_prompt.en\") text=\"Max context window (tokens) [150000]\" ;;\n \"llm.custom_model.max_tokens_prompt.zh\") text=\"最大输出长度(token 数)[128000]\" ;;\n \"llm.custom_model.max_tokens_prompt.en\") text=\"Max output tokens [128000]\" ;;\n \"llm.custom_model.reasoning_prompt.zh\") text=\"是否支持推理/思考模式?[Y/n]\" ;;\n \"llm.custom_model.reasoning_prompt.en\") text=\"Does it support reasoning/thinking mode? [Y/n]\" ;;\n \"llm.custom_model.vision_prompt.zh\") text=\"是否支持图片输入?[y/N]\" ;;\n \"llm.custom_model.vision_prompt.en\") text=\"Does it support image input? [y/N]\" ;;\n \"llm.custom_model.summary.zh\") text=\" 自定义模型参数: 上下文=%s, 最大输出=%s, 推理=%s, 图片=%s\" ;;\n \"llm.custom_model.summary.en\") text=\" Custom model params: context=%s, maxTokens=%s, reasoning=%s, vision=%s\" ;;\n # --- Admin Credentials ---\n \"admin.title.zh\") text=\"--- 管理员凭据 ---\" ;;\n \"admin.title.en\") text=\"--- Admin Credentials ---\" ;;\n \"admin.username_prompt.zh\") text=\"管理员用户名\" ;;\n \"admin.username_prompt.en\") text=\"Admin Username\" ;;\n \"admin.password_prompt.zh\") text=\"管理员密码(留空自动生成,最少 8 位)\" ;;\n \"admin.password_prompt.en\") text=\"Admin Password (leave empty to auto-generate, min 8 chars)\" ;;\n \"admin.password_generated.zh\") text=\" 已自动生成管理员密码\" ;;\n \"admin.password_generated.en\") text=\" Auto-generated admin password\" ;;\n \"admin.password_too_short.zh\") text=\"管理员密码至少需要 8 个字符(MinIO 要求)。当前长度: %s\" ;;\n \"admin.password_too_short.en\") text=\"Admin password must be at least 8 characters (MinIO requirement). Current length: %s\" ;;\n # --- Port Configuration ---\n \"port.title.zh\") text=\"--- 端口配置(按回车使用默认值)---\" ;;\n \"port.title.en\") text=\"--- Port Configuration (press Enter for defaults) ---\" ;;\n \"port.gateway_prompt.zh\") text=\"网关主机端口(容器内 8080)\" ;;\n \"port.gateway_prompt.en\") text=\"Host port for gateway (8080 inside container)\" ;;\n \"port.console_prompt.zh\") text=\"Higress 控制台主机端口(容器内 8001)\" ;;\n \"port.console_prompt.en\") text=\"Host port for Higress console (8001 inside container)\" ;;\n \"port.element_prompt.zh\") text=\"Element Web 直接访问主机端口(容器内 8088)\" ;;\n \"port.element_prompt.en\") text=\"Host port for Element Web direct access (8088 inside container)\" ;;\n \"port.manager_console_prompt.zh\") text=\"Manager 控制台主机端口(容器内 18888)\" ;;\n \"port.manager_console_prompt.en\") text=\"Host port for Manager console (18888 inside container)\" ;;\n \"port.copaw_app_prompt.zh\") text=\"CoPaw App API 主机端口(容器内 18799)\" ;;\n \"port.copaw_app_prompt.en\") text=\"Host port for CoPaw App API (18799 inside container)\" ;;\n # --- Local-only binding ---\n \"port.local_only.title.zh\") text=\"--- 网络访问模式 ---\" ;;\n \"port.local_only.title.en\") text=\"--- Network Access Mode ---\" ;;\n \"port.local_only.prompt.zh\") text=\"是否仅允许本机访问(端口绑定到 127.0.0.1)?\" ;;\n \"port.local_only.prompt.en\") text=\"Bind ports to localhost only (127.0.0.1)?\" ;;\n \"port.local_only.hint_yes.zh\") text=\" 仅本机使用,无需开放外部端口(推荐)\" ;;\n \"port.local_only.hint_yes.en\") text=\" Local use only, no external port exposure (recommended)\" ;;\n \"port.local_only.hint_no.zh\") text=\" 允许外部访问(局域网 / 公网)\" ;;\n \"port.local_only.hint_no.en\") text=\" Allow external access (LAN / public network)\" ;;\n \"port.local_only.choice.zh\") text=\"请选择 [1/2]\" ;;\n \"port.local_only.choice.en\") text=\"Enter choice [1/2]\" ;;\n \"port.local_only.selected_local.zh\") text=\"端口已绑定到 127.0.0.1(仅本机访问)\" ;;\n \"port.local_only.selected_local.en\") text=\"Ports bound to 127.0.0.1 (localhost only)\" ;;\n \"port.local_only.selected_external.zh\") text=\"端口已绑定到所有网络接口(0.0.0.0)\" ;;\n \"port.local_only.selected_external.en\") text=\"Ports bound to all interfaces (0.0.0.0)\" ;;\n \"port.local_only.https_hint.zh\") text=\"⚠️ 建议在 Higress 控制台配置 TLS 证书并启用 HTTPS,避免明文传输。\" ;;\n \"port.local_only.https_hint.en\") text=\"⚠️ It is recommended to configure TLS certificates and enable HTTPS in the Higress Console to avoid plaintext transmission.\" ;;\n \"port.local_only.https_docs.zh\") text=\"\" ;;\n \"port.local_only.https_docs.en\") text=\"\" ;;\n # --- Domain Configuration ---\n \"domain.title.zh\") text=\"--- 域名配置(按回车使用默认值)---\" ;;\n \"domain.title.en\") text=\"--- Domain Configuration (press Enter for defaults) ---\" ;;\n \"domain.hint.zh\") text=\"提示: 自定义域名前必须事先做好 DNS 解析。单机 ECS 部署时无需修改 aigw、fs 等域名;Element Web 和 Matrix Server 也可通过 IP 直接访问。\" ;;\n \"domain.hint.en\") text=\"Hint: Configure DNS resolution before customizing domains. For single ECS deployment, no need to change aigw, fs, etc.; Element Web and Matrix Server can also be accessed directly via IP.\" ;;\n \"domain.matrix_prompt.zh\") text=\"Matrix 域名\" ;;\n \"domain.matrix_prompt.en\") text=\"Matrix Domain\" ;;\n \"domain.element_prompt.zh\") text=\"Element Web 域名\" ;;\n \"domain.element_prompt.en\") text=\"Element Web Domain\" ;;\n \"domain.gateway_prompt.zh\") text=\"AI 网关域名\" ;;\n \"domain.gateway_prompt.en\") text=\"AI Gateway Domain\" ;;\n \"domain.fs_prompt.zh\") text=\"文件系统域名\" ;;\n \"domain.fs_prompt.en\") text=\"File System Domain\" ;;\n \"domain.console_prompt.zh\") text=\"Manager 控制台域名\" ;;\n \"domain.console_prompt.en\") text=\"Manager Console Domain\" ;;\n # --- GitHub Integration ---\n \"github.title.zh\") text=\"--- GitHub 集成(可选,按回车跳过)---\" ;;\n \"github.title.en\") text=\"--- GitHub Integration (optional, press Enter to skip) ---\" ;;\n \"github.token_prompt.zh\") text=\"GitHub 个人访问令牌(可选)\" ;;\n \"github.token_prompt.en\") text=\"GitHub Personal Access Token (optional)\" ;;\n # --- Skills Registry ---\n \"skills.title.zh\") text=\"--- Skills 注册中心(可选,按回车使用默认 nacos://market.agentteams.io:80/public)---\" ;;\n \"skills.title.en\") text=\"--- Skills Registry (optional, press Enter for default nacos://market.agentteams.io:80/public) ---\" ;;\n \"skills.url_prompt.zh\") text=\"Skills 注册中心 URL(留空使用默认 nacos://market.agentteams.io:80/public)\" ;;\n \"skills.url_prompt.en\") text=\"Skills Registry URL (leave empty for default nacos://market.agentteams.io:80/public)\" ;;\n # --- Data Persistence ---\n \"data.title.zh\") text=\"--- 数据持久化 ---\" ;;\n \"data.title.en\") text=\"--- Data Persistence ---\" ;;\n \"data.volume_prompt.zh\") text=\"Docker 卷名称 [agentteams-data]\" ;;\n \"data.volume_prompt.en\") text=\"Docker volume name for persistent data [agentteams-data]\" ;;\n \"data.volume_using.zh\") text=\" 使用 Docker 卷: %s\" ;;\n \"data.volume_using.en\") text=\" Using Docker volume: %s\" ;;\n # --- Manager Workspace ---\n \"workspace.title.zh\") text=\"--- Manager 工作空间 ---\" ;;\n \"workspace.title.en\") text=\"--- Manager Workspace ---\" ;;\n \"workspace.dir_prompt.zh\") text=\"Manager 工作空间目录 [%s]\" ;;\n \"workspace.dir_prompt.en\") text=\"Manager workspace directory [%s]\" ;;\n \"workspace.dir_label.zh\") text=\" Manager 工作空间: %s\" ;;\n \"workspace.dir_label.en\") text=\" Manager workspace: %s\" ;;\n # --- Host directory sharing ---\n \"host_share.prompt.zh\") text=\"与 Agent 共享的主机目录(默认: %s)\" ;;\n \"host_share.prompt.en\") text=\"Host directory to share with agents (default: %s)\" ;;\n \"host_share.sharing.zh\") text=\"共享主机目录: %s -> 容器内 /host-share\" ;;\n \"host_share.sharing.en\") text=\"Sharing host directory: %s -> /host-share in container\" ;;\n \"host_share.not_exist.zh\") text=\"警告: 主机目录 %s 不存在,跳过验证继续使用\" ;;\n \"host_share.not_exist.en\") text=\"WARNING: Host directory %s does not exist, using without validation\" ;;\n # --- Default worker runtime ---\n \"worker_runtime.title.zh\") text=\"--- 默认 Worker 运行时 ---\" ;;\n \"worker_runtime.title.en\") text=\"--- Default Worker Runtime ---\" ;;\n \"worker_runtime.openclaw.zh\") text=\"OpenClaw\" ;;\n \"worker_runtime.openclaw.en\") text=\"OpenClaw\" ;;\n \"worker_runtime.copaw.zh\") text=\"CoPaw\" ;;\n \"worker_runtime.copaw.en\") text=\"CoPaw\" ;;\n \"worker_runtime.hermes.zh\") text=\"Hermes\" ;;\n \"worker_runtime.hermes.en\") text=\"Hermes\" ;;\n \"worker_runtime.choice.zh\") text=\"请选择 [1/2/3]\" ;;\n \"worker_runtime.choice.en\") text=\"Enter choice [1/2/3]\" ;;\n \"worker_runtime.selected.zh\") text=\"默认 Worker 运行时: %s\" ;;\n \"worker_runtime.selected.en\") text=\"Default Worker runtime: %s\" ;;\n \"worker_runtime.title_short.zh\") text=\"默认 Worker 运行时\" ;;\n \"worker_runtime.title_short.en\") text=\"Default Worker Runtime\" ;;\n \"manager_runtime.title.zh\") text=\"--- Manager 运行时 ---\" ;;\n \"manager_runtime.title.en\") text=\"--- Manager Runtime ---\" ;;\n \"manager_runtime.openclaw.zh\") text=\"OpenClaw\" ;;\n \"manager_runtime.openclaw.en\") text=\"OpenClaw\" ;;\n \"manager_runtime.copaw.zh\") text=\"CoPaw\" ;;\n \"manager_runtime.copaw.en\") text=\"CoPaw\" ;;\n \"manager_runtime.choice.zh\") text=\"请选择 [1/2]\" ;;\n \"manager_runtime.choice.en\") text=\"Enter choice [1/2]\" ;;\n \"manager_runtime.selected.zh\") text=\"Manager 运行时: %s\" ;;\n \"manager_runtime.selected.en\") text=\"Manager runtime: %s\" ;;\n \"manager_runtime.title_short.zh\") text=\"Manager 运行时\" ;;\n \"manager_runtime.title_short.en\") text=\"Manager Runtime\" ;;\n # --- Secrets and config ---\n \"install.generating_secrets.zh\") text=\"正在生成密钥...\" ;;\n \"install.generating_secrets.en\") text=\"Generating secrets...\" ;;\n \"install.config_saved.zh\") text=\"配置已保存到 %s\" ;;\n \"install.config_saved.en\") text=\"Configuration saved to %s\" ;;\n # --- Container runtime socket ---\n \"install.socket_detected.zh\") text=\"容器运行时 socket: %s(已启用直接创建 Worker)\" ;;\n \"install.socket_detected.en\") text=\"Container runtime socket: %s (direct Worker creation enabled)\" ;;\n \"install.socket_not_found.zh\") text=\"未找到容器运行时 socket(Manager 无法直接创建 Worker 容器,需要你手动执行 docker 命令创建)\" ;;\n \"install.socket_not_found.en\") text=\"No container runtime socket found (Manager cannot create Worker containers directly, you will need to create them manually using docker commands)\" ;;\n \"install.socket_confirm.title.zh\") text=\"⚠️ 未检测到容器运行时 Socket\" ;;\n \"install.socket_confirm.title.en\") text=\"⚠️ Container Runtime Socket Not Detected\" ;;\n \"install.socket_confirm.message.zh\") text=\"未找到 Docker/Podman socket,Manager 将无法自动创建 Worker 容器。\\n你需要手动执行 docker run 命令来创建 Worker。\\n\\n是否继续安装?\" ;;\n \"install.socket_confirm.message.en\") text=\"Docker/Podman socket not found. Manager will not be able to create Worker containers automatically.\\nYou will need to manually run docker commands to create Workers.\\n\\nContinue installation?\" ;;\n \"install.socket_confirm.prompt.zh\") text=\"继续安装? [y/N]: \" ;;\n \"install.socket_confirm.prompt.en\") text=\"Continue? [y/N]: \" ;;\n \"install.socket_confirm.cancelled.zh\") text=\"安装已取消。如需启用 Worker 自动创建,请确保 Docker/Podman 正在运行,然后重新运行安装脚本。\" ;;\n \"install.socket_confirm.cancelled.en\") text=\"Installation cancelled. To enable automatic Worker creation, ensure Docker/Podman is running and re-run the installer.\" ;;\n # --- Container management ---\n \"install.removing_existing.zh\") text=\"正在移除现有 agentteams-manager 容器...\" ;;\n \"install.removing_existing.en\") text=\"Removing existing agentteams-manager container...\" ;;\n # --- Matrix E2EE ---\n \"matrix_e2ee.title.zh\") text=\"--- Matrix 端到端加密(E2EE)---\" ;;\n \"matrix_e2ee.title.en\") text=\"--- Matrix End-to-End Encryption (E2EE) ---\" ;;\n \"matrix_e2ee.desc.zh\") text=\"E2EE 会对 Manager 与 Worker 之间的 Matrix 消息进行端到端加密。\\n 启用后,即使 Matrix 服务器被入侵,消息内容也无法被窃取。\\n 但 E2EE 会增加首次握手耗时,且要求所有 Agent 都支持 matrix-sdk-crypto。\\n 如果不确定,建议保持禁用。\\n ⚠ 注意:禁用 E2EE 后,请勿在 Element 上创建默认启用加密的 Private 房间,\\n 否则 Agent 将无法读取该房间中的加密消息。请改用 Public 房间或关闭房间加密。\" ;;\n \"matrix_e2ee.desc.en\") text=\"E2EE encrypts Matrix messages between Manager and Workers end-to-end.\\n When enabled, message content stays private even if the Matrix server is compromised.\\n However, E2EE adds overhead to the initial handshake and requires all Agents\\n to support matrix-sdk-crypto. If unsure, keep it disabled.\\n ⚠ Note: When E2EE is disabled, do NOT create Private rooms in Element (which\\n enable encryption by default) — Agents cannot read encrypted messages without\\n E2EE support. Use Public rooms or turn off room encryption instead.\" ;;\n \"matrix_e2ee.enable.zh\") text=\"启用 E2EE\" ;;\n \"matrix_e2ee.enable.en\") text=\"Enable E2EE\" ;;\n \"matrix_e2ee.disable.zh\") text=\"禁用 E2EE(推荐)\" ;;\n \"matrix_e2ee.disable.en\") text=\"Disable E2EE (recommended)\" ;;\n \"matrix_e2ee.choice.zh\") text=\"请选择 [1/2]\" ;;\n \"matrix_e2ee.choice.en\") text=\"Enter choice [1/2]\" ;;\n \"matrix_e2ee.selected_enabled.zh\") text=\"Matrix E2EE: 已启用\" ;;\n \"matrix_e2ee.selected_enabled.en\") text=\"Matrix E2EE: enabled\" ;;\n \"matrix_e2ee.selected_disabled.zh\") text=\"Matrix E2EE: 已禁用(默认)\" ;;\n \"matrix_e2ee.selected_disabled.en\") text=\"Matrix E2EE: disabled (default)\" ;;\n \"matrix_e2ee.title_short.zh\") text=\"Matrix E2EE\" ;;\n \"matrix_e2ee.title_short.en\") text=\"Matrix E2EE\" ;;\n \"matrix_e2ee.val_enabled.zh\") text=\"已启用\" ;;\n \"matrix_e2ee.val_enabled.en\") text=\"enabled\" ;;\n \"matrix_e2ee.val_disabled.zh\") text=\"已禁用\" ;;\n \"matrix_e2ee.val_disabled.en\") text=\"disabled\" ;;\n # --- Docker API proxy ---\n \"docker_proxy.title.zh\") text=\"--- Docker API 安全代理 ---\" ;;\n \"docker_proxy.title.en\") text=\"--- Docker API Security Proxy ---\" ;;\n \"docker_proxy.desc.zh\") text=\"Docker API 代理可防止 AI Agent 通过 Docker API 越狱访问宿主机。\\n 启用后,Manager 不再直接持有 Docker socket,所有容器操作经过安全校验。\" ;;\n \"docker_proxy.desc.en\") text=\"Docker API proxy prevents AI Agents from escaping via Docker API to access the host.\\n When enabled, Manager no longer has direct Docker socket access; all container operations go through security validation.\" ;;\n \"docker_proxy.enable.zh\") text=\"启用(推荐)\" ;;\n \"docker_proxy.enable.en\") text=\"Enable (recommended)\" ;;\n \"docker_proxy.disable.zh\") text=\"禁用(直接挂载 Docker socket)\" ;;\n \"docker_proxy.disable.en\") text=\"Disable (mount Docker socket directly)\" ;;\n \"docker_proxy.choice.zh\") text=\"请选择 [1/2]\" ;;\n \"docker_proxy.choice.en\") text=\"Enter choice [1/2]\" ;;\n \"docker_proxy.selected_enabled.zh\") text=\"Docker API 代理: 已启用\" ;;\n \"docker_proxy.selected_enabled.en\") text=\"Docker API proxy: enabled\" ;;\n \"docker_proxy.selected_disabled.zh\") text=\"Docker API 代理: 已禁用\" ;;\n \"docker_proxy.selected_disabled.en\") text=\"Docker API proxy: disabled\" ;;\n \"docker_proxy.title_short.zh\") text=\"Docker API 代理\" ;;\n \"docker_proxy.title_short.en\") text=\"Docker API Proxy\" ;;\n \"docker_proxy.val_enabled.zh\") text=\"已启用\" ;;\n \"docker_proxy.val_enabled.en\") text=\"enabled\" ;;\n \"docker_proxy.val_disabled.zh\") text=\"已禁用\" ;;\n \"docker_proxy.val_disabled.en\") text=\"disabled\" ;;\n \"docker_proxy.registries_desc.zh\") text=\"默认放行的镜像来源:本地镜像、localhost、Higress 仓库(所有 region)。\\n 如需放行其他镜像仓库,请输入逗号分隔的地址前缀。\\n 示例: ghcr.io/myorg,registry.example.com/team\" ;;\n \"docker_proxy.registries_desc.en\") text=\"Default allowed image sources: local images, localhost, Higress registries (all regions).\\n To allow additional image sources, enter comma-separated address prefixes.\\n Example: ghcr.io/myorg,registry.example.com/team\" ;;\n \"docker_proxy.registries_prompt.zh\") text=\"额外放行的镜像来源(按回车跳过)\" ;;\n \"docker_proxy.registries_prompt.en\") text=\"Additional allowed image sources (press Enter to skip)\" ;;\n \"docker_proxy.registries_label.zh\") text=\"额外放行的镜像来源\" ;;\n \"docker_proxy.registries_label.en\") text=\"Additional allowed image sources\" ;;\n # --- Podman Autostart ---\n \"podman.autostart.title.zh\") text=\"--- Podman 驻留与开机自启 ---\" ;;\n \"podman.autostart.title.en\") text=\"--- Podman Linger & Autostart ---\" ;;\n \"podman.autostart.desc.zh\") text=\"配置为 systemd 服务,支持开机自动启动及后台驻留。\" ;;\n \"podman.autostart.desc.en\") text=\"Configure as a systemd service for automatic startup on boot.\" ;;\n \"podman.autostart.enable.zh\") text=\"启用(推荐)\" ;;\n \"podman.autostart.enable.en\") text=\"Enable (recommended)\" ;;\n \"podman.autostart.disable.zh\") text=\"不启用(容器随当前会话关闭而停止)\" ;;\n \"podman.autostart.disable.en\") text=\"Disable (containers stop when session ends)\" ;;\n \"podman.autostart.choice.zh\") text=\"请选择 [1/2]\" ;;\n \"podman.autostart.choice.en\") text=\"Enter choice [1/2]\" ;;\n \"podman.autostart.selected_enabled.zh\") text=\"Podman 开机自启: 已启用\" ;;\n \"podman.autostart.selected_enabled.en\") text=\"Podman Autostart: enabled\" ;;\n \"podman.autostart.selected_disabled.zh\") text=\"Podman 开机自启: 已禁用\" ;;\n \"podman.autostart.selected_disabled.en\") text=\"Podman Autostart: disabled\" ;;\n \"podman.autostart.title_short.zh\") text=\"Podman 开机自启\" ;;\n \"podman.autostart.title_short.en\") text=\"Podman Autostart\" ;;\n \"podman.autostart.val_enabled.zh\") text=\"已启用\" ;;\n \"podman.autostart.val_enabled.en\") text=\"enabled\" ;;\n \"podman.autostart.val_disabled.zh\") text=\"已禁用\" ;;\n \"podman.autostart.val_disabled.en\") text=\"disabled\" ;;\n \"install.podman.autostart_title.zh\") text=\"正在配置 Podman 容器开机自启...\" ;;\n \"install.podman.autostart_title.en\") text=\"Configuring Podman container autostart...\" ;;\n \"install.podman.linger_enable.zh\") text=\"尝试启用 systemd linger (如有提示请在此输入 sudo 密码)...\" ;;\n \"install.podman.linger_enable.en\") text=\"Attempting to enable systemd linger (enter sudo password if prompted)...\" ;;\n \"install.podman.linger_warn.zh\") text=\"警告: 无法自动启用 linger。驻留可能失败,请手动执行: sudo loginctl enable-linger \\$(whoami)\" ;;\n \"install.podman.linger_warn.en\") text=\"WARNING: Failed to auto-enable linger. Please run manually: sudo loginctl enable-linger \\$(whoami)\" ;;\n \"install.podman.root_setup.zh\") text=\"检测到 root 用户。正在配置系统级 agentteams-podman-restart 服务...\" ;;\n \"install.podman.root_setup.en\") text=\"Root user detected. Configuring system-wide agentteams-podman-restart service...\" ;;\n \"install.podman.root_success.zh\") text=\"✅ 系统级 agentteams-podman-restart 服务已成功启用并运行。\" ;;\n \"install.podman.root_success.en\") text=\"✅ System-wide agentteams-podman-restart service successfully enabled and running.\" ;;\n \"install.podman.root_fail.zh\") text=\"⚠️ 系统级 agentteams-podman-restart 服务启用失败,请稍后手动检查。\" ;;\n \"install.podman.root_fail.en\") text=\"⚠️ Failed to enable system-wide agentteams-podman-restart service. Please check manually later.\" ;;\n \"install.podman.user_setup.zh\") text=\"检测到普通用户 (%s)。正在配置用户级 agentteams-podman-restart 服务...\" ;;\n \"install.podman.user_setup.en\") text=\"Non-root user (%s) detected. Configuring user-level agentteams-podman-restart service...\" ;;\n \"install.podman.user_success.zh\") text=\"✅ 用户级 agentteams-podman-restart 服务已成功启用并运行。\" ;;\n \"install.podman.user_success.en\") text=\"✅ User-level agentteams-podman-restart service successfully enabled and running.\" ;;\n \"install.podman.user_fail.zh\") text=\"⚠️ 用户级 agentteams-podman-restart 服务启用失败。\\n提示:这通常是因为缺少 XDG_RUNTIME_DIR 或 dbus 没有运行。您可以尝试手动执行: systemctl --user enable --now agentteams-podman-restart.service\" ;;\n \"install.podman.user_fail.en\") text=\"⚠️ Failed to enable user-level agentteams-podman-restart service.\\nHint: This is usually due to missing XDG_RUNTIME_DIR or dbus not running. Try manually: systemctl --user enable --now agentteams-podman-restart.service\" ;;\n \"install.podman.success.zh\") text=\"Podman 开机自启配置完成。\" ;;\n \"install.podman.success.en\") text=\"Podman autostart successfully configured.\" ;;\n # --- Worker idle timeout ---\n \"idle_timeout.prompt.zh\") text=\"Worker 空闲自动停止超时(分钟)[720]\" ;;\n \"idle_timeout.prompt.en\") text=\"Worker idle auto-stop timeout in minutes [720]\" ;;\n \"idle_timeout.selected.zh\") text=\"Worker 空闲超时: %s 分钟\" ;;\n \"idle_timeout.selected.en\") text=\"Worker idle timeout: %s minutes\" ;;\n \"idle_timeout.label.zh\") text=\"Worker 空闲超时(分钟)\" ;;\n \"idle_timeout.label.en\") text=\"Worker idle timeout (min)\" ;;\n # --- YOLO mode ---\n \"install.yolo.zh\") text=\"YOLO 模式已启用(自主决策,无交互提示)\" ;;\n \"install.yolo.en\") text=\"YOLO mode enabled (autonomous decisions, no interactive prompts)\" ;;\n # --- Image pulling ---\n \"install.image.exists.zh\") text=\"Manager 镜像已存在: %s\" ;;\n \"install.image.exists.en\") text=\"Manager image already exists locally: %s\" ;;\n \"install.image.pulling_manager.zh\") text=\"正在拉取 Manager 镜像: %s\" ;;\n \"install.image.pulling_manager.en\") text=\"Pulling Manager image: %s\" ;;\n \"install.image.worker_exists.zh\") text=\"Worker 镜像已存在: %s\" ;;\n \"install.image.worker_exists.en\") text=\"Worker image already exists locally: %s\" ;;\n \"install.image.pulling_worker.zh\") text=\"正在拉取 Worker 镜像: %s\" ;;\n \"install.image.pulling_worker.en\") text=\"Pulling Worker image: %s\" ;;\n # --- Starting container ---\n \"install.starting_manager.zh\") text=\"正在启动 Manager 容器...\" ;;\n \"install.starting_manager.en\") text=\"Starting Manager container...\" ;;\n # --- Wait for Manager ready ---\n \"install.wait_ready.zh\") text=\"等待 Manager Agent 就绪(超时: %ss)...\" ;;\n \"install.wait_ready.en\") text=\"Waiting for Manager agent to be ready (timeout: %ss)...\" ;;\n \"install.wait_ready.ok.zh\") text=\"Manager Agent 已就绪!\" ;;\n \"install.wait_ready.ok.en\") text=\"Manager agent is ready!\" ;;\n \"install.wait_ready.waiting.zh\") text=\"等待中... (%ds/%ds)\" ;;\n \"install.wait_ready.waiting.en\") text=\"Waiting... (%ds/%ds)\" ;;\n \"install.wait_ready.timeout.zh\") text=\"Manager Agent 在 %ss 内未就绪。请检查: docker logs %s\" ;;\n \"install.wait_ready.timeout.en\") text=\"Manager agent did not become ready within %ss. Check: docker logs %s\" ;;\n # --- Wait for Matrix ready ---\n \"install.wait_matrix.zh\") text=\"等待 Matrix 服务就绪(超时: %ss)...\" ;;\n \"install.wait_matrix.en\") text=\"Waiting for Matrix server to be ready (timeout: %ss)...\" ;;\n \"install.wait_matrix.ok.zh\") text=\"Matrix 服务已就绪!\" ;;\n \"install.wait_matrix.ok.en\") text=\"Matrix server is ready!\" ;;\n \"install.wait_matrix.waiting.zh\") text=\"等待 Matrix 中... (%ds/%ds)\" ;;\n \"install.wait_matrix.waiting.en\") text=\"Waiting for Matrix... (%ds/%ds)\" ;;\n \"install.wait_matrix.timeout.zh\") text=\"Matrix 服务在 %ss 内未就绪。请检查: docker logs %s\" ;;\n \"install.wait_matrix.timeout.en\") text=\"Matrix server did not become ready within %ss. Check: docker logs %s\" ;;\n # --- OpenAI-compatible connectivity test ---\n \"llm.openai.test.testing.zh\") text=\"正在测试 API 联通性...\" ;;\n \"llm.openai.test.testing.en\") text=\"Testing API connectivity...\" ;;\n \"llm.openai.test.ok.zh\") text=\"✅ API 联通性测试通过\" ;;\n \"llm.openai.test.ok.en\") text=\"✅ API connectivity test passed\" ;;\n \"llm.openai.test.fail.zh\") text=\"⚠️ API 联通性测试失败(HTTP %s)。响应内容:\\n%s\\n请根据以上错误信息联系您的模型服务商解决。\" ;;\n \"llm.openai.test.fail.en\") text=\"⚠️ API connectivity test failed (HTTP %s). Response body:\\n%s\\nPlease contact your model provider to resolve the issue.\" ;;\n \"llm.openai.test.fail.tokenplan.zh\") text=\"⚠️ 提示: 请确认 API Key 有效且已开通通义 Token 套餐。文档: https://help.aliyun.com/zh/model-studio/token-plan-quickstart\" ;;\n \"llm.openai.test.fail.tokenplan.en\") text=\"⚠️ Hint: Verify your Token Plan API key and compatible-mode access. Docs: https://help.aliyun.com/zh/model-studio/token-plan-quickstart\" ;;\n \"llm.openai.test.fail.codingplan.zh\") text=\"⚠️ 提示: 请确认 API Key 有效且已开通通义 Token 套餐。文档: https://help.aliyun.com/zh/model-studio/token-plan-quickstart\" ;;\n \"llm.openai.test.fail.codingplan.en\") text=\"⚠️ Hint: Verify your DASHSCOPE_API_KEY for Qwen Cloud. API keys: https://home.qwencloud.com/api-keys Docs: https://docs.qwencloud.com/\" ;;\n \"llm.openai.test.fail.codingplan_legacy.zh\") text=\"⚠️ 提示: 请确认 API Key 有效且 Coding 套餐接口可用。文档: https://help.aliyun.com/zh/model-studio/get-api-key\" ;;\n \"llm.openai.test.fail.codingplan_legacy.en\") text=\"⚠️ Hint: Verify your DashScope API key and Coding Plan access. Docs: https://help.aliyun.com/zh/model-studio/get-api-key\" ;;\n \"llm.openai.test.no_curl.zh\") text=\"⚠️ 未找到 curl,跳过 API 联通性测试\" ;;\n \"llm.openai.test.no_curl.en\") text=\"⚠️ curl not found, skipping API connectivity test\" ;;\n \"llm.openai.test.confirm.zh\") text=\"是否仍要继续安装?[y/N/b] \" ;;\n \"llm.openai.test.confirm.en\") text=\"Continue with installation anyway? [y/N/b] \" ;;\n \"llm.embedding.title.zh\") text=\"📦 记忆搜索配置\" ;;\n \"llm.embedding.title.en\") text=\"📦 Memory Search Configuration\" ;;\n \"llm.embedding.hint.zh\") text=\" Embedding 模型可提升记忆搜索质量(语义匹配)。不启用也可正常使用记忆功能(关键词匹配)。\" ;;\n \"llm.embedding.hint.en\") text=\" Embedding model improves memory search quality (semantic matching). Memory still works without it (keyword matching).\" ;;\n \"llm.embedding.option.default.zh\") text=\" 1) text-embedding-v4(推荐)\" ;;\n \"llm.embedding.option.default.en\") text=\" 1) text-embedding-v4 (Recommended)\" ;;\n \"llm.embedding.option.custom.zh\") text=\" 2) 自定义 Embedding 模型\" ;;\n \"llm.embedding.option.custom.en\") text=\" 2) Custom embedding model\" ;;\n \"llm.embedding.option.disable.zh\") text=\" 3) 不启用\" ;;\n \"llm.embedding.option.disable.en\") text=\" 3) Do not enable\" ;;\n \"llm.embedding.select.zh\") text=\"选择\" ;;\n \"llm.embedding.select.en\") text=\"Select\" ;;\n \"llm.embedding.custom_prompt.zh\") text=\" Embedding 模型名称\" ;;\n \"llm.embedding.custom_prompt.en\") text=\" Embedding model name\" ;;\n \"llm.embedding.test.testing.zh\") text=\"正在测试 Embedding API 联通性...\" ;;\n \"llm.embedding.test.testing.en\") text=\"Testing Embedding API connectivity...\" ;;\n \"llm.embedding.test.ok.zh\") text=\"✅ Embedding API 联通性测试通过\" ;;\n \"llm.embedding.test.ok.en\") text=\"✅ Embedding API connectivity test passed\" ;;\n \"llm.embedding.test.fail.zh\") text=\"⚠️ Embedding API 测试失败(HTTP %s)。响应: %s\" ;;\n \"llm.embedding.test.fail.en\") text=\"⚠️ Embedding API test failed (HTTP %s). Response: %s\" ;;\n \"llm.embedding.auto_disabled.zh\") text=\"⚠️ Embedding 已自动禁用,记忆搜索将使用关键词匹配。您可以稍后在 agentteams-manager.env 中设置 AGENTTEAMS_EMBEDDING_MODEL 启用。\" ;;\n \"llm.embedding.auto_disabled.en\") text=\"⚠️ Embedding auto-disabled. Memory search will use keyword matching. You can enable it later in agentteams-manager.env by setting AGENTTEAMS_EMBEDDING_MODEL.\" ;;\n \"llm.embedding.disabled.zh\") text=\"ℹ️ Embedding 已禁用,记忆搜索将使用关键词匹配。\" ;;\n \"llm.embedding.disabled.en\") text=\"ℹ️ Embedding disabled. Memory search will use keyword matching.\" ;;\n \"llm.openai.test.aborted.zh\") text=\"安装已中止。\" ;;\n \"llm.openai.test.aborted.en\") text=\"Installation aborted.\" ;;\n \"nav.back_hint.zh\") text=\"(输入 b 返回上一步)\" ;;\n \"nav.back_hint.en\") text=\"(enter b to go back)\" ;;\n # --- OpenAI-compatible provider creation ---\n \"install.openai_compat.missing.zh\") text=\"警告: OpenAI Base URL 或 API Key 未设置,跳过提供商创建\" ;;\n \"install.openai_compat.missing.en\") text=\"WARNING: OpenAI Base URL or API Key not set, skipping provider creation\" ;;\n \"install.openai_compat.creating.zh\") text=\"正在创建 OpenAI 兼容提供商...\" ;;\n \"install.openai_compat.creating.en\") text=\"Creating OpenAI-compatible provider...\" ;;\n \"install.openai_compat.domain.zh\") text=\" 域名: %s\" ;;\n \"install.openai_compat.domain.en\") text=\" Domain: %s\" ;;\n \"install.openai_compat.port.zh\") text=\" 端口: %s\" ;;\n \"install.openai_compat.port.en\") text=\" Port: %s\" ;;\n \"install.openai_compat.protocol.zh\") text=\" 协议: %s\" ;;\n \"install.openai_compat.protocol.en\") text=\" Protocol: %s\" ;;\n \"install.openai_compat.service_fail.zh\") text=\"警告: 创建 DNS 服务源失败(可能已存在)\" ;;\n \"install.openai_compat.service_fail.en\") text=\"WARNING: Failed to create DNS service source (may already exist)\" ;;\n \"install.openai_compat.provider_fail.zh\") text=\"警告: 创建 AI 提供商失败(可能已存在)\" ;;\n \"install.openai_compat.provider_fail.en\") text=\"WARNING: Failed to create AI provider (may already exist)\" ;;\n \"install.openai_compat.success.zh\") text=\"OpenAI 兼容提供商创建成功\" ;;\n \"install.openai_compat.success.en\") text=\"OpenAI-compatible provider created successfully\" ;;\n # --- Welcome message ---\n \"install.welcome_msg.soul_configured.zh\") text=\"Soul 已配置(找到 soul-configured 标记),跳过 onboarding 消息\" ;;\n \"install.welcome_msg.soul_configured.en\") text=\"Soul already configured (soul-configured marker found), skipping onboarding message\" ;;\n \"install.welcome_msg.logging_in.zh\") text=\"正在以 %s 身份登录以发送欢迎消息...\" ;;\n \"install.welcome_msg.logging_in.en\") text=\"Logging in as %s to send welcome message...\" ;;\n \"install.welcome_msg.login_failed.zh\") text=\"警告: 以 %s 身份登录失败,跳过欢迎消息\" ;;\n \"install.welcome_msg.login_failed.en\") text=\"WARNING: Failed to login as %s, skipping welcome message\" ;;\n \"install.welcome_msg.finding_room.zh\") text=\"正在查找与 Manager 的 DM 房间...\" ;;\n \"install.welcome_msg.finding_room.en\") text=\"Finding DM room with Manager...\" ;;\n \"install.welcome_msg.creating_room.zh\") text=\"正在创建与 Manager 的 DM 房间...\" ;;\n \"install.welcome_msg.creating_room.en\") text=\"Creating DM room with Manager...\" ;;\n \"install.welcome_msg.no_room.zh\") text=\"警告: 无法找到或创建与 Manager 的 DM 房间\" ;;\n \"install.welcome_msg.no_room.en\") text=\"WARNING: Could not find or create DM room with Manager\" ;;\n \"install.welcome_msg.waiting_join.zh\") text=\"等待 Manager 加入房间...\" ;;\n \"install.welcome_msg.waiting_join.en\") text=\"Waiting for Manager to join the room...\" ;;\n \"install.welcome_msg.sending.zh\") text=\"正在向 Manager 发送欢迎消息...\" ;;\n \"install.welcome_msg.sending.en\") text=\"Sending welcome message to Manager...\" ;;\n \"install.welcome_msg.send_failed.zh\") text=\"警告: 发送欢迎消息失败\" ;;\n \"install.welcome_msg.send_failed.en\") text=\"WARNING: Failed to send welcome message\" ;;\n \"install.welcome_msg.sent.zh\") text=\"欢迎消息已发送给 Manager\" ;;\n \"install.welcome_msg.sent.en\") text=\"Welcome message sent to Manager\" ;;\n \"install.welcome_msg.waiting.zh\") text=\"等待 Manager 发送欢迎消息(Higress 路由授权 + LLM 探活,约 45-90s)...\" ;;\n \"install.welcome_msg.waiting.en\") text=\"Waiting for Manager to send the welcome message (Higress route auth + LLM probe, ~45-90s)...\" ;;\n \"install.welcome_msg.confirmed.zh\") text=\"Manager 已确认发送欢迎消息(status.welcomeSent=true,用时 %ss)\" ;;\n \"install.welcome_msg.confirmed.en\") text=\"Manager confirmed welcome message sent (status.welcomeSent=true, %ss elapsed)\" ;;\n \"install.welcome_msg.timeout.zh\") text=\"警告: 在 %ss 内未观察到 Manager 发送欢迎消息(status.welcomeSent=true)。安装仍然成功,所有服务已就绪——可继续按下方提示登录 Element Web。\" ;;\n \"install.welcome_msg.timeout.en\") text=\"WARNING: Did not observe the Manager sending its welcome message (status.welcomeSent=true) within %ss. Installation is still successful, all services are up — continue with the Element Web instructions below.\" ;;\n \"install.welcome_msg.timeout_hint.zh\") text=\"手动触发 onboarding: 登录 Element Web → 打开与 Manager 的 DM 房间 → 发送任意一句话(例如 \\\"hi\\\"),Manager 会接管对话并开始引导。\" ;;\n \"install.welcome_msg.timeout_hint.en\") text=\"Manual onboarding: log in to Element Web → open the DM with the Manager → send any message (e.g. \\\"hi\\\") and the Manager will take over and start the guided setup.\" ;;\n \"install.welcome_msg.timeout_inspect.zh\") text=\"排查命令: docker exec agentteams-controller agt get managers default\" ;;\n \"install.welcome_msg.timeout_inspect.en\") text=\"Inspect status: docker exec agentteams-controller agt get managers default\" ;;\n \"install.welcome_msg.poll_unavailable.zh\") text=\"提示: agentteams-manager 内未找到 AgentTeams CLI,跳过 welcome 等待(旧镜像?)\" ;;\n \"install.welcome_msg.poll_unavailable.en\") text=\"Note: AgentTeams CLI not found inside agentteams-manager; skipping welcome wait (old image?)\" ;;\n # --- Dashboard wizard ---\n \"dash.prompt.zh\") text=\"是否安装 agentteams-dashboard 管理面板?\" ;;\n \"dash.prompt.en\") text=\"Install agentteams-dashboard management UI?\" ;;\n \"dash.version_prompt.zh\") text=\"Dashboard 版本\" ;;\n \"dash.version_prompt.en\") text=\"Dashboard version\" ;;\n \"dash.port_prompt.zh\") text=\"Dashboard 端口号\" ;;\n \"dash.port_prompt.en\") text=\"Dashboard port\" ;;\n \"dash.image_prompt.zh\") text=\"Dashboard 镜像\" ;;\n \"dash.image_prompt.en\") text=\"Dashboard image\" ;;\n \"dash.gateway_prompt.zh\") text=\"Higress Console URL (用于共享登录)\" ;;\n \"dash.gateway_prompt.en\") text=\"Higress Console URL (for shared login)\" ;;\n \"dash.skip.zh\") text=\"跳过 Dashboard 安装\" ;;\n \"dash.skip.en\") text=\"Skipping Dashboard installation\" ;;\n \"dash.auto_url.zh\") text=\"自动检测到 Higress Console URL\" ;;\n \"dash.auto_url.en\") text=\"Auto-detected Higress Console URL\" ;;\n \"dash.auto_url_fail.zh\") text=\"无法自动检测 Higress Console URL,请手动输入\" ;;\n \"dash.auto_url_fail.en\") text=\"Cannot auto-detect Higress Console URL, please enter manually\" ;;\n \"dash.summary.zh\") text=\"Dashboard 配置: 端口=%s 镜像=%s\" ;;\n \"dash.summary.en\") text=\"Dashboard config: port=%s image=%s\" ;;\n # --- Dashboard success ---\n \"success.dashboard.zh\") text=\" agentteams-dashboard 管理面板: http://localhost:%s/\" ;;\n \"success.dashboard.en\") text=\" agentteams-dashboard: http://localhost:%s/\" ;;\n # --- Final output panel ---\n \"success.title.zh\") text=\"=== AgentTeams Manager 已启动!===\" ;;\n \"success.title.en\") text=\"=== AgentTeams Manager Started! ===\" ;;\n \"success.domains_configured.zh\") text=\"以下域名已配置解析到 127.0.0.1:\" ;;\n \"success.domains_configured.en\") text=\"The following domains are configured to resolve to 127.0.0.1:\" ;;\n \"success.open_url.zh\") text=\" ★ 在浏览器中打开以下 URL 开始使用: ★\" ;;\n \"success.open_url.en\") text=\" ★ Open the following URL in your browser to start: ★\" ;;\n \"success.login_with.zh\") text=\" 登录信息:\" ;;\n \"success.login_with.en\") text=\" Login with:\" ;;\n \"success.username.zh\") text=\" 用户名: %s\" ;;\n \"success.username.en\") text=\" Username: %s\" ;;\n \"success.password.zh\") text=\" 密码: %s\" ;;\n \"success.password.en\") text=\" Password: %s\" ;;\n \"success.after_login.zh\") text=\" 登录后,开始与 Manager 聊天!\" ;;\n \"success.after_login.en\") text=\" After login, start chatting with the Manager!\" ;;\n \"success.tell_it.zh\") text=\" 告诉它: \\\"创建一个名为 alice 的前端开发 Worker\\\"\" ;;\n \"success.tell_it.en\") text=\" Tell it: \\\"Create a Worker named alice for frontend dev\\\"\" ;;\n \"success.manager_auto.zh\") text=\" Manager 会自动处理一切。\" ;;\n \"success.manager_auto.en\") text=\" The Manager will handle everything automatically.\" ;;\n \"success.mobile_title.zh\") text=\" 📱 移动端访问(FluffyChat / Element Mobile):\" ;;\n \"success.mobile_title.en\") text=\" 📱 Mobile access (FluffyChat / Element Mobile):\" ;;\n \"success.mobile_step1.zh\") text=\" 1. 在手机上下载 FluffyChat 或 Element\" ;;\n \"success.mobile_step1.en\") text=\" 1. Download FluffyChat or Element on your phone\" ;;\n \"success.mobile_step2.zh\") text=\" 2. 设置 homeserver 为: %s\" ;;\n \"success.mobile_step2.en\") text=\" 2. Set homeserver to: %s\" ;;\n \"success.mobile_step2_noip.zh\") text=\" 2. 设置 homeserver 为: http://<本机局域网IP>:%s\" ;;\n \"success.mobile_step2_noip.en\") text=\" 2. Set homeserver to: http://<this-machine-LAN-IP>:%s\" ;;\n \"success.mobile_noip_hint.zh\") text=\" (无法自动检测局域网 IP — 请使用 ifconfig / ip addr 查看)\" ;;\n \"success.mobile_noip_hint.en\") text=\" (Could not detect LAN IP automatically — check with: ifconfig / ip addr)\" ;;\n \"success.mobile_step3.zh\") text=\" 3. 登录信息:\" ;;\n \"success.mobile_step3.en\") text=\" 3. Login with:\" ;;\n \"success.mobile_username.zh\") text=\" 用户名: %s\" ;;\n \"success.mobile_username.en\") text=\" Username: %s\" ;;\n \"success.mobile_password.zh\") text=\" 密码: %s\" ;;\n \"success.mobile_password.en\") text=\" Password: %s\" ;;\n # --- Other consoles and tips ---\n \"success.other_consoles.zh\") text=\"--- 其他控制台 ---\" ;;\n \"success.other_consoles.en\") text=\"--- Other Consoles ---\" ;;\n \"success.higress_console.zh\") text=\" Higress 控制台: http://localhost:%s(用户名: %s / 密码: %s)\" ;;\n \"success.higress_console.en\") text=\" Higress Console: http://localhost:%s (Username: %s / Password: %s)\" ;;\n \"success.manager_console.zh\") text=\" Manager 控制台(本地): http://localhost:%s(无需登录)\" ;;\n \"success.manager_console.en\") text=\" Manager Console (local): http://localhost:%s (no login required)\" ;;\n \"success.manager_console_gateway.zh\") text=\" Manager 控制台(网关): http://console-local.agentteams.io(用户名: %s / 密码: %s)\" ;;\n \"success.manager_console_gateway.en\") text=\" Manager Console (gateway): http://console-local.agentteams.io (Username: %s / Password: %s)\" ;;\n \"success.copaw_console.zh\") text=\" CoPaw App API: http://localhost:%s(无需登录)\" ;;\n \"success.copaw_console.en\") text=\" CoPaw App API: http://localhost:%s (no login required)\" ;;\n \"success.switch_llm.title.zh\") text=\"--- 切换 LLM 提供商 ---\" ;;\n \"success.switch_llm.title.en\") text=\"--- Switch LLM Providers ---\" ;;\n \"success.switch_llm.hint.zh\") text=\" 您可以通过 Higress 控制台切换到其他 LLM 提供商(OpenAI、Anthropic 等)。\" ;;\n \"success.switch_llm.hint.en\") text=\" You can switch to other LLM providers (OpenAI, Anthropic, etc.) via Higress Console.\" ;;\n \"success.switch_llm.docs.zh\") text=\" 详细说明请参阅:\" ;;\n \"success.switch_llm.docs.en\") text=\" For detailed instructions, see:\" ;;\n \"success.switch_llm.url.zh\") text=\" https://higress.ai/en/docs/ai/scene-guide/multi-proxy#console-configuration\" ;;\n \"success.switch_llm.url.en\") text=\" https://higress.ai/en/docs/ai/scene-guide/multi-proxy#console-configuration\" ;;\n \"success.tip.zh\") text=\"提示: 您也可以在聊天中让 Manager 为您配置 LLM 提供商。\" ;;\n \"success.tip.en\") text=\"Tip: You can also ask the Manager to configure LLM providers for you in the chat.\" ;;\n \"success.config_file.zh\") text=\"配置文件: %s\" ;;\n \"success.config_file.en\") text=\"Configuration file: %s\" ;;\n \"success.data_volume.zh\") text=\"数据卷: %s\" ;;\n \"success.data_volume.en\") text=\"Data volume: %s\" ;;\n \"success.workspace.zh\") text=\"Manager 工作空间: %s\" ;;\n \"success.workspace.en\") text=\"Manager workspace: %s\" ;;\n # --- Worker installation ---\n \"worker.resetting.zh\") text=\"正在重置 Worker: %s...\" ;;\n \"worker.resetting.en\") text=\"Resetting Worker: %s...\" ;;\n \"worker.exists.zh\") text=\"容器 '%s' 已存在。使用 --reset 重新创建。\" ;;\n \"worker.exists.en\") text=\"Container '%s' already exists. Use --reset to recreate.\" ;;\n \"worker.starting.zh\") text=\"正在启动 Worker: %s...\" ;;\n \"worker.starting.en\") text=\"Starting Worker: %s...\" ;;\n \"worker.skills_url.zh\") text=\" Skills API URL: %s\" ;;\n \"worker.skills_url.en\") text=\" Skills API URL: %s\" ;;\n \"worker.started.zh\") text=\"=== Worker %s 已启动!===\" ;;\n \"worker.started.en\") text=\"=== Worker %s Started! ===\" ;;\n \"worker.container.zh\") text=\"容器: %s\" ;;\n \"worker.container.en\") text=\"Container: %s\" ;;\n \"worker.view_logs.zh\") text=\"查看日志: docker logs -f %s\" ;;\n \"worker.view_logs.en\") text=\"View logs: docker logs -f %s\" ;;\n # --- Prompt function messages ---\n \"prompt.preset.zh\") text=\" %s = (已通过环境变量预设)\" ;;\n \"prompt.preset.en\") text=\" %s = (pre-set via env)\" ;;\n \"prompt.upgrade_keep.zh\") text=\" %s = %s(当前值,回车保留 / 输入新值覆盖)\" ;;\n \"prompt.upgrade_keep.en\") text=\" %s = %s (current value, press Enter to keep / type new value to change)\" ;;\n \"prompt.upgrade_keep_secret.zh\") text=\" %s = %s(当前值,回车保留 / 输入新值覆盖)\" ;;\n \"prompt.upgrade_keep_secret.en\") text=\" %s = %s (current value, press Enter to keep / type new value to change)\" ;;\n \"prompt.upgrade_empty.zh\") text=\" %s = (未设置,回车跳过 / 输入新值设置)\" ;;\n \"prompt.upgrade_empty.en\") text=\" %s = (not set, press Enter to skip / type new value to set)\" ;;\n \"prompt.default.zh\") text=\" %s = %s(默认)\" ;;\n \"prompt.default.en\") text=\" %s = %s (default)\" ;;\n \"prompt.required.zh\") text=\"%s 是必需的(在非交互模式下通过环境变量设置)\" ;;\n \"prompt.required.en\") text=\"%s is required (set via environment variable in non-interactive mode)\" ;;\n \"prompt.required_empty.zh\") text=\"%s 是必需的\" ;;\n \"prompt.required_empty.en\") text=\"%s is required\" ;;\n # --- Language switch prompt (bilingual by design) ---\n \"lang.detected.zh\") text=\"检测到语言 / Detected language: 中文\" ;;\n \"lang.detected.en\") text=\"检测到语言 / Detected language: English\" ;;\n \"lang.switch_title.zh\") text=\"切换语言 / Switch language:\" ;;\n \"lang.switch_title.en\") text=\"切换语言 / Switch language:\" ;;\n \"lang.option_zh.zh\") text=\" 1) 中文\" ;;\n \"lang.option_zh.en\") text=\" 1) 中文\" ;;\n \"lang.option_en.zh\") text=\" 2) English\" ;;\n \"lang.option_en.en\") text=\" 2) English\" ;;\n \"lang.prompt.zh\") text=\"请选择 / Enter choice\" ;;\n \"lang.prompt.en\") text=\"请选择 / Enter choice\" ;;\n # --- Uninstall messages ---\n \"uninstall.title.zh\") text=\"正在卸载 AgentTeams...\" ;;\n \"uninstall.title.en\") text=\"Uninstalling AgentTeams...\" ;;\n \"uninstall.stopping_manager.zh\") text=\"正在停止并移除 agentteams-manager...\" ;;\n \"uninstall.stopping_manager.en\") text=\"Stopping and removing agentteams-manager...\" ;;\n \"uninstall.stopping_workers.zh\") text=\"正在停止并移除 Worker 容器...\" ;;\n \"uninstall.stopping_workers.en\") text=\"Stopping and removing worker containers...\" ;;\n \"uninstall.removed.zh\") text=\" 已移除: %s\" ;;\n \"uninstall.removed.en\") text=\" Removed: %s\" ;;\n \"uninstall.removing_volume.zh\") text=\"正在移除 Docker 卷: %s\" ;;\n \"uninstall.removing_volume.en\") text=\"Removing Docker volume: %s\" ;;\n \"uninstall.removing_env.zh\") text=\"正在移除 env 文件: %s\" ;;\n \"uninstall.removing_env.en\") text=\"Removing env file: %s\" ;;\n \"uninstall.removing_proxy.zh\") text=\"正在停止并移除 Docker API 代理容器: agentteams-docker-proxy\" ;;\n \"uninstall.removing_proxy.en\") text=\"Stopping and removing Docker API proxy container: agentteams-docker-proxy\" ;;\n \"uninstall.stopping_controller.zh\") text=\"正在停止并移除 agentteams-controller (内嵌 Tuwunel/MinIO/Higress)...\" ;;\n \"uninstall.stopping_controller.en\") text=\"Stopping and removing agentteams-controller (embedded Tuwunel/MinIO/Higress)...\" ;;\n \"uninstall.removing_network.zh\") text=\"正在移除 Docker 网络: agentteams-net\" ;;\n \"uninstall.removing_network.en\") text=\"Removing Docker network: agentteams-net\" ;;\n \"uninstall.removing_workspace.zh\") text=\"正在移除工作空间目录: %s\" ;;\n \"uninstall.removing_workspace.en\") text=\"Removing workspace directory: %s\" ;;\n \"uninstall.removing_workspace_elevated.zh\") text=\" 工作空间包含 root 文件,通过容器清理...\" ;;\n \"uninstall.removing_workspace_elevated.en\") text=\" Workspace contains root-owned files, cleaning via container...\" ;;\n \"uninstall.removing_log.zh\") text=\"正在移除日志文件: %s\" ;;\n \"uninstall.removing_log.en\") text=\"Removing log file: %s\" ;;\n \"uninstall.done.zh\") text=\"AgentTeams 已卸载。\" ;;\n \"uninstall.done.en\") text=\"AgentTeams has been uninstalled.\" ;;\n # --- Error messages ---\n \"error.name_required.zh\") text=\"--name 是必需的\" ;;\n \"error.name_required.en\") text=\"--name is required\" ;;\n \"error.fs_required.zh\") text=\"--fs 是必需的\" ;;\n \"error.fs_required.en\") text=\"--fs is required\" ;;\n \"error.fs_key_required.zh\") text=\"--fs-key 是必需的\" ;;\n \"error.fs_key_required.en\") text=\"--fs-key is required\" ;;\n \"error.fs_secret_required.zh\") text=\"--fs-secret 是必需的\" ;;\n \"error.fs_secret_required.en\") text=\"--fs-secret is required\" ;;\n \"error.unknown_option.zh\") text=\"未知选项: %s\" ;;\n \"error.unknown_option.en\") text=\"Unknown option: %s\" ;;\n \"error.docker_not_found.zh\") text=\"未找到 docker 或 podman 命令。请先安装 Docker Desktop 或 Podman Desktop:\\n Docker Desktop: https://www.docker.com/products/docker-desktop/\\n Podman Desktop: https://podman-desktop.io/\" ;;\n \"error.docker_not_found.en\") text=\"docker or podman command not found. Please install Docker Desktop or Podman Desktop first:\\n Docker Desktop: https://www.docker.com/products/docker-desktop/\\n Podman Desktop: https://podman-desktop.io/\" ;;\n \"error.docker_not_running.zh\") text=\"Docker 未运行。请先启动 Docker Desktop 或 Podman Desktop。\" ;;\n \"error.docker_not_running.en\") text=\"Docker is not running. Please start Docker Desktop or Podman Desktop first.\" ;;\n # --- Fallback: try English for unknown lang ---\n *)\n case \"${key}.en\" in\n \"tz.warning.title.en\") text=\"Could not detect timezone automatically.\" ;;\n \"install.title.en\") text=\"=== AgentTeams Manager Installation ===\" ;;\n *) text=\"${key}\" ;;\n esac\n ;;\n esac\n if [ $# -gt 0 ]; then\n # shellcheck disable=SC2059\n printf \"${text}\\n\" \"$@\"\n else\n echo \"${text}\"\n fi\n}\n\n# ============================================================\n# Registry selection based on timezone\n# ============================================================\n\ndetect_registry() {\n local tz=\"${AGENTTEAMS_TIMEZONE}\"\n\n case \"${tz}\" in\n America/*)\n echo \"higress-registry.us-west-1.cr.aliyuncs.com\"\n ;;\n Asia/Singapore|Asia/Bangkok|Asia/Jakarta|Asia/Makassar|Asia/Jayapura|\\\n Asia/Kuala_Lumpur|Asia/Ho_Chi_Minh|Asia/Manila|Asia/Yangon|\\\n Asia/Vientiane|Asia/Phnom_Penh|Asia/Pontianak|Asia/Ujung_Pandang)\n echo \"higress-registry.ap-southeast-7.cr.aliyuncs.com\"\n ;;\n *)\n echo \"higress-registry.cn-hangzhou.cr.aliyuncs.com\"\n ;;\n esac\n}\n\nAGENTTEAMS_REGISTRY=\"${AGENTTEAMS_REGISTRY:-$(detect_registry)}\"\n# Backward compatibility: accept old env var names from previous versions\nAGENTTEAMS_INSTALL_CONTROLLER_IMAGE=\"${AGENTTEAMS_INSTALL_CONTROLLER_IMAGE:-${AGENTTEAMS_INSTALL_DOCKER_PROXY_IMAGE:-}}\"\n# Image variables are resolved after version selection in step_version().\n# These placeholders allow early code paths to reference them without errors.\nMANAGER_IMAGE=\"${AGENTTEAMS_INSTALL_MANAGER_IMAGE:-}\"\nMANAGER_COPAW_IMAGE=\"${AGENTTEAMS_INSTALL_MANAGER_COPAW_IMAGE:-}\"\nWORKER_IMAGE=\"${AGENTTEAMS_INSTALL_WORKER_IMAGE:-}\"\nCOPAW_WORKER_IMAGE=\"${AGENTTEAMS_INSTALL_COPAW_WORKER_IMAGE:-}\"\nQWENPAW_WORKER_IMAGE=\"${AGENTTEAMS_INSTALL_QWENPAW_WORKER_IMAGE:-}\"\nHERMES_WORKER_IMAGE=\"${AGENTTEAMS_INSTALL_HERMES_WORKER_IMAGE:-}\"\nCONTROLLER_IMAGE=\"${AGENTTEAMS_INSTALL_CONTROLLER_IMAGE:-}\"\n\nresolve_image_tags() {\n AGENTTEAMS_VERSION=\"$(_normalize_version \"${AGENTTEAMS_VERSION}\")\"\n MANAGER_IMAGE=\"${AGENTTEAMS_INSTALL_MANAGER_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-manager:${AGENTTEAMS_VERSION}}\"\n MANAGER_COPAW_IMAGE=\"${AGENTTEAMS_INSTALL_MANAGER_COPAW_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-manager-copaw:${AGENTTEAMS_VERSION}}\"\n WORKER_IMAGE=\"${AGENTTEAMS_INSTALL_WORKER_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-worker:${AGENTTEAMS_VERSION}}\"\n COPAW_WORKER_IMAGE=\"${AGENTTEAMS_INSTALL_COPAW_WORKER_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-copaw-worker:${AGENTTEAMS_VERSION}}\"\n QWENPAW_WORKER_IMAGE=\"${AGENTTEAMS_INSTALL_QWENPAW_WORKER_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-qwenpaw-worker:${AGENTTEAMS_VERSION}}\"\n HERMES_WORKER_IMAGE=\"${AGENTTEAMS_INSTALL_HERMES_WORKER_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-hermes-worker:${AGENTTEAMS_VERSION}}\"\n EMBEDDED_IMAGE=\"${AGENTTEAMS_INSTALL_EMBEDDED_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-embedded:${AGENTTEAMS_VERSION}}\"\n # CoPaw Worker introduced in v1.0.4; Hermes Worker introduced in v1.1.0\n if [ -z \"${AGENTTEAMS_INSTALL_COPAW_WORKER_IMAGE:-}\" ] && _ver_lt \"${AGENTTEAMS_VERSION}\" \"v1.0.4\"; then\n COPAW_WORKER_IMAGE=\"\"\n fi\n if [ -z \"${AGENTTEAMS_INSTALL_HERMES_WORKER_IMAGE:-}\" ] && _ver_lt \"${AGENTTEAMS_VERSION}\" \"v1.1.0\"; then\n HERMES_WORKER_IMAGE=\"\"\n fi\n}\n\n# Resolve the embedded controller image. Embedded mode is the only supported\n# architecture since PR #616 (manager image no longer bundles Higress/Tuwunel/MinIO).\n# If the embedded image is unavailable for the requested version, fail fast with an\n# actionable error rather than silently falling back to the legacy single-container\n# path — that path is permanently broken with the slim manager image and would just\n# leave the user with a manager container looping on \"Waiting for Higress Gateway\".\n# Sets EMBEDDED_IMAGE and AGENTTEAMS_USE_EMBEDDED.\nresolve_embedded_image() {\n AGENTTEAMS_USE_EMBEDDED=1\n\n # If the user explicitly overrode the image (e.g. `make install-embedded` passes\n # a locally-built tag), respect it as-is without any registry probe.\n if [ -n \"${AGENTTEAMS_INSTALL_EMBEDDED_IMAGE:-}\" ]; then\n EMBEDDED_IMAGE=\"${AGENTTEAMS_INSTALL_EMBEDDED_IMAGE}\"\n return 0\n fi\n\n local _versioned=\"${AGENTTEAMS_REGISTRY}/agentteams/agentteams-embedded:${AGENTTEAMS_VERSION}\"\n local _latest=\"${AGENTTEAMS_REGISTRY}/agentteams/agentteams-embedded:latest\"\n\n # Skip probe when AGENTTEAMS_VERSION is \"latest\" — no point trying the same tag twice.\n if [ \"${AGENTTEAMS_VERSION}\" = \"latest\" ]; then\n EMBEDDED_IMAGE=\"${_latest}\"\n return 0\n fi\n\n if ${DOCKER_CMD} pull \"${_versioned}\" >/dev/null 2>&1; then\n EMBEDDED_IMAGE=\"${_versioned}\"\n return 0\n fi\n\n # Versions before v1.1.0 predate agentteams-embedded entirely — their manager image\n # bundled all infrastructure. Falling back to agentteams-embedded:latest would\n # silently swap in the v1.1.0 architecture (embedded kube-apiserver) which\n # crashes under QEMU on Apple Silicon. Auto-activate legacy mode instead.\n if _ver_lt \"${AGENTTEAMS_VERSION}\" \"v1.1.0\"; then\n log \"INFO: ${AGENTTEAMS_VERSION} predates agentteams-embedded; switching to legacy all-in-one manager architecture.\"\n log \"WARNING: Legacy all-in-one mode requires AGENTTEAMS_VERSION <= v1.0.9 (older bundled manager image).\"\n log \"WARNING: Newer slim manager images will hang on 'Waiting for Higress Gateway'.\"\n AGENTTEAMS_USE_EMBEDDED=0\n return 0\n fi\n\n if ${DOCKER_CMD} pull \"${_latest}\" >/dev/null 2>&1; then\n log \"embedded ${AGENTTEAMS_VERSION} not found, using latest\"\n EMBEDDED_IMAGE=\"${_latest}\"\n return 0\n fi\n\n # Explicit escape hatch — still honoured for edge cases.\n if [ \"${AGENTTEAMS_FORCE_LEGACY:-0}\" = \"1\" ]; then\n log \"WARNING: AGENTTEAMS_FORCE_LEGACY=1 — using legacy all-in-one manager architecture.\"\n log \"WARNING: This requires AGENTTEAMS_VERSION <= v1.0.9 (older bundled manager image).\"\n log \"WARNING: Newer slim manager images will hang on 'Waiting for Higress Gateway'.\"\n AGENTTEAMS_USE_EMBEDDED=0\n return 0\n fi\n\n error \"Embedded controller image is not available in the registry:\"\n error \" - tried: ${_versioned}\"\n error \" - tried: ${_latest}\"\n error \"\"\n error \"Embedded mode is the only supported architecture since PR #616.\"\n error \"How to resolve:\"\n error \" 1) Pin to a AGENTTEAMS_VERSION whose embedded image has been published, or\"\n error \" wait for the release pipeline to publish it.\"\n error \" 2) For a local build, run: make install-embedded\"\n error \" (builds and uses the local embedded image without touching the registry).\"\n error \" 3) Override with a custom image: AGENTTEAMS_INSTALL_EMBEDDED_IMAGE=... ./agentteams-install.sh\"\n exit 1\n}\n\n# ============================================================\n# Known models list — used to detect custom models during install\n# ============================================================\nKNOWN_MODELS=\"gpt-5.4 gpt-5.3-codex gpt-5-mini gpt-5-nano claude-opus-4-6 claude-sonnet-4-6 claude-haiku-4-5 qwen3.6-plus qwen3.5-plus deepseek-chat deepseek-reasoner kimi-k2.5 glm-5 MiniMax-M2.7 MiniMax-M2.7-highspeed MiniMax-M2.5\"\n\nis_known_model() {\n local model=\"$1\"\n for m in ${KNOWN_MODELS}; do\n [ \"${m}\" = \"${model}\" ] && return 0\n done\n return 1\n}\n\n# Prompt user for custom model parameters when model is not in the known list.\n# Sets: AGENTTEAMS_MODEL_CONTEXT_WINDOW, AGENTTEAMS_MODEL_MAX_TOKENS, AGENTTEAMS_MODEL_REASONING, AGENTTEAMS_MODEL_VISION\nprompt_custom_model_params() {\n local model=\"$1\"\n if is_known_model \"${model}\"; then\n # Clear any stale custom params for known models\n AGENTTEAMS_MODEL_CONTEXT_WINDOW=\"\"\n AGENTTEAMS_MODEL_MAX_TOKENS=\"\"\n AGENTTEAMS_MODEL_REASONING=\"\"\n AGENTTEAMS_MODEL_VISION=\"\"\n return\n fi\n echo \"\"\n log \"$(msg llm.custom_model.detected \"${model}\")\"\n echo \"\"\n read -e -p \" $(msg llm.custom_model.context_prompt): \" AGENTTEAMS_MODEL_CONTEXT_WINDOW\n if [ \"${AGENTTEAMS_MODEL_CONTEXT_WINDOW}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n AGENTTEAMS_MODEL_CONTEXT_WINDOW=\"${AGENTTEAMS_MODEL_CONTEXT_WINDOW:-150000}\"\n read -e -p \" $(msg llm.custom_model.max_tokens_prompt): \" AGENTTEAMS_MODEL_MAX_TOKENS\n if [ \"${AGENTTEAMS_MODEL_MAX_TOKENS}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n AGENTTEAMS_MODEL_MAX_TOKENS=\"${AGENTTEAMS_MODEL_MAX_TOKENS:-128000}\"\n read -e -p \" $(msg llm.custom_model.reasoning_prompt): \" _reasoning\n if [ \"${_reasoning}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n case \"${_reasoning}\" in\n n|N|no|NO) AGENTTEAMS_MODEL_REASONING=\"false\" ;;\n *) AGENTTEAMS_MODEL_REASONING=\"true\" ;;\n esac\n read -e -p \" $(msg llm.custom_model.vision_prompt): \" _vision\n if [ \"${_vision}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n case \"${_vision}\" in\n y|Y|yes|YES) AGENTTEAMS_MODEL_VISION=\"true\" ;;\n *) AGENTTEAMS_MODEL_VISION=\"false\" ;;\n esac\n log \"$(msg llm.custom_model.summary \"${AGENTTEAMS_MODEL_CONTEXT_WINDOW}\" \"${AGENTTEAMS_MODEL_MAX_TOKENS}\" \"${AGENTTEAMS_MODEL_REASONING}\" \"${AGENTTEAMS_MODEL_VISION}\")\"\n}\n\n# ============================================================\n# Wait for Manager agent to be ready\n# Uses `openclaw gateway health` inside the container to confirm the gateway is running\n# ============================================================\n\nwait_manager_ready() {\n local timeout=\"${AGENTTEAMS_READY_TIMEOUT:-300}\"\n local elapsed=0\n local container=\"${1:-agentteams-manager}\"\n\n log \"$(msg install.wait_ready \"${timeout}\")\"\n\n # Wait for Manager agent to be healthy inside the container\n local runtime=\"${AGENTTEAMS_MANAGER_RUNTIME:-copaw}\"\n while [ \"${elapsed}\" -lt \"${timeout}\" ]; do\n case \"${runtime}\" in\n copaw)\n if ${DOCKER_CMD} exec \"${container}\" curl -sf http://127.0.0.1:18799/api/agents 2>/dev/null | grep -q '\"agents\"'; then\n log \"$(msg install.wait_ready.ok)\"\n return 0\n fi\n ;;\n *)\n if ${DOCKER_CMD} exec \"${container}\" openclaw gateway health --json 2>/dev/null | grep -q '\"ok\"' 2>/dev/null; then\n log \"$(msg install.wait_ready.ok)\"\n return 0\n fi\n ;;\n esac\n sleep 5\n elapsed=$((elapsed + 5))\n printf \"\\r\\033[36m[AgentTeams]\\033[0m $(msg install.wait_ready.waiting \"${elapsed}\" \"${timeout}\")\"\n done\n\n echo \"\"\n die \"$(msg install.wait_ready.timeout \"${timeout}\" \"${container}\")\"\n}\n\nwait_matrix_ready() {\n local timeout=\"${AGENTTEAMS_READY_TIMEOUT:-300}\"\n local elapsed=0\n local container=\"${1:-agentteams-manager}\"\n\n log \"$(msg install.wait_matrix \"${timeout}\")\"\n\n while [ \"${elapsed}\" -lt \"${timeout}\" ]; do\n if ${DOCKER_CMD} exec \"${container}\" curl -sf http://127.0.0.1:6167/_tuwunel/server_version >/dev/null 2>&1; then\n log \"$(msg install.wait_matrix.ok)\"\n return 0\n fi\n sleep 5\n elapsed=$((elapsed + 5))\n printf \"\\r\\033[36m[AgentTeams]\\033[0m $(msg install.wait_matrix.waiting \"${elapsed}\" \"${timeout}\")\"\n done\n\n echo \"\"\n die \"$(msg install.wait_matrix.timeout \"${timeout}\" \"${container}\")\"\n}\n\n# Read KEY=value from /data/agentteams-secrets.env on a Docker volume (manager container not required).\n# Requires EMBEDDED_IMAGE (resolved earlier in install_manager). Uses ${DOCKER_CMD}.\nagentteams_read_secret_from_data_volume() {\n local _vol=\"$1\" _key=\"$2\"\n if [ -z \"${_vol}\" ] || [ -z \"${_key}\" ] || [ -z \"${EMBEDDED_IMAGE:-}\" ]; then\n echo \"\"\n return 0\n fi\n ${DOCKER_CMD} run --rm --entrypoint sh \\\n -v \"${_vol}:/data:ro\" \\\n \"${EMBEDDED_IMAGE}\" -c \"grep \\\"^${_key}=\\\" /data/agentteams-secrets.env 2>/dev/null | cut -d= -f2- | head -1 | tr -d '\\r'\" 2>/dev/null\n}\n\n# Read KEY=value from /data/worker-creds/<worker>.env on a Docker volume.\nagentteams_read_worker_creds_value_from_volume() {\n local _vol=\"$1\" _worker=\"$2\" _key=\"$3\"\n if [ -z \"${_vol}\" ] || [ -z \"${_worker}\" ] || [ -z \"${_key}\" ] || [ -z \"${EMBEDDED_IMAGE:-}\" ]; then\n echo \"\"\n return 0\n fi\n ${DOCKER_CMD} run --rm --entrypoint sh \\\n -v \"${_vol}:/data:ro\" \\\n \"${EMBEDDED_IMAGE}\" -c \"grep \\\"^${_key}=\\\" \\\"/data/worker-creds/${_worker}.env\\\" 2>/dev/null | cut -d= -f2- | head -1 | tr -d \\\"\\\\r\\\"\" 2>/dev/null\n}\n\n# Read admin_dm_room_id from host workspace state.json (fallback when Matrix API is unavailable).\nagentteams_read_admin_dm_room_from_workspace() {\n local _ws=\"$1\"\n local _f=\"${_ws}/state.json\"\n if [ ! -f \"${_f}\" ] || ! command -v jq >/dev/null 2>&1; then\n echo \"\"\n return 0\n fi\n jq -r '.admin_dm_room_id // empty | select(. != \"null\")' \"${_f}\" 2>/dev/null\n}\n\n# Read secret input with masked echo (shows * per keystroke, supports backspace)\n# Usage: read_secret \"prompt text: \"; value=\"${_RS_RESULT}\"\nread_secret() {\n local _rs_prompt=\"$1\"\n _RS_RESULT=\"\"\n local _rs_char=\"\"\n\n printf \"%s\" \"${_rs_prompt}\"\n\n while IFS= read -r -s -n 1 _rs_char; do\n if [[ -z \"${_rs_char}\" ]]; then\n break\n elif [[ \"${_rs_char}\" == $'\\177' ]] || [[ \"${_rs_char}\" == $'\\b' ]]; then\n if [ -n \"${_RS_RESULT}\" ]; then\n _RS_RESULT=\"${_RS_RESULT%?}\"\n printf \"\\b \\b\"\n fi\n else\n _RS_RESULT=\"${_RS_RESULT}${_rs_char}\"\n printf \"*\"\n fi\n done\n echo\n}\n\n# Load current parameter values from env file for upgrade mode display\nload_current_params_from_env() {\n local env_file=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n if [ -f \"${env_file}\" ]; then\n [ -z \"${AGENTTEAMS_LLM_PROVIDER:+x}\" ] && AGENTTEAMS_LLM_PROVIDER=\"$(grep '^AGENTTEAMS_LLM_PROVIDER=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_OPENAI_BASE_URL:+x}\" ] && AGENTTEAMS_OPENAI_BASE_URL=\"$(grep '^AGENTTEAMS_OPENAI_BASE_URL=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_DEFAULT_MODEL:+x}\" ] && AGENTTEAMS_DEFAULT_MODEL=\"$(grep '^AGENTTEAMS_DEFAULT_MODEL=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_EMBEDDING_MODEL:+x}\" ] && AGENTTEAMS_EMBEDDING_MODEL=\"$(grep '^AGENTTEAMS_EMBEDDING_MODEL=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_WORKSPACE_DIR:+x}\" ] && AGENTTEAMS_WORKSPACE_DIR=\"$(grep '^AGENTTEAMS_WORKSPACE_DIR=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_HOST_SHARE_DIR:+x}\" ] && AGENTTEAMS_HOST_SHARE_DIR=\"$(grep '^AGENTTEAMS_HOST_SHARE_DIR=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_PODMAN_AUTOSTART:+x}\" ] && AGENTTEAMS_PODMAN_AUTOSTART=\"$(grep '^AGENTTEAMS_PODMAN_AUTOSTART=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_DASHBOARD:+x}\" ] && AGENTTEAMS_DASHBOARD=\"$(grep '^AGENTTEAMS_DASHBOARD=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_DASHBOARD_VERSION:+x}\" ] && AGENTTEAMS_DASHBOARD_VERSION=\"$(grep '^AGENTTEAMS_DASHBOARD_VERSION=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_PORT_DASHBOARD:+x}\" ] && AGENTTEAMS_PORT_DASHBOARD=\"$(grep '^AGENTTEAMS_PORT_DASHBOARD=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_DASHBOARD_IMAGE:+x}\" ] && AGENTTEAMS_DASHBOARD_IMAGE=\"$(grep '^AGENTTEAMS_DASHBOARD_IMAGE=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL:+x}\" ] && AGENTTEAMS_AI_GATEWAY_ADMIN_URL=\"$(grep '^AGENTTEAMS_AI_GATEWAY_ADMIN_URL=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n fi\n}\n\n# In non-interactive mode, uses default or errors if required and no default.\n# Usage: prompt VAR_NAME \"Prompt text\" \"default\" [true=secret]\nprompt() {\n local var_name=\"$1\"\n local prompt_text=\"$2\"\n local default_value=\"$3\"\n local is_secret=\"${4:-false}\"\n\n # If the variable is already set in the environment, use it silently\n # In upgrade mode, show current value and let user change it\n eval \"local current_value=\\\"\\${${var_name}}\\\"\"\n if [ -n \"${current_value}\" ]; then\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_NON_INTERACTIVE}\" != \"1\" ]; then\n # Show masked value for secrets, full value otherwise\n local display_value=\"${current_value}\"\n if [ \"${is_secret}\" = \"true\" ]; then\n local len=${#current_value}\n if [ \"${len}\" -le 8 ]; then\n display_value=\"****\"\n else\n display_value=\"${current_value:0:4}****${current_value: -4}\"\n fi\n fi\n log \"$(msg prompt.upgrade_keep \"${prompt_text}\" \"${display_value}\")\"\n local new_value=\"\"\n if [ \"${is_secret}\" = \"true\" ]; then\n read_secret \"${prompt_text}: \"\n new_value=\"${_RS_RESULT}\"\n else\n read -e -p \"${prompt_text}: \" new_value\n if [ \"${new_value}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n fi\n if [ -n \"${new_value}\" ]; then\n eval \"export ${var_name}='${new_value}'\"\n fi\n return\n fi\n log \"$(msg prompt.preset \"${prompt_text}\")\"\n return\n fi\n\n # Non-interactive or quickstart: use default or error\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] || [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ]; then\n if [ -n \"${default_value}\" ]; then\n eval \"export ${var_name}='${default_value}'\"\n log \"$(msg prompt.default \"${prompt_text}\" \"${default_value}\")\"\n return\n elif [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n # Only hard-error in fully non-interactive mode, not quickstart\n die \"$(msg prompt.required \"${prompt_text}\")\"\n fi\n # quickstart + no default: fall through to interactive prompt below\n fi\n\n if [ -n \"${default_value}\" ]; then\n prompt_text=\"${prompt_text} [${default_value}]\"\n fi\n\n local value=\"\"\n if [ \"${is_secret}\" = \"true\" ]; then\n read_secret \"${prompt_text}: \"\n value=\"${_RS_RESULT}\"\n else\n read -e -p \"${prompt_text}: \" value\n if [ \"${value}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n fi\n\n value=\"${value:-${default_value}}\"\n if [ -z \"${value}\" ]; then\n die \"$(msg prompt.required_empty \"${prompt_text}\")\"\n fi\n\n eval \"export ${var_name}='${value}'\"\n}\n\n# Prompt for an optional value (empty string is acceptable)\n# Skips prompt if variable is already defined in environment (even if empty)\n# In upgrade mode, shows current value and lets user change it.\n# In non-interactive mode, defaults to empty string.\nprompt_optional() {\n local var_name=\"$1\"\n local prompt_text=\"$2\"\n local is_secret=\"${3:-false}\"\n\n # Check if variable is defined (even if set to empty string)\n eval \"local _chk=\\\"\\${${var_name}+x}\\\"\"\n if [ -n \"${_chk}\" ]; then\n # In upgrade mode, show current value and let user change it\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_NON_INTERACTIVE}\" != \"1\" ]; then\n eval \"local current_value=\\\"\\${${var_name}}\\\"\"\n local display_value=\"${current_value}\"\n if [ \"${is_secret}\" = \"true\" ] && [ -n \"${current_value}\" ]; then\n local len=${#current_value}\n if [ \"${len}\" -le 8 ]; then\n display_value=\"****\"\n else\n display_value=\"${current_value:0:4}****${current_value: -4}\"\n fi\n fi\n if [ -n \"${current_value}\" ]; then\n if [ \"${is_secret}\" = \"true\" ]; then\n log \"$(msg prompt.upgrade_keep_secret \"${prompt_text}\" \"${display_value}\")\"\n else\n log \"$(msg prompt.upgrade_keep \"${prompt_text}\" \"${display_value}\")\"\n fi\n else\n log \"$(msg prompt.upgrade_empty \"${prompt_text}\")\"\n fi\n local new_value=\"\"\n if [ \"${is_secret}\" = \"true\" ]; then\n read_secret \"${prompt_text}: \"\n new_value=\"${_RS_RESULT}\"\n else\n read -e -p \"${prompt_text}: \" new_value\n if [ \"${new_value}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n fi\n if [ -n \"${new_value}\" ]; then\n eval \"export ${var_name}='${new_value}'\"\n fi\n return\n fi\n log \"$(msg prompt.preset \"${prompt_text}\")\"\n return\n fi\n\n # Non-interactive or quickstart: skip, leave unset\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] || [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ]; then\n eval \"export ${var_name}=''\"\n return\n fi\n\n local value=\"\"\n if [ \"${is_secret}\" = \"true\" ]; then\n read_secret \"${prompt_text}: \"\n value=\"${_RS_RESULT}\"\n else\n read -e -p \"${prompt_text}: \" value\n if [ \"${value}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n fi\n\n eval \"export ${var_name}='${value}'\"\n}\n\ngenerate_key() {\n openssl rand -hex 32\n}\n\n# ============================================================\n# Detect container runtime socket on the host\n#\n# Dependency: Requires DOCKER_CMD to reflect the TRUE underlying runtime.\n# (Crucial for unmasking 'podman' disguised as 'docker'.\n# Must be strictly resolved prior, e.g., via check_container_runtime)\n# ============================================================\ndetect_socket() {\n local socket_path\n\n # 1. Respect explicitly defined DOCKER_HOST environment variable\n if [ -n \"${DOCKER_HOST}\" ]; then\n socket_path=$(echo \"${DOCKER_HOST}\" | sed 's|^unix://||')\n if [ -S \"${socket_path}\" ]; then\n echo \"${socket_path}\"\n return 0\n fi\n fi\n\n # 2. Route based on the resolved container runtime\n case \"${DOCKER_CMD}\" in\n docker)\n # Docker path handling\n socket_path=$(docker context ls --format '{{if .Current}}{{.DockerEndpoint}}{{end}}' 2>/dev/null | grep . | sed 's|^unix://||')\n if [ -n \"${socket_path}\" ] && [ -S \"${socket_path}\" ]; then\n echo \"${socket_path}\"\n return 0\n fi\n\n # Rootless Docker\n if [ -n \"${XDG_RUNTIME_DIR:-}\" ] && [ -S \"${XDG_RUNTIME_DIR}/docker.sock\" ]; then\n echo \"${XDG_RUNTIME_DIR}/docker.sock\"\n return 0\n fi\n\n # Rootless Docker (macOS fallback)\n if [ -S \"${HOME}/.docker/run/docker.sock\" ]; then\n echo \"${HOME}/.docker/run/docker.sock\"\n return 0\n fi\n\n # Root Docker\n if [ -S \"/var/run/docker.sock\" ]; then\n echo \"/var/run/docker.sock\"\n return 0\n fi\n ;;\n podman)\n # Podman path handling\n if [ \"$(id -u)\" -eq 0 ]; then\n socket_path=\"/run/podman/podman.sock\"\n else\n socket_path=\"${XDG_RUNTIME_DIR}/podman/podman.sock\"\n fi\n if [ -S \"${socket_path}\" ]; then\n echo \"${socket_path}\"\n return 0\n fi\n ;;\n esac\n\n # Return empty if no socket is found\n echo \"\"\n return 0\n}\n\n# Ensure Podman API socket is active (State mutation)\nensure_podman_socket() {\n local socket_path\n\n # Only applicable if runtime is podman and systemctl is available\n if [ \"${DOCKER_CMD:-}\" != \"podman\" ] || ! command -v systemctl >/dev/null 2>&1; then\n return 0\n fi\n\n echo \"Ensuring Podman API socket is active...\" >&2\n\n # Enable and start the socket based on user privileges\n if [ \"$(id -u)\" -eq 0 ]; then\n socket_path=\"/run/podman/podman.sock\"\n systemctl enable --now podman.socket >/dev/null 2>&1 || true\n else\n socket_path=\"${XDG_RUNTIME_DIR}/podman/podman.sock\"\n systemctl --user enable --now podman.socket >/dev/null 2>&1 || true\n fi\n\n # Give systemd a brief moment to assert the socket for root or rootless user\n local _i\n for _i in 1 2 3; do\n [ -S \"${socket_path}\" ] && break\n sleep 1\n done\n}\n\n# Detect local LAN IP address (cross-platform: macOS and Linux)\ndetect_lan_ip() {\n local ip=\"\"\n\n # macOS: try common Wi-Fi / Ethernet interfaces\n if command -v ipconfig >/dev/null 2>&1; then\n for iface in en0 en1 en2 en3 en4; do\n ip=$(ipconfig getifaddr \"${iface}\" 2>/dev/null)\n if [ -n \"${ip}\" ]; then\n echo \"${ip}\"\n return 0\n fi\n done\n fi\n\n # Linux: ip route — most reliable\n if command -v ip >/dev/null 2>&1; then\n ip=$(ip route get 1.1.1.1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i==\"src\"){print $(i+1); exit}}')\n if [ -n \"${ip}\" ]; then\n echo \"${ip}\"\n return 0\n fi\n fi\n\n # Linux fallback: hostname -I (space-separated list, take first non-loopback)\n if command -v hostname >/dev/null 2>&1; then\n ip=$(hostname -I 2>/dev/null | tr ' ' '\\n' | grep -v '^127\\.' | grep -v '^::' | head -1)\n if [ -n \"${ip}\" ]; then\n echo \"${ip}\"\n return 0\n fi\n fi\n\n # Last resort: ifconfig\n if command -v ifconfig >/dev/null 2>&1; then\n ip=$(ifconfig 2>/dev/null | awk '/inet /{if($2!~/^127\\./){print $2; exit}}')\n # Strip \"addr:\" prefix that some ifconfig versions add\n ip=\"${ip#addr:}\"\n if [ -n \"${ip}\" ]; then\n echo \"${ip}\"\n return 0\n fi\n fi\n\n echo \"\"\n}\n\n\n# ============================================================\n# Step-back navigation helpers\n# ============================================================\n\n# should_skip_step: returns 0 (skip) when the step is irrelevant in current mode\nshould_skip_step() {\n local step_fn=\"$1\"\n case \"${step_fn}\" in\n step_lang|step_mode)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n ;;\n step_version)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n ;;\n step_existing)\n local _env=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n [ ! -f \"${_env}\" ] && return 0\n ;;\n # Keep-All upgrade mode: skip all config steps (step_volume/step_workspace handled separately)\n step_llm|step_admin|step_network|step_ports|step_domains|step_github|step_skills|step_runtime|step_manager_runtime|step_dashboard|step_e2ee|step_docker_proxy|step_idle|step_hostshare)\n [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ] && return 0\n ;;\n step_volume|step_workspace)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ] && return 0\n [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ] && return 0\n ;;\n step_e2ee|step_idle)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE}\" != \"1\" ] && return 0\n ;;\n step_docker_proxy)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE}\" != \"1\" ] && return 0\n # Embedded mode handles docker access natively — skip this step\n [ \"${AGENTTEAMS_USE_EMBEDDED:-}\" = \"1\" ] && return 0\n ;;\n step_manager_runtime)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n ;;\n step_hostshare)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ] && return 0\n ;;\n step_podman_autostart)\n # Only relevant for Podman with systemd\n [ \"${DOCKER_CMD}\" != \"podman\" ] && return 0\n ! command -v systemctl >/dev/null 2>&1 && return 0\n\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE}\" != \"1\" ] && return 0\n [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ] && return 0\n ;;\n esac\n return 1\n}\n\n# clear_step_vars: unset variables set by a step so it will re-prompt on re-entry\nclear_step_vars() {\n local step_fn=\"$1\"\n case \"${step_fn}\" in\n step_mode) unset AGENTTEAMS_QUICKSTART ;;\n step_version) unset AGENTTEAMS_VERSION ;;\n step_existing) unset AGENTTEAMS_UPGRADE UPGRADE_EXISTING_WORKERS ;;\n step_llm)\n unset AGENTTEAMS_LLM_PROVIDER AGENTTEAMS_DEFAULT_MODEL AGENTTEAMS_OPENAI_BASE_URL\n unset AGENTTEAMS_LLM_API_KEY AGENTTEAMS_MODEL_CONTEXT_WINDOW AGENTTEAMS_MODEL_MAX_TOKENS\n unset AGENTTEAMS_MODEL_REASONING AGENTTEAMS_MODEL_VISION\n ;;\n step_admin) unset AGENTTEAMS_ADMIN_USER AGENTTEAMS_ADMIN_PASSWORD ;;\n step_network) unset AGENTTEAMS_LOCAL_ONLY ;;\n step_ports)\n unset AGENTTEAMS_PORT_GATEWAY AGENTTEAMS_PORT_CONSOLE\n unset AGENTTEAMS_PORT_ELEMENT_WEB AGENTTEAMS_PORT_MANAGER_CONSOLE\n ;;\n step_domains)\n unset AGENTTEAMS_MATRIX_DOMAIN AGENTTEAMS_MATRIX_CLIENT_DOMAIN\n unset AGENTTEAMS_AI_GATEWAY_DOMAIN AGENTTEAMS_FS_DOMAIN AGENTTEAMS_CONSOLE_DOMAIN\n ;;\n step_github) unset AGENTTEAMS_GITHUB_TOKEN ;;\n step_skills) unset AGENTTEAMS_SKILLS_API_URL ;;\n step_volume) unset AGENTTEAMS_DATA_DIR ;;\n step_workspace) unset AGENTTEAMS_WORKSPACE_DIR ;;\n step_dashboard) unset AGENTTEAMS_DASHBOARD AGENTTEAMS_DASHBOARD_VERSION AGENTTEAMS_PORT_DASHBOARD AGENTTEAMS_DASHBOARD_IMAGE AGENTTEAMS_AI_GATEWAY_ADMIN_URL ;;\n step_runtime) unset AGENTTEAMS_DEFAULT_WORKER_RUNTIME ;;\n step_manager_runtime) unset AGENTTEAMS_MANAGER_RUNTIME ;;\n step_e2ee) unset AGENTTEAMS_MATRIX_E2EE ;;\n step_docker_proxy) unset AGENTTEAMS_DOCKER_PROXY; unset AGENTTEAMS_PROXY_ALLOWED_REGISTRIES ;;\n step_idle) unset AGENTTEAMS_WORKER_IDLE_TIMEOUT ;;\n step_hostshare) unset AGENTTEAMS_HOST_SHARE_DIR ;;\n step_podman_autostart) unset AGENTTEAMS_PODMAN_AUTOSTART ;;\n esac\n}\n\n# ============================================================\n# Individual step functions\n# ============================================================\n\nstep_lang() {\n local lang_default_choice=\"2\"\n [ \"${AGENTTEAMS_LANGUAGE}\" = \"zh\" ] && lang_default_choice=\"1\"\n log \"$(msg lang.detected)\"\n log \"$(msg lang.switch_title)\"\n echo \"$(msg lang.option_zh)\"\n echo \"$(msg lang.option_en)\"\n echo \"\"\n local LANG_CHOICE\n read -e -p \"$(msg lang.prompt) [${lang_default_choice}]: \" LANG_CHOICE\n LANG_CHOICE=\"${LANG_CHOICE:-${lang_default_choice}}\"\n if [ \"${LANG_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n case \"${LANG_CHOICE}\" in\n 1) AGENTTEAMS_LANGUAGE=\"zh\" ;;\n 2) AGENTTEAMS_LANGUAGE=\"en\" ;;\n esac\n export AGENTTEAMS_LANGUAGE\n log \"\"\n}\n\nstep_mode() {\n log \"$(msg install.mode.title)\"\n echo \"\"\n echo \"$(msg install.mode.choose)\"\n echo \"$(msg install.mode.quickstart)\"\n echo \"$(msg install.mode.manual)\"\n echo \"\"\n local ONBOARDING_CHOICE\n read -e -p \"$(msg install.mode.prompt): \" ONBOARDING_CHOICE\n ONBOARDING_CHOICE=\"${ONBOARDING_CHOICE:-1}\"\n if [ \"${ONBOARDING_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n case \"${ONBOARDING_CHOICE}\" in\n 1|quick|quickstart)\n log \"$(msg install.mode.quickstart_selected)\"\n AGENTTEAMS_QUICKSTART=1\n ;;\n 2|manual)\n log \"$(msg install.mode.manual_selected)\"\n ;;\n *)\n log \"$(msg install.mode.invalid)\"\n AGENTTEAMS_QUICKSTART=1\n ;;\n esac\n log \"\"\n}\n\nstep_version() {\n # Skip if version already provided via env var\n if [ -n \"${AGENTTEAMS_VERSION}\" ]; then\n resolve_image_tags\n return 0\n fi\n # Try to fetch the latest stable release from GitHub\n log \"$(msg install.version.fetching)\"\n local _fetched\n _fetched=$(curl -sf --max-time 5 \\\n -H \"Accept: application/vnd.github+json\" \\\n \"https://api.github.com/repos/agentscope-ai/AgentTeams/releases/latest\" \\\n 2>/dev/null | grep '\"tag_name\"' | head -1 | sed 's/.*\"tag_name\": *\"\\([^\"]*\\)\".*/\\1/')\n if [ -n \"${_fetched}\" ]; then\n AGENTTEAMS_KNOWN_STABLE_VERSION=\"${_fetched}\"\n else\n log \"$(msg install.version.fetch_failed \"${AGENTTEAMS_KNOWN_STABLE_VERSION}\")\"\n fi\n log \"$(msg install.version.title)\"\n echo \"\"\n echo \"$(msg install.version.choose)\"\n echo \"$(msg install.version.option_latest)\"\n printf \"%s\\n\" \"$(msg install.version.option_stable \"${AGENTTEAMS_KNOWN_STABLE_VERSION}\")\"\n echo \"$(msg install.version.option_custom)\"\n echo \"\"\n local VERSION_CHOICE\n read -e -p \"$(msg install.version.prompt) [1]: \" VERSION_CHOICE\n VERSION_CHOICE=\"${VERSION_CHOICE:-1}\"\n if [ \"${VERSION_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n case \"${VERSION_CHOICE}\" in\n 1|latest)\n AGENTTEAMS_VERSION=\"latest\"\n log \"$(msg install.version.selected_latest)\"\n ;;\n 2|stable)\n AGENTTEAMS_VERSION=\"${AGENTTEAMS_KNOWN_STABLE_VERSION}\"\n log \"$(msg install.version.selected_stable \"${AGENTTEAMS_VERSION}\")\"\n ;;\n 3|custom)\n local CUSTOM_VERSION\n read -e -p \"$(msg install.version.custom_prompt): \" CUSTOM_VERSION\n AGENTTEAMS_VERSION=\"${CUSTOM_VERSION:-${AGENTTEAMS_KNOWN_STABLE_VERSION}}\"\n log \"$(msg install.version.selected_custom \"${AGENTTEAMS_VERSION}\")\"\n ;;\n *)\n AGENTTEAMS_VERSION=\"${AGENTTEAMS_KNOWN_STABLE_VERSION}\"\n log \"$(msg install.version.invalid \"${AGENTTEAMS_VERSION}\")\"\n ;;\n esac\n log \"\"\n resolve_image_tags\n}\n\nstep_existing() {\n local existing_env=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n log \"$(msg install.existing.detected \"${existing_env}\")\"\n local running_manager=\"\" running_workers=\"\" existing_workers=\"\"\n if ${DOCKER_CMD} ps --format '{{.Names}}' | grep -q \"^agentteams-manager$\"; then\n running_manager=\"agentteams-manager\"\n fi\n running_workers=$(${DOCKER_CMD} ps --format '{{.Names}}' | grep \"^agentteams-worker-\" || true)\n existing_workers=$(${DOCKER_CMD} ps -a --format '{{.Names}}' | grep \"^agentteams-worker-\" || true)\n local UPGRADE_CHOICE\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n log \"$(msg install.existing.upgrade_noninteractive)\"\n UPGRADE_CHOICE=\"upgrade\"\n else\n echo \"\"\n echo \"$(msg install.existing.choose)\"\n echo \"$(msg install.existing.upgrade)\"\n echo \"$(msg install.existing.reinstall)\"\n echo \"$(msg install.existing.cancel)\"\n echo \"\"\n read -e -p \"$(msg install.existing.prompt): \" UPGRADE_CHOICE\n UPGRADE_CHOICE=\"${UPGRADE_CHOICE:-1}\"\n if [ \"${UPGRADE_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n fi\n case \"${UPGRADE_CHOICE}\" in\n 1|upgrade)\n AGENTTEAMS_UPGRADE=1\n log \"$(msg install.existing.upgrading)\"\n # Show upgrade mode sub-menu (unless already set via env var)\n if [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL:-0}\" != \"1\" ]; then\n echo \"\"\n echo \"$(msg upgrade.mode.prompt)\"\n echo \"$(msg upgrade.mode.keep_all)\"\n echo \"$(msg upgrade.mode.confirm_each)\"\n echo \"$(msg upgrade.mode.back)\"\n echo \"\"\n local UPGRADE_MODE_CHOICE\n read -e -p \"$(msg install.existing.prompt): \" UPGRADE_MODE_CHOICE\n UPGRADE_MODE_CHOICE=\"${UPGRADE_MODE_CHOICE:-1}\"\n case \"${UPGRADE_MODE_CHOICE}\" in\n 1|keep)\n AGENTTEAMS_UPGRADE_KEEP_ALL=1\n ;;\n 2|confirm)\n AGENTTEAMS_UPGRADE_KEEP_ALL=0\n ;;\n 3|b)\n STEP_RESULT=\"back\"\n return 0\n ;;\n *)\n AGENTTEAMS_UPGRADE_KEEP_ALL=0\n ;;\n esac\n fi\n # Load current parameters for both Keep-All and confirm-each modes\n load_current_params_from_env\n if [ -n \"${running_manager}\" ] || [ -n \"${running_workers}\" ]; then\n echo \"\"\n echo -e \"\\033[33m$(msg install.existing.warn_manager_stop)\\033[0m\"\n if [ -n \"${existing_workers}\" ]; then\n echo -e \"\\033[33m$(msg install.existing.warn_worker_recreate)\\033[0m\"\n fi\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" != \"1\" ]; then\n echo \"\"\n local CONFIRM_STOP\n read -e -p \"$(msg install.existing.continue_prompt): \" CONFIRM_STOP\n if [ \"${CONFIRM_STOP}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n if [ \"${CONFIRM_STOP}\" != \"y\" ] && [ \"${CONFIRM_STOP}\" != \"Y\" ]; then\n log \"$(msg install.existing.cancelled)\"\n exit 0\n fi\n fi\n fi\n UPGRADE_EXISTING_WORKERS=\"${existing_workers}\"\n ;;\n 2|reinstall)\n log \"$(msg install.reinstall.performing)\"\n local existing_workspace=\"\"\n if [ -f \"${existing_env}\" ]; then\n existing_workspace=$(grep '^AGENTTEAMS_WORKSPACE_DIR=' \"${existing_env}\" 2>/dev/null | cut -d= -f2-)\n fi\n [ -z \"${existing_workspace}\" ] && existing_workspace=\"${HOME}/agentteams-manager\"\n echo \"\"\n echo -e \"\\033[33m$(msg install.reinstall.warn_stop)\\033[0m\"\n [ -n \"${running_manager}\" ] && echo -e \"\\033[33m - ${running_manager} (manager)\\033[0m\"\n for w in ${running_workers}; do\n echo -e \"\\033[33m - ${w} (worker)\\033[0m\"\n done\n echo \"\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_delete)\\033[0m\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_volume)\\033[0m\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_env \"${existing_env}\")\\033[0m\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_workspace \"${existing_workspace}\")\\033[0m\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_workers)\\033[0m\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_proxy)\\033[0m\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_network)\\033[0m\"\n echo \"\"\n echo -e \"\\033[31m$(msg install.reinstall.confirm_type)\\033[0m\"\n echo -e \"\\033[31m ${existing_workspace}\\033[0m\"\n echo \"\"\n local CONFIRM_PATH\n read -e -p \"$(msg install.reinstall.confirm_path): \" CONFIRM_PATH\n if [ \"${CONFIRM_PATH}\" != \"${existing_workspace}\" ]; then\n die \"$(msg install.reinstall.path_mismatch \"${CONFIRM_PATH}\" \"${existing_workspace}\")\"\n fi\n log \"$(msg install.reinstall.confirmed)\"\n ${DOCKER_CMD} stop agentteams-manager 2>/dev/null || true\n ${DOCKER_CMD} rm agentteams-manager 2>/dev/null || true\n for w in $(${DOCKER_CMD} ps -a --format '{{.Names}}' | grep \"^agentteams-worker-\" || true); do\n ${DOCKER_CMD} stop \"${w}\" 2>/dev/null || true\n ${DOCKER_CMD} rm \"${w}\" 2>/dev/null || true\n log \"$(msg install.reinstall.removed_worker \"${w}\")\"\n done\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' | grep -q \"^agentteams-controller$\"; then\n log \"$(msg install.reinstall.removing_proxy)\"\n ${DOCKER_CMD} stop agentteams-controller 2>/dev/null || true\n ${DOCKER_CMD} rm agentteams-controller 2>/dev/null || true\n fi\n if ${DOCKER_CMD} network ls --format '{{.Name}}' | grep -q \"^agentteams-net$\"; then\n log \"$(msg install.reinstall.removing_network)\"\n ${DOCKER_CMD} network rm agentteams-net 2>/dev/null || true\n fi\n if ${DOCKER_CMD} volume ls -q | grep -q \"^agentteams-data$\"; then\n log \"$(msg install.reinstall.removing_volume)\"\n ${DOCKER_CMD} volume rm agentteams-data 2>/dev/null || log \"$(msg install.reinstall.warn_volume_fail)\"\n fi\n if [ -d \"${existing_workspace}\" ]; then\n log \"$(msg install.reinstall.removing_workspace \"${existing_workspace}\")\"\n rm -rf \"${existing_workspace}\" || die \"$(msg install.reinstall.failed_rm_workspace)\"\n fi\n if [ -f \"${existing_env}\" ]; then\n log \"$(msg install.reinstall.removing_env \"${existing_env}\")\"\n rm -f \"${existing_env}\"\n fi\n log \"$(msg install.reinstall.cleanup_done)\"\n unset AGENTTEAMS_WORKSPACE_DIR\n return 0\n ;;\n 3|cancel|*)\n log \"$(msg install.existing.cancelled)\"\n exit 0\n ;;\n esac\n # Load existing env file as fallback (shell env vars take priority)\n if [ -f \"${existing_env}\" ]; then\n log \"$(msg install.loading_config \"${existing_env}\")\"\n while IFS='=' read -r key value; do\n case \"${key}\" in \\#*|\"\") continue ;; esac\n value=\"${value%%#*}\"\n value=\"${value#\"${value%%[![:space:]]*}\"}\"\n value=\"${value%\"${value##*[![:space:]]}\"}\"\n eval \"_existing_val=\\\"\\${${key}+x}\\\"\"\n if [ -z \"${_existing_val}\" ]; then export \"${key}=${value}\"; fi\n done < \"${existing_env}\"\n fi\n}\n\nstep_llm() {\n log \"$(msg llm.title)\"\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n if [ \"${AGENTTEAMS_LANGUAGE}\" = \"zh\" ]; then\n AGENTTEAMS_LLM_PROVIDER=\"${AGENTTEAMS_LLM_PROVIDER:-openai-compat}\"\n AGENTTEAMS_DEFAULT_MODEL=\"${AGENTTEAMS_DEFAULT_MODEL:-qwen3.6-plus}\"\n AGENTTEAMS_OPENAI_BASE_URL=\"${AGENTTEAMS_OPENAI_BASE_URL:-https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1}\"\n log \"$(msg llm.provider.label \"${AGENTTEAMS_LLM_PROVIDER}\")\"\n log \"$(msg llm.openai.base_url_label \"${AGENTTEAMS_OPENAI_BASE_URL}\")\"\n else\n AGENTTEAMS_LLM_PROVIDER=\"${AGENTTEAMS_LLM_PROVIDER:-qwen}\"\n AGENTTEAMS_DEFAULT_MODEL=\"${AGENTTEAMS_DEFAULT_MODEL:-qwen3.6-plus}\"\n AGENTTEAMS_OPENAI_BASE_URL=\"${AGENTTEAMS_OPENAI_BASE_URL:-}\"\n log \"$(msg llm.provider.qwen_default \"${AGENTTEAMS_LLM_PROVIDER}\")\"\n fi\n log \"$(msg llm.model.default \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n prompt AGENTTEAMS_LLM_API_KEY \"$(msg llm.apikey_prompt)\" \"\" \"true\"\n AGENTTEAMS_EMBEDDING_MODEL=\"${AGENTTEAMS_EMBEDDING_MODEL-text-embedding-v4}\"\n export AGENTTEAMS_LLM_PROVIDER AGENTTEAMS_DEFAULT_MODEL AGENTTEAMS_EMBEDDING_MODEL\n [ -n \"${AGENTTEAMS_OPENAI_BASE_URL+x}\" ] && export AGENTTEAMS_OPENAI_BASE_URL\n return 0\n fi\n\n # Upgrade Keep-All mode: skip provider selection, use loaded values\n local SKIP_TO_EMBEDDING=0\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ] && [ -n \"${AGENTTEAMS_LLM_PROVIDER}\" ]; then\n log \"$(msg llm.provider.label \"${AGENTTEAMS_LLM_PROVIDER}\")\"\n log \"$(msg llm.openai.base_url_label \"${AGENTTEAMS_OPENAI_BASE_URL}\")\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n PROVIDER_CHOICE=\"${AGENTTEAMS_LLM_PROVIDER}\"\n SKIP_TO_EMBEDDING=1\n else\n echo \"\"\n echo \"$(msg llm.providers_title)\"\n echo \"$(msg llm.provider.alibaba)\"\n echo \"$(msg llm.provider.openai_compat)\"\n echo \"\"\n local PROVIDER_CHOICE\n # If upgrade mode with loaded provider, show current as default\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_LLM_PROVIDER}\" ]; then\n local _prov_display\n case \"${AGENTTEAMS_LLM_PROVIDER}\" in\n openai-compat) _prov_display=\"2\" ;;\n *) _prov_display=\"1\" ;;\n esac\n read -e -p \"$(msg llm.provider.select) [${_prov_display}]: \" PROVIDER_CHOICE\n PROVIDER_CHOICE=\"${PROVIDER_CHOICE:-${_prov_display}}\"\n elif [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ]; then\n read -e -p \"$(msg llm.provider.select) [1]: \" PROVIDER_CHOICE\n PROVIDER_CHOICE=\"${PROVIDER_CHOICE:-1}\"\n else\n read -e -p \"$(msg llm.provider.select): \" PROVIDER_CHOICE\n PROVIDER_CHOICE=\"${PROVIDER_CHOICE:-1}\"\n fi\n if [ \"${PROVIDER_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n fi\n local ALIBABA_MODEL_CHOICE=\"\"\n local ALIBABA_ACCESS=\"\"\n case \"${PROVIDER_CHOICE}\" in\n 1|alibaba-cloud|openai-compat)\n if [ \"${AGENTTEAMS_LANGUAGE}\" = \"en\" ]; then\n AGENTTEAMS_LLM_PROVIDER=\"openai-compat\"\n AGENTTEAMS_OPENAI_BASE_URL=\"https://dashscope-intl.aliyuncs.com/compatible-mode/v1\"\n ALIBABA_ACCESS=\"tokenplan\"\n echo \"\"\n echo \"$(msg llm.codingplan.models_title)\"\n echo \"$(msg llm.codingplan.model.qwen36plus)\"\n echo \"$(msg llm.codingplan.model.glm5)\"\n echo \"$(msg llm.codingplan.model.kimi)\"\n echo \"$(msg llm.codingplan.model.minimax)\"\n echo \"\"\n local CODINGPLAN_MODEL_CHOICE\n # If upgrade with loaded model, show current as default\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_DEFAULT_MODEL}\" ]; then\n local _model_default\n case \"${AGENTTEAMS_DEFAULT_MODEL}\" in\n qwen3.6-plus) _model_default=\"1\" ;;\n glm-5) _model_default=\"2\" ;;\n kimi-k2.5) _model_default=\"3\" ;;\n MiniMax-M2.5) _model_default=\"4\" ;;\n *) _model_default=\"1\" ;;\n esac\n read -e -p \"$(msg llm.codingplan.model.select) [${_model_default}]: \" CODINGPLAN_MODEL_CHOICE\n CODINGPLAN_MODEL_CHOICE=\"${CODINGPLAN_MODEL_CHOICE:-${_model_default}}\"\n elif [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ]; then\n read -e -p \"$(msg llm.codingplan.model.select) [1]: \" CODINGPLAN_MODEL_CHOICE\n CODINGPLAN_MODEL_CHOICE=\"${CODINGPLAN_MODEL_CHOICE:-1}\"\n else\n read -e -p \"$(msg llm.codingplan.model.select): \" CODINGPLAN_MODEL_CHOICE\n CODINGPLAN_MODEL_CHOICE=\"${CODINGPLAN_MODEL_CHOICE:-1}\"\n fi\n if [ \"${CODINGPLAN_MODEL_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n case \"${CODINGPLAN_MODEL_CHOICE}\" in\n 1|qwen3.6-plus) AGENTTEAMS_DEFAULT_MODEL=\"qwen3.6-plus\" ;;\n 2|glm-5) AGENTTEAMS_DEFAULT_MODEL=\"glm-5\" ;;\n 3|kimi-k2.5) AGENTTEAMS_DEFAULT_MODEL=\"kimi-k2.5\" ;;\n 4|MiniMax-M2.5) AGENTTEAMS_DEFAULT_MODEL=\"MiniMax-M2.5\" ;;\n *) AGENTTEAMS_DEFAULT_MODEL=\"qwen3.6-plus\" ;;\n esac\n log \"$(msg llm.provider.selected_codingplan)\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n else\n echo \"\"\n echo \"$(msg llm.alibaba.models_title)\"\n echo \"$(msg llm.alibaba.model.tokenplan)\"\n echo \"$(msg llm.alibaba.model.bailian)\"\n echo \"$(msg llm.alibaba.model.codingplan_legacy)\"\n echo \"\"\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_OPENAI_BASE_URL}\" ]; then\n # Determine alibaba access type from loaded URL\n local _access_default=\"1\"\n if echo \"${AGENTTEAMS_OPENAI_BASE_URL}\" | grep -q \"token-plan\"; then\n _access_default=\"1\"\n elif echo \"${AGENTTEAMS_OPENAI_BASE_URL}\" | grep -q \"dashscope\"; then\n if echo \"${AGENTTEAMS_OPENAI_BASE_URL}\" | grep -q \"intl\"; then\n _access_default=\"3\"\n else\n _access_default=\"2\"\n fi\n fi\n read -e -p \"$(msg llm.alibaba.model.select) [${_access_default}]: \" ALIBABA_MODEL_CHOICE\n ALIBABA_MODEL_CHOICE=\"${ALIBABA_MODEL_CHOICE:-${_access_default}}\"\n elif [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ]; then\n read -e -p \"$(msg llm.alibaba.model.select) [1]: \" ALIBABA_MODEL_CHOICE\n ALIBABA_MODEL_CHOICE=\"${ALIBABA_MODEL_CHOICE:-1}\"\n else\n read -e -p \"$(msg llm.alibaba.model.select): \" ALIBABA_MODEL_CHOICE\n ALIBABA_MODEL_CHOICE=\"${ALIBABA_MODEL_CHOICE:-1}\"\n fi\n if [ \"${ALIBABA_MODEL_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n case \"${ALIBABA_MODEL_CHOICE}\" in\n 1|token-plan|tokenplan)\n ALIBABA_ACCESS=\"tokenplan\"\n AGENTTEAMS_LLM_PROVIDER=\"openai-compat\"\n AGENTTEAMS_OPENAI_BASE_URL=\"https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1\"\n echo \"\"\n echo \"$(msg llm.codingplan.models_title)\"\n echo \"$(msg llm.codingplan.model.qwen36plus)\"\n echo \"$(msg llm.codingplan.model.glm5)\"\n echo \"$(msg llm.codingplan.model.kimi)\"\n echo \"$(msg llm.codingplan.model.minimax)\"\n echo \"\"\n local CODINGPLAN_MODEL_CHOICE\n # If upgrade with loaded model, show current as default\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_DEFAULT_MODEL}\" ]; then\n local _model_default\n case \"${AGENTTEAMS_DEFAULT_MODEL}\" in\n qwen3.6-plus) _model_default=\"1\" ;;\n glm-5) _model_default=\"2\" ;;\n kimi-k2.5) _model_default=\"3\" ;;\n MiniMax-M2.5) _model_default=\"4\" ;;\n *) _model_default=\"1\" ;;\n esac\n read -e -p \"$(msg llm.codingplan.model.select) [${_model_default}]: \" CODINGPLAN_MODEL_CHOICE\n CODINGPLAN_MODEL_CHOICE=\"${CODINGPLAN_MODEL_CHOICE:-${_model_default}}\"\n elif [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ]; then\n read -e -p \"$(msg llm.codingplan.model.select) [1]: \" CODINGPLAN_MODEL_CHOICE\n CODINGPLAN_MODEL_CHOICE=\"${CODINGPLAN_MODEL_CHOICE:-1}\"\n else\n read -e -p \"$(msg llm.codingplan.model.select): \" CODINGPLAN_MODEL_CHOICE\n CODINGPLAN_MODEL_CHOICE=\"${CODINGPLAN_MODEL_CHOICE:-1}\"\n fi\n if [ \"${CODINGPLAN_MODEL_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n case \"${CODINGPLAN_MODEL_CHOICE}\" in\n 1|qwen3.6-plus) AGENTTEAMS_DEFAULT_MODEL=\"qwen3.6-plus\" ;;\n 2|glm-5) AGENTTEAMS_DEFAULT_MODEL=\"glm-5\" ;;\n 3|kimi-k2.5) AGENTTEAMS_DEFAULT_MODEL=\"kimi-k2.5\" ;;\n 4|MiniMax-M2.5) AGENTTEAMS_DEFAULT_MODEL=\"MiniMax-M2.5\" ;;\n *) AGENTTEAMS_DEFAULT_MODEL=\"qwen3.6-plus\" ;;\n esac\n log \"$(msg llm.provider.selected_tokenplan)\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n ;;\n 2|qwen|bailian)\n ALIBABA_ACCESS=\"bailian\"\n AGENTTEAMS_LLM_PROVIDER=\"qwen\"\n AGENTTEAMS_OPENAI_BASE_URL=\"\"\n echo \"\"\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_DEFAULT_MODEL}\" ]; then\n read -e -p \"$(msg llm.qwen.model_prompt) [${AGENTTEAMS_DEFAULT_MODEL}]: \" AGENTTEAMS_DEFAULT_MODEL\n else\n read -e -p \"$(msg llm.qwen.model_prompt) [qwen3.6-plus]: \" AGENTTEAMS_DEFAULT_MODEL\n fi\n if [ \"${AGENTTEAMS_DEFAULT_MODEL}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_DEFAULT_MODEL=\"${AGENTTEAMS_DEFAULT_MODEL:-qwen3.6-plus}\"\n log \"$(msg llm.provider.selected_qwen)\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n prompt_custom_model_params \"${AGENTTEAMS_DEFAULT_MODEL}\" || return 0\n ;;\n 3|coding-plan|codingplan)\n ALIBABA_ACCESS=\"codingplan_legacy\"\n AGENTTEAMS_LLM_PROVIDER=\"openai-compat\"\n AGENTTEAMS_OPENAI_BASE_URL=\"https://coding.dashscope.aliyuncs.com/v1\"\n echo \"\"\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_DEFAULT_MODEL}\" ]; then\n read -e -p \"$(msg llm.qwen.model_prompt) [${AGENTTEAMS_DEFAULT_MODEL}]: \" AGENTTEAMS_DEFAULT_MODEL\n else\n read -e -p \"$(msg llm.qwen.model_prompt): \" AGENTTEAMS_DEFAULT_MODEL\n fi\n if [ \"${AGENTTEAMS_DEFAULT_MODEL}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_DEFAULT_MODEL=\"${AGENTTEAMS_DEFAULT_MODEL:-qwen3.6-plus}\"\n log \"$(msg llm.provider.selected_codingplan_legacy)\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n prompt_custom_model_params \"${AGENTTEAMS_DEFAULT_MODEL}\" || return 0\n ;;\n *)\n die \"$(msg llm.alibaba.model.invalid \"${ALIBABA_MODEL_CHOICE}\")\"\n ;;\n esac\n fi\n if [ \"${ALIBABA_ACCESS}\" = \"bailian\" ]; then\n log \"$(msg llm.apikey_hint_bailian)\"\n log \"$(msg llm.apikey_url_bailian)\"\n elif [ \"${AGENTTEAMS_LANGUAGE}\" = \"en\" ]; then\n log \"$(msg llm.apikey_hint_qwencloud)\"\n log \"$(msg llm.apikey_url_qwencloud)\"\n elif [ \"${ALIBABA_ACCESS}\" = \"codingplan_legacy\" ]; then\n log \"$(msg llm.apikey_hint_codingplan)\"\n log \"$(msg llm.apikey_url_codingplan)\"\n else\n log \"$(msg llm.apikey_hint_tokenplan)\"\n log \"$(msg llm.apikey_url_tokenplan)\"\n fi\n log \"\"\n prompt AGENTTEAMS_LLM_API_KEY \"$(msg llm.apikey_prompt)\" \"\" \"true\" || return 0\n if [ \"${ALIBABA_ACCESS}\" = \"bailian\" ]; then\n test_llm_connectivity \"https://dashscope.aliyuncs.com/compatible-mode/v1\" \"${AGENTTEAMS_LLM_API_KEY}\" \"${AGENTTEAMS_DEFAULT_MODEL}\" || return 0\n elif [ \"${ALIBABA_ACCESS}\" = \"codingplan_legacy\" ]; then\n test_llm_connectivity \"https://coding.dashscope.aliyuncs.com/v1\" \"${AGENTTEAMS_LLM_API_KEY}\" \"${AGENTTEAMS_DEFAULT_MODEL}\" \"$(msg llm.openai.test.fail.codingplan_legacy)\" || return 0\n elif [ \"${AGENTTEAMS_LANGUAGE}\" = \"en\" ]; then\n test_llm_connectivity \"${AGENTTEAMS_OPENAI_BASE_URL}\" \"${AGENTTEAMS_LLM_API_KEY}\" \"${AGENTTEAMS_DEFAULT_MODEL}\" \"$(msg llm.openai.test.fail.codingplan)\" || return 0\n else\n test_llm_connectivity \"${AGENTTEAMS_OPENAI_BASE_URL}\" \"${AGENTTEAMS_LLM_API_KEY}\" \"${AGENTTEAMS_DEFAULT_MODEL}\" \"$(msg llm.openai.test.fail.tokenplan)\" || return 0\n fi\n ;;\n 2|openai-compat)\n AGENTTEAMS_LLM_PROVIDER=\"openai-compat\"\n log \"$(msg llm.provider.selected_openai \"${AGENTTEAMS_LLM_PROVIDER}\")\"\n echo \"\"\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg llm.openai.base_url_prompt)\" \"${AGENTTEAMS_OPENAI_BASE_URL}\")\"\n else\n _current=\"${AGENTTEAMS_OPENAI_BASE_URL}\"\n if [ -n \"${_current}\" ]; then\n read -e -p \"$(msg llm.openai.base_url_prompt) [${_current}]: \" AGENTTEAMS_OPENAI_BASE_URL\n else\n read -e -p \"$(msg llm.openai.base_url_prompt) [https://api.openai.com/v1]: \" AGENTTEAMS_OPENAI_BASE_URL\n fi\n if [ \"${AGENTTEAMS_OPENAI_BASE_URL}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_OPENAI_BASE_URL=\"${AGENTTEAMS_OPENAI_BASE_URL:-${_current}}\"\n [ -z \"${AGENTTEAMS_OPENAI_BASE_URL}\" ] && AGENTTEAMS_OPENAI_BASE_URL=\"https://api.openai.com/v1\"\n fi\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg llm.openai.model_prompt)\" \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n else\n _current=\"${AGENTTEAMS_DEFAULT_MODEL}\"\n if [ -n \"${_current}\" ]; then\n read -e -p \"$(msg llm.openai.model_prompt) [${_current}]: \" AGENTTEAMS_DEFAULT_MODEL\n else\n read -e -p \"$(msg llm.openai.model_prompt) [gpt-5.4]: \" AGENTTEAMS_DEFAULT_MODEL\n fi\n if [ \"${AGENTTEAMS_DEFAULT_MODEL}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_DEFAULT_MODEL=\"${AGENTTEAMS_DEFAULT_MODEL:-${_current}}\"\n [ -z \"${AGENTTEAMS_DEFAULT_MODEL}\" ] && AGENTTEAMS_DEFAULT_MODEL=\"gpt-5.4\"\n fi\n log \"$(msg llm.openai.base_url_label \"${AGENTTEAMS_OPENAI_BASE_URL}\")\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n prompt_custom_model_params \"${AGENTTEAMS_DEFAULT_MODEL}\" || return 0\n log \"\"\n prompt AGENTTEAMS_LLM_API_KEY \"$(msg llm.apikey_prompt)\" \"\" \"true\" || return 0\n test_llm_connectivity \"${AGENTTEAMS_OPENAI_BASE_URL}\" \"${AGENTTEAMS_LLM_API_KEY}\" \"${AGENTTEAMS_DEFAULT_MODEL}\" || return 0\n ;;\n *)\n die \"$(msg llm.provider.invalid \"${PROVIDER_CHOICE}\")\"\n ;;\n esac\n\n # Skip to embedding if Keep-All mode handled LLM params\n if [ \"${SKIP_TO_EMBEDDING}\" = \"1\" ]; then\n if [ -n \"${AGENTTEAMS_EMBEDDING_MODEL}\" ]; then\n log \"$(msg llm.embedding.title)\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_EMBEDDING_MODEL}\")\"\n log \"\"\n fi\n return 0\n fi\n\n # --- Embedding model (optional, auto-tested) ---\n echo \"\"\n log \"$(msg llm.embedding.title)\"\n log \"$(msg llm.embedding.hint)\"\n echo \"\"\n echo \"$(msg llm.embedding.option.default)\"\n echo \"$(msg llm.embedding.option.custom)\"\n echo \"$(msg llm.embedding.option.disable)\"\n echo \"\"\n local EMB_CHOICE\n read -e -p \"$(msg llm.embedding.select) [1]: \" EMB_CHOICE\n EMB_CHOICE=\"${EMB_CHOICE:-1}\"\n if [ \"${EMB_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n\n case \"${EMB_CHOICE}\" in\n 1)\n AGENTTEAMS_EMBEDDING_MODEL=\"text-embedding-v4\"\n ;;\n 2)\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg llm.embedding.custom_prompt)\" \"${AGENTTEAMS_EMBEDDING_MODEL}\")\"\n else\n _current=\"${AGENTTEAMS_EMBEDDING_MODEL}\"\n if [ -n \"${_current}\" ]; then\n read -e -p \"$(msg llm.embedding.custom_prompt) [${_current}]: \" AGENTTEAMS_EMBEDDING_MODEL\n else\n read -e -p \"$(msg llm.embedding.custom_prompt): \" AGENTTEAMS_EMBEDDING_MODEL\n fi\n if [ \"${AGENTTEAMS_EMBEDDING_MODEL}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_EMBEDDING_MODEL=\"${AGENTTEAMS_EMBEDDING_MODEL:-${_current}}\"\n fi\n ;;\n 3)\n AGENTTEAMS_EMBEDDING_MODEL=\"\"\n log \"$(msg llm.embedding.disabled)\"\n ;;\n *)\n AGENTTEAMS_EMBEDDING_MODEL=\"text-embedding-v4\"\n ;;\n esac\n\n if [ -n \"${AGENTTEAMS_EMBEDDING_MODEL}\" ]; then\n # Qwen provider uses dashscope directly; others use OPENAI_BASE_URL\n local EMB_BASE_URL=\"${AGENTTEAMS_OPENAI_BASE_URL}\"\n if [ \"${AGENTTEAMS_LLM_PROVIDER}\" = \"qwen\" ]; then\n EMB_BASE_URL=\"https://dashscope.aliyuncs.com/compatible-mode/v1\"\n fi\n if ! test_embedding_connectivity \"${EMB_BASE_URL}\" \"${AGENTTEAMS_LLM_API_KEY}\" \"${AGENTTEAMS_EMBEDDING_MODEL}\"; then\n AGENTTEAMS_EMBEDDING_MODEL=\"\"\n log \"$(msg llm.embedding.auto_disabled)\"\n fi\n fi\n\n export AGENTTEAMS_LLM_PROVIDER AGENTTEAMS_DEFAULT_MODEL\n [ -n \"${AGENTTEAMS_OPENAI_BASE_URL+x}\" ] && export AGENTTEAMS_OPENAI_BASE_URL\n log \"\"\n}\n\nstep_admin() {\n log \"$(msg admin.title)\"\n prompt AGENTTEAMS_ADMIN_USER \"$(msg admin.username_prompt)\" \"admin\" || return 0\n AGENTTEAMS_ADMIN_USER=\"$(printf '%s' \"${AGENTTEAMS_ADMIN_USER}\" | tr '[:upper:]' '[:lower:]')\"\n\n # Pre-set via env var: validate; in non-interactive mode fail fast,\n # in interactive mode warn and fall through to the retry prompt.\n if [ -n \"${AGENTTEAMS_ADMIN_PASSWORD:-}\" ]; then\n log \" $(msg prompt.preset \"$(msg admin.password_prompt)\")\"\n if [ ${#AGENTTEAMS_ADMIN_PASSWORD} -ge 8 ]; then\n log \"\"\n return 0\n fi\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n die \"$(msg admin.password_too_short \"${#AGENTTEAMS_ADMIN_PASSWORD}\")\"\n fi\n error \"$(msg admin.password_too_short \"${#AGENTTEAMS_ADMIN_PASSWORD}\")\"\n unset AGENTTEAMS_ADMIN_PASSWORD\n fi\n\n while true; do\n unset AGENTTEAMS_ADMIN_PASSWORD\n prompt_optional AGENTTEAMS_ADMIN_PASSWORD \"$(msg admin.password_prompt)\" \"true\" || return 0\n if [ -z \"${AGENTTEAMS_ADMIN_PASSWORD}\" ]; then\n AGENTTEAMS_ADMIN_PASSWORD=\"admin$(openssl rand -hex 6)\"\n log \"$(msg admin.password_generated)\"\n break\n fi\n if [ ${#AGENTTEAMS_ADMIN_PASSWORD} -ge 8 ]; then\n break\n fi\n error \"$(msg admin.password_too_short \"${#AGENTTEAMS_ADMIN_PASSWORD}\")\"\n done\n log \"\"\n}\n\nstep_network() {\n log \"$(msg port.local_only.title)\"\n echo \"\"\n echo \" 1) $(msg port.local_only.hint_yes)\"\n echo \" 2) $(msg port.local_only.hint_no)\"\n echo \"\"\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_LOCAL_ONLY=\"${AGENTTEAMS_LOCAL_ONLY:-1}\"\n elif [ -z \"${AGENTTEAMS_LOCAL_ONLY+x}\" ]; then\n local _local_choice\n read -e -p \"$(msg port.local_only.choice): \" _local_choice\n if [ \"${_local_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _local_choice=\"${_local_choice:-1}\"\n case \"${_local_choice}\" in\n 2|n|N|no|NO) AGENTTEAMS_LOCAL_ONLY=\"0\" ;;\n *) AGENTTEAMS_LOCAL_ONLY=\"1\" ;;\n esac\n fi\n export AGENTTEAMS_LOCAL_ONLY\n if [ \"${AGENTTEAMS_LOCAL_ONLY}\" = \"1\" ]; then\n log \"$(msg port.local_only.selected_local)\"\n else\n log \"$(msg port.local_only.selected_external)\"\n echo \"\"\n echo -e \"\\033[33m$(msg port.local_only.https_hint)\\033[0m\"\n fi\n}\n\nstep_ports() {\n log \"$(msg port.title)\"\n prompt AGENTTEAMS_PORT_GATEWAY \"$(msg port.gateway_prompt)\" \"18080\" || return 0\n prompt AGENTTEAMS_PORT_CONSOLE \"$(msg port.console_prompt)\" \"18001\" || return 0\n prompt AGENTTEAMS_PORT_ELEMENT_WEB \"$(msg port.element_prompt)\" \"18088\" || return 0\n prompt AGENTTEAMS_PORT_MANAGER_CONSOLE \"$(msg port.manager_console_prompt)\" \"18888\" || return 0\n log \"\"\n}\n\nstep_domains() {\n log \"$(msg domain.title)\"\n log \"$(msg domain.hint)\"\n prompt AGENTTEAMS_MATRIX_DOMAIN \"$(msg domain.matrix_prompt)\" \"matrix-local.agentteams.io:${AGENTTEAMS_PORT_GATEWAY}\" || return 0\n prompt AGENTTEAMS_MATRIX_CLIENT_DOMAIN \"$(msg domain.element_prompt)\" \"matrix-client-local.agentteams.io\" || return 0\n prompt AGENTTEAMS_AI_GATEWAY_DOMAIN \"$(msg domain.gateway_prompt)\" \"aigw-local.agentteams.io\" || return 0\n prompt AGENTTEAMS_FS_DOMAIN \"$(msg domain.fs_prompt)\" \"fs-local.agentteams.io\" || return 0\n if [ \"${AGENTTEAMS_MANAGER_RUNTIME}\" != \"copaw\" ]; then\n prompt AGENTTEAMS_CONSOLE_DOMAIN \"$(msg domain.console_prompt)\" \"console-local.agentteams.io\" || return 0\n fi\n log \"\"\n}\n\nstep_github() {\n log \"$(msg github.title)\"\n prompt_optional AGENTTEAMS_GITHUB_TOKEN \"$(msg github.token_prompt)\" \"true\" || return 0\n}\n\nstep_skills() {\n log \"\"\n log \"$(msg skills.title)\"\n prompt_optional AGENTTEAMS_SKILLS_API_URL \"$(msg skills.url_prompt)\" || return 0\n log \"\"\n}\n\nstep_volume() {\n log \"$(msg data.title)\"\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_DATA_DIR=\"${AGENTTEAMS_DATA_DIR:-agentteams-data}\"\n log \" $(msg data.volume_using \"${AGENTTEAMS_DATA_DIR}\") (non-interactive, skipped)\"\n export AGENTTEAMS_DATA_DIR\n return 0\n fi\n # ─────────────────────────────────────────────────────────────────\n if [ -z \"${AGENTTEAMS_DATA_DIR+x}\" ]; then\n local _input\n read -e -p \"$(msg data.volume_prompt): \" _input\n if [ \"${_input}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_DATA_DIR=\"${_input:-agentteams-data}\"\n export AGENTTEAMS_DATA_DIR\n fi\n AGENTTEAMS_DATA_DIR=\"${AGENTTEAMS_DATA_DIR:-agentteams-data}\"\n log \"$(msg data.volume_using \"${AGENTTEAMS_DATA_DIR}\")\"\n}\n\nstep_workspace() {\n log \"$(msg workspace.title)\"\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_WORKSPACE_DIR=\"${AGENTTEAMS_WORKSPACE_DIR:-${HOME}/agentteams-manager}\"\n AGENTTEAMS_WORKSPACE_DIR=\"$(cd \"${AGENTTEAMS_WORKSPACE_DIR}\" 2>/dev/null && pwd || echo \"${AGENTTEAMS_WORKSPACE_DIR}\")\"\n mkdir -p \"${AGENTTEAMS_WORKSPACE_DIR}\"\n log \" $(msg workspace.dir_label \"${AGENTTEAMS_WORKSPACE_DIR}\") (non-interactive, skipped)\"\n export AGENTTEAMS_WORKSPACE_DIR\n return 0\n fi\n # ─────────────────────────────────────────────────────────────────\n local _input\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg workspace.dir_prompt \"${HOME}/agentteams-manager\")\" \"${AGENTTEAMS_WORKSPACE_DIR}\")\"\n _input=\"${AGENTTEAMS_WORKSPACE_DIR}\"\n else\n local _current=\"${AGENTTEAMS_WORKSPACE_DIR}\"\n if [ -n \"${_current}\" ]; then\n read -e -p \"$(msg workspace.dir_prompt \"${HOME}/agentteams-manager\") [${_current}]: \" _input\n else\n read -e -p \"$(msg workspace.dir_prompt \"${HOME}/agentteams-manager\"): \" _input\n fi\n if [ \"${_input}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _input=\"${_input:-${_current}}\"\n fi\n AGENTTEAMS_WORKSPACE_DIR=\"${_input:-${HOME}/agentteams-manager}\"\n export AGENTTEAMS_WORKSPACE_DIR\n AGENTTEAMS_WORKSPACE_DIR=\"$(cd \"${AGENTTEAMS_WORKSPACE_DIR}\" 2>/dev/null && pwd || echo \"${AGENTTEAMS_WORKSPACE_DIR}\")\"\n mkdir -p \"${AGENTTEAMS_WORKSPACE_DIR}\"\n log \"$(msg workspace.dir_label \"${AGENTTEAMS_WORKSPACE_DIR}\")\"\n}\n\nstep_dashboard() {\n AGENTTEAMS_DASHBOARD=\"${AGENTTEAMS_DASHBOARD:-1}\"\n AGENTTEAMS_DASHBOARD_VERSION=\"${AGENTTEAMS_DASHBOARD_VERSION:-v1.2.0-beta.2}\"\n AGENTTEAMS_PORT_DASHBOARD=\"${AGENTTEAMS_PORT_DASHBOARD:-13000}\"\n AGENTTEAMS_AI_GATEWAY_ADMIN_URL=\"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL:-}\"\n\n # Compute the default image for the current version.\n # We track whether the current image matches the derived default so that\n # changing the version also updates the image (unless the user has\n # explicitly overridden it to a non-default value).\n _dashboard_default_image() {\n echo \"${AGENTTEAMS_REGISTRY}/agentteams/agentteams-dashboard:${1:-${AGENTTEAMS_DASHBOARD_VERSION}}\"\n }\n local _init_default\n _init_default=$(_dashboard_default_image)\n if [ -z \"${AGENTTEAMS_DASHBOARD_IMAGE:-}\" ]; then\n AGENTTEAMS_DASHBOARD_IMAGE=\"${_init_default}\"\n fi\n\n # Recompute default image when the version changes AND the current image\n # matches the default for the *previous* (env-file) version.\n # This handles non-interactive upgrades correctly:\n # - env file loads old default image + old version\n # - CLI passes new version\n # - image should follow (because it was auto-derived before)\n # But preserves truly custom images (same repo but custom tag like :canary)\n # because they won't match the old-version default exactly.\n local _old_saved_version=\"\"\n local _env_file=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n if [ -f \"${_env_file}\" ]; then\n _old_saved_version=\"$(grep '^AGENTTEAMS_DASHBOARD_VERSION=' \"${_env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n fi\n if [ -n \"${_old_saved_version}\" ] && \\\n [ \"${_old_saved_version}\" != \"${AGENTTEAMS_DASHBOARD_VERSION}\" ] && \\\n [ \"${AGENTTEAMS_DASHBOARD_IMAGE}\" = \"$(_dashboard_default_image \"${_old_saved_version}\")\" ]; then\n AGENTTEAMS_DASHBOARD_IMAGE=$(_dashboard_default_image)\n fi\n\n # Quick Start: accept all defaults without prompting. The dashboard is\n # installed by default (AGENTTEAMS_DASHBOARD=1); set AGENTTEAMS_DASHBOARD=0\n # to opt out.\n if [ \"${AGENTTEAMS_QUICKSTART:-0}\" = \"1\" ]; then\n if [ \"${AGENTTEAMS_DASHBOARD}\" = \"1\" ]; then\n log \"$(msg dash.summary \"${AGENTTEAMS_PORT_DASHBOARD}\" \"${AGENTTEAMS_DASHBOARD_IMAGE}\") (quick start)\"\n else\n log \"$(msg dash.skip) (quick start)\"\n fi\n export AGENTTEAMS_DASHBOARD AGENTTEAMS_DASHBOARD_VERSION AGENTTEAMS_PORT_DASHBOARD AGENTTEAMS_DASHBOARD_IMAGE AGENTTEAMS_AI_GATEWAY_ADMIN_URL\n return 0\n fi\n\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_DASHBOARD=\"${AGENTTEAMS_DASHBOARD:-1}\"\n if [ \"${AGENTTEAMS_DASHBOARD}\" = \"1\" ]; then\n log \"$(msg dash.summary \"${AGENTTEAMS_PORT_DASHBOARD}\" \"${AGENTTEAMS_DASHBOARD_IMAGE}\") (non-interactive)\"\n else\n log \"$(msg dash.skip) (non-interactive)\"\n fi\n export AGENTTEAMS_DASHBOARD AGENTTEAMS_DASHBOARD_VERSION AGENTTEAMS_PORT_DASHBOARD AGENTTEAMS_DASHBOARD_IMAGE AGENTTEAMS_AI_GATEWAY_ADMIN_URL\n return 0\n fi\n\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ]; then\n log \"$(msg prompt.upgrade_keep \"\" \"${AGENTTEAMS_DASHBOARD}\") dashboard=${AGENTTEAMS_DASHBOARD} version=${AGENTTEAMS_DASHBOARD_VERSION} port=${AGENTTEAMS_PORT_DASHBOARD}\"\n export AGENTTEAMS_DASHBOARD AGENTTEAMS_DASHBOARD_VERSION AGENTTEAMS_PORT_DASHBOARD AGENTTEAMS_DASHBOARD_IMAGE AGENTTEAMS_AI_GATEWAY_ADMIN_URL\n return 0\n fi\n\n local _input\n echo \"\"\n read -p \"$(msg dash.prompt) [Y/n]: \" _input\n case \"${_input:-}\" in\n [nN0]) AGENTTEAMS_DASHBOARD=0 ;;\n *) AGENTTEAMS_DASHBOARD=1 ;;\n esac\n if [ \"${_input}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n\n if [ \"${AGENTTEAMS_DASHBOARD}\" != \"1\" ]; then\n log \"$(msg dash.skip)\"\n export AGENTTEAMS_DASHBOARD AGENTTEAMS_DASHBOARD_VERSION AGENTTEAMS_PORT_DASHBOARD AGENTTEAMS_DASHBOARD_IMAGE AGENTTEAMS_AI_GATEWAY_ADMIN_URL\n return 0\n fi\n\n # Dashboard version\n local _current_version=\"${AGENTTEAMS_DASHBOARD_VERSION}\"\n local _old_version=\"${AGENTTEAMS_DASHBOARD_VERSION}\"\n local _old_default_image\n _old_default_image=$(_dashboard_default_image \"${_old_version}\")\n read -p \"$(msg dash.version_prompt) [${_current_version}]: \" _input\n AGENTTEAMS_DASHBOARD_VERSION=\"${_input:-${_current_version}}\"\n\n # Dashboard port\n local _current_port=\"${AGENTTEAMS_PORT_DASHBOARD}\"\n read -p \"$(msg dash.port_prompt) [${_current_port}]: \" _input\n AGENTTEAMS_PORT_DASHBOARD=\"${_input:-${_current_port}}\"\n\n # Dashboard image: if the current image matches the default derived from\n # the OLD version, recompute it from the NEW version. This handles both\n # fresh installs and upgrades where the previous image was the default.\n # Only a real user override (image != default for old version) is preserved.\n if [ \"${AGENTTEAMS_DASHBOARD_VERSION}\" != \"${_old_version}\" ] && \\\n [ \"${AGENTTEAMS_DASHBOARD_IMAGE}\" = \"${_old_default_image}\" ]; then\n AGENTTEAMS_DASHBOARD_IMAGE=$(_dashboard_default_image)\n fi\n local _current_image=\"${AGENTTEAMS_DASHBOARD_IMAGE}\"\n read -p \"$(msg dash.image_prompt) [${_current_image}]: \" _input\n if [ -n \"${_input}\" ]; then\n AGENTTEAMS_DASHBOARD_IMAGE=\"${_input}\"\n fi\n\n # Higress Console URL (explicit config takes priority; auto-detect as fallback)\n local _higress_url=\"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\"\n if [ -z \"${_higress_url}\" ]; then\n if ${DOCKER_CMD} ps --format '{{.Names}}' | grep -q \"^agentteams-controller$\"; then\n if ${DOCKER_CMD} exec agentteams-controller curl -sf --max-time 2 http://127.0.0.1:8001/ >/dev/null 2>&1; then\n _higress_url=\"http://agentteams-controller:8001\"\n log \"$(msg dash.auto_url): ${_higress_url}\"\n else\n log \"$(msg dash.auto_url_fail)\"\n fi\n fi\n fi\n read -p \"$(msg dash.gateway_prompt) [${_higress_url:-<auto>}]: \" _input\n AGENTTEAMS_AI_GATEWAY_ADMIN_URL=\"${_input:-${_higress_url}}\"\n\n # Normalize URL: add http:// prefix if missing\n if [ -n \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\" ]; then\n case \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\" in\n http://*|https://*) ;;\n *) AGENTTEAMS_AI_GATEWAY_ADMIN_URL=\"http://${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\" ;;\n esac\n fi\n\n # Verify the Higress Console URL is reachable (best-effort warning only)\n if [ -n \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\" ]; then\n local _gw_reachable=0\n # If the URL points to the controller container, test from inside the container\n if echo \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\" | grep -qE \"(agentteams-controller|hiclaw-controller|127\\.0\\.0\\.1|localhost)\"; then\n if ${DOCKER_CMD} ps --format '{{.Names}}' | grep -q \"^agentteams-controller$\"; then\n if ${DOCKER_CMD} exec agentteams-controller curl -sf --max-time 3 \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}/\" >/dev/null 2>&1; then\n _gw_reachable=1\n fi\n fi\n else\n # External URL — probe from inside the controller (same network as\n # the Dashboard container) when possible; fall back to the host.\n if ${DOCKER_CMD} ps --format '{{.Names}}' | grep -q \"^agentteams-controller$\"; then\n if ${DOCKER_CMD} exec agentteams-controller curl -sf --max-time 3 \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}/\" >/dev/null 2>&1; then\n _gw_reachable=1\n fi\n elif curl -sf --max-time 3 \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}/\" >/dev/null 2>&1; then\n _gw_reachable=1\n fi\n fi\n if [ \"${_gw_reachable}\" = \"0\" ]; then\n log \"WARNING: Higress Console URL may not be reachable: ${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\"\n log \" Dashboard will still work, but shared login via Higress Console may fail.\"\n log \" To fix: verify the URL is correct and the Higress Console service is running.\"\n fi\n fi\n\n log \"$(msg dash.summary \"${AGENTTEAMS_PORT_DASHBOARD}\" \"${AGENTTEAMS_DASHBOARD_IMAGE}\")\"\n export AGENTTEAMS_DASHBOARD AGENTTEAMS_DASHBOARD_VERSION AGENTTEAMS_PORT_DASHBOARD AGENTTEAMS_DASHBOARD_IMAGE AGENTTEAMS_AI_GATEWAY_ADMIN_URL\n}\n\nstep_runtime() {\n log \"$(msg worker_runtime.title)\"\n echo \"\"\n echo \" 1) $(msg worker_runtime.copaw)\"\n echo \" 2) $(msg worker_runtime.openclaw)\"\n if ! _ver_lt \"${AGENTTEAMS_VERSION}\" \"v1.1.0\"; then\n echo \" 3) $(msg worker_runtime.hermes)\"\n fi\n echo \"\"\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"${AGENTTEAMS_DEFAULT_WORKER_RUNTIME:-copaw}\"\n elif [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_DEFAULT_WORKER_RUNTIME}\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg worker_runtime.title_short)\" \"${AGENTTEAMS_DEFAULT_WORKER_RUNTIME}\")\"\n local _runtime_choice\n read -e -p \"$(msg worker_runtime.choice): \" _runtime_choice\n if [ \"${_runtime_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n if [ -n \"${_runtime_choice}\" ]; then\n case \"${_runtime_choice}\" in\n 2) AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"openclaw\" ;;\n 3) if ! _ver_lt \"${AGENTTEAMS_VERSION}\" \"v1.1.0\"; then\n AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"hermes\"\n else\n AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"copaw\"\n fi ;;\n *) AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"copaw\" ;;\n esac\n fi\n elif [ -z \"${AGENTTEAMS_DEFAULT_WORKER_RUNTIME+x}\" ]; then\n local _runtime_choice\n read -e -p \"$(msg worker_runtime.choice): \" _runtime_choice\n if [ \"${_runtime_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _runtime_choice=\"${_runtime_choice:-1}\"\n case \"${_runtime_choice}\" in\n 2) AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"openclaw\" ;;\n 3) if ! _ver_lt \"${AGENTTEAMS_VERSION}\" \"v1.1.0\"; then\n AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"hermes\"\n else\n AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"copaw\"\n fi ;;\n *) AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"copaw\" ;;\n esac\n fi\n export AGENTTEAMS_DEFAULT_WORKER_RUNTIME\n log \"$(msg worker_runtime.selected \"${AGENTTEAMS_DEFAULT_WORKER_RUNTIME}\")\"\n}\n\nstep_manager_runtime() {\n log \"$(msg manager_runtime.title)\"\n echo \"\"\n echo \" 1) $(msg manager_runtime.copaw)\"\n echo \" 2) $(msg manager_runtime.openclaw)\"\n echo \"\"\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_MANAGER_RUNTIME=\"${AGENTTEAMS_MANAGER_RUNTIME:-copaw}\"\n elif [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_MANAGER_RUNTIME}\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg manager_runtime.title_short)\" \"${AGENTTEAMS_MANAGER_RUNTIME}\")\"\n local _runtime_choice\n read -e -p \"$(msg manager_runtime.choice): \" _runtime_choice\n if [ \"${_runtime_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n if [ -n \"${_runtime_choice}\" ]; then\n case \"${_runtime_choice}\" in\n 2) AGENTTEAMS_MANAGER_RUNTIME=\"openclaw\" ;;\n *) AGENTTEAMS_MANAGER_RUNTIME=\"copaw\" ;;\n esac\n fi\n elif [ -z \"${AGENTTEAMS_MANAGER_RUNTIME+x}\" ]; then\n local _runtime_choice\n read -e -p \"$(msg manager_runtime.choice): \" _runtime_choice\n if [ \"${_runtime_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _runtime_choice=\"${_runtime_choice:-1}\"\n case \"${_runtime_choice}\" in\n 2) AGENTTEAMS_MANAGER_RUNTIME=\"openclaw\" ;;\n *) AGENTTEAMS_MANAGER_RUNTIME=\"copaw\" ;;\n esac\n fi\n export AGENTTEAMS_MANAGER_RUNTIME\n log \"$(msg manager_runtime.selected \"${AGENTTEAMS_MANAGER_RUNTIME}\")\"\n}\n\nstep_e2ee() {\n log \"\"\n log \"$(msg matrix_e2ee.title)\"\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_MATRIX_E2EE=\"${AGENTTEAMS_MATRIX_E2EE:-0}\"\n log \" $(msg matrix_e2ee.title_short) = ${AGENTTEAMS_MATRIX_E2EE} (non-interactive, skipped)\"\n export AGENTTEAMS_MATRIX_E2EE\n return 0\n fi\n # ─────────────────────────────────────────────────────────────────\n echo \"\"\n echo -e \" $(msg matrix_e2ee.desc)\"\n echo \"\"\n echo \" 1) $(msg matrix_e2ee.disable)\"\n echo \" 2) $(msg matrix_e2ee.enable)\"\n echo \"\"\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_MATRIX_E2EE}\" ]; then\n local _e2ee_display; if [ \"${AGENTTEAMS_MATRIX_E2EE}\" = \"1\" ]; then _e2ee_display=\"$(msg matrix_e2ee.val_enabled)\"; else _e2ee_display=\"$(msg matrix_e2ee.val_disabled)\"; fi\n log \"$(msg prompt.upgrade_keep \"$(msg matrix_e2ee.title_short)\" \"${_e2ee_display}\")\"\n local _e2ee_choice\n read -e -p \"$(msg matrix_e2ee.choice): \" _e2ee_choice\n if [ \"${_e2ee_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n if [ -n \"${_e2ee_choice}\" ]; then\n case \"${_e2ee_choice}\" in\n 2) AGENTTEAMS_MATRIX_E2EE=\"1\" ;;\n *) AGENTTEAMS_MATRIX_E2EE=\"0\" ;;\n esac\n fi\n elif [ -z \"${AGENTTEAMS_MATRIX_E2EE+x}\" ]; then\n local _e2ee_choice\n read -e -p \"$(msg matrix_e2ee.choice): \" _e2ee_choice\n if [ \"${_e2ee_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _e2ee_choice=\"${_e2ee_choice:-1}\"\n case \"${_e2ee_choice}\" in\n 2) AGENTTEAMS_MATRIX_E2EE=\"1\" ;;\n *) AGENTTEAMS_MATRIX_E2EE=\"0\" ;;\n esac\n fi\n AGENTTEAMS_MATRIX_E2EE=\"${AGENTTEAMS_MATRIX_E2EE:-0}\"\n export AGENTTEAMS_MATRIX_E2EE\n if [ \"${AGENTTEAMS_MATRIX_E2EE}\" = \"1\" ]; then\n log \"$(msg matrix_e2ee.selected_enabled)\"\n else\n log \"$(msg matrix_e2ee.selected_disabled)\"\n fi\n}\n\nstep_docker_proxy() {\n # Only relevant when socket mounting is enabled\n if [ \"${AGENTTEAMS_MOUNT_SOCKET}\" != \"1\" ]; then\n AGENTTEAMS_DOCKER_PROXY=\"0\"\n return 0\n fi\n\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_DOCKER_PROXY=\"${AGENTTEAMS_DOCKER_PROXY:-0}\"\n log \" $(msg docker_proxy.title_short) = ${AGENTTEAMS_DOCKER_PROXY} (non-interactive, skipped)\"\n export AGENTTEAMS_DOCKER_PROXY\n return 0\n fi\n # ─────────────────────────────────────────────────────────────────\n\n echo \"\"\n echo -e \" \\033[1m$(msg docker_proxy.title)\\033[0m\"\n echo \"\"\n echo -e \" $(msg docker_proxy.desc)\"\n echo \"\"\n echo \" 1) $(msg docker_proxy.enable)\"\n echo \" 2) $(msg docker_proxy.disable)\"\n echo \"\"\n\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_DOCKER_PROXY}\" ]; then\n local _proxy_display; if [ \"${AGENTTEAMS_DOCKER_PROXY}\" = \"1\" ]; then _proxy_display=\"$(msg docker_proxy.val_enabled)\"; else _proxy_display=\"$(msg docker_proxy.val_disabled)\"; fi\n log \"$(msg prompt.upgrade_keep \"$(msg docker_proxy.title_short)\" \"${_proxy_display}\")\"\n local _choice\n read -e -p \"$(msg docker_proxy.choice): \" _choice\n if [ \"${_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n if [ -n \"${_choice}\" ]; then\n case \"${_choice}\" in\n 2) AGENTTEAMS_DOCKER_PROXY=\"0\" ;;\n *) AGENTTEAMS_DOCKER_PROXY=\"1\" ;;\n esac\n fi\n elif [ -z \"${AGENTTEAMS_DOCKER_PROXY+x}\" ]; then\n local _choice\n read -e -p \"$(msg docker_proxy.choice): \" _choice\n if [ \"${_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _choice=\"${_choice:-1}\"\n case \"${_choice}\" in\n 2) AGENTTEAMS_DOCKER_PROXY=\"0\" ;;\n *) AGENTTEAMS_DOCKER_PROXY=\"1\" ;;\n esac\n fi\n AGENTTEAMS_DOCKER_PROXY=\"${AGENTTEAMS_DOCKER_PROXY:-1}\"\n export AGENTTEAMS_DOCKER_PROXY\n if [ \"${AGENTTEAMS_DOCKER_PROXY}\" = \"1\" ]; then\n log \"$(msg docker_proxy.selected_enabled)\"\n\n # Prompt for additional allowed image sources\n echo \"\"\n echo -e \" $(msg docker_proxy.registries_desc)\"\n echo \"\"\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_PROXY_ALLOWED_REGISTRIES}\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg docker_proxy.registries_label)\" \"${AGENTTEAMS_PROXY_ALLOWED_REGISTRIES}\")\"\n local _reg_input\n read -e -p \"$(msg docker_proxy.registries_prompt): \" _reg_input\n if [ \"${_reg_input}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n [ -n \"${_reg_input}\" ] && AGENTTEAMS_PROXY_ALLOWED_REGISTRIES=\"${_reg_input}\"\n elif [ -z \"${AGENTTEAMS_PROXY_ALLOWED_REGISTRIES+x}\" ]; then\n local _reg_input\n read -e -p \"$(msg docker_proxy.registries_prompt): \" _reg_input\n if [ \"${_reg_input}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_PROXY_ALLOWED_REGISTRIES=\"${_reg_input:-}\"\n fi\n export AGENTTEAMS_PROXY_ALLOWED_REGISTRIES\n else\n log \"$(msg docker_proxy.selected_disabled)\"\n fi\n}\n\nstep_idle() {\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_WORKER_IDLE_TIMEOUT=\"${AGENTTEAMS_WORKER_IDLE_TIMEOUT:-720}\"\n log \" $(msg idle_timeout.label) = ${AGENTTEAMS_WORKER_IDLE_TIMEOUT} (non-interactive, skipped)\"\n export AGENTTEAMS_WORKER_IDLE_TIMEOUT\n return 0\n fi\n # ─────────────────────────────────────────────────────────────────\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_WORKER_IDLE_TIMEOUT}\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg idle_timeout.label)\" \"${AGENTTEAMS_WORKER_IDLE_TIMEOUT}\")\"\n local _idle_timeout\n read -e -p \"$(msg idle_timeout.prompt): \" _idle_timeout\n if [ \"${_idle_timeout}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n [ -n \"${_idle_timeout}\" ] && AGENTTEAMS_WORKER_IDLE_TIMEOUT=\"${_idle_timeout}\"\n elif [ -z \"${AGENTTEAMS_WORKER_IDLE_TIMEOUT+x}\" ]; then\n local _idle_timeout\n read -e -p \"$(msg idle_timeout.prompt): \" _idle_timeout\n if [ \"${_idle_timeout}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_WORKER_IDLE_TIMEOUT=\"${_idle_timeout:-720}\"\n fi\n AGENTTEAMS_WORKER_IDLE_TIMEOUT=\"${AGENTTEAMS_WORKER_IDLE_TIMEOUT:-720}\"\n export AGENTTEAMS_WORKER_IDLE_TIMEOUT\n log \"$(msg idle_timeout.selected \"${AGENTTEAMS_WORKER_IDLE_TIMEOUT}\")\"\n}\n\nstep_hostshare() {\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_HOST_SHARE_DIR=\"${AGENTTEAMS_HOST_SHARE_DIR:-${HOME}}\"\n log \" $(msg host_share.label) = ${AGENTTEAMS_HOST_SHARE_DIR} (non-interactive, skipped)\"\n export AGENTTEAMS_HOST_SHARE_DIR\n return 0\n fi\n # ─────────────────────────────────────────────────────────────────\n local _current=\"${AGENTTEAMS_HOST_SHARE_DIR}\"\n local _share_dir\n if [ -n \"${_current}\" ]; then\n read -e -p \"$(msg host_share.prompt \"$HOME\") [${_current}]: \" _share_dir\n else\n read -e -p \"$(msg host_share.prompt \"$HOME\"): \" _share_dir\n fi\n if [ \"${_share_dir}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_HOST_SHARE_DIR=\"${_share_dir:-${_current:-$HOME}}\"\n export AGENTTEAMS_HOST_SHARE_DIR\n}\n\nstep_podman_autostart() {\n echo \"\"\n log \"$(msg podman.autostart.title)\"\n echo \"\"\n echo -e \" $(msg podman.autostart.desc)\"\n echo \"\"\n echo \" 1) $(msg podman.autostart.enable)\"\n echo \" 2) $(msg podman.autostart.disable)\"\n echo \"\"\n\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_PODMAN_AUTOSTART}\" ]; then\n local _disp; if [ \"${AGENTTEAMS_PODMAN_AUTOSTART}\" = \"1\" ]; then _disp=\"$(msg podman.autostart.val_enabled)\"; else _disp=\"$(msg podman.autostart.val_disabled)\"; fi\n log \"$(msg prompt.upgrade_keep \"$(msg podman.autostart.title_short)\" \"${_disp}\")\"\n local _choice\n read -e -p \"$(msg podman.autostart.choice): \" _choice\n if [ \"${_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n if [ -n \"${_choice}\" ]; then\n case \"${_choice}\" in\n 2) AGENTTEAMS_PODMAN_AUTOSTART=\"0\" ;;\n *) AGENTTEAMS_PODMAN_AUTOSTART=\"1\" ;;\n esac\n fi\n elif [ -z \"${AGENTTEAMS_PODMAN_AUTOSTART+x}\" ]; then\n local _choice\n read -e -p \"$(msg podman.autostart.choice) [1]: \" _choice\n if [ \"${_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _choice=\"${_choice:-1}\"\n case \"${_choice}\" in\n 2) AGENTTEAMS_PODMAN_AUTOSTART=\"0\" ;;\n *) AGENTTEAMS_PODMAN_AUTOSTART=\"1\" ;;\n esac\n fi\n AGENTTEAMS_PODMAN_AUTOSTART=\"${AGENTTEAMS_PODMAN_AUTOSTART:-1}\"\n export AGENTTEAMS_PODMAN_AUTOSTART\n if [ \"${AGENTTEAMS_PODMAN_AUTOSTART}\" = \"1\" ]; then\n log \"$(msg podman.autostart.selected_enabled)\"\n else\n log \"$(msg podman.autostart.selected_disabled)\"\n fi\n}\n\n# ============================================================\n# Setup dedicated Podman autostart service for AgentTeams\n# ============================================================\nsetup_podman_autostart() {\n # Only execute if using podman and autostart is enabled\n if [ \"${DOCKER_CMD:-docker}\" != \"podman\" ] || [ \"${AGENTTEAMS_PODMAN_AUTOSTART:-0}\" != \"1\" ]; then\n return 0\n fi\n\n log \"$(msg install.podman.autostart_title)\"\n\n local current_user\n current_user=\"$(whoami)\"\n\n # Define the dedicated AgentTeams service content\n # Strict scoping: Only manage containers matching '--filter name=agentteams'\n local _service_content=\"[Unit]\nDescription=AgentTeams Dedicated Podman Autostart Service\nDocumentation=https://github.com/agentscope-ai/AgentTeams\nWants=network-online.target\nAfter=network-online.target\n\n[Service]\nType=oneshot\nRemainAfterExit=true\nEnvironment=LOGGING=\\\"--log-level=info\\\"\nExecStart=/usr/bin/podman \\$LOGGING start --filter name=agentteams --filter restart-policy=always --filter restart-policy=unless-stopped\nExecStop=/usr/bin/podman \\$LOGGING stop --filter name=agentteams --filter restart-policy=always --filter restart-policy=unless-stopped\n\n[Install]\nWantedBy=default.target\"\n\n local _autostart_ok=0\n\n if [ \"${current_user}\" = \"root\" ]; then\n # Rootful Mode (System-wide dedicated service)\n log \"$(msg install.podman.root_setup)\"\n\n local _sys_dir=\"/etc/systemd/system\"\n mkdir -p \"${_sys_dir}\"\n echo \"${_service_content}\" > \"${_sys_dir}/agentteams-podman-restart.service\"\n\n systemctl daemon-reload >/dev/null 2>&1 || true\n\n if systemctl enable --now agentteams-podman-restart.service >/dev/null 2>&1; then\n log \"$(msg install.podman.root_success)\"\n _autostart_ok=1\n else\n log \"$(msg install.podman.root_fail)\"\n fi\n else\n # Rootless Mode (User-level dedicated service)\n log \"$(msg install.podman.user_setup \"${current_user}\")\"\n\n local _linger_ok=0\n\n # 1. Enable Linger for background execution (Graceful escalation strategy)\n if loginctl show-user \"${current_user}\" --property=Linger 2>/dev/null | grep -q \"Linger=yes\"; then\n _linger_ok=1\n else\n log \"$(msg install.podman.linger_enable)\"\n\n # Step A: Try to enable linger natively without sudo\n if loginctl enable-linger \"${current_user}\" >/dev/null 2>&1; then\n log \"Successfully enabled systemd linger natively.\"\n _linger_ok=1\n else\n # Step B: Fallback to sudo\n log \"Native linger enablement failed. Distro security policy requires privileges.\"\n if command -v sudo >/dev/null 2>&1; then\n log \"Attempting to enable systemd linger via sudo (enter password if prompted)...\"\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n sudo -n loginctl enable-linger \"${current_user}\" 2>/dev/null && _linger_ok=1 || log \"$(msg install.podman.linger_warn)\"\n else\n sudo loginctl enable-linger \"${current_user}\" && _linger_ok=1 || log \"$(msg install.podman.linger_warn)\"\n fi\n else\n log \"$(msg install.podman.linger_warn)\"\n fi\n fi\n fi\n\n # 2. Write dedicated service file to user directory\n local _user_dir=\"${HOME}/.config/systemd/user\"\n mkdir -p \"${_user_dir}\"\n echo \"${_service_content}\" > \"${_user_dir}/agentteams-podman-restart.service\"\n\n systemctl --user daemon-reload >/dev/null 2>&1 || true\n\n if systemctl --user enable --now agentteams-podman-restart.service >/dev/null 2>&1; then\n log \"$(msg install.podman.user_success)\"\n # Crucial: Only consider autostart fully successful if systemd linger is also enabled\n if [ \"${_linger_ok}\" = \"1\" ]; then\n _autostart_ok=1\n else\n log \"⚠️ Service enabled, but since systemd linger is not active, containers will not auto-recover on system reboot.\"\n fi\n else\n log \"$(msg install.podman.user_fail)\"\n fi\n fi\n\n # Only print final success banner if the service AND linger were actually enabled\n if [ \"${_autostart_ok}\" = \"1\" ]; then\n log \"$(msg install.podman.success)\"\n fi\n}\n\n# ============================================================\n# Dashboard startup (called after controller is ready)\n# ============================================================\n\n_start_dashboard() {\n local CTRL_CONTAINER=\"agentteams-controller\"\n local DASHBOARD_CONTAINER=\"agentteams-dashboard\"\n\n if [ \"${AGENTTEAMS_DASHBOARD:-0}\" != \"1\" ]; then\n # Stop and remove the existing dashboard container if present.\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -q \"^${DASHBOARD_CONTAINER}$\"; then\n log \"Stopping and removing ${DASHBOARD_CONTAINER}...\"\n ${DOCKER_CMD} stop \"${DASHBOARD_CONTAINER}\" >/dev/null 2>&1 || true\n ${DOCKER_CMD} rm -f \"${DASHBOARD_CONTAINER}\" >/dev/null 2>&1 || true\n log \"Dashboard stopped.\"\n fi\n return 0\n fi\n\n # Dashboard requires the embedded architecture (controller container).\n # In legacy all-in-one mode the controller API is not available.\n if [ \"${AGENTTEAMS_USE_EMBEDDED:-0}\" != \"1\" ]; then\n log \"Skipping Dashboard: requires embedded architecture (agentteams-controller), not available in legacy mode.\"\n log \"Use Quick Start (embedded) mode to enable agentteams-dashboard.\"\n return 0\n fi\n\n AGENTTEAMS_PORT_DASHBOARD=\"${AGENTTEAMS_PORT_DASHBOARD:-13000}\"\n AGENTTEAMS_DASHBOARD_VERSION=\"${AGENTTEAMS_DASHBOARD_VERSION:-v1.2.0-beta.2}\"\n AGENTTEAMS_DASHBOARD_IMAGE=\"${AGENTTEAMS_DASHBOARD_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-dashboard:${AGENTTEAMS_DASHBOARD_VERSION}}\"\n\n log \"\"\n log \"Starting agentteams-dashboard...\"\n\n local BIND_ADDR=\"127.0.0.1\"\n [ \"${AGENTTEAMS_LOCAL_ONLY:-1}\" = \"0\" ] && BIND_ADDR=\"0.0.0.0\"\n\n # Remove existing container\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' | grep -qx \"${DASHBOARD_CONTAINER}\"; then\n ${DOCKER_CMD} rm -f \"${DASHBOARD_CONTAINER}\" >/dev/null 2>&1 || true\n fi\n\n # Pull or use local image\n if ! ${DOCKER_CMD} image inspect \"${AGENTTEAMS_DASHBOARD_IMAGE}\" >/dev/null 2>&1; then\n log \"Pulling dashboard image: ${AGENTTEAMS_DASHBOARD_IMAGE}\"\n ${DOCKER_CMD} pull \"${AGENTTEAMS_DASHBOARD_IMAGE}\" 2>/dev/null || {\n log \"WARNING: Could not pull dashboard image. Skipping.\"\n return 0\n }\n fi\n\n # Build env args from controller container (MinIO/LLM/auth).\n local env_args=()\n env_args+=(-e AGENTTEAMS_CONTROLLER_URL=\"http://${CTRL_CONTAINER}:8090\")\n env_args+=(-e NEXT_PUBLIC_MATRIX_API_URL=\"http://${CTRL_CONTAINER}:6167\")\n env_args+=(-e MATRIX_HOMESERVER_ALLOWLIST=\"${CTRL_CONTAINER},matrix-local.agentteams.io,matrix.org\")\n\n if ${DOCKER_CMD} ps --format '{{.Names}}' | grep -qx \"${CTRL_CONTAINER}\"; then\n local env_out\n env_out=$(${DOCKER_CMD} inspect \"${CTRL_CONTAINER}\" --format='{{range .Config.Env}}{{.}}{{\"\\n\"}}{{end}}')\n\n _env() { echo \"${env_out}\" | sed -n \"s|^${1}=||p\" | head -n1; }\n\n local fs_endpoint; fs_endpoint=$(_env AGENTTEAMS_FS_ENDPOINT)\n [ -z \"${fs_endpoint}\" ] && fs_endpoint=$(_env AGENTTEAMS_MINIO_ENDPOINT)\n if [ -n \"${fs_endpoint}\" ]; then\n fs_endpoint=$(echo \"${fs_endpoint}\" | sed -e \"s|127\\.0\\.0\\.1|${CTRL_CONTAINER}|\" -e \"s|localhost|${CTRL_CONTAINER}|\")\n env_args+=(-e AGENTTEAMS_FS_ENDPOINT=\"${fs_endpoint}\")\n else\n env_args+=(-e AGENTTEAMS_FS_ENDPOINT=\"http://${CTRL_CONTAINER}:9000\")\n fi\n\n local fs_bucket; fs_bucket=$(_env AGENTTEAMS_FS_BUCKET)\n [ -z \"${fs_bucket}\" ] && fs_bucket=$(_env AGENTTEAMS_MINIO_BUCKET)\n [ -n \"${fs_bucket}\" ] && env_args+=(-e AGENTTEAMS_FS_BUCKET=\"${fs_bucket}\")\n\n local fs_user; fs_user=$(_env AGENTTEAMS_FS_ACCESS_KEY)\n [ -z \"${fs_user}\" ] && fs_user=$(_env AGENTTEAMS_MINIO_USER)\n [ -n \"${fs_user}\" ] && env_args+=(-e AGENTTEAMS_FS_ACCESS_KEY=\"${fs_user}\")\n\n local fs_pass; fs_pass=$(_env AGENTTEAMS_FS_SECRET_KEY)\n [ -z \"${fs_pass}\" ] && fs_pass=$(_env AGENTTEAMS_MINIO_PASSWORD)\n [ -n \"${fs_pass}\" ] && env_args+=(-e AGENTTEAMS_FS_SECRET_KEY=\"${fs_pass}\")\n\n for k in AGENTTEAMS_LLM_PROVIDER AGENTTEAMS_LLM_API_KEY AGENTTEAMS_OPENAI_BASE_URL AGENTTEAMS_DEFAULT_MODEL; do\n local v; v=$(_env \"${k}\")\n [ -n \"${v}\" ] && env_args+=(-e \"${k}=${v}\")\n done\n\n # The controller writes the CLI SA token to /var/run/agentteams/cli-token.\n # The token is generated asynchronously during the first reconcile loop,\n # so it may not be available immediately after the container starts.\n # We poll for up to 30s to handle both fresh installs (token not yet minted)\n # and older images that don't support cli-token at all.\n # For backward compatibility, also try the legacy /var/run/hiclaw/cli-token\n # path used by older HiClaw images.\n local auth_token=\"${AGENTTEAMS_AUTH_TOKEN:-}\"\n if [ -z \"${auth_token}\" ]; then\n local _token_wait=0\n local _token_max_wait=30\n while [ ${_token_wait} -lt ${_token_max_wait} ]; do\n auth_token=$(${DOCKER_CMD} exec \"${CTRL_CONTAINER}\" sh -c 'cat /var/run/agentteams/cli-token 2>/dev/null || cat /var/run/hiclaw/cli-token 2>/dev/null' | tr -d '\\n' || true)\n if [ -n \"${auth_token}\" ]; then\n break\n fi\n sleep 2\n _token_wait=$((_token_wait + 2))\n done\n fi\n if [ -n \"${auth_token}\" ]; then\n env_args+=(-e AGENTTEAMS_AUTH_TOKEN=\"${auth_token}\")\n else\n log \"WARNING: could not read controller SA token (timed out after ${_token_max_wait:-30}s)\"\n log \" This can happen with older AgentTeams/HiClaw images or if the controller is still starting.\"\n log \" Dashboard will still work but some API calls may fail until you log in via Higress Console.\"\n log \" To fix: upgrade AgentTeams to v1.2.0-beta.1+ or set AGENTTEAMS_AUTH_TOKEN manually.\"\n fi\n\n local admin_user; admin_user=$(_env AGENTTEAMS_ADMIN_USER)\n local admin_pass; admin_pass=$(_env AGENTTEAMS_ADMIN_PASSWORD)\n [ -n \"${admin_user}\" ] && env_args+=(-e AGENTTEAMS_ADMIN_USER=\"${admin_user}\")\n [ -n \"${admin_pass}\" ] && env_args+=(-e AGENTTEAMS_ADMIN_PASSWORD=\"${admin_pass}\")\n\n # Higress Console URL: explicit config takes priority, auto-detect as fallback.\n local _resolved_gw_url=\"\"\n if [ -n \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\" ]; then\n # Normalize URL: add http:// prefix if missing\n _resolved_gw_url=\"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\"\n case \"${_resolved_gw_url}\" in\n http://*|https://*) ;;\n *) _resolved_gw_url=\"http://${_resolved_gw_url}\" ;;\n esac\n env_args+=(-e AGENTTEAMS_AI_GATEWAY_ADMIN_URL=\"${_resolved_gw_url}\")\n elif ${DOCKER_CMD} exec \"${CTRL_CONTAINER}\" wget -q -O- --timeout=2 http://127.0.0.1:8001/ >/dev/null 2>&1; then\n _resolved_gw_url=\"http://${CTRL_CONTAINER}:8001\"\n env_args+=(-e AGENTTEAMS_AI_GATEWAY_ADMIN_URL=\"${_resolved_gw_url}\")\n fi\n\n # Verify Higress Console URL reachability (best-effort warning)\n if [ -n \"${_resolved_gw_url}\" ]; then\n if ! ${DOCKER_CMD} exec \"${CTRL_CONTAINER}\" curl -sf --max-time 3 \"${_resolved_gw_url}/\" >/dev/null 2>&1; then\n log \"WARNING: Higress Console URL may not be reachable: ${_resolved_gw_url}\"\n log \" Dashboard will still work, but shared login via Higress Console may fail.\"\n log \" To fix: verify the URL is correct and the Higress Console service is running.\"\n fi\n fi\n else\n log \"WARNING: ${CTRL_CONTAINER} not running; launching Dashboard without Controller/MinIO/LLM env.\"\n if [ -n \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\" ]; then\n local _gw_norm=\"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\"\n case \"${_gw_norm}\" in\n http://*|https://*) ;;\n *) _gw_norm=\"http://${_gw_norm}\" ;;\n esac\n env_args+=(-e AGENTTEAMS_AI_GATEWAY_ADMIN_URL=\"${_gw_norm}\")\n fi\n fi\n\n # Create persistent data volume (for future use)\n ${DOCKER_CMD} volume create agentteams-dashboard-data >/dev/null 2>&1 || true\n\n ${DOCKER_CMD} run -d \\\n --name \"${DASHBOARD_CONTAINER}\" \\\n --restart unless-stopped \\\n --network \"${AGENTTEAMS_NETWORK:-agentteams-net}\" \\\n --network-alias dashboard.agentteams.io \\\n -p \"${BIND_ADDR}:${AGENTTEAMS_PORT_DASHBOARD}:3000\" \\\n \"${env_args[@]}\" \\\n -v agentteams-dashboard-data:/app/db \\\n \"${AGENTTEAMS_DASHBOARD_IMAGE}\"\n\n # Wait for dashboard to be ready\n local max_wait=60\n local waited=0\n while [ $waited -lt $max_wait ]; do\n if curl -sf \"http://127.0.0.1:${AGENTTEAMS_PORT_DASHBOARD}/\" >/dev/null 2>&1; then\n log \"Dashboard ready on port ${AGENTTEAMS_PORT_DASHBOARD}\"\n return 0\n fi\n sleep 2\n waited=$((waited + 2))\n done\n log \"WARNING: Dashboard startup timed out after ${max_wait}s\"\n ${DOCKER_CMD} logs \"${DASHBOARD_CONTAINER}\" 2>&1 | tail -20\n return 0\n}\n\n# ============================================================\n# Manager Installation (Interactive)\n# ============================================================\n\ninstall_manager() {\n log \"$(msg install.title)\"\n log \"$(msg install.registry \"${AGENTTEAMS_REGISTRY}\")\"\n log \"\"\n log \"$(msg install.dir \"$(pwd)\")\"\n log \"$(msg install.dir_hint)\"\n log \"$(msg install.dir_hint2)\"\n log \"\"\n\n # Non-interactive fallback: resolve version immediately so image tags are available\n # before the step state machine runs. Interactive mode lets step_version handle it.\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_VERSION=\"${AGENTTEAMS_VERSION:-${AGENTTEAMS_KNOWN_STABLE_VERSION}}\"\n resolve_image_tags\n fi\n\n # Migrate legacy env file location before checks\n local existing_env=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n if [ ! -f \"${existing_env}\" ] && [ -f \"./agentteams-manager.env\" ]; then\n log \"Migrating agentteams-manager.env from current directory to ${existing_env}...\"\n mv \"./agentteams-manager.env\" \"${existing_env}\"\n fi\n\n # Orphan volume detection (only when no env file — step_existing handles the env-file case)\n if [ ! -f \"${existing_env}\" ]; then\n local data_vol=\"${AGENTTEAMS_DATA_DIR:-agentteams-data}\"\n if ${DOCKER_CMD} volume ls -q | grep -q \"^${data_vol}$\"; then\n echo \"\"\n log \"$(msg install.orphan_volume.detected \"${data_vol}\")\"\n log \"$(msg install.orphan_volume.warn)\"\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n log \"$(msg install.orphan_volume.clean_noninteractive)\"\n ${DOCKER_CMD} stop agentteams-manager 2>/dev/null || true\n ${DOCKER_CMD} rm agentteams-manager 2>/dev/null || true\n for w in $(${DOCKER_CMD} ps -a --format '{{.Names}}' | grep \"^agentteams-worker-\" || true); do\n ${DOCKER_CMD} stop \"${w}\" 2>/dev/null || true\n ${DOCKER_CMD} rm \"${w}\" 2>/dev/null || true\n done\n log \"$(msg install.orphan_volume.cleaning)\"\n ${DOCKER_CMD} volume rm \"${data_vol}\" 2>/dev/null || true\n log \"$(msg install.orphan_volume.cleaned)\"\n else\n echo \"\"\n echo \"$(msg install.orphan_volume.choose)\"\n echo \"$(msg install.orphan_volume.clean)\"\n echo \"$(msg install.orphan_volume.keep)\"\n echo \"\"\n local ORPHAN_CHOICE\n read -e -p \"$(msg install.orphan_volume.prompt): \" ORPHAN_CHOICE\n ORPHAN_CHOICE=\"${ORPHAN_CHOICE:-1}\"\n case \"${ORPHAN_CHOICE}\" in\n 1|clean)\n ${DOCKER_CMD} stop agentteams-manager 2>/dev/null || true\n ${DOCKER_CMD} rm agentteams-manager 2>/dev/null || true\n for w in $(${DOCKER_CMD} ps -a --format '{{.Names}}' | grep \"^agentteams-worker-\" || true); do\n ${DOCKER_CMD} stop \"${w}\" 2>/dev/null || true\n ${DOCKER_CMD} rm \"${w}\" 2>/dev/null || true\n done\n log \"$(msg install.orphan_volume.cleaning)\"\n ${DOCKER_CMD} volume rm \"${data_vol}\" 2>/dev/null || true\n log \"$(msg install.orphan_volume.cleaned)\"\n ;;\n 2|keep)\n log \"$(msg install.orphan_volume.keeping)\"\n ;;\n esac\n fi\n fi\n fi\n\n # ── State machine ─────────────────────────────────────────────────────────\n local _STEPS=( step_lang step_mode step_version step_existing step_llm step_manager_runtime step_runtime step_admin step_network\n step_ports step_domains step_github step_skills step_volume\n step_workspace step_dashboard step_e2ee step_docker_proxy step_idle step_hostshare step_podman_autostart )\n local _STEP_HISTORY=()\n local _step_idx=0\n while [ \"${_step_idx}\" -lt \"${#_STEPS[@]}\" ]; do\n local _step_fn=\"${_STEPS[$_step_idx]}\"\n if should_skip_step \"${_step_fn}\"; then\n _step_idx=$((_step_idx + 1))\n continue\n fi\n if [ \"${#_STEP_HISTORY[@]}\" -gt 0 ]; then\n log \"$(msg nav.back_hint)\"\n fi\n STEP_RESULT=\"\"\n \"${_step_fn}\"\n if [ \"${STEP_RESULT}\" = \"back\" ]; then\n if [ \"${#_STEP_HISTORY[@]}\" -gt 0 ]; then\n local _last=$(( ${#_STEP_HISTORY[@]} - 1 ))\n _step_idx=\"${_STEP_HISTORY[$_last]}\"\n _STEP_HISTORY=(\"${_STEP_HISTORY[@]:0:${_last}}\")\n clear_step_vars \"${_STEPS[$_step_idx]}\"\n fi\n # else: first step, ignore 'b'\n else\n _STEP_HISTORY+=(\"${_step_idx}\")\n _step_idx=$((_step_idx + 1))\n fi\n done\n # ── End state machine ──────────────────────────────────────────────────────\n\n # Post-machine defaults for any steps that were skipped\n AGENTTEAMS_DATA_DIR=\"${AGENTTEAMS_DATA_DIR:-agentteams-data}\"\n if [ -z \"${AGENTTEAMS_WORKSPACE_DIR+x}\" ] || [ -z \"${AGENTTEAMS_WORKSPACE_DIR}\" ]; then\n AGENTTEAMS_WORKSPACE_DIR=\"${HOME}/agentteams-manager\"\n export AGENTTEAMS_WORKSPACE_DIR\n fi\n AGENTTEAMS_WORKSPACE_DIR=\"$(cd \"${AGENTTEAMS_WORKSPACE_DIR}\" 2>/dev/null && pwd || echo \"${AGENTTEAMS_WORKSPACE_DIR}\")\"\n mkdir -p \"${AGENTTEAMS_WORKSPACE_DIR}\"\n AGENTTEAMS_MANAGER_RUNTIME=\"${AGENTTEAMS_MANAGER_RUNTIME:-copaw}\"\n export AGENTTEAMS_MANAGER_RUNTIME\n AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"${AGENTTEAMS_DEFAULT_WORKER_RUNTIME:-copaw}\"\n AGENTTEAMS_MATRIX_E2EE=\"${AGENTTEAMS_MATRIX_E2EE:-0}\"\n export AGENTTEAMS_MATRIX_E2EE\n AGENTTEAMS_WORKER_IDLE_TIMEOUT=\"${AGENTTEAMS_WORKER_IDLE_TIMEOUT:-720}\"\n export AGENTTEAMS_WORKER_IDLE_TIMEOUT\n AGENTTEAMS_HOST_SHARE_DIR=\"${AGENTTEAMS_HOST_SHARE_DIR:-$HOME}\"\n export AGENTTEAMS_HOST_SHARE_DIR\n\n log \"\"\n\n # Generate secrets (only if not already set)\n log \"$(msg install.generating_secrets)\"\n AGENTTEAMS_MANAGER_PASSWORD=\"${AGENTTEAMS_MANAGER_PASSWORD:-$(generate_key)}\"\n AGENTTEAMS_REGISTRATION_TOKEN=\"${AGENTTEAMS_REGISTRATION_TOKEN:-$(generate_key)}\"\n AGENTTEAMS_MINIO_USER=\"${AGENTTEAMS_MINIO_USER:-${AGENTTEAMS_ADMIN_USER}}\"\n AGENTTEAMS_MINIO_PASSWORD=\"${AGENTTEAMS_MINIO_PASSWORD:-${AGENTTEAMS_ADMIN_PASSWORD}}\"\n AGENTTEAMS_MANAGER_GATEWAY_KEY=\"${AGENTTEAMS_MANAGER_GATEWAY_KEY:-$(generate_key)}\"\n\n # Matrix AppService tokens — generate once during install/upgrade if not provided.\n # Persisted to env file so they survive controller restarts.\n AGENTTEAMS_MATRIX_APPSERVICE_ENABLED=\"${AGENTTEAMS_MATRIX_APPSERVICE_ENABLED:-true}\"\n export AGENTTEAMS_MATRIX_APPSERVICE_ENABLED\n if [ \"${AGENTTEAMS_MATRIX_APPSERVICE_ENABLED}\" != \"false\" ] && [ \"${AGENTTEAMS_MATRIX_APPSERVICE_ENABLED}\" != \"0\" ]; then\n if [ -z \"${AGENTTEAMS_MATRIX_APPSERVICE_AS_TOKEN}\" ]; then\n AGENTTEAMS_MATRIX_APPSERVICE_AS_TOKEN=\"$(openssl rand -hex 32)\"\n log \" Auto-generated AppService as_token (saved to env file)\"\n fi\n if [ -z \"${AGENTTEAMS_MATRIX_APPSERVICE_HS_TOKEN}\" ]; then\n AGENTTEAMS_MATRIX_APPSERVICE_HS_TOKEN=\"$(openssl rand -hex 32)\"\n log \" Auto-generated AppService hs_token (saved to env file)\"\n fi\n export AGENTTEAMS_MATRIX_APPSERVICE_AS_TOKEN\n export AGENTTEAMS_MATRIX_APPSERVICE_HS_TOKEN\n fi\n\n # Detect Apple Silicon (M1/M2/M3/M4) - need JVM fix for Higress Console\n # See: https://github.com/agentscope-ai/AgentTeams/issues/249\n if [ -z \"${JVM_ARGS:-}\" ] && [ \"$(uname -m)\" = \"arm64\" ] && [ \"$(uname -s)\" = \"Darwin\" ]; then\n log \"Apple Silicon detected - setting JVM_ARGS to fix Higress Console SIGILL issue\"\n JVM_ARGS=\"-XX:+UnlockDiagnosticVMOptions -XX:-UseAESCTRIntrinsics -XX:UseSVE=0\"\n fi\n\n # Write .env file\n ENV_FILE=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n cat > \"${ENV_FILE}\" << EOF\n# AgentTeams Manager Configuration\n# Generated by agentteams-install.sh on $(date)\n\n# Language\nAGENTTEAMS_LANGUAGE=${AGENTTEAMS_LANGUAGE}\n\n# LLM\nAGENTTEAMS_LLM_PROVIDER=${AGENTTEAMS_LLM_PROVIDER}\nAGENTTEAMS_DEFAULT_MODEL=${AGENTTEAMS_DEFAULT_MODEL}\nAGENTTEAMS_LLM_API_KEY=${AGENTTEAMS_LLM_API_KEY}\nAGENTTEAMS_OPENAI_BASE_URL=${AGENTTEAMS_OPENAI_BASE_URL:-}\nAGENTTEAMS_MODEL_CONTEXT_WINDOW=${AGENTTEAMS_MODEL_CONTEXT_WINDOW:-}\nAGENTTEAMS_MODEL_MAX_TOKENS=${AGENTTEAMS_MODEL_MAX_TOKENS:-}\nAGENTTEAMS_MODEL_REASONING=${AGENTTEAMS_MODEL_REASONING:-}\nAGENTTEAMS_MODEL_VISION=${AGENTTEAMS_MODEL_VISION:-}\n\n# Embedding model (empty = disabled, default: text-embedding-v4)\nAGENTTEAMS_EMBEDDING_MODEL=${AGENTTEAMS_EMBEDDING_MODEL}\n\n# Admin\nAGENTTEAMS_ADMIN_USER=${AGENTTEAMS_ADMIN_USER}\nAGENTTEAMS_ADMIN_PASSWORD=${AGENTTEAMS_ADMIN_PASSWORD}\n\n# Ports\nAGENTTEAMS_LOCAL_ONLY=${AGENTTEAMS_LOCAL_ONLY}\nAGENTTEAMS_PORT_GATEWAY=${AGENTTEAMS_PORT_GATEWAY}\nAGENTTEAMS_PORT_CONSOLE=${AGENTTEAMS_PORT_CONSOLE}\nAGENTTEAMS_PORT_ELEMENT_WEB=${AGENTTEAMS_PORT_ELEMENT_WEB}\nAGENTTEAMS_PORT_MANAGER_CONSOLE=${AGENTTEAMS_PORT_MANAGER_CONSOLE:-18888}\n\n# Manager runtime (openclaw | copaw)\nAGENTTEAMS_MANAGER_RUNTIME=${AGENTTEAMS_MANAGER_RUNTIME:-copaw}\n\n# Matrix\nAGENTTEAMS_MATRIX_DOMAIN=${AGENTTEAMS_MATRIX_DOMAIN}\nAGENTTEAMS_MATRIX_CLIENT_DOMAIN=${AGENTTEAMS_MATRIX_CLIENT_DOMAIN}\n\n# Gateway\nAGENTTEAMS_AI_GATEWAY_DOMAIN=${AGENTTEAMS_AI_GATEWAY_DOMAIN}\nAGENTTEAMS_MANAGER_GATEWAY_KEY=${AGENTTEAMS_MANAGER_GATEWAY_KEY}\n\n# File System\nAGENTTEAMS_FS_DOMAIN=${AGENTTEAMS_FS_DOMAIN}\nAGENTTEAMS_CONSOLE_DOMAIN=${AGENTTEAMS_CONSOLE_DOMAIN}\nAGENTTEAMS_MINIO_USER=${AGENTTEAMS_MINIO_USER}\nAGENTTEAMS_MINIO_PASSWORD=${AGENTTEAMS_MINIO_PASSWORD}\n\n# Internal\nAGENTTEAMS_MANAGER_PASSWORD=${AGENTTEAMS_MANAGER_PASSWORD}\nAGENTTEAMS_REGISTRATION_TOKEN=${AGENTTEAMS_REGISTRATION_TOKEN}\n\n# GitHub (optional)\nAGENTTEAMS_GITHUB_TOKEN=${AGENTTEAMS_GITHUB_TOKEN:-}\n\n# Nacos package import defaults\nAGENTTEAMS_NACOS_REGISTRY_URI=${AGENTTEAMS_NACOS_REGISTRY_URI:-nacos://market.agentteams.io:80/public}\nAGENTTEAMS_NACOS_USERNAME=${AGENTTEAMS_NACOS_USERNAME:-}\nAGENTTEAMS_NACOS_PASSWORD=${AGENTTEAMS_NACOS_PASSWORD:-}\nAGENTTEAMS_NACOS_TOKEN=${AGENTTEAMS_NACOS_TOKEN:-}\n\n# Skills Registry (optional, default: nacos://market.agentteams.io:80/public)\nAGENTTEAMS_SKILLS_API_URL=${AGENTTEAMS_SKILLS_API_URL:-nacos://market.agentteams.io:80/public}\n\n# OpenClaw CMS plugin (optional)\nAGENTTEAMS_CMS_TRACES_ENABLED=${AGENTTEAMS_CMS_TRACES_ENABLED:-false}\nAGENTTEAMS_CMS_ENDPOINT=${AGENTTEAMS_CMS_ENDPOINT:-}\nAGENTTEAMS_CMS_LICENSE_KEY=${AGENTTEAMS_CMS_LICENSE_KEY:-}\nAGENTTEAMS_CMS_PROJECT=${AGENTTEAMS_CMS_PROJECT:-}\nAGENTTEAMS_CMS_WORKSPACE=${AGENTTEAMS_CMS_WORKSPACE:-}\nAGENTTEAMS_CMS_SERVICE_NAME=${AGENTTEAMS_CMS_SERVICE_NAME:-agentteams-manager}\nAGENTTEAMS_CMS_METRICS_ENABLED=${AGENTTEAMS_CMS_METRICS_ENABLED:-false}\n\n# Worker images (for direct container creation)\nAGENTTEAMS_WORKER_IMAGE=${WORKER_IMAGE}\nAGENTTEAMS_COPAW_WORKER_IMAGE=${COPAW_WORKER_IMAGE}\nAGENTTEAMS_QWENPAW_WORKER_IMAGE=${QWENPAW_WORKER_IMAGE}\nAGENTTEAMS_HERMES_WORKER_IMAGE=${HERMES_WORKER_IMAGE}\n\n# Default Worker runtime (openclaw | copaw | qwenpaw | hermes)\nAGENTTEAMS_DEFAULT_WORKER_RUNTIME=${AGENTTEAMS_DEFAULT_WORKER_RUNTIME:-copaw}\n\n# Matrix E2EE (0=disabled, 1=enabled; default: 0)\nAGENTTEAMS_MATRIX_E2EE=${AGENTTEAMS_MATRIX_E2EE:-0}\n\n# Matrix AppService\nAGENTTEAMS_MATRIX_APPSERVICE_ENABLED=${AGENTTEAMS_MATRIX_APPSERVICE_ENABLED:-true}\nAGENTTEAMS_MATRIX_APPSERVICE_AS_TOKEN=${AGENTTEAMS_MATRIX_APPSERVICE_AS_TOKEN:-}\nAGENTTEAMS_MATRIX_APPSERVICE_HS_TOKEN=${AGENTTEAMS_MATRIX_APPSERVICE_HS_TOKEN:-}\n\n# Docker API proxy (0=disabled, 1=enabled; default: 1)\nAGENTTEAMS_DOCKER_PROXY=${AGENTTEAMS_DOCKER_PROXY:-1}\n\n# Docker API proxy: additional allowed image sources (comma-separated)\nAGENTTEAMS_PROXY_ALLOWED_REGISTRIES=${AGENTTEAMS_PROXY_ALLOWED_REGISTRIES:-}\n\n# Worker idle timeout in minutes (default: 720 = 12 hours)\nAGENTTEAMS_WORKER_IDLE_TIMEOUT=${AGENTTEAMS_WORKER_IDLE_TIMEOUT:-720}\n\n# Podman autostart via systemd (0=disabled, 1=enabled)\nAGENTTEAMS_PODMAN_AUTOSTART=${AGENTTEAMS_PODMAN_AUTOSTART:-0}\n\n# JVM Args for Higress Console (fixes SIGILL on Apple Silicon)\nJVM_ARGS=${JVM_ARGS:-}\n\n# Higress WASM plugin image registry (auto-selected by timezone)\nHIGRESS_ADMIN_WASM_PLUGIN_IMAGE_REGISTRY=${AGENTTEAMS_REGISTRY}\n\n# Data persistence\nAGENTTEAMS_DATA_DIR=${AGENTTEAMS_DATA_DIR:-agentteams-data}\n# Manager workspace (skills, memory, state — host-editable)\nAGENTTEAMS_WORKSPACE_DIR=${AGENTTEAMS_WORKSPACE_DIR:-}\n# Host directory sharing\nAGENTTEAMS_HOST_SHARE_DIR=${AGENTTEAMS_HOST_SHARE_DIR:-}\n\n# agentteams-dashboard (management UI)\nAGENTTEAMS_DASHBOARD=${AGENTTEAMS_DASHBOARD:-1}\nAGENTTEAMS_DASHBOARD_VERSION=${AGENTTEAMS_DASHBOARD_VERSION:-v1.2.0-beta.2}\nAGENTTEAMS_PORT_DASHBOARD=${AGENTTEAMS_PORT_DASHBOARD:-13000}\nAGENTTEAMS_DASHBOARD_IMAGE=${AGENTTEAMS_DASHBOARD_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-dashboard:${AGENTTEAMS_DASHBOARD_VERSION}}\nAGENTTEAMS_AI_GATEWAY_ADMIN_URL=${AGENTTEAMS_AI_GATEWAY_ADMIN_URL:-}\nEOF\n\n chmod 600 \"${ENV_FILE}\"\n log \"$(msg install.config_saved \"${ENV_FILE}\")\"\n\n # Detect container runtime socket\n SOCKET_MOUNT_ARGS=\"\"\n if [ \"${AGENTTEAMS_MOUNT_SOCKET}\" = \"1\" ]; then\n # Actively ensure the API socket is enabled and active before detection.\n if [ \"${DOCKER_CMD:-}\" = \"podman\" ]; then\n ensure_podman_socket\n fi\n CONTAINER_SOCK=$(detect_socket)\n if [ -n \"${CONTAINER_SOCK}\" ]; then\n log \"$(msg install.socket_detected \"${CONTAINER_SOCK}\")\"\n SOCKET_MOUNT_ARGS=\"-v ${CONTAINER_SOCK}:/var/run/docker.sock --security-opt label=disable\"\n else\n log \"$(msg install.socket_not_found)\"\n # Interactive confirmation when socket not found\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" != \"1\" ]; then\n echo \"\"\n echo -e \"\\033[33m$(msg install.socket_confirm.title)\\033[0m\"\n echo \"\"\n echo -e \"$(msg install.socket_confirm.message)\"\n echo \"\"\n read -p \"$(msg install.socket_confirm.prompt)\" SOCKET_CONFIRM\n if [ \"${SOCKET_CONFIRM}\" != \"y\" ] && [ \"${SOCKET_CONFIRM}\" != \"Y\" ]; then\n log \"$(msg install.socket_confirm.cancelled)\"\n exit 0\n fi\n fi\n fi\n fi\n\n # Create the data volume if it doesn't already exist (reuse on reinstall)\n if ! ${DOCKER_CMD} volume ls -q | grep -q \"^${AGENTTEAMS_DATA_DIR}$\"; then\n ${DOCKER_CMD} volume create \"${AGENTTEAMS_DATA_DIR}\" > /dev/null\n fi\n\n # Data mount: Docker volume\n DATA_MOUNT_ARGS=\"-v ${AGENTTEAMS_DATA_DIR}:/data\"\n\n # Manager workspace mount (always a host directory, defaulting to ~/agentteams-manager)\n WORKSPACE_MOUNT_ARGS=\"-v ${AGENTTEAMS_WORKSPACE_DIR}:/root/manager-workspace\"\n\n # Pass host timezone to container so date/time commands reflect local time\n TZ_ARGS=\"-e TZ=${AGENTTEAMS_TIMEZONE}\"\n\n # Host directory mount\n if [ -d \"${AGENTTEAMS_HOST_SHARE_DIR}\" ]; then\n HOST_SHARE_MOUNT_ARGS=\"-v ${AGENTTEAMS_HOST_SHARE_DIR}:/host-share\"\n log \"$(msg host_share.sharing \"${AGENTTEAMS_HOST_SHARE_DIR}\")\"\n else\n log \"$(msg host_share.not_exist \"${AGENTTEAMS_HOST_SHARE_DIR}\")\"\n HOST_SHARE_MOUNT_ARGS=\"-v ${AGENTTEAMS_HOST_SHARE_DIR}:/host-share\"\n fi\n\n # YOLO mode: pass through if set in environment (enables autonomous decisions)\n YOLO_ARGS=\"\"\n if [ \"${AGENTTEAMS_YOLO:-}\" = \"1\" ]; then\n YOLO_ARGS=\"-e AGENTTEAMS_YOLO=1\"\n log \"$(msg install.yolo)\"\n fi\n\n # Matrix-plugin debug tracing: pass through if AGENTTEAMS_MATRIX_DEBUG=1.\n # The container entrypoints translate this to OPENCLAW_MATRIX_DEBUG=1\n # so the openclaw matrix plugin emits structured INFO-level lifecycle\n # traces (sync state transitions, room.invite/join, message handler\n # arrival + filter outcomes). Used to diagnose worker/manager hangs.\n MATRIX_DEBUG_ARGS=\"\"\n if [ \"${AGENTTEAMS_MATRIX_DEBUG:-}\" = \"1\" ]; then\n MATRIX_DEBUG_ARGS=\"-e AGENTTEAMS_MATRIX_DEBUG=1\"\n fi\n\n # E2EE is already in the env file; but also pass explicitly in case env file is not the source\n # (AGENTTEAMS_MATRIX_E2EE is already written to ENV_FILE above via --env-file)\n\n # Pull images (manager based on runtime config; all worker runtimes always pulled)\n _is_local_image() {\n case \"$1\" in\n agentteams/*) return 0 ;;\n *) return 1 ;;\n esac\n }\n\n _local_image_exists() {\n local _img=\"$1\"\n [ -z \"${_img}\" ] && return 1\n _is_local_image \"${_img}\" || return 1\n ${DOCKER_CMD} image inspect \"${_img}\" >/dev/null 2>&1\n }\n\n # Helper: pull or skip a single image\n # Args: $1=image $2=exists_msg_key $3=pulling_msg_key\n _pull_image() {\n local _img=\"$1\" _exists_key=\"$2\" _pull_key=\"$3\"\n [ -z \"${_img}\" ] && return 0\n if _local_image_exists \"${_img}\"; then\n log \"$(msg \"${_exists_key}\" \"${_img}\")\"\n return 0\n fi\n log \"$(msg \"${_pull_key}\" \"${_img}\")\"\n local _attempt=1\n while [ $_attempt -le 3 ]; do\n if ${DOCKER_CMD} pull \"${_img}\"; then\n return 0\n fi\n if [ $_attempt -lt 3 ]; then\n log \"Pull failed (attempt ${_attempt}/3), retrying in 5s...\"\n sleep 5\n fi\n _attempt=$((_attempt + 1))\n done\n die \"Failed to pull ${_img} after 3 attempts\"\n return 1\n }\n\n # Embedded controller image (resolve versioned tag, fallback to latest)\n resolve_embedded_image\n if [ \"${AGENTTEAMS_USE_EMBEDDED}\" = \"1\" ]; then\n _local_image_exists \"${EMBEDDED_IMAGE}\" || true\n fi\n\n # Manager image is always required (select based on runtime)\n if [ \"${AGENTTEAMS_MANAGER_RUNTIME}\" = \"copaw\" ]; then\n _pull_image \"${MANAGER_COPAW_IMAGE}\" \"install.image.exists\" \"install.image.pulling_manager\"\n else\n _pull_image \"${MANAGER_IMAGE}\" \"install.image.exists\" \"install.image.pulling_manager\"\n fi\n\n # Pull all worker runtime images (workers may use any runtime regardless of the default)\n _pull_image \"${WORKER_IMAGE}\" \"install.image.worker_exists\" \"install.image.pulling_worker\"\n _pull_image \"${COPAW_WORKER_IMAGE}\" \"install.image.worker_exists\" \"install.image.pulling_worker\"\n # Temporarily disabled until the QwenPaw worker image is published.\n # _pull_image \"${QWENPAW_WORKER_IMAGE}\" \"install.image.worker_exists\" \"install.image.pulling_worker\"\n _pull_image \"${HERMES_WORKER_IMAGE}\" \"install.image.worker_exists\" \"install.image.pulling_worker\"\n\n # --- Pre-upgrade: extract Matrix passwords from running old containers ---\n # Only needed when upgrading FROM old architecture (v1.0.9) TO embedded.\n # For new-arch-to-new-arch upgrades, credential files already exist with\n # correct room IDs — we must NOT overwrite them.\n _creds_tmp=\"\"\n if [ \"${AGENTTEAMS_UPGRADE:-0}\" = \"1\" ] && [ \"${AGENTTEAMS_USE_EMBEDDED}\" = \"1\" ]; then\n # Detect if upgrading from old arch: old arch has no agentteams-controller container\n # (or has it as a docker-proxy only, not embedded). Check if the existing\n # agentteams-controller is an embedded image (has supervisord) or just a proxy.\n _is_old_arch=0\n if ! ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -q \"^agentteams-controller$\"; then\n _is_old_arch=1\n elif ${DOCKER_CMD} ps -a --format '{{.Names}} {{.Image}}' 2>/dev/null | grep \"^agentteams-controller \" | grep -qv \"embedded\"; then\n _is_old_arch=1\n fi\n\n if [ \"${_is_old_arch}\" = \"1\" ]; then\n _creds_tmp=$(mktemp -d)\n\n # docker exec for Matrix/minio paths only works while agentteams-manager is running.\n _mgr_creds_tempstart=0\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -q '^agentteams-manager$'; then\n if ! ${DOCKER_CMD} ps --format '{{.Names}}' 2>/dev/null | grep -q '^agentteams-manager$'; then\n log \"agentteams-manager is stopped; starting it temporarily to extract Matrix credentials for upgrade...\"\n ${DOCKER_CMD} start agentteams-manager 2>/dev/null || true\n wait_matrix_ready \"agentteams-manager\"\n _mgr_creds_tempstart=1\n fi\n fi\n\n # Manager password (container Config.Env, then secrets file inside running manager, then data volume)\n _mgr_pw=$(${DOCKER_CMD} inspect agentteams-manager --format '{{range .Config.Env}}{{println .}}{{end}}' 2>/dev/null | grep '^AGENTTEAMS_MANAGER_PASSWORD=' | cut -d= -f2-)\n if [ -z \"${_mgr_pw}\" ] && ${DOCKER_CMD} ps --format '{{.Names}}' 2>/dev/null | grep -q '^agentteams-manager$'; then\n _mgr_pw=$(${DOCKER_CMD} exec agentteams-manager bash -c 'source /data/agentteams-secrets.env 2>/dev/null && echo \"${AGENTTEAMS_MANAGER_PASSWORD}\"' 2>/dev/null)\n fi\n if [ -z \"${_mgr_pw}\" ] && ${DOCKER_CMD} volume ls -q 2>/dev/null | grep -q \"^${AGENTTEAMS_DATA_DIR}$\"; then\n _mgr_pw=$(agentteams_read_secret_from_data_volume \"${AGENTTEAMS_DATA_DIR}\" AGENTTEAMS_MANAGER_PASSWORD)\n fi\n\n # Manager admin DM room ID: login as admin, find DM room with @manager\n _mgr_room=\"\"\n if [ -n \"${_mgr_pw}\" ]; then\n _admin_pw=$(grep AGENTTEAMS_ADMIN_PASSWORD \"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\" 2>/dev/null | cut -d= -f2-)\n _admin_user=$(grep AGENTTEAMS_ADMIN_USER \"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\" 2>/dev/null | cut -d= -f2-)\n _admin_user=\"${_admin_user:-admin}\"\n _matrix_domain=$(grep AGENTTEAMS_MATRIX_DOMAIN \"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\" 2>/dev/null | cut -d= -f2-)\n if [ -n \"${_admin_pw}\" ] && ${DOCKER_CMD} ps --format '{{.Names}}' 2>/dev/null | grep -q '^agentteams-manager$'; then\n _admin_token=$(${DOCKER_CMD} exec agentteams-manager curl -sf -X POST http://127.0.0.1:6167/_matrix/client/v3/login \\\n -H \"Content-Type: application/json\" \\\n -d '{\"type\":\"m.login.password\",\"identifier\":{\"type\":\"m.id.user\",\"user\":\"'\"${_admin_user}\"'\"},\"password\":\"'\"${_admin_pw}\"'\"}' 2>/dev/null | python3 -c \"import sys,json; print(json.load(sys.stdin).get('access_token',''))\" 2>/dev/null || true)\n if [ -n \"${_admin_token}\" ]; then\n _mgr_room=$(${DOCKER_CMD} exec agentteams-manager curl -sf -X GET \\\n -H \"Authorization: Bearer ${_admin_token}\" \\\n \"http://127.0.0.1:6167/_matrix/client/v3/joined_rooms\" 2>/dev/null | python3 -c \"\nimport sys,json,subprocess\nrooms = json.load(sys.stdin).get('joined_rooms',[])\nfor room_id in rooms:\n enc = room_id.replace('!','%21')\n members = json.loads(subprocess.check_output([\n 'docker','exec','agentteams-manager','curl','-sf','-X','GET',\n '-H','Authorization: Bearer ${_admin_token}',\n 'http://127.0.0.1:6167/_matrix/client/v3/rooms/'+enc+'/members'\n ]).decode()).get('chunk',[])\n member_ids = [m['state_key'] for m in members]\n if any('manager' in m and 'admin' not in m.split(':')[0] for m in member_ids):\n if len(member_ids) <= 3:\n print(room_id)\n break\n\" 2>/dev/null || true)\n fi\n fi\n if [ -z \"${_mgr_room}\" ]; then\n _mgr_room=$(agentteams_read_admin_dm_room_from_workspace \"${AGENTTEAMS_WORKSPACE_DIR}\")\n fi\n cat > \"${_creds_tmp}/default.env\" <<CREDEOF\nWORKER_PASSWORD=\"${_mgr_pw}\"\nWORKER_MINIO_PASSWORD=\"$(openssl rand -hex 24)\"\nWORKER_GATEWAY_KEY=\"${AGENTTEAMS_MANAGER_GATEWAY_KEY}\"\nWORKER_ROOM_ID=\"${_mgr_room}\"\nCREDEOF\n log \"Extracted Manager Matrix password${_mgr_room:+ and room ID}\"\n fi\n\n # Worker passwords and room IDs from workers-registry.json\n if [ -f \"${AGENTTEAMS_WORKSPACE_DIR}/workers-registry.json\" ]; then\n _worker_names=$(python3 -c \"import json; d=json.load(open('${AGENTTEAMS_WORKSPACE_DIR}/workers-registry.json')); print(' '.join(d.get('workers',{}).keys()))\" 2>/dev/null || true)\n for _wname in ${_worker_names}; do\n _wpw=\"\"\n if ${DOCKER_CMD} ps --format '{{.Names}}' 2>/dev/null | grep -q '^agentteams-manager$'; then\n _wpw=$(${DOCKER_CMD} exec agentteams-manager cat \"/root/agentteams-fs/agents/${_wname}/credentials/matrix/password\" 2>/dev/null || true)\n fi\n if [ -z \"${_wpw}\" ] && ${DOCKER_CMD} volume ls -q 2>/dev/null | grep -q \"^${AGENTTEAMS_DATA_DIR}$\"; then\n _wpw=$(agentteams_read_worker_creds_value_from_volume \"${AGENTTEAMS_DATA_DIR}\" \"${_wname}\" WORKER_PASSWORD)\n fi\n _wroom=$(python3 -c \"import json; d=json.load(open('${AGENTTEAMS_WORKSPACE_DIR}/workers-registry.json')); print(d.get('workers',{}).get('${_wname}',{}).get('room_id',''))\" 2>/dev/null || true)\n if [ -z \"${_wroom}\" ] && ${DOCKER_CMD} volume ls -q 2>/dev/null | grep -q \"^${AGENTTEAMS_DATA_DIR}$\"; then\n _wroom=$(agentteams_read_worker_creds_value_from_volume \"${AGENTTEAMS_DATA_DIR}\" \"${_wname}\" WORKER_ROOM_ID)\n fi\n if [ -n \"${_wpw}\" ]; then\n cat > \"${_creds_tmp}/${_wname}.env\" <<CREDEOF\nWORKER_PASSWORD=\"${_wpw}\"\nWORKER_MINIO_PASSWORD=\"$(openssl rand -hex 24)\"\nWORKER_GATEWAY_KEY=\"$(openssl rand -hex 32)\"\nWORKER_ROOM_ID=\"${_wroom}\"\nCREDEOF\n log \"Extracted ${_wname} Matrix password${_wroom:+ and room ID}\"\n fi\n done\n fi\n\n if [ \"${_mgr_creds_tempstart}\" = \"1\" ]; then\n log \"Stopping agentteams-manager after credential extraction (upgrade will recreate containers)...\"\n ${DOCKER_CMD} stop agentteams-manager 2>/dev/null || true\n fi\n fi # _is_old_arch\n fi\n\n # --- Stop and remove existing containers ---\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' | grep -q \"^agentteams-controller$\"; then\n ${DOCKER_CMD} stop agentteams-controller 2>/dev/null || true\n ${DOCKER_CMD} rm agentteams-controller 2>/dev/null || true\n fi\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' | grep -q \"^agentteams-manager$\"; then\n log \"$(msg install.removing_existing)\"\n ${DOCKER_CMD} stop agentteams-manager 2>/dev/null || true\n ${DOCKER_CMD} rm agentteams-manager 2>/dev/null || true\n fi\n\n # Stop and remove worker containers (controller will recreate via CR reconciliation)\n if [ -n \"${UPGRADE_EXISTING_WORKERS:-}\" ]; then\n log \"$(msg install.existing.stopping_workers)\"\n for w in ${UPGRADE_EXISTING_WORKERS}; do\n ${DOCKER_CMD} stop \"${w}\" 2>/dev/null || true\n ${DOCKER_CMD} rm \"${w}\" 2>/dev/null || true\n log \"$(msg install.existing.removed \"${w}\")\"\n done\n fi\n\n # Clean up legacy containers from v1.0.x and earlier\n for _legacy_name in agentteams-docker-proxy; do\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -qx \"${_legacy_name}\"; then\n log \"Removing legacy container: ${_legacy_name}\"\n ${DOCKER_CMD} stop \"${_legacy_name}\" 2>/dev/null || true\n ${DOCKER_CMD} rm -f \"${_legacy_name}\" 2>/dev/null || true\n fi\n done\n\n # --- Upgrade: inject extracted credentials into data volume ---\n # Only needed for old-arch upgrades (credential files were extracted above).\n if [ -n \"${_creds_tmp}\" ] && [ -d \"${_creds_tmp}\" ] && [ -n \"$(ls -A \"${_creds_tmp}\" 2>/dev/null)\" ]; then\n local _cleanup_ctr=\"agentteams-upgrade-cleanup\"\n ${DOCKER_CMD} rm -f \"${_cleanup_ctr}\" 2>/dev/null || true\n ${DOCKER_CMD} run --rm --name \"${_cleanup_ctr}\" \\\n --entrypoint sh \\\n -v \"${AGENTTEAMS_DATA_DIR}:/data\" \\\n -v \"${_creds_tmp}:/creds:ro\" \\\n \"${EMBEDDED_IMAGE}\" -c '\n rm -rf /data/worker-creds\n mkdir -p /data/worker-creds\n cp /creds/*.env /data/worker-creds/ 2>/dev/null || true\n chmod 600 /data/worker-creds/*.env 2>/dev/null || true\n ' 2>/dev/null && log \"Injected credentials for upgrade\" || log \"Warning: credential injection failed, continuing\"\n rm -rf \"${_creds_tmp}\"\n fi\n\n # --- Start containers ---\n log \"$(msg install.starting_manager)\"\n\n # Build port binding args\n if [ \"${AGENTTEAMS_LOCAL_ONLY:-1}\" = \"1\" ]; then\n _port_prefix=\"127.0.0.1:\"\n else\n _port_prefix=\"\"\n fi\n\n # Ensure agentteams-net Docker network exists\n ${DOCKER_CMD} network inspect agentteams-net >/dev/null 2>&1 || ${DOCKER_CMD} network create agentteams-net\n\n if [ \"${AGENTTEAMS_USE_EMBEDDED}\" != \"1\" ] && [ \"${AGENTTEAMS_UPGRADE:-0}\" = \"1\" ]; then\n # Check if current installation is embedded — downgrade to legacy is not supported\n if ${DOCKER_CMD} ps -a --format '{{.Names}} {{.Image}}' 2>/dev/null | grep \"^agentteams-controller \" | grep -q \"embedded\"; then\n error \"Downgrade from embedded architecture to legacy version (${AGENTTEAMS_VERSION}) is not supported.\"\n error \"Please use 'make uninstall-embedded' first, then do a clean install of the target version.\"\n exit 1\n fi\n fi\n\n if [ \"${AGENTTEAMS_USE_EMBEDDED}\" = \"1\" ]; then\n # ============================================================\n # New architecture: embedded controller + auto-created manager\n # ============================================================\n\n # Internal port: 8080 (Higress gateway inside the container).\n local _internal_gw_port=8080\n local _matrix_domain=\"${AGENTTEAMS_MATRIX_DOMAIN:-matrix-local.agentteams.io:${AGENTTEAMS_PORT_GATEWAY}}\"\n local _aigw_domain=\"${AGENTTEAMS_AI_GATEWAY_DOMAIN:-aigw-local.agentteams.io}\"\n # Ensure internal gateway port is present (container-internal traffic uses 8080)\n case \"${_aigw_domain}\" in *:*) ;; *) _aigw_domain=\"${_aigw_domain}:${_internal_gw_port}\" ;; esac\n local _fs_domain=\"${AGENTTEAMS_FS_DOMAIN:-fs-local.agentteams.io}\"\n case \"${_fs_domain}\" in *:*) ;; *) _fs_domain=\"${_fs_domain}:${_internal_gw_port}\" ;; esac\n\n # Controller env args\n local _ctrl_env_prefix\n _ctrl_env_prefix=\"$(_controller_env_prefix \"${AGENTTEAMS_VERSION}\")\"\n local _storage_prefix\n _storage_prefix=\"$(_controller_storage_prefix \"${AGENTTEAMS_VERSION}\")\"\n local _ctrl_env_args=(\n -e \"${_ctrl_env_prefix}ADMIN_USER=${AGENTTEAMS_ADMIN_USER}\"\n -e \"${_ctrl_env_prefix}ADMIN_PASSWORD=${AGENTTEAMS_ADMIN_PASSWORD}\"\n -e \"${_ctrl_env_prefix}MANAGER_PASSWORD=${AGENTTEAMS_MANAGER_PASSWORD}\"\n -e \"${_ctrl_env_prefix}REGISTRATION_TOKEN=${AGENTTEAMS_REGISTRATION_TOKEN}\"\n -e \"${_ctrl_env_prefix}MINIO_USER=${AGENTTEAMS_MINIO_USER}\"\n -e \"${_ctrl_env_prefix}MINIO_PASSWORD=${AGENTTEAMS_MINIO_PASSWORD}\"\n -e \"${_ctrl_env_prefix}LLM_PROVIDER=${AGENTTEAMS_LLM_PROVIDER}\"\n -e \"${_ctrl_env_prefix}LLM_API_KEY=${AGENTTEAMS_LLM_API_KEY}\"\n -e \"${_ctrl_env_prefix}DEFAULT_MODEL=${AGENTTEAMS_DEFAULT_MODEL}\"\n -e \"${_ctrl_env_prefix}MANAGER_GATEWAY_KEY=${AGENTTEAMS_MANAGER_GATEWAY_KEY}\"\n -e \"${_ctrl_env_prefix}MANAGER_RUNTIME=${AGENTTEAMS_MANAGER_RUNTIME:-copaw}\"\n -e \"${_ctrl_env_prefix}MANAGER_IMAGE=$([ \"${AGENTTEAMS_MANAGER_RUNTIME}\" = \"copaw\" ] && echo \"${MANAGER_COPAW_IMAGE}\" || echo \"${MANAGER_IMAGE}\")\"\n -e \"${_ctrl_env_prefix}DEFAULT_WORKER_RUNTIME=${AGENTTEAMS_DEFAULT_WORKER_RUNTIME:-copaw}\"\n -e \"${_ctrl_env_prefix}WORKER_IMAGE=${WORKER_IMAGE}\"\n -e \"${_ctrl_env_prefix}COPAW_WORKER_IMAGE=${COPAW_WORKER_IMAGE}\"\n -e \"${_ctrl_env_prefix}QWENPAW_WORKER_IMAGE=${QWENPAW_WORKER_IMAGE}\"\n -e \"${_ctrl_env_prefix}HERMES_WORKER_IMAGE=${HERMES_WORKER_IMAGE}\"\n -e \"${_ctrl_env_prefix}MATRIX_DOMAIN=${_matrix_domain}\"\n -e \"${_ctrl_env_prefix}ELEMENT_HOMESERVER_URL=http://127.0.0.1:${AGENTTEAMS_PORT_GATEWAY}\"\n -e \"${_ctrl_env_prefix}MATRIX_URL=http://127.0.0.1:6167\"\n -e \"${_ctrl_env_prefix}MATRIX_E2EE=${AGENTTEAMS_MATRIX_E2EE:-0}\"\n -e \"${_ctrl_env_prefix}MINIO_ENDPOINT=http://127.0.0.1:9000\"\n -e \"${_ctrl_env_prefix}MINIO_BUCKET=agentteams-storage\"\n -e \"${_ctrl_env_prefix}STORAGE_PREFIX=${_storage_prefix}\"\n -e \"${_ctrl_env_prefix}FS_ENDPOINT=http://127.0.0.1:9000\"\n -e \"${_ctrl_env_prefix}AI_GATEWAY_URL=http://${_aigw_domain}\"\n -e \"${_ctrl_env_prefix}CONTROLLER_URL=http://agentteams-controller:8090\"\n -e \"${_ctrl_env_prefix}DOCKER_NETWORK=agentteams-net\"\n -e \"${_ctrl_env_prefix}WORKSPACE_DIR=${AGENTTEAMS_WORKSPACE_DIR}\"\n -e \"${_ctrl_env_prefix}HOST_SHARE_DIR=${AGENTTEAMS_HOST_SHARE_DIR}\"\n -e \"${_ctrl_env_prefix}MANAGER_ENABLED=true\"\n -e \"${_ctrl_env_prefix}PORT_MANAGER_CONSOLE=${AGENTTEAMS_PORT_MANAGER_CONSOLE:-18888}\"\n )\n if _use_legacy_image_env \"${AGENTTEAMS_VERSION}\"; then\n _ctrl_env_args+=(\n -e \"${_ctrl_env_prefix}FS_BUCKET=agentteams-storage\"\n -e \"${_ctrl_env_prefix}RESOURCE_PREFIX=agentteams-\"\n )\n else\n _ctrl_env_args+=(\n -e \"${_ctrl_env_prefix}MATRIX_APPSERVICE_ENABLED=${AGENTTEAMS_MATRIX_APPSERVICE_ENABLED:-true}\"\n -e \"${_ctrl_env_prefix}MATRIX_APPSERVICE_AS_TOKEN=${AGENTTEAMS_MATRIX_APPSERVICE_AS_TOKEN:-}\"\n -e \"${_ctrl_env_prefix}MATRIX_APPSERVICE_HS_TOKEN=${AGENTTEAMS_MATRIX_APPSERVICE_HS_TOKEN:-}\"\n )\n fi\n\n # Timezone\n if [ -n \"${AGENTTEAMS_TIMEZONE:-}\" ]; then\n _ctrl_env_args+=(-e \"TZ=${AGENTTEAMS_TIMEZONE}\")\n fi\n\n # Yolo mode\n if [ \"${AGENTTEAMS_YOLO:-}\" = \"1\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}YOLO=1\")\n fi\n\n # Matrix-plugin debug tracing — propagated to every manager + worker\n # the controller spawns, then translated to OPENCLAW_MATRIX_DEBUG=1\n # by the container entrypoints. Use this to diagnose\n # \"worker did not join\" / \"manager replied empty\" hangs.\n if [ \"${AGENTTEAMS_MATRIX_DEBUG:-}\" = \"1\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}MATRIX_DEBUG=1\")\n fi\n\n # Optional: GitHub token\n if [ -n \"${AGENTTEAMS_GITHUB_TOKEN:-}\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}GITHUB_TOKEN=${AGENTTEAMS_GITHUB_TOKEN}\")\n fi\n\n # Optional: embedding model\n if [ -n \"${AGENTTEAMS_EMBEDDING_MODEL:-}\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}EMBEDDING_MODEL=${AGENTTEAMS_EMBEDDING_MODEL}\")\n fi\n\n # Optional: OpenAI-compatible base URL\n if [ -n \"${AGENTTEAMS_OPENAI_BASE_URL:-}\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}OPENAI_BASE_URL=${AGENTTEAMS_OPENAI_BASE_URL}\")\n fi\n # Optional: language\n if [ -n \"${AGENTTEAMS_LANGUAGE:-}\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}LANGUAGE=${AGENTTEAMS_LANGUAGE}\")\n fi\n\n # Optional: CMS/ARMS observability. In embedded mode the controller\n # spawns the Manager and Workers, so it must receive these settings.\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}CMS_TRACES_ENABLED=${AGENTTEAMS_CMS_TRACES_ENABLED:-false}\")\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}CMS_SERVICE_NAME=${AGENTTEAMS_CMS_SERVICE_NAME:-agentteams-manager}\")\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}CMS_METRICS_ENABLED=${AGENTTEAMS_CMS_METRICS_ENABLED:-false}\")\n if [ -n \"${AGENTTEAMS_CMS_ENDPOINT:-}\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}CMS_ENDPOINT=${AGENTTEAMS_CMS_ENDPOINT}\")\n fi\n if [ -n \"${AGENTTEAMS_CMS_LICENSE_KEY:-}\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}CMS_LICENSE_KEY=${AGENTTEAMS_CMS_LICENSE_KEY}\")\n fi\n if [ -n \"${AGENTTEAMS_CMS_PROJECT:-}\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}CMS_PROJECT=${AGENTTEAMS_CMS_PROJECT}\")\n fi\n if [ -n \"${AGENTTEAMS_CMS_WORKSPACE:-}\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}CMS_WORKSPACE=${AGENTTEAMS_CMS_WORKSPACE}\")\n fi\n\n # shellcheck disable=SC2086\n ${DOCKER_CMD} run -d \\\n --name agentteams-controller \\\n --network agentteams-net \\\n --network-alias matrix-local.agentteams.io \\\n --network-alias aigw-local.agentteams.io \\\n --network-alias fs-local.agentteams.io \\\n \"${_ctrl_env_args[@]}\" \\\n -v \"${CONTAINER_SOCK}:/var/run/docker.sock\" \\\n --security-opt label=disable \\\n -v \"${AGENTTEAMS_DATA_DIR}:/data\" \\\n -v \"${AGENTTEAMS_WORKSPACE_DIR}:/root/agentteams-fs/agents/manager\" \\\n -p \"${_port_prefix}${AGENTTEAMS_PORT_GATEWAY}:8080\" \\\n -p \"${_port_prefix}${AGENTTEAMS_PORT_CONSOLE}:8001\" \\\n -p \"${_port_prefix}${AGENTTEAMS_PORT_ELEMENT_WEB:-18088}:8088\" \\\n --restart unless-stopped \\\n \"${EMBEDDED_IMAGE}\"\n\n log \"Embedded controller started: agentteams-controller\"\n\n # Wait for infrastructure inside the controller container\n _wait_for_url() {\n local url=\"$1\" ctr=\"$2\" max_wait=\"${3:-120}\" desc=\"${4:-service}\"\n local elapsed=0\n log \"Waiting for ${desc}...\"\n while [ $elapsed -lt $max_wait ]; do\n if ${DOCKER_CMD} exec \"${ctr}\" curl -sf \"${url}\" >/dev/null 2>&1; then\n log \"${desc} is ready (${elapsed}s)\"\n return 0\n fi\n sleep 2\n elapsed=$((elapsed + 2))\n done\n log \"ERROR: ${desc} not ready after ${max_wait}s\"\n return 1\n }\n\n _wait_for_url \"http://127.0.0.1:6167/_tuwunel/server_version\" agentteams-controller 120 \"Tuwunel (Matrix)\" || exit 1\n _wait_for_url \"http://127.0.0.1:9000/minio/health/live\" agentteams-controller 60 \"MinIO\" || exit 1\n _wait_for_url \"http://127.0.0.1:8080/status\" agentteams-controller 120 \"Higress Gateway\" || exit 1\n\n # Wait for controller to create Manager Agent container\n log \"Waiting for Manager Agent container...\"\n local _mgr_wait=0\n local _mgr_max=300\n while [ $_mgr_wait -lt $_mgr_max ]; do\n if ${DOCKER_CMD} ps --format '{{.Names}}' 2>/dev/null | grep -q \"^agentteams-manager$\"; then\n log \"Manager Agent container detected (${_mgr_wait}s)\"\n break\n fi\n sleep 3\n _mgr_wait=$((_mgr_wait + 3))\n done\n if [ $_mgr_wait -ge $_mgr_max ]; then\n log \"ERROR: Manager Agent container not created after ${_mgr_max}s\"\n log \"Controller logs:\"\n ${DOCKER_CMD} exec agentteams-controller tail -30 /var/log/agentteams/agentteams-controller-error.log 2>/dev/null || true\n exit 1\n fi\n\n # Wait for Manager Agent to be running\n log \"Waiting for Manager Agent to start...\"\n local _agent_wait=0\n while [ $_agent_wait -lt 120 ]; do\n local _state\n _state=$(${DOCKER_CMD} inspect --format '{{.State.Status}}' agentteams-manager 2>/dev/null || echo \"missing\")\n if [ \"${_state}\" = \"running\" ]; then\n log \"Manager Agent is running\"\n break\n fi\n sleep 2\n _agent_wait=$((_agent_wait + 2))\n done\n\n # Enable yolo mode in agent if requested\n if [ \"${AGENTTEAMS_YOLO:-}\" = \"1\" ]; then\n ${DOCKER_CMD} exec agentteams-manager touch /root/manager-workspace/yolo-mode 2>/dev/null || true\n fi\n\n # Wait for the controller to send the first-boot welcome message.\n # The controller gates this on (a) Manager joining the DM room and\n # (b) Higress WASM key-auth propagation actually clearing /v1/chat/completions\n # for the Manager's gateway key — typically ~45-90s on a fresh install.\n # We poll Manager CR Status.WelcomeSent via the in-container agt CLI,\n # exec'd inside agentteams-controller (the source-of-truth container — its\n # bundled CLI binary is always in lockstep with whatever controller\n # binary is currently serving the HTTP API, since they're the same\n # `go build` output. The agentteams-manager container's CLI may lag the\n # controller across image upgrades and silently drop the welcomeSent\n # field, leaving this loop hung). The controller container mints a\n # long-lived admin SA token at startup and writes it to\n # AGENTTEAMS_AUTH_TOKEN_FILE=/var/run/agentteams/cli-token (set as a Dockerfile\n # ENV default), so a bare `docker exec agentteams-controller agt …`\n # auto-discovers both the endpoint and the token. There is a brief\n # window after container start before bootstrapAdminCLIToken completes\n # where the file may be empty / absent — the loop's silent retry\n # handles that the same way it handles the manager-not-yet-running\n # case below.\n if ${DOCKER_CMD} exec agentteams-controller sh -c 'command -v agt' >/dev/null 2>&1; then\n log \"$(msg install.welcome_msg.waiting)\"\n local _welcome_wait=0\n local _welcome_max=\"${AGENTTEAMS_WELCOME_TIMEOUT:-300}\"\n local _welcome_done=0\n while [ $_welcome_wait -lt $_welcome_max ]; do\n # `tr -d` strips whitespace/CR so the grep stays robust to\n # any future change in go-json field ordering or formatting.\n local _wjson\n _wjson=$(${DOCKER_CMD} exec agentteams-controller \\\n agt get managers default -o json 2>/dev/null || true)\n if [ -n \"${_wjson}\" ] && printf '%s' \"${_wjson}\" | tr -d ' \\r\\n' | grep -q '\"welcomeSent\":true'; then\n log \"$(msg install.welcome_msg.confirmed \"${_welcome_wait}\")\"\n _welcome_done=1\n break\n fi\n sleep 3\n _welcome_wait=$((_welcome_wait + 3))\n done\n if [ $_welcome_done -ne 1 ]; then\n # Non-fatal: install is still good. Keep going to the success\n # banner so the admin can use Element Web to nudge Manager into\n # onboarding manually (one DM message is enough).\n log \"$(msg install.welcome_msg.timeout \"${_welcome_max}\")\"\n log \"$(msg install.welcome_msg.timeout_hint)\"\n log \"$(msg install.welcome_msg.timeout_inspect)\"\n fi\n else\n log \"$(msg install.welcome_msg.poll_unavailable)\"\n fi\n\n else\n # ============================================================\n # Legacy architecture: all-in-one manager container\n # ============================================================\n\n NETWORK_ARGS=\"--network agentteams-net\"\n NETWORK_ALIAS_ARGS=\"--network-alias matrix-local.agentteams.io --network-alias aigw-local.agentteams.io --network-alias fs-local.agentteams.io\"\n for _domain in \"${AGENTTEAMS_MATRIX_CLIENT_DOMAIN:-}\" \"${AGENTTEAMS_CONSOLE_DOMAIN:-}\"; do\n if [ -n \"${_domain}\" ] && [[ \"${_domain}\" == *-local.agentteams.io ]]; then\n NETWORK_ALIAS_ARGS=\"${NETWORK_ALIAS_ARGS} --network-alias ${_domain}\"\n fi\n done\n\n # Start Docker API proxy if enabled (security layer between Manager and Docker daemon)\n PROXY_ARGS=\"\"\n if [ \"${AGENTTEAMS_DOCKER_PROXY:-1}\" = \"1\" ] && [ -n \"${CONTAINER_SOCK:-}\" ]; then\n local _proxy_image=\"${AGENTTEAMS_REGISTRY}/higress/agentteams-docker-proxy:${AGENTTEAMS_VERSION}\"\n # Try versioned tag, fallback to latest\n if ! ${DOCKER_CMD} image inspect \"${_proxy_image}\" >/dev/null 2>&1; then\n ${DOCKER_CMD} pull \"${_proxy_image}\" 2>/dev/null || {\n _proxy_image=\"${AGENTTEAMS_REGISTRY}/higress/agentteams-docker-proxy:latest\"\n ${DOCKER_CMD} pull \"${_proxy_image}\" 2>/dev/null || true\n }\n fi\n if ${DOCKER_CMD} image inspect \"${_proxy_image}\" >/dev/null 2>&1; then\n log \"Starting Docker API proxy...\"\n ${DOCKER_CMD} run -d \\\n --name agentteams-docker-proxy \\\n --network agentteams-net \\\n -v \"${CONTAINER_SOCK}:/var/run/docker.sock\" \\\n --security-opt label=disable \\\n -e AGENTTEAMS_WORKER_IMAGE=\"${WORKER_IMAGE}\" \\\n -e AGENTTEAMS_COPAW_WORKER_IMAGE=\"${COPAW_WORKER_IMAGE}\" \\\n -e AGENTTEAMS_QWENPAW_WORKER_IMAGE=\"${QWENPAW_WORKER_IMAGE}\" \\\n -e AGENTTEAMS_HERMES_WORKER_IMAGE=\"${HERMES_WORKER_IMAGE}\" \\\n ${AGENTTEAMS_PROXY_ALLOWED_REGISTRIES:+-e AGENTTEAMS_PROXY_ALLOWED_REGISTRIES=\"${AGENTTEAMS_PROXY_ALLOWED_REGISTRIES}\"} \\\n --restart unless-stopped \\\n \"${_proxy_image}\"\n PROXY_ARGS=\"-e AGENTTEAMS_CONTROLLER_URL=http://agentteams-docker-proxy:2375 -e AGENTTEAMS_CONTAINER_API=http://agentteams-docker-proxy:2375\"\n SOCKET_MOUNT_ARGS=\"\"\n fi\n fi\n\n # Pass host timezone to container\n TZ_ARGS=\"\"\n if [ -n \"${AGENTTEAMS_TIMEZONE:-}\" ]; then\n TZ_ARGS=\"-e TZ=${AGENTTEAMS_TIMEZONE}\"\n fi\n\n YOLO_ARGS=\"\"\n if [ \"${AGENTTEAMS_YOLO:-}\" = \"1\" ]; then\n YOLO_ARGS=\"-e AGENTTEAMS_YOLO=1\"\n fi\n\n MATRIX_DEBUG_ARGS=\"\"\n if [ \"${AGENTTEAMS_MATRIX_DEBUG:-}\" = \"1\" ]; then\n MATRIX_DEBUG_ARGS=\"-e AGENTTEAMS_MATRIX_DEBUG=1\"\n fi\n\n # shellcheck disable=SC2086\n ${DOCKER_CMD} run -d \\\n --name agentteams-manager \\\n --env-file \"${ENV_FILE}\" \\\n -e HOME=/root/manager-workspace \\\n -w /root/manager-workspace \\\n -e HOST_ORIGINAL_HOME=\"${AGENTTEAMS_HOST_SHARE_DIR}\" \\\n -e AGENTTEAMS_MANAGER_RUNTIME=\"${AGENTTEAMS_MANAGER_RUNTIME:-copaw}\" \\\n ${JVM_ARGS:+-e JVM_ARGS=\"${JVM_ARGS}\"} \\\n ${YOLO_ARGS} \\\n ${MATRIX_DEBUG_ARGS} \\\n ${TZ_ARGS} \\\n ${SOCKET_MOUNT_ARGS} \\\n ${NETWORK_ARGS} \\\n ${NETWORK_ALIAS_ARGS} \\\n ${PROXY_ARGS} \\\n -p \"${_port_prefix}${AGENTTEAMS_PORT_GATEWAY}:8080\" \\\n -p \"${_port_prefix}${AGENTTEAMS_PORT_CONSOLE}:8001\" \\\n -p \"${_port_prefix}${AGENTTEAMS_PORT_ELEMENT_WEB:-18088}:8088\" \\\n -p \"127.0.0.1:${AGENTTEAMS_PORT_MANAGER_CONSOLE:-18888}:18888\" \\\n ${DATA_MOUNT_ARGS} \\\n ${WORKSPACE_MOUNT_ARGS} \\\n ${HOST_SHARE_MOUNT_ARGS} \\\n --restart unless-stopped \\\n \"$([ \"${AGENTTEAMS_MANAGER_RUNTIME}\" = \"copaw\" ] && echo \"${MANAGER_COPAW_IMAGE}\" || echo \"${MANAGER_IMAGE}\")\"\n\n # Wait for Manager agent to be ready\n wait_manager_ready \"agentteams-manager\"\n\n # Wait for Matrix server to be ready\n wait_matrix_ready \"agentteams-manager\"\n\n # Post-install verification (non-fatal: warnings only)\n local _verify_script\n _verify_script=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)/agentteams-verify.sh\"\n if [ -f \"${_verify_script}\" ]; then\n bash \"${_verify_script}\" \"agentteams-manager\" || {\n log \"WARNING: Some post-install checks failed. Re-run: bash install/agentteams-verify.sh\"\n }\n fi\n fi\n unset _port_prefix\n\n # ── Start Dashboard (optional) ────────────────────────────────────\n _start_dashboard\n\n # Apply Podman autostart if selected and environment matches\n if [ \"${DOCKER_CMD}\" = \"podman\" ] && [ \"${AGENTTEAMS_PODMAN_AUTOSTART:-0}\" = \"1\" ]; then\n setup_podman_autostart\n fi\n\n log \"\"\n log \"$(msg success.title)\"\n log \"\"\n log \"$(msg success.domains_configured)\"\n log \" ${AGENTTEAMS_MATRIX_DOMAIN%%:*} ${AGENTTEAMS_MATRIX_CLIENT_DOMAIN} ${AGENTTEAMS_AI_GATEWAY_DOMAIN} ${AGENTTEAMS_FS_DOMAIN} ${AGENTTEAMS_CONSOLE_DOMAIN}\"\n log \"\"\n local lan_ip\n lan_ip=$(detect_lan_ip)\n echo -e \"\\033[33m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\033[0m\"\n echo -e \"\\033[33m $(msg success.open_url)\\033[0m\"\n echo -e \"\\033[33m \\033[0m\"\n echo -e \"\\033[1;36m http://127.0.0.1:${AGENTTEAMS_PORT_ELEMENT_WEB:-18088}/#/login\\033[0m\"\n echo -e \"\\033[33m \\033[0m\"\n echo -e \"\\033[33m $(msg success.login_with)\\033[0m\"\n echo -e \"\\033[33m $(msg success.username \"${AGENTTEAMS_ADMIN_USER}\")\\033[0m\"\n echo -e \"\\033[33m $(msg success.password \"${AGENTTEAMS_ADMIN_PASSWORD}\")\\033[0m\"\n echo -e \"\\033[33m \\033[0m\"\n echo -e \"\\033[33m $(msg success.after_login)\\033[0m\"\n echo -e \"\\033[33m $(msg success.tell_it)\\033[0m\"\n echo -e \"\\033[33m $(msg success.manager_auto)\\033[0m\"\n echo -e \"\\033[33m \\033[0m\"\n echo -e \"\\033[33m ───────────────────────────────────────────────────────────────────────────── \\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_title)\\033[0m\"\n echo -e \"\\033[33m \\033[0m\"\n if [ -n \"${lan_ip}\" ]; then\n echo -e \"\\033[33m $(msg success.mobile_step1)\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_step2 \"http://${lan_ip}:${AGENTTEAMS_PORT_GATEWAY}\")\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_step3)\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_username \"${AGENTTEAMS_ADMIN_USER}\")\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_password \"${AGENTTEAMS_ADMIN_PASSWORD}\")\\033[0m\"\n else\n echo -e \"\\033[33m $(msg success.mobile_step1)\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_step2_noip \"${AGENTTEAMS_PORT_GATEWAY}\")\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_noip_hint)\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_step3)\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_username \"${AGENTTEAMS_ADMIN_USER}\")\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_password \"${AGENTTEAMS_ADMIN_PASSWORD}\")\\033[0m\"\n fi\n echo -e \"\\033[33m \\033[0m\"\n echo -e \"\\033[33m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\033[0m\"\n log \"\"\n log \"$(msg success.other_consoles)\"\n log \"$(msg success.higress_console \"${AGENTTEAMS_PORT_CONSOLE}\" \"${AGENTTEAMS_ADMIN_USER}\" \"${AGENTTEAMS_ADMIN_PASSWORD}\")\"\n if [ \"${AGENTTEAMS_DASHBOARD:-0}\" = \"1\" ]; then\n log \"$(msg success.dashboard \"${AGENTTEAMS_PORT_DASHBOARD:-13000}\")\"\n fi\n if [ \"${AGENTTEAMS_USE_EMBEDDED}\" != \"1\" ]; then\n log \"$(msg success.manager_console \"${AGENTTEAMS_PORT_MANAGER_CONSOLE:-18888}\")\"\n log \"$(msg success.manager_console_gateway \"${AGENTTEAMS_ADMIN_USER}\" \"${AGENTTEAMS_ADMIN_PASSWORD}\")\"\n fi\n log \"\"\n log \"$(msg success.switch_llm.title)\"\n log \"$(msg success.switch_llm.hint)\"\n log \"$(msg success.switch_llm.docs)\"\n log \"$(msg success.switch_llm.url)\"\n log \"\"\n log \"$(msg success.tip)\"\n log \"\"\n if [ \"${AGENTTEAMS_LOCAL_ONLY:-1}\" != \"1\" ]; then\n echo -e \"\\033[33m$(msg port.local_only.https_hint)\\033[0m\"\n log \"\"\n fi\n log \"$(msg success.config_file \"${ENV_FILE}\")\"\n log \"$(msg success.data_volume \"${AGENTTEAMS_DATA_DIR}\")\"\n log \"$(msg success.workspace \"${AGENTTEAMS_WORKSPACE_DIR}\")\"\n}\n\n# ============================================================\n# Worker Installation (One-Click)\n# ============================================================\n\ninstall_worker() {\n local WORKER_NAME=\"\"\n local FS=\"\"\n local FS_KEY=\"\"\n local FS_SECRET=\"\"\n local RESET=false\n local SKILLS_API_URL=\"\"\n\n # Parse arguments\n while [ $# -gt 0 ]; do\n case $1 in\n --name) WORKER_NAME=\"$2\"; shift 2 ;;\n --fs) FS=\"$2\"; shift 2 ;;\n --fs-key) FS_KEY=\"$2\"; shift 2 ;;\n --fs-secret) FS_SECRET=\"$2\"; shift 2 ;;\n --skills-api-url) SKILLS_API_URL=\"$2\"; shift 2 ;;\n --reset) RESET=true; shift ;;\n *) die \"$(msg error.unknown_option \"$1\")\" ;;\n esac\n done\n\n # Validate required params\n [ -z \"${WORKER_NAME}\" ] && die \"$(msg error.name_required)\"\n [ -z \"${FS}\" ] && die \"$(msg error.fs_required)\"\n [ -z \"${FS_KEY}\" ] && die \"$(msg error.fs_key_required)\"\n [ -z \"${FS_SECRET}\" ] && die \"$(msg error.fs_secret_required)\"\n\n local CONTAINER_NAME=\"agentteams-worker-${WORKER_NAME}\"\n\n # Handle reset\n if [ \"${RESET}\" = true ]; then\n log \"$(msg worker.resetting \"${WORKER_NAME}\")\"\n ${DOCKER_CMD} stop \"${CONTAINER_NAME}\" 2>/dev/null || true\n ${DOCKER_CMD} rm \"${CONTAINER_NAME}\" 2>/dev/null || true\n fi\n\n # Check for existing container\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' | grep -q \"^${CONTAINER_NAME}$\"; then\n die \"$(msg worker.exists \"${CONTAINER_NAME}\")\"\n fi\n\n log \"$(msg worker.starting \"${WORKER_NAME}\")\"\n\n # Build docker run args\n local DOCKER_ENV=\"\"\n DOCKER_ENV=\"${DOCKER_ENV} -e HOME=/root/agentteams-fs/agents/${WORKER_NAME}\"\n DOCKER_ENV=\"${DOCKER_ENV} -w /root/agentteams-fs/agents/${WORKER_NAME}\"\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_WORKER_NAME=${WORKER_NAME}\"\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_FS_ENDPOINT=${FS}\"\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_FS_ACCESS_KEY=${FS_KEY}\"\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_FS_SECRET_KEY=${FS_SECRET}\"\n\n if [ -z \"${SKILLS_API_URL}\" ]; then\n if [ -n \"${AGENTTEAMS_SKILLS_API_URL:-}\" ]; then\n SKILLS_API_URL=\"${AGENTTEAMS_SKILLS_API_URL}\"\n else\n SKILLS_API_URL=\"nacos://market.agentteams.io:80/public\"\n fi\n fi\n\n # Add SKILLS_API_URL if specified\n DOCKER_ENV=\"${DOCKER_ENV} -e SKILLS_API_URL=${SKILLS_API_URL}\"\n log \"$(msg worker.skills_url \"${SKILLS_API_URL}\")\"\n if [ -n \"${AGENTTEAMS_NACOS_USERNAME:-}\" ]; then\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_NACOS_USERNAME=${AGENTTEAMS_NACOS_USERNAME}\"\n fi\n if [ -n \"${AGENTTEAMS_NACOS_PASSWORD:-}\" ]; then\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_NACOS_PASSWORD=${AGENTTEAMS_NACOS_PASSWORD}\"\n fi\n if [ -n \"${AGENTTEAMS_NACOS_TOKEN:-}\" ]; then\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_NACOS_TOKEN=${AGENTTEAMS_NACOS_TOKEN}\"\n fi\n\n # shellcheck disable=SC2086\n ${DOCKER_CMD} run -d \\\n --name \"${CONTAINER_NAME}\" \\\n ${DOCKER_ENV} \\\n --restart unless-stopped \\\n \"${WORKER_IMAGE}\"\n\n log \"\"\n log \"$(msg worker.started \"${WORKER_NAME}\")\"\n log \"$(msg worker.container \"${CONTAINER_NAME}\")\"\n log \"$(msg worker.view_logs \"${CONTAINER_NAME}\")\"\n}\n\n# ============================================================\n# Main\n# ============================================================\n\n# ============================================================\n# LLM API connectivity test\n# ============================================================\n\ntest_llm_connectivity() {\n local base_url=\"$1\"\n local api_key=\"$2\"\n local model=\"$3\"\n local hint=\"${4:-}\" # optional: extra hint shown on failure\n if ! command -v curl >/dev/null 2>&1; then\n echo -e \"\\033[33m$(msg llm.openai.test.no_curl)\\033[0m\"\n return\n fi\n log \"$(msg llm.openai.test.testing)\"\n local _body _http_code _tmpfile\n _tmpfile=$(mktemp)\n _http_code=$(curl -s -o \"${_tmpfile}\" -w \"%{http_code}\" \\\n -X POST \"${base_url%/}/chat/completions\" \\\n -H \"Authorization: Bearer ${api_key}\" \\\n -H \"Content-Type: application/json\" \\\n -H \"User-Agent: AgentTeams/${AGENTTEAMS_VERSION:-latest}\" \\\n --max-time 30 \\\n -d \"{\\\"model\\\":\\\"${model}\\\",\\\"messages\\\":[{\\\"role\\\":\\\"user\\\",\\\"content\\\":\\\"hi\\\"}]}\" \\\n 2>/dev/null)\n _body=$(cat \"${_tmpfile}\")\n rm -f \"${_tmpfile}\"\n if [ \"${_http_code}\" = \"200\" ] || [ \"${_http_code}\" = \"201\" ]; then\n log \"$(msg llm.openai.test.ok)\"\n else\n echo -e \"\\033[33m$(msg llm.openai.test.fail \"${_http_code}\" \"${_body}\")\\033[0m\"\n if [ -n \"${hint}\" ]; then\n echo -e \"\\033[33m${hint}\\033[0m\"\n fi\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" != \"1\" ]; then\n local _confirm\n read -e -p \"$(msg llm.openai.test.confirm)\" _confirm\n if [ \"${_confirm}\" = \"b\" ]; then\n STEP_RESULT=\"back\"\n return 1\n fi\n if [ \"${_confirm}\" != \"y\" ] && [ \"${_confirm}\" != \"Y\" ]; then\n log \"$(msg llm.openai.test.aborted)\"\n exit 1\n fi\n fi\n fi\n}\n\ntest_embedding_connectivity() {\n local base_url=\"$1\"\n local api_key=\"$2\"\n local model=\"$3\"\n if ! command -v curl >/dev/null 2>&1; then\n return 0\n fi\n log \"$(msg llm.embedding.test.testing)\"\n local _body _http_code _tmpfile\n _tmpfile=$(mktemp)\n _http_code=$(curl -s -o \"${_tmpfile}\" -w \"%{http_code}\" \\\n -X POST \"${base_url%/}/embeddings\" \\\n -H \"Authorization: Bearer ${api_key}\" \\\n -H \"Content-Type: application/json\" \\\n -H \"User-Agent: AgentTeams/${AGENTTEAMS_VERSION:-latest}\" \\\n --max-time 30 \\\n -d \"{\\\"model\\\":\\\"${model}\\\",\\\"input\\\":\\\"test\\\"}\" \\\n 2>/dev/null)\n _body=$(cat \"${_tmpfile}\")\n rm -f \"${_tmpfile}\"\n if [ \"${_http_code}\" = \"200\" ] || [ \"${_http_code}\" = \"201\" ]; then\n log \"$(msg llm.embedding.test.ok)\"\n return 0\n else\n echo -e \"\\033[33m$(msg llm.embedding.test.fail \"${_http_code}\" \"${_body}\")\\033[0m\"\n return 1\n fi\n}\n\n# ============================================================\n# Uninstall\n# ============================================================\n\nuninstall_agentteams() {\n log \"$(msg uninstall.title)\"\n\n # Safely disable and remove the dedicated AgentTeams Podman autostart service\n if [ \"${DOCKER_CMD}\" = \"podman\" ] && command -v systemctl >/dev/null 2>&1; then\n local _systemctl_cmd=\"systemctl\"\n local _service_dir=\"/etc/systemd/system\"\n\n if [ \"$(id -u)\" != \"0\" ]; then\n _systemctl_cmd=\"systemctl --user\"\n _service_dir=\"${HOME}/.config/systemd/user\"\n fi\n\n if [ -f \"${_service_dir}/agentteams-podman-restart.service\" ]; then\n log \"Disabling and removing dedicated AgentTeams Podman autostart service...\"\n ${_systemctl_cmd} disable --now agentteams-podman-restart.service 2>/dev/null || true\n rm -f \"${_service_dir}/agentteams-podman-restart.service\"\n ${_systemctl_cmd} daemon-reload 2>/dev/null || true\n fi\n # Note: Native podman-restart.service is strictly untouched per maintainer review.\n fi\n\n # Capture manager image before removing (needed for workspace cleanup on Linux)\n local manager_image=\"\"\n manager_image=$(${DOCKER_CMD} inspect agentteams-manager --format '{{.Config.Image}}' 2>/dev/null || true)\n\n # Stop and remove manager\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -q \"^agentteams-manager$\"; then\n log \"$(msg uninstall.stopping_manager)\"\n ${DOCKER_CMD} stop agentteams-manager >/dev/null 2>&1 || true\n ${DOCKER_CMD} rm agentteams-manager >/dev/null 2>&1 || true\n fi\n\n # Stop and remove workers\n local workers\n workers=$(${DOCKER_CMD} ps -a --filter \"name=agentteams-worker-\" --format '{{.Names}}' 2>/dev/null || true)\n if [ -n \"${workers}\" ]; then\n log \"$(msg uninstall.stopping_workers)\"\n echo \"${workers}\" | while read -r w; do\n ${DOCKER_CMD} rm -f \"${w}\" >/dev/null 2>&1 || true\n log \"$(msg uninstall.removed \"${w}\")\"\n done\n fi\n\n # Stop and remove the dashboard container (installed via step_dashboard)\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -q \"^agentteams-dashboard$\"; then\n log \"Stopping and removing agentteams-dashboard...\"\n ${DOCKER_CMD} stop agentteams-dashboard >/dev/null 2>&1 || true\n ${DOCKER_CMD} rm agentteams-dashboard >/dev/null 2>&1 || true\n fi\n if ${DOCKER_CMD} volume inspect agentteams-dashboard-data >/dev/null 2>&1; then\n log \"Removing dashboard data volume: agentteams-dashboard-data\"\n ${DOCKER_CMD} volume rm agentteams-dashboard-data >/dev/null 2>&1 || true\n fi\n\n # Stop and remove docker-proxy (legacy ≤ v1.0.x; current arch uses\n # agentteams-controller for the same role)\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -q \"^agentteams-docker-proxy$\"; then\n log \"$(msg uninstall.removing_proxy)\"\n ${DOCKER_CMD} stop agentteams-docker-proxy >/dev/null 2>&1 || true\n ${DOCKER_CMD} rm agentteams-docker-proxy >/dev/null 2>&1 || true\n fi\n\n # Stop and remove the embedded controller container. MUST happen\n # before the `docker volume rm agentteams-data` step below — in embedded\n # mode agentteams-controller mounts agentteams-data at /data (Tuwunel DB,\n # MinIO state, Higress state, all the room messages), and `volume\n # rm` against an in-use volume fails silently because of the trailing\n # `|| true`. Skipping this used to leave room/message history behind\n # across \"uninstall + reinstall\" cycles. See PR #692.\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -q \"^agentteams-controller$\"; then\n log \"$(msg uninstall.stopping_controller)\"\n ${DOCKER_CMD} stop agentteams-controller >/dev/null 2>&1 || true\n ${DOCKER_CMD} rm agentteams-controller >/dev/null 2>&1 || true\n fi\n\n # Read env file for data/workspace info before removing\n local env_file=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n [ ! -f \"${env_file}\" ] && [ -f \"./agentteams-manager.env\" ] && env_file=\"./agentteams-manager.env\"\n\n local data_dir=\"\" workspace_dir=\"\"\n if [ -f \"${env_file}\" ]; then\n data_dir=$(grep '^AGENTTEAMS_DATA_DIR=' \"${env_file}\" 2>/dev/null | cut -d= -f2- || true)\n workspace_dir=$(grep '^AGENTTEAMS_WORKSPACE_DIR=' \"${env_file}\" 2>/dev/null | cut -d= -f2- || true)\n fi\n\n # Remove Docker volume\n local vol=\"${data_dir:-agentteams-data}\"\n if ${DOCKER_CMD} volume inspect \"${vol}\" >/dev/null 2>&1; then\n log \"$(msg uninstall.removing_volume \"${vol}\")\"\n ${DOCKER_CMD} volume rm \"${vol}\" >/dev/null 2>&1 || true\n fi\n\n # Remove Docker network\n if ${DOCKER_CMD} network ls --format '{{.Name}}' 2>/dev/null | grep -q \"^agentteams-net$\"; then\n log \"$(msg uninstall.removing_network)\"\n ${DOCKER_CMD} network rm agentteams-net >/dev/null 2>&1 || true\n fi\n\n # Remove workspace directory\n if [ -n \"${workspace_dir}\" ] && [ -d \"${workspace_dir}\" ]; then\n log \"$(msg uninstall.removing_workspace \"${workspace_dir}\")\"\n rm -rf \"${workspace_dir}\" 2>/dev/null || true\n if [ -d \"${workspace_dir}\" ]; then\n log \"$(msg uninstall.removing_workspace_elevated)\"\n local _parent _base\n _parent=$(dirname \"${workspace_dir}\")\n _base=$(basename \"${workspace_dir}\")\n local _rm_image=\"${manager_image:-busybox}\"\n ${DOCKER_CMD} run --rm --entrypoint sh \\\n -v \"${_parent}:/host-parent\" \\\n \"${_rm_image}\" \\\n -c \"rm -rf /host-parent/${_base}\" 2>/dev/null || true\n fi\n fi\n\n # Remove env file\n if [ -f \"${env_file}\" ]; then\n log \"$(msg uninstall.removing_env \"${env_file}\")\"\n rm -f \"${env_file}\"\n fi\n\n # Remove install log\n if [ -f \"${HOME}/agentteams-install.log\" ]; then\n log \"$(msg uninstall.removing_log \"${HOME}/agentteams-install.log\")\"\n rm -f \"${HOME}/agentteams-install.log\"\n fi\n\n echo \"\"\n log \"$(msg uninstall.done)\"\n}\n\n# ============================================================\n# Check container runtime (docker or podman) and environment\n# ============================================================\ncheck_container_runtime() {\n # Ensure the rootless environment baseline (XDG_RUNTIME_DIR) is ready before engine detection.\n if [ \"$(id -u)\" -ne 0 ] && [ -z \"${XDG_RUNTIME_DIR:-}\" ]; then\n export XDG_RUNTIME_DIR=\"/run/user/$(id -u)\"\n fi\n\n if command -v docker >/dev/null 2>&1; then\n DOCKER_CMD=\"docker\"\n # Check for podman disguised as docker (alias or podman-docker package)\n if docker --version 2>/dev/null | grep -qi \"podman\"; then\n DOCKER_CMD=\"podman\"\n fi\n elif command -v podman >/dev/null 2>&1; then\n DOCKER_CMD=\"podman\"\n else\n echo -e \"\\033[31m[AgentTeams ERROR]\\033[0m $(msg error.docker_not_found)\" >&2\n exit 1\n fi\n\n # Unified health check (Validates runtime responsiveness)\n if ! ${DOCKER_CMD} ps >/dev/null 2>&1; then\n echo -e \"\\033[31m[AgentTeams ERROR]\\033[0m $(msg error.docker_not_running)\" >&2\n exit 1\n fi\n}\n\ncheck_container_runtime\n\ncase \"${1:-}\" in\n manager|\"\")\n install_manager\n ;;\n worker)\n shift\n install_worker \"$@\"\n ;;\n dashboard)\n check_container_runtime\n load_current_params_from_env\n AGENTTEAMS_DASHBOARD=\"${AGENTTEAMS_DASHBOARD:-1}\"\n AGENTTEAMS_DASHBOARD_VERSION=\"${AGENTTEAMS_DASHBOARD_VERSION:-v1.2.0-beta.2}\"\n AGENTTEAMS_PORT_DASHBOARD=\"${AGENTTEAMS_PORT_DASHBOARD:-13000}\"\n AGENTTEAMS_DASHBOARD_IMAGE=\"${AGENTTEAMS_DASHBOARD_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-dashboard:${AGENTTEAMS_DASHBOARD_VERSION}}\"\n AGENTTEAMS_USE_EMBEDDED=1\n _start_dashboard\n ;;\n uninstall)\n uninstall_agentteams\n ;;\n *)\n echo \"Usage: $0 [manager|worker [options]|dashboard|uninstall]\"\n echo \"\"\n echo \"Commands:\"\n echo \" manager Interactive Manager installation (default)\"\n echo \" Choose Quick Start (all defaults) or Manual mode\"\n echo \" worker Worker installation (requires --name and connection params)\"\n echo \" dashboard Start/stop agentteams-dashboard (use with AGENTTEAMS_DASHBOARD=0 to stop)\"\n echo \" uninstall Stop and remove Manager + all Worker containers\"\n echo \"\"\n echo \"Quick Start (fastest):\"\n echo \" $0\"\n echo \" # Then select '1' for Quick Start mode\"\n echo \"\"\n echo \"Non-interactive (for automation):\"\n echo \" AGENTTEAMS_NON_INTERACTIVE=1 AGENTTEAMS_LLM_API_KEY=sk-xxx $0\"\n echo \"\"\n echo \"Worker Options:\"\n echo \" --name <name> Worker name (required)\"\n echo \" --fs <url> MinIO endpoint URL (required)\"\n echo \" --fs-key <key> MinIO access key (required)\"\n echo \" --fs-secret <secret> MinIO secret key (required)\"\n echo \" --reset Remove existing Worker container before creating\"\n exit 1\n ;;\nesac\n"}, "fb3a40be1f005bd584f45544fc73bd4601d5c52a": {"t": 1785764737.745075, "n": 248758, "h": "abf7883224aa17b3", "c": "#!/bin/bash\n# agentteams-install.sh - One-click installation for AgentTeams Manager, Worker, and Dashboard\n#\n# Usage:\n# ./agentteams-install.sh # Interactive installation (choose Quick Start or Manual)\n# ./agentteams-install.sh manager # Same as above (explicit)\n# ./agentteams-install.sh worker --name <name> ... # Worker installation\n# ./agentteams-install.sh dashboard # Start/stop agentteams-dashboard (AGENTTEAMS_DASHBOARD=0 to stop)\n# ./agentteams-install.sh uninstall # Stop and remove Manager + all Workers\n#\n# NOTE: agentteams-dashboard is currently only supported via the Bash installer.\n# The PowerShell installer (agentteams-install.ps1) does not yet include dashboard support.\n#\n# Onboarding Modes:\n# Quick Start - Fast installation with all default values (recommended)\n# Manual - Customize each option step by step\n#\n# Environment variables (for automation):\n# AGENTTEAMS_NON_INTERACTIVE Skip all prompts, use defaults (default: 0)\n# AGENTTEAMS_LLM_PROVIDER LLM provider (default: openai-compat for zh non-interactive Token Plan; qwen for en)\n# AGENTTEAMS_DEFAULT_MODEL Default model (default: qwen3.6-plus for zh Token Plan and en non-interactive)\n# AGENTTEAMS_OPENAI_BASE_URL OpenAI-compatible base URL (default for zh non-interactive: Alibaba Token Plan endpoint)\n# AGENTTEAMS_LLM_API_KEY LLM API key (required)\n# AGENTTEAMS_ADMIN_USER Admin username (default: admin)\n# AGENTTEAMS_ADMIN_PASSWORD Admin password (auto-generated if not set, min 8 chars)\n# AGENTTEAMS_MATRIX_DOMAIN Matrix domain (default: matrix-local.agentteams.io:18080)\n# AGENTTEAMS_MOUNT_SOCKET Mount container runtime socket (default: 1)\n# AGENTTEAMS_DATA_DIR Docker volume name for persistent data (default: agentteams-data)\n# AGENTTEAMS_WORKSPACE_DIR Host directory for manager workspace (default: ~/agentteams-manager)\n# AGENTTEAMS_VERSION Image tag (default: latest)\n# AGENTTEAMS_REGISTRY Image registry (default: auto-detected by timezone)\n# AGENTTEAMS_INSTALL_MANAGER_IMAGE Override manager image (e.g., local build)\n# AGENTTEAMS_INSTALL_WORKER_IMAGE Override worker image (e.g., local build)\n# AGENTTEAMS_INSTALL_COPAW_WORKER_IMAGE Override copaw worker image (e.g., local build)\n# AGENTTEAMS_INSTALL_HERMES_WORKER_IMAGE Override hermes worker image (e.g., local build)\n# AGENTTEAMS_NACOS_REGISTRY_URI Default Nacos registry URI for Worker market search/import\n# (default: nacos://market.agentteams.io:80/public)\n# AGENTTEAMS_NACOS_USERNAME Default Nacos username for nacos:// package imports (optional)\n# AGENTTEAMS_NACOS_PASSWORD Default Nacos password for nacos:// package imports (optional)\n# AGENTTEAMS_CMS_TRACES_ENABLED Enable openclaw-cms-plugin traces for Manager AND all Workers (default: false)\n# AGENTTEAMS_CMS_ENDPOINT ARMS OTLP endpoint (required if traces enabled)\n# AGENTTEAMS_CMS_LICENSE_KEY CMS license key (required if traces enabled)\n# AGENTTEAMS_CMS_PROJECT CMS project name (optional)\n# AGENTTEAMS_CMS_WORKSPACE CMS workspace ID (required if traces enabled)\n# AGENTTEAMS_CMS_SERVICE_NAME Manager service name in ARMS (default: agentteams-manager)\n# Workers always report as agentteams-worker-<name> automatically\n# AGENTTEAMS_CMS_METRICS_ENABLED Enable diagnostics-otel metrics for Manager AND all Workers (default: false)\n# AGENTTEAMS_PORT_GATEWAY Host port for Higress gateway (default: 18080)\n# AGENTTEAMS_PORT_CONSOLE Host port for Higress console (default: 18001)\n# AGENTTEAMS_PORT_ELEMENT_WEB Host port for Element Web direct access (default: 18088)\n# AGENTTEAMS_PORT_MANAGER_CONSOLE Host port for Manager console (default: 18888)\n# AGENTTEAMS_WORKER_IDLE_TIMEOUT Worker idle timeout in minutes (default: 720, i.e. 12 hours)\n# AGENTTEAMS_DASHBOARD Install agentteams-dashboard management UI (default: 1)\n# AGENTTEAMS_DASHBOARD_VERSION Dashboard version (default: v1.2.0-beta.1, independent of AgentTeams version)\n# AGENTTEAMS_PORT_DASHBOARD Dashboard host port (default: 13000)\n# AGENTTEAMS_DASHBOARD_IMAGE Override dashboard image (default: <registry>/agentteams/agentteams-dashboard:<DASHBOARD_VERSION>)\n# AGENTTEAMS_AI_GATEWAY_ADMIN_URL Higress Console URL for shared auth (auto-detected)\n\nset -e\n\nAGENTTEAMS_VERSION=\"${AGENTTEAMS_VERSION:-}\"\nAGENTTEAMS_KNOWN_STABLE_VERSION=\"v1.2.0\" # fallback if GitHub API is unreachable\n\n_normalize_version() {\n local version=\"$1\"\n case \"${version}\" in\n [0-9]*.[0-9]*.[0-9]*) version=\"v${version}\" ;;\n esac\n case \"${version}\" in\n v[0-9]*.[0-9]*.[0-9]*.beta.[0-9]*) version=\"${version/.beta./-beta.}\" ;;\n esac\n printf '%s' \"${version}\"\n}\n\n# Returns 0 (true) if $1 < $2 using semver order; \"latest\" is treated as greatest\n_ver_lt() {\n [ \"$1\" = \"latest\" ] && return 1\n [ \"$2\" = \"latest\" ] && return 0\n [ \"$1\" = \"$2\" ] && return 1\n [ \"$(printf '%s\\n%s' \"$1\" \"$2\" | sort -V | head -1)\" = \"$1\" ]\n}\n\n_use_legacy_image_env() {\n if [ \"$1\" = \"latest\" ]; then\n _ver_lt \"${AGENTTEAMS_KNOWN_STABLE_VERSION}\" \"v1.2.0\"\n else\n _ver_lt \"$1\" \"v1.2.0\"\n fi\n}\n\n_controller_env_prefix() {\n if _use_legacy_image_env \"$1\"; then\n printf '%s%s' 'HIC' 'LAW_'\n else\n printf '%s' 'AGENTTEAMS_'\n fi\n}\n\n_controller_storage_prefix() {\n if _use_legacy_image_env \"$1\"; then\n printf '%s%s' 'hic' 'law/agentteams-storage'\n else\n printf '%s' 'agentteams/agentteams-storage'\n fi\n}\nAGENTTEAMS_NON_INTERACTIVE=\"${AGENTTEAMS_NON_INTERACTIVE:-0}\"\nAGENTTEAMS_MOUNT_SOCKET=\"${AGENTTEAMS_MOUNT_SOCKET:-1}\"\nAGENTTEAMS_DOCKER_PROXY=\"${AGENTTEAMS_DOCKER_PROXY:-1}\"\nSTEP_RESULT=\"\" # Used by state machine to signal \"back\" navigation\n\n# ============================================================\n# Log all output to file\n# ============================================================\n\nAGENTTEAMS_LOG_FILE=\"${HOME}/agentteams-install.log\"\n\nif [ \"${1:-}\" != \"uninstall\" ]; then\n # Redirect all output (stdout and stderr) to both terminal and log file\n exec > >(tee -a \"${AGENTTEAMS_LOG_FILE}\") 2>&1\n\n echo \"\"\n echo \"========================================\"\n echo \"AgentTeams Installation Log\"\n echo \"Started: $(date)\"\n echo \"User: $(whoami)\"\n echo \"System: $(uname -a)\"\n echo \"Log file: ${AGENTTEAMS_LOG_FILE}\"\n echo \"========================================\"\n echo \"\"\nfi\n\n# ============================================================\n# Utility functions (needed early for timezone detection)\n# ============================================================\n\nlog() {\n echo -e \"\\033[36m[AgentTeams]\\033[0m $1\"\n}\n\nerror() {\n echo -e \"\\033[31m[AgentTeams ERROR]\\033[0m $1\" >&2\n}\n\ndie() {\n error \"$1\"\n exit 1\n}\n\n# ============================================================\n# Timezone detection (compatible with Linux and macOS)\n# ============================================================\n\ndetect_timezone() {\n local tz=\"\"\n\n # Try /etc/timezone (Debian/Ubuntu)\n if [ -f /etc/timezone ]; then\n tz=$(cat /etc/timezone 2>/dev/null | tr -d '[:space:]')\n fi\n\n # Try /etc/localtime symlink (macOS and some Linux)\n if [ -z \"${tz}\" ] && [ -L /etc/localtime ]; then\n tz=$(ls -l /etc/localtime 2>/dev/null | sed 's|.*/zoneinfo/||')\n fi\n\n # Try timedatectl (systemd)\n if [ -z \"${tz}\" ]; then\n tz=$(timedatectl show --value -p Timezone 2>/dev/null)\n fi\n\n # If still not detected, warn and prompt user. Diagnostics go to stderr —\n # this function returns its value via stdout to `$(detect_timezone)`, so\n # any echo here would leak into AGENTTEAMS_TIMEZONE and downstream image tags.\n if [ -z \"${tz}\" ]; then\n echo \"\" >&2\n echo -e \"\\033[33m[AgentTeams WARNING]\\033[0m Could not detect timezone automatically.\" >&2\n echo -e \"\\033[33m[AgentTeams]\\033[0m Please enter your timezone (e.g., Asia/Shanghai, America/New_York).\" >&2\n echo \"\" >&2\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n tz=\"Asia/Shanghai\"\n log \"Using default timezone: ${tz}\" >&2\n else\n read -e -p \"Timezone [Asia/Shanghai]: \" tz\n tz=\"${tz:-Asia/Shanghai}\"\n fi\n fi\n\n echo \"${tz}\"\n}\n\n# Detect timezone once at startup (used by registry selection and container TZ)\nAGENTTEAMS_TIMEZONE=\"${AGENTTEAMS_TIMEZONE:-$(detect_timezone)}\"\n\n# ============================================================\n# Language detection based on timezone\n# ============================================================\n\ndetect_language() {\n local tz=\"${AGENTTEAMS_TIMEZONE}\"\n case \"${tz}\" in\n Asia/Shanghai|Asia/Chongqing|Asia/Harbin|Asia/Urumqi|\\\n Asia/Taipei|Asia/Hong_Kong|Asia/Macau)\n echo \"zh\"\n ;;\n *)\n echo \"en\"\n ;;\n esac\n}\n\n# Language priority: env var > existing env file > timezone detection\nif [ -z \"${AGENTTEAMS_LANGUAGE}\" ]; then\n # Check existing env file for saved language preference (upgrade scenario)\n _env_file=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n # Migrate from legacy location (current directory) if needed\n if [ ! -f \"${_env_file}\" ] && [ -f \"./agentteams-manager.env\" ]; then\n mv \"./agentteams-manager.env\" \"${_env_file}\" 2>/dev/null || true\n fi\n if [ -f \"${_env_file}\" ]; then\n _saved_lang=$(grep '^AGENTTEAMS_LANGUAGE=' \"${_env_file}\" 2>/dev/null | cut -d= -f2-)\n if [ -n \"${_saved_lang}\" ]; then\n AGENTTEAMS_LANGUAGE=\"${_saved_lang}\"\n fi\n fi\n # Fall back to timezone-based detection\n if [ -z \"${AGENTTEAMS_LANGUAGE}\" ]; then\n AGENTTEAMS_LANGUAGE=\"$(detect_language)\"\n fi\n unset _env_file _saved_lang\nfi\nexport AGENTTEAMS_LANGUAGE\n\n# ============================================================\n# Centralized message dictionary and msg() function\n# Compatible with bash 3.2+ (macOS default) — uses case instead of declare -A\n# ============================================================\n\n# msg() function: look up message by key, with printf-style argument substitution\n# Falls back to English if the current language translation is missing.\nmsg() {\n local key=\"$1\"\n shift\n local lang=\"${AGENTTEAMS_LANGUAGE:-en}\"\n local text=\"\"\n case \"${key}.${lang}\" in\n # --- Timezone detection messages ---\n \"tz.warning.title.zh\") text=\"无法自动检测时区。\" ;;\n \"tz.warning.title.en\") text=\"Could not detect timezone automatically.\" ;;\n \"tz.warning.prompt.zh\") text=\"请输入您的时区(例如 Asia/Shanghai、America/New_York)。\" ;;\n \"tz.warning.prompt.en\") text=\"Please enter your timezone (e.g., Asia/Shanghai, America/New_York).\" ;;\n \"tz.default.zh\") text=\"使用默认时区: %s\" ;;\n \"tz.default.en\") text=\"Using default timezone: %s\" ;;\n \"tz.input_prompt.zh\") text=\"时区\" ;;\n \"tz.input_prompt.en\") text=\"Timezone\" ;;\n # --- Installation title and info ---\n \"install.title.zh\") text=\"=== AgentTeams Manager 安装 ===\" ;;\n \"install.title.en\") text=\"=== AgentTeams Manager Installation ===\" ;;\n \"install.registry.zh\") text=\"镜像仓库: %s\" ;;\n \"install.registry.en\") text=\"Registry: %s\" ;;\n \"install.dir.zh\") text=\"安装目录: %s\" ;;\n \"install.dir.en\") text=\"Installation directory: %s\" ;;\n \"install.dir_hint.zh\") text=\" (env 文件 'agentteams-manager.env' 将保存到 HOME 目录。)\" ;;\n \"install.dir_hint.en\") text=\" (The env file 'agentteams-manager.env' will be saved to your HOME directory.)\" ;;\n \"install.dir_hint2.zh\") text=\" (请从您希望管理此安装的目录运行此脚本。)\" ;;\n \"install.dir_hint2.en\") text=\" (Run this script from the directory where you want to manage this installation.)\" ;;\n # --- Onboarding mode ---\n \"install.mode.title.zh\") text=\"--- Onboarding 模式 ---\" ;;\n \"install.mode.title.en\") text=\"--- Onboarding Mode ---\" ;;\n \"install.mode.choose.zh\") text=\"选择安装模式:\" ;;\n \"install.mode.choose.en\") text=\"Choose your installation mode:\" ;;\n \"install.mode.quickstart.zh\") text=\" 1) 快速开始 - 使用阿里云通义 Token 套餐快速安装(推荐)\" ;;\n \"install.mode.quickstart.en\") text=\" 1) Quick Start - Fast installation with Qwen Cloud (recommended)\" ;;\n \"install.mode.manual.zh\") text=\" 2) 手动配置 - 选择 LLM 提供商并自定义选项\" ;;\n \"install.mode.manual.en\") text=\" 2) Manual - Choose LLM provider and customize options\" ;;\n \"install.mode.prompt.zh\") text=\"请选择 [1/2]\" ;;\n \"install.mode.prompt.en\") text=\"Enter choice [1/2]\" ;;\n \"install.mode.quickstart_selected.zh\") text=\"已选择快速开始模式 - 使用阿里云通义 Token 套餐\" ;;\n \"install.mode.quickstart_selected.en\") text=\"Quick Start mode selected - using Qwen Cloud\" ;;\n \"install.mode.manual_selected.zh\") text=\"已选择手动配置模式 - 您将选择 LLM 提供商并自定义选项\" ;;\n \"install.mode.manual_selected.en\") text=\"Manual mode selected - you will choose LLM provider and customize options\" ;;\n \"install.mode.invalid.zh\") text=\"无效选择,默认使用快速开始模式\" ;;\n \"install.mode.invalid.en\") text=\"Invalid choice, defaulting to Quick Start mode\" ;;\n # --- Upgrade mode sub-menu ---\n \"upgrade.mode.prompt.zh\") text=\"请选择升级方式:\" ;;\n \"upgrade.mode.prompt.en\") text=\"Select upgrade mode:\" ;;\n \"upgrade.mode.keep_all.zh\") text=\" 1) 保留所有参数,一键升级(推荐)\" ;;\n \"upgrade.mode.keep_all.en\") text=\" 1) Keep all parameters, quick upgrade (recommended)\" ;;\n \"upgrade.mode.confirm_each.zh\") text=\" 2) 逐个确认参数(可修改)\" ;;\n \"upgrade.mode.confirm_each.en\") text=\" 2) Confirm each parameter (can modify)\" ;;\n \"upgrade.mode.back.zh\") text=\" 3) 返回\" ;;\n \"upgrade.mode.back.en\") text=\" 3) Back\" ;;\n # --- Version selection ---\n \"install.version.title.zh\") text=\"--- 版本选择 ---\" ;;\n \"install.version.title.en\") text=\"--- Version Selection ---\" ;;\n \"install.version.choose.zh\") text=\"选择要安装的版本:\" ;;\n \"install.version.choose.en\") text=\"Choose the version to install:\" ;;\n \"install.version.option_latest.zh\") text=\" 1) latest - 最新版(默认)\" ;;\n \"install.version.option_latest.en\") text=\" 1) latest - Latest build (default)\" ;;\n \"install.version.option_stable.zh\") text=\" 2) %s - 最新稳定版\" ;;\n \"install.version.option_stable.en\") text=\" 2) %s - Latest stable release\" ;;\n \"install.version.fetching.zh\") text=\"正在查询最新稳定版本...\" ;;\n \"install.version.fetching.en\") text=\"Fetching latest stable release...\" ;;\n \"install.version.fetch_failed.zh\") text=\"无法查询 GitHub,使用内置版本 %s\" ;;\n \"install.version.fetch_failed.en\") text=\"Could not reach GitHub, using built-in version %s\" ;;\n \"install.version.option_custom.zh\") text=\" 3) 自定义 - 手动输入版本号(如 v1.0.5)\" ;;\n \"install.version.option_custom.en\") text=\" 3) Custom - Enter a specific version (e.g. v1.0.5)\" ;;\n \"install.version.prompt.zh\") text=\"请选择 [1/2/3]\" ;;\n \"install.version.prompt.en\") text=\"Enter choice [1/2/3]\" ;;\n \"install.version.custom_prompt.zh\") text=\"请输入版本号\" ;;\n \"install.version.custom_prompt.en\") text=\"Enter version tag\" ;;\n \"install.version.selected_latest.zh\") text=\"已选择最新版 (latest)\" ;;\n \"install.version.selected_latest.en\") text=\"Selected latest version\" ;;\n \"install.version.selected_stable.zh\") text=\"已选择最新稳定版 (%s)\" ;;\n \"install.version.selected_stable.en\") text=\"Selected latest stable version (%s)\" ;;\n \"install.version.selected_custom.zh\") text=\"已选择自定义版本 (%s)\" ;;\n \"install.version.selected_custom.en\") text=\"Selected custom version (%s)\" ;;\n \"install.version.invalid.zh\") text=\"无效选择,使用最新稳定版 (%s)\" ;;\n \"install.version.invalid.en\") text=\"Invalid choice, defaulting to latest stable version (%s)\" ;;\n # --- Existing installation detected ---\n \"install.existing.detected.zh\") text=\"检测到已有 Manager 安装(env 文件: %s)\" ;;\n \"install.existing.detected.en\") text=\"Existing Manager installation detected (env file: %s)\" ;;\n \"install.existing.choose.zh\") text=\"选择操作:\" ;;\n \"install.existing.choose.en\") text=\"Choose an action:\" ;;\n \"install.existing.upgrade.zh\") text=\" 1) 就地升级(保留数据、工作空间、env 文件)\" ;;\n \"install.existing.upgrade.en\") text=\" 1) In-place upgrade (keep data, workspace, env file)\" ;;\n \"install.existing.reinstall.zh\") text=\" 2) 全新重装(删除所有数据,重新开始)\" ;;\n \"install.existing.reinstall.en\") text=\" 2) Clean reinstall (remove all data, start fresh)\" ;;\n \"install.existing.cancel.zh\") text=\" 3) 取消\" ;;\n \"install.existing.cancel.en\") text=\" 3) Cancel\" ;;\n \"install.existing.prompt.zh\") text=\"请选择 [1/2/3]\" ;;\n \"install.existing.prompt.en\") text=\"Enter choice [1/2/3]\" ;;\n \"install.existing.upgrade_noninteractive.zh\") text=\"非交互模式: 执行就地升级...\" ;;\n \"install.existing.upgrade_noninteractive.en\") text=\"Non-interactive mode: performing in-place upgrade...\" ;;\n \"install.existing.upgrading.zh\") text=\"执行就地升级...\" ;;\n \"install.existing.upgrading.en\") text=\"Performing in-place upgrade...\" ;;\n \"install.existing.warn_manager_stop.zh\") text=\"⚠️ Manager 容器将被停止并重新创建。\" ;;\n \"install.existing.warn_manager_stop.en\") text=\"⚠️ Manager container will be stopped and recreated.\" ;;\n \"install.existing.warn_worker_recreate.zh\") text=\"⚠️ Worker 容器也将被重新创建(以更新 Manager IP)。\" ;;\n \"install.existing.warn_worker_recreate.en\") text=\"⚠️ Worker containers will also be recreated (to update Manager IP in hosts).\" ;;\n \"install.existing.continue_prompt.zh\") text=\"继续?[y/N]\" ;;\n \"install.existing.continue_prompt.en\") text=\"Continue? [y/N]\" ;;\n \"install.existing.cancelled.zh\") text=\"安装已取消。\" ;;\n \"install.existing.cancelled.en\") text=\"Installation cancelled.\" ;;\n \"install.existing.stopping_manager.zh\") text=\"停止并移除现有 manager 容器...\" ;;\n \"install.existing.stopping_manager.en\") text=\"Stopping and removing existing manager container...\" ;;\n \"install.existing.stopping_workers.zh\") text=\"停止并移除现有 worker 容器...\" ;;\n \"install.existing.stopping_workers.en\") text=\"Stopping and removing existing worker containers...\" ;;\n \"install.existing.removed.zh\") text=\" 已移除: %s\" ;;\n \"install.existing.removed.en\") text=\" Removed: %s\" ;;\n # --- Clean reinstall messages ---\n \"install.reinstall.performing.zh\") text=\"执行全新重装...\" ;;\n \"install.reinstall.performing.en\") text=\"Performing clean reinstall...\" ;;\n \"install.reinstall.warn_stop.zh\") text=\"⚠️ 以下运行中的容器将被停止:\" ;;\n \"install.reinstall.warn_stop.en\") text=\"⚠️ The following running containers will be stopped:\" ;;\n \"install.reinstall.warn_delete.zh\") text=\"⚠️ 警告: 以下内容将被删除:\" ;;\n \"install.reinstall.warn_delete.en\") text=\"⚠️ WARNING: This will DELETE the following:\" ;;\n \"install.reinstall.warn_volume.zh\") text=\" - Docker 卷: agentteams-data\" ;;\n \"install.reinstall.warn_volume.en\") text=\" - Docker volume: agentteams-data\" ;;\n \"install.reinstall.warn_env.zh\") text=\" - Env 文件: %s\" ;;\n \"install.reinstall.warn_env.en\") text=\" - Env file: %s\" ;;\n \"install.reinstall.warn_workspace.zh\") text=\" - Manager 工作空间: %s\" ;;\n \"install.reinstall.warn_workspace.en\") text=\" - Manager workspace: %s\" ;;\n \"install.reinstall.warn_workers.zh\") text=\" - 所有 worker 容器\" ;;\n \"install.reinstall.warn_workers.en\") text=\" - All worker containers\" ;;\n \"install.reinstall.warn_proxy.zh\") text=\" - Docker API 代理容器: agentteams-controller\" ;;\n \"install.reinstall.warn_proxy.en\") text=\" - Docker API proxy container: agentteams-controller\" ;;\n \"install.reinstall.warn_network.zh\") text=\" - Docker 网络: agentteams-net\" ;;\n \"install.reinstall.warn_network.en\") text=\" - Docker network: agentteams-net\" ;;\n \"install.reinstall.confirm_type.zh\") text=\"请输入工作空间路径以确认删除(或按 Ctrl+C 取消):\" ;;\n \"install.reinstall.confirm_type.en\") text=\"To confirm deletion, please type the workspace path:\" ;;\n \"install.reinstall.confirm_path.zh\") text=\"输入路径以确认(或按 Ctrl+C 取消)\" ;;\n \"install.reinstall.confirm_path.en\") text=\"Type the path to confirm (or press Ctrl+C to cancel)\" ;;\n \"install.reinstall.path_mismatch.zh\") text=\"路径不匹配。中止重装。输入: '%s',期望: '%s'\" ;;\n \"install.reinstall.path_mismatch.en\") text=\"Path mismatch. Aborting reinstall. Input: '%s', Expected: '%s'\" ;;\n \"install.reinstall.confirmed.zh\") text=\"已确认。正在清理...\" ;;\n \"install.reinstall.confirmed.en\") text=\"Confirmed. Cleaning up...\" ;;\n \"install.reinstall.removed_worker.zh\") text=\" 已移除 worker: %s\" ;;\n \"install.reinstall.removed_worker.en\") text=\" Removed worker: %s\" ;;\n \"install.reinstall.removing_volume.zh\") text=\"正在移除 Docker 卷: agentteams-data\" ;;\n \"install.reinstall.removing_volume.en\") text=\"Removing Docker volume: agentteams-data\" ;;\n \"install.reinstall.warn_volume_fail.zh\") text=\" 警告: 无法移除卷(可能有引用)\" ;;\n \"install.reinstall.warn_volume_fail.en\") text=\" Warning: Could not remove volume (may have references)\" ;;\n \"install.reinstall.removing_proxy.zh\") text=\"正在移除 Docker API 代理容器: agentteams-controller\" ;;\n \"install.reinstall.removing_proxy.en\") text=\"Removing Docker API proxy container: agentteams-controller\" ;;\n \"install.reinstall.removing_network.zh\") text=\"正在移除 Docker 网络: agentteams-net\" ;;\n \"install.reinstall.removing_network.en\") text=\"Removing Docker network: agentteams-net\" ;;\n \"install.reinstall.removing_workspace.zh\") text=\"正在移除工作空间目录: %s\" ;;\n \"install.reinstall.removing_workspace.en\") text=\"Removing workspace directory: %s\" ;;\n \"install.reinstall.removing_env.zh\") text=\"正在移除 env 文件: %s\" ;;\n \"install.reinstall.removing_env.en\") text=\"Removing env file: %s\" ;;\n \"install.reinstall.cleanup_done.zh\") text=\"清理完成。开始全新安装...\" ;;\n \"install.reinstall.cleanup_done.en\") text=\"Cleanup complete. Starting fresh installation...\" ;;\n \"install.reinstall.failed_rm_workspace.zh\") text=\"无法移除工作空间目录\" ;;\n \"install.reinstall.failed_rm_workspace.en\") text=\"Failed to remove workspace directory\" ;;\n # --- Orphan volume detection ---\n \"install.orphan_volume.detected.zh\") text=\"⚠️ 检测到残留数据卷 '%s',但未找到对应的 env 配置文件。\" ;;\n \"install.orphan_volume.detected.en\") text=\"⚠️ Found leftover data volume '%s' but no matching env config file.\" ;;\n \"install.orphan_volume.warn.zh\") text=\"这可能是之前安装的残留数据,会导致新安装出现异常(如密码冲突、服务启动失败)。\" ;;\n \"install.orphan_volume.warn.en\") text=\"This is likely leftover data from a previous installation and may cause issues (e.g., credential conflicts, service startup failures).\" ;;\n \"install.orphan_volume.choose.zh\") text=\"选择操作:\" ;;\n \"install.orphan_volume.choose.en\") text=\"Choose an action:\" ;;\n \"install.orphan_volume.clean.zh\") text=\" 1) 清理残留数据卷后继续安装(推荐)\" ;;\n \"install.orphan_volume.clean.en\") text=\" 1) Remove leftover volume and continue installation (recommended)\" ;;\n \"install.orphan_volume.keep.zh\") text=\" 2) 保留数据卷继续安装(可能出现异常)\" ;;\n \"install.orphan_volume.keep.en\") text=\" 2) Keep the volume and continue installation (may cause issues)\" ;;\n \"install.orphan_volume.prompt.zh\") text=\"请选择 [1/2]\" ;;\n \"install.orphan_volume.prompt.en\") text=\"Enter choice [1/2]\" ;;\n \"install.orphan_volume.cleaning.zh\") text=\"正在清理残留数据卷...\" ;;\n \"install.orphan_volume.cleaning.en\") text=\"Removing leftover data volume...\" ;;\n \"install.orphan_volume.cleaned.zh\") text=\"残留数据卷已清理。继续全新安装...\" ;;\n \"install.orphan_volume.cleaned.en\") text=\"Leftover volume removed. Continuing with fresh installation...\" ;;\n \"install.orphan_volume.keeping.zh\") text=\"保留数据卷,继续安装。如遇异常请选择全新重装。\" ;;\n \"install.orphan_volume.keeping.en\") text=\"Keeping existing volume. If you encounter issues, consider a clean reinstall.\" ;;\n \"install.orphan_volume.clean_noninteractive.zh\") text=\"非交互模式: 自动清理残留数据卷...\" ;;\n \"install.orphan_volume.clean_noninteractive.en\") text=\"Non-interactive mode: automatically removing leftover volume...\" ;;\n # --- Loading existing config ---\n \"install.loading_config.zh\") text=\"从 %s 加载已有配置(shell 环境变量优先)...\" ;;\n \"install.loading_config.en\") text=\"Loading existing config from %s (shell env vars take priority)...\" ;;\n # --- LLM Configuration ---\n \"llm.title.zh\") text=\"--- LLM 配置 ---\" ;;\n \"llm.title.en\") text=\"--- LLM Configuration ---\" ;;\n \"llm.provider.label.zh\") text=\" 提供商: %s\" ;;\n \"llm.provider.label.en\") text=\" Provider: %s\" ;;\n \"llm.model.label.zh\") text=\" 模型: %s\" ;;\n \"llm.model.label.en\") text=\" Model: %s\" ;;\n \"llm.provider.qwen.zh\") text=\" 提供商: qwen(阿里云百炼)\" ;;\n \"llm.provider.qwen.en\") text=\" Provider: qwen (Alibaba Cloud Bailian)\" ;;\n \"llm.provider.qwen_default.zh\") text=\" 提供商: %s(默认)\" ;;\n \"llm.provider.qwen_default.en\") text=\" Provider: %s (default)\" ;;\n \"llm.model.default.zh\") text=\" 模型: %s(默认)\" ;;\n \"llm.model.default.en\") text=\" Model: %s (default)\" ;;\n \"llm.apikey_hint_bailian.zh\") text=\" 💡 获取阿里云百炼(DashScope)API Key:\" ;;\n \"llm.apikey_hint_bailian.en\") text=\" 💡 Get your Alibaba Cloud Bailian (DashScope) API Key:\" ;;\n \"llm.apikey_url_bailian.zh\") text=\" https://www.aliyun.com/product/bailian\" ;;\n \"llm.apikey_url_bailian.en\") text=\" https://www.aliyun.com/product/bailian\" ;;\n \"llm.apikey_hint_qwencloud.zh\") text=\" 💡 从 Qwen Cloud(国际站)获取 DASHSCOPE_API_KEY:\" ;;\n \"llm.apikey_hint_qwencloud.en\") text=\" 💡 Get your DASHSCOPE_API_KEY for Qwen Cloud (international) from:\" ;;\n \"llm.apikey_url_qwencloud.zh\") text=\" https://home.qwencloud.com/api-keys (文档: https://docs.qwencloud.com/)\" ;;\n \"llm.apikey_url_qwencloud.en\") text=\" https://home.qwencloud.com/api-keys | Docs: https://docs.qwencloud.com/\" ;;\n \"llm.apikey_hint_tokenplan.zh\") text=\" 💡 获取 DashScope API Key 或开通通义 Token 套餐,请参考:\" ;;\n \"llm.apikey_hint_tokenplan.en\") text=\" 💡 Get your DashScope or Token Plan API key (Alibaba Model Studio):\" ;;\n \"llm.apikey_url_tokenplan.zh\") text=\" https://help.aliyun.com/zh/model-studio/token-plan-quickstart\" ;;\n \"llm.apikey_url_tokenplan.en\") text=\" https://common-buy.aliyun.com/token-plan/ | https://help.aliyun.com/zh/model-studio/token-plan-quickstart\" ;;\n \"llm.apikey_hint_codingplan.zh\") text=\" 💡 获取 DashScope API Key(Coding 套餐 / coding.dashscope 接口):\" ;;\n \"llm.apikey_hint_codingplan.en\") text=\" 💡 Get your DashScope API key for Coding Plan (coding.dashscope endpoint):\" ;;\n \"llm.apikey_url_codingplan.zh\") text=\" https://help.aliyun.com/zh/model-studio/get-api-key\" ;;\n \"llm.apikey_url_codingplan.en\") text=\" https://help.aliyun.com/zh/model-studio/get-api-key\" ;;\n \"llm.apikey_prompt.zh\") text=\"LLM API Key\" ;;\n \"llm.apikey_prompt.en\") text=\"LLM API Key\" ;;\n \"llm.providers_title.zh\") text=\"可用 LLM 提供商:\" ;;\n \"llm.providers_title.en\") text=\"Available LLM Providers:\" ;;\n \"llm.provider.alibaba.zh\") text=\" 1) 阿里云通义 Token 套餐 - 推荐中国用户使用\" ;;\n \"llm.provider.alibaba.en\") text=\" 1) Qwen Cloud - International (OpenAI-compatible API, recommended)\" ;;\n \"llm.provider.openai_compat.zh\") text=\" 2) OpenAI 兼容 API - 自定义 Base URL(OpenAI、DeepSeek 等)\" ;;\n \"llm.provider.openai_compat.en\") text=\" 2) OpenAI-compatible API - Custom Base URL (OpenAI, DeepSeek, etc.)\" ;;\n \"llm.provider.select.zh\") text=\"选择提供商 [1/2]\" ;;\n \"llm.provider.select.en\") text=\"Select provider [1/2]\" ;;\n \"llm.alibaba.models_title.zh\") text=\"选择阿里云模型接入方式:\" ;;\n \"llm.alibaba.models_title.en\") text=\"Select Alibaba Cloud model access:\" ;;\n \"llm.alibaba.model.tokenplan.zh\") text=\" 1) 阿里云通义 Token 套餐 - 兼容模式(推荐)\" ;;\n \"llm.alibaba.model.tokenplan.en\") text=\" 1) Alibaba Cloud Token Plan - compatible-mode (recommended)\" ;;\n \"llm.alibaba.model.bailian.zh\") text=\" 2) 阿里云百炼 - DashScope 通用兼容接口\" ;;\n \"llm.alibaba.model.bailian.en\") text=\" 2) Alibaba Cloud Bailian - DashScope compatible mode\" ;;\n \"llm.alibaba.model.codingplan_legacy.zh\") text=\" 3) 阿里云 Coding 套餐 - 旧版端点(兼容保留)\" ;;\n \"llm.alibaba.model.codingplan_legacy.en\") text=\" 3) Alibaba Cloud Coding Plan - legacy endpoint (backward compatible)\" ;;\n \"llm.alibaba.model.select.zh\") text=\"选择接入方式 [1/2/3]\" ;;\n \"llm.alibaba.model.select.en\") text=\"Select access option [1/2/3]\" ;;\n \"llm.alibaba.model.invalid.zh\") text=\"无效选择: %s(请输入 1、2 或 3)\" ;;\n \"llm.alibaba.model.invalid.en\") text=\"Invalid choice: %s (please enter 1, 2, or 3)\" ;;\n \"llm.codingplan.models_title.zh\") text=\"选择通义 Token 套餐默认模型:\" ;;\n \"llm.codingplan.models_title.en\") text=\"Select Qwen Cloud default model:\" ;;\n \"llm.codingplan.model.qwen36plus.zh\") text=\" 1) qwen3.6-plus - 千问 3.6(推荐)\" ;;\n \"llm.codingplan.model.qwen36plus.en\") text=\" 1) qwen3.6-plus - Qwen 3.6 (recommended)\" ;;\n \"llm.codingplan.model.glm5.zh\") text=\" 2) glm-5 - 智谱 GLM-5(编程推荐)\" ;;\n \"llm.codingplan.model.glm5.en\") text=\" 2) glm-5 - Zhipu GLM-5 (recommended for coding)\" ;;\n \"llm.codingplan.model.kimi.zh\") text=\" 3) kimi-k2.5 - Moonshot Kimi K2.5\" ;;\n \"llm.codingplan.model.kimi.en\") text=\" 3) kimi-k2.5 - Moonshot Kimi K2.5\" ;;\n \"llm.codingplan.model.minimax.zh\") text=\" 4) MiniMax-M2.5 - MiniMax M2.5\" ;;\n \"llm.codingplan.model.minimax.en\") text=\" 4) MiniMax-M2.5 - MiniMax M2.5\" ;;\n \"llm.codingplan.model.select.zh\") text=\"选择模型 [1/2/3/4]\" ;;\n \"llm.codingplan.model.select.en\") text=\"Select model [1/2/3/4]\" ;;\n \"llm.provider.selected_tokenplan.zh\") text=\" 提供商: 阿里云通义 Token 套餐(兼容模式)\" ;;\n \"llm.provider.selected_tokenplan.en\") text=\" Provider: Alibaba Cloud Token Plan (compatible mode)\" ;;\n \"llm.provider.selected_codingplan.zh\") text=\" 提供商: 阿里云通义 Token 套餐(alibaba-cloud)\" ;;\n \"llm.provider.selected_codingplan.en\") text=\" Provider: Qwen Cloud (international) (alibaba-cloud)\" ;;\n \"llm.provider.selected_codingplan_legacy.zh\") text=\" 提供商: 阿里云 Coding 套餐(coding.dashscope)\" ;;\n \"llm.provider.selected_codingplan_legacy.en\") text=\" Provider: Alibaba Cloud Coding Plan (coding.dashscope)\" ;;\n \"llm.provider.selected_qwen.zh\") text=\" 提供商: 阿里云百炼\" ;;\n \"llm.provider.selected_qwen.en\") text=\" Provider: Alibaba Cloud Bailian\" ;;\n \"llm.provider.selected_openai.zh\") text=\" 提供商: %s(OpenAI 兼容)\" ;;\n \"llm.provider.selected_openai.en\") text=\" Provider: %s (OpenAI-compatible)\" ;;\n \"llm.provider.invalid.zh\") text=\"无效选择: %s(请输入 1 或 2)\" ;;\n \"llm.provider.invalid.en\") text=\"Invalid choice: %s (please enter 1 or 2)\" ;;\n \"llm.qwen.model_prompt.zh\") text=\"默认模型 ID\" ;;\n \"llm.qwen.model_prompt.en\") text=\"Default Model ID\" ;;\n \"llm.openai.base_url_prompt.zh\") text=\"Base URL\" ;;\n \"llm.openai.base_url_prompt.en\") text=\"Base URL\" ;;\n \"llm.openai.model_prompt.zh\") text=\"默认模型 ID\" ;;\n \"llm.openai.model_prompt.en\") text=\"Default Model ID\" ;;\n \"llm.openai.base_url_label.zh\") text=\" Base URL: %s\" ;;\n \"llm.openai.base_url_label.en\") text=\" Base URL: %s\" ;;\n # --- Custom model parameters ---\n \"llm.custom_model.detected.zh\") text=\" ⚠️ 模型 '%s' 不在内置模型列表中,请配置模型参数:\" ;;\n \"llm.custom_model.detected.en\") text=\" ⚠️ Model '%s' is not in the built-in model list. Please configure model parameters:\" ;;\n \"llm.custom_model.context_prompt.zh\") text=\"最大上下文长度(token 数)[150000]\" ;;\n \"llm.custom_model.context_prompt.en\") text=\"Max context window (tokens) [150000]\" ;;\n \"llm.custom_model.max_tokens_prompt.zh\") text=\"最大输出长度(token 数)[128000]\" ;;\n \"llm.custom_model.max_tokens_prompt.en\") text=\"Max output tokens [128000]\" ;;\n \"llm.custom_model.reasoning_prompt.zh\") text=\"是否支持推理/思考模式?[Y/n]\" ;;\n \"llm.custom_model.reasoning_prompt.en\") text=\"Does it support reasoning/thinking mode? [Y/n]\" ;;\n \"llm.custom_model.vision_prompt.zh\") text=\"是否支持图片输入?[y/N]\" ;;\n \"llm.custom_model.vision_prompt.en\") text=\"Does it support image input? [y/N]\" ;;\n \"llm.custom_model.summary.zh\") text=\" 自定义模型参数: 上下文=%s, 最大输出=%s, 推理=%s, 图片=%s\" ;;\n \"llm.custom_model.summary.en\") text=\" Custom model params: context=%s, maxTokens=%s, reasoning=%s, vision=%s\" ;;\n # --- Admin Credentials ---\n \"admin.title.zh\") text=\"--- 管理员凭据 ---\" ;;\n \"admin.title.en\") text=\"--- Admin Credentials ---\" ;;\n \"admin.username_prompt.zh\") text=\"管理员用户名\" ;;\n \"admin.username_prompt.en\") text=\"Admin Username\" ;;\n \"admin.password_prompt.zh\") text=\"管理员密码(留空自动生成,最少 8 位)\" ;;\n \"admin.password_prompt.en\") text=\"Admin Password (leave empty to auto-generate, min 8 chars)\" ;;\n \"admin.password_generated.zh\") text=\" 已自动生成管理员密码\" ;;\n \"admin.password_generated.en\") text=\" Auto-generated admin password\" ;;\n \"admin.password_too_short.zh\") text=\"管理员密码至少需要 8 个字符(MinIO 要求)。当前长度: %s\" ;;\n \"admin.password_too_short.en\") text=\"Admin password must be at least 8 characters (MinIO requirement). Current length: %s\" ;;\n # --- Port Configuration ---\n \"port.title.zh\") text=\"--- 端口配置(按回车使用默认值)---\" ;;\n \"port.title.en\") text=\"--- Port Configuration (press Enter for defaults) ---\" ;;\n \"port.gateway_prompt.zh\") text=\"网关主机端口(容器内 8080)\" ;;\n \"port.gateway_prompt.en\") text=\"Host port for gateway (8080 inside container)\" ;;\n \"port.console_prompt.zh\") text=\"Higress 控制台主机端口(容器内 8001)\" ;;\n \"port.console_prompt.en\") text=\"Host port for Higress console (8001 inside container)\" ;;\n \"port.element_prompt.zh\") text=\"Element Web 直接访问主机端口(容器内 8088)\" ;;\n \"port.element_prompt.en\") text=\"Host port for Element Web direct access (8088 inside container)\" ;;\n \"port.manager_console_prompt.zh\") text=\"Manager 控制台主机端口(容器内 18888)\" ;;\n \"port.manager_console_prompt.en\") text=\"Host port for Manager console (18888 inside container)\" ;;\n \"port.copaw_app_prompt.zh\") text=\"QwenPaw App API 主机端口(容器内 18799)\" ;;\n \"port.copaw_app_prompt.en\") text=\"Host port for QwenPaw App API (18799 inside container)\" ;;\n # --- Local-only binding ---\n \"port.local_only.title.zh\") text=\"--- 网络访问模式 ---\" ;;\n \"port.local_only.title.en\") text=\"--- Network Access Mode ---\" ;;\n \"port.local_only.prompt.zh\") text=\"是否仅允许本机访问(端口绑定到 127.0.0.1)?\" ;;\n \"port.local_only.prompt.en\") text=\"Bind ports to localhost only (127.0.0.1)?\" ;;\n \"port.local_only.hint_yes.zh\") text=\" 仅本机使用,无需开放外部端口(推荐)\" ;;\n \"port.local_only.hint_yes.en\") text=\" Local use only, no external port exposure (recommended)\" ;;\n \"port.local_only.hint_no.zh\") text=\" 允许外部访问(局域网 / 公网)\" ;;\n \"port.local_only.hint_no.en\") text=\" Allow external access (LAN / public network)\" ;;\n \"port.local_only.choice.zh\") text=\"请选择 [1/2]\" ;;\n \"port.local_only.choice.en\") text=\"Enter choice [1/2]\" ;;\n \"port.local_only.selected_local.zh\") text=\"端口已绑定到 127.0.0.1(仅本机访问)\" ;;\n \"port.local_only.selected_local.en\") text=\"Ports bound to 127.0.0.1 (localhost only)\" ;;\n \"port.local_only.selected_external.zh\") text=\"端口已绑定到所有网络接口(0.0.0.0)\" ;;\n \"port.local_only.selected_external.en\") text=\"Ports bound to all interfaces (0.0.0.0)\" ;;\n \"port.local_only.https_hint.zh\") text=\"⚠️ 建议在 Higress 控制台配置 TLS 证书并启用 HTTPS,避免明文传输。\" ;;\n \"port.local_only.https_hint.en\") text=\"⚠️ It is recommended to configure TLS certificates and enable HTTPS in the Higress Console to avoid plaintext transmission.\" ;;\n \"port.local_only.https_docs.zh\") text=\"\" ;;\n \"port.local_only.https_docs.en\") text=\"\" ;;\n # --- Domain Configuration ---\n \"domain.title.zh\") text=\"--- 域名配置(按回车使用默认值)---\" ;;\n \"domain.title.en\") text=\"--- Domain Configuration (press Enter for defaults) ---\" ;;\n \"domain.hint.zh\") text=\"提示: 自定义域名前必须事先做好 DNS 解析。单机 ECS 部署时无需修改 aigw、fs 等域名;Element Web 和 Matrix Server 也可通过 IP 直接访问。\" ;;\n \"domain.hint.en\") text=\"Hint: Configure DNS resolution before customizing domains. For single ECS deployment, no need to change aigw, fs, etc.; Element Web and Matrix Server can also be accessed directly via IP.\" ;;\n \"domain.matrix_prompt.zh\") text=\"Matrix 域名\" ;;\n \"domain.matrix_prompt.en\") text=\"Matrix Domain\" ;;\n \"domain.element_prompt.zh\") text=\"Element Web 域名\" ;;\n \"domain.element_prompt.en\") text=\"Element Web Domain\" ;;\n \"domain.gateway_prompt.zh\") text=\"AI 网关域名\" ;;\n \"domain.gateway_prompt.en\") text=\"AI Gateway Domain\" ;;\n \"domain.fs_prompt.zh\") text=\"文件系统域名\" ;;\n \"domain.fs_prompt.en\") text=\"File System Domain\" ;;\n \"domain.console_prompt.zh\") text=\"Manager 控制台域名\" ;;\n \"domain.console_prompt.en\") text=\"Manager Console Domain\" ;;\n # --- GitHub Integration ---\n \"github.title.zh\") text=\"--- GitHub 集成(可选,按回车跳过)---\" ;;\n \"github.title.en\") text=\"--- GitHub Integration (optional, press Enter to skip) ---\" ;;\n \"github.token_prompt.zh\") text=\"GitHub 个人访问令牌(可选)\" ;;\n \"github.token_prompt.en\") text=\"GitHub Personal Access Token (optional)\" ;;\n # --- Skills Registry ---\n \"skills.title.zh\") text=\"--- Skills 注册中心(可选,按回车使用默认 nacos://market.agentteams.io:80/public)---\" ;;\n \"skills.title.en\") text=\"--- Skills Registry (optional, press Enter for default nacos://market.agentteams.io:80/public) ---\" ;;\n \"skills.url_prompt.zh\") text=\"Skills 注册中心 URL(留空使用默认 nacos://market.agentteams.io:80/public)\" ;;\n \"skills.url_prompt.en\") text=\"Skills Registry URL (leave empty for default nacos://market.agentteams.io:80/public)\" ;;\n # --- Data Persistence ---\n \"data.title.zh\") text=\"--- 数据持久化 ---\" ;;\n \"data.title.en\") text=\"--- Data Persistence ---\" ;;\n \"data.volume_prompt.zh\") text=\"Docker 卷名称 [agentteams-data]\" ;;\n \"data.volume_prompt.en\") text=\"Docker volume name for persistent data [agentteams-data]\" ;;\n \"data.volume_using.zh\") text=\" 使用 Docker 卷: %s\" ;;\n \"data.volume_using.en\") text=\" Using Docker volume: %s\" ;;\n # --- Manager Workspace ---\n \"workspace.title.zh\") text=\"--- Manager 工作空间 ---\" ;;\n \"workspace.title.en\") text=\"--- Manager Workspace ---\" ;;\n \"workspace.dir_prompt.zh\") text=\"Manager 工作空间目录 [%s]\" ;;\n \"workspace.dir_prompt.en\") text=\"Manager workspace directory [%s]\" ;;\n \"workspace.dir_label.zh\") text=\" Manager 工作空间: %s\" ;;\n \"workspace.dir_label.en\") text=\" Manager workspace: %s\" ;;\n # --- Host directory sharing ---\n \"host_share.prompt.zh\") text=\"与 Agent 共享的主机目录(默认: %s)\" ;;\n \"host_share.prompt.en\") text=\"Host directory to share with agents (default: %s)\" ;;\n \"host_share.sharing.zh\") text=\"共享主机目录: %s -> 容器内 /host-share\" ;;\n \"host_share.sharing.en\") text=\"Sharing host directory: %s -> /host-share in container\" ;;\n \"host_share.not_exist.zh\") text=\"警告: 主机目录 %s 不存在,跳过验证继续使用\" ;;\n \"host_share.not_exist.en\") text=\"WARNING: Host directory %s does not exist, using without validation\" ;;\n # --- Default worker runtime ---\n \"worker_runtime.title.zh\") text=\"--- 默认 Worker 运行时 ---\" ;;\n \"worker_runtime.title.en\") text=\"--- Default Worker Runtime ---\" ;;\n \"worker_runtime.openclaw.zh\") text=\"OpenClaw\" ;;\n \"worker_runtime.openclaw.en\") text=\"OpenClaw\" ;;\n \"worker_runtime.copaw.zh\") text=\"QwenPaw\" ;;\n \"worker_runtime.copaw.en\") text=\"QwenPaw\" ;;\n \"worker_runtime.hermes.zh\") text=\"Hermes\" ;;\n \"worker_runtime.hermes.en\") text=\"Hermes\" ;;\n \"worker_runtime.choice.zh\") text=\"请选择 [1/2/3]\" ;;\n \"worker_runtime.choice.en\") text=\"Enter choice [1/2/3]\" ;;\n \"worker_runtime.selected.zh\") text=\"默认 Worker 运行时: %s\" ;;\n \"worker_runtime.selected.en\") text=\"Default Worker runtime: %s\" ;;\n \"worker_runtime.title_short.zh\") text=\"默认 Worker 运行时\" ;;\n \"worker_runtime.title_short.en\") text=\"Default Worker Runtime\" ;;\n \"manager_runtime.title.zh\") text=\"--- Manager 运行时 ---\" ;;\n \"manager_runtime.title.en\") text=\"--- Manager Runtime ---\" ;;\n \"manager_runtime.openclaw.zh\") text=\"OpenClaw\" ;;\n \"manager_runtime.openclaw.en\") text=\"OpenClaw\" ;;\n \"manager_runtime.copaw.zh\") text=\"QwenPaw\" ;;\n \"manager_runtime.copaw.en\") text=\"QwenPaw\" ;;\n \"manager_runtime.choice.zh\") text=\"请选择 [1/2]\" ;;\n \"manager_runtime.choice.en\") text=\"Enter choice [1/2]\" ;;\n \"manager_runtime.selected.zh\") text=\"Manager 运行时: %s\" ;;\n \"manager_runtime.selected.en\") text=\"Manager runtime: %s\" ;;\n \"manager_runtime.title_short.zh\") text=\"Manager 运行时\" ;;\n \"manager_runtime.title_short.en\") text=\"Manager Runtime\" ;;\n # --- Secrets and config ---\n \"install.generating_secrets.zh\") text=\"正在生成密钥...\" ;;\n \"install.generating_secrets.en\") text=\"Generating secrets...\" ;;\n \"install.config_saved.zh\") text=\"配置已保存到 %s\" ;;\n \"install.config_saved.en\") text=\"Configuration saved to %s\" ;;\n # --- Container runtime socket ---\n \"install.socket_detected.zh\") text=\"容器运行时 socket: %s(已启用直接创建 Worker)\" ;;\n \"install.socket_detected.en\") text=\"Container runtime socket: %s (direct Worker creation enabled)\" ;;\n \"install.socket_not_found.zh\") text=\"未找到容器运行时 socket(Manager 无法直接创建 Worker 容器,需要你手动执行 docker 命令创建)\" ;;\n \"install.socket_not_found.en\") text=\"No container runtime socket found (Manager cannot create Worker containers directly, you will need to create them manually using docker commands)\" ;;\n \"install.socket_confirm.title.zh\") text=\"⚠️ 未检测到容器运行时 Socket\" ;;\n \"install.socket_confirm.title.en\") text=\"⚠️ Container Runtime Socket Not Detected\" ;;\n \"install.socket_confirm.message.zh\") text=\"未找到 Docker/Podman socket,Manager 将无法自动创建 Worker 容器。\\n你需要手动执行 docker run 命令来创建 Worker。\\n\\n是否继续安装?\" ;;\n \"install.socket_confirm.message.en\") text=\"Docker/Podman socket not found. Manager will not be able to create Worker containers automatically.\\nYou will need to manually run docker commands to create Workers.\\n\\nContinue installation?\" ;;\n \"install.socket_confirm.prompt.zh\") text=\"继续安装? [y/N]: \" ;;\n \"install.socket_confirm.prompt.en\") text=\"Continue? [y/N]: \" ;;\n \"install.socket_confirm.cancelled.zh\") text=\"安装已取消。如需启用 Worker 自动创建,请确保 Docker/Podman 正在运行,然后重新运行安装脚本。\" ;;\n \"install.socket_confirm.cancelled.en\") text=\"Installation cancelled. To enable automatic Worker creation, ensure Docker/Podman is running and re-run the installer.\" ;;\n # --- Container management ---\n \"install.removing_existing.zh\") text=\"正在移除现有 agentteams-manager 容器...\" ;;\n \"install.removing_existing.en\") text=\"Removing existing agentteams-manager container...\" ;;\n # --- Matrix E2EE ---\n \"matrix_e2ee.title.zh\") text=\"--- Matrix 端到端加密(E2EE)---\" ;;\n \"matrix_e2ee.title.en\") text=\"--- Matrix End-to-End Encryption (E2EE) ---\" ;;\n \"matrix_e2ee.desc.zh\") text=\"E2EE 会对 Manager 与 Worker 之间的 Matrix 消息进行端到端加密。\\n 启用后,即使 Matrix 服务器被入侵,消息内容也无法被窃取。\\n 但 E2EE 会增加首次握手耗时,且要求所有 Agent 都支持 matrix-sdk-crypto。\\n 如果不确定,建议保持禁用。\\n ⚠ 注意:禁用 E2EE 后,请勿在 Element 上创建默认启用加密的 Private 房间,\\n 否则 Agent 将无法读取该房间中的加密消息。请改用 Public 房间或关闭房间加密。\" ;;\n \"matrix_e2ee.desc.en\") text=\"E2EE encrypts Matrix messages between Manager and Workers end-to-end.\\n When enabled, message content stays private even if the Matrix server is compromised.\\n However, E2EE adds overhead to the initial handshake and requires all Agents\\n to support matrix-sdk-crypto. If unsure, keep it disabled.\\n ⚠ Note: When E2EE is disabled, do NOT create Private rooms in Element (which\\n enable encryption by default) — Agents cannot read encrypted messages without\\n E2EE support. Use Public rooms or turn off room encryption instead.\" ;;\n \"matrix_e2ee.enable.zh\") text=\"启用 E2EE\" ;;\n \"matrix_e2ee.enable.en\") text=\"Enable E2EE\" ;;\n \"matrix_e2ee.disable.zh\") text=\"禁用 E2EE(推荐)\" ;;\n \"matrix_e2ee.disable.en\") text=\"Disable E2EE (recommended)\" ;;\n \"matrix_e2ee.choice.zh\") text=\"请选择 [1/2]\" ;;\n \"matrix_e2ee.choice.en\") text=\"Enter choice [1/2]\" ;;\n \"matrix_e2ee.selected_enabled.zh\") text=\"Matrix E2EE: 已启用\" ;;\n \"matrix_e2ee.selected_enabled.en\") text=\"Matrix E2EE: enabled\" ;;\n \"matrix_e2ee.selected_disabled.zh\") text=\"Matrix E2EE: 已禁用(默认)\" ;;\n \"matrix_e2ee.selected_disabled.en\") text=\"Matrix E2EE: disabled (default)\" ;;\n \"matrix_e2ee.title_short.zh\") text=\"Matrix E2EE\" ;;\n \"matrix_e2ee.title_short.en\") text=\"Matrix E2EE\" ;;\n \"matrix_e2ee.val_enabled.zh\") text=\"已启用\" ;;\n \"matrix_e2ee.val_enabled.en\") text=\"enabled\" ;;\n \"matrix_e2ee.val_disabled.zh\") text=\"已禁用\" ;;\n \"matrix_e2ee.val_disabled.en\") text=\"disabled\" ;;\n # --- Docker API proxy ---\n \"docker_proxy.title.zh\") text=\"--- Docker API 安全代理 ---\" ;;\n \"docker_proxy.title.en\") text=\"--- Docker API Security Proxy ---\" ;;\n \"docker_proxy.desc.zh\") text=\"Docker API 代理可防止 AI Agent 通过 Docker API 越狱访问宿主机。\\n 启用后,Manager 不再直接持有 Docker socket,所有容器操作经过安全校验。\" ;;\n \"docker_proxy.desc.en\") text=\"Docker API proxy prevents AI Agents from escaping via Docker API to access the host.\\n When enabled, Manager no longer has direct Docker socket access; all container operations go through security validation.\" ;;\n \"docker_proxy.enable.zh\") text=\"启用(推荐)\" ;;\n \"docker_proxy.enable.en\") text=\"Enable (recommended)\" ;;\n \"docker_proxy.disable.zh\") text=\"禁用(直接挂载 Docker socket)\" ;;\n \"docker_proxy.disable.en\") text=\"Disable (mount Docker socket directly)\" ;;\n \"docker_proxy.choice.zh\") text=\"请选择 [1/2]\" ;;\n \"docker_proxy.choice.en\") text=\"Enter choice [1/2]\" ;;\n \"docker_proxy.selected_enabled.zh\") text=\"Docker API 代理: 已启用\" ;;\n \"docker_proxy.selected_enabled.en\") text=\"Docker API proxy: enabled\" ;;\n \"docker_proxy.selected_disabled.zh\") text=\"Docker API 代理: 已禁用\" ;;\n \"docker_proxy.selected_disabled.en\") text=\"Docker API proxy: disabled\" ;;\n \"docker_proxy.title_short.zh\") text=\"Docker API 代理\" ;;\n \"docker_proxy.title_short.en\") text=\"Docker API Proxy\" ;;\n \"docker_proxy.val_enabled.zh\") text=\"已启用\" ;;\n \"docker_proxy.val_enabled.en\") text=\"enabled\" ;;\n \"docker_proxy.val_disabled.zh\") text=\"已禁用\" ;;\n \"docker_proxy.val_disabled.en\") text=\"disabled\" ;;\n \"docker_proxy.registries_desc.zh\") text=\"默认放行的镜像来源:本地镜像、localhost、Higress 仓库(所有 region)。\\n 如需放行其他镜像仓库,请输入逗号分隔的地址前缀。\\n 示例: ghcr.io/myorg,registry.example.com/team\" ;;\n \"docker_proxy.registries_desc.en\") text=\"Default allowed image sources: local images, localhost, Higress registries (all regions).\\n To allow additional image sources, enter comma-separated address prefixes.\\n Example: ghcr.io/myorg,registry.example.com/team\" ;;\n \"docker_proxy.registries_prompt.zh\") text=\"额外放行的镜像来源(按回车跳过)\" ;;\n \"docker_proxy.registries_prompt.en\") text=\"Additional allowed image sources (press Enter to skip)\" ;;\n \"docker_proxy.registries_label.zh\") text=\"额外放行的镜像来源\" ;;\n \"docker_proxy.registries_label.en\") text=\"Additional allowed image sources\" ;;\n # --- Podman Autostart ---\n \"podman.autostart.title.zh\") text=\"--- Podman 驻留与开机自启 ---\" ;;\n \"podman.autostart.title.en\") text=\"--- Podman Linger & Autostart ---\" ;;\n \"podman.autostart.desc.zh\") text=\"配置为 systemd 服务,支持开机自动启动及后台驻留。\" ;;\n \"podman.autostart.desc.en\") text=\"Configure as a systemd service for automatic startup on boot.\" ;;\n \"podman.autostart.enable.zh\") text=\"启用(推荐)\" ;;\n \"podman.autostart.enable.en\") text=\"Enable (recommended)\" ;;\n \"podman.autostart.disable.zh\") text=\"不启用(容器随当前会话关闭而停止)\" ;;\n \"podman.autostart.disable.en\") text=\"Disable (containers stop when session ends)\" ;;\n \"podman.autostart.choice.zh\") text=\"请选择 [1/2]\" ;;\n \"podman.autostart.choice.en\") text=\"Enter choice [1/2]\" ;;\n \"podman.autostart.selected_enabled.zh\") text=\"Podman 开机自启: 已启用\" ;;\n \"podman.autostart.selected_enabled.en\") text=\"Podman Autostart: enabled\" ;;\n \"podman.autostart.selected_disabled.zh\") text=\"Podman 开机自启: 已禁用\" ;;\n \"podman.autostart.selected_disabled.en\") text=\"Podman Autostart: disabled\" ;;\n \"podman.autostart.title_short.zh\") text=\"Podman 开机自启\" ;;\n \"podman.autostart.title_short.en\") text=\"Podman Autostart\" ;;\n \"podman.autostart.val_enabled.zh\") text=\"已启用\" ;;\n \"podman.autostart.val_enabled.en\") text=\"enabled\" ;;\n \"podman.autostart.val_disabled.zh\") text=\"已禁用\" ;;\n \"podman.autostart.val_disabled.en\") text=\"disabled\" ;;\n \"install.podman.autostart_title.zh\") text=\"正在配置 Podman 容器开机自启...\" ;;\n \"install.podman.autostart_title.en\") text=\"Configuring Podman container autostart...\" ;;\n \"install.podman.linger_enable.zh\") text=\"尝试启用 systemd linger (如有提示请在此输入 sudo 密码)...\" ;;\n \"install.podman.linger_enable.en\") text=\"Attempting to enable systemd linger (enter sudo password if prompted)...\" ;;\n \"install.podman.linger_warn.zh\") text=\"警告: 无法自动启用 linger。驻留可能失败,请手动执行: sudo loginctl enable-linger \\$(whoami)\" ;;\n \"install.podman.linger_warn.en\") text=\"WARNING: Failed to auto-enable linger. Please run manually: sudo loginctl enable-linger \\$(whoami)\" ;;\n \"install.podman.root_setup.zh\") text=\"检测到 root 用户。正在配置系统级 agentteams-podman-restart 服务...\" ;;\n \"install.podman.root_setup.en\") text=\"Root user detected. Configuring system-wide agentteams-podman-restart service...\" ;;\n \"install.podman.root_success.zh\") text=\"✅ 系统级 agentteams-podman-restart 服务已成功启用并运行。\" ;;\n \"install.podman.root_success.en\") text=\"✅ System-wide agentteams-podman-restart service successfully enabled and running.\" ;;\n \"install.podman.root_fail.zh\") text=\"⚠️ 系统级 agentteams-podman-restart 服务启用失败,请稍后手动检查。\" ;;\n \"install.podman.root_fail.en\") text=\"⚠️ Failed to enable system-wide agentteams-podman-restart service. Please check manually later.\" ;;\n \"install.podman.user_setup.zh\") text=\"检测到普通用户 (%s)。正在配置用户级 agentteams-podman-restart 服务...\" ;;\n \"install.podman.user_setup.en\") text=\"Non-root user (%s) detected. Configuring user-level agentteams-podman-restart service...\" ;;\n \"install.podman.user_success.zh\") text=\"✅ 用户级 agentteams-podman-restart 服务已成功启用并运行。\" ;;\n \"install.podman.user_success.en\") text=\"✅ User-level agentteams-podman-restart service successfully enabled and running.\" ;;\n \"install.podman.user_fail.zh\") text=\"⚠️ 用户级 agentteams-podman-restart 服务启用失败。\\n提示:这通常是因为缺少 XDG_RUNTIME_DIR 或 dbus 没有运行。您可以尝试手动执行: systemctl --user enable --now agentteams-podman-restart.service\" ;;\n \"install.podman.user_fail.en\") text=\"⚠️ Failed to enable user-level agentteams-podman-restart service.\\nHint: This is usually due to missing XDG_RUNTIME_DIR or dbus not running. Try manually: systemctl --user enable --now agentteams-podman-restart.service\" ;;\n \"install.podman.success.zh\") text=\"Podman 开机自启配置完成。\" ;;\n \"install.podman.success.en\") text=\"Podman autostart successfully configured.\" ;;\n # --- Worker idle timeout ---\n \"idle_timeout.prompt.zh\") text=\"Worker 空闲自动停止超时(分钟)[720]\" ;;\n \"idle_timeout.prompt.en\") text=\"Worker idle auto-stop timeout in minutes [720]\" ;;\n \"idle_timeout.selected.zh\") text=\"Worker 空闲超时: %s 分钟\" ;;\n \"idle_timeout.selected.en\") text=\"Worker idle timeout: %s minutes\" ;;\n \"idle_timeout.label.zh\") text=\"Worker 空闲超时(分钟)\" ;;\n \"idle_timeout.label.en\") text=\"Worker idle timeout (min)\" ;;\n # --- YOLO mode ---\n \"install.yolo.zh\") text=\"YOLO 模式已启用(自主决策,无交互提示)\" ;;\n \"install.yolo.en\") text=\"YOLO mode enabled (autonomous decisions, no interactive prompts)\" ;;\n # --- Image pulling ---\n \"install.image.exists.zh\") text=\"Manager 镜像已存在: %s\" ;;\n \"install.image.exists.en\") text=\"Manager image already exists locally: %s\" ;;\n \"install.image.pulling_manager.zh\") text=\"正在拉取 Manager 镜像: %s\" ;;\n \"install.image.pulling_manager.en\") text=\"Pulling Manager image: %s\" ;;\n \"install.image.worker_exists.zh\") text=\"Worker 镜像已存在: %s\" ;;\n \"install.image.worker_exists.en\") text=\"Worker image already exists locally: %s\" ;;\n \"install.image.pulling_worker.zh\") text=\"正在拉取 Worker 镜像: %s\" ;;\n \"install.image.pulling_worker.en\") text=\"Pulling Worker image: %s\" ;;\n # --- Starting container ---\n \"install.starting_manager.zh\") text=\"正在启动 Manager 容器...\" ;;\n \"install.starting_manager.en\") text=\"Starting Manager container...\" ;;\n # --- Wait for Manager ready ---\n \"install.wait_ready.zh\") text=\"等待 Manager Agent 就绪(超时: %ss)...\" ;;\n \"install.wait_ready.en\") text=\"Waiting for Manager agent to be ready (timeout: %ss)...\" ;;\n \"install.wait_ready.ok.zh\") text=\"Manager Agent 已就绪!\" ;;\n \"install.wait_ready.ok.en\") text=\"Manager agent is ready!\" ;;\n \"install.wait_ready.waiting.zh\") text=\"等待中... (%ds/%ds)\" ;;\n \"install.wait_ready.waiting.en\") text=\"Waiting... (%ds/%ds)\" ;;\n \"install.wait_ready.timeout.zh\") text=\"Manager Agent 在 %ss 内未就绪。请检查: docker logs %s\" ;;\n \"install.wait_ready.timeout.en\") text=\"Manager agent did not become ready within %ss. Check: docker logs %s\" ;;\n # --- Wait for Matrix ready ---\n \"install.wait_matrix.zh\") text=\"等待 Matrix 服务就绪(超时: %ss)...\" ;;\n \"install.wait_matrix.en\") text=\"Waiting for Matrix server to be ready (timeout: %ss)...\" ;;\n \"install.wait_matrix.ok.zh\") text=\"Matrix 服务已就绪!\" ;;\n \"install.wait_matrix.ok.en\") text=\"Matrix server is ready!\" ;;\n \"install.wait_matrix.waiting.zh\") text=\"等待 Matrix 中... (%ds/%ds)\" ;;\n \"install.wait_matrix.waiting.en\") text=\"Waiting for Matrix... (%ds/%ds)\" ;;\n \"install.wait_matrix.timeout.zh\") text=\"Matrix 服务在 %ss 内未就绪。请检查: docker logs %s\" ;;\n \"install.wait_matrix.timeout.en\") text=\"Matrix server did not become ready within %ss. Check: docker logs %s\" ;;\n # --- OpenAI-compatible connectivity test ---\n \"llm.openai.test.testing.zh\") text=\"正在测试 API 联通性...\" ;;\n \"llm.openai.test.testing.en\") text=\"Testing API connectivity...\" ;;\n \"llm.openai.test.ok.zh\") text=\"✅ API 联通性测试通过\" ;;\n \"llm.openai.test.ok.en\") text=\"✅ API connectivity test passed\" ;;\n \"llm.openai.test.fail.zh\") text=\"⚠️ API 联通性测试失败(HTTP %s)。响应内容:\\n%s\\n请根据以上错误信息联系您的模型服务商解决。\" ;;\n \"llm.openai.test.fail.en\") text=\"⚠️ API connectivity test failed (HTTP %s). Response body:\\n%s\\nPlease contact your model provider to resolve the issue.\" ;;\n \"llm.openai.test.fail.tokenplan.zh\") text=\"⚠️ 提示: 请确认 API Key 有效且已开通通义 Token 套餐。文档: https://help.aliyun.com/zh/model-studio/token-plan-quickstart\" ;;\n \"llm.openai.test.fail.tokenplan.en\") text=\"⚠️ Hint: Verify your Token Plan API key and compatible-mode access. Docs: https://help.aliyun.com/zh/model-studio/token-plan-quickstart\" ;;\n \"llm.openai.test.fail.codingplan.zh\") text=\"⚠️ 提示: 请确认 API Key 有效且已开通通义 Token 套餐。文档: https://help.aliyun.com/zh/model-studio/token-plan-quickstart\" ;;\n \"llm.openai.test.fail.codingplan.en\") text=\"⚠️ Hint: Verify your DASHSCOPE_API_KEY for Qwen Cloud. API keys: https://home.qwencloud.com/api-keys Docs: https://docs.qwencloud.com/\" ;;\n \"llm.openai.test.fail.codingplan_legacy.zh\") text=\"⚠️ 提示: 请确认 API Key 有效且 Coding 套餐接口可用。文档: https://help.aliyun.com/zh/model-studio/get-api-key\" ;;\n \"llm.openai.test.fail.codingplan_legacy.en\") text=\"⚠️ Hint: Verify your DashScope API key and Coding Plan access. Docs: https://help.aliyun.com/zh/model-studio/get-api-key\" ;;\n \"llm.openai.test.no_curl.zh\") text=\"⚠️ 未找到 curl,跳过 API 联通性测试\" ;;\n \"llm.openai.test.no_curl.en\") text=\"⚠️ curl not found, skipping API connectivity test\" ;;\n \"llm.openai.test.confirm.zh\") text=\"是否仍要继续安装?[y/N/b] \" ;;\n \"llm.openai.test.confirm.en\") text=\"Continue with installation anyway? [y/N/b] \" ;;\n \"llm.embedding.title.zh\") text=\"📦 记忆搜索配置\" ;;\n \"llm.embedding.title.en\") text=\"📦 Memory Search Configuration\" ;;\n \"llm.embedding.hint.zh\") text=\" Embedding 模型可提升记忆搜索质量(语义匹配)。不启用也可正常使用记忆功能(关键词匹配)。\" ;;\n \"llm.embedding.hint.en\") text=\" Embedding model improves memory search quality (semantic matching). Memory still works without it (keyword matching).\" ;;\n \"llm.embedding.option.default.zh\") text=\" 1) text-embedding-v4(推荐)\" ;;\n \"llm.embedding.option.default.en\") text=\" 1) text-embedding-v4 (Recommended)\" ;;\n \"llm.embedding.option.custom.zh\") text=\" 2) 自定义 Embedding 模型\" ;;\n \"llm.embedding.option.custom.en\") text=\" 2) Custom embedding model\" ;;\n \"llm.embedding.option.disable.zh\") text=\" 3) 不启用\" ;;\n \"llm.embedding.option.disable.en\") text=\" 3) Do not enable\" ;;\n \"llm.embedding.select.zh\") text=\"选择\" ;;\n \"llm.embedding.select.en\") text=\"Select\" ;;\n \"llm.embedding.custom_prompt.zh\") text=\" Embedding 模型名称\" ;;\n \"llm.embedding.custom_prompt.en\") text=\" Embedding model name\" ;;\n \"llm.embedding.test.testing.zh\") text=\"正在测试 Embedding API 联通性...\" ;;\n \"llm.embedding.test.testing.en\") text=\"Testing Embedding API connectivity...\" ;;\n \"llm.embedding.test.ok.zh\") text=\"✅ Embedding API 联通性测试通过\" ;;\n \"llm.embedding.test.ok.en\") text=\"✅ Embedding API connectivity test passed\" ;;\n \"llm.embedding.test.fail.zh\") text=\"⚠️ Embedding API 测试失败(HTTP %s)。响应: %s\" ;;\n \"llm.embedding.test.fail.en\") text=\"⚠️ Embedding API test failed (HTTP %s). Response: %s\" ;;\n \"llm.embedding.auto_disabled.zh\") text=\"⚠️ Embedding 已自动禁用,记忆搜索将使用关键词匹配。您可以稍后在 agentteams-manager.env 中设置 AGENTTEAMS_EMBEDDING_MODEL 启用。\" ;;\n \"llm.embedding.auto_disabled.en\") text=\"⚠️ Embedding auto-disabled. Memory search will use keyword matching. You can enable it later in agentteams-manager.env by setting AGENTTEAMS_EMBEDDING_MODEL.\" ;;\n \"llm.embedding.disabled.zh\") text=\"ℹ️ Embedding 已禁用,记忆搜索将使用关键词匹配。\" ;;\n \"llm.embedding.disabled.en\") text=\"ℹ️ Embedding disabled. Memory search will use keyword matching.\" ;;\n \"llm.openai.test.aborted.zh\") text=\"安装已中止。\" ;;\n \"llm.openai.test.aborted.en\") text=\"Installation aborted.\" ;;\n \"nav.back_hint.zh\") text=\"(输入 b 返回上一步)\" ;;\n \"nav.back_hint.en\") text=\"(enter b to go back)\" ;;\n # --- OpenAI-compatible provider creation ---\n \"install.openai_compat.missing.zh\") text=\"警告: OpenAI Base URL 或 API Key 未设置,跳过提供商创建\" ;;\n \"install.openai_compat.missing.en\") text=\"WARNING: OpenAI Base URL or API Key not set, skipping provider creation\" ;;\n \"install.openai_compat.creating.zh\") text=\"正在创建 OpenAI 兼容提供商...\" ;;\n \"install.openai_compat.creating.en\") text=\"Creating OpenAI-compatible provider...\" ;;\n \"install.openai_compat.domain.zh\") text=\" 域名: %s\" ;;\n \"install.openai_compat.domain.en\") text=\" Domain: %s\" ;;\n \"install.openai_compat.port.zh\") text=\" 端口: %s\" ;;\n \"install.openai_compat.port.en\") text=\" Port: %s\" ;;\n \"install.openai_compat.protocol.zh\") text=\" 协议: %s\" ;;\n \"install.openai_compat.protocol.en\") text=\" Protocol: %s\" ;;\n \"install.openai_compat.service_fail.zh\") text=\"警告: 创建 DNS 服务源失败(可能已存在)\" ;;\n \"install.openai_compat.service_fail.en\") text=\"WARNING: Failed to create DNS service source (may already exist)\" ;;\n \"install.openai_compat.provider_fail.zh\") text=\"警告: 创建 AI 提供商失败(可能已存在)\" ;;\n \"install.openai_compat.provider_fail.en\") text=\"WARNING: Failed to create AI provider (may already exist)\" ;;\n \"install.openai_compat.success.zh\") text=\"OpenAI 兼容提供商创建成功\" ;;\n \"install.openai_compat.success.en\") text=\"OpenAI-compatible provider created successfully\" ;;\n # --- Welcome message ---\n \"install.welcome_msg.soul_configured.zh\") text=\"Soul 已配置(找到 soul-configured 标记),跳过 onboarding 消息\" ;;\n \"install.welcome_msg.soul_configured.en\") text=\"Soul already configured (soul-configured marker found), skipping onboarding message\" ;;\n \"install.welcome_msg.logging_in.zh\") text=\"正在以 %s 身份登录以发送欢迎消息...\" ;;\n \"install.welcome_msg.logging_in.en\") text=\"Logging in as %s to send welcome message...\" ;;\n \"install.welcome_msg.login_failed.zh\") text=\"警告: 以 %s 身份登录失败,跳过欢迎消息\" ;;\n \"install.welcome_msg.login_failed.en\") text=\"WARNING: Failed to login as %s, skipping welcome message\" ;;\n \"install.welcome_msg.finding_room.zh\") text=\"正在查找与 Manager 的 DM 房间...\" ;;\n \"install.welcome_msg.finding_room.en\") text=\"Finding DM room with Manager...\" ;;\n \"install.welcome_msg.creating_room.zh\") text=\"正在创建与 Manager 的 DM 房间...\" ;;\n \"install.welcome_msg.creating_room.en\") text=\"Creating DM room with Manager...\" ;;\n \"install.welcome_msg.no_room.zh\") text=\"警告: 无法找到或创建与 Manager 的 DM 房间\" ;;\n \"install.welcome_msg.no_room.en\") text=\"WARNING: Could not find or create DM room with Manager\" ;;\n \"install.welcome_msg.waiting_join.zh\") text=\"等待 Manager 加入房间...\" ;;\n \"install.welcome_msg.waiting_join.en\") text=\"Waiting for Manager to join the room...\" ;;\n \"install.welcome_msg.sending.zh\") text=\"正在向 Manager 发送欢迎消息...\" ;;\n \"install.welcome_msg.sending.en\") text=\"Sending welcome message to Manager...\" ;;\n \"install.welcome_msg.send_failed.zh\") text=\"警告: 发送欢迎消息失败\" ;;\n \"install.welcome_msg.send_failed.en\") text=\"WARNING: Failed to send welcome message\" ;;\n \"install.welcome_msg.sent.zh\") text=\"欢迎消息已发送给 Manager\" ;;\n \"install.welcome_msg.sent.en\") text=\"Welcome message sent to Manager\" ;;\n \"install.welcome_msg.waiting.zh\") text=\"等待 Manager 发送欢迎消息(Higress 路由授权 + LLM 探活,约 45-90s)...\" ;;\n \"install.welcome_msg.waiting.en\") text=\"Waiting for Manager to send the welcome message (Higress route auth + LLM probe, ~45-90s)...\" ;;\n \"install.welcome_msg.confirmed.zh\") text=\"Manager 已确认发送欢迎消息(status.welcomeSent=true,用时 %ss)\" ;;\n \"install.welcome_msg.confirmed.en\") text=\"Manager confirmed welcome message sent (status.welcomeSent=true, %ss elapsed)\" ;;\n \"install.welcome_msg.timeout.zh\") text=\"警告: 在 %ss 内未观察到 Manager 发送欢迎消息(status.welcomeSent=true)。安装仍然成功,所有服务已就绪——可继续按下方提示登录 Element Web。\" ;;\n \"install.welcome_msg.timeout.en\") text=\"WARNING: Did not observe the Manager sending its welcome message (status.welcomeSent=true) within %ss. Installation is still successful, all services are up — continue with the Element Web instructions below.\" ;;\n \"install.welcome_msg.timeout_hint.zh\") text=\"手动触发 onboarding: 登录 Element Web → 打开与 Manager 的 DM 房间 → 发送任意一句话(例如 \\\"hi\\\"),Manager 会接管对话并开始引导。\" ;;\n \"install.welcome_msg.timeout_hint.en\") text=\"Manual onboarding: log in to Element Web → open the DM with the Manager → send any message (e.g. \\\"hi\\\") and the Manager will take over and start the guided setup.\" ;;\n \"install.welcome_msg.timeout_inspect.zh\") text=\"排查命令: docker exec agentteams-controller agt get managers default\" ;;\n \"install.welcome_msg.timeout_inspect.en\") text=\"Inspect status: docker exec agentteams-controller agt get managers default\" ;;\n \"install.welcome_msg.poll_unavailable.zh\") text=\"提示: agentteams-manager 内未找到 AgentTeams CLI,跳过 welcome 等待(旧镜像?)\" ;;\n \"install.welcome_msg.poll_unavailable.en\") text=\"Note: AgentTeams CLI not found inside agentteams-manager; skipping welcome wait (old image?)\" ;;\n # --- Dashboard wizard ---\n \"dash.prompt.zh\") text=\"是否安装 agentteams-dashboard 管理面板?\" ;;\n \"dash.prompt.en\") text=\"Install agentteams-dashboard management UI?\" ;;\n \"dash.version_prompt.zh\") text=\"Dashboard 版本\" ;;\n \"dash.version_prompt.en\") text=\"Dashboard version\" ;;\n \"dash.port_prompt.zh\") text=\"Dashboard 端口号\" ;;\n \"dash.port_prompt.en\") text=\"Dashboard port\" ;;\n \"dash.image_prompt.zh\") text=\"Dashboard 镜像\" ;;\n \"dash.image_prompt.en\") text=\"Dashboard image\" ;;\n \"dash.gateway_prompt.zh\") text=\"Higress Console URL (用于共享登录)\" ;;\n \"dash.gateway_prompt.en\") text=\"Higress Console URL (for shared login)\" ;;\n \"dash.skip.zh\") text=\"跳过 Dashboard 安装\" ;;\n \"dash.skip.en\") text=\"Skipping Dashboard installation\" ;;\n \"dash.auto_url.zh\") text=\"自动检测到 Higress Console URL\" ;;\n \"dash.auto_url.en\") text=\"Auto-detected Higress Console URL\" ;;\n \"dash.auto_url_fail.zh\") text=\"无法自动检测 Higress Console URL,请手动输入\" ;;\n \"dash.auto_url_fail.en\") text=\"Cannot auto-detect Higress Console URL, please enter manually\" ;;\n \"dash.summary.zh\") text=\"Dashboard 配置: 端口=%s 镜像=%s\" ;;\n \"dash.summary.en\") text=\"Dashboard config: port=%s image=%s\" ;;\n # --- Dashboard success ---\n \"success.dashboard.zh\") text=\" agentteams-dashboard 管理面板: http://localhost:%s/\" ;;\n \"success.dashboard.en\") text=\" agentteams-dashboard: http://localhost:%s/\" ;;\n # --- Final output panel ---\n \"success.title.zh\") text=\"=== AgentTeams Manager 已启动!===\" ;;\n \"success.title.en\") text=\"=== AgentTeams Manager Started! ===\" ;;\n \"success.domains_configured.zh\") text=\"以下域名已配置解析到 127.0.0.1:\" ;;\n \"success.domains_configured.en\") text=\"The following domains are configured to resolve to 127.0.0.1:\" ;;\n \"success.open_url.zh\") text=\" ★ 在浏览器中打开以下 URL 开始使用: ★\" ;;\n \"success.open_url.en\") text=\" ★ Open the following URL in your browser to start: ★\" ;;\n \"success.login_with.zh\") text=\" 登录信息:\" ;;\n \"success.login_with.en\") text=\" Login with:\" ;;\n \"success.username.zh\") text=\" 用户名: %s\" ;;\n \"success.username.en\") text=\" Username: %s\" ;;\n \"success.password.zh\") text=\" 密码: %s\" ;;\n \"success.password.en\") text=\" Password: %s\" ;;\n \"success.after_login.zh\") text=\" 登录后,开始与 Manager 聊天!\" ;;\n \"success.after_login.en\") text=\" After login, start chatting with the Manager!\" ;;\n \"success.tell_it.zh\") text=\" 告诉它: \\\"创建一个名为 alice 的前端开发 Worker\\\"\" ;;\n \"success.tell_it.en\") text=\" Tell it: \\\"Create a Worker named alice for frontend dev\\\"\" ;;\n \"success.manager_auto.zh\") text=\" Manager 会自动处理一切。\" ;;\n \"success.manager_auto.en\") text=\" The Manager will handle everything automatically.\" ;;\n \"success.mobile_title.zh\") text=\" 📱 移动端访问(FluffyChat / Element Mobile):\" ;;\n \"success.mobile_title.en\") text=\" 📱 Mobile access (FluffyChat / Element Mobile):\" ;;\n \"success.mobile_step1.zh\") text=\" 1. 在手机上下载 FluffyChat 或 Element\" ;;\n \"success.mobile_step1.en\") text=\" 1. Download FluffyChat or Element on your phone\" ;;\n \"success.mobile_step2.zh\") text=\" 2. 设置 homeserver 为: %s\" ;;\n \"success.mobile_step2.en\") text=\" 2. Set homeserver to: %s\" ;;\n \"success.mobile_step2_noip.zh\") text=\" 2. 设置 homeserver 为: http://<本机局域网IP>:%s\" ;;\n \"success.mobile_step2_noip.en\") text=\" 2. Set homeserver to: http://<this-machine-LAN-IP>:%s\" ;;\n \"success.mobile_noip_hint.zh\") text=\" (无法自动检测局域网 IP — 请使用 ifconfig / ip addr 查看)\" ;;\n \"success.mobile_noip_hint.en\") text=\" (Could not detect LAN IP automatically — check with: ifconfig / ip addr)\" ;;\n \"success.mobile_step3.zh\") text=\" 3. 登录信息:\" ;;\n \"success.mobile_step3.en\") text=\" 3. Login with:\" ;;\n \"success.mobile_username.zh\") text=\" 用户名: %s\" ;;\n \"success.mobile_username.en\") text=\" Username: %s\" ;;\n \"success.mobile_password.zh\") text=\" 密码: %s\" ;;\n \"success.mobile_password.en\") text=\" Password: %s\" ;;\n # --- Other consoles and tips ---\n \"success.other_consoles.zh\") text=\"--- 其他控制台 ---\" ;;\n \"success.other_consoles.en\") text=\"--- Other Consoles ---\" ;;\n \"success.higress_console.zh\") text=\" Higress 控制台: http://localhost:%s(用户名: %s / 密码: %s)\" ;;\n \"success.higress_console.en\") text=\" Higress Console: http://localhost:%s (Username: %s / Password: %s)\" ;;\n \"success.manager_console.zh\") text=\" Manager 控制台(本地): http://localhost:%s(无需登录)\" ;;\n \"success.manager_console.en\") text=\" Manager Console (local): http://localhost:%s (no login required)\" ;;\n \"success.manager_console_gateway.zh\") text=\" Manager 控制台(网关): http://console-local.agentteams.io(用户名: %s / 密码: %s)\" ;;\n \"success.manager_console_gateway.en\") text=\" Manager Console (gateway): http://console-local.agentteams.io (Username: %s / Password: %s)\" ;;\n \"success.copaw_console.zh\") text=\" QwenPaw App API: http://localhost:%s(无需登录)\" ;;\n \"success.copaw_console.en\") text=\" QwenPaw App API: http://localhost:%s (no login required)\" ;;\n \"success.switch_llm.title.zh\") text=\"--- 切换 LLM 提供商 ---\" ;;\n \"success.switch_llm.title.en\") text=\"--- Switch LLM Providers ---\" ;;\n \"success.switch_llm.hint.zh\") text=\" 您可以通过 Higress 控制台切换到其他 LLM 提供商(OpenAI、Anthropic 等)。\" ;;\n \"success.switch_llm.hint.en\") text=\" You can switch to other LLM providers (OpenAI, Anthropic, etc.) via Higress Console.\" ;;\n \"success.switch_llm.docs.zh\") text=\" 详细说明请参阅:\" ;;\n \"success.switch_llm.docs.en\") text=\" For detailed instructions, see:\" ;;\n \"success.switch_llm.url.zh\") text=\" https://higress.ai/en/docs/ai/scene-guide/multi-proxy#console-configuration\" ;;\n \"success.switch_llm.url.en\") text=\" https://higress.ai/en/docs/ai/scene-guide/multi-proxy#console-configuration\" ;;\n \"success.tip.zh\") text=\"提示: 您也可以在聊天中让 Manager 为您配置 LLM 提供商。\" ;;\n \"success.tip.en\") text=\"Tip: You can also ask the Manager to configure LLM providers for you in the chat.\" ;;\n \"success.config_file.zh\") text=\"配置文件: %s\" ;;\n \"success.config_file.en\") text=\"Configuration file: %s\" ;;\n \"success.data_volume.zh\") text=\"数据卷: %s\" ;;\n \"success.data_volume.en\") text=\"Data volume: %s\" ;;\n \"success.workspace.zh\") text=\"Manager 工作空间: %s\" ;;\n \"success.workspace.en\") text=\"Manager workspace: %s\" ;;\n # --- Worker installation ---\n \"worker.resetting.zh\") text=\"正在重置 Worker: %s...\" ;;\n \"worker.resetting.en\") text=\"Resetting Worker: %s...\" ;;\n \"worker.exists.zh\") text=\"容器 '%s' 已存在。使用 --reset 重新创建。\" ;;\n \"worker.exists.en\") text=\"Container '%s' already exists. Use --reset to recreate.\" ;;\n \"worker.starting.zh\") text=\"正在启动 Worker: %s...\" ;;\n \"worker.starting.en\") text=\"Starting Worker: %s...\" ;;\n \"worker.skills_url.zh\") text=\" Skills API URL: %s\" ;;\n \"worker.skills_url.en\") text=\" Skills API URL: %s\" ;;\n \"worker.started.zh\") text=\"=== Worker %s 已启动!===\" ;;\n \"worker.started.en\") text=\"=== Worker %s Started! ===\" ;;\n \"worker.container.zh\") text=\"容器: %s\" ;;\n \"worker.container.en\") text=\"Container: %s\" ;;\n \"worker.view_logs.zh\") text=\"查看日志: docker logs -f %s\" ;;\n \"worker.view_logs.en\") text=\"View logs: docker logs -f %s\" ;;\n # --- Prompt function messages ---\n \"prompt.preset.zh\") text=\" %s = (已通过环境变量预设)\" ;;\n \"prompt.preset.en\") text=\" %s = (pre-set via env)\" ;;\n \"prompt.upgrade_keep.zh\") text=\" %s = %s(当前值,回车保留 / 输入新值覆盖)\" ;;\n \"prompt.upgrade_keep.en\") text=\" %s = %s (current value, press Enter to keep / type new value to change)\" ;;\n \"prompt.upgrade_keep_secret.zh\") text=\" %s = %s(当前值,回车保留 / 输入新值覆盖)\" ;;\n \"prompt.upgrade_keep_secret.en\") text=\" %s = %s (current value, press Enter to keep / type new value to change)\" ;;\n \"prompt.upgrade_empty.zh\") text=\" %s = (未设置,回车跳过 / 输入新值设置)\" ;;\n \"prompt.upgrade_empty.en\") text=\" %s = (not set, press Enter to skip / type new value to set)\" ;;\n \"prompt.default.zh\") text=\" %s = %s(默认)\" ;;\n \"prompt.default.en\") text=\" %s = %s (default)\" ;;\n \"prompt.required.zh\") text=\"%s 是必需的(在非交互模式下通过环境变量设置)\" ;;\n \"prompt.required.en\") text=\"%s is required (set via environment variable in non-interactive mode)\" ;;\n \"prompt.required_empty.zh\") text=\"%s 是必需的\" ;;\n \"prompt.required_empty.en\") text=\"%s is required\" ;;\n # --- Language switch prompt (bilingual by design) ---\n \"lang.detected.zh\") text=\"检测到语言 / Detected language: 中文\" ;;\n \"lang.detected.en\") text=\"检测到语言 / Detected language: English\" ;;\n \"lang.switch_title.zh\") text=\"切换语言 / Switch language:\" ;;\n \"lang.switch_title.en\") text=\"切换语言 / Switch language:\" ;;\n \"lang.option_zh.zh\") text=\" 1) 中文\" ;;\n \"lang.option_zh.en\") text=\" 1) 中文\" ;;\n \"lang.option_en.zh\") text=\" 2) English\" ;;\n \"lang.option_en.en\") text=\" 2) English\" ;;\n \"lang.prompt.zh\") text=\"请选择 / Enter choice\" ;;\n \"lang.prompt.en\") text=\"请选择 / Enter choice\" ;;\n # --- Uninstall messages ---\n \"uninstall.title.zh\") text=\"正在卸载 AgentTeams...\" ;;\n \"uninstall.title.en\") text=\"Uninstalling AgentTeams...\" ;;\n \"uninstall.stopping_manager.zh\") text=\"正在停止并移除 agentteams-manager...\" ;;\n \"uninstall.stopping_manager.en\") text=\"Stopping and removing agentteams-manager...\" ;;\n \"uninstall.stopping_workers.zh\") text=\"正在停止并移除 Worker 容器...\" ;;\n \"uninstall.stopping_workers.en\") text=\"Stopping and removing worker containers...\" ;;\n \"uninstall.removed.zh\") text=\" 已移除: %s\" ;;\n \"uninstall.removed.en\") text=\" Removed: %s\" ;;\n \"uninstall.removing_volume.zh\") text=\"正在移除 Docker 卷: %s\" ;;\n \"uninstall.removing_volume.en\") text=\"Removing Docker volume: %s\" ;;\n \"uninstall.removing_env.zh\") text=\"正在移除 env 文件: %s\" ;;\n \"uninstall.removing_env.en\") text=\"Removing env file: %s\" ;;\n \"uninstall.removing_proxy.zh\") text=\"正在停止并移除 Docker API 代理容器: agentteams-docker-proxy\" ;;\n \"uninstall.removing_proxy.en\") text=\"Stopping and removing Docker API proxy container: agentteams-docker-proxy\" ;;\n \"uninstall.stopping_controller.zh\") text=\"正在停止并移除 agentteams-controller (内嵌 Tuwunel/MinIO/Higress)...\" ;;\n \"uninstall.stopping_controller.en\") text=\"Stopping and removing agentteams-controller (embedded Tuwunel/MinIO/Higress)...\" ;;\n \"uninstall.removing_network.zh\") text=\"正在移除 Docker 网络: agentteams-net\" ;;\n \"uninstall.removing_network.en\") text=\"Removing Docker network: agentteams-net\" ;;\n \"uninstall.removing_workspace.zh\") text=\"正在移除工作空间目录: %s\" ;;\n \"uninstall.removing_workspace.en\") text=\"Removing workspace directory: %s\" ;;\n \"uninstall.removing_workspace_elevated.zh\") text=\" 工作空间包含 root 文件,通过容器清理...\" ;;\n \"uninstall.removing_workspace_elevated.en\") text=\" Workspace contains root-owned files, cleaning via container...\" ;;\n \"uninstall.removing_log.zh\") text=\"正在移除日志文件: %s\" ;;\n \"uninstall.removing_log.en\") text=\"Removing log file: %s\" ;;\n \"uninstall.done.zh\") text=\"AgentTeams 已卸载。\" ;;\n \"uninstall.done.en\") text=\"AgentTeams has been uninstalled.\" ;;\n # --- Error messages ---\n \"error.name_required.zh\") text=\"--name 是必需的\" ;;\n \"error.name_required.en\") text=\"--name is required\" ;;\n \"error.fs_required.zh\") text=\"--fs 是必需的\" ;;\n \"error.fs_required.en\") text=\"--fs is required\" ;;\n \"error.fs_key_required.zh\") text=\"--fs-key 是必需的\" ;;\n \"error.fs_key_required.en\") text=\"--fs-key is required\" ;;\n \"error.fs_secret_required.zh\") text=\"--fs-secret 是必需的\" ;;\n \"error.fs_secret_required.en\") text=\"--fs-secret is required\" ;;\n \"error.unknown_option.zh\") text=\"未知选项: %s\" ;;\n \"error.unknown_option.en\") text=\"Unknown option: %s\" ;;\n \"error.docker_not_found.zh\") text=\"未找到 docker 或 podman 命令。请先安装 Docker Desktop 或 Podman Desktop:\\n Docker Desktop: https://www.docker.com/products/docker-desktop/\\n Podman Desktop: https://podman-desktop.io/\" ;;\n \"error.docker_not_found.en\") text=\"docker or podman command not found. Please install Docker Desktop or Podman Desktop first:\\n Docker Desktop: https://www.docker.com/products/docker-desktop/\\n Podman Desktop: https://podman-desktop.io/\" ;;\n \"error.docker_not_running.zh\") text=\"Docker 未运行。请先启动 Docker Desktop 或 Podman Desktop。\" ;;\n \"error.docker_not_running.en\") text=\"Docker is not running. Please start Docker Desktop or Podman Desktop first.\" ;;\n # --- Fallback: try English for unknown lang ---\n *)\n case \"${key}.en\" in\n \"tz.warning.title.en\") text=\"Could not detect timezone automatically.\" ;;\n \"install.title.en\") text=\"=== AgentTeams Manager Installation ===\" ;;\n *) text=\"${key}\" ;;\n esac\n ;;\n esac\n if [ $# -gt 0 ]; then\n # shellcheck disable=SC2059\n printf \"${text}\\n\" \"$@\"\n else\n echo \"${text}\"\n fi\n}\n\n# ============================================================\n# Registry selection based on timezone\n# ============================================================\n\ndetect_registry() {\n local tz=\"${AGENTTEAMS_TIMEZONE}\"\n\n case \"${tz}\" in\n America/*)\n echo \"higress-registry.us-west-1.cr.aliyuncs.com\"\n ;;\n Asia/Singapore|Asia/Bangkok|Asia/Jakarta|Asia/Makassar|Asia/Jayapura|\\\n Asia/Kuala_Lumpur|Asia/Ho_Chi_Minh|Asia/Manila|Asia/Yangon|\\\n Asia/Vientiane|Asia/Phnom_Penh|Asia/Pontianak|Asia/Ujung_Pandang)\n echo \"higress-registry.ap-southeast-7.cr.aliyuncs.com\"\n ;;\n *)\n echo \"higress-registry.cn-hangzhou.cr.aliyuncs.com\"\n ;;\n esac\n}\n\nAGENTTEAMS_REGISTRY=\"${AGENTTEAMS_REGISTRY:-$(detect_registry)}\"\n# Backward compatibility: accept old env var names from previous versions\nAGENTTEAMS_INSTALL_CONTROLLER_IMAGE=\"${AGENTTEAMS_INSTALL_CONTROLLER_IMAGE:-${AGENTTEAMS_INSTALL_DOCKER_PROXY_IMAGE:-}}\"\n# Image variables are resolved after version selection in step_version().\n# These placeholders allow early code paths to reference them without errors.\nMANAGER_IMAGE=\"${AGENTTEAMS_INSTALL_MANAGER_IMAGE:-}\"\nMANAGER_COPAW_IMAGE=\"${AGENTTEAMS_INSTALL_MANAGER_COPAW_IMAGE:-}\"\nWORKER_IMAGE=\"${AGENTTEAMS_INSTALL_WORKER_IMAGE:-}\"\nCOPAW_WORKER_IMAGE=\"${AGENTTEAMS_INSTALL_COPAW_WORKER_IMAGE:-}\"\nQWENPAW_WORKER_IMAGE=\"${AGENTTEAMS_INSTALL_QWENPAW_WORKER_IMAGE:-}\"\nHERMES_WORKER_IMAGE=\"${AGENTTEAMS_INSTALL_HERMES_WORKER_IMAGE:-}\"\nCONTROLLER_IMAGE=\"${AGENTTEAMS_INSTALL_CONTROLLER_IMAGE:-}\"\n\nresolve_image_tags() {\n AGENTTEAMS_VERSION=\"$(_normalize_version \"${AGENTTEAMS_VERSION}\")\"\n MANAGER_IMAGE=\"${AGENTTEAMS_INSTALL_MANAGER_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-manager:${AGENTTEAMS_VERSION}}\"\n MANAGER_COPAW_IMAGE=\"${AGENTTEAMS_INSTALL_MANAGER_COPAW_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-manager-copaw:${AGENTTEAMS_VERSION}}\"\n WORKER_IMAGE=\"${AGENTTEAMS_INSTALL_WORKER_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-worker:${AGENTTEAMS_VERSION}}\"\n COPAW_WORKER_IMAGE=\"${AGENTTEAMS_INSTALL_COPAW_WORKER_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-copaw-worker:${AGENTTEAMS_VERSION}}\"\n QWENPAW_WORKER_IMAGE=\"${AGENTTEAMS_INSTALL_QWENPAW_WORKER_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-qwenpaw-worker:${AGENTTEAMS_VERSION}}\"\n HERMES_WORKER_IMAGE=\"${AGENTTEAMS_INSTALL_HERMES_WORKER_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-hermes-worker:${AGENTTEAMS_VERSION}}\"\n EMBEDDED_IMAGE=\"${AGENTTEAMS_INSTALL_EMBEDDED_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-embedded:${AGENTTEAMS_VERSION}}\"\n # CoPaw Worker introduced in v1.0.4; Hermes Worker introduced in v1.1.0\n if [ -z \"${AGENTTEAMS_INSTALL_COPAW_WORKER_IMAGE:-}\" ] && _ver_lt \"${AGENTTEAMS_VERSION}\" \"v1.0.4\"; then\n COPAW_WORKER_IMAGE=\"\"\n fi\n if [ -z \"${AGENTTEAMS_INSTALL_HERMES_WORKER_IMAGE:-}\" ] && _ver_lt \"${AGENTTEAMS_VERSION}\" \"v1.1.0\"; then\n HERMES_WORKER_IMAGE=\"\"\n fi\n}\n\n# Resolve the embedded controller image. Embedded mode is the only supported\n# architecture since PR #616 (manager image no longer bundles Higress/Tuwunel/MinIO).\n# If the embedded image is unavailable for the requested version, fail fast with an\n# actionable error rather than silently falling back to the legacy single-container\n# path — that path is permanently broken with the slim manager image and would just\n# leave the user with a manager container looping on \"Waiting for Higress Gateway\".\n# Sets EMBEDDED_IMAGE and AGENTTEAMS_USE_EMBEDDED.\nresolve_embedded_image() {\n AGENTTEAMS_USE_EMBEDDED=1\n\n # If the user explicitly overrode the image (e.g. `make install-embedded` passes\n # a locally-built tag), respect it as-is without any registry probe.\n if [ -n \"${AGENTTEAMS_INSTALL_EMBEDDED_IMAGE:-}\" ]; then\n EMBEDDED_IMAGE=\"${AGENTTEAMS_INSTALL_EMBEDDED_IMAGE}\"\n return 0\n fi\n\n local _versioned=\"${AGENTTEAMS_REGISTRY}/agentteams/agentteams-embedded:${AGENTTEAMS_VERSION}\"\n local _latest=\"${AGENTTEAMS_REGISTRY}/agentteams/agentteams-embedded:latest\"\n\n # Skip probe when AGENTTEAMS_VERSION is \"latest\" — no point trying the same tag twice.\n if [ \"${AGENTTEAMS_VERSION}\" = \"latest\" ]; then\n EMBEDDED_IMAGE=\"${_latest}\"\n return 0\n fi\n\n if ${DOCKER_CMD} pull \"${_versioned}\" >/dev/null 2>&1; then\n EMBEDDED_IMAGE=\"${_versioned}\"\n return 0\n fi\n\n # Versions before v1.1.0 predate agentteams-embedded entirely — their manager image\n # bundled all infrastructure. Falling back to agentteams-embedded:latest would\n # silently swap in the v1.1.0 architecture (embedded kube-apiserver) which\n # crashes under QEMU on Apple Silicon. Auto-activate legacy mode instead.\n if _ver_lt \"${AGENTTEAMS_VERSION}\" \"v1.1.0\"; then\n log \"INFO: ${AGENTTEAMS_VERSION} predates agentteams-embedded; switching to legacy all-in-one manager architecture.\"\n log \"WARNING: Legacy all-in-one mode requires AGENTTEAMS_VERSION <= v1.0.9 (older bundled manager image).\"\n log \"WARNING: Newer slim manager images will hang on 'Waiting for Higress Gateway'.\"\n AGENTTEAMS_USE_EMBEDDED=0\n return 0\n fi\n\n if ${DOCKER_CMD} pull \"${_latest}\" >/dev/null 2>&1; then\n log \"embedded ${AGENTTEAMS_VERSION} not found, using latest\"\n EMBEDDED_IMAGE=\"${_latest}\"\n return 0\n fi\n\n # Explicit escape hatch — still honoured for edge cases.\n if [ \"${AGENTTEAMS_FORCE_LEGACY:-0}\" = \"1\" ]; then\n log \"WARNING: AGENTTEAMS_FORCE_LEGACY=1 — using legacy all-in-one manager architecture.\"\n log \"WARNING: This requires AGENTTEAMS_VERSION <= v1.0.9 (older bundled manager image).\"\n log \"WARNING: Newer slim manager images will hang on 'Waiting for Higress Gateway'.\"\n AGENTTEAMS_USE_EMBEDDED=0\n return 0\n fi\n\n error \"Embedded controller image is not available in the registry:\"\n error \" - tried: ${_versioned}\"\n error \" - tried: ${_latest}\"\n error \"\"\n error \"Embedded mode is the only supported architecture since PR #616.\"\n error \"How to resolve:\"\n error \" 1) Pin to a AGENTTEAMS_VERSION whose embedded image has been published, or\"\n error \" wait for the release pipeline to publish it.\"\n error \" 2) For a local build, run: make install-embedded\"\n error \" (builds and uses the local embedded image without touching the registry).\"\n error \" 3) Override with a custom image: AGENTTEAMS_INSTALL_EMBEDDED_IMAGE=... ./agentteams-install.sh\"\n exit 1\n}\n\n# ============================================================\n# Known models list — used to detect custom models during install\n# ============================================================\nKNOWN_MODELS=\"gpt-5.4 gpt-5.3-codex gpt-5-mini gpt-5-nano claude-opus-4-6 claude-sonnet-4-6 claude-haiku-4-5 qwen3.6-plus qwen3.5-plus deepseek-chat deepseek-reasoner kimi-k2.5 glm-5 MiniMax-M2.7 MiniMax-M2.7-highspeed MiniMax-M2.5\"\n\nis_known_model() {\n local model=\"$1\"\n for m in ${KNOWN_MODELS}; do\n [ \"${m}\" = \"${model}\" ] && return 0\n done\n return 1\n}\n\n# Prompt user for custom model parameters when model is not in the known list.\n# Sets: AGENTTEAMS_MODEL_CONTEXT_WINDOW, AGENTTEAMS_MODEL_MAX_TOKENS, AGENTTEAMS_MODEL_REASONING, AGENTTEAMS_MODEL_VISION\nprompt_custom_model_params() {\n local model=\"$1\"\n if is_known_model \"${model}\"; then\n # Clear any stale custom params for known models\n AGENTTEAMS_MODEL_CONTEXT_WINDOW=\"\"\n AGENTTEAMS_MODEL_MAX_TOKENS=\"\"\n AGENTTEAMS_MODEL_REASONING=\"\"\n AGENTTEAMS_MODEL_VISION=\"\"\n return\n fi\n echo \"\"\n log \"$(msg llm.custom_model.detected \"${model}\")\"\n echo \"\"\n read -e -p \" $(msg llm.custom_model.context_prompt): \" AGENTTEAMS_MODEL_CONTEXT_WINDOW\n if [ \"${AGENTTEAMS_MODEL_CONTEXT_WINDOW}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n AGENTTEAMS_MODEL_CONTEXT_WINDOW=\"${AGENTTEAMS_MODEL_CONTEXT_WINDOW:-150000}\"\n read -e -p \" $(msg llm.custom_model.max_tokens_prompt): \" AGENTTEAMS_MODEL_MAX_TOKENS\n if [ \"${AGENTTEAMS_MODEL_MAX_TOKENS}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n AGENTTEAMS_MODEL_MAX_TOKENS=\"${AGENTTEAMS_MODEL_MAX_TOKENS:-128000}\"\n read -e -p \" $(msg llm.custom_model.reasoning_prompt): \" _reasoning\n if [ \"${_reasoning}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n case \"${_reasoning}\" in\n n|N|no|NO) AGENTTEAMS_MODEL_REASONING=\"false\" ;;\n *) AGENTTEAMS_MODEL_REASONING=\"true\" ;;\n esac\n read -e -p \" $(msg llm.custom_model.vision_prompt): \" _vision\n if [ \"${_vision}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n case \"${_vision}\" in\n y|Y|yes|YES) AGENTTEAMS_MODEL_VISION=\"true\" ;;\n *) AGENTTEAMS_MODEL_VISION=\"false\" ;;\n esac\n log \"$(msg llm.custom_model.summary \"${AGENTTEAMS_MODEL_CONTEXT_WINDOW}\" \"${AGENTTEAMS_MODEL_MAX_TOKENS}\" \"${AGENTTEAMS_MODEL_REASONING}\" \"${AGENTTEAMS_MODEL_VISION}\")\"\n}\n\n# ============================================================\n# Wait for Manager agent to be ready\n# Uses `openclaw gateway health` inside the container to confirm the gateway is running\n# ============================================================\n\nwait_manager_ready() {\n local timeout=\"${AGENTTEAMS_READY_TIMEOUT:-300}\"\n local elapsed=0\n local container=\"${1:-agentteams-manager}\"\n\n log \"$(msg install.wait_ready \"${timeout}\")\"\n\n # Wait for Manager agent to be healthy inside the container\n local runtime=\"${AGENTTEAMS_MANAGER_RUNTIME:-copaw}\"\n while [ \"${elapsed}\" -lt \"${timeout}\" ]; do\n case \"${runtime}\" in\n copaw)\n if ${DOCKER_CMD} exec \"${container}\" curl -sf http://127.0.0.1:18799/api/agents 2>/dev/null | grep -q '\"agents\"'; then\n log \"$(msg install.wait_ready.ok)\"\n return 0\n fi\n ;;\n *)\n if ${DOCKER_CMD} exec \"${container}\" openclaw gateway health --json 2>/dev/null | grep -q '\"ok\"' 2>/dev/null; then\n log \"$(msg install.wait_ready.ok)\"\n return 0\n fi\n ;;\n esac\n sleep 5\n elapsed=$((elapsed + 5))\n printf \"\\r\\033[36m[AgentTeams]\\033[0m $(msg install.wait_ready.waiting \"${elapsed}\" \"${timeout}\")\"\n done\n\n echo \"\"\n die \"$(msg install.wait_ready.timeout \"${timeout}\" \"${container}\")\"\n}\n\nwait_matrix_ready() {\n local timeout=\"${AGENTTEAMS_READY_TIMEOUT:-300}\"\n local elapsed=0\n local container=\"${1:-agentteams-manager}\"\n\n log \"$(msg install.wait_matrix \"${timeout}\")\"\n\n while [ \"${elapsed}\" -lt \"${timeout}\" ]; do\n if ${DOCKER_CMD} exec \"${container}\" curl -sf http://127.0.0.1:6167/_tuwunel/server_version >/dev/null 2>&1; then\n log \"$(msg install.wait_matrix.ok)\"\n return 0\n fi\n sleep 5\n elapsed=$((elapsed + 5))\n printf \"\\r\\033[36m[AgentTeams]\\033[0m $(msg install.wait_matrix.waiting \"${elapsed}\" \"${timeout}\")\"\n done\n\n echo \"\"\n die \"$(msg install.wait_matrix.timeout \"${timeout}\" \"${container}\")\"\n}\n\n# Read KEY=value from /data/agentteams-secrets.env on a Docker volume (manager container not required).\n# Requires EMBEDDED_IMAGE (resolved earlier in install_manager). Uses ${DOCKER_CMD}.\nagentteams_read_secret_from_data_volume() {\n local _vol=\"$1\" _key=\"$2\"\n if [ -z \"${_vol}\" ] || [ -z \"${_key}\" ] || [ -z \"${EMBEDDED_IMAGE:-}\" ]; then\n echo \"\"\n return 0\n fi\n ${DOCKER_CMD} run --rm --entrypoint sh \\\n -v \"${_vol}:/data:ro\" \\\n \"${EMBEDDED_IMAGE}\" -c \"grep \\\"^${_key}=\\\" /data/agentteams-secrets.env 2>/dev/null | cut -d= -f2- | head -1 | tr -d '\\r'\" 2>/dev/null\n}\n\n# Read KEY=value from /data/worker-creds/<worker>.env on a Docker volume.\nagentteams_read_worker_creds_value_from_volume() {\n local _vol=\"$1\" _worker=\"$2\" _key=\"$3\"\n if [ -z \"${_vol}\" ] || [ -z \"${_worker}\" ] || [ -z \"${_key}\" ] || [ -z \"${EMBEDDED_IMAGE:-}\" ]; then\n echo \"\"\n return 0\n fi\n ${DOCKER_CMD} run --rm --entrypoint sh \\\n -v \"${_vol}:/data:ro\" \\\n \"${EMBEDDED_IMAGE}\" -c \"grep \\\"^${_key}=\\\" \\\"/data/worker-creds/${_worker}.env\\\" 2>/dev/null | cut -d= -f2- | head -1 | tr -d \\\"\\\\r\\\"\" 2>/dev/null\n}\n\n# Read admin_dm_room_id from host workspace state.json (fallback when Matrix API is unavailable).\nagentteams_read_admin_dm_room_from_workspace() {\n local _ws=\"$1\"\n local _f=\"${_ws}/state.json\"\n if [ ! -f \"${_f}\" ] || ! command -v jq >/dev/null 2>&1; then\n echo \"\"\n return 0\n fi\n jq -r '.admin_dm_room_id // empty | select(. != \"null\")' \"${_f}\" 2>/dev/null\n}\n\n# Read secret input with masked echo (shows * per keystroke, supports backspace)\n# Usage: read_secret \"prompt text: \"; value=\"${_RS_RESULT}\"\nread_secret() {\n local _rs_prompt=\"$1\"\n _RS_RESULT=\"\"\n local _rs_char=\"\"\n\n printf \"%s\" \"${_rs_prompt}\"\n\n while IFS= read -r -s -n 1 _rs_char; do\n if [[ -z \"${_rs_char}\" ]]; then\n break\n elif [[ \"${_rs_char}\" == $'\\177' ]] || [[ \"${_rs_char}\" == $'\\b' ]]; then\n if [ -n \"${_RS_RESULT}\" ]; then\n _RS_RESULT=\"${_RS_RESULT%?}\"\n printf \"\\b \\b\"\n fi\n else\n _RS_RESULT=\"${_RS_RESULT}${_rs_char}\"\n printf \"*\"\n fi\n done\n echo\n}\n\n# Load current parameter values from env file for upgrade mode display\nload_current_params_from_env() {\n local env_file=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n if [ -f \"${env_file}\" ]; then\n [ -z \"${AGENTTEAMS_LLM_PROVIDER:+x}\" ] && AGENTTEAMS_LLM_PROVIDER=\"$(grep '^AGENTTEAMS_LLM_PROVIDER=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_OPENAI_BASE_URL:+x}\" ] && AGENTTEAMS_OPENAI_BASE_URL=\"$(grep '^AGENTTEAMS_OPENAI_BASE_URL=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_DEFAULT_MODEL:+x}\" ] && AGENTTEAMS_DEFAULT_MODEL=\"$(grep '^AGENTTEAMS_DEFAULT_MODEL=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_EMBEDDING_MODEL:+x}\" ] && AGENTTEAMS_EMBEDDING_MODEL=\"$(grep '^AGENTTEAMS_EMBEDDING_MODEL=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_WORKSPACE_DIR:+x}\" ] && AGENTTEAMS_WORKSPACE_DIR=\"$(grep '^AGENTTEAMS_WORKSPACE_DIR=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_HOST_SHARE_DIR:+x}\" ] && AGENTTEAMS_HOST_SHARE_DIR=\"$(grep '^AGENTTEAMS_HOST_SHARE_DIR=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_PODMAN_AUTOSTART:+x}\" ] && AGENTTEAMS_PODMAN_AUTOSTART=\"$(grep '^AGENTTEAMS_PODMAN_AUTOSTART=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_DASHBOARD:+x}\" ] && AGENTTEAMS_DASHBOARD=\"$(grep '^AGENTTEAMS_DASHBOARD=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_DASHBOARD_VERSION:+x}\" ] && AGENTTEAMS_DASHBOARD_VERSION=\"$(grep '^AGENTTEAMS_DASHBOARD_VERSION=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_PORT_DASHBOARD:+x}\" ] && AGENTTEAMS_PORT_DASHBOARD=\"$(grep '^AGENTTEAMS_PORT_DASHBOARD=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_DASHBOARD_IMAGE:+x}\" ] && AGENTTEAMS_DASHBOARD_IMAGE=\"$(grep '^AGENTTEAMS_DASHBOARD_IMAGE=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL:+x}\" ] && AGENTTEAMS_AI_GATEWAY_ADMIN_URL=\"$(grep '^AGENTTEAMS_AI_GATEWAY_ADMIN_URL=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n fi\n}\n\n# In non-interactive mode, uses default or errors if required and no default.\n# Usage: prompt VAR_NAME \"Prompt text\" \"default\" [true=secret]\nprompt() {\n local var_name=\"$1\"\n local prompt_text=\"$2\"\n local default_value=\"$3\"\n local is_secret=\"${4:-false}\"\n\n # If the variable is already set in the environment, use it silently\n # In upgrade mode, show current value and let user change it\n eval \"local current_value=\\\"\\${${var_name}}\\\"\"\n if [ -n \"${current_value}\" ]; then\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_NON_INTERACTIVE}\" != \"1\" ]; then\n # Show masked value for secrets, full value otherwise\n local display_value=\"${current_value}\"\n if [ \"${is_secret}\" = \"true\" ]; then\n local len=${#current_value}\n if [ \"${len}\" -le 8 ]; then\n display_value=\"****\"\n else\n display_value=\"${current_value:0:4}****${current_value: -4}\"\n fi\n fi\n log \"$(msg prompt.upgrade_keep \"${prompt_text}\" \"${display_value}\")\"\n local new_value=\"\"\n if [ \"${is_secret}\" = \"true\" ]; then\n read_secret \"${prompt_text}: \"\n new_value=\"${_RS_RESULT}\"\n else\n read -e -p \"${prompt_text}: \" new_value\n if [ \"${new_value}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n fi\n if [ -n \"${new_value}\" ]; then\n eval \"export ${var_name}='${new_value}'\"\n fi\n return\n fi\n log \"$(msg prompt.preset \"${prompt_text}\")\"\n return\n fi\n\n # Non-interactive or quickstart: use default or error\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] || [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ]; then\n if [ -n \"${default_value}\" ]; then\n eval \"export ${var_name}='${default_value}'\"\n log \"$(msg prompt.default \"${prompt_text}\" \"${default_value}\")\"\n return\n elif [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n # Only hard-error in fully non-interactive mode, not quickstart\n die \"$(msg prompt.required \"${prompt_text}\")\"\n fi\n # quickstart + no default: fall through to interactive prompt below\n fi\n\n if [ -n \"${default_value}\" ]; then\n prompt_text=\"${prompt_text} [${default_value}]\"\n fi\n\n local value=\"\"\n if [ \"${is_secret}\" = \"true\" ]; then\n read_secret \"${prompt_text}: \"\n value=\"${_RS_RESULT}\"\n else\n read -e -p \"${prompt_text}: \" value\n if [ \"${value}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n fi\n\n value=\"${value:-${default_value}}\"\n if [ -z \"${value}\" ]; then\n die \"$(msg prompt.required_empty \"${prompt_text}\")\"\n fi\n\n eval \"export ${var_name}='${value}'\"\n}\n\n# Prompt for an optional value (empty string is acceptable)\n# Skips prompt if variable is already defined in environment (even if empty)\n# In upgrade mode, shows current value and lets user change it.\n# In non-interactive mode, defaults to empty string.\nprompt_optional() {\n local var_name=\"$1\"\n local prompt_text=\"$2\"\n local is_secret=\"${3:-false}\"\n\n # Check if variable is defined (even if set to empty string)\n eval \"local _chk=\\\"\\${${var_name}+x}\\\"\"\n if [ -n \"${_chk}\" ]; then\n # In upgrade mode, show current value and let user change it\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_NON_INTERACTIVE}\" != \"1\" ]; then\n eval \"local current_value=\\\"\\${${var_name}}\\\"\"\n local display_value=\"${current_value}\"\n if [ \"${is_secret}\" = \"true\" ] && [ -n \"${current_value}\" ]; then\n local len=${#current_value}\n if [ \"${len}\" -le 8 ]; then\n display_value=\"****\"\n else\n display_value=\"${current_value:0:4}****${current_value: -4}\"\n fi\n fi\n if [ -n \"${current_value}\" ]; then\n if [ \"${is_secret}\" = \"true\" ]; then\n log \"$(msg prompt.upgrade_keep_secret \"${prompt_text}\" \"${display_value}\")\"\n else\n log \"$(msg prompt.upgrade_keep \"${prompt_text}\" \"${display_value}\")\"\n fi\n else\n log \"$(msg prompt.upgrade_empty \"${prompt_text}\")\"\n fi\n local new_value=\"\"\n if [ \"${is_secret}\" = \"true\" ]; then\n read_secret \"${prompt_text}: \"\n new_value=\"${_RS_RESULT}\"\n else\n read -e -p \"${prompt_text}: \" new_value\n if [ \"${new_value}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n fi\n if [ -n \"${new_value}\" ]; then\n eval \"export ${var_name}='${new_value}'\"\n fi\n return\n fi\n log \"$(msg prompt.preset \"${prompt_text}\")\"\n return\n fi\n\n # Non-interactive or quickstart: skip, leave unset\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] || [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ]; then\n eval \"export ${var_name}=''\"\n return\n fi\n\n local value=\"\"\n if [ \"${is_secret}\" = \"true\" ]; then\n read_secret \"${prompt_text}: \"\n value=\"${_RS_RESULT}\"\n else\n read -e -p \"${prompt_text}: \" value\n if [ \"${value}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n fi\n\n eval \"export ${var_name}='${value}'\"\n}\n\ngenerate_key() {\n openssl rand -hex 32\n}\n\n# ============================================================\n# Detect container runtime socket on the host\n#\n# Dependency: Requires DOCKER_CMD to reflect the TRUE underlying runtime.\n# (Crucial for unmasking 'podman' disguised as 'docker'.\n# Must be strictly resolved prior, e.g., via check_container_runtime)\n# ============================================================\ndetect_socket() {\n local socket_path\n\n # 1. Respect explicitly defined DOCKER_HOST environment variable\n if [ -n \"${DOCKER_HOST}\" ]; then\n socket_path=$(echo \"${DOCKER_HOST}\" | sed 's|^unix://||')\n if [ -S \"${socket_path}\" ]; then\n echo \"${socket_path}\"\n return 0\n fi\n fi\n\n # 2. Route based on the resolved container runtime\n case \"${DOCKER_CMD}\" in\n docker)\n # Docker path handling\n socket_path=$(docker context ls --format '{{if .Current}}{{.DockerEndpoint}}{{end}}' 2>/dev/null | grep . | sed 's|^unix://||')\n if [ -n \"${socket_path}\" ] && [ -S \"${socket_path}\" ]; then\n echo \"${socket_path}\"\n return 0\n fi\n\n # Rootless Docker\n if [ -n \"${XDG_RUNTIME_DIR:-}\" ] && [ -S \"${XDG_RUNTIME_DIR}/docker.sock\" ]; then\n echo \"${XDG_RUNTIME_DIR}/docker.sock\"\n return 0\n fi\n\n # Rootless Docker (macOS fallback)\n if [ -S \"${HOME}/.docker/run/docker.sock\" ]; then\n echo \"${HOME}/.docker/run/docker.sock\"\n return 0\n fi\n\n # Root Docker\n if [ -S \"/var/run/docker.sock\" ]; then\n echo \"/var/run/docker.sock\"\n return 0\n fi\n ;;\n podman)\n # Podman path handling\n if [ \"$(id -u)\" -eq 0 ]; then\n socket_path=\"/run/podman/podman.sock\"\n else\n socket_path=\"${XDG_RUNTIME_DIR}/podman/podman.sock\"\n fi\n if [ -S \"${socket_path}\" ]; then\n echo \"${socket_path}\"\n return 0\n fi\n ;;\n esac\n\n # Return empty if no socket is found\n echo \"\"\n return 0\n}\n\n# Ensure Podman API socket is active (State mutation)\nensure_podman_socket() {\n local socket_path\n\n # Only applicable if runtime is podman and systemctl is available\n if [ \"${DOCKER_CMD:-}\" != \"podman\" ] || ! command -v systemctl >/dev/null 2>&1; then\n return 0\n fi\n\n echo \"Ensuring Podman API socket is active...\" >&2\n\n # Enable and start the socket based on user privileges\n if [ \"$(id -u)\" -eq 0 ]; then\n socket_path=\"/run/podman/podman.sock\"\n systemctl enable --now podman.socket >/dev/null 2>&1 || true\n else\n socket_path=\"${XDG_RUNTIME_DIR}/podman/podman.sock\"\n systemctl --user enable --now podman.socket >/dev/null 2>&1 || true\n fi\n\n # Give systemd a brief moment to assert the socket for root or rootless user\n local _i\n for _i in 1 2 3; do\n [ -S \"${socket_path}\" ] && break\n sleep 1\n done\n}\n\n# Detect local LAN IP address (cross-platform: macOS and Linux)\ndetect_lan_ip() {\n local ip=\"\"\n\n # macOS: try common Wi-Fi / Ethernet interfaces\n if command -v ipconfig >/dev/null 2>&1; then\n for iface in en0 en1 en2 en3 en4; do\n ip=$(ipconfig getifaddr \"${iface}\" 2>/dev/null)\n if [ -n \"${ip}\" ]; then\n echo \"${ip}\"\n return 0\n fi\n done\n fi\n\n # Linux: ip route — most reliable\n if command -v ip >/dev/null 2>&1; then\n ip=$(ip route get 1.1.1.1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i==\"src\"){print $(i+1); exit}}')\n if [ -n \"${ip}\" ]; then\n echo \"${ip}\"\n return 0\n fi\n fi\n\n # Linux fallback: hostname -I (space-separated list, take first non-loopback)\n if command -v hostname >/dev/null 2>&1; then\n ip=$(hostname -I 2>/dev/null | tr ' ' '\\n' | grep -v '^127\\.' | grep -v '^::' | head -1)\n if [ -n \"${ip}\" ]; then\n echo \"${ip}\"\n return 0\n fi\n fi\n\n # Last resort: ifconfig\n if command -v ifconfig >/dev/null 2>&1; then\n ip=$(ifconfig 2>/dev/null | awk '/inet /{if($2!~/^127\\./){print $2; exit}}')\n # Strip \"addr:\" prefix that some ifconfig versions add\n ip=\"${ip#addr:}\"\n if [ -n \"${ip}\" ]; then\n echo \"${ip}\"\n return 0\n fi\n fi\n\n echo \"\"\n}\n\n\n# ============================================================\n# Step-back navigation helpers\n# ============================================================\n\n# should_skip_step: returns 0 (skip) when the step is irrelevant in current mode\nshould_skip_step() {\n local step_fn=\"$1\"\n case \"${step_fn}\" in\n step_lang|step_mode)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n ;;\n step_version)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n ;;\n step_existing)\n local _env=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n [ ! -f \"${_env}\" ] && return 0\n ;;\n # Keep-All upgrade mode: skip all config steps (step_volume/step_workspace handled separately)\n step_llm|step_admin|step_network|step_ports|step_domains|step_github|step_skills|step_runtime|step_manager_runtime|step_dashboard|step_e2ee|step_docker_proxy|step_idle|step_hostshare)\n [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ] && return 0\n ;;\n step_volume|step_workspace)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ] && return 0\n [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ] && return 0\n ;;\n step_e2ee|step_idle)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE}\" != \"1\" ] && return 0\n ;;\n step_docker_proxy)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE}\" != \"1\" ] && return 0\n # Embedded mode handles docker access natively — skip this step\n [ \"${AGENTTEAMS_USE_EMBEDDED:-}\" = \"1\" ] && return 0\n ;;\n step_manager_runtime)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n ;;\n step_hostshare)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ] && return 0\n ;;\n step_podman_autostart)\n # Only relevant for Podman with systemd\n [ \"${DOCKER_CMD}\" != \"podman\" ] && return 0\n ! command -v systemctl >/dev/null 2>&1 && return 0\n\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE}\" != \"1\" ] && return 0\n [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ] && return 0\n ;;\n esac\n return 1\n}\n\n# clear_step_vars: unset variables set by a step so it will re-prompt on re-entry\nclear_step_vars() {\n local step_fn=\"$1\"\n case \"${step_fn}\" in\n step_mode) unset AGENTTEAMS_QUICKSTART ;;\n step_version) unset AGENTTEAMS_VERSION ;;\n step_existing) unset AGENTTEAMS_UPGRADE UPGRADE_EXISTING_WORKERS ;;\n step_llm)\n unset AGENTTEAMS_LLM_PROVIDER AGENTTEAMS_DEFAULT_MODEL AGENTTEAMS_OPENAI_BASE_URL\n unset AGENTTEAMS_LLM_API_KEY AGENTTEAMS_MODEL_CONTEXT_WINDOW AGENTTEAMS_MODEL_MAX_TOKENS\n unset AGENTTEAMS_MODEL_REASONING AGENTTEAMS_MODEL_VISION\n ;;\n step_admin) unset AGENTTEAMS_ADMIN_USER AGENTTEAMS_ADMIN_PASSWORD ;;\n step_network) unset AGENTTEAMS_LOCAL_ONLY ;;\n step_ports)\n unset AGENTTEAMS_PORT_GATEWAY AGENTTEAMS_PORT_CONSOLE\n unset AGENTTEAMS_PORT_ELEMENT_WEB AGENTTEAMS_PORT_MANAGER_CONSOLE\n ;;\n step_domains)\n unset AGENTTEAMS_MATRIX_DOMAIN AGENTTEAMS_MATRIX_CLIENT_DOMAIN\n unset AGENTTEAMS_AI_GATEWAY_DOMAIN AGENTTEAMS_FS_DOMAIN AGENTTEAMS_CONSOLE_DOMAIN\n ;;\n step_github) unset AGENTTEAMS_GITHUB_TOKEN ;;\n step_skills) unset AGENTTEAMS_SKILLS_API_URL ;;\n step_volume) unset AGENTTEAMS_DATA_DIR ;;\n step_workspace) unset AGENTTEAMS_WORKSPACE_DIR ;;\n step_dashboard) unset AGENTTEAMS_DASHBOARD AGENTTEAMS_DASHBOARD_VERSION AGENTTEAMS_PORT_DASHBOARD AGENTTEAMS_DASHBOARD_IMAGE AGENTTEAMS_AI_GATEWAY_ADMIN_URL ;;\n step_runtime) unset AGENTTEAMS_DEFAULT_WORKER_RUNTIME ;;\n step_manager_runtime) unset AGENTTEAMS_MANAGER_RUNTIME ;;\n step_e2ee) unset AGENTTEAMS_MATRIX_E2EE ;;\n step_docker_proxy) unset AGENTTEAMS_DOCKER_PROXY; unset AGENTTEAMS_PROXY_ALLOWED_REGISTRIES ;;\n step_idle) unset AGENTTEAMS_WORKER_IDLE_TIMEOUT ;;\n step_hostshare) unset AGENTTEAMS_HOST_SHARE_DIR ;;\n step_podman_autostart) unset AGENTTEAMS_PODMAN_AUTOSTART ;;\n esac\n}\n\n# ============================================================\n# Individual step functions\n# ============================================================\n\nstep_lang() {\n local lang_default_choice=\"2\"\n [ \"${AGENTTEAMS_LANGUAGE}\" = \"zh\" ] && lang_default_choice=\"1\"\n log \"$(msg lang.detected)\"\n log \"$(msg lang.switch_title)\"\n echo \"$(msg lang.option_zh)\"\n echo \"$(msg lang.option_en)\"\n echo \"\"\n local LANG_CHOICE\n read -e -p \"$(msg lang.prompt) [${lang_default_choice}]: \" LANG_CHOICE\n LANG_CHOICE=\"${LANG_CHOICE:-${lang_default_choice}}\"\n if [ \"${LANG_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n case \"${LANG_CHOICE}\" in\n 1) AGENTTEAMS_LANGUAGE=\"zh\" ;;\n 2) AGENTTEAMS_LANGUAGE=\"en\" ;;\n esac\n export AGENTTEAMS_LANGUAGE\n log \"\"\n}\n\nstep_mode() {\n log \"$(msg install.mode.title)\"\n echo \"\"\n echo \"$(msg install.mode.choose)\"\n echo \"$(msg install.mode.quickstart)\"\n echo \"$(msg install.mode.manual)\"\n echo \"\"\n local ONBOARDING_CHOICE\n read -e -p \"$(msg install.mode.prompt): \" ONBOARDING_CHOICE\n ONBOARDING_CHOICE=\"${ONBOARDING_CHOICE:-1}\"\n if [ \"${ONBOARDING_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n case \"${ONBOARDING_CHOICE}\" in\n 1|quick|quickstart)\n log \"$(msg install.mode.quickstart_selected)\"\n AGENTTEAMS_QUICKSTART=1\n ;;\n 2|manual)\n log \"$(msg install.mode.manual_selected)\"\n ;;\n *)\n log \"$(msg install.mode.invalid)\"\n AGENTTEAMS_QUICKSTART=1\n ;;\n esac\n log \"\"\n}\n\nstep_version() {\n # Skip if version already provided via env var\n if [ -n \"${AGENTTEAMS_VERSION}\" ]; then\n resolve_image_tags\n return 0\n fi\n # Try to fetch the latest stable release from GitHub\n log \"$(msg install.version.fetching)\"\n local _fetched\n _fetched=$(curl -sf --max-time 5 \\\n -H \"Accept: application/vnd.github+json\" \\\n \"https://api.github.com/repos/agentscope-ai/AgentTeams/releases/latest\" \\\n 2>/dev/null | grep '\"tag_name\"' | head -1 | sed 's/.*\"tag_name\": *\"\\([^\"]*\\)\".*/\\1/')\n if [ -n \"${_fetched}\" ]; then\n AGENTTEAMS_KNOWN_STABLE_VERSION=\"${_fetched}\"\n else\n log \"$(msg install.version.fetch_failed \"${AGENTTEAMS_KNOWN_STABLE_VERSION}\")\"\n fi\n log \"$(msg install.version.title)\"\n echo \"\"\n echo \"$(msg install.version.choose)\"\n echo \"$(msg install.version.option_latest)\"\n printf \"%s\\n\" \"$(msg install.version.option_stable \"${AGENTTEAMS_KNOWN_STABLE_VERSION}\")\"\n echo \"$(msg install.version.option_custom)\"\n echo \"\"\n local VERSION_CHOICE\n read -e -p \"$(msg install.version.prompt) [1]: \" VERSION_CHOICE\n VERSION_CHOICE=\"${VERSION_CHOICE:-1}\"\n if [ \"${VERSION_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n case \"${VERSION_CHOICE}\" in\n 1|latest)\n AGENTTEAMS_VERSION=\"latest\"\n log \"$(msg install.version.selected_latest)\"\n ;;\n 2|stable)\n AGENTTEAMS_VERSION=\"${AGENTTEAMS_KNOWN_STABLE_VERSION}\"\n log \"$(msg install.version.selected_stable \"${AGENTTEAMS_VERSION}\")\"\n ;;\n 3|custom)\n local CUSTOM_VERSION\n read -e -p \"$(msg install.version.custom_prompt): \" CUSTOM_VERSION\n AGENTTEAMS_VERSION=\"${CUSTOM_VERSION:-${AGENTTEAMS_KNOWN_STABLE_VERSION}}\"\n log \"$(msg install.version.selected_custom \"${AGENTTEAMS_VERSION}\")\"\n ;;\n *)\n AGENTTEAMS_VERSION=\"${AGENTTEAMS_KNOWN_STABLE_VERSION}\"\n log \"$(msg install.version.invalid \"${AGENTTEAMS_VERSION}\")\"\n ;;\n esac\n log \"\"\n resolve_image_tags\n}\n\nstep_existing() {\n local existing_env=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n log \"$(msg install.existing.detected \"${existing_env}\")\"\n local running_manager=\"\" running_workers=\"\" existing_workers=\"\"\n if ${DOCKER_CMD} ps --format '{{.Names}}' | grep -q \"^agentteams-manager$\"; then\n running_manager=\"agentteams-manager\"\n fi\n running_workers=$(${DOCKER_CMD} ps --format '{{.Names}}' | grep \"^agentteams-worker-\" || true)\n existing_workers=$(${DOCKER_CMD} ps -a --format '{{.Names}}' | grep \"^agentteams-worker-\" || true)\n local UPGRADE_CHOICE\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n log \"$(msg install.existing.upgrade_noninteractive)\"\n UPGRADE_CHOICE=\"upgrade\"\n else\n echo \"\"\n echo \"$(msg install.existing.choose)\"\n echo \"$(msg install.existing.upgrade)\"\n echo \"$(msg install.existing.reinstall)\"\n echo \"$(msg install.existing.cancel)\"\n echo \"\"\n read -e -p \"$(msg install.existing.prompt): \" UPGRADE_CHOICE\n UPGRADE_CHOICE=\"${UPGRADE_CHOICE:-1}\"\n if [ \"${UPGRADE_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n fi\n case \"${UPGRADE_CHOICE}\" in\n 1|upgrade)\n AGENTTEAMS_UPGRADE=1\n log \"$(msg install.existing.upgrading)\"\n # Show upgrade mode sub-menu (unless already set via env var)\n if [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL:-0}\" != \"1\" ]; then\n echo \"\"\n echo \"$(msg upgrade.mode.prompt)\"\n echo \"$(msg upgrade.mode.keep_all)\"\n echo \"$(msg upgrade.mode.confirm_each)\"\n echo \"$(msg upgrade.mode.back)\"\n echo \"\"\n local UPGRADE_MODE_CHOICE\n read -e -p \"$(msg install.existing.prompt): \" UPGRADE_MODE_CHOICE\n UPGRADE_MODE_CHOICE=\"${UPGRADE_MODE_CHOICE:-1}\"\n case \"${UPGRADE_MODE_CHOICE}\" in\n 1|keep)\n AGENTTEAMS_UPGRADE_KEEP_ALL=1\n ;;\n 2|confirm)\n AGENTTEAMS_UPGRADE_KEEP_ALL=0\n ;;\n 3|b)\n STEP_RESULT=\"back\"\n return 0\n ;;\n *)\n AGENTTEAMS_UPGRADE_KEEP_ALL=0\n ;;\n esac\n fi\n # Load current parameters for both Keep-All and confirm-each modes\n load_current_params_from_env\n if [ -n \"${running_manager}\" ] || [ -n \"${running_workers}\" ]; then\n echo \"\"\n echo -e \"\\033[33m$(msg install.existing.warn_manager_stop)\\033[0m\"\n if [ -n \"${existing_workers}\" ]; then\n echo -e \"\\033[33m$(msg install.existing.warn_worker_recreate)\\033[0m\"\n fi\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" != \"1\" ]; then\n echo \"\"\n local CONFIRM_STOP\n read -e -p \"$(msg install.existing.continue_prompt): \" CONFIRM_STOP\n if [ \"${CONFIRM_STOP}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n if [ \"${CONFIRM_STOP}\" != \"y\" ] && [ \"${CONFIRM_STOP}\" != \"Y\" ]; then\n log \"$(msg install.existing.cancelled)\"\n exit 0\n fi\n fi\n fi\n UPGRADE_EXISTING_WORKERS=\"${existing_workers}\"\n ;;\n 2|reinstall)\n log \"$(msg install.reinstall.performing)\"\n local existing_workspace=\"\"\n if [ -f \"${existing_env}\" ]; then\n existing_workspace=$(grep '^AGENTTEAMS_WORKSPACE_DIR=' \"${existing_env}\" 2>/dev/null | cut -d= -f2-)\n fi\n [ -z \"${existing_workspace}\" ] && existing_workspace=\"${HOME}/agentteams-manager\"\n echo \"\"\n echo -e \"\\033[33m$(msg install.reinstall.warn_stop)\\033[0m\"\n [ -n \"${running_manager}\" ] && echo -e \"\\033[33m - ${running_manager} (manager)\\033[0m\"\n for w in ${running_workers}; do\n echo -e \"\\033[33m - ${w} (worker)\\033[0m\"\n done\n echo \"\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_delete)\\033[0m\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_volume)\\033[0m\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_env \"${existing_env}\")\\033[0m\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_workspace \"${existing_workspace}\")\\033[0m\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_workers)\\033[0m\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_proxy)\\033[0m\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_network)\\033[0m\"\n echo \"\"\n echo -e \"\\033[31m$(msg install.reinstall.confirm_type)\\033[0m\"\n echo -e \"\\033[31m ${existing_workspace}\\033[0m\"\n echo \"\"\n local CONFIRM_PATH\n read -e -p \"$(msg install.reinstall.confirm_path): \" CONFIRM_PATH\n if [ \"${CONFIRM_PATH}\" != \"${existing_workspace}\" ]; then\n die \"$(msg install.reinstall.path_mismatch \"${CONFIRM_PATH}\" \"${existing_workspace}\")\"\n fi\n log \"$(msg install.reinstall.confirmed)\"\n ${DOCKER_CMD} stop agentteams-manager 2>/dev/null || true\n ${DOCKER_CMD} rm agentteams-manager 2>/dev/null || true\n for w in $(${DOCKER_CMD} ps -a --format '{{.Names}}' | grep \"^agentteams-worker-\" || true); do\n ${DOCKER_CMD} stop \"${w}\" 2>/dev/null || true\n ${DOCKER_CMD} rm \"${w}\" 2>/dev/null || true\n log \"$(msg install.reinstall.removed_worker \"${w}\")\"\n done\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' | grep -q \"^agentteams-controller$\"; then\n log \"$(msg install.reinstall.removing_proxy)\"\n ${DOCKER_CMD} stop agentteams-controller 2>/dev/null || true\n ${DOCKER_CMD} rm agentteams-controller 2>/dev/null || true\n fi\n if ${DOCKER_CMD} network ls --format '{{.Name}}' | grep -q \"^agentteams-net$\"; then\n log \"$(msg install.reinstall.removing_network)\"\n ${DOCKER_CMD} network rm agentteams-net 2>/dev/null || true\n fi\n if ${DOCKER_CMD} volume ls -q | grep -q \"^agentteams-data$\"; then\n log \"$(msg install.reinstall.removing_volume)\"\n ${DOCKER_CMD} volume rm agentteams-data 2>/dev/null || log \"$(msg install.reinstall.warn_volume_fail)\"\n fi\n if [ -d \"${existing_workspace}\" ]; then\n log \"$(msg install.reinstall.removing_workspace \"${existing_workspace}\")\"\n rm -rf \"${existing_workspace}\" || die \"$(msg install.reinstall.failed_rm_workspace)\"\n fi\n if [ -f \"${existing_env}\" ]; then\n log \"$(msg install.reinstall.removing_env \"${existing_env}\")\"\n rm -f \"${existing_env}\"\n fi\n log \"$(msg install.reinstall.cleanup_done)\"\n unset AGENTTEAMS_WORKSPACE_DIR\n return 0\n ;;\n 3|cancel|*)\n log \"$(msg install.existing.cancelled)\"\n exit 0\n ;;\n esac\n # Load existing env file as fallback (shell env vars take priority)\n if [ -f \"${existing_env}\" ]; then\n log \"$(msg install.loading_config \"${existing_env}\")\"\n while IFS='=' read -r key value; do\n case \"${key}\" in \\#*|\"\") continue ;; esac\n value=\"${value%%#*}\"\n value=\"${value#\"${value%%[![:space:]]*}\"}\"\n value=\"${value%\"${value##*[![:space:]]}\"}\"\n eval \"_existing_val=\\\"\\${${key}+x}\\\"\"\n if [ -z \"${_existing_val}\" ]; then export \"${key}=${value}\"; fi\n done < \"${existing_env}\"\n fi\n}\n\nstep_llm() {\n log \"$(msg llm.title)\"\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n if [ \"${AGENTTEAMS_LANGUAGE}\" = \"zh\" ]; then\n AGENTTEAMS_LLM_PROVIDER=\"${AGENTTEAMS_LLM_PROVIDER:-openai-compat}\"\n AGENTTEAMS_DEFAULT_MODEL=\"${AGENTTEAMS_DEFAULT_MODEL:-qwen3.6-plus}\"\n AGENTTEAMS_OPENAI_BASE_URL=\"${AGENTTEAMS_OPENAI_BASE_URL:-https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1}\"\n log \"$(msg llm.provider.label \"${AGENTTEAMS_LLM_PROVIDER}\")\"\n log \"$(msg llm.openai.base_url_label \"${AGENTTEAMS_OPENAI_BASE_URL}\")\"\n else\n AGENTTEAMS_LLM_PROVIDER=\"${AGENTTEAMS_LLM_PROVIDER:-qwen}\"\n AGENTTEAMS_DEFAULT_MODEL=\"${AGENTTEAMS_DEFAULT_MODEL:-qwen3.6-plus}\"\n AGENTTEAMS_OPENAI_BASE_URL=\"${AGENTTEAMS_OPENAI_BASE_URL:-}\"\n log \"$(msg llm.provider.qwen_default \"${AGENTTEAMS_LLM_PROVIDER}\")\"\n fi\n log \"$(msg llm.model.default \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n prompt AGENTTEAMS_LLM_API_KEY \"$(msg llm.apikey_prompt)\" \"\" \"true\"\n AGENTTEAMS_EMBEDDING_MODEL=\"${AGENTTEAMS_EMBEDDING_MODEL-text-embedding-v4}\"\n export AGENTTEAMS_LLM_PROVIDER AGENTTEAMS_DEFAULT_MODEL AGENTTEAMS_EMBEDDING_MODEL\n [ -n \"${AGENTTEAMS_OPENAI_BASE_URL+x}\" ] && export AGENTTEAMS_OPENAI_BASE_URL\n return 0\n fi\n\n # Upgrade Keep-All mode: skip provider selection, use loaded values\n local SKIP_TO_EMBEDDING=0\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ] && [ -n \"${AGENTTEAMS_LLM_PROVIDER}\" ]; then\n log \"$(msg llm.provider.label \"${AGENTTEAMS_LLM_PROVIDER}\")\"\n log \"$(msg llm.openai.base_url_label \"${AGENTTEAMS_OPENAI_BASE_URL}\")\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n PROVIDER_CHOICE=\"${AGENTTEAMS_LLM_PROVIDER}\"\n SKIP_TO_EMBEDDING=1\n else\n echo \"\"\n echo \"$(msg llm.providers_title)\"\n echo \"$(msg llm.provider.alibaba)\"\n echo \"$(msg llm.provider.openai_compat)\"\n echo \"\"\n local PROVIDER_CHOICE\n # If upgrade mode with loaded provider, show current as default\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_LLM_PROVIDER}\" ]; then\n local _prov_display\n case \"${AGENTTEAMS_LLM_PROVIDER}\" in\n openai-compat) _prov_display=\"2\" ;;\n *) _prov_display=\"1\" ;;\n esac\n read -e -p \"$(msg llm.provider.select) [${_prov_display}]: \" PROVIDER_CHOICE\n PROVIDER_CHOICE=\"${PROVIDER_CHOICE:-${_prov_display}}\"\n elif [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ]; then\n read -e -p \"$(msg llm.provider.select) [1]: \" PROVIDER_CHOICE\n PROVIDER_CHOICE=\"${PROVIDER_CHOICE:-1}\"\n else\n read -e -p \"$(msg llm.provider.select): \" PROVIDER_CHOICE\n PROVIDER_CHOICE=\"${PROVIDER_CHOICE:-1}\"\n fi\n if [ \"${PROVIDER_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n fi\n local ALIBABA_MODEL_CHOICE=\"\"\n local ALIBABA_ACCESS=\"\"\n case \"${PROVIDER_CHOICE}\" in\n 1|alibaba-cloud|openai-compat)\n if [ \"${AGENTTEAMS_LANGUAGE}\" = \"en\" ]; then\n AGENTTEAMS_LLM_PROVIDER=\"openai-compat\"\n AGENTTEAMS_OPENAI_BASE_URL=\"https://dashscope-intl.aliyuncs.com/compatible-mode/v1\"\n ALIBABA_ACCESS=\"tokenplan\"\n echo \"\"\n echo \"$(msg llm.codingplan.models_title)\"\n echo \"$(msg llm.codingplan.model.qwen36plus)\"\n echo \"$(msg llm.codingplan.model.glm5)\"\n echo \"$(msg llm.codingplan.model.kimi)\"\n echo \"$(msg llm.codingplan.model.minimax)\"\n echo \"\"\n local CODINGPLAN_MODEL_CHOICE\n # If upgrade with loaded model, show current as default\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_DEFAULT_MODEL}\" ]; then\n local _model_default\n case \"${AGENTTEAMS_DEFAULT_MODEL}\" in\n qwen3.6-plus) _model_default=\"1\" ;;\n glm-5) _model_default=\"2\" ;;\n kimi-k2.5) _model_default=\"3\" ;;\n MiniMax-M2.5) _model_default=\"4\" ;;\n *) _model_default=\"1\" ;;\n esac\n read -e -p \"$(msg llm.codingplan.model.select) [${_model_default}]: \" CODINGPLAN_MODEL_CHOICE\n CODINGPLAN_MODEL_CHOICE=\"${CODINGPLAN_MODEL_CHOICE:-${_model_default}}\"\n elif [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ]; then\n read -e -p \"$(msg llm.codingplan.model.select) [1]: \" CODINGPLAN_MODEL_CHOICE\n CODINGPLAN_MODEL_CHOICE=\"${CODINGPLAN_MODEL_CHOICE:-1}\"\n else\n read -e -p \"$(msg llm.codingplan.model.select): \" CODINGPLAN_MODEL_CHOICE\n CODINGPLAN_MODEL_CHOICE=\"${CODINGPLAN_MODEL_CHOICE:-1}\"\n fi\n if [ \"${CODINGPLAN_MODEL_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n case \"${CODINGPLAN_MODEL_CHOICE}\" in\n 1|qwen3.6-plus) AGENTTEAMS_DEFAULT_MODEL=\"qwen3.6-plus\" ;;\n 2|glm-5) AGENTTEAMS_DEFAULT_MODEL=\"glm-5\" ;;\n 3|kimi-k2.5) AGENTTEAMS_DEFAULT_MODEL=\"kimi-k2.5\" ;;\n 4|MiniMax-M2.5) AGENTTEAMS_DEFAULT_MODEL=\"MiniMax-M2.5\" ;;\n *) AGENTTEAMS_DEFAULT_MODEL=\"qwen3.6-plus\" ;;\n esac\n log \"$(msg llm.provider.selected_codingplan)\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n else\n echo \"\"\n echo \"$(msg llm.alibaba.models_title)\"\n echo \"$(msg llm.alibaba.model.tokenplan)\"\n echo \"$(msg llm.alibaba.model.bailian)\"\n echo \"$(msg llm.alibaba.model.codingplan_legacy)\"\n echo \"\"\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_OPENAI_BASE_URL}\" ]; then\n # Determine alibaba access type from loaded URL\n local _access_default=\"1\"\n if echo \"${AGENTTEAMS_OPENAI_BASE_URL}\" | grep -q \"token-plan\"; then\n _access_default=\"1\"\n elif echo \"${AGENTTEAMS_OPENAI_BASE_URL}\" | grep -q \"dashscope\"; then\n if echo \"${AGENTTEAMS_OPENAI_BASE_URL}\" | grep -q \"intl\"; then\n _access_default=\"3\"\n else\n _access_default=\"2\"\n fi\n fi\n read -e -p \"$(msg llm.alibaba.model.select) [${_access_default}]: \" ALIBABA_MODEL_CHOICE\n ALIBABA_MODEL_CHOICE=\"${ALIBABA_MODEL_CHOICE:-${_access_default}}\"\n elif [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ]; then\n read -e -p \"$(msg llm.alibaba.model.select) [1]: \" ALIBABA_MODEL_CHOICE\n ALIBABA_MODEL_CHOICE=\"${ALIBABA_MODEL_CHOICE:-1}\"\n else\n read -e -p \"$(msg llm.alibaba.model.select): \" ALIBABA_MODEL_CHOICE\n ALIBABA_MODEL_CHOICE=\"${ALIBABA_MODEL_CHOICE:-1}\"\n fi\n if [ \"${ALIBABA_MODEL_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n case \"${ALIBABA_MODEL_CHOICE}\" in\n 1|token-plan|tokenplan)\n ALIBABA_ACCESS=\"tokenplan\"\n AGENTTEAMS_LLM_PROVIDER=\"openai-compat\"\n AGENTTEAMS_OPENAI_BASE_URL=\"https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1\"\n echo \"\"\n echo \"$(msg llm.codingplan.models_title)\"\n echo \"$(msg llm.codingplan.model.qwen36plus)\"\n echo \"$(msg llm.codingplan.model.glm5)\"\n echo \"$(msg llm.codingplan.model.kimi)\"\n echo \"$(msg llm.codingplan.model.minimax)\"\n echo \"\"\n local CODINGPLAN_MODEL_CHOICE\n # If upgrade with loaded model, show current as default\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_DEFAULT_MODEL}\" ]; then\n local _model_default\n case \"${AGENTTEAMS_DEFAULT_MODEL}\" in\n qwen3.6-plus) _model_default=\"1\" ;;\n glm-5) _model_default=\"2\" ;;\n kimi-k2.5) _model_default=\"3\" ;;\n MiniMax-M2.5) _model_default=\"4\" ;;\n *) _model_default=\"1\" ;;\n esac\n read -e -p \"$(msg llm.codingplan.model.select) [${_model_default}]: \" CODINGPLAN_MODEL_CHOICE\n CODINGPLAN_MODEL_CHOICE=\"${CODINGPLAN_MODEL_CHOICE:-${_model_default}}\"\n elif [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ]; then\n read -e -p \"$(msg llm.codingplan.model.select) [1]: \" CODINGPLAN_MODEL_CHOICE\n CODINGPLAN_MODEL_CHOICE=\"${CODINGPLAN_MODEL_CHOICE:-1}\"\n else\n read -e -p \"$(msg llm.codingplan.model.select): \" CODINGPLAN_MODEL_CHOICE\n CODINGPLAN_MODEL_CHOICE=\"${CODINGPLAN_MODEL_CHOICE:-1}\"\n fi\n if [ \"${CODINGPLAN_MODEL_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n case \"${CODINGPLAN_MODEL_CHOICE}\" in\n 1|qwen3.6-plus) AGENTTEAMS_DEFAULT_MODEL=\"qwen3.6-plus\" ;;\n 2|glm-5) AGENTTEAMS_DEFAULT_MODEL=\"glm-5\" ;;\n 3|kimi-k2.5) AGENTTEAMS_DEFAULT_MODEL=\"kimi-k2.5\" ;;\n 4|MiniMax-M2.5) AGENTTEAMS_DEFAULT_MODEL=\"MiniMax-M2.5\" ;;\n *) AGENTTEAMS_DEFAULT_MODEL=\"qwen3.6-plus\" ;;\n esac\n log \"$(msg llm.provider.selected_tokenplan)\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n ;;\n 2|qwen|bailian)\n ALIBABA_ACCESS=\"bailian\"\n AGENTTEAMS_LLM_PROVIDER=\"qwen\"\n AGENTTEAMS_OPENAI_BASE_URL=\"\"\n echo \"\"\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_DEFAULT_MODEL}\" ]; then\n read -e -p \"$(msg llm.qwen.model_prompt) [${AGENTTEAMS_DEFAULT_MODEL}]: \" AGENTTEAMS_DEFAULT_MODEL\n else\n read -e -p \"$(msg llm.qwen.model_prompt) [qwen3.6-plus]: \" AGENTTEAMS_DEFAULT_MODEL\n fi\n if [ \"${AGENTTEAMS_DEFAULT_MODEL}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_DEFAULT_MODEL=\"${AGENTTEAMS_DEFAULT_MODEL:-qwen3.6-plus}\"\n log \"$(msg llm.provider.selected_qwen)\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n prompt_custom_model_params \"${AGENTTEAMS_DEFAULT_MODEL}\" || return 0\n ;;\n 3|coding-plan|codingplan)\n ALIBABA_ACCESS=\"codingplan_legacy\"\n AGENTTEAMS_LLM_PROVIDER=\"openai-compat\"\n AGENTTEAMS_OPENAI_BASE_URL=\"https://coding.dashscope.aliyuncs.com/v1\"\n echo \"\"\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_DEFAULT_MODEL}\" ]; then\n read -e -p \"$(msg llm.qwen.model_prompt) [${AGENTTEAMS_DEFAULT_MODEL}]: \" AGENTTEAMS_DEFAULT_MODEL\n else\n read -e -p \"$(msg llm.qwen.model_prompt): \" AGENTTEAMS_DEFAULT_MODEL\n fi\n if [ \"${AGENTTEAMS_DEFAULT_MODEL}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_DEFAULT_MODEL=\"${AGENTTEAMS_DEFAULT_MODEL:-qwen3.6-plus}\"\n log \"$(msg llm.provider.selected_codingplan_legacy)\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n prompt_custom_model_params \"${AGENTTEAMS_DEFAULT_MODEL}\" || return 0\n ;;\n *)\n die \"$(msg llm.alibaba.model.invalid \"${ALIBABA_MODEL_CHOICE}\")\"\n ;;\n esac\n fi\n if [ \"${ALIBABA_ACCESS}\" = \"bailian\" ]; then\n log \"$(msg llm.apikey_hint_bailian)\"\n log \"$(msg llm.apikey_url_bailian)\"\n elif [ \"${AGENTTEAMS_LANGUAGE}\" = \"en\" ]; then\n log \"$(msg llm.apikey_hint_qwencloud)\"\n log \"$(msg llm.apikey_url_qwencloud)\"\n elif [ \"${ALIBABA_ACCESS}\" = \"codingplan_legacy\" ]; then\n log \"$(msg llm.apikey_hint_codingplan)\"\n log \"$(msg llm.apikey_url_codingplan)\"\n else\n log \"$(msg llm.apikey_hint_tokenplan)\"\n log \"$(msg llm.apikey_url_tokenplan)\"\n fi\n log \"\"\n prompt AGENTTEAMS_LLM_API_KEY \"$(msg llm.apikey_prompt)\" \"\" \"true\" || return 0\n if [ \"${ALIBABA_ACCESS}\" = \"bailian\" ]; then\n test_llm_connectivity \"https://dashscope.aliyuncs.com/compatible-mode/v1\" \"${AGENTTEAMS_LLM_API_KEY}\" \"${AGENTTEAMS_DEFAULT_MODEL}\" || return 0\n elif [ \"${ALIBABA_ACCESS}\" = \"codingplan_legacy\" ]; then\n test_llm_connectivity \"https://coding.dashscope.aliyuncs.com/v1\" \"${AGENTTEAMS_LLM_API_KEY}\" \"${AGENTTEAMS_DEFAULT_MODEL}\" \"$(msg llm.openai.test.fail.codingplan_legacy)\" || return 0\n elif [ \"${AGENTTEAMS_LANGUAGE}\" = \"en\" ]; then\n test_llm_connectivity \"${AGENTTEAMS_OPENAI_BASE_URL}\" \"${AGENTTEAMS_LLM_API_KEY}\" \"${AGENTTEAMS_DEFAULT_MODEL}\" \"$(msg llm.openai.test.fail.codingplan)\" || return 0\n else\n test_llm_connectivity \"${AGENTTEAMS_OPENAI_BASE_URL}\" \"${AGENTTEAMS_LLM_API_KEY}\" \"${AGENTTEAMS_DEFAULT_MODEL}\" \"$(msg llm.openai.test.fail.tokenplan)\" || return 0\n fi\n ;;\n 2|openai-compat)\n AGENTTEAMS_LLM_PROVIDER=\"openai-compat\"\n log \"$(msg llm.provider.selected_openai \"${AGENTTEAMS_LLM_PROVIDER}\")\"\n echo \"\"\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg llm.openai.base_url_prompt)\" \"${AGENTTEAMS_OPENAI_BASE_URL}\")\"\n else\n _current=\"${AGENTTEAMS_OPENAI_BASE_URL}\"\n if [ -n \"${_current}\" ]; then\n read -e -p \"$(msg llm.openai.base_url_prompt) [${_current}]: \" AGENTTEAMS_OPENAI_BASE_URL\n else\n read -e -p \"$(msg llm.openai.base_url_prompt) [https://api.openai.com/v1]: \" AGENTTEAMS_OPENAI_BASE_URL\n fi\n if [ \"${AGENTTEAMS_OPENAI_BASE_URL}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_OPENAI_BASE_URL=\"${AGENTTEAMS_OPENAI_BASE_URL:-${_current}}\"\n [ -z \"${AGENTTEAMS_OPENAI_BASE_URL}\" ] && AGENTTEAMS_OPENAI_BASE_URL=\"https://api.openai.com/v1\"\n fi\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg llm.openai.model_prompt)\" \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n else\n _current=\"${AGENTTEAMS_DEFAULT_MODEL}\"\n if [ -n \"${_current}\" ]; then\n read -e -p \"$(msg llm.openai.model_prompt) [${_current}]: \" AGENTTEAMS_DEFAULT_MODEL\n else\n read -e -p \"$(msg llm.openai.model_prompt) [gpt-5.4]: \" AGENTTEAMS_DEFAULT_MODEL\n fi\n if [ \"${AGENTTEAMS_DEFAULT_MODEL}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_DEFAULT_MODEL=\"${AGENTTEAMS_DEFAULT_MODEL:-${_current}}\"\n [ -z \"${AGENTTEAMS_DEFAULT_MODEL}\" ] && AGENTTEAMS_DEFAULT_MODEL=\"gpt-5.4\"\n fi\n log \"$(msg llm.openai.base_url_label \"${AGENTTEAMS_OPENAI_BASE_URL}\")\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n prompt_custom_model_params \"${AGENTTEAMS_DEFAULT_MODEL}\" || return 0\n log \"\"\n prompt AGENTTEAMS_LLM_API_KEY \"$(msg llm.apikey_prompt)\" \"\" \"true\" || return 0\n test_llm_connectivity \"${AGENTTEAMS_OPENAI_BASE_URL}\" \"${AGENTTEAMS_LLM_API_KEY}\" \"${AGENTTEAMS_DEFAULT_MODEL}\" || return 0\n ;;\n *)\n die \"$(msg llm.provider.invalid \"${PROVIDER_CHOICE}\")\"\n ;;\n esac\n\n # Skip to embedding if Keep-All mode handled LLM params\n if [ \"${SKIP_TO_EMBEDDING}\" = \"1\" ]; then\n if [ -n \"${AGENTTEAMS_EMBEDDING_MODEL}\" ]; then\n log \"$(msg llm.embedding.title)\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_EMBEDDING_MODEL}\")\"\n log \"\"\n fi\n return 0\n fi\n\n # --- Embedding model (optional, auto-tested) ---\n echo \"\"\n log \"$(msg llm.embedding.title)\"\n log \"$(msg llm.embedding.hint)\"\n echo \"\"\n echo \"$(msg llm.embedding.option.default)\"\n echo \"$(msg llm.embedding.option.custom)\"\n echo \"$(msg llm.embedding.option.disable)\"\n echo \"\"\n local EMB_CHOICE\n read -e -p \"$(msg llm.embedding.select) [1]: \" EMB_CHOICE\n EMB_CHOICE=\"${EMB_CHOICE:-1}\"\n if [ \"${EMB_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n\n case \"${EMB_CHOICE}\" in\n 1)\n AGENTTEAMS_EMBEDDING_MODEL=\"text-embedding-v4\"\n ;;\n 2)\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg llm.embedding.custom_prompt)\" \"${AGENTTEAMS_EMBEDDING_MODEL}\")\"\n else\n _current=\"${AGENTTEAMS_EMBEDDING_MODEL}\"\n if [ -n \"${_current}\" ]; then\n read -e -p \"$(msg llm.embedding.custom_prompt) [${_current}]: \" AGENTTEAMS_EMBEDDING_MODEL\n else\n read -e -p \"$(msg llm.embedding.custom_prompt): \" AGENTTEAMS_EMBEDDING_MODEL\n fi\n if [ \"${AGENTTEAMS_EMBEDDING_MODEL}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_EMBEDDING_MODEL=\"${AGENTTEAMS_EMBEDDING_MODEL:-${_current}}\"\n fi\n ;;\n 3)\n AGENTTEAMS_EMBEDDING_MODEL=\"\"\n log \"$(msg llm.embedding.disabled)\"\n ;;\n *)\n AGENTTEAMS_EMBEDDING_MODEL=\"text-embedding-v4\"\n ;;\n esac\n\n if [ -n \"${AGENTTEAMS_EMBEDDING_MODEL}\" ]; then\n # Qwen provider uses dashscope directly; others use OPENAI_BASE_URL\n local EMB_BASE_URL=\"${AGENTTEAMS_OPENAI_BASE_URL}\"\n if [ \"${AGENTTEAMS_LLM_PROVIDER}\" = \"qwen\" ]; then\n EMB_BASE_URL=\"https://dashscope.aliyuncs.com/compatible-mode/v1\"\n fi\n if ! test_embedding_connectivity \"${EMB_BASE_URL}\" \"${AGENTTEAMS_LLM_API_KEY}\" \"${AGENTTEAMS_EMBEDDING_MODEL}\"; then\n AGENTTEAMS_EMBEDDING_MODEL=\"\"\n log \"$(msg llm.embedding.auto_disabled)\"\n fi\n fi\n\n export AGENTTEAMS_LLM_PROVIDER AGENTTEAMS_DEFAULT_MODEL\n [ -n \"${AGENTTEAMS_OPENAI_BASE_URL+x}\" ] && export AGENTTEAMS_OPENAI_BASE_URL\n log \"\"\n}\n\nstep_admin() {\n log \"$(msg admin.title)\"\n prompt AGENTTEAMS_ADMIN_USER \"$(msg admin.username_prompt)\" \"admin\" || return 0\n AGENTTEAMS_ADMIN_USER=\"$(printf '%s' \"${AGENTTEAMS_ADMIN_USER}\" | tr '[:upper:]' '[:lower:]')\"\n\n # Pre-set via env var: validate; in non-interactive mode fail fast,\n # in interactive mode warn and fall through to the retry prompt.\n if [ -n \"${AGENTTEAMS_ADMIN_PASSWORD:-}\" ]; then\n log \" $(msg prompt.preset \"$(msg admin.password_prompt)\")\"\n if [ ${#AGENTTEAMS_ADMIN_PASSWORD} -ge 8 ]; then\n log \"\"\n return 0\n fi\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n die \"$(msg admin.password_too_short \"${#AGENTTEAMS_ADMIN_PASSWORD}\")\"\n fi\n error \"$(msg admin.password_too_short \"${#AGENTTEAMS_ADMIN_PASSWORD}\")\"\n unset AGENTTEAMS_ADMIN_PASSWORD\n fi\n\n while true; do\n unset AGENTTEAMS_ADMIN_PASSWORD\n prompt_optional AGENTTEAMS_ADMIN_PASSWORD \"$(msg admin.password_prompt)\" \"true\" || return 0\n if [ -z \"${AGENTTEAMS_ADMIN_PASSWORD}\" ]; then\n AGENTTEAMS_ADMIN_PASSWORD=\"admin$(openssl rand -hex 6)\"\n log \"$(msg admin.password_generated)\"\n break\n fi\n if [ ${#AGENTTEAMS_ADMIN_PASSWORD} -ge 8 ]; then\n break\n fi\n error \"$(msg admin.password_too_short \"${#AGENTTEAMS_ADMIN_PASSWORD}\")\"\n done\n log \"\"\n}\n\nstep_network() {\n log \"$(msg port.local_only.title)\"\n echo \"\"\n echo \" 1) $(msg port.local_only.hint_yes)\"\n echo \" 2) $(msg port.local_only.hint_no)\"\n echo \"\"\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_LOCAL_ONLY=\"${AGENTTEAMS_LOCAL_ONLY:-1}\"\n elif [ -z \"${AGENTTEAMS_LOCAL_ONLY+x}\" ]; then\n local _local_choice\n read -e -p \"$(msg port.local_only.choice): \" _local_choice\n if [ \"${_local_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _local_choice=\"${_local_choice:-1}\"\n case \"${_local_choice}\" in\n 2|n|N|no|NO) AGENTTEAMS_LOCAL_ONLY=\"0\" ;;\n *) AGENTTEAMS_LOCAL_ONLY=\"1\" ;;\n esac\n fi\n export AGENTTEAMS_LOCAL_ONLY\n if [ \"${AGENTTEAMS_LOCAL_ONLY}\" = \"1\" ]; then\n log \"$(msg port.local_only.selected_local)\"\n else\n log \"$(msg port.local_only.selected_external)\"\n echo \"\"\n echo -e \"\\033[33m$(msg port.local_only.https_hint)\\033[0m\"\n fi\n}\n\nstep_ports() {\n log \"$(msg port.title)\"\n prompt AGENTTEAMS_PORT_GATEWAY \"$(msg port.gateway_prompt)\" \"18080\" || return 0\n prompt AGENTTEAMS_PORT_CONSOLE \"$(msg port.console_prompt)\" \"18001\" || return 0\n prompt AGENTTEAMS_PORT_ELEMENT_WEB \"$(msg port.element_prompt)\" \"18088\" || return 0\n prompt AGENTTEAMS_PORT_MANAGER_CONSOLE \"$(msg port.manager_console_prompt)\" \"18888\" || return 0\n log \"\"\n}\n\nstep_domains() {\n log \"$(msg domain.title)\"\n log \"$(msg domain.hint)\"\n prompt AGENTTEAMS_MATRIX_DOMAIN \"$(msg domain.matrix_prompt)\" \"matrix-local.agentteams.io:${AGENTTEAMS_PORT_GATEWAY}\" || return 0\n prompt AGENTTEAMS_MATRIX_CLIENT_DOMAIN \"$(msg domain.element_prompt)\" \"matrix-client-local.agentteams.io\" || return 0\n prompt AGENTTEAMS_AI_GATEWAY_DOMAIN \"$(msg domain.gateway_prompt)\" \"aigw-local.agentteams.io\" || return 0\n prompt AGENTTEAMS_FS_DOMAIN \"$(msg domain.fs_prompt)\" \"fs-local.agentteams.io\" || return 0\n if [ \"${AGENTTEAMS_MANAGER_RUNTIME}\" != \"copaw\" ]; then\n prompt AGENTTEAMS_CONSOLE_DOMAIN \"$(msg domain.console_prompt)\" \"console-local.agentteams.io\" || return 0\n fi\n log \"\"\n}\n\nstep_github() {\n log \"$(msg github.title)\"\n prompt_optional AGENTTEAMS_GITHUB_TOKEN \"$(msg github.token_prompt)\" \"true\" || return 0\n}\n\nstep_skills() {\n log \"\"\n log \"$(msg skills.title)\"\n prompt_optional AGENTTEAMS_SKILLS_API_URL \"$(msg skills.url_prompt)\" || return 0\n log \"\"\n}\n\nstep_volume() {\n log \"$(msg data.title)\"\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_DATA_DIR=\"${AGENTTEAMS_DATA_DIR:-agentteams-data}\"\n log \" $(msg data.volume_using \"${AGENTTEAMS_DATA_DIR}\") (non-interactive, skipped)\"\n export AGENTTEAMS_DATA_DIR\n return 0\n fi\n # ─────────────────────────────────────────────────────────────────\n if [ -z \"${AGENTTEAMS_DATA_DIR+x}\" ]; then\n local _input\n read -e -p \"$(msg data.volume_prompt): \" _input\n if [ \"${_input}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_DATA_DIR=\"${_input:-agentteams-data}\"\n export AGENTTEAMS_DATA_DIR\n fi\n AGENTTEAMS_DATA_DIR=\"${AGENTTEAMS_DATA_DIR:-agentteams-data}\"\n log \"$(msg data.volume_using \"${AGENTTEAMS_DATA_DIR}\")\"\n}\n\nstep_workspace() {\n log \"$(msg workspace.title)\"\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_WORKSPACE_DIR=\"${AGENTTEAMS_WORKSPACE_DIR:-${HOME}/agentteams-manager}\"\n AGENTTEAMS_WORKSPACE_DIR=\"$(cd \"${AGENTTEAMS_WORKSPACE_DIR}\" 2>/dev/null && pwd || echo \"${AGENTTEAMS_WORKSPACE_DIR}\")\"\n mkdir -p \"${AGENTTEAMS_WORKSPACE_DIR}\"\n log \" $(msg workspace.dir_label \"${AGENTTEAMS_WORKSPACE_DIR}\") (non-interactive, skipped)\"\n export AGENTTEAMS_WORKSPACE_DIR\n return 0\n fi\n # ─────────────────────────────────────────────────────────────────\n local _input\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg workspace.dir_prompt \"${HOME}/agentteams-manager\")\" \"${AGENTTEAMS_WORKSPACE_DIR}\")\"\n _input=\"${AGENTTEAMS_WORKSPACE_DIR}\"\n else\n local _current=\"${AGENTTEAMS_WORKSPACE_DIR}\"\n if [ -n \"${_current}\" ]; then\n read -e -p \"$(msg workspace.dir_prompt \"${HOME}/agentteams-manager\") [${_current}]: \" _input\n else\n read -e -p \"$(msg workspace.dir_prompt \"${HOME}/agentteams-manager\"): \" _input\n fi\n if [ \"${_input}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _input=\"${_input:-${_current}}\"\n fi\n AGENTTEAMS_WORKSPACE_DIR=\"${_input:-${HOME}/agentteams-manager}\"\n export AGENTTEAMS_WORKSPACE_DIR\n AGENTTEAMS_WORKSPACE_DIR=\"$(cd \"${AGENTTEAMS_WORKSPACE_DIR}\" 2>/dev/null && pwd || echo \"${AGENTTEAMS_WORKSPACE_DIR}\")\"\n mkdir -p \"${AGENTTEAMS_WORKSPACE_DIR}\"\n log \"$(msg workspace.dir_label \"${AGENTTEAMS_WORKSPACE_DIR}\")\"\n}\n\nstep_dashboard() {\n AGENTTEAMS_DASHBOARD=\"${AGENTTEAMS_DASHBOARD:-1}\"\n AGENTTEAMS_DASHBOARD_VERSION=\"${AGENTTEAMS_DASHBOARD_VERSION:-v1.2.0-beta.1}\"\n AGENTTEAMS_PORT_DASHBOARD=\"${AGENTTEAMS_PORT_DASHBOARD:-13000}\"\n AGENTTEAMS_AI_GATEWAY_ADMIN_URL=\"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL:-}\"\n\n # Compute the default image for the current version.\n # We track whether the current image matches the derived default so that\n # changing the version also updates the image (unless the user has\n # explicitly overridden it to a non-default value).\n _dashboard_default_image() {\n echo \"${AGENTTEAMS_REGISTRY}/agentteams/agentteams-dashboard:${1:-${AGENTTEAMS_DASHBOARD_VERSION}}\"\n }\n local _init_default\n _init_default=$(_dashboard_default_image)\n if [ -z \"${AGENTTEAMS_DASHBOARD_IMAGE:-}\" ]; then\n AGENTTEAMS_DASHBOARD_IMAGE=\"${_init_default}\"\n fi\n\n # Recompute default image when the version changes AND the current image\n # matches the default for the *previous* (env-file) version.\n # This handles non-interactive upgrades correctly:\n # - env file loads old default image + old version\n # - CLI passes new version\n # - image should follow (because it was auto-derived before)\n # But preserves truly custom images (same repo but custom tag like :canary)\n # because they won't match the old-version default exactly.\n local _old_saved_version=\"\"\n local _env_file=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n if [ -f \"${_env_file}\" ]; then\n _old_saved_version=\"$(grep '^AGENTTEAMS_DASHBOARD_VERSION=' \"${_env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n fi\n if [ -n \"${_old_saved_version}\" ] && \\\n [ \"${_old_saved_version}\" != \"${AGENTTEAMS_DASHBOARD_VERSION}\" ] && \\\n [ \"${AGENTTEAMS_DASHBOARD_IMAGE}\" = \"$(_dashboard_default_image \"${_old_saved_version}\")\" ]; then\n AGENTTEAMS_DASHBOARD_IMAGE=$(_dashboard_default_image)\n fi\n\n # Quick Start: accept all defaults without prompting. The dashboard is\n # installed by default (AGENTTEAMS_DASHBOARD=1); set AGENTTEAMS_DASHBOARD=0\n # to opt out.\n if [ \"${AGENTTEAMS_QUICKSTART:-0}\" = \"1\" ]; then\n if [ \"${AGENTTEAMS_DASHBOARD}\" = \"1\" ]; then\n log \"$(msg dash.summary \"${AGENTTEAMS_PORT_DASHBOARD}\" \"${AGENTTEAMS_DASHBOARD_IMAGE}\") (quick start)\"\n else\n log \"$(msg dash.skip) (quick start)\"\n fi\n export AGENTTEAMS_DASHBOARD AGENTTEAMS_DASHBOARD_VERSION AGENTTEAMS_PORT_DASHBOARD AGENTTEAMS_DASHBOARD_IMAGE AGENTTEAMS_AI_GATEWAY_ADMIN_URL\n return 0\n fi\n\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_DASHBOARD=\"${AGENTTEAMS_DASHBOARD:-1}\"\n if [ \"${AGENTTEAMS_DASHBOARD}\" = \"1\" ]; then\n log \"$(msg dash.summary \"${AGENTTEAMS_PORT_DASHBOARD}\" \"${AGENTTEAMS_DASHBOARD_IMAGE}\") (non-interactive)\"\n else\n log \"$(msg dash.skip) (non-interactive)\"\n fi\n export AGENTTEAMS_DASHBOARD AGENTTEAMS_DASHBOARD_VERSION AGENTTEAMS_PORT_DASHBOARD AGENTTEAMS_DASHBOARD_IMAGE AGENTTEAMS_AI_GATEWAY_ADMIN_URL\n return 0\n fi\n\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ]; then\n log \"$(msg prompt.upgrade_keep \"\" \"${AGENTTEAMS_DASHBOARD}\") dashboard=${AGENTTEAMS_DASHBOARD} version=${AGENTTEAMS_DASHBOARD_VERSION} port=${AGENTTEAMS_PORT_DASHBOARD}\"\n export AGENTTEAMS_DASHBOARD AGENTTEAMS_DASHBOARD_VERSION AGENTTEAMS_PORT_DASHBOARD AGENTTEAMS_DASHBOARD_IMAGE AGENTTEAMS_AI_GATEWAY_ADMIN_URL\n return 0\n fi\n\n local _input\n echo \"\"\n read -p \"$(msg dash.prompt) [Y/n]: \" _input\n case \"${_input:-}\" in\n [nN0]) AGENTTEAMS_DASHBOARD=0 ;;\n *) AGENTTEAMS_DASHBOARD=1 ;;\n esac\n if [ \"${_input}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n\n if [ \"${AGENTTEAMS_DASHBOARD}\" != \"1\" ]; then\n log \"$(msg dash.skip)\"\n export AGENTTEAMS_DASHBOARD AGENTTEAMS_DASHBOARD_VERSION AGENTTEAMS_PORT_DASHBOARD AGENTTEAMS_DASHBOARD_IMAGE AGENTTEAMS_AI_GATEWAY_ADMIN_URL\n return 0\n fi\n\n # Dashboard version\n local _current_version=\"${AGENTTEAMS_DASHBOARD_VERSION}\"\n local _old_version=\"${AGENTTEAMS_DASHBOARD_VERSION}\"\n local _old_default_image\n _old_default_image=$(_dashboard_default_image \"${_old_version}\")\n read -p \"$(msg dash.version_prompt) [${_current_version}]: \" _input\n AGENTTEAMS_DASHBOARD_VERSION=\"${_input:-${_current_version}}\"\n\n # Dashboard port\n local _current_port=\"${AGENTTEAMS_PORT_DASHBOARD}\"\n read -p \"$(msg dash.port_prompt) [${_current_port}]: \" _input\n AGENTTEAMS_PORT_DASHBOARD=\"${_input:-${_current_port}}\"\n\n # Dashboard image: if the current image matches the default derived from\n # the OLD version, recompute it from the NEW version. This handles both\n # fresh installs and upgrades where the previous image was the default.\n # Only a real user override (image != default for old version) is preserved.\n if [ \"${AGENTTEAMS_DASHBOARD_VERSION}\" != \"${_old_version}\" ] && \\\n [ \"${AGENTTEAMS_DASHBOARD_IMAGE}\" = \"${_old_default_image}\" ]; then\n AGENTTEAMS_DASHBOARD_IMAGE=$(_dashboard_default_image)\n fi\n local _current_image=\"${AGENTTEAMS_DASHBOARD_IMAGE}\"\n read -p \"$(msg dash.image_prompt) [${_current_image}]: \" _input\n if [ -n \"${_input}\" ]; then\n AGENTTEAMS_DASHBOARD_IMAGE=\"${_input}\"\n fi\n\n # Higress Console URL (explicit config takes priority; auto-detect as fallback)\n local _higress_url=\"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\"\n if [ -z \"${_higress_url}\" ]; then\n if ${DOCKER_CMD} ps --format '{{.Names}}' | grep -q \"^agentteams-controller$\"; then\n if ${DOCKER_CMD} exec agentteams-controller curl -sf --max-time 2 http://127.0.0.1:8001/ >/dev/null 2>&1; then\n _higress_url=\"http://agentteams-controller:8001\"\n log \"$(msg dash.auto_url): ${_higress_url}\"\n else\n log \"$(msg dash.auto_url_fail)\"\n fi\n fi\n fi\n read -p \"$(msg dash.gateway_prompt) [${_higress_url:-<auto>}]: \" _input\n AGENTTEAMS_AI_GATEWAY_ADMIN_URL=\"${_input:-${_higress_url}}\"\n\n # Normalize URL: add http:// prefix if missing\n if [ -n \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\" ]; then\n case \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\" in\n http://*|https://*) ;;\n *) AGENTTEAMS_AI_GATEWAY_ADMIN_URL=\"http://${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\" ;;\n esac\n fi\n\n # Verify the Higress Console URL is reachable (best-effort warning only)\n if [ -n \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\" ]; then\n local _gw_reachable=0\n # If the URL points to the controller container, test from inside the container\n if echo \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\" | grep -qE \"(agentteams-controller|hiclaw-controller|127\\.0\\.0\\.1|localhost)\"; then\n if ${DOCKER_CMD} ps --format '{{.Names}}' | grep -q \"^agentteams-controller$\"; then\n if ${DOCKER_CMD} exec agentteams-controller curl -sf --max-time 3 \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}/\" >/dev/null 2>&1; then\n _gw_reachable=1\n fi\n fi\n else\n # External URL — probe from inside the controller (same network as\n # the Dashboard container) when possible; fall back to the host.\n if ${DOCKER_CMD} ps --format '{{.Names}}' | grep -q \"^agentteams-controller$\"; then\n if ${DOCKER_CMD} exec agentteams-controller curl -sf --max-time 3 \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}/\" >/dev/null 2>&1; then\n _gw_reachable=1\n fi\n elif curl -sf --max-time 3 \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}/\" >/dev/null 2>&1; then\n _gw_reachable=1\n fi\n fi\n if [ \"${_gw_reachable}\" = \"0\" ]; then\n log \"WARNING: Higress Console URL may not be reachable: ${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\"\n log \" Dashboard will still work, but shared login via Higress Console may fail.\"\n log \" To fix: verify the URL is correct and the Higress Console service is running.\"\n fi\n fi\n\n log \"$(msg dash.summary \"${AGENTTEAMS_PORT_DASHBOARD}\" \"${AGENTTEAMS_DASHBOARD_IMAGE}\")\"\n export AGENTTEAMS_DASHBOARD AGENTTEAMS_DASHBOARD_VERSION AGENTTEAMS_PORT_DASHBOARD AGENTTEAMS_DASHBOARD_IMAGE AGENTTEAMS_AI_GATEWAY_ADMIN_URL\n}\n\nstep_runtime() {\n log \"$(msg worker_runtime.title)\"\n echo \"\"\n echo \" 1) $(msg worker_runtime.copaw)\"\n echo \" 2) $(msg worker_runtime.openclaw)\"\n if ! _ver_lt \"${AGENTTEAMS_VERSION}\" \"v1.1.0\"; then\n echo \" 3) $(msg worker_runtime.hermes)\"\n fi\n echo \"\"\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"${AGENTTEAMS_DEFAULT_WORKER_RUNTIME:-copaw}\"\n elif [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_DEFAULT_WORKER_RUNTIME}\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg worker_runtime.title_short)\" \"${AGENTTEAMS_DEFAULT_WORKER_RUNTIME}\")\"\n local _runtime_choice\n read -e -p \"$(msg worker_runtime.choice): \" _runtime_choice\n if [ \"${_runtime_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n if [ -n \"${_runtime_choice}\" ]; then\n case \"${_runtime_choice}\" in\n 2) AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"openclaw\" ;;\n 3) if ! _ver_lt \"${AGENTTEAMS_VERSION}\" \"v1.1.0\"; then\n AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"hermes\"\n else\n AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"copaw\"\n fi ;;\n *) AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"copaw\" ;;\n esac\n fi\n elif [ -z \"${AGENTTEAMS_DEFAULT_WORKER_RUNTIME+x}\" ]; then\n local _runtime_choice\n read -e -p \"$(msg worker_runtime.choice): \" _runtime_choice\n if [ \"${_runtime_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _runtime_choice=\"${_runtime_choice:-1}\"\n case \"${_runtime_choice}\" in\n 2) AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"openclaw\" ;;\n 3) if ! _ver_lt \"${AGENTTEAMS_VERSION}\" \"v1.1.0\"; then\n AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"hermes\"\n else\n AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"copaw\"\n fi ;;\n *) AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"copaw\" ;;\n esac\n fi\n export AGENTTEAMS_DEFAULT_WORKER_RUNTIME\n log \"$(msg worker_runtime.selected \"${AGENTTEAMS_DEFAULT_WORKER_RUNTIME}\")\"\n}\n\nstep_manager_runtime() {\n log \"$(msg manager_runtime.title)\"\n echo \"\"\n echo \" 1) $(msg manager_runtime.copaw)\"\n echo \" 2) $(msg manager_runtime.openclaw)\"\n echo \"\"\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_MANAGER_RUNTIME=\"${AGENTTEAMS_MANAGER_RUNTIME:-copaw}\"\n elif [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_MANAGER_RUNTIME}\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg manager_runtime.title_short)\" \"${AGENTTEAMS_MANAGER_RUNTIME}\")\"\n local _runtime_choice\n read -e -p \"$(msg manager_runtime.choice): \" _runtime_choice\n if [ \"${_runtime_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n if [ -n \"${_runtime_choice}\" ]; then\n case \"${_runtime_choice}\" in\n 2) AGENTTEAMS_MANAGER_RUNTIME=\"openclaw\" ;;\n *) AGENTTEAMS_MANAGER_RUNTIME=\"copaw\" ;;\n esac\n fi\n elif [ -z \"${AGENTTEAMS_MANAGER_RUNTIME+x}\" ]; then\n local _runtime_choice\n read -e -p \"$(msg manager_runtime.choice): \" _runtime_choice\n if [ \"${_runtime_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _runtime_choice=\"${_runtime_choice:-1}\"\n case \"${_runtime_choice}\" in\n 2) AGENTTEAMS_MANAGER_RUNTIME=\"openclaw\" ;;\n *) AGENTTEAMS_MANAGER_RUNTIME=\"copaw\" ;;\n esac\n fi\n export AGENTTEAMS_MANAGER_RUNTIME\n log \"$(msg manager_runtime.selected \"${AGENTTEAMS_MANAGER_RUNTIME}\")\"\n}\n\nstep_e2ee() {\n log \"\"\n log \"$(msg matrix_e2ee.title)\"\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_MATRIX_E2EE=\"${AGENTTEAMS_MATRIX_E2EE:-0}\"\n log \" $(msg matrix_e2ee.title_short) = ${AGENTTEAMS_MATRIX_E2EE} (non-interactive, skipped)\"\n export AGENTTEAMS_MATRIX_E2EE\n return 0\n fi\n # ─────────────────────────────────────────────────────────────────\n echo \"\"\n echo -e \" $(msg matrix_e2ee.desc)\"\n echo \"\"\n echo \" 1) $(msg matrix_e2ee.disable)\"\n echo \" 2) $(msg matrix_e2ee.enable)\"\n echo \"\"\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_MATRIX_E2EE}\" ]; then\n local _e2ee_display; if [ \"${AGENTTEAMS_MATRIX_E2EE}\" = \"1\" ]; then _e2ee_display=\"$(msg matrix_e2ee.val_enabled)\"; else _e2ee_display=\"$(msg matrix_e2ee.val_disabled)\"; fi\n log \"$(msg prompt.upgrade_keep \"$(msg matrix_e2ee.title_short)\" \"${_e2ee_display}\")\"\n local _e2ee_choice\n read -e -p \"$(msg matrix_e2ee.choice): \" _e2ee_choice\n if [ \"${_e2ee_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n if [ -n \"${_e2ee_choice}\" ]; then\n case \"${_e2ee_choice}\" in\n 2) AGENTTEAMS_MATRIX_E2EE=\"1\" ;;\n *) AGENTTEAMS_MATRIX_E2EE=\"0\" ;;\n esac\n fi\n elif [ -z \"${AGENTTEAMS_MATRIX_E2EE+x}\" ]; then\n local _e2ee_choice\n read -e -p \"$(msg matrix_e2ee.choice): \" _e2ee_choice\n if [ \"${_e2ee_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _e2ee_choice=\"${_e2ee_choice:-1}\"\n case \"${_e2ee_choice}\" in\n 2) AGENTTEAMS_MATRIX_E2EE=\"1\" ;;\n *) AGENTTEAMS_MATRIX_E2EE=\"0\" ;;\n esac\n fi\n AGENTTEAMS_MATRIX_E2EE=\"${AGENTTEAMS_MATRIX_E2EE:-0}\"\n export AGENTTEAMS_MATRIX_E2EE\n if [ \"${AGENTTEAMS_MATRIX_E2EE}\" = \"1\" ]; then\n log \"$(msg matrix_e2ee.selected_enabled)\"\n else\n log \"$(msg matrix_e2ee.selected_disabled)\"\n fi\n}\n\nstep_docker_proxy() {\n # Only relevant when socket mounting is enabled\n if [ \"${AGENTTEAMS_MOUNT_SOCKET}\" != \"1\" ]; then\n AGENTTEAMS_DOCKER_PROXY=\"0\"\n return 0\n fi\n\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_DOCKER_PROXY=\"${AGENTTEAMS_DOCKER_PROXY:-0}\"\n log \" $(msg docker_proxy.title_short) = ${AGENTTEAMS_DOCKER_PROXY} (non-interactive, skipped)\"\n export AGENTTEAMS_DOCKER_PROXY\n return 0\n fi\n # ─────────────────────────────────────────────────────────────────\n\n echo \"\"\n echo -e \" \\033[1m$(msg docker_proxy.title)\\033[0m\"\n echo \"\"\n echo -e \" $(msg docker_proxy.desc)\"\n echo \"\"\n echo \" 1) $(msg docker_proxy.enable)\"\n echo \" 2) $(msg docker_proxy.disable)\"\n echo \"\"\n\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_DOCKER_PROXY}\" ]; then\n local _proxy_display; if [ \"${AGENTTEAMS_DOCKER_PROXY}\" = \"1\" ]; then _proxy_display=\"$(msg docker_proxy.val_enabled)\"; else _proxy_display=\"$(msg docker_proxy.val_disabled)\"; fi\n log \"$(msg prompt.upgrade_keep \"$(msg docker_proxy.title_short)\" \"${_proxy_display}\")\"\n local _choice\n read -e -p \"$(msg docker_proxy.choice): \" _choice\n if [ \"${_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n if [ -n \"${_choice}\" ]; then\n case \"${_choice}\" in\n 2) AGENTTEAMS_DOCKER_PROXY=\"0\" ;;\n *) AGENTTEAMS_DOCKER_PROXY=\"1\" ;;\n esac\n fi\n elif [ -z \"${AGENTTEAMS_DOCKER_PROXY+x}\" ]; then\n local _choice\n read -e -p \"$(msg docker_proxy.choice): \" _choice\n if [ \"${_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _choice=\"${_choice:-1}\"\n case \"${_choice}\" in\n 2) AGENTTEAMS_DOCKER_PROXY=\"0\" ;;\n *) AGENTTEAMS_DOCKER_PROXY=\"1\" ;;\n esac\n fi\n AGENTTEAMS_DOCKER_PROXY=\"${AGENTTEAMS_DOCKER_PROXY:-1}\"\n export AGENTTEAMS_DOCKER_PROXY\n if [ \"${AGENTTEAMS_DOCKER_PROXY}\" = \"1\" ]; then\n log \"$(msg docker_proxy.selected_enabled)\"\n\n # Prompt for additional allowed image sources\n echo \"\"\n echo -e \" $(msg docker_proxy.registries_desc)\"\n echo \"\"\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_PROXY_ALLOWED_REGISTRIES}\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg docker_proxy.registries_label)\" \"${AGENTTEAMS_PROXY_ALLOWED_REGISTRIES}\")\"\n local _reg_input\n read -e -p \"$(msg docker_proxy.registries_prompt): \" _reg_input\n if [ \"${_reg_input}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n [ -n \"${_reg_input}\" ] && AGENTTEAMS_PROXY_ALLOWED_REGISTRIES=\"${_reg_input}\"\n elif [ -z \"${AGENTTEAMS_PROXY_ALLOWED_REGISTRIES+x}\" ]; then\n local _reg_input\n read -e -p \"$(msg docker_proxy.registries_prompt): \" _reg_input\n if [ \"${_reg_input}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_PROXY_ALLOWED_REGISTRIES=\"${_reg_input:-}\"\n fi\n export AGENTTEAMS_PROXY_ALLOWED_REGISTRIES\n else\n log \"$(msg docker_proxy.selected_disabled)\"\n fi\n}\n\nstep_idle() {\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_WORKER_IDLE_TIMEOUT=\"${AGENTTEAMS_WORKER_IDLE_TIMEOUT:-720}\"\n log \" $(msg idle_timeout.label) = ${AGENTTEAMS_WORKER_IDLE_TIMEOUT} (non-interactive, skipped)\"\n export AGENTTEAMS_WORKER_IDLE_TIMEOUT\n return 0\n fi\n # ─────────────────────────────────────────────────────────────────\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_WORKER_IDLE_TIMEOUT}\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg idle_timeout.label)\" \"${AGENTTEAMS_WORKER_IDLE_TIMEOUT}\")\"\n local _idle_timeout\n read -e -p \"$(msg idle_timeout.prompt): \" _idle_timeout\n if [ \"${_idle_timeout}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n [ -n \"${_idle_timeout}\" ] && AGENTTEAMS_WORKER_IDLE_TIMEOUT=\"${_idle_timeout}\"\n elif [ -z \"${AGENTTEAMS_WORKER_IDLE_TIMEOUT+x}\" ]; then\n local _idle_timeout\n read -e -p \"$(msg idle_timeout.prompt): \" _idle_timeout\n if [ \"${_idle_timeout}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_WORKER_IDLE_TIMEOUT=\"${_idle_timeout:-720}\"\n fi\n AGENTTEAMS_WORKER_IDLE_TIMEOUT=\"${AGENTTEAMS_WORKER_IDLE_TIMEOUT:-720}\"\n export AGENTTEAMS_WORKER_IDLE_TIMEOUT\n log \"$(msg idle_timeout.selected \"${AGENTTEAMS_WORKER_IDLE_TIMEOUT}\")\"\n}\n\nstep_hostshare() {\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_HOST_SHARE_DIR=\"${AGENTTEAMS_HOST_SHARE_DIR:-${HOME}}\"\n log \" $(msg host_share.label) = ${AGENTTEAMS_HOST_SHARE_DIR} (non-interactive, skipped)\"\n export AGENTTEAMS_HOST_SHARE_DIR\n return 0\n fi\n # ─────────────────────────────────────────────────────────────────\n local _current=\"${AGENTTEAMS_HOST_SHARE_DIR}\"\n local _share_dir\n if [ -n \"${_current}\" ]; then\n read -e -p \"$(msg host_share.prompt \"$HOME\") [${_current}]: \" _share_dir\n else\n read -e -p \"$(msg host_share.prompt \"$HOME\"): \" _share_dir\n fi\n if [ \"${_share_dir}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_HOST_SHARE_DIR=\"${_share_dir:-${_current:-$HOME}}\"\n export AGENTTEAMS_HOST_SHARE_DIR\n}\n\nstep_podman_autostart() {\n echo \"\"\n log \"$(msg podman.autostart.title)\"\n echo \"\"\n echo -e \" $(msg podman.autostart.desc)\"\n echo \"\"\n echo \" 1) $(msg podman.autostart.enable)\"\n echo \" 2) $(msg podman.autostart.disable)\"\n echo \"\"\n\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_PODMAN_AUTOSTART}\" ]; then\n local _disp; if [ \"${AGENTTEAMS_PODMAN_AUTOSTART}\" = \"1\" ]; then _disp=\"$(msg podman.autostart.val_enabled)\"; else _disp=\"$(msg podman.autostart.val_disabled)\"; fi\n log \"$(msg prompt.upgrade_keep \"$(msg podman.autostart.title_short)\" \"${_disp}\")\"\n local _choice\n read -e -p \"$(msg podman.autostart.choice): \" _choice\n if [ \"${_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n if [ -n \"${_choice}\" ]; then\n case \"${_choice}\" in\n 2) AGENTTEAMS_PODMAN_AUTOSTART=\"0\" ;;\n *) AGENTTEAMS_PODMAN_AUTOSTART=\"1\" ;;\n esac\n fi\n elif [ -z \"${AGENTTEAMS_PODMAN_AUTOSTART+x}\" ]; then\n local _choice\n read -e -p \"$(msg podman.autostart.choice) [1]: \" _choice\n if [ \"${_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _choice=\"${_choice:-1}\"\n case \"${_choice}\" in\n 2) AGENTTEAMS_PODMAN_AUTOSTART=\"0\" ;;\n *) AGENTTEAMS_PODMAN_AUTOSTART=\"1\" ;;\n esac\n fi\n AGENTTEAMS_PODMAN_AUTOSTART=\"${AGENTTEAMS_PODMAN_AUTOSTART:-1}\"\n export AGENTTEAMS_PODMAN_AUTOSTART\n if [ \"${AGENTTEAMS_PODMAN_AUTOSTART}\" = \"1\" ]; then\n log \"$(msg podman.autostart.selected_enabled)\"\n else\n log \"$(msg podman.autostart.selected_disabled)\"\n fi\n}\n\n# ============================================================\n# Setup dedicated Podman autostart service for AgentTeams\n# ============================================================\nsetup_podman_autostart() {\n # Only execute if using podman and autostart is enabled\n if [ \"${DOCKER_CMD:-docker}\" != \"podman\" ] || [ \"${AGENTTEAMS_PODMAN_AUTOSTART:-0}\" != \"1\" ]; then\n return 0\n fi\n\n log \"$(msg install.podman.autostart_title)\"\n\n local current_user\n current_user=\"$(whoami)\"\n\n # Define the dedicated AgentTeams service content\n # Strict scoping: Only manage containers matching '--filter name=agentteams'\n local _service_content=\"[Unit]\nDescription=AgentTeams Dedicated Podman Autostart Service\nDocumentation=https://github.com/agentscope-ai/AgentTeams\nWants=network-online.target\nAfter=network-online.target\n\n[Service]\nType=oneshot\nRemainAfterExit=true\nEnvironment=LOGGING=\\\"--log-level=info\\\"\nExecStart=/usr/bin/podman \\$LOGGING start --filter name=agentteams --filter restart-policy=always --filter restart-policy=unless-stopped\nExecStop=/usr/bin/podman \\$LOGGING stop --filter name=agentteams --filter restart-policy=always --filter restart-policy=unless-stopped\n\n[Install]\nWantedBy=default.target\"\n\n local _autostart_ok=0\n\n if [ \"${current_user}\" = \"root\" ]; then\n # Rootful Mode (System-wide dedicated service)\n log \"$(msg install.podman.root_setup)\"\n\n local _sys_dir=\"/etc/systemd/system\"\n mkdir -p \"${_sys_dir}\"\n echo \"${_service_content}\" > \"${_sys_dir}/agentteams-podman-restart.service\"\n\n systemctl daemon-reload >/dev/null 2>&1 || true\n\n if systemctl enable --now agentteams-podman-restart.service >/dev/null 2>&1; then\n log \"$(msg install.podman.root_success)\"\n _autostart_ok=1\n else\n log \"$(msg install.podman.root_fail)\"\n fi\n else\n # Rootless Mode (User-level dedicated service)\n log \"$(msg install.podman.user_setup \"${current_user}\")\"\n\n local _linger_ok=0\n\n # 1. Enable Linger for background execution (Graceful escalation strategy)\n if loginctl show-user \"${current_user}\" --property=Linger 2>/dev/null | grep -q \"Linger=yes\"; then\n _linger_ok=1\n else\n log \"$(msg install.podman.linger_enable)\"\n\n # Step A: Try to enable linger natively without sudo\n if loginctl enable-linger \"${current_user}\" >/dev/null 2>&1; then\n log \"Successfully enabled systemd linger natively.\"\n _linger_ok=1\n else\n # Step B: Fallback to sudo\n log \"Native linger enablement failed. Distro security policy requires privileges.\"\n if command -v sudo >/dev/null 2>&1; then\n log \"Attempting to enable systemd linger via sudo (enter password if prompted)...\"\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n sudo -n loginctl enable-linger \"${current_user}\" 2>/dev/null && _linger_ok=1 || log \"$(msg install.podman.linger_warn)\"\n else\n sudo loginctl enable-linger \"${current_user}\" && _linger_ok=1 || log \"$(msg install.podman.linger_warn)\"\n fi\n else\n log \"$(msg install.podman.linger_warn)\"\n fi\n fi\n fi\n\n # 2. Write dedicated service file to user directory\n local _user_dir=\"${HOME}/.config/systemd/user\"\n mkdir -p \"${_user_dir}\"\n echo \"${_service_content}\" > \"${_user_dir}/agentteams-podman-restart.service\"\n\n systemctl --user daemon-reload >/dev/null 2>&1 || true\n\n if systemctl --user enable --now agentteams-podman-restart.service >/dev/null 2>&1; then\n log \"$(msg install.podman.user_success)\"\n # Crucial: Only consider autostart fully successful if systemd linger is also enabled\n if [ \"${_linger_ok}\" = \"1\" ]; then\n _autostart_ok=1\n else\n log \"⚠️ Service enabled, but since systemd linger is not active, containers will not auto-recover on system reboot.\"\n fi\n else\n log \"$(msg install.podman.user_fail)\"\n fi\n fi\n\n # Only print final success banner if the service AND linger were actually enabled\n if [ \"${_autostart_ok}\" = \"1\" ]; then\n log \"$(msg install.podman.success)\"\n fi\n}\n\n# ============================================================\n# Dashboard startup (called after controller is ready)\n# ============================================================\n\n_start_dashboard() {\n local CTRL_CONTAINER=\"agentteams-controller\"\n local DASHBOARD_CONTAINER=\"agentteams-dashboard\"\n\n if [ \"${AGENTTEAMS_DASHBOARD:-0}\" != \"1\" ]; then\n # Stop and remove the existing dashboard container if present.\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -q \"^${DASHBOARD_CONTAINER}$\"; then\n log \"Stopping and removing ${DASHBOARD_CONTAINER}...\"\n ${DOCKER_CMD} stop \"${DASHBOARD_CONTAINER}\" >/dev/null 2>&1 || true\n ${DOCKER_CMD} rm -f \"${DASHBOARD_CONTAINER}\" >/dev/null 2>&1 || true\n log \"Dashboard stopped.\"\n fi\n return 0\n fi\n\n # Dashboard requires the embedded architecture (controller container).\n # In legacy all-in-one mode the controller API is not available.\n if [ \"${AGENTTEAMS_USE_EMBEDDED:-0}\" != \"1\" ]; then\n log \"Skipping Dashboard: requires embedded architecture (agentteams-controller), not available in legacy mode.\"\n log \"Use Quick Start (embedded) mode to enable agentteams-dashboard.\"\n return 0\n fi\n\n AGENTTEAMS_PORT_DASHBOARD=\"${AGENTTEAMS_PORT_DASHBOARD:-13000}\"\n AGENTTEAMS_DASHBOARD_VERSION=\"${AGENTTEAMS_DASHBOARD_VERSION:-v1.2.0-beta.1}\"\n AGENTTEAMS_DASHBOARD_IMAGE=\"${AGENTTEAMS_DASHBOARD_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-dashboard:${AGENTTEAMS_DASHBOARD_VERSION}}\"\n\n log \"\"\n log \"Starting agentteams-dashboard...\"\n\n local BIND_ADDR=\"127.0.0.1\"\n [ \"${AGENTTEAMS_LOCAL_ONLY:-1}\" = \"0\" ] && BIND_ADDR=\"0.0.0.0\"\n\n # Remove existing container\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' | grep -qx \"${DASHBOARD_CONTAINER}\"; then\n ${DOCKER_CMD} rm -f \"${DASHBOARD_CONTAINER}\" >/dev/null 2>&1 || true\n fi\n\n # Pull or use local image\n if ! ${DOCKER_CMD} image inspect \"${AGENTTEAMS_DASHBOARD_IMAGE}\" >/dev/null 2>&1; then\n log \"Pulling dashboard image: ${AGENTTEAMS_DASHBOARD_IMAGE}\"\n ${DOCKER_CMD} pull \"${AGENTTEAMS_DASHBOARD_IMAGE}\" 2>/dev/null || {\n log \"WARNING: Could not pull dashboard image. Skipping.\"\n return 0\n }\n fi\n\n # Build env args from controller container (MinIO/LLM/auth).\n local env_args=()\n env_args+=(-e AGENTTEAMS_CONTROLLER_URL=\"http://${CTRL_CONTAINER}:8090\")\n env_args+=(-e NEXT_PUBLIC_MATRIX_API_URL=\"http://${CTRL_CONTAINER}:6167\")\n env_args+=(-e MATRIX_HOMESERVER_ALLOWLIST=\"${CTRL_CONTAINER},matrix-local.agentteams.io,matrix.org\")\n\n if ${DOCKER_CMD} ps --format '{{.Names}}' | grep -qx \"${CTRL_CONTAINER}\"; then\n local env_out\n env_out=$(${DOCKER_CMD} inspect \"${CTRL_CONTAINER}\" --format='{{range .Config.Env}}{{.}}{{\"\\n\"}}{{end}}')\n\n _env() { echo \"${env_out}\" | sed -n \"s|^${1}=||p\" | head -n1; }\n\n local fs_endpoint; fs_endpoint=$(_env AGENTTEAMS_FS_ENDPOINT)\n [ -z \"${fs_endpoint}\" ] && fs_endpoint=$(_env AGENTTEAMS_MINIO_ENDPOINT)\n if [ -n \"${fs_endpoint}\" ]; then\n fs_endpoint=$(echo \"${fs_endpoint}\" | sed -e \"s|127\\.0\\.0\\.1|${CTRL_CONTAINER}|\" -e \"s|localhost|${CTRL_CONTAINER}|\")\n env_args+=(-e AGENTTEAMS_FS_ENDPOINT=\"${fs_endpoint}\")\n else\n env_args+=(-e AGENTTEAMS_FS_ENDPOINT=\"http://${CTRL_CONTAINER}:9000\")\n fi\n\n local fs_bucket; fs_bucket=$(_env AGENTTEAMS_FS_BUCKET)\n [ -z \"${fs_bucket}\" ] && fs_bucket=$(_env AGENTTEAMS_MINIO_BUCKET)\n [ -n \"${fs_bucket}\" ] && env_args+=(-e AGENTTEAMS_FS_BUCKET=\"${fs_bucket}\")\n\n local fs_user; fs_user=$(_env AGENTTEAMS_FS_ACCESS_KEY)\n [ -z \"${fs_user}\" ] && fs_user=$(_env AGENTTEAMS_MINIO_USER)\n [ -n \"${fs_user}\" ] && env_args+=(-e AGENTTEAMS_FS_ACCESS_KEY=\"${fs_user}\")\n\n local fs_pass; fs_pass=$(_env AGENTTEAMS_FS_SECRET_KEY)\n [ -z \"${fs_pass}\" ] && fs_pass=$(_env AGENTTEAMS_MINIO_PASSWORD)\n [ -n \"${fs_pass}\" ] && env_args+=(-e AGENTTEAMS_FS_SECRET_KEY=\"${fs_pass}\")\n\n for k in AGENTTEAMS_LLM_PROVIDER AGENTTEAMS_LLM_API_KEY AGENTTEAMS_OPENAI_BASE_URL AGENTTEAMS_DEFAULT_MODEL; do\n local v; v=$(_env \"${k}\")\n [ -n \"${v}\" ] && env_args+=(-e \"${k}=${v}\")\n done\n\n # The controller writes the CLI SA token to /var/run/agentteams/cli-token.\n # The token is generated asynchronously during the first reconcile loop,\n # so it may not be available immediately after the container starts.\n # We poll for up to 30s to handle both fresh installs (token not yet minted)\n # and older images that don't support cli-token at all.\n # For backward compatibility, also try the legacy /var/run/hiclaw/cli-token\n # path used by older HiClaw images.\n local auth_token=\"${AGENTTEAMS_AUTH_TOKEN:-}\"\n if [ -z \"${auth_token}\" ]; then\n local _token_wait=0\n local _token_max_wait=30\n while [ ${_token_wait} -lt ${_token_max_wait} ]; do\n auth_token=$(${DOCKER_CMD} exec \"${CTRL_CONTAINER}\" sh -c 'cat /var/run/agentteams/cli-token 2>/dev/null || cat /var/run/hiclaw/cli-token 2>/dev/null' | tr -d '\\n' || true)\n if [ -n \"${auth_token}\" ]; then\n break\n fi\n sleep 2\n _token_wait=$((_token_wait + 2))\n done\n fi\n if [ -n \"${auth_token}\" ]; then\n env_args+=(-e AGENTTEAMS_AUTH_TOKEN=\"${auth_token}\")\n else\n log \"WARNING: could not read controller SA token (timed out after ${_token_max_wait:-30}s)\"\n log \" This can happen with older AgentTeams/HiClaw images or if the controller is still starting.\"\n log \" Dashboard will still work but some API calls may fail until you log in via Higress Console.\"\n log \" To fix: upgrade AgentTeams to v1.2.0-beta.1+ or set AGENTTEAMS_AUTH_TOKEN manually.\"\n fi\n\n local admin_user; admin_user=$(_env AGENTTEAMS_ADMIN_USER)\n local admin_pass; admin_pass=$(_env AGENTTEAMS_ADMIN_PASSWORD)\n [ -n \"${admin_user}\" ] && env_args+=(-e AGENTTEAMS_ADMIN_USER=\"${admin_user}\")\n [ -n \"${admin_pass}\" ] && env_args+=(-e AGENTTEAMS_ADMIN_PASSWORD=\"${admin_pass}\")\n\n # Higress Console URL: explicit config takes priority, auto-detect as fallback.\n local _resolved_gw_url=\"\"\n if [ -n \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\" ]; then\n # Normalize URL: add http:// prefix if missing\n _resolved_gw_url=\"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\"\n case \"${_resolved_gw_url}\" in\n http://*|https://*) ;;\n *) _resolved_gw_url=\"http://${_resolved_gw_url}\" ;;\n esac\n env_args+=(-e AGENTTEAMS_AI_GATEWAY_ADMIN_URL=\"${_resolved_gw_url}\")\n elif ${DOCKER_CMD} exec \"${CTRL_CONTAINER}\" wget -q -O- --timeout=2 http://127.0.0.1:8001/ >/dev/null 2>&1; then\n _resolved_gw_url=\"http://${CTRL_CONTAINER}:8001\"\n env_args+=(-e AGENTTEAMS_AI_GATEWAY_ADMIN_URL=\"${_resolved_gw_url}\")\n fi\n\n # Verify Higress Console URL reachability (best-effort warning)\n if [ -n \"${_resolved_gw_url}\" ]; then\n if ! ${DOCKER_CMD} exec \"${CTRL_CONTAINER}\" curl -sf --max-time 3 \"${_resolved_gw_url}/\" >/dev/null 2>&1; then\n log \"WARNING: Higress Console URL may not be reachable: ${_resolved_gw_url}\"\n log \" Dashboard will still work, but shared login via Higress Console may fail.\"\n log \" To fix: verify the URL is correct and the Higress Console service is running.\"\n fi\n fi\n else\n log \"WARNING: ${CTRL_CONTAINER} not running; launching Dashboard without Controller/MinIO/LLM env.\"\n if [ -n \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\" ]; then\n local _gw_norm=\"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\"\n case \"${_gw_norm}\" in\n http://*|https://*) ;;\n *) _gw_norm=\"http://${_gw_norm}\" ;;\n esac\n env_args+=(-e AGENTTEAMS_AI_GATEWAY_ADMIN_URL=\"${_gw_norm}\")\n fi\n fi\n\n # Create persistent data volume (for future use)\n ${DOCKER_CMD} volume create agentteams-dashboard-data >/dev/null 2>&1 || true\n\n ${DOCKER_CMD} run -d \\\n --name \"${DASHBOARD_CONTAINER}\" \\\n --restart unless-stopped \\\n --network \"${AGENTTEAMS_NETWORK:-agentteams-net}\" \\\n --network-alias dashboard.agentteams.io \\\n -p \"${BIND_ADDR}:${AGENTTEAMS_PORT_DASHBOARD}:3000\" \\\n \"${env_args[@]}\" \\\n -v agentteams-dashboard-data:/app/db \\\n \"${AGENTTEAMS_DASHBOARD_IMAGE}\"\n\n # Wait for dashboard to be ready\n local max_wait=60\n local waited=0\n while [ $waited -lt $max_wait ]; do\n if curl -sf \"http://127.0.0.1:${AGENTTEAMS_PORT_DASHBOARD}/\" >/dev/null 2>&1; then\n log \"Dashboard ready on port ${AGENTTEAMS_PORT_DASHBOARD}\"\n return 0\n fi\n sleep 2\n waited=$((waited + 2))\n done\n log \"WARNING: Dashboard startup timed out after ${max_wait}s\"\n ${DOCKER_CMD} logs \"${DASHBOARD_CONTAINER}\" 2>&1 | tail -20\n return 0\n}\n\n# ============================================================\n# Manager Installation (Interactive)\n# ============================================================\n\ninstall_manager() {\n log \"$(msg install.title)\"\n log \"$(msg install.registry \"${AGENTTEAMS_REGISTRY}\")\"\n log \"\"\n log \"$(msg install.dir \"$(pwd)\")\"\n log \"$(msg install.dir_hint)\"\n log \"$(msg install.dir_hint2)\"\n log \"\"\n\n # Non-interactive fallback: resolve version immediately so image tags are available\n # before the step state machine runs. Interactive mode lets step_version handle it.\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_VERSION=\"${AGENTTEAMS_VERSION:-${AGENTTEAMS_KNOWN_STABLE_VERSION}}\"\n resolve_image_tags\n fi\n\n # Migrate legacy env file location before checks\n local existing_env=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n if [ ! -f \"${existing_env}\" ] && [ -f \"./agentteams-manager.env\" ]; then\n log \"Migrating agentteams-manager.env from current directory to ${existing_env}...\"\n mv \"./agentteams-manager.env\" \"${existing_env}\"\n fi\n\n # Orphan volume detection (only when no env file — step_existing handles the env-file case)\n if [ ! -f \"${existing_env}\" ]; then\n local data_vol=\"${AGENTTEAMS_DATA_DIR:-agentteams-data}\"\n if ${DOCKER_CMD} volume ls -q | grep -q \"^${data_vol}$\"; then\n echo \"\"\n log \"$(msg install.orphan_volume.detected \"${data_vol}\")\"\n log \"$(msg install.orphan_volume.warn)\"\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n log \"$(msg install.orphan_volume.clean_noninteractive)\"\n ${DOCKER_CMD} stop agentteams-manager 2>/dev/null || true\n ${DOCKER_CMD} rm agentteams-manager 2>/dev/null || true\n for w in $(${DOCKER_CMD} ps -a --format '{{.Names}}' | grep \"^agentteams-worker-\" || true); do\n ${DOCKER_CMD} stop \"${w}\" 2>/dev/null || true\n ${DOCKER_CMD} rm \"${w}\" 2>/dev/null || true\n done\n log \"$(msg install.orphan_volume.cleaning)\"\n ${DOCKER_CMD} volume rm \"${data_vol}\" 2>/dev/null || true\n log \"$(msg install.orphan_volume.cleaned)\"\n else\n echo \"\"\n echo \"$(msg install.orphan_volume.choose)\"\n echo \"$(msg install.orphan_volume.clean)\"\n echo \"$(msg install.orphan_volume.keep)\"\n echo \"\"\n local ORPHAN_CHOICE\n read -e -p \"$(msg install.orphan_volume.prompt): \" ORPHAN_CHOICE\n ORPHAN_CHOICE=\"${ORPHAN_CHOICE:-1}\"\n case \"${ORPHAN_CHOICE}\" in\n 1|clean)\n ${DOCKER_CMD} stop agentteams-manager 2>/dev/null || true\n ${DOCKER_CMD} rm agentteams-manager 2>/dev/null || true\n for w in $(${DOCKER_CMD} ps -a --format '{{.Names}}' | grep \"^agentteams-worker-\" || true); do\n ${DOCKER_CMD} stop \"${w}\" 2>/dev/null || true\n ${DOCKER_CMD} rm \"${w}\" 2>/dev/null || true\n done\n log \"$(msg install.orphan_volume.cleaning)\"\n ${DOCKER_CMD} volume rm \"${data_vol}\" 2>/dev/null || true\n log \"$(msg install.orphan_volume.cleaned)\"\n ;;\n 2|keep)\n log \"$(msg install.orphan_volume.keeping)\"\n ;;\n esac\n fi\n fi\n fi\n\n # ── State machine ─────────────────────────────────────────────────────────\n local _STEPS=( step_lang step_mode step_version step_existing step_llm step_manager_runtime step_runtime step_admin step_network\n step_ports step_domains step_github step_skills step_volume\n step_workspace step_dashboard step_e2ee step_docker_proxy step_idle step_hostshare step_podman_autostart )\n local _STEP_HISTORY=()\n local _step_idx=0\n while [ \"${_step_idx}\" -lt \"${#_STEPS[@]}\" ]; do\n local _step_fn=\"${_STEPS[$_step_idx]}\"\n if should_skip_step \"${_step_fn}\"; then\n _step_idx=$((_step_idx + 1))\n continue\n fi\n if [ \"${#_STEP_HISTORY[@]}\" -gt 0 ]; then\n log \"$(msg nav.back_hint)\"\n fi\n STEP_RESULT=\"\"\n \"${_step_fn}\"\n if [ \"${STEP_RESULT}\" = \"back\" ]; then\n if [ \"${#_STEP_HISTORY[@]}\" -gt 0 ]; then\n local _last=$(( ${#_STEP_HISTORY[@]} - 1 ))\n _step_idx=\"${_STEP_HISTORY[$_last]}\"\n _STEP_HISTORY=(\"${_STEP_HISTORY[@]:0:${_last}}\")\n clear_step_vars \"${_STEPS[$_step_idx]}\"\n fi\n # else: first step, ignore 'b'\n else\n _STEP_HISTORY+=(\"${_step_idx}\")\n _step_idx=$((_step_idx + 1))\n fi\n done\n # ── End state machine ──────────────────────────────────────────────────────\n\n # Post-machine defaults for any steps that were skipped\n AGENTTEAMS_DATA_DIR=\"${AGENTTEAMS_DATA_DIR:-agentteams-data}\"\n if [ -z \"${AGENTTEAMS_WORKSPACE_DIR+x}\" ] || [ -z \"${AGENTTEAMS_WORKSPACE_DIR}\" ]; then\n AGENTTEAMS_WORKSPACE_DIR=\"${HOME}/agentteams-manager\"\n export AGENTTEAMS_WORKSPACE_DIR\n fi\n AGENTTEAMS_WORKSPACE_DIR=\"$(cd \"${AGENTTEAMS_WORKSPACE_DIR}\" 2>/dev/null && pwd || echo \"${AGENTTEAMS_WORKSPACE_DIR}\")\"\n mkdir -p \"${AGENTTEAMS_WORKSPACE_DIR}\"\n AGENTTEAMS_MANAGER_RUNTIME=\"${AGENTTEAMS_MANAGER_RUNTIME:-copaw}\"\n export AGENTTEAMS_MANAGER_RUNTIME\n AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"${AGENTTEAMS_DEFAULT_WORKER_RUNTIME:-copaw}\"\n AGENTTEAMS_MATRIX_E2EE=\"${AGENTTEAMS_MATRIX_E2EE:-0}\"\n export AGENTTEAMS_MATRIX_E2EE\n AGENTTEAMS_WORKER_IDLE_TIMEOUT=\"${AGENTTEAMS_WORKER_IDLE_TIMEOUT:-720}\"\n export AGENTTEAMS_WORKER_IDLE_TIMEOUT\n AGENTTEAMS_HOST_SHARE_DIR=\"${AGENTTEAMS_HOST_SHARE_DIR:-$HOME}\"\n export AGENTTEAMS_HOST_SHARE_DIR\n\n log \"\"\n\n # Generate secrets (only if not already set)\n log \"$(msg install.generating_secrets)\"\n AGENTTEAMS_MANAGER_PASSWORD=\"${AGENTTEAMS_MANAGER_PASSWORD:-$(generate_key)}\"\n AGENTTEAMS_REGISTRATION_TOKEN=\"${AGENTTEAMS_REGISTRATION_TOKEN:-$(generate_key)}\"\n AGENTTEAMS_MINIO_USER=\"${AGENTTEAMS_MINIO_USER:-${AGENTTEAMS_ADMIN_USER}}\"\n AGENTTEAMS_MINIO_PASSWORD=\"${AGENTTEAMS_MINIO_PASSWORD:-${AGENTTEAMS_ADMIN_PASSWORD}}\"\n AGENTTEAMS_MANAGER_GATEWAY_KEY=\"${AGENTTEAMS_MANAGER_GATEWAY_KEY:-$(generate_key)}\"\n\n # Matrix AppService tokens — generate once during install/upgrade if not provided.\n # Persisted to env file so they survive controller restarts.\n AGENTTEAMS_MATRIX_APPSERVICE_ENABLED=\"${AGENTTEAMS_MATRIX_APPSERVICE_ENABLED:-true}\"\n export AGENTTEAMS_MATRIX_APPSERVICE_ENABLED\n if [ \"${AGENTTEAMS_MATRIX_APPSERVICE_ENABLED}\" != \"false\" ] && [ \"${AGENTTEAMS_MATRIX_APPSERVICE_ENABLED}\" != \"0\" ]; then\n if [ -z \"${AGENTTEAMS_MATRIX_APPSERVICE_AS_TOKEN}\" ]; then\n AGENTTEAMS_MATRIX_APPSERVICE_AS_TOKEN=\"$(openssl rand -hex 32)\"\n log \" Auto-generated AppService as_token (saved to env file)\"\n fi\n if [ -z \"${AGENTTEAMS_MATRIX_APPSERVICE_HS_TOKEN}\" ]; then\n AGENTTEAMS_MATRIX_APPSERVICE_HS_TOKEN=\"$(openssl rand -hex 32)\"\n log \" Auto-generated AppService hs_token (saved to env file)\"\n fi\n export AGENTTEAMS_MATRIX_APPSERVICE_AS_TOKEN\n export AGENTTEAMS_MATRIX_APPSERVICE_HS_TOKEN\n fi\n\n # Detect Apple Silicon (M1/M2/M3/M4) - need JVM fix for Higress Console\n # See: https://github.com/agentscope-ai/AgentTeams/issues/249\n if [ -z \"${JVM_ARGS:-}\" ] && [ \"$(uname -m)\" = \"arm64\" ] && [ \"$(uname -s)\" = \"Darwin\" ]; then\n log \"Apple Silicon detected - setting JVM_ARGS to fix Higress Console SIGILL issue\"\n JVM_ARGS=\"-XX:+UnlockDiagnosticVMOptions -XX:-UseAESCTRIntrinsics -XX:UseSVE=0\"\n fi\n\n # Write .env file\n ENV_FILE=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n cat > \"${ENV_FILE}\" << EOF\n# AgentTeams Manager Configuration\n# Generated by agentteams-install.sh on $(date)\n\n# Language\nAGENTTEAMS_LANGUAGE=${AGENTTEAMS_LANGUAGE}\n\n# LLM\nAGENTTEAMS_LLM_PROVIDER=${AGENTTEAMS_LLM_PROVIDER}\nAGENTTEAMS_DEFAULT_MODEL=${AGENTTEAMS_DEFAULT_MODEL}\nAGENTTEAMS_LLM_API_KEY=${AGENTTEAMS_LLM_API_KEY}\nAGENTTEAMS_OPENAI_BASE_URL=${AGENTTEAMS_OPENAI_BASE_URL:-}\nAGENTTEAMS_MODEL_CONTEXT_WINDOW=${AGENTTEAMS_MODEL_CONTEXT_WINDOW:-}\nAGENTTEAMS_MODEL_MAX_TOKENS=${AGENTTEAMS_MODEL_MAX_TOKENS:-}\nAGENTTEAMS_MODEL_REASONING=${AGENTTEAMS_MODEL_REASONING:-}\nAGENTTEAMS_MODEL_VISION=${AGENTTEAMS_MODEL_VISION:-}\n\n# Embedding model (empty = disabled, default: text-embedding-v4)\nAGENTTEAMS_EMBEDDING_MODEL=${AGENTTEAMS_EMBEDDING_MODEL}\n\n# Admin\nAGENTTEAMS_ADMIN_USER=${AGENTTEAMS_ADMIN_USER}\nAGENTTEAMS_ADMIN_PASSWORD=${AGENTTEAMS_ADMIN_PASSWORD}\n\n# Ports\nAGENTTEAMS_LOCAL_ONLY=${AGENTTEAMS_LOCAL_ONLY}\nAGENTTEAMS_PORT_GATEWAY=${AGENTTEAMS_PORT_GATEWAY}\nAGENTTEAMS_PORT_CONSOLE=${AGENTTEAMS_PORT_CONSOLE}\nAGENTTEAMS_PORT_ELEMENT_WEB=${AGENTTEAMS_PORT_ELEMENT_WEB}\nAGENTTEAMS_PORT_MANAGER_CONSOLE=${AGENTTEAMS_PORT_MANAGER_CONSOLE:-18888}\n\n# Manager runtime (openclaw | copaw)\nAGENTTEAMS_MANAGER_RUNTIME=${AGENTTEAMS_MANAGER_RUNTIME:-copaw}\n\n# Matrix\nAGENTTEAMS_MATRIX_DOMAIN=${AGENTTEAMS_MATRIX_DOMAIN}\nAGENTTEAMS_MATRIX_CLIENT_DOMAIN=${AGENTTEAMS_MATRIX_CLIENT_DOMAIN}\n\n# Gateway\nAGENTTEAMS_AI_GATEWAY_DOMAIN=${AGENTTEAMS_AI_GATEWAY_DOMAIN}\nAGENTTEAMS_MANAGER_GATEWAY_KEY=${AGENTTEAMS_MANAGER_GATEWAY_KEY}\n\n# File System\nAGENTTEAMS_FS_DOMAIN=${AGENTTEAMS_FS_DOMAIN}\nAGENTTEAMS_CONSOLE_DOMAIN=${AGENTTEAMS_CONSOLE_DOMAIN}\nAGENTTEAMS_MINIO_USER=${AGENTTEAMS_MINIO_USER}\nAGENTTEAMS_MINIO_PASSWORD=${AGENTTEAMS_MINIO_PASSWORD}\n\n# Internal\nAGENTTEAMS_MANAGER_PASSWORD=${AGENTTEAMS_MANAGER_PASSWORD}\nAGENTTEAMS_REGISTRATION_TOKEN=${AGENTTEAMS_REGISTRATION_TOKEN}\n\n# GitHub (optional)\nAGENTTEAMS_GITHUB_TOKEN=${AGENTTEAMS_GITHUB_TOKEN:-}\n\n# Nacos package import defaults\nAGENTTEAMS_NACOS_REGISTRY_URI=${AGENTTEAMS_NACOS_REGISTRY_URI:-nacos://market.agentteams.io:80/public}\nAGENTTEAMS_NACOS_USERNAME=${AGENTTEAMS_NACOS_USERNAME:-}\nAGENTTEAMS_NACOS_PASSWORD=${AGENTTEAMS_NACOS_PASSWORD:-}\nAGENTTEAMS_NACOS_TOKEN=${AGENTTEAMS_NACOS_TOKEN:-}\n\n# Skills Registry (optional, default: nacos://market.agentteams.io:80/public)\nAGENTTEAMS_SKILLS_API_URL=${AGENTTEAMS_SKILLS_API_URL:-nacos://market.agentteams.io:80/public}\n\n# OpenClaw CMS plugin (optional)\nAGENTTEAMS_CMS_TRACES_ENABLED=${AGENTTEAMS_CMS_TRACES_ENABLED:-false}\nAGENTTEAMS_CMS_ENDPOINT=${AGENTTEAMS_CMS_ENDPOINT:-}\nAGENTTEAMS_CMS_LICENSE_KEY=${AGENTTEAMS_CMS_LICENSE_KEY:-}\nAGENTTEAMS_CMS_PROJECT=${AGENTTEAMS_CMS_PROJECT:-}\nAGENTTEAMS_CMS_WORKSPACE=${AGENTTEAMS_CMS_WORKSPACE:-}\nAGENTTEAMS_CMS_SERVICE_NAME=${AGENTTEAMS_CMS_SERVICE_NAME:-agentteams-manager}\nAGENTTEAMS_CMS_METRICS_ENABLED=${AGENTTEAMS_CMS_METRICS_ENABLED:-false}\n\n# Worker images (for direct container creation)\nAGENTTEAMS_WORKER_IMAGE=${WORKER_IMAGE}\nAGENTTEAMS_COPAW_WORKER_IMAGE=${COPAW_WORKER_IMAGE}\nAGENTTEAMS_QWENPAW_WORKER_IMAGE=${QWENPAW_WORKER_IMAGE}\nAGENTTEAMS_HERMES_WORKER_IMAGE=${HERMES_WORKER_IMAGE}\n\n# Default Worker runtime (openclaw | copaw | qwenpaw | hermes)\nAGENTTEAMS_DEFAULT_WORKER_RUNTIME=${AGENTTEAMS_DEFAULT_WORKER_RUNTIME:-copaw}\n\n# Matrix E2EE (0=disabled, 1=enabled; default: 0)\nAGENTTEAMS_MATRIX_E2EE=${AGENTTEAMS_MATRIX_E2EE:-0}\n\n# Matrix AppService\nAGENTTEAMS_MATRIX_APPSERVICE_ENABLED=${AGENTTEAMS_MATRIX_APPSERVICE_ENABLED:-true}\nAGENTTEAMS_MATRIX_APPSERVICE_AS_TOKEN=${AGENTTEAMS_MATRIX_APPSERVICE_AS_TOKEN:-}\nAGENTTEAMS_MATRIX_APPSERVICE_HS_TOKEN=${AGENTTEAMS_MATRIX_APPSERVICE_HS_TOKEN:-}\n\n# Docker API proxy (0=disabled, 1=enabled; default: 1)\nAGENTTEAMS_DOCKER_PROXY=${AGENTTEAMS_DOCKER_PROXY:-1}\n\n# Docker API proxy: additional allowed image sources (comma-separated)\nAGENTTEAMS_PROXY_ALLOWED_REGISTRIES=${AGENTTEAMS_PROXY_ALLOWED_REGISTRIES:-}\n\n# Worker idle timeout in minutes (default: 720 = 12 hours)\nAGENTTEAMS_WORKER_IDLE_TIMEOUT=${AGENTTEAMS_WORKER_IDLE_TIMEOUT:-720}\n\n# Podman autostart via systemd (0=disabled, 1=enabled)\nAGENTTEAMS_PODMAN_AUTOSTART=${AGENTTEAMS_PODMAN_AUTOSTART:-0}\n\n# JVM Args for Higress Console (fixes SIGILL on Apple Silicon)\nJVM_ARGS=${JVM_ARGS:-}\n\n# Higress WASM plugin image registry (auto-selected by timezone)\nHIGRESS_ADMIN_WASM_PLUGIN_IMAGE_REGISTRY=${AGENTTEAMS_REGISTRY}\n\n# Data persistence\nAGENTTEAMS_DATA_DIR=${AGENTTEAMS_DATA_DIR:-agentteams-data}\n# Manager workspace (skills, memory, state — host-editable)\nAGENTTEAMS_WORKSPACE_DIR=${AGENTTEAMS_WORKSPACE_DIR:-}\n# Host directory sharing\nAGENTTEAMS_HOST_SHARE_DIR=${AGENTTEAMS_HOST_SHARE_DIR:-}\n\n# agentteams-dashboard (management UI)\nAGENTTEAMS_DASHBOARD=${AGENTTEAMS_DASHBOARD:-1}\nAGENTTEAMS_DASHBOARD_VERSION=${AGENTTEAMS_DASHBOARD_VERSION:-v1.2.0-beta.1}\nAGENTTEAMS_PORT_DASHBOARD=${AGENTTEAMS_PORT_DASHBOARD:-13000}\nAGENTTEAMS_DASHBOARD_IMAGE=${AGENTTEAMS_DASHBOARD_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-dashboard:${AGENTTEAMS_DASHBOARD_VERSION}}\nAGENTTEAMS_AI_GATEWAY_ADMIN_URL=${AGENTTEAMS_AI_GATEWAY_ADMIN_URL:-}\nEOF\n\n chmod 600 \"${ENV_FILE}\"\n log \"$(msg install.config_saved \"${ENV_FILE}\")\"\n\n # Detect container runtime socket\n SOCKET_MOUNT_ARGS=\"\"\n if [ \"${AGENTTEAMS_MOUNT_SOCKET}\" = \"1\" ]; then\n # Actively ensure the API socket is enabled and active before detection.\n if [ \"${DOCKER_CMD:-}\" = \"podman\" ]; then\n ensure_podman_socket\n fi\n CONTAINER_SOCK=$(detect_socket)\n if [ -n \"${CONTAINER_SOCK}\" ]; then\n log \"$(msg install.socket_detected \"${CONTAINER_SOCK}\")\"\n SOCKET_MOUNT_ARGS=\"-v ${CONTAINER_SOCK}:/var/run/docker.sock --security-opt label=disable\"\n else\n log \"$(msg install.socket_not_found)\"\n # Interactive confirmation when socket not found\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" != \"1\" ]; then\n echo \"\"\n echo -e \"\\033[33m$(msg install.socket_confirm.title)\\033[0m\"\n echo \"\"\n echo -e \"$(msg install.socket_confirm.message)\"\n echo \"\"\n read -p \"$(msg install.socket_confirm.prompt)\" SOCKET_CONFIRM\n if [ \"${SOCKET_CONFIRM}\" != \"y\" ] && [ \"${SOCKET_CONFIRM}\" != \"Y\" ]; then\n log \"$(msg install.socket_confirm.cancelled)\"\n exit 0\n fi\n fi\n fi\n fi\n\n # Create the data volume if it doesn't already exist (reuse on reinstall)\n if ! ${DOCKER_CMD} volume ls -q | grep -q \"^${AGENTTEAMS_DATA_DIR}$\"; then\n ${DOCKER_CMD} volume create \"${AGENTTEAMS_DATA_DIR}\" > /dev/null\n fi\n\n # Data mount: Docker volume\n DATA_MOUNT_ARGS=\"-v ${AGENTTEAMS_DATA_DIR}:/data\"\n\n # Manager workspace mount (always a host directory, defaulting to ~/agentteams-manager)\n WORKSPACE_MOUNT_ARGS=\"-v ${AGENTTEAMS_WORKSPACE_DIR}:/root/manager-workspace\"\n\n # Pass host timezone to container so date/time commands reflect local time\n TZ_ARGS=\"-e TZ=${AGENTTEAMS_TIMEZONE}\"\n\n # Host directory mount\n if [ -d \"${AGENTTEAMS_HOST_SHARE_DIR}\" ]; then\n HOST_SHARE_MOUNT_ARGS=\"-v ${AGENTTEAMS_HOST_SHARE_DIR}:/host-share\"\n log \"$(msg host_share.sharing \"${AGENTTEAMS_HOST_SHARE_DIR}\")\"\n else\n log \"$(msg host_share.not_exist \"${AGENTTEAMS_HOST_SHARE_DIR}\")\"\n HOST_SHARE_MOUNT_ARGS=\"-v ${AGENTTEAMS_HOST_SHARE_DIR}:/host-share\"\n fi\n\n # YOLO mode: pass through if set in environment (enables autonomous decisions)\n YOLO_ARGS=\"\"\n if [ \"${AGENTTEAMS_YOLO:-}\" = \"1\" ]; then\n YOLO_ARGS=\"-e AGENTTEAMS_YOLO=1\"\n log \"$(msg install.yolo)\"\n fi\n\n # Matrix-plugin debug tracing: pass through if AGENTTEAMS_MATRIX_DEBUG=1.\n # The container entrypoints translate this to OPENCLAW_MATRIX_DEBUG=1\n # so the openclaw matrix plugin emits structured INFO-level lifecycle\n # traces (sync state transitions, room.invite/join, message handler\n # arrival + filter outcomes). Used to diagnose worker/manager hangs.\n MATRIX_DEBUG_ARGS=\"\"\n if [ \"${AGENTTEAMS_MATRIX_DEBUG:-}\" = \"1\" ]; then\n MATRIX_DEBUG_ARGS=\"-e AGENTTEAMS_MATRIX_DEBUG=1\"\n fi\n\n # E2EE is already in the env file; but also pass explicitly in case env file is not the source\n # (AGENTTEAMS_MATRIX_E2EE is already written to ENV_FILE above via --env-file)\n\n # Pull images (manager based on runtime config; all worker runtimes always pulled)\n _is_local_image() {\n case \"$1\" in\n agentteams/*) return 0 ;;\n *) return 1 ;;\n esac\n }\n\n _local_image_exists() {\n local _img=\"$1\"\n [ -z \"${_img}\" ] && return 1\n _is_local_image \"${_img}\" || return 1\n ${DOCKER_CMD} image inspect \"${_img}\" >/dev/null 2>&1\n }\n\n # Helper: pull or skip a single image\n # Args: $1=image $2=exists_msg_key $3=pulling_msg_key\n _pull_image() {\n local _img=\"$1\" _exists_key=\"$2\" _pull_key=\"$3\"\n [ -z \"${_img}\" ] && return 0\n if _local_image_exists \"${_img}\"; then\n log \"$(msg \"${_exists_key}\" \"${_img}\")\"\n return 0\n fi\n log \"$(msg \"${_pull_key}\" \"${_img}\")\"\n local _attempt=1\n while [ $_attempt -le 3 ]; do\n if ${DOCKER_CMD} pull \"${_img}\"; then\n return 0\n fi\n if [ $_attempt -lt 3 ]; then\n log \"Pull failed (attempt ${_attempt}/3), retrying in 5s...\"\n sleep 5\n fi\n _attempt=$((_attempt + 1))\n done\n die \"Failed to pull ${_img} after 3 attempts\"\n return 1\n }\n\n # Embedded controller image (resolve versioned tag, fallback to latest)\n resolve_embedded_image\n if [ \"${AGENTTEAMS_USE_EMBEDDED}\" = \"1\" ]; then\n _local_image_exists \"${EMBEDDED_IMAGE}\" || true\n fi\n\n # Manager image is always required (select based on runtime)\n if [ \"${AGENTTEAMS_MANAGER_RUNTIME}\" = \"copaw\" ]; then\n _pull_image \"${MANAGER_COPAW_IMAGE}\" \"install.image.exists\" \"install.image.pulling_manager\"\n else\n _pull_image \"${MANAGER_IMAGE}\" \"install.image.exists\" \"install.image.pulling_manager\"\n fi\n\n # Pull all worker runtime images (workers may use any runtime regardless of the default)\n _pull_image \"${WORKER_IMAGE}\" \"install.image.worker_exists\" \"install.image.pulling_worker\"\n _pull_image \"${COPAW_WORKER_IMAGE}\" \"install.image.worker_exists\" \"install.image.pulling_worker\"\n _pull_image \"${QWENPAW_WORKER_IMAGE}\" \"install.image.worker_exists\" \"install.image.pulling_worker\"\n _pull_image \"${HERMES_WORKER_IMAGE}\" \"install.image.worker_exists\" \"install.image.pulling_worker\"\n\n # --- Pre-upgrade: extract Matrix passwords from running old containers ---\n # Only needed when upgrading FROM old architecture (v1.0.9) TO embedded.\n # For new-arch-to-new-arch upgrades, credential files already exist with\n # correct room IDs — we must NOT overwrite them.\n _creds_tmp=\"\"\n if [ \"${AGENTTEAMS_UPGRADE:-0}\" = \"1\" ] && [ \"${AGENTTEAMS_USE_EMBEDDED}\" = \"1\" ]; then\n # Detect if upgrading from old arch: old arch has no agentteams-controller container\n # (or has it as a docker-proxy only, not embedded). Check if the existing\n # agentteams-controller is an embedded image (has supervisord) or just a proxy.\n _is_old_arch=0\n if ! ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -q \"^agentteams-controller$\"; then\n _is_old_arch=1\n elif ${DOCKER_CMD} ps -a --format '{{.Names}} {{.Image}}' 2>/dev/null | grep \"^agentteams-controller \" | grep -qv \"embedded\"; then\n _is_old_arch=1\n fi\n\n if [ \"${_is_old_arch}\" = \"1\" ]; then\n _creds_tmp=$(mktemp -d)\n\n # docker exec for Matrix/minio paths only works while agentteams-manager is running.\n _mgr_creds_tempstart=0\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -q '^agentteams-manager$'; then\n if ! ${DOCKER_CMD} ps --format '{{.Names}}' 2>/dev/null | grep -q '^agentteams-manager$'; then\n log \"agentteams-manager is stopped; starting it temporarily to extract Matrix credentials for upgrade...\"\n ${DOCKER_CMD} start agentteams-manager 2>/dev/null || true\n wait_matrix_ready \"agentteams-manager\"\n _mgr_creds_tempstart=1\n fi\n fi\n\n # Manager password (container Config.Env, then secrets file inside running manager, then data volume)\n _mgr_pw=$(${DOCKER_CMD} inspect agentteams-manager --format '{{range .Config.Env}}{{println .}}{{end}}' 2>/dev/null | grep '^AGENTTEAMS_MANAGER_PASSWORD=' | cut -d= -f2-)\n if [ -z \"${_mgr_pw}\" ] && ${DOCKER_CMD} ps --format '{{.Names}}' 2>/dev/null | grep -q '^agentteams-manager$'; then\n _mgr_pw=$(${DOCKER_CMD} exec agentteams-manager bash -c 'source /data/agentteams-secrets.env 2>/dev/null && echo \"${AGENTTEAMS_MANAGER_PASSWORD}\"' 2>/dev/null)\n fi\n if [ -z \"${_mgr_pw}\" ] && ${DOCKER_CMD} volume ls -q 2>/dev/null | grep -q \"^${AGENTTEAMS_DATA_DIR}$\"; then\n _mgr_pw=$(agentteams_read_secret_from_data_volume \"${AGENTTEAMS_DATA_DIR}\" AGENTTEAMS_MANAGER_PASSWORD)\n fi\n\n # Manager admin DM room ID: login as admin, find DM room with @manager\n _mgr_room=\"\"\n if [ -n \"${_mgr_pw}\" ]; then\n _admin_pw=$(grep AGENTTEAMS_ADMIN_PASSWORD \"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\" 2>/dev/null | cut -d= -f2-)\n _admin_user=$(grep AGENTTEAMS_ADMIN_USER \"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\" 2>/dev/null | cut -d= -f2-)\n _admin_user=\"${_admin_user:-admin}\"\n _matrix_domain=$(grep AGENTTEAMS_MATRIX_DOMAIN \"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\" 2>/dev/null | cut -d= -f2-)\n if [ -n \"${_admin_pw}\" ] && ${DOCKER_CMD} ps --format '{{.Names}}' 2>/dev/null | grep -q '^agentteams-manager$'; then\n _admin_token=$(${DOCKER_CMD} exec agentteams-manager curl -sf -X POST http://127.0.0.1:6167/_matrix/client/v3/login \\\n -H \"Content-Type: application/json\" \\\n -d '{\"type\":\"m.login.password\",\"identifier\":{\"type\":\"m.id.user\",\"user\":\"'\"${_admin_user}\"'\"},\"password\":\"'\"${_admin_pw}\"'\"}' 2>/dev/null | python3 -c \"import sys,json; print(json.load(sys.stdin).get('access_token',''))\" 2>/dev/null || true)\n if [ -n \"${_admin_token}\" ]; then\n _mgr_room=$(${DOCKER_CMD} exec agentteams-manager curl -sf -X GET \\\n -H \"Authorization: Bearer ${_admin_token}\" \\\n \"http://127.0.0.1:6167/_matrix/client/v3/joined_rooms\" 2>/dev/null | python3 -c \"\nimport sys,json,subprocess\nrooms = json.load(sys.stdin).get('joined_rooms',[])\nfor room_id in rooms:\n enc = room_id.replace('!','%21')\n members = json.loads(subprocess.check_output([\n 'docker','exec','agentteams-manager','curl','-sf','-X','GET',\n '-H','Authorization: Bearer ${_admin_token}',\n 'http://127.0.0.1:6167/_matrix/client/v3/rooms/'+enc+'/members'\n ]).decode()).get('chunk',[])\n member_ids = [m['state_key'] for m in members]\n if any('manager' in m and 'admin' not in m.split(':')[0] for m in member_ids):\n if len(member_ids) <= 3:\n print(room_id)\n break\n\" 2>/dev/null || true)\n fi\n fi\n if [ -z \"${_mgr_room}\" ]; then\n _mgr_room=$(agentteams_read_admin_dm_room_from_workspace \"${AGENTTEAMS_WORKSPACE_DIR}\")\n fi\n cat > \"${_creds_tmp}/default.env\" <<CREDEOF\nWORKER_PASSWORD=\"${_mgr_pw}\"\nWORKER_MINIO_PASSWORD=\"$(openssl rand -hex 24)\"\nWORKER_GATEWAY_KEY=\"${AGENTTEAMS_MANAGER_GATEWAY_KEY}\"\nWORKER_ROOM_ID=\"${_mgr_room}\"\nCREDEOF\n log \"Extracted Manager Matrix password${_mgr_room:+ and room ID}\"\n fi\n\n # Worker passwords and room IDs from workers-registry.json\n if [ -f \"${AGENTTEAMS_WORKSPACE_DIR}/workers-registry.json\" ]; then\n _worker_names=$(python3 -c \"import json; d=json.load(open('${AGENTTEAMS_WORKSPACE_DIR}/workers-registry.json')); print(' '.join(d.get('workers',{}).keys()))\" 2>/dev/null || true)\n for _wname in ${_worker_names}; do\n _wpw=\"\"\n if ${DOCKER_CMD} ps --format '{{.Names}}' 2>/dev/null | grep -q '^agentteams-manager$'; then\n _wpw=$(${DOCKER_CMD} exec agentteams-manager cat \"/root/agentteams-fs/agents/${_wname}/credentials/matrix/password\" 2>/dev/null || true)\n fi\n if [ -z \"${_wpw}\" ] && ${DOCKER_CMD} volume ls -q 2>/dev/null | grep -q \"^${AGENTTEAMS_DATA_DIR}$\"; then\n _wpw=$(agentteams_read_worker_creds_value_from_volume \"${AGENTTEAMS_DATA_DIR}\" \"${_wname}\" WORKER_PASSWORD)\n fi\n _wroom=$(python3 -c \"import json; d=json.load(open('${AGENTTEAMS_WORKSPACE_DIR}/workers-registry.json')); print(d.get('workers',{}).get('${_wname}',{}).get('room_id',''))\" 2>/dev/null || true)\n if [ -z \"${_wroom}\" ] && ${DOCKER_CMD} volume ls -q 2>/dev/null | grep -q \"^${AGENTTEAMS_DATA_DIR}$\"; then\n _wroom=$(agentteams_read_worker_creds_value_from_volume \"${AGENTTEAMS_DATA_DIR}\" \"${_wname}\" WORKER_ROOM_ID)\n fi\n if [ -n \"${_wpw}\" ]; then\n cat > \"${_creds_tmp}/${_wname}.env\" <<CREDEOF\nWORKER_PASSWORD=\"${_wpw}\"\nWORKER_MINIO_PASSWORD=\"$(openssl rand -hex 24)\"\nWORKER_GATEWAY_KEY=\"$(openssl rand -hex 32)\"\nWORKER_ROOM_ID=\"${_wroom}\"\nCREDEOF\n log \"Extracted ${_wname} Matrix password${_wroom:+ and room ID}\"\n fi\n done\n fi\n\n if [ \"${_mgr_creds_tempstart}\" = \"1\" ]; then\n log \"Stopping agentteams-manager after credential extraction (upgrade will recreate containers)...\"\n ${DOCKER_CMD} stop agentteams-manager 2>/dev/null || true\n fi\n fi # _is_old_arch\n fi\n\n # --- Stop and remove existing containers ---\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' | grep -q \"^agentteams-controller$\"; then\n ${DOCKER_CMD} stop agentteams-controller 2>/dev/null || true\n ${DOCKER_CMD} rm agentteams-controller 2>/dev/null || true\n fi\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' | grep -q \"^agentteams-manager$\"; then\n log \"$(msg install.removing_existing)\"\n ${DOCKER_CMD} stop agentteams-manager 2>/dev/null || true\n ${DOCKER_CMD} rm agentteams-manager 2>/dev/null || true\n fi\n\n # Stop and remove worker containers (controller will recreate via CR reconciliation)\n if [ -n \"${UPGRADE_EXISTING_WORKERS:-}\" ]; then\n log \"$(msg install.existing.stopping_workers)\"\n for w in ${UPGRADE_EXISTING_WORKERS}; do\n ${DOCKER_CMD} stop \"${w}\" 2>/dev/null || true\n ${DOCKER_CMD} rm \"${w}\" 2>/dev/null || true\n log \"$(msg install.existing.removed \"${w}\")\"\n done\n fi\n\n # Clean up legacy containers from v1.0.x and earlier\n for _legacy_name in agentteams-docker-proxy; do\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -qx \"${_legacy_name}\"; then\n log \"Removing legacy container: ${_legacy_name}\"\n ${DOCKER_CMD} stop \"${_legacy_name}\" 2>/dev/null || true\n ${DOCKER_CMD} rm -f \"${_legacy_name}\" 2>/dev/null || true\n fi\n done\n\n # --- Upgrade: inject extracted credentials into data volume ---\n # Only needed for old-arch upgrades (credential files were extracted above).\n if [ -n \"${_creds_tmp}\" ] && [ -d \"${_creds_tmp}\" ] && [ -n \"$(ls -A \"${_creds_tmp}\" 2>/dev/null)\" ]; then\n local _cleanup_ctr=\"agentteams-upgrade-cleanup\"\n ${DOCKER_CMD} rm -f \"${_cleanup_ctr}\" 2>/dev/null || true\n ${DOCKER_CMD} run --rm --name \"${_cleanup_ctr}\" \\\n --entrypoint sh \\\n -v \"${AGENTTEAMS_DATA_DIR}:/data\" \\\n -v \"${_creds_tmp}:/creds:ro\" \\\n \"${EMBEDDED_IMAGE}\" -c '\n rm -rf /data/worker-creds\n mkdir -p /data/worker-creds\n cp /creds/*.env /data/worker-creds/ 2>/dev/null || true\n chmod 600 /data/worker-creds/*.env 2>/dev/null || true\n ' 2>/dev/null && log \"Injected credentials for upgrade\" || log \"Warning: credential injection failed, continuing\"\n rm -rf \"${_creds_tmp}\"\n fi\n\n # --- Start containers ---\n log \"$(msg install.starting_manager)\"\n\n # Build port binding args\n if [ \"${AGENTTEAMS_LOCAL_ONLY:-1}\" = \"1\" ]; then\n _port_prefix=\"127.0.0.1:\"\n else\n _port_prefix=\"\"\n fi\n\n # Ensure agentteams-net Docker network exists\n ${DOCKER_CMD} network inspect agentteams-net >/dev/null 2>&1 || ${DOCKER_CMD} network create agentteams-net\n\n if [ \"${AGENTTEAMS_USE_EMBEDDED}\" != \"1\" ] && [ \"${AGENTTEAMS_UPGRADE:-0}\" = \"1\" ]; then\n # Check if current installation is embedded — downgrade to legacy is not supported\n if ${DOCKER_CMD} ps -a --format '{{.Names}} {{.Image}}' 2>/dev/null | grep \"^agentteams-controller \" | grep -q \"embedded\"; then\n error \"Downgrade from embedded architecture to legacy version (${AGENTTEAMS_VERSION}) is not supported.\"\n error \"Please use 'make uninstall-embedded' first, then do a clean install of the target version.\"\n exit 1\n fi\n fi\n\n if [ \"${AGENTTEAMS_USE_EMBEDDED}\" = \"1\" ]; then\n # ============================================================\n # New architecture: embedded controller + auto-created manager\n # ============================================================\n\n # Internal port: 8080 (Higress gateway inside the container).\n local _internal_gw_port=8080\n local _matrix_domain=\"${AGENTTEAMS_MATRIX_DOMAIN:-matrix-local.agentteams.io:${AGENTTEAMS_PORT_GATEWAY}}\"\n local _aigw_domain=\"${AGENTTEAMS_AI_GATEWAY_DOMAIN:-aigw-local.agentteams.io}\"\n # Ensure internal gateway port is present (container-internal traffic uses 8080)\n case \"${_aigw_domain}\" in *:*) ;; *) _aigw_domain=\"${_aigw_domain}:${_internal_gw_port}\" ;; esac\n local _fs_domain=\"${AGENTTEAMS_FS_DOMAIN:-fs-local.agentteams.io}\"\n case \"${_fs_domain}\" in *:*) ;; *) _fs_domain=\"${_fs_domain}:${_internal_gw_port}\" ;; esac\n\n # Controller env args\n local _ctrl_env_prefix\n _ctrl_env_prefix=\"$(_controller_env_prefix \"${AGENTTEAMS_VERSION}\")\"\n local _storage_prefix\n _storage_prefix=\"$(_controller_storage_prefix \"${AGENTTEAMS_VERSION}\")\"\n local _ctrl_env_args=(\n -e \"${_ctrl_env_prefix}ADMIN_USER=${AGENTTEAMS_ADMIN_USER}\"\n -e \"${_ctrl_env_prefix}ADMIN_PASSWORD=${AGENTTEAMS_ADMIN_PASSWORD}\"\n -e \"${_ctrl_env_prefix}MANAGER_PASSWORD=${AGENTTEAMS_MANAGER_PASSWORD}\"\n -e \"${_ctrl_env_prefix}REGISTRATION_TOKEN=${AGENTTEAMS_REGISTRATION_TOKEN}\"\n -e \"${_ctrl_env_prefix}MINIO_USER=${AGENTTEAMS_MINIO_USER}\"\n -e \"${_ctrl_env_prefix}MINIO_PASSWORD=${AGENTTEAMS_MINIO_PASSWORD}\"\n -e \"${_ctrl_env_prefix}LLM_PROVIDER=${AGENTTEAMS_LLM_PROVIDER}\"\n -e \"${_ctrl_env_prefix}LLM_API_KEY=${AGENTTEAMS_LLM_API_KEY}\"\n -e \"${_ctrl_env_prefix}DEFAULT_MODEL=${AGENTTEAMS_DEFAULT_MODEL}\"\n -e \"${_ctrl_env_prefix}MANAGER_GATEWAY_KEY=${AGENTTEAMS_MANAGER_GATEWAY_KEY}\"\n -e \"${_ctrl_env_prefix}MANAGER_RUNTIME=${AGENTTEAMS_MANAGER_RUNTIME:-copaw}\"\n -e \"${_ctrl_env_prefix}MANAGER_IMAGE=$([ \"${AGENTTEAMS_MANAGER_RUNTIME}\" = \"copaw\" ] && echo \"${MANAGER_COPAW_IMAGE}\" || echo \"${MANAGER_IMAGE}\")\"\n -e \"${_ctrl_env_prefix}DEFAULT_WORKER_RUNTIME=${AGENTTEAMS_DEFAULT_WORKER_RUNTIME:-copaw}\"\n -e \"${_ctrl_env_prefix}WORKER_IMAGE=${WORKER_IMAGE}\"\n -e \"${_ctrl_env_prefix}COPAW_WORKER_IMAGE=${COPAW_WORKER_IMAGE}\"\n -e \"${_ctrl_env_prefix}QWENPAW_WORKER_IMAGE=${QWENPAW_WORKER_IMAGE}\"\n -e \"${_ctrl_env_prefix}HERMES_WORKER_IMAGE=${HERMES_WORKER_IMAGE}\"\n -e \"${_ctrl_env_prefix}MATRIX_DOMAIN=${_matrix_domain}\"\n -e \"${_ctrl_env_prefix}ELEMENT_HOMESERVER_URL=http://127.0.0.1:${AGENTTEAMS_PORT_GATEWAY}\"\n -e \"${_ctrl_env_prefix}MATRIX_URL=http://127.0.0.1:6167\"\n -e \"${_ctrl_env_prefix}MATRIX_E2EE=${AGENTTEAMS_MATRIX_E2EE:-0}\"\n -e \"${_ctrl_env_prefix}MINIO_ENDPOINT=http://127.0.0.1:9000\"\n -e \"${_ctrl_env_prefix}MINIO_BUCKET=agentteams-storage\"\n -e \"${_ctrl_env_prefix}STORAGE_PREFIX=${_storage_prefix}\"\n -e \"${_ctrl_env_prefix}FS_ENDPOINT=http://127.0.0.1:9000\"\n -e \"${_ctrl_env_prefix}AI_GATEWAY_URL=http://${_aigw_domain}\"\n -e \"${_ctrl_env_prefix}CONTROLLER_URL=http://agentteams-controller:8090\"\n -e \"${_ctrl_env_prefix}DOCKER_NETWORK=agentteams-net\"\n -e \"${_ctrl_env_prefix}WORKSPACE_DIR=${AGENTTEAMS_WORKSPACE_DIR}\"\n -e \"${_ctrl_env_prefix}HOST_SHARE_DIR=${AGENTTEAMS_HOST_SHARE_DIR}\"\n -e \"${_ctrl_env_prefix}MANAGER_ENABLED=true\"\n -e \"${_ctrl_env_prefix}PORT_MANAGER_CONSOLE=${AGENTTEAMS_PORT_MANAGER_CONSOLE:-18888}\"\n )\n if _use_legacy_image_env \"${AGENTTEAMS_VERSION}\"; then\n _ctrl_env_args+=(\n -e \"${_ctrl_env_prefix}FS_BUCKET=agentteams-storage\"\n -e \"${_ctrl_env_prefix}RESOURCE_PREFIX=agentteams-\"\n )\n else\n _ctrl_env_args+=(\n -e \"${_ctrl_env_prefix}MATRIX_APPSERVICE_ENABLED=${AGENTTEAMS_MATRIX_APPSERVICE_ENABLED:-true}\"\n -e \"${_ctrl_env_prefix}MATRIX_APPSERVICE_AS_TOKEN=${AGENTTEAMS_MATRIX_APPSERVICE_AS_TOKEN:-}\"\n -e \"${_ctrl_env_prefix}MATRIX_APPSERVICE_HS_TOKEN=${AGENTTEAMS_MATRIX_APPSERVICE_HS_TOKEN:-}\"\n )\n fi\n\n # Timezone\n if [ -n \"${AGENTTEAMS_TIMEZONE:-}\" ]; then\n _ctrl_env_args+=(-e \"TZ=${AGENTTEAMS_TIMEZONE}\")\n fi\n\n # Yolo mode\n if [ \"${AGENTTEAMS_YOLO:-}\" = \"1\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}YOLO=1\")\n fi\n\n # Matrix-plugin debug tracing — propagated to every manager + worker\n # the controller spawns, then translated to OPENCLAW_MATRIX_DEBUG=1\n # by the container entrypoints. Use this to diagnose\n # \"worker did not join\" / \"manager replied empty\" hangs.\n if [ \"${AGENTTEAMS_MATRIX_DEBUG:-}\" = \"1\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}MATRIX_DEBUG=1\")\n fi\n\n # Optional: GitHub token\n if [ -n \"${AGENTTEAMS_GITHUB_TOKEN:-}\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}GITHUB_TOKEN=${AGENTTEAMS_GITHUB_TOKEN}\")\n fi\n\n # Optional: embedding model\n if [ -n \"${AGENTTEAMS_EMBEDDING_MODEL:-}\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}EMBEDDING_MODEL=${AGENTTEAMS_EMBEDDING_MODEL}\")\n fi\n\n # Optional: OpenAI-compatible base URL\n if [ -n \"${AGENTTEAMS_OPENAI_BASE_URL:-}\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}OPENAI_BASE_URL=${AGENTTEAMS_OPENAI_BASE_URL}\")\n fi\n # Optional: language\n if [ -n \"${AGENTTEAMS_LANGUAGE:-}\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}LANGUAGE=${AGENTTEAMS_LANGUAGE}\")\n fi\n\n # Optional: CMS/ARMS observability. In embedded mode the controller\n # spawns the Manager and Workers, so it must receive these settings.\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}CMS_TRACES_ENABLED=${AGENTTEAMS_CMS_TRACES_ENABLED:-false}\")\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}CMS_SERVICE_NAME=${AGENTTEAMS_CMS_SERVICE_NAME:-agentteams-manager}\")\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}CMS_METRICS_ENABLED=${AGENTTEAMS_CMS_METRICS_ENABLED:-false}\")\n if [ -n \"${AGENTTEAMS_CMS_ENDPOINT:-}\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}CMS_ENDPOINT=${AGENTTEAMS_CMS_ENDPOINT}\")\n fi\n if [ -n \"${AGENTTEAMS_CMS_LICENSE_KEY:-}\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}CMS_LICENSE_KEY=${AGENTTEAMS_CMS_LICENSE_KEY}\")\n fi\n if [ -n \"${AGENTTEAMS_CMS_PROJECT:-}\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}CMS_PROJECT=${AGENTTEAMS_CMS_PROJECT}\")\n fi\n if [ -n \"${AGENTTEAMS_CMS_WORKSPACE:-}\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}CMS_WORKSPACE=${AGENTTEAMS_CMS_WORKSPACE}\")\n fi\n\n # shellcheck disable=SC2086\n ${DOCKER_CMD} run -d \\\n --name agentteams-controller \\\n --network agentteams-net \\\n --network-alias matrix-local.agentteams.io \\\n --network-alias aigw-local.agentteams.io \\\n --network-alias fs-local.agentteams.io \\\n \"${_ctrl_env_args[@]}\" \\\n -v \"${CONTAINER_SOCK}:/var/run/docker.sock\" \\\n --security-opt label=disable \\\n -v \"${AGENTTEAMS_DATA_DIR}:/data\" \\\n -v \"${AGENTTEAMS_WORKSPACE_DIR}:/root/agentteams-fs/agents/manager\" \\\n -p \"${_port_prefix}${AGENTTEAMS_PORT_GATEWAY}:8080\" \\\n -p \"${_port_prefix}${AGENTTEAMS_PORT_CONSOLE}:8001\" \\\n -p \"${_port_prefix}${AGENTTEAMS_PORT_ELEMENT_WEB:-18088}:8088\" \\\n --restart unless-stopped \\\n \"${EMBEDDED_IMAGE}\"\n\n log \"Embedded controller started: agentteams-controller\"\n\n # Wait for infrastructure inside the controller container\n _wait_for_url() {\n local url=\"$1\" ctr=\"$2\" max_wait=\"${3:-120}\" desc=\"${4:-service}\"\n local elapsed=0\n log \"Waiting for ${desc}...\"\n while [ $elapsed -lt $max_wait ]; do\n if ${DOCKER_CMD} exec \"${ctr}\" curl -sf \"${url}\" >/dev/null 2>&1; then\n log \"${desc} is ready (${elapsed}s)\"\n return 0\n fi\n sleep 2\n elapsed=$((elapsed + 2))\n done\n log \"ERROR: ${desc} not ready after ${max_wait}s\"\n return 1\n }\n\n _wait_for_url \"http://127.0.0.1:6167/_tuwunel/server_version\" agentteams-controller 120 \"Tuwunel (Matrix)\" || exit 1\n _wait_for_url \"http://127.0.0.1:9000/minio/health/live\" agentteams-controller 60 \"MinIO\" || exit 1\n _wait_for_url \"http://127.0.0.1:8080/status\" agentteams-controller 120 \"Higress Gateway\" || exit 1\n\n # Wait for controller to create Manager Agent container\n log \"Waiting for Manager Agent container...\"\n local _mgr_wait=0\n local _mgr_max=300\n while [ $_mgr_wait -lt $_mgr_max ]; do\n if ${DOCKER_CMD} ps --format '{{.Names}}' 2>/dev/null | grep -q \"^agentteams-manager$\"; then\n log \"Manager Agent container detected (${_mgr_wait}s)\"\n break\n fi\n sleep 3\n _mgr_wait=$((_mgr_wait + 3))\n done\n if [ $_mgr_wait -ge $_mgr_max ]; then\n log \"ERROR: Manager Agent container not created after ${_mgr_max}s\"\n log \"Controller logs:\"\n ${DOCKER_CMD} exec agentteams-controller tail -30 /var/log/agentteams/agentteams-controller-error.log 2>/dev/null || true\n exit 1\n fi\n\n # Wait for Manager Agent to be running\n log \"Waiting for Manager Agent to start...\"\n local _agent_wait=0\n while [ $_agent_wait -lt 120 ]; do\n local _state\n _state=$(${DOCKER_CMD} inspect --format '{{.State.Status}}' agentteams-manager 2>/dev/null || echo \"missing\")\n if [ \"${_state}\" = \"running\" ]; then\n log \"Manager Agent is running\"\n break\n fi\n sleep 2\n _agent_wait=$((_agent_wait + 2))\n done\n\n # Enable yolo mode in agent if requested\n if [ \"${AGENTTEAMS_YOLO:-}\" = \"1\" ]; then\n ${DOCKER_CMD} exec agentteams-manager touch /root/manager-workspace/yolo-mode 2>/dev/null || true\n fi\n\n # Wait for the controller to send the first-boot welcome message.\n # The controller gates this on (a) Manager joining the DM room and\n # (b) Higress WASM key-auth propagation actually clearing /v1/chat/completions\n # for the Manager's gateway key — typically ~45-90s on a fresh install.\n # We poll Manager CR Status.WelcomeSent via the in-container agt CLI,\n # exec'd inside agentteams-controller (the source-of-truth container — its\n # bundled CLI binary is always in lockstep with whatever controller\n # binary is currently serving the HTTP API, since they're the same\n # `go build` output. The agentteams-manager container's CLI may lag the\n # controller across image upgrades and silently drop the welcomeSent\n # field, leaving this loop hung). The controller container mints a\n # long-lived admin SA token at startup and writes it to\n # AGENTTEAMS_AUTH_TOKEN_FILE=/var/run/agentteams/cli-token (set as a Dockerfile\n # ENV default), so a bare `docker exec agentteams-controller agt …`\n # auto-discovers both the endpoint and the token. There is a brief\n # window after container start before bootstrapAdminCLIToken completes\n # where the file may be empty / absent — the loop's silent retry\n # handles that the same way it handles the manager-not-yet-running\n # case below.\n if ${DOCKER_CMD} exec agentteams-controller sh -c 'command -v agt' >/dev/null 2>&1; then\n log \"$(msg install.welcome_msg.waiting)\"\n local _welcome_wait=0\n local _welcome_max=\"${AGENTTEAMS_WELCOME_TIMEOUT:-300}\"\n local _welcome_done=0\n while [ $_welcome_wait -lt $_welcome_max ]; do\n # `tr -d` strips whitespace/CR so the grep stays robust to\n # any future change in go-json field ordering or formatting.\n local _wjson\n _wjson=$(${DOCKER_CMD} exec agentteams-controller \\\n agt get managers default -o json 2>/dev/null || true)\n if [ -n \"${_wjson}\" ] && printf '%s' \"${_wjson}\" | tr -d ' \\r\\n' | grep -q '\"welcomeSent\":true'; then\n log \"$(msg install.welcome_msg.confirmed \"${_welcome_wait}\")\"\n _welcome_done=1\n break\n fi\n sleep 3\n _welcome_wait=$((_welcome_wait + 3))\n done\n if [ $_welcome_done -ne 1 ]; then\n # Non-fatal: install is still good. Keep going to the success\n # banner so the admin can use Element Web to nudge Manager into\n # onboarding manually (one DM message is enough).\n log \"$(msg install.welcome_msg.timeout \"${_welcome_max}\")\"\n log \"$(msg install.welcome_msg.timeout_hint)\"\n log \"$(msg install.welcome_msg.timeout_inspect)\"\n fi\n else\n log \"$(msg install.welcome_msg.poll_unavailable)\"\n fi\n\n else\n # ============================================================\n # Legacy architecture: all-in-one manager container\n # ============================================================\n\n NETWORK_ARGS=\"--network agentteams-net\"\n NETWORK_ALIAS_ARGS=\"--network-alias matrix-local.agentteams.io --network-alias aigw-local.agentteams.io --network-alias fs-local.agentteams.io\"\n for _domain in \"${AGENTTEAMS_MATRIX_CLIENT_DOMAIN:-}\" \"${AGENTTEAMS_CONSOLE_DOMAIN:-}\"; do\n if [ -n \"${_domain}\" ] && [[ \"${_domain}\" == *-local.agentteams.io ]]; then\n NETWORK_ALIAS_ARGS=\"${NETWORK_ALIAS_ARGS} --network-alias ${_domain}\"\n fi\n done\n\n # Start Docker API proxy if enabled (security layer between Manager and Docker daemon)\n PROXY_ARGS=\"\"\n if [ \"${AGENTTEAMS_DOCKER_PROXY:-1}\" = \"1\" ] && [ -n \"${CONTAINER_SOCK:-}\" ]; then\n local _proxy_image=\"${AGENTTEAMS_REGISTRY}/higress/agentteams-docker-proxy:${AGENTTEAMS_VERSION}\"\n # Try versioned tag, fallback to latest\n if ! ${DOCKER_CMD} image inspect \"${_proxy_image}\" >/dev/null 2>&1; then\n ${DOCKER_CMD} pull \"${_proxy_image}\" 2>/dev/null || {\n _proxy_image=\"${AGENTTEAMS_REGISTRY}/higress/agentteams-docker-proxy:latest\"\n ${DOCKER_CMD} pull \"${_proxy_image}\" 2>/dev/null || true\n }\n fi\n if ${DOCKER_CMD} image inspect \"${_proxy_image}\" >/dev/null 2>&1; then\n log \"Starting Docker API proxy...\"\n ${DOCKER_CMD} run -d \\\n --name agentteams-docker-proxy \\\n --network agentteams-net \\\n -v \"${CONTAINER_SOCK}:/var/run/docker.sock\" \\\n --security-opt label=disable \\\n -e AGENTTEAMS_WORKER_IMAGE=\"${WORKER_IMAGE}\" \\\n -e AGENTTEAMS_COPAW_WORKER_IMAGE=\"${COPAW_WORKER_IMAGE}\" \\\n -e AGENTTEAMS_QWENPAW_WORKER_IMAGE=\"${QWENPAW_WORKER_IMAGE}\" \\\n -e AGENTTEAMS_HERMES_WORKER_IMAGE=\"${HERMES_WORKER_IMAGE}\" \\\n ${AGENTTEAMS_PROXY_ALLOWED_REGISTRIES:+-e AGENTTEAMS_PROXY_ALLOWED_REGISTRIES=\"${AGENTTEAMS_PROXY_ALLOWED_REGISTRIES}\"} \\\n --restart unless-stopped \\\n \"${_proxy_image}\"\n PROXY_ARGS=\"-e AGENTTEAMS_CONTROLLER_URL=http://agentteams-docker-proxy:2375 -e AGENTTEAMS_CONTAINER_API=http://agentteams-docker-proxy:2375\"\n SOCKET_MOUNT_ARGS=\"\"\n fi\n fi\n\n # Pass host timezone to container\n TZ_ARGS=\"\"\n if [ -n \"${AGENTTEAMS_TIMEZONE:-}\" ]; then\n TZ_ARGS=\"-e TZ=${AGENTTEAMS_TIMEZONE}\"\n fi\n\n YOLO_ARGS=\"\"\n if [ \"${AGENTTEAMS_YOLO:-}\" = \"1\" ]; then\n YOLO_ARGS=\"-e AGENTTEAMS_YOLO=1\"\n fi\n\n MATRIX_DEBUG_ARGS=\"\"\n if [ \"${AGENTTEAMS_MATRIX_DEBUG:-}\" = \"1\" ]; then\n MATRIX_DEBUG_ARGS=\"-e AGENTTEAMS_MATRIX_DEBUG=1\"\n fi\n\n # shellcheck disable=SC2086\n ${DOCKER_CMD} run -d \\\n --name agentteams-manager \\\n --env-file \"${ENV_FILE}\" \\\n -e HOME=/root/manager-workspace \\\n -w /root/manager-workspace \\\n -e HOST_ORIGINAL_HOME=\"${AGENTTEAMS_HOST_SHARE_DIR}\" \\\n -e AGENTTEAMS_MANAGER_RUNTIME=\"${AGENTTEAMS_MANAGER_RUNTIME:-copaw}\" \\\n ${JVM_ARGS:+-e JVM_ARGS=\"${JVM_ARGS}\"} \\\n ${YOLO_ARGS} \\\n ${MATRIX_DEBUG_ARGS} \\\n ${TZ_ARGS} \\\n ${SOCKET_MOUNT_ARGS} \\\n ${NETWORK_ARGS} \\\n ${NETWORK_ALIAS_ARGS} \\\n ${PROXY_ARGS} \\\n -p \"${_port_prefix}${AGENTTEAMS_PORT_GATEWAY}:8080\" \\\n -p \"${_port_prefix}${AGENTTEAMS_PORT_CONSOLE}:8001\" \\\n -p \"${_port_prefix}${AGENTTEAMS_PORT_ELEMENT_WEB:-18088}:8088\" \\\n -p \"127.0.0.1:${AGENTTEAMS_PORT_MANAGER_CONSOLE:-18888}:18888\" \\\n ${DATA_MOUNT_ARGS} \\\n ${WORKSPACE_MOUNT_ARGS} \\\n ${HOST_SHARE_MOUNT_ARGS} \\\n --restart unless-stopped \\\n \"$([ \"${AGENTTEAMS_MANAGER_RUNTIME}\" = \"copaw\" ] && echo \"${MANAGER_COPAW_IMAGE}\" || echo \"${MANAGER_IMAGE}\")\"\n\n # Wait for Manager agent to be ready\n wait_manager_ready \"agentteams-manager\"\n\n # Wait for Matrix server to be ready\n wait_matrix_ready \"agentteams-manager\"\n\n # Post-install verification (non-fatal: warnings only)\n local _verify_script\n _verify_script=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)/agentteams-verify.sh\"\n if [ -f \"${_verify_script}\" ]; then\n bash \"${_verify_script}\" \"agentteams-manager\" || {\n log \"WARNING: Some post-install checks failed. Re-run: bash install/agentteams-verify.sh\"\n }\n fi\n fi\n unset _port_prefix\n\n # ── Start Dashboard (optional) ────────────────────────────────────\n _start_dashboard\n\n # Apply Podman autostart if selected and environment matches\n if [ \"${DOCKER_CMD}\" = \"podman\" ] && [ \"${AGENTTEAMS_PODMAN_AUTOSTART:-0}\" = \"1\" ]; then\n setup_podman_autostart\n fi\n\n log \"\"\n log \"$(msg success.title)\"\n log \"\"\n log \"$(msg success.domains_configured)\"\n log \" ${AGENTTEAMS_MATRIX_DOMAIN%%:*} ${AGENTTEAMS_MATRIX_CLIENT_DOMAIN} ${AGENTTEAMS_AI_GATEWAY_DOMAIN} ${AGENTTEAMS_FS_DOMAIN} ${AGENTTEAMS_CONSOLE_DOMAIN}\"\n log \"\"\n local lan_ip\n lan_ip=$(detect_lan_ip)\n echo -e \"\\033[33m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\033[0m\"\n echo -e \"\\033[33m $(msg success.open_url)\\033[0m\"\n echo -e \"\\033[33m \\033[0m\"\n echo -e \"\\033[1;36m http://127.0.0.1:${AGENTTEAMS_PORT_ELEMENT_WEB:-18088}/#/login\\033[0m\"\n echo -e \"\\033[33m \\033[0m\"\n echo -e \"\\033[33m $(msg success.login_with)\\033[0m\"\n echo -e \"\\033[33m $(msg success.username \"${AGENTTEAMS_ADMIN_USER}\")\\033[0m\"\n echo -e \"\\033[33m $(msg success.password \"${AGENTTEAMS_ADMIN_PASSWORD}\")\\033[0m\"\n echo -e \"\\033[33m \\033[0m\"\n echo -e \"\\033[33m $(msg success.after_login)\\033[0m\"\n echo -e \"\\033[33m $(msg success.tell_it)\\033[0m\"\n echo -e \"\\033[33m $(msg success.manager_auto)\\033[0m\"\n echo -e \"\\033[33m \\033[0m\"\n echo -e \"\\033[33m ───────────────────────────────────────────────────────────────────────────── \\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_title)\\033[0m\"\n echo -e \"\\033[33m \\033[0m\"\n if [ -n \"${lan_ip}\" ]; then\n echo -e \"\\033[33m $(msg success.mobile_step1)\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_step2 \"http://${lan_ip}:${AGENTTEAMS_PORT_GATEWAY}\")\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_step3)\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_username \"${AGENTTEAMS_ADMIN_USER}\")\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_password \"${AGENTTEAMS_ADMIN_PASSWORD}\")\\033[0m\"\n else\n echo -e \"\\033[33m $(msg success.mobile_step1)\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_step2_noip \"${AGENTTEAMS_PORT_GATEWAY}\")\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_noip_hint)\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_step3)\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_username \"${AGENTTEAMS_ADMIN_USER}\")\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_password \"${AGENTTEAMS_ADMIN_PASSWORD}\")\\033[0m\"\n fi\n echo -e \"\\033[33m \\033[0m\"\n echo -e \"\\033[33m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\033[0m\"\n log \"\"\n log \"$(msg success.other_consoles)\"\n log \"$(msg success.higress_console \"${AGENTTEAMS_PORT_CONSOLE}\" \"${AGENTTEAMS_ADMIN_USER}\" \"${AGENTTEAMS_ADMIN_PASSWORD}\")\"\n if [ \"${AGENTTEAMS_DASHBOARD:-0}\" = \"1\" ]; then\n log \"$(msg success.dashboard \"${AGENTTEAMS_PORT_DASHBOARD:-13000}\")\"\n fi\n if [ \"${AGENTTEAMS_USE_EMBEDDED}\" != \"1\" ]; then\n log \"$(msg success.manager_console \"${AGENTTEAMS_PORT_MANAGER_CONSOLE:-18888}\")\"\n log \"$(msg success.manager_console_gateway \"${AGENTTEAMS_ADMIN_USER}\" \"${AGENTTEAMS_ADMIN_PASSWORD}\")\"\n fi\n log \"\"\n log \"$(msg success.switch_llm.title)\"\n log \"$(msg success.switch_llm.hint)\"\n log \"$(msg success.switch_llm.docs)\"\n log \"$(msg success.switch_llm.url)\"\n log \"\"\n log \"$(msg success.tip)\"\n log \"\"\n if [ \"${AGENTTEAMS_LOCAL_ONLY:-1}\" != \"1\" ]; then\n echo -e \"\\033[33m$(msg port.local_only.https_hint)\\033[0m\"\n log \"\"\n fi\n log \"$(msg success.config_file \"${ENV_FILE}\")\"\n log \"$(msg success.data_volume \"${AGENTTEAMS_DATA_DIR}\")\"\n log \"$(msg success.workspace \"${AGENTTEAMS_WORKSPACE_DIR}\")\"\n}\n\n# ============================================================\n# Worker Installation (One-Click)\n# ============================================================\n\ninstall_worker() {\n local WORKER_NAME=\"\"\n local FS=\"\"\n local FS_KEY=\"\"\n local FS_SECRET=\"\"\n local RESET=false\n local SKILLS_API_URL=\"\"\n\n # Parse arguments\n while [ $# -gt 0 ]; do\n case $1 in\n --name) WORKER_NAME=\"$2\"; shift 2 ;;\n --fs) FS=\"$2\"; shift 2 ;;\n --fs-key) FS_KEY=\"$2\"; shift 2 ;;\n --fs-secret) FS_SECRET=\"$2\"; shift 2 ;;\n --skills-api-url) SKILLS_API_URL=\"$2\"; shift 2 ;;\n --reset) RESET=true; shift ;;\n *) die \"$(msg error.unknown_option \"$1\")\" ;;\n esac\n done\n\n # Validate required params\n [ -z \"${WORKER_NAME}\" ] && die \"$(msg error.name_required)\"\n [ -z \"${FS}\" ] && die \"$(msg error.fs_required)\"\n [ -z \"${FS_KEY}\" ] && die \"$(msg error.fs_key_required)\"\n [ -z \"${FS_SECRET}\" ] && die \"$(msg error.fs_secret_required)\"\n\n local CONTAINER_NAME=\"agentteams-worker-${WORKER_NAME}\"\n\n # Handle reset\n if [ \"${RESET}\" = true ]; then\n log \"$(msg worker.resetting \"${WORKER_NAME}\")\"\n ${DOCKER_CMD} stop \"${CONTAINER_NAME}\" 2>/dev/null || true\n ${DOCKER_CMD} rm \"${CONTAINER_NAME}\" 2>/dev/null || true\n fi\n\n # Check for existing container\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' | grep -q \"^${CONTAINER_NAME}$\"; then\n die \"$(msg worker.exists \"${CONTAINER_NAME}\")\"\n fi\n\n log \"$(msg worker.starting \"${WORKER_NAME}\")\"\n\n # Build docker run args\n local DOCKER_ENV=\"\"\n DOCKER_ENV=\"${DOCKER_ENV} -e HOME=/root/agentteams-fs/agents/${WORKER_NAME}\"\n DOCKER_ENV=\"${DOCKER_ENV} -w /root/agentteams-fs/agents/${WORKER_NAME}\"\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_WORKER_NAME=${WORKER_NAME}\"\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_FS_ENDPOINT=${FS}\"\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_FS_ACCESS_KEY=${FS_KEY}\"\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_FS_SECRET_KEY=${FS_SECRET}\"\n\n if [ -z \"${SKILLS_API_URL}\" ]; then\n if [ -n \"${AGENTTEAMS_SKILLS_API_URL:-}\" ]; then\n SKILLS_API_URL=\"${AGENTTEAMS_SKILLS_API_URL}\"\n else\n SKILLS_API_URL=\"nacos://market.agentteams.io:80/public\"\n fi\n fi\n\n # Add SKILLS_API_URL if specified\n DOCKER_ENV=\"${DOCKER_ENV} -e SKILLS_API_URL=${SKILLS_API_URL}\"\n log \"$(msg worker.skills_url \"${SKILLS_API_URL}\")\"\n if [ -n \"${AGENTTEAMS_NACOS_USERNAME:-}\" ]; then\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_NACOS_USERNAME=${AGENTTEAMS_NACOS_USERNAME}\"\n fi\n if [ -n \"${AGENTTEAMS_NACOS_PASSWORD:-}\" ]; then\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_NACOS_PASSWORD=${AGENTTEAMS_NACOS_PASSWORD}\"\n fi\n if [ -n \"${AGENTTEAMS_NACOS_TOKEN:-}\" ]; then\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_NACOS_TOKEN=${AGENTTEAMS_NACOS_TOKEN}\"\n fi\n\n # shellcheck disable=SC2086\n ${DOCKER_CMD} run -d \\\n --name \"${CONTAINER_NAME}\" \\\n ${DOCKER_ENV} \\\n --restart unless-stopped \\\n \"${WORKER_IMAGE}\"\n\n log \"\"\n log \"$(msg worker.started \"${WORKER_NAME}\")\"\n log \"$(msg worker.container \"${CONTAINER_NAME}\")\"\n log \"$(msg worker.view_logs \"${CONTAINER_NAME}\")\"\n}\n\n# ============================================================\n# Main\n# ============================================================\n\n# ============================================================\n# LLM API connectivity test\n# ============================================================\n\ntest_llm_connectivity() {\n local base_url=\"$1\"\n local api_key=\"$2\"\n local model=\"$3\"\n local hint=\"${4:-}\" # optional: extra hint shown on failure\n if ! command -v curl >/dev/null 2>&1; then\n echo -e \"\\033[33m$(msg llm.openai.test.no_curl)\\033[0m\"\n return\n fi\n log \"$(msg llm.openai.test.testing)\"\n local _body _http_code _tmpfile\n _tmpfile=$(mktemp)\n _http_code=$(curl -s -o \"${_tmpfile}\" -w \"%{http_code}\" \\\n -X POST \"${base_url%/}/chat/completions\" \\\n -H \"Authorization: Bearer ${api_key}\" \\\n -H \"Content-Type: application/json\" \\\n -H \"User-Agent: AgentTeams/${AGENTTEAMS_VERSION:-latest}\" \\\n --max-time 30 \\\n -d \"{\\\"model\\\":\\\"${model}\\\",\\\"messages\\\":[{\\\"role\\\":\\\"user\\\",\\\"content\\\":\\\"hi\\\"}]}\" \\\n 2>/dev/null)\n _body=$(cat \"${_tmpfile}\")\n rm -f \"${_tmpfile}\"\n if [ \"${_http_code}\" = \"200\" ] || [ \"${_http_code}\" = \"201\" ]; then\n log \"$(msg llm.openai.test.ok)\"\n else\n echo -e \"\\033[33m$(msg llm.openai.test.fail \"${_http_code}\" \"${_body}\")\\033[0m\"\n if [ -n \"${hint}\" ]; then\n echo -e \"\\033[33m${hint}\\033[0m\"\n fi\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" != \"1\" ]; then\n local _confirm\n read -e -p \"$(msg llm.openai.test.confirm)\" _confirm\n if [ \"${_confirm}\" = \"b\" ]; then\n STEP_RESULT=\"back\"\n return 1\n fi\n if [ \"${_confirm}\" != \"y\" ] && [ \"${_confirm}\" != \"Y\" ]; then\n log \"$(msg llm.openai.test.aborted)\"\n exit 1\n fi\n fi\n fi\n}\n\ntest_embedding_connectivity() {\n local base_url=\"$1\"\n local api_key=\"$2\"\n local model=\"$3\"\n if ! command -v curl >/dev/null 2>&1; then\n return 0\n fi\n log \"$(msg llm.embedding.test.testing)\"\n local _body _http_code _tmpfile\n _tmpfile=$(mktemp)\n _http_code=$(curl -s -o \"${_tmpfile}\" -w \"%{http_code}\" \\\n -X POST \"${base_url%/}/embeddings\" \\\n -H \"Authorization: Bearer ${api_key}\" \\\n -H \"Content-Type: application/json\" \\\n -H \"User-Agent: AgentTeams/${AGENTTEAMS_VERSION:-latest}\" \\\n --max-time 30 \\\n -d \"{\\\"model\\\":\\\"${model}\\\",\\\"input\\\":\\\"test\\\"}\" \\\n 2>/dev/null)\n _body=$(cat \"${_tmpfile}\")\n rm -f \"${_tmpfile}\"\n if [ \"${_http_code}\" = \"200\" ] || [ \"${_http_code}\" = \"201\" ]; then\n log \"$(msg llm.embedding.test.ok)\"\n return 0\n else\n echo -e \"\\033[33m$(msg llm.embedding.test.fail \"${_http_code}\" \"${_body}\")\\033[0m\"\n return 1\n fi\n}\n\n# ============================================================\n# Uninstall\n# ============================================================\n\nuninstall_agentteams() {\n log \"$(msg uninstall.title)\"\n\n # Safely disable and remove the dedicated AgentTeams Podman autostart service\n if [ \"${DOCKER_CMD}\" = \"podman\" ] && command -v systemctl >/dev/null 2>&1; then\n local _systemctl_cmd=\"systemctl\"\n local _service_dir=\"/etc/systemd/system\"\n\n if [ \"$(id -u)\" != \"0\" ]; then\n _systemctl_cmd=\"systemctl --user\"\n _service_dir=\"${HOME}/.config/systemd/user\"\n fi\n\n if [ -f \"${_service_dir}/agentteams-podman-restart.service\" ]; then\n log \"Disabling and removing dedicated AgentTeams Podman autostart service...\"\n ${_systemctl_cmd} disable --now agentteams-podman-restart.service 2>/dev/null || true\n rm -f \"${_service_dir}/agentteams-podman-restart.service\"\n ${_systemctl_cmd} daemon-reload 2>/dev/null || true\n fi\n # Note: Native podman-restart.service is strictly untouched per maintainer review.\n fi\n\n # Capture manager image before removing (needed for workspace cleanup on Linux)\n local manager_image=\"\"\n manager_image=$(${DOCKER_CMD} inspect agentteams-manager --format '{{.Config.Image}}' 2>/dev/null || true)\n\n # Stop and remove manager\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -q \"^agentteams-manager$\"; then\n log \"$(msg uninstall.stopping_manager)\"\n ${DOCKER_CMD} stop agentteams-manager >/dev/null 2>&1 || true\n ${DOCKER_CMD} rm agentteams-manager >/dev/null 2>&1 || true\n fi\n\n # Stop and remove workers\n local workers\n workers=$(${DOCKER_CMD} ps -a --filter \"name=agentteams-worker-\" --format '{{.Names}}' 2>/dev/null || true)\n if [ -n \"${workers}\" ]; then\n log \"$(msg uninstall.stopping_workers)\"\n echo \"${workers}\" | while read -r w; do\n ${DOCKER_CMD} rm -f \"${w}\" >/dev/null 2>&1 || true\n log \"$(msg uninstall.removed \"${w}\")\"\n done\n fi\n\n # Stop and remove the dashboard container (installed via step_dashboard)\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -q \"^agentteams-dashboard$\"; then\n log \"Stopping and removing agentteams-dashboard...\"\n ${DOCKER_CMD} stop agentteams-dashboard >/dev/null 2>&1 || true\n ${DOCKER_CMD} rm agentteams-dashboard >/dev/null 2>&1 || true\n fi\n if ${DOCKER_CMD} volume inspect agentteams-dashboard-data >/dev/null 2>&1; then\n log \"Removing dashboard data volume: agentteams-dashboard-data\"\n ${DOCKER_CMD} volume rm agentteams-dashboard-data >/dev/null 2>&1 || true\n fi\n\n # Stop and remove docker-proxy (legacy ≤ v1.0.x; current arch uses\n # agentteams-controller for the same role)\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -q \"^agentteams-docker-proxy$\"; then\n log \"$(msg uninstall.removing_proxy)\"\n ${DOCKER_CMD} stop agentteams-docker-proxy >/dev/null 2>&1 || true\n ${DOCKER_CMD} rm agentteams-docker-proxy >/dev/null 2>&1 || true\n fi\n\n # Stop and remove the embedded controller container. MUST happen\n # before the `docker volume rm agentteams-data` step below — in embedded\n # mode agentteams-controller mounts agentteams-data at /data (Tuwunel DB,\n # MinIO state, Higress state, all the room messages), and `volume\n # rm` against an in-use volume fails silently because of the trailing\n # `|| true`. Skipping this used to leave room/message history behind\n # across \"uninstall + reinstall\" cycles. See PR #692.\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -q \"^agentteams-controller$\"; then\n log \"$(msg uninstall.stopping_controller)\"\n ${DOCKER_CMD} stop agentteams-controller >/dev/null 2>&1 || true\n ${DOCKER_CMD} rm agentteams-controller >/dev/null 2>&1 || true\n fi\n\n # Read env file for data/workspace info before removing\n local env_file=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n [ ! -f \"${env_file}\" ] && [ -f \"./agentteams-manager.env\" ] && env_file=\"./agentteams-manager.env\"\n\n local data_dir=\"\" workspace_dir=\"\"\n if [ -f \"${env_file}\" ]; then\n data_dir=$(grep '^AGENTTEAMS_DATA_DIR=' \"${env_file}\" 2>/dev/null | cut -d= -f2- || true)\n workspace_dir=$(grep '^AGENTTEAMS_WORKSPACE_DIR=' \"${env_file}\" 2>/dev/null | cut -d= -f2- || true)\n fi\n\n # Remove Docker volume\n local vol=\"${data_dir:-agentteams-data}\"\n if ${DOCKER_CMD} volume inspect \"${vol}\" >/dev/null 2>&1; then\n log \"$(msg uninstall.removing_volume \"${vol}\")\"\n ${DOCKER_CMD} volume rm \"${vol}\" >/dev/null 2>&1 || true\n fi\n\n # Remove Docker network\n if ${DOCKER_CMD} network ls --format '{{.Name}}' 2>/dev/null | grep -q \"^agentteams-net$\"; then\n log \"$(msg uninstall.removing_network)\"\n ${DOCKER_CMD} network rm agentteams-net >/dev/null 2>&1 || true\n fi\n\n # Remove workspace directory\n if [ -n \"${workspace_dir}\" ] && [ -d \"${workspace_dir}\" ]; then\n log \"$(msg uninstall.removing_workspace \"${workspace_dir}\")\"\n rm -rf \"${workspace_dir}\" 2>/dev/null || true\n if [ -d \"${workspace_dir}\" ]; then\n log \"$(msg uninstall.removing_workspace_elevated)\"\n local _parent _base\n _parent=$(dirname \"${workspace_dir}\")\n _base=$(basename \"${workspace_dir}\")\n local _rm_image=\"${manager_image:-busybox}\"\n ${DOCKER_CMD} run --rm --entrypoint sh \\\n -v \"${_parent}:/host-parent\" \\\n \"${_rm_image}\" \\\n -c \"rm -rf /host-parent/${_base}\" 2>/dev/null || true\n fi\n fi\n\n # Remove env file\n if [ -f \"${env_file}\" ]; then\n log \"$(msg uninstall.removing_env \"${env_file}\")\"\n rm -f \"${env_file}\"\n fi\n\n # Remove install log\n if [ -f \"${HOME}/agentteams-install.log\" ]; then\n log \"$(msg uninstall.removing_log \"${HOME}/agentteams-install.log\")\"\n rm -f \"${HOME}/agentteams-install.log\"\n fi\n\n echo \"\"\n log \"$(msg uninstall.done)\"\n}\n\n# ============================================================\n# Check container runtime (docker or podman) and environment\n# ============================================================\ncheck_container_runtime() {\n # Ensure the rootless environment baseline (XDG_RUNTIME_DIR) is ready before engine detection.\n if [ \"$(id -u)\" -ne 0 ] && [ -z \"${XDG_RUNTIME_DIR:-}\" ]; then\n export XDG_RUNTIME_DIR=\"/run/user/$(id -u)\"\n fi\n\n if command -v docker >/dev/null 2>&1; then\n DOCKER_CMD=\"docker\"\n # Check for podman disguised as docker (alias or podman-docker package)\n if docker --version 2>/dev/null | grep -qi \"podman\"; then\n DOCKER_CMD=\"podman\"\n fi\n elif command -v podman >/dev/null 2>&1; then\n DOCKER_CMD=\"podman\"\n else\n echo -e \"\\033[31m[AgentTeams ERROR]\\033[0m $(msg error.docker_not_found)\" >&2\n exit 1\n fi\n\n # Unified health check (Validates runtime responsiveness)\n if ! ${DOCKER_CMD} ps >/dev/null 2>&1; then\n echo -e \"\\033[31m[AgentTeams ERROR]\\033[0m $(msg error.docker_not_running)\" >&2\n exit 1\n fi\n}\n\ncheck_container_runtime\n\ncase \"${1:-}\" in\n manager|\"\")\n install_manager\n ;;\n worker)\n shift\n install_worker \"$@\"\n ;;\n dashboard)\n check_container_runtime\n load_current_params_from_env\n AGENTTEAMS_DASHBOARD=\"${AGENTTEAMS_DASHBOARD:-1}\"\n AGENTTEAMS_DASHBOARD_VERSION=\"${AGENTTEAMS_DASHBOARD_VERSION:-v1.2.0-beta.1}\"\n AGENTTEAMS_PORT_DASHBOARD=\"${AGENTTEAMS_PORT_DASHBOARD:-13000}\"\n AGENTTEAMS_DASHBOARD_IMAGE=\"${AGENTTEAMS_DASHBOARD_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-dashboard:${AGENTTEAMS_DASHBOARD_VERSION}}\"\n AGENTTEAMS_USE_EMBEDDED=1\n _start_dashboard\n ;;\n uninstall)\n uninstall_agentteams\n ;;\n *)\n echo \"Usage: $0 [manager|worker [options]|dashboard|uninstall]\"\n echo \"\"\n echo \"Commands:\"\n echo \" manager Interactive Manager installation (default)\"\n echo \" Choose Quick Start (all defaults) or Manual mode\"\n echo \" worker Worker installation (requires --name and connection params)\"\n echo \" dashboard Start/stop agentteams-dashboard (use with AGENTTEAMS_DASHBOARD=0 to stop)\"\n echo \" uninstall Stop and remove Manager + all Worker containers\"\n echo \"\"\n echo \"Quick Start (fastest):\"\n echo \" $0\"\n echo \" # Then select '1' for Quick Start mode\"\n echo \"\"\n echo \"Non-interactive (for automation):\"\n echo \" AGENTTEAMS_NON_INTERACTIVE=1 AGENTTEAMS_LLM_API_KEY=sk-xxx $0\"\n echo \"\"\n echo \"Worker Options:\"\n echo \" --name <name> Worker name (required)\"\n echo \" --fs <url> MinIO endpoint URL (required)\"\n echo \" --fs-key <key> MinIO access key (required)\"\n echo \" --fs-secret <secret> MinIO secret key (required)\"\n echo \" --reset Remove existing Worker container before creating\"\n exit 1\n ;;\nesac\n"}, "793db242257a569d911b1aa59c1cd554af78511f": {"t": 1785764739.7774034, "n": 248204, "h": "2bdb3dfebb18f051", "c": "#!/bin/bash\n# agentteams-install.sh - One-click installation for AgentTeams Manager, Worker, and Dashboard\n#\n# Usage:\n# ./agentteams-install.sh # Interactive installation (choose Quick Start or Manual)\n# ./agentteams-install.sh manager # Same as above (explicit)\n# ./agentteams-install.sh worker --name <name> ... # Worker installation\n# ./agentteams-install.sh dashboard # Start/stop agentteams-dashboard (AGENTTEAMS_DASHBOARD=0 to stop)\n# ./agentteams-install.sh uninstall # Stop and remove Manager + all Workers\n#\n# NOTE: agentteams-dashboard is currently only supported via the Bash installer.\n# The PowerShell installer (agentteams-install.ps1) does not yet include dashboard support.\n#\n# Onboarding Modes:\n# Quick Start - Fast installation with all default values (recommended)\n# Manual - Customize each option step by step\n#\n# Environment variables (for automation):\n# AGENTTEAMS_NON_INTERACTIVE Skip all prompts, use defaults (default: 0)\n# AGENTTEAMS_LLM_PROVIDER LLM provider (default: openai-compat for zh non-interactive Token Plan; qwen for en)\n# AGENTTEAMS_DEFAULT_MODEL Default model (default: qwen3.6-plus for zh Token Plan and en non-interactive)\n# AGENTTEAMS_OPENAI_BASE_URL OpenAI-compatible base URL (default for zh non-interactive: Alibaba Token Plan endpoint)\n# AGENTTEAMS_LLM_API_KEY LLM API key (required)\n# AGENTTEAMS_ADMIN_USER Admin username (default: admin)\n# AGENTTEAMS_ADMIN_PASSWORD Admin password (auto-generated if not set, min 8 chars)\n# AGENTTEAMS_MATRIX_DOMAIN Matrix domain (default: matrix-local.agentteams.io:18080)\n# AGENTTEAMS_MOUNT_SOCKET Mount container runtime socket (default: 1)\n# AGENTTEAMS_DATA_DIR Docker volume name for persistent data (default: agentteams-data)\n# AGENTTEAMS_WORKSPACE_DIR Host directory for manager workspace (default: ~/agentteams-manager)\n# AGENTTEAMS_VERSION Image tag (default: latest)\n# AGENTTEAMS_REGISTRY Image registry (default: auto-detected by timezone)\n# AGENTTEAMS_INSTALL_MANAGER_IMAGE Override manager image (e.g., local build)\n# AGENTTEAMS_INSTALL_WORKER_IMAGE Override worker image (e.g., local build)\n# AGENTTEAMS_INSTALL_COPAW_WORKER_IMAGE Override copaw worker image (e.g., local build)\n# AGENTTEAMS_INSTALL_HERMES_WORKER_IMAGE Override hermes worker image (e.g., local build)\n# AGENTTEAMS_NACOS_REGISTRY_URI Default Nacos registry URI for Worker market search/import\n# (default: nacos://market.agentteams.io:80/public)\n# AGENTTEAMS_NACOS_USERNAME Default Nacos username for nacos:// package imports (optional)\n# AGENTTEAMS_NACOS_PASSWORD Default Nacos password for nacos:// package imports (optional)\n# AGENTTEAMS_CMS_TRACES_ENABLED Enable openclaw-cms-plugin traces for Manager AND all Workers (default: false)\n# AGENTTEAMS_CMS_ENDPOINT ARMS OTLP endpoint (required if traces enabled)\n# AGENTTEAMS_CMS_LICENSE_KEY CMS license key (required if traces enabled)\n# AGENTTEAMS_CMS_PROJECT CMS project name (optional)\n# AGENTTEAMS_CMS_WORKSPACE CMS workspace ID (required if traces enabled)\n# AGENTTEAMS_CMS_SERVICE_NAME Manager service name in ARMS (default: agentteams-manager)\n# Workers always report as agentteams-worker-<name> automatically\n# AGENTTEAMS_CMS_METRICS_ENABLED Enable diagnostics-otel metrics for Manager AND all Workers (default: false)\n# AGENTTEAMS_PORT_GATEWAY Host port for Higress gateway (default: 18080)\n# AGENTTEAMS_PORT_CONSOLE Host port for Higress console (default: 18001)\n# AGENTTEAMS_PORT_ELEMENT_WEB Host port for Element Web direct access (default: 18088)\n# AGENTTEAMS_PORT_MANAGER_CONSOLE Host port for Manager console (default: 18888)\n# AGENTTEAMS_WORKER_IDLE_TIMEOUT Worker idle timeout in minutes (default: 720, i.e. 12 hours)\n# AGENTTEAMS_DASHBOARD Install agentteams-dashboard management UI (default: 1)\n# AGENTTEAMS_DASHBOARD_VERSION Dashboard version (default: v1.2.0-beta.1, independent of AgentTeams version)\n# AGENTTEAMS_PORT_DASHBOARD Dashboard host port (default: 13000)\n# AGENTTEAMS_DASHBOARD_IMAGE Override dashboard image (default: <registry>/agentteams/agentteams-dashboard:<DASHBOARD_VERSION>)\n# AGENTTEAMS_AI_GATEWAY_ADMIN_URL Higress Console URL for shared auth (auto-detected)\n\nset -e\n\nAGENTTEAMS_VERSION=\"${AGENTTEAMS_VERSION:-}\"\nAGENTTEAMS_KNOWN_STABLE_VERSION=\"v1.2.0\" # fallback if GitHub API is unreachable\n\n_normalize_version() {\n local version=\"$1\"\n case \"${version}\" in\n [0-9]*.[0-9]*.[0-9]*) version=\"v${version}\" ;;\n esac\n case \"${version}\" in\n v[0-9]*.[0-9]*.[0-9]*.beta.[0-9]*) version=\"${version/.beta./-beta.}\" ;;\n esac\n printf '%s' \"${version}\"\n}\n\n# Returns 0 (true) if $1 < $2 using semver order; \"latest\" is treated as greatest\n_ver_lt() {\n [ \"$1\" = \"latest\" ] && return 1\n [ \"$2\" = \"latest\" ] && return 0\n [ \"$1\" = \"$2\" ] && return 1\n [ \"$(printf '%s\\n%s' \"$1\" \"$2\" | sort -V | head -1)\" = \"$1\" ]\n}\n\n_use_legacy_image_env() {\n if [ \"$1\" = \"latest\" ]; then\n _ver_lt \"${AGENTTEAMS_KNOWN_STABLE_VERSION}\" \"v1.2.0\"\n else\n _ver_lt \"$1\" \"v1.2.0\"\n fi\n}\n\n_controller_env_prefix() {\n if _use_legacy_image_env \"$1\"; then\n printf '%s%s' 'HIC' 'LAW_'\n else\n printf '%s' 'AGENTTEAMS_'\n fi\n}\n\n_controller_storage_prefix() {\n if _use_legacy_image_env \"$1\"; then\n printf '%s%s' 'hic' 'law/agentteams-storage'\n else\n printf '%s' 'agentteams/agentteams-storage'\n fi\n}\nAGENTTEAMS_NON_INTERACTIVE=\"${AGENTTEAMS_NON_INTERACTIVE:-0}\"\nAGENTTEAMS_MOUNT_SOCKET=\"${AGENTTEAMS_MOUNT_SOCKET:-1}\"\nAGENTTEAMS_DOCKER_PROXY=\"${AGENTTEAMS_DOCKER_PROXY:-1}\"\nSTEP_RESULT=\"\" # Used by state machine to signal \"back\" navigation\n\n# ============================================================\n# Log all output to file\n# ============================================================\n\nAGENTTEAMS_LOG_FILE=\"${HOME}/agentteams-install.log\"\n\nif [ \"${1:-}\" != \"uninstall\" ]; then\n # Redirect all output (stdout and stderr) to both terminal and log file\n exec > >(tee -a \"${AGENTTEAMS_LOG_FILE}\") 2>&1\n\n echo \"\"\n echo \"========================================\"\n echo \"AgentTeams Installation Log\"\n echo \"Started: $(date)\"\n echo \"User: $(whoami)\"\n echo \"System: $(uname -a)\"\n echo \"Log file: ${AGENTTEAMS_LOG_FILE}\"\n echo \"========================================\"\n echo \"\"\nfi\n\n# ============================================================\n# Utility functions (needed early for timezone detection)\n# ============================================================\n\nlog() {\n echo -e \"\\033[36m[AgentTeams]\\033[0m $1\"\n}\n\nerror() {\n echo -e \"\\033[31m[AgentTeams ERROR]\\033[0m $1\" >&2\n}\n\ndie() {\n error \"$1\"\n exit 1\n}\n\n# ============================================================\n# Timezone detection (compatible with Linux and macOS)\n# ============================================================\n\ndetect_timezone() {\n local tz=\"\"\n\n # Try /etc/timezone (Debian/Ubuntu)\n if [ -f /etc/timezone ]; then\n tz=$(cat /etc/timezone 2>/dev/null | tr -d '[:space:]')\n fi\n\n # Try /etc/localtime symlink (macOS and some Linux)\n if [ -z \"${tz}\" ] && [ -L /etc/localtime ]; then\n tz=$(ls -l /etc/localtime 2>/dev/null | sed 's|.*/zoneinfo/||')\n fi\n\n # Try timedatectl (systemd)\n if [ -z \"${tz}\" ]; then\n tz=$(timedatectl show --value -p Timezone 2>/dev/null)\n fi\n\n # If still not detected, warn and prompt user. Diagnostics go to stderr —\n # this function returns its value via stdout to `$(detect_timezone)`, so\n # any echo here would leak into AGENTTEAMS_TIMEZONE and downstream image tags.\n if [ -z \"${tz}\" ]; then\n echo \"\" >&2\n echo -e \"\\033[33m[AgentTeams WARNING]\\033[0m Could not detect timezone automatically.\" >&2\n echo -e \"\\033[33m[AgentTeams]\\033[0m Please enter your timezone (e.g., Asia/Shanghai, America/New_York).\" >&2\n echo \"\" >&2\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n tz=\"Asia/Shanghai\"\n log \"Using default timezone: ${tz}\" >&2\n else\n read -e -p \"Timezone [Asia/Shanghai]: \" tz\n tz=\"${tz:-Asia/Shanghai}\"\n fi\n fi\n\n echo \"${tz}\"\n}\n\n# Detect timezone once at startup (used by registry selection and container TZ)\nAGENTTEAMS_TIMEZONE=\"${AGENTTEAMS_TIMEZONE:-$(detect_timezone)}\"\n\n# ============================================================\n# Language detection based on timezone\n# ============================================================\n\ndetect_language() {\n local tz=\"${AGENTTEAMS_TIMEZONE}\"\n case \"${tz}\" in\n Asia/Shanghai|Asia/Chongqing|Asia/Harbin|Asia/Urumqi|\\\n Asia/Taipei|Asia/Hong_Kong|Asia/Macau)\n echo \"zh\"\n ;;\n *)\n echo \"en\"\n ;;\n esac\n}\n\n# Language priority: env var > existing env file > timezone detection\nif [ -z \"${AGENTTEAMS_LANGUAGE}\" ]; then\n # Check existing env file for saved language preference (upgrade scenario)\n _env_file=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n # Migrate from legacy location (current directory) if needed\n if [ ! -f \"${_env_file}\" ] && [ -f \"./agentteams-manager.env\" ]; then\n mv \"./agentteams-manager.env\" \"${_env_file}\" 2>/dev/null || true\n fi\n if [ -f \"${_env_file}\" ]; then\n _saved_lang=$(grep '^AGENTTEAMS_LANGUAGE=' \"${_env_file}\" 2>/dev/null | cut -d= -f2-)\n if [ -n \"${_saved_lang}\" ]; then\n AGENTTEAMS_LANGUAGE=\"${_saved_lang}\"\n fi\n fi\n # Fall back to timezone-based detection\n if [ -z \"${AGENTTEAMS_LANGUAGE}\" ]; then\n AGENTTEAMS_LANGUAGE=\"$(detect_language)\"\n fi\n unset _env_file _saved_lang\nfi\nexport AGENTTEAMS_LANGUAGE\n\n# ============================================================\n# Centralized message dictionary and msg() function\n# Compatible with bash 3.2+ (macOS default) — uses case instead of declare -A\n# ============================================================\n\n# msg() function: look up message by key, with printf-style argument substitution\n# Falls back to English if the current language translation is missing.\nmsg() {\n local key=\"$1\"\n shift\n local lang=\"${AGENTTEAMS_LANGUAGE:-en}\"\n local text=\"\"\n case \"${key}.${lang}\" in\n # --- Timezone detection messages ---\n \"tz.warning.title.zh\") text=\"无法自动检测时区。\" ;;\n \"tz.warning.title.en\") text=\"Could not detect timezone automatically.\" ;;\n \"tz.warning.prompt.zh\") text=\"请输入您的时区(例如 Asia/Shanghai、America/New_York)。\" ;;\n \"tz.warning.prompt.en\") text=\"Please enter your timezone (e.g., Asia/Shanghai, America/New_York).\" ;;\n \"tz.default.zh\") text=\"使用默认时区: %s\" ;;\n \"tz.default.en\") text=\"Using default timezone: %s\" ;;\n \"tz.input_prompt.zh\") text=\"时区\" ;;\n \"tz.input_prompt.en\") text=\"Timezone\" ;;\n # --- Installation title and info ---\n \"install.title.zh\") text=\"=== AgentTeams Manager 安装 ===\" ;;\n \"install.title.en\") text=\"=== AgentTeams Manager Installation ===\" ;;\n \"install.registry.zh\") text=\"镜像仓库: %s\" ;;\n \"install.registry.en\") text=\"Registry: %s\" ;;\n \"install.dir.zh\") text=\"安装目录: %s\" ;;\n \"install.dir.en\") text=\"Installation directory: %s\" ;;\n \"install.dir_hint.zh\") text=\" (env 文件 'agentteams-manager.env' 将保存到 HOME 目录。)\" ;;\n \"install.dir_hint.en\") text=\" (The env file 'agentteams-manager.env' will be saved to your HOME directory.)\" ;;\n \"install.dir_hint2.zh\") text=\" (请从您希望管理此安装的目录运行此脚本。)\" ;;\n \"install.dir_hint2.en\") text=\" (Run this script from the directory where you want to manage this installation.)\" ;;\n # --- Onboarding mode ---\n \"install.mode.title.zh\") text=\"--- Onboarding 模式 ---\" ;;\n \"install.mode.title.en\") text=\"--- Onboarding Mode ---\" ;;\n \"install.mode.choose.zh\") text=\"选择安装模式:\" ;;\n \"install.mode.choose.en\") text=\"Choose your installation mode:\" ;;\n \"install.mode.quickstart.zh\") text=\" 1) 快速开始 - 使用阿里云通义 Token 套餐快速安装(推荐)\" ;;\n \"install.mode.quickstart.en\") text=\" 1) Quick Start - Fast installation with Qwen Cloud (recommended)\" ;;\n \"install.mode.manual.zh\") text=\" 2) 手动配置 - 选择 LLM 提供商并自定义选项\" ;;\n \"install.mode.manual.en\") text=\" 2) Manual - Choose LLM provider and customize options\" ;;\n \"install.mode.prompt.zh\") text=\"请选择 [1/2]\" ;;\n \"install.mode.prompt.en\") text=\"Enter choice [1/2]\" ;;\n \"install.mode.quickstart_selected.zh\") text=\"已选择快速开始模式 - 使用阿里云通义 Token 套餐\" ;;\n \"install.mode.quickstart_selected.en\") text=\"Quick Start mode selected - using Qwen Cloud\" ;;\n \"install.mode.manual_selected.zh\") text=\"已选择手动配置模式 - 您将选择 LLM 提供商并自定义选项\" ;;\n \"install.mode.manual_selected.en\") text=\"Manual mode selected - you will choose LLM provider and customize options\" ;;\n \"install.mode.invalid.zh\") text=\"无效选择,默认使用快速开始模式\" ;;\n \"install.mode.invalid.en\") text=\"Invalid choice, defaulting to Quick Start mode\" ;;\n # --- Upgrade mode sub-menu ---\n \"upgrade.mode.prompt.zh\") text=\"请选择升级方式:\" ;;\n \"upgrade.mode.prompt.en\") text=\"Select upgrade mode:\" ;;\n \"upgrade.mode.keep_all.zh\") text=\" 1) 保留所有参数,一键升级(推荐)\" ;;\n \"upgrade.mode.keep_all.en\") text=\" 1) Keep all parameters, quick upgrade (recommended)\" ;;\n \"upgrade.mode.confirm_each.zh\") text=\" 2) 逐个确认参数(可修改)\" ;;\n \"upgrade.mode.confirm_each.en\") text=\" 2) Confirm each parameter (can modify)\" ;;\n \"upgrade.mode.back.zh\") text=\" 3) 返回\" ;;\n \"upgrade.mode.back.en\") text=\" 3) Back\" ;;\n # --- Version selection ---\n \"install.version.title.zh\") text=\"--- 版本选择 ---\" ;;\n \"install.version.title.en\") text=\"--- Version Selection ---\" ;;\n \"install.version.choose.zh\") text=\"选择要安装的版本:\" ;;\n \"install.version.choose.en\") text=\"Choose the version to install:\" ;;\n \"install.version.option_latest.zh\") text=\" 1) latest - 最新版(默认)\" ;;\n \"install.version.option_latest.en\") text=\" 1) latest - Latest build (default)\" ;;\n \"install.version.option_stable.zh\") text=\" 2) %s - 最新稳定版\" ;;\n \"install.version.option_stable.en\") text=\" 2) %s - Latest stable release\" ;;\n \"install.version.fetching.zh\") text=\"正在查询最新稳定版本...\" ;;\n \"install.version.fetching.en\") text=\"Fetching latest stable release...\" ;;\n \"install.version.fetch_failed.zh\") text=\"无法查询 GitHub,使用内置版本 %s\" ;;\n \"install.version.fetch_failed.en\") text=\"Could not reach GitHub, using built-in version %s\" ;;\n \"install.version.option_custom.zh\") text=\" 3) 自定义 - 手动输入版本号(如 v1.0.5)\" ;;\n \"install.version.option_custom.en\") text=\" 3) Custom - Enter a specific version (e.g. v1.0.5)\" ;;\n \"install.version.prompt.zh\") text=\"请选择 [1/2/3]\" ;;\n \"install.version.prompt.en\") text=\"Enter choice [1/2/3]\" ;;\n \"install.version.custom_prompt.zh\") text=\"请输入版本号\" ;;\n \"install.version.custom_prompt.en\") text=\"Enter version tag\" ;;\n \"install.version.selected_latest.zh\") text=\"已选择最新版 (latest)\" ;;\n \"install.version.selected_latest.en\") text=\"Selected latest version\" ;;\n \"install.version.selected_stable.zh\") text=\"已选择最新稳定版 (%s)\" ;;\n \"install.version.selected_stable.en\") text=\"Selected latest stable version (%s)\" ;;\n \"install.version.selected_custom.zh\") text=\"已选择自定义版本 (%s)\" ;;\n \"install.version.selected_custom.en\") text=\"Selected custom version (%s)\" ;;\n \"install.version.invalid.zh\") text=\"无效选择,使用最新稳定版 (%s)\" ;;\n \"install.version.invalid.en\") text=\"Invalid choice, defaulting to latest stable version (%s)\" ;;\n # --- Existing installation detected ---\n \"install.existing.detected.zh\") text=\"检测到已有 Manager 安装(env 文件: %s)\" ;;\n \"install.existing.detected.en\") text=\"Existing Manager installation detected (env file: %s)\" ;;\n \"install.existing.choose.zh\") text=\"选择操作:\" ;;\n \"install.existing.choose.en\") text=\"Choose an action:\" ;;\n \"install.existing.upgrade.zh\") text=\" 1) 就地升级(保留数据、工作空间、env 文件)\" ;;\n \"install.existing.upgrade.en\") text=\" 1) In-place upgrade (keep data, workspace, env file)\" ;;\n \"install.existing.reinstall.zh\") text=\" 2) 全新重装(删除所有数据,重新开始)\" ;;\n \"install.existing.reinstall.en\") text=\" 2) Clean reinstall (remove all data, start fresh)\" ;;\n \"install.existing.cancel.zh\") text=\" 3) 取消\" ;;\n \"install.existing.cancel.en\") text=\" 3) Cancel\" ;;\n \"install.existing.prompt.zh\") text=\"请选择 [1/2/3]\" ;;\n \"install.existing.prompt.en\") text=\"Enter choice [1/2/3]\" ;;\n \"install.existing.upgrade_noninteractive.zh\") text=\"非交互模式: 执行就地升级...\" ;;\n \"install.existing.upgrade_noninteractive.en\") text=\"Non-interactive mode: performing in-place upgrade...\" ;;\n \"install.existing.upgrading.zh\") text=\"执行就地升级...\" ;;\n \"install.existing.upgrading.en\") text=\"Performing in-place upgrade...\" ;;\n \"install.existing.warn_manager_stop.zh\") text=\"⚠️ Manager 容器将被停止并重新创建。\" ;;\n \"install.existing.warn_manager_stop.en\") text=\"⚠️ Manager container will be stopped and recreated.\" ;;\n \"install.existing.warn_worker_recreate.zh\") text=\"⚠️ Worker 容器也将被重新创建(以更新 Manager IP)。\" ;;\n \"install.existing.warn_worker_recreate.en\") text=\"⚠️ Worker containers will also be recreated (to update Manager IP in hosts).\" ;;\n \"install.existing.continue_prompt.zh\") text=\"继续?[y/N]\" ;;\n \"install.existing.continue_prompt.en\") text=\"Continue? [y/N]\" ;;\n \"install.existing.cancelled.zh\") text=\"安装已取消。\" ;;\n \"install.existing.cancelled.en\") text=\"Installation cancelled.\" ;;\n \"install.existing.stopping_manager.zh\") text=\"停止并移除现有 manager 容器...\" ;;\n \"install.existing.stopping_manager.en\") text=\"Stopping and removing existing manager container...\" ;;\n \"install.existing.stopping_workers.zh\") text=\"停止并移除现有 worker 容器...\" ;;\n \"install.existing.stopping_workers.en\") text=\"Stopping and removing existing worker containers...\" ;;\n \"install.existing.removed.zh\") text=\" 已移除: %s\" ;;\n \"install.existing.removed.en\") text=\" Removed: %s\" ;;\n # --- Clean reinstall messages ---\n \"install.reinstall.performing.zh\") text=\"执行全新重装...\" ;;\n \"install.reinstall.performing.en\") text=\"Performing clean reinstall...\" ;;\n \"install.reinstall.warn_stop.zh\") text=\"⚠️ 以下运行中的容器将被停止:\" ;;\n \"install.reinstall.warn_stop.en\") text=\"⚠️ The following running containers will be stopped:\" ;;\n \"install.reinstall.warn_delete.zh\") text=\"⚠️ 警告: 以下内容将被删除:\" ;;\n \"install.reinstall.warn_delete.en\") text=\"⚠️ WARNING: This will DELETE the following:\" ;;\n \"install.reinstall.warn_volume.zh\") text=\" - Docker 卷: agentteams-data\" ;;\n \"install.reinstall.warn_volume.en\") text=\" - Docker volume: agentteams-data\" ;;\n \"install.reinstall.warn_env.zh\") text=\" - Env 文件: %s\" ;;\n \"install.reinstall.warn_env.en\") text=\" - Env file: %s\" ;;\n \"install.reinstall.warn_workspace.zh\") text=\" - Manager 工作空间: %s\" ;;\n \"install.reinstall.warn_workspace.en\") text=\" - Manager workspace: %s\" ;;\n \"install.reinstall.warn_workers.zh\") text=\" - 所有 worker 容器\" ;;\n \"install.reinstall.warn_workers.en\") text=\" - All worker containers\" ;;\n \"install.reinstall.warn_proxy.zh\") text=\" - Docker API 代理容器: agentteams-controller\" ;;\n \"install.reinstall.warn_proxy.en\") text=\" - Docker API proxy container: agentteams-controller\" ;;\n \"install.reinstall.warn_network.zh\") text=\" - Docker 网络: agentteams-net\" ;;\n \"install.reinstall.warn_network.en\") text=\" - Docker network: agentteams-net\" ;;\n \"install.reinstall.confirm_type.zh\") text=\"请输入工作空间路径以确认删除(或按 Ctrl+C 取消):\" ;;\n \"install.reinstall.confirm_type.en\") text=\"To confirm deletion, please type the workspace path:\" ;;\n \"install.reinstall.confirm_path.zh\") text=\"输入路径以确认(或按 Ctrl+C 取消)\" ;;\n \"install.reinstall.confirm_path.en\") text=\"Type the path to confirm (or press Ctrl+C to cancel)\" ;;\n \"install.reinstall.path_mismatch.zh\") text=\"路径不匹配。中止重装。输入: '%s',期望: '%s'\" ;;\n \"install.reinstall.path_mismatch.en\") text=\"Path mismatch. Aborting reinstall. Input: '%s', Expected: '%s'\" ;;\n \"install.reinstall.confirmed.zh\") text=\"已确认。正在清理...\" ;;\n \"install.reinstall.confirmed.en\") text=\"Confirmed. Cleaning up...\" ;;\n \"install.reinstall.removed_worker.zh\") text=\" 已移除 worker: %s\" ;;\n \"install.reinstall.removed_worker.en\") text=\" Removed worker: %s\" ;;\n \"install.reinstall.removing_volume.zh\") text=\"正在移除 Docker 卷: agentteams-data\" ;;\n \"install.reinstall.removing_volume.en\") text=\"Removing Docker volume: agentteams-data\" ;;\n \"install.reinstall.warn_volume_fail.zh\") text=\" 警告: 无法移除卷(可能有引用)\" ;;\n \"install.reinstall.warn_volume_fail.en\") text=\" Warning: Could not remove volume (may have references)\" ;;\n \"install.reinstall.removing_proxy.zh\") text=\"正在移除 Docker API 代理容器: agentteams-controller\" ;;\n \"install.reinstall.removing_proxy.en\") text=\"Removing Docker API proxy container: agentteams-controller\" ;;\n \"install.reinstall.removing_network.zh\") text=\"正在移除 Docker 网络: agentteams-net\" ;;\n \"install.reinstall.removing_network.en\") text=\"Removing Docker network: agentteams-net\" ;;\n \"install.reinstall.removing_workspace.zh\") text=\"正在移除工作空间目录: %s\" ;;\n \"install.reinstall.removing_workspace.en\") text=\"Removing workspace directory: %s\" ;;\n \"install.reinstall.removing_env.zh\") text=\"正在移除 env 文件: %s\" ;;\n \"install.reinstall.removing_env.en\") text=\"Removing env file: %s\" ;;\n \"install.reinstall.cleanup_done.zh\") text=\"清理完成。开始全新安装...\" ;;\n \"install.reinstall.cleanup_done.en\") text=\"Cleanup complete. Starting fresh installation...\" ;;\n \"install.reinstall.failed_rm_workspace.zh\") text=\"无法移除工作空间目录\" ;;\n \"install.reinstall.failed_rm_workspace.en\") text=\"Failed to remove workspace directory\" ;;\n # --- Orphan volume detection ---\n \"install.orphan_volume.detected.zh\") text=\"⚠️ 检测到残留数据卷 '%s',但未找到对应的 env 配置文件。\" ;;\n \"install.orphan_volume.detected.en\") text=\"⚠️ Found leftover data volume '%s' but no matching env config file.\" ;;\n \"install.orphan_volume.warn.zh\") text=\"这可能是之前安装的残留数据,会导致新安装出现异常(如密码冲突、服务启动失败)。\" ;;\n \"install.orphan_volume.warn.en\") text=\"This is likely leftover data from a previous installation and may cause issues (e.g., credential conflicts, service startup failures).\" ;;\n \"install.orphan_volume.choose.zh\") text=\"选择操作:\" ;;\n \"install.orphan_volume.choose.en\") text=\"Choose an action:\" ;;\n \"install.orphan_volume.clean.zh\") text=\" 1) 清理残留数据卷后继续安装(推荐)\" ;;\n \"install.orphan_volume.clean.en\") text=\" 1) Remove leftover volume and continue installation (recommended)\" ;;\n \"install.orphan_volume.keep.zh\") text=\" 2) 保留数据卷继续安装(可能出现异常)\" ;;\n \"install.orphan_volume.keep.en\") text=\" 2) Keep the volume and continue installation (may cause issues)\" ;;\n \"install.orphan_volume.prompt.zh\") text=\"请选择 [1/2]\" ;;\n \"install.orphan_volume.prompt.en\") text=\"Enter choice [1/2]\" ;;\n \"install.orphan_volume.cleaning.zh\") text=\"正在清理残留数据卷...\" ;;\n \"install.orphan_volume.cleaning.en\") text=\"Removing leftover data volume...\" ;;\n \"install.orphan_volume.cleaned.zh\") text=\"残留数据卷已清理。继续全新安装...\" ;;\n \"install.orphan_volume.cleaned.en\") text=\"Leftover volume removed. Continuing with fresh installation...\" ;;\n \"install.orphan_volume.keeping.zh\") text=\"保留数据卷,继续安装。如遇异常请选择全新重装。\" ;;\n \"install.orphan_volume.keeping.en\") text=\"Keeping existing volume. If you encounter issues, consider a clean reinstall.\" ;;\n \"install.orphan_volume.clean_noninteractive.zh\") text=\"非交互模式: 自动清理残留数据卷...\" ;;\n \"install.orphan_volume.clean_noninteractive.en\") text=\"Non-interactive mode: automatically removing leftover volume...\" ;;\n # --- Loading existing config ---\n \"install.loading_config.zh\") text=\"从 %s 加载已有配置(shell 环境变量优先)...\" ;;\n \"install.loading_config.en\") text=\"Loading existing config from %s (shell env vars take priority)...\" ;;\n # --- LLM Configuration ---\n \"llm.title.zh\") text=\"--- LLM 配置 ---\" ;;\n \"llm.title.en\") text=\"--- LLM Configuration ---\" ;;\n \"llm.provider.label.zh\") text=\" 提供商: %s\" ;;\n \"llm.provider.label.en\") text=\" Provider: %s\" ;;\n \"llm.model.label.zh\") text=\" 模型: %s\" ;;\n \"llm.model.label.en\") text=\" Model: %s\" ;;\n \"llm.provider.qwen.zh\") text=\" 提供商: qwen(阿里云百炼)\" ;;\n \"llm.provider.qwen.en\") text=\" Provider: qwen (Alibaba Cloud Bailian)\" ;;\n \"llm.provider.qwen_default.zh\") text=\" 提供商: %s(默认)\" ;;\n \"llm.provider.qwen_default.en\") text=\" Provider: %s (default)\" ;;\n \"llm.model.default.zh\") text=\" 模型: %s(默认)\" ;;\n \"llm.model.default.en\") text=\" Model: %s (default)\" ;;\n \"llm.apikey_hint_bailian.zh\") text=\" 💡 获取阿里云百炼(DashScope)API Key:\" ;;\n \"llm.apikey_hint_bailian.en\") text=\" 💡 Get your Alibaba Cloud Bailian (DashScope) API Key:\" ;;\n \"llm.apikey_url_bailian.zh\") text=\" https://www.aliyun.com/product/bailian\" ;;\n \"llm.apikey_url_bailian.en\") text=\" https://www.aliyun.com/product/bailian\" ;;\n \"llm.apikey_hint_qwencloud.zh\") text=\" 💡 从 Qwen Cloud(国际站)获取 DASHSCOPE_API_KEY:\" ;;\n \"llm.apikey_hint_qwencloud.en\") text=\" 💡 Get your DASHSCOPE_API_KEY for Qwen Cloud (international) from:\" ;;\n \"llm.apikey_url_qwencloud.zh\") text=\" https://home.qwencloud.com/api-keys (文档: https://docs.qwencloud.com/)\" ;;\n \"llm.apikey_url_qwencloud.en\") text=\" https://home.qwencloud.com/api-keys | Docs: https://docs.qwencloud.com/\" ;;\n \"llm.apikey_hint_tokenplan.zh\") text=\" 💡 获取 DashScope API Key 或开通通义 Token 套餐,请参考:\" ;;\n \"llm.apikey_hint_tokenplan.en\") text=\" 💡 Get your DashScope or Token Plan API key (Alibaba Model Studio):\" ;;\n \"llm.apikey_url_tokenplan.zh\") text=\" https://help.aliyun.com/zh/model-studio/token-plan-quickstart\" ;;\n \"llm.apikey_url_tokenplan.en\") text=\" https://common-buy.aliyun.com/token-plan/ | https://help.aliyun.com/zh/model-studio/token-plan-quickstart\" ;;\n \"llm.apikey_hint_codingplan.zh\") text=\" 💡 获取 DashScope API Key(Coding 套餐 / coding.dashscope 接口):\" ;;\n \"llm.apikey_hint_codingplan.en\") text=\" 💡 Get your DashScope API key for Coding Plan (coding.dashscope endpoint):\" ;;\n \"llm.apikey_url_codingplan.zh\") text=\" https://help.aliyun.com/zh/model-studio/get-api-key\" ;;\n \"llm.apikey_url_codingplan.en\") text=\" https://help.aliyun.com/zh/model-studio/get-api-key\" ;;\n \"llm.apikey_prompt.zh\") text=\"LLM API Key\" ;;\n \"llm.apikey_prompt.en\") text=\"LLM API Key\" ;;\n \"llm.providers_title.zh\") text=\"可用 LLM 提供商:\" ;;\n \"llm.providers_title.en\") text=\"Available LLM Providers:\" ;;\n \"llm.provider.alibaba.zh\") text=\" 1) 阿里云通义 Token 套餐 - 推荐中国用户使用\" ;;\n \"llm.provider.alibaba.en\") text=\" 1) Qwen Cloud - International (OpenAI-compatible API, recommended)\" ;;\n \"llm.provider.openai_compat.zh\") text=\" 2) OpenAI 兼容 API - 自定义 Base URL(OpenAI、DeepSeek 等)\" ;;\n \"llm.provider.openai_compat.en\") text=\" 2) OpenAI-compatible API - Custom Base URL (OpenAI, DeepSeek, etc.)\" ;;\n \"llm.provider.select.zh\") text=\"选择提供商 [1/2]\" ;;\n \"llm.provider.select.en\") text=\"Select provider [1/2]\" ;;\n \"llm.alibaba.models_title.zh\") text=\"选择阿里云模型接入方式:\" ;;\n \"llm.alibaba.models_title.en\") text=\"Select Alibaba Cloud model access:\" ;;\n \"llm.alibaba.model.tokenplan.zh\") text=\" 1) 阿里云通义 Token 套餐 - 兼容模式(推荐)\" ;;\n \"llm.alibaba.model.tokenplan.en\") text=\" 1) Alibaba Cloud Token Plan - compatible-mode (recommended)\" ;;\n \"llm.alibaba.model.bailian.zh\") text=\" 2) 阿里云百炼 - DashScope 通用兼容接口\" ;;\n \"llm.alibaba.model.bailian.en\") text=\" 2) Alibaba Cloud Bailian - DashScope compatible mode\" ;;\n \"llm.alibaba.model.codingplan_legacy.zh\") text=\" 3) 阿里云 Coding 套餐 - 旧版端点(兼容保留)\" ;;\n \"llm.alibaba.model.codingplan_legacy.en\") text=\" 3) Alibaba Cloud Coding Plan - legacy endpoint (backward compatible)\" ;;\n \"llm.alibaba.model.select.zh\") text=\"选择接入方式 [1/2/3]\" ;;\n \"llm.alibaba.model.select.en\") text=\"Select access option [1/2/3]\" ;;\n \"llm.alibaba.model.invalid.zh\") text=\"无效选择: %s(请输入 1、2 或 3)\" ;;\n \"llm.alibaba.model.invalid.en\") text=\"Invalid choice: %s (please enter 1, 2, or 3)\" ;;\n \"llm.codingplan.models_title.zh\") text=\"选择通义 Token 套餐默认模型:\" ;;\n \"llm.codingplan.models_title.en\") text=\"Select Qwen Cloud default model:\" ;;\n \"llm.codingplan.model.qwen36plus.zh\") text=\" 1) qwen3.6-plus - 千问 3.6(推荐)\" ;;\n \"llm.codingplan.model.qwen36plus.en\") text=\" 1) qwen3.6-plus - Qwen 3.6 (recommended)\" ;;\n \"llm.codingplan.model.glm5.zh\") text=\" 2) glm-5 - 智谱 GLM-5(编程推荐)\" ;;\n \"llm.codingplan.model.glm5.en\") text=\" 2) glm-5 - Zhipu GLM-5 (recommended for coding)\" ;;\n \"llm.codingplan.model.kimi.zh\") text=\" 3) kimi-k2.5 - Moonshot Kimi K2.5\" ;;\n \"llm.codingplan.model.kimi.en\") text=\" 3) kimi-k2.5 - Moonshot Kimi K2.5\" ;;\n \"llm.codingplan.model.minimax.zh\") text=\" 4) MiniMax-M2.5 - MiniMax M2.5\" ;;\n \"llm.codingplan.model.minimax.en\") text=\" 4) MiniMax-M2.5 - MiniMax M2.5\" ;;\n \"llm.codingplan.model.select.zh\") text=\"选择模型 [1/2/3/4]\" ;;\n \"llm.codingplan.model.select.en\") text=\"Select model [1/2/3/4]\" ;;\n \"llm.provider.selected_tokenplan.zh\") text=\" 提供商: 阿里云通义 Token 套餐(兼容模式)\" ;;\n \"llm.provider.selected_tokenplan.en\") text=\" Provider: Alibaba Cloud Token Plan (compatible mode)\" ;;\n \"llm.provider.selected_codingplan.zh\") text=\" 提供商: 阿里云通义 Token 套餐(alibaba-cloud)\" ;;\n \"llm.provider.selected_codingplan.en\") text=\" Provider: Qwen Cloud (international) (alibaba-cloud)\" ;;\n \"llm.provider.selected_codingplan_legacy.zh\") text=\" 提供商: 阿里云 Coding 套餐(coding.dashscope)\" ;;\n \"llm.provider.selected_codingplan_legacy.en\") text=\" Provider: Alibaba Cloud Coding Plan (coding.dashscope)\" ;;\n \"llm.provider.selected_qwen.zh\") text=\" 提供商: 阿里云百炼\" ;;\n \"llm.provider.selected_qwen.en\") text=\" Provider: Alibaba Cloud Bailian\" ;;\n \"llm.provider.selected_openai.zh\") text=\" 提供商: %s(OpenAI 兼容)\" ;;\n \"llm.provider.selected_openai.en\") text=\" Provider: %s (OpenAI-compatible)\" ;;\n \"llm.provider.invalid.zh\") text=\"无效选择: %s(请输入 1 或 2)\" ;;\n \"llm.provider.invalid.en\") text=\"Invalid choice: %s (please enter 1 or 2)\" ;;\n \"llm.qwen.model_prompt.zh\") text=\"默认模型 ID\" ;;\n \"llm.qwen.model_prompt.en\") text=\"Default Model ID\" ;;\n \"llm.openai.base_url_prompt.zh\") text=\"Base URL\" ;;\n \"llm.openai.base_url_prompt.en\") text=\"Base URL\" ;;\n \"llm.openai.model_prompt.zh\") text=\"默认模型 ID\" ;;\n \"llm.openai.model_prompt.en\") text=\"Default Model ID\" ;;\n \"llm.openai.base_url_label.zh\") text=\" Base URL: %s\" ;;\n \"llm.openai.base_url_label.en\") text=\" Base URL: %s\" ;;\n # --- Custom model parameters ---\n \"llm.custom_model.detected.zh\") text=\" ⚠️ 模型 '%s' 不在内置模型列表中,请配置模型参数:\" ;;\n \"llm.custom_model.detected.en\") text=\" ⚠️ Model '%s' is not in the built-in model list. Please configure model parameters:\" ;;\n \"llm.custom_model.context_prompt.zh\") text=\"最大上下文长度(token 数)[150000]\" ;;\n \"llm.custom_model.context_prompt.en\") text=\"Max context window (tokens) [150000]\" ;;\n \"llm.custom_model.max_tokens_prompt.zh\") text=\"最大输出长度(token 数)[128000]\" ;;\n \"llm.custom_model.max_tokens_prompt.en\") text=\"Max output tokens [128000]\" ;;\n \"llm.custom_model.reasoning_prompt.zh\") text=\"是否支持推理/思考模式?[Y/n]\" ;;\n \"llm.custom_model.reasoning_prompt.en\") text=\"Does it support reasoning/thinking mode? [Y/n]\" ;;\n \"llm.custom_model.vision_prompt.zh\") text=\"是否支持图片输入?[y/N]\" ;;\n \"llm.custom_model.vision_prompt.en\") text=\"Does it support image input? [y/N]\" ;;\n \"llm.custom_model.summary.zh\") text=\" 自定义模型参数: 上下文=%s, 最大输出=%s, 推理=%s, 图片=%s\" ;;\n \"llm.custom_model.summary.en\") text=\" Custom model params: context=%s, maxTokens=%s, reasoning=%s, vision=%s\" ;;\n # --- Admin Credentials ---\n \"admin.title.zh\") text=\"--- 管理员凭据 ---\" ;;\n \"admin.title.en\") text=\"--- Admin Credentials ---\" ;;\n \"admin.username_prompt.zh\") text=\"管理员用户名\" ;;\n \"admin.username_prompt.en\") text=\"Admin Username\" ;;\n \"admin.password_prompt.zh\") text=\"管理员密码(留空自动生成,最少 8 位)\" ;;\n \"admin.password_prompt.en\") text=\"Admin Password (leave empty to auto-generate, min 8 chars)\" ;;\n \"admin.password_generated.zh\") text=\" 已自动生成管理员密码\" ;;\n \"admin.password_generated.en\") text=\" Auto-generated admin password\" ;;\n \"admin.password_too_short.zh\") text=\"管理员密码至少需要 8 个字符(MinIO 要求)。当前长度: %s\" ;;\n \"admin.password_too_short.en\") text=\"Admin password must be at least 8 characters (MinIO requirement). Current length: %s\" ;;\n # --- Port Configuration ---\n \"port.title.zh\") text=\"--- 端口配置(按回车使用默认值)---\" ;;\n \"port.title.en\") text=\"--- Port Configuration (press Enter for defaults) ---\" ;;\n \"port.gateway_prompt.zh\") text=\"网关主机端口(容器内 8080)\" ;;\n \"port.gateway_prompt.en\") text=\"Host port for gateway (8080 inside container)\" ;;\n \"port.console_prompt.zh\") text=\"Higress 控制台主机端口(容器内 8001)\" ;;\n \"port.console_prompt.en\") text=\"Host port for Higress console (8001 inside container)\" ;;\n \"port.element_prompt.zh\") text=\"Element Web 直接访问主机端口(容器内 8088)\" ;;\n \"port.element_prompt.en\") text=\"Host port for Element Web direct access (8088 inside container)\" ;;\n \"port.manager_console_prompt.zh\") text=\"Manager 控制台主机端口(容器内 18888)\" ;;\n \"port.manager_console_prompt.en\") text=\"Host port for Manager console (18888 inside container)\" ;;\n \"port.copaw_app_prompt.zh\") text=\"QwenPaw App API 主机端口(容器内 18799)\" ;;\n \"port.copaw_app_prompt.en\") text=\"Host port for QwenPaw App API (18799 inside container)\" ;;\n # --- Local-only binding ---\n \"port.local_only.title.zh\") text=\"--- 网络访问模式 ---\" ;;\n \"port.local_only.title.en\") text=\"--- Network Access Mode ---\" ;;\n \"port.local_only.prompt.zh\") text=\"是否仅允许本机访问(端口绑定到 127.0.0.1)?\" ;;\n \"port.local_only.prompt.en\") text=\"Bind ports to localhost only (127.0.0.1)?\" ;;\n \"port.local_only.hint_yes.zh\") text=\" 仅本机使用,无需开放外部端口(推荐)\" ;;\n \"port.local_only.hint_yes.en\") text=\" Local use only, no external port exposure (recommended)\" ;;\n \"port.local_only.hint_no.zh\") text=\" 允许外部访问(局域网 / 公网)\" ;;\n \"port.local_only.hint_no.en\") text=\" Allow external access (LAN / public network)\" ;;\n \"port.local_only.choice.zh\") text=\"请选择 [1/2]\" ;;\n \"port.local_only.choice.en\") text=\"Enter choice [1/2]\" ;;\n \"port.local_only.selected_local.zh\") text=\"端口已绑定到 127.0.0.1(仅本机访问)\" ;;\n \"port.local_only.selected_local.en\") text=\"Ports bound to 127.0.0.1 (localhost only)\" ;;\n \"port.local_only.selected_external.zh\") text=\"端口已绑定到所有网络接口(0.0.0.0)\" ;;\n \"port.local_only.selected_external.en\") text=\"Ports bound to all interfaces (0.0.0.0)\" ;;\n \"port.local_only.https_hint.zh\") text=\"⚠️ 建议在 Higress 控制台配置 TLS 证书并启用 HTTPS,避免明文传输。\" ;;\n \"port.local_only.https_hint.en\") text=\"⚠️ It is recommended to configure TLS certificates and enable HTTPS in the Higress Console to avoid plaintext transmission.\" ;;\n \"port.local_only.https_docs.zh\") text=\"\" ;;\n \"port.local_only.https_docs.en\") text=\"\" ;;\n # --- Domain Configuration ---\n \"domain.title.zh\") text=\"--- 域名配置(按回车使用默认值)---\" ;;\n \"domain.title.en\") text=\"--- Domain Configuration (press Enter for defaults) ---\" ;;\n \"domain.hint.zh\") text=\"提示: 自定义域名前必须事先做好 DNS 解析。单机 ECS 部署时无需修改 aigw、fs 等域名;Element Web 和 Matrix Server 也可通过 IP 直接访问。\" ;;\n \"domain.hint.en\") text=\"Hint: Configure DNS resolution before customizing domains. For single ECS deployment, no need to change aigw, fs, etc.; Element Web and Matrix Server can also be accessed directly via IP.\" ;;\n \"domain.matrix_prompt.zh\") text=\"Matrix 域名\" ;;\n \"domain.matrix_prompt.en\") text=\"Matrix Domain\" ;;\n \"domain.element_prompt.zh\") text=\"Element Web 域名\" ;;\n \"domain.element_prompt.en\") text=\"Element Web Domain\" ;;\n \"domain.gateway_prompt.zh\") text=\"AI 网关域名\" ;;\n \"domain.gateway_prompt.en\") text=\"AI Gateway Domain\" ;;\n \"domain.fs_prompt.zh\") text=\"文件系统域名\" ;;\n \"domain.fs_prompt.en\") text=\"File System Domain\" ;;\n \"domain.console_prompt.zh\") text=\"Manager 控制台域名\" ;;\n \"domain.console_prompt.en\") text=\"Manager Console Domain\" ;;\n # --- GitHub Integration ---\n \"github.title.zh\") text=\"--- GitHub 集成(可选,按回车跳过)---\" ;;\n \"github.title.en\") text=\"--- GitHub Integration (optional, press Enter to skip) ---\" ;;\n \"github.token_prompt.zh\") text=\"GitHub 个人访问令牌(可选)\" ;;\n \"github.token_prompt.en\") text=\"GitHub Personal Access Token (optional)\" ;;\n # --- Skills Registry ---\n \"skills.title.zh\") text=\"--- Skills 注册中心(可选,按回车使用默认 nacos://market.agentteams.io:80/public)---\" ;;\n \"skills.title.en\") text=\"--- Skills Registry (optional, press Enter for default nacos://market.agentteams.io:80/public) ---\" ;;\n \"skills.url_prompt.zh\") text=\"Skills 注册中心 URL(留空使用默认 nacos://market.agentteams.io:80/public)\" ;;\n \"skills.url_prompt.en\") text=\"Skills Registry URL (leave empty for default nacos://market.agentteams.io:80/public)\" ;;\n # --- Data Persistence ---\n \"data.title.zh\") text=\"--- 数据持久化 ---\" ;;\n \"data.title.en\") text=\"--- Data Persistence ---\" ;;\n \"data.volume_prompt.zh\") text=\"Docker 卷名称 [agentteams-data]\" ;;\n \"data.volume_prompt.en\") text=\"Docker volume name for persistent data [agentteams-data]\" ;;\n \"data.volume_using.zh\") text=\" 使用 Docker 卷: %s\" ;;\n \"data.volume_using.en\") text=\" Using Docker volume: %s\" ;;\n # --- Manager Workspace ---\n \"workspace.title.zh\") text=\"--- Manager 工作空间 ---\" ;;\n \"workspace.title.en\") text=\"--- Manager Workspace ---\" ;;\n \"workspace.dir_prompt.zh\") text=\"Manager 工作空间目录 [%s]\" ;;\n \"workspace.dir_prompt.en\") text=\"Manager workspace directory [%s]\" ;;\n \"workspace.dir_label.zh\") text=\" Manager 工作空间: %s\" ;;\n \"workspace.dir_label.en\") text=\" Manager workspace: %s\" ;;\n # --- Host directory sharing ---\n \"host_share.prompt.zh\") text=\"与 Agent 共享的主机目录(默认: %s)\" ;;\n \"host_share.prompt.en\") text=\"Host directory to share with agents (default: %s)\" ;;\n \"host_share.sharing.zh\") text=\"共享主机目录: %s -> 容器内 /host-share\" ;;\n \"host_share.sharing.en\") text=\"Sharing host directory: %s -> /host-share in container\" ;;\n \"host_share.not_exist.zh\") text=\"警告: 主机目录 %s 不存在,跳过验证继续使用\" ;;\n \"host_share.not_exist.en\") text=\"WARNING: Host directory %s does not exist, using without validation\" ;;\n # --- Default worker runtime ---\n \"worker_runtime.title.zh\") text=\"--- 默认 Worker 运行时 ---\" ;;\n \"worker_runtime.title.en\") text=\"--- Default Worker Runtime ---\" ;;\n \"worker_runtime.openclaw.zh\") text=\"OpenClaw\" ;;\n \"worker_runtime.openclaw.en\") text=\"OpenClaw\" ;;\n \"worker_runtime.copaw.zh\") text=\"QwenPaw\" ;;\n \"worker_runtime.copaw.en\") text=\"QwenPaw\" ;;\n \"worker_runtime.hermes.zh\") text=\"Hermes\" ;;\n \"worker_runtime.hermes.en\") text=\"Hermes\" ;;\n \"worker_runtime.choice.zh\") text=\"请选择 [1/2/3]\" ;;\n \"worker_runtime.choice.en\") text=\"Enter choice [1/2/3]\" ;;\n \"worker_runtime.selected.zh\") text=\"默认 Worker 运行时: %s\" ;;\n \"worker_runtime.selected.en\") text=\"Default Worker runtime: %s\" ;;\n \"worker_runtime.title_short.zh\") text=\"默认 Worker 运行时\" ;;\n \"worker_runtime.title_short.en\") text=\"Default Worker Runtime\" ;;\n \"manager_runtime.title.zh\") text=\"--- Manager 运行时 ---\" ;;\n \"manager_runtime.title.en\") text=\"--- Manager Runtime ---\" ;;\n \"manager_runtime.openclaw.zh\") text=\"OpenClaw\" ;;\n \"manager_runtime.openclaw.en\") text=\"OpenClaw\" ;;\n \"manager_runtime.copaw.zh\") text=\"QwenPaw\" ;;\n \"manager_runtime.copaw.en\") text=\"QwenPaw\" ;;\n \"manager_runtime.choice.zh\") text=\"请选择 [1/2]\" ;;\n \"manager_runtime.choice.en\") text=\"Enter choice [1/2]\" ;;\n \"manager_runtime.selected.zh\") text=\"Manager 运行时: %s\" ;;\n \"manager_runtime.selected.en\") text=\"Manager runtime: %s\" ;;\n \"manager_runtime.title_short.zh\") text=\"Manager 运行时\" ;;\n \"manager_runtime.title_short.en\") text=\"Manager Runtime\" ;;\n # --- Secrets and config ---\n \"install.generating_secrets.zh\") text=\"正在生成密钥...\" ;;\n \"install.generating_secrets.en\") text=\"Generating secrets...\" ;;\n \"install.config_saved.zh\") text=\"配置已保存到 %s\" ;;\n \"install.config_saved.en\") text=\"Configuration saved to %s\" ;;\n # --- Container runtime socket ---\n \"install.socket_detected.zh\") text=\"容器运行时 socket: %s(已启用直接创建 Worker)\" ;;\n \"install.socket_detected.en\") text=\"Container runtime socket: %s (direct Worker creation enabled)\" ;;\n \"install.socket_not_found.zh\") text=\"未找到容器运行时 socket(Manager 无法直接创建 Worker 容器,需要你手动执行 docker 命令创建)\" ;;\n \"install.socket_not_found.en\") text=\"No container runtime socket found (Manager cannot create Worker containers directly, you will need to create them manually using docker commands)\" ;;\n \"install.socket_confirm.title.zh\") text=\"⚠️ 未检测到容器运行时 Socket\" ;;\n \"install.socket_confirm.title.en\") text=\"⚠️ Container Runtime Socket Not Detected\" ;;\n \"install.socket_confirm.message.zh\") text=\"未找到 Docker/Podman socket,Manager 将无法自动创建 Worker 容器。\\n你需要手动执行 docker run 命令来创建 Worker。\\n\\n是否继续安装?\" ;;\n \"install.socket_confirm.message.en\") text=\"Docker/Podman socket not found. Manager will not be able to create Worker containers automatically.\\nYou will need to manually run docker commands to create Workers.\\n\\nContinue installation?\" ;;\n \"install.socket_confirm.prompt.zh\") text=\"继续安装? [y/N]: \" ;;\n \"install.socket_confirm.prompt.en\") text=\"Continue? [y/N]: \" ;;\n \"install.socket_confirm.cancelled.zh\") text=\"安装已取消。如需启用 Worker 自动创建,请确保 Docker/Podman 正在运行,然后重新运行安装脚本。\" ;;\n \"install.socket_confirm.cancelled.en\") text=\"Installation cancelled. To enable automatic Worker creation, ensure Docker/Podman is running and re-run the installer.\" ;;\n # --- Container management ---\n \"install.removing_existing.zh\") text=\"正在移除现有 agentteams-manager 容器...\" ;;\n \"install.removing_existing.en\") text=\"Removing existing agentteams-manager container...\" ;;\n # --- Matrix E2EE ---\n \"matrix_e2ee.title.zh\") text=\"--- Matrix 端到端加密(E2EE)---\" ;;\n \"matrix_e2ee.title.en\") text=\"--- Matrix End-to-End Encryption (E2EE) ---\" ;;\n \"matrix_e2ee.desc.zh\") text=\"E2EE 会对 Manager 与 Worker 之间的 Matrix 消息进行端到端加密。\\n 启用后,即使 Matrix 服务器被入侵,消息内容也无法被窃取。\\n 但 E2EE 会增加首次握手耗时,且要求所有 Agent 都支持 matrix-sdk-crypto。\\n 如果不确定,建议保持禁用。\\n ⚠ 注意:禁用 E2EE 后,请勿在 Element 上创建默认启用加密的 Private 房间,\\n 否则 Agent 将无法读取该房间中的加密消息。请改用 Public 房间或关闭房间加密。\" ;;\n \"matrix_e2ee.desc.en\") text=\"E2EE encrypts Matrix messages between Manager and Workers end-to-end.\\n When enabled, message content stays private even if the Matrix server is compromised.\\n However, E2EE adds overhead to the initial handshake and requires all Agents\\n to support matrix-sdk-crypto. If unsure, keep it disabled.\\n ⚠ Note: When E2EE is disabled, do NOT create Private rooms in Element (which\\n enable encryption by default) — Agents cannot read encrypted messages without\\n E2EE support. Use Public rooms or turn off room encryption instead.\" ;;\n \"matrix_e2ee.enable.zh\") text=\"启用 E2EE\" ;;\n \"matrix_e2ee.enable.en\") text=\"Enable E2EE\" ;;\n \"matrix_e2ee.disable.zh\") text=\"禁用 E2EE(推荐)\" ;;\n \"matrix_e2ee.disable.en\") text=\"Disable E2EE (recommended)\" ;;\n \"matrix_e2ee.choice.zh\") text=\"请选择 [1/2]\" ;;\n \"matrix_e2ee.choice.en\") text=\"Enter choice [1/2]\" ;;\n \"matrix_e2ee.selected_enabled.zh\") text=\"Matrix E2EE: 已启用\" ;;\n \"matrix_e2ee.selected_enabled.en\") text=\"Matrix E2EE: enabled\" ;;\n \"matrix_e2ee.selected_disabled.zh\") text=\"Matrix E2EE: 已禁用(默认)\" ;;\n \"matrix_e2ee.selected_disabled.en\") text=\"Matrix E2EE: disabled (default)\" ;;\n \"matrix_e2ee.title_short.zh\") text=\"Matrix E2EE\" ;;\n \"matrix_e2ee.title_short.en\") text=\"Matrix E2EE\" ;;\n \"matrix_e2ee.val_enabled.zh\") text=\"已启用\" ;;\n \"matrix_e2ee.val_enabled.en\") text=\"enabled\" ;;\n \"matrix_e2ee.val_disabled.zh\") text=\"已禁用\" ;;\n \"matrix_e2ee.val_disabled.en\") text=\"disabled\" ;;\n # --- Docker API proxy ---\n \"docker_proxy.title.zh\") text=\"--- Docker API 安全代理 ---\" ;;\n \"docker_proxy.title.en\") text=\"--- Docker API Security Proxy ---\" ;;\n \"docker_proxy.desc.zh\") text=\"Docker API 代理可防止 AI Agent 通过 Docker API 越狱访问宿主机。\\n 启用后,Manager 不再直接持有 Docker socket,所有容器操作经过安全校验。\" ;;\n \"docker_proxy.desc.en\") text=\"Docker API proxy prevents AI Agents from escaping via Docker API to access the host.\\n When enabled, Manager no longer has direct Docker socket access; all container operations go through security validation.\" ;;\n \"docker_proxy.enable.zh\") text=\"启用(推荐)\" ;;\n \"docker_proxy.enable.en\") text=\"Enable (recommended)\" ;;\n \"docker_proxy.disable.zh\") text=\"禁用(直接挂载 Docker socket)\" ;;\n \"docker_proxy.disable.en\") text=\"Disable (mount Docker socket directly)\" ;;\n \"docker_proxy.choice.zh\") text=\"请选择 [1/2]\" ;;\n \"docker_proxy.choice.en\") text=\"Enter choice [1/2]\" ;;\n \"docker_proxy.selected_enabled.zh\") text=\"Docker API 代理: 已启用\" ;;\n \"docker_proxy.selected_enabled.en\") text=\"Docker API proxy: enabled\" ;;\n \"docker_proxy.selected_disabled.zh\") text=\"Docker API 代理: 已禁用\" ;;\n \"docker_proxy.selected_disabled.en\") text=\"Docker API proxy: disabled\" ;;\n \"docker_proxy.title_short.zh\") text=\"Docker API 代理\" ;;\n \"docker_proxy.title_short.en\") text=\"Docker API Proxy\" ;;\n \"docker_proxy.val_enabled.zh\") text=\"已启用\" ;;\n \"docker_proxy.val_enabled.en\") text=\"enabled\" ;;\n \"docker_proxy.val_disabled.zh\") text=\"已禁用\" ;;\n \"docker_proxy.val_disabled.en\") text=\"disabled\" ;;\n \"docker_proxy.registries_desc.zh\") text=\"默认放行的镜像来源:本地镜像、localhost、Higress 仓库(所有 region)。\\n 如需放行其他镜像仓库,请输入逗号分隔的地址前缀。\\n 示例: ghcr.io/myorg,registry.example.com/team\" ;;\n \"docker_proxy.registries_desc.en\") text=\"Default allowed image sources: local images, localhost, Higress registries (all regions).\\n To allow additional image sources, enter comma-separated address prefixes.\\n Example: ghcr.io/myorg,registry.example.com/team\" ;;\n \"docker_proxy.registries_prompt.zh\") text=\"额外放行的镜像来源(按回车跳过)\" ;;\n \"docker_proxy.registries_prompt.en\") text=\"Additional allowed image sources (press Enter to skip)\" ;;\n \"docker_proxy.registries_label.zh\") text=\"额外放行的镜像来源\" ;;\n \"docker_proxy.registries_label.en\") text=\"Additional allowed image sources\" ;;\n # --- Podman Autostart ---\n \"podman.autostart.title.zh\") text=\"--- Podman 驻留与开机自启 ---\" ;;\n \"podman.autostart.title.en\") text=\"--- Podman Linger & Autostart ---\" ;;\n \"podman.autostart.desc.zh\") text=\"配置为 systemd 服务,支持开机自动启动及后台驻留。\" ;;\n \"podman.autostart.desc.en\") text=\"Configure as a systemd service for automatic startup on boot.\" ;;\n \"podman.autostart.enable.zh\") text=\"启用(推荐)\" ;;\n \"podman.autostart.enable.en\") text=\"Enable (recommended)\" ;;\n \"podman.autostart.disable.zh\") text=\"不启用(容器随当前会话关闭而停止)\" ;;\n \"podman.autostart.disable.en\") text=\"Disable (containers stop when session ends)\" ;;\n \"podman.autostart.choice.zh\") text=\"请选择 [1/2]\" ;;\n \"podman.autostart.choice.en\") text=\"Enter choice [1/2]\" ;;\n \"podman.autostart.selected_enabled.zh\") text=\"Podman 开机自启: 已启用\" ;;\n \"podman.autostart.selected_enabled.en\") text=\"Podman Autostart: enabled\" ;;\n \"podman.autostart.selected_disabled.zh\") text=\"Podman 开机自启: 已禁用\" ;;\n \"podman.autostart.selected_disabled.en\") text=\"Podman Autostart: disabled\" ;;\n \"podman.autostart.title_short.zh\") text=\"Podman 开机自启\" ;;\n \"podman.autostart.title_short.en\") text=\"Podman Autostart\" ;;\n \"podman.autostart.val_enabled.zh\") text=\"已启用\" ;;\n \"podman.autostart.val_enabled.en\") text=\"enabled\" ;;\n \"podman.autostart.val_disabled.zh\") text=\"已禁用\" ;;\n \"podman.autostart.val_disabled.en\") text=\"disabled\" ;;\n \"install.podman.autostart_title.zh\") text=\"正在配置 Podman 容器开机自启...\" ;;\n \"install.podman.autostart_title.en\") text=\"Configuring Podman container autostart...\" ;;\n \"install.podman.linger_enable.zh\") text=\"尝试启用 systemd linger (如有提示请在此输入 sudo 密码)...\" ;;\n \"install.podman.linger_enable.en\") text=\"Attempting to enable systemd linger (enter sudo password if prompted)...\" ;;\n \"install.podman.linger_warn.zh\") text=\"警告: 无法自动启用 linger。驻留可能失败,请手动执行: sudo loginctl enable-linger \\$(whoami)\" ;;\n \"install.podman.linger_warn.en\") text=\"WARNING: Failed to auto-enable linger. Please run manually: sudo loginctl enable-linger \\$(whoami)\" ;;\n \"install.podman.root_setup.zh\") text=\"检测到 root 用户。正在配置系统级 agentteams-podman-restart 服务...\" ;;\n \"install.podman.root_setup.en\") text=\"Root user detected. Configuring system-wide agentteams-podman-restart service...\" ;;\n \"install.podman.root_success.zh\") text=\"✅ 系统级 agentteams-podman-restart 服务已成功启用并运行。\" ;;\n \"install.podman.root_success.en\") text=\"✅ System-wide agentteams-podman-restart service successfully enabled and running.\" ;;\n \"install.podman.root_fail.zh\") text=\"⚠️ 系统级 agentteams-podman-restart 服务启用失败,请稍后手动检查。\" ;;\n \"install.podman.root_fail.en\") text=\"⚠️ Failed to enable system-wide agentteams-podman-restart service. Please check manually later.\" ;;\n \"install.podman.user_setup.zh\") text=\"检测到普通用户 (%s)。正在配置用户级 agentteams-podman-restart 服务...\" ;;\n \"install.podman.user_setup.en\") text=\"Non-root user (%s) detected. Configuring user-level agentteams-podman-restart service...\" ;;\n \"install.podman.user_success.zh\") text=\"✅ 用户级 agentteams-podman-restart 服务已成功启用并运行。\" ;;\n \"install.podman.user_success.en\") text=\"✅ User-level agentteams-podman-restart service successfully enabled and running.\" ;;\n \"install.podman.user_fail.zh\") text=\"⚠️ 用户级 agentteams-podman-restart 服务启用失败。\\n提示:这通常是因为缺少 XDG_RUNTIME_DIR 或 dbus 没有运行。您可以尝试手动执行: systemctl --user enable --now agentteams-podman-restart.service\" ;;\n \"install.podman.user_fail.en\") text=\"⚠️ Failed to enable user-level agentteams-podman-restart service.\\nHint: This is usually due to missing XDG_RUNTIME_DIR or dbus not running. Try manually: systemctl --user enable --now agentteams-podman-restart.service\" ;;\n \"install.podman.success.zh\") text=\"Podman 开机自启配置完成。\" ;;\n \"install.podman.success.en\") text=\"Podman autostart successfully configured.\" ;;\n # --- Worker idle timeout ---\n \"idle_timeout.prompt.zh\") text=\"Worker 空闲自动停止超时(分钟)[720]\" ;;\n \"idle_timeout.prompt.en\") text=\"Worker idle auto-stop timeout in minutes [720]\" ;;\n \"idle_timeout.selected.zh\") text=\"Worker 空闲超时: %s 分钟\" ;;\n \"idle_timeout.selected.en\") text=\"Worker idle timeout: %s minutes\" ;;\n \"idle_timeout.label.zh\") text=\"Worker 空闲超时(分钟)\" ;;\n \"idle_timeout.label.en\") text=\"Worker idle timeout (min)\" ;;\n # --- YOLO mode ---\n \"install.yolo.zh\") text=\"YOLO 模式已启用(自主决策,无交互提示)\" ;;\n \"install.yolo.en\") text=\"YOLO mode enabled (autonomous decisions, no interactive prompts)\" ;;\n # --- Image pulling ---\n \"install.image.exists.zh\") text=\"Manager 镜像已存在: %s\" ;;\n \"install.image.exists.en\") text=\"Manager image already exists locally: %s\" ;;\n \"install.image.pulling_manager.zh\") text=\"正在拉取 Manager 镜像: %s\" ;;\n \"install.image.pulling_manager.en\") text=\"Pulling Manager image: %s\" ;;\n \"install.image.worker_exists.zh\") text=\"Worker 镜像已存在: %s\" ;;\n \"install.image.worker_exists.en\") text=\"Worker image already exists locally: %s\" ;;\n \"install.image.pulling_worker.zh\") text=\"正在拉取 Worker 镜像: %s\" ;;\n \"install.image.pulling_worker.en\") text=\"Pulling Worker image: %s\" ;;\n # --- Starting container ---\n \"install.starting_manager.zh\") text=\"正在启动 Manager 容器...\" ;;\n \"install.starting_manager.en\") text=\"Starting Manager container...\" ;;\n # --- Wait for Manager ready ---\n \"install.wait_ready.zh\") text=\"等待 Manager Agent 就绪(超时: %ss)...\" ;;\n \"install.wait_ready.en\") text=\"Waiting for Manager agent to be ready (timeout: %ss)...\" ;;\n \"install.wait_ready.ok.zh\") text=\"Manager Agent 已就绪!\" ;;\n \"install.wait_ready.ok.en\") text=\"Manager agent is ready!\" ;;\n \"install.wait_ready.waiting.zh\") text=\"等待中... (%ds/%ds)\" ;;\n \"install.wait_ready.waiting.en\") text=\"Waiting... (%ds/%ds)\" ;;\n \"install.wait_ready.timeout.zh\") text=\"Manager Agent 在 %ss 内未就绪。请检查: docker logs %s\" ;;\n \"install.wait_ready.timeout.en\") text=\"Manager agent did not become ready within %ss. Check: docker logs %s\" ;;\n # --- Wait for Matrix ready ---\n \"install.wait_matrix.zh\") text=\"等待 Matrix 服务就绪(超时: %ss)...\" ;;\n \"install.wait_matrix.en\") text=\"Waiting for Matrix server to be ready (timeout: %ss)...\" ;;\n \"install.wait_matrix.ok.zh\") text=\"Matrix 服务已就绪!\" ;;\n \"install.wait_matrix.ok.en\") text=\"Matrix server is ready!\" ;;\n \"install.wait_matrix.waiting.zh\") text=\"等待 Matrix 中... (%ds/%ds)\" ;;\n \"install.wait_matrix.waiting.en\") text=\"Waiting for Matrix... (%ds/%ds)\" ;;\n \"install.wait_matrix.timeout.zh\") text=\"Matrix 服务在 %ss 内未就绪。请检查: docker logs %s\" ;;\n \"install.wait_matrix.timeout.en\") text=\"Matrix server did not become ready within %ss. Check: docker logs %s\" ;;\n # --- OpenAI-compatible connectivity test ---\n \"llm.openai.test.testing.zh\") text=\"正在测试 API 联通性...\" ;;\n \"llm.openai.test.testing.en\") text=\"Testing API connectivity...\" ;;\n \"llm.openai.test.ok.zh\") text=\"✅ API 联通性测试通过\" ;;\n \"llm.openai.test.ok.en\") text=\"✅ API connectivity test passed\" ;;\n \"llm.openai.test.fail.zh\") text=\"⚠️ API 联通性测试失败(HTTP %s)。响应内容:\\n%s\\n请根据以上错误信息联系您的模型服务商解决。\" ;;\n \"llm.openai.test.fail.en\") text=\"⚠️ API connectivity test failed (HTTP %s). Response body:\\n%s\\nPlease contact your model provider to resolve the issue.\" ;;\n \"llm.openai.test.fail.tokenplan.zh\") text=\"⚠️ 提示: 请确认 API Key 有效且已开通通义 Token 套餐。文档: https://help.aliyun.com/zh/model-studio/token-plan-quickstart\" ;;\n \"llm.openai.test.fail.tokenplan.en\") text=\"⚠️ Hint: Verify your Token Plan API key and compatible-mode access. Docs: https://help.aliyun.com/zh/model-studio/token-plan-quickstart\" ;;\n \"llm.openai.test.fail.codingplan.zh\") text=\"⚠️ 提示: 请确认 API Key 有效且已开通通义 Token 套餐。文档: https://help.aliyun.com/zh/model-studio/token-plan-quickstart\" ;;\n \"llm.openai.test.fail.codingplan.en\") text=\"⚠️ Hint: Verify your DASHSCOPE_API_KEY for Qwen Cloud. API keys: https://home.qwencloud.com/api-keys Docs: https://docs.qwencloud.com/\" ;;\n \"llm.openai.test.fail.codingplan_legacy.zh\") text=\"⚠️ 提示: 请确认 API Key 有效且 Coding 套餐接口可用。文档: https://help.aliyun.com/zh/model-studio/get-api-key\" ;;\n \"llm.openai.test.fail.codingplan_legacy.en\") text=\"⚠️ Hint: Verify your DashScope API key and Coding Plan access. Docs: https://help.aliyun.com/zh/model-studio/get-api-key\" ;;\n \"llm.openai.test.no_curl.zh\") text=\"⚠️ 未找到 curl,跳过 API 联通性测试\" ;;\n \"llm.openai.test.no_curl.en\") text=\"⚠️ curl not found, skipping API connectivity test\" ;;\n \"llm.openai.test.confirm.zh\") text=\"是否仍要继续安装?[y/N/b] \" ;;\n \"llm.openai.test.confirm.en\") text=\"Continue with installation anyway? [y/N/b] \" ;;\n \"llm.embedding.title.zh\") text=\"📦 记忆搜索配置\" ;;\n \"llm.embedding.title.en\") text=\"📦 Memory Search Configuration\" ;;\n \"llm.embedding.hint.zh\") text=\" Embedding 模型可提升记忆搜索质量(语义匹配)。不启用也可正常使用记忆功能(关键词匹配)。\" ;;\n \"llm.embedding.hint.en\") text=\" Embedding model improves memory search quality (semantic matching). Memory still works without it (keyword matching).\" ;;\n \"llm.embedding.option.default.zh\") text=\" 1) text-embedding-v4(推荐)\" ;;\n \"llm.embedding.option.default.en\") text=\" 1) text-embedding-v4 (Recommended)\" ;;\n \"llm.embedding.option.custom.zh\") text=\" 2) 自定义 Embedding 模型\" ;;\n \"llm.embedding.option.custom.en\") text=\" 2) Custom embedding model\" ;;\n \"llm.embedding.option.disable.zh\") text=\" 3) 不启用\" ;;\n \"llm.embedding.option.disable.en\") text=\" 3) Do not enable\" ;;\n \"llm.embedding.select.zh\") text=\"选择\" ;;\n \"llm.embedding.select.en\") text=\"Select\" ;;\n \"llm.embedding.custom_prompt.zh\") text=\" Embedding 模型名称\" ;;\n \"llm.embedding.custom_prompt.en\") text=\" Embedding model name\" ;;\n \"llm.embedding.test.testing.zh\") text=\"正在测试 Embedding API 联通性...\" ;;\n \"llm.embedding.test.testing.en\") text=\"Testing Embedding API connectivity...\" ;;\n \"llm.embedding.test.ok.zh\") text=\"✅ Embedding API 联通性测试通过\" ;;\n \"llm.embedding.test.ok.en\") text=\"✅ Embedding API connectivity test passed\" ;;\n \"llm.embedding.test.fail.zh\") text=\"⚠️ Embedding API 测试失败(HTTP %s)。响应: %s\" ;;\n \"llm.embedding.test.fail.en\") text=\"⚠️ Embedding API test failed (HTTP %s). Response: %s\" ;;\n \"llm.embedding.auto_disabled.zh\") text=\"⚠️ Embedding 已自动禁用,记忆搜索将使用关键词匹配。您可以稍后在 agentteams-manager.env 中设置 AGENTTEAMS_EMBEDDING_MODEL 启用。\" ;;\n \"llm.embedding.auto_disabled.en\") text=\"⚠️ Embedding auto-disabled. Memory search will use keyword matching. You can enable it later in agentteams-manager.env by setting AGENTTEAMS_EMBEDDING_MODEL.\" ;;\n \"llm.embedding.disabled.zh\") text=\"ℹ️ Embedding 已禁用,记忆搜索将使用关键词匹配。\" ;;\n \"llm.embedding.disabled.en\") text=\"ℹ️ Embedding disabled. Memory search will use keyword matching.\" ;;\n \"llm.openai.test.aborted.zh\") text=\"安装已中止。\" ;;\n \"llm.openai.test.aborted.en\") text=\"Installation aborted.\" ;;\n \"nav.back_hint.zh\") text=\"(输入 b 返回上一步)\" ;;\n \"nav.back_hint.en\") text=\"(enter b to go back)\" ;;\n # --- OpenAI-compatible provider creation ---\n \"install.openai_compat.missing.zh\") text=\"警告: OpenAI Base URL 或 API Key 未设置,跳过提供商创建\" ;;\n \"install.openai_compat.missing.en\") text=\"WARNING: OpenAI Base URL or API Key not set, skipping provider creation\" ;;\n \"install.openai_compat.creating.zh\") text=\"正在创建 OpenAI 兼容提供商...\" ;;\n \"install.openai_compat.creating.en\") text=\"Creating OpenAI-compatible provider...\" ;;\n \"install.openai_compat.domain.zh\") text=\" 域名: %s\" ;;\n \"install.openai_compat.domain.en\") text=\" Domain: %s\" ;;\n \"install.openai_compat.port.zh\") text=\" 端口: %s\" ;;\n \"install.openai_compat.port.en\") text=\" Port: %s\" ;;\n \"install.openai_compat.protocol.zh\") text=\" 协议: %s\" ;;\n \"install.openai_compat.protocol.en\") text=\" Protocol: %s\" ;;\n \"install.openai_compat.service_fail.zh\") text=\"警告: 创建 DNS 服务源失败(可能已存在)\" ;;\n \"install.openai_compat.service_fail.en\") text=\"WARNING: Failed to create DNS service source (may already exist)\" ;;\n \"install.openai_compat.provider_fail.zh\") text=\"警告: 创建 AI 提供商失败(可能已存在)\" ;;\n \"install.openai_compat.provider_fail.en\") text=\"WARNING: Failed to create AI provider (may already exist)\" ;;\n \"install.openai_compat.success.zh\") text=\"OpenAI 兼容提供商创建成功\" ;;\n \"install.openai_compat.success.en\") text=\"OpenAI-compatible provider created successfully\" ;;\n # --- Welcome message ---\n \"install.welcome_msg.soul_configured.zh\") text=\"Soul 已配置(找到 soul-configured 标记),跳过 onboarding 消息\" ;;\n \"install.welcome_msg.soul_configured.en\") text=\"Soul already configured (soul-configured marker found), skipping onboarding message\" ;;\n \"install.welcome_msg.logging_in.zh\") text=\"正在以 %s 身份登录以发送欢迎消息...\" ;;\n \"install.welcome_msg.logging_in.en\") text=\"Logging in as %s to send welcome message...\" ;;\n \"install.welcome_msg.login_failed.zh\") text=\"警告: 以 %s 身份登录失败,跳过欢迎消息\" ;;\n \"install.welcome_msg.login_failed.en\") text=\"WARNING: Failed to login as %s, skipping welcome message\" ;;\n \"install.welcome_msg.finding_room.zh\") text=\"正在查找与 Manager 的 DM 房间...\" ;;\n \"install.welcome_msg.finding_room.en\") text=\"Finding DM room with Manager...\" ;;\n \"install.welcome_msg.creating_room.zh\") text=\"正在创建与 Manager 的 DM 房间...\" ;;\n \"install.welcome_msg.creating_room.en\") text=\"Creating DM room with Manager...\" ;;\n \"install.welcome_msg.no_room.zh\") text=\"警告: 无法找到或创建与 Manager 的 DM 房间\" ;;\n \"install.welcome_msg.no_room.en\") text=\"WARNING: Could not find or create DM room with Manager\" ;;\n \"install.welcome_msg.waiting_join.zh\") text=\"等待 Manager 加入房间...\" ;;\n \"install.welcome_msg.waiting_join.en\") text=\"Waiting for Manager to join the room...\" ;;\n \"install.welcome_msg.sending.zh\") text=\"正在向 Manager 发送欢迎消息...\" ;;\n \"install.welcome_msg.sending.en\") text=\"Sending welcome message to Manager...\" ;;\n \"install.welcome_msg.send_failed.zh\") text=\"警告: 发送欢迎消息失败\" ;;\n \"install.welcome_msg.send_failed.en\") text=\"WARNING: Failed to send welcome message\" ;;\n \"install.welcome_msg.sent.zh\") text=\"欢迎消息已发送给 Manager\" ;;\n \"install.welcome_msg.sent.en\") text=\"Welcome message sent to Manager\" ;;\n \"install.welcome_msg.waiting.zh\") text=\"等待 Manager 发送欢迎消息(Higress 路由授权 + LLM 探活,约 45-90s)...\" ;;\n \"install.welcome_msg.waiting.en\") text=\"Waiting for Manager to send the welcome message (Higress route auth + LLM probe, ~45-90s)...\" ;;\n \"install.welcome_msg.confirmed.zh\") text=\"Manager 已确认发送欢迎消息(status.welcomeSent=true,用时 %ss)\" ;;\n \"install.welcome_msg.confirmed.en\") text=\"Manager confirmed welcome message sent (status.welcomeSent=true, %ss elapsed)\" ;;\n \"install.welcome_msg.timeout.zh\") text=\"警告: 在 %ss 内未观察到 Manager 发送欢迎消息(status.welcomeSent=true)。安装仍然成功,所有服务已就绪——可继续按下方提示登录 Element Web。\" ;;\n \"install.welcome_msg.timeout.en\") text=\"WARNING: Did not observe the Manager sending its welcome message (status.welcomeSent=true) within %ss. Installation is still successful, all services are up — continue with the Element Web instructions below.\" ;;\n \"install.welcome_msg.timeout_hint.zh\") text=\"手动触发 onboarding: 登录 Element Web → 打开与 Manager 的 DM 房间 → 发送任意一句话(例如 \\\"hi\\\"),Manager 会接管对话并开始引导。\" ;;\n \"install.welcome_msg.timeout_hint.en\") text=\"Manual onboarding: log in to Element Web → open the DM with the Manager → send any message (e.g. \\\"hi\\\") and the Manager will take over and start the guided setup.\" ;;\n \"install.welcome_msg.timeout_inspect.zh\") text=\"排查命令: docker exec agentteams-controller agt get managers default\" ;;\n \"install.welcome_msg.timeout_inspect.en\") text=\"Inspect status: docker exec agentteams-controller agt get managers default\" ;;\n \"install.welcome_msg.poll_unavailable.zh\") text=\"提示: agentteams-manager 内未找到 AgentTeams CLI,跳过 welcome 等待(旧镜像?)\" ;;\n \"install.welcome_msg.poll_unavailable.en\") text=\"Note: AgentTeams CLI not found inside agentteams-manager; skipping welcome wait (old image?)\" ;;\n # --- Dashboard wizard ---\n \"dash.prompt.zh\") text=\"是否安装 agentteams-dashboard 管理面板?\" ;;\n \"dash.prompt.en\") text=\"Install agentteams-dashboard management UI?\" ;;\n \"dash.version_prompt.zh\") text=\"Dashboard 版本\" ;;\n \"dash.version_prompt.en\") text=\"Dashboard version\" ;;\n \"dash.port_prompt.zh\") text=\"Dashboard 端口号\" ;;\n \"dash.port_prompt.en\") text=\"Dashboard port\" ;;\n \"dash.image_prompt.zh\") text=\"Dashboard 镜像\" ;;\n \"dash.image_prompt.en\") text=\"Dashboard image\" ;;\n \"dash.gateway_prompt.zh\") text=\"Higress Console URL (用于共享登录)\" ;;\n \"dash.gateway_prompt.en\") text=\"Higress Console URL (for shared login)\" ;;\n \"dash.skip.zh\") text=\"跳过 Dashboard 安装\" ;;\n \"dash.skip.en\") text=\"Skipping Dashboard installation\" ;;\n \"dash.auto_url.zh\") text=\"自动检测到 Higress Console URL\" ;;\n \"dash.auto_url.en\") text=\"Auto-detected Higress Console URL\" ;;\n \"dash.auto_url_fail.zh\") text=\"无法自动检测 Higress Console URL,请手动输入\" ;;\n \"dash.auto_url_fail.en\") text=\"Cannot auto-detect Higress Console URL, please enter manually\" ;;\n \"dash.summary.zh\") text=\"Dashboard 配置: 端口=%s 镜像=%s\" ;;\n \"dash.summary.en\") text=\"Dashboard config: port=%s image=%s\" ;;\n # --- Dashboard success ---\n \"success.dashboard.zh\") text=\" agentteams-dashboard 管理面板: http://localhost:%s/\" ;;\n \"success.dashboard.en\") text=\" agentteams-dashboard: http://localhost:%s/\" ;;\n # --- Final output panel ---\n \"success.title.zh\") text=\"=== AgentTeams Manager 已启动!===\" ;;\n \"success.title.en\") text=\"=== AgentTeams Manager Started! ===\" ;;\n \"success.domains_configured.zh\") text=\"以下域名已配置解析到 127.0.0.1:\" ;;\n \"success.domains_configured.en\") text=\"The following domains are configured to resolve to 127.0.0.1:\" ;;\n \"success.open_url.zh\") text=\" ★ 在浏览器中打开以下 URL 开始使用: ★\" ;;\n \"success.open_url.en\") text=\" ★ Open the following URL in your browser to start: ★\" ;;\n \"success.login_with.zh\") text=\" 登录信息:\" ;;\n \"success.login_with.en\") text=\" Login with:\" ;;\n \"success.username.zh\") text=\" 用户名: %s\" ;;\n \"success.username.en\") text=\" Username: %s\" ;;\n \"success.password.zh\") text=\" 密码: %s\" ;;\n \"success.password.en\") text=\" Password: %s\" ;;\n \"success.after_login.zh\") text=\" 登录后,开始与 Manager 聊天!\" ;;\n \"success.after_login.en\") text=\" After login, start chatting with the Manager!\" ;;\n \"success.tell_it.zh\") text=\" 告诉它: \\\"创建一个名为 alice 的前端开发 Worker\\\"\" ;;\n \"success.tell_it.en\") text=\" Tell it: \\\"Create a Worker named alice for frontend dev\\\"\" ;;\n \"success.manager_auto.zh\") text=\" Manager 会自动处理一切。\" ;;\n \"success.manager_auto.en\") text=\" The Manager will handle everything automatically.\" ;;\n \"success.mobile_title.zh\") text=\" 📱 移动端访问(FluffyChat / Element Mobile):\" ;;\n \"success.mobile_title.en\") text=\" 📱 Mobile access (FluffyChat / Element Mobile):\" ;;\n \"success.mobile_step1.zh\") text=\" 1. 在手机上下载 FluffyChat 或 Element\" ;;\n \"success.mobile_step1.en\") text=\" 1. Download FluffyChat or Element on your phone\" ;;\n \"success.mobile_step2.zh\") text=\" 2. 设置 homeserver 为: %s\" ;;\n \"success.mobile_step2.en\") text=\" 2. Set homeserver to: %s\" ;;\n \"success.mobile_step2_noip.zh\") text=\" 2. 设置 homeserver 为: http://<本机局域网IP>:%s\" ;;\n \"success.mobile_step2_noip.en\") text=\" 2. Set homeserver to: http://<this-machine-LAN-IP>:%s\" ;;\n \"success.mobile_noip_hint.zh\") text=\" (无法自动检测局域网 IP — 请使用 ifconfig / ip addr 查看)\" ;;\n \"success.mobile_noip_hint.en\") text=\" (Could not detect LAN IP automatically — check with: ifconfig / ip addr)\" ;;\n \"success.mobile_step3.zh\") text=\" 3. 登录信息:\" ;;\n \"success.mobile_step3.en\") text=\" 3. Login with:\" ;;\n \"success.mobile_username.zh\") text=\" 用户名: %s\" ;;\n \"success.mobile_username.en\") text=\" Username: %s\" ;;\n \"success.mobile_password.zh\") text=\" 密码: %s\" ;;\n \"success.mobile_password.en\") text=\" Password: %s\" ;;\n # --- Other consoles and tips ---\n \"success.other_consoles.zh\") text=\"--- 其他控制台 ---\" ;;\n \"success.other_consoles.en\") text=\"--- Other Consoles ---\" ;;\n \"success.higress_console.zh\") text=\" Higress 控制台: http://localhost:%s(用户名: %s / 密码: %s)\" ;;\n \"success.higress_console.en\") text=\" Higress Console: http://localhost:%s (Username: %s / Password: %s)\" ;;\n \"success.manager_console.zh\") text=\" Manager 控制台(本地): http://localhost:%s(无需登录)\" ;;\n \"success.manager_console.en\") text=\" Manager Console (local): http://localhost:%s (no login required)\" ;;\n \"success.manager_console_gateway.zh\") text=\" Manager 控制台(网关): http://console-local.agentteams.io(用户名: %s / 密码: %s)\" ;;\n \"success.manager_console_gateway.en\") text=\" Manager Console (gateway): http://console-local.agentteams.io (Username: %s / Password: %s)\" ;;\n \"success.copaw_console.zh\") text=\" QwenPaw App API: http://localhost:%s(无需登录)\" ;;\n \"success.copaw_console.en\") text=\" QwenPaw App API: http://localhost:%s (no login required)\" ;;\n \"success.switch_llm.title.zh\") text=\"--- 切换 LLM 提供商 ---\" ;;\n \"success.switch_llm.title.en\") text=\"--- Switch LLM Providers ---\" ;;\n \"success.switch_llm.hint.zh\") text=\" 您可以通过 Higress 控制台切换到其他 LLM 提供商(OpenAI、Anthropic 等)。\" ;;\n \"success.switch_llm.hint.en\") text=\" You can switch to other LLM providers (OpenAI, Anthropic, etc.) via Higress Console.\" ;;\n \"success.switch_llm.docs.zh\") text=\" 详细说明请参阅:\" ;;\n \"success.switch_llm.docs.en\") text=\" For detailed instructions, see:\" ;;\n \"success.switch_llm.url.zh\") text=\" https://higress.ai/en/docs/ai/scene-guide/multi-proxy#console-configuration\" ;;\n \"success.switch_llm.url.en\") text=\" https://higress.ai/en/docs/ai/scene-guide/multi-proxy#console-configuration\" ;;\n \"success.tip.zh\") text=\"提示: 您也可以在聊天中让 Manager 为您配置 LLM 提供商。\" ;;\n \"success.tip.en\") text=\"Tip: You can also ask the Manager to configure LLM providers for you in the chat.\" ;;\n \"success.config_file.zh\") text=\"配置文件: %s\" ;;\n \"success.config_file.en\") text=\"Configuration file: %s\" ;;\n \"success.data_volume.zh\") text=\"数据卷: %s\" ;;\n \"success.data_volume.en\") text=\"Data volume: %s\" ;;\n \"success.workspace.zh\") text=\"Manager 工作空间: %s\" ;;\n \"success.workspace.en\") text=\"Manager workspace: %s\" ;;\n # --- Worker installation ---\n \"worker.resetting.zh\") text=\"正在重置 Worker: %s...\" ;;\n \"worker.resetting.en\") text=\"Resetting Worker: %s...\" ;;\n \"worker.exists.zh\") text=\"容器 '%s' 已存在。使用 --reset 重新创建。\" ;;\n \"worker.exists.en\") text=\"Container '%s' already exists. Use --reset to recreate.\" ;;\n \"worker.starting.zh\") text=\"正在启动 Worker: %s...\" ;;\n \"worker.starting.en\") text=\"Starting Worker: %s...\" ;;\n \"worker.skills_url.zh\") text=\" Skills API URL: %s\" ;;\n \"worker.skills_url.en\") text=\" Skills API URL: %s\" ;;\n \"worker.started.zh\") text=\"=== Worker %s 已启动!===\" ;;\n \"worker.started.en\") text=\"=== Worker %s Started! ===\" ;;\n \"worker.container.zh\") text=\"容器: %s\" ;;\n \"worker.container.en\") text=\"Container: %s\" ;;\n \"worker.view_logs.zh\") text=\"查看日志: docker logs -f %s\" ;;\n \"worker.view_logs.en\") text=\"View logs: docker logs -f %s\" ;;\n # --- Prompt function messages ---\n \"prompt.preset.zh\") text=\" %s = (已通过环境变量预设)\" ;;\n \"prompt.preset.en\") text=\" %s = (pre-set via env)\" ;;\n \"prompt.upgrade_keep.zh\") text=\" %s = %s(当前值,回车保留 / 输入新值覆盖)\" ;;\n \"prompt.upgrade_keep.en\") text=\" %s = %s (current value, press Enter to keep / type new value to change)\" ;;\n \"prompt.upgrade_keep_secret.zh\") text=\" %s = %s(当前值,回车保留 / 输入新值覆盖)\" ;;\n \"prompt.upgrade_keep_secret.en\") text=\" %s = %s (current value, press Enter to keep / type new value to change)\" ;;\n \"prompt.upgrade_empty.zh\") text=\" %s = (未设置,回车跳过 / 输入新值设置)\" ;;\n \"prompt.upgrade_empty.en\") text=\" %s = (not set, press Enter to skip / type new value to set)\" ;;\n \"prompt.default.zh\") text=\" %s = %s(默认)\" ;;\n \"prompt.default.en\") text=\" %s = %s (default)\" ;;\n \"prompt.required.zh\") text=\"%s 是必需的(在非交互模式下通过环境变量设置)\" ;;\n \"prompt.required.en\") text=\"%s is required (set via environment variable in non-interactive mode)\" ;;\n \"prompt.required_empty.zh\") text=\"%s 是必需的\" ;;\n \"prompt.required_empty.en\") text=\"%s is required\" ;;\n # --- Language switch prompt (bilingual by design) ---\n \"lang.detected.zh\") text=\"检测到语言 / Detected language: 中文\" ;;\n \"lang.detected.en\") text=\"检测到语言 / Detected language: English\" ;;\n \"lang.switch_title.zh\") text=\"切换语言 / Switch language:\" ;;\n \"lang.switch_title.en\") text=\"切换语言 / Switch language:\" ;;\n \"lang.option_zh.zh\") text=\" 1) 中文\" ;;\n \"lang.option_zh.en\") text=\" 1) 中文\" ;;\n \"lang.option_en.zh\") text=\" 2) English\" ;;\n \"lang.option_en.en\") text=\" 2) English\" ;;\n \"lang.prompt.zh\") text=\"请选择 / Enter choice\" ;;\n \"lang.prompt.en\") text=\"请选择 / Enter choice\" ;;\n # --- Uninstall messages ---\n \"uninstall.title.zh\") text=\"正在卸载 AgentTeams...\" ;;\n \"uninstall.title.en\") text=\"Uninstalling AgentTeams...\" ;;\n \"uninstall.stopping_manager.zh\") text=\"正在停止并移除 agentteams-manager...\" ;;\n \"uninstall.stopping_manager.en\") text=\"Stopping and removing agentteams-manager...\" ;;\n \"uninstall.stopping_workers.zh\") text=\"正在停止并移除 Worker 容器...\" ;;\n \"uninstall.stopping_workers.en\") text=\"Stopping and removing worker containers...\" ;;\n \"uninstall.removed.zh\") text=\" 已移除: %s\" ;;\n \"uninstall.removed.en\") text=\" Removed: %s\" ;;\n \"uninstall.removing_volume.zh\") text=\"正在移除 Docker 卷: %s\" ;;\n \"uninstall.removing_volume.en\") text=\"Removing Docker volume: %s\" ;;\n \"uninstall.removing_env.zh\") text=\"正在移除 env 文件: %s\" ;;\n \"uninstall.removing_env.en\") text=\"Removing env file: %s\" ;;\n \"uninstall.removing_proxy.zh\") text=\"正在停止并移除 Docker API 代理容器: agentteams-docker-proxy\" ;;\n \"uninstall.removing_proxy.en\") text=\"Stopping and removing Docker API proxy container: agentteams-docker-proxy\" ;;\n \"uninstall.stopping_controller.zh\") text=\"正在停止并移除 agentteams-controller (内嵌 Tuwunel/MinIO/Higress)...\" ;;\n \"uninstall.stopping_controller.en\") text=\"Stopping and removing agentteams-controller (embedded Tuwunel/MinIO/Higress)...\" ;;\n \"uninstall.removing_network.zh\") text=\"正在移除 Docker 网络: agentteams-net\" ;;\n \"uninstall.removing_network.en\") text=\"Removing Docker network: agentteams-net\" ;;\n \"uninstall.removing_workspace.zh\") text=\"正在移除工作空间目录: %s\" ;;\n \"uninstall.removing_workspace.en\") text=\"Removing workspace directory: %s\" ;;\n \"uninstall.removing_workspace_elevated.zh\") text=\" 工作空间包含 root 文件,通过容器清理...\" ;;\n \"uninstall.removing_workspace_elevated.en\") text=\" Workspace contains root-owned files, cleaning via container...\" ;;\n \"uninstall.removing_log.zh\") text=\"正在移除日志文件: %s\" ;;\n \"uninstall.removing_log.en\") text=\"Removing log file: %s\" ;;\n \"uninstall.done.zh\") text=\"AgentTeams 已卸载。\" ;;\n \"uninstall.done.en\") text=\"AgentTeams has been uninstalled.\" ;;\n # --- Error messages ---\n \"error.name_required.zh\") text=\"--name 是必需的\" ;;\n \"error.name_required.en\") text=\"--name is required\" ;;\n \"error.fs_required.zh\") text=\"--fs 是必需的\" ;;\n \"error.fs_required.en\") text=\"--fs is required\" ;;\n \"error.fs_key_required.zh\") text=\"--fs-key 是必需的\" ;;\n \"error.fs_key_required.en\") text=\"--fs-key is required\" ;;\n \"error.fs_secret_required.zh\") text=\"--fs-secret 是必需的\" ;;\n \"error.fs_secret_required.en\") text=\"--fs-secret is required\" ;;\n \"error.unknown_option.zh\") text=\"未知选项: %s\" ;;\n \"error.unknown_option.en\") text=\"Unknown option: %s\" ;;\n \"error.docker_not_found.zh\") text=\"未找到 docker 或 podman 命令。请先安装 Docker Desktop 或 Podman Desktop:\\n Docker Desktop: https://www.docker.com/products/docker-desktop/\\n Podman Desktop: https://podman-desktop.io/\" ;;\n \"error.docker_not_found.en\") text=\"docker or podman command not found. Please install Docker Desktop or Podman Desktop first:\\n Docker Desktop: https://www.docker.com/products/docker-desktop/\\n Podman Desktop: https://podman-desktop.io/\" ;;\n \"error.docker_not_running.zh\") text=\"Docker 未运行。请先启动 Docker Desktop 或 Podman Desktop。\" ;;\n \"error.docker_not_running.en\") text=\"Docker is not running. Please start Docker Desktop or Podman Desktop first.\" ;;\n # --- Fallback: try English for unknown lang ---\n *)\n case \"${key}.en\" in\n \"tz.warning.title.en\") text=\"Could not detect timezone automatically.\" ;;\n \"install.title.en\") text=\"=== AgentTeams Manager Installation ===\" ;;\n *) text=\"${key}\" ;;\n esac\n ;;\n esac\n if [ $# -gt 0 ]; then\n # shellcheck disable=SC2059\n printf \"${text}\\n\" \"$@\"\n else\n echo \"${text}\"\n fi\n}\n\n# ============================================================\n# Registry selection based on timezone\n# ============================================================\n\ndetect_registry() {\n local tz=\"${AGENTTEAMS_TIMEZONE}\"\n\n case \"${tz}\" in\n America/*)\n echo \"higress-registry.us-west-1.cr.aliyuncs.com\"\n ;;\n Asia/Singapore|Asia/Bangkok|Asia/Jakarta|Asia/Makassar|Asia/Jayapura|\\\n Asia/Kuala_Lumpur|Asia/Ho_Chi_Minh|Asia/Manila|Asia/Yangon|\\\n Asia/Vientiane|Asia/Phnom_Penh|Asia/Pontianak|Asia/Ujung_Pandang)\n echo \"higress-registry.ap-southeast-7.cr.aliyuncs.com\"\n ;;\n *)\n echo \"higress-registry.cn-hangzhou.cr.aliyuncs.com\"\n ;;\n esac\n}\n\nAGENTTEAMS_REGISTRY=\"${AGENTTEAMS_REGISTRY:-$(detect_registry)}\"\n# Backward compatibility: accept old env var names from previous versions\nAGENTTEAMS_INSTALL_CONTROLLER_IMAGE=\"${AGENTTEAMS_INSTALL_CONTROLLER_IMAGE:-${AGENTTEAMS_INSTALL_DOCKER_PROXY_IMAGE:-}}\"\n# Image variables are resolved after version selection in step_version().\n# These placeholders allow early code paths to reference them without errors.\nMANAGER_IMAGE=\"${AGENTTEAMS_INSTALL_MANAGER_IMAGE:-}\"\nMANAGER_COPAW_IMAGE=\"${AGENTTEAMS_INSTALL_MANAGER_COPAW_IMAGE:-}\"\nWORKER_IMAGE=\"${AGENTTEAMS_INSTALL_WORKER_IMAGE:-}\"\nCOPAW_WORKER_IMAGE=\"${AGENTTEAMS_INSTALL_COPAW_WORKER_IMAGE:-}\"\nHERMES_WORKER_IMAGE=\"${AGENTTEAMS_INSTALL_HERMES_WORKER_IMAGE:-}\"\nCONTROLLER_IMAGE=\"${AGENTTEAMS_INSTALL_CONTROLLER_IMAGE:-}\"\n\nresolve_image_tags() {\n AGENTTEAMS_VERSION=\"$(_normalize_version \"${AGENTTEAMS_VERSION}\")\"\n MANAGER_IMAGE=\"${AGENTTEAMS_INSTALL_MANAGER_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-manager:${AGENTTEAMS_VERSION}}\"\n MANAGER_COPAW_IMAGE=\"${AGENTTEAMS_INSTALL_MANAGER_COPAW_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-manager-copaw:${AGENTTEAMS_VERSION}}\"\n WORKER_IMAGE=\"${AGENTTEAMS_INSTALL_WORKER_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-worker:${AGENTTEAMS_VERSION}}\"\n COPAW_WORKER_IMAGE=\"${AGENTTEAMS_INSTALL_COPAW_WORKER_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-copaw-worker:${AGENTTEAMS_VERSION}}\"\n HERMES_WORKER_IMAGE=\"${AGENTTEAMS_INSTALL_HERMES_WORKER_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-hermes-worker:${AGENTTEAMS_VERSION}}\"\n EMBEDDED_IMAGE=\"${AGENTTEAMS_INSTALL_EMBEDDED_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-embedded:${AGENTTEAMS_VERSION}}\"\n # CoPaw Worker introduced in v1.0.4; Hermes Worker introduced in v1.1.0\n if [ -z \"${AGENTTEAMS_INSTALL_COPAW_WORKER_IMAGE:-}\" ] && _ver_lt \"${AGENTTEAMS_VERSION}\" \"v1.0.4\"; then\n COPAW_WORKER_IMAGE=\"\"\n fi\n if [ -z \"${AGENTTEAMS_INSTALL_HERMES_WORKER_IMAGE:-}\" ] && _ver_lt \"${AGENTTEAMS_VERSION}\" \"v1.1.0\"; then\n HERMES_WORKER_IMAGE=\"\"\n fi\n}\n\n# Resolve the embedded controller image. Embedded mode is the only supported\n# architecture since PR #616 (manager image no longer bundles Higress/Tuwunel/MinIO).\n# If the embedded image is unavailable for the requested version, fail fast with an\n# actionable error rather than silently falling back to the legacy single-container\n# path — that path is permanently broken with the slim manager image and would just\n# leave the user with a manager container looping on \"Waiting for Higress Gateway\".\n# Sets EMBEDDED_IMAGE and AGENTTEAMS_USE_EMBEDDED.\nresolve_embedded_image() {\n AGENTTEAMS_USE_EMBEDDED=1\n\n # If the user explicitly overrode the image (e.g. `make install-embedded` passes\n # a locally-built tag), respect it as-is without any registry probe.\n if [ -n \"${AGENTTEAMS_INSTALL_EMBEDDED_IMAGE:-}\" ]; then\n EMBEDDED_IMAGE=\"${AGENTTEAMS_INSTALL_EMBEDDED_IMAGE}\"\n return 0\n fi\n\n local _versioned=\"${AGENTTEAMS_REGISTRY}/agentteams/agentteams-embedded:${AGENTTEAMS_VERSION}\"\n local _latest=\"${AGENTTEAMS_REGISTRY}/agentteams/agentteams-embedded:latest\"\n\n # Skip probe when AGENTTEAMS_VERSION is \"latest\" — no point trying the same tag twice.\n if [ \"${AGENTTEAMS_VERSION}\" = \"latest\" ]; then\n EMBEDDED_IMAGE=\"${_latest}\"\n return 0\n fi\n\n if ${DOCKER_CMD} pull \"${_versioned}\" >/dev/null 2>&1; then\n EMBEDDED_IMAGE=\"${_versioned}\"\n return 0\n fi\n\n # Versions before v1.1.0 predate agentteams-embedded entirely — their manager image\n # bundled all infrastructure. Falling back to agentteams-embedded:latest would\n # silently swap in the v1.1.0 architecture (embedded kube-apiserver) which\n # crashes under QEMU on Apple Silicon. Auto-activate legacy mode instead.\n if _ver_lt \"${AGENTTEAMS_VERSION}\" \"v1.1.0\"; then\n log \"INFO: ${AGENTTEAMS_VERSION} predates agentteams-embedded; switching to legacy all-in-one manager architecture.\"\n log \"WARNING: Legacy all-in-one mode requires AGENTTEAMS_VERSION <= v1.0.9 (older bundled manager image).\"\n log \"WARNING: Newer slim manager images will hang on 'Waiting for Higress Gateway'.\"\n AGENTTEAMS_USE_EMBEDDED=0\n return 0\n fi\n\n if ${DOCKER_CMD} pull \"${_latest}\" >/dev/null 2>&1; then\n log \"embedded ${AGENTTEAMS_VERSION} not found, using latest\"\n EMBEDDED_IMAGE=\"${_latest}\"\n return 0\n fi\n\n # Explicit escape hatch — still honoured for edge cases.\n if [ \"${AGENTTEAMS_FORCE_LEGACY:-0}\" = \"1\" ]; then\n log \"WARNING: AGENTTEAMS_FORCE_LEGACY=1 — using legacy all-in-one manager architecture.\"\n log \"WARNING: This requires AGENTTEAMS_VERSION <= v1.0.9 (older bundled manager image).\"\n log \"WARNING: Newer slim manager images will hang on 'Waiting for Higress Gateway'.\"\n AGENTTEAMS_USE_EMBEDDED=0\n return 0\n fi\n\n error \"Embedded controller image is not available in the registry:\"\n error \" - tried: ${_versioned}\"\n error \" - tried: ${_latest}\"\n error \"\"\n error \"Embedded mode is the only supported architecture since PR #616.\"\n error \"How to resolve:\"\n error \" 1) Pin to a AGENTTEAMS_VERSION whose embedded image has been published, or\"\n error \" wait for the release pipeline to publish it.\"\n error \" 2) For a local build, run: make install-embedded\"\n error \" (builds and uses the local embedded image without touching the registry).\"\n error \" 3) Override with a custom image: AGENTTEAMS_INSTALL_EMBEDDED_IMAGE=... ./agentteams-install.sh\"\n exit 1\n}\n\n# ============================================================\n# Known models list — used to detect custom models during install\n# ============================================================\nKNOWN_MODELS=\"gpt-5.4 gpt-5.3-codex gpt-5-mini gpt-5-nano claude-opus-4-6 claude-sonnet-4-6 claude-haiku-4-5 qwen3.6-plus qwen3.5-plus deepseek-chat deepseek-reasoner kimi-k2.5 glm-5 MiniMax-M2.7 MiniMax-M2.7-highspeed MiniMax-M2.5\"\n\nis_known_model() {\n local model=\"$1\"\n for m in ${KNOWN_MODELS}; do\n [ \"${m}\" = \"${model}\" ] && return 0\n done\n return 1\n}\n\n# Prompt user for custom model parameters when model is not in the known list.\n# Sets: AGENTTEAMS_MODEL_CONTEXT_WINDOW, AGENTTEAMS_MODEL_MAX_TOKENS, AGENTTEAMS_MODEL_REASONING, AGENTTEAMS_MODEL_VISION\nprompt_custom_model_params() {\n local model=\"$1\"\n if is_known_model \"${model}\"; then\n # Clear any stale custom params for known models\n AGENTTEAMS_MODEL_CONTEXT_WINDOW=\"\"\n AGENTTEAMS_MODEL_MAX_TOKENS=\"\"\n AGENTTEAMS_MODEL_REASONING=\"\"\n AGENTTEAMS_MODEL_VISION=\"\"\n return\n fi\n echo \"\"\n log \"$(msg llm.custom_model.detected \"${model}\")\"\n echo \"\"\n read -e -p \" $(msg llm.custom_model.context_prompt): \" AGENTTEAMS_MODEL_CONTEXT_WINDOW\n if [ \"${AGENTTEAMS_MODEL_CONTEXT_WINDOW}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n AGENTTEAMS_MODEL_CONTEXT_WINDOW=\"${AGENTTEAMS_MODEL_CONTEXT_WINDOW:-150000}\"\n read -e -p \" $(msg llm.custom_model.max_tokens_prompt): \" AGENTTEAMS_MODEL_MAX_TOKENS\n if [ \"${AGENTTEAMS_MODEL_MAX_TOKENS}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n AGENTTEAMS_MODEL_MAX_TOKENS=\"${AGENTTEAMS_MODEL_MAX_TOKENS:-128000}\"\n read -e -p \" $(msg llm.custom_model.reasoning_prompt): \" _reasoning\n if [ \"${_reasoning}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n case \"${_reasoning}\" in\n n|N|no|NO) AGENTTEAMS_MODEL_REASONING=\"false\" ;;\n *) AGENTTEAMS_MODEL_REASONING=\"true\" ;;\n esac\n read -e -p \" $(msg llm.custom_model.vision_prompt): \" _vision\n if [ \"${_vision}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n case \"${_vision}\" in\n y|Y|yes|YES) AGENTTEAMS_MODEL_VISION=\"true\" ;;\n *) AGENTTEAMS_MODEL_VISION=\"false\" ;;\n esac\n log \"$(msg llm.custom_model.summary \"${AGENTTEAMS_MODEL_CONTEXT_WINDOW}\" \"${AGENTTEAMS_MODEL_MAX_TOKENS}\" \"${AGENTTEAMS_MODEL_REASONING}\" \"${AGENTTEAMS_MODEL_VISION}\")\"\n}\n\n# ============================================================\n# Wait for Manager agent to be ready\n# Uses `openclaw gateway health` inside the container to confirm the gateway is running\n# ============================================================\n\nwait_manager_ready() {\n local timeout=\"${AGENTTEAMS_READY_TIMEOUT:-300}\"\n local elapsed=0\n local container=\"${1:-agentteams-manager}\"\n\n log \"$(msg install.wait_ready \"${timeout}\")\"\n\n # Wait for Manager agent to be healthy inside the container\n local runtime=\"${AGENTTEAMS_MANAGER_RUNTIME:-copaw}\"\n while [ \"${elapsed}\" -lt \"${timeout}\" ]; do\n case \"${runtime}\" in\n copaw)\n if ${DOCKER_CMD} exec \"${container}\" curl -sf http://127.0.0.1:18799/api/agents 2>/dev/null | grep -q '\"agents\"'; then\n log \"$(msg install.wait_ready.ok)\"\n return 0\n fi\n ;;\n *)\n if ${DOCKER_CMD} exec \"${container}\" openclaw gateway health --json 2>/dev/null | grep -q '\"ok\"' 2>/dev/null; then\n log \"$(msg install.wait_ready.ok)\"\n return 0\n fi\n ;;\n esac\n sleep 5\n elapsed=$((elapsed + 5))\n printf \"\\r\\033[36m[AgentTeams]\\033[0m $(msg install.wait_ready.waiting \"${elapsed}\" \"${timeout}\")\"\n done\n\n echo \"\"\n die \"$(msg install.wait_ready.timeout \"${timeout}\" \"${container}\")\"\n}\n\nwait_matrix_ready() {\n local timeout=\"${AGENTTEAMS_READY_TIMEOUT:-300}\"\n local elapsed=0\n local container=\"${1:-agentteams-manager}\"\n\n log \"$(msg install.wait_matrix \"${timeout}\")\"\n\n while [ \"${elapsed}\" -lt \"${timeout}\" ]; do\n if ${DOCKER_CMD} exec \"${container}\" curl -sf http://127.0.0.1:6167/_tuwunel/server_version >/dev/null 2>&1; then\n log \"$(msg install.wait_matrix.ok)\"\n return 0\n fi\n sleep 5\n elapsed=$((elapsed + 5))\n printf \"\\r\\033[36m[AgentTeams]\\033[0m $(msg install.wait_matrix.waiting \"${elapsed}\" \"${timeout}\")\"\n done\n\n echo \"\"\n die \"$(msg install.wait_matrix.timeout \"${timeout}\" \"${container}\")\"\n}\n\n# Read KEY=value from /data/agentteams-secrets.env on a Docker volume (manager container not required).\n# Requires EMBEDDED_IMAGE (resolved earlier in install_manager). Uses ${DOCKER_CMD}.\nagentteams_read_secret_from_data_volume() {\n local _vol=\"$1\" _key=\"$2\"\n if [ -z \"${_vol}\" ] || [ -z \"${_key}\" ] || [ -z \"${EMBEDDED_IMAGE:-}\" ]; then\n echo \"\"\n return 0\n fi\n ${DOCKER_CMD} run --rm --entrypoint sh \\\n -v \"${_vol}:/data:ro\" \\\n \"${EMBEDDED_IMAGE}\" -c \"grep \\\"^${_key}=\\\" /data/agentteams-secrets.env 2>/dev/null | cut -d= -f2- | head -1 | tr -d '\\r'\" 2>/dev/null\n}\n\n# Read KEY=value from /data/worker-creds/<worker>.env on a Docker volume.\nagentteams_read_worker_creds_value_from_volume() {\n local _vol=\"$1\" _worker=\"$2\" _key=\"$3\"\n if [ -z \"${_vol}\" ] || [ -z \"${_worker}\" ] || [ -z \"${_key}\" ] || [ -z \"${EMBEDDED_IMAGE:-}\" ]; then\n echo \"\"\n return 0\n fi\n ${DOCKER_CMD} run --rm --entrypoint sh \\\n -v \"${_vol}:/data:ro\" \\\n \"${EMBEDDED_IMAGE}\" -c \"grep \\\"^${_key}=\\\" \\\"/data/worker-creds/${_worker}.env\\\" 2>/dev/null | cut -d= -f2- | head -1 | tr -d \\\"\\\\r\\\"\" 2>/dev/null\n}\n\n# Read admin_dm_room_id from host workspace state.json (fallback when Matrix API is unavailable).\nagentteams_read_admin_dm_room_from_workspace() {\n local _ws=\"$1\"\n local _f=\"${_ws}/state.json\"\n if [ ! -f \"${_f}\" ] || ! command -v jq >/dev/null 2>&1; then\n echo \"\"\n return 0\n fi\n jq -r '.admin_dm_room_id // empty | select(. != \"null\")' \"${_f}\" 2>/dev/null\n}\n\n# Read secret input with masked echo (shows * per keystroke, supports backspace)\n# Usage: read_secret \"prompt text: \"; value=\"${_RS_RESULT}\"\nread_secret() {\n local _rs_prompt=\"$1\"\n _RS_RESULT=\"\"\n local _rs_char=\"\"\n\n printf \"%s\" \"${_rs_prompt}\"\n\n while IFS= read -r -s -n 1 _rs_char; do\n if [[ -z \"${_rs_char}\" ]]; then\n break\n elif [[ \"${_rs_char}\" == $'\\177' ]] || [[ \"${_rs_char}\" == $'\\b' ]]; then\n if [ -n \"${_RS_RESULT}\" ]; then\n _RS_RESULT=\"${_RS_RESULT%?}\"\n printf \"\\b \\b\"\n fi\n else\n _RS_RESULT=\"${_RS_RESULT}${_rs_char}\"\n printf \"*\"\n fi\n done\n echo\n}\n\n# Load current parameter values from env file for upgrade mode display\nload_current_params_from_env() {\n local env_file=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n if [ -f \"${env_file}\" ]; then\n [ -z \"${AGENTTEAMS_LLM_PROVIDER:+x}\" ] && AGENTTEAMS_LLM_PROVIDER=\"$(grep '^AGENTTEAMS_LLM_PROVIDER=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_OPENAI_BASE_URL:+x}\" ] && AGENTTEAMS_OPENAI_BASE_URL=\"$(grep '^AGENTTEAMS_OPENAI_BASE_URL=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_DEFAULT_MODEL:+x}\" ] && AGENTTEAMS_DEFAULT_MODEL=\"$(grep '^AGENTTEAMS_DEFAULT_MODEL=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_EMBEDDING_MODEL:+x}\" ] && AGENTTEAMS_EMBEDDING_MODEL=\"$(grep '^AGENTTEAMS_EMBEDDING_MODEL=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_WORKSPACE_DIR:+x}\" ] && AGENTTEAMS_WORKSPACE_DIR=\"$(grep '^AGENTTEAMS_WORKSPACE_DIR=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_HOST_SHARE_DIR:+x}\" ] && AGENTTEAMS_HOST_SHARE_DIR=\"$(grep '^AGENTTEAMS_HOST_SHARE_DIR=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_PODMAN_AUTOSTART:+x}\" ] && AGENTTEAMS_PODMAN_AUTOSTART=\"$(grep '^AGENTTEAMS_PODMAN_AUTOSTART=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_DASHBOARD:+x}\" ] && AGENTTEAMS_DASHBOARD=\"$(grep '^AGENTTEAMS_DASHBOARD=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_DASHBOARD_VERSION:+x}\" ] && AGENTTEAMS_DASHBOARD_VERSION=\"$(grep '^AGENTTEAMS_DASHBOARD_VERSION=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_PORT_DASHBOARD:+x}\" ] && AGENTTEAMS_PORT_DASHBOARD=\"$(grep '^AGENTTEAMS_PORT_DASHBOARD=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_DASHBOARD_IMAGE:+x}\" ] && AGENTTEAMS_DASHBOARD_IMAGE=\"$(grep '^AGENTTEAMS_DASHBOARD_IMAGE=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL:+x}\" ] && AGENTTEAMS_AI_GATEWAY_ADMIN_URL=\"$(grep '^AGENTTEAMS_AI_GATEWAY_ADMIN_URL=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n fi\n}\n\n# In non-interactive mode, uses default or errors if required and no default.\n# Usage: prompt VAR_NAME \"Prompt text\" \"default\" [true=secret]\nprompt() {\n local var_name=\"$1\"\n local prompt_text=\"$2\"\n local default_value=\"$3\"\n local is_secret=\"${4:-false}\"\n\n # If the variable is already set in the environment, use it silently\n # In upgrade mode, show current value and let user change it\n eval \"local current_value=\\\"\\${${var_name}}\\\"\"\n if [ -n \"${current_value}\" ]; then\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_NON_INTERACTIVE}\" != \"1\" ]; then\n # Show masked value for secrets, full value otherwise\n local display_value=\"${current_value}\"\n if [ \"${is_secret}\" = \"true\" ]; then\n local len=${#current_value}\n if [ \"${len}\" -le 8 ]; then\n display_value=\"****\"\n else\n display_value=\"${current_value:0:4}****${current_value: -4}\"\n fi\n fi\n log \"$(msg prompt.upgrade_keep \"${prompt_text}\" \"${display_value}\")\"\n local new_value=\"\"\n if [ \"${is_secret}\" = \"true\" ]; then\n read_secret \"${prompt_text}: \"\n new_value=\"${_RS_RESULT}\"\n else\n read -e -p \"${prompt_text}: \" new_value\n if [ \"${new_value}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n fi\n if [ -n \"${new_value}\" ]; then\n eval \"export ${var_name}='${new_value}'\"\n fi\n return\n fi\n log \"$(msg prompt.preset \"${prompt_text}\")\"\n return\n fi\n\n # Non-interactive or quickstart: use default or error\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] || [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ]; then\n if [ -n \"${default_value}\" ]; then\n eval \"export ${var_name}='${default_value}'\"\n log \"$(msg prompt.default \"${prompt_text}\" \"${default_value}\")\"\n return\n elif [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n # Only hard-error in fully non-interactive mode, not quickstart\n die \"$(msg prompt.required \"${prompt_text}\")\"\n fi\n # quickstart + no default: fall through to interactive prompt below\n fi\n\n if [ -n \"${default_value}\" ]; then\n prompt_text=\"${prompt_text} [${default_value}]\"\n fi\n\n local value=\"\"\n if [ \"${is_secret}\" = \"true\" ]; then\n read_secret \"${prompt_text}: \"\n value=\"${_RS_RESULT}\"\n else\n read -e -p \"${prompt_text}: \" value\n if [ \"${value}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n fi\n\n value=\"${value:-${default_value}}\"\n if [ -z \"${value}\" ]; then\n die \"$(msg prompt.required_empty \"${prompt_text}\")\"\n fi\n\n eval \"export ${var_name}='${value}'\"\n}\n\n# Prompt for an optional value (empty string is acceptable)\n# Skips prompt if variable is already defined in environment (even if empty)\n# In upgrade mode, shows current value and lets user change it.\n# In non-interactive mode, defaults to empty string.\nprompt_optional() {\n local var_name=\"$1\"\n local prompt_text=\"$2\"\n local is_secret=\"${3:-false}\"\n\n # Check if variable is defined (even if set to empty string)\n eval \"local _chk=\\\"\\${${var_name}+x}\\\"\"\n if [ -n \"${_chk}\" ]; then\n # In upgrade mode, show current value and let user change it\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_NON_INTERACTIVE}\" != \"1\" ]; then\n eval \"local current_value=\\\"\\${${var_name}}\\\"\"\n local display_value=\"${current_value}\"\n if [ \"${is_secret}\" = \"true\" ] && [ -n \"${current_value}\" ]; then\n local len=${#current_value}\n if [ \"${len}\" -le 8 ]; then\n display_value=\"****\"\n else\n display_value=\"${current_value:0:4}****${current_value: -4}\"\n fi\n fi\n if [ -n \"${current_value}\" ]; then\n if [ \"${is_secret}\" = \"true\" ]; then\n log \"$(msg prompt.upgrade_keep_secret \"${prompt_text}\" \"${display_value}\")\"\n else\n log \"$(msg prompt.upgrade_keep \"${prompt_text}\" \"${display_value}\")\"\n fi\n else\n log \"$(msg prompt.upgrade_empty \"${prompt_text}\")\"\n fi\n local new_value=\"\"\n if [ \"${is_secret}\" = \"true\" ]; then\n read_secret \"${prompt_text}: \"\n new_value=\"${_RS_RESULT}\"\n else\n read -e -p \"${prompt_text}: \" new_value\n if [ \"${new_value}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n fi\n if [ -n \"${new_value}\" ]; then\n eval \"export ${var_name}='${new_value}'\"\n fi\n return\n fi\n log \"$(msg prompt.preset \"${prompt_text}\")\"\n return\n fi\n\n # Non-interactive or quickstart: skip, leave unset\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] || [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ]; then\n eval \"export ${var_name}=''\"\n return\n fi\n\n local value=\"\"\n if [ \"${is_secret}\" = \"true\" ]; then\n read_secret \"${prompt_text}: \"\n value=\"${_RS_RESULT}\"\n else\n read -e -p \"${prompt_text}: \" value\n if [ \"${value}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n fi\n\n eval \"export ${var_name}='${value}'\"\n}\n\ngenerate_key() {\n openssl rand -hex 32\n}\n\n# ============================================================\n# Detect container runtime socket on the host\n#\n# Dependency: Requires DOCKER_CMD to reflect the TRUE underlying runtime.\n# (Crucial for unmasking 'podman' disguised as 'docker'.\n# Must be strictly resolved prior, e.g., via check_container_runtime)\n# ============================================================\ndetect_socket() {\n local socket_path\n\n # 1. Respect explicitly defined DOCKER_HOST environment variable\n if [ -n \"${DOCKER_HOST}\" ]; then\n socket_path=$(echo \"${DOCKER_HOST}\" | sed 's|^unix://||')\n if [ -S \"${socket_path}\" ]; then\n echo \"${socket_path}\"\n return 0\n fi\n fi\n\n # 2. Route based on the resolved container runtime\n case \"${DOCKER_CMD}\" in\n docker)\n # Docker path handling\n socket_path=$(docker context ls --format '{{if .Current}}{{.DockerEndpoint}}{{end}}' 2>/dev/null | grep . | sed 's|^unix://||')\n if [ -n \"${socket_path}\" ] && [ -S \"${socket_path}\" ]; then\n echo \"${socket_path}\"\n return 0\n fi\n\n # Rootless Docker\n if [ -n \"${XDG_RUNTIME_DIR:-}\" ] && [ -S \"${XDG_RUNTIME_DIR}/docker.sock\" ]; then\n echo \"${XDG_RUNTIME_DIR}/docker.sock\"\n return 0\n fi\n\n # Rootless Docker (macOS fallback)\n if [ -S \"${HOME}/.docker/run/docker.sock\" ]; then\n echo \"${HOME}/.docker/run/docker.sock\"\n return 0\n fi\n\n # Root Docker\n if [ -S \"/var/run/docker.sock\" ]; then\n echo \"/var/run/docker.sock\"\n return 0\n fi\n ;;\n podman)\n # Podman path handling\n if [ \"$(id -u)\" -eq 0 ]; then\n socket_path=\"/run/podman/podman.sock\"\n else\n socket_path=\"${XDG_RUNTIME_DIR}/podman/podman.sock\"\n fi\n if [ -S \"${socket_path}\" ]; then\n echo \"${socket_path}\"\n return 0\n fi\n ;;\n esac\n\n # Return empty if no socket is found\n echo \"\"\n return 0\n}\n\n# Ensure Podman API socket is active (State mutation)\nensure_podman_socket() {\n local socket_path\n\n # Only applicable if runtime is podman and systemctl is available\n if [ \"${DOCKER_CMD:-}\" != \"podman\" ] || ! command -v systemctl >/dev/null 2>&1; then\n return 0\n fi\n\n echo \"Ensuring Podman API socket is active...\" >&2\n\n # Enable and start the socket based on user privileges\n if [ \"$(id -u)\" -eq 0 ]; then\n socket_path=\"/run/podman/podman.sock\"\n systemctl enable --now podman.socket >/dev/null 2>&1 || true\n else\n socket_path=\"${XDG_RUNTIME_DIR}/podman/podman.sock\"\n systemctl --user enable --now podman.socket >/dev/null 2>&1 || true\n fi\n\n # Give systemd a brief moment to assert the socket for root or rootless user\n local _i\n for _i in 1 2 3; do\n [ -S \"${socket_path}\" ] && break\n sleep 1\n done\n}\n\n# Detect local LAN IP address (cross-platform: macOS and Linux)\ndetect_lan_ip() {\n local ip=\"\"\n\n # macOS: try common Wi-Fi / Ethernet interfaces\n if command -v ipconfig >/dev/null 2>&1; then\n for iface in en0 en1 en2 en3 en4; do\n ip=$(ipconfig getifaddr \"${iface}\" 2>/dev/null)\n if [ -n \"${ip}\" ]; then\n echo \"${ip}\"\n return 0\n fi\n done\n fi\n\n # Linux: ip route — most reliable\n if command -v ip >/dev/null 2>&1; then\n ip=$(ip route get 1.1.1.1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i==\"src\"){print $(i+1); exit}}')\n if [ -n \"${ip}\" ]; then\n echo \"${ip}\"\n return 0\n fi\n fi\n\n # Linux fallback: hostname -I (space-separated list, take first non-loopback)\n if command -v hostname >/dev/null 2>&1; then\n ip=$(hostname -I 2>/dev/null | tr ' ' '\\n' | grep -v '^127\\.' | grep -v '^::' | head -1)\n if [ -n \"${ip}\" ]; then\n echo \"${ip}\"\n return 0\n fi\n fi\n\n # Last resort: ifconfig\n if command -v ifconfig >/dev/null 2>&1; then\n ip=$(ifconfig 2>/dev/null | awk '/inet /{if($2!~/^127\\./){print $2; exit}}')\n # Strip \"addr:\" prefix that some ifconfig versions add\n ip=\"${ip#addr:}\"\n if [ -n \"${ip}\" ]; then\n echo \"${ip}\"\n return 0\n fi\n fi\n\n echo \"\"\n}\n\n\n# ============================================================\n# Step-back navigation helpers\n# ============================================================\n\n# should_skip_step: returns 0 (skip) when the step is irrelevant in current mode\nshould_skip_step() {\n local step_fn=\"$1\"\n case \"${step_fn}\" in\n step_lang|step_mode)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n ;;\n step_version)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n ;;\n step_existing)\n local _env=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n [ ! -f \"${_env}\" ] && return 0\n ;;\n # Keep-All upgrade mode: skip all config steps (step_volume/step_workspace handled separately)\n step_llm|step_admin|step_network|step_ports|step_domains|step_github|step_skills|step_runtime|step_manager_runtime|step_dashboard|step_e2ee|step_docker_proxy|step_idle|step_hostshare)\n [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ] && return 0\n ;;\n step_volume|step_workspace)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ] && return 0\n [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ] && return 0\n ;;\n step_e2ee|step_idle)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE}\" != \"1\" ] && return 0\n ;;\n step_docker_proxy)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE}\" != \"1\" ] && return 0\n # Embedded mode handles docker access natively — skip this step\n [ \"${AGENTTEAMS_USE_EMBEDDED:-}\" = \"1\" ] && return 0\n ;;\n step_manager_runtime)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n ;;\n step_hostshare)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ] && return 0\n ;;\n step_podman_autostart)\n # Only relevant for Podman with systemd\n [ \"${DOCKER_CMD}\" != \"podman\" ] && return 0\n ! command -v systemctl >/dev/null 2>&1 && return 0\n\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE}\" != \"1\" ] && return 0\n [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ] && return 0\n ;;\n esac\n return 1\n}\n\n# clear_step_vars: unset variables set by a step so it will re-prompt on re-entry\nclear_step_vars() {\n local step_fn=\"$1\"\n case \"${step_fn}\" in\n step_mode) unset AGENTTEAMS_QUICKSTART ;;\n step_version) unset AGENTTEAMS_VERSION ;;\n step_existing) unset AGENTTEAMS_UPGRADE UPGRADE_EXISTING_WORKERS ;;\n step_llm)\n unset AGENTTEAMS_LLM_PROVIDER AGENTTEAMS_DEFAULT_MODEL AGENTTEAMS_OPENAI_BASE_URL\n unset AGENTTEAMS_LLM_API_KEY AGENTTEAMS_MODEL_CONTEXT_WINDOW AGENTTEAMS_MODEL_MAX_TOKENS\n unset AGENTTEAMS_MODEL_REASONING AGENTTEAMS_MODEL_VISION\n ;;\n step_admin) unset AGENTTEAMS_ADMIN_USER AGENTTEAMS_ADMIN_PASSWORD ;;\n step_network) unset AGENTTEAMS_LOCAL_ONLY ;;\n step_ports)\n unset AGENTTEAMS_PORT_GATEWAY AGENTTEAMS_PORT_CONSOLE\n unset AGENTTEAMS_PORT_ELEMENT_WEB AGENTTEAMS_PORT_MANAGER_CONSOLE\n ;;\n step_domains)\n unset AGENTTEAMS_MATRIX_DOMAIN AGENTTEAMS_MATRIX_CLIENT_DOMAIN\n unset AGENTTEAMS_AI_GATEWAY_DOMAIN AGENTTEAMS_FS_DOMAIN AGENTTEAMS_CONSOLE_DOMAIN\n ;;\n step_github) unset AGENTTEAMS_GITHUB_TOKEN ;;\n step_skills) unset AGENTTEAMS_SKILLS_API_URL ;;\n step_volume) unset AGENTTEAMS_DATA_DIR ;;\n step_workspace) unset AGENTTEAMS_WORKSPACE_DIR ;;\n step_dashboard) unset AGENTTEAMS_DASHBOARD AGENTTEAMS_DASHBOARD_VERSION AGENTTEAMS_PORT_DASHBOARD AGENTTEAMS_DASHBOARD_IMAGE AGENTTEAMS_AI_GATEWAY_ADMIN_URL ;;\n step_runtime) unset AGENTTEAMS_DEFAULT_WORKER_RUNTIME ;;\n step_manager_runtime) unset AGENTTEAMS_MANAGER_RUNTIME ;;\n step_e2ee) unset AGENTTEAMS_MATRIX_E2EE ;;\n step_docker_proxy) unset AGENTTEAMS_DOCKER_PROXY; unset AGENTTEAMS_PROXY_ALLOWED_REGISTRIES ;;\n step_idle) unset AGENTTEAMS_WORKER_IDLE_TIMEOUT ;;\n step_hostshare) unset AGENTTEAMS_HOST_SHARE_DIR ;;\n step_podman_autostart) unset AGENTTEAMS_PODMAN_AUTOSTART ;;\n esac\n}\n\n# ============================================================\n# Individual step functions\n# ============================================================\n\nstep_lang() {\n local lang_default_choice=\"2\"\n [ \"${AGENTTEAMS_LANGUAGE}\" = \"zh\" ] && lang_default_choice=\"1\"\n log \"$(msg lang.detected)\"\n log \"$(msg lang.switch_title)\"\n echo \"$(msg lang.option_zh)\"\n echo \"$(msg lang.option_en)\"\n echo \"\"\n local LANG_CHOICE\n read -e -p \"$(msg lang.prompt) [${lang_default_choice}]: \" LANG_CHOICE\n LANG_CHOICE=\"${LANG_CHOICE:-${lang_default_choice}}\"\n if [ \"${LANG_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n case \"${LANG_CHOICE}\" in\n 1) AGENTTEAMS_LANGUAGE=\"zh\" ;;\n 2) AGENTTEAMS_LANGUAGE=\"en\" ;;\n esac\n export AGENTTEAMS_LANGUAGE\n log \"\"\n}\n\nstep_mode() {\n log \"$(msg install.mode.title)\"\n echo \"\"\n echo \"$(msg install.mode.choose)\"\n echo \"$(msg install.mode.quickstart)\"\n echo \"$(msg install.mode.manual)\"\n echo \"\"\n local ONBOARDING_CHOICE\n read -e -p \"$(msg install.mode.prompt): \" ONBOARDING_CHOICE\n ONBOARDING_CHOICE=\"${ONBOARDING_CHOICE:-1}\"\n if [ \"${ONBOARDING_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n case \"${ONBOARDING_CHOICE}\" in\n 1|quick|quickstart)\n log \"$(msg install.mode.quickstart_selected)\"\n AGENTTEAMS_QUICKSTART=1\n ;;\n 2|manual)\n log \"$(msg install.mode.manual_selected)\"\n ;;\n *)\n log \"$(msg install.mode.invalid)\"\n AGENTTEAMS_QUICKSTART=1\n ;;\n esac\n log \"\"\n}\n\nstep_version() {\n # Skip if version already provided via env var\n if [ -n \"${AGENTTEAMS_VERSION}\" ]; then\n resolve_image_tags\n return 0\n fi\n # Try to fetch the latest stable release from GitHub\n log \"$(msg install.version.fetching)\"\n local _fetched\n _fetched=$(curl -sf --max-time 5 \\\n -H \"Accept: application/vnd.github+json\" \\\n \"https://api.github.com/repos/agentscope-ai/AgentTeams/releases/latest\" \\\n 2>/dev/null | grep '\"tag_name\"' | head -1 | sed 's/.*\"tag_name\": *\"\\([^\"]*\\)\".*/\\1/')\n if [ -n \"${_fetched}\" ]; then\n AGENTTEAMS_KNOWN_STABLE_VERSION=\"${_fetched}\"\n else\n log \"$(msg install.version.fetch_failed \"${AGENTTEAMS_KNOWN_STABLE_VERSION}\")\"\n fi\n log \"$(msg install.version.title)\"\n echo \"\"\n echo \"$(msg install.version.choose)\"\n echo \"$(msg install.version.option_latest)\"\n printf \"%s\\n\" \"$(msg install.version.option_stable \"${AGENTTEAMS_KNOWN_STABLE_VERSION}\")\"\n echo \"$(msg install.version.option_custom)\"\n echo \"\"\n local VERSION_CHOICE\n read -e -p \"$(msg install.version.prompt) [1]: \" VERSION_CHOICE\n VERSION_CHOICE=\"${VERSION_CHOICE:-1}\"\n if [ \"${VERSION_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n case \"${VERSION_CHOICE}\" in\n 1|latest)\n AGENTTEAMS_VERSION=\"latest\"\n log \"$(msg install.version.selected_latest)\"\n ;;\n 2|stable)\n AGENTTEAMS_VERSION=\"${AGENTTEAMS_KNOWN_STABLE_VERSION}\"\n log \"$(msg install.version.selected_stable \"${AGENTTEAMS_VERSION}\")\"\n ;;\n 3|custom)\n local CUSTOM_VERSION\n read -e -p \"$(msg install.version.custom_prompt): \" CUSTOM_VERSION\n AGENTTEAMS_VERSION=\"${CUSTOM_VERSION:-${AGENTTEAMS_KNOWN_STABLE_VERSION}}\"\n log \"$(msg install.version.selected_custom \"${AGENTTEAMS_VERSION}\")\"\n ;;\n *)\n AGENTTEAMS_VERSION=\"${AGENTTEAMS_KNOWN_STABLE_VERSION}\"\n log \"$(msg install.version.invalid \"${AGENTTEAMS_VERSION}\")\"\n ;;\n esac\n log \"\"\n resolve_image_tags\n}\n\nstep_existing() {\n local existing_env=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n log \"$(msg install.existing.detected \"${existing_env}\")\"\n local running_manager=\"\" running_workers=\"\" existing_workers=\"\"\n if ${DOCKER_CMD} ps --format '{{.Names}}' | grep -q \"^agentteams-manager$\"; then\n running_manager=\"agentteams-manager\"\n fi\n running_workers=$(${DOCKER_CMD} ps --format '{{.Names}}' | grep \"^agentteams-worker-\" || true)\n existing_workers=$(${DOCKER_CMD} ps -a --format '{{.Names}}' | grep \"^agentteams-worker-\" || true)\n local UPGRADE_CHOICE\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n log \"$(msg install.existing.upgrade_noninteractive)\"\n UPGRADE_CHOICE=\"upgrade\"\n else\n echo \"\"\n echo \"$(msg install.existing.choose)\"\n echo \"$(msg install.existing.upgrade)\"\n echo \"$(msg install.existing.reinstall)\"\n echo \"$(msg install.existing.cancel)\"\n echo \"\"\n read -e -p \"$(msg install.existing.prompt): \" UPGRADE_CHOICE\n UPGRADE_CHOICE=\"${UPGRADE_CHOICE:-1}\"\n if [ \"${UPGRADE_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n fi\n case \"${UPGRADE_CHOICE}\" in\n 1|upgrade)\n AGENTTEAMS_UPGRADE=1\n log \"$(msg install.existing.upgrading)\"\n # Show upgrade mode sub-menu (unless already set via env var)\n if [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL:-0}\" != \"1\" ]; then\n echo \"\"\n echo \"$(msg upgrade.mode.prompt)\"\n echo \"$(msg upgrade.mode.keep_all)\"\n echo \"$(msg upgrade.mode.confirm_each)\"\n echo \"$(msg upgrade.mode.back)\"\n echo \"\"\n local UPGRADE_MODE_CHOICE\n read -e -p \"$(msg install.existing.prompt): \" UPGRADE_MODE_CHOICE\n UPGRADE_MODE_CHOICE=\"${UPGRADE_MODE_CHOICE:-1}\"\n case \"${UPGRADE_MODE_CHOICE}\" in\n 1|keep)\n AGENTTEAMS_UPGRADE_KEEP_ALL=1\n ;;\n 2|confirm)\n AGENTTEAMS_UPGRADE_KEEP_ALL=0\n ;;\n 3|b)\n STEP_RESULT=\"back\"\n return 0\n ;;\n *)\n AGENTTEAMS_UPGRADE_KEEP_ALL=0\n ;;\n esac\n fi\n # Load current parameters for both Keep-All and confirm-each modes\n load_current_params_from_env\n if [ -n \"${running_manager}\" ] || [ -n \"${running_workers}\" ]; then\n echo \"\"\n echo -e \"\\033[33m$(msg install.existing.warn_manager_stop)\\033[0m\"\n if [ -n \"${existing_workers}\" ]; then\n echo -e \"\\033[33m$(msg install.existing.warn_worker_recreate)\\033[0m\"\n fi\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" != \"1\" ]; then\n echo \"\"\n local CONFIRM_STOP\n read -e -p \"$(msg install.existing.continue_prompt): \" CONFIRM_STOP\n if [ \"${CONFIRM_STOP}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n if [ \"${CONFIRM_STOP}\" != \"y\" ] && [ \"${CONFIRM_STOP}\" != \"Y\" ]; then\n log \"$(msg install.existing.cancelled)\"\n exit 0\n fi\n fi\n fi\n UPGRADE_EXISTING_WORKERS=\"${existing_workers}\"\n ;;\n 2|reinstall)\n log \"$(msg install.reinstall.performing)\"\n local existing_workspace=\"\"\n if [ -f \"${existing_env}\" ]; then\n existing_workspace=$(grep '^AGENTTEAMS_WORKSPACE_DIR=' \"${existing_env}\" 2>/dev/null | cut -d= -f2-)\n fi\n [ -z \"${existing_workspace}\" ] && existing_workspace=\"${HOME}/agentteams-manager\"\n echo \"\"\n echo -e \"\\033[33m$(msg install.reinstall.warn_stop)\\033[0m\"\n [ -n \"${running_manager}\" ] && echo -e \"\\033[33m - ${running_manager} (manager)\\033[0m\"\n for w in ${running_workers}; do\n echo -e \"\\033[33m - ${w} (worker)\\033[0m\"\n done\n echo \"\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_delete)\\033[0m\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_volume)\\033[0m\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_env \"${existing_env}\")\\033[0m\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_workspace \"${existing_workspace}\")\\033[0m\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_workers)\\033[0m\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_proxy)\\033[0m\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_network)\\033[0m\"\n echo \"\"\n echo -e \"\\033[31m$(msg install.reinstall.confirm_type)\\033[0m\"\n echo -e \"\\033[31m ${existing_workspace}\\033[0m\"\n echo \"\"\n local CONFIRM_PATH\n read -e -p \"$(msg install.reinstall.confirm_path): \" CONFIRM_PATH\n if [ \"${CONFIRM_PATH}\" != \"${existing_workspace}\" ]; then\n die \"$(msg install.reinstall.path_mismatch \"${CONFIRM_PATH}\" \"${existing_workspace}\")\"\n fi\n log \"$(msg install.reinstall.confirmed)\"\n ${DOCKER_CMD} stop agentteams-manager 2>/dev/null || true\n ${DOCKER_CMD} rm agentteams-manager 2>/dev/null || true\n for w in $(${DOCKER_CMD} ps -a --format '{{.Names}}' | grep \"^agentteams-worker-\" || true); do\n ${DOCKER_CMD} stop \"${w}\" 2>/dev/null || true\n ${DOCKER_CMD} rm \"${w}\" 2>/dev/null || true\n log \"$(msg install.reinstall.removed_worker \"${w}\")\"\n done\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' | grep -q \"^agentteams-controller$\"; then\n log \"$(msg install.reinstall.removing_proxy)\"\n ${DOCKER_CMD} stop agentteams-controller 2>/dev/null || true\n ${DOCKER_CMD} rm agentteams-controller 2>/dev/null || true\n fi\n if ${DOCKER_CMD} network ls --format '{{.Name}}' | grep -q \"^agentteams-net$\"; then\n log \"$(msg install.reinstall.removing_network)\"\n ${DOCKER_CMD} network rm agentteams-net 2>/dev/null || true\n fi\n if ${DOCKER_CMD} volume ls -q | grep -q \"^agentteams-data$\"; then\n log \"$(msg install.reinstall.removing_volume)\"\n ${DOCKER_CMD} volume rm agentteams-data 2>/dev/null || log \"$(msg install.reinstall.warn_volume_fail)\"\n fi\n if [ -d \"${existing_workspace}\" ]; then\n log \"$(msg install.reinstall.removing_workspace \"${existing_workspace}\")\"\n rm -rf \"${existing_workspace}\" || die \"$(msg install.reinstall.failed_rm_workspace)\"\n fi\n if [ -f \"${existing_env}\" ]; then\n log \"$(msg install.reinstall.removing_env \"${existing_env}\")\"\n rm -f \"${existing_env}\"\n fi\n log \"$(msg install.reinstall.cleanup_done)\"\n unset AGENTTEAMS_WORKSPACE_DIR\n return 0\n ;;\n 3|cancel|*)\n log \"$(msg install.existing.cancelled)\"\n exit 0\n ;;\n esac\n # Load existing env file as fallback (shell env vars take priority)\n if [ -f \"${existing_env}\" ]; then\n log \"$(msg install.loading_config \"${existing_env}\")\"\n while IFS='=' read -r key value; do\n case \"${key}\" in \\#*|\"\") continue ;; esac\n value=\"${value%%#*}\"\n value=\"${value#\"${value%%[![:space:]]*}\"}\"\n value=\"${value%\"${value##*[![:space:]]}\"}\"\n eval \"_existing_val=\\\"\\${${key}+x}\\\"\"\n if [ -z \"${_existing_val}\" ]; then export \"${key}=${value}\"; fi\n done < \"${existing_env}\"\n fi\n}\n\nstep_llm() {\n log \"$(msg llm.title)\"\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n if [ \"${AGENTTEAMS_LANGUAGE}\" = \"zh\" ]; then\n AGENTTEAMS_LLM_PROVIDER=\"${AGENTTEAMS_LLM_PROVIDER:-openai-compat}\"\n AGENTTEAMS_DEFAULT_MODEL=\"${AGENTTEAMS_DEFAULT_MODEL:-qwen3.6-plus}\"\n AGENTTEAMS_OPENAI_BASE_URL=\"${AGENTTEAMS_OPENAI_BASE_URL:-https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1}\"\n log \"$(msg llm.provider.label \"${AGENTTEAMS_LLM_PROVIDER}\")\"\n log \"$(msg llm.openai.base_url_label \"${AGENTTEAMS_OPENAI_BASE_URL}\")\"\n else\n AGENTTEAMS_LLM_PROVIDER=\"${AGENTTEAMS_LLM_PROVIDER:-qwen}\"\n AGENTTEAMS_DEFAULT_MODEL=\"${AGENTTEAMS_DEFAULT_MODEL:-qwen3.6-plus}\"\n AGENTTEAMS_OPENAI_BASE_URL=\"${AGENTTEAMS_OPENAI_BASE_URL:-}\"\n log \"$(msg llm.provider.qwen_default \"${AGENTTEAMS_LLM_PROVIDER}\")\"\n fi\n log \"$(msg llm.model.default \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n prompt AGENTTEAMS_LLM_API_KEY \"$(msg llm.apikey_prompt)\" \"\" \"true\"\n AGENTTEAMS_EMBEDDING_MODEL=\"${AGENTTEAMS_EMBEDDING_MODEL-text-embedding-v4}\"\n export AGENTTEAMS_LLM_PROVIDER AGENTTEAMS_DEFAULT_MODEL AGENTTEAMS_EMBEDDING_MODEL\n [ -n \"${AGENTTEAMS_OPENAI_BASE_URL+x}\" ] && export AGENTTEAMS_OPENAI_BASE_URL\n return 0\n fi\n\n # Upgrade Keep-All mode: skip provider selection, use loaded values\n local SKIP_TO_EMBEDDING=0\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ] && [ -n \"${AGENTTEAMS_LLM_PROVIDER}\" ]; then\n log \"$(msg llm.provider.label \"${AGENTTEAMS_LLM_PROVIDER}\")\"\n log \"$(msg llm.openai.base_url_label \"${AGENTTEAMS_OPENAI_BASE_URL}\")\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n PROVIDER_CHOICE=\"${AGENTTEAMS_LLM_PROVIDER}\"\n SKIP_TO_EMBEDDING=1\n else\n echo \"\"\n echo \"$(msg llm.providers_title)\"\n echo \"$(msg llm.provider.alibaba)\"\n echo \"$(msg llm.provider.openai_compat)\"\n echo \"\"\n local PROVIDER_CHOICE\n # If upgrade mode with loaded provider, show current as default\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_LLM_PROVIDER}\" ]; then\n local _prov_display\n case \"${AGENTTEAMS_LLM_PROVIDER}\" in\n openai-compat) _prov_display=\"2\" ;;\n *) _prov_display=\"1\" ;;\n esac\n read -e -p \"$(msg llm.provider.select) [${_prov_display}]: \" PROVIDER_CHOICE\n PROVIDER_CHOICE=\"${PROVIDER_CHOICE:-${_prov_display}}\"\n elif [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ]; then\n read -e -p \"$(msg llm.provider.select) [1]: \" PROVIDER_CHOICE\n PROVIDER_CHOICE=\"${PROVIDER_CHOICE:-1}\"\n else\n read -e -p \"$(msg llm.provider.select): \" PROVIDER_CHOICE\n PROVIDER_CHOICE=\"${PROVIDER_CHOICE:-1}\"\n fi\n if [ \"${PROVIDER_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n fi\n local ALIBABA_MODEL_CHOICE=\"\"\n local ALIBABA_ACCESS=\"\"\n case \"${PROVIDER_CHOICE}\" in\n 1|alibaba-cloud|openai-compat)\n if [ \"${AGENTTEAMS_LANGUAGE}\" = \"en\" ]; then\n AGENTTEAMS_LLM_PROVIDER=\"openai-compat\"\n AGENTTEAMS_OPENAI_BASE_URL=\"https://dashscope-intl.aliyuncs.com/compatible-mode/v1\"\n ALIBABA_ACCESS=\"tokenplan\"\n echo \"\"\n echo \"$(msg llm.codingplan.models_title)\"\n echo \"$(msg llm.codingplan.model.qwen36plus)\"\n echo \"$(msg llm.codingplan.model.glm5)\"\n echo \"$(msg llm.codingplan.model.kimi)\"\n echo \"$(msg llm.codingplan.model.minimax)\"\n echo \"\"\n local CODINGPLAN_MODEL_CHOICE\n # If upgrade with loaded model, show current as default\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_DEFAULT_MODEL}\" ]; then\n local _model_default\n case \"${AGENTTEAMS_DEFAULT_MODEL}\" in\n qwen3.6-plus) _model_default=\"1\" ;;\n glm-5) _model_default=\"2\" ;;\n kimi-k2.5) _model_default=\"3\" ;;\n MiniMax-M2.5) _model_default=\"4\" ;;\n *) _model_default=\"1\" ;;\n esac\n read -e -p \"$(msg llm.codingplan.model.select) [${_model_default}]: \" CODINGPLAN_MODEL_CHOICE\n CODINGPLAN_MODEL_CHOICE=\"${CODINGPLAN_MODEL_CHOICE:-${_model_default}}\"\n elif [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ]; then\n read -e -p \"$(msg llm.codingplan.model.select) [1]: \" CODINGPLAN_MODEL_CHOICE\n CODINGPLAN_MODEL_CHOICE=\"${CODINGPLAN_MODEL_CHOICE:-1}\"\n else\n read -e -p \"$(msg llm.codingplan.model.select): \" CODINGPLAN_MODEL_CHOICE\n CODINGPLAN_MODEL_CHOICE=\"${CODINGPLAN_MODEL_CHOICE:-1}\"\n fi\n if [ \"${CODINGPLAN_MODEL_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n case \"${CODINGPLAN_MODEL_CHOICE}\" in\n 1|qwen3.6-plus) AGENTTEAMS_DEFAULT_MODEL=\"qwen3.6-plus\" ;;\n 2|glm-5) AGENTTEAMS_DEFAULT_MODEL=\"glm-5\" ;;\n 3|kimi-k2.5) AGENTTEAMS_DEFAULT_MODEL=\"kimi-k2.5\" ;;\n 4|MiniMax-M2.5) AGENTTEAMS_DEFAULT_MODEL=\"MiniMax-M2.5\" ;;\n *) AGENTTEAMS_DEFAULT_MODEL=\"qwen3.6-plus\" ;;\n esac\n log \"$(msg llm.provider.selected_codingplan)\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n else\n echo \"\"\n echo \"$(msg llm.alibaba.models_title)\"\n echo \"$(msg llm.alibaba.model.tokenplan)\"\n echo \"$(msg llm.alibaba.model.bailian)\"\n echo \"$(msg llm.alibaba.model.codingplan_legacy)\"\n echo \"\"\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_OPENAI_BASE_URL}\" ]; then\n # Determine alibaba access type from loaded URL\n local _access_default=\"1\"\n if echo \"${AGENTTEAMS_OPENAI_BASE_URL}\" | grep -q \"token-plan\"; then\n _access_default=\"1\"\n elif echo \"${AGENTTEAMS_OPENAI_BASE_URL}\" | grep -q \"dashscope\"; then\n if echo \"${AGENTTEAMS_OPENAI_BASE_URL}\" | grep -q \"intl\"; then\n _access_default=\"3\"\n else\n _access_default=\"2\"\n fi\n fi\n read -e -p \"$(msg llm.alibaba.model.select) [${_access_default}]: \" ALIBABA_MODEL_CHOICE\n ALIBABA_MODEL_CHOICE=\"${ALIBABA_MODEL_CHOICE:-${_access_default}}\"\n elif [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ]; then\n read -e -p \"$(msg llm.alibaba.model.select) [1]: \" ALIBABA_MODEL_CHOICE\n ALIBABA_MODEL_CHOICE=\"${ALIBABA_MODEL_CHOICE:-1}\"\n else\n read -e -p \"$(msg llm.alibaba.model.select): \" ALIBABA_MODEL_CHOICE\n ALIBABA_MODEL_CHOICE=\"${ALIBABA_MODEL_CHOICE:-1}\"\n fi\n if [ \"${ALIBABA_MODEL_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n case \"${ALIBABA_MODEL_CHOICE}\" in\n 1|token-plan|tokenplan)\n ALIBABA_ACCESS=\"tokenplan\"\n AGENTTEAMS_LLM_PROVIDER=\"openai-compat\"\n AGENTTEAMS_OPENAI_BASE_URL=\"https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1\"\n echo \"\"\n echo \"$(msg llm.codingplan.models_title)\"\n echo \"$(msg llm.codingplan.model.qwen36plus)\"\n echo \"$(msg llm.codingplan.model.glm5)\"\n echo \"$(msg llm.codingplan.model.kimi)\"\n echo \"$(msg llm.codingplan.model.minimax)\"\n echo \"\"\n local CODINGPLAN_MODEL_CHOICE\n # If upgrade with loaded model, show current as default\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_DEFAULT_MODEL}\" ]; then\n local _model_default\n case \"${AGENTTEAMS_DEFAULT_MODEL}\" in\n qwen3.6-plus) _model_default=\"1\" ;;\n glm-5) _model_default=\"2\" ;;\n kimi-k2.5) _model_default=\"3\" ;;\n MiniMax-M2.5) _model_default=\"4\" ;;\n *) _model_default=\"1\" ;;\n esac\n read -e -p \"$(msg llm.codingplan.model.select) [${_model_default}]: \" CODINGPLAN_MODEL_CHOICE\n CODINGPLAN_MODEL_CHOICE=\"${CODINGPLAN_MODEL_CHOICE:-${_model_default}}\"\n elif [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ]; then\n read -e -p \"$(msg llm.codingplan.model.select) [1]: \" CODINGPLAN_MODEL_CHOICE\n CODINGPLAN_MODEL_CHOICE=\"${CODINGPLAN_MODEL_CHOICE:-1}\"\n else\n read -e -p \"$(msg llm.codingplan.model.select): \" CODINGPLAN_MODEL_CHOICE\n CODINGPLAN_MODEL_CHOICE=\"${CODINGPLAN_MODEL_CHOICE:-1}\"\n fi\n if [ \"${CODINGPLAN_MODEL_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n case \"${CODINGPLAN_MODEL_CHOICE}\" in\n 1|qwen3.6-plus) AGENTTEAMS_DEFAULT_MODEL=\"qwen3.6-plus\" ;;\n 2|glm-5) AGENTTEAMS_DEFAULT_MODEL=\"glm-5\" ;;\n 3|kimi-k2.5) AGENTTEAMS_DEFAULT_MODEL=\"kimi-k2.5\" ;;\n 4|MiniMax-M2.5) AGENTTEAMS_DEFAULT_MODEL=\"MiniMax-M2.5\" ;;\n *) AGENTTEAMS_DEFAULT_MODEL=\"qwen3.6-plus\" ;;\n esac\n log \"$(msg llm.provider.selected_tokenplan)\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n ;;\n 2|qwen|bailian)\n ALIBABA_ACCESS=\"bailian\"\n AGENTTEAMS_LLM_PROVIDER=\"qwen\"\n AGENTTEAMS_OPENAI_BASE_URL=\"\"\n echo \"\"\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_DEFAULT_MODEL}\" ]; then\n read -e -p \"$(msg llm.qwen.model_prompt) [${AGENTTEAMS_DEFAULT_MODEL}]: \" AGENTTEAMS_DEFAULT_MODEL\n else\n read -e -p \"$(msg llm.qwen.model_prompt) [qwen3.6-plus]: \" AGENTTEAMS_DEFAULT_MODEL\n fi\n if [ \"${AGENTTEAMS_DEFAULT_MODEL}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_DEFAULT_MODEL=\"${AGENTTEAMS_DEFAULT_MODEL:-qwen3.6-plus}\"\n log \"$(msg llm.provider.selected_qwen)\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n prompt_custom_model_params \"${AGENTTEAMS_DEFAULT_MODEL}\" || return 0\n ;;\n 3|coding-plan|codingplan)\n ALIBABA_ACCESS=\"codingplan_legacy\"\n AGENTTEAMS_LLM_PROVIDER=\"openai-compat\"\n AGENTTEAMS_OPENAI_BASE_URL=\"https://coding.dashscope.aliyuncs.com/v1\"\n echo \"\"\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_DEFAULT_MODEL}\" ]; then\n read -e -p \"$(msg llm.qwen.model_prompt) [${AGENTTEAMS_DEFAULT_MODEL}]: \" AGENTTEAMS_DEFAULT_MODEL\n else\n read -e -p \"$(msg llm.qwen.model_prompt): \" AGENTTEAMS_DEFAULT_MODEL\n fi\n if [ \"${AGENTTEAMS_DEFAULT_MODEL}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_DEFAULT_MODEL=\"${AGENTTEAMS_DEFAULT_MODEL:-qwen3.6-plus}\"\n log \"$(msg llm.provider.selected_codingplan_legacy)\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n prompt_custom_model_params \"${AGENTTEAMS_DEFAULT_MODEL}\" || return 0\n ;;\n *)\n die \"$(msg llm.alibaba.model.invalid \"${ALIBABA_MODEL_CHOICE}\")\"\n ;;\n esac\n fi\n if [ \"${ALIBABA_ACCESS}\" = \"bailian\" ]; then\n log \"$(msg llm.apikey_hint_bailian)\"\n log \"$(msg llm.apikey_url_bailian)\"\n elif [ \"${AGENTTEAMS_LANGUAGE}\" = \"en\" ]; then\n log \"$(msg llm.apikey_hint_qwencloud)\"\n log \"$(msg llm.apikey_url_qwencloud)\"\n elif [ \"${ALIBABA_ACCESS}\" = \"codingplan_legacy\" ]; then\n log \"$(msg llm.apikey_hint_codingplan)\"\n log \"$(msg llm.apikey_url_codingplan)\"\n else\n log \"$(msg llm.apikey_hint_tokenplan)\"\n log \"$(msg llm.apikey_url_tokenplan)\"\n fi\n log \"\"\n prompt AGENTTEAMS_LLM_API_KEY \"$(msg llm.apikey_prompt)\" \"\" \"true\" || return 0\n if [ \"${ALIBABA_ACCESS}\" = \"bailian\" ]; then\n test_llm_connectivity \"https://dashscope.aliyuncs.com/compatible-mode/v1\" \"${AGENTTEAMS_LLM_API_KEY}\" \"${AGENTTEAMS_DEFAULT_MODEL}\" || return 0\n elif [ \"${ALIBABA_ACCESS}\" = \"codingplan_legacy\" ]; then\n test_llm_connectivity \"https://coding.dashscope.aliyuncs.com/v1\" \"${AGENTTEAMS_LLM_API_KEY}\" \"${AGENTTEAMS_DEFAULT_MODEL}\" \"$(msg llm.openai.test.fail.codingplan_legacy)\" || return 0\n elif [ \"${AGENTTEAMS_LANGUAGE}\" = \"en\" ]; then\n test_llm_connectivity \"${AGENTTEAMS_OPENAI_BASE_URL}\" \"${AGENTTEAMS_LLM_API_KEY}\" \"${AGENTTEAMS_DEFAULT_MODEL}\" \"$(msg llm.openai.test.fail.codingplan)\" || return 0\n else\n test_llm_connectivity \"${AGENTTEAMS_OPENAI_BASE_URL}\" \"${AGENTTEAMS_LLM_API_KEY}\" \"${AGENTTEAMS_DEFAULT_MODEL}\" \"$(msg llm.openai.test.fail.tokenplan)\" || return 0\n fi\n ;;\n 2|openai-compat)\n AGENTTEAMS_LLM_PROVIDER=\"openai-compat\"\n log \"$(msg llm.provider.selected_openai \"${AGENTTEAMS_LLM_PROVIDER}\")\"\n echo \"\"\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg llm.openai.base_url_prompt)\" \"${AGENTTEAMS_OPENAI_BASE_URL}\")\"\n else\n _current=\"${AGENTTEAMS_OPENAI_BASE_URL}\"\n if [ -n \"${_current}\" ]; then\n read -e -p \"$(msg llm.openai.base_url_prompt) [${_current}]: \" AGENTTEAMS_OPENAI_BASE_URL\n else\n read -e -p \"$(msg llm.openai.base_url_prompt) [https://api.openai.com/v1]: \" AGENTTEAMS_OPENAI_BASE_URL\n fi\n if [ \"${AGENTTEAMS_OPENAI_BASE_URL}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_OPENAI_BASE_URL=\"${AGENTTEAMS_OPENAI_BASE_URL:-${_current}}\"\n [ -z \"${AGENTTEAMS_OPENAI_BASE_URL}\" ] && AGENTTEAMS_OPENAI_BASE_URL=\"https://api.openai.com/v1\"\n fi\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg llm.openai.model_prompt)\" \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n else\n _current=\"${AGENTTEAMS_DEFAULT_MODEL}\"\n if [ -n \"${_current}\" ]; then\n read -e -p \"$(msg llm.openai.model_prompt) [${_current}]: \" AGENTTEAMS_DEFAULT_MODEL\n else\n read -e -p \"$(msg llm.openai.model_prompt) [gpt-5.4]: \" AGENTTEAMS_DEFAULT_MODEL\n fi\n if [ \"${AGENTTEAMS_DEFAULT_MODEL}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_DEFAULT_MODEL=\"${AGENTTEAMS_DEFAULT_MODEL:-${_current}}\"\n [ -z \"${AGENTTEAMS_DEFAULT_MODEL}\" ] && AGENTTEAMS_DEFAULT_MODEL=\"gpt-5.4\"\n fi\n log \"$(msg llm.openai.base_url_label \"${AGENTTEAMS_OPENAI_BASE_URL}\")\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n prompt_custom_model_params \"${AGENTTEAMS_DEFAULT_MODEL}\" || return 0\n log \"\"\n prompt AGENTTEAMS_LLM_API_KEY \"$(msg llm.apikey_prompt)\" \"\" \"true\" || return 0\n test_llm_connectivity \"${AGENTTEAMS_OPENAI_BASE_URL}\" \"${AGENTTEAMS_LLM_API_KEY}\" \"${AGENTTEAMS_DEFAULT_MODEL}\" || return 0\n ;;\n *)\n die \"$(msg llm.provider.invalid \"${PROVIDER_CHOICE}\")\"\n ;;\n esac\n\n # Skip to embedding if Keep-All mode handled LLM params\n if [ \"${SKIP_TO_EMBEDDING}\" = \"1\" ]; then\n if [ -n \"${AGENTTEAMS_EMBEDDING_MODEL}\" ]; then\n log \"$(msg llm.embedding.title)\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_EMBEDDING_MODEL}\")\"\n log \"\"\n fi\n return 0\n fi\n\n # --- Embedding model (optional, auto-tested) ---\n echo \"\"\n log \"$(msg llm.embedding.title)\"\n log \"$(msg llm.embedding.hint)\"\n echo \"\"\n echo \"$(msg llm.embedding.option.default)\"\n echo \"$(msg llm.embedding.option.custom)\"\n echo \"$(msg llm.embedding.option.disable)\"\n echo \"\"\n local EMB_CHOICE\n read -e -p \"$(msg llm.embedding.select) [1]: \" EMB_CHOICE\n EMB_CHOICE=\"${EMB_CHOICE:-1}\"\n if [ \"${EMB_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n\n case \"${EMB_CHOICE}\" in\n 1)\n AGENTTEAMS_EMBEDDING_MODEL=\"text-embedding-v4\"\n ;;\n 2)\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg llm.embedding.custom_prompt)\" \"${AGENTTEAMS_EMBEDDING_MODEL}\")\"\n else\n _current=\"${AGENTTEAMS_EMBEDDING_MODEL}\"\n if [ -n \"${_current}\" ]; then\n read -e -p \"$(msg llm.embedding.custom_prompt) [${_current}]: \" AGENTTEAMS_EMBEDDING_MODEL\n else\n read -e -p \"$(msg llm.embedding.custom_prompt): \" AGENTTEAMS_EMBEDDING_MODEL\n fi\n if [ \"${AGENTTEAMS_EMBEDDING_MODEL}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_EMBEDDING_MODEL=\"${AGENTTEAMS_EMBEDDING_MODEL:-${_current}}\"\n fi\n ;;\n 3)\n AGENTTEAMS_EMBEDDING_MODEL=\"\"\n log \"$(msg llm.embedding.disabled)\"\n ;;\n *)\n AGENTTEAMS_EMBEDDING_MODEL=\"text-embedding-v4\"\n ;;\n esac\n\n if [ -n \"${AGENTTEAMS_EMBEDDING_MODEL}\" ]; then\n # Qwen provider uses dashscope directly; others use OPENAI_BASE_URL\n local EMB_BASE_URL=\"${AGENTTEAMS_OPENAI_BASE_URL}\"\n if [ \"${AGENTTEAMS_LLM_PROVIDER}\" = \"qwen\" ]; then\n EMB_BASE_URL=\"https://dashscope.aliyuncs.com/compatible-mode/v1\"\n fi\n if ! test_embedding_connectivity \"${EMB_BASE_URL}\" \"${AGENTTEAMS_LLM_API_KEY}\" \"${AGENTTEAMS_EMBEDDING_MODEL}\"; then\n AGENTTEAMS_EMBEDDING_MODEL=\"\"\n log \"$(msg llm.embedding.auto_disabled)\"\n fi\n fi\n\n export AGENTTEAMS_LLM_PROVIDER AGENTTEAMS_DEFAULT_MODEL\n [ -n \"${AGENTTEAMS_OPENAI_BASE_URL+x}\" ] && export AGENTTEAMS_OPENAI_BASE_URL\n log \"\"\n}\n\nstep_admin() {\n log \"$(msg admin.title)\"\n prompt AGENTTEAMS_ADMIN_USER \"$(msg admin.username_prompt)\" \"admin\" || return 0\n AGENTTEAMS_ADMIN_USER=\"$(printf '%s' \"${AGENTTEAMS_ADMIN_USER}\" | tr '[:upper:]' '[:lower:]')\"\n\n # Pre-set via env var: validate; in non-interactive mode fail fast,\n # in interactive mode warn and fall through to the retry prompt.\n if [ -n \"${AGENTTEAMS_ADMIN_PASSWORD:-}\" ]; then\n log \" $(msg prompt.preset \"$(msg admin.password_prompt)\")\"\n if [ ${#AGENTTEAMS_ADMIN_PASSWORD} -ge 8 ]; then\n log \"\"\n return 0\n fi\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n die \"$(msg admin.password_too_short \"${#AGENTTEAMS_ADMIN_PASSWORD}\")\"\n fi\n error \"$(msg admin.password_too_short \"${#AGENTTEAMS_ADMIN_PASSWORD}\")\"\n unset AGENTTEAMS_ADMIN_PASSWORD\n fi\n\n while true; do\n unset AGENTTEAMS_ADMIN_PASSWORD\n prompt_optional AGENTTEAMS_ADMIN_PASSWORD \"$(msg admin.password_prompt)\" \"true\" || return 0\n if [ -z \"${AGENTTEAMS_ADMIN_PASSWORD}\" ]; then\n AGENTTEAMS_ADMIN_PASSWORD=\"admin$(openssl rand -hex 6)\"\n log \"$(msg admin.password_generated)\"\n break\n fi\n if [ ${#AGENTTEAMS_ADMIN_PASSWORD} -ge 8 ]; then\n break\n fi\n error \"$(msg admin.password_too_short \"${#AGENTTEAMS_ADMIN_PASSWORD}\")\"\n done\n log \"\"\n}\n\nstep_network() {\n log \"$(msg port.local_only.title)\"\n echo \"\"\n echo \" 1) $(msg port.local_only.hint_yes)\"\n echo \" 2) $(msg port.local_only.hint_no)\"\n echo \"\"\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_LOCAL_ONLY=\"${AGENTTEAMS_LOCAL_ONLY:-1}\"\n elif [ -z \"${AGENTTEAMS_LOCAL_ONLY+x}\" ]; then\n local _local_choice\n read -e -p \"$(msg port.local_only.choice): \" _local_choice\n if [ \"${_local_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _local_choice=\"${_local_choice:-1}\"\n case \"${_local_choice}\" in\n 2|n|N|no|NO) AGENTTEAMS_LOCAL_ONLY=\"0\" ;;\n *) AGENTTEAMS_LOCAL_ONLY=\"1\" ;;\n esac\n fi\n export AGENTTEAMS_LOCAL_ONLY\n if [ \"${AGENTTEAMS_LOCAL_ONLY}\" = \"1\" ]; then\n log \"$(msg port.local_only.selected_local)\"\n else\n log \"$(msg port.local_only.selected_external)\"\n echo \"\"\n echo -e \"\\033[33m$(msg port.local_only.https_hint)\\033[0m\"\n fi\n}\n\nstep_ports() {\n log \"$(msg port.title)\"\n prompt AGENTTEAMS_PORT_GATEWAY \"$(msg port.gateway_prompt)\" \"18080\" || return 0\n prompt AGENTTEAMS_PORT_CONSOLE \"$(msg port.console_prompt)\" \"18001\" || return 0\n prompt AGENTTEAMS_PORT_ELEMENT_WEB \"$(msg port.element_prompt)\" \"18088\" || return 0\n prompt AGENTTEAMS_PORT_MANAGER_CONSOLE \"$(msg port.manager_console_prompt)\" \"18888\" || return 0\n log \"\"\n}\n\nstep_domains() {\n log \"$(msg domain.title)\"\n log \"$(msg domain.hint)\"\n prompt AGENTTEAMS_MATRIX_DOMAIN \"$(msg domain.matrix_prompt)\" \"matrix-local.agentteams.io:${AGENTTEAMS_PORT_GATEWAY}\" || return 0\n prompt AGENTTEAMS_MATRIX_CLIENT_DOMAIN \"$(msg domain.element_prompt)\" \"matrix-client-local.agentteams.io\" || return 0\n prompt AGENTTEAMS_AI_GATEWAY_DOMAIN \"$(msg domain.gateway_prompt)\" \"aigw-local.agentteams.io\" || return 0\n prompt AGENTTEAMS_FS_DOMAIN \"$(msg domain.fs_prompt)\" \"fs-local.agentteams.io\" || return 0\n if [ \"${AGENTTEAMS_MANAGER_RUNTIME}\" != \"copaw\" ]; then\n prompt AGENTTEAMS_CONSOLE_DOMAIN \"$(msg domain.console_prompt)\" \"console-local.agentteams.io\" || return 0\n fi\n log \"\"\n}\n\nstep_github() {\n log \"$(msg github.title)\"\n prompt_optional AGENTTEAMS_GITHUB_TOKEN \"$(msg github.token_prompt)\" \"true\" || return 0\n}\n\nstep_skills() {\n log \"\"\n log \"$(msg skills.title)\"\n prompt_optional AGENTTEAMS_SKILLS_API_URL \"$(msg skills.url_prompt)\" || return 0\n log \"\"\n}\n\nstep_volume() {\n log \"$(msg data.title)\"\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_DATA_DIR=\"${AGENTTEAMS_DATA_DIR:-agentteams-data}\"\n log \" $(msg data.volume_using \"${AGENTTEAMS_DATA_DIR}\") (non-interactive, skipped)\"\n export AGENTTEAMS_DATA_DIR\n return 0\n fi\n # ─────────────────────────────────────────────────────────────────\n if [ -z \"${AGENTTEAMS_DATA_DIR+x}\" ]; then\n local _input\n read -e -p \"$(msg data.volume_prompt): \" _input\n if [ \"${_input}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_DATA_DIR=\"${_input:-agentteams-data}\"\n export AGENTTEAMS_DATA_DIR\n fi\n AGENTTEAMS_DATA_DIR=\"${AGENTTEAMS_DATA_DIR:-agentteams-data}\"\n log \"$(msg data.volume_using \"${AGENTTEAMS_DATA_DIR}\")\"\n}\n\nstep_workspace() {\n log \"$(msg workspace.title)\"\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_WORKSPACE_DIR=\"${AGENTTEAMS_WORKSPACE_DIR:-${HOME}/agentteams-manager}\"\n AGENTTEAMS_WORKSPACE_DIR=\"$(cd \"${AGENTTEAMS_WORKSPACE_DIR}\" 2>/dev/null && pwd || echo \"${AGENTTEAMS_WORKSPACE_DIR}\")\"\n mkdir -p \"${AGENTTEAMS_WORKSPACE_DIR}\"\n log \" $(msg workspace.dir_label \"${AGENTTEAMS_WORKSPACE_DIR}\") (non-interactive, skipped)\"\n export AGENTTEAMS_WORKSPACE_DIR\n return 0\n fi\n # ─────────────────────────────────────────────────────────────────\n local _input\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg workspace.dir_prompt \"${HOME}/agentteams-manager\")\" \"${AGENTTEAMS_WORKSPACE_DIR}\")\"\n _input=\"${AGENTTEAMS_WORKSPACE_DIR}\"\n else\n local _current=\"${AGENTTEAMS_WORKSPACE_DIR}\"\n if [ -n \"${_current}\" ]; then\n read -e -p \"$(msg workspace.dir_prompt \"${HOME}/agentteams-manager\") [${_current}]: \" _input\n else\n read -e -p \"$(msg workspace.dir_prompt \"${HOME}/agentteams-manager\"): \" _input\n fi\n if [ \"${_input}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _input=\"${_input:-${_current}}\"\n fi\n AGENTTEAMS_WORKSPACE_DIR=\"${_input:-${HOME}/agentteams-manager}\"\n export AGENTTEAMS_WORKSPACE_DIR\n AGENTTEAMS_WORKSPACE_DIR=\"$(cd \"${AGENTTEAMS_WORKSPACE_DIR}\" 2>/dev/null && pwd || echo \"${AGENTTEAMS_WORKSPACE_DIR}\")\"\n mkdir -p \"${AGENTTEAMS_WORKSPACE_DIR}\"\n log \"$(msg workspace.dir_label \"${AGENTTEAMS_WORKSPACE_DIR}\")\"\n}\n\nstep_dashboard() {\n AGENTTEAMS_DASHBOARD=\"${AGENTTEAMS_DASHBOARD:-1}\"\n AGENTTEAMS_DASHBOARD_VERSION=\"${AGENTTEAMS_DASHBOARD_VERSION:-v1.2.0-beta.1}\"\n AGENTTEAMS_PORT_DASHBOARD=\"${AGENTTEAMS_PORT_DASHBOARD:-13000}\"\n AGENTTEAMS_AI_GATEWAY_ADMIN_URL=\"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL:-}\"\n\n # Compute the default image for the current version.\n # We track whether the current image matches the derived default so that\n # changing the version also updates the image (unless the user has\n # explicitly overridden it to a non-default value).\n _dashboard_default_image() {\n echo \"${AGENTTEAMS_REGISTRY}/agentteams/agentteams-dashboard:${1:-${AGENTTEAMS_DASHBOARD_VERSION}}\"\n }\n local _init_default\n _init_default=$(_dashboard_default_image)\n if [ -z \"${AGENTTEAMS_DASHBOARD_IMAGE:-}\" ]; then\n AGENTTEAMS_DASHBOARD_IMAGE=\"${_init_default}\"\n fi\n\n # Recompute default image when the version changes AND the current image\n # matches the default for the *previous* (env-file) version.\n # This handles non-interactive upgrades correctly:\n # - env file loads old default image + old version\n # - CLI passes new version\n # - image should follow (because it was auto-derived before)\n # But preserves truly custom images (same repo but custom tag like :canary)\n # because they won't match the old-version default exactly.\n local _old_saved_version=\"\"\n local _env_file=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n if [ -f \"${_env_file}\" ]; then\n _old_saved_version=\"$(grep '^AGENTTEAMS_DASHBOARD_VERSION=' \"${_env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n fi\n if [ -n \"${_old_saved_version}\" ] && \\\n [ \"${_old_saved_version}\" != \"${AGENTTEAMS_DASHBOARD_VERSION}\" ] && \\\n [ \"${AGENTTEAMS_DASHBOARD_IMAGE}\" = \"$(_dashboard_default_image \"${_old_saved_version}\")\" ]; then\n AGENTTEAMS_DASHBOARD_IMAGE=$(_dashboard_default_image)\n fi\n\n # Quick Start: accept all defaults without prompting. The dashboard is\n # installed by default (AGENTTEAMS_DASHBOARD=1); set AGENTTEAMS_DASHBOARD=0\n # to opt out.\n if [ \"${AGENTTEAMS_QUICKSTART:-0}\" = \"1\" ]; then\n if [ \"${AGENTTEAMS_DASHBOARD}\" = \"1\" ]; then\n log \"$(msg dash.summary \"${AGENTTEAMS_PORT_DASHBOARD}\" \"${AGENTTEAMS_DASHBOARD_IMAGE}\") (quick start)\"\n else\n log \"$(msg dash.skip) (quick start)\"\n fi\n export AGENTTEAMS_DASHBOARD AGENTTEAMS_DASHBOARD_VERSION AGENTTEAMS_PORT_DASHBOARD AGENTTEAMS_DASHBOARD_IMAGE AGENTTEAMS_AI_GATEWAY_ADMIN_URL\n return 0\n fi\n\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_DASHBOARD=\"${AGENTTEAMS_DASHBOARD:-1}\"\n if [ \"${AGENTTEAMS_DASHBOARD}\" = \"1\" ]; then\n log \"$(msg dash.summary \"${AGENTTEAMS_PORT_DASHBOARD}\" \"${AGENTTEAMS_DASHBOARD_IMAGE}\") (non-interactive)\"\n else\n log \"$(msg dash.skip) (non-interactive)\"\n fi\n export AGENTTEAMS_DASHBOARD AGENTTEAMS_DASHBOARD_VERSION AGENTTEAMS_PORT_DASHBOARD AGENTTEAMS_DASHBOARD_IMAGE AGENTTEAMS_AI_GATEWAY_ADMIN_URL\n return 0\n fi\n\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ]; then\n log \"$(msg prompt.upgrade_keep \"\" \"${AGENTTEAMS_DASHBOARD}\") dashboard=${AGENTTEAMS_DASHBOARD} version=${AGENTTEAMS_DASHBOARD_VERSION} port=${AGENTTEAMS_PORT_DASHBOARD}\"\n export AGENTTEAMS_DASHBOARD AGENTTEAMS_DASHBOARD_VERSION AGENTTEAMS_PORT_DASHBOARD AGENTTEAMS_DASHBOARD_IMAGE AGENTTEAMS_AI_GATEWAY_ADMIN_URL\n return 0\n fi\n\n local _input\n echo \"\"\n read -p \"$(msg dash.prompt) [Y/n]: \" _input\n case \"${_input:-}\" in\n [nN0]) AGENTTEAMS_DASHBOARD=0 ;;\n *) AGENTTEAMS_DASHBOARD=1 ;;\n esac\n if [ \"${_input}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n\n if [ \"${AGENTTEAMS_DASHBOARD}\" != \"1\" ]; then\n log \"$(msg dash.skip)\"\n export AGENTTEAMS_DASHBOARD AGENTTEAMS_DASHBOARD_VERSION AGENTTEAMS_PORT_DASHBOARD AGENTTEAMS_DASHBOARD_IMAGE AGENTTEAMS_AI_GATEWAY_ADMIN_URL\n return 0\n fi\n\n # Dashboard version\n local _current_version=\"${AGENTTEAMS_DASHBOARD_VERSION}\"\n local _old_version=\"${AGENTTEAMS_DASHBOARD_VERSION}\"\n local _old_default_image\n _old_default_image=$(_dashboard_default_image \"${_old_version}\")\n read -p \"$(msg dash.version_prompt) [${_current_version}]: \" _input\n AGENTTEAMS_DASHBOARD_VERSION=\"${_input:-${_current_version}}\"\n\n # Dashboard port\n local _current_port=\"${AGENTTEAMS_PORT_DASHBOARD}\"\n read -p \"$(msg dash.port_prompt) [${_current_port}]: \" _input\n AGENTTEAMS_PORT_DASHBOARD=\"${_input:-${_current_port}}\"\n\n # Dashboard image: if the current image matches the default derived from\n # the OLD version, recompute it from the NEW version. This handles both\n # fresh installs and upgrades where the previous image was the default.\n # Only a real user override (image != default for old version) is preserved.\n if [ \"${AGENTTEAMS_DASHBOARD_VERSION}\" != \"${_old_version}\" ] && \\\n [ \"${AGENTTEAMS_DASHBOARD_IMAGE}\" = \"${_old_default_image}\" ]; then\n AGENTTEAMS_DASHBOARD_IMAGE=$(_dashboard_default_image)\n fi\n local _current_image=\"${AGENTTEAMS_DASHBOARD_IMAGE}\"\n read -p \"$(msg dash.image_prompt) [${_current_image}]: \" _input\n if [ -n \"${_input}\" ]; then\n AGENTTEAMS_DASHBOARD_IMAGE=\"${_input}\"\n fi\n\n # Higress Console URL (explicit config takes priority; auto-detect as fallback)\n local _higress_url=\"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\"\n if [ -z \"${_higress_url}\" ]; then\n if ${DOCKER_CMD} ps --format '{{.Names}}' | grep -q \"^agentteams-controller$\"; then\n if ${DOCKER_CMD} exec agentteams-controller curl -sf --max-time 2 http://127.0.0.1:8001/ >/dev/null 2>&1; then\n _higress_url=\"http://agentteams-controller:8001\"\n log \"$(msg dash.auto_url): ${_higress_url}\"\n else\n log \"$(msg dash.auto_url_fail)\"\n fi\n fi\n fi\n read -p \"$(msg dash.gateway_prompt) [${_higress_url:-<auto>}]: \" _input\n AGENTTEAMS_AI_GATEWAY_ADMIN_URL=\"${_input:-${_higress_url}}\"\n\n # Normalize URL: add http:// prefix if missing\n if [ -n \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\" ]; then\n case \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\" in\n http://*|https://*) ;;\n *) AGENTTEAMS_AI_GATEWAY_ADMIN_URL=\"http://${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\" ;;\n esac\n fi\n\n # Verify the Higress Console URL is reachable (best-effort warning only)\n if [ -n \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\" ]; then\n local _gw_reachable=0\n # If the URL points to the controller container, test from inside the container\n if echo \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\" | grep -qE \"(agentteams-controller|hiclaw-controller|127\\.0\\.0\\.1|localhost)\"; then\n if ${DOCKER_CMD} ps --format '{{.Names}}' | grep -q \"^agentteams-controller$\"; then\n if ${DOCKER_CMD} exec agentteams-controller curl -sf --max-time 3 \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}/\" >/dev/null 2>&1; then\n _gw_reachable=1\n fi\n fi\n else\n # External URL — probe from inside the controller (same network as\n # the Dashboard container) when possible; fall back to the host.\n if ${DOCKER_CMD} ps --format '{{.Names}}' | grep -q \"^agentteams-controller$\"; then\n if ${DOCKER_CMD} exec agentteams-controller curl -sf --max-time 3 \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}/\" >/dev/null 2>&1; then\n _gw_reachable=1\n fi\n elif curl -sf --max-time 3 \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}/\" >/dev/null 2>&1; then\n _gw_reachable=1\n fi\n fi\n if [ \"${_gw_reachable}\" = \"0\" ]; then\n log \"WARNING: Higress Console URL may not be reachable: ${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\"\n log \" Dashboard will still work, but shared login via Higress Console may fail.\"\n log \" To fix: verify the URL is correct and the Higress Console service is running.\"\n fi\n fi\n\n log \"$(msg dash.summary \"${AGENTTEAMS_PORT_DASHBOARD}\" \"${AGENTTEAMS_DASHBOARD_IMAGE}\")\"\n export AGENTTEAMS_DASHBOARD AGENTTEAMS_DASHBOARD_VERSION AGENTTEAMS_PORT_DASHBOARD AGENTTEAMS_DASHBOARD_IMAGE AGENTTEAMS_AI_GATEWAY_ADMIN_URL\n}\n\nstep_runtime() {\n log \"$(msg worker_runtime.title)\"\n echo \"\"\n echo \" 1) $(msg worker_runtime.copaw)\"\n echo \" 2) $(msg worker_runtime.openclaw)\"\n if ! _ver_lt \"${AGENTTEAMS_VERSION}\" \"v1.1.0\"; then\n echo \" 3) $(msg worker_runtime.hermes)\"\n fi\n echo \"\"\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"${AGENTTEAMS_DEFAULT_WORKER_RUNTIME:-copaw}\"\n elif [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_DEFAULT_WORKER_RUNTIME}\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg worker_runtime.title_short)\" \"${AGENTTEAMS_DEFAULT_WORKER_RUNTIME}\")\"\n local _runtime_choice\n read -e -p \"$(msg worker_runtime.choice): \" _runtime_choice\n if [ \"${_runtime_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n if [ -n \"${_runtime_choice}\" ]; then\n case \"${_runtime_choice}\" in\n 2) AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"openclaw\" ;;\n 3) if ! _ver_lt \"${AGENTTEAMS_VERSION}\" \"v1.1.0\"; then\n AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"hermes\"\n else\n AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"copaw\"\n fi ;;\n *) AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"copaw\" ;;\n esac\n fi\n elif [ -z \"${AGENTTEAMS_DEFAULT_WORKER_RUNTIME+x}\" ]; then\n local _runtime_choice\n read -e -p \"$(msg worker_runtime.choice): \" _runtime_choice\n if [ \"${_runtime_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _runtime_choice=\"${_runtime_choice:-1}\"\n case \"${_runtime_choice}\" in\n 2) AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"openclaw\" ;;\n 3) if ! _ver_lt \"${AGENTTEAMS_VERSION}\" \"v1.1.0\"; then\n AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"hermes\"\n else\n AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"copaw\"\n fi ;;\n *) AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"copaw\" ;;\n esac\n fi\n export AGENTTEAMS_DEFAULT_WORKER_RUNTIME\n log \"$(msg worker_runtime.selected \"${AGENTTEAMS_DEFAULT_WORKER_RUNTIME}\")\"\n}\n\nstep_manager_runtime() {\n log \"$(msg manager_runtime.title)\"\n echo \"\"\n echo \" 1) $(msg manager_runtime.copaw)\"\n echo \" 2) $(msg manager_runtime.openclaw)\"\n echo \"\"\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_MANAGER_RUNTIME=\"${AGENTTEAMS_MANAGER_RUNTIME:-copaw}\"\n elif [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_MANAGER_RUNTIME}\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg manager_runtime.title_short)\" \"${AGENTTEAMS_MANAGER_RUNTIME}\")\"\n local _runtime_choice\n read -e -p \"$(msg manager_runtime.choice): \" _runtime_choice\n if [ \"${_runtime_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n if [ -n \"${_runtime_choice}\" ]; then\n case \"${_runtime_choice}\" in\n 2) AGENTTEAMS_MANAGER_RUNTIME=\"openclaw\" ;;\n *) AGENTTEAMS_MANAGER_RUNTIME=\"copaw\" ;;\n esac\n fi\n elif [ -z \"${AGENTTEAMS_MANAGER_RUNTIME+x}\" ]; then\n local _runtime_choice\n read -e -p \"$(msg manager_runtime.choice): \" _runtime_choice\n if [ \"${_runtime_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _runtime_choice=\"${_runtime_choice:-1}\"\n case \"${_runtime_choice}\" in\n 2) AGENTTEAMS_MANAGER_RUNTIME=\"openclaw\" ;;\n *) AGENTTEAMS_MANAGER_RUNTIME=\"copaw\" ;;\n esac\n fi\n export AGENTTEAMS_MANAGER_RUNTIME\n log \"$(msg manager_runtime.selected \"${AGENTTEAMS_MANAGER_RUNTIME}\")\"\n}\n\nstep_e2ee() {\n log \"\"\n log \"$(msg matrix_e2ee.title)\"\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_MATRIX_E2EE=\"${AGENTTEAMS_MATRIX_E2EE:-0}\"\n log \" $(msg matrix_e2ee.title_short) = ${AGENTTEAMS_MATRIX_E2EE} (non-interactive, skipped)\"\n export AGENTTEAMS_MATRIX_E2EE\n return 0\n fi\n # ─────────────────────────────────────────────────────────────────\n echo \"\"\n echo -e \" $(msg matrix_e2ee.desc)\"\n echo \"\"\n echo \" 1) $(msg matrix_e2ee.disable)\"\n echo \" 2) $(msg matrix_e2ee.enable)\"\n echo \"\"\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_MATRIX_E2EE}\" ]; then\n local _e2ee_display; if [ \"${AGENTTEAMS_MATRIX_E2EE}\" = \"1\" ]; then _e2ee_display=\"$(msg matrix_e2ee.val_enabled)\"; else _e2ee_display=\"$(msg matrix_e2ee.val_disabled)\"; fi\n log \"$(msg prompt.upgrade_keep \"$(msg matrix_e2ee.title_short)\" \"${_e2ee_display}\")\"\n local _e2ee_choice\n read -e -p \"$(msg matrix_e2ee.choice): \" _e2ee_choice\n if [ \"${_e2ee_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n if [ -n \"${_e2ee_choice}\" ]; then\n case \"${_e2ee_choice}\" in\n 2) AGENTTEAMS_MATRIX_E2EE=\"1\" ;;\n *) AGENTTEAMS_MATRIX_E2EE=\"0\" ;;\n esac\n fi\n elif [ -z \"${AGENTTEAMS_MATRIX_E2EE+x}\" ]; then\n local _e2ee_choice\n read -e -p \"$(msg matrix_e2ee.choice): \" _e2ee_choice\n if [ \"${_e2ee_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _e2ee_choice=\"${_e2ee_choice:-1}\"\n case \"${_e2ee_choice}\" in\n 2) AGENTTEAMS_MATRIX_E2EE=\"1\" ;;\n *) AGENTTEAMS_MATRIX_E2EE=\"0\" ;;\n esac\n fi\n AGENTTEAMS_MATRIX_E2EE=\"${AGENTTEAMS_MATRIX_E2EE:-0}\"\n export AGENTTEAMS_MATRIX_E2EE\n if [ \"${AGENTTEAMS_MATRIX_E2EE}\" = \"1\" ]; then\n log \"$(msg matrix_e2ee.selected_enabled)\"\n else\n log \"$(msg matrix_e2ee.selected_disabled)\"\n fi\n}\n\nstep_docker_proxy() {\n # Only relevant when socket mounting is enabled\n if [ \"${AGENTTEAMS_MOUNT_SOCKET}\" != \"1\" ]; then\n AGENTTEAMS_DOCKER_PROXY=\"0\"\n return 0\n fi\n\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_DOCKER_PROXY=\"${AGENTTEAMS_DOCKER_PROXY:-0}\"\n log \" $(msg docker_proxy.title_short) = ${AGENTTEAMS_DOCKER_PROXY} (non-interactive, skipped)\"\n export AGENTTEAMS_DOCKER_PROXY\n return 0\n fi\n # ─────────────────────────────────────────────────────────────────\n\n echo \"\"\n echo -e \" \\033[1m$(msg docker_proxy.title)\\033[0m\"\n echo \"\"\n echo -e \" $(msg docker_proxy.desc)\"\n echo \"\"\n echo \" 1) $(msg docker_proxy.enable)\"\n echo \" 2) $(msg docker_proxy.disable)\"\n echo \"\"\n\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_DOCKER_PROXY}\" ]; then\n local _proxy_display; if [ \"${AGENTTEAMS_DOCKER_PROXY}\" = \"1\" ]; then _proxy_display=\"$(msg docker_proxy.val_enabled)\"; else _proxy_display=\"$(msg docker_proxy.val_disabled)\"; fi\n log \"$(msg prompt.upgrade_keep \"$(msg docker_proxy.title_short)\" \"${_proxy_display}\")\"\n local _choice\n read -e -p \"$(msg docker_proxy.choice): \" _choice\n if [ \"${_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n if [ -n \"${_choice}\" ]; then\n case \"${_choice}\" in\n 2) AGENTTEAMS_DOCKER_PROXY=\"0\" ;;\n *) AGENTTEAMS_DOCKER_PROXY=\"1\" ;;\n esac\n fi\n elif [ -z \"${AGENTTEAMS_DOCKER_PROXY+x}\" ]; then\n local _choice\n read -e -p \"$(msg docker_proxy.choice): \" _choice\n if [ \"${_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _choice=\"${_choice:-1}\"\n case \"${_choice}\" in\n 2) AGENTTEAMS_DOCKER_PROXY=\"0\" ;;\n *) AGENTTEAMS_DOCKER_PROXY=\"1\" ;;\n esac\n fi\n AGENTTEAMS_DOCKER_PROXY=\"${AGENTTEAMS_DOCKER_PROXY:-1}\"\n export AGENTTEAMS_DOCKER_PROXY\n if [ \"${AGENTTEAMS_DOCKER_PROXY}\" = \"1\" ]; then\n log \"$(msg docker_proxy.selected_enabled)\"\n\n # Prompt for additional allowed image sources\n echo \"\"\n echo -e \" $(msg docker_proxy.registries_desc)\"\n echo \"\"\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_PROXY_ALLOWED_REGISTRIES}\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg docker_proxy.registries_label)\" \"${AGENTTEAMS_PROXY_ALLOWED_REGISTRIES}\")\"\n local _reg_input\n read -e -p \"$(msg docker_proxy.registries_prompt): \" _reg_input\n if [ \"${_reg_input}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n [ -n \"${_reg_input}\" ] && AGENTTEAMS_PROXY_ALLOWED_REGISTRIES=\"${_reg_input}\"\n elif [ -z \"${AGENTTEAMS_PROXY_ALLOWED_REGISTRIES+x}\" ]; then\n local _reg_input\n read -e -p \"$(msg docker_proxy.registries_prompt): \" _reg_input\n if [ \"${_reg_input}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_PROXY_ALLOWED_REGISTRIES=\"${_reg_input:-}\"\n fi\n export AGENTTEAMS_PROXY_ALLOWED_REGISTRIES\n else\n log \"$(msg docker_proxy.selected_disabled)\"\n fi\n}\n\nstep_idle() {\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_WORKER_IDLE_TIMEOUT=\"${AGENTTEAMS_WORKER_IDLE_TIMEOUT:-720}\"\n log \" $(msg idle_timeout.label) = ${AGENTTEAMS_WORKER_IDLE_TIMEOUT} (non-interactive, skipped)\"\n export AGENTTEAMS_WORKER_IDLE_TIMEOUT\n return 0\n fi\n # ─────────────────────────────────────────────────────────────────\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_WORKER_IDLE_TIMEOUT}\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg idle_timeout.label)\" \"${AGENTTEAMS_WORKER_IDLE_TIMEOUT}\")\"\n local _idle_timeout\n read -e -p \"$(msg idle_timeout.prompt): \" _idle_timeout\n if [ \"${_idle_timeout}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n [ -n \"${_idle_timeout}\" ] && AGENTTEAMS_WORKER_IDLE_TIMEOUT=\"${_idle_timeout}\"\n elif [ -z \"${AGENTTEAMS_WORKER_IDLE_TIMEOUT+x}\" ]; then\n local _idle_timeout\n read -e -p \"$(msg idle_timeout.prompt): \" _idle_timeout\n if [ \"${_idle_timeout}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_WORKER_IDLE_TIMEOUT=\"${_idle_timeout:-720}\"\n fi\n AGENTTEAMS_WORKER_IDLE_TIMEOUT=\"${AGENTTEAMS_WORKER_IDLE_TIMEOUT:-720}\"\n export AGENTTEAMS_WORKER_IDLE_TIMEOUT\n log \"$(msg idle_timeout.selected \"${AGENTTEAMS_WORKER_IDLE_TIMEOUT}\")\"\n}\n\nstep_hostshare() {\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_HOST_SHARE_DIR=\"${AGENTTEAMS_HOST_SHARE_DIR:-${HOME}}\"\n log \" $(msg host_share.label) = ${AGENTTEAMS_HOST_SHARE_DIR} (non-interactive, skipped)\"\n export AGENTTEAMS_HOST_SHARE_DIR\n return 0\n fi\n # ─────────────────────────────────────────────────────────────────\n local _current=\"${AGENTTEAMS_HOST_SHARE_DIR}\"\n local _share_dir\n if [ -n \"${_current}\" ]; then\n read -e -p \"$(msg host_share.prompt \"$HOME\") [${_current}]: \" _share_dir\n else\n read -e -p \"$(msg host_share.prompt \"$HOME\"): \" _share_dir\n fi\n if [ \"${_share_dir}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_HOST_SHARE_DIR=\"${_share_dir:-${_current:-$HOME}}\"\n export AGENTTEAMS_HOST_SHARE_DIR\n}\n\nstep_podman_autostart() {\n echo \"\"\n log \"$(msg podman.autostart.title)\"\n echo \"\"\n echo -e \" $(msg podman.autostart.desc)\"\n echo \"\"\n echo \" 1) $(msg podman.autostart.enable)\"\n echo \" 2) $(msg podman.autostart.disable)\"\n echo \"\"\n\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_PODMAN_AUTOSTART}\" ]; then\n local _disp; if [ \"${AGENTTEAMS_PODMAN_AUTOSTART}\" = \"1\" ]; then _disp=\"$(msg podman.autostart.val_enabled)\"; else _disp=\"$(msg podman.autostart.val_disabled)\"; fi\n log \"$(msg prompt.upgrade_keep \"$(msg podman.autostart.title_short)\" \"${_disp}\")\"\n local _choice\n read -e -p \"$(msg podman.autostart.choice): \" _choice\n if [ \"${_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n if [ -n \"${_choice}\" ]; then\n case \"${_choice}\" in\n 2) AGENTTEAMS_PODMAN_AUTOSTART=\"0\" ;;\n *) AGENTTEAMS_PODMAN_AUTOSTART=\"1\" ;;\n esac\n fi\n elif [ -z \"${AGENTTEAMS_PODMAN_AUTOSTART+x}\" ]; then\n local _choice\n read -e -p \"$(msg podman.autostart.choice) [1]: \" _choice\n if [ \"${_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _choice=\"${_choice:-1}\"\n case \"${_choice}\" in\n 2) AGENTTEAMS_PODMAN_AUTOSTART=\"0\" ;;\n *) AGENTTEAMS_PODMAN_AUTOSTART=\"1\" ;;\n esac\n fi\n AGENTTEAMS_PODMAN_AUTOSTART=\"${AGENTTEAMS_PODMAN_AUTOSTART:-1}\"\n export AGENTTEAMS_PODMAN_AUTOSTART\n if [ \"${AGENTTEAMS_PODMAN_AUTOSTART}\" = \"1\" ]; then\n log \"$(msg podman.autostart.selected_enabled)\"\n else\n log \"$(msg podman.autostart.selected_disabled)\"\n fi\n}\n\n# ============================================================\n# Setup dedicated Podman autostart service for AgentTeams\n# ============================================================\nsetup_podman_autostart() {\n # Only execute if using podman and autostart is enabled\n if [ \"${DOCKER_CMD:-docker}\" != \"podman\" ] || [ \"${AGENTTEAMS_PODMAN_AUTOSTART:-0}\" != \"1\" ]; then\n return 0\n fi\n\n log \"$(msg install.podman.autostart_title)\"\n\n local current_user\n current_user=\"$(whoami)\"\n\n # Define the dedicated AgentTeams service content\n # Strict scoping: Only manage containers matching '--filter name=agentteams'\n local _service_content=\"[Unit]\nDescription=AgentTeams Dedicated Podman Autostart Service\nDocumentation=https://github.com/agentscope-ai/AgentTeams\nWants=network-online.target\nAfter=network-online.target\n\n[Service]\nType=oneshot\nRemainAfterExit=true\nEnvironment=LOGGING=\\\"--log-level=info\\\"\nExecStart=/usr/bin/podman \\$LOGGING start --filter name=agentteams --filter restart-policy=always --filter restart-policy=unless-stopped\nExecStop=/usr/bin/podman \\$LOGGING stop --filter name=agentteams --filter restart-policy=always --filter restart-policy=unless-stopped\n\n[Install]\nWantedBy=default.target\"\n\n local _autostart_ok=0\n\n if [ \"${current_user}\" = \"root\" ]; then\n # Rootful Mode (System-wide dedicated service)\n log \"$(msg install.podman.root_setup)\"\n\n local _sys_dir=\"/etc/systemd/system\"\n mkdir -p \"${_sys_dir}\"\n echo \"${_service_content}\" > \"${_sys_dir}/agentteams-podman-restart.service\"\n\n systemctl daemon-reload >/dev/null 2>&1 || true\n\n if systemctl enable --now agentteams-podman-restart.service >/dev/null 2>&1; then\n log \"$(msg install.podman.root_success)\"\n _autostart_ok=1\n else\n log \"$(msg install.podman.root_fail)\"\n fi\n else\n # Rootless Mode (User-level dedicated service)\n log \"$(msg install.podman.user_setup \"${current_user}\")\"\n\n local _linger_ok=0\n\n # 1. Enable Linger for background execution (Graceful escalation strategy)\n if loginctl show-user \"${current_user}\" --property=Linger 2>/dev/null | grep -q \"Linger=yes\"; then\n _linger_ok=1\n else\n log \"$(msg install.podman.linger_enable)\"\n\n # Step A: Try to enable linger natively without sudo\n if loginctl enable-linger \"${current_user}\" >/dev/null 2>&1; then\n log \"Successfully enabled systemd linger natively.\"\n _linger_ok=1\n else\n # Step B: Fallback to sudo\n log \"Native linger enablement failed. Distro security policy requires privileges.\"\n if command -v sudo >/dev/null 2>&1; then\n log \"Attempting to enable systemd linger via sudo (enter password if prompted)...\"\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n sudo -n loginctl enable-linger \"${current_user}\" 2>/dev/null && _linger_ok=1 || log \"$(msg install.podman.linger_warn)\"\n else\n sudo loginctl enable-linger \"${current_user}\" && _linger_ok=1 || log \"$(msg install.podman.linger_warn)\"\n fi\n else\n log \"$(msg install.podman.linger_warn)\"\n fi\n fi\n fi\n\n # 2. Write dedicated service file to user directory\n local _user_dir=\"${HOME}/.config/systemd/user\"\n mkdir -p \"${_user_dir}\"\n echo \"${_service_content}\" > \"${_user_dir}/agentteams-podman-restart.service\"\n\n systemctl --user daemon-reload >/dev/null 2>&1 || true\n\n if systemctl --user enable --now agentteams-podman-restart.service >/dev/null 2>&1; then\n log \"$(msg install.podman.user_success)\"\n # Crucial: Only consider autostart fully successful if systemd linger is also enabled\n if [ \"${_linger_ok}\" = \"1\" ]; then\n _autostart_ok=1\n else\n log \"⚠️ Service enabled, but since systemd linger is not active, containers will not auto-recover on system reboot.\"\n fi\n else\n log \"$(msg install.podman.user_fail)\"\n fi\n fi\n\n # Only print final success banner if the service AND linger were actually enabled\n if [ \"${_autostart_ok}\" = \"1\" ]; then\n log \"$(msg install.podman.success)\"\n fi\n}\n\n# ============================================================\n# Dashboard startup (called after controller is ready)\n# ============================================================\n\n_start_dashboard() {\n local CTRL_CONTAINER=\"agentteams-controller\"\n local DASHBOARD_CONTAINER=\"agentteams-dashboard\"\n\n if [ \"${AGENTTEAMS_DASHBOARD:-0}\" != \"1\" ]; then\n # Stop and remove the existing dashboard container if present.\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -q \"^${DASHBOARD_CONTAINER}$\"; then\n log \"Stopping and removing ${DASHBOARD_CONTAINER}...\"\n ${DOCKER_CMD} stop \"${DASHBOARD_CONTAINER}\" >/dev/null 2>&1 || true\n ${DOCKER_CMD} rm -f \"${DASHBOARD_CONTAINER}\" >/dev/null 2>&1 || true\n log \"Dashboard stopped.\"\n fi\n return 0\n fi\n\n # Dashboard requires the embedded architecture (controller container).\n # In legacy all-in-one mode the controller API is not available.\n if [ \"${AGENTTEAMS_USE_EMBEDDED:-0}\" != \"1\" ]; then\n log \"Skipping Dashboard: requires embedded architecture (agentteams-controller), not available in legacy mode.\"\n log \"Use Quick Start (embedded) mode to enable agentteams-dashboard.\"\n return 0\n fi\n\n AGENTTEAMS_PORT_DASHBOARD=\"${AGENTTEAMS_PORT_DASHBOARD:-13000}\"\n AGENTTEAMS_DASHBOARD_VERSION=\"${AGENTTEAMS_DASHBOARD_VERSION:-v1.2.0-beta.1}\"\n AGENTTEAMS_DASHBOARD_IMAGE=\"${AGENTTEAMS_DASHBOARD_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-dashboard:${AGENTTEAMS_DASHBOARD_VERSION}}\"\n\n log \"\"\n log \"Starting agentteams-dashboard...\"\n\n local BIND_ADDR=\"127.0.0.1\"\n [ \"${AGENTTEAMS_LOCAL_ONLY:-1}\" = \"0\" ] && BIND_ADDR=\"0.0.0.0\"\n\n # Remove existing container\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' | grep -qx \"${DASHBOARD_CONTAINER}\"; then\n ${DOCKER_CMD} rm -f \"${DASHBOARD_CONTAINER}\" >/dev/null 2>&1 || true\n fi\n\n # Pull or use local image\n if ! ${DOCKER_CMD} image inspect \"${AGENTTEAMS_DASHBOARD_IMAGE}\" >/dev/null 2>&1; then\n log \"Pulling dashboard image: ${AGENTTEAMS_DASHBOARD_IMAGE}\"\n ${DOCKER_CMD} pull \"${AGENTTEAMS_DASHBOARD_IMAGE}\" 2>/dev/null || {\n log \"WARNING: Could not pull dashboard image. Skipping.\"\n return 0\n }\n fi\n\n # Build env args from controller container (MinIO/LLM/auth).\n local env_args=()\n env_args+=(-e AGENTTEAMS_CONTROLLER_URL=\"http://${CTRL_CONTAINER}:8090\")\n env_args+=(-e NEXT_PUBLIC_MATRIX_API_URL=\"http://${CTRL_CONTAINER}:6167\")\n env_args+=(-e MATRIX_HOMESERVER_ALLOWLIST=\"${CTRL_CONTAINER},matrix-local.agentteams.io,matrix.org\")\n\n if ${DOCKER_CMD} ps --format '{{.Names}}' | grep -qx \"${CTRL_CONTAINER}\"; then\n local env_out\n env_out=$(${DOCKER_CMD} inspect \"${CTRL_CONTAINER}\" --format='{{range .Config.Env}}{{.}}{{\"\\n\"}}{{end}}')\n\n _env() { echo \"${env_out}\" | sed -n \"s|^${1}=||p\" | head -n1; }\n\n local fs_endpoint; fs_endpoint=$(_env AGENTTEAMS_FS_ENDPOINT)\n [ -z \"${fs_endpoint}\" ] && fs_endpoint=$(_env AGENTTEAMS_MINIO_ENDPOINT)\n if [ -n \"${fs_endpoint}\" ]; then\n fs_endpoint=$(echo \"${fs_endpoint}\" | sed -e \"s|127\\.0\\.0\\.1|${CTRL_CONTAINER}|\" -e \"s|localhost|${CTRL_CONTAINER}|\")\n env_args+=(-e AGENTTEAMS_FS_ENDPOINT=\"${fs_endpoint}\")\n else\n env_args+=(-e AGENTTEAMS_FS_ENDPOINT=\"http://${CTRL_CONTAINER}:9000\")\n fi\n\n local fs_bucket; fs_bucket=$(_env AGENTTEAMS_FS_BUCKET)\n [ -z \"${fs_bucket}\" ] && fs_bucket=$(_env AGENTTEAMS_MINIO_BUCKET)\n [ -n \"${fs_bucket}\" ] && env_args+=(-e AGENTTEAMS_FS_BUCKET=\"${fs_bucket}\")\n\n local fs_user; fs_user=$(_env AGENTTEAMS_FS_ACCESS_KEY)\n [ -z \"${fs_user}\" ] && fs_user=$(_env AGENTTEAMS_MINIO_USER)\n [ -n \"${fs_user}\" ] && env_args+=(-e AGENTTEAMS_FS_ACCESS_KEY=\"${fs_user}\")\n\n local fs_pass; fs_pass=$(_env AGENTTEAMS_FS_SECRET_KEY)\n [ -z \"${fs_pass}\" ] && fs_pass=$(_env AGENTTEAMS_MINIO_PASSWORD)\n [ -n \"${fs_pass}\" ] && env_args+=(-e AGENTTEAMS_FS_SECRET_KEY=\"${fs_pass}\")\n\n for k in AGENTTEAMS_LLM_PROVIDER AGENTTEAMS_LLM_API_KEY AGENTTEAMS_OPENAI_BASE_URL AGENTTEAMS_DEFAULT_MODEL; do\n local v; v=$(_env \"${k}\")\n [ -n \"${v}\" ] && env_args+=(-e \"${k}=${v}\")\n done\n\n # The controller writes the CLI SA token to /var/run/agentteams/cli-token.\n # The token is generated asynchronously during the first reconcile loop,\n # so it may not be available immediately after the container starts.\n # We poll for up to 30s to handle both fresh installs (token not yet minted)\n # and older images that don't support cli-token at all.\n # For backward compatibility, also try the legacy /var/run/hiclaw/cli-token\n # path used by older HiClaw images.\n local auth_token=\"${AGENTTEAMS_AUTH_TOKEN:-}\"\n if [ -z \"${auth_token}\" ]; then\n local _token_wait=0\n local _token_max_wait=30\n while [ ${_token_wait} -lt ${_token_max_wait} ]; do\n auth_token=$(${DOCKER_CMD} exec \"${CTRL_CONTAINER}\" sh -c 'cat /var/run/agentteams/cli-token 2>/dev/null || cat /var/run/hiclaw/cli-token 2>/dev/null' | tr -d '\\n' || true)\n if [ -n \"${auth_token}\" ]; then\n break\n fi\n sleep 2\n _token_wait=$((_token_wait + 2))\n done\n fi\n if [ -n \"${auth_token}\" ]; then\n env_args+=(-e AGENTTEAMS_AUTH_TOKEN=\"${auth_token}\")\n else\n log \"WARNING: could not read controller SA token (timed out after ${_token_max_wait:-30}s)\"\n log \" This can happen with older AgentTeams/HiClaw images or if the controller is still starting.\"\n log \" Dashboard will still work but some API calls may fail until you log in via Higress Console.\"\n log \" To fix: upgrade AgentTeams to v1.2.0-beta.1+ or set AGENTTEAMS_AUTH_TOKEN manually.\"\n fi\n\n local admin_user; admin_user=$(_env AGENTTEAMS_ADMIN_USER)\n local admin_pass; admin_pass=$(_env AGENTTEAMS_ADMIN_PASSWORD)\n [ -n \"${admin_user}\" ] && env_args+=(-e AGENTTEAMS_ADMIN_USER=\"${admin_user}\")\n [ -n \"${admin_pass}\" ] && env_args+=(-e AGENTTEAMS_ADMIN_PASSWORD=\"${admin_pass}\")\n\n # Higress Console URL: explicit config takes priority, auto-detect as fallback.\n local _resolved_gw_url=\"\"\n if [ -n \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\" ]; then\n # Normalize URL: add http:// prefix if missing\n _resolved_gw_url=\"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\"\n case \"${_resolved_gw_url}\" in\n http://*|https://*) ;;\n *) _resolved_gw_url=\"http://${_resolved_gw_url}\" ;;\n esac\n env_args+=(-e AGENTTEAMS_AI_GATEWAY_ADMIN_URL=\"${_resolved_gw_url}\")\n elif ${DOCKER_CMD} exec \"${CTRL_CONTAINER}\" wget -q -O- --timeout=2 http://127.0.0.1:8001/ >/dev/null 2>&1; then\n _resolved_gw_url=\"http://${CTRL_CONTAINER}:8001\"\n env_args+=(-e AGENTTEAMS_AI_GATEWAY_ADMIN_URL=\"${_resolved_gw_url}\")\n fi\n\n # Verify Higress Console URL reachability (best-effort warning)\n if [ -n \"${_resolved_gw_url}\" ]; then\n if ! ${DOCKER_CMD} exec \"${CTRL_CONTAINER}\" curl -sf --max-time 3 \"${_resolved_gw_url}/\" >/dev/null 2>&1; then\n log \"WARNING: Higress Console URL may not be reachable: ${_resolved_gw_url}\"\n log \" Dashboard will still work, but shared login via Higress Console may fail.\"\n log \" To fix: verify the URL is correct and the Higress Console service is running.\"\n fi\n fi\n else\n log \"WARNING: ${CTRL_CONTAINER} not running; launching Dashboard without Controller/MinIO/LLM env.\"\n if [ -n \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\" ]; then\n local _gw_norm=\"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\"\n case \"${_gw_norm}\" in\n http://*|https://*) ;;\n *) _gw_norm=\"http://${_gw_norm}\" ;;\n esac\n env_args+=(-e AGENTTEAMS_AI_GATEWAY_ADMIN_URL=\"${_gw_norm}\")\n fi\n fi\n\n # Create persistent data volume (for future use)\n ${DOCKER_CMD} volume create agentteams-dashboard-data >/dev/null 2>&1 || true\n\n ${DOCKER_CMD} run -d \\\n --name \"${DASHBOARD_CONTAINER}\" \\\n --restart unless-stopped \\\n --network \"${AGENTTEAMS_NETWORK:-agentteams-net}\" \\\n --network-alias dashboard.agentteams.io \\\n -p \"${BIND_ADDR}:${AGENTTEAMS_PORT_DASHBOARD}:3000\" \\\n \"${env_args[@]}\" \\\n -v agentteams-dashboard-data:/app/db \\\n \"${AGENTTEAMS_DASHBOARD_IMAGE}\"\n\n # Wait for dashboard to be ready\n local max_wait=60\n local waited=0\n while [ $waited -lt $max_wait ]; do\n if curl -sf \"http://127.0.0.1:${AGENTTEAMS_PORT_DASHBOARD}/\" >/dev/null 2>&1; then\n log \"Dashboard ready on port ${AGENTTEAMS_PORT_DASHBOARD}\"\n return 0\n fi\n sleep 2\n waited=$((waited + 2))\n done\n log \"WARNING: Dashboard startup timed out after ${max_wait}s\"\n ${DOCKER_CMD} logs \"${DASHBOARD_CONTAINER}\" 2>&1 | tail -20\n return 0\n}\n\n# ============================================================\n# Manager Installation (Interactive)\n# ============================================================\n\ninstall_manager() {\n log \"$(msg install.title)\"\n log \"$(msg install.registry \"${AGENTTEAMS_REGISTRY}\")\"\n log \"\"\n log \"$(msg install.dir \"$(pwd)\")\"\n log \"$(msg install.dir_hint)\"\n log \"$(msg install.dir_hint2)\"\n log \"\"\n\n # Non-interactive fallback: resolve version immediately so image tags are available\n # before the step state machine runs. Interactive mode lets step_version handle it.\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_VERSION=\"${AGENTTEAMS_VERSION:-${AGENTTEAMS_KNOWN_STABLE_VERSION}}\"\n resolve_image_tags\n fi\n\n # Migrate legacy env file location before checks\n local existing_env=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n if [ ! -f \"${existing_env}\" ] && [ -f \"./agentteams-manager.env\" ]; then\n log \"Migrating agentteams-manager.env from current directory to ${existing_env}...\"\n mv \"./agentteams-manager.env\" \"${existing_env}\"\n fi\n\n # Orphan volume detection (only when no env file — step_existing handles the env-file case)\n if [ ! -f \"${existing_env}\" ]; then\n local data_vol=\"${AGENTTEAMS_DATA_DIR:-agentteams-data}\"\n if ${DOCKER_CMD} volume ls -q | grep -q \"^${data_vol}$\"; then\n echo \"\"\n log \"$(msg install.orphan_volume.detected \"${data_vol}\")\"\n log \"$(msg install.orphan_volume.warn)\"\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n log \"$(msg install.orphan_volume.clean_noninteractive)\"\n ${DOCKER_CMD} stop agentteams-manager 2>/dev/null || true\n ${DOCKER_CMD} rm agentteams-manager 2>/dev/null || true\n for w in $(${DOCKER_CMD} ps -a --format '{{.Names}}' | grep \"^agentteams-worker-\" || true); do\n ${DOCKER_CMD} stop \"${w}\" 2>/dev/null || true\n ${DOCKER_CMD} rm \"${w}\" 2>/dev/null || true\n done\n log \"$(msg install.orphan_volume.cleaning)\"\n ${DOCKER_CMD} volume rm \"${data_vol}\" 2>/dev/null || true\n log \"$(msg install.orphan_volume.cleaned)\"\n else\n echo \"\"\n echo \"$(msg install.orphan_volume.choose)\"\n echo \"$(msg install.orphan_volume.clean)\"\n echo \"$(msg install.orphan_volume.keep)\"\n echo \"\"\n local ORPHAN_CHOICE\n read -e -p \"$(msg install.orphan_volume.prompt): \" ORPHAN_CHOICE\n ORPHAN_CHOICE=\"${ORPHAN_CHOICE:-1}\"\n case \"${ORPHAN_CHOICE}\" in\n 1|clean)\n ${DOCKER_CMD} stop agentteams-manager 2>/dev/null || true\n ${DOCKER_CMD} rm agentteams-manager 2>/dev/null || true\n for w in $(${DOCKER_CMD} ps -a --format '{{.Names}}' | grep \"^agentteams-worker-\" || true); do\n ${DOCKER_CMD} stop \"${w}\" 2>/dev/null || true\n ${DOCKER_CMD} rm \"${w}\" 2>/dev/null || true\n done\n log \"$(msg install.orphan_volume.cleaning)\"\n ${DOCKER_CMD} volume rm \"${data_vol}\" 2>/dev/null || true\n log \"$(msg install.orphan_volume.cleaned)\"\n ;;\n 2|keep)\n log \"$(msg install.orphan_volume.keeping)\"\n ;;\n esac\n fi\n fi\n fi\n\n # ── State machine ─────────────────────────────────────────────────────────\n local _STEPS=( step_lang step_mode step_version step_existing step_llm step_manager_runtime step_runtime step_admin step_network\n step_ports step_domains step_github step_skills step_volume\n step_workspace step_dashboard step_e2ee step_docker_proxy step_idle step_hostshare step_podman_autostart )\n local _STEP_HISTORY=()\n local _step_idx=0\n while [ \"${_step_idx}\" -lt \"${#_STEPS[@]}\" ]; do\n local _step_fn=\"${_STEPS[$_step_idx]}\"\n if should_skip_step \"${_step_fn}\"; then\n _step_idx=$((_step_idx + 1))\n continue\n fi\n if [ \"${#_STEP_HISTORY[@]}\" -gt 0 ]; then\n log \"$(msg nav.back_hint)\"\n fi\n STEP_RESULT=\"\"\n \"${_step_fn}\"\n if [ \"${STEP_RESULT}\" = \"back\" ]; then\n if [ \"${#_STEP_HISTORY[@]}\" -gt 0 ]; then\n local _last=$(( ${#_STEP_HISTORY[@]} - 1 ))\n _step_idx=\"${_STEP_HISTORY[$_last]}\"\n _STEP_HISTORY=(\"${_STEP_HISTORY[@]:0:${_last}}\")\n clear_step_vars \"${_STEPS[$_step_idx]}\"\n fi\n # else: first step, ignore 'b'\n else\n _STEP_HISTORY+=(\"${_step_idx}\")\n _step_idx=$((_step_idx + 1))\n fi\n done\n # ── End state machine ──────────────────────────────────────────────────────\n\n # Post-machine defaults for any steps that were skipped\n AGENTTEAMS_DATA_DIR=\"${AGENTTEAMS_DATA_DIR:-agentteams-data}\"\n if [ -z \"${AGENTTEAMS_WORKSPACE_DIR+x}\" ] || [ -z \"${AGENTTEAMS_WORKSPACE_DIR}\" ]; then\n AGENTTEAMS_WORKSPACE_DIR=\"${HOME}/agentteams-manager\"\n export AGENTTEAMS_WORKSPACE_DIR\n fi\n AGENTTEAMS_WORKSPACE_DIR=\"$(cd \"${AGENTTEAMS_WORKSPACE_DIR}\" 2>/dev/null && pwd || echo \"${AGENTTEAMS_WORKSPACE_DIR}\")\"\n mkdir -p \"${AGENTTEAMS_WORKSPACE_DIR}\"\n AGENTTEAMS_MANAGER_RUNTIME=\"${AGENTTEAMS_MANAGER_RUNTIME:-copaw}\"\n export AGENTTEAMS_MANAGER_RUNTIME\n AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"${AGENTTEAMS_DEFAULT_WORKER_RUNTIME:-copaw}\"\n AGENTTEAMS_MATRIX_E2EE=\"${AGENTTEAMS_MATRIX_E2EE:-0}\"\n export AGENTTEAMS_MATRIX_E2EE\n AGENTTEAMS_WORKER_IDLE_TIMEOUT=\"${AGENTTEAMS_WORKER_IDLE_TIMEOUT:-720}\"\n export AGENTTEAMS_WORKER_IDLE_TIMEOUT\n AGENTTEAMS_HOST_SHARE_DIR=\"${AGENTTEAMS_HOST_SHARE_DIR:-$HOME}\"\n export AGENTTEAMS_HOST_SHARE_DIR\n\n log \"\"\n\n # Generate secrets (only if not already set)\n log \"$(msg install.generating_secrets)\"\n AGENTTEAMS_MANAGER_PASSWORD=\"${AGENTTEAMS_MANAGER_PASSWORD:-$(generate_key)}\"\n AGENTTEAMS_REGISTRATION_TOKEN=\"${AGENTTEAMS_REGISTRATION_TOKEN:-$(generate_key)}\"\n AGENTTEAMS_MINIO_USER=\"${AGENTTEAMS_MINIO_USER:-${AGENTTEAMS_ADMIN_USER}}\"\n AGENTTEAMS_MINIO_PASSWORD=\"${AGENTTEAMS_MINIO_PASSWORD:-${AGENTTEAMS_ADMIN_PASSWORD}}\"\n AGENTTEAMS_MANAGER_GATEWAY_KEY=\"${AGENTTEAMS_MANAGER_GATEWAY_KEY:-$(generate_key)}\"\n\n # Matrix AppService tokens — generate once during install/upgrade if not provided.\n # Persisted to env file so they survive controller restarts.\n AGENTTEAMS_MATRIX_APPSERVICE_ENABLED=\"${AGENTTEAMS_MATRIX_APPSERVICE_ENABLED:-true}\"\n export AGENTTEAMS_MATRIX_APPSERVICE_ENABLED\n if [ \"${AGENTTEAMS_MATRIX_APPSERVICE_ENABLED}\" != \"false\" ] && [ \"${AGENTTEAMS_MATRIX_APPSERVICE_ENABLED}\" != \"0\" ]; then\n if [ -z \"${AGENTTEAMS_MATRIX_APPSERVICE_AS_TOKEN}\" ]; then\n AGENTTEAMS_MATRIX_APPSERVICE_AS_TOKEN=\"$(openssl rand -hex 32)\"\n log \" Auto-generated AppService as_token (saved to env file)\"\n fi\n if [ -z \"${AGENTTEAMS_MATRIX_APPSERVICE_HS_TOKEN}\" ]; then\n AGENTTEAMS_MATRIX_APPSERVICE_HS_TOKEN=\"$(openssl rand -hex 32)\"\n log \" Auto-generated AppService hs_token (saved to env file)\"\n fi\n export AGENTTEAMS_MATRIX_APPSERVICE_AS_TOKEN\n export AGENTTEAMS_MATRIX_APPSERVICE_HS_TOKEN\n fi\n\n # Detect Apple Silicon (M1/M2/M3/M4) - need JVM fix for Higress Console\n # See: https://github.com/agentscope-ai/AgentTeams/issues/249\n if [ -z \"${JVM_ARGS:-}\" ] && [ \"$(uname -m)\" = \"arm64\" ] && [ \"$(uname -s)\" = \"Darwin\" ]; then\n log \"Apple Silicon detected - setting JVM_ARGS to fix Higress Console SIGILL issue\"\n JVM_ARGS=\"-XX:+UnlockDiagnosticVMOptions -XX:-UseAESCTRIntrinsics -XX:UseSVE=0\"\n fi\n\n # Write .env file\n ENV_FILE=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n cat > \"${ENV_FILE}\" << EOF\n# AgentTeams Manager Configuration\n# Generated by agentteams-install.sh on $(date)\n\n# Language\nAGENTTEAMS_LANGUAGE=${AGENTTEAMS_LANGUAGE}\n\n# LLM\nAGENTTEAMS_LLM_PROVIDER=${AGENTTEAMS_LLM_PROVIDER}\nAGENTTEAMS_DEFAULT_MODEL=${AGENTTEAMS_DEFAULT_MODEL}\nAGENTTEAMS_LLM_API_KEY=${AGENTTEAMS_LLM_API_KEY}\nAGENTTEAMS_OPENAI_BASE_URL=${AGENTTEAMS_OPENAI_BASE_URL:-}\nAGENTTEAMS_MODEL_CONTEXT_WINDOW=${AGENTTEAMS_MODEL_CONTEXT_WINDOW:-}\nAGENTTEAMS_MODEL_MAX_TOKENS=${AGENTTEAMS_MODEL_MAX_TOKENS:-}\nAGENTTEAMS_MODEL_REASONING=${AGENTTEAMS_MODEL_REASONING:-}\nAGENTTEAMS_MODEL_VISION=${AGENTTEAMS_MODEL_VISION:-}\n\n# Embedding model (empty = disabled, default: text-embedding-v4)\nAGENTTEAMS_EMBEDDING_MODEL=${AGENTTEAMS_EMBEDDING_MODEL}\n\n# Admin\nAGENTTEAMS_ADMIN_USER=${AGENTTEAMS_ADMIN_USER}\nAGENTTEAMS_ADMIN_PASSWORD=${AGENTTEAMS_ADMIN_PASSWORD}\n\n# Ports\nAGENTTEAMS_LOCAL_ONLY=${AGENTTEAMS_LOCAL_ONLY}\nAGENTTEAMS_PORT_GATEWAY=${AGENTTEAMS_PORT_GATEWAY}\nAGENTTEAMS_PORT_CONSOLE=${AGENTTEAMS_PORT_CONSOLE}\nAGENTTEAMS_PORT_ELEMENT_WEB=${AGENTTEAMS_PORT_ELEMENT_WEB}\nAGENTTEAMS_PORT_MANAGER_CONSOLE=${AGENTTEAMS_PORT_MANAGER_CONSOLE:-18888}\n\n# Manager runtime (openclaw | copaw)\nAGENTTEAMS_MANAGER_RUNTIME=${AGENTTEAMS_MANAGER_RUNTIME:-copaw}\n\n# Matrix\nAGENTTEAMS_MATRIX_DOMAIN=${AGENTTEAMS_MATRIX_DOMAIN}\nAGENTTEAMS_MATRIX_CLIENT_DOMAIN=${AGENTTEAMS_MATRIX_CLIENT_DOMAIN}\n\n# Gateway\nAGENTTEAMS_AI_GATEWAY_DOMAIN=${AGENTTEAMS_AI_GATEWAY_DOMAIN}\nAGENTTEAMS_MANAGER_GATEWAY_KEY=${AGENTTEAMS_MANAGER_GATEWAY_KEY}\n\n# File System\nAGENTTEAMS_FS_DOMAIN=${AGENTTEAMS_FS_DOMAIN}\nAGENTTEAMS_CONSOLE_DOMAIN=${AGENTTEAMS_CONSOLE_DOMAIN}\nAGENTTEAMS_MINIO_USER=${AGENTTEAMS_MINIO_USER}\nAGENTTEAMS_MINIO_PASSWORD=${AGENTTEAMS_MINIO_PASSWORD}\n\n# Internal\nAGENTTEAMS_MANAGER_PASSWORD=${AGENTTEAMS_MANAGER_PASSWORD}\nAGENTTEAMS_REGISTRATION_TOKEN=${AGENTTEAMS_REGISTRATION_TOKEN}\n\n# GitHub (optional)\nAGENTTEAMS_GITHUB_TOKEN=${AGENTTEAMS_GITHUB_TOKEN:-}\n\n# Nacos package import defaults\nAGENTTEAMS_NACOS_REGISTRY_URI=${AGENTTEAMS_NACOS_REGISTRY_URI:-nacos://market.agentteams.io:80/public}\nAGENTTEAMS_NACOS_USERNAME=${AGENTTEAMS_NACOS_USERNAME:-}\nAGENTTEAMS_NACOS_PASSWORD=${AGENTTEAMS_NACOS_PASSWORD:-}\nAGENTTEAMS_NACOS_TOKEN=${AGENTTEAMS_NACOS_TOKEN:-}\n\n# Skills Registry (optional, default: nacos://market.agentteams.io:80/public)\nAGENTTEAMS_SKILLS_API_URL=${AGENTTEAMS_SKILLS_API_URL:-nacos://market.agentteams.io:80/public}\n\n# OpenClaw CMS plugin (optional)\nAGENTTEAMS_CMS_TRACES_ENABLED=${AGENTTEAMS_CMS_TRACES_ENABLED:-false}\nAGENTTEAMS_CMS_ENDPOINT=${AGENTTEAMS_CMS_ENDPOINT:-}\nAGENTTEAMS_CMS_LICENSE_KEY=${AGENTTEAMS_CMS_LICENSE_KEY:-}\nAGENTTEAMS_CMS_PROJECT=${AGENTTEAMS_CMS_PROJECT:-}\nAGENTTEAMS_CMS_WORKSPACE=${AGENTTEAMS_CMS_WORKSPACE:-}\nAGENTTEAMS_CMS_SERVICE_NAME=${AGENTTEAMS_CMS_SERVICE_NAME:-agentteams-manager}\nAGENTTEAMS_CMS_METRICS_ENABLED=${AGENTTEAMS_CMS_METRICS_ENABLED:-false}\n\n# Worker images (for direct container creation)\nAGENTTEAMS_WORKER_IMAGE=${WORKER_IMAGE}\nAGENTTEAMS_COPAW_WORKER_IMAGE=${COPAW_WORKER_IMAGE}\nAGENTTEAMS_HERMES_WORKER_IMAGE=${HERMES_WORKER_IMAGE}\n\n# Default Worker runtime (openclaw | copaw | hermes)\nAGENTTEAMS_DEFAULT_WORKER_RUNTIME=${AGENTTEAMS_DEFAULT_WORKER_RUNTIME:-copaw}\n\n# Matrix E2EE (0=disabled, 1=enabled; default: 0)\nAGENTTEAMS_MATRIX_E2EE=${AGENTTEAMS_MATRIX_E2EE:-0}\n\n# Matrix AppService\nAGENTTEAMS_MATRIX_APPSERVICE_ENABLED=${AGENTTEAMS_MATRIX_APPSERVICE_ENABLED:-true}\nAGENTTEAMS_MATRIX_APPSERVICE_AS_TOKEN=${AGENTTEAMS_MATRIX_APPSERVICE_AS_TOKEN:-}\nAGENTTEAMS_MATRIX_APPSERVICE_HS_TOKEN=${AGENTTEAMS_MATRIX_APPSERVICE_HS_TOKEN:-}\n\n# Docker API proxy (0=disabled, 1=enabled; default: 1)\nAGENTTEAMS_DOCKER_PROXY=${AGENTTEAMS_DOCKER_PROXY:-1}\n\n# Docker API proxy: additional allowed image sources (comma-separated)\nAGENTTEAMS_PROXY_ALLOWED_REGISTRIES=${AGENTTEAMS_PROXY_ALLOWED_REGISTRIES:-}\n\n# Worker idle timeout in minutes (default: 720 = 12 hours)\nAGENTTEAMS_WORKER_IDLE_TIMEOUT=${AGENTTEAMS_WORKER_IDLE_TIMEOUT:-720}\n\n# Podman autostart via systemd (0=disabled, 1=enabled)\nAGENTTEAMS_PODMAN_AUTOSTART=${AGENTTEAMS_PODMAN_AUTOSTART:-0}\n\n# JVM Args for Higress Console (fixes SIGILL on Apple Silicon)\nJVM_ARGS=${JVM_ARGS:-}\n\n# Higress WASM plugin image registry (auto-selected by timezone)\nHIGRESS_ADMIN_WASM_PLUGIN_IMAGE_REGISTRY=${AGENTTEAMS_REGISTRY}\n\n# Data persistence\nAGENTTEAMS_DATA_DIR=${AGENTTEAMS_DATA_DIR:-agentteams-data}\n# Manager workspace (skills, memory, state — host-editable)\nAGENTTEAMS_WORKSPACE_DIR=${AGENTTEAMS_WORKSPACE_DIR:-}\n# Host directory sharing\nAGENTTEAMS_HOST_SHARE_DIR=${AGENTTEAMS_HOST_SHARE_DIR:-}\n\n# agentteams-dashboard (management UI)\nAGENTTEAMS_DASHBOARD=${AGENTTEAMS_DASHBOARD:-1}\nAGENTTEAMS_DASHBOARD_VERSION=${AGENTTEAMS_DASHBOARD_VERSION:-v1.2.0-beta.1}\nAGENTTEAMS_PORT_DASHBOARD=${AGENTTEAMS_PORT_DASHBOARD:-13000}\nAGENTTEAMS_DASHBOARD_IMAGE=${AGENTTEAMS_DASHBOARD_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-dashboard:${AGENTTEAMS_DASHBOARD_VERSION}}\nAGENTTEAMS_AI_GATEWAY_ADMIN_URL=${AGENTTEAMS_AI_GATEWAY_ADMIN_URL:-}\nEOF\n\n chmod 600 \"${ENV_FILE}\"\n log \"$(msg install.config_saved \"${ENV_FILE}\")\"\n\n # Detect container runtime socket\n SOCKET_MOUNT_ARGS=\"\"\n if [ \"${AGENTTEAMS_MOUNT_SOCKET}\" = \"1\" ]; then\n # Actively ensure the API socket is enabled and active before detection.\n if [ \"${DOCKER_CMD:-}\" = \"podman\" ]; then\n ensure_podman_socket\n fi\n CONTAINER_SOCK=$(detect_socket)\n if [ -n \"${CONTAINER_SOCK}\" ]; then\n log \"$(msg install.socket_detected \"${CONTAINER_SOCK}\")\"\n SOCKET_MOUNT_ARGS=\"-v ${CONTAINER_SOCK}:/var/run/docker.sock --security-opt label=disable\"\n else\n log \"$(msg install.socket_not_found)\"\n # Interactive confirmation when socket not found\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" != \"1\" ]; then\n echo \"\"\n echo -e \"\\033[33m$(msg install.socket_confirm.title)\\033[0m\"\n echo \"\"\n echo -e \"$(msg install.socket_confirm.message)\"\n echo \"\"\n read -p \"$(msg install.socket_confirm.prompt)\" SOCKET_CONFIRM\n if [ \"${SOCKET_CONFIRM}\" != \"y\" ] && [ \"${SOCKET_CONFIRM}\" != \"Y\" ]; then\n log \"$(msg install.socket_confirm.cancelled)\"\n exit 0\n fi\n fi\n fi\n fi\n\n # Create the data volume if it doesn't already exist (reuse on reinstall)\n if ! ${DOCKER_CMD} volume ls -q | grep -q \"^${AGENTTEAMS_DATA_DIR}$\"; then\n ${DOCKER_CMD} volume create \"${AGENTTEAMS_DATA_DIR}\" > /dev/null\n fi\n\n # Data mount: Docker volume\n DATA_MOUNT_ARGS=\"-v ${AGENTTEAMS_DATA_DIR}:/data\"\n\n # Manager workspace mount (always a host directory, defaulting to ~/agentteams-manager)\n WORKSPACE_MOUNT_ARGS=\"-v ${AGENTTEAMS_WORKSPACE_DIR}:/root/manager-workspace\"\n\n # Pass host timezone to container so date/time commands reflect local time\n TZ_ARGS=\"-e TZ=${AGENTTEAMS_TIMEZONE}\"\n\n # Host directory mount\n if [ -d \"${AGENTTEAMS_HOST_SHARE_DIR}\" ]; then\n HOST_SHARE_MOUNT_ARGS=\"-v ${AGENTTEAMS_HOST_SHARE_DIR}:/host-share\"\n log \"$(msg host_share.sharing \"${AGENTTEAMS_HOST_SHARE_DIR}\")\"\n else\n log \"$(msg host_share.not_exist \"${AGENTTEAMS_HOST_SHARE_DIR}\")\"\n HOST_SHARE_MOUNT_ARGS=\"-v ${AGENTTEAMS_HOST_SHARE_DIR}:/host-share\"\n fi\n\n # YOLO mode: pass through if set in environment (enables autonomous decisions)\n YOLO_ARGS=\"\"\n if [ \"${AGENTTEAMS_YOLO:-}\" = \"1\" ]; then\n YOLO_ARGS=\"-e AGENTTEAMS_YOLO=1\"\n log \"$(msg install.yolo)\"\n fi\n\n # Matrix-plugin debug tracing: pass through if AGENTTEAMS_MATRIX_DEBUG=1.\n # The container entrypoints translate this to OPENCLAW_MATRIX_DEBUG=1\n # so the openclaw matrix plugin emits structured INFO-level lifecycle\n # traces (sync state transitions, room.invite/join, message handler\n # arrival + filter outcomes). Used to diagnose worker/manager hangs.\n MATRIX_DEBUG_ARGS=\"\"\n if [ \"${AGENTTEAMS_MATRIX_DEBUG:-}\" = \"1\" ]; then\n MATRIX_DEBUG_ARGS=\"-e AGENTTEAMS_MATRIX_DEBUG=1\"\n fi\n\n # E2EE is already in the env file; but also pass explicitly in case env file is not the source\n # (AGENTTEAMS_MATRIX_E2EE is already written to ENV_FILE above via --env-file)\n\n # Pull images (manager based on runtime config; all worker runtimes always pulled)\n _is_local_image() {\n case \"$1\" in\n agentteams/*) return 0 ;;\n *) return 1 ;;\n esac\n }\n\n _local_image_exists() {\n local _img=\"$1\"\n [ -z \"${_img}\" ] && return 1\n _is_local_image \"${_img}\" || return 1\n ${DOCKER_CMD} image inspect \"${_img}\" >/dev/null 2>&1\n }\n\n # Helper: pull or skip a single image\n # Args: $1=image $2=exists_msg_key $3=pulling_msg_key\n _pull_image() {\n local _img=\"$1\" _exists_key=\"$2\" _pull_key=\"$3\"\n [ -z \"${_img}\" ] && return 0\n if _local_image_exists \"${_img}\"; then\n log \"$(msg \"${_exists_key}\" \"${_img}\")\"\n return 0\n fi\n log \"$(msg \"${_pull_key}\" \"${_img}\")\"\n local _attempt=1\n while [ $_attempt -le 3 ]; do\n if ${DOCKER_CMD} pull \"${_img}\"; then\n return 0\n fi\n if [ $_attempt -lt 3 ]; then\n log \"Pull failed (attempt ${_attempt}/3), retrying in 5s...\"\n sleep 5\n fi\n _attempt=$((_attempt + 1))\n done\n die \"Failed to pull ${_img} after 3 attempts\"\n return 1\n }\n\n # Embedded controller image (resolve versioned tag, fallback to latest)\n resolve_embedded_image\n if [ \"${AGENTTEAMS_USE_EMBEDDED}\" = \"1\" ]; then\n _local_image_exists \"${EMBEDDED_IMAGE}\" || true\n fi\n\n # Manager image is always required (select based on runtime)\n if [ \"${AGENTTEAMS_MANAGER_RUNTIME}\" = \"copaw\" ]; then\n _pull_image \"${MANAGER_COPAW_IMAGE}\" \"install.image.exists\" \"install.image.pulling_manager\"\n else\n _pull_image \"${MANAGER_IMAGE}\" \"install.image.exists\" \"install.image.pulling_manager\"\n fi\n\n # Pull all worker runtime images (workers may use any runtime regardless of the default)\n _pull_image \"${WORKER_IMAGE}\" \"install.image.worker_exists\" \"install.image.pulling_worker\"\n _pull_image \"${COPAW_WORKER_IMAGE}\" \"install.image.worker_exists\" \"install.image.pulling_worker\"\n _pull_image \"${HERMES_WORKER_IMAGE}\" \"install.image.worker_exists\" \"install.image.pulling_worker\"\n\n # --- Pre-upgrade: extract Matrix passwords from running old containers ---\n # Only needed when upgrading FROM old architecture (v1.0.9) TO embedded.\n # For new-arch-to-new-arch upgrades, credential files already exist with\n # correct room IDs — we must NOT overwrite them.\n _creds_tmp=\"\"\n if [ \"${AGENTTEAMS_UPGRADE:-0}\" = \"1\" ] && [ \"${AGENTTEAMS_USE_EMBEDDED}\" = \"1\" ]; then\n # Detect if upgrading from old arch: old arch has no agentteams-controller container\n # (or has it as a docker-proxy only, not embedded). Check if the existing\n # agentteams-controller is an embedded image (has supervisord) or just a proxy.\n _is_old_arch=0\n if ! ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -q \"^agentteams-controller$\"; then\n _is_old_arch=1\n elif ${DOCKER_CMD} ps -a --format '{{.Names}} {{.Image}}' 2>/dev/null | grep \"^agentteams-controller \" | grep -qv \"embedded\"; then\n _is_old_arch=1\n fi\n\n if [ \"${_is_old_arch}\" = \"1\" ]; then\n _creds_tmp=$(mktemp -d)\n\n # docker exec for Matrix/minio paths only works while agentteams-manager is running.\n _mgr_creds_tempstart=0\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -q '^agentteams-manager$'; then\n if ! ${DOCKER_CMD} ps --format '{{.Names}}' 2>/dev/null | grep -q '^agentteams-manager$'; then\n log \"agentteams-manager is stopped; starting it temporarily to extract Matrix credentials for upgrade...\"\n ${DOCKER_CMD} start agentteams-manager 2>/dev/null || true\n wait_matrix_ready \"agentteams-manager\"\n _mgr_creds_tempstart=1\n fi\n fi\n\n # Manager password (container Config.Env, then secrets file inside running manager, then data volume)\n _mgr_pw=$(${DOCKER_CMD} inspect agentteams-manager --format '{{range .Config.Env}}{{println .}}{{end}}' 2>/dev/null | grep '^AGENTTEAMS_MANAGER_PASSWORD=' | cut -d= -f2-)\n if [ -z \"${_mgr_pw}\" ] && ${DOCKER_CMD} ps --format '{{.Names}}' 2>/dev/null | grep -q '^agentteams-manager$'; then\n _mgr_pw=$(${DOCKER_CMD} exec agentteams-manager bash -c 'source /data/agentteams-secrets.env 2>/dev/null && echo \"${AGENTTEAMS_MANAGER_PASSWORD}\"' 2>/dev/null)\n fi\n if [ -z \"${_mgr_pw}\" ] && ${DOCKER_CMD} volume ls -q 2>/dev/null | grep -q \"^${AGENTTEAMS_DATA_DIR}$\"; then\n _mgr_pw=$(agentteams_read_secret_from_data_volume \"${AGENTTEAMS_DATA_DIR}\" AGENTTEAMS_MANAGER_PASSWORD)\n fi\n\n # Manager admin DM room ID: login as admin, find DM room with @manager\n _mgr_room=\"\"\n if [ -n \"${_mgr_pw}\" ]; then\n _admin_pw=$(grep AGENTTEAMS_ADMIN_PASSWORD \"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\" 2>/dev/null | cut -d= -f2-)\n _admin_user=$(grep AGENTTEAMS_ADMIN_USER \"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\" 2>/dev/null | cut -d= -f2-)\n _admin_user=\"${_admin_user:-admin}\"\n _matrix_domain=$(grep AGENTTEAMS_MATRIX_DOMAIN \"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\" 2>/dev/null | cut -d= -f2-)\n if [ -n \"${_admin_pw}\" ] && ${DOCKER_CMD} ps --format '{{.Names}}' 2>/dev/null | grep -q '^agentteams-manager$'; then\n _admin_token=$(${DOCKER_CMD} exec agentteams-manager curl -sf -X POST http://127.0.0.1:6167/_matrix/client/v3/login \\\n -H \"Content-Type: application/json\" \\\n -d '{\"type\":\"m.login.password\",\"identifier\":{\"type\":\"m.id.user\",\"user\":\"'\"${_admin_user}\"'\"},\"password\":\"'\"${_admin_pw}\"'\"}' 2>/dev/null | python3 -c \"import sys,json; print(json.load(sys.stdin).get('access_token',''))\" 2>/dev/null || true)\n if [ -n \"${_admin_token}\" ]; then\n _mgr_room=$(${DOCKER_CMD} exec agentteams-manager curl -sf -X GET \\\n -H \"Authorization: Bearer ${_admin_token}\" \\\n \"http://127.0.0.1:6167/_matrix/client/v3/joined_rooms\" 2>/dev/null | python3 -c \"\nimport sys,json,subprocess\nrooms = json.load(sys.stdin).get('joined_rooms',[])\nfor room_id in rooms:\n enc = room_id.replace('!','%21')\n members = json.loads(subprocess.check_output([\n 'docker','exec','agentteams-manager','curl','-sf','-X','GET',\n '-H','Authorization: Bearer ${_admin_token}',\n 'http://127.0.0.1:6167/_matrix/client/v3/rooms/'+enc+'/members'\n ]).decode()).get('chunk',[])\n member_ids = [m['state_key'] for m in members]\n if any('manager' in m and 'admin' not in m.split(':')[0] for m in member_ids):\n if len(member_ids) <= 3:\n print(room_id)\n break\n\" 2>/dev/null || true)\n fi\n fi\n if [ -z \"${_mgr_room}\" ]; then\n _mgr_room=$(agentteams_read_admin_dm_room_from_workspace \"${AGENTTEAMS_WORKSPACE_DIR}\")\n fi\n cat > \"${_creds_tmp}/default.env\" <<CREDEOF\nWORKER_PASSWORD=\"${_mgr_pw}\"\nWORKER_MINIO_PASSWORD=\"$(openssl rand -hex 24)\"\nWORKER_GATEWAY_KEY=\"${AGENTTEAMS_MANAGER_GATEWAY_KEY}\"\nWORKER_ROOM_ID=\"${_mgr_room}\"\nCREDEOF\n log \"Extracted Manager Matrix password${_mgr_room:+ and room ID}\"\n fi\n\n # Worker passwords and room IDs from workers-registry.json\n if [ -f \"${AGENTTEAMS_WORKSPACE_DIR}/workers-registry.json\" ]; then\n _worker_names=$(python3 -c \"import json; d=json.load(open('${AGENTTEAMS_WORKSPACE_DIR}/workers-registry.json')); print(' '.join(d.get('workers',{}).keys()))\" 2>/dev/null || true)\n for _wname in ${_worker_names}; do\n _wpw=\"\"\n if ${DOCKER_CMD} ps --format '{{.Names}}' 2>/dev/null | grep -q '^agentteams-manager$'; then\n _wpw=$(${DOCKER_CMD} exec agentteams-manager cat \"/root/agentteams-fs/agents/${_wname}/credentials/matrix/password\" 2>/dev/null || true)\n fi\n if [ -z \"${_wpw}\" ] && ${DOCKER_CMD} volume ls -q 2>/dev/null | grep -q \"^${AGENTTEAMS_DATA_DIR}$\"; then\n _wpw=$(agentteams_read_worker_creds_value_from_volume \"${AGENTTEAMS_DATA_DIR}\" \"${_wname}\" WORKER_PASSWORD)\n fi\n _wroom=$(python3 -c \"import json; d=json.load(open('${AGENTTEAMS_WORKSPACE_DIR}/workers-registry.json')); print(d.get('workers',{}).get('${_wname}',{}).get('room_id',''))\" 2>/dev/null || true)\n if [ -z \"${_wroom}\" ] && ${DOCKER_CMD} volume ls -q 2>/dev/null | grep -q \"^${AGENTTEAMS_DATA_DIR}$\"; then\n _wroom=$(agentteams_read_worker_creds_value_from_volume \"${AGENTTEAMS_DATA_DIR}\" \"${_wname}\" WORKER_ROOM_ID)\n fi\n if [ -n \"${_wpw}\" ]; then\n cat > \"${_creds_tmp}/${_wname}.env\" <<CREDEOF\nWORKER_PASSWORD=\"${_wpw}\"\nWORKER_MINIO_PASSWORD=\"$(openssl rand -hex 24)\"\nWORKER_GATEWAY_KEY=\"$(openssl rand -hex 32)\"\nWORKER_ROOM_ID=\"${_wroom}\"\nCREDEOF\n log \"Extracted ${_wname} Matrix password${_wroom:+ and room ID}\"\n fi\n done\n fi\n\n if [ \"${_mgr_creds_tempstart}\" = \"1\" ]; then\n log \"Stopping agentteams-manager after credential extraction (upgrade will recreate containers)...\"\n ${DOCKER_CMD} stop agentteams-manager 2>/dev/null || true\n fi\n fi # _is_old_arch\n fi\n\n # --- Stop and remove existing containers ---\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' | grep -q \"^agentteams-controller$\"; then\n ${DOCKER_CMD} stop agentteams-controller 2>/dev/null || true\n ${DOCKER_CMD} rm agentteams-controller 2>/dev/null || true\n fi\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' | grep -q \"^agentteams-manager$\"; then\n log \"$(msg install.removing_existing)\"\n ${DOCKER_CMD} stop agentteams-manager 2>/dev/null || true\n ${DOCKER_CMD} rm agentteams-manager 2>/dev/null || true\n fi\n\n # Stop and remove worker containers (controller will recreate via CR reconciliation)\n if [ -n \"${UPGRADE_EXISTING_WORKERS:-}\" ]; then\n log \"$(msg install.existing.stopping_workers)\"\n for w in ${UPGRADE_EXISTING_WORKERS}; do\n ${DOCKER_CMD} stop \"${w}\" 2>/dev/null || true\n ${DOCKER_CMD} rm \"${w}\" 2>/dev/null || true\n log \"$(msg install.existing.removed \"${w}\")\"\n done\n fi\n\n # Clean up legacy containers from v1.0.x and earlier\n for _legacy_name in agentteams-docker-proxy; do\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -qx \"${_legacy_name}\"; then\n log \"Removing legacy container: ${_legacy_name}\"\n ${DOCKER_CMD} stop \"${_legacy_name}\" 2>/dev/null || true\n ${DOCKER_CMD} rm -f \"${_legacy_name}\" 2>/dev/null || true\n fi\n done\n\n # --- Upgrade: inject extracted credentials into data volume ---\n # Only needed for old-arch upgrades (credential files were extracted above).\n if [ -n \"${_creds_tmp}\" ] && [ -d \"${_creds_tmp}\" ] && [ -n \"$(ls -A \"${_creds_tmp}\" 2>/dev/null)\" ]; then\n local _cleanup_ctr=\"agentteams-upgrade-cleanup\"\n ${DOCKER_CMD} rm -f \"${_cleanup_ctr}\" 2>/dev/null || true\n ${DOCKER_CMD} run --rm --name \"${_cleanup_ctr}\" \\\n --entrypoint sh \\\n -v \"${AGENTTEAMS_DATA_DIR}:/data\" \\\n -v \"${_creds_tmp}:/creds:ro\" \\\n \"${EMBEDDED_IMAGE}\" -c '\n rm -rf /data/worker-creds\n mkdir -p /data/worker-creds\n cp /creds/*.env /data/worker-creds/ 2>/dev/null || true\n chmod 600 /data/worker-creds/*.env 2>/dev/null || true\n ' 2>/dev/null && log \"Injected credentials for upgrade\" || log \"Warning: credential injection failed, continuing\"\n rm -rf \"${_creds_tmp}\"\n fi\n\n # --- Start containers ---\n log \"$(msg install.starting_manager)\"\n\n # Build port binding args\n if [ \"${AGENTTEAMS_LOCAL_ONLY:-1}\" = \"1\" ]; then\n _port_prefix=\"127.0.0.1:\"\n else\n _port_prefix=\"\"\n fi\n\n # Ensure agentteams-net Docker network exists\n ${DOCKER_CMD} network inspect agentteams-net >/dev/null 2>&1 || ${DOCKER_CMD} network create agentteams-net\n\n if [ \"${AGENTTEAMS_USE_EMBEDDED}\" != \"1\" ] && [ \"${AGENTTEAMS_UPGRADE:-0}\" = \"1\" ]; then\n # Check if current installation is embedded — downgrade to legacy is not supported\n if ${DOCKER_CMD} ps -a --format '{{.Names}} {{.Image}}' 2>/dev/null | grep \"^agentteams-controller \" | grep -q \"embedded\"; then\n error \"Downgrade from embedded architecture to legacy version (${AGENTTEAMS_VERSION}) is not supported.\"\n error \"Please use 'make uninstall-embedded' first, then do a clean install of the target version.\"\n exit 1\n fi\n fi\n\n if [ \"${AGENTTEAMS_USE_EMBEDDED}\" = \"1\" ]; then\n # ============================================================\n # New architecture: embedded controller + auto-created manager\n # ============================================================\n\n # Internal port: 8080 (Higress gateway inside the container).\n local _internal_gw_port=8080\n local _matrix_domain=\"${AGENTTEAMS_MATRIX_DOMAIN:-matrix-local.agentteams.io:${AGENTTEAMS_PORT_GATEWAY}}\"\n local _aigw_domain=\"${AGENTTEAMS_AI_GATEWAY_DOMAIN:-aigw-local.agentteams.io}\"\n # Ensure internal gateway port is present (container-internal traffic uses 8080)\n case \"${_aigw_domain}\" in *:*) ;; *) _aigw_domain=\"${_aigw_domain}:${_internal_gw_port}\" ;; esac\n local _fs_domain=\"${AGENTTEAMS_FS_DOMAIN:-fs-local.agentteams.io}\"\n case \"${_fs_domain}\" in *:*) ;; *) _fs_domain=\"${_fs_domain}:${_internal_gw_port}\" ;; esac\n\n # Controller env args\n local _ctrl_env_prefix\n _ctrl_env_prefix=\"$(_controller_env_prefix \"${AGENTTEAMS_VERSION}\")\"\n local _storage_prefix\n _storage_prefix=\"$(_controller_storage_prefix \"${AGENTTEAMS_VERSION}\")\"\n local _ctrl_env_args=(\n -e \"${_ctrl_env_prefix}ADMIN_USER=${AGENTTEAMS_ADMIN_USER}\"\n -e \"${_ctrl_env_prefix}ADMIN_PASSWORD=${AGENTTEAMS_ADMIN_PASSWORD}\"\n -e \"${_ctrl_env_prefix}MANAGER_PASSWORD=${AGENTTEAMS_MANAGER_PASSWORD}\"\n -e \"${_ctrl_env_prefix}REGISTRATION_TOKEN=${AGENTTEAMS_REGISTRATION_TOKEN}\"\n -e \"${_ctrl_env_prefix}MINIO_USER=${AGENTTEAMS_MINIO_USER}\"\n -e \"${_ctrl_env_prefix}MINIO_PASSWORD=${AGENTTEAMS_MINIO_PASSWORD}\"\n -e \"${_ctrl_env_prefix}LLM_PROVIDER=${AGENTTEAMS_LLM_PROVIDER}\"\n -e \"${_ctrl_env_prefix}LLM_API_KEY=${AGENTTEAMS_LLM_API_KEY}\"\n -e \"${_ctrl_env_prefix}DEFAULT_MODEL=${AGENTTEAMS_DEFAULT_MODEL}\"\n -e \"${_ctrl_env_prefix}MANAGER_GATEWAY_KEY=${AGENTTEAMS_MANAGER_GATEWAY_KEY}\"\n -e \"${_ctrl_env_prefix}MANAGER_RUNTIME=${AGENTTEAMS_MANAGER_RUNTIME:-copaw}\"\n -e \"${_ctrl_env_prefix}MANAGER_IMAGE=$([ \"${AGENTTEAMS_MANAGER_RUNTIME}\" = \"copaw\" ] && echo \"${MANAGER_COPAW_IMAGE}\" || echo \"${MANAGER_IMAGE}\")\"\n -e \"${_ctrl_env_prefix}DEFAULT_WORKER_RUNTIME=${AGENTTEAMS_DEFAULT_WORKER_RUNTIME:-copaw}\"\n -e \"${_ctrl_env_prefix}WORKER_IMAGE=${WORKER_IMAGE}\"\n -e \"${_ctrl_env_prefix}COPAW_WORKER_IMAGE=${COPAW_WORKER_IMAGE}\"\n -e \"${_ctrl_env_prefix}HERMES_WORKER_IMAGE=${HERMES_WORKER_IMAGE}\"\n -e \"${_ctrl_env_prefix}MATRIX_DOMAIN=${_matrix_domain}\"\n -e \"${_ctrl_env_prefix}ELEMENT_HOMESERVER_URL=http://127.0.0.1:${AGENTTEAMS_PORT_GATEWAY}\"\n -e \"${_ctrl_env_prefix}MATRIX_URL=http://127.0.0.1:6167\"\n -e \"${_ctrl_env_prefix}MATRIX_E2EE=${AGENTTEAMS_MATRIX_E2EE:-0}\"\n -e \"${_ctrl_env_prefix}MINIO_ENDPOINT=http://127.0.0.1:9000\"\n -e \"${_ctrl_env_prefix}MINIO_BUCKET=agentteams-storage\"\n -e \"${_ctrl_env_prefix}STORAGE_PREFIX=${_storage_prefix}\"\n -e \"${_ctrl_env_prefix}FS_ENDPOINT=http://127.0.0.1:9000\"\n -e \"${_ctrl_env_prefix}AI_GATEWAY_URL=http://${_aigw_domain}\"\n -e \"${_ctrl_env_prefix}CONTROLLER_URL=http://agentteams-controller:8090\"\n -e \"${_ctrl_env_prefix}DOCKER_NETWORK=agentteams-net\"\n -e \"${_ctrl_env_prefix}WORKSPACE_DIR=${AGENTTEAMS_WORKSPACE_DIR}\"\n -e \"${_ctrl_env_prefix}HOST_SHARE_DIR=${AGENTTEAMS_HOST_SHARE_DIR}\"\n -e \"${_ctrl_env_prefix}MANAGER_ENABLED=true\"\n -e \"${_ctrl_env_prefix}PORT_MANAGER_CONSOLE=${AGENTTEAMS_PORT_MANAGER_CONSOLE:-18888}\"\n )\n if _use_legacy_image_env \"${AGENTTEAMS_VERSION}\"; then\n _ctrl_env_args+=(\n -e \"${_ctrl_env_prefix}FS_BUCKET=agentteams-storage\"\n -e \"${_ctrl_env_prefix}RESOURCE_PREFIX=agentteams-\"\n )\n else\n _ctrl_env_args+=(\n -e \"${_ctrl_env_prefix}MATRIX_APPSERVICE_ENABLED=${AGENTTEAMS_MATRIX_APPSERVICE_ENABLED:-true}\"\n -e \"${_ctrl_env_prefix}MATRIX_APPSERVICE_AS_TOKEN=${AGENTTEAMS_MATRIX_APPSERVICE_AS_TOKEN:-}\"\n -e \"${_ctrl_env_prefix}MATRIX_APPSERVICE_HS_TOKEN=${AGENTTEAMS_MATRIX_APPSERVICE_HS_TOKEN:-}\"\n )\n fi\n\n # Timezone\n if [ -n \"${AGENTTEAMS_TIMEZONE:-}\" ]; then\n _ctrl_env_args+=(-e \"TZ=${AGENTTEAMS_TIMEZONE}\")\n fi\n\n # Yolo mode\n if [ \"${AGENTTEAMS_YOLO:-}\" = \"1\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}YOLO=1\")\n fi\n\n # Matrix-plugin debug tracing — propagated to every manager + worker\n # the controller spawns, then translated to OPENCLAW_MATRIX_DEBUG=1\n # by the container entrypoints. Use this to diagnose\n # \"worker did not join\" / \"manager replied empty\" hangs.\n if [ \"${AGENTTEAMS_MATRIX_DEBUG:-}\" = \"1\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}MATRIX_DEBUG=1\")\n fi\n\n # Optional: GitHub token\n if [ -n \"${AGENTTEAMS_GITHUB_TOKEN:-}\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}GITHUB_TOKEN=${AGENTTEAMS_GITHUB_TOKEN}\")\n fi\n\n # Optional: embedding model\n if [ -n \"${AGENTTEAMS_EMBEDDING_MODEL:-}\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}EMBEDDING_MODEL=${AGENTTEAMS_EMBEDDING_MODEL}\")\n fi\n\n # Optional: OpenAI-compatible base URL\n if [ -n \"${AGENTTEAMS_OPENAI_BASE_URL:-}\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}OPENAI_BASE_URL=${AGENTTEAMS_OPENAI_BASE_URL}\")\n fi\n # Optional: language\n if [ -n \"${AGENTTEAMS_LANGUAGE:-}\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}LANGUAGE=${AGENTTEAMS_LANGUAGE}\")\n fi\n\n # Optional: CMS/ARMS observability. In embedded mode the controller\n # spawns the Manager and Workers, so it must receive these settings.\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}CMS_TRACES_ENABLED=${AGENTTEAMS_CMS_TRACES_ENABLED:-false}\")\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}CMS_SERVICE_NAME=${AGENTTEAMS_CMS_SERVICE_NAME:-agentteams-manager}\")\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}CMS_METRICS_ENABLED=${AGENTTEAMS_CMS_METRICS_ENABLED:-false}\")\n if [ -n \"${AGENTTEAMS_CMS_ENDPOINT:-}\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}CMS_ENDPOINT=${AGENTTEAMS_CMS_ENDPOINT}\")\n fi\n if [ -n \"${AGENTTEAMS_CMS_LICENSE_KEY:-}\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}CMS_LICENSE_KEY=${AGENTTEAMS_CMS_LICENSE_KEY}\")\n fi\n if [ -n \"${AGENTTEAMS_CMS_PROJECT:-}\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}CMS_PROJECT=${AGENTTEAMS_CMS_PROJECT}\")\n fi\n if [ -n \"${AGENTTEAMS_CMS_WORKSPACE:-}\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}CMS_WORKSPACE=${AGENTTEAMS_CMS_WORKSPACE}\")\n fi\n\n # shellcheck disable=SC2086\n ${DOCKER_CMD} run -d \\\n --name agentteams-controller \\\n --network agentteams-net \\\n --network-alias matrix-local.agentteams.io \\\n --network-alias aigw-local.agentteams.io \\\n --network-alias fs-local.agentteams.io \\\n \"${_ctrl_env_args[@]}\" \\\n -v \"${CONTAINER_SOCK}:/var/run/docker.sock\" \\\n --security-opt label=disable \\\n -v \"${AGENTTEAMS_DATA_DIR}:/data\" \\\n -v \"${AGENTTEAMS_WORKSPACE_DIR}:/root/agentteams-fs/agents/manager\" \\\n -p \"${_port_prefix}${AGENTTEAMS_PORT_GATEWAY}:8080\" \\\n -p \"${_port_prefix}${AGENTTEAMS_PORT_CONSOLE}:8001\" \\\n -p \"${_port_prefix}${AGENTTEAMS_PORT_ELEMENT_WEB:-18088}:8088\" \\\n --restart unless-stopped \\\n \"${EMBEDDED_IMAGE}\"\n\n log \"Embedded controller started: agentteams-controller\"\n\n # Wait for infrastructure inside the controller container\n _wait_for_url() {\n local url=\"$1\" ctr=\"$2\" max_wait=\"${3:-120}\" desc=\"${4:-service}\"\n local elapsed=0\n log \"Waiting for ${desc}...\"\n while [ $elapsed -lt $max_wait ]; do\n if ${DOCKER_CMD} exec \"${ctr}\" curl -sf \"${url}\" >/dev/null 2>&1; then\n log \"${desc} is ready (${elapsed}s)\"\n return 0\n fi\n sleep 2\n elapsed=$((elapsed + 2))\n done\n log \"ERROR: ${desc} not ready after ${max_wait}s\"\n return 1\n }\n\n _wait_for_url \"http://127.0.0.1:6167/_tuwunel/server_version\" agentteams-controller 120 \"Tuwunel (Matrix)\" || exit 1\n _wait_for_url \"http://127.0.0.1:9000/minio/health/live\" agentteams-controller 60 \"MinIO\" || exit 1\n _wait_for_url \"http://127.0.0.1:8080/status\" agentteams-controller 120 \"Higress Gateway\" || exit 1\n\n # Wait for controller to create Manager Agent container\n log \"Waiting for Manager Agent container...\"\n local _mgr_wait=0\n local _mgr_max=300\n while [ $_mgr_wait -lt $_mgr_max ]; do\n if ${DOCKER_CMD} ps --format '{{.Names}}' 2>/dev/null | grep -q \"^agentteams-manager$\"; then\n log \"Manager Agent container detected (${_mgr_wait}s)\"\n break\n fi\n sleep 3\n _mgr_wait=$((_mgr_wait + 3))\n done\n if [ $_mgr_wait -ge $_mgr_max ]; then\n log \"ERROR: Manager Agent container not created after ${_mgr_max}s\"\n log \"Controller logs:\"\n ${DOCKER_CMD} exec agentteams-controller tail -30 /var/log/agentteams/agentteams-controller-error.log 2>/dev/null || true\n exit 1\n fi\n\n # Wait for Manager Agent to be running\n log \"Waiting for Manager Agent to start...\"\n local _agent_wait=0\n while [ $_agent_wait -lt 120 ]; do\n local _state\n _state=$(${DOCKER_CMD} inspect --format '{{.State.Status}}' agentteams-manager 2>/dev/null || echo \"missing\")\n if [ \"${_state}\" = \"running\" ]; then\n log \"Manager Agent is running\"\n break\n fi\n sleep 2\n _agent_wait=$((_agent_wait + 2))\n done\n\n # Enable yolo mode in agent if requested\n if [ \"${AGENTTEAMS_YOLO:-}\" = \"1\" ]; then\n ${DOCKER_CMD} exec agentteams-manager touch /root/manager-workspace/yolo-mode 2>/dev/null || true\n fi\n\n # Wait for the controller to send the first-boot welcome message.\n # The controller gates this on (a) Manager joining the DM room and\n # (b) Higress WASM key-auth propagation actually clearing /v1/chat/completions\n # for the Manager's gateway key — typically ~45-90s on a fresh install.\n # We poll Manager CR Status.WelcomeSent via the in-container agt CLI,\n # exec'd inside agentteams-controller (the source-of-truth container — its\n # bundled CLI binary is always in lockstep with whatever controller\n # binary is currently serving the HTTP API, since they're the same\n # `go build` output. The agentteams-manager container's CLI may lag the\n # controller across image upgrades and silently drop the welcomeSent\n # field, leaving this loop hung). The controller container mints a\n # long-lived admin SA token at startup and writes it to\n # AGENTTEAMS_AUTH_TOKEN_FILE=/var/run/agentteams/cli-token (set as a Dockerfile\n # ENV default), so a bare `docker exec agentteams-controller agt …`\n # auto-discovers both the endpoint and the token. There is a brief\n # window after container start before bootstrapAdminCLIToken completes\n # where the file may be empty / absent — the loop's silent retry\n # handles that the same way it handles the manager-not-yet-running\n # case below.\n if ${DOCKER_CMD} exec agentteams-controller sh -c 'command -v agt' >/dev/null 2>&1; then\n log \"$(msg install.welcome_msg.waiting)\"\n local _welcome_wait=0\n local _welcome_max=\"${AGENTTEAMS_WELCOME_TIMEOUT:-300}\"\n local _welcome_done=0\n while [ $_welcome_wait -lt $_welcome_max ]; do\n # `tr -d` strips whitespace/CR so the grep stays robust to\n # any future change in go-json field ordering or formatting.\n local _wjson\n _wjson=$(${DOCKER_CMD} exec agentteams-controller \\\n agt get managers default -o json 2>/dev/null || true)\n if [ -n \"${_wjson}\" ] && printf '%s' \"${_wjson}\" | tr -d ' \\r\\n' | grep -q '\"welcomeSent\":true'; then\n log \"$(msg install.welcome_msg.confirmed \"${_welcome_wait}\")\"\n _welcome_done=1\n break\n fi\n sleep 3\n _welcome_wait=$((_welcome_wait + 3))\n done\n if [ $_welcome_done -ne 1 ]; then\n # Non-fatal: install is still good. Keep going to the success\n # banner so the admin can use Element Web to nudge Manager into\n # onboarding manually (one DM message is enough).\n log \"$(msg install.welcome_msg.timeout \"${_welcome_max}\")\"\n log \"$(msg install.welcome_msg.timeout_hint)\"\n log \"$(msg install.welcome_msg.timeout_inspect)\"\n fi\n else\n log \"$(msg install.welcome_msg.poll_unavailable)\"\n fi\n\n else\n # ============================================================\n # Legacy architecture: all-in-one manager container\n # ============================================================\n\n NETWORK_ARGS=\"--network agentteams-net\"\n NETWORK_ALIAS_ARGS=\"--network-alias matrix-local.agentteams.io --network-alias aigw-local.agentteams.io --network-alias fs-local.agentteams.io\"\n for _domain in \"${AGENTTEAMS_MATRIX_CLIENT_DOMAIN:-}\" \"${AGENTTEAMS_CONSOLE_DOMAIN:-}\"; do\n if [ -n \"${_domain}\" ] && [[ \"${_domain}\" == *-local.agentteams.io ]]; then\n NETWORK_ALIAS_ARGS=\"${NETWORK_ALIAS_ARGS} --network-alias ${_domain}\"\n fi\n done\n\n # Start Docker API proxy if enabled (security layer between Manager and Docker daemon)\n PROXY_ARGS=\"\"\n if [ \"${AGENTTEAMS_DOCKER_PROXY:-1}\" = \"1\" ] && [ -n \"${CONTAINER_SOCK:-}\" ]; then\n local _proxy_image=\"${AGENTTEAMS_REGISTRY}/higress/agentteams-docker-proxy:${AGENTTEAMS_VERSION}\"\n # Try versioned tag, fallback to latest\n if ! ${DOCKER_CMD} image inspect \"${_proxy_image}\" >/dev/null 2>&1; then\n ${DOCKER_CMD} pull \"${_proxy_image}\" 2>/dev/null || {\n _proxy_image=\"${AGENTTEAMS_REGISTRY}/higress/agentteams-docker-proxy:latest\"\n ${DOCKER_CMD} pull \"${_proxy_image}\" 2>/dev/null || true\n }\n fi\n if ${DOCKER_CMD} image inspect \"${_proxy_image}\" >/dev/null 2>&1; then\n log \"Starting Docker API proxy...\"\n ${DOCKER_CMD} run -d \\\n --name agentteams-docker-proxy \\\n --network agentteams-net \\\n -v \"${CONTAINER_SOCK}:/var/run/docker.sock\" \\\n --security-opt label=disable \\\n -e AGENTTEAMS_WORKER_IMAGE=\"${WORKER_IMAGE}\" \\\n -e AGENTTEAMS_COPAW_WORKER_IMAGE=\"${COPAW_WORKER_IMAGE}\" \\\n -e AGENTTEAMS_HERMES_WORKER_IMAGE=\"${HERMES_WORKER_IMAGE}\" \\\n ${AGENTTEAMS_PROXY_ALLOWED_REGISTRIES:+-e AGENTTEAMS_PROXY_ALLOWED_REGISTRIES=\"${AGENTTEAMS_PROXY_ALLOWED_REGISTRIES}\"} \\\n --restart unless-stopped \\\n \"${_proxy_image}\"\n PROXY_ARGS=\"-e AGENTTEAMS_CONTROLLER_URL=http://agentteams-docker-proxy:2375 -e AGENTTEAMS_CONTAINER_API=http://agentteams-docker-proxy:2375\"\n SOCKET_MOUNT_ARGS=\"\"\n fi\n fi\n\n # Pass host timezone to container\n TZ_ARGS=\"\"\n if [ -n \"${AGENTTEAMS_TIMEZONE:-}\" ]; then\n TZ_ARGS=\"-e TZ=${AGENTTEAMS_TIMEZONE}\"\n fi\n\n YOLO_ARGS=\"\"\n if [ \"${AGENTTEAMS_YOLO:-}\" = \"1\" ]; then\n YOLO_ARGS=\"-e AGENTTEAMS_YOLO=1\"\n fi\n\n MATRIX_DEBUG_ARGS=\"\"\n if [ \"${AGENTTEAMS_MATRIX_DEBUG:-}\" = \"1\" ]; then\n MATRIX_DEBUG_ARGS=\"-e AGENTTEAMS_MATRIX_DEBUG=1\"\n fi\n\n # shellcheck disable=SC2086\n ${DOCKER_CMD} run -d \\\n --name agentteams-manager \\\n --env-file \"${ENV_FILE}\" \\\n -e HOME=/root/manager-workspace \\\n -w /root/manager-workspace \\\n -e HOST_ORIGINAL_HOME=\"${AGENTTEAMS_HOST_SHARE_DIR}\" \\\n -e AGENTTEAMS_MANAGER_RUNTIME=\"${AGENTTEAMS_MANAGER_RUNTIME:-copaw}\" \\\n ${JVM_ARGS:+-e JVM_ARGS=\"${JVM_ARGS}\"} \\\n ${YOLO_ARGS} \\\n ${MATRIX_DEBUG_ARGS} \\\n ${TZ_ARGS} \\\n ${SOCKET_MOUNT_ARGS} \\\n ${NETWORK_ARGS} \\\n ${NETWORK_ALIAS_ARGS} \\\n ${PROXY_ARGS} \\\n -p \"${_port_prefix}${AGENTTEAMS_PORT_GATEWAY}:8080\" \\\n -p \"${_port_prefix}${AGENTTEAMS_PORT_CONSOLE}:8001\" \\\n -p \"${_port_prefix}${AGENTTEAMS_PORT_ELEMENT_WEB:-18088}:8088\" \\\n -p \"127.0.0.1:${AGENTTEAMS_PORT_MANAGER_CONSOLE:-18888}:18888\" \\\n ${DATA_MOUNT_ARGS} \\\n ${WORKSPACE_MOUNT_ARGS} \\\n ${HOST_SHARE_MOUNT_ARGS} \\\n --restart unless-stopped \\\n \"$([ \"${AGENTTEAMS_MANAGER_RUNTIME}\" = \"copaw\" ] && echo \"${MANAGER_COPAW_IMAGE}\" || echo \"${MANAGER_IMAGE}\")\"\n\n # Wait for Manager agent to be ready\n wait_manager_ready \"agentteams-manager\"\n\n # Wait for Matrix server to be ready\n wait_matrix_ready \"agentteams-manager\"\n\n # Post-install verification (non-fatal: warnings only)\n local _verify_script\n _verify_script=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)/agentteams-verify.sh\"\n if [ -f \"${_verify_script}\" ]; then\n bash \"${_verify_script}\" \"agentteams-manager\" || {\n log \"WARNING: Some post-install checks failed. Re-run: bash install/agentteams-verify.sh\"\n }\n fi\n fi\n unset _port_prefix\n\n # ── Start Dashboard (optional) ────────────────────────────────────\n _start_dashboard\n\n # Apply Podman autostart if selected and environment matches\n if [ \"${DOCKER_CMD}\" = \"podman\" ] && [ \"${AGENTTEAMS_PODMAN_AUTOSTART:-0}\" = \"1\" ]; then\n setup_podman_autostart\n fi\n\n log \"\"\n log \"$(msg success.title)\"\n log \"\"\n log \"$(msg success.domains_configured)\"\n log \" ${AGENTTEAMS_MATRIX_DOMAIN%%:*} ${AGENTTEAMS_MATRIX_CLIENT_DOMAIN} ${AGENTTEAMS_AI_GATEWAY_DOMAIN} ${AGENTTEAMS_FS_DOMAIN} ${AGENTTEAMS_CONSOLE_DOMAIN}\"\n log \"\"\n local lan_ip\n lan_ip=$(detect_lan_ip)\n echo -e \"\\033[33m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\033[0m\"\n echo -e \"\\033[33m $(msg success.open_url)\\033[0m\"\n echo -e \"\\033[33m \\033[0m\"\n echo -e \"\\033[1;36m http://127.0.0.1:${AGENTTEAMS_PORT_ELEMENT_WEB:-18088}/#/login\\033[0m\"\n echo -e \"\\033[33m \\033[0m\"\n echo -e \"\\033[33m $(msg success.login_with)\\033[0m\"\n echo -e \"\\033[33m $(msg success.username \"${AGENTTEAMS_ADMIN_USER}\")\\033[0m\"\n echo -e \"\\033[33m $(msg success.password \"${AGENTTEAMS_ADMIN_PASSWORD}\")\\033[0m\"\n echo -e \"\\033[33m \\033[0m\"\n echo -e \"\\033[33m $(msg success.after_login)\\033[0m\"\n echo -e \"\\033[33m $(msg success.tell_it)\\033[0m\"\n echo -e \"\\033[33m $(msg success.manager_auto)\\033[0m\"\n echo -e \"\\033[33m \\033[0m\"\n echo -e \"\\033[33m ───────────────────────────────────────────────────────────────────────────── \\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_title)\\033[0m\"\n echo -e \"\\033[33m \\033[0m\"\n if [ -n \"${lan_ip}\" ]; then\n echo -e \"\\033[33m $(msg success.mobile_step1)\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_step2 \"http://${lan_ip}:${AGENTTEAMS_PORT_GATEWAY}\")\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_step3)\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_username \"${AGENTTEAMS_ADMIN_USER}\")\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_password \"${AGENTTEAMS_ADMIN_PASSWORD}\")\\033[0m\"\n else\n echo -e \"\\033[33m $(msg success.mobile_step1)\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_step2_noip \"${AGENTTEAMS_PORT_GATEWAY}\")\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_noip_hint)\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_step3)\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_username \"${AGENTTEAMS_ADMIN_USER}\")\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_password \"${AGENTTEAMS_ADMIN_PASSWORD}\")\\033[0m\"\n fi\n echo -e \"\\033[33m \\033[0m\"\n echo -e \"\\033[33m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\033[0m\"\n log \"\"\n log \"$(msg success.other_consoles)\"\n log \"$(msg success.higress_console \"${AGENTTEAMS_PORT_CONSOLE}\" \"${AGENTTEAMS_ADMIN_USER}\" \"${AGENTTEAMS_ADMIN_PASSWORD}\")\"\n if [ \"${AGENTTEAMS_DASHBOARD:-0}\" = \"1\" ]; then\n log \"$(msg success.dashboard \"${AGENTTEAMS_PORT_DASHBOARD:-13000}\")\"\n fi\n if [ \"${AGENTTEAMS_USE_EMBEDDED}\" != \"1\" ]; then\n log \"$(msg success.manager_console \"${AGENTTEAMS_PORT_MANAGER_CONSOLE:-18888}\")\"\n log \"$(msg success.manager_console_gateway \"${AGENTTEAMS_ADMIN_USER}\" \"${AGENTTEAMS_ADMIN_PASSWORD}\")\"\n fi\n log \"\"\n log \"$(msg success.switch_llm.title)\"\n log \"$(msg success.switch_llm.hint)\"\n log \"$(msg success.switch_llm.docs)\"\n log \"$(msg success.switch_llm.url)\"\n log \"\"\n log \"$(msg success.tip)\"\n log \"\"\n if [ \"${AGENTTEAMS_LOCAL_ONLY:-1}\" != \"1\" ]; then\n echo -e \"\\033[33m$(msg port.local_only.https_hint)\\033[0m\"\n log \"\"\n fi\n log \"$(msg success.config_file \"${ENV_FILE}\")\"\n log \"$(msg success.data_volume \"${AGENTTEAMS_DATA_DIR}\")\"\n log \"$(msg success.workspace \"${AGENTTEAMS_WORKSPACE_DIR}\")\"\n}\n\n# ============================================================\n# Worker Installation (One-Click)\n# ============================================================\n\ninstall_worker() {\n local WORKER_NAME=\"\"\n local FS=\"\"\n local FS_KEY=\"\"\n local FS_SECRET=\"\"\n local RESET=false\n local SKILLS_API_URL=\"\"\n\n # Parse arguments\n while [ $# -gt 0 ]; do\n case $1 in\n --name) WORKER_NAME=\"$2\"; shift 2 ;;\n --fs) FS=\"$2\"; shift 2 ;;\n --fs-key) FS_KEY=\"$2\"; shift 2 ;;\n --fs-secret) FS_SECRET=\"$2\"; shift 2 ;;\n --skills-api-url) SKILLS_API_URL=\"$2\"; shift 2 ;;\n --reset) RESET=true; shift ;;\n *) die \"$(msg error.unknown_option \"$1\")\" ;;\n esac\n done\n\n # Validate required params\n [ -z \"${WORKER_NAME}\" ] && die \"$(msg error.name_required)\"\n [ -z \"${FS}\" ] && die \"$(msg error.fs_required)\"\n [ -z \"${FS_KEY}\" ] && die \"$(msg error.fs_key_required)\"\n [ -z \"${FS_SECRET}\" ] && die \"$(msg error.fs_secret_required)\"\n\n local CONTAINER_NAME=\"agentteams-worker-${WORKER_NAME}\"\n\n # Handle reset\n if [ \"${RESET}\" = true ]; then\n log \"$(msg worker.resetting \"${WORKER_NAME}\")\"\n ${DOCKER_CMD} stop \"${CONTAINER_NAME}\" 2>/dev/null || true\n ${DOCKER_CMD} rm \"${CONTAINER_NAME}\" 2>/dev/null || true\n fi\n\n # Check for existing container\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' | grep -q \"^${CONTAINER_NAME}$\"; then\n die \"$(msg worker.exists \"${CONTAINER_NAME}\")\"\n fi\n\n log \"$(msg worker.starting \"${WORKER_NAME}\")\"\n\n # Build docker run args\n local DOCKER_ENV=\"\"\n DOCKER_ENV=\"${DOCKER_ENV} -e HOME=/root/agentteams-fs/agents/${WORKER_NAME}\"\n DOCKER_ENV=\"${DOCKER_ENV} -w /root/agentteams-fs/agents/${WORKER_NAME}\"\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_WORKER_NAME=${WORKER_NAME}\"\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_FS_ENDPOINT=${FS}\"\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_FS_ACCESS_KEY=${FS_KEY}\"\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_FS_SECRET_KEY=${FS_SECRET}\"\n\n if [ -z \"${SKILLS_API_URL}\" ]; then\n if [ -n \"${AGENTTEAMS_SKILLS_API_URL:-}\" ]; then\n SKILLS_API_URL=\"${AGENTTEAMS_SKILLS_API_URL}\"\n else\n SKILLS_API_URL=\"nacos://market.agentteams.io:80/public\"\n fi\n fi\n\n # Add SKILLS_API_URL if specified\n DOCKER_ENV=\"${DOCKER_ENV} -e SKILLS_API_URL=${SKILLS_API_URL}\"\n log \"$(msg worker.skills_url \"${SKILLS_API_URL}\")\"\n if [ -n \"${AGENTTEAMS_NACOS_USERNAME:-}\" ]; then\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_NACOS_USERNAME=${AGENTTEAMS_NACOS_USERNAME}\"\n fi\n if [ -n \"${AGENTTEAMS_NACOS_PASSWORD:-}\" ]; then\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_NACOS_PASSWORD=${AGENTTEAMS_NACOS_PASSWORD}\"\n fi\n if [ -n \"${AGENTTEAMS_NACOS_TOKEN:-}\" ]; then\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_NACOS_TOKEN=${AGENTTEAMS_NACOS_TOKEN}\"\n fi\n\n # shellcheck disable=SC2086\n ${DOCKER_CMD} run -d \\\n --name \"${CONTAINER_NAME}\" \\\n ${DOCKER_ENV} \\\n --restart unless-stopped \\\n \"${WORKER_IMAGE}\"\n\n log \"\"\n log \"$(msg worker.started \"${WORKER_NAME}\")\"\n log \"$(msg worker.container \"${CONTAINER_NAME}\")\"\n log \"$(msg worker.view_logs \"${CONTAINER_NAME}\")\"\n}\n\n# ============================================================\n# Main\n# ============================================================\n\n# ============================================================\n# LLM API connectivity test\n# ============================================================\n\ntest_llm_connectivity() {\n local base_url=\"$1\"\n local api_key=\"$2\"\n local model=\"$3\"\n local hint=\"${4:-}\" # optional: extra hint shown on failure\n if ! command -v curl >/dev/null 2>&1; then\n echo -e \"\\033[33m$(msg llm.openai.test.no_curl)\\033[0m\"\n return\n fi\n log \"$(msg llm.openai.test.testing)\"\n local _body _http_code _tmpfile\n _tmpfile=$(mktemp)\n _http_code=$(curl -s -o \"${_tmpfile}\" -w \"%{http_code}\" \\\n -X POST \"${base_url%/}/chat/completions\" \\\n -H \"Authorization: Bearer ${api_key}\" \\\n -H \"Content-Type: application/json\" \\\n -H \"User-Agent: AgentTeams/${AGENTTEAMS_VERSION:-latest}\" \\\n --max-time 30 \\\n -d \"{\\\"model\\\":\\\"${model}\\\",\\\"messages\\\":[{\\\"role\\\":\\\"user\\\",\\\"content\\\":\\\"hi\\\"}]}\" \\\n 2>/dev/null)\n _body=$(cat \"${_tmpfile}\")\n rm -f \"${_tmpfile}\"\n if [ \"${_http_code}\" = \"200\" ] || [ \"${_http_code}\" = \"201\" ]; then\n log \"$(msg llm.openai.test.ok)\"\n else\n echo -e \"\\033[33m$(msg llm.openai.test.fail \"${_http_code}\" \"${_body}\")\\033[0m\"\n if [ -n \"${hint}\" ]; then\n echo -e \"\\033[33m${hint}\\033[0m\"\n fi\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" != \"1\" ]; then\n local _confirm\n read -e -p \"$(msg llm.openai.test.confirm)\" _confirm\n if [ \"${_confirm}\" = \"b\" ]; then\n STEP_RESULT=\"back\"\n return 1\n fi\n if [ \"${_confirm}\" != \"y\" ] && [ \"${_confirm}\" != \"Y\" ]; then\n log \"$(msg llm.openai.test.aborted)\"\n exit 1\n fi\n fi\n fi\n}\n\ntest_embedding_connectivity() {\n local base_url=\"$1\"\n local api_key=\"$2\"\n local model=\"$3\"\n if ! command -v curl >/dev/null 2>&1; then\n return 0\n fi\n log \"$(msg llm.embedding.test.testing)\"\n local _body _http_code _tmpfile\n _tmpfile=$(mktemp)\n _http_code=$(curl -s -o \"${_tmpfile}\" -w \"%{http_code}\" \\\n -X POST \"${base_url%/}/embeddings\" \\\n -H \"Authorization: Bearer ${api_key}\" \\\n -H \"Content-Type: application/json\" \\\n -H \"User-Agent: AgentTeams/${AGENTTEAMS_VERSION:-latest}\" \\\n --max-time 30 \\\n -d \"{\\\"model\\\":\\\"${model}\\\",\\\"input\\\":\\\"test\\\"}\" \\\n 2>/dev/null)\n _body=$(cat \"${_tmpfile}\")\n rm -f \"${_tmpfile}\"\n if [ \"${_http_code}\" = \"200\" ] || [ \"${_http_code}\" = \"201\" ]; then\n log \"$(msg llm.embedding.test.ok)\"\n return 0\n else\n echo -e \"\\033[33m$(msg llm.embedding.test.fail \"${_http_code}\" \"${_body}\")\\033[0m\"\n return 1\n fi\n}\n\n# ============================================================\n# Uninstall\n# ============================================================\n\nuninstall_agentteams() {\n log \"$(msg uninstall.title)\"\n\n # Safely disable and remove the dedicated AgentTeams Podman autostart service\n if [ \"${DOCKER_CMD}\" = \"podman\" ] && command -v systemctl >/dev/null 2>&1; then\n local _systemctl_cmd=\"systemctl\"\n local _service_dir=\"/etc/systemd/system\"\n\n if [ \"$(id -u)\" != \"0\" ]; then\n _systemctl_cmd=\"systemctl --user\"\n _service_dir=\"${HOME}/.config/systemd/user\"\n fi\n\n if [ -f \"${_service_dir}/agentteams-podman-restart.service\" ]; then\n log \"Disabling and removing dedicated AgentTeams Podman autostart service...\"\n ${_systemctl_cmd} disable --now agentteams-podman-restart.service 2>/dev/null || true\n rm -f \"${_service_dir}/agentteams-podman-restart.service\"\n ${_systemctl_cmd} daemon-reload 2>/dev/null || true\n fi\n # Note: Native podman-restart.service is strictly untouched per maintainer review.\n fi\n\n # Capture manager image before removing (needed for workspace cleanup on Linux)\n local manager_image=\"\"\n manager_image=$(${DOCKER_CMD} inspect agentteams-manager --format '{{.Config.Image}}' 2>/dev/null || true)\n\n # Stop and remove manager\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -q \"^agentteams-manager$\"; then\n log \"$(msg uninstall.stopping_manager)\"\n ${DOCKER_CMD} stop agentteams-manager >/dev/null 2>&1 || true\n ${DOCKER_CMD} rm agentteams-manager >/dev/null 2>&1 || true\n fi\n\n # Stop and remove workers\n local workers\n workers=$(${DOCKER_CMD} ps -a --filter \"name=agentteams-worker-\" --format '{{.Names}}' 2>/dev/null || true)\n if [ -n \"${workers}\" ]; then\n log \"$(msg uninstall.stopping_workers)\"\n echo \"${workers}\" | while read -r w; do\n ${DOCKER_CMD} rm -f \"${w}\" >/dev/null 2>&1 || true\n log \"$(msg uninstall.removed \"${w}\")\"\n done\n fi\n\n # Stop and remove the dashboard container (installed via step_dashboard)\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -q \"^agentteams-dashboard$\"; then\n log \"Stopping and removing agentteams-dashboard...\"\n ${DOCKER_CMD} stop agentteams-dashboard >/dev/null 2>&1 || true\n ${DOCKER_CMD} rm agentteams-dashboard >/dev/null 2>&1 || true\n fi\n if ${DOCKER_CMD} volume inspect agentteams-dashboard-data >/dev/null 2>&1; then\n log \"Removing dashboard data volume: agentteams-dashboard-data\"\n ${DOCKER_CMD} volume rm agentteams-dashboard-data >/dev/null 2>&1 || true\n fi\n\n # Stop and remove docker-proxy (legacy ≤ v1.0.x; current arch uses\n # agentteams-controller for the same role)\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -q \"^agentteams-docker-proxy$\"; then\n log \"$(msg uninstall.removing_proxy)\"\n ${DOCKER_CMD} stop agentteams-docker-proxy >/dev/null 2>&1 || true\n ${DOCKER_CMD} rm agentteams-docker-proxy >/dev/null 2>&1 || true\n fi\n\n # Stop and remove the embedded controller container. MUST happen\n # before the `docker volume rm agentteams-data` step below — in embedded\n # mode agentteams-controller mounts agentteams-data at /data (Tuwunel DB,\n # MinIO state, Higress state, all the room messages), and `volume\n # rm` against an in-use volume fails silently because of the trailing\n # `|| true`. Skipping this used to leave room/message history behind\n # across \"uninstall + reinstall\" cycles. See PR #692.\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -q \"^agentteams-controller$\"; then\n log \"$(msg uninstall.stopping_controller)\"\n ${DOCKER_CMD} stop agentteams-controller >/dev/null 2>&1 || true\n ${DOCKER_CMD} rm agentteams-controller >/dev/null 2>&1 || true\n fi\n\n # Read env file for data/workspace info before removing\n local env_file=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n [ ! -f \"${env_file}\" ] && [ -f \"./agentteams-manager.env\" ] && env_file=\"./agentteams-manager.env\"\n\n local data_dir=\"\" workspace_dir=\"\"\n if [ -f \"${env_file}\" ]; then\n data_dir=$(grep '^AGENTTEAMS_DATA_DIR=' \"${env_file}\" 2>/dev/null | cut -d= -f2- || true)\n workspace_dir=$(grep '^AGENTTEAMS_WORKSPACE_DIR=' \"${env_file}\" 2>/dev/null | cut -d= -f2- || true)\n fi\n\n # Remove Docker volume\n local vol=\"${data_dir:-agentteams-data}\"\n if ${DOCKER_CMD} volume inspect \"${vol}\" >/dev/null 2>&1; then\n log \"$(msg uninstall.removing_volume \"${vol}\")\"\n ${DOCKER_CMD} volume rm \"${vol}\" >/dev/null 2>&1 || true\n fi\n\n # Remove Docker network\n if ${DOCKER_CMD} network ls --format '{{.Name}}' 2>/dev/null | grep -q \"^agentteams-net$\"; then\n log \"$(msg uninstall.removing_network)\"\n ${DOCKER_CMD} network rm agentteams-net >/dev/null 2>&1 || true\n fi\n\n # Remove workspace directory\n if [ -n \"${workspace_dir}\" ] && [ -d \"${workspace_dir}\" ]; then\n log \"$(msg uninstall.removing_workspace \"${workspace_dir}\")\"\n rm -rf \"${workspace_dir}\" 2>/dev/null || true\n if [ -d \"${workspace_dir}\" ]; then\n log \"$(msg uninstall.removing_workspace_elevated)\"\n local _parent _base\n _parent=$(dirname \"${workspace_dir}\")\n _base=$(basename \"${workspace_dir}\")\n local _rm_image=\"${manager_image:-busybox}\"\n ${DOCKER_CMD} run --rm --entrypoint sh \\\n -v \"${_parent}:/host-parent\" \\\n \"${_rm_image}\" \\\n -c \"rm -rf /host-parent/${_base}\" 2>/dev/null || true\n fi\n fi\n\n # Remove env file\n if [ -f \"${env_file}\" ]; then\n log \"$(msg uninstall.removing_env \"${env_file}\")\"\n rm -f \"${env_file}\"\n fi\n\n # Remove install log\n if [ -f \"${HOME}/agentteams-install.log\" ]; then\n log \"$(msg uninstall.removing_log \"${HOME}/agentteams-install.log\")\"\n rm -f \"${HOME}/agentteams-install.log\"\n fi\n\n echo \"\"\n log \"$(msg uninstall.done)\"\n}\n\n# ============================================================\n# Check container runtime (docker or podman) and environment\n# ============================================================\ncheck_container_runtime() {\n # Ensure the rootless environment baseline (XDG_RUNTIME_DIR) is ready before engine detection.\n if [ \"$(id -u)\" -ne 0 ] && [ -z \"${XDG_RUNTIME_DIR:-}\" ]; then\n export XDG_RUNTIME_DIR=\"/run/user/$(id -u)\"\n fi\n\n if command -v docker >/dev/null 2>&1; then\n DOCKER_CMD=\"docker\"\n # Check for podman disguised as docker (alias or podman-docker package)\n if docker --version 2>/dev/null | grep -qi \"podman\"; then\n DOCKER_CMD=\"podman\"\n fi\n elif command -v podman >/dev/null 2>&1; then\n DOCKER_CMD=\"podman\"\n else\n echo -e \"\\033[31m[AgentTeams ERROR]\\033[0m $(msg error.docker_not_found)\" >&2\n exit 1\n fi\n\n # Unified health check (Validates runtime responsiveness)\n if ! ${DOCKER_CMD} ps >/dev/null 2>&1; then\n echo -e \"\\033[31m[AgentTeams ERROR]\\033[0m $(msg error.docker_not_running)\" >&2\n exit 1\n fi\n}\n\ncheck_container_runtime\n\ncase \"${1:-}\" in\n manager|\"\")\n install_manager\n ;;\n worker)\n shift\n install_worker \"$@\"\n ;;\n dashboard)\n check_container_runtime\n load_current_params_from_env\n AGENTTEAMS_DASHBOARD=\"${AGENTTEAMS_DASHBOARD:-1}\"\n AGENTTEAMS_DASHBOARD_VERSION=\"${AGENTTEAMS_DASHBOARD_VERSION:-v1.2.0-beta.1}\"\n AGENTTEAMS_PORT_DASHBOARD=\"${AGENTTEAMS_PORT_DASHBOARD:-13000}\"\n AGENTTEAMS_DASHBOARD_IMAGE=\"${AGENTTEAMS_DASHBOARD_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-dashboard:${AGENTTEAMS_DASHBOARD_VERSION}}\"\n AGENTTEAMS_USE_EMBEDDED=1\n _start_dashboard\n ;;\n uninstall)\n uninstall_agentteams\n ;;\n *)\n echo \"Usage: $0 [manager|worker [options]|dashboard|uninstall]\"\n echo \"\"\n echo \"Commands:\"\n echo \" manager Interactive Manager installation (default)\"\n echo \" Choose Quick Start (all defaults) or Manual mode\"\n echo \" worker Worker installation (requires --name and connection params)\"\n echo \" dashboard Start/stop agentteams-dashboard (use with AGENTTEAMS_DASHBOARD=0 to stop)\"\n echo \" uninstall Stop and remove Manager + all Worker containers\"\n echo \"\"\n echo \"Quick Start (fastest):\"\n echo \" $0\"\n echo \" # Then select '1' for Quick Start mode\"\n echo \"\"\n echo \"Non-interactive (for automation):\"\n echo \" AGENTTEAMS_NON_INTERACTIVE=1 AGENTTEAMS_LLM_API_KEY=sk-xxx $0\"\n echo \"\"\n echo \"Worker Options:\"\n echo \" --name <name> Worker name (required)\"\n echo \" --fs <url> MinIO endpoint URL (required)\"\n echo \" --fs-key <key> MinIO access key (required)\"\n echo \" --fs-secret <secret> MinIO secret key (required)\"\n echo \" --reset Remove existing Worker container before creating\"\n exit 1\n ;;\nesac\n"}, "fcd4297ec8cd48a65b63ff4bcc0ffd8febc55b4b": {"t": 1785764741.7483768, "n": 248204, "h": "3e54bc005d513e8d", "c": "#!/bin/bash\n# agentteams-install.sh - One-click installation for AgentTeams Manager, Worker, and Dashboard\n#\n# Usage:\n# ./agentteams-install.sh # Interactive installation (choose Quick Start or Manual)\n# ./agentteams-install.sh manager # Same as above (explicit)\n# ./agentteams-install.sh worker --name <name> ... # Worker installation\n# ./agentteams-install.sh dashboard # Start/stop agentteams-dashboard (AGENTTEAMS_DASHBOARD=0 to stop)\n# ./agentteams-install.sh uninstall # Stop and remove Manager + all Workers\n#\n# NOTE: agentteams-dashboard is currently only supported via the Bash installer.\n# The PowerShell installer (agentteams-install.ps1) does not yet include dashboard support.\n#\n# Onboarding Modes:\n# Quick Start - Fast installation with all default values (recommended)\n# Manual - Customize each option step by step\n#\n# Environment variables (for automation):\n# AGENTTEAMS_NON_INTERACTIVE Skip all prompts, use defaults (default: 0)\n# AGENTTEAMS_LLM_PROVIDER LLM provider (default: openai-compat for zh non-interactive Token Plan; qwen for en)\n# AGENTTEAMS_DEFAULT_MODEL Default model (default: qwen3.6-plus for zh Token Plan and en non-interactive)\n# AGENTTEAMS_OPENAI_BASE_URL OpenAI-compatible base URL (default for zh non-interactive: Alibaba Token Plan endpoint)\n# AGENTTEAMS_LLM_API_KEY LLM API key (required)\n# AGENTTEAMS_ADMIN_USER Admin username (default: admin)\n# AGENTTEAMS_ADMIN_PASSWORD Admin password (auto-generated if not set, min 8 chars)\n# AGENTTEAMS_MATRIX_DOMAIN Matrix domain (default: matrix-local.agentteams.io:18080)\n# AGENTTEAMS_MOUNT_SOCKET Mount container runtime socket (default: 1)\n# AGENTTEAMS_DATA_DIR Docker volume name for persistent data (default: agentteams-data)\n# AGENTTEAMS_WORKSPACE_DIR Host directory for manager workspace (default: ~/agentteams-manager)\n# AGENTTEAMS_VERSION Image tag (default: latest)\n# AGENTTEAMS_REGISTRY Image registry (default: auto-detected by timezone)\n# AGENTTEAMS_INSTALL_MANAGER_IMAGE Override manager image (e.g., local build)\n# AGENTTEAMS_INSTALL_WORKER_IMAGE Override worker image (e.g., local build)\n# AGENTTEAMS_INSTALL_COPAW_WORKER_IMAGE Override copaw worker image (e.g., local build)\n# AGENTTEAMS_INSTALL_HERMES_WORKER_IMAGE Override hermes worker image (e.g., local build)\n# AGENTTEAMS_NACOS_REGISTRY_URI Default Nacos registry URI for Worker market search/import\n# (default: nacos://market.agentteams.io:80/public)\n# AGENTTEAMS_NACOS_USERNAME Default Nacos username for nacos:// package imports (optional)\n# AGENTTEAMS_NACOS_PASSWORD Default Nacos password for nacos:// package imports (optional)\n# AGENTTEAMS_CMS_TRACES_ENABLED Enable openclaw-cms-plugin traces for Manager AND all Workers (default: false)\n# AGENTTEAMS_CMS_ENDPOINT ARMS OTLP endpoint (required if traces enabled)\n# AGENTTEAMS_CMS_LICENSE_KEY CMS license key (required if traces enabled)\n# AGENTTEAMS_CMS_PROJECT CMS project name (optional)\n# AGENTTEAMS_CMS_WORKSPACE CMS workspace ID (required if traces enabled)\n# AGENTTEAMS_CMS_SERVICE_NAME Manager service name in ARMS (default: agentteams-manager)\n# Workers always report as agentteams-worker-<name> automatically\n# AGENTTEAMS_CMS_METRICS_ENABLED Enable diagnostics-otel metrics for Manager AND all Workers (default: false)\n# AGENTTEAMS_PORT_GATEWAY Host port for Higress gateway (default: 18080)\n# AGENTTEAMS_PORT_CONSOLE Host port for Higress console (default: 18001)\n# AGENTTEAMS_PORT_ELEMENT_WEB Host port for Element Web direct access (default: 18088)\n# AGENTTEAMS_PORT_MANAGER_CONSOLE Host port for Manager console (default: 18888)\n# AGENTTEAMS_WORKER_IDLE_TIMEOUT Worker idle timeout in minutes (default: 720, i.e. 12 hours)\n# AGENTTEAMS_DASHBOARD Install agentteams-dashboard management UI (default: 1)\n# AGENTTEAMS_DASHBOARD_VERSION Dashboard version (default: v1.2.0-beta.1, independent of AgentTeams version)\n# AGENTTEAMS_PORT_DASHBOARD Dashboard host port (default: 13000)\n# AGENTTEAMS_DASHBOARD_IMAGE Override dashboard image (default: <registry>/agentteams/agentteams-dashboard:<DASHBOARD_VERSION>)\n# AGENTTEAMS_AI_GATEWAY_ADMIN_URL Higress Console URL for shared auth (auto-detected)\n\nset -e\n\nAGENTTEAMS_VERSION=\"${AGENTTEAMS_VERSION:-}\"\nAGENTTEAMS_KNOWN_STABLE_VERSION=\"v1.1.2\" # fallback if GitHub API is unreachable\n\n_normalize_version() {\n local version=\"$1\"\n case \"${version}\" in\n [0-9]*.[0-9]*.[0-9]*) version=\"v${version}\" ;;\n esac\n case \"${version}\" in\n v[0-9]*.[0-9]*.[0-9]*.beta.[0-9]*) version=\"${version/.beta./-beta.}\" ;;\n esac\n printf '%s' \"${version}\"\n}\n\n# Returns 0 (true) if $1 < $2 using semver order; \"latest\" is treated as greatest\n_ver_lt() {\n [ \"$1\" = \"latest\" ] && return 1\n [ \"$2\" = \"latest\" ] && return 0\n [ \"$1\" = \"$2\" ] && return 1\n [ \"$(printf '%s\\n%s' \"$1\" \"$2\" | sort -V | head -1)\" = \"$1\" ]\n}\n\n_use_legacy_image_env() {\n if [ \"$1\" = \"latest\" ]; then\n _ver_lt \"${AGENTTEAMS_KNOWN_STABLE_VERSION}\" \"v1.2.0\"\n else\n _ver_lt \"$1\" \"v1.2.0\"\n fi\n}\n\n_controller_env_prefix() {\n if _use_legacy_image_env \"$1\"; then\n printf '%s%s' 'HIC' 'LAW_'\n else\n printf '%s' 'AGENTTEAMS_'\n fi\n}\n\n_controller_storage_prefix() {\n if _use_legacy_image_env \"$1\"; then\n printf '%s%s' 'hic' 'law/agentteams-storage'\n else\n printf '%s' 'agentteams/agentteams-storage'\n fi\n}\nAGENTTEAMS_NON_INTERACTIVE=\"${AGENTTEAMS_NON_INTERACTIVE:-0}\"\nAGENTTEAMS_MOUNT_SOCKET=\"${AGENTTEAMS_MOUNT_SOCKET:-1}\"\nAGENTTEAMS_DOCKER_PROXY=\"${AGENTTEAMS_DOCKER_PROXY:-1}\"\nSTEP_RESULT=\"\" # Used by state machine to signal \"back\" navigation\n\n# ============================================================\n# Log all output to file\n# ============================================================\n\nAGENTTEAMS_LOG_FILE=\"${HOME}/agentteams-install.log\"\n\nif [ \"${1:-}\" != \"uninstall\" ]; then\n # Redirect all output (stdout and stderr) to both terminal and log file\n exec > >(tee -a \"${AGENTTEAMS_LOG_FILE}\") 2>&1\n\n echo \"\"\n echo \"========================================\"\n echo \"AgentTeams Installation Log\"\n echo \"Started: $(date)\"\n echo \"User: $(whoami)\"\n echo \"System: $(uname -a)\"\n echo \"Log file: ${AGENTTEAMS_LOG_FILE}\"\n echo \"========================================\"\n echo \"\"\nfi\n\n# ============================================================\n# Utility functions (needed early for timezone detection)\n# ============================================================\n\nlog() {\n echo -e \"\\033[36m[AgentTeams]\\033[0m $1\"\n}\n\nerror() {\n echo -e \"\\033[31m[AgentTeams ERROR]\\033[0m $1\" >&2\n}\n\ndie() {\n error \"$1\"\n exit 1\n}\n\n# ============================================================\n# Timezone detection (compatible with Linux and macOS)\n# ============================================================\n\ndetect_timezone() {\n local tz=\"\"\n\n # Try /etc/timezone (Debian/Ubuntu)\n if [ -f /etc/timezone ]; then\n tz=$(cat /etc/timezone 2>/dev/null | tr -d '[:space:]')\n fi\n\n # Try /etc/localtime symlink (macOS and some Linux)\n if [ -z \"${tz}\" ] && [ -L /etc/localtime ]; then\n tz=$(ls -l /etc/localtime 2>/dev/null | sed 's|.*/zoneinfo/||')\n fi\n\n # Try timedatectl (systemd)\n if [ -z \"${tz}\" ]; then\n tz=$(timedatectl show --value -p Timezone 2>/dev/null)\n fi\n\n # If still not detected, warn and prompt user. Diagnostics go to stderr —\n # this function returns its value via stdout to `$(detect_timezone)`, so\n # any echo here would leak into AGENTTEAMS_TIMEZONE and downstream image tags.\n if [ -z \"${tz}\" ]; then\n echo \"\" >&2\n echo -e \"\\033[33m[AgentTeams WARNING]\\033[0m Could not detect timezone automatically.\" >&2\n echo -e \"\\033[33m[AgentTeams]\\033[0m Please enter your timezone (e.g., Asia/Shanghai, America/New_York).\" >&2\n echo \"\" >&2\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n tz=\"Asia/Shanghai\"\n log \"Using default timezone: ${tz}\" >&2\n else\n read -e -p \"Timezone [Asia/Shanghai]: \" tz\n tz=\"${tz:-Asia/Shanghai}\"\n fi\n fi\n\n echo \"${tz}\"\n}\n\n# Detect timezone once at startup (used by registry selection and container TZ)\nAGENTTEAMS_TIMEZONE=\"${AGENTTEAMS_TIMEZONE:-$(detect_timezone)}\"\n\n# ============================================================\n# Language detection based on timezone\n# ============================================================\n\ndetect_language() {\n local tz=\"${AGENTTEAMS_TIMEZONE}\"\n case \"${tz}\" in\n Asia/Shanghai|Asia/Chongqing|Asia/Harbin|Asia/Urumqi|\\\n Asia/Taipei|Asia/Hong_Kong|Asia/Macau)\n echo \"zh\"\n ;;\n *)\n echo \"en\"\n ;;\n esac\n}\n\n# Language priority: env var > existing env file > timezone detection\nif [ -z \"${AGENTTEAMS_LANGUAGE}\" ]; then\n # Check existing env file for saved language preference (upgrade scenario)\n _env_file=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n # Migrate from legacy location (current directory) if needed\n if [ ! -f \"${_env_file}\" ] && [ -f \"./agentteams-manager.env\" ]; then\n mv \"./agentteams-manager.env\" \"${_env_file}\" 2>/dev/null || true\n fi\n if [ -f \"${_env_file}\" ]; then\n _saved_lang=$(grep '^AGENTTEAMS_LANGUAGE=' \"${_env_file}\" 2>/dev/null | cut -d= -f2-)\n if [ -n \"${_saved_lang}\" ]; then\n AGENTTEAMS_LANGUAGE=\"${_saved_lang}\"\n fi\n fi\n # Fall back to timezone-based detection\n if [ -z \"${AGENTTEAMS_LANGUAGE}\" ]; then\n AGENTTEAMS_LANGUAGE=\"$(detect_language)\"\n fi\n unset _env_file _saved_lang\nfi\nexport AGENTTEAMS_LANGUAGE\n\n# ============================================================\n# Centralized message dictionary and msg() function\n# Compatible with bash 3.2+ (macOS default) — uses case instead of declare -A\n# ============================================================\n\n# msg() function: look up message by key, with printf-style argument substitution\n# Falls back to English if the current language translation is missing.\nmsg() {\n local key=\"$1\"\n shift\n local lang=\"${AGENTTEAMS_LANGUAGE:-en}\"\n local text=\"\"\n case \"${key}.${lang}\" in\n # --- Timezone detection messages ---\n \"tz.warning.title.zh\") text=\"无法自动检测时区。\" ;;\n \"tz.warning.title.en\") text=\"Could not detect timezone automatically.\" ;;\n \"tz.warning.prompt.zh\") text=\"请输入您的时区(例如 Asia/Shanghai、America/New_York)。\" ;;\n \"tz.warning.prompt.en\") text=\"Please enter your timezone (e.g., Asia/Shanghai, America/New_York).\" ;;\n \"tz.default.zh\") text=\"使用默认时区: %s\" ;;\n \"tz.default.en\") text=\"Using default timezone: %s\" ;;\n \"tz.input_prompt.zh\") text=\"时区\" ;;\n \"tz.input_prompt.en\") text=\"Timezone\" ;;\n # --- Installation title and info ---\n \"install.title.zh\") text=\"=== AgentTeams Manager 安装 ===\" ;;\n \"install.title.en\") text=\"=== AgentTeams Manager Installation ===\" ;;\n \"install.registry.zh\") text=\"镜像仓库: %s\" ;;\n \"install.registry.en\") text=\"Registry: %s\" ;;\n \"install.dir.zh\") text=\"安装目录: %s\" ;;\n \"install.dir.en\") text=\"Installation directory: %s\" ;;\n \"install.dir_hint.zh\") text=\" (env 文件 'agentteams-manager.env' 将保存到 HOME 目录。)\" ;;\n \"install.dir_hint.en\") text=\" (The env file 'agentteams-manager.env' will be saved to your HOME directory.)\" ;;\n \"install.dir_hint2.zh\") text=\" (请从您希望管理此安装的目录运行此脚本。)\" ;;\n \"install.dir_hint2.en\") text=\" (Run this script from the directory where you want to manage this installation.)\" ;;\n # --- Onboarding mode ---\n \"install.mode.title.zh\") text=\"--- Onboarding 模式 ---\" ;;\n \"install.mode.title.en\") text=\"--- Onboarding Mode ---\" ;;\n \"install.mode.choose.zh\") text=\"选择安装模式:\" ;;\n \"install.mode.choose.en\") text=\"Choose your installation mode:\" ;;\n \"install.mode.quickstart.zh\") text=\" 1) 快速开始 - 使用阿里云通义 Token 套餐快速安装(推荐)\" ;;\n \"install.mode.quickstart.en\") text=\" 1) Quick Start - Fast installation with Qwen Cloud (recommended)\" ;;\n \"install.mode.manual.zh\") text=\" 2) 手动配置 - 选择 LLM 提供商并自定义选项\" ;;\n \"install.mode.manual.en\") text=\" 2) Manual - Choose LLM provider and customize options\" ;;\n \"install.mode.prompt.zh\") text=\"请选择 [1/2]\" ;;\n \"install.mode.prompt.en\") text=\"Enter choice [1/2]\" ;;\n \"install.mode.quickstart_selected.zh\") text=\"已选择快速开始模式 - 使用阿里云通义 Token 套餐\" ;;\n \"install.mode.quickstart_selected.en\") text=\"Quick Start mode selected - using Qwen Cloud\" ;;\n \"install.mode.manual_selected.zh\") text=\"已选择手动配置模式 - 您将选择 LLM 提供商并自定义选项\" ;;\n \"install.mode.manual_selected.en\") text=\"Manual mode selected - you will choose LLM provider and customize options\" ;;\n \"install.mode.invalid.zh\") text=\"无效选择,默认使用快速开始模式\" ;;\n \"install.mode.invalid.en\") text=\"Invalid choice, defaulting to Quick Start mode\" ;;\n # --- Upgrade mode sub-menu ---\n \"upgrade.mode.prompt.zh\") text=\"请选择升级方式:\" ;;\n \"upgrade.mode.prompt.en\") text=\"Select upgrade mode:\" ;;\n \"upgrade.mode.keep_all.zh\") text=\" 1) 保留所有参数,一键升级(推荐)\" ;;\n \"upgrade.mode.keep_all.en\") text=\" 1) Keep all parameters, quick upgrade (recommended)\" ;;\n \"upgrade.mode.confirm_each.zh\") text=\" 2) 逐个确认参数(可修改)\" ;;\n \"upgrade.mode.confirm_each.en\") text=\" 2) Confirm each parameter (can modify)\" ;;\n \"upgrade.mode.back.zh\") text=\" 3) 返回\" ;;\n \"upgrade.mode.back.en\") text=\" 3) Back\" ;;\n # --- Version selection ---\n \"install.version.title.zh\") text=\"--- 版本选择 ---\" ;;\n \"install.version.title.en\") text=\"--- Version Selection ---\" ;;\n \"install.version.choose.zh\") text=\"选择要安装的版本:\" ;;\n \"install.version.choose.en\") text=\"Choose the version to install:\" ;;\n \"install.version.option_latest.zh\") text=\" 1) latest - 最新版(默认)\" ;;\n \"install.version.option_latest.en\") text=\" 1) latest - Latest build (default)\" ;;\n \"install.version.option_stable.zh\") text=\" 2) %s - 最新稳定版\" ;;\n \"install.version.option_stable.en\") text=\" 2) %s - Latest stable release\" ;;\n \"install.version.fetching.zh\") text=\"正在查询最新稳定版本...\" ;;\n \"install.version.fetching.en\") text=\"Fetching latest stable release...\" ;;\n \"install.version.fetch_failed.zh\") text=\"无法查询 GitHub,使用内置版本 %s\" ;;\n \"install.version.fetch_failed.en\") text=\"Could not reach GitHub, using built-in version %s\" ;;\n \"install.version.option_custom.zh\") text=\" 3) 自定义 - 手动输入版本号(如 v1.0.5)\" ;;\n \"install.version.option_custom.en\") text=\" 3) Custom - Enter a specific version (e.g. v1.0.5)\" ;;\n \"install.version.prompt.zh\") text=\"请选择 [1/2/3]\" ;;\n \"install.version.prompt.en\") text=\"Enter choice [1/2/3]\" ;;\n \"install.version.custom_prompt.zh\") text=\"请输入版本号\" ;;\n \"install.version.custom_prompt.en\") text=\"Enter version tag\" ;;\n \"install.version.selected_latest.zh\") text=\"已选择最新版 (latest)\" ;;\n \"install.version.selected_latest.en\") text=\"Selected latest version\" ;;\n \"install.version.selected_stable.zh\") text=\"已选择最新稳定版 (%s)\" ;;\n \"install.version.selected_stable.en\") text=\"Selected latest stable version (%s)\" ;;\n \"install.version.selected_custom.zh\") text=\"已选择自定义版本 (%s)\" ;;\n \"install.version.selected_custom.en\") text=\"Selected custom version (%s)\" ;;\n \"install.version.invalid.zh\") text=\"无效选择,使用最新稳定版 (%s)\" ;;\n \"install.version.invalid.en\") text=\"Invalid choice, defaulting to latest stable version (%s)\" ;;\n # --- Existing installation detected ---\n \"install.existing.detected.zh\") text=\"检测到已有 Manager 安装(env 文件: %s)\" ;;\n \"install.existing.detected.en\") text=\"Existing Manager installation detected (env file: %s)\" ;;\n \"install.existing.choose.zh\") text=\"选择操作:\" ;;\n \"install.existing.choose.en\") text=\"Choose an action:\" ;;\n \"install.existing.upgrade.zh\") text=\" 1) 就地升级(保留数据、工作空间、env 文件)\" ;;\n \"install.existing.upgrade.en\") text=\" 1) In-place upgrade (keep data, workspace, env file)\" ;;\n \"install.existing.reinstall.zh\") text=\" 2) 全新重装(删除所有数据,重新开始)\" ;;\n \"install.existing.reinstall.en\") text=\" 2) Clean reinstall (remove all data, start fresh)\" ;;\n \"install.existing.cancel.zh\") text=\" 3) 取消\" ;;\n \"install.existing.cancel.en\") text=\" 3) Cancel\" ;;\n \"install.existing.prompt.zh\") text=\"请选择 [1/2/3]\" ;;\n \"install.existing.prompt.en\") text=\"Enter choice [1/2/3]\" ;;\n \"install.existing.upgrade_noninteractive.zh\") text=\"非交互模式: 执行就地升级...\" ;;\n \"install.existing.upgrade_noninteractive.en\") text=\"Non-interactive mode: performing in-place upgrade...\" ;;\n \"install.existing.upgrading.zh\") text=\"执行就地升级...\" ;;\n \"install.existing.upgrading.en\") text=\"Performing in-place upgrade...\" ;;\n \"install.existing.warn_manager_stop.zh\") text=\"⚠️ Manager 容器将被停止并重新创建。\" ;;\n \"install.existing.warn_manager_stop.en\") text=\"⚠️ Manager container will be stopped and recreated.\" ;;\n \"install.existing.warn_worker_recreate.zh\") text=\"⚠️ Worker 容器也将被重新创建(以更新 Manager IP)。\" ;;\n \"install.existing.warn_worker_recreate.en\") text=\"⚠️ Worker containers will also be recreated (to update Manager IP in hosts).\" ;;\n \"install.existing.continue_prompt.zh\") text=\"继续?[y/N]\" ;;\n \"install.existing.continue_prompt.en\") text=\"Continue? [y/N]\" ;;\n \"install.existing.cancelled.zh\") text=\"安装已取消。\" ;;\n \"install.existing.cancelled.en\") text=\"Installation cancelled.\" ;;\n \"install.existing.stopping_manager.zh\") text=\"停止并移除现有 manager 容器...\" ;;\n \"install.existing.stopping_manager.en\") text=\"Stopping and removing existing manager container...\" ;;\n \"install.existing.stopping_workers.zh\") text=\"停止并移除现有 worker 容器...\" ;;\n \"install.existing.stopping_workers.en\") text=\"Stopping and removing existing worker containers...\" ;;\n \"install.existing.removed.zh\") text=\" 已移除: %s\" ;;\n \"install.existing.removed.en\") text=\" Removed: %s\" ;;\n # --- Clean reinstall messages ---\n \"install.reinstall.performing.zh\") text=\"执行全新重装...\" ;;\n \"install.reinstall.performing.en\") text=\"Performing clean reinstall...\" ;;\n \"install.reinstall.warn_stop.zh\") text=\"⚠️ 以下运行中的容器将被停止:\" ;;\n \"install.reinstall.warn_stop.en\") text=\"⚠️ The following running containers will be stopped:\" ;;\n \"install.reinstall.warn_delete.zh\") text=\"⚠️ 警告: 以下内容将被删除:\" ;;\n \"install.reinstall.warn_delete.en\") text=\"⚠️ WARNING: This will DELETE the following:\" ;;\n \"install.reinstall.warn_volume.zh\") text=\" - Docker 卷: agentteams-data\" ;;\n \"install.reinstall.warn_volume.en\") text=\" - Docker volume: agentteams-data\" ;;\n \"install.reinstall.warn_env.zh\") text=\" - Env 文件: %s\" ;;\n \"install.reinstall.warn_env.en\") text=\" - Env file: %s\" ;;\n \"install.reinstall.warn_workspace.zh\") text=\" - Manager 工作空间: %s\" ;;\n \"install.reinstall.warn_workspace.en\") text=\" - Manager workspace: %s\" ;;\n \"install.reinstall.warn_workers.zh\") text=\" - 所有 worker 容器\" ;;\n \"install.reinstall.warn_workers.en\") text=\" - All worker containers\" ;;\n \"install.reinstall.warn_proxy.zh\") text=\" - Docker API 代理容器: agentteams-controller\" ;;\n \"install.reinstall.warn_proxy.en\") text=\" - Docker API proxy container: agentteams-controller\" ;;\n \"install.reinstall.warn_network.zh\") text=\" - Docker 网络: agentteams-net\" ;;\n \"install.reinstall.warn_network.en\") text=\" - Docker network: agentteams-net\" ;;\n \"install.reinstall.confirm_type.zh\") text=\"请输入工作空间路径以确认删除(或按 Ctrl+C 取消):\" ;;\n \"install.reinstall.confirm_type.en\") text=\"To confirm deletion, please type the workspace path:\" ;;\n \"install.reinstall.confirm_path.zh\") text=\"输入路径以确认(或按 Ctrl+C 取消)\" ;;\n \"install.reinstall.confirm_path.en\") text=\"Type the path to confirm (or press Ctrl+C to cancel)\" ;;\n \"install.reinstall.path_mismatch.zh\") text=\"路径不匹配。中止重装。输入: '%s',期望: '%s'\" ;;\n \"install.reinstall.path_mismatch.en\") text=\"Path mismatch. Aborting reinstall. Input: '%s', Expected: '%s'\" ;;\n \"install.reinstall.confirmed.zh\") text=\"已确认。正在清理...\" ;;\n \"install.reinstall.confirmed.en\") text=\"Confirmed. Cleaning up...\" ;;\n \"install.reinstall.removed_worker.zh\") text=\" 已移除 worker: %s\" ;;\n \"install.reinstall.removed_worker.en\") text=\" Removed worker: %s\" ;;\n \"install.reinstall.removing_volume.zh\") text=\"正在移除 Docker 卷: agentteams-data\" ;;\n \"install.reinstall.removing_volume.en\") text=\"Removing Docker volume: agentteams-data\" ;;\n \"install.reinstall.warn_volume_fail.zh\") text=\" 警告: 无法移除卷(可能有引用)\" ;;\n \"install.reinstall.warn_volume_fail.en\") text=\" Warning: Could not remove volume (may have references)\" ;;\n \"install.reinstall.removing_proxy.zh\") text=\"正在移除 Docker API 代理容器: agentteams-controller\" ;;\n \"install.reinstall.removing_proxy.en\") text=\"Removing Docker API proxy container: agentteams-controller\" ;;\n \"install.reinstall.removing_network.zh\") text=\"正在移除 Docker 网络: agentteams-net\" ;;\n \"install.reinstall.removing_network.en\") text=\"Removing Docker network: agentteams-net\" ;;\n \"install.reinstall.removing_workspace.zh\") text=\"正在移除工作空间目录: %s\" ;;\n \"install.reinstall.removing_workspace.en\") text=\"Removing workspace directory: %s\" ;;\n \"install.reinstall.removing_env.zh\") text=\"正在移除 env 文件: %s\" ;;\n \"install.reinstall.removing_env.en\") text=\"Removing env file: %s\" ;;\n \"install.reinstall.cleanup_done.zh\") text=\"清理完成。开始全新安装...\" ;;\n \"install.reinstall.cleanup_done.en\") text=\"Cleanup complete. Starting fresh installation...\" ;;\n \"install.reinstall.failed_rm_workspace.zh\") text=\"无法移除工作空间目录\" ;;\n \"install.reinstall.failed_rm_workspace.en\") text=\"Failed to remove workspace directory\" ;;\n # --- Orphan volume detection ---\n \"install.orphan_volume.detected.zh\") text=\"⚠️ 检测到残留数据卷 '%s',但未找到对应的 env 配置文件。\" ;;\n \"install.orphan_volume.detected.en\") text=\"⚠️ Found leftover data volume '%s' but no matching env config file.\" ;;\n \"install.orphan_volume.warn.zh\") text=\"这可能是之前安装的残留数据,会导致新安装出现异常(如密码冲突、服务启动失败)。\" ;;\n \"install.orphan_volume.warn.en\") text=\"This is likely leftover data from a previous installation and may cause issues (e.g., credential conflicts, service startup failures).\" ;;\n \"install.orphan_volume.choose.zh\") text=\"选择操作:\" ;;\n \"install.orphan_volume.choose.en\") text=\"Choose an action:\" ;;\n \"install.orphan_volume.clean.zh\") text=\" 1) 清理残留数据卷后继续安装(推荐)\" ;;\n \"install.orphan_volume.clean.en\") text=\" 1) Remove leftover volume and continue installation (recommended)\" ;;\n \"install.orphan_volume.keep.zh\") text=\" 2) 保留数据卷继续安装(可能出现异常)\" ;;\n \"install.orphan_volume.keep.en\") text=\" 2) Keep the volume and continue installation (may cause issues)\" ;;\n \"install.orphan_volume.prompt.zh\") text=\"请选择 [1/2]\" ;;\n \"install.orphan_volume.prompt.en\") text=\"Enter choice [1/2]\" ;;\n \"install.orphan_volume.cleaning.zh\") text=\"正在清理残留数据卷...\" ;;\n \"install.orphan_volume.cleaning.en\") text=\"Removing leftover data volume...\" ;;\n \"install.orphan_volume.cleaned.zh\") text=\"残留数据卷已清理。继续全新安装...\" ;;\n \"install.orphan_volume.cleaned.en\") text=\"Leftover volume removed. Continuing with fresh installation...\" ;;\n \"install.orphan_volume.keeping.zh\") text=\"保留数据卷,继续安装。如遇异常请选择全新重装。\" ;;\n \"install.orphan_volume.keeping.en\") text=\"Keeping existing volume. If you encounter issues, consider a clean reinstall.\" ;;\n \"install.orphan_volume.clean_noninteractive.zh\") text=\"非交互模式: 自动清理残留数据卷...\" ;;\n \"install.orphan_volume.clean_noninteractive.en\") text=\"Non-interactive mode: automatically removing leftover volume...\" ;;\n # --- Loading existing config ---\n \"install.loading_config.zh\") text=\"从 %s 加载已有配置(shell 环境变量优先)...\" ;;\n \"install.loading_config.en\") text=\"Loading existing config from %s (shell env vars take priority)...\" ;;\n # --- LLM Configuration ---\n \"llm.title.zh\") text=\"--- LLM 配置 ---\" ;;\n \"llm.title.en\") text=\"--- LLM Configuration ---\" ;;\n \"llm.provider.label.zh\") text=\" 提供商: %s\" ;;\n \"llm.provider.label.en\") text=\" Provider: %s\" ;;\n \"llm.model.label.zh\") text=\" 模型: %s\" ;;\n \"llm.model.label.en\") text=\" Model: %s\" ;;\n \"llm.provider.qwen.zh\") text=\" 提供商: qwen(阿里云百炼)\" ;;\n \"llm.provider.qwen.en\") text=\" Provider: qwen (Alibaba Cloud Bailian)\" ;;\n \"llm.provider.qwen_default.zh\") text=\" 提供商: %s(默认)\" ;;\n \"llm.provider.qwen_default.en\") text=\" Provider: %s (default)\" ;;\n \"llm.model.default.zh\") text=\" 模型: %s(默认)\" ;;\n \"llm.model.default.en\") text=\" Model: %s (default)\" ;;\n \"llm.apikey_hint_bailian.zh\") text=\" 💡 获取阿里云百炼(DashScope)API Key:\" ;;\n \"llm.apikey_hint_bailian.en\") text=\" 💡 Get your Alibaba Cloud Bailian (DashScope) API Key:\" ;;\n \"llm.apikey_url_bailian.zh\") text=\" https://www.aliyun.com/product/bailian\" ;;\n \"llm.apikey_url_bailian.en\") text=\" https://www.aliyun.com/product/bailian\" ;;\n \"llm.apikey_hint_qwencloud.zh\") text=\" 💡 从 Qwen Cloud(国际站)获取 DASHSCOPE_API_KEY:\" ;;\n \"llm.apikey_hint_qwencloud.en\") text=\" 💡 Get your DASHSCOPE_API_KEY for Qwen Cloud (international) from:\" ;;\n \"llm.apikey_url_qwencloud.zh\") text=\" https://home.qwencloud.com/api-keys (文档: https://docs.qwencloud.com/)\" ;;\n \"llm.apikey_url_qwencloud.en\") text=\" https://home.qwencloud.com/api-keys | Docs: https://docs.qwencloud.com/\" ;;\n \"llm.apikey_hint_tokenplan.zh\") text=\" 💡 获取 DashScope API Key 或开通通义 Token 套餐,请参考:\" ;;\n \"llm.apikey_hint_tokenplan.en\") text=\" 💡 Get your DashScope or Token Plan API key (Alibaba Model Studio):\" ;;\n \"llm.apikey_url_tokenplan.zh\") text=\" https://help.aliyun.com/zh/model-studio/token-plan-quickstart\" ;;\n \"llm.apikey_url_tokenplan.en\") text=\" https://common-buy.aliyun.com/token-plan/ | https://help.aliyun.com/zh/model-studio/token-plan-quickstart\" ;;\n \"llm.apikey_hint_codingplan.zh\") text=\" 💡 获取 DashScope API Key(Coding 套餐 / coding.dashscope 接口):\" ;;\n \"llm.apikey_hint_codingplan.en\") text=\" 💡 Get your DashScope API key for Coding Plan (coding.dashscope endpoint):\" ;;\n \"llm.apikey_url_codingplan.zh\") text=\" https://help.aliyun.com/zh/model-studio/get-api-key\" ;;\n \"llm.apikey_url_codingplan.en\") text=\" https://help.aliyun.com/zh/model-studio/get-api-key\" ;;\n \"llm.apikey_prompt.zh\") text=\"LLM API Key\" ;;\n \"llm.apikey_prompt.en\") text=\"LLM API Key\" ;;\n \"llm.providers_title.zh\") text=\"可用 LLM 提供商:\" ;;\n \"llm.providers_title.en\") text=\"Available LLM Providers:\" ;;\n \"llm.provider.alibaba.zh\") text=\" 1) 阿里云通义 Token 套餐 - 推荐中国用户使用\" ;;\n \"llm.provider.alibaba.en\") text=\" 1) Qwen Cloud - International (OpenAI-compatible API, recommended)\" ;;\n \"llm.provider.openai_compat.zh\") text=\" 2) OpenAI 兼容 API - 自定义 Base URL(OpenAI、DeepSeek 等)\" ;;\n \"llm.provider.openai_compat.en\") text=\" 2) OpenAI-compatible API - Custom Base URL (OpenAI, DeepSeek, etc.)\" ;;\n \"llm.provider.select.zh\") text=\"选择提供商 [1/2]\" ;;\n \"llm.provider.select.en\") text=\"Select provider [1/2]\" ;;\n \"llm.alibaba.models_title.zh\") text=\"选择阿里云模型接入方式:\" ;;\n \"llm.alibaba.models_title.en\") text=\"Select Alibaba Cloud model access:\" ;;\n \"llm.alibaba.model.tokenplan.zh\") text=\" 1) 阿里云通义 Token 套餐 - 兼容模式(推荐)\" ;;\n \"llm.alibaba.model.tokenplan.en\") text=\" 1) Alibaba Cloud Token Plan - compatible-mode (recommended)\" ;;\n \"llm.alibaba.model.bailian.zh\") text=\" 2) 阿里云百炼 - DashScope 通用兼容接口\" ;;\n \"llm.alibaba.model.bailian.en\") text=\" 2) Alibaba Cloud Bailian - DashScope compatible mode\" ;;\n \"llm.alibaba.model.codingplan_legacy.zh\") text=\" 3) 阿里云 Coding 套餐 - 旧版端点(兼容保留)\" ;;\n \"llm.alibaba.model.codingplan_legacy.en\") text=\" 3) Alibaba Cloud Coding Plan - legacy endpoint (backward compatible)\" ;;\n \"llm.alibaba.model.select.zh\") text=\"选择接入方式 [1/2/3]\" ;;\n \"llm.alibaba.model.select.en\") text=\"Select access option [1/2/3]\" ;;\n \"llm.alibaba.model.invalid.zh\") text=\"无效选择: %s(请输入 1、2 或 3)\" ;;\n \"llm.alibaba.model.invalid.en\") text=\"Invalid choice: %s (please enter 1, 2, or 3)\" ;;\n \"llm.codingplan.models_title.zh\") text=\"选择通义 Token 套餐默认模型:\" ;;\n \"llm.codingplan.models_title.en\") text=\"Select Qwen Cloud default model:\" ;;\n \"llm.codingplan.model.qwen36plus.zh\") text=\" 1) qwen3.6-plus - 千问 3.6(推荐)\" ;;\n \"llm.codingplan.model.qwen36plus.en\") text=\" 1) qwen3.6-plus - Qwen 3.6 (recommended)\" ;;\n \"llm.codingplan.model.glm5.zh\") text=\" 2) glm-5 - 智谱 GLM-5(编程推荐)\" ;;\n \"llm.codingplan.model.glm5.en\") text=\" 2) glm-5 - Zhipu GLM-5 (recommended for coding)\" ;;\n \"llm.codingplan.model.kimi.zh\") text=\" 3) kimi-k2.5 - Moonshot Kimi K2.5\" ;;\n \"llm.codingplan.model.kimi.en\") text=\" 3) kimi-k2.5 - Moonshot Kimi K2.5\" ;;\n \"llm.codingplan.model.minimax.zh\") text=\" 4) MiniMax-M2.5 - MiniMax M2.5\" ;;\n \"llm.codingplan.model.minimax.en\") text=\" 4) MiniMax-M2.5 - MiniMax M2.5\" ;;\n \"llm.codingplan.model.select.zh\") text=\"选择模型 [1/2/3/4]\" ;;\n \"llm.codingplan.model.select.en\") text=\"Select model [1/2/3/4]\" ;;\n \"llm.provider.selected_tokenplan.zh\") text=\" 提供商: 阿里云通义 Token 套餐(兼容模式)\" ;;\n \"llm.provider.selected_tokenplan.en\") text=\" Provider: Alibaba Cloud Token Plan (compatible mode)\" ;;\n \"llm.provider.selected_codingplan.zh\") text=\" 提供商: 阿里云通义 Token 套餐(alibaba-cloud)\" ;;\n \"llm.provider.selected_codingplan.en\") text=\" Provider: Qwen Cloud (international) (alibaba-cloud)\" ;;\n \"llm.provider.selected_codingplan_legacy.zh\") text=\" 提供商: 阿里云 Coding 套餐(coding.dashscope)\" ;;\n \"llm.provider.selected_codingplan_legacy.en\") text=\" Provider: Alibaba Cloud Coding Plan (coding.dashscope)\" ;;\n \"llm.provider.selected_qwen.zh\") text=\" 提供商: 阿里云百炼\" ;;\n \"llm.provider.selected_qwen.en\") text=\" Provider: Alibaba Cloud Bailian\" ;;\n \"llm.provider.selected_openai.zh\") text=\" 提供商: %s(OpenAI 兼容)\" ;;\n \"llm.provider.selected_openai.en\") text=\" Provider: %s (OpenAI-compatible)\" ;;\n \"llm.provider.invalid.zh\") text=\"无效选择: %s(请输入 1 或 2)\" ;;\n \"llm.provider.invalid.en\") text=\"Invalid choice: %s (please enter 1 or 2)\" ;;\n \"llm.qwen.model_prompt.zh\") text=\"默认模型 ID\" ;;\n \"llm.qwen.model_prompt.en\") text=\"Default Model ID\" ;;\n \"llm.openai.base_url_prompt.zh\") text=\"Base URL\" ;;\n \"llm.openai.base_url_prompt.en\") text=\"Base URL\" ;;\n \"llm.openai.model_prompt.zh\") text=\"默认模型 ID\" ;;\n \"llm.openai.model_prompt.en\") text=\"Default Model ID\" ;;\n \"llm.openai.base_url_label.zh\") text=\" Base URL: %s\" ;;\n \"llm.openai.base_url_label.en\") text=\" Base URL: %s\" ;;\n # --- Custom model parameters ---\n \"llm.custom_model.detected.zh\") text=\" ⚠️ 模型 '%s' 不在内置模型列表中,请配置模型参数:\" ;;\n \"llm.custom_model.detected.en\") text=\" ⚠️ Model '%s' is not in the built-in model list. Please configure model parameters:\" ;;\n \"llm.custom_model.context_prompt.zh\") text=\"最大上下文长度(token 数)[150000]\" ;;\n \"llm.custom_model.context_prompt.en\") text=\"Max context window (tokens) [150000]\" ;;\n \"llm.custom_model.max_tokens_prompt.zh\") text=\"最大输出长度(token 数)[128000]\" ;;\n \"llm.custom_model.max_tokens_prompt.en\") text=\"Max output tokens [128000]\" ;;\n \"llm.custom_model.reasoning_prompt.zh\") text=\"是否支持推理/思考模式?[Y/n]\" ;;\n \"llm.custom_model.reasoning_prompt.en\") text=\"Does it support reasoning/thinking mode? [Y/n]\" ;;\n \"llm.custom_model.vision_prompt.zh\") text=\"是否支持图片输入?[y/N]\" ;;\n \"llm.custom_model.vision_prompt.en\") text=\"Does it support image input? [y/N]\" ;;\n \"llm.custom_model.summary.zh\") text=\" 自定义模型参数: 上下文=%s, 最大输出=%s, 推理=%s, 图片=%s\" ;;\n \"llm.custom_model.summary.en\") text=\" Custom model params: context=%s, maxTokens=%s, reasoning=%s, vision=%s\" ;;\n # --- Admin Credentials ---\n \"admin.title.zh\") text=\"--- 管理员凭据 ---\" ;;\n \"admin.title.en\") text=\"--- Admin Credentials ---\" ;;\n \"admin.username_prompt.zh\") text=\"管理员用户名\" ;;\n \"admin.username_prompt.en\") text=\"Admin Username\" ;;\n \"admin.password_prompt.zh\") text=\"管理员密码(留空自动生成,最少 8 位)\" ;;\n \"admin.password_prompt.en\") text=\"Admin Password (leave empty to auto-generate, min 8 chars)\" ;;\n \"admin.password_generated.zh\") text=\" 已自动生成管理员密码\" ;;\n \"admin.password_generated.en\") text=\" Auto-generated admin password\" ;;\n \"admin.password_too_short.zh\") text=\"管理员密码至少需要 8 个字符(MinIO 要求)。当前长度: %s\" ;;\n \"admin.password_too_short.en\") text=\"Admin password must be at least 8 characters (MinIO requirement). Current length: %s\" ;;\n # --- Port Configuration ---\n \"port.title.zh\") text=\"--- 端口配置(按回车使用默认值)---\" ;;\n \"port.title.en\") text=\"--- Port Configuration (press Enter for defaults) ---\" ;;\n \"port.gateway_prompt.zh\") text=\"网关主机端口(容器内 8080)\" ;;\n \"port.gateway_prompt.en\") text=\"Host port for gateway (8080 inside container)\" ;;\n \"port.console_prompt.zh\") text=\"Higress 控制台主机端口(容器内 8001)\" ;;\n \"port.console_prompt.en\") text=\"Host port for Higress console (8001 inside container)\" ;;\n \"port.element_prompt.zh\") text=\"Element Web 直接访问主机端口(容器内 8088)\" ;;\n \"port.element_prompt.en\") text=\"Host port for Element Web direct access (8088 inside container)\" ;;\n \"port.manager_console_prompt.zh\") text=\"Manager 控制台主机端口(容器内 18888)\" ;;\n \"port.manager_console_prompt.en\") text=\"Host port for Manager console (18888 inside container)\" ;;\n \"port.copaw_app_prompt.zh\") text=\"QwenPaw App API 主机端口(容器内 18799)\" ;;\n \"port.copaw_app_prompt.en\") text=\"Host port for QwenPaw App API (18799 inside container)\" ;;\n # --- Local-only binding ---\n \"port.local_only.title.zh\") text=\"--- 网络访问模式 ---\" ;;\n \"port.local_only.title.en\") text=\"--- Network Access Mode ---\" ;;\n \"port.local_only.prompt.zh\") text=\"是否仅允许本机访问(端口绑定到 127.0.0.1)?\" ;;\n \"port.local_only.prompt.en\") text=\"Bind ports to localhost only (127.0.0.1)?\" ;;\n \"port.local_only.hint_yes.zh\") text=\" 仅本机使用,无需开放外部端口(推荐)\" ;;\n \"port.local_only.hint_yes.en\") text=\" Local use only, no external port exposure (recommended)\" ;;\n \"port.local_only.hint_no.zh\") text=\" 允许外部访问(局域网 / 公网)\" ;;\n \"port.local_only.hint_no.en\") text=\" Allow external access (LAN / public network)\" ;;\n \"port.local_only.choice.zh\") text=\"请选择 [1/2]\" ;;\n \"port.local_only.choice.en\") text=\"Enter choice [1/2]\" ;;\n \"port.local_only.selected_local.zh\") text=\"端口已绑定到 127.0.0.1(仅本机访问)\" ;;\n \"port.local_only.selected_local.en\") text=\"Ports bound to 127.0.0.1 (localhost only)\" ;;\n \"port.local_only.selected_external.zh\") text=\"端口已绑定到所有网络接口(0.0.0.0)\" ;;\n \"port.local_only.selected_external.en\") text=\"Ports bound to all interfaces (0.0.0.0)\" ;;\n \"port.local_only.https_hint.zh\") text=\"⚠️ 建议在 Higress 控制台配置 TLS 证书并启用 HTTPS,避免明文传输。\" ;;\n \"port.local_only.https_hint.en\") text=\"⚠️ It is recommended to configure TLS certificates and enable HTTPS in the Higress Console to avoid plaintext transmission.\" ;;\n \"port.local_only.https_docs.zh\") text=\"\" ;;\n \"port.local_only.https_docs.en\") text=\"\" ;;\n # --- Domain Configuration ---\n \"domain.title.zh\") text=\"--- 域名配置(按回车使用默认值)---\" ;;\n \"domain.title.en\") text=\"--- Domain Configuration (press Enter for defaults) ---\" ;;\n \"domain.hint.zh\") text=\"提示: 自定义域名前必须事先做好 DNS 解析。单机 ECS 部署时无需修改 aigw、fs 等域名;Element Web 和 Matrix Server 也可通过 IP 直接访问。\" ;;\n \"domain.hint.en\") text=\"Hint: Configure DNS resolution before customizing domains. For single ECS deployment, no need to change aigw, fs, etc.; Element Web and Matrix Server can also be accessed directly via IP.\" ;;\n \"domain.matrix_prompt.zh\") text=\"Matrix 域名\" ;;\n \"domain.matrix_prompt.en\") text=\"Matrix Domain\" ;;\n \"domain.element_prompt.zh\") text=\"Element Web 域名\" ;;\n \"domain.element_prompt.en\") text=\"Element Web Domain\" ;;\n \"domain.gateway_prompt.zh\") text=\"AI 网关域名\" ;;\n \"domain.gateway_prompt.en\") text=\"AI Gateway Domain\" ;;\n \"domain.fs_prompt.zh\") text=\"文件系统域名\" ;;\n \"domain.fs_prompt.en\") text=\"File System Domain\" ;;\n \"domain.console_prompt.zh\") text=\"Manager 控制台域名\" ;;\n \"domain.console_prompt.en\") text=\"Manager Console Domain\" ;;\n # --- GitHub Integration ---\n \"github.title.zh\") text=\"--- GitHub 集成(可选,按回车跳过)---\" ;;\n \"github.title.en\") text=\"--- GitHub Integration (optional, press Enter to skip) ---\" ;;\n \"github.token_prompt.zh\") text=\"GitHub 个人访问令牌(可选)\" ;;\n \"github.token_prompt.en\") text=\"GitHub Personal Access Token (optional)\" ;;\n # --- Skills Registry ---\n \"skills.title.zh\") text=\"--- Skills 注册中心(可选,按回车使用默认 nacos://market.agentteams.io:80/public)---\" ;;\n \"skills.title.en\") text=\"--- Skills Registry (optional, press Enter for default nacos://market.agentteams.io:80/public) ---\" ;;\n \"skills.url_prompt.zh\") text=\"Skills 注册中心 URL(留空使用默认 nacos://market.agentteams.io:80/public)\" ;;\n \"skills.url_prompt.en\") text=\"Skills Registry URL (leave empty for default nacos://market.agentteams.io:80/public)\" ;;\n # --- Data Persistence ---\n \"data.title.zh\") text=\"--- 数据持久化 ---\" ;;\n \"data.title.en\") text=\"--- Data Persistence ---\" ;;\n \"data.volume_prompt.zh\") text=\"Docker 卷名称 [agentteams-data]\" ;;\n \"data.volume_prompt.en\") text=\"Docker volume name for persistent data [agentteams-data]\" ;;\n \"data.volume_using.zh\") text=\" 使用 Docker 卷: %s\" ;;\n \"data.volume_using.en\") text=\" Using Docker volume: %s\" ;;\n # --- Manager Workspace ---\n \"workspace.title.zh\") text=\"--- Manager 工作空间 ---\" ;;\n \"workspace.title.en\") text=\"--- Manager Workspace ---\" ;;\n \"workspace.dir_prompt.zh\") text=\"Manager 工作空间目录 [%s]\" ;;\n \"workspace.dir_prompt.en\") text=\"Manager workspace directory [%s]\" ;;\n \"workspace.dir_label.zh\") text=\" Manager 工作空间: %s\" ;;\n \"workspace.dir_label.en\") text=\" Manager workspace: %s\" ;;\n # --- Host directory sharing ---\n \"host_share.prompt.zh\") text=\"与 Agent 共享的主机目录(默认: %s)\" ;;\n \"host_share.prompt.en\") text=\"Host directory to share with agents (default: %s)\" ;;\n \"host_share.sharing.zh\") text=\"共享主机目录: %s -> 容器内 /host-share\" ;;\n \"host_share.sharing.en\") text=\"Sharing host directory: %s -> /host-share in container\" ;;\n \"host_share.not_exist.zh\") text=\"警告: 主机目录 %s 不存在,跳过验证继续使用\" ;;\n \"host_share.not_exist.en\") text=\"WARNING: Host directory %s does not exist, using without validation\" ;;\n # --- Default worker runtime ---\n \"worker_runtime.title.zh\") text=\"--- 默认 Worker 运行时 ---\" ;;\n \"worker_runtime.title.en\") text=\"--- Default Worker Runtime ---\" ;;\n \"worker_runtime.openclaw.zh\") text=\"OpenClaw\" ;;\n \"worker_runtime.openclaw.en\") text=\"OpenClaw\" ;;\n \"worker_runtime.copaw.zh\") text=\"QwenPaw\" ;;\n \"worker_runtime.copaw.en\") text=\"QwenPaw\" ;;\n \"worker_runtime.hermes.zh\") text=\"Hermes\" ;;\n \"worker_runtime.hermes.en\") text=\"Hermes\" ;;\n \"worker_runtime.choice.zh\") text=\"请选择 [1/2/3]\" ;;\n \"worker_runtime.choice.en\") text=\"Enter choice [1/2/3]\" ;;\n \"worker_runtime.selected.zh\") text=\"默认 Worker 运行时: %s\" ;;\n \"worker_runtime.selected.en\") text=\"Default Worker runtime: %s\" ;;\n \"worker_runtime.title_short.zh\") text=\"默认 Worker 运行时\" ;;\n \"worker_runtime.title_short.en\") text=\"Default Worker Runtime\" ;;\n \"manager_runtime.title.zh\") text=\"--- Manager 运行时 ---\" ;;\n \"manager_runtime.title.en\") text=\"--- Manager Runtime ---\" ;;\n \"manager_runtime.openclaw.zh\") text=\"OpenClaw\" ;;\n \"manager_runtime.openclaw.en\") text=\"OpenClaw\" ;;\n \"manager_runtime.copaw.zh\") text=\"QwenPaw\" ;;\n \"manager_runtime.copaw.en\") text=\"QwenPaw\" ;;\n \"manager_runtime.choice.zh\") text=\"请选择 [1/2]\" ;;\n \"manager_runtime.choice.en\") text=\"Enter choice [1/2]\" ;;\n \"manager_runtime.selected.zh\") text=\"Manager 运行时: %s\" ;;\n \"manager_runtime.selected.en\") text=\"Manager runtime: %s\" ;;\n \"manager_runtime.title_short.zh\") text=\"Manager 运行时\" ;;\n \"manager_runtime.title_short.en\") text=\"Manager Runtime\" ;;\n # --- Secrets and config ---\n \"install.generating_secrets.zh\") text=\"正在生成密钥...\" ;;\n \"install.generating_secrets.en\") text=\"Generating secrets...\" ;;\n \"install.config_saved.zh\") text=\"配置已保存到 %s\" ;;\n \"install.config_saved.en\") text=\"Configuration saved to %s\" ;;\n # --- Container runtime socket ---\n \"install.socket_detected.zh\") text=\"容器运行时 socket: %s(已启用直接创建 Worker)\" ;;\n \"install.socket_detected.en\") text=\"Container runtime socket: %s (direct Worker creation enabled)\" ;;\n \"install.socket_not_found.zh\") text=\"未找到容器运行时 socket(Manager 无法直接创建 Worker 容器,需要你手动执行 docker 命令创建)\" ;;\n \"install.socket_not_found.en\") text=\"No container runtime socket found (Manager cannot create Worker containers directly, you will need to create them manually using docker commands)\" ;;\n \"install.socket_confirm.title.zh\") text=\"⚠️ 未检测到容器运行时 Socket\" ;;\n \"install.socket_confirm.title.en\") text=\"⚠️ Container Runtime Socket Not Detected\" ;;\n \"install.socket_confirm.message.zh\") text=\"未找到 Docker/Podman socket,Manager 将无法自动创建 Worker 容器。\\n你需要手动执行 docker run 命令来创建 Worker。\\n\\n是否继续安装?\" ;;\n \"install.socket_confirm.message.en\") text=\"Docker/Podman socket not found. Manager will not be able to create Worker containers automatically.\\nYou will need to manually run docker commands to create Workers.\\n\\nContinue installation?\" ;;\n \"install.socket_confirm.prompt.zh\") text=\"继续安装? [y/N]: \" ;;\n \"install.socket_confirm.prompt.en\") text=\"Continue? [y/N]: \" ;;\n \"install.socket_confirm.cancelled.zh\") text=\"安装已取消。如需启用 Worker 自动创建,请确保 Docker/Podman 正在运行,然后重新运行安装脚本。\" ;;\n \"install.socket_confirm.cancelled.en\") text=\"Installation cancelled. To enable automatic Worker creation, ensure Docker/Podman is running and re-run the installer.\" ;;\n # --- Container management ---\n \"install.removing_existing.zh\") text=\"正在移除现有 agentteams-manager 容器...\" ;;\n \"install.removing_existing.en\") text=\"Removing existing agentteams-manager container...\" ;;\n # --- Matrix E2EE ---\n \"matrix_e2ee.title.zh\") text=\"--- Matrix 端到端加密(E2EE)---\" ;;\n \"matrix_e2ee.title.en\") text=\"--- Matrix End-to-End Encryption (E2EE) ---\" ;;\n \"matrix_e2ee.desc.zh\") text=\"E2EE 会对 Manager 与 Worker 之间的 Matrix 消息进行端到端加密。\\n 启用后,即使 Matrix 服务器被入侵,消息内容也无法被窃取。\\n 但 E2EE 会增加首次握手耗时,且要求所有 Agent 都支持 matrix-sdk-crypto。\\n 如果不确定,建议保持禁用。\\n ⚠ 注意:禁用 E2EE 后,请勿在 Element 上创建默认启用加密的 Private 房间,\\n 否则 Agent 将无法读取该房间中的加密消息。请改用 Public 房间或关闭房间加密。\" ;;\n \"matrix_e2ee.desc.en\") text=\"E2EE encrypts Matrix messages between Manager and Workers end-to-end.\\n When enabled, message content stays private even if the Matrix server is compromised.\\n However, E2EE adds overhead to the initial handshake and requires all Agents\\n to support matrix-sdk-crypto. If unsure, keep it disabled.\\n ⚠ Note: When E2EE is disabled, do NOT create Private rooms in Element (which\\n enable encryption by default) — Agents cannot read encrypted messages without\\n E2EE support. Use Public rooms or turn off room encryption instead.\" ;;\n \"matrix_e2ee.enable.zh\") text=\"启用 E2EE\" ;;\n \"matrix_e2ee.enable.en\") text=\"Enable E2EE\" ;;\n \"matrix_e2ee.disable.zh\") text=\"禁用 E2EE(推荐)\" ;;\n \"matrix_e2ee.disable.en\") text=\"Disable E2EE (recommended)\" ;;\n \"matrix_e2ee.choice.zh\") text=\"请选择 [1/2]\" ;;\n \"matrix_e2ee.choice.en\") text=\"Enter choice [1/2]\" ;;\n \"matrix_e2ee.selected_enabled.zh\") text=\"Matrix E2EE: 已启用\" ;;\n \"matrix_e2ee.selected_enabled.en\") text=\"Matrix E2EE: enabled\" ;;\n \"matrix_e2ee.selected_disabled.zh\") text=\"Matrix E2EE: 已禁用(默认)\" ;;\n \"matrix_e2ee.selected_disabled.en\") text=\"Matrix E2EE: disabled (default)\" ;;\n \"matrix_e2ee.title_short.zh\") text=\"Matrix E2EE\" ;;\n \"matrix_e2ee.title_short.en\") text=\"Matrix E2EE\" ;;\n \"matrix_e2ee.val_enabled.zh\") text=\"已启用\" ;;\n \"matrix_e2ee.val_enabled.en\") text=\"enabled\" ;;\n \"matrix_e2ee.val_disabled.zh\") text=\"已禁用\" ;;\n \"matrix_e2ee.val_disabled.en\") text=\"disabled\" ;;\n # --- Docker API proxy ---\n \"docker_proxy.title.zh\") text=\"--- Docker API 安全代理 ---\" ;;\n \"docker_proxy.title.en\") text=\"--- Docker API Security Proxy ---\" ;;\n \"docker_proxy.desc.zh\") text=\"Docker API 代理可防止 AI Agent 通过 Docker API 越狱访问宿主机。\\n 启用后,Manager 不再直接持有 Docker socket,所有容器操作经过安全校验。\" ;;\n \"docker_proxy.desc.en\") text=\"Docker API proxy prevents AI Agents from escaping via Docker API to access the host.\\n When enabled, Manager no longer has direct Docker socket access; all container operations go through security validation.\" ;;\n \"docker_proxy.enable.zh\") text=\"启用(推荐)\" ;;\n \"docker_proxy.enable.en\") text=\"Enable (recommended)\" ;;\n \"docker_proxy.disable.zh\") text=\"禁用(直接挂载 Docker socket)\" ;;\n \"docker_proxy.disable.en\") text=\"Disable (mount Docker socket directly)\" ;;\n \"docker_proxy.choice.zh\") text=\"请选择 [1/2]\" ;;\n \"docker_proxy.choice.en\") text=\"Enter choice [1/2]\" ;;\n \"docker_proxy.selected_enabled.zh\") text=\"Docker API 代理: 已启用\" ;;\n \"docker_proxy.selected_enabled.en\") text=\"Docker API proxy: enabled\" ;;\n \"docker_proxy.selected_disabled.zh\") text=\"Docker API 代理: 已禁用\" ;;\n \"docker_proxy.selected_disabled.en\") text=\"Docker API proxy: disabled\" ;;\n \"docker_proxy.title_short.zh\") text=\"Docker API 代理\" ;;\n \"docker_proxy.title_short.en\") text=\"Docker API Proxy\" ;;\n \"docker_proxy.val_enabled.zh\") text=\"已启用\" ;;\n \"docker_proxy.val_enabled.en\") text=\"enabled\" ;;\n \"docker_proxy.val_disabled.zh\") text=\"已禁用\" ;;\n \"docker_proxy.val_disabled.en\") text=\"disabled\" ;;\n \"docker_proxy.registries_desc.zh\") text=\"默认放行的镜像来源:本地镜像、localhost、Higress 仓库(所有 region)。\\n 如需放行其他镜像仓库,请输入逗号分隔的地址前缀。\\n 示例: ghcr.io/myorg,registry.example.com/team\" ;;\n \"docker_proxy.registries_desc.en\") text=\"Default allowed image sources: local images, localhost, Higress registries (all regions).\\n To allow additional image sources, enter comma-separated address prefixes.\\n Example: ghcr.io/myorg,registry.example.com/team\" ;;\n \"docker_proxy.registries_prompt.zh\") text=\"额外放行的镜像来源(按回车跳过)\" ;;\n \"docker_proxy.registries_prompt.en\") text=\"Additional allowed image sources (press Enter to skip)\" ;;\n \"docker_proxy.registries_label.zh\") text=\"额外放行的镜像来源\" ;;\n \"docker_proxy.registries_label.en\") text=\"Additional allowed image sources\" ;;\n # --- Podman Autostart ---\n \"podman.autostart.title.zh\") text=\"--- Podman 驻留与开机自启 ---\" ;;\n \"podman.autostart.title.en\") text=\"--- Podman Linger & Autostart ---\" ;;\n \"podman.autostart.desc.zh\") text=\"配置为 systemd 服务,支持开机自动启动及后台驻留。\" ;;\n \"podman.autostart.desc.en\") text=\"Configure as a systemd service for automatic startup on boot.\" ;;\n \"podman.autostart.enable.zh\") text=\"启用(推荐)\" ;;\n \"podman.autostart.enable.en\") text=\"Enable (recommended)\" ;;\n \"podman.autostart.disable.zh\") text=\"不启用(容器随当前会话关闭而停止)\" ;;\n \"podman.autostart.disable.en\") text=\"Disable (containers stop when session ends)\" ;;\n \"podman.autostart.choice.zh\") text=\"请选择 [1/2]\" ;;\n \"podman.autostart.choice.en\") text=\"Enter choice [1/2]\" ;;\n \"podman.autostart.selected_enabled.zh\") text=\"Podman 开机自启: 已启用\" ;;\n \"podman.autostart.selected_enabled.en\") text=\"Podman Autostart: enabled\" ;;\n \"podman.autostart.selected_disabled.zh\") text=\"Podman 开机自启: 已禁用\" ;;\n \"podman.autostart.selected_disabled.en\") text=\"Podman Autostart: disabled\" ;;\n \"podman.autostart.title_short.zh\") text=\"Podman 开机自启\" ;;\n \"podman.autostart.title_short.en\") text=\"Podman Autostart\" ;;\n \"podman.autostart.val_enabled.zh\") text=\"已启用\" ;;\n \"podman.autostart.val_enabled.en\") text=\"enabled\" ;;\n \"podman.autostart.val_disabled.zh\") text=\"已禁用\" ;;\n \"podman.autostart.val_disabled.en\") text=\"disabled\" ;;\n \"install.podman.autostart_title.zh\") text=\"正在配置 Podman 容器开机自启...\" ;;\n \"install.podman.autostart_title.en\") text=\"Configuring Podman container autostart...\" ;;\n \"install.podman.linger_enable.zh\") text=\"尝试启用 systemd linger (如有提示请在此输入 sudo 密码)...\" ;;\n \"install.podman.linger_enable.en\") text=\"Attempting to enable systemd linger (enter sudo password if prompted)...\" ;;\n \"install.podman.linger_warn.zh\") text=\"警告: 无法自动启用 linger。驻留可能失败,请手动执行: sudo loginctl enable-linger \\$(whoami)\" ;;\n \"install.podman.linger_warn.en\") text=\"WARNING: Failed to auto-enable linger. Please run manually: sudo loginctl enable-linger \\$(whoami)\" ;;\n \"install.podman.root_setup.zh\") text=\"检测到 root 用户。正在配置系统级 agentteams-podman-restart 服务...\" ;;\n \"install.podman.root_setup.en\") text=\"Root user detected. Configuring system-wide agentteams-podman-restart service...\" ;;\n \"install.podman.root_success.zh\") text=\"✅ 系统级 agentteams-podman-restart 服务已成功启用并运行。\" ;;\n \"install.podman.root_success.en\") text=\"✅ System-wide agentteams-podman-restart service successfully enabled and running.\" ;;\n \"install.podman.root_fail.zh\") text=\"⚠️ 系统级 agentteams-podman-restart 服务启用失败,请稍后手动检查。\" ;;\n \"install.podman.root_fail.en\") text=\"⚠️ Failed to enable system-wide agentteams-podman-restart service. Please check manually later.\" ;;\n \"install.podman.user_setup.zh\") text=\"检测到普通用户 (%s)。正在配置用户级 agentteams-podman-restart 服务...\" ;;\n \"install.podman.user_setup.en\") text=\"Non-root user (%s) detected. Configuring user-level agentteams-podman-restart service...\" ;;\n \"install.podman.user_success.zh\") text=\"✅ 用户级 agentteams-podman-restart 服务已成功启用并运行。\" ;;\n \"install.podman.user_success.en\") text=\"✅ User-level agentteams-podman-restart service successfully enabled and running.\" ;;\n \"install.podman.user_fail.zh\") text=\"⚠️ 用户级 agentteams-podman-restart 服务启用失败。\\n提示:这通常是因为缺少 XDG_RUNTIME_DIR 或 dbus 没有运行。您可以尝试手动执行: systemctl --user enable --now agentteams-podman-restart.service\" ;;\n \"install.podman.user_fail.en\") text=\"⚠️ Failed to enable user-level agentteams-podman-restart service.\\nHint: This is usually due to missing XDG_RUNTIME_DIR or dbus not running. Try manually: systemctl --user enable --now agentteams-podman-restart.service\" ;;\n \"install.podman.success.zh\") text=\"Podman 开机自启配置完成。\" ;;\n \"install.podman.success.en\") text=\"Podman autostart successfully configured.\" ;;\n # --- Worker idle timeout ---\n \"idle_timeout.prompt.zh\") text=\"Worker 空闲自动停止超时(分钟)[720]\" ;;\n \"idle_timeout.prompt.en\") text=\"Worker idle auto-stop timeout in minutes [720]\" ;;\n \"idle_timeout.selected.zh\") text=\"Worker 空闲超时: %s 分钟\" ;;\n \"idle_timeout.selected.en\") text=\"Worker idle timeout: %s minutes\" ;;\n \"idle_timeout.label.zh\") text=\"Worker 空闲超时(分钟)\" ;;\n \"idle_timeout.label.en\") text=\"Worker idle timeout (min)\" ;;\n # --- YOLO mode ---\n \"install.yolo.zh\") text=\"YOLO 模式已启用(自主决策,无交互提示)\" ;;\n \"install.yolo.en\") text=\"YOLO mode enabled (autonomous decisions, no interactive prompts)\" ;;\n # --- Image pulling ---\n \"install.image.exists.zh\") text=\"Manager 镜像已存在: %s\" ;;\n \"install.image.exists.en\") text=\"Manager image already exists locally: %s\" ;;\n \"install.image.pulling_manager.zh\") text=\"正在拉取 Manager 镜像: %s\" ;;\n \"install.image.pulling_manager.en\") text=\"Pulling Manager image: %s\" ;;\n \"install.image.worker_exists.zh\") text=\"Worker 镜像已存在: %s\" ;;\n \"install.image.worker_exists.en\") text=\"Worker image already exists locally: %s\" ;;\n \"install.image.pulling_worker.zh\") text=\"正在拉取 Worker 镜像: %s\" ;;\n \"install.image.pulling_worker.en\") text=\"Pulling Worker image: %s\" ;;\n # --- Starting container ---\n \"install.starting_manager.zh\") text=\"正在启动 Manager 容器...\" ;;\n \"install.starting_manager.en\") text=\"Starting Manager container...\" ;;\n # --- Wait for Manager ready ---\n \"install.wait_ready.zh\") text=\"等待 Manager Agent 就绪(超时: %ss)...\" ;;\n \"install.wait_ready.en\") text=\"Waiting for Manager agent to be ready (timeout: %ss)...\" ;;\n \"install.wait_ready.ok.zh\") text=\"Manager Agent 已就绪!\" ;;\n \"install.wait_ready.ok.en\") text=\"Manager agent is ready!\" ;;\n \"install.wait_ready.waiting.zh\") text=\"等待中... (%ds/%ds)\" ;;\n \"install.wait_ready.waiting.en\") text=\"Waiting... (%ds/%ds)\" ;;\n \"install.wait_ready.timeout.zh\") text=\"Manager Agent 在 %ss 内未就绪。请检查: docker logs %s\" ;;\n \"install.wait_ready.timeout.en\") text=\"Manager agent did not become ready within %ss. Check: docker logs %s\" ;;\n # --- Wait for Matrix ready ---\n \"install.wait_matrix.zh\") text=\"等待 Matrix 服务就绪(超时: %ss)...\" ;;\n \"install.wait_matrix.en\") text=\"Waiting for Matrix server to be ready (timeout: %ss)...\" ;;\n \"install.wait_matrix.ok.zh\") text=\"Matrix 服务已就绪!\" ;;\n \"install.wait_matrix.ok.en\") text=\"Matrix server is ready!\" ;;\n \"install.wait_matrix.waiting.zh\") text=\"等待 Matrix 中... (%ds/%ds)\" ;;\n \"install.wait_matrix.waiting.en\") text=\"Waiting for Matrix... (%ds/%ds)\" ;;\n \"install.wait_matrix.timeout.zh\") text=\"Matrix 服务在 %ss 内未就绪。请检查: docker logs %s\" ;;\n \"install.wait_matrix.timeout.en\") text=\"Matrix server did not become ready within %ss. Check: docker logs %s\" ;;\n # --- OpenAI-compatible connectivity test ---\n \"llm.openai.test.testing.zh\") text=\"正在测试 API 联通性...\" ;;\n \"llm.openai.test.testing.en\") text=\"Testing API connectivity...\" ;;\n \"llm.openai.test.ok.zh\") text=\"✅ API 联通性测试通过\" ;;\n \"llm.openai.test.ok.en\") text=\"✅ API connectivity test passed\" ;;\n \"llm.openai.test.fail.zh\") text=\"⚠️ API 联通性测试失败(HTTP %s)。响应内容:\\n%s\\n请根据以上错误信息联系您的模型服务商解决。\" ;;\n \"llm.openai.test.fail.en\") text=\"⚠️ API connectivity test failed (HTTP %s). Response body:\\n%s\\nPlease contact your model provider to resolve the issue.\" ;;\n \"llm.openai.test.fail.tokenplan.zh\") text=\"⚠️ 提示: 请确认 API Key 有效且已开通通义 Token 套餐。文档: https://help.aliyun.com/zh/model-studio/token-plan-quickstart\" ;;\n \"llm.openai.test.fail.tokenplan.en\") text=\"⚠️ Hint: Verify your Token Plan API key and compatible-mode access. Docs: https://help.aliyun.com/zh/model-studio/token-plan-quickstart\" ;;\n \"llm.openai.test.fail.codingplan.zh\") text=\"⚠️ 提示: 请确认 API Key 有效且已开通通义 Token 套餐。文档: https://help.aliyun.com/zh/model-studio/token-plan-quickstart\" ;;\n \"llm.openai.test.fail.codingplan.en\") text=\"⚠️ Hint: Verify your DASHSCOPE_API_KEY for Qwen Cloud. API keys: https://home.qwencloud.com/api-keys Docs: https://docs.qwencloud.com/\" ;;\n \"llm.openai.test.fail.codingplan_legacy.zh\") text=\"⚠️ 提示: 请确认 API Key 有效且 Coding 套餐接口可用。文档: https://help.aliyun.com/zh/model-studio/get-api-key\" ;;\n \"llm.openai.test.fail.codingplan_legacy.en\") text=\"⚠️ Hint: Verify your DashScope API key and Coding Plan access. Docs: https://help.aliyun.com/zh/model-studio/get-api-key\" ;;\n \"llm.openai.test.no_curl.zh\") text=\"⚠️ 未找到 curl,跳过 API 联通性测试\" ;;\n \"llm.openai.test.no_curl.en\") text=\"⚠️ curl not found, skipping API connectivity test\" ;;\n \"llm.openai.test.confirm.zh\") text=\"是否仍要继续安装?[y/N/b] \" ;;\n \"llm.openai.test.confirm.en\") text=\"Continue with installation anyway? [y/N/b] \" ;;\n \"llm.embedding.title.zh\") text=\"📦 记忆搜索配置\" ;;\n \"llm.embedding.title.en\") text=\"📦 Memory Search Configuration\" ;;\n \"llm.embedding.hint.zh\") text=\" Embedding 模型可提升记忆搜索质量(语义匹配)。不启用也可正常使用记忆功能(关键词匹配)。\" ;;\n \"llm.embedding.hint.en\") text=\" Embedding model improves memory search quality (semantic matching). Memory still works without it (keyword matching).\" ;;\n \"llm.embedding.option.default.zh\") text=\" 1) text-embedding-v4(推荐)\" ;;\n \"llm.embedding.option.default.en\") text=\" 1) text-embedding-v4 (Recommended)\" ;;\n \"llm.embedding.option.custom.zh\") text=\" 2) 自定义 Embedding 模型\" ;;\n \"llm.embedding.option.custom.en\") text=\" 2) Custom embedding model\" ;;\n \"llm.embedding.option.disable.zh\") text=\" 3) 不启用\" ;;\n \"llm.embedding.option.disable.en\") text=\" 3) Do not enable\" ;;\n \"llm.embedding.select.zh\") text=\"选择\" ;;\n \"llm.embedding.select.en\") text=\"Select\" ;;\n \"llm.embedding.custom_prompt.zh\") text=\" Embedding 模型名称\" ;;\n \"llm.embedding.custom_prompt.en\") text=\" Embedding model name\" ;;\n \"llm.embedding.test.testing.zh\") text=\"正在测试 Embedding API 联通性...\" ;;\n \"llm.embedding.test.testing.en\") text=\"Testing Embedding API connectivity...\" ;;\n \"llm.embedding.test.ok.zh\") text=\"✅ Embedding API 联通性测试通过\" ;;\n \"llm.embedding.test.ok.en\") text=\"✅ Embedding API connectivity test passed\" ;;\n \"llm.embedding.test.fail.zh\") text=\"⚠️ Embedding API 测试失败(HTTP %s)。响应: %s\" ;;\n \"llm.embedding.test.fail.en\") text=\"⚠️ Embedding API test failed (HTTP %s). Response: %s\" ;;\n \"llm.embedding.auto_disabled.zh\") text=\"⚠️ Embedding 已自动禁用,记忆搜索将使用关键词匹配。您可以稍后在 agentteams-manager.env 中设置 AGENTTEAMS_EMBEDDING_MODEL 启用。\" ;;\n \"llm.embedding.auto_disabled.en\") text=\"⚠️ Embedding auto-disabled. Memory search will use keyword matching. You can enable it later in agentteams-manager.env by setting AGENTTEAMS_EMBEDDING_MODEL.\" ;;\n \"llm.embedding.disabled.zh\") text=\"ℹ️ Embedding 已禁用,记忆搜索将使用关键词匹配。\" ;;\n \"llm.embedding.disabled.en\") text=\"ℹ️ Embedding disabled. Memory search will use keyword matching.\" ;;\n \"llm.openai.test.aborted.zh\") text=\"安装已中止。\" ;;\n \"llm.openai.test.aborted.en\") text=\"Installation aborted.\" ;;\n \"nav.back_hint.zh\") text=\"(输入 b 返回上一步)\" ;;\n \"nav.back_hint.en\") text=\"(enter b to go back)\" ;;\n # --- OpenAI-compatible provider creation ---\n \"install.openai_compat.missing.zh\") text=\"警告: OpenAI Base URL 或 API Key 未设置,跳过提供商创建\" ;;\n \"install.openai_compat.missing.en\") text=\"WARNING: OpenAI Base URL or API Key not set, skipping provider creation\" ;;\n \"install.openai_compat.creating.zh\") text=\"正在创建 OpenAI 兼容提供商...\" ;;\n \"install.openai_compat.creating.en\") text=\"Creating OpenAI-compatible provider...\" ;;\n \"install.openai_compat.domain.zh\") text=\" 域名: %s\" ;;\n \"install.openai_compat.domain.en\") text=\" Domain: %s\" ;;\n \"install.openai_compat.port.zh\") text=\" 端口: %s\" ;;\n \"install.openai_compat.port.en\") text=\" Port: %s\" ;;\n \"install.openai_compat.protocol.zh\") text=\" 协议: %s\" ;;\n \"install.openai_compat.protocol.en\") text=\" Protocol: %s\" ;;\n \"install.openai_compat.service_fail.zh\") text=\"警告: 创建 DNS 服务源失败(可能已存在)\" ;;\n \"install.openai_compat.service_fail.en\") text=\"WARNING: Failed to create DNS service source (may already exist)\" ;;\n \"install.openai_compat.provider_fail.zh\") text=\"警告: 创建 AI 提供商失败(可能已存在)\" ;;\n \"install.openai_compat.provider_fail.en\") text=\"WARNING: Failed to create AI provider (may already exist)\" ;;\n \"install.openai_compat.success.zh\") text=\"OpenAI 兼容提供商创建成功\" ;;\n \"install.openai_compat.success.en\") text=\"OpenAI-compatible provider created successfully\" ;;\n # --- Welcome message ---\n \"install.welcome_msg.soul_configured.zh\") text=\"Soul 已配置(找到 soul-configured 标记),跳过 onboarding 消息\" ;;\n \"install.welcome_msg.soul_configured.en\") text=\"Soul already configured (soul-configured marker found), skipping onboarding message\" ;;\n \"install.welcome_msg.logging_in.zh\") text=\"正在以 %s 身份登录以发送欢迎消息...\" ;;\n \"install.welcome_msg.logging_in.en\") text=\"Logging in as %s to send welcome message...\" ;;\n \"install.welcome_msg.login_failed.zh\") text=\"警告: 以 %s 身份登录失败,跳过欢迎消息\" ;;\n \"install.welcome_msg.login_failed.en\") text=\"WARNING: Failed to login as %s, skipping welcome message\" ;;\n \"install.welcome_msg.finding_room.zh\") text=\"正在查找与 Manager 的 DM 房间...\" ;;\n \"install.welcome_msg.finding_room.en\") text=\"Finding DM room with Manager...\" ;;\n \"install.welcome_msg.creating_room.zh\") text=\"正在创建与 Manager 的 DM 房间...\" ;;\n \"install.welcome_msg.creating_room.en\") text=\"Creating DM room with Manager...\" ;;\n \"install.welcome_msg.no_room.zh\") text=\"警告: 无法找到或创建与 Manager 的 DM 房间\" ;;\n \"install.welcome_msg.no_room.en\") text=\"WARNING: Could not find or create DM room with Manager\" ;;\n \"install.welcome_msg.waiting_join.zh\") text=\"等待 Manager 加入房间...\" ;;\n \"install.welcome_msg.waiting_join.en\") text=\"Waiting for Manager to join the room...\" ;;\n \"install.welcome_msg.sending.zh\") text=\"正在向 Manager 发送欢迎消息...\" ;;\n \"install.welcome_msg.sending.en\") text=\"Sending welcome message to Manager...\" ;;\n \"install.welcome_msg.send_failed.zh\") text=\"警告: 发送欢迎消息失败\" ;;\n \"install.welcome_msg.send_failed.en\") text=\"WARNING: Failed to send welcome message\" ;;\n \"install.welcome_msg.sent.zh\") text=\"欢迎消息已发送给 Manager\" ;;\n \"install.welcome_msg.sent.en\") text=\"Welcome message sent to Manager\" ;;\n \"install.welcome_msg.waiting.zh\") text=\"等待 Manager 发送欢迎消息(Higress 路由授权 + LLM 探活,约 45-90s)...\" ;;\n \"install.welcome_msg.waiting.en\") text=\"Waiting for Manager to send the welcome message (Higress route auth + LLM probe, ~45-90s)...\" ;;\n \"install.welcome_msg.confirmed.zh\") text=\"Manager 已确认发送欢迎消息(status.welcomeSent=true,用时 %ss)\" ;;\n \"install.welcome_msg.confirmed.en\") text=\"Manager confirmed welcome message sent (status.welcomeSent=true, %ss elapsed)\" ;;\n \"install.welcome_msg.timeout.zh\") text=\"警告: 在 %ss 内未观察到 Manager 发送欢迎消息(status.welcomeSent=true)。安装仍然成功,所有服务已就绪——可继续按下方提示登录 Element Web。\" ;;\n \"install.welcome_msg.timeout.en\") text=\"WARNING: Did not observe the Manager sending its welcome message (status.welcomeSent=true) within %ss. Installation is still successful, all services are up — continue with the Element Web instructions below.\" ;;\n \"install.welcome_msg.timeout_hint.zh\") text=\"手动触发 onboarding: 登录 Element Web → 打开与 Manager 的 DM 房间 → 发送任意一句话(例如 \\\"hi\\\"),Manager 会接管对话并开始引导。\" ;;\n \"install.welcome_msg.timeout_hint.en\") text=\"Manual onboarding: log in to Element Web → open the DM with the Manager → send any message (e.g. \\\"hi\\\") and the Manager will take over and start the guided setup.\" ;;\n \"install.welcome_msg.timeout_inspect.zh\") text=\"排查命令: docker exec agentteams-controller agt get managers default\" ;;\n \"install.welcome_msg.timeout_inspect.en\") text=\"Inspect status: docker exec agentteams-controller agt get managers default\" ;;\n \"install.welcome_msg.poll_unavailable.zh\") text=\"提示: agentteams-manager 内未找到 AgentTeams CLI,跳过 welcome 等待(旧镜像?)\" ;;\n \"install.welcome_msg.poll_unavailable.en\") text=\"Note: AgentTeams CLI not found inside agentteams-manager; skipping welcome wait (old image?)\" ;;\n # --- Dashboard wizard ---\n \"dash.prompt.zh\") text=\"是否安装 agentteams-dashboard 管理面板?\" ;;\n \"dash.prompt.en\") text=\"Install agentteams-dashboard management UI?\" ;;\n \"dash.version_prompt.zh\") text=\"Dashboard 版本\" ;;\n \"dash.version_prompt.en\") text=\"Dashboard version\" ;;\n \"dash.port_prompt.zh\") text=\"Dashboard 端口号\" ;;\n \"dash.port_prompt.en\") text=\"Dashboard port\" ;;\n \"dash.image_prompt.zh\") text=\"Dashboard 镜像\" ;;\n \"dash.image_prompt.en\") text=\"Dashboard image\" ;;\n \"dash.gateway_prompt.zh\") text=\"Higress Console URL (用于共享登录)\" ;;\n \"dash.gateway_prompt.en\") text=\"Higress Console URL (for shared login)\" ;;\n \"dash.skip.zh\") text=\"跳过 Dashboard 安装\" ;;\n \"dash.skip.en\") text=\"Skipping Dashboard installation\" ;;\n \"dash.auto_url.zh\") text=\"自动检测到 Higress Console URL\" ;;\n \"dash.auto_url.en\") text=\"Auto-detected Higress Console URL\" ;;\n \"dash.auto_url_fail.zh\") text=\"无法自动检测 Higress Console URL,请手动输入\" ;;\n \"dash.auto_url_fail.en\") text=\"Cannot auto-detect Higress Console URL, please enter manually\" ;;\n \"dash.summary.zh\") text=\"Dashboard 配置: 端口=%s 镜像=%s\" ;;\n \"dash.summary.en\") text=\"Dashboard config: port=%s image=%s\" ;;\n # --- Dashboard success ---\n \"success.dashboard.zh\") text=\" agentteams-dashboard 管理面板: http://localhost:%s/\" ;;\n \"success.dashboard.en\") text=\" agentteams-dashboard: http://localhost:%s/\" ;;\n # --- Final output panel ---\n \"success.title.zh\") text=\"=== AgentTeams Manager 已启动!===\" ;;\n \"success.title.en\") text=\"=== AgentTeams Manager Started! ===\" ;;\n \"success.domains_configured.zh\") text=\"以下域名已配置解析到 127.0.0.1:\" ;;\n \"success.domains_configured.en\") text=\"The following domains are configured to resolve to 127.0.0.1:\" ;;\n \"success.open_url.zh\") text=\" ★ 在浏览器中打开以下 URL 开始使用: ★\" ;;\n \"success.open_url.en\") text=\" ★ Open the following URL in your browser to start: ★\" ;;\n \"success.login_with.zh\") text=\" 登录信息:\" ;;\n \"success.login_with.en\") text=\" Login with:\" ;;\n \"success.username.zh\") text=\" 用户名: %s\" ;;\n \"success.username.en\") text=\" Username: %s\" ;;\n \"success.password.zh\") text=\" 密码: %s\" ;;\n \"success.password.en\") text=\" Password: %s\" ;;\n \"success.after_login.zh\") text=\" 登录后,开始与 Manager 聊天!\" ;;\n \"success.after_login.en\") text=\" After login, start chatting with the Manager!\" ;;\n \"success.tell_it.zh\") text=\" 告诉它: \\\"创建一个名为 alice 的前端开发 Worker\\\"\" ;;\n \"success.tell_it.en\") text=\" Tell it: \\\"Create a Worker named alice for frontend dev\\\"\" ;;\n \"success.manager_auto.zh\") text=\" Manager 会自动处理一切。\" ;;\n \"success.manager_auto.en\") text=\" The Manager will handle everything automatically.\" ;;\n \"success.mobile_title.zh\") text=\" 📱 移动端访问(FluffyChat / Element Mobile):\" ;;\n \"success.mobile_title.en\") text=\" 📱 Mobile access (FluffyChat / Element Mobile):\" ;;\n \"success.mobile_step1.zh\") text=\" 1. 在手机上下载 FluffyChat 或 Element\" ;;\n \"success.mobile_step1.en\") text=\" 1. Download FluffyChat or Element on your phone\" ;;\n \"success.mobile_step2.zh\") text=\" 2. 设置 homeserver 为: %s\" ;;\n \"success.mobile_step2.en\") text=\" 2. Set homeserver to: %s\" ;;\n \"success.mobile_step2_noip.zh\") text=\" 2. 设置 homeserver 为: http://<本机局域网IP>:%s\" ;;\n \"success.mobile_step2_noip.en\") text=\" 2. Set homeserver to: http://<this-machine-LAN-IP>:%s\" ;;\n \"success.mobile_noip_hint.zh\") text=\" (无法自动检测局域网 IP — 请使用 ifconfig / ip addr 查看)\" ;;\n \"success.mobile_noip_hint.en\") text=\" (Could not detect LAN IP automatically — check with: ifconfig / ip addr)\" ;;\n \"success.mobile_step3.zh\") text=\" 3. 登录信息:\" ;;\n \"success.mobile_step3.en\") text=\" 3. Login with:\" ;;\n \"success.mobile_username.zh\") text=\" 用户名: %s\" ;;\n \"success.mobile_username.en\") text=\" Username: %s\" ;;\n \"success.mobile_password.zh\") text=\" 密码: %s\" ;;\n \"success.mobile_password.en\") text=\" Password: %s\" ;;\n # --- Other consoles and tips ---\n \"success.other_consoles.zh\") text=\"--- 其他控制台 ---\" ;;\n \"success.other_consoles.en\") text=\"--- Other Consoles ---\" ;;\n \"success.higress_console.zh\") text=\" Higress 控制台: http://localhost:%s(用户名: %s / 密码: %s)\" ;;\n \"success.higress_console.en\") text=\" Higress Console: http://localhost:%s (Username: %s / Password: %s)\" ;;\n \"success.manager_console.zh\") text=\" Manager 控制台(本地): http://localhost:%s(无需登录)\" ;;\n \"success.manager_console.en\") text=\" Manager Console (local): http://localhost:%s (no login required)\" ;;\n \"success.manager_console_gateway.zh\") text=\" Manager 控制台(网关): http://console-local.agentteams.io(用户名: %s / 密码: %s)\" ;;\n \"success.manager_console_gateway.en\") text=\" Manager Console (gateway): http://console-local.agentteams.io (Username: %s / Password: %s)\" ;;\n \"success.copaw_console.zh\") text=\" QwenPaw App API: http://localhost:%s(无需登录)\" ;;\n \"success.copaw_console.en\") text=\" QwenPaw App API: http://localhost:%s (no login required)\" ;;\n \"success.switch_llm.title.zh\") text=\"--- 切换 LLM 提供商 ---\" ;;\n \"success.switch_llm.title.en\") text=\"--- Switch LLM Providers ---\" ;;\n \"success.switch_llm.hint.zh\") text=\" 您可以通过 Higress 控制台切换到其他 LLM 提供商(OpenAI、Anthropic 等)。\" ;;\n \"success.switch_llm.hint.en\") text=\" You can switch to other LLM providers (OpenAI, Anthropic, etc.) via Higress Console.\" ;;\n \"success.switch_llm.docs.zh\") text=\" 详细说明请参阅:\" ;;\n \"success.switch_llm.docs.en\") text=\" For detailed instructions, see:\" ;;\n \"success.switch_llm.url.zh\") text=\" https://higress.ai/en/docs/ai/scene-guide/multi-proxy#console-configuration\" ;;\n \"success.switch_llm.url.en\") text=\" https://higress.ai/en/docs/ai/scene-guide/multi-proxy#console-configuration\" ;;\n \"success.tip.zh\") text=\"提示: 您也可以在聊天中让 Manager 为您配置 LLM 提供商。\" ;;\n \"success.tip.en\") text=\"Tip: You can also ask the Manager to configure LLM providers for you in the chat.\" ;;\n \"success.config_file.zh\") text=\"配置文件: %s\" ;;\n \"success.config_file.en\") text=\"Configuration file: %s\" ;;\n \"success.data_volume.zh\") text=\"数据卷: %s\" ;;\n \"success.data_volume.en\") text=\"Data volume: %s\" ;;\n \"success.workspace.zh\") text=\"Manager 工作空间: %s\" ;;\n \"success.workspace.en\") text=\"Manager workspace: %s\" ;;\n # --- Worker installation ---\n \"worker.resetting.zh\") text=\"正在重置 Worker: %s...\" ;;\n \"worker.resetting.en\") text=\"Resetting Worker: %s...\" ;;\n \"worker.exists.zh\") text=\"容器 '%s' 已存在。使用 --reset 重新创建。\" ;;\n \"worker.exists.en\") text=\"Container '%s' already exists. Use --reset to recreate.\" ;;\n \"worker.starting.zh\") text=\"正在启动 Worker: %s...\" ;;\n \"worker.starting.en\") text=\"Starting Worker: %s...\" ;;\n \"worker.skills_url.zh\") text=\" Skills API URL: %s\" ;;\n \"worker.skills_url.en\") text=\" Skills API URL: %s\" ;;\n \"worker.started.zh\") text=\"=== Worker %s 已启动!===\" ;;\n \"worker.started.en\") text=\"=== Worker %s Started! ===\" ;;\n \"worker.container.zh\") text=\"容器: %s\" ;;\n \"worker.container.en\") text=\"Container: %s\" ;;\n \"worker.view_logs.zh\") text=\"查看日志: docker logs -f %s\" ;;\n \"worker.view_logs.en\") text=\"View logs: docker logs -f %s\" ;;\n # --- Prompt function messages ---\n \"prompt.preset.zh\") text=\" %s = (已通过环境变量预设)\" ;;\n \"prompt.preset.en\") text=\" %s = (pre-set via env)\" ;;\n \"prompt.upgrade_keep.zh\") text=\" %s = %s(当前值,回车保留 / 输入新值覆盖)\" ;;\n \"prompt.upgrade_keep.en\") text=\" %s = %s (current value, press Enter to keep / type new value to change)\" ;;\n \"prompt.upgrade_keep_secret.zh\") text=\" %s = %s(当前值,回车保留 / 输入新值覆盖)\" ;;\n \"prompt.upgrade_keep_secret.en\") text=\" %s = %s (current value, press Enter to keep / type new value to change)\" ;;\n \"prompt.upgrade_empty.zh\") text=\" %s = (未设置,回车跳过 / 输入新值设置)\" ;;\n \"prompt.upgrade_empty.en\") text=\" %s = (not set, press Enter to skip / type new value to set)\" ;;\n \"prompt.default.zh\") text=\" %s = %s(默认)\" ;;\n \"prompt.default.en\") text=\" %s = %s (default)\" ;;\n \"prompt.required.zh\") text=\"%s 是必需的(在非交互模式下通过环境变量设置)\" ;;\n \"prompt.required.en\") text=\"%s is required (set via environment variable in non-interactive mode)\" ;;\n \"prompt.required_empty.zh\") text=\"%s 是必需的\" ;;\n \"prompt.required_empty.en\") text=\"%s is required\" ;;\n # --- Language switch prompt (bilingual by design) ---\n \"lang.detected.zh\") text=\"检测到语言 / Detected language: 中文\" ;;\n \"lang.detected.en\") text=\"检测到语言 / Detected language: English\" ;;\n \"lang.switch_title.zh\") text=\"切换语言 / Switch language:\" ;;\n \"lang.switch_title.en\") text=\"切换语言 / Switch language:\" ;;\n \"lang.option_zh.zh\") text=\" 1) 中文\" ;;\n \"lang.option_zh.en\") text=\" 1) 中文\" ;;\n \"lang.option_en.zh\") text=\" 2) English\" ;;\n \"lang.option_en.en\") text=\" 2) English\" ;;\n \"lang.prompt.zh\") text=\"请选择 / Enter choice\" ;;\n \"lang.prompt.en\") text=\"请选择 / Enter choice\" ;;\n # --- Uninstall messages ---\n \"uninstall.title.zh\") text=\"正在卸载 AgentTeams...\" ;;\n \"uninstall.title.en\") text=\"Uninstalling AgentTeams...\" ;;\n \"uninstall.stopping_manager.zh\") text=\"正在停止并移除 agentteams-manager...\" ;;\n \"uninstall.stopping_manager.en\") text=\"Stopping and removing agentteams-manager...\" ;;\n \"uninstall.stopping_workers.zh\") text=\"正在停止并移除 Worker 容器...\" ;;\n \"uninstall.stopping_workers.en\") text=\"Stopping and removing worker containers...\" ;;\n \"uninstall.removed.zh\") text=\" 已移除: %s\" ;;\n \"uninstall.removed.en\") text=\" Removed: %s\" ;;\n \"uninstall.removing_volume.zh\") text=\"正在移除 Docker 卷: %s\" ;;\n \"uninstall.removing_volume.en\") text=\"Removing Docker volume: %s\" ;;\n \"uninstall.removing_env.zh\") text=\"正在移除 env 文件: %s\" ;;\n \"uninstall.removing_env.en\") text=\"Removing env file: %s\" ;;\n \"uninstall.removing_proxy.zh\") text=\"正在停止并移除 Docker API 代理容器: agentteams-docker-proxy\" ;;\n \"uninstall.removing_proxy.en\") text=\"Stopping and removing Docker API proxy container: agentteams-docker-proxy\" ;;\n \"uninstall.stopping_controller.zh\") text=\"正在停止并移除 agentteams-controller (内嵌 Tuwunel/MinIO/Higress)...\" ;;\n \"uninstall.stopping_controller.en\") text=\"Stopping and removing agentteams-controller (embedded Tuwunel/MinIO/Higress)...\" ;;\n \"uninstall.removing_network.zh\") text=\"正在移除 Docker 网络: agentteams-net\" ;;\n \"uninstall.removing_network.en\") text=\"Removing Docker network: agentteams-net\" ;;\n \"uninstall.removing_workspace.zh\") text=\"正在移除工作空间目录: %s\" ;;\n \"uninstall.removing_workspace.en\") text=\"Removing workspace directory: %s\" ;;\n \"uninstall.removing_workspace_elevated.zh\") text=\" 工作空间包含 root 文件,通过容器清理...\" ;;\n \"uninstall.removing_workspace_elevated.en\") text=\" Workspace contains root-owned files, cleaning via container...\" ;;\n \"uninstall.removing_log.zh\") text=\"正在移除日志文件: %s\" ;;\n \"uninstall.removing_log.en\") text=\"Removing log file: %s\" ;;\n \"uninstall.done.zh\") text=\"AgentTeams 已卸载。\" ;;\n \"uninstall.done.en\") text=\"AgentTeams has been uninstalled.\" ;;\n # --- Error messages ---\n \"error.name_required.zh\") text=\"--name 是必需的\" ;;\n \"error.name_required.en\") text=\"--name is required\" ;;\n \"error.fs_required.zh\") text=\"--fs 是必需的\" ;;\n \"error.fs_required.en\") text=\"--fs is required\" ;;\n \"error.fs_key_required.zh\") text=\"--fs-key 是必需的\" ;;\n \"error.fs_key_required.en\") text=\"--fs-key is required\" ;;\n \"error.fs_secret_required.zh\") text=\"--fs-secret 是必需的\" ;;\n \"error.fs_secret_required.en\") text=\"--fs-secret is required\" ;;\n \"error.unknown_option.zh\") text=\"未知选项: %s\" ;;\n \"error.unknown_option.en\") text=\"Unknown option: %s\" ;;\n \"error.docker_not_found.zh\") text=\"未找到 docker 或 podman 命令。请先安装 Docker Desktop 或 Podman Desktop:\\n Docker Desktop: https://www.docker.com/products/docker-desktop/\\n Podman Desktop: https://podman-desktop.io/\" ;;\n \"error.docker_not_found.en\") text=\"docker or podman command not found. Please install Docker Desktop or Podman Desktop first:\\n Docker Desktop: https://www.docker.com/products/docker-desktop/\\n Podman Desktop: https://podman-desktop.io/\" ;;\n \"error.docker_not_running.zh\") text=\"Docker 未运行。请先启动 Docker Desktop 或 Podman Desktop。\" ;;\n \"error.docker_not_running.en\") text=\"Docker is not running. Please start Docker Desktop or Podman Desktop first.\" ;;\n # --- Fallback: try English for unknown lang ---\n *)\n case \"${key}.en\" in\n \"tz.warning.title.en\") text=\"Could not detect timezone automatically.\" ;;\n \"install.title.en\") text=\"=== AgentTeams Manager Installation ===\" ;;\n *) text=\"${key}\" ;;\n esac\n ;;\n esac\n if [ $# -gt 0 ]; then\n # shellcheck disable=SC2059\n printf \"${text}\\n\" \"$@\"\n else\n echo \"${text}\"\n fi\n}\n\n# ============================================================\n# Registry selection based on timezone\n# ============================================================\n\ndetect_registry() {\n local tz=\"${AGENTTEAMS_TIMEZONE}\"\n\n case \"${tz}\" in\n America/*)\n echo \"higress-registry.us-west-1.cr.aliyuncs.com\"\n ;;\n Asia/Singapore|Asia/Bangkok|Asia/Jakarta|Asia/Makassar|Asia/Jayapura|\\\n Asia/Kuala_Lumpur|Asia/Ho_Chi_Minh|Asia/Manila|Asia/Yangon|\\\n Asia/Vientiane|Asia/Phnom_Penh|Asia/Pontianak|Asia/Ujung_Pandang)\n echo \"higress-registry.ap-southeast-7.cr.aliyuncs.com\"\n ;;\n *)\n echo \"higress-registry.cn-hangzhou.cr.aliyuncs.com\"\n ;;\n esac\n}\n\nAGENTTEAMS_REGISTRY=\"${AGENTTEAMS_REGISTRY:-$(detect_registry)}\"\n# Backward compatibility: accept old env var names from previous versions\nAGENTTEAMS_INSTALL_CONTROLLER_IMAGE=\"${AGENTTEAMS_INSTALL_CONTROLLER_IMAGE:-${AGENTTEAMS_INSTALL_DOCKER_PROXY_IMAGE:-}}\"\n# Image variables are resolved after version selection in step_version().\n# These placeholders allow early code paths to reference them without errors.\nMANAGER_IMAGE=\"${AGENTTEAMS_INSTALL_MANAGER_IMAGE:-}\"\nMANAGER_COPAW_IMAGE=\"${AGENTTEAMS_INSTALL_MANAGER_COPAW_IMAGE:-}\"\nWORKER_IMAGE=\"${AGENTTEAMS_INSTALL_WORKER_IMAGE:-}\"\nCOPAW_WORKER_IMAGE=\"${AGENTTEAMS_INSTALL_COPAW_WORKER_IMAGE:-}\"\nHERMES_WORKER_IMAGE=\"${AGENTTEAMS_INSTALL_HERMES_WORKER_IMAGE:-}\"\nCONTROLLER_IMAGE=\"${AGENTTEAMS_INSTALL_CONTROLLER_IMAGE:-}\"\n\nresolve_image_tags() {\n AGENTTEAMS_VERSION=\"$(_normalize_version \"${AGENTTEAMS_VERSION}\")\"\n MANAGER_IMAGE=\"${AGENTTEAMS_INSTALL_MANAGER_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-manager:${AGENTTEAMS_VERSION}}\"\n MANAGER_COPAW_IMAGE=\"${AGENTTEAMS_INSTALL_MANAGER_COPAW_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-manager-copaw:${AGENTTEAMS_VERSION}}\"\n WORKER_IMAGE=\"${AGENTTEAMS_INSTALL_WORKER_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-worker:${AGENTTEAMS_VERSION}}\"\n COPAW_WORKER_IMAGE=\"${AGENTTEAMS_INSTALL_COPAW_WORKER_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-copaw-worker:${AGENTTEAMS_VERSION}}\"\n HERMES_WORKER_IMAGE=\"${AGENTTEAMS_INSTALL_HERMES_WORKER_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-hermes-worker:${AGENTTEAMS_VERSION}}\"\n EMBEDDED_IMAGE=\"${AGENTTEAMS_INSTALL_EMBEDDED_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-embedded:${AGENTTEAMS_VERSION}}\"\n # CoPaw Worker introduced in v1.0.4; Hermes Worker introduced in v1.1.0\n if [ -z \"${AGENTTEAMS_INSTALL_COPAW_WORKER_IMAGE:-}\" ] && _ver_lt \"${AGENTTEAMS_VERSION}\" \"v1.0.4\"; then\n COPAW_WORKER_IMAGE=\"\"\n fi\n if [ -z \"${AGENTTEAMS_INSTALL_HERMES_WORKER_IMAGE:-}\" ] && _ver_lt \"${AGENTTEAMS_VERSION}\" \"v1.1.0\"; then\n HERMES_WORKER_IMAGE=\"\"\n fi\n}\n\n# Resolve the embedded controller image. Embedded mode is the only supported\n# architecture since PR #616 (manager image no longer bundles Higress/Tuwunel/MinIO).\n# If the embedded image is unavailable for the requested version, fail fast with an\n# actionable error rather than silently falling back to the legacy single-container\n# path — that path is permanently broken with the slim manager image and would just\n# leave the user with a manager container looping on \"Waiting for Higress Gateway\".\n# Sets EMBEDDED_IMAGE and AGENTTEAMS_USE_EMBEDDED.\nresolve_embedded_image() {\n AGENTTEAMS_USE_EMBEDDED=1\n\n # If the user explicitly overrode the image (e.g. `make install-embedded` passes\n # a locally-built tag), respect it as-is without any registry probe.\n if [ -n \"${AGENTTEAMS_INSTALL_EMBEDDED_IMAGE:-}\" ]; then\n EMBEDDED_IMAGE=\"${AGENTTEAMS_INSTALL_EMBEDDED_IMAGE}\"\n return 0\n fi\n\n local _versioned=\"${AGENTTEAMS_REGISTRY}/agentteams/agentteams-embedded:${AGENTTEAMS_VERSION}\"\n local _latest=\"${AGENTTEAMS_REGISTRY}/agentteams/agentteams-embedded:latest\"\n\n # Skip probe when AGENTTEAMS_VERSION is \"latest\" — no point trying the same tag twice.\n if [ \"${AGENTTEAMS_VERSION}\" = \"latest\" ]; then\n EMBEDDED_IMAGE=\"${_latest}\"\n return 0\n fi\n\n if ${DOCKER_CMD} pull \"${_versioned}\" >/dev/null 2>&1; then\n EMBEDDED_IMAGE=\"${_versioned}\"\n return 0\n fi\n\n # Versions before v1.1.0 predate agentteams-embedded entirely — their manager image\n # bundled all infrastructure. Falling back to agentteams-embedded:latest would\n # silently swap in the v1.1.0 architecture (embedded kube-apiserver) which\n # crashes under QEMU on Apple Silicon. Auto-activate legacy mode instead.\n if _ver_lt \"${AGENTTEAMS_VERSION}\" \"v1.1.0\"; then\n log \"INFO: ${AGENTTEAMS_VERSION} predates agentteams-embedded; switching to legacy all-in-one manager architecture.\"\n log \"WARNING: Legacy all-in-one mode requires AGENTTEAMS_VERSION <= v1.0.9 (older bundled manager image).\"\n log \"WARNING: Newer slim manager images will hang on 'Waiting for Higress Gateway'.\"\n AGENTTEAMS_USE_EMBEDDED=0\n return 0\n fi\n\n if ${DOCKER_CMD} pull \"${_latest}\" >/dev/null 2>&1; then\n log \"embedded ${AGENTTEAMS_VERSION} not found, using latest\"\n EMBEDDED_IMAGE=\"${_latest}\"\n return 0\n fi\n\n # Explicit escape hatch — still honoured for edge cases.\n if [ \"${AGENTTEAMS_FORCE_LEGACY:-0}\" = \"1\" ]; then\n log \"WARNING: AGENTTEAMS_FORCE_LEGACY=1 — using legacy all-in-one manager architecture.\"\n log \"WARNING: This requires AGENTTEAMS_VERSION <= v1.0.9 (older bundled manager image).\"\n log \"WARNING: Newer slim manager images will hang on 'Waiting for Higress Gateway'.\"\n AGENTTEAMS_USE_EMBEDDED=0\n return 0\n fi\n\n error \"Embedded controller image is not available in the registry:\"\n error \" - tried: ${_versioned}\"\n error \" - tried: ${_latest}\"\n error \"\"\n error \"Embedded mode is the only supported architecture since PR #616.\"\n error \"How to resolve:\"\n error \" 1) Pin to a AGENTTEAMS_VERSION whose embedded image has been published, or\"\n error \" wait for the release pipeline to publish it.\"\n error \" 2) For a local build, run: make install-embedded\"\n error \" (builds and uses the local embedded image without touching the registry).\"\n error \" 3) Override with a custom image: AGENTTEAMS_INSTALL_EMBEDDED_IMAGE=... ./agentteams-install.sh\"\n exit 1\n}\n\n# ============================================================\n# Known models list — used to detect custom models during install\n# ============================================================\nKNOWN_MODELS=\"gpt-5.4 gpt-5.3-codex gpt-5-mini gpt-5-nano claude-opus-4-6 claude-sonnet-4-6 claude-haiku-4-5 qwen3.6-plus qwen3.5-plus deepseek-chat deepseek-reasoner kimi-k2.5 glm-5 MiniMax-M2.7 MiniMax-M2.7-highspeed MiniMax-M2.5\"\n\nis_known_model() {\n local model=\"$1\"\n for m in ${KNOWN_MODELS}; do\n [ \"${m}\" = \"${model}\" ] && return 0\n done\n return 1\n}\n\n# Prompt user for custom model parameters when model is not in the known list.\n# Sets: AGENTTEAMS_MODEL_CONTEXT_WINDOW, AGENTTEAMS_MODEL_MAX_TOKENS, AGENTTEAMS_MODEL_REASONING, AGENTTEAMS_MODEL_VISION\nprompt_custom_model_params() {\n local model=\"$1\"\n if is_known_model \"${model}\"; then\n # Clear any stale custom params for known models\n AGENTTEAMS_MODEL_CONTEXT_WINDOW=\"\"\n AGENTTEAMS_MODEL_MAX_TOKENS=\"\"\n AGENTTEAMS_MODEL_REASONING=\"\"\n AGENTTEAMS_MODEL_VISION=\"\"\n return\n fi\n echo \"\"\n log \"$(msg llm.custom_model.detected \"${model}\")\"\n echo \"\"\n read -e -p \" $(msg llm.custom_model.context_prompt): \" AGENTTEAMS_MODEL_CONTEXT_WINDOW\n if [ \"${AGENTTEAMS_MODEL_CONTEXT_WINDOW}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n AGENTTEAMS_MODEL_CONTEXT_WINDOW=\"${AGENTTEAMS_MODEL_CONTEXT_WINDOW:-150000}\"\n read -e -p \" $(msg llm.custom_model.max_tokens_prompt): \" AGENTTEAMS_MODEL_MAX_TOKENS\n if [ \"${AGENTTEAMS_MODEL_MAX_TOKENS}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n AGENTTEAMS_MODEL_MAX_TOKENS=\"${AGENTTEAMS_MODEL_MAX_TOKENS:-128000}\"\n read -e -p \" $(msg llm.custom_model.reasoning_prompt): \" _reasoning\n if [ \"${_reasoning}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n case \"${_reasoning}\" in\n n|N|no|NO) AGENTTEAMS_MODEL_REASONING=\"false\" ;;\n *) AGENTTEAMS_MODEL_REASONING=\"true\" ;;\n esac\n read -e -p \" $(msg llm.custom_model.vision_prompt): \" _vision\n if [ \"${_vision}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n case \"${_vision}\" in\n y|Y|yes|YES) AGENTTEAMS_MODEL_VISION=\"true\" ;;\n *) AGENTTEAMS_MODEL_VISION=\"false\" ;;\n esac\n log \"$(msg llm.custom_model.summary \"${AGENTTEAMS_MODEL_CONTEXT_WINDOW}\" \"${AGENTTEAMS_MODEL_MAX_TOKENS}\" \"${AGENTTEAMS_MODEL_REASONING}\" \"${AGENTTEAMS_MODEL_VISION}\")\"\n}\n\n# ============================================================\n# Wait for Manager agent to be ready\n# Uses `openclaw gateway health` inside the container to confirm the gateway is running\n# ============================================================\n\nwait_manager_ready() {\n local timeout=\"${AGENTTEAMS_READY_TIMEOUT:-300}\"\n local elapsed=0\n local container=\"${1:-agentteams-manager}\"\n\n log \"$(msg install.wait_ready \"${timeout}\")\"\n\n # Wait for Manager agent to be healthy inside the container\n local runtime=\"${AGENTTEAMS_MANAGER_RUNTIME:-copaw}\"\n while [ \"${elapsed}\" -lt \"${timeout}\" ]; do\n case \"${runtime}\" in\n copaw)\n if ${DOCKER_CMD} exec \"${container}\" curl -sf http://127.0.0.1:18799/api/agents 2>/dev/null | grep -q '\"agents\"'; then\n log \"$(msg install.wait_ready.ok)\"\n return 0\n fi\n ;;\n *)\n if ${DOCKER_CMD} exec \"${container}\" openclaw gateway health --json 2>/dev/null | grep -q '\"ok\"' 2>/dev/null; then\n log \"$(msg install.wait_ready.ok)\"\n return 0\n fi\n ;;\n esac\n sleep 5\n elapsed=$((elapsed + 5))\n printf \"\\r\\033[36m[AgentTeams]\\033[0m $(msg install.wait_ready.waiting \"${elapsed}\" \"${timeout}\")\"\n done\n\n echo \"\"\n die \"$(msg install.wait_ready.timeout \"${timeout}\" \"${container}\")\"\n}\n\nwait_matrix_ready() {\n local timeout=\"${AGENTTEAMS_READY_TIMEOUT:-300}\"\n local elapsed=0\n local container=\"${1:-agentteams-manager}\"\n\n log \"$(msg install.wait_matrix \"${timeout}\")\"\n\n while [ \"${elapsed}\" -lt \"${timeout}\" ]; do\n if ${DOCKER_CMD} exec \"${container}\" curl -sf http://127.0.0.1:6167/_tuwunel/server_version >/dev/null 2>&1; then\n log \"$(msg install.wait_matrix.ok)\"\n return 0\n fi\n sleep 5\n elapsed=$((elapsed + 5))\n printf \"\\r\\033[36m[AgentTeams]\\033[0m $(msg install.wait_matrix.waiting \"${elapsed}\" \"${timeout}\")\"\n done\n\n echo \"\"\n die \"$(msg install.wait_matrix.timeout \"${timeout}\" \"${container}\")\"\n}\n\n# Read KEY=value from /data/agentteams-secrets.env on a Docker volume (manager container not required).\n# Requires EMBEDDED_IMAGE (resolved earlier in install_manager). Uses ${DOCKER_CMD}.\nagentteams_read_secret_from_data_volume() {\n local _vol=\"$1\" _key=\"$2\"\n if [ -z \"${_vol}\" ] || [ -z \"${_key}\" ] || [ -z \"${EMBEDDED_IMAGE:-}\" ]; then\n echo \"\"\n return 0\n fi\n ${DOCKER_CMD} run --rm --entrypoint sh \\\n -v \"${_vol}:/data:ro\" \\\n \"${EMBEDDED_IMAGE}\" -c \"grep \\\"^${_key}=\\\" /data/agentteams-secrets.env 2>/dev/null | cut -d= -f2- | head -1 | tr -d '\\r'\" 2>/dev/null\n}\n\n# Read KEY=value from /data/worker-creds/<worker>.env on a Docker volume.\nagentteams_read_worker_creds_value_from_volume() {\n local _vol=\"$1\" _worker=\"$2\" _key=\"$3\"\n if [ -z \"${_vol}\" ] || [ -z \"${_worker}\" ] || [ -z \"${_key}\" ] || [ -z \"${EMBEDDED_IMAGE:-}\" ]; then\n echo \"\"\n return 0\n fi\n ${DOCKER_CMD} run --rm --entrypoint sh \\\n -v \"${_vol}:/data:ro\" \\\n \"${EMBEDDED_IMAGE}\" -c \"grep \\\"^${_key}=\\\" \\\"/data/worker-creds/${_worker}.env\\\" 2>/dev/null | cut -d= -f2- | head -1 | tr -d \\\"\\\\r\\\"\" 2>/dev/null\n}\n\n# Read admin_dm_room_id from host workspace state.json (fallback when Matrix API is unavailable).\nagentteams_read_admin_dm_room_from_workspace() {\n local _ws=\"$1\"\n local _f=\"${_ws}/state.json\"\n if [ ! -f \"${_f}\" ] || ! command -v jq >/dev/null 2>&1; then\n echo \"\"\n return 0\n fi\n jq -r '.admin_dm_room_id // empty | select(. != \"null\")' \"${_f}\" 2>/dev/null\n}\n\n# Read secret input with masked echo (shows * per keystroke, supports backspace)\n# Usage: read_secret \"prompt text: \"; value=\"${_RS_RESULT}\"\nread_secret() {\n local _rs_prompt=\"$1\"\n _RS_RESULT=\"\"\n local _rs_char=\"\"\n\n printf \"%s\" \"${_rs_prompt}\"\n\n while IFS= read -r -s -n 1 _rs_char; do\n if [[ -z \"${_rs_char}\" ]]; then\n break\n elif [[ \"${_rs_char}\" == $'\\177' ]] || [[ \"${_rs_char}\" == $'\\b' ]]; then\n if [ -n \"${_RS_RESULT}\" ]; then\n _RS_RESULT=\"${_RS_RESULT%?}\"\n printf \"\\b \\b\"\n fi\n else\n _RS_RESULT=\"${_RS_RESULT}${_rs_char}\"\n printf \"*\"\n fi\n done\n echo\n}\n\n# Load current parameter values from env file for upgrade mode display\nload_current_params_from_env() {\n local env_file=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n if [ -f \"${env_file}\" ]; then\n [ -z \"${AGENTTEAMS_LLM_PROVIDER:+x}\" ] && AGENTTEAMS_LLM_PROVIDER=\"$(grep '^AGENTTEAMS_LLM_PROVIDER=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_OPENAI_BASE_URL:+x}\" ] && AGENTTEAMS_OPENAI_BASE_URL=\"$(grep '^AGENTTEAMS_OPENAI_BASE_URL=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_DEFAULT_MODEL:+x}\" ] && AGENTTEAMS_DEFAULT_MODEL=\"$(grep '^AGENTTEAMS_DEFAULT_MODEL=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_EMBEDDING_MODEL:+x}\" ] && AGENTTEAMS_EMBEDDING_MODEL=\"$(grep '^AGENTTEAMS_EMBEDDING_MODEL=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_WORKSPACE_DIR:+x}\" ] && AGENTTEAMS_WORKSPACE_DIR=\"$(grep '^AGENTTEAMS_WORKSPACE_DIR=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_HOST_SHARE_DIR:+x}\" ] && AGENTTEAMS_HOST_SHARE_DIR=\"$(grep '^AGENTTEAMS_HOST_SHARE_DIR=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_PODMAN_AUTOSTART:+x}\" ] && AGENTTEAMS_PODMAN_AUTOSTART=\"$(grep '^AGENTTEAMS_PODMAN_AUTOSTART=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_DASHBOARD:+x}\" ] && AGENTTEAMS_DASHBOARD=\"$(grep '^AGENTTEAMS_DASHBOARD=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_DASHBOARD_VERSION:+x}\" ] && AGENTTEAMS_DASHBOARD_VERSION=\"$(grep '^AGENTTEAMS_DASHBOARD_VERSION=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_PORT_DASHBOARD:+x}\" ] && AGENTTEAMS_PORT_DASHBOARD=\"$(grep '^AGENTTEAMS_PORT_DASHBOARD=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_DASHBOARD_IMAGE:+x}\" ] && AGENTTEAMS_DASHBOARD_IMAGE=\"$(grep '^AGENTTEAMS_DASHBOARD_IMAGE=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL:+x}\" ] && AGENTTEAMS_AI_GATEWAY_ADMIN_URL=\"$(grep '^AGENTTEAMS_AI_GATEWAY_ADMIN_URL=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n fi\n}\n\n# In non-interactive mode, uses default or errors if required and no default.\n# Usage: prompt VAR_NAME \"Prompt text\" \"default\" [true=secret]\nprompt() {\n local var_name=\"$1\"\n local prompt_text=\"$2\"\n local default_value=\"$3\"\n local is_secret=\"${4:-false}\"\n\n # If the variable is already set in the environment, use it silently\n # In upgrade mode, show current value and let user change it\n eval \"local current_value=\\\"\\${${var_name}}\\\"\"\n if [ -n \"${current_value}\" ]; then\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_NON_INTERACTIVE}\" != \"1\" ]; then\n # Show masked value for secrets, full value otherwise\n local display_value=\"${current_value}\"\n if [ \"${is_secret}\" = \"true\" ]; then\n local len=${#current_value}\n if [ \"${len}\" -le 8 ]; then\n display_value=\"****\"\n else\n display_value=\"${current_value:0:4}****${current_value: -4}\"\n fi\n fi\n log \"$(msg prompt.upgrade_keep \"${prompt_text}\" \"${display_value}\")\"\n local new_value=\"\"\n if [ \"${is_secret}\" = \"true\" ]; then\n read_secret \"${prompt_text}: \"\n new_value=\"${_RS_RESULT}\"\n else\n read -e -p \"${prompt_text}: \" new_value\n if [ \"${new_value}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n fi\n if [ -n \"${new_value}\" ]; then\n eval \"export ${var_name}='${new_value}'\"\n fi\n return\n fi\n log \"$(msg prompt.preset \"${prompt_text}\")\"\n return\n fi\n\n # Non-interactive or quickstart: use default or error\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] || [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ]; then\n if [ -n \"${default_value}\" ]; then\n eval \"export ${var_name}='${default_value}'\"\n log \"$(msg prompt.default \"${prompt_text}\" \"${default_value}\")\"\n return\n elif [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n # Only hard-error in fully non-interactive mode, not quickstart\n die \"$(msg prompt.required \"${prompt_text}\")\"\n fi\n # quickstart + no default: fall through to interactive prompt below\n fi\n\n if [ -n \"${default_value}\" ]; then\n prompt_text=\"${prompt_text} [${default_value}]\"\n fi\n\n local value=\"\"\n if [ \"${is_secret}\" = \"true\" ]; then\n read_secret \"${prompt_text}: \"\n value=\"${_RS_RESULT}\"\n else\n read -e -p \"${prompt_text}: \" value\n if [ \"${value}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n fi\n\n value=\"${value:-${default_value}}\"\n if [ -z \"${value}\" ]; then\n die \"$(msg prompt.required_empty \"${prompt_text}\")\"\n fi\n\n eval \"export ${var_name}='${value}'\"\n}\n\n# Prompt for an optional value (empty string is acceptable)\n# Skips prompt if variable is already defined in environment (even if empty)\n# In upgrade mode, shows current value and lets user change it.\n# In non-interactive mode, defaults to empty string.\nprompt_optional() {\n local var_name=\"$1\"\n local prompt_text=\"$2\"\n local is_secret=\"${3:-false}\"\n\n # Check if variable is defined (even if set to empty string)\n eval \"local _chk=\\\"\\${${var_name}+x}\\\"\"\n if [ -n \"${_chk}\" ]; then\n # In upgrade mode, show current value and let user change it\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_NON_INTERACTIVE}\" != \"1\" ]; then\n eval \"local current_value=\\\"\\${${var_name}}\\\"\"\n local display_value=\"${current_value}\"\n if [ \"${is_secret}\" = \"true\" ] && [ -n \"${current_value}\" ]; then\n local len=${#current_value}\n if [ \"${len}\" -le 8 ]; then\n display_value=\"****\"\n else\n display_value=\"${current_value:0:4}****${current_value: -4}\"\n fi\n fi\n if [ -n \"${current_value}\" ]; then\n if [ \"${is_secret}\" = \"true\" ]; then\n log \"$(msg prompt.upgrade_keep_secret \"${prompt_text}\" \"${display_value}\")\"\n else\n log \"$(msg prompt.upgrade_keep \"${prompt_text}\" \"${display_value}\")\"\n fi\n else\n log \"$(msg prompt.upgrade_empty \"${prompt_text}\")\"\n fi\n local new_value=\"\"\n if [ \"${is_secret}\" = \"true\" ]; then\n read_secret \"${prompt_text}: \"\n new_value=\"${_RS_RESULT}\"\n else\n read -e -p \"${prompt_text}: \" new_value\n if [ \"${new_value}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n fi\n if [ -n \"${new_value}\" ]; then\n eval \"export ${var_name}='${new_value}'\"\n fi\n return\n fi\n log \"$(msg prompt.preset \"${prompt_text}\")\"\n return\n fi\n\n # Non-interactive or quickstart: skip, leave unset\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] || [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ]; then\n eval \"export ${var_name}=''\"\n return\n fi\n\n local value=\"\"\n if [ \"${is_secret}\" = \"true\" ]; then\n read_secret \"${prompt_text}: \"\n value=\"${_RS_RESULT}\"\n else\n read -e -p \"${prompt_text}: \" value\n if [ \"${value}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n fi\n\n eval \"export ${var_name}='${value}'\"\n}\n\ngenerate_key() {\n openssl rand -hex 32\n}\n\n# ============================================================\n# Detect container runtime socket on the host\n#\n# Dependency: Requires DOCKER_CMD to reflect the TRUE underlying runtime.\n# (Crucial for unmasking 'podman' disguised as 'docker'.\n# Must be strictly resolved prior, e.g., via check_container_runtime)\n# ============================================================\ndetect_socket() {\n local socket_path\n\n # 1. Respect explicitly defined DOCKER_HOST environment variable\n if [ -n \"${DOCKER_HOST}\" ]; then\n socket_path=$(echo \"${DOCKER_HOST}\" | sed 's|^unix://||')\n if [ -S \"${socket_path}\" ]; then\n echo \"${socket_path}\"\n return 0\n fi\n fi\n\n # 2. Route based on the resolved container runtime\n case \"${DOCKER_CMD}\" in\n docker)\n # Docker path handling\n socket_path=$(docker context ls --format '{{if .Current}}{{.DockerEndpoint}}{{end}}' 2>/dev/null | grep . | sed 's|^unix://||')\n if [ -n \"${socket_path}\" ] && [ -S \"${socket_path}\" ]; then\n echo \"${socket_path}\"\n return 0\n fi\n\n # Rootless Docker\n if [ -n \"${XDG_RUNTIME_DIR:-}\" ] && [ -S \"${XDG_RUNTIME_DIR}/docker.sock\" ]; then\n echo \"${XDG_RUNTIME_DIR}/docker.sock\"\n return 0\n fi\n\n # Rootless Docker (macOS fallback)\n if [ -S \"${HOME}/.docker/run/docker.sock\" ]; then\n echo \"${HOME}/.docker/run/docker.sock\"\n return 0\n fi\n\n # Root Docker\n if [ -S \"/var/run/docker.sock\" ]; then\n echo \"/var/run/docker.sock\"\n return 0\n fi\n ;;\n podman)\n # Podman path handling\n if [ \"$(id -u)\" -eq 0 ]; then\n socket_path=\"/run/podman/podman.sock\"\n else\n socket_path=\"${XDG_RUNTIME_DIR}/podman/podman.sock\"\n fi\n if [ -S \"${socket_path}\" ]; then\n echo \"${socket_path}\"\n return 0\n fi\n ;;\n esac\n\n # Return empty if no socket is found\n echo \"\"\n return 0\n}\n\n# Ensure Podman API socket is active (State mutation)\nensure_podman_socket() {\n local socket_path\n\n # Only applicable if runtime is podman and systemctl is available\n if [ \"${DOCKER_CMD:-}\" != \"podman\" ] || ! command -v systemctl >/dev/null 2>&1; then\n return 0\n fi\n\n echo \"Ensuring Podman API socket is active...\" >&2\n\n # Enable and start the socket based on user privileges\n if [ \"$(id -u)\" -eq 0 ]; then\n socket_path=\"/run/podman/podman.sock\"\n systemctl enable --now podman.socket >/dev/null 2>&1 || true\n else\n socket_path=\"${XDG_RUNTIME_DIR}/podman/podman.sock\"\n systemctl --user enable --now podman.socket >/dev/null 2>&1 || true\n fi\n\n # Give systemd a brief moment to assert the socket for root or rootless user\n local _i\n for _i in 1 2 3; do\n [ -S \"${socket_path}\" ] && break\n sleep 1\n done\n}\n\n# Detect local LAN IP address (cross-platform: macOS and Linux)\ndetect_lan_ip() {\n local ip=\"\"\n\n # macOS: try common Wi-Fi / Ethernet interfaces\n if command -v ipconfig >/dev/null 2>&1; then\n for iface in en0 en1 en2 en3 en4; do\n ip=$(ipconfig getifaddr \"${iface}\" 2>/dev/null)\n if [ -n \"${ip}\" ]; then\n echo \"${ip}\"\n return 0\n fi\n done\n fi\n\n # Linux: ip route — most reliable\n if command -v ip >/dev/null 2>&1; then\n ip=$(ip route get 1.1.1.1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i==\"src\"){print $(i+1); exit}}')\n if [ -n \"${ip}\" ]; then\n echo \"${ip}\"\n return 0\n fi\n fi\n\n # Linux fallback: hostname -I (space-separated list, take first non-loopback)\n if command -v hostname >/dev/null 2>&1; then\n ip=$(hostname -I 2>/dev/null | tr ' ' '\\n' | grep -v '^127\\.' | grep -v '^::' | head -1)\n if [ -n \"${ip}\" ]; then\n echo \"${ip}\"\n return 0\n fi\n fi\n\n # Last resort: ifconfig\n if command -v ifconfig >/dev/null 2>&1; then\n ip=$(ifconfig 2>/dev/null | awk '/inet /{if($2!~/^127\\./){print $2; exit}}')\n # Strip \"addr:\" prefix that some ifconfig versions add\n ip=\"${ip#addr:}\"\n if [ -n \"${ip}\" ]; then\n echo \"${ip}\"\n return 0\n fi\n fi\n\n echo \"\"\n}\n\n\n# ============================================================\n# Step-back navigation helpers\n# ============================================================\n\n# should_skip_step: returns 0 (skip) when the step is irrelevant in current mode\nshould_skip_step() {\n local step_fn=\"$1\"\n case \"${step_fn}\" in\n step_lang|step_mode)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n ;;\n step_version)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n ;;\n step_existing)\n local _env=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n [ ! -f \"${_env}\" ] && return 0\n ;;\n # Keep-All upgrade mode: skip all config steps (step_volume/step_workspace handled separately)\n step_llm|step_admin|step_network|step_ports|step_domains|step_github|step_skills|step_runtime|step_manager_runtime|step_dashboard|step_e2ee|step_docker_proxy|step_idle|step_hostshare)\n [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ] && return 0\n ;;\n step_volume|step_workspace)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ] && return 0\n [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ] && return 0\n ;;\n step_e2ee|step_idle)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE}\" != \"1\" ] && return 0\n ;;\n step_docker_proxy)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE}\" != \"1\" ] && return 0\n # Embedded mode handles docker access natively — skip this step\n [ \"${AGENTTEAMS_USE_EMBEDDED:-}\" = \"1\" ] && return 0\n ;;\n step_manager_runtime)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n ;;\n step_hostshare)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ] && return 0\n ;;\n step_podman_autostart)\n # Only relevant for Podman with systemd\n [ \"${DOCKER_CMD}\" != \"podman\" ] && return 0\n ! command -v systemctl >/dev/null 2>&1 && return 0\n\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE}\" != \"1\" ] && return 0\n [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ] && return 0\n ;;\n esac\n return 1\n}\n\n# clear_step_vars: unset variables set by a step so it will re-prompt on re-entry\nclear_step_vars() {\n local step_fn=\"$1\"\n case \"${step_fn}\" in\n step_mode) unset AGENTTEAMS_QUICKSTART ;;\n step_version) unset AGENTTEAMS_VERSION ;;\n step_existing) unset AGENTTEAMS_UPGRADE UPGRADE_EXISTING_WORKERS ;;\n step_llm)\n unset AGENTTEAMS_LLM_PROVIDER AGENTTEAMS_DEFAULT_MODEL AGENTTEAMS_OPENAI_BASE_URL\n unset AGENTTEAMS_LLM_API_KEY AGENTTEAMS_MODEL_CONTEXT_WINDOW AGENTTEAMS_MODEL_MAX_TOKENS\n unset AGENTTEAMS_MODEL_REASONING AGENTTEAMS_MODEL_VISION\n ;;\n step_admin) unset AGENTTEAMS_ADMIN_USER AGENTTEAMS_ADMIN_PASSWORD ;;\n step_network) unset AGENTTEAMS_LOCAL_ONLY ;;\n step_ports)\n unset AGENTTEAMS_PORT_GATEWAY AGENTTEAMS_PORT_CONSOLE\n unset AGENTTEAMS_PORT_ELEMENT_WEB AGENTTEAMS_PORT_MANAGER_CONSOLE\n ;;\n step_domains)\n unset AGENTTEAMS_MATRIX_DOMAIN AGENTTEAMS_MATRIX_CLIENT_DOMAIN\n unset AGENTTEAMS_AI_GATEWAY_DOMAIN AGENTTEAMS_FS_DOMAIN AGENTTEAMS_CONSOLE_DOMAIN\n ;;\n step_github) unset AGENTTEAMS_GITHUB_TOKEN ;;\n step_skills) unset AGENTTEAMS_SKILLS_API_URL ;;\n step_volume) unset AGENTTEAMS_DATA_DIR ;;\n step_workspace) unset AGENTTEAMS_WORKSPACE_DIR ;;\n step_dashboard) unset AGENTTEAMS_DASHBOARD AGENTTEAMS_DASHBOARD_VERSION AGENTTEAMS_PORT_DASHBOARD AGENTTEAMS_DASHBOARD_IMAGE AGENTTEAMS_AI_GATEWAY_ADMIN_URL ;;\n step_runtime) unset AGENTTEAMS_DEFAULT_WORKER_RUNTIME ;;\n step_manager_runtime) unset AGENTTEAMS_MANAGER_RUNTIME ;;\n step_e2ee) unset AGENTTEAMS_MATRIX_E2EE ;;\n step_docker_proxy) unset AGENTTEAMS_DOCKER_PROXY; unset AGENTTEAMS_PROXY_ALLOWED_REGISTRIES ;;\n step_idle) unset AGENTTEAMS_WORKER_IDLE_TIMEOUT ;;\n step_hostshare) unset AGENTTEAMS_HOST_SHARE_DIR ;;\n step_podman_autostart) unset AGENTTEAMS_PODMAN_AUTOSTART ;;\n esac\n}\n\n# ============================================================\n# Individual step functions\n# ============================================================\n\nstep_lang() {\n local lang_default_choice=\"2\"\n [ \"${AGENTTEAMS_LANGUAGE}\" = \"zh\" ] && lang_default_choice=\"1\"\n log \"$(msg lang.detected)\"\n log \"$(msg lang.switch_title)\"\n echo \"$(msg lang.option_zh)\"\n echo \"$(msg lang.option_en)\"\n echo \"\"\n local LANG_CHOICE\n read -e -p \"$(msg lang.prompt) [${lang_default_choice}]: \" LANG_CHOICE\n LANG_CHOICE=\"${LANG_CHOICE:-${lang_default_choice}}\"\n if [ \"${LANG_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n case \"${LANG_CHOICE}\" in\n 1) AGENTTEAMS_LANGUAGE=\"zh\" ;;\n 2) AGENTTEAMS_LANGUAGE=\"en\" ;;\n esac\n export AGENTTEAMS_LANGUAGE\n log \"\"\n}\n\nstep_mode() {\n log \"$(msg install.mode.title)\"\n echo \"\"\n echo \"$(msg install.mode.choose)\"\n echo \"$(msg install.mode.quickstart)\"\n echo \"$(msg install.mode.manual)\"\n echo \"\"\n local ONBOARDING_CHOICE\n read -e -p \"$(msg install.mode.prompt): \" ONBOARDING_CHOICE\n ONBOARDING_CHOICE=\"${ONBOARDING_CHOICE:-1}\"\n if [ \"${ONBOARDING_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n case \"${ONBOARDING_CHOICE}\" in\n 1|quick|quickstart)\n log \"$(msg install.mode.quickstart_selected)\"\n AGENTTEAMS_QUICKSTART=1\n ;;\n 2|manual)\n log \"$(msg install.mode.manual_selected)\"\n ;;\n *)\n log \"$(msg install.mode.invalid)\"\n AGENTTEAMS_QUICKSTART=1\n ;;\n esac\n log \"\"\n}\n\nstep_version() {\n # Skip if version already provided via env var\n if [ -n \"${AGENTTEAMS_VERSION}\" ]; then\n resolve_image_tags\n return 0\n fi\n # Try to fetch the latest stable release from GitHub\n log \"$(msg install.version.fetching)\"\n local _fetched\n _fetched=$(curl -sf --max-time 5 \\\n -H \"Accept: application/vnd.github+json\" \\\n \"https://api.github.com/repos/agentscope-ai/AgentTeams/releases/latest\" \\\n 2>/dev/null | grep '\"tag_name\"' | head -1 | sed 's/.*\"tag_name\": *\"\\([^\"]*\\)\".*/\\1/')\n if [ -n \"${_fetched}\" ]; then\n AGENTTEAMS_KNOWN_STABLE_VERSION=\"${_fetched}\"\n else\n log \"$(msg install.version.fetch_failed \"${AGENTTEAMS_KNOWN_STABLE_VERSION}\")\"\n fi\n log \"$(msg install.version.title)\"\n echo \"\"\n echo \"$(msg install.version.choose)\"\n echo \"$(msg install.version.option_latest)\"\n printf \"%s\\n\" \"$(msg install.version.option_stable \"${AGENTTEAMS_KNOWN_STABLE_VERSION}\")\"\n echo \"$(msg install.version.option_custom)\"\n echo \"\"\n local VERSION_CHOICE\n read -e -p \"$(msg install.version.prompt) [1]: \" VERSION_CHOICE\n VERSION_CHOICE=\"${VERSION_CHOICE:-1}\"\n if [ \"${VERSION_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n case \"${VERSION_CHOICE}\" in\n 1|latest)\n AGENTTEAMS_VERSION=\"latest\"\n log \"$(msg install.version.selected_latest)\"\n ;;\n 2|stable)\n AGENTTEAMS_VERSION=\"${AGENTTEAMS_KNOWN_STABLE_VERSION}\"\n log \"$(msg install.version.selected_stable \"${AGENTTEAMS_VERSION}\")\"\n ;;\n 3|custom)\n local CUSTOM_VERSION\n read -e -p \"$(msg install.version.custom_prompt): \" CUSTOM_VERSION\n AGENTTEAMS_VERSION=\"${CUSTOM_VERSION:-${AGENTTEAMS_KNOWN_STABLE_VERSION}}\"\n log \"$(msg install.version.selected_custom \"${AGENTTEAMS_VERSION}\")\"\n ;;\n *)\n AGENTTEAMS_VERSION=\"${AGENTTEAMS_KNOWN_STABLE_VERSION}\"\n log \"$(msg install.version.invalid \"${AGENTTEAMS_VERSION}\")\"\n ;;\n esac\n log \"\"\n resolve_image_tags\n}\n\nstep_existing() {\n local existing_env=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n log \"$(msg install.existing.detected \"${existing_env}\")\"\n local running_manager=\"\" running_workers=\"\" existing_workers=\"\"\n if ${DOCKER_CMD} ps --format '{{.Names}}' | grep -q \"^agentteams-manager$\"; then\n running_manager=\"agentteams-manager\"\n fi\n running_workers=$(${DOCKER_CMD} ps --format '{{.Names}}' | grep \"^agentteams-worker-\" || true)\n existing_workers=$(${DOCKER_CMD} ps -a --format '{{.Names}}' | grep \"^agentteams-worker-\" || true)\n local UPGRADE_CHOICE\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n log \"$(msg install.existing.upgrade_noninteractive)\"\n UPGRADE_CHOICE=\"upgrade\"\n else\n echo \"\"\n echo \"$(msg install.existing.choose)\"\n echo \"$(msg install.existing.upgrade)\"\n echo \"$(msg install.existing.reinstall)\"\n echo \"$(msg install.existing.cancel)\"\n echo \"\"\n read -e -p \"$(msg install.existing.prompt): \" UPGRADE_CHOICE\n UPGRADE_CHOICE=\"${UPGRADE_CHOICE:-1}\"\n if [ \"${UPGRADE_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n fi\n case \"${UPGRADE_CHOICE}\" in\n 1|upgrade)\n AGENTTEAMS_UPGRADE=1\n log \"$(msg install.existing.upgrading)\"\n # Show upgrade mode sub-menu (unless already set via env var)\n if [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL:-0}\" != \"1\" ]; then\n echo \"\"\n echo \"$(msg upgrade.mode.prompt)\"\n echo \"$(msg upgrade.mode.keep_all)\"\n echo \"$(msg upgrade.mode.confirm_each)\"\n echo \"$(msg upgrade.mode.back)\"\n echo \"\"\n local UPGRADE_MODE_CHOICE\n read -e -p \"$(msg install.existing.prompt): \" UPGRADE_MODE_CHOICE\n UPGRADE_MODE_CHOICE=\"${UPGRADE_MODE_CHOICE:-1}\"\n case \"${UPGRADE_MODE_CHOICE}\" in\n 1|keep)\n AGENTTEAMS_UPGRADE_KEEP_ALL=1\n ;;\n 2|confirm)\n AGENTTEAMS_UPGRADE_KEEP_ALL=0\n ;;\n 3|b)\n STEP_RESULT=\"back\"\n return 0\n ;;\n *)\n AGENTTEAMS_UPGRADE_KEEP_ALL=0\n ;;\n esac\n fi\n # Load current parameters for both Keep-All and confirm-each modes\n load_current_params_from_env\n if [ -n \"${running_manager}\" ] || [ -n \"${running_workers}\" ]; then\n echo \"\"\n echo -e \"\\033[33m$(msg install.existing.warn_manager_stop)\\033[0m\"\n if [ -n \"${existing_workers}\" ]; then\n echo -e \"\\033[33m$(msg install.existing.warn_worker_recreate)\\033[0m\"\n fi\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" != \"1\" ]; then\n echo \"\"\n local CONFIRM_STOP\n read -e -p \"$(msg install.existing.continue_prompt): \" CONFIRM_STOP\n if [ \"${CONFIRM_STOP}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n if [ \"${CONFIRM_STOP}\" != \"y\" ] && [ \"${CONFIRM_STOP}\" != \"Y\" ]; then\n log \"$(msg install.existing.cancelled)\"\n exit 0\n fi\n fi\n fi\n UPGRADE_EXISTING_WORKERS=\"${existing_workers}\"\n ;;\n 2|reinstall)\n log \"$(msg install.reinstall.performing)\"\n local existing_workspace=\"\"\n if [ -f \"${existing_env}\" ]; then\n existing_workspace=$(grep '^AGENTTEAMS_WORKSPACE_DIR=' \"${existing_env}\" 2>/dev/null | cut -d= -f2-)\n fi\n [ -z \"${existing_workspace}\" ] && existing_workspace=\"${HOME}/agentteams-manager\"\n echo \"\"\n echo -e \"\\033[33m$(msg install.reinstall.warn_stop)\\033[0m\"\n [ -n \"${running_manager}\" ] && echo -e \"\\033[33m - ${running_manager} (manager)\\033[0m\"\n for w in ${running_workers}; do\n echo -e \"\\033[33m - ${w} (worker)\\033[0m\"\n done\n echo \"\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_delete)\\033[0m\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_volume)\\033[0m\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_env \"${existing_env}\")\\033[0m\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_workspace \"${existing_workspace}\")\\033[0m\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_workers)\\033[0m\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_proxy)\\033[0m\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_network)\\033[0m\"\n echo \"\"\n echo -e \"\\033[31m$(msg install.reinstall.confirm_type)\\033[0m\"\n echo -e \"\\033[31m ${existing_workspace}\\033[0m\"\n echo \"\"\n local CONFIRM_PATH\n read -e -p \"$(msg install.reinstall.confirm_path): \" CONFIRM_PATH\n if [ \"${CONFIRM_PATH}\" != \"${existing_workspace}\" ]; then\n die \"$(msg install.reinstall.path_mismatch \"${CONFIRM_PATH}\" \"${existing_workspace}\")\"\n fi\n log \"$(msg install.reinstall.confirmed)\"\n ${DOCKER_CMD} stop agentteams-manager 2>/dev/null || true\n ${DOCKER_CMD} rm agentteams-manager 2>/dev/null || true\n for w in $(${DOCKER_CMD} ps -a --format '{{.Names}}' | grep \"^agentteams-worker-\" || true); do\n ${DOCKER_CMD} stop \"${w}\" 2>/dev/null || true\n ${DOCKER_CMD} rm \"${w}\" 2>/dev/null || true\n log \"$(msg install.reinstall.removed_worker \"${w}\")\"\n done\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' | grep -q \"^agentteams-controller$\"; then\n log \"$(msg install.reinstall.removing_proxy)\"\n ${DOCKER_CMD} stop agentteams-controller 2>/dev/null || true\n ${DOCKER_CMD} rm agentteams-controller 2>/dev/null || true\n fi\n if ${DOCKER_CMD} network ls --format '{{.Name}}' | grep -q \"^agentteams-net$\"; then\n log \"$(msg install.reinstall.removing_network)\"\n ${DOCKER_CMD} network rm agentteams-net 2>/dev/null || true\n fi\n if ${DOCKER_CMD} volume ls -q | grep -q \"^agentteams-data$\"; then\n log \"$(msg install.reinstall.removing_volume)\"\n ${DOCKER_CMD} volume rm agentteams-data 2>/dev/null || log \"$(msg install.reinstall.warn_volume_fail)\"\n fi\n if [ -d \"${existing_workspace}\" ]; then\n log \"$(msg install.reinstall.removing_workspace \"${existing_workspace}\")\"\n rm -rf \"${existing_workspace}\" || die \"$(msg install.reinstall.failed_rm_workspace)\"\n fi\n if [ -f \"${existing_env}\" ]; then\n log \"$(msg install.reinstall.removing_env \"${existing_env}\")\"\n rm -f \"${existing_env}\"\n fi\n log \"$(msg install.reinstall.cleanup_done)\"\n unset AGENTTEAMS_WORKSPACE_DIR\n return 0\n ;;\n 3|cancel|*)\n log \"$(msg install.existing.cancelled)\"\n exit 0\n ;;\n esac\n # Load existing env file as fallback (shell env vars take priority)\n if [ -f \"${existing_env}\" ]; then\n log \"$(msg install.loading_config \"${existing_env}\")\"\n while IFS='=' read -r key value; do\n case \"${key}\" in \\#*|\"\") continue ;; esac\n value=\"${value%%#*}\"\n value=\"${value#\"${value%%[![:space:]]*}\"}\"\n value=\"${value%\"${value##*[![:space:]]}\"}\"\n eval \"_existing_val=\\\"\\${${key}+x}\\\"\"\n if [ -z \"${_existing_val}\" ]; then export \"${key}=${value}\"; fi\n done < \"${existing_env}\"\n fi\n}\n\nstep_llm() {\n log \"$(msg llm.title)\"\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n if [ \"${AGENTTEAMS_LANGUAGE}\" = \"zh\" ]; then\n AGENTTEAMS_LLM_PROVIDER=\"${AGENTTEAMS_LLM_PROVIDER:-openai-compat}\"\n AGENTTEAMS_DEFAULT_MODEL=\"${AGENTTEAMS_DEFAULT_MODEL:-qwen3.6-plus}\"\n AGENTTEAMS_OPENAI_BASE_URL=\"${AGENTTEAMS_OPENAI_BASE_URL:-https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1}\"\n log \"$(msg llm.provider.label \"${AGENTTEAMS_LLM_PROVIDER}\")\"\n log \"$(msg llm.openai.base_url_label \"${AGENTTEAMS_OPENAI_BASE_URL}\")\"\n else\n AGENTTEAMS_LLM_PROVIDER=\"${AGENTTEAMS_LLM_PROVIDER:-qwen}\"\n AGENTTEAMS_DEFAULT_MODEL=\"${AGENTTEAMS_DEFAULT_MODEL:-qwen3.6-plus}\"\n AGENTTEAMS_OPENAI_BASE_URL=\"${AGENTTEAMS_OPENAI_BASE_URL:-}\"\n log \"$(msg llm.provider.qwen_default \"${AGENTTEAMS_LLM_PROVIDER}\")\"\n fi\n log \"$(msg llm.model.default \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n prompt AGENTTEAMS_LLM_API_KEY \"$(msg llm.apikey_prompt)\" \"\" \"true\"\n AGENTTEAMS_EMBEDDING_MODEL=\"${AGENTTEAMS_EMBEDDING_MODEL-text-embedding-v4}\"\n export AGENTTEAMS_LLM_PROVIDER AGENTTEAMS_DEFAULT_MODEL AGENTTEAMS_EMBEDDING_MODEL\n [ -n \"${AGENTTEAMS_OPENAI_BASE_URL+x}\" ] && export AGENTTEAMS_OPENAI_BASE_URL\n return 0\n fi\n\n # Upgrade Keep-All mode: skip provider selection, use loaded values\n local SKIP_TO_EMBEDDING=0\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ] && [ -n \"${AGENTTEAMS_LLM_PROVIDER}\" ]; then\n log \"$(msg llm.provider.label \"${AGENTTEAMS_LLM_PROVIDER}\")\"\n log \"$(msg llm.openai.base_url_label \"${AGENTTEAMS_OPENAI_BASE_URL}\")\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n PROVIDER_CHOICE=\"${AGENTTEAMS_LLM_PROVIDER}\"\n SKIP_TO_EMBEDDING=1\n else\n echo \"\"\n echo \"$(msg llm.providers_title)\"\n echo \"$(msg llm.provider.alibaba)\"\n echo \"$(msg llm.provider.openai_compat)\"\n echo \"\"\n local PROVIDER_CHOICE\n # If upgrade mode with loaded provider, show current as default\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_LLM_PROVIDER}\" ]; then\n local _prov_display\n case \"${AGENTTEAMS_LLM_PROVIDER}\" in\n openai-compat) _prov_display=\"2\" ;;\n *) _prov_display=\"1\" ;;\n esac\n read -e -p \"$(msg llm.provider.select) [${_prov_display}]: \" PROVIDER_CHOICE\n PROVIDER_CHOICE=\"${PROVIDER_CHOICE:-${_prov_display}}\"\n elif [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ]; then\n read -e -p \"$(msg llm.provider.select) [1]: \" PROVIDER_CHOICE\n PROVIDER_CHOICE=\"${PROVIDER_CHOICE:-1}\"\n else\n read -e -p \"$(msg llm.provider.select): \" PROVIDER_CHOICE\n PROVIDER_CHOICE=\"${PROVIDER_CHOICE:-1}\"\n fi\n if [ \"${PROVIDER_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n fi\n local ALIBABA_MODEL_CHOICE=\"\"\n local ALIBABA_ACCESS=\"\"\n case \"${PROVIDER_CHOICE}\" in\n 1|alibaba-cloud|openai-compat)\n if [ \"${AGENTTEAMS_LANGUAGE}\" = \"en\" ]; then\n AGENTTEAMS_LLM_PROVIDER=\"openai-compat\"\n AGENTTEAMS_OPENAI_BASE_URL=\"https://dashscope-intl.aliyuncs.com/compatible-mode/v1\"\n ALIBABA_ACCESS=\"tokenplan\"\n echo \"\"\n echo \"$(msg llm.codingplan.models_title)\"\n echo \"$(msg llm.codingplan.model.qwen36plus)\"\n echo \"$(msg llm.codingplan.model.glm5)\"\n echo \"$(msg llm.codingplan.model.kimi)\"\n echo \"$(msg llm.codingplan.model.minimax)\"\n echo \"\"\n local CODINGPLAN_MODEL_CHOICE\n # If upgrade with loaded model, show current as default\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_DEFAULT_MODEL}\" ]; then\n local _model_default\n case \"${AGENTTEAMS_DEFAULT_MODEL}\" in\n qwen3.6-plus) _model_default=\"1\" ;;\n glm-5) _model_default=\"2\" ;;\n kimi-k2.5) _model_default=\"3\" ;;\n MiniMax-M2.5) _model_default=\"4\" ;;\n *) _model_default=\"1\" ;;\n esac\n read -e -p \"$(msg llm.codingplan.model.select) [${_model_default}]: \" CODINGPLAN_MODEL_CHOICE\n CODINGPLAN_MODEL_CHOICE=\"${CODINGPLAN_MODEL_CHOICE:-${_model_default}}\"\n elif [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ]; then\n read -e -p \"$(msg llm.codingplan.model.select) [1]: \" CODINGPLAN_MODEL_CHOICE\n CODINGPLAN_MODEL_CHOICE=\"${CODINGPLAN_MODEL_CHOICE:-1}\"\n else\n read -e -p \"$(msg llm.codingplan.model.select): \" CODINGPLAN_MODEL_CHOICE\n CODINGPLAN_MODEL_CHOICE=\"${CODINGPLAN_MODEL_CHOICE:-1}\"\n fi\n if [ \"${CODINGPLAN_MODEL_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n case \"${CODINGPLAN_MODEL_CHOICE}\" in\n 1|qwen3.6-plus) AGENTTEAMS_DEFAULT_MODEL=\"qwen3.6-plus\" ;;\n 2|glm-5) AGENTTEAMS_DEFAULT_MODEL=\"glm-5\" ;;\n 3|kimi-k2.5) AGENTTEAMS_DEFAULT_MODEL=\"kimi-k2.5\" ;;\n 4|MiniMax-M2.5) AGENTTEAMS_DEFAULT_MODEL=\"MiniMax-M2.5\" ;;\n *) AGENTTEAMS_DEFAULT_MODEL=\"qwen3.6-plus\" ;;\n esac\n log \"$(msg llm.provider.selected_codingplan)\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n else\n echo \"\"\n echo \"$(msg llm.alibaba.models_title)\"\n echo \"$(msg llm.alibaba.model.tokenplan)\"\n echo \"$(msg llm.alibaba.model.bailian)\"\n echo \"$(msg llm.alibaba.model.codingplan_legacy)\"\n echo \"\"\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_OPENAI_BASE_URL}\" ]; then\n # Determine alibaba access type from loaded URL\n local _access_default=\"1\"\n if echo \"${AGENTTEAMS_OPENAI_BASE_URL}\" | grep -q \"token-plan\"; then\n _access_default=\"1\"\n elif echo \"${AGENTTEAMS_OPENAI_BASE_URL}\" | grep -q \"dashscope\"; then\n if echo \"${AGENTTEAMS_OPENAI_BASE_URL}\" | grep -q \"intl\"; then\n _access_default=\"3\"\n else\n _access_default=\"2\"\n fi\n fi\n read -e -p \"$(msg llm.alibaba.model.select) [${_access_default}]: \" ALIBABA_MODEL_CHOICE\n ALIBABA_MODEL_CHOICE=\"${ALIBABA_MODEL_CHOICE:-${_access_default}}\"\n elif [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ]; then\n read -e -p \"$(msg llm.alibaba.model.select) [1]: \" ALIBABA_MODEL_CHOICE\n ALIBABA_MODEL_CHOICE=\"${ALIBABA_MODEL_CHOICE:-1}\"\n else\n read -e -p \"$(msg llm.alibaba.model.select): \" ALIBABA_MODEL_CHOICE\n ALIBABA_MODEL_CHOICE=\"${ALIBABA_MODEL_CHOICE:-1}\"\n fi\n if [ \"${ALIBABA_MODEL_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n case \"${ALIBABA_MODEL_CHOICE}\" in\n 1|token-plan|tokenplan)\n ALIBABA_ACCESS=\"tokenplan\"\n AGENTTEAMS_LLM_PROVIDER=\"openai-compat\"\n AGENTTEAMS_OPENAI_BASE_URL=\"https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1\"\n echo \"\"\n echo \"$(msg llm.codingplan.models_title)\"\n echo \"$(msg llm.codingplan.model.qwen36plus)\"\n echo \"$(msg llm.codingplan.model.glm5)\"\n echo \"$(msg llm.codingplan.model.kimi)\"\n echo \"$(msg llm.codingplan.model.minimax)\"\n echo \"\"\n local CODINGPLAN_MODEL_CHOICE\n # If upgrade with loaded model, show current as default\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_DEFAULT_MODEL}\" ]; then\n local _model_default\n case \"${AGENTTEAMS_DEFAULT_MODEL}\" in\n qwen3.6-plus) _model_default=\"1\" ;;\n glm-5) _model_default=\"2\" ;;\n kimi-k2.5) _model_default=\"3\" ;;\n MiniMax-M2.5) _model_default=\"4\" ;;\n *) _model_default=\"1\" ;;\n esac\n read -e -p \"$(msg llm.codingplan.model.select) [${_model_default}]: \" CODINGPLAN_MODEL_CHOICE\n CODINGPLAN_MODEL_CHOICE=\"${CODINGPLAN_MODEL_CHOICE:-${_model_default}}\"\n elif [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ]; then\n read -e -p \"$(msg llm.codingplan.model.select) [1]: \" CODINGPLAN_MODEL_CHOICE\n CODINGPLAN_MODEL_CHOICE=\"${CODINGPLAN_MODEL_CHOICE:-1}\"\n else\n read -e -p \"$(msg llm.codingplan.model.select): \" CODINGPLAN_MODEL_CHOICE\n CODINGPLAN_MODEL_CHOICE=\"${CODINGPLAN_MODEL_CHOICE:-1}\"\n fi\n if [ \"${CODINGPLAN_MODEL_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n case \"${CODINGPLAN_MODEL_CHOICE}\" in\n 1|qwen3.6-plus) AGENTTEAMS_DEFAULT_MODEL=\"qwen3.6-plus\" ;;\n 2|glm-5) AGENTTEAMS_DEFAULT_MODEL=\"glm-5\" ;;\n 3|kimi-k2.5) AGENTTEAMS_DEFAULT_MODEL=\"kimi-k2.5\" ;;\n 4|MiniMax-M2.5) AGENTTEAMS_DEFAULT_MODEL=\"MiniMax-M2.5\" ;;\n *) AGENTTEAMS_DEFAULT_MODEL=\"qwen3.6-plus\" ;;\n esac\n log \"$(msg llm.provider.selected_tokenplan)\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n ;;\n 2|qwen|bailian)\n ALIBABA_ACCESS=\"bailian\"\n AGENTTEAMS_LLM_PROVIDER=\"qwen\"\n AGENTTEAMS_OPENAI_BASE_URL=\"\"\n echo \"\"\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_DEFAULT_MODEL}\" ]; then\n read -e -p \"$(msg llm.qwen.model_prompt) [${AGENTTEAMS_DEFAULT_MODEL}]: \" AGENTTEAMS_DEFAULT_MODEL\n else\n read -e -p \"$(msg llm.qwen.model_prompt) [qwen3.6-plus]: \" AGENTTEAMS_DEFAULT_MODEL\n fi\n if [ \"${AGENTTEAMS_DEFAULT_MODEL}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_DEFAULT_MODEL=\"${AGENTTEAMS_DEFAULT_MODEL:-qwen3.6-plus}\"\n log \"$(msg llm.provider.selected_qwen)\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n prompt_custom_model_params \"${AGENTTEAMS_DEFAULT_MODEL}\" || return 0\n ;;\n 3|coding-plan|codingplan)\n ALIBABA_ACCESS=\"codingplan_legacy\"\n AGENTTEAMS_LLM_PROVIDER=\"openai-compat\"\n AGENTTEAMS_OPENAI_BASE_URL=\"https://coding.dashscope.aliyuncs.com/v1\"\n echo \"\"\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_DEFAULT_MODEL}\" ]; then\n read -e -p \"$(msg llm.qwen.model_prompt) [${AGENTTEAMS_DEFAULT_MODEL}]: \" AGENTTEAMS_DEFAULT_MODEL\n else\n read -e -p \"$(msg llm.qwen.model_prompt): \" AGENTTEAMS_DEFAULT_MODEL\n fi\n if [ \"${AGENTTEAMS_DEFAULT_MODEL}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_DEFAULT_MODEL=\"${AGENTTEAMS_DEFAULT_MODEL:-qwen3.6-plus}\"\n log \"$(msg llm.provider.selected_codingplan_legacy)\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n prompt_custom_model_params \"${AGENTTEAMS_DEFAULT_MODEL}\" || return 0\n ;;\n *)\n die \"$(msg llm.alibaba.model.invalid \"${ALIBABA_MODEL_CHOICE}\")\"\n ;;\n esac\n fi\n if [ \"${ALIBABA_ACCESS}\" = \"bailian\" ]; then\n log \"$(msg llm.apikey_hint_bailian)\"\n log \"$(msg llm.apikey_url_bailian)\"\n elif [ \"${AGENTTEAMS_LANGUAGE}\" = \"en\" ]; then\n log \"$(msg llm.apikey_hint_qwencloud)\"\n log \"$(msg llm.apikey_url_qwencloud)\"\n elif [ \"${ALIBABA_ACCESS}\" = \"codingplan_legacy\" ]; then\n log \"$(msg llm.apikey_hint_codingplan)\"\n log \"$(msg llm.apikey_url_codingplan)\"\n else\n log \"$(msg llm.apikey_hint_tokenplan)\"\n log \"$(msg llm.apikey_url_tokenplan)\"\n fi\n log \"\"\n prompt AGENTTEAMS_LLM_API_KEY \"$(msg llm.apikey_prompt)\" \"\" \"true\" || return 0\n if [ \"${ALIBABA_ACCESS}\" = \"bailian\" ]; then\n test_llm_connectivity \"https://dashscope.aliyuncs.com/compatible-mode/v1\" \"${AGENTTEAMS_LLM_API_KEY}\" \"${AGENTTEAMS_DEFAULT_MODEL}\" || return 0\n elif [ \"${ALIBABA_ACCESS}\" = \"codingplan_legacy\" ]; then\n test_llm_connectivity \"https://coding.dashscope.aliyuncs.com/v1\" \"${AGENTTEAMS_LLM_API_KEY}\" \"${AGENTTEAMS_DEFAULT_MODEL}\" \"$(msg llm.openai.test.fail.codingplan_legacy)\" || return 0\n elif [ \"${AGENTTEAMS_LANGUAGE}\" = \"en\" ]; then\n test_llm_connectivity \"${AGENTTEAMS_OPENAI_BASE_URL}\" \"${AGENTTEAMS_LLM_API_KEY}\" \"${AGENTTEAMS_DEFAULT_MODEL}\" \"$(msg llm.openai.test.fail.codingplan)\" || return 0\n else\n test_llm_connectivity \"${AGENTTEAMS_OPENAI_BASE_URL}\" \"${AGENTTEAMS_LLM_API_KEY}\" \"${AGENTTEAMS_DEFAULT_MODEL}\" \"$(msg llm.openai.test.fail.tokenplan)\" || return 0\n fi\n ;;\n 2|openai-compat)\n AGENTTEAMS_LLM_PROVIDER=\"openai-compat\"\n log \"$(msg llm.provider.selected_openai \"${AGENTTEAMS_LLM_PROVIDER}\")\"\n echo \"\"\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg llm.openai.base_url_prompt)\" \"${AGENTTEAMS_OPENAI_BASE_URL}\")\"\n else\n _current=\"${AGENTTEAMS_OPENAI_BASE_URL}\"\n if [ -n \"${_current}\" ]; then\n read -e -p \"$(msg llm.openai.base_url_prompt) [${_current}]: \" AGENTTEAMS_OPENAI_BASE_URL\n else\n read -e -p \"$(msg llm.openai.base_url_prompt) [https://api.openai.com/v1]: \" AGENTTEAMS_OPENAI_BASE_URL\n fi\n if [ \"${AGENTTEAMS_OPENAI_BASE_URL}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_OPENAI_BASE_URL=\"${AGENTTEAMS_OPENAI_BASE_URL:-${_current}}\"\n [ -z \"${AGENTTEAMS_OPENAI_BASE_URL}\" ] && AGENTTEAMS_OPENAI_BASE_URL=\"https://api.openai.com/v1\"\n fi\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg llm.openai.model_prompt)\" \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n else\n _current=\"${AGENTTEAMS_DEFAULT_MODEL}\"\n if [ -n \"${_current}\" ]; then\n read -e -p \"$(msg llm.openai.model_prompt) [${_current}]: \" AGENTTEAMS_DEFAULT_MODEL\n else\n read -e -p \"$(msg llm.openai.model_prompt) [gpt-5.4]: \" AGENTTEAMS_DEFAULT_MODEL\n fi\n if [ \"${AGENTTEAMS_DEFAULT_MODEL}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_DEFAULT_MODEL=\"${AGENTTEAMS_DEFAULT_MODEL:-${_current}}\"\n [ -z \"${AGENTTEAMS_DEFAULT_MODEL}\" ] && AGENTTEAMS_DEFAULT_MODEL=\"gpt-5.4\"\n fi\n log \"$(msg llm.openai.base_url_label \"${AGENTTEAMS_OPENAI_BASE_URL}\")\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n prompt_custom_model_params \"${AGENTTEAMS_DEFAULT_MODEL}\" || return 0\n log \"\"\n prompt AGENTTEAMS_LLM_API_KEY \"$(msg llm.apikey_prompt)\" \"\" \"true\" || return 0\n test_llm_connectivity \"${AGENTTEAMS_OPENAI_BASE_URL}\" \"${AGENTTEAMS_LLM_API_KEY}\" \"${AGENTTEAMS_DEFAULT_MODEL}\" || return 0\n ;;\n *)\n die \"$(msg llm.provider.invalid \"${PROVIDER_CHOICE}\")\"\n ;;\n esac\n\n # Skip to embedding if Keep-All mode handled LLM params\n if [ \"${SKIP_TO_EMBEDDING}\" = \"1\" ]; then\n if [ -n \"${AGENTTEAMS_EMBEDDING_MODEL}\" ]; then\n log \"$(msg llm.embedding.title)\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_EMBEDDING_MODEL}\")\"\n log \"\"\n fi\n return 0\n fi\n\n # --- Embedding model (optional, auto-tested) ---\n echo \"\"\n log \"$(msg llm.embedding.title)\"\n log \"$(msg llm.embedding.hint)\"\n echo \"\"\n echo \"$(msg llm.embedding.option.default)\"\n echo \"$(msg llm.embedding.option.custom)\"\n echo \"$(msg llm.embedding.option.disable)\"\n echo \"\"\n local EMB_CHOICE\n read -e -p \"$(msg llm.embedding.select) [1]: \" EMB_CHOICE\n EMB_CHOICE=\"${EMB_CHOICE:-1}\"\n if [ \"${EMB_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n\n case \"${EMB_CHOICE}\" in\n 1)\n AGENTTEAMS_EMBEDDING_MODEL=\"text-embedding-v4\"\n ;;\n 2)\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg llm.embedding.custom_prompt)\" \"${AGENTTEAMS_EMBEDDING_MODEL}\")\"\n else\n _current=\"${AGENTTEAMS_EMBEDDING_MODEL}\"\n if [ -n \"${_current}\" ]; then\n read -e -p \"$(msg llm.embedding.custom_prompt) [${_current}]: \" AGENTTEAMS_EMBEDDING_MODEL\n else\n read -e -p \"$(msg llm.embedding.custom_prompt): \" AGENTTEAMS_EMBEDDING_MODEL\n fi\n if [ \"${AGENTTEAMS_EMBEDDING_MODEL}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_EMBEDDING_MODEL=\"${AGENTTEAMS_EMBEDDING_MODEL:-${_current}}\"\n fi\n ;;\n 3)\n AGENTTEAMS_EMBEDDING_MODEL=\"\"\n log \"$(msg llm.embedding.disabled)\"\n ;;\n *)\n AGENTTEAMS_EMBEDDING_MODEL=\"text-embedding-v4\"\n ;;\n esac\n\n if [ -n \"${AGENTTEAMS_EMBEDDING_MODEL}\" ]; then\n # Qwen provider uses dashscope directly; others use OPENAI_BASE_URL\n local EMB_BASE_URL=\"${AGENTTEAMS_OPENAI_BASE_URL}\"\n if [ \"${AGENTTEAMS_LLM_PROVIDER}\" = \"qwen\" ]; then\n EMB_BASE_URL=\"https://dashscope.aliyuncs.com/compatible-mode/v1\"\n fi\n if ! test_embedding_connectivity \"${EMB_BASE_URL}\" \"${AGENTTEAMS_LLM_API_KEY}\" \"${AGENTTEAMS_EMBEDDING_MODEL}\"; then\n AGENTTEAMS_EMBEDDING_MODEL=\"\"\n log \"$(msg llm.embedding.auto_disabled)\"\n fi\n fi\n\n export AGENTTEAMS_LLM_PROVIDER AGENTTEAMS_DEFAULT_MODEL\n [ -n \"${AGENTTEAMS_OPENAI_BASE_URL+x}\" ] && export AGENTTEAMS_OPENAI_BASE_URL\n log \"\"\n}\n\nstep_admin() {\n log \"$(msg admin.title)\"\n prompt AGENTTEAMS_ADMIN_USER \"$(msg admin.username_prompt)\" \"admin\" || return 0\n AGENTTEAMS_ADMIN_USER=\"$(printf '%s' \"${AGENTTEAMS_ADMIN_USER}\" | tr '[:upper:]' '[:lower:]')\"\n\n # Pre-set via env var: validate; in non-interactive mode fail fast,\n # in interactive mode warn and fall through to the retry prompt.\n if [ -n \"${AGENTTEAMS_ADMIN_PASSWORD:-}\" ]; then\n log \" $(msg prompt.preset \"$(msg admin.password_prompt)\")\"\n if [ ${#AGENTTEAMS_ADMIN_PASSWORD} -ge 8 ]; then\n log \"\"\n return 0\n fi\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n die \"$(msg admin.password_too_short \"${#AGENTTEAMS_ADMIN_PASSWORD}\")\"\n fi\n error \"$(msg admin.password_too_short \"${#AGENTTEAMS_ADMIN_PASSWORD}\")\"\n unset AGENTTEAMS_ADMIN_PASSWORD\n fi\n\n while true; do\n unset AGENTTEAMS_ADMIN_PASSWORD\n prompt_optional AGENTTEAMS_ADMIN_PASSWORD \"$(msg admin.password_prompt)\" \"true\" || return 0\n if [ -z \"${AGENTTEAMS_ADMIN_PASSWORD}\" ]; then\n AGENTTEAMS_ADMIN_PASSWORD=\"admin$(openssl rand -hex 6)\"\n log \"$(msg admin.password_generated)\"\n break\n fi\n if [ ${#AGENTTEAMS_ADMIN_PASSWORD} -ge 8 ]; then\n break\n fi\n error \"$(msg admin.password_too_short \"${#AGENTTEAMS_ADMIN_PASSWORD}\")\"\n done\n log \"\"\n}\n\nstep_network() {\n log \"$(msg port.local_only.title)\"\n echo \"\"\n echo \" 1) $(msg port.local_only.hint_yes)\"\n echo \" 2) $(msg port.local_only.hint_no)\"\n echo \"\"\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_LOCAL_ONLY=\"${AGENTTEAMS_LOCAL_ONLY:-1}\"\n elif [ -z \"${AGENTTEAMS_LOCAL_ONLY+x}\" ]; then\n local _local_choice\n read -e -p \"$(msg port.local_only.choice): \" _local_choice\n if [ \"${_local_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _local_choice=\"${_local_choice:-1}\"\n case \"${_local_choice}\" in\n 2|n|N|no|NO) AGENTTEAMS_LOCAL_ONLY=\"0\" ;;\n *) AGENTTEAMS_LOCAL_ONLY=\"1\" ;;\n esac\n fi\n export AGENTTEAMS_LOCAL_ONLY\n if [ \"${AGENTTEAMS_LOCAL_ONLY}\" = \"1\" ]; then\n log \"$(msg port.local_only.selected_local)\"\n else\n log \"$(msg port.local_only.selected_external)\"\n echo \"\"\n echo -e \"\\033[33m$(msg port.local_only.https_hint)\\033[0m\"\n fi\n}\n\nstep_ports() {\n log \"$(msg port.title)\"\n prompt AGENTTEAMS_PORT_GATEWAY \"$(msg port.gateway_prompt)\" \"18080\" || return 0\n prompt AGENTTEAMS_PORT_CONSOLE \"$(msg port.console_prompt)\" \"18001\" || return 0\n prompt AGENTTEAMS_PORT_ELEMENT_WEB \"$(msg port.element_prompt)\" \"18088\" || return 0\n prompt AGENTTEAMS_PORT_MANAGER_CONSOLE \"$(msg port.manager_console_prompt)\" \"18888\" || return 0\n log \"\"\n}\n\nstep_domains() {\n log \"$(msg domain.title)\"\n log \"$(msg domain.hint)\"\n prompt AGENTTEAMS_MATRIX_DOMAIN \"$(msg domain.matrix_prompt)\" \"matrix-local.agentteams.io:${AGENTTEAMS_PORT_GATEWAY}\" || return 0\n prompt AGENTTEAMS_MATRIX_CLIENT_DOMAIN \"$(msg domain.element_prompt)\" \"matrix-client-local.agentteams.io\" || return 0\n prompt AGENTTEAMS_AI_GATEWAY_DOMAIN \"$(msg domain.gateway_prompt)\" \"aigw-local.agentteams.io\" || return 0\n prompt AGENTTEAMS_FS_DOMAIN \"$(msg domain.fs_prompt)\" \"fs-local.agentteams.io\" || return 0\n if [ \"${AGENTTEAMS_MANAGER_RUNTIME}\" != \"copaw\" ]; then\n prompt AGENTTEAMS_CONSOLE_DOMAIN \"$(msg domain.console_prompt)\" \"console-local.agentteams.io\" || return 0\n fi\n log \"\"\n}\n\nstep_github() {\n log \"$(msg github.title)\"\n prompt_optional AGENTTEAMS_GITHUB_TOKEN \"$(msg github.token_prompt)\" \"true\" || return 0\n}\n\nstep_skills() {\n log \"\"\n log \"$(msg skills.title)\"\n prompt_optional AGENTTEAMS_SKILLS_API_URL \"$(msg skills.url_prompt)\" || return 0\n log \"\"\n}\n\nstep_volume() {\n log \"$(msg data.title)\"\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_DATA_DIR=\"${AGENTTEAMS_DATA_DIR:-agentteams-data}\"\n log \" $(msg data.volume_using \"${AGENTTEAMS_DATA_DIR}\") (non-interactive, skipped)\"\n export AGENTTEAMS_DATA_DIR\n return 0\n fi\n # ─────────────────────────────────────────────────────────────────\n if [ -z \"${AGENTTEAMS_DATA_DIR+x}\" ]; then\n local _input\n read -e -p \"$(msg data.volume_prompt): \" _input\n if [ \"${_input}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_DATA_DIR=\"${_input:-agentteams-data}\"\n export AGENTTEAMS_DATA_DIR\n fi\n AGENTTEAMS_DATA_DIR=\"${AGENTTEAMS_DATA_DIR:-agentteams-data}\"\n log \"$(msg data.volume_using \"${AGENTTEAMS_DATA_DIR}\")\"\n}\n\nstep_workspace() {\n log \"$(msg workspace.title)\"\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_WORKSPACE_DIR=\"${AGENTTEAMS_WORKSPACE_DIR:-${HOME}/agentteams-manager}\"\n AGENTTEAMS_WORKSPACE_DIR=\"$(cd \"${AGENTTEAMS_WORKSPACE_DIR}\" 2>/dev/null && pwd || echo \"${AGENTTEAMS_WORKSPACE_DIR}\")\"\n mkdir -p \"${AGENTTEAMS_WORKSPACE_DIR}\"\n log \" $(msg workspace.dir_label \"${AGENTTEAMS_WORKSPACE_DIR}\") (non-interactive, skipped)\"\n export AGENTTEAMS_WORKSPACE_DIR\n return 0\n fi\n # ─────────────────────────────────────────────────────────────────\n local _input\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg workspace.dir_prompt \"${HOME}/agentteams-manager\")\" \"${AGENTTEAMS_WORKSPACE_DIR}\")\"\n _input=\"${AGENTTEAMS_WORKSPACE_DIR}\"\n else\n local _current=\"${AGENTTEAMS_WORKSPACE_DIR}\"\n if [ -n \"${_current}\" ]; then\n read -e -p \"$(msg workspace.dir_prompt \"${HOME}/agentteams-manager\") [${_current}]: \" _input\n else\n read -e -p \"$(msg workspace.dir_prompt \"${HOME}/agentteams-manager\"): \" _input\n fi\n if [ \"${_input}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _input=\"${_input:-${_current}}\"\n fi\n AGENTTEAMS_WORKSPACE_DIR=\"${_input:-${HOME}/agentteams-manager}\"\n export AGENTTEAMS_WORKSPACE_DIR\n AGENTTEAMS_WORKSPACE_DIR=\"$(cd \"${AGENTTEAMS_WORKSPACE_DIR}\" 2>/dev/null && pwd || echo \"${AGENTTEAMS_WORKSPACE_DIR}\")\"\n mkdir -p \"${AGENTTEAMS_WORKSPACE_DIR}\"\n log \"$(msg workspace.dir_label \"${AGENTTEAMS_WORKSPACE_DIR}\")\"\n}\n\nstep_dashboard() {\n AGENTTEAMS_DASHBOARD=\"${AGENTTEAMS_DASHBOARD:-1}\"\n AGENTTEAMS_DASHBOARD_VERSION=\"${AGENTTEAMS_DASHBOARD_VERSION:-v1.2.0-beta.1}\"\n AGENTTEAMS_PORT_DASHBOARD=\"${AGENTTEAMS_PORT_DASHBOARD:-13000}\"\n AGENTTEAMS_AI_GATEWAY_ADMIN_URL=\"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL:-}\"\n\n # Compute the default image for the current version.\n # We track whether the current image matches the derived default so that\n # changing the version also updates the image (unless the user has\n # explicitly overridden it to a non-default value).\n _dashboard_default_image() {\n echo \"${AGENTTEAMS_REGISTRY}/agentteams/agentteams-dashboard:${1:-${AGENTTEAMS_DASHBOARD_VERSION}}\"\n }\n local _init_default\n _init_default=$(_dashboard_default_image)\n if [ -z \"${AGENTTEAMS_DASHBOARD_IMAGE:-}\" ]; then\n AGENTTEAMS_DASHBOARD_IMAGE=\"${_init_default}\"\n fi\n\n # Recompute default image when the version changes AND the current image\n # matches the default for the *previous* (env-file) version.\n # This handles non-interactive upgrades correctly:\n # - env file loads old default image + old version\n # - CLI passes new version\n # - image should follow (because it was auto-derived before)\n # But preserves truly custom images (same repo but custom tag like :canary)\n # because they won't match the old-version default exactly.\n local _old_saved_version=\"\"\n local _env_file=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n if [ -f \"${_env_file}\" ]; then\n _old_saved_version=\"$(grep '^AGENTTEAMS_DASHBOARD_VERSION=' \"${_env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n fi\n if [ -n \"${_old_saved_version}\" ] && \\\n [ \"${_old_saved_version}\" != \"${AGENTTEAMS_DASHBOARD_VERSION}\" ] && \\\n [ \"${AGENTTEAMS_DASHBOARD_IMAGE}\" = \"$(_dashboard_default_image \"${_old_saved_version}\")\" ]; then\n AGENTTEAMS_DASHBOARD_IMAGE=$(_dashboard_default_image)\n fi\n\n # Quick Start: accept all defaults without prompting. The dashboard is\n # installed by default (AGENTTEAMS_DASHBOARD=1); set AGENTTEAMS_DASHBOARD=0\n # to opt out.\n if [ \"${AGENTTEAMS_QUICKSTART:-0}\" = \"1\" ]; then\n if [ \"${AGENTTEAMS_DASHBOARD}\" = \"1\" ]; then\n log \"$(msg dash.summary \"${AGENTTEAMS_PORT_DASHBOARD}\" \"${AGENTTEAMS_DASHBOARD_IMAGE}\") (quick start)\"\n else\n log \"$(msg dash.skip) (quick start)\"\n fi\n export AGENTTEAMS_DASHBOARD AGENTTEAMS_DASHBOARD_VERSION AGENTTEAMS_PORT_DASHBOARD AGENTTEAMS_DASHBOARD_IMAGE AGENTTEAMS_AI_GATEWAY_ADMIN_URL\n return 0\n fi\n\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_DASHBOARD=\"${AGENTTEAMS_DASHBOARD:-1}\"\n if [ \"${AGENTTEAMS_DASHBOARD}\" = \"1\" ]; then\n log \"$(msg dash.summary \"${AGENTTEAMS_PORT_DASHBOARD}\" \"${AGENTTEAMS_DASHBOARD_IMAGE}\") (non-interactive)\"\n else\n log \"$(msg dash.skip) (non-interactive)\"\n fi\n export AGENTTEAMS_DASHBOARD AGENTTEAMS_DASHBOARD_VERSION AGENTTEAMS_PORT_DASHBOARD AGENTTEAMS_DASHBOARD_IMAGE AGENTTEAMS_AI_GATEWAY_ADMIN_URL\n return 0\n fi\n\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ]; then\n log \"$(msg prompt.upgrade_keep \"\" \"${AGENTTEAMS_DASHBOARD}\") dashboard=${AGENTTEAMS_DASHBOARD} version=${AGENTTEAMS_DASHBOARD_VERSION} port=${AGENTTEAMS_PORT_DASHBOARD}\"\n export AGENTTEAMS_DASHBOARD AGENTTEAMS_DASHBOARD_VERSION AGENTTEAMS_PORT_DASHBOARD AGENTTEAMS_DASHBOARD_IMAGE AGENTTEAMS_AI_GATEWAY_ADMIN_URL\n return 0\n fi\n\n local _input\n echo \"\"\n read -p \"$(msg dash.prompt) [Y/n]: \" _input\n case \"${_input:-}\" in\n [nN0]) AGENTTEAMS_DASHBOARD=0 ;;\n *) AGENTTEAMS_DASHBOARD=1 ;;\n esac\n if [ \"${_input}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n\n if [ \"${AGENTTEAMS_DASHBOARD}\" != \"1\" ]; then\n log \"$(msg dash.skip)\"\n export AGENTTEAMS_DASHBOARD AGENTTEAMS_DASHBOARD_VERSION AGENTTEAMS_PORT_DASHBOARD AGENTTEAMS_DASHBOARD_IMAGE AGENTTEAMS_AI_GATEWAY_ADMIN_URL\n return 0\n fi\n\n # Dashboard version\n local _current_version=\"${AGENTTEAMS_DASHBOARD_VERSION}\"\n local _old_version=\"${AGENTTEAMS_DASHBOARD_VERSION}\"\n local _old_default_image\n _old_default_image=$(_dashboard_default_image \"${_old_version}\")\n read -p \"$(msg dash.version_prompt) [${_current_version}]: \" _input\n AGENTTEAMS_DASHBOARD_VERSION=\"${_input:-${_current_version}}\"\n\n # Dashboard port\n local _current_port=\"${AGENTTEAMS_PORT_DASHBOARD}\"\n read -p \"$(msg dash.port_prompt) [${_current_port}]: \" _input\n AGENTTEAMS_PORT_DASHBOARD=\"${_input:-${_current_port}}\"\n\n # Dashboard image: if the current image matches the default derived from\n # the OLD version, recompute it from the NEW version. This handles both\n # fresh installs and upgrades where the previous image was the default.\n # Only a real user override (image != default for old version) is preserved.\n if [ \"${AGENTTEAMS_DASHBOARD_VERSION}\" != \"${_old_version}\" ] && \\\n [ \"${AGENTTEAMS_DASHBOARD_IMAGE}\" = \"${_old_default_image}\" ]; then\n AGENTTEAMS_DASHBOARD_IMAGE=$(_dashboard_default_image)\n fi\n local _current_image=\"${AGENTTEAMS_DASHBOARD_IMAGE}\"\n read -p \"$(msg dash.image_prompt) [${_current_image}]: \" _input\n if [ -n \"${_input}\" ]; then\n AGENTTEAMS_DASHBOARD_IMAGE=\"${_input}\"\n fi\n\n # Higress Console URL (explicit config takes priority; auto-detect as fallback)\n local _higress_url=\"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\"\n if [ -z \"${_higress_url}\" ]; then\n if ${DOCKER_CMD} ps --format '{{.Names}}' | grep -q \"^agentteams-controller$\"; then\n if ${DOCKER_CMD} exec agentteams-controller curl -sf --max-time 2 http://127.0.0.1:8001/ >/dev/null 2>&1; then\n _higress_url=\"http://agentteams-controller:8001\"\n log \"$(msg dash.auto_url): ${_higress_url}\"\n else\n log \"$(msg dash.auto_url_fail)\"\n fi\n fi\n fi\n read -p \"$(msg dash.gateway_prompt) [${_higress_url:-<auto>}]: \" _input\n AGENTTEAMS_AI_GATEWAY_ADMIN_URL=\"${_input:-${_higress_url}}\"\n\n # Normalize URL: add http:// prefix if missing\n if [ -n \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\" ]; then\n case \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\" in\n http://*|https://*) ;;\n *) AGENTTEAMS_AI_GATEWAY_ADMIN_URL=\"http://${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\" ;;\n esac\n fi\n\n # Verify the Higress Console URL is reachable (best-effort warning only)\n if [ -n \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\" ]; then\n local _gw_reachable=0\n # If the URL points to the controller container, test from inside the container\n if echo \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\" | grep -qE \"(agentteams-controller|hiclaw-controller|127\\.0\\.0\\.1|localhost)\"; then\n if ${DOCKER_CMD} ps --format '{{.Names}}' | grep -q \"^agentteams-controller$\"; then\n if ${DOCKER_CMD} exec agentteams-controller curl -sf --max-time 3 \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}/\" >/dev/null 2>&1; then\n _gw_reachable=1\n fi\n fi\n else\n # External URL — probe from inside the controller (same network as\n # the Dashboard container) when possible; fall back to the host.\n if ${DOCKER_CMD} ps --format '{{.Names}}' | grep -q \"^agentteams-controller$\"; then\n if ${DOCKER_CMD} exec agentteams-controller curl -sf --max-time 3 \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}/\" >/dev/null 2>&1; then\n _gw_reachable=1\n fi\n elif curl -sf --max-time 3 \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}/\" >/dev/null 2>&1; then\n _gw_reachable=1\n fi\n fi\n if [ \"${_gw_reachable}\" = \"0\" ]; then\n log \"WARNING: Higress Console URL may not be reachable: ${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\"\n log \" Dashboard will still work, but shared login via Higress Console may fail.\"\n log \" To fix: verify the URL is correct and the Higress Console service is running.\"\n fi\n fi\n\n log \"$(msg dash.summary \"${AGENTTEAMS_PORT_DASHBOARD}\" \"${AGENTTEAMS_DASHBOARD_IMAGE}\")\"\n export AGENTTEAMS_DASHBOARD AGENTTEAMS_DASHBOARD_VERSION AGENTTEAMS_PORT_DASHBOARD AGENTTEAMS_DASHBOARD_IMAGE AGENTTEAMS_AI_GATEWAY_ADMIN_URL\n}\n\nstep_runtime() {\n log \"$(msg worker_runtime.title)\"\n echo \"\"\n echo \" 1) $(msg worker_runtime.copaw)\"\n echo \" 2) $(msg worker_runtime.openclaw)\"\n if ! _ver_lt \"${AGENTTEAMS_VERSION}\" \"v1.1.0\"; then\n echo \" 3) $(msg worker_runtime.hermes)\"\n fi\n echo \"\"\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"${AGENTTEAMS_DEFAULT_WORKER_RUNTIME:-copaw}\"\n elif [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_DEFAULT_WORKER_RUNTIME}\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg worker_runtime.title_short)\" \"${AGENTTEAMS_DEFAULT_WORKER_RUNTIME}\")\"\n local _runtime_choice\n read -e -p \"$(msg worker_runtime.choice): \" _runtime_choice\n if [ \"${_runtime_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n if [ -n \"${_runtime_choice}\" ]; then\n case \"${_runtime_choice}\" in\n 2) AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"openclaw\" ;;\n 3) if ! _ver_lt \"${AGENTTEAMS_VERSION}\" \"v1.1.0\"; then\n AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"hermes\"\n else\n AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"copaw\"\n fi ;;\n *) AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"copaw\" ;;\n esac\n fi\n elif [ -z \"${AGENTTEAMS_DEFAULT_WORKER_RUNTIME+x}\" ]; then\n local _runtime_choice\n read -e -p \"$(msg worker_runtime.choice): \" _runtime_choice\n if [ \"${_runtime_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _runtime_choice=\"${_runtime_choice:-1}\"\n case \"${_runtime_choice}\" in\n 2) AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"openclaw\" ;;\n 3) if ! _ver_lt \"${AGENTTEAMS_VERSION}\" \"v1.1.0\"; then\n AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"hermes\"\n else\n AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"copaw\"\n fi ;;\n *) AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"copaw\" ;;\n esac\n fi\n export AGENTTEAMS_DEFAULT_WORKER_RUNTIME\n log \"$(msg worker_runtime.selected \"${AGENTTEAMS_DEFAULT_WORKER_RUNTIME}\")\"\n}\n\nstep_manager_runtime() {\n log \"$(msg manager_runtime.title)\"\n echo \"\"\n echo \" 1) $(msg manager_runtime.copaw)\"\n echo \" 2) $(msg manager_runtime.openclaw)\"\n echo \"\"\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_MANAGER_RUNTIME=\"${AGENTTEAMS_MANAGER_RUNTIME:-copaw}\"\n elif [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_MANAGER_RUNTIME}\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg manager_runtime.title_short)\" \"${AGENTTEAMS_MANAGER_RUNTIME}\")\"\n local _runtime_choice\n read -e -p \"$(msg manager_runtime.choice): \" _runtime_choice\n if [ \"${_runtime_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n if [ -n \"${_runtime_choice}\" ]; then\n case \"${_runtime_choice}\" in\n 2) AGENTTEAMS_MANAGER_RUNTIME=\"openclaw\" ;;\n *) AGENTTEAMS_MANAGER_RUNTIME=\"copaw\" ;;\n esac\n fi\n elif [ -z \"${AGENTTEAMS_MANAGER_RUNTIME+x}\" ]; then\n local _runtime_choice\n read -e -p \"$(msg manager_runtime.choice): \" _runtime_choice\n if [ \"${_runtime_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _runtime_choice=\"${_runtime_choice:-1}\"\n case \"${_runtime_choice}\" in\n 2) AGENTTEAMS_MANAGER_RUNTIME=\"openclaw\" ;;\n *) AGENTTEAMS_MANAGER_RUNTIME=\"copaw\" ;;\n esac\n fi\n export AGENTTEAMS_MANAGER_RUNTIME\n log \"$(msg manager_runtime.selected \"${AGENTTEAMS_MANAGER_RUNTIME}\")\"\n}\n\nstep_e2ee() {\n log \"\"\n log \"$(msg matrix_e2ee.title)\"\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_MATRIX_E2EE=\"${AGENTTEAMS_MATRIX_E2EE:-0}\"\n log \" $(msg matrix_e2ee.title_short) = ${AGENTTEAMS_MATRIX_E2EE} (non-interactive, skipped)\"\n export AGENTTEAMS_MATRIX_E2EE\n return 0\n fi\n # ─────────────────────────────────────────────────────────────────\n echo \"\"\n echo -e \" $(msg matrix_e2ee.desc)\"\n echo \"\"\n echo \" 1) $(msg matrix_e2ee.disable)\"\n echo \" 2) $(msg matrix_e2ee.enable)\"\n echo \"\"\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_MATRIX_E2EE}\" ]; then\n local _e2ee_display; if [ \"${AGENTTEAMS_MATRIX_E2EE}\" = \"1\" ]; then _e2ee_display=\"$(msg matrix_e2ee.val_enabled)\"; else _e2ee_display=\"$(msg matrix_e2ee.val_disabled)\"; fi\n log \"$(msg prompt.upgrade_keep \"$(msg matrix_e2ee.title_short)\" \"${_e2ee_display}\")\"\n local _e2ee_choice\n read -e -p \"$(msg matrix_e2ee.choice): \" _e2ee_choice\n if [ \"${_e2ee_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n if [ -n \"${_e2ee_choice}\" ]; then\n case \"${_e2ee_choice}\" in\n 2) AGENTTEAMS_MATRIX_E2EE=\"1\" ;;\n *) AGENTTEAMS_MATRIX_E2EE=\"0\" ;;\n esac\n fi\n elif [ -z \"${AGENTTEAMS_MATRIX_E2EE+x}\" ]; then\n local _e2ee_choice\n read -e -p \"$(msg matrix_e2ee.choice): \" _e2ee_choice\n if [ \"${_e2ee_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _e2ee_choice=\"${_e2ee_choice:-1}\"\n case \"${_e2ee_choice}\" in\n 2) AGENTTEAMS_MATRIX_E2EE=\"1\" ;;\n *) AGENTTEAMS_MATRIX_E2EE=\"0\" ;;\n esac\n fi\n AGENTTEAMS_MATRIX_E2EE=\"${AGENTTEAMS_MATRIX_E2EE:-0}\"\n export AGENTTEAMS_MATRIX_E2EE\n if [ \"${AGENTTEAMS_MATRIX_E2EE}\" = \"1\" ]; then\n log \"$(msg matrix_e2ee.selected_enabled)\"\n else\n log \"$(msg matrix_e2ee.selected_disabled)\"\n fi\n}\n\nstep_docker_proxy() {\n # Only relevant when socket mounting is enabled\n if [ \"${AGENTTEAMS_MOUNT_SOCKET}\" != \"1\" ]; then\n AGENTTEAMS_DOCKER_PROXY=\"0\"\n return 0\n fi\n\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_DOCKER_PROXY=\"${AGENTTEAMS_DOCKER_PROXY:-0}\"\n log \" $(msg docker_proxy.title_short) = ${AGENTTEAMS_DOCKER_PROXY} (non-interactive, skipped)\"\n export AGENTTEAMS_DOCKER_PROXY\n return 0\n fi\n # ─────────────────────────────────────────────────────────────────\n\n echo \"\"\n echo -e \" \\033[1m$(msg docker_proxy.title)\\033[0m\"\n echo \"\"\n echo -e \" $(msg docker_proxy.desc)\"\n echo \"\"\n echo \" 1) $(msg docker_proxy.enable)\"\n echo \" 2) $(msg docker_proxy.disable)\"\n echo \"\"\n\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_DOCKER_PROXY}\" ]; then\n local _proxy_display; if [ \"${AGENTTEAMS_DOCKER_PROXY}\" = \"1\" ]; then _proxy_display=\"$(msg docker_proxy.val_enabled)\"; else _proxy_display=\"$(msg docker_proxy.val_disabled)\"; fi\n log \"$(msg prompt.upgrade_keep \"$(msg docker_proxy.title_short)\" \"${_proxy_display}\")\"\n local _choice\n read -e -p \"$(msg docker_proxy.choice): \" _choice\n if [ \"${_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n if [ -n \"${_choice}\" ]; then\n case \"${_choice}\" in\n 2) AGENTTEAMS_DOCKER_PROXY=\"0\" ;;\n *) AGENTTEAMS_DOCKER_PROXY=\"1\" ;;\n esac\n fi\n elif [ -z \"${AGENTTEAMS_DOCKER_PROXY+x}\" ]; then\n local _choice\n read -e -p \"$(msg docker_proxy.choice): \" _choice\n if [ \"${_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _choice=\"${_choice:-1}\"\n case \"${_choice}\" in\n 2) AGENTTEAMS_DOCKER_PROXY=\"0\" ;;\n *) AGENTTEAMS_DOCKER_PROXY=\"1\" ;;\n esac\n fi\n AGENTTEAMS_DOCKER_PROXY=\"${AGENTTEAMS_DOCKER_PROXY:-1}\"\n export AGENTTEAMS_DOCKER_PROXY\n if [ \"${AGENTTEAMS_DOCKER_PROXY}\" = \"1\" ]; then\n log \"$(msg docker_proxy.selected_enabled)\"\n\n # Prompt for additional allowed image sources\n echo \"\"\n echo -e \" $(msg docker_proxy.registries_desc)\"\n echo \"\"\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_PROXY_ALLOWED_REGISTRIES}\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg docker_proxy.registries_label)\" \"${AGENTTEAMS_PROXY_ALLOWED_REGISTRIES}\")\"\n local _reg_input\n read -e -p \"$(msg docker_proxy.registries_prompt): \" _reg_input\n if [ \"${_reg_input}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n [ -n \"${_reg_input}\" ] && AGENTTEAMS_PROXY_ALLOWED_REGISTRIES=\"${_reg_input}\"\n elif [ -z \"${AGENTTEAMS_PROXY_ALLOWED_REGISTRIES+x}\" ]; then\n local _reg_input\n read -e -p \"$(msg docker_proxy.registries_prompt): \" _reg_input\n if [ \"${_reg_input}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_PROXY_ALLOWED_REGISTRIES=\"${_reg_input:-}\"\n fi\n export AGENTTEAMS_PROXY_ALLOWED_REGISTRIES\n else\n log \"$(msg docker_proxy.selected_disabled)\"\n fi\n}\n\nstep_idle() {\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_WORKER_IDLE_TIMEOUT=\"${AGENTTEAMS_WORKER_IDLE_TIMEOUT:-720}\"\n log \" $(msg idle_timeout.label) = ${AGENTTEAMS_WORKER_IDLE_TIMEOUT} (non-interactive, skipped)\"\n export AGENTTEAMS_WORKER_IDLE_TIMEOUT\n return 0\n fi\n # ─────────────────────────────────────────────────────────────────\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_WORKER_IDLE_TIMEOUT}\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg idle_timeout.label)\" \"${AGENTTEAMS_WORKER_IDLE_TIMEOUT}\")\"\n local _idle_timeout\n read -e -p \"$(msg idle_timeout.prompt): \" _idle_timeout\n if [ \"${_idle_timeout}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n [ -n \"${_idle_timeout}\" ] && AGENTTEAMS_WORKER_IDLE_TIMEOUT=\"${_idle_timeout}\"\n elif [ -z \"${AGENTTEAMS_WORKER_IDLE_TIMEOUT+x}\" ]; then\n local _idle_timeout\n read -e -p \"$(msg idle_timeout.prompt): \" _idle_timeout\n if [ \"${_idle_timeout}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_WORKER_IDLE_TIMEOUT=\"${_idle_timeout:-720}\"\n fi\n AGENTTEAMS_WORKER_IDLE_TIMEOUT=\"${AGENTTEAMS_WORKER_IDLE_TIMEOUT:-720}\"\n export AGENTTEAMS_WORKER_IDLE_TIMEOUT\n log \"$(msg idle_timeout.selected \"${AGENTTEAMS_WORKER_IDLE_TIMEOUT}\")\"\n}\n\nstep_hostshare() {\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_HOST_SHARE_DIR=\"${AGENTTEAMS_HOST_SHARE_DIR:-${HOME}}\"\n log \" $(msg host_share.label) = ${AGENTTEAMS_HOST_SHARE_DIR} (non-interactive, skipped)\"\n export AGENTTEAMS_HOST_SHARE_DIR\n return 0\n fi\n # ─────────────────────────────────────────────────────────────────\n local _current=\"${AGENTTEAMS_HOST_SHARE_DIR}\"\n local _share_dir\n if [ -n \"${_current}\" ]; then\n read -e -p \"$(msg host_share.prompt \"$HOME\") [${_current}]: \" _share_dir\n else\n read -e -p \"$(msg host_share.prompt \"$HOME\"): \" _share_dir\n fi\n if [ \"${_share_dir}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_HOST_SHARE_DIR=\"${_share_dir:-${_current:-$HOME}}\"\n export AGENTTEAMS_HOST_SHARE_DIR\n}\n\nstep_podman_autostart() {\n echo \"\"\n log \"$(msg podman.autostart.title)\"\n echo \"\"\n echo -e \" $(msg podman.autostart.desc)\"\n echo \"\"\n echo \" 1) $(msg podman.autostart.enable)\"\n echo \" 2) $(msg podman.autostart.disable)\"\n echo \"\"\n\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_PODMAN_AUTOSTART}\" ]; then\n local _disp; if [ \"${AGENTTEAMS_PODMAN_AUTOSTART}\" = \"1\" ]; then _disp=\"$(msg podman.autostart.val_enabled)\"; else _disp=\"$(msg podman.autostart.val_disabled)\"; fi\n log \"$(msg prompt.upgrade_keep \"$(msg podman.autostart.title_short)\" \"${_disp}\")\"\n local _choice\n read -e -p \"$(msg podman.autostart.choice): \" _choice\n if [ \"${_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n if [ -n \"${_choice}\" ]; then\n case \"${_choice}\" in\n 2) AGENTTEAMS_PODMAN_AUTOSTART=\"0\" ;;\n *) AGENTTEAMS_PODMAN_AUTOSTART=\"1\" ;;\n esac\n fi\n elif [ -z \"${AGENTTEAMS_PODMAN_AUTOSTART+x}\" ]; then\n local _choice\n read -e -p \"$(msg podman.autostart.choice) [1]: \" _choice\n if [ \"${_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _choice=\"${_choice:-1}\"\n case \"${_choice}\" in\n 2) AGENTTEAMS_PODMAN_AUTOSTART=\"0\" ;;\n *) AGENTTEAMS_PODMAN_AUTOSTART=\"1\" ;;\n esac\n fi\n AGENTTEAMS_PODMAN_AUTOSTART=\"${AGENTTEAMS_PODMAN_AUTOSTART:-1}\"\n export AGENTTEAMS_PODMAN_AUTOSTART\n if [ \"${AGENTTEAMS_PODMAN_AUTOSTART}\" = \"1\" ]; then\n log \"$(msg podman.autostart.selected_enabled)\"\n else\n log \"$(msg podman.autostart.selected_disabled)\"\n fi\n}\n\n# ============================================================\n# Setup dedicated Podman autostart service for AgentTeams\n# ============================================================\nsetup_podman_autostart() {\n # Only execute if using podman and autostart is enabled\n if [ \"${DOCKER_CMD:-docker}\" != \"podman\" ] || [ \"${AGENTTEAMS_PODMAN_AUTOSTART:-0}\" != \"1\" ]; then\n return 0\n fi\n\n log \"$(msg install.podman.autostart_title)\"\n\n local current_user\n current_user=\"$(whoami)\"\n\n # Define the dedicated AgentTeams service content\n # Strict scoping: Only manage containers matching '--filter name=agentteams'\n local _service_content=\"[Unit]\nDescription=AgentTeams Dedicated Podman Autostart Service\nDocumentation=https://github.com/agentscope-ai/AgentTeams\nWants=network-online.target\nAfter=network-online.target\n\n[Service]\nType=oneshot\nRemainAfterExit=true\nEnvironment=LOGGING=\\\"--log-level=info\\\"\nExecStart=/usr/bin/podman \\$LOGGING start --filter name=agentteams --filter restart-policy=always --filter restart-policy=unless-stopped\nExecStop=/usr/bin/podman \\$LOGGING stop --filter name=agentteams --filter restart-policy=always --filter restart-policy=unless-stopped\n\n[Install]\nWantedBy=default.target\"\n\n local _autostart_ok=0\n\n if [ \"${current_user}\" = \"root\" ]; then\n # Rootful Mode (System-wide dedicated service)\n log \"$(msg install.podman.root_setup)\"\n\n local _sys_dir=\"/etc/systemd/system\"\n mkdir -p \"${_sys_dir}\"\n echo \"${_service_content}\" > \"${_sys_dir}/agentteams-podman-restart.service\"\n\n systemctl daemon-reload >/dev/null 2>&1 || true\n\n if systemctl enable --now agentteams-podman-restart.service >/dev/null 2>&1; then\n log \"$(msg install.podman.root_success)\"\n _autostart_ok=1\n else\n log \"$(msg install.podman.root_fail)\"\n fi\n else\n # Rootless Mode (User-level dedicated service)\n log \"$(msg install.podman.user_setup \"${current_user}\")\"\n\n local _linger_ok=0\n\n # 1. Enable Linger for background execution (Graceful escalation strategy)\n if loginctl show-user \"${current_user}\" --property=Linger 2>/dev/null | grep -q \"Linger=yes\"; then\n _linger_ok=1\n else\n log \"$(msg install.podman.linger_enable)\"\n\n # Step A: Try to enable linger natively without sudo\n if loginctl enable-linger \"${current_user}\" >/dev/null 2>&1; then\n log \"Successfully enabled systemd linger natively.\"\n _linger_ok=1\n else\n # Step B: Fallback to sudo\n log \"Native linger enablement failed. Distro security policy requires privileges.\"\n if command -v sudo >/dev/null 2>&1; then\n log \"Attempting to enable systemd linger via sudo (enter password if prompted)...\"\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n sudo -n loginctl enable-linger \"${current_user}\" 2>/dev/null && _linger_ok=1 || log \"$(msg install.podman.linger_warn)\"\n else\n sudo loginctl enable-linger \"${current_user}\" && _linger_ok=1 || log \"$(msg install.podman.linger_warn)\"\n fi\n else\n log \"$(msg install.podman.linger_warn)\"\n fi\n fi\n fi\n\n # 2. Write dedicated service file to user directory\n local _user_dir=\"${HOME}/.config/systemd/user\"\n mkdir -p \"${_user_dir}\"\n echo \"${_service_content}\" > \"${_user_dir}/agentteams-podman-restart.service\"\n\n systemctl --user daemon-reload >/dev/null 2>&1 || true\n\n if systemctl --user enable --now agentteams-podman-restart.service >/dev/null 2>&1; then\n log \"$(msg install.podman.user_success)\"\n # Crucial: Only consider autostart fully successful if systemd linger is also enabled\n if [ \"${_linger_ok}\" = \"1\" ]; then\n _autostart_ok=1\n else\n log \"⚠️ Service enabled, but since systemd linger is not active, containers will not auto-recover on system reboot.\"\n fi\n else\n log \"$(msg install.podman.user_fail)\"\n fi\n fi\n\n # Only print final success banner if the service AND linger were actually enabled\n if [ \"${_autostart_ok}\" = \"1\" ]; then\n log \"$(msg install.podman.success)\"\n fi\n}\n\n# ============================================================\n# Dashboard startup (called after controller is ready)\n# ============================================================\n\n_start_dashboard() {\n local CTRL_CONTAINER=\"agentteams-controller\"\n local DASHBOARD_CONTAINER=\"agentteams-dashboard\"\n\n if [ \"${AGENTTEAMS_DASHBOARD:-0}\" != \"1\" ]; then\n # Stop and remove the existing dashboard container if present.\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -q \"^${DASHBOARD_CONTAINER}$\"; then\n log \"Stopping and removing ${DASHBOARD_CONTAINER}...\"\n ${DOCKER_CMD} stop \"${DASHBOARD_CONTAINER}\" >/dev/null 2>&1 || true\n ${DOCKER_CMD} rm -f \"${DASHBOARD_CONTAINER}\" >/dev/null 2>&1 || true\n log \"Dashboard stopped.\"\n fi\n return 0\n fi\n\n # Dashboard requires the embedded architecture (controller container).\n # In legacy all-in-one mode the controller API is not available.\n if [ \"${AGENTTEAMS_USE_EMBEDDED:-0}\" != \"1\" ]; then\n log \"Skipping Dashboard: requires embedded architecture (agentteams-controller), not available in legacy mode.\"\n log \"Use Quick Start (embedded) mode to enable agentteams-dashboard.\"\n return 0\n fi\n\n AGENTTEAMS_PORT_DASHBOARD=\"${AGENTTEAMS_PORT_DASHBOARD:-13000}\"\n AGENTTEAMS_DASHBOARD_VERSION=\"${AGENTTEAMS_DASHBOARD_VERSION:-v1.2.0-beta.1}\"\n AGENTTEAMS_DASHBOARD_IMAGE=\"${AGENTTEAMS_DASHBOARD_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-dashboard:${AGENTTEAMS_DASHBOARD_VERSION}}\"\n\n log \"\"\n log \"Starting agentteams-dashboard...\"\n\n local BIND_ADDR=\"127.0.0.1\"\n [ \"${AGENTTEAMS_LOCAL_ONLY:-1}\" = \"0\" ] && BIND_ADDR=\"0.0.0.0\"\n\n # Remove existing container\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' | grep -qx \"${DASHBOARD_CONTAINER}\"; then\n ${DOCKER_CMD} rm -f \"${DASHBOARD_CONTAINER}\" >/dev/null 2>&1 || true\n fi\n\n # Pull or use local image\n if ! ${DOCKER_CMD} image inspect \"${AGENTTEAMS_DASHBOARD_IMAGE}\" >/dev/null 2>&1; then\n log \"Pulling dashboard image: ${AGENTTEAMS_DASHBOARD_IMAGE}\"\n ${DOCKER_CMD} pull \"${AGENTTEAMS_DASHBOARD_IMAGE}\" 2>/dev/null || {\n log \"WARNING: Could not pull dashboard image. Skipping.\"\n return 0\n }\n fi\n\n # Build env args from controller container (MinIO/LLM/auth).\n local env_args=()\n env_args+=(-e AGENTTEAMS_CONTROLLER_URL=\"http://${CTRL_CONTAINER}:8090\")\n env_args+=(-e NEXT_PUBLIC_MATRIX_API_URL=\"http://${CTRL_CONTAINER}:6167\")\n env_args+=(-e MATRIX_HOMESERVER_ALLOWLIST=\"${CTRL_CONTAINER},matrix-local.agentteams.io,matrix.org\")\n\n if ${DOCKER_CMD} ps --format '{{.Names}}' | grep -qx \"${CTRL_CONTAINER}\"; then\n local env_out\n env_out=$(${DOCKER_CMD} inspect \"${CTRL_CONTAINER}\" --format='{{range .Config.Env}}{{.}}{{\"\\n\"}}{{end}}')\n\n _env() { echo \"${env_out}\" | sed -n \"s|^${1}=||p\" | head -n1; }\n\n local fs_endpoint; fs_endpoint=$(_env AGENTTEAMS_FS_ENDPOINT)\n [ -z \"${fs_endpoint}\" ] && fs_endpoint=$(_env AGENTTEAMS_MINIO_ENDPOINT)\n if [ -n \"${fs_endpoint}\" ]; then\n fs_endpoint=$(echo \"${fs_endpoint}\" | sed -e \"s|127\\.0\\.0\\.1|${CTRL_CONTAINER}|\" -e \"s|localhost|${CTRL_CONTAINER}|\")\n env_args+=(-e AGENTTEAMS_FS_ENDPOINT=\"${fs_endpoint}\")\n else\n env_args+=(-e AGENTTEAMS_FS_ENDPOINT=\"http://${CTRL_CONTAINER}:9000\")\n fi\n\n local fs_bucket; fs_bucket=$(_env AGENTTEAMS_FS_BUCKET)\n [ -z \"${fs_bucket}\" ] && fs_bucket=$(_env AGENTTEAMS_MINIO_BUCKET)\n [ -n \"${fs_bucket}\" ] && env_args+=(-e AGENTTEAMS_FS_BUCKET=\"${fs_bucket}\")\n\n local fs_user; fs_user=$(_env AGENTTEAMS_FS_ACCESS_KEY)\n [ -z \"${fs_user}\" ] && fs_user=$(_env AGENTTEAMS_MINIO_USER)\n [ -n \"${fs_user}\" ] && env_args+=(-e AGENTTEAMS_FS_ACCESS_KEY=\"${fs_user}\")\n\n local fs_pass; fs_pass=$(_env AGENTTEAMS_FS_SECRET_KEY)\n [ -z \"${fs_pass}\" ] && fs_pass=$(_env AGENTTEAMS_MINIO_PASSWORD)\n [ -n \"${fs_pass}\" ] && env_args+=(-e AGENTTEAMS_FS_SECRET_KEY=\"${fs_pass}\")\n\n for k in AGENTTEAMS_LLM_PROVIDER AGENTTEAMS_LLM_API_KEY AGENTTEAMS_OPENAI_BASE_URL AGENTTEAMS_DEFAULT_MODEL; do\n local v; v=$(_env \"${k}\")\n [ -n \"${v}\" ] && env_args+=(-e \"${k}=${v}\")\n done\n\n # The controller writes the CLI SA token to /var/run/agentteams/cli-token.\n # The token is generated asynchronously during the first reconcile loop,\n # so it may not be available immediately after the container starts.\n # We poll for up to 30s to handle both fresh installs (token not yet minted)\n # and older images that don't support cli-token at all.\n # For backward compatibility, also try the legacy /var/run/hiclaw/cli-token\n # path used by older HiClaw images.\n local auth_token=\"${AGENTTEAMS_AUTH_TOKEN:-}\"\n if [ -z \"${auth_token}\" ]; then\n local _token_wait=0\n local _token_max_wait=30\n while [ ${_token_wait} -lt ${_token_max_wait} ]; do\n auth_token=$(${DOCKER_CMD} exec \"${CTRL_CONTAINER}\" sh -c 'cat /var/run/agentteams/cli-token 2>/dev/null || cat /var/run/hiclaw/cli-token 2>/dev/null' | tr -d '\\n' || true)\n if [ -n \"${auth_token}\" ]; then\n break\n fi\n sleep 2\n _token_wait=$((_token_wait + 2))\n done\n fi\n if [ -n \"${auth_token}\" ]; then\n env_args+=(-e AGENTTEAMS_AUTH_TOKEN=\"${auth_token}\")\n else\n log \"WARNING: could not read controller SA token (timed out after ${_token_max_wait:-30}s)\"\n log \" This can happen with older AgentTeams/HiClaw images or if the controller is still starting.\"\n log \" Dashboard will still work but some API calls may fail until you log in via Higress Console.\"\n log \" To fix: upgrade AgentTeams to v1.2.0-beta.1+ or set AGENTTEAMS_AUTH_TOKEN manually.\"\n fi\n\n local admin_user; admin_user=$(_env AGENTTEAMS_ADMIN_USER)\n local admin_pass; admin_pass=$(_env AGENTTEAMS_ADMIN_PASSWORD)\n [ -n \"${admin_user}\" ] && env_args+=(-e AGENTTEAMS_ADMIN_USER=\"${admin_user}\")\n [ -n \"${admin_pass}\" ] && env_args+=(-e AGENTTEAMS_ADMIN_PASSWORD=\"${admin_pass}\")\n\n # Higress Console URL: explicit config takes priority, auto-detect as fallback.\n local _resolved_gw_url=\"\"\n if [ -n \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\" ]; then\n # Normalize URL: add http:// prefix if missing\n _resolved_gw_url=\"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\"\n case \"${_resolved_gw_url}\" in\n http://*|https://*) ;;\n *) _resolved_gw_url=\"http://${_resolved_gw_url}\" ;;\n esac\n env_args+=(-e AGENTTEAMS_AI_GATEWAY_ADMIN_URL=\"${_resolved_gw_url}\")\n elif ${DOCKER_CMD} exec \"${CTRL_CONTAINER}\" wget -q -O- --timeout=2 http://127.0.0.1:8001/ >/dev/null 2>&1; then\n _resolved_gw_url=\"http://${CTRL_CONTAINER}:8001\"\n env_args+=(-e AGENTTEAMS_AI_GATEWAY_ADMIN_URL=\"${_resolved_gw_url}\")\n fi\n\n # Verify Higress Console URL reachability (best-effort warning)\n if [ -n \"${_resolved_gw_url}\" ]; then\n if ! ${DOCKER_CMD} exec \"${CTRL_CONTAINER}\" curl -sf --max-time 3 \"${_resolved_gw_url}/\" >/dev/null 2>&1; then\n log \"WARNING: Higress Console URL may not be reachable: ${_resolved_gw_url}\"\n log \" Dashboard will still work, but shared login via Higress Console may fail.\"\n log \" To fix: verify the URL is correct and the Higress Console service is running.\"\n fi\n fi\n else\n log \"WARNING: ${CTRL_CONTAINER} not running; launching Dashboard without Controller/MinIO/LLM env.\"\n if [ -n \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\" ]; then\n local _gw_norm=\"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\"\n case \"${_gw_norm}\" in\n http://*|https://*) ;;\n *) _gw_norm=\"http://${_gw_norm}\" ;;\n esac\n env_args+=(-e AGENTTEAMS_AI_GATEWAY_ADMIN_URL=\"${_gw_norm}\")\n fi\n fi\n\n # Create persistent data volume (for future use)\n ${DOCKER_CMD} volume create agentteams-dashboard-data >/dev/null 2>&1 || true\n\n ${DOCKER_CMD} run -d \\\n --name \"${DASHBOARD_CONTAINER}\" \\\n --restart unless-stopped \\\n --network \"${AGENTTEAMS_NETWORK:-agentteams-net}\" \\\n --network-alias dashboard.agentteams.io \\\n -p \"${BIND_ADDR}:${AGENTTEAMS_PORT_DASHBOARD}:3000\" \\\n \"${env_args[@]}\" \\\n -v agentteams-dashboard-data:/app/db \\\n \"${AGENTTEAMS_DASHBOARD_IMAGE}\"\n\n # Wait for dashboard to be ready\n local max_wait=60\n local waited=0\n while [ $waited -lt $max_wait ]; do\n if curl -sf \"http://127.0.0.1:${AGENTTEAMS_PORT_DASHBOARD}/\" >/dev/null 2>&1; then\n log \"Dashboard ready on port ${AGENTTEAMS_PORT_DASHBOARD}\"\n return 0\n fi\n sleep 2\n waited=$((waited + 2))\n done\n log \"WARNING: Dashboard startup timed out after ${max_wait}s\"\n ${DOCKER_CMD} logs \"${DASHBOARD_CONTAINER}\" 2>&1 | tail -20\n return 0\n}\n\n# ============================================================\n# Manager Installation (Interactive)\n# ============================================================\n\ninstall_manager() {\n log \"$(msg install.title)\"\n log \"$(msg install.registry \"${AGENTTEAMS_REGISTRY}\")\"\n log \"\"\n log \"$(msg install.dir \"$(pwd)\")\"\n log \"$(msg install.dir_hint)\"\n log \"$(msg install.dir_hint2)\"\n log \"\"\n\n # Non-interactive fallback: resolve version immediately so image tags are available\n # before the step state machine runs. Interactive mode lets step_version handle it.\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_VERSION=\"${AGENTTEAMS_VERSION:-${AGENTTEAMS_KNOWN_STABLE_VERSION}}\"\n resolve_image_tags\n fi\n\n # Migrate legacy env file location before checks\n local existing_env=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n if [ ! -f \"${existing_env}\" ] && [ -f \"./agentteams-manager.env\" ]; then\n log \"Migrating agentteams-manager.env from current directory to ${existing_env}...\"\n mv \"./agentteams-manager.env\" \"${existing_env}\"\n fi\n\n # Orphan volume detection (only when no env file — step_existing handles the env-file case)\n if [ ! -f \"${existing_env}\" ]; then\n local data_vol=\"${AGENTTEAMS_DATA_DIR:-agentteams-data}\"\n if ${DOCKER_CMD} volume ls -q | grep -q \"^${data_vol}$\"; then\n echo \"\"\n log \"$(msg install.orphan_volume.detected \"${data_vol}\")\"\n log \"$(msg install.orphan_volume.warn)\"\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n log \"$(msg install.orphan_volume.clean_noninteractive)\"\n ${DOCKER_CMD} stop agentteams-manager 2>/dev/null || true\n ${DOCKER_CMD} rm agentteams-manager 2>/dev/null || true\n for w in $(${DOCKER_CMD} ps -a --format '{{.Names}}' | grep \"^agentteams-worker-\" || true); do\n ${DOCKER_CMD} stop \"${w}\" 2>/dev/null || true\n ${DOCKER_CMD} rm \"${w}\" 2>/dev/null || true\n done\n log \"$(msg install.orphan_volume.cleaning)\"\n ${DOCKER_CMD} volume rm \"${data_vol}\" 2>/dev/null || true\n log \"$(msg install.orphan_volume.cleaned)\"\n else\n echo \"\"\n echo \"$(msg install.orphan_volume.choose)\"\n echo \"$(msg install.orphan_volume.clean)\"\n echo \"$(msg install.orphan_volume.keep)\"\n echo \"\"\n local ORPHAN_CHOICE\n read -e -p \"$(msg install.orphan_volume.prompt): \" ORPHAN_CHOICE\n ORPHAN_CHOICE=\"${ORPHAN_CHOICE:-1}\"\n case \"${ORPHAN_CHOICE}\" in\n 1|clean)\n ${DOCKER_CMD} stop agentteams-manager 2>/dev/null || true\n ${DOCKER_CMD} rm agentteams-manager 2>/dev/null || true\n for w in $(${DOCKER_CMD} ps -a --format '{{.Names}}' | grep \"^agentteams-worker-\" || true); do\n ${DOCKER_CMD} stop \"${w}\" 2>/dev/null || true\n ${DOCKER_CMD} rm \"${w}\" 2>/dev/null || true\n done\n log \"$(msg install.orphan_volume.cleaning)\"\n ${DOCKER_CMD} volume rm \"${data_vol}\" 2>/dev/null || true\n log \"$(msg install.orphan_volume.cleaned)\"\n ;;\n 2|keep)\n log \"$(msg install.orphan_volume.keeping)\"\n ;;\n esac\n fi\n fi\n fi\n\n # ── State machine ─────────────────────────────────────────────────────────\n local _STEPS=( step_lang step_mode step_version step_existing step_llm step_manager_runtime step_runtime step_admin step_network\n step_ports step_domains step_github step_skills step_volume\n step_workspace step_dashboard step_e2ee step_docker_proxy step_idle step_hostshare step_podman_autostart )\n local _STEP_HISTORY=()\n local _step_idx=0\n while [ \"${_step_idx}\" -lt \"${#_STEPS[@]}\" ]; do\n local _step_fn=\"${_STEPS[$_step_idx]}\"\n if should_skip_step \"${_step_fn}\"; then\n _step_idx=$((_step_idx + 1))\n continue\n fi\n if [ \"${#_STEP_HISTORY[@]}\" -gt 0 ]; then\n log \"$(msg nav.back_hint)\"\n fi\n STEP_RESULT=\"\"\n \"${_step_fn}\"\n if [ \"${STEP_RESULT}\" = \"back\" ]; then\n if [ \"${#_STEP_HISTORY[@]}\" -gt 0 ]; then\n local _last=$(( ${#_STEP_HISTORY[@]} - 1 ))\n _step_idx=\"${_STEP_HISTORY[$_last]}\"\n _STEP_HISTORY=(\"${_STEP_HISTORY[@]:0:${_last}}\")\n clear_step_vars \"${_STEPS[$_step_idx]}\"\n fi\n # else: first step, ignore 'b'\n else\n _STEP_HISTORY+=(\"${_step_idx}\")\n _step_idx=$((_step_idx + 1))\n fi\n done\n # ── End state machine ──────────────────────────────────────────────────────\n\n # Post-machine defaults for any steps that were skipped\n AGENTTEAMS_DATA_DIR=\"${AGENTTEAMS_DATA_DIR:-agentteams-data}\"\n if [ -z \"${AGENTTEAMS_WORKSPACE_DIR+x}\" ] || [ -z \"${AGENTTEAMS_WORKSPACE_DIR}\" ]; then\n AGENTTEAMS_WORKSPACE_DIR=\"${HOME}/agentteams-manager\"\n export AGENTTEAMS_WORKSPACE_DIR\n fi\n AGENTTEAMS_WORKSPACE_DIR=\"$(cd \"${AGENTTEAMS_WORKSPACE_DIR}\" 2>/dev/null && pwd || echo \"${AGENTTEAMS_WORKSPACE_DIR}\")\"\n mkdir -p \"${AGENTTEAMS_WORKSPACE_DIR}\"\n AGENTTEAMS_MANAGER_RUNTIME=\"${AGENTTEAMS_MANAGER_RUNTIME:-copaw}\"\n export AGENTTEAMS_MANAGER_RUNTIME\n AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"${AGENTTEAMS_DEFAULT_WORKER_RUNTIME:-copaw}\"\n AGENTTEAMS_MATRIX_E2EE=\"${AGENTTEAMS_MATRIX_E2EE:-0}\"\n export AGENTTEAMS_MATRIX_E2EE\n AGENTTEAMS_WORKER_IDLE_TIMEOUT=\"${AGENTTEAMS_WORKER_IDLE_TIMEOUT:-720}\"\n export AGENTTEAMS_WORKER_IDLE_TIMEOUT\n AGENTTEAMS_HOST_SHARE_DIR=\"${AGENTTEAMS_HOST_SHARE_DIR:-$HOME}\"\n export AGENTTEAMS_HOST_SHARE_DIR\n\n log \"\"\n\n # Generate secrets (only if not already set)\n log \"$(msg install.generating_secrets)\"\n AGENTTEAMS_MANAGER_PASSWORD=\"${AGENTTEAMS_MANAGER_PASSWORD:-$(generate_key)}\"\n AGENTTEAMS_REGISTRATION_TOKEN=\"${AGENTTEAMS_REGISTRATION_TOKEN:-$(generate_key)}\"\n AGENTTEAMS_MINIO_USER=\"${AGENTTEAMS_MINIO_USER:-${AGENTTEAMS_ADMIN_USER}}\"\n AGENTTEAMS_MINIO_PASSWORD=\"${AGENTTEAMS_MINIO_PASSWORD:-${AGENTTEAMS_ADMIN_PASSWORD}}\"\n AGENTTEAMS_MANAGER_GATEWAY_KEY=\"${AGENTTEAMS_MANAGER_GATEWAY_KEY:-$(generate_key)}\"\n\n # Matrix AppService tokens — generate once during install/upgrade if not provided.\n # Persisted to env file so they survive controller restarts.\n AGENTTEAMS_MATRIX_APPSERVICE_ENABLED=\"${AGENTTEAMS_MATRIX_APPSERVICE_ENABLED:-true}\"\n export AGENTTEAMS_MATRIX_APPSERVICE_ENABLED\n if [ \"${AGENTTEAMS_MATRIX_APPSERVICE_ENABLED}\" != \"false\" ] && [ \"${AGENTTEAMS_MATRIX_APPSERVICE_ENABLED}\" != \"0\" ]; then\n if [ -z \"${AGENTTEAMS_MATRIX_APPSERVICE_AS_TOKEN}\" ]; then\n AGENTTEAMS_MATRIX_APPSERVICE_AS_TOKEN=\"$(openssl rand -hex 32)\"\n log \" Auto-generated AppService as_token (saved to env file)\"\n fi\n if [ -z \"${AGENTTEAMS_MATRIX_APPSERVICE_HS_TOKEN}\" ]; then\n AGENTTEAMS_MATRIX_APPSERVICE_HS_TOKEN=\"$(openssl rand -hex 32)\"\n log \" Auto-generated AppService hs_token (saved to env file)\"\n fi\n export AGENTTEAMS_MATRIX_APPSERVICE_AS_TOKEN\n export AGENTTEAMS_MATRIX_APPSERVICE_HS_TOKEN\n fi\n\n # Detect Apple Silicon (M1/M2/M3/M4) - need JVM fix for Higress Console\n # See: https://github.com/agentscope-ai/AgentTeams/issues/249\n if [ -z \"${JVM_ARGS:-}\" ] && [ \"$(uname -m)\" = \"arm64\" ] && [ \"$(uname -s)\" = \"Darwin\" ]; then\n log \"Apple Silicon detected - setting JVM_ARGS to fix Higress Console SIGILL issue\"\n JVM_ARGS=\"-XX:+UnlockDiagnosticVMOptions -XX:-UseAESCTRIntrinsics -XX:UseSVE=0\"\n fi\n\n # Write .env file\n ENV_FILE=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n cat > \"${ENV_FILE}\" << EOF\n# AgentTeams Manager Configuration\n# Generated by agentteams-install.sh on $(date)\n\n# Language\nAGENTTEAMS_LANGUAGE=${AGENTTEAMS_LANGUAGE}\n\n# LLM\nAGENTTEAMS_LLM_PROVIDER=${AGENTTEAMS_LLM_PROVIDER}\nAGENTTEAMS_DEFAULT_MODEL=${AGENTTEAMS_DEFAULT_MODEL}\nAGENTTEAMS_LLM_API_KEY=${AGENTTEAMS_LLM_API_KEY}\nAGENTTEAMS_OPENAI_BASE_URL=${AGENTTEAMS_OPENAI_BASE_URL:-}\nAGENTTEAMS_MODEL_CONTEXT_WINDOW=${AGENTTEAMS_MODEL_CONTEXT_WINDOW:-}\nAGENTTEAMS_MODEL_MAX_TOKENS=${AGENTTEAMS_MODEL_MAX_TOKENS:-}\nAGENTTEAMS_MODEL_REASONING=${AGENTTEAMS_MODEL_REASONING:-}\nAGENTTEAMS_MODEL_VISION=${AGENTTEAMS_MODEL_VISION:-}\n\n# Embedding model (empty = disabled, default: text-embedding-v4)\nAGENTTEAMS_EMBEDDING_MODEL=${AGENTTEAMS_EMBEDDING_MODEL}\n\n# Admin\nAGENTTEAMS_ADMIN_USER=${AGENTTEAMS_ADMIN_USER}\nAGENTTEAMS_ADMIN_PASSWORD=${AGENTTEAMS_ADMIN_PASSWORD}\n\n# Ports\nAGENTTEAMS_LOCAL_ONLY=${AGENTTEAMS_LOCAL_ONLY}\nAGENTTEAMS_PORT_GATEWAY=${AGENTTEAMS_PORT_GATEWAY}\nAGENTTEAMS_PORT_CONSOLE=${AGENTTEAMS_PORT_CONSOLE}\nAGENTTEAMS_PORT_ELEMENT_WEB=${AGENTTEAMS_PORT_ELEMENT_WEB}\nAGENTTEAMS_PORT_MANAGER_CONSOLE=${AGENTTEAMS_PORT_MANAGER_CONSOLE:-18888}\n\n# Manager runtime (openclaw | copaw)\nAGENTTEAMS_MANAGER_RUNTIME=${AGENTTEAMS_MANAGER_RUNTIME:-copaw}\n\n# Matrix\nAGENTTEAMS_MATRIX_DOMAIN=${AGENTTEAMS_MATRIX_DOMAIN}\nAGENTTEAMS_MATRIX_CLIENT_DOMAIN=${AGENTTEAMS_MATRIX_CLIENT_DOMAIN}\n\n# Gateway\nAGENTTEAMS_AI_GATEWAY_DOMAIN=${AGENTTEAMS_AI_GATEWAY_DOMAIN}\nAGENTTEAMS_MANAGER_GATEWAY_KEY=${AGENTTEAMS_MANAGER_GATEWAY_KEY}\n\n# File System\nAGENTTEAMS_FS_DOMAIN=${AGENTTEAMS_FS_DOMAIN}\nAGENTTEAMS_CONSOLE_DOMAIN=${AGENTTEAMS_CONSOLE_DOMAIN}\nAGENTTEAMS_MINIO_USER=${AGENTTEAMS_MINIO_USER}\nAGENTTEAMS_MINIO_PASSWORD=${AGENTTEAMS_MINIO_PASSWORD}\n\n# Internal\nAGENTTEAMS_MANAGER_PASSWORD=${AGENTTEAMS_MANAGER_PASSWORD}\nAGENTTEAMS_REGISTRATION_TOKEN=${AGENTTEAMS_REGISTRATION_TOKEN}\n\n# GitHub (optional)\nAGENTTEAMS_GITHUB_TOKEN=${AGENTTEAMS_GITHUB_TOKEN:-}\n\n# Nacos package import defaults\nAGENTTEAMS_NACOS_REGISTRY_URI=${AGENTTEAMS_NACOS_REGISTRY_URI:-nacos://market.agentteams.io:80/public}\nAGENTTEAMS_NACOS_USERNAME=${AGENTTEAMS_NACOS_USERNAME:-}\nAGENTTEAMS_NACOS_PASSWORD=${AGENTTEAMS_NACOS_PASSWORD:-}\nAGENTTEAMS_NACOS_TOKEN=${AGENTTEAMS_NACOS_TOKEN:-}\n\n# Skills Registry (optional, default: nacos://market.agentteams.io:80/public)\nAGENTTEAMS_SKILLS_API_URL=${AGENTTEAMS_SKILLS_API_URL:-nacos://market.agentteams.io:80/public}\n\n# OpenClaw CMS plugin (optional)\nAGENTTEAMS_CMS_TRACES_ENABLED=${AGENTTEAMS_CMS_TRACES_ENABLED:-false}\nAGENTTEAMS_CMS_ENDPOINT=${AGENTTEAMS_CMS_ENDPOINT:-}\nAGENTTEAMS_CMS_LICENSE_KEY=${AGENTTEAMS_CMS_LICENSE_KEY:-}\nAGENTTEAMS_CMS_PROJECT=${AGENTTEAMS_CMS_PROJECT:-}\nAGENTTEAMS_CMS_WORKSPACE=${AGENTTEAMS_CMS_WORKSPACE:-}\nAGENTTEAMS_CMS_SERVICE_NAME=${AGENTTEAMS_CMS_SERVICE_NAME:-agentteams-manager}\nAGENTTEAMS_CMS_METRICS_ENABLED=${AGENTTEAMS_CMS_METRICS_ENABLED:-false}\n\n# Worker images (for direct container creation)\nAGENTTEAMS_WORKER_IMAGE=${WORKER_IMAGE}\nAGENTTEAMS_COPAW_WORKER_IMAGE=${COPAW_WORKER_IMAGE}\nAGENTTEAMS_HERMES_WORKER_IMAGE=${HERMES_WORKER_IMAGE}\n\n# Default Worker runtime (openclaw | copaw | hermes)\nAGENTTEAMS_DEFAULT_WORKER_RUNTIME=${AGENTTEAMS_DEFAULT_WORKER_RUNTIME:-copaw}\n\n# Matrix E2EE (0=disabled, 1=enabled; default: 0)\nAGENTTEAMS_MATRIX_E2EE=${AGENTTEAMS_MATRIX_E2EE:-0}\n\n# Matrix AppService\nAGENTTEAMS_MATRIX_APPSERVICE_ENABLED=${AGENTTEAMS_MATRIX_APPSERVICE_ENABLED:-true}\nAGENTTEAMS_MATRIX_APPSERVICE_AS_TOKEN=${AGENTTEAMS_MATRIX_APPSERVICE_AS_TOKEN:-}\nAGENTTEAMS_MATRIX_APPSERVICE_HS_TOKEN=${AGENTTEAMS_MATRIX_APPSERVICE_HS_TOKEN:-}\n\n# Docker API proxy (0=disabled, 1=enabled; default: 1)\nAGENTTEAMS_DOCKER_PROXY=${AGENTTEAMS_DOCKER_PROXY:-1}\n\n# Docker API proxy: additional allowed image sources (comma-separated)\nAGENTTEAMS_PROXY_ALLOWED_REGISTRIES=${AGENTTEAMS_PROXY_ALLOWED_REGISTRIES:-}\n\n# Worker idle timeout in minutes (default: 720 = 12 hours)\nAGENTTEAMS_WORKER_IDLE_TIMEOUT=${AGENTTEAMS_WORKER_IDLE_TIMEOUT:-720}\n\n# Podman autostart via systemd (0=disabled, 1=enabled)\nAGENTTEAMS_PODMAN_AUTOSTART=${AGENTTEAMS_PODMAN_AUTOSTART:-0}\n\n# JVM Args for Higress Console (fixes SIGILL on Apple Silicon)\nJVM_ARGS=${JVM_ARGS:-}\n\n# Higress WASM plugin image registry (auto-selected by timezone)\nHIGRESS_ADMIN_WASM_PLUGIN_IMAGE_REGISTRY=${AGENTTEAMS_REGISTRY}\n\n# Data persistence\nAGENTTEAMS_DATA_DIR=${AGENTTEAMS_DATA_DIR:-agentteams-data}\n# Manager workspace (skills, memory, state — host-editable)\nAGENTTEAMS_WORKSPACE_DIR=${AGENTTEAMS_WORKSPACE_DIR:-}\n# Host directory sharing\nAGENTTEAMS_HOST_SHARE_DIR=${AGENTTEAMS_HOST_SHARE_DIR:-}\n\n# agentteams-dashboard (management UI)\nAGENTTEAMS_DASHBOARD=${AGENTTEAMS_DASHBOARD:-1}\nAGENTTEAMS_DASHBOARD_VERSION=${AGENTTEAMS_DASHBOARD_VERSION:-v1.2.0-beta.1}\nAGENTTEAMS_PORT_DASHBOARD=${AGENTTEAMS_PORT_DASHBOARD:-13000}\nAGENTTEAMS_DASHBOARD_IMAGE=${AGENTTEAMS_DASHBOARD_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-dashboard:${AGENTTEAMS_DASHBOARD_VERSION}}\nAGENTTEAMS_AI_GATEWAY_ADMIN_URL=${AGENTTEAMS_AI_GATEWAY_ADMIN_URL:-}\nEOF\n\n chmod 600 \"${ENV_FILE}\"\n log \"$(msg install.config_saved \"${ENV_FILE}\")\"\n\n # Detect container runtime socket\n SOCKET_MOUNT_ARGS=\"\"\n if [ \"${AGENTTEAMS_MOUNT_SOCKET}\" = \"1\" ]; then\n # Actively ensure the API socket is enabled and active before detection.\n if [ \"${DOCKER_CMD:-}\" = \"podman\" ]; then\n ensure_podman_socket\n fi\n CONTAINER_SOCK=$(detect_socket)\n if [ -n \"${CONTAINER_SOCK}\" ]; then\n log \"$(msg install.socket_detected \"${CONTAINER_SOCK}\")\"\n SOCKET_MOUNT_ARGS=\"-v ${CONTAINER_SOCK}:/var/run/docker.sock --security-opt label=disable\"\n else\n log \"$(msg install.socket_not_found)\"\n # Interactive confirmation when socket not found\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" != \"1\" ]; then\n echo \"\"\n echo -e \"\\033[33m$(msg install.socket_confirm.title)\\033[0m\"\n echo \"\"\n echo -e \"$(msg install.socket_confirm.message)\"\n echo \"\"\n read -p \"$(msg install.socket_confirm.prompt)\" SOCKET_CONFIRM\n if [ \"${SOCKET_CONFIRM}\" != \"y\" ] && [ \"${SOCKET_CONFIRM}\" != \"Y\" ]; then\n log \"$(msg install.socket_confirm.cancelled)\"\n exit 0\n fi\n fi\n fi\n fi\n\n # Create the data volume if it doesn't already exist (reuse on reinstall)\n if ! ${DOCKER_CMD} volume ls -q | grep -q \"^${AGENTTEAMS_DATA_DIR}$\"; then\n ${DOCKER_CMD} volume create \"${AGENTTEAMS_DATA_DIR}\" > /dev/null\n fi\n\n # Data mount: Docker volume\n DATA_MOUNT_ARGS=\"-v ${AGENTTEAMS_DATA_DIR}:/data\"\n\n # Manager workspace mount (always a host directory, defaulting to ~/agentteams-manager)\n WORKSPACE_MOUNT_ARGS=\"-v ${AGENTTEAMS_WORKSPACE_DIR}:/root/manager-workspace\"\n\n # Pass host timezone to container so date/time commands reflect local time\n TZ_ARGS=\"-e TZ=${AGENTTEAMS_TIMEZONE}\"\n\n # Host directory mount\n if [ -d \"${AGENTTEAMS_HOST_SHARE_DIR}\" ]; then\n HOST_SHARE_MOUNT_ARGS=\"-v ${AGENTTEAMS_HOST_SHARE_DIR}:/host-share\"\n log \"$(msg host_share.sharing \"${AGENTTEAMS_HOST_SHARE_DIR}\")\"\n else\n log \"$(msg host_share.not_exist \"${AGENTTEAMS_HOST_SHARE_DIR}\")\"\n HOST_SHARE_MOUNT_ARGS=\"-v ${AGENTTEAMS_HOST_SHARE_DIR}:/host-share\"\n fi\n\n # YOLO mode: pass through if set in environment (enables autonomous decisions)\n YOLO_ARGS=\"\"\n if [ \"${AGENTTEAMS_YOLO:-}\" = \"1\" ]; then\n YOLO_ARGS=\"-e AGENTTEAMS_YOLO=1\"\n log \"$(msg install.yolo)\"\n fi\n\n # Matrix-plugin debug tracing: pass through if AGENTTEAMS_MATRIX_DEBUG=1.\n # The container entrypoints translate this to OPENCLAW_MATRIX_DEBUG=1\n # so the openclaw matrix plugin emits structured INFO-level lifecycle\n # traces (sync state transitions, room.invite/join, message handler\n # arrival + filter outcomes). Used to diagnose worker/manager hangs.\n MATRIX_DEBUG_ARGS=\"\"\n if [ \"${AGENTTEAMS_MATRIX_DEBUG:-}\" = \"1\" ]; then\n MATRIX_DEBUG_ARGS=\"-e AGENTTEAMS_MATRIX_DEBUG=1\"\n fi\n\n # E2EE is already in the env file; but also pass explicitly in case env file is not the source\n # (AGENTTEAMS_MATRIX_E2EE is already written to ENV_FILE above via --env-file)\n\n # Pull images (manager based on runtime config; all worker runtimes always pulled)\n _is_local_image() {\n case \"$1\" in\n agentteams/*) return 0 ;;\n *) return 1 ;;\n esac\n }\n\n _local_image_exists() {\n local _img=\"$1\"\n [ -z \"${_img}\" ] && return 1\n _is_local_image \"${_img}\" || return 1\n ${DOCKER_CMD} image inspect \"${_img}\" >/dev/null 2>&1\n }\n\n # Helper: pull or skip a single image\n # Args: $1=image $2=exists_msg_key $3=pulling_msg_key\n _pull_image() {\n local _img=\"$1\" _exists_key=\"$2\" _pull_key=\"$3\"\n [ -z \"${_img}\" ] && return 0\n if _local_image_exists \"${_img}\"; then\n log \"$(msg \"${_exists_key}\" \"${_img}\")\"\n return 0\n fi\n log \"$(msg \"${_pull_key}\" \"${_img}\")\"\n local _attempt=1\n while [ $_attempt -le 3 ]; do\n if ${DOCKER_CMD} pull \"${_img}\"; then\n return 0\n fi\n if [ $_attempt -lt 3 ]; then\n log \"Pull failed (attempt ${_attempt}/3), retrying in 5s...\"\n sleep 5\n fi\n _attempt=$((_attempt + 1))\n done\n die \"Failed to pull ${_img} after 3 attempts\"\n return 1\n }\n\n # Embedded controller image (resolve versioned tag, fallback to latest)\n resolve_embedded_image\n if [ \"${AGENTTEAMS_USE_EMBEDDED}\" = \"1\" ]; then\n _local_image_exists \"${EMBEDDED_IMAGE}\" || true\n fi\n\n # Manager image is always required (select based on runtime)\n if [ \"${AGENTTEAMS_MANAGER_RUNTIME}\" = \"copaw\" ]; then\n _pull_image \"${MANAGER_COPAW_IMAGE}\" \"install.image.exists\" \"install.image.pulling_manager\"\n else\n _pull_image \"${MANAGER_IMAGE}\" \"install.image.exists\" \"install.image.pulling_manager\"\n fi\n\n # Pull all worker runtime images (workers may use any runtime regardless of the default)\n _pull_image \"${WORKER_IMAGE}\" \"install.image.worker_exists\" \"install.image.pulling_worker\"\n _pull_image \"${COPAW_WORKER_IMAGE}\" \"install.image.worker_exists\" \"install.image.pulling_worker\"\n _pull_image \"${HERMES_WORKER_IMAGE}\" \"install.image.worker_exists\" \"install.image.pulling_worker\"\n\n # --- Pre-upgrade: extract Matrix passwords from running old containers ---\n # Only needed when upgrading FROM old architecture (v1.0.9) TO embedded.\n # For new-arch-to-new-arch upgrades, credential files already exist with\n # correct room IDs — we must NOT overwrite them.\n _creds_tmp=\"\"\n if [ \"${AGENTTEAMS_UPGRADE:-0}\" = \"1\" ] && [ \"${AGENTTEAMS_USE_EMBEDDED}\" = \"1\" ]; then\n # Detect if upgrading from old arch: old arch has no agentteams-controller container\n # (or has it as a docker-proxy only, not embedded). Check if the existing\n # agentteams-controller is an embedded image (has supervisord) or just a proxy.\n _is_old_arch=0\n if ! ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -q \"^agentteams-controller$\"; then\n _is_old_arch=1\n elif ${DOCKER_CMD} ps -a --format '{{.Names}} {{.Image}}' 2>/dev/null | grep \"^agentteams-controller \" | grep -qv \"embedded\"; then\n _is_old_arch=1\n fi\n\n if [ \"${_is_old_arch}\" = \"1\" ]; then\n _creds_tmp=$(mktemp -d)\n\n # docker exec for Matrix/minio paths only works while agentteams-manager is running.\n _mgr_creds_tempstart=0\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -q '^agentteams-manager$'; then\n if ! ${DOCKER_CMD} ps --format '{{.Names}}' 2>/dev/null | grep -q '^agentteams-manager$'; then\n log \"agentteams-manager is stopped; starting it temporarily to extract Matrix credentials for upgrade...\"\n ${DOCKER_CMD} start agentteams-manager 2>/dev/null || true\n wait_matrix_ready \"agentteams-manager\"\n _mgr_creds_tempstart=1\n fi\n fi\n\n # Manager password (container Config.Env, then secrets file inside running manager, then data volume)\n _mgr_pw=$(${DOCKER_CMD} inspect agentteams-manager --format '{{range .Config.Env}}{{println .}}{{end}}' 2>/dev/null | grep '^AGENTTEAMS_MANAGER_PASSWORD=' | cut -d= -f2-)\n if [ -z \"${_mgr_pw}\" ] && ${DOCKER_CMD} ps --format '{{.Names}}' 2>/dev/null | grep -q '^agentteams-manager$'; then\n _mgr_pw=$(${DOCKER_CMD} exec agentteams-manager bash -c 'source /data/agentteams-secrets.env 2>/dev/null && echo \"${AGENTTEAMS_MANAGER_PASSWORD}\"' 2>/dev/null)\n fi\n if [ -z \"${_mgr_pw}\" ] && ${DOCKER_CMD} volume ls -q 2>/dev/null | grep -q \"^${AGENTTEAMS_DATA_DIR}$\"; then\n _mgr_pw=$(agentteams_read_secret_from_data_volume \"${AGENTTEAMS_DATA_DIR}\" AGENTTEAMS_MANAGER_PASSWORD)\n fi\n\n # Manager admin DM room ID: login as admin, find DM room with @manager\n _mgr_room=\"\"\n if [ -n \"${_mgr_pw}\" ]; then\n _admin_pw=$(grep AGENTTEAMS_ADMIN_PASSWORD \"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\" 2>/dev/null | cut -d= -f2-)\n _admin_user=$(grep AGENTTEAMS_ADMIN_USER \"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\" 2>/dev/null | cut -d= -f2-)\n _admin_user=\"${_admin_user:-admin}\"\n _matrix_domain=$(grep AGENTTEAMS_MATRIX_DOMAIN \"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\" 2>/dev/null | cut -d= -f2-)\n if [ -n \"${_admin_pw}\" ] && ${DOCKER_CMD} ps --format '{{.Names}}' 2>/dev/null | grep -q '^agentteams-manager$'; then\n _admin_token=$(${DOCKER_CMD} exec agentteams-manager curl -sf -X POST http://127.0.0.1:6167/_matrix/client/v3/login \\\n -H \"Content-Type: application/json\" \\\n -d '{\"type\":\"m.login.password\",\"identifier\":{\"type\":\"m.id.user\",\"user\":\"'\"${_admin_user}\"'\"},\"password\":\"'\"${_admin_pw}\"'\"}' 2>/dev/null | python3 -c \"import sys,json; print(json.load(sys.stdin).get('access_token',''))\" 2>/dev/null || true)\n if [ -n \"${_admin_token}\" ]; then\n _mgr_room=$(${DOCKER_CMD} exec agentteams-manager curl -sf -X GET \\\n -H \"Authorization: Bearer ${_admin_token}\" \\\n \"http://127.0.0.1:6167/_matrix/client/v3/joined_rooms\" 2>/dev/null | python3 -c \"\nimport sys,json,subprocess\nrooms = json.load(sys.stdin).get('joined_rooms',[])\nfor room_id in rooms:\n enc = room_id.replace('!','%21')\n members = json.loads(subprocess.check_output([\n 'docker','exec','agentteams-manager','curl','-sf','-X','GET',\n '-H','Authorization: Bearer ${_admin_token}',\n 'http://127.0.0.1:6167/_matrix/client/v3/rooms/'+enc+'/members'\n ]).decode()).get('chunk',[])\n member_ids = [m['state_key'] for m in members]\n if any('manager' in m and 'admin' not in m.split(':')[0] for m in member_ids):\n if len(member_ids) <= 3:\n print(room_id)\n break\n\" 2>/dev/null || true)\n fi\n fi\n if [ -z \"${_mgr_room}\" ]; then\n _mgr_room=$(agentteams_read_admin_dm_room_from_workspace \"${AGENTTEAMS_WORKSPACE_DIR}\")\n fi\n cat > \"${_creds_tmp}/default.env\" <<CREDEOF\nWORKER_PASSWORD=\"${_mgr_pw}\"\nWORKER_MINIO_PASSWORD=\"$(openssl rand -hex 24)\"\nWORKER_GATEWAY_KEY=\"${AGENTTEAMS_MANAGER_GATEWAY_KEY}\"\nWORKER_ROOM_ID=\"${_mgr_room}\"\nCREDEOF\n log \"Extracted Manager Matrix password${_mgr_room:+ and room ID}\"\n fi\n\n # Worker passwords and room IDs from workers-registry.json\n if [ -f \"${AGENTTEAMS_WORKSPACE_DIR}/workers-registry.json\" ]; then\n _worker_names=$(python3 -c \"import json; d=json.load(open('${AGENTTEAMS_WORKSPACE_DIR}/workers-registry.json')); print(' '.join(d.get('workers',{}).keys()))\" 2>/dev/null || true)\n for _wname in ${_worker_names}; do\n _wpw=\"\"\n if ${DOCKER_CMD} ps --format '{{.Names}}' 2>/dev/null | grep -q '^agentteams-manager$'; then\n _wpw=$(${DOCKER_CMD} exec agentteams-manager cat \"/root/agentteams-fs/agents/${_wname}/credentials/matrix/password\" 2>/dev/null || true)\n fi\n if [ -z \"${_wpw}\" ] && ${DOCKER_CMD} volume ls -q 2>/dev/null | grep -q \"^${AGENTTEAMS_DATA_DIR}$\"; then\n _wpw=$(agentteams_read_worker_creds_value_from_volume \"${AGENTTEAMS_DATA_DIR}\" \"${_wname}\" WORKER_PASSWORD)\n fi\n _wroom=$(python3 -c \"import json; d=json.load(open('${AGENTTEAMS_WORKSPACE_DIR}/workers-registry.json')); print(d.get('workers',{}).get('${_wname}',{}).get('room_id',''))\" 2>/dev/null || true)\n if [ -z \"${_wroom}\" ] && ${DOCKER_CMD} volume ls -q 2>/dev/null | grep -q \"^${AGENTTEAMS_DATA_DIR}$\"; then\n _wroom=$(agentteams_read_worker_creds_value_from_volume \"${AGENTTEAMS_DATA_DIR}\" \"${_wname}\" WORKER_ROOM_ID)\n fi\n if [ -n \"${_wpw}\" ]; then\n cat > \"${_creds_tmp}/${_wname}.env\" <<CREDEOF\nWORKER_PASSWORD=\"${_wpw}\"\nWORKER_MINIO_PASSWORD=\"$(openssl rand -hex 24)\"\nWORKER_GATEWAY_KEY=\"$(openssl rand -hex 32)\"\nWORKER_ROOM_ID=\"${_wroom}\"\nCREDEOF\n log \"Extracted ${_wname} Matrix password${_wroom:+ and room ID}\"\n fi\n done\n fi\n\n if [ \"${_mgr_creds_tempstart}\" = \"1\" ]; then\n log \"Stopping agentteams-manager after credential extraction (upgrade will recreate containers)...\"\n ${DOCKER_CMD} stop agentteams-manager 2>/dev/null || true\n fi\n fi # _is_old_arch\n fi\n\n # --- Stop and remove existing containers ---\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' | grep -q \"^agentteams-controller$\"; then\n ${DOCKER_CMD} stop agentteams-controller 2>/dev/null || true\n ${DOCKER_CMD} rm agentteams-controller 2>/dev/null || true\n fi\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' | grep -q \"^agentteams-manager$\"; then\n log \"$(msg install.removing_existing)\"\n ${DOCKER_CMD} stop agentteams-manager 2>/dev/null || true\n ${DOCKER_CMD} rm agentteams-manager 2>/dev/null || true\n fi\n\n # Stop and remove worker containers (controller will recreate via CR reconciliation)\n if [ -n \"${UPGRADE_EXISTING_WORKERS:-}\" ]; then\n log \"$(msg install.existing.stopping_workers)\"\n for w in ${UPGRADE_EXISTING_WORKERS}; do\n ${DOCKER_CMD} stop \"${w}\" 2>/dev/null || true\n ${DOCKER_CMD} rm \"${w}\" 2>/dev/null || true\n log \"$(msg install.existing.removed \"${w}\")\"\n done\n fi\n\n # Clean up legacy containers from v1.0.x and earlier\n for _legacy_name in agentteams-docker-proxy; do\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -qx \"${_legacy_name}\"; then\n log \"Removing legacy container: ${_legacy_name}\"\n ${DOCKER_CMD} stop \"${_legacy_name}\" 2>/dev/null || true\n ${DOCKER_CMD} rm -f \"${_legacy_name}\" 2>/dev/null || true\n fi\n done\n\n # --- Upgrade: inject extracted credentials into data volume ---\n # Only needed for old-arch upgrades (credential files were extracted above).\n if [ -n \"${_creds_tmp}\" ] && [ -d \"${_creds_tmp}\" ] && [ -n \"$(ls -A \"${_creds_tmp}\" 2>/dev/null)\" ]; then\n local _cleanup_ctr=\"agentteams-upgrade-cleanup\"\n ${DOCKER_CMD} rm -f \"${_cleanup_ctr}\" 2>/dev/null || true\n ${DOCKER_CMD} run --rm --name \"${_cleanup_ctr}\" \\\n --entrypoint sh \\\n -v \"${AGENTTEAMS_DATA_DIR}:/data\" \\\n -v \"${_creds_tmp}:/creds:ro\" \\\n \"${EMBEDDED_IMAGE}\" -c '\n rm -rf /data/worker-creds\n mkdir -p /data/worker-creds\n cp /creds/*.env /data/worker-creds/ 2>/dev/null || true\n chmod 600 /data/worker-creds/*.env 2>/dev/null || true\n ' 2>/dev/null && log \"Injected credentials for upgrade\" || log \"Warning: credential injection failed, continuing\"\n rm -rf \"${_creds_tmp}\"\n fi\n\n # --- Start containers ---\n log \"$(msg install.starting_manager)\"\n\n # Build port binding args\n if [ \"${AGENTTEAMS_LOCAL_ONLY:-1}\" = \"1\" ]; then\n _port_prefix=\"127.0.0.1:\"\n else\n _port_prefix=\"\"\n fi\n\n # Ensure agentteams-net Docker network exists\n ${DOCKER_CMD} network inspect agentteams-net >/dev/null 2>&1 || ${DOCKER_CMD} network create agentteams-net\n\n if [ \"${AGENTTEAMS_USE_EMBEDDED}\" != \"1\" ] && [ \"${AGENTTEAMS_UPGRADE:-0}\" = \"1\" ]; then\n # Check if current installation is embedded — downgrade to legacy is not supported\n if ${DOCKER_CMD} ps -a --format '{{.Names}} {{.Image}}' 2>/dev/null | grep \"^agentteams-controller \" | grep -q \"embedded\"; then\n error \"Downgrade from embedded architecture to legacy version (${AGENTTEAMS_VERSION}) is not supported.\"\n error \"Please use 'make uninstall-embedded' first, then do a clean install of the target version.\"\n exit 1\n fi\n fi\n\n if [ \"${AGENTTEAMS_USE_EMBEDDED}\" = \"1\" ]; then\n # ============================================================\n # New architecture: embedded controller + auto-created manager\n # ============================================================\n\n # Internal port: 8080 (Higress gateway inside the container).\n local _internal_gw_port=8080\n local _matrix_domain=\"${AGENTTEAMS_MATRIX_DOMAIN:-matrix-local.agentteams.io:${AGENTTEAMS_PORT_GATEWAY}}\"\n local _aigw_domain=\"${AGENTTEAMS_AI_GATEWAY_DOMAIN:-aigw-local.agentteams.io}\"\n # Ensure internal gateway port is present (container-internal traffic uses 8080)\n case \"${_aigw_domain}\" in *:*) ;; *) _aigw_domain=\"${_aigw_domain}:${_internal_gw_port}\" ;; esac\n local _fs_domain=\"${AGENTTEAMS_FS_DOMAIN:-fs-local.agentteams.io}\"\n case \"${_fs_domain}\" in *:*) ;; *) _fs_domain=\"${_fs_domain}:${_internal_gw_port}\" ;; esac\n\n # Controller env args\n local _ctrl_env_prefix\n _ctrl_env_prefix=\"$(_controller_env_prefix \"${AGENTTEAMS_VERSION}\")\"\n local _storage_prefix\n _storage_prefix=\"$(_controller_storage_prefix \"${AGENTTEAMS_VERSION}\")\"\n local _ctrl_env_args=(\n -e \"${_ctrl_env_prefix}ADMIN_USER=${AGENTTEAMS_ADMIN_USER}\"\n -e \"${_ctrl_env_prefix}ADMIN_PASSWORD=${AGENTTEAMS_ADMIN_PASSWORD}\"\n -e \"${_ctrl_env_prefix}MANAGER_PASSWORD=${AGENTTEAMS_MANAGER_PASSWORD}\"\n -e \"${_ctrl_env_prefix}REGISTRATION_TOKEN=${AGENTTEAMS_REGISTRATION_TOKEN}\"\n -e \"${_ctrl_env_prefix}MINIO_USER=${AGENTTEAMS_MINIO_USER}\"\n -e \"${_ctrl_env_prefix}MINIO_PASSWORD=${AGENTTEAMS_MINIO_PASSWORD}\"\n -e \"${_ctrl_env_prefix}LLM_PROVIDER=${AGENTTEAMS_LLM_PROVIDER}\"\n -e \"${_ctrl_env_prefix}LLM_API_KEY=${AGENTTEAMS_LLM_API_KEY}\"\n -e \"${_ctrl_env_prefix}DEFAULT_MODEL=${AGENTTEAMS_DEFAULT_MODEL}\"\n -e \"${_ctrl_env_prefix}MANAGER_GATEWAY_KEY=${AGENTTEAMS_MANAGER_GATEWAY_KEY}\"\n -e \"${_ctrl_env_prefix}MANAGER_RUNTIME=${AGENTTEAMS_MANAGER_RUNTIME:-copaw}\"\n -e \"${_ctrl_env_prefix}MANAGER_IMAGE=$([ \"${AGENTTEAMS_MANAGER_RUNTIME}\" = \"copaw\" ] && echo \"${MANAGER_COPAW_IMAGE}\" || echo \"${MANAGER_IMAGE}\")\"\n -e \"${_ctrl_env_prefix}DEFAULT_WORKER_RUNTIME=${AGENTTEAMS_DEFAULT_WORKER_RUNTIME:-copaw}\"\n -e \"${_ctrl_env_prefix}WORKER_IMAGE=${WORKER_IMAGE}\"\n -e \"${_ctrl_env_prefix}COPAW_WORKER_IMAGE=${COPAW_WORKER_IMAGE}\"\n -e \"${_ctrl_env_prefix}HERMES_WORKER_IMAGE=${HERMES_WORKER_IMAGE}\"\n -e \"${_ctrl_env_prefix}MATRIX_DOMAIN=${_matrix_domain}\"\n -e \"${_ctrl_env_prefix}ELEMENT_HOMESERVER_URL=http://127.0.0.1:${AGENTTEAMS_PORT_GATEWAY}\"\n -e \"${_ctrl_env_prefix}MATRIX_URL=http://127.0.0.1:6167\"\n -e \"${_ctrl_env_prefix}MATRIX_E2EE=${AGENTTEAMS_MATRIX_E2EE:-0}\"\n -e \"${_ctrl_env_prefix}MINIO_ENDPOINT=http://127.0.0.1:9000\"\n -e \"${_ctrl_env_prefix}MINIO_BUCKET=agentteams-storage\"\n -e \"${_ctrl_env_prefix}STORAGE_PREFIX=${_storage_prefix}\"\n -e \"${_ctrl_env_prefix}FS_ENDPOINT=http://127.0.0.1:9000\"\n -e \"${_ctrl_env_prefix}AI_GATEWAY_URL=http://${_aigw_domain}\"\n -e \"${_ctrl_env_prefix}CONTROLLER_URL=http://agentteams-controller:8090\"\n -e \"${_ctrl_env_prefix}DOCKER_NETWORK=agentteams-net\"\n -e \"${_ctrl_env_prefix}WORKSPACE_DIR=${AGENTTEAMS_WORKSPACE_DIR}\"\n -e \"${_ctrl_env_prefix}HOST_SHARE_DIR=${AGENTTEAMS_HOST_SHARE_DIR}\"\n -e \"${_ctrl_env_prefix}MANAGER_ENABLED=true\"\n -e \"${_ctrl_env_prefix}PORT_MANAGER_CONSOLE=${AGENTTEAMS_PORT_MANAGER_CONSOLE:-18888}\"\n )\n if _use_legacy_image_env \"${AGENTTEAMS_VERSION}\"; then\n _ctrl_env_args+=(\n -e \"${_ctrl_env_prefix}FS_BUCKET=agentteams-storage\"\n -e \"${_ctrl_env_prefix}RESOURCE_PREFIX=agentteams-\"\n )\n else\n _ctrl_env_args+=(\n -e \"${_ctrl_env_prefix}MATRIX_APPSERVICE_ENABLED=${AGENTTEAMS_MATRIX_APPSERVICE_ENABLED:-true}\"\n -e \"${_ctrl_env_prefix}MATRIX_APPSERVICE_AS_TOKEN=${AGENTTEAMS_MATRIX_APPSERVICE_AS_TOKEN:-}\"\n -e \"${_ctrl_env_prefix}MATRIX_APPSERVICE_HS_TOKEN=${AGENTTEAMS_MATRIX_APPSERVICE_HS_TOKEN:-}\"\n )\n fi\n\n # Timezone\n if [ -n \"${AGENTTEAMS_TIMEZONE:-}\" ]; then\n _ctrl_env_args+=(-e \"TZ=${AGENTTEAMS_TIMEZONE}\")\n fi\n\n # Yolo mode\n if [ \"${AGENTTEAMS_YOLO:-}\" = \"1\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}YOLO=1\")\n fi\n\n # Matrix-plugin debug tracing — propagated to every manager + worker\n # the controller spawns, then translated to OPENCLAW_MATRIX_DEBUG=1\n # by the container entrypoints. Use this to diagnose\n # \"worker did not join\" / \"manager replied empty\" hangs.\n if [ \"${AGENTTEAMS_MATRIX_DEBUG:-}\" = \"1\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}MATRIX_DEBUG=1\")\n fi\n\n # Optional: GitHub token\n if [ -n \"${AGENTTEAMS_GITHUB_TOKEN:-}\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}GITHUB_TOKEN=${AGENTTEAMS_GITHUB_TOKEN}\")\n fi\n\n # Optional: embedding model\n if [ -n \"${AGENTTEAMS_EMBEDDING_MODEL:-}\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}EMBEDDING_MODEL=${AGENTTEAMS_EMBEDDING_MODEL}\")\n fi\n\n # Optional: OpenAI-compatible base URL\n if [ -n \"${AGENTTEAMS_OPENAI_BASE_URL:-}\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}OPENAI_BASE_URL=${AGENTTEAMS_OPENAI_BASE_URL}\")\n fi\n # Optional: language\n if [ -n \"${AGENTTEAMS_LANGUAGE:-}\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}LANGUAGE=${AGENTTEAMS_LANGUAGE}\")\n fi\n\n # Optional: CMS/ARMS observability. In embedded mode the controller\n # spawns the Manager and Workers, so it must receive these settings.\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}CMS_TRACES_ENABLED=${AGENTTEAMS_CMS_TRACES_ENABLED:-false}\")\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}CMS_SERVICE_NAME=${AGENTTEAMS_CMS_SERVICE_NAME:-agentteams-manager}\")\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}CMS_METRICS_ENABLED=${AGENTTEAMS_CMS_METRICS_ENABLED:-false}\")\n if [ -n \"${AGENTTEAMS_CMS_ENDPOINT:-}\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}CMS_ENDPOINT=${AGENTTEAMS_CMS_ENDPOINT}\")\n fi\n if [ -n \"${AGENTTEAMS_CMS_LICENSE_KEY:-}\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}CMS_LICENSE_KEY=${AGENTTEAMS_CMS_LICENSE_KEY}\")\n fi\n if [ -n \"${AGENTTEAMS_CMS_PROJECT:-}\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}CMS_PROJECT=${AGENTTEAMS_CMS_PROJECT}\")\n fi\n if [ -n \"${AGENTTEAMS_CMS_WORKSPACE:-}\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}CMS_WORKSPACE=${AGENTTEAMS_CMS_WORKSPACE}\")\n fi\n\n # shellcheck disable=SC2086\n ${DOCKER_CMD} run -d \\\n --name agentteams-controller \\\n --network agentteams-net \\\n --network-alias matrix-local.agentteams.io \\\n --network-alias aigw-local.agentteams.io \\\n --network-alias fs-local.agentteams.io \\\n \"${_ctrl_env_args[@]}\" \\\n -v \"${CONTAINER_SOCK}:/var/run/docker.sock\" \\\n --security-opt label=disable \\\n -v \"${AGENTTEAMS_DATA_DIR}:/data\" \\\n -v \"${AGENTTEAMS_WORKSPACE_DIR}:/root/agentteams-fs/agents/manager\" \\\n -p \"${_port_prefix}${AGENTTEAMS_PORT_GATEWAY}:8080\" \\\n -p \"${_port_prefix}${AGENTTEAMS_PORT_CONSOLE}:8001\" \\\n -p \"${_port_prefix}${AGENTTEAMS_PORT_ELEMENT_WEB:-18088}:8088\" \\\n --restart unless-stopped \\\n \"${EMBEDDED_IMAGE}\"\n\n log \"Embedded controller started: agentteams-controller\"\n\n # Wait for infrastructure inside the controller container\n _wait_for_url() {\n local url=\"$1\" ctr=\"$2\" max_wait=\"${3:-120}\" desc=\"${4:-service}\"\n local elapsed=0\n log \"Waiting for ${desc}...\"\n while [ $elapsed -lt $max_wait ]; do\n if ${DOCKER_CMD} exec \"${ctr}\" curl -sf \"${url}\" >/dev/null 2>&1; then\n log \"${desc} is ready (${elapsed}s)\"\n return 0\n fi\n sleep 2\n elapsed=$((elapsed + 2))\n done\n log \"ERROR: ${desc} not ready after ${max_wait}s\"\n return 1\n }\n\n _wait_for_url \"http://127.0.0.1:6167/_tuwunel/server_version\" agentteams-controller 120 \"Tuwunel (Matrix)\" || exit 1\n _wait_for_url \"http://127.0.0.1:9000/minio/health/live\" agentteams-controller 60 \"MinIO\" || exit 1\n _wait_for_url \"http://127.0.0.1:8080/status\" agentteams-controller 120 \"Higress Gateway\" || exit 1\n\n # Wait for controller to create Manager Agent container\n log \"Waiting for Manager Agent container...\"\n local _mgr_wait=0\n local _mgr_max=300\n while [ $_mgr_wait -lt $_mgr_max ]; do\n if ${DOCKER_CMD} ps --format '{{.Names}}' 2>/dev/null | grep -q \"^agentteams-manager$\"; then\n log \"Manager Agent container detected (${_mgr_wait}s)\"\n break\n fi\n sleep 3\n _mgr_wait=$((_mgr_wait + 3))\n done\n if [ $_mgr_wait -ge $_mgr_max ]; then\n log \"ERROR: Manager Agent container not created after ${_mgr_max}s\"\n log \"Controller logs:\"\n ${DOCKER_CMD} exec agentteams-controller tail -30 /var/log/agentteams/agentteams-controller-error.log 2>/dev/null || true\n exit 1\n fi\n\n # Wait for Manager Agent to be running\n log \"Waiting for Manager Agent to start...\"\n local _agent_wait=0\n while [ $_agent_wait -lt 120 ]; do\n local _state\n _state=$(${DOCKER_CMD} inspect --format '{{.State.Status}}' agentteams-manager 2>/dev/null || echo \"missing\")\n if [ \"${_state}\" = \"running\" ]; then\n log \"Manager Agent is running\"\n break\n fi\n sleep 2\n _agent_wait=$((_agent_wait + 2))\n done\n\n # Enable yolo mode in agent if requested\n if [ \"${AGENTTEAMS_YOLO:-}\" = \"1\" ]; then\n ${DOCKER_CMD} exec agentteams-manager touch /root/manager-workspace/yolo-mode 2>/dev/null || true\n fi\n\n # Wait for the controller to send the first-boot welcome message.\n # The controller gates this on (a) Manager joining the DM room and\n # (b) Higress WASM key-auth propagation actually clearing /v1/chat/completions\n # for the Manager's gateway key — typically ~45-90s on a fresh install.\n # We poll Manager CR Status.WelcomeSent via the in-container agt CLI,\n # exec'd inside agentteams-controller (the source-of-truth container — its\n # bundled CLI binary is always in lockstep with whatever controller\n # binary is currently serving the HTTP API, since they're the same\n # `go build` output. The agentteams-manager container's CLI may lag the\n # controller across image upgrades and silently drop the welcomeSent\n # field, leaving this loop hung). The controller container mints a\n # long-lived admin SA token at startup and writes it to\n # AGENTTEAMS_AUTH_TOKEN_FILE=/var/run/agentteams/cli-token (set as a Dockerfile\n # ENV default), so a bare `docker exec agentteams-controller agt …`\n # auto-discovers both the endpoint and the token. There is a brief\n # window after container start before bootstrapAdminCLIToken completes\n # where the file may be empty / absent — the loop's silent retry\n # handles that the same way it handles the manager-not-yet-running\n # case below.\n if ${DOCKER_CMD} exec agentteams-controller sh -c 'command -v agt' >/dev/null 2>&1; then\n log \"$(msg install.welcome_msg.waiting)\"\n local _welcome_wait=0\n local _welcome_max=\"${AGENTTEAMS_WELCOME_TIMEOUT:-300}\"\n local _welcome_done=0\n while [ $_welcome_wait -lt $_welcome_max ]; do\n # `tr -d` strips whitespace/CR so the grep stays robust to\n # any future change in go-json field ordering or formatting.\n local _wjson\n _wjson=$(${DOCKER_CMD} exec agentteams-controller \\\n agt get managers default -o json 2>/dev/null || true)\n if [ -n \"${_wjson}\" ] && printf '%s' \"${_wjson}\" | tr -d ' \\r\\n' | grep -q '\"welcomeSent\":true'; then\n log \"$(msg install.welcome_msg.confirmed \"${_welcome_wait}\")\"\n _welcome_done=1\n break\n fi\n sleep 3\n _welcome_wait=$((_welcome_wait + 3))\n done\n if [ $_welcome_done -ne 1 ]; then\n # Non-fatal: install is still good. Keep going to the success\n # banner so the admin can use Element Web to nudge Manager into\n # onboarding manually (one DM message is enough).\n log \"$(msg install.welcome_msg.timeout \"${_welcome_max}\")\"\n log \"$(msg install.welcome_msg.timeout_hint)\"\n log \"$(msg install.welcome_msg.timeout_inspect)\"\n fi\n else\n log \"$(msg install.welcome_msg.poll_unavailable)\"\n fi\n\n else\n # ============================================================\n # Legacy architecture: all-in-one manager container\n # ============================================================\n\n NETWORK_ARGS=\"--network agentteams-net\"\n NETWORK_ALIAS_ARGS=\"--network-alias matrix-local.agentteams.io --network-alias aigw-local.agentteams.io --network-alias fs-local.agentteams.io\"\n for _domain in \"${AGENTTEAMS_MATRIX_CLIENT_DOMAIN:-}\" \"${AGENTTEAMS_CONSOLE_DOMAIN:-}\"; do\n if [ -n \"${_domain}\" ] && [[ \"${_domain}\" == *-local.agentteams.io ]]; then\n NETWORK_ALIAS_ARGS=\"${NETWORK_ALIAS_ARGS} --network-alias ${_domain}\"\n fi\n done\n\n # Start Docker API proxy if enabled (security layer between Manager and Docker daemon)\n PROXY_ARGS=\"\"\n if [ \"${AGENTTEAMS_DOCKER_PROXY:-1}\" = \"1\" ] && [ -n \"${CONTAINER_SOCK:-}\" ]; then\n local _proxy_image=\"${AGENTTEAMS_REGISTRY}/higress/agentteams-docker-proxy:${AGENTTEAMS_VERSION}\"\n # Try versioned tag, fallback to latest\n if ! ${DOCKER_CMD} image inspect \"${_proxy_image}\" >/dev/null 2>&1; then\n ${DOCKER_CMD} pull \"${_proxy_image}\" 2>/dev/null || {\n _proxy_image=\"${AGENTTEAMS_REGISTRY}/higress/agentteams-docker-proxy:latest\"\n ${DOCKER_CMD} pull \"${_proxy_image}\" 2>/dev/null || true\n }\n fi\n if ${DOCKER_CMD} image inspect \"${_proxy_image}\" >/dev/null 2>&1; then\n log \"Starting Docker API proxy...\"\n ${DOCKER_CMD} run -d \\\n --name agentteams-docker-proxy \\\n --network agentteams-net \\\n -v \"${CONTAINER_SOCK}:/var/run/docker.sock\" \\\n --security-opt label=disable \\\n -e AGENTTEAMS_WORKER_IMAGE=\"${WORKER_IMAGE}\" \\\n -e AGENTTEAMS_COPAW_WORKER_IMAGE=\"${COPAW_WORKER_IMAGE}\" \\\n -e AGENTTEAMS_HERMES_WORKER_IMAGE=\"${HERMES_WORKER_IMAGE}\" \\\n ${AGENTTEAMS_PROXY_ALLOWED_REGISTRIES:+-e AGENTTEAMS_PROXY_ALLOWED_REGISTRIES=\"${AGENTTEAMS_PROXY_ALLOWED_REGISTRIES}\"} \\\n --restart unless-stopped \\\n \"${_proxy_image}\"\n PROXY_ARGS=\"-e AGENTTEAMS_CONTROLLER_URL=http://agentteams-docker-proxy:2375 -e AGENTTEAMS_CONTAINER_API=http://agentteams-docker-proxy:2375\"\n SOCKET_MOUNT_ARGS=\"\"\n fi\n fi\n\n # Pass host timezone to container\n TZ_ARGS=\"\"\n if [ -n \"${AGENTTEAMS_TIMEZONE:-}\" ]; then\n TZ_ARGS=\"-e TZ=${AGENTTEAMS_TIMEZONE}\"\n fi\n\n YOLO_ARGS=\"\"\n if [ \"${AGENTTEAMS_YOLO:-}\" = \"1\" ]; then\n YOLO_ARGS=\"-e AGENTTEAMS_YOLO=1\"\n fi\n\n MATRIX_DEBUG_ARGS=\"\"\n if [ \"${AGENTTEAMS_MATRIX_DEBUG:-}\" = \"1\" ]; then\n MATRIX_DEBUG_ARGS=\"-e AGENTTEAMS_MATRIX_DEBUG=1\"\n fi\n\n # shellcheck disable=SC2086\n ${DOCKER_CMD} run -d \\\n --name agentteams-manager \\\n --env-file \"${ENV_FILE}\" \\\n -e HOME=/root/manager-workspace \\\n -w /root/manager-workspace \\\n -e HOST_ORIGINAL_HOME=\"${AGENTTEAMS_HOST_SHARE_DIR}\" \\\n -e AGENTTEAMS_MANAGER_RUNTIME=\"${AGENTTEAMS_MANAGER_RUNTIME:-copaw}\" \\\n ${JVM_ARGS:+-e JVM_ARGS=\"${JVM_ARGS}\"} \\\n ${YOLO_ARGS} \\\n ${MATRIX_DEBUG_ARGS} \\\n ${TZ_ARGS} \\\n ${SOCKET_MOUNT_ARGS} \\\n ${NETWORK_ARGS} \\\n ${NETWORK_ALIAS_ARGS} \\\n ${PROXY_ARGS} \\\n -p \"${_port_prefix}${AGENTTEAMS_PORT_GATEWAY}:8080\" \\\n -p \"${_port_prefix}${AGENTTEAMS_PORT_CONSOLE}:8001\" \\\n -p \"${_port_prefix}${AGENTTEAMS_PORT_ELEMENT_WEB:-18088}:8088\" \\\n -p \"127.0.0.1:${AGENTTEAMS_PORT_MANAGER_CONSOLE:-18888}:18888\" \\\n ${DATA_MOUNT_ARGS} \\\n ${WORKSPACE_MOUNT_ARGS} \\\n ${HOST_SHARE_MOUNT_ARGS} \\\n --restart unless-stopped \\\n \"$([ \"${AGENTTEAMS_MANAGER_RUNTIME}\" = \"copaw\" ] && echo \"${MANAGER_COPAW_IMAGE}\" || echo \"${MANAGER_IMAGE}\")\"\n\n # Wait for Manager agent to be ready\n wait_manager_ready \"agentteams-manager\"\n\n # Wait for Matrix server to be ready\n wait_matrix_ready \"agentteams-manager\"\n\n # Post-install verification (non-fatal: warnings only)\n local _verify_script\n _verify_script=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)/agentteams-verify.sh\"\n if [ -f \"${_verify_script}\" ]; then\n bash \"${_verify_script}\" \"agentteams-manager\" || {\n log \"WARNING: Some post-install checks failed. Re-run: bash install/agentteams-verify.sh\"\n }\n fi\n fi\n unset _port_prefix\n\n # ── Start Dashboard (optional) ────────────────────────────────────\n _start_dashboard\n\n # Apply Podman autostart if selected and environment matches\n if [ \"${DOCKER_CMD}\" = \"podman\" ] && [ \"${AGENTTEAMS_PODMAN_AUTOSTART:-0}\" = \"1\" ]; then\n setup_podman_autostart\n fi\n\n log \"\"\n log \"$(msg success.title)\"\n log \"\"\n log \"$(msg success.domains_configured)\"\n log \" ${AGENTTEAMS_MATRIX_DOMAIN%%:*} ${AGENTTEAMS_MATRIX_CLIENT_DOMAIN} ${AGENTTEAMS_AI_GATEWAY_DOMAIN} ${AGENTTEAMS_FS_DOMAIN} ${AGENTTEAMS_CONSOLE_DOMAIN}\"\n log \"\"\n local lan_ip\n lan_ip=$(detect_lan_ip)\n echo -e \"\\033[33m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\033[0m\"\n echo -e \"\\033[33m $(msg success.open_url)\\033[0m\"\n echo -e \"\\033[33m \\033[0m\"\n echo -e \"\\033[1;36m http://127.0.0.1:${AGENTTEAMS_PORT_ELEMENT_WEB:-18088}/#/login\\033[0m\"\n echo -e \"\\033[33m \\033[0m\"\n echo -e \"\\033[33m $(msg success.login_with)\\033[0m\"\n echo -e \"\\033[33m $(msg success.username \"${AGENTTEAMS_ADMIN_USER}\")\\033[0m\"\n echo -e \"\\033[33m $(msg success.password \"${AGENTTEAMS_ADMIN_PASSWORD}\")\\033[0m\"\n echo -e \"\\033[33m \\033[0m\"\n echo -e \"\\033[33m $(msg success.after_login)\\033[0m\"\n echo -e \"\\033[33m $(msg success.tell_it)\\033[0m\"\n echo -e \"\\033[33m $(msg success.manager_auto)\\033[0m\"\n echo -e \"\\033[33m \\033[0m\"\n echo -e \"\\033[33m ───────────────────────────────────────────────────────────────────────────── \\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_title)\\033[0m\"\n echo -e \"\\033[33m \\033[0m\"\n if [ -n \"${lan_ip}\" ]; then\n echo -e \"\\033[33m $(msg success.mobile_step1)\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_step2 \"http://${lan_ip}:${AGENTTEAMS_PORT_GATEWAY}\")\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_step3)\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_username \"${AGENTTEAMS_ADMIN_USER}\")\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_password \"${AGENTTEAMS_ADMIN_PASSWORD}\")\\033[0m\"\n else\n echo -e \"\\033[33m $(msg success.mobile_step1)\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_step2_noip \"${AGENTTEAMS_PORT_GATEWAY}\")\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_noip_hint)\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_step3)\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_username \"${AGENTTEAMS_ADMIN_USER}\")\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_password \"${AGENTTEAMS_ADMIN_PASSWORD}\")\\033[0m\"\n fi\n echo -e \"\\033[33m \\033[0m\"\n echo -e \"\\033[33m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\033[0m\"\n log \"\"\n log \"$(msg success.other_consoles)\"\n log \"$(msg success.higress_console \"${AGENTTEAMS_PORT_CONSOLE}\" \"${AGENTTEAMS_ADMIN_USER}\" \"${AGENTTEAMS_ADMIN_PASSWORD}\")\"\n if [ \"${AGENTTEAMS_DASHBOARD:-0}\" = \"1\" ]; then\n log \"$(msg success.dashboard \"${AGENTTEAMS_PORT_DASHBOARD:-13000}\")\"\n fi\n if [ \"${AGENTTEAMS_USE_EMBEDDED}\" != \"1\" ]; then\n log \"$(msg success.manager_console \"${AGENTTEAMS_PORT_MANAGER_CONSOLE:-18888}\")\"\n log \"$(msg success.manager_console_gateway \"${AGENTTEAMS_ADMIN_USER}\" \"${AGENTTEAMS_ADMIN_PASSWORD}\")\"\n fi\n log \"\"\n log \"$(msg success.switch_llm.title)\"\n log \"$(msg success.switch_llm.hint)\"\n log \"$(msg success.switch_llm.docs)\"\n log \"$(msg success.switch_llm.url)\"\n log \"\"\n log \"$(msg success.tip)\"\n log \"\"\n if [ \"${AGENTTEAMS_LOCAL_ONLY:-1}\" != \"1\" ]; then\n echo -e \"\\033[33m$(msg port.local_only.https_hint)\\033[0m\"\n log \"\"\n fi\n log \"$(msg success.config_file \"${ENV_FILE}\")\"\n log \"$(msg success.data_volume \"${AGENTTEAMS_DATA_DIR}\")\"\n log \"$(msg success.workspace \"${AGENTTEAMS_WORKSPACE_DIR}\")\"\n}\n\n# ============================================================\n# Worker Installation (One-Click)\n# ============================================================\n\ninstall_worker() {\n local WORKER_NAME=\"\"\n local FS=\"\"\n local FS_KEY=\"\"\n local FS_SECRET=\"\"\n local RESET=false\n local SKILLS_API_URL=\"\"\n\n # Parse arguments\n while [ $# -gt 0 ]; do\n case $1 in\n --name) WORKER_NAME=\"$2\"; shift 2 ;;\n --fs) FS=\"$2\"; shift 2 ;;\n --fs-key) FS_KEY=\"$2\"; shift 2 ;;\n --fs-secret) FS_SECRET=\"$2\"; shift 2 ;;\n --skills-api-url) SKILLS_API_URL=\"$2\"; shift 2 ;;\n --reset) RESET=true; shift ;;\n *) die \"$(msg error.unknown_option \"$1\")\" ;;\n esac\n done\n\n # Validate required params\n [ -z \"${WORKER_NAME}\" ] && die \"$(msg error.name_required)\"\n [ -z \"${FS}\" ] && die \"$(msg error.fs_required)\"\n [ -z \"${FS_KEY}\" ] && die \"$(msg error.fs_key_required)\"\n [ -z \"${FS_SECRET}\" ] && die \"$(msg error.fs_secret_required)\"\n\n local CONTAINER_NAME=\"agentteams-worker-${WORKER_NAME}\"\n\n # Handle reset\n if [ \"${RESET}\" = true ]; then\n log \"$(msg worker.resetting \"${WORKER_NAME}\")\"\n ${DOCKER_CMD} stop \"${CONTAINER_NAME}\" 2>/dev/null || true\n ${DOCKER_CMD} rm \"${CONTAINER_NAME}\" 2>/dev/null || true\n fi\n\n # Check for existing container\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' | grep -q \"^${CONTAINER_NAME}$\"; then\n die \"$(msg worker.exists \"${CONTAINER_NAME}\")\"\n fi\n\n log \"$(msg worker.starting \"${WORKER_NAME}\")\"\n\n # Build docker run args\n local DOCKER_ENV=\"\"\n DOCKER_ENV=\"${DOCKER_ENV} -e HOME=/root/agentteams-fs/agents/${WORKER_NAME}\"\n DOCKER_ENV=\"${DOCKER_ENV} -w /root/agentteams-fs/agents/${WORKER_NAME}\"\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_WORKER_NAME=${WORKER_NAME}\"\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_FS_ENDPOINT=${FS}\"\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_FS_ACCESS_KEY=${FS_KEY}\"\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_FS_SECRET_KEY=${FS_SECRET}\"\n\n if [ -z \"${SKILLS_API_URL}\" ]; then\n if [ -n \"${AGENTTEAMS_SKILLS_API_URL:-}\" ]; then\n SKILLS_API_URL=\"${AGENTTEAMS_SKILLS_API_URL}\"\n else\n SKILLS_API_URL=\"nacos://market.agentteams.io:80/public\"\n fi\n fi\n\n # Add SKILLS_API_URL if specified\n DOCKER_ENV=\"${DOCKER_ENV} -e SKILLS_API_URL=${SKILLS_API_URL}\"\n log \"$(msg worker.skills_url \"${SKILLS_API_URL}\")\"\n if [ -n \"${AGENTTEAMS_NACOS_USERNAME:-}\" ]; then\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_NACOS_USERNAME=${AGENTTEAMS_NACOS_USERNAME}\"\n fi\n if [ -n \"${AGENTTEAMS_NACOS_PASSWORD:-}\" ]; then\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_NACOS_PASSWORD=${AGENTTEAMS_NACOS_PASSWORD}\"\n fi\n if [ -n \"${AGENTTEAMS_NACOS_TOKEN:-}\" ]; then\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_NACOS_TOKEN=${AGENTTEAMS_NACOS_TOKEN}\"\n fi\n\n # shellcheck disable=SC2086\n ${DOCKER_CMD} run -d \\\n --name \"${CONTAINER_NAME}\" \\\n ${DOCKER_ENV} \\\n --restart unless-stopped \\\n \"${WORKER_IMAGE}\"\n\n log \"\"\n log \"$(msg worker.started \"${WORKER_NAME}\")\"\n log \"$(msg worker.container \"${CONTAINER_NAME}\")\"\n log \"$(msg worker.view_logs \"${CONTAINER_NAME}\")\"\n}\n\n# ============================================================\n# Main\n# ============================================================\n\n# ============================================================\n# LLM API connectivity test\n# ============================================================\n\ntest_llm_connectivity() {\n local base_url=\"$1\"\n local api_key=\"$2\"\n local model=\"$3\"\n local hint=\"${4:-}\" # optional: extra hint shown on failure\n if ! command -v curl >/dev/null 2>&1; then\n echo -e \"\\033[33m$(msg llm.openai.test.no_curl)\\033[0m\"\n return\n fi\n log \"$(msg llm.openai.test.testing)\"\n local _body _http_code _tmpfile\n _tmpfile=$(mktemp)\n _http_code=$(curl -s -o \"${_tmpfile}\" -w \"%{http_code}\" \\\n -X POST \"${base_url%/}/chat/completions\" \\\n -H \"Authorization: Bearer ${api_key}\" \\\n -H \"Content-Type: application/json\" \\\n -H \"User-Agent: AgentTeams/${AGENTTEAMS_VERSION:-latest}\" \\\n --max-time 30 \\\n -d \"{\\\"model\\\":\\\"${model}\\\",\\\"messages\\\":[{\\\"role\\\":\\\"user\\\",\\\"content\\\":\\\"hi\\\"}]}\" \\\n 2>/dev/null)\n _body=$(cat \"${_tmpfile}\")\n rm -f \"${_tmpfile}\"\n if [ \"${_http_code}\" = \"200\" ] || [ \"${_http_code}\" = \"201\" ]; then\n log \"$(msg llm.openai.test.ok)\"\n else\n echo -e \"\\033[33m$(msg llm.openai.test.fail \"${_http_code}\" \"${_body}\")\\033[0m\"\n if [ -n \"${hint}\" ]; then\n echo -e \"\\033[33m${hint}\\033[0m\"\n fi\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" != \"1\" ]; then\n local _confirm\n read -e -p \"$(msg llm.openai.test.confirm)\" _confirm\n if [ \"${_confirm}\" = \"b\" ]; then\n STEP_RESULT=\"back\"\n return 1\n fi\n if [ \"${_confirm}\" != \"y\" ] && [ \"${_confirm}\" != \"Y\" ]; then\n log \"$(msg llm.openai.test.aborted)\"\n exit 1\n fi\n fi\n fi\n}\n\ntest_embedding_connectivity() {\n local base_url=\"$1\"\n local api_key=\"$2\"\n local model=\"$3\"\n if ! command -v curl >/dev/null 2>&1; then\n return 0\n fi\n log \"$(msg llm.embedding.test.testing)\"\n local _body _http_code _tmpfile\n _tmpfile=$(mktemp)\n _http_code=$(curl -s -o \"${_tmpfile}\" -w \"%{http_code}\" \\\n -X POST \"${base_url%/}/embeddings\" \\\n -H \"Authorization: Bearer ${api_key}\" \\\n -H \"Content-Type: application/json\" \\\n -H \"User-Agent: AgentTeams/${AGENTTEAMS_VERSION:-latest}\" \\\n --max-time 30 \\\n -d \"{\\\"model\\\":\\\"${model}\\\",\\\"input\\\":\\\"test\\\"}\" \\\n 2>/dev/null)\n _body=$(cat \"${_tmpfile}\")\n rm -f \"${_tmpfile}\"\n if [ \"${_http_code}\" = \"200\" ] || [ \"${_http_code}\" = \"201\" ]; then\n log \"$(msg llm.embedding.test.ok)\"\n return 0\n else\n echo -e \"\\033[33m$(msg llm.embedding.test.fail \"${_http_code}\" \"${_body}\")\\033[0m\"\n return 1\n fi\n}\n\n# ============================================================\n# Uninstall\n# ============================================================\n\nuninstall_agentteams() {\n log \"$(msg uninstall.title)\"\n\n # Safely disable and remove the dedicated AgentTeams Podman autostart service\n if [ \"${DOCKER_CMD}\" = \"podman\" ] && command -v systemctl >/dev/null 2>&1; then\n local _systemctl_cmd=\"systemctl\"\n local _service_dir=\"/etc/systemd/system\"\n\n if [ \"$(id -u)\" != \"0\" ]; then\n _systemctl_cmd=\"systemctl --user\"\n _service_dir=\"${HOME}/.config/systemd/user\"\n fi\n\n if [ -f \"${_service_dir}/agentteams-podman-restart.service\" ]; then\n log \"Disabling and removing dedicated AgentTeams Podman autostart service...\"\n ${_systemctl_cmd} disable --now agentteams-podman-restart.service 2>/dev/null || true\n rm -f \"${_service_dir}/agentteams-podman-restart.service\"\n ${_systemctl_cmd} daemon-reload 2>/dev/null || true\n fi\n # Note: Native podman-restart.service is strictly untouched per maintainer review.\n fi\n\n # Capture manager image before removing (needed for workspace cleanup on Linux)\n local manager_image=\"\"\n manager_image=$(${DOCKER_CMD} inspect agentteams-manager --format '{{.Config.Image}}' 2>/dev/null || true)\n\n # Stop and remove manager\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -q \"^agentteams-manager$\"; then\n log \"$(msg uninstall.stopping_manager)\"\n ${DOCKER_CMD} stop agentteams-manager >/dev/null 2>&1 || true\n ${DOCKER_CMD} rm agentteams-manager >/dev/null 2>&1 || true\n fi\n\n # Stop and remove workers\n local workers\n workers=$(${DOCKER_CMD} ps -a --filter \"name=agentteams-worker-\" --format '{{.Names}}' 2>/dev/null || true)\n if [ -n \"${workers}\" ]; then\n log \"$(msg uninstall.stopping_workers)\"\n echo \"${workers}\" | while read -r w; do\n ${DOCKER_CMD} rm -f \"${w}\" >/dev/null 2>&1 || true\n log \"$(msg uninstall.removed \"${w}\")\"\n done\n fi\n\n # Stop and remove the dashboard container (installed via step_dashboard)\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -q \"^agentteams-dashboard$\"; then\n log \"Stopping and removing agentteams-dashboard...\"\n ${DOCKER_CMD} stop agentteams-dashboard >/dev/null 2>&1 || true\n ${DOCKER_CMD} rm agentteams-dashboard >/dev/null 2>&1 || true\n fi\n if ${DOCKER_CMD} volume inspect agentteams-dashboard-data >/dev/null 2>&1; then\n log \"Removing dashboard data volume: agentteams-dashboard-data\"\n ${DOCKER_CMD} volume rm agentteams-dashboard-data >/dev/null 2>&1 || true\n fi\n\n # Stop and remove docker-proxy (legacy ≤ v1.0.x; current arch uses\n # agentteams-controller for the same role)\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -q \"^agentteams-docker-proxy$\"; then\n log \"$(msg uninstall.removing_proxy)\"\n ${DOCKER_CMD} stop agentteams-docker-proxy >/dev/null 2>&1 || true\n ${DOCKER_CMD} rm agentteams-docker-proxy >/dev/null 2>&1 || true\n fi\n\n # Stop and remove the embedded controller container. MUST happen\n # before the `docker volume rm agentteams-data` step below — in embedded\n # mode agentteams-controller mounts agentteams-data at /data (Tuwunel DB,\n # MinIO state, Higress state, all the room messages), and `volume\n # rm` against an in-use volume fails silently because of the trailing\n # `|| true`. Skipping this used to leave room/message history behind\n # across \"uninstall + reinstall\" cycles. See PR #692.\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -q \"^agentteams-controller$\"; then\n log \"$(msg uninstall.stopping_controller)\"\n ${DOCKER_CMD} stop agentteams-controller >/dev/null 2>&1 || true\n ${DOCKER_CMD} rm agentteams-controller >/dev/null 2>&1 || true\n fi\n\n # Read env file for data/workspace info before removing\n local env_file=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n [ ! -f \"${env_file}\" ] && [ -f \"./agentteams-manager.env\" ] && env_file=\"./agentteams-manager.env\"\n\n local data_dir=\"\" workspace_dir=\"\"\n if [ -f \"${env_file}\" ]; then\n data_dir=$(grep '^AGENTTEAMS_DATA_DIR=' \"${env_file}\" 2>/dev/null | cut -d= -f2- || true)\n workspace_dir=$(grep '^AGENTTEAMS_WORKSPACE_DIR=' \"${env_file}\" 2>/dev/null | cut -d= -f2- || true)\n fi\n\n # Remove Docker volume\n local vol=\"${data_dir:-agentteams-data}\"\n if ${DOCKER_CMD} volume inspect \"${vol}\" >/dev/null 2>&1; then\n log \"$(msg uninstall.removing_volume \"${vol}\")\"\n ${DOCKER_CMD} volume rm \"${vol}\" >/dev/null 2>&1 || true\n fi\n\n # Remove Docker network\n if ${DOCKER_CMD} network ls --format '{{.Name}}' 2>/dev/null | grep -q \"^agentteams-net$\"; then\n log \"$(msg uninstall.removing_network)\"\n ${DOCKER_CMD} network rm agentteams-net >/dev/null 2>&1 || true\n fi\n\n # Remove workspace directory\n if [ -n \"${workspace_dir}\" ] && [ -d \"${workspace_dir}\" ]; then\n log \"$(msg uninstall.removing_workspace \"${workspace_dir}\")\"\n rm -rf \"${workspace_dir}\" 2>/dev/null || true\n if [ -d \"${workspace_dir}\" ]; then\n log \"$(msg uninstall.removing_workspace_elevated)\"\n local _parent _base\n _parent=$(dirname \"${workspace_dir}\")\n _base=$(basename \"${workspace_dir}\")\n local _rm_image=\"${manager_image:-busybox}\"\n ${DOCKER_CMD} run --rm --entrypoint sh \\\n -v \"${_parent}:/host-parent\" \\\n \"${_rm_image}\" \\\n -c \"rm -rf /host-parent/${_base}\" 2>/dev/null || true\n fi\n fi\n\n # Remove env file\n if [ -f \"${env_file}\" ]; then\n log \"$(msg uninstall.removing_env \"${env_file}\")\"\n rm -f \"${env_file}\"\n fi\n\n # Remove install log\n if [ -f \"${HOME}/agentteams-install.log\" ]; then\n log \"$(msg uninstall.removing_log \"${HOME}/agentteams-install.log\")\"\n rm -f \"${HOME}/agentteams-install.log\"\n fi\n\n echo \"\"\n log \"$(msg uninstall.done)\"\n}\n\n# ============================================================\n# Check container runtime (docker or podman) and environment\n# ============================================================\ncheck_container_runtime() {\n # Ensure the rootless environment baseline (XDG_RUNTIME_DIR) is ready before engine detection.\n if [ \"$(id -u)\" -ne 0 ] && [ -z \"${XDG_RUNTIME_DIR:-}\" ]; then\n export XDG_RUNTIME_DIR=\"/run/user/$(id -u)\"\n fi\n\n if command -v docker >/dev/null 2>&1; then\n DOCKER_CMD=\"docker\"\n # Check for podman disguised as docker (alias or podman-docker package)\n if docker --version 2>/dev/null | grep -qi \"podman\"; then\n DOCKER_CMD=\"podman\"\n fi\n elif command -v podman >/dev/null 2>&1; then\n DOCKER_CMD=\"podman\"\n else\n echo -e \"\\033[31m[AgentTeams ERROR]\\033[0m $(msg error.docker_not_found)\" >&2\n exit 1\n fi\n\n # Unified health check (Validates runtime responsiveness)\n if ! ${DOCKER_CMD} ps >/dev/null 2>&1; then\n echo -e \"\\033[31m[AgentTeams ERROR]\\033[0m $(msg error.docker_not_running)\" >&2\n exit 1\n fi\n}\n\ncheck_container_runtime\n\ncase \"${1:-}\" in\n manager|\"\")\n install_manager\n ;;\n worker)\n shift\n install_worker \"$@\"\n ;;\n dashboard)\n check_container_runtime\n load_current_params_from_env\n AGENTTEAMS_DASHBOARD=\"${AGENTTEAMS_DASHBOARD:-1}\"\n AGENTTEAMS_DASHBOARD_VERSION=\"${AGENTTEAMS_DASHBOARD_VERSION:-v1.2.0-beta.1}\"\n AGENTTEAMS_PORT_DASHBOARD=\"${AGENTTEAMS_PORT_DASHBOARD:-13000}\"\n AGENTTEAMS_DASHBOARD_IMAGE=\"${AGENTTEAMS_DASHBOARD_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-dashboard:${AGENTTEAMS_DASHBOARD_VERSION}}\"\n AGENTTEAMS_USE_EMBEDDED=1\n _start_dashboard\n ;;\n uninstall)\n uninstall_agentteams\n ;;\n *)\n echo \"Usage: $0 [manager|worker [options]|dashboard|uninstall]\"\n echo \"\"\n echo \"Commands:\"\n echo \" manager Interactive Manager installation (default)\"\n echo \" Choose Quick Start (all defaults) or Manual mode\"\n echo \" worker Worker installation (requires --name and connection params)\"\n echo \" dashboard Start/stop agentteams-dashboard (use with AGENTTEAMS_DASHBOARD=0 to stop)\"\n echo \" uninstall Stop and remove Manager + all Worker containers\"\n echo \"\"\n echo \"Quick Start (fastest):\"\n echo \" $0\"\n echo \" # Then select '1' for Quick Start mode\"\n echo \"\"\n echo \"Non-interactive (for automation):\"\n echo \" AGENTTEAMS_NON_INTERACTIVE=1 AGENTTEAMS_LLM_API_KEY=sk-xxx $0\"\n echo \"\"\n echo \"Worker Options:\"\n echo \" --name <name> Worker name (required)\"\n echo \" --fs <url> MinIO endpoint URL (required)\"\n echo \" --fs-key <key> MinIO access key (required)\"\n echo \" --fs-secret <secret> MinIO secret key (required)\"\n echo \" --reset Remove existing Worker container before creating\"\n exit 1\n ;;\nesac\n"}, "8de237da736a542766e132836b29c0a2a9c48740": {"t": 1785764743.9787395, "n": 247910, "h": "6a9766a819dceda9", "c": "#!/bin/bash\n# agentteams-install.sh - One-click installation for AgentTeams Manager, Worker, and Dashboard\n#\n# Usage:\n# ./agentteams-install.sh # Interactive installation (choose Quick Start or Manual)\n# ./agentteams-install.sh manager # Same as above (explicit)\n# ./agentteams-install.sh worker --name <name> ... # Worker installation\n# ./agentteams-install.sh dashboard # Start/stop agentteams-dashboard (AGENTTEAMS_DASHBOARD=0 to stop)\n# ./agentteams-install.sh uninstall # Stop and remove Manager + all Workers\n#\n# NOTE: agentteams-dashboard is currently only supported via the Bash installer.\n# The PowerShell installer (agentteams-install.ps1) does not yet include dashboard support.\n#\n# Onboarding Modes:\n# Quick Start - Fast installation with all default values (recommended)\n# Manual - Customize each option step by step\n#\n# Environment variables (for automation):\n# AGENTTEAMS_NON_INTERACTIVE Skip all prompts, use defaults (default: 0)\n# AGENTTEAMS_LLM_PROVIDER LLM provider (default: openai-compat for zh non-interactive Token Plan; qwen for en)\n# AGENTTEAMS_DEFAULT_MODEL Default model (default: qwen3.6-plus for zh Token Plan and en non-interactive)\n# AGENTTEAMS_OPENAI_BASE_URL OpenAI-compatible base URL (default for zh non-interactive: Alibaba Token Plan endpoint)\n# AGENTTEAMS_LLM_API_KEY LLM API key (required)\n# AGENTTEAMS_ADMIN_USER Admin username (default: admin)\n# AGENTTEAMS_ADMIN_PASSWORD Admin password (auto-generated if not set, min 8 chars)\n# AGENTTEAMS_MATRIX_DOMAIN Matrix domain (default: matrix-local.agentteams.io:18080)\n# AGENTTEAMS_MOUNT_SOCKET Mount container runtime socket (default: 1)\n# AGENTTEAMS_DATA_DIR Docker volume name for persistent data (default: agentteams-data)\n# AGENTTEAMS_WORKSPACE_DIR Host directory for manager workspace (default: ~/agentteams-manager)\n# AGENTTEAMS_VERSION Image tag (default: latest)\n# AGENTTEAMS_REGISTRY Image registry (default: auto-detected by timezone)\n# AGENTTEAMS_INSTALL_MANAGER_IMAGE Override manager image (e.g., local build)\n# AGENTTEAMS_INSTALL_WORKER_IMAGE Override worker image (e.g., local build)\n# AGENTTEAMS_INSTALL_COPAW_WORKER_IMAGE Override copaw worker image (e.g., local build)\n# AGENTTEAMS_INSTALL_HERMES_WORKER_IMAGE Override hermes worker image (e.g., local build)\n# AGENTTEAMS_NACOS_REGISTRY_URI Default Nacos registry URI for Worker market search/import\n# (default: nacos://market.agentteams.io:80/public)\n# AGENTTEAMS_NACOS_USERNAME Default Nacos username for nacos:// package imports (optional)\n# AGENTTEAMS_NACOS_PASSWORD Default Nacos password for nacos:// package imports (optional)\n# AGENTTEAMS_CMS_TRACES_ENABLED Enable openclaw-cms-plugin traces for Manager AND all Workers (default: false)\n# AGENTTEAMS_CMS_ENDPOINT ARMS OTLP endpoint (required if traces enabled)\n# AGENTTEAMS_CMS_LICENSE_KEY CMS license key (required if traces enabled)\n# AGENTTEAMS_CMS_PROJECT CMS project name (optional)\n# AGENTTEAMS_CMS_WORKSPACE CMS workspace ID (required if traces enabled)\n# AGENTTEAMS_CMS_SERVICE_NAME Manager service name in ARMS (default: agentteams-manager)\n# Workers always report as agentteams-worker-<name> automatically\n# AGENTTEAMS_CMS_METRICS_ENABLED Enable diagnostics-otel metrics for Manager AND all Workers (default: false)\n# AGENTTEAMS_PORT_GATEWAY Host port for Higress gateway (default: 18080)\n# AGENTTEAMS_PORT_CONSOLE Host port for Higress console (default: 18001)\n# AGENTTEAMS_PORT_ELEMENT_WEB Host port for Element Web direct access (default: 18088)\n# AGENTTEAMS_PORT_MANAGER_CONSOLE Host port for Manager console (default: 18888)\n# AGENTTEAMS_WORKER_IDLE_TIMEOUT Worker idle timeout in minutes (default: 720, i.e. 12 hours)\n# AGENTTEAMS_DASHBOARD Install agentteams-dashboard management UI (default: 1)\n# AGENTTEAMS_DASHBOARD_VERSION Dashboard version (default: v1.2.0-beta.1, independent of AgentTeams version)\n# AGENTTEAMS_PORT_DASHBOARD Dashboard host port (default: 13000)\n# AGENTTEAMS_DASHBOARD_IMAGE Override dashboard image (default: <registry>/agentteams/agentteams-dashboard:<DASHBOARD_VERSION>)\n# AGENTTEAMS_AI_GATEWAY_ADMIN_URL Higress Console URL for shared auth (auto-detected)\n\nset -e\n\nAGENTTEAMS_VERSION=\"${AGENTTEAMS_VERSION:-}\"\nAGENTTEAMS_KNOWN_STABLE_VERSION=\"v1.1.2\" # fallback if GitHub API is unreachable\n\n_normalize_version() {\n local version=\"$1\"\n case \"${version}\" in\n [0-9]*.[0-9]*.[0-9]*) version=\"v${version}\" ;;\n esac\n case \"${version}\" in\n v[0-9]*.[0-9]*.[0-9]*.beta.[0-9]*) version=\"${version/.beta./-beta.}\" ;;\n esac\n printf '%s' \"${version}\"\n}\n\n# Returns 0 (true) if $1 < $2 using semver order; \"latest\" is treated as greatest\n_ver_lt() {\n [ \"$1\" = \"latest\" ] && return 1\n [ \"$2\" = \"latest\" ] && return 0\n [ \"$1\" = \"$2\" ] && return 1\n [ \"$(printf '%s\\n%s' \"$1\" \"$2\" | sort -V | head -1)\" = \"$1\" ]\n}\n\n_use_legacy_image_env() {\n if [ \"$1\" = \"latest\" ]; then\n _ver_lt \"${AGENTTEAMS_KNOWN_STABLE_VERSION}\" \"v1.2.0\"\n else\n _ver_lt \"$1\" \"v1.2.0\"\n fi\n}\n\n_controller_env_prefix() {\n if _use_legacy_image_env \"$1\"; then\n printf '%s%s' 'HIC' 'LAW_'\n else\n printf '%s' 'AGENTTEAMS_'\n fi\n}\nAGENTTEAMS_NON_INTERACTIVE=\"${AGENTTEAMS_NON_INTERACTIVE:-0}\"\nAGENTTEAMS_MOUNT_SOCKET=\"${AGENTTEAMS_MOUNT_SOCKET:-1}\"\nAGENTTEAMS_DOCKER_PROXY=\"${AGENTTEAMS_DOCKER_PROXY:-1}\"\nSTEP_RESULT=\"\" # Used by state machine to signal \"back\" navigation\n\n# ============================================================\n# Log all output to file\n# ============================================================\n\nAGENTTEAMS_LOG_FILE=\"${HOME}/agentteams-install.log\"\n\nif [ \"${1:-}\" != \"uninstall\" ]; then\n # Redirect all output (stdout and stderr) to both terminal and log file\n exec > >(tee -a \"${AGENTTEAMS_LOG_FILE}\") 2>&1\n\n echo \"\"\n echo \"========================================\"\n echo \"AgentTeams Installation Log\"\n echo \"Started: $(date)\"\n echo \"User: $(whoami)\"\n echo \"System: $(uname -a)\"\n echo \"Log file: ${AGENTTEAMS_LOG_FILE}\"\n echo \"========================================\"\n echo \"\"\nfi\n\n# ============================================================\n# Utility functions (needed early for timezone detection)\n# ============================================================\n\nlog() {\n echo -e \"\\033[36m[AgentTeams]\\033[0m $1\"\n}\n\nerror() {\n echo -e \"\\033[31m[AgentTeams ERROR]\\033[0m $1\" >&2\n}\n\ndie() {\n error \"$1\"\n exit 1\n}\n\n# ============================================================\n# Timezone detection (compatible with Linux and macOS)\n# ============================================================\n\ndetect_timezone() {\n local tz=\"\"\n\n # Try /etc/timezone (Debian/Ubuntu)\n if [ -f /etc/timezone ]; then\n tz=$(cat /etc/timezone 2>/dev/null | tr -d '[:space:]')\n fi\n\n # Try /etc/localtime symlink (macOS and some Linux)\n if [ -z \"${tz}\" ] && [ -L /etc/localtime ]; then\n tz=$(ls -l /etc/localtime 2>/dev/null | sed 's|.*/zoneinfo/||')\n fi\n\n # Try timedatectl (systemd)\n if [ -z \"${tz}\" ]; then\n tz=$(timedatectl show --value -p Timezone 2>/dev/null)\n fi\n\n # If still not detected, warn and prompt user. Diagnostics go to stderr —\n # this function returns its value via stdout to `$(detect_timezone)`, so\n # any echo here would leak into AGENTTEAMS_TIMEZONE and downstream image tags.\n if [ -z \"${tz}\" ]; then\n echo \"\" >&2\n echo -e \"\\033[33m[AgentTeams WARNING]\\033[0m Could not detect timezone automatically.\" >&2\n echo -e \"\\033[33m[AgentTeams]\\033[0m Please enter your timezone (e.g., Asia/Shanghai, America/New_York).\" >&2\n echo \"\" >&2\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n tz=\"Asia/Shanghai\"\n log \"Using default timezone: ${tz}\" >&2\n else\n read -e -p \"Timezone [Asia/Shanghai]: \" tz\n tz=\"${tz:-Asia/Shanghai}\"\n fi\n fi\n\n echo \"${tz}\"\n}\n\n# Detect timezone once at startup (used by registry selection and container TZ)\nAGENTTEAMS_TIMEZONE=\"${AGENTTEAMS_TIMEZONE:-$(detect_timezone)}\"\n\n# ============================================================\n# Language detection based on timezone\n# ============================================================\n\ndetect_language() {\n local tz=\"${AGENTTEAMS_TIMEZONE}\"\n case \"${tz}\" in\n Asia/Shanghai|Asia/Chongqing|Asia/Harbin|Asia/Urumqi|\\\n Asia/Taipei|Asia/Hong_Kong|Asia/Macau)\n echo \"zh\"\n ;;\n *)\n echo \"en\"\n ;;\n esac\n}\n\n# Language priority: env var > existing env file > timezone detection\nif [ -z \"${AGENTTEAMS_LANGUAGE}\" ]; then\n # Check existing env file for saved language preference (upgrade scenario)\n _env_file=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n # Migrate from legacy location (current directory) if needed\n if [ ! -f \"${_env_file}\" ] && [ -f \"./agentteams-manager.env\" ]; then\n mv \"./agentteams-manager.env\" \"${_env_file}\" 2>/dev/null || true\n fi\n if [ -f \"${_env_file}\" ]; then\n _saved_lang=$(grep '^AGENTTEAMS_LANGUAGE=' \"${_env_file}\" 2>/dev/null | cut -d= -f2-)\n if [ -n \"${_saved_lang}\" ]; then\n AGENTTEAMS_LANGUAGE=\"${_saved_lang}\"\n fi\n fi\n # Fall back to timezone-based detection\n if [ -z \"${AGENTTEAMS_LANGUAGE}\" ]; then\n AGENTTEAMS_LANGUAGE=\"$(detect_language)\"\n fi\n unset _env_file _saved_lang\nfi\nexport AGENTTEAMS_LANGUAGE\n\n# ============================================================\n# Centralized message dictionary and msg() function\n# Compatible with bash 3.2+ (macOS default) — uses case instead of declare -A\n# ============================================================\n\n# msg() function: look up message by key, with printf-style argument substitution\n# Falls back to English if the current language translation is missing.\nmsg() {\n local key=\"$1\"\n shift\n local lang=\"${AGENTTEAMS_LANGUAGE:-en}\"\n local text=\"\"\n case \"${key}.${lang}\" in\n # --- Timezone detection messages ---\n \"tz.warning.title.zh\") text=\"无法自动检测时区。\" ;;\n \"tz.warning.title.en\") text=\"Could not detect timezone automatically.\" ;;\n \"tz.warning.prompt.zh\") text=\"请输入您的时区(例如 Asia/Shanghai、America/New_York)。\" ;;\n \"tz.warning.prompt.en\") text=\"Please enter your timezone (e.g., Asia/Shanghai, America/New_York).\" ;;\n \"tz.default.zh\") text=\"使用默认时区: %s\" ;;\n \"tz.default.en\") text=\"Using default timezone: %s\" ;;\n \"tz.input_prompt.zh\") text=\"时区\" ;;\n \"tz.input_prompt.en\") text=\"Timezone\" ;;\n # --- Installation title and info ---\n \"install.title.zh\") text=\"=== AgentTeams Manager 安装 ===\" ;;\n \"install.title.en\") text=\"=== AgentTeams Manager Installation ===\" ;;\n \"install.registry.zh\") text=\"镜像仓库: %s\" ;;\n \"install.registry.en\") text=\"Registry: %s\" ;;\n \"install.dir.zh\") text=\"安装目录: %s\" ;;\n \"install.dir.en\") text=\"Installation directory: %s\" ;;\n \"install.dir_hint.zh\") text=\" (env 文件 'agentteams-manager.env' 将保存到 HOME 目录。)\" ;;\n \"install.dir_hint.en\") text=\" (The env file 'agentteams-manager.env' will be saved to your HOME directory.)\" ;;\n \"install.dir_hint2.zh\") text=\" (请从您希望管理此安装的目录运行此脚本。)\" ;;\n \"install.dir_hint2.en\") text=\" (Run this script from the directory where you want to manage this installation.)\" ;;\n # --- Onboarding mode ---\n \"install.mode.title.zh\") text=\"--- Onboarding 模式 ---\" ;;\n \"install.mode.title.en\") text=\"--- Onboarding Mode ---\" ;;\n \"install.mode.choose.zh\") text=\"选择安装模式:\" ;;\n \"install.mode.choose.en\") text=\"Choose your installation mode:\" ;;\n \"install.mode.quickstart.zh\") text=\" 1) 快速开始 - 使用阿里云通义 Token 套餐快速安装(推荐)\" ;;\n \"install.mode.quickstart.en\") text=\" 1) Quick Start - Fast installation with Qwen Cloud (recommended)\" ;;\n \"install.mode.manual.zh\") text=\" 2) 手动配置 - 选择 LLM 提供商并自定义选项\" ;;\n \"install.mode.manual.en\") text=\" 2) Manual - Choose LLM provider and customize options\" ;;\n \"install.mode.prompt.zh\") text=\"请选择 [1/2]\" ;;\n \"install.mode.prompt.en\") text=\"Enter choice [1/2]\" ;;\n \"install.mode.quickstart_selected.zh\") text=\"已选择快速开始模式 - 使用阿里云通义 Token 套餐\" ;;\n \"install.mode.quickstart_selected.en\") text=\"Quick Start mode selected - using Qwen Cloud\" ;;\n \"install.mode.manual_selected.zh\") text=\"已选择手动配置模式 - 您将选择 LLM 提供商并自定义选项\" ;;\n \"install.mode.manual_selected.en\") text=\"Manual mode selected - you will choose LLM provider and customize options\" ;;\n \"install.mode.invalid.zh\") text=\"无效选择,默认使用快速开始模式\" ;;\n \"install.mode.invalid.en\") text=\"Invalid choice, defaulting to Quick Start mode\" ;;\n # --- Upgrade mode sub-menu ---\n \"upgrade.mode.prompt.zh\") text=\"请选择升级方式:\" ;;\n \"upgrade.mode.prompt.en\") text=\"Select upgrade mode:\" ;;\n \"upgrade.mode.keep_all.zh\") text=\" 1) 保留所有参数,一键升级(推荐)\" ;;\n \"upgrade.mode.keep_all.en\") text=\" 1) Keep all parameters, quick upgrade (recommended)\" ;;\n \"upgrade.mode.confirm_each.zh\") text=\" 2) 逐个确认参数(可修改)\" ;;\n \"upgrade.mode.confirm_each.en\") text=\" 2) Confirm each parameter (can modify)\" ;;\n \"upgrade.mode.back.zh\") text=\" 3) 返回\" ;;\n \"upgrade.mode.back.en\") text=\" 3) Back\" ;;\n # --- Version selection ---\n \"install.version.title.zh\") text=\"--- 版本选择 ---\" ;;\n \"install.version.title.en\") text=\"--- Version Selection ---\" ;;\n \"install.version.choose.zh\") text=\"选择要安装的版本:\" ;;\n \"install.version.choose.en\") text=\"Choose the version to install:\" ;;\n \"install.version.option_latest.zh\") text=\" 1) latest - 最新版(默认)\" ;;\n \"install.version.option_latest.en\") text=\" 1) latest - Latest build (default)\" ;;\n \"install.version.option_stable.zh\") text=\" 2) %s - 最新稳定版\" ;;\n \"install.version.option_stable.en\") text=\" 2) %s - Latest stable release\" ;;\n \"install.version.fetching.zh\") text=\"正在查询最新稳定版本...\" ;;\n \"install.version.fetching.en\") text=\"Fetching latest stable release...\" ;;\n \"install.version.fetch_failed.zh\") text=\"无法查询 GitHub,使用内置版本 %s\" ;;\n \"install.version.fetch_failed.en\") text=\"Could not reach GitHub, using built-in version %s\" ;;\n \"install.version.option_custom.zh\") text=\" 3) 自定义 - 手动输入版本号(如 v1.0.5)\" ;;\n \"install.version.option_custom.en\") text=\" 3) Custom - Enter a specific version (e.g. v1.0.5)\" ;;\n \"install.version.prompt.zh\") text=\"请选择 [1/2/3]\" ;;\n \"install.version.prompt.en\") text=\"Enter choice [1/2/3]\" ;;\n \"install.version.custom_prompt.zh\") text=\"请输入版本号\" ;;\n \"install.version.custom_prompt.en\") text=\"Enter version tag\" ;;\n \"install.version.selected_latest.zh\") text=\"已选择最新版 (latest)\" ;;\n \"install.version.selected_latest.en\") text=\"Selected latest version\" ;;\n \"install.version.selected_stable.zh\") text=\"已选择最新稳定版 (%s)\" ;;\n \"install.version.selected_stable.en\") text=\"Selected latest stable version (%s)\" ;;\n \"install.version.selected_custom.zh\") text=\"已选择自定义版本 (%s)\" ;;\n \"install.version.selected_custom.en\") text=\"Selected custom version (%s)\" ;;\n \"install.version.invalid.zh\") text=\"无效选择,使用最新稳定版 (%s)\" ;;\n \"install.version.invalid.en\") text=\"Invalid choice, defaulting to latest stable version (%s)\" ;;\n # --- Existing installation detected ---\n \"install.existing.detected.zh\") text=\"检测到已有 Manager 安装(env 文件: %s)\" ;;\n \"install.existing.detected.en\") text=\"Existing Manager installation detected (env file: %s)\" ;;\n \"install.existing.choose.zh\") text=\"选择操作:\" ;;\n \"install.existing.choose.en\") text=\"Choose an action:\" ;;\n \"install.existing.upgrade.zh\") text=\" 1) 就地升级(保留数据、工作空间、env 文件)\" ;;\n \"install.existing.upgrade.en\") text=\" 1) In-place upgrade (keep data, workspace, env file)\" ;;\n \"install.existing.reinstall.zh\") text=\" 2) 全新重装(删除所有数据,重新开始)\" ;;\n \"install.existing.reinstall.en\") text=\" 2) Clean reinstall (remove all data, start fresh)\" ;;\n \"install.existing.cancel.zh\") text=\" 3) 取消\" ;;\n \"install.existing.cancel.en\") text=\" 3) Cancel\" ;;\n \"install.existing.prompt.zh\") text=\"请选择 [1/2/3]\" ;;\n \"install.existing.prompt.en\") text=\"Enter choice [1/2/3]\" ;;\n \"install.existing.upgrade_noninteractive.zh\") text=\"非交互模式: 执行就地升级...\" ;;\n \"install.existing.upgrade_noninteractive.en\") text=\"Non-interactive mode: performing in-place upgrade...\" ;;\n \"install.existing.upgrading.zh\") text=\"执行就地升级...\" ;;\n \"install.existing.upgrading.en\") text=\"Performing in-place upgrade...\" ;;\n \"install.existing.warn_manager_stop.zh\") text=\"⚠️ Manager 容器将被停止并重新创建。\" ;;\n \"install.existing.warn_manager_stop.en\") text=\"⚠️ Manager container will be stopped and recreated.\" ;;\n \"install.existing.warn_worker_recreate.zh\") text=\"⚠️ Worker 容器也将被重新创建(以更新 Manager IP)。\" ;;\n \"install.existing.warn_worker_recreate.en\") text=\"⚠️ Worker containers will also be recreated (to update Manager IP in hosts).\" ;;\n \"install.existing.continue_prompt.zh\") text=\"继续?[y/N]\" ;;\n \"install.existing.continue_prompt.en\") text=\"Continue? [y/N]\" ;;\n \"install.existing.cancelled.zh\") text=\"安装已取消。\" ;;\n \"install.existing.cancelled.en\") text=\"Installation cancelled.\" ;;\n \"install.existing.stopping_manager.zh\") text=\"停止并移除现有 manager 容器...\" ;;\n \"install.existing.stopping_manager.en\") text=\"Stopping and removing existing manager container...\" ;;\n \"install.existing.stopping_workers.zh\") text=\"停止并移除现有 worker 容器...\" ;;\n \"install.existing.stopping_workers.en\") text=\"Stopping and removing existing worker containers...\" ;;\n \"install.existing.removed.zh\") text=\" 已移除: %s\" ;;\n \"install.existing.removed.en\") text=\" Removed: %s\" ;;\n # --- Clean reinstall messages ---\n \"install.reinstall.performing.zh\") text=\"执行全新重装...\" ;;\n \"install.reinstall.performing.en\") text=\"Performing clean reinstall...\" ;;\n \"install.reinstall.warn_stop.zh\") text=\"⚠️ 以下运行中的容器将被停止:\" ;;\n \"install.reinstall.warn_stop.en\") text=\"⚠️ The following running containers will be stopped:\" ;;\n \"install.reinstall.warn_delete.zh\") text=\"⚠️ 警告: 以下内容将被删除:\" ;;\n \"install.reinstall.warn_delete.en\") text=\"⚠️ WARNING: This will DELETE the following:\" ;;\n \"install.reinstall.warn_volume.zh\") text=\" - Docker 卷: agentteams-data\" ;;\n \"install.reinstall.warn_volume.en\") text=\" - Docker volume: agentteams-data\" ;;\n \"install.reinstall.warn_env.zh\") text=\" - Env 文件: %s\" ;;\n \"install.reinstall.warn_env.en\") text=\" - Env file: %s\" ;;\n \"install.reinstall.warn_workspace.zh\") text=\" - Manager 工作空间: %s\" ;;\n \"install.reinstall.warn_workspace.en\") text=\" - Manager workspace: %s\" ;;\n \"install.reinstall.warn_workers.zh\") text=\" - 所有 worker 容器\" ;;\n \"install.reinstall.warn_workers.en\") text=\" - All worker containers\" ;;\n \"install.reinstall.warn_proxy.zh\") text=\" - Docker API 代理容器: agentteams-controller\" ;;\n \"install.reinstall.warn_proxy.en\") text=\" - Docker API proxy container: agentteams-controller\" ;;\n \"install.reinstall.warn_network.zh\") text=\" - Docker 网络: agentteams-net\" ;;\n \"install.reinstall.warn_network.en\") text=\" - Docker network: agentteams-net\" ;;\n \"install.reinstall.confirm_type.zh\") text=\"请输入工作空间路径以确认删除(或按 Ctrl+C 取消):\" ;;\n \"install.reinstall.confirm_type.en\") text=\"To confirm deletion, please type the workspace path:\" ;;\n \"install.reinstall.confirm_path.zh\") text=\"输入路径以确认(或按 Ctrl+C 取消)\" ;;\n \"install.reinstall.confirm_path.en\") text=\"Type the path to confirm (or press Ctrl+C to cancel)\" ;;\n \"install.reinstall.path_mismatch.zh\") text=\"路径不匹配。中止重装。输入: '%s',期望: '%s'\" ;;\n \"install.reinstall.path_mismatch.en\") text=\"Path mismatch. Aborting reinstall. Input: '%s', Expected: '%s'\" ;;\n \"install.reinstall.confirmed.zh\") text=\"已确认。正在清理...\" ;;\n \"install.reinstall.confirmed.en\") text=\"Confirmed. Cleaning up...\" ;;\n \"install.reinstall.removed_worker.zh\") text=\" 已移除 worker: %s\" ;;\n \"install.reinstall.removed_worker.en\") text=\" Removed worker: %s\" ;;\n \"install.reinstall.removing_volume.zh\") text=\"正在移除 Docker 卷: agentteams-data\" ;;\n \"install.reinstall.removing_volume.en\") text=\"Removing Docker volume: agentteams-data\" ;;\n \"install.reinstall.warn_volume_fail.zh\") text=\" 警告: 无法移除卷(可能有引用)\" ;;\n \"install.reinstall.warn_volume_fail.en\") text=\" Warning: Could not remove volume (may have references)\" ;;\n \"install.reinstall.removing_proxy.zh\") text=\"正在移除 Docker API 代理容器: agentteams-controller\" ;;\n \"install.reinstall.removing_proxy.en\") text=\"Removing Docker API proxy container: agentteams-controller\" ;;\n \"install.reinstall.removing_network.zh\") text=\"正在移除 Docker 网络: agentteams-net\" ;;\n \"install.reinstall.removing_network.en\") text=\"Removing Docker network: agentteams-net\" ;;\n \"install.reinstall.removing_workspace.zh\") text=\"正在移除工作空间目录: %s\" ;;\n \"install.reinstall.removing_workspace.en\") text=\"Removing workspace directory: %s\" ;;\n \"install.reinstall.removing_env.zh\") text=\"正在移除 env 文件: %s\" ;;\n \"install.reinstall.removing_env.en\") text=\"Removing env file: %s\" ;;\n \"install.reinstall.cleanup_done.zh\") text=\"清理完成。开始全新安装...\" ;;\n \"install.reinstall.cleanup_done.en\") text=\"Cleanup complete. Starting fresh installation...\" ;;\n \"install.reinstall.failed_rm_workspace.zh\") text=\"无法移除工作空间目录\" ;;\n \"install.reinstall.failed_rm_workspace.en\") text=\"Failed to remove workspace directory\" ;;\n # --- Orphan volume detection ---\n \"install.orphan_volume.detected.zh\") text=\"⚠️ 检测到残留数据卷 '%s',但未找到对应的 env 配置文件。\" ;;\n \"install.orphan_volume.detected.en\") text=\"⚠️ Found leftover data volume '%s' but no matching env config file.\" ;;\n \"install.orphan_volume.warn.zh\") text=\"这可能是之前安装的残留数据,会导致新安装出现异常(如密码冲突、服务启动失败)。\" ;;\n \"install.orphan_volume.warn.en\") text=\"This is likely leftover data from a previous installation and may cause issues (e.g., credential conflicts, service startup failures).\" ;;\n \"install.orphan_volume.choose.zh\") text=\"选择操作:\" ;;\n \"install.orphan_volume.choose.en\") text=\"Choose an action:\" ;;\n \"install.orphan_volume.clean.zh\") text=\" 1) 清理残留数据卷后继续安装(推荐)\" ;;\n \"install.orphan_volume.clean.en\") text=\" 1) Remove leftover volume and continue installation (recommended)\" ;;\n \"install.orphan_volume.keep.zh\") text=\" 2) 保留数据卷继续安装(可能出现异常)\" ;;\n \"install.orphan_volume.keep.en\") text=\" 2) Keep the volume and continue installation (may cause issues)\" ;;\n \"install.orphan_volume.prompt.zh\") text=\"请选择 [1/2]\" ;;\n \"install.orphan_volume.prompt.en\") text=\"Enter choice [1/2]\" ;;\n \"install.orphan_volume.cleaning.zh\") text=\"正在清理残留数据卷...\" ;;\n \"install.orphan_volume.cleaning.en\") text=\"Removing leftover data volume...\" ;;\n \"install.orphan_volume.cleaned.zh\") text=\"残留数据卷已清理。继续全新安装...\" ;;\n \"install.orphan_volume.cleaned.en\") text=\"Leftover volume removed. Continuing with fresh installation...\" ;;\n \"install.orphan_volume.keeping.zh\") text=\"保留数据卷,继续安装。如遇异常请选择全新重装。\" ;;\n \"install.orphan_volume.keeping.en\") text=\"Keeping existing volume. If you encounter issues, consider a clean reinstall.\" ;;\n \"install.orphan_volume.clean_noninteractive.zh\") text=\"非交互模式: 自动清理残留数据卷...\" ;;\n \"install.orphan_volume.clean_noninteractive.en\") text=\"Non-interactive mode: automatically removing leftover volume...\" ;;\n # --- Loading existing config ---\n \"install.loading_config.zh\") text=\"从 %s 加载已有配置(shell 环境变量优先)...\" ;;\n \"install.loading_config.en\") text=\"Loading existing config from %s (shell env vars take priority)...\" ;;\n # --- LLM Configuration ---\n \"llm.title.zh\") text=\"--- LLM 配置 ---\" ;;\n \"llm.title.en\") text=\"--- LLM Configuration ---\" ;;\n \"llm.provider.label.zh\") text=\" 提供商: %s\" ;;\n \"llm.provider.label.en\") text=\" Provider: %s\" ;;\n \"llm.model.label.zh\") text=\" 模型: %s\" ;;\n \"llm.model.label.en\") text=\" Model: %s\" ;;\n \"llm.provider.qwen.zh\") text=\" 提供商: qwen(阿里云百炼)\" ;;\n \"llm.provider.qwen.en\") text=\" Provider: qwen (Alibaba Cloud Bailian)\" ;;\n \"llm.provider.qwen_default.zh\") text=\" 提供商: %s(默认)\" ;;\n \"llm.provider.qwen_default.en\") text=\" Provider: %s (default)\" ;;\n \"llm.model.default.zh\") text=\" 模型: %s(默认)\" ;;\n \"llm.model.default.en\") text=\" Model: %s (default)\" ;;\n \"llm.apikey_hint_bailian.zh\") text=\" 💡 获取阿里云百炼(DashScope)API Key:\" ;;\n \"llm.apikey_hint_bailian.en\") text=\" 💡 Get your Alibaba Cloud Bailian (DashScope) API Key:\" ;;\n \"llm.apikey_url_bailian.zh\") text=\" https://www.aliyun.com/product/bailian\" ;;\n \"llm.apikey_url_bailian.en\") text=\" https://www.aliyun.com/product/bailian\" ;;\n \"llm.apikey_hint_qwencloud.zh\") text=\" 💡 从 Qwen Cloud(国际站)获取 DASHSCOPE_API_KEY:\" ;;\n \"llm.apikey_hint_qwencloud.en\") text=\" 💡 Get your DASHSCOPE_API_KEY for Qwen Cloud (international) from:\" ;;\n \"llm.apikey_url_qwencloud.zh\") text=\" https://home.qwencloud.com/api-keys (文档: https://docs.qwencloud.com/)\" ;;\n \"llm.apikey_url_qwencloud.en\") text=\" https://home.qwencloud.com/api-keys | Docs: https://docs.qwencloud.com/\" ;;\n \"llm.apikey_hint_tokenplan.zh\") text=\" 💡 获取 DashScope API Key 或开通通义 Token 套餐,请参考:\" ;;\n \"llm.apikey_hint_tokenplan.en\") text=\" 💡 Get your DashScope or Token Plan API key (Alibaba Model Studio):\" ;;\n \"llm.apikey_url_tokenplan.zh\") text=\" https://help.aliyun.com/zh/model-studio/token-plan-quickstart\" ;;\n \"llm.apikey_url_tokenplan.en\") text=\" https://common-buy.aliyun.com/token-plan/ | https://help.aliyun.com/zh/model-studio/token-plan-quickstart\" ;;\n \"llm.apikey_hint_codingplan.zh\") text=\" 💡 获取 DashScope API Key(Coding 套餐 / coding.dashscope 接口):\" ;;\n \"llm.apikey_hint_codingplan.en\") text=\" 💡 Get your DashScope API key for Coding Plan (coding.dashscope endpoint):\" ;;\n \"llm.apikey_url_codingplan.zh\") text=\" https://help.aliyun.com/zh/model-studio/get-api-key\" ;;\n \"llm.apikey_url_codingplan.en\") text=\" https://help.aliyun.com/zh/model-studio/get-api-key\" ;;\n \"llm.apikey_prompt.zh\") text=\"LLM API Key\" ;;\n \"llm.apikey_prompt.en\") text=\"LLM API Key\" ;;\n \"llm.providers_title.zh\") text=\"可用 LLM 提供商:\" ;;\n \"llm.providers_title.en\") text=\"Available LLM Providers:\" ;;\n \"llm.provider.alibaba.zh\") text=\" 1) 阿里云通义 Token 套餐 - 推荐中国用户使用\" ;;\n \"llm.provider.alibaba.en\") text=\" 1) Qwen Cloud - International (OpenAI-compatible API, recommended)\" ;;\n \"llm.provider.openai_compat.zh\") text=\" 2) OpenAI 兼容 API - 自定义 Base URL(OpenAI、DeepSeek 等)\" ;;\n \"llm.provider.openai_compat.en\") text=\" 2) OpenAI-compatible API - Custom Base URL (OpenAI, DeepSeek, etc.)\" ;;\n \"llm.provider.select.zh\") text=\"选择提供商 [1/2]\" ;;\n \"llm.provider.select.en\") text=\"Select provider [1/2]\" ;;\n \"llm.alibaba.models_title.zh\") text=\"选择阿里云模型接入方式:\" ;;\n \"llm.alibaba.models_title.en\") text=\"Select Alibaba Cloud model access:\" ;;\n \"llm.alibaba.model.tokenplan.zh\") text=\" 1) 阿里云通义 Token 套餐 - 兼容模式(推荐)\" ;;\n \"llm.alibaba.model.tokenplan.en\") text=\" 1) Alibaba Cloud Token Plan - compatible-mode (recommended)\" ;;\n \"llm.alibaba.model.bailian.zh\") text=\" 2) 阿里云百炼 - DashScope 通用兼容接口\" ;;\n \"llm.alibaba.model.bailian.en\") text=\" 2) Alibaba Cloud Bailian - DashScope compatible mode\" ;;\n \"llm.alibaba.model.codingplan_legacy.zh\") text=\" 3) 阿里云 Coding 套餐 - 旧版端点(兼容保留)\" ;;\n \"llm.alibaba.model.codingplan_legacy.en\") text=\" 3) Alibaba Cloud Coding Plan - legacy endpoint (backward compatible)\" ;;\n \"llm.alibaba.model.select.zh\") text=\"选择接入方式 [1/2/3]\" ;;\n \"llm.alibaba.model.select.en\") text=\"Select access option [1/2/3]\" ;;\n \"llm.alibaba.model.invalid.zh\") text=\"无效选择: %s(请输入 1、2 或 3)\" ;;\n \"llm.alibaba.model.invalid.en\") text=\"Invalid choice: %s (please enter 1, 2, or 3)\" ;;\n \"llm.codingplan.models_title.zh\") text=\"选择通义 Token 套餐默认模型:\" ;;\n \"llm.codingplan.models_title.en\") text=\"Select Qwen Cloud default model:\" ;;\n \"llm.codingplan.model.qwen36plus.zh\") text=\" 1) qwen3.6-plus - 千问 3.6(推荐)\" ;;\n \"llm.codingplan.model.qwen36plus.en\") text=\" 1) qwen3.6-plus - Qwen 3.6 (recommended)\" ;;\n \"llm.codingplan.model.glm5.zh\") text=\" 2) glm-5 - 智谱 GLM-5(编程推荐)\" ;;\n \"llm.codingplan.model.glm5.en\") text=\" 2) glm-5 - Zhipu GLM-5 (recommended for coding)\" ;;\n \"llm.codingplan.model.kimi.zh\") text=\" 3) kimi-k2.5 - Moonshot Kimi K2.5\" ;;\n \"llm.codingplan.model.kimi.en\") text=\" 3) kimi-k2.5 - Moonshot Kimi K2.5\" ;;\n \"llm.codingplan.model.minimax.zh\") text=\" 4) MiniMax-M2.5 - MiniMax M2.5\" ;;\n \"llm.codingplan.model.minimax.en\") text=\" 4) MiniMax-M2.5 - MiniMax M2.5\" ;;\n \"llm.codingplan.model.select.zh\") text=\"选择模型 [1/2/3/4]\" ;;\n \"llm.codingplan.model.select.en\") text=\"Select model [1/2/3/4]\" ;;\n \"llm.provider.selected_tokenplan.zh\") text=\" 提供商: 阿里云通义 Token 套餐(兼容模式)\" ;;\n \"llm.provider.selected_tokenplan.en\") text=\" Provider: Alibaba Cloud Token Plan (compatible mode)\" ;;\n \"llm.provider.selected_codingplan.zh\") text=\" 提供商: 阿里云通义 Token 套餐(alibaba-cloud)\" ;;\n \"llm.provider.selected_codingplan.en\") text=\" Provider: Qwen Cloud (international) (alibaba-cloud)\" ;;\n \"llm.provider.selected_codingplan_legacy.zh\") text=\" 提供商: 阿里云 Coding 套餐(coding.dashscope)\" ;;\n \"llm.provider.selected_codingplan_legacy.en\") text=\" Provider: Alibaba Cloud Coding Plan (coding.dashscope)\" ;;\n \"llm.provider.selected_qwen.zh\") text=\" 提供商: 阿里云百炼\" ;;\n \"llm.provider.selected_qwen.en\") text=\" Provider: Alibaba Cloud Bailian\" ;;\n \"llm.provider.selected_openai.zh\") text=\" 提供商: %s(OpenAI 兼容)\" ;;\n \"llm.provider.selected_openai.en\") text=\" Provider: %s (OpenAI-compatible)\" ;;\n \"llm.provider.invalid.zh\") text=\"无效选择: %s(请输入 1 或 2)\" ;;\n \"llm.provider.invalid.en\") text=\"Invalid choice: %s (please enter 1 or 2)\" ;;\n \"llm.qwen.model_prompt.zh\") text=\"默认模型 ID\" ;;\n \"llm.qwen.model_prompt.en\") text=\"Default Model ID\" ;;\n \"llm.openai.base_url_prompt.zh\") text=\"Base URL\" ;;\n \"llm.openai.base_url_prompt.en\") text=\"Base URL\" ;;\n \"llm.openai.model_prompt.zh\") text=\"默认模型 ID\" ;;\n \"llm.openai.model_prompt.en\") text=\"Default Model ID\" ;;\n \"llm.openai.base_url_label.zh\") text=\" Base URL: %s\" ;;\n \"llm.openai.base_url_label.en\") text=\" Base URL: %s\" ;;\n # --- Custom model parameters ---\n \"llm.custom_model.detected.zh\") text=\" ⚠️ 模型 '%s' 不在内置模型列表中,请配置模型参数:\" ;;\n \"llm.custom_model.detected.en\") text=\" ⚠️ Model '%s' is not in the built-in model list. Please configure model parameters:\" ;;\n \"llm.custom_model.context_prompt.zh\") text=\"最大上下文长度(token 数)[150000]\" ;;\n \"llm.custom_model.context_prompt.en\") text=\"Max context window (tokens) [150000]\" ;;\n \"llm.custom_model.max_tokens_prompt.zh\") text=\"最大输出长度(token 数)[128000]\" ;;\n \"llm.custom_model.max_tokens_prompt.en\") text=\"Max output tokens [128000]\" ;;\n \"llm.custom_model.reasoning_prompt.zh\") text=\"是否支持推理/思考模式?[Y/n]\" ;;\n \"llm.custom_model.reasoning_prompt.en\") text=\"Does it support reasoning/thinking mode? [Y/n]\" ;;\n \"llm.custom_model.vision_prompt.zh\") text=\"是否支持图片输入?[y/N]\" ;;\n \"llm.custom_model.vision_prompt.en\") text=\"Does it support image input? [y/N]\" ;;\n \"llm.custom_model.summary.zh\") text=\" 自定义模型参数: 上下文=%s, 最大输出=%s, 推理=%s, 图片=%s\" ;;\n \"llm.custom_model.summary.en\") text=\" Custom model params: context=%s, maxTokens=%s, reasoning=%s, vision=%s\" ;;\n # --- Admin Credentials ---\n \"admin.title.zh\") text=\"--- 管理员凭据 ---\" ;;\n \"admin.title.en\") text=\"--- Admin Credentials ---\" ;;\n \"admin.username_prompt.zh\") text=\"管理员用户名\" ;;\n \"admin.username_prompt.en\") text=\"Admin Username\" ;;\n \"admin.password_prompt.zh\") text=\"管理员密码(留空自动生成,最少 8 位)\" ;;\n \"admin.password_prompt.en\") text=\"Admin Password (leave empty to auto-generate, min 8 chars)\" ;;\n \"admin.password_generated.zh\") text=\" 已自动生成管理员密码\" ;;\n \"admin.password_generated.en\") text=\" Auto-generated admin password\" ;;\n \"admin.password_too_short.zh\") text=\"管理员密码至少需要 8 个字符(MinIO 要求)。当前长度: %s\" ;;\n \"admin.password_too_short.en\") text=\"Admin password must be at least 8 characters (MinIO requirement). Current length: %s\" ;;\n # --- Port Configuration ---\n \"port.title.zh\") text=\"--- 端口配置(按回车使用默认值)---\" ;;\n \"port.title.en\") text=\"--- Port Configuration (press Enter for defaults) ---\" ;;\n \"port.gateway_prompt.zh\") text=\"网关主机端口(容器内 8080)\" ;;\n \"port.gateway_prompt.en\") text=\"Host port for gateway (8080 inside container)\" ;;\n \"port.console_prompt.zh\") text=\"Higress 控制台主机端口(容器内 8001)\" ;;\n \"port.console_prompt.en\") text=\"Host port for Higress console (8001 inside container)\" ;;\n \"port.element_prompt.zh\") text=\"Element Web 直接访问主机端口(容器内 8088)\" ;;\n \"port.element_prompt.en\") text=\"Host port for Element Web direct access (8088 inside container)\" ;;\n \"port.manager_console_prompt.zh\") text=\"Manager 控制台主机端口(容器内 18888)\" ;;\n \"port.manager_console_prompt.en\") text=\"Host port for Manager console (18888 inside container)\" ;;\n \"port.copaw_app_prompt.zh\") text=\"QwenPaw App API 主机端口(容器内 18799)\" ;;\n \"port.copaw_app_prompt.en\") text=\"Host port for QwenPaw App API (18799 inside container)\" ;;\n # --- Local-only binding ---\n \"port.local_only.title.zh\") text=\"--- 网络访问模式 ---\" ;;\n \"port.local_only.title.en\") text=\"--- Network Access Mode ---\" ;;\n \"port.local_only.prompt.zh\") text=\"是否仅允许本机访问(端口绑定到 127.0.0.1)?\" ;;\n \"port.local_only.prompt.en\") text=\"Bind ports to localhost only (127.0.0.1)?\" ;;\n \"port.local_only.hint_yes.zh\") text=\" 仅本机使用,无需开放外部端口(推荐)\" ;;\n \"port.local_only.hint_yes.en\") text=\" Local use only, no external port exposure (recommended)\" ;;\n \"port.local_only.hint_no.zh\") text=\" 允许外部访问(局域网 / 公网)\" ;;\n \"port.local_only.hint_no.en\") text=\" Allow external access (LAN / public network)\" ;;\n \"port.local_only.choice.zh\") text=\"请选择 [1/2]\" ;;\n \"port.local_only.choice.en\") text=\"Enter choice [1/2]\" ;;\n \"port.local_only.selected_local.zh\") text=\"端口已绑定到 127.0.0.1(仅本机访问)\" ;;\n \"port.local_only.selected_local.en\") text=\"Ports bound to 127.0.0.1 (localhost only)\" ;;\n \"port.local_only.selected_external.zh\") text=\"端口已绑定到所有网络接口(0.0.0.0)\" ;;\n \"port.local_only.selected_external.en\") text=\"Ports bound to all interfaces (0.0.0.0)\" ;;\n \"port.local_only.https_hint.zh\") text=\"⚠️ 建议在 Higress 控制台配置 TLS 证书并启用 HTTPS,避免明文传输。\" ;;\n \"port.local_only.https_hint.en\") text=\"⚠️ It is recommended to configure TLS certificates and enable HTTPS in the Higress Console to avoid plaintext transmission.\" ;;\n \"port.local_only.https_docs.zh\") text=\"\" ;;\n \"port.local_only.https_docs.en\") text=\"\" ;;\n # --- Domain Configuration ---\n \"domain.title.zh\") text=\"--- 域名配置(按回车使用默认值)---\" ;;\n \"domain.title.en\") text=\"--- Domain Configuration (press Enter for defaults) ---\" ;;\n \"domain.hint.zh\") text=\"提示: 自定义域名前必须事先做好 DNS 解析。单机 ECS 部署时无需修改 aigw、fs 等域名;Element Web 和 Matrix Server 也可通过 IP 直接访问。\" ;;\n \"domain.hint.en\") text=\"Hint: Configure DNS resolution before customizing domains. For single ECS deployment, no need to change aigw, fs, etc.; Element Web and Matrix Server can also be accessed directly via IP.\" ;;\n \"domain.matrix_prompt.zh\") text=\"Matrix 域名\" ;;\n \"domain.matrix_prompt.en\") text=\"Matrix Domain\" ;;\n \"domain.element_prompt.zh\") text=\"Element Web 域名\" ;;\n \"domain.element_prompt.en\") text=\"Element Web Domain\" ;;\n \"domain.gateway_prompt.zh\") text=\"AI 网关域名\" ;;\n \"domain.gateway_prompt.en\") text=\"AI Gateway Domain\" ;;\n \"domain.fs_prompt.zh\") text=\"文件系统域名\" ;;\n \"domain.fs_prompt.en\") text=\"File System Domain\" ;;\n \"domain.console_prompt.zh\") text=\"Manager 控制台域名\" ;;\n \"domain.console_prompt.en\") text=\"Manager Console Domain\" ;;\n # --- GitHub Integration ---\n \"github.title.zh\") text=\"--- GitHub 集成(可选,按回车跳过)---\" ;;\n \"github.title.en\") text=\"--- GitHub Integration (optional, press Enter to skip) ---\" ;;\n \"github.token_prompt.zh\") text=\"GitHub 个人访问令牌(可选)\" ;;\n \"github.token_prompt.en\") text=\"GitHub Personal Access Token (optional)\" ;;\n # --- Skills Registry ---\n \"skills.title.zh\") text=\"--- Skills 注册中心(可选,按回车使用默认 nacos://market.agentteams.io:80/public)---\" ;;\n \"skills.title.en\") text=\"--- Skills Registry (optional, press Enter for default nacos://market.agentteams.io:80/public) ---\" ;;\n \"skills.url_prompt.zh\") text=\"Skills 注册中心 URL(留空使用默认 nacos://market.agentteams.io:80/public)\" ;;\n \"skills.url_prompt.en\") text=\"Skills Registry URL (leave empty for default nacos://market.agentteams.io:80/public)\" ;;\n # --- Data Persistence ---\n \"data.title.zh\") text=\"--- 数据持久化 ---\" ;;\n \"data.title.en\") text=\"--- Data Persistence ---\" ;;\n \"data.volume_prompt.zh\") text=\"Docker 卷名称 [agentteams-data]\" ;;\n \"data.volume_prompt.en\") text=\"Docker volume name for persistent data [agentteams-data]\" ;;\n \"data.volume_using.zh\") text=\" 使用 Docker 卷: %s\" ;;\n \"data.volume_using.en\") text=\" Using Docker volume: %s\" ;;\n # --- Manager Workspace ---\n \"workspace.title.zh\") text=\"--- Manager 工作空间 ---\" ;;\n \"workspace.title.en\") text=\"--- Manager Workspace ---\" ;;\n \"workspace.dir_prompt.zh\") text=\"Manager 工作空间目录 [%s]\" ;;\n \"workspace.dir_prompt.en\") text=\"Manager workspace directory [%s]\" ;;\n \"workspace.dir_label.zh\") text=\" Manager 工作空间: %s\" ;;\n \"workspace.dir_label.en\") text=\" Manager workspace: %s\" ;;\n # --- Host directory sharing ---\n \"host_share.prompt.zh\") text=\"与 Agent 共享的主机目录(默认: %s)\" ;;\n \"host_share.prompt.en\") text=\"Host directory to share with agents (default: %s)\" ;;\n \"host_share.sharing.zh\") text=\"共享主机目录: %s -> 容器内 /host-share\" ;;\n \"host_share.sharing.en\") text=\"Sharing host directory: %s -> /host-share in container\" ;;\n \"host_share.not_exist.zh\") text=\"警告: 主机目录 %s 不存在,跳过验证继续使用\" ;;\n \"host_share.not_exist.en\") text=\"WARNING: Host directory %s does not exist, using without validation\" ;;\n # --- Default worker runtime ---\n \"worker_runtime.title.zh\") text=\"--- 默认 Worker 运行时 ---\" ;;\n \"worker_runtime.title.en\") text=\"--- Default Worker Runtime ---\" ;;\n \"worker_runtime.openclaw.zh\") text=\"OpenClaw\" ;;\n \"worker_runtime.openclaw.en\") text=\"OpenClaw\" ;;\n \"worker_runtime.copaw.zh\") text=\"QwenPaw\" ;;\n \"worker_runtime.copaw.en\") text=\"QwenPaw\" ;;\n \"worker_runtime.hermes.zh\") text=\"Hermes\" ;;\n \"worker_runtime.hermes.en\") text=\"Hermes\" ;;\n \"worker_runtime.choice.zh\") text=\"请选择 [1/2/3]\" ;;\n \"worker_runtime.choice.en\") text=\"Enter choice [1/2/3]\" ;;\n \"worker_runtime.selected.zh\") text=\"默认 Worker 运行时: %s\" ;;\n \"worker_runtime.selected.en\") text=\"Default Worker runtime: %s\" ;;\n \"worker_runtime.title_short.zh\") text=\"默认 Worker 运行时\" ;;\n \"worker_runtime.title_short.en\") text=\"Default Worker Runtime\" ;;\n \"manager_runtime.title.zh\") text=\"--- Manager 运行时 ---\" ;;\n \"manager_runtime.title.en\") text=\"--- Manager Runtime ---\" ;;\n \"manager_runtime.openclaw.zh\") text=\"OpenClaw\" ;;\n \"manager_runtime.openclaw.en\") text=\"OpenClaw\" ;;\n \"manager_runtime.copaw.zh\") text=\"QwenPaw\" ;;\n \"manager_runtime.copaw.en\") text=\"QwenPaw\" ;;\n \"manager_runtime.choice.zh\") text=\"请选择 [1/2]\" ;;\n \"manager_runtime.choice.en\") text=\"Enter choice [1/2]\" ;;\n \"manager_runtime.selected.zh\") text=\"Manager 运行时: %s\" ;;\n \"manager_runtime.selected.en\") text=\"Manager runtime: %s\" ;;\n \"manager_runtime.title_short.zh\") text=\"Manager 运行时\" ;;\n \"manager_runtime.title_short.en\") text=\"Manager Runtime\" ;;\n # --- Secrets and config ---\n \"install.generating_secrets.zh\") text=\"正在生成密钥...\" ;;\n \"install.generating_secrets.en\") text=\"Generating secrets...\" ;;\n \"install.config_saved.zh\") text=\"配置已保存到 %s\" ;;\n \"install.config_saved.en\") text=\"Configuration saved to %s\" ;;\n # --- Container runtime socket ---\n \"install.socket_detected.zh\") text=\"容器运行时 socket: %s(已启用直接创建 Worker)\" ;;\n \"install.socket_detected.en\") text=\"Container runtime socket: %s (direct Worker creation enabled)\" ;;\n \"install.socket_not_found.zh\") text=\"未找到容器运行时 socket(Manager 无法直接创建 Worker 容器,需要你手动执行 docker 命令创建)\" ;;\n \"install.socket_not_found.en\") text=\"No container runtime socket found (Manager cannot create Worker containers directly, you will need to create them manually using docker commands)\" ;;\n \"install.socket_confirm.title.zh\") text=\"⚠️ 未检测到容器运行时 Socket\" ;;\n \"install.socket_confirm.title.en\") text=\"⚠️ Container Runtime Socket Not Detected\" ;;\n \"install.socket_confirm.message.zh\") text=\"未找到 Docker/Podman socket,Manager 将无法自动创建 Worker 容器。\\n你需要手动执行 docker run 命令来创建 Worker。\\n\\n是否继续安装?\" ;;\n \"install.socket_confirm.message.en\") text=\"Docker/Podman socket not found. Manager will not be able to create Worker containers automatically.\\nYou will need to manually run docker commands to create Workers.\\n\\nContinue installation?\" ;;\n \"install.socket_confirm.prompt.zh\") text=\"继续安装? [y/N]: \" ;;\n \"install.socket_confirm.prompt.en\") text=\"Continue? [y/N]: \" ;;\n \"install.socket_confirm.cancelled.zh\") text=\"安装已取消。如需启用 Worker 自动创建,请确保 Docker/Podman 正在运行,然后重新运行安装脚本。\" ;;\n \"install.socket_confirm.cancelled.en\") text=\"Installation cancelled. To enable automatic Worker creation, ensure Docker/Podman is running and re-run the installer.\" ;;\n # --- Container management ---\n \"install.removing_existing.zh\") text=\"正在移除现有 agentteams-manager 容器...\" ;;\n \"install.removing_existing.en\") text=\"Removing existing agentteams-manager container...\" ;;\n # --- Matrix E2EE ---\n \"matrix_e2ee.title.zh\") text=\"--- Matrix 端到端加密(E2EE)---\" ;;\n \"matrix_e2ee.title.en\") text=\"--- Matrix End-to-End Encryption (E2EE) ---\" ;;\n \"matrix_e2ee.desc.zh\") text=\"E2EE 会对 Manager 与 Worker 之间的 Matrix 消息进行端到端加密。\\n 启用后,即使 Matrix 服务器被入侵,消息内容也无法被窃取。\\n 但 E2EE 会增加首次握手耗时,且要求所有 Agent 都支持 matrix-sdk-crypto。\\n 如果不确定,建议保持禁用。\\n ⚠ 注意:禁用 E2EE 后,请勿在 Element 上创建默认启用加密的 Private 房间,\\n 否则 Agent 将无法读取该房间中的加密消息。请改用 Public 房间或关闭房间加密。\" ;;\n \"matrix_e2ee.desc.en\") text=\"E2EE encrypts Matrix messages between Manager and Workers end-to-end.\\n When enabled, message content stays private even if the Matrix server is compromised.\\n However, E2EE adds overhead to the initial handshake and requires all Agents\\n to support matrix-sdk-crypto. If unsure, keep it disabled.\\n ⚠ Note: When E2EE is disabled, do NOT create Private rooms in Element (which\\n enable encryption by default) — Agents cannot read encrypted messages without\\n E2EE support. Use Public rooms or turn off room encryption instead.\" ;;\n \"matrix_e2ee.enable.zh\") text=\"启用 E2EE\" ;;\n \"matrix_e2ee.enable.en\") text=\"Enable E2EE\" ;;\n \"matrix_e2ee.disable.zh\") text=\"禁用 E2EE(推荐)\" ;;\n \"matrix_e2ee.disable.en\") text=\"Disable E2EE (recommended)\" ;;\n \"matrix_e2ee.choice.zh\") text=\"请选择 [1/2]\" ;;\n \"matrix_e2ee.choice.en\") text=\"Enter choice [1/2]\" ;;\n \"matrix_e2ee.selected_enabled.zh\") text=\"Matrix E2EE: 已启用\" ;;\n \"matrix_e2ee.selected_enabled.en\") text=\"Matrix E2EE: enabled\" ;;\n \"matrix_e2ee.selected_disabled.zh\") text=\"Matrix E2EE: 已禁用(默认)\" ;;\n \"matrix_e2ee.selected_disabled.en\") text=\"Matrix E2EE: disabled (default)\" ;;\n \"matrix_e2ee.title_short.zh\") text=\"Matrix E2EE\" ;;\n \"matrix_e2ee.title_short.en\") text=\"Matrix E2EE\" ;;\n \"matrix_e2ee.val_enabled.zh\") text=\"已启用\" ;;\n \"matrix_e2ee.val_enabled.en\") text=\"enabled\" ;;\n \"matrix_e2ee.val_disabled.zh\") text=\"已禁用\" ;;\n \"matrix_e2ee.val_disabled.en\") text=\"disabled\" ;;\n # --- Docker API proxy ---\n \"docker_proxy.title.zh\") text=\"--- Docker API 安全代理 ---\" ;;\n \"docker_proxy.title.en\") text=\"--- Docker API Security Proxy ---\" ;;\n \"docker_proxy.desc.zh\") text=\"Docker API 代理可防止 AI Agent 通过 Docker API 越狱访问宿主机。\\n 启用后,Manager 不再直接持有 Docker socket,所有容器操作经过安全校验。\" ;;\n \"docker_proxy.desc.en\") text=\"Docker API proxy prevents AI Agents from escaping via Docker API to access the host.\\n When enabled, Manager no longer has direct Docker socket access; all container operations go through security validation.\" ;;\n \"docker_proxy.enable.zh\") text=\"启用(推荐)\" ;;\n \"docker_proxy.enable.en\") text=\"Enable (recommended)\" ;;\n \"docker_proxy.disable.zh\") text=\"禁用(直接挂载 Docker socket)\" ;;\n \"docker_proxy.disable.en\") text=\"Disable (mount Docker socket directly)\" ;;\n \"docker_proxy.choice.zh\") text=\"请选择 [1/2]\" ;;\n \"docker_proxy.choice.en\") text=\"Enter choice [1/2]\" ;;\n \"docker_proxy.selected_enabled.zh\") text=\"Docker API 代理: 已启用\" ;;\n \"docker_proxy.selected_enabled.en\") text=\"Docker API proxy: enabled\" ;;\n \"docker_proxy.selected_disabled.zh\") text=\"Docker API 代理: 已禁用\" ;;\n \"docker_proxy.selected_disabled.en\") text=\"Docker API proxy: disabled\" ;;\n \"docker_proxy.title_short.zh\") text=\"Docker API 代理\" ;;\n \"docker_proxy.title_short.en\") text=\"Docker API Proxy\" ;;\n \"docker_proxy.val_enabled.zh\") text=\"已启用\" ;;\n \"docker_proxy.val_enabled.en\") text=\"enabled\" ;;\n \"docker_proxy.val_disabled.zh\") text=\"已禁用\" ;;\n \"docker_proxy.val_disabled.en\") text=\"disabled\" ;;\n \"docker_proxy.registries_desc.zh\") text=\"默认放行的镜像来源:本地镜像、localhost、Higress 仓库(所有 region)。\\n 如需放行其他镜像仓库,请输入逗号分隔的地址前缀。\\n 示例: ghcr.io/myorg,registry.example.com/team\" ;;\n \"docker_proxy.registries_desc.en\") text=\"Default allowed image sources: local images, localhost, Higress registries (all regions).\\n To allow additional image sources, enter comma-separated address prefixes.\\n Example: ghcr.io/myorg,registry.example.com/team\" ;;\n \"docker_proxy.registries_prompt.zh\") text=\"额外放行的镜像来源(按回车跳过)\" ;;\n \"docker_proxy.registries_prompt.en\") text=\"Additional allowed image sources (press Enter to skip)\" ;;\n \"docker_proxy.registries_label.zh\") text=\"额外放行的镜像来源\" ;;\n \"docker_proxy.registries_label.en\") text=\"Additional allowed image sources\" ;;\n # --- Podman Autostart ---\n \"podman.autostart.title.zh\") text=\"--- Podman 驻留与开机自启 ---\" ;;\n \"podman.autostart.title.en\") text=\"--- Podman Linger & Autostart ---\" ;;\n \"podman.autostart.desc.zh\") text=\"配置为 systemd 服务,支持开机自动启动及后台驻留。\" ;;\n \"podman.autostart.desc.en\") text=\"Configure as a systemd service for automatic startup on boot.\" ;;\n \"podman.autostart.enable.zh\") text=\"启用(推荐)\" ;;\n \"podman.autostart.enable.en\") text=\"Enable (recommended)\" ;;\n \"podman.autostart.disable.zh\") text=\"不启用(容器随当前会话关闭而停止)\" ;;\n \"podman.autostart.disable.en\") text=\"Disable (containers stop when session ends)\" ;;\n \"podman.autostart.choice.zh\") text=\"请选择 [1/2]\" ;;\n \"podman.autostart.choice.en\") text=\"Enter choice [1/2]\" ;;\n \"podman.autostart.selected_enabled.zh\") text=\"Podman 开机自启: 已启用\" ;;\n \"podman.autostart.selected_enabled.en\") text=\"Podman Autostart: enabled\" ;;\n \"podman.autostart.selected_disabled.zh\") text=\"Podman 开机自启: 已禁用\" ;;\n \"podman.autostart.selected_disabled.en\") text=\"Podman Autostart: disabled\" ;;\n \"podman.autostart.title_short.zh\") text=\"Podman 开机自启\" ;;\n \"podman.autostart.title_short.en\") text=\"Podman Autostart\" ;;\n \"podman.autostart.val_enabled.zh\") text=\"已启用\" ;;\n \"podman.autostart.val_enabled.en\") text=\"enabled\" ;;\n \"podman.autostart.val_disabled.zh\") text=\"已禁用\" ;;\n \"podman.autostart.val_disabled.en\") text=\"disabled\" ;;\n \"install.podman.autostart_title.zh\") text=\"正在配置 Podman 容器开机自启...\" ;;\n \"install.podman.autostart_title.en\") text=\"Configuring Podman container autostart...\" ;;\n \"install.podman.linger_enable.zh\") text=\"尝试启用 systemd linger (如有提示请在此输入 sudo 密码)...\" ;;\n \"install.podman.linger_enable.en\") text=\"Attempting to enable systemd linger (enter sudo password if prompted)...\" ;;\n \"install.podman.linger_warn.zh\") text=\"警告: 无法自动启用 linger。驻留可能失败,请手动执行: sudo loginctl enable-linger \\$(whoami)\" ;;\n \"install.podman.linger_warn.en\") text=\"WARNING: Failed to auto-enable linger. Please run manually: sudo loginctl enable-linger \\$(whoami)\" ;;\n \"install.podman.root_setup.zh\") text=\"检测到 root 用户。正在配置系统级 agentteams-podman-restart 服务...\" ;;\n \"install.podman.root_setup.en\") text=\"Root user detected. Configuring system-wide agentteams-podman-restart service...\" ;;\n \"install.podman.root_success.zh\") text=\"✅ 系统级 agentteams-podman-restart 服务已成功启用并运行。\" ;;\n \"install.podman.root_success.en\") text=\"✅ System-wide agentteams-podman-restart service successfully enabled and running.\" ;;\n \"install.podman.root_fail.zh\") text=\"⚠️ 系统级 agentteams-podman-restart 服务启用失败,请稍后手动检查。\" ;;\n \"install.podman.root_fail.en\") text=\"⚠️ Failed to enable system-wide agentteams-podman-restart service. Please check manually later.\" ;;\n \"install.podman.user_setup.zh\") text=\"检测到普通用户 (%s)。正在配置用户级 agentteams-podman-restart 服务...\" ;;\n \"install.podman.user_setup.en\") text=\"Non-root user (%s) detected. Configuring user-level agentteams-podman-restart service...\" ;;\n \"install.podman.user_success.zh\") text=\"✅ 用户级 agentteams-podman-restart 服务已成功启用并运行。\" ;;\n \"install.podman.user_success.en\") text=\"✅ User-level agentteams-podman-restart service successfully enabled and running.\" ;;\n \"install.podman.user_fail.zh\") text=\"⚠️ 用户级 agentteams-podman-restart 服务启用失败。\\n提示:这通常是因为缺少 XDG_RUNTIME_DIR 或 dbus 没有运行。您可以尝试手动执行: systemctl --user enable --now agentteams-podman-restart.service\" ;;\n \"install.podman.user_fail.en\") text=\"⚠️ Failed to enable user-level agentteams-podman-restart service.\\nHint: This is usually due to missing XDG_RUNTIME_DIR or dbus not running. Try manually: systemctl --user enable --now agentteams-podman-restart.service\" ;;\n \"install.podman.success.zh\") text=\"Podman 开机自启配置完成。\" ;;\n \"install.podman.success.en\") text=\"Podman autostart successfully configured.\" ;;\n # --- Worker idle timeout ---\n \"idle_timeout.prompt.zh\") text=\"Worker 空闲自动停止超时(分钟)[720]\" ;;\n \"idle_timeout.prompt.en\") text=\"Worker idle auto-stop timeout in minutes [720]\" ;;\n \"idle_timeout.selected.zh\") text=\"Worker 空闲超时: %s 分钟\" ;;\n \"idle_timeout.selected.en\") text=\"Worker idle timeout: %s minutes\" ;;\n \"idle_timeout.label.zh\") text=\"Worker 空闲超时(分钟)\" ;;\n \"idle_timeout.label.en\") text=\"Worker idle timeout (min)\" ;;\n # --- YOLO mode ---\n \"install.yolo.zh\") text=\"YOLO 模式已启用(自主决策,无交互提示)\" ;;\n \"install.yolo.en\") text=\"YOLO mode enabled (autonomous decisions, no interactive prompts)\" ;;\n # --- Image pulling ---\n \"install.image.exists.zh\") text=\"Manager 镜像已存在: %s\" ;;\n \"install.image.exists.en\") text=\"Manager image already exists locally: %s\" ;;\n \"install.image.pulling_manager.zh\") text=\"正在拉取 Manager 镜像: %s\" ;;\n \"install.image.pulling_manager.en\") text=\"Pulling Manager image: %s\" ;;\n \"install.image.worker_exists.zh\") text=\"Worker 镜像已存在: %s\" ;;\n \"install.image.worker_exists.en\") text=\"Worker image already exists locally: %s\" ;;\n \"install.image.pulling_worker.zh\") text=\"正在拉取 Worker 镜像: %s\" ;;\n \"install.image.pulling_worker.en\") text=\"Pulling Worker image: %s\" ;;\n # --- Starting container ---\n \"install.starting_manager.zh\") text=\"正在启动 Manager 容器...\" ;;\n \"install.starting_manager.en\") text=\"Starting Manager container...\" ;;\n # --- Wait for Manager ready ---\n \"install.wait_ready.zh\") text=\"等待 Manager Agent 就绪(超时: %ss)...\" ;;\n \"install.wait_ready.en\") text=\"Waiting for Manager agent to be ready (timeout: %ss)...\" ;;\n \"install.wait_ready.ok.zh\") text=\"Manager Agent 已就绪!\" ;;\n \"install.wait_ready.ok.en\") text=\"Manager agent is ready!\" ;;\n \"install.wait_ready.waiting.zh\") text=\"等待中... (%ds/%ds)\" ;;\n \"install.wait_ready.waiting.en\") text=\"Waiting... (%ds/%ds)\" ;;\n \"install.wait_ready.timeout.zh\") text=\"Manager Agent 在 %ss 内未就绪。请检查: docker logs %s\" ;;\n \"install.wait_ready.timeout.en\") text=\"Manager agent did not become ready within %ss. Check: docker logs %s\" ;;\n # --- Wait for Matrix ready ---\n \"install.wait_matrix.zh\") text=\"等待 Matrix 服务就绪(超时: %ss)...\" ;;\n \"install.wait_matrix.en\") text=\"Waiting for Matrix server to be ready (timeout: %ss)...\" ;;\n \"install.wait_matrix.ok.zh\") text=\"Matrix 服务已就绪!\" ;;\n \"install.wait_matrix.ok.en\") text=\"Matrix server is ready!\" ;;\n \"install.wait_matrix.waiting.zh\") text=\"等待 Matrix 中... (%ds/%ds)\" ;;\n \"install.wait_matrix.waiting.en\") text=\"Waiting for Matrix... (%ds/%ds)\" ;;\n \"install.wait_matrix.timeout.zh\") text=\"Matrix 服务在 %ss 内未就绪。请检查: docker logs %s\" ;;\n \"install.wait_matrix.timeout.en\") text=\"Matrix server did not become ready within %ss. Check: docker logs %s\" ;;\n # --- OpenAI-compatible connectivity test ---\n \"llm.openai.test.testing.zh\") text=\"正在测试 API 联通性...\" ;;\n \"llm.openai.test.testing.en\") text=\"Testing API connectivity...\" ;;\n \"llm.openai.test.ok.zh\") text=\"✅ API 联通性测试通过\" ;;\n \"llm.openai.test.ok.en\") text=\"✅ API connectivity test passed\" ;;\n \"llm.openai.test.fail.zh\") text=\"⚠️ API 联通性测试失败(HTTP %s)。响应内容:\\n%s\\n请根据以上错误信息联系您的模型服务商解决。\" ;;\n \"llm.openai.test.fail.en\") text=\"⚠️ API connectivity test failed (HTTP %s). Response body:\\n%s\\nPlease contact your model provider to resolve the issue.\" ;;\n \"llm.openai.test.fail.tokenplan.zh\") text=\"⚠️ 提示: 请确认 API Key 有效且已开通通义 Token 套餐。文档: https://help.aliyun.com/zh/model-studio/token-plan-quickstart\" ;;\n \"llm.openai.test.fail.tokenplan.en\") text=\"⚠️ Hint: Verify your Token Plan API key and compatible-mode access. Docs: https://help.aliyun.com/zh/model-studio/token-plan-quickstart\" ;;\n \"llm.openai.test.fail.codingplan.zh\") text=\"⚠️ 提示: 请确认 API Key 有效且已开通通义 Token 套餐。文档: https://help.aliyun.com/zh/model-studio/token-plan-quickstart\" ;;\n \"llm.openai.test.fail.codingplan.en\") text=\"⚠️ Hint: Verify your DASHSCOPE_API_KEY for Qwen Cloud. API keys: https://home.qwencloud.com/api-keys Docs: https://docs.qwencloud.com/\" ;;\n \"llm.openai.test.fail.codingplan_legacy.zh\") text=\"⚠️ 提示: 请确认 API Key 有效且 Coding 套餐接口可用。文档: https://help.aliyun.com/zh/model-studio/get-api-key\" ;;\n \"llm.openai.test.fail.codingplan_legacy.en\") text=\"⚠️ Hint: Verify your DashScope API key and Coding Plan access. Docs: https://help.aliyun.com/zh/model-studio/get-api-key\" ;;\n \"llm.openai.test.no_curl.zh\") text=\"⚠️ 未找到 curl,跳过 API 联通性测试\" ;;\n \"llm.openai.test.no_curl.en\") text=\"⚠️ curl not found, skipping API connectivity test\" ;;\n \"llm.openai.test.confirm.zh\") text=\"是否仍要继续安装?[y/N/b] \" ;;\n \"llm.openai.test.confirm.en\") text=\"Continue with installation anyway? [y/N/b] \" ;;\n \"llm.embedding.title.zh\") text=\"📦 记忆搜索配置\" ;;\n \"llm.embedding.title.en\") text=\"📦 Memory Search Configuration\" ;;\n \"llm.embedding.hint.zh\") text=\" Embedding 模型可提升记忆搜索质量(语义匹配)。不启用也可正常使用记忆功能(关键词匹配)。\" ;;\n \"llm.embedding.hint.en\") text=\" Embedding model improves memory search quality (semantic matching). Memory still works without it (keyword matching).\" ;;\n \"llm.embedding.option.default.zh\") text=\" 1) text-embedding-v4(推荐)\" ;;\n \"llm.embedding.option.default.en\") text=\" 1) text-embedding-v4 (Recommended)\" ;;\n \"llm.embedding.option.custom.zh\") text=\" 2) 自定义 Embedding 模型\" ;;\n \"llm.embedding.option.custom.en\") text=\" 2) Custom embedding model\" ;;\n \"llm.embedding.option.disable.zh\") text=\" 3) 不启用\" ;;\n \"llm.embedding.option.disable.en\") text=\" 3) Do not enable\" ;;\n \"llm.embedding.select.zh\") text=\"选择\" ;;\n \"llm.embedding.select.en\") text=\"Select\" ;;\n \"llm.embedding.custom_prompt.zh\") text=\" Embedding 模型名称\" ;;\n \"llm.embedding.custom_prompt.en\") text=\" Embedding model name\" ;;\n \"llm.embedding.test.testing.zh\") text=\"正在测试 Embedding API 联通性...\" ;;\n \"llm.embedding.test.testing.en\") text=\"Testing Embedding API connectivity...\" ;;\n \"llm.embedding.test.ok.zh\") text=\"✅ Embedding API 联通性测试通过\" ;;\n \"llm.embedding.test.ok.en\") text=\"✅ Embedding API connectivity test passed\" ;;\n \"llm.embedding.test.fail.zh\") text=\"⚠️ Embedding API 测试失败(HTTP %s)。响应: %s\" ;;\n \"llm.embedding.test.fail.en\") text=\"⚠️ Embedding API test failed (HTTP %s). Response: %s\" ;;\n \"llm.embedding.auto_disabled.zh\") text=\"⚠️ Embedding 已自动禁用,记忆搜索将使用关键词匹配。您可以稍后在 agentteams-manager.env 中设置 AGENTTEAMS_EMBEDDING_MODEL 启用。\" ;;\n \"llm.embedding.auto_disabled.en\") text=\"⚠️ Embedding auto-disabled. Memory search will use keyword matching. You can enable it later in agentteams-manager.env by setting AGENTTEAMS_EMBEDDING_MODEL.\" ;;\n \"llm.embedding.disabled.zh\") text=\"ℹ️ Embedding 已禁用,记忆搜索将使用关键词匹配。\" ;;\n \"llm.embedding.disabled.en\") text=\"ℹ️ Embedding disabled. Memory search will use keyword matching.\" ;;\n \"llm.openai.test.aborted.zh\") text=\"安装已中止。\" ;;\n \"llm.openai.test.aborted.en\") text=\"Installation aborted.\" ;;\n \"nav.back_hint.zh\") text=\"(输入 b 返回上一步)\" ;;\n \"nav.back_hint.en\") text=\"(enter b to go back)\" ;;\n # --- OpenAI-compatible provider creation ---\n \"install.openai_compat.missing.zh\") text=\"警告: OpenAI Base URL 或 API Key 未设置,跳过提供商创建\" ;;\n \"install.openai_compat.missing.en\") text=\"WARNING: OpenAI Base URL or API Key not set, skipping provider creation\" ;;\n \"install.openai_compat.creating.zh\") text=\"正在创建 OpenAI 兼容提供商...\" ;;\n \"install.openai_compat.creating.en\") text=\"Creating OpenAI-compatible provider...\" ;;\n \"install.openai_compat.domain.zh\") text=\" 域名: %s\" ;;\n \"install.openai_compat.domain.en\") text=\" Domain: %s\" ;;\n \"install.openai_compat.port.zh\") text=\" 端口: %s\" ;;\n \"install.openai_compat.port.en\") text=\" Port: %s\" ;;\n \"install.openai_compat.protocol.zh\") text=\" 协议: %s\" ;;\n \"install.openai_compat.protocol.en\") text=\" Protocol: %s\" ;;\n \"install.openai_compat.service_fail.zh\") text=\"警告: 创建 DNS 服务源失败(可能已存在)\" ;;\n \"install.openai_compat.service_fail.en\") text=\"WARNING: Failed to create DNS service source (may already exist)\" ;;\n \"install.openai_compat.provider_fail.zh\") text=\"警告: 创建 AI 提供商失败(可能已存在)\" ;;\n \"install.openai_compat.provider_fail.en\") text=\"WARNING: Failed to create AI provider (may already exist)\" ;;\n \"install.openai_compat.success.zh\") text=\"OpenAI 兼容提供商创建成功\" ;;\n \"install.openai_compat.success.en\") text=\"OpenAI-compatible provider created successfully\" ;;\n # --- Welcome message ---\n \"install.welcome_msg.soul_configured.zh\") text=\"Soul 已配置(找到 soul-configured 标记),跳过 onboarding 消息\" ;;\n \"install.welcome_msg.soul_configured.en\") text=\"Soul already configured (soul-configured marker found), skipping onboarding message\" ;;\n \"install.welcome_msg.logging_in.zh\") text=\"正在以 %s 身份登录以发送欢迎消息...\" ;;\n \"install.welcome_msg.logging_in.en\") text=\"Logging in as %s to send welcome message...\" ;;\n \"install.welcome_msg.login_failed.zh\") text=\"警告: 以 %s 身份登录失败,跳过欢迎消息\" ;;\n \"install.welcome_msg.login_failed.en\") text=\"WARNING: Failed to login as %s, skipping welcome message\" ;;\n \"install.welcome_msg.finding_room.zh\") text=\"正在查找与 Manager 的 DM 房间...\" ;;\n \"install.welcome_msg.finding_room.en\") text=\"Finding DM room with Manager...\" ;;\n \"install.welcome_msg.creating_room.zh\") text=\"正在创建与 Manager 的 DM 房间...\" ;;\n \"install.welcome_msg.creating_room.en\") text=\"Creating DM room with Manager...\" ;;\n \"install.welcome_msg.no_room.zh\") text=\"警告: 无法找到或创建与 Manager 的 DM 房间\" ;;\n \"install.welcome_msg.no_room.en\") text=\"WARNING: Could not find or create DM room with Manager\" ;;\n \"install.welcome_msg.waiting_join.zh\") text=\"等待 Manager 加入房间...\" ;;\n \"install.welcome_msg.waiting_join.en\") text=\"Waiting for Manager to join the room...\" ;;\n \"install.welcome_msg.sending.zh\") text=\"正在向 Manager 发送欢迎消息...\" ;;\n \"install.welcome_msg.sending.en\") text=\"Sending welcome message to Manager...\" ;;\n \"install.welcome_msg.send_failed.zh\") text=\"警告: 发送欢迎消息失败\" ;;\n \"install.welcome_msg.send_failed.en\") text=\"WARNING: Failed to send welcome message\" ;;\n \"install.welcome_msg.sent.zh\") text=\"欢迎消息已发送给 Manager\" ;;\n \"install.welcome_msg.sent.en\") text=\"Welcome message sent to Manager\" ;;\n \"install.welcome_msg.waiting.zh\") text=\"等待 Manager 发送欢迎消息(Higress 路由授权 + LLM 探活,约 45-90s)...\" ;;\n \"install.welcome_msg.waiting.en\") text=\"Waiting for Manager to send the welcome message (Higress route auth + LLM probe, ~45-90s)...\" ;;\n \"install.welcome_msg.confirmed.zh\") text=\"Manager 已确认发送欢迎消息(status.welcomeSent=true,用时 %ss)\" ;;\n \"install.welcome_msg.confirmed.en\") text=\"Manager confirmed welcome message sent (status.welcomeSent=true, %ss elapsed)\" ;;\n \"install.welcome_msg.timeout.zh\") text=\"警告: 在 %ss 内未观察到 Manager 发送欢迎消息(status.welcomeSent=true)。安装仍然成功,所有服务已就绪——可继续按下方提示登录 Element Web。\" ;;\n \"install.welcome_msg.timeout.en\") text=\"WARNING: Did not observe the Manager sending its welcome message (status.welcomeSent=true) within %ss. Installation is still successful, all services are up — continue with the Element Web instructions below.\" ;;\n \"install.welcome_msg.timeout_hint.zh\") text=\"手动触发 onboarding: 登录 Element Web → 打开与 Manager 的 DM 房间 → 发送任意一句话(例如 \\\"hi\\\"),Manager 会接管对话并开始引导。\" ;;\n \"install.welcome_msg.timeout_hint.en\") text=\"Manual onboarding: log in to Element Web → open the DM with the Manager → send any message (e.g. \\\"hi\\\") and the Manager will take over and start the guided setup.\" ;;\n \"install.welcome_msg.timeout_inspect.zh\") text=\"排查命令: docker exec agentteams-controller agt get managers default\" ;;\n \"install.welcome_msg.timeout_inspect.en\") text=\"Inspect status: docker exec agentteams-controller agt get managers default\" ;;\n \"install.welcome_msg.poll_unavailable.zh\") text=\"提示: agentteams-manager 内未找到 AgentTeams CLI,跳过 welcome 等待(旧镜像?)\" ;;\n \"install.welcome_msg.poll_unavailable.en\") text=\"Note: AgentTeams CLI not found inside agentteams-manager; skipping welcome wait (old image?)\" ;;\n # --- Dashboard wizard ---\n \"dash.prompt.zh\") text=\"是否安装 agentteams-dashboard 管理面板?\" ;;\n \"dash.prompt.en\") text=\"Install agentteams-dashboard management UI?\" ;;\n \"dash.version_prompt.zh\") text=\"Dashboard 版本\" ;;\n \"dash.version_prompt.en\") text=\"Dashboard version\" ;;\n \"dash.port_prompt.zh\") text=\"Dashboard 端口号\" ;;\n \"dash.port_prompt.en\") text=\"Dashboard port\" ;;\n \"dash.image_prompt.zh\") text=\"Dashboard 镜像\" ;;\n \"dash.image_prompt.en\") text=\"Dashboard image\" ;;\n \"dash.gateway_prompt.zh\") text=\"Higress Console URL (用于共享登录)\" ;;\n \"dash.gateway_prompt.en\") text=\"Higress Console URL (for shared login)\" ;;\n \"dash.skip.zh\") text=\"跳过 Dashboard 安装\" ;;\n \"dash.skip.en\") text=\"Skipping Dashboard installation\" ;;\n \"dash.auto_url.zh\") text=\"自动检测到 Higress Console URL\" ;;\n \"dash.auto_url.en\") text=\"Auto-detected Higress Console URL\" ;;\n \"dash.auto_url_fail.zh\") text=\"无法自动检测 Higress Console URL,请手动输入\" ;;\n \"dash.auto_url_fail.en\") text=\"Cannot auto-detect Higress Console URL, please enter manually\" ;;\n \"dash.summary.zh\") text=\"Dashboard 配置: 端口=%s 镜像=%s\" ;;\n \"dash.summary.en\") text=\"Dashboard config: port=%s image=%s\" ;;\n # --- Dashboard success ---\n \"success.dashboard.zh\") text=\" agentteams-dashboard 管理面板: http://localhost:%s/\" ;;\n \"success.dashboard.en\") text=\" agentteams-dashboard: http://localhost:%s/\" ;;\n # --- Final output panel ---\n \"success.title.zh\") text=\"=== AgentTeams Manager 已启动!===\" ;;\n \"success.title.en\") text=\"=== AgentTeams Manager Started! ===\" ;;\n \"success.domains_configured.zh\") text=\"以下域名已配置解析到 127.0.0.1:\" ;;\n \"success.domains_configured.en\") text=\"The following domains are configured to resolve to 127.0.0.1:\" ;;\n \"success.open_url.zh\") text=\" ★ 在浏览器中打开以下 URL 开始使用: ★\" ;;\n \"success.open_url.en\") text=\" ★ Open the following URL in your browser to start: ★\" ;;\n \"success.login_with.zh\") text=\" 登录信息:\" ;;\n \"success.login_with.en\") text=\" Login with:\" ;;\n \"success.username.zh\") text=\" 用户名: %s\" ;;\n \"success.username.en\") text=\" Username: %s\" ;;\n \"success.password.zh\") text=\" 密码: %s\" ;;\n \"success.password.en\") text=\" Password: %s\" ;;\n \"success.after_login.zh\") text=\" 登录后,开始与 Manager 聊天!\" ;;\n \"success.after_login.en\") text=\" After login, start chatting with the Manager!\" ;;\n \"success.tell_it.zh\") text=\" 告诉它: \\\"创建一个名为 alice 的前端开发 Worker\\\"\" ;;\n \"success.tell_it.en\") text=\" Tell it: \\\"Create a Worker named alice for frontend dev\\\"\" ;;\n \"success.manager_auto.zh\") text=\" Manager 会自动处理一切。\" ;;\n \"success.manager_auto.en\") text=\" The Manager will handle everything automatically.\" ;;\n \"success.mobile_title.zh\") text=\" 📱 移动端访问(FluffyChat / Element Mobile):\" ;;\n \"success.mobile_title.en\") text=\" 📱 Mobile access (FluffyChat / Element Mobile):\" ;;\n \"success.mobile_step1.zh\") text=\" 1. 在手机上下载 FluffyChat 或 Element\" ;;\n \"success.mobile_step1.en\") text=\" 1. Download FluffyChat or Element on your phone\" ;;\n \"success.mobile_step2.zh\") text=\" 2. 设置 homeserver 为: %s\" ;;\n \"success.mobile_step2.en\") text=\" 2. Set homeserver to: %s\" ;;\n \"success.mobile_step2_noip.zh\") text=\" 2. 设置 homeserver 为: http://<本机局域网IP>:%s\" ;;\n \"success.mobile_step2_noip.en\") text=\" 2. Set homeserver to: http://<this-machine-LAN-IP>:%s\" ;;\n \"success.mobile_noip_hint.zh\") text=\" (无法自动检测局域网 IP — 请使用 ifconfig / ip addr 查看)\" ;;\n \"success.mobile_noip_hint.en\") text=\" (Could not detect LAN IP automatically — check with: ifconfig / ip addr)\" ;;\n \"success.mobile_step3.zh\") text=\" 3. 登录信息:\" ;;\n \"success.mobile_step3.en\") text=\" 3. Login with:\" ;;\n \"success.mobile_username.zh\") text=\" 用户名: %s\" ;;\n \"success.mobile_username.en\") text=\" Username: %s\" ;;\n \"success.mobile_password.zh\") text=\" 密码: %s\" ;;\n \"success.mobile_password.en\") text=\" Password: %s\" ;;\n # --- Other consoles and tips ---\n \"success.other_consoles.zh\") text=\"--- 其他控制台 ---\" ;;\n \"success.other_consoles.en\") text=\"--- Other Consoles ---\" ;;\n \"success.higress_console.zh\") text=\" Higress 控制台: http://localhost:%s(用户名: %s / 密码: %s)\" ;;\n \"success.higress_console.en\") text=\" Higress Console: http://localhost:%s (Username: %s / Password: %s)\" ;;\n \"success.manager_console.zh\") text=\" Manager 控制台(本地): http://localhost:%s(无需登录)\" ;;\n \"success.manager_console.en\") text=\" Manager Console (local): http://localhost:%s (no login required)\" ;;\n \"success.manager_console_gateway.zh\") text=\" Manager 控制台(网关): http://console-local.agentteams.io(用户名: %s / 密码: %s)\" ;;\n \"success.manager_console_gateway.en\") text=\" Manager Console (gateway): http://console-local.agentteams.io (Username: %s / Password: %s)\" ;;\n \"success.copaw_console.zh\") text=\" QwenPaw App API: http://localhost:%s(无需登录)\" ;;\n \"success.copaw_console.en\") text=\" QwenPaw App API: http://localhost:%s (no login required)\" ;;\n \"success.switch_llm.title.zh\") text=\"--- 切换 LLM 提供商 ---\" ;;\n \"success.switch_llm.title.en\") text=\"--- Switch LLM Providers ---\" ;;\n \"success.switch_llm.hint.zh\") text=\" 您可以通过 Higress 控制台切换到其他 LLM 提供商(OpenAI、Anthropic 等)。\" ;;\n \"success.switch_llm.hint.en\") text=\" You can switch to other LLM providers (OpenAI, Anthropic, etc.) via Higress Console.\" ;;\n \"success.switch_llm.docs.zh\") text=\" 详细说明请参阅:\" ;;\n \"success.switch_llm.docs.en\") text=\" For detailed instructions, see:\" ;;\n \"success.switch_llm.url.zh\") text=\" https://higress.ai/en/docs/ai/scene-guide/multi-proxy#console-configuration\" ;;\n \"success.switch_llm.url.en\") text=\" https://higress.ai/en/docs/ai/scene-guide/multi-proxy#console-configuration\" ;;\n \"success.tip.zh\") text=\"提示: 您也可以在聊天中让 Manager 为您配置 LLM 提供商。\" ;;\n \"success.tip.en\") text=\"Tip: You can also ask the Manager to configure LLM providers for you in the chat.\" ;;\n \"success.config_file.zh\") text=\"配置文件: %s\" ;;\n \"success.config_file.en\") text=\"Configuration file: %s\" ;;\n \"success.data_volume.zh\") text=\"数据卷: %s\" ;;\n \"success.data_volume.en\") text=\"Data volume: %s\" ;;\n \"success.workspace.zh\") text=\"Manager 工作空间: %s\" ;;\n \"success.workspace.en\") text=\"Manager workspace: %s\" ;;\n # --- Worker installation ---\n \"worker.resetting.zh\") text=\"正在重置 Worker: %s...\" ;;\n \"worker.resetting.en\") text=\"Resetting Worker: %s...\" ;;\n \"worker.exists.zh\") text=\"容器 '%s' 已存在。使用 --reset 重新创建。\" ;;\n \"worker.exists.en\") text=\"Container '%s' already exists. Use --reset to recreate.\" ;;\n \"worker.starting.zh\") text=\"正在启动 Worker: %s...\" ;;\n \"worker.starting.en\") text=\"Starting Worker: %s...\" ;;\n \"worker.skills_url.zh\") text=\" Skills API URL: %s\" ;;\n \"worker.skills_url.en\") text=\" Skills API URL: %s\" ;;\n \"worker.started.zh\") text=\"=== Worker %s 已启动!===\" ;;\n \"worker.started.en\") text=\"=== Worker %s Started! ===\" ;;\n \"worker.container.zh\") text=\"容器: %s\" ;;\n \"worker.container.en\") text=\"Container: %s\" ;;\n \"worker.view_logs.zh\") text=\"查看日志: docker logs -f %s\" ;;\n \"worker.view_logs.en\") text=\"View logs: docker logs -f %s\" ;;\n # --- Prompt function messages ---\n \"prompt.preset.zh\") text=\" %s = (已通过环境变量预设)\" ;;\n \"prompt.preset.en\") text=\" %s = (pre-set via env)\" ;;\n \"prompt.upgrade_keep.zh\") text=\" %s = %s(当前值,回车保留 / 输入新值覆盖)\" ;;\n \"prompt.upgrade_keep.en\") text=\" %s = %s (current value, press Enter to keep / type new value to change)\" ;;\n \"prompt.upgrade_keep_secret.zh\") text=\" %s = %s(当前值,回车保留 / 输入新值覆盖)\" ;;\n \"prompt.upgrade_keep_secret.en\") text=\" %s = %s (current value, press Enter to keep / type new value to change)\" ;;\n \"prompt.upgrade_empty.zh\") text=\" %s = (未设置,回车跳过 / 输入新值设置)\" ;;\n \"prompt.upgrade_empty.en\") text=\" %s = (not set, press Enter to skip / type new value to set)\" ;;\n \"prompt.default.zh\") text=\" %s = %s(默认)\" ;;\n \"prompt.default.en\") text=\" %s = %s (default)\" ;;\n \"prompt.required.zh\") text=\"%s 是必需的(在非交互模式下通过环境变量设置)\" ;;\n \"prompt.required.en\") text=\"%s is required (set via environment variable in non-interactive mode)\" ;;\n \"prompt.required_empty.zh\") text=\"%s 是必需的\" ;;\n \"prompt.required_empty.en\") text=\"%s is required\" ;;\n # --- Language switch prompt (bilingual by design) ---\n \"lang.detected.zh\") text=\"检测到语言 / Detected language: 中文\" ;;\n \"lang.detected.en\") text=\"检测到语言 / Detected language: English\" ;;\n \"lang.switch_title.zh\") text=\"切换语言 / Switch language:\" ;;\n \"lang.switch_title.en\") text=\"切换语言 / Switch language:\" ;;\n \"lang.option_zh.zh\") text=\" 1) 中文\" ;;\n \"lang.option_zh.en\") text=\" 1) 中文\" ;;\n \"lang.option_en.zh\") text=\" 2) English\" ;;\n \"lang.option_en.en\") text=\" 2) English\" ;;\n \"lang.prompt.zh\") text=\"请选择 / Enter choice\" ;;\n \"lang.prompt.en\") text=\"请选择 / Enter choice\" ;;\n # --- Uninstall messages ---\n \"uninstall.title.zh\") text=\"正在卸载 AgentTeams...\" ;;\n \"uninstall.title.en\") text=\"Uninstalling AgentTeams...\" ;;\n \"uninstall.stopping_manager.zh\") text=\"正在停止并移除 agentteams-manager...\" ;;\n \"uninstall.stopping_manager.en\") text=\"Stopping and removing agentteams-manager...\" ;;\n \"uninstall.stopping_workers.zh\") text=\"正在停止并移除 Worker 容器...\" ;;\n \"uninstall.stopping_workers.en\") text=\"Stopping and removing worker containers...\" ;;\n \"uninstall.removed.zh\") text=\" 已移除: %s\" ;;\n \"uninstall.removed.en\") text=\" Removed: %s\" ;;\n \"uninstall.removing_volume.zh\") text=\"正在移除 Docker 卷: %s\" ;;\n \"uninstall.removing_volume.en\") text=\"Removing Docker volume: %s\" ;;\n \"uninstall.removing_env.zh\") text=\"正在移除 env 文件: %s\" ;;\n \"uninstall.removing_env.en\") text=\"Removing env file: %s\" ;;\n \"uninstall.removing_proxy.zh\") text=\"正在停止并移除 Docker API 代理容器: agentteams-docker-proxy\" ;;\n \"uninstall.removing_proxy.en\") text=\"Stopping and removing Docker API proxy container: agentteams-docker-proxy\" ;;\n \"uninstall.stopping_controller.zh\") text=\"正在停止并移除 agentteams-controller (内嵌 Tuwunel/MinIO/Higress)...\" ;;\n \"uninstall.stopping_controller.en\") text=\"Stopping and removing agentteams-controller (embedded Tuwunel/MinIO/Higress)...\" ;;\n \"uninstall.removing_network.zh\") text=\"正在移除 Docker 网络: agentteams-net\" ;;\n \"uninstall.removing_network.en\") text=\"Removing Docker network: agentteams-net\" ;;\n \"uninstall.removing_workspace.zh\") text=\"正在移除工作空间目录: %s\" ;;\n \"uninstall.removing_workspace.en\") text=\"Removing workspace directory: %s\" ;;\n \"uninstall.removing_workspace_elevated.zh\") text=\" 工作空间包含 root 文件,通过容器清理...\" ;;\n \"uninstall.removing_workspace_elevated.en\") text=\" Workspace contains root-owned files, cleaning via container...\" ;;\n \"uninstall.removing_log.zh\") text=\"正在移除日志文件: %s\" ;;\n \"uninstall.removing_log.en\") text=\"Removing log file: %s\" ;;\n \"uninstall.done.zh\") text=\"AgentTeams 已卸载。\" ;;\n \"uninstall.done.en\") text=\"AgentTeams has been uninstalled.\" ;;\n # --- Error messages ---\n \"error.name_required.zh\") text=\"--name 是必需的\" ;;\n \"error.name_required.en\") text=\"--name is required\" ;;\n \"error.fs_required.zh\") text=\"--fs 是必需的\" ;;\n \"error.fs_required.en\") text=\"--fs is required\" ;;\n \"error.fs_key_required.zh\") text=\"--fs-key 是必需的\" ;;\n \"error.fs_key_required.en\") text=\"--fs-key is required\" ;;\n \"error.fs_secret_required.zh\") text=\"--fs-secret 是必需的\" ;;\n \"error.fs_secret_required.en\") text=\"--fs-secret is required\" ;;\n \"error.unknown_option.zh\") text=\"未知选项: %s\" ;;\n \"error.unknown_option.en\") text=\"Unknown option: %s\" ;;\n \"error.docker_not_found.zh\") text=\"未找到 docker 或 podman 命令。请先安装 Docker Desktop 或 Podman Desktop:\\n Docker Desktop: https://www.docker.com/products/docker-desktop/\\n Podman Desktop: https://podman-desktop.io/\" ;;\n \"error.docker_not_found.en\") text=\"docker or podman command not found. Please install Docker Desktop or Podman Desktop first:\\n Docker Desktop: https://www.docker.com/products/docker-desktop/\\n Podman Desktop: https://podman-desktop.io/\" ;;\n \"error.docker_not_running.zh\") text=\"Docker 未运行。请先启动 Docker Desktop 或 Podman Desktop。\" ;;\n \"error.docker_not_running.en\") text=\"Docker is not running. Please start Docker Desktop or Podman Desktop first.\" ;;\n # --- Fallback: try English for unknown lang ---\n *)\n case \"${key}.en\" in\n \"tz.warning.title.en\") text=\"Could not detect timezone automatically.\" ;;\n \"install.title.en\") text=\"=== AgentTeams Manager Installation ===\" ;;\n *) text=\"${key}\" ;;\n esac\n ;;\n esac\n if [ $# -gt 0 ]; then\n # shellcheck disable=SC2059\n printf \"${text}\\n\" \"$@\"\n else\n echo \"${text}\"\n fi\n}\n\n# ============================================================\n# Registry selection based on timezone\n# ============================================================\n\ndetect_registry() {\n local tz=\"${AGENTTEAMS_TIMEZONE}\"\n\n case \"${tz}\" in\n America/*)\n echo \"higress-registry.us-west-1.cr.aliyuncs.com\"\n ;;\n Asia/Singapore|Asia/Bangkok|Asia/Jakarta|Asia/Makassar|Asia/Jayapura|\\\n Asia/Kuala_Lumpur|Asia/Ho_Chi_Minh|Asia/Manila|Asia/Yangon|\\\n Asia/Vientiane|Asia/Phnom_Penh|Asia/Pontianak|Asia/Ujung_Pandang)\n echo \"higress-registry.ap-southeast-7.cr.aliyuncs.com\"\n ;;\n *)\n echo \"higress-registry.cn-hangzhou.cr.aliyuncs.com\"\n ;;\n esac\n}\n\nAGENTTEAMS_REGISTRY=\"${AGENTTEAMS_REGISTRY:-$(detect_registry)}\"\n# Backward compatibility: accept old env var names from previous versions\nAGENTTEAMS_INSTALL_CONTROLLER_IMAGE=\"${AGENTTEAMS_INSTALL_CONTROLLER_IMAGE:-${AGENTTEAMS_INSTALL_DOCKER_PROXY_IMAGE:-}}\"\n# Image variables are resolved after version selection in step_version().\n# These placeholders allow early code paths to reference them without errors.\nMANAGER_IMAGE=\"${AGENTTEAMS_INSTALL_MANAGER_IMAGE:-}\"\nMANAGER_COPAW_IMAGE=\"${AGENTTEAMS_INSTALL_MANAGER_COPAW_IMAGE:-}\"\nWORKER_IMAGE=\"${AGENTTEAMS_INSTALL_WORKER_IMAGE:-}\"\nCOPAW_WORKER_IMAGE=\"${AGENTTEAMS_INSTALL_COPAW_WORKER_IMAGE:-}\"\nHERMES_WORKER_IMAGE=\"${AGENTTEAMS_INSTALL_HERMES_WORKER_IMAGE:-}\"\nCONTROLLER_IMAGE=\"${AGENTTEAMS_INSTALL_CONTROLLER_IMAGE:-}\"\n\nresolve_image_tags() {\n AGENTTEAMS_VERSION=\"$(_normalize_version \"${AGENTTEAMS_VERSION}\")\"\n MANAGER_IMAGE=\"${AGENTTEAMS_INSTALL_MANAGER_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-manager:${AGENTTEAMS_VERSION}}\"\n MANAGER_COPAW_IMAGE=\"${AGENTTEAMS_INSTALL_MANAGER_COPAW_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-manager-copaw:${AGENTTEAMS_VERSION}}\"\n WORKER_IMAGE=\"${AGENTTEAMS_INSTALL_WORKER_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-worker:${AGENTTEAMS_VERSION}}\"\n COPAW_WORKER_IMAGE=\"${AGENTTEAMS_INSTALL_COPAW_WORKER_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-copaw-worker:${AGENTTEAMS_VERSION}}\"\n HERMES_WORKER_IMAGE=\"${AGENTTEAMS_INSTALL_HERMES_WORKER_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-hermes-worker:${AGENTTEAMS_VERSION}}\"\n EMBEDDED_IMAGE=\"${AGENTTEAMS_INSTALL_EMBEDDED_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-embedded:${AGENTTEAMS_VERSION}}\"\n # CoPaw Worker introduced in v1.0.4; Hermes Worker introduced in v1.1.0\n if [ -z \"${AGENTTEAMS_INSTALL_COPAW_WORKER_IMAGE:-}\" ] && _ver_lt \"${AGENTTEAMS_VERSION}\" \"v1.0.4\"; then\n COPAW_WORKER_IMAGE=\"\"\n fi\n if [ -z \"${AGENTTEAMS_INSTALL_HERMES_WORKER_IMAGE:-}\" ] && _ver_lt \"${AGENTTEAMS_VERSION}\" \"v1.1.0\"; then\n HERMES_WORKER_IMAGE=\"\"\n fi\n}\n\n# Resolve the embedded controller image. Embedded mode is the only supported\n# architecture since PR #616 (manager image no longer bundles Higress/Tuwunel/MinIO).\n# If the embedded image is unavailable for the requested version, fail fast with an\n# actionable error rather than silently falling back to the legacy single-container\n# path — that path is permanently broken with the slim manager image and would just\n# leave the user with a manager container looping on \"Waiting for Higress Gateway\".\n# Sets EMBEDDED_IMAGE and AGENTTEAMS_USE_EMBEDDED.\nresolve_embedded_image() {\n AGENTTEAMS_USE_EMBEDDED=1\n\n # If the user explicitly overrode the image (e.g. `make install-embedded` passes\n # a locally-built tag), respect it as-is without any registry probe.\n if [ -n \"${AGENTTEAMS_INSTALL_EMBEDDED_IMAGE:-}\" ]; then\n EMBEDDED_IMAGE=\"${AGENTTEAMS_INSTALL_EMBEDDED_IMAGE}\"\n return 0\n fi\n\n local _versioned=\"${AGENTTEAMS_REGISTRY}/agentteams/agentteams-embedded:${AGENTTEAMS_VERSION}\"\n local _latest=\"${AGENTTEAMS_REGISTRY}/agentteams/agentteams-embedded:latest\"\n\n # Skip probe when AGENTTEAMS_VERSION is \"latest\" — no point trying the same tag twice.\n if [ \"${AGENTTEAMS_VERSION}\" = \"latest\" ]; then\n EMBEDDED_IMAGE=\"${_latest}\"\n return 0\n fi\n\n if ${DOCKER_CMD} pull \"${_versioned}\" >/dev/null 2>&1; then\n EMBEDDED_IMAGE=\"${_versioned}\"\n return 0\n fi\n\n # Versions before v1.1.0 predate agentteams-embedded entirely — their manager image\n # bundled all infrastructure. Falling back to agentteams-embedded:latest would\n # silently swap in the v1.1.0 architecture (embedded kube-apiserver) which\n # crashes under QEMU on Apple Silicon. Auto-activate legacy mode instead.\n if _ver_lt \"${AGENTTEAMS_VERSION}\" \"v1.1.0\"; then\n log \"INFO: ${AGENTTEAMS_VERSION} predates agentteams-embedded; switching to legacy all-in-one manager architecture.\"\n log \"WARNING: Legacy all-in-one mode requires AGENTTEAMS_VERSION <= v1.0.9 (older bundled manager image).\"\n log \"WARNING: Newer slim manager images will hang on 'Waiting for Higress Gateway'.\"\n AGENTTEAMS_USE_EMBEDDED=0\n return 0\n fi\n\n if ${DOCKER_CMD} pull \"${_latest}\" >/dev/null 2>&1; then\n log \"embedded ${AGENTTEAMS_VERSION} not found, using latest\"\n EMBEDDED_IMAGE=\"${_latest}\"\n return 0\n fi\n\n # Explicit escape hatch — still honoured for edge cases.\n if [ \"${AGENTTEAMS_FORCE_LEGACY:-0}\" = \"1\" ]; then\n log \"WARNING: AGENTTEAMS_FORCE_LEGACY=1 — using legacy all-in-one manager architecture.\"\n log \"WARNING: This requires AGENTTEAMS_VERSION <= v1.0.9 (older bundled manager image).\"\n log \"WARNING: Newer slim manager images will hang on 'Waiting for Higress Gateway'.\"\n AGENTTEAMS_USE_EMBEDDED=0\n return 0\n fi\n\n error \"Embedded controller image is not available in the registry:\"\n error \" - tried: ${_versioned}\"\n error \" - tried: ${_latest}\"\n error \"\"\n error \"Embedded mode is the only supported architecture since PR #616.\"\n error \"How to resolve:\"\n error \" 1) Pin to a AGENTTEAMS_VERSION whose embedded image has been published, or\"\n error \" wait for the release pipeline to publish it.\"\n error \" 2) For a local build, run: make install-embedded\"\n error \" (builds and uses the local embedded image without touching the registry).\"\n error \" 3) Override with a custom image: AGENTTEAMS_INSTALL_EMBEDDED_IMAGE=... ./agentteams-install.sh\"\n exit 1\n}\n\n# ============================================================\n# Known models list — used to detect custom models during install\n# ============================================================\nKNOWN_MODELS=\"gpt-5.4 gpt-5.3-codex gpt-5-mini gpt-5-nano claude-opus-4-6 claude-sonnet-4-6 claude-haiku-4-5 qwen3.6-plus qwen3.5-plus deepseek-chat deepseek-reasoner kimi-k2.5 glm-5 MiniMax-M2.7 MiniMax-M2.7-highspeed MiniMax-M2.5\"\n\nis_known_model() {\n local model=\"$1\"\n for m in ${KNOWN_MODELS}; do\n [ \"${m}\" = \"${model}\" ] && return 0\n done\n return 1\n}\n\n# Prompt user for custom model parameters when model is not in the known list.\n# Sets: AGENTTEAMS_MODEL_CONTEXT_WINDOW, AGENTTEAMS_MODEL_MAX_TOKENS, AGENTTEAMS_MODEL_REASONING, AGENTTEAMS_MODEL_VISION\nprompt_custom_model_params() {\n local model=\"$1\"\n if is_known_model \"${model}\"; then\n # Clear any stale custom params for known models\n AGENTTEAMS_MODEL_CONTEXT_WINDOW=\"\"\n AGENTTEAMS_MODEL_MAX_TOKENS=\"\"\n AGENTTEAMS_MODEL_REASONING=\"\"\n AGENTTEAMS_MODEL_VISION=\"\"\n return\n fi\n echo \"\"\n log \"$(msg llm.custom_model.detected \"${model}\")\"\n echo \"\"\n read -e -p \" $(msg llm.custom_model.context_prompt): \" AGENTTEAMS_MODEL_CONTEXT_WINDOW\n if [ \"${AGENTTEAMS_MODEL_CONTEXT_WINDOW}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n AGENTTEAMS_MODEL_CONTEXT_WINDOW=\"${AGENTTEAMS_MODEL_CONTEXT_WINDOW:-150000}\"\n read -e -p \" $(msg llm.custom_model.max_tokens_prompt): \" AGENTTEAMS_MODEL_MAX_TOKENS\n if [ \"${AGENTTEAMS_MODEL_MAX_TOKENS}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n AGENTTEAMS_MODEL_MAX_TOKENS=\"${AGENTTEAMS_MODEL_MAX_TOKENS:-128000}\"\n read -e -p \" $(msg llm.custom_model.reasoning_prompt): \" _reasoning\n if [ \"${_reasoning}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n case \"${_reasoning}\" in\n n|N|no|NO) AGENTTEAMS_MODEL_REASONING=\"false\" ;;\n *) AGENTTEAMS_MODEL_REASONING=\"true\" ;;\n esac\n read -e -p \" $(msg llm.custom_model.vision_prompt): \" _vision\n if [ \"${_vision}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n case \"${_vision}\" in\n y|Y|yes|YES) AGENTTEAMS_MODEL_VISION=\"true\" ;;\n *) AGENTTEAMS_MODEL_VISION=\"false\" ;;\n esac\n log \"$(msg llm.custom_model.summary \"${AGENTTEAMS_MODEL_CONTEXT_WINDOW}\" \"${AGENTTEAMS_MODEL_MAX_TOKENS}\" \"${AGENTTEAMS_MODEL_REASONING}\" \"${AGENTTEAMS_MODEL_VISION}\")\"\n}\n\n# ============================================================\n# Wait for Manager agent to be ready\n# Uses `openclaw gateway health` inside the container to confirm the gateway is running\n# ============================================================\n\nwait_manager_ready() {\n local timeout=\"${AGENTTEAMS_READY_TIMEOUT:-300}\"\n local elapsed=0\n local container=\"${1:-agentteams-manager}\"\n\n log \"$(msg install.wait_ready \"${timeout}\")\"\n\n # Wait for Manager agent to be healthy inside the container\n local runtime=\"${AGENTTEAMS_MANAGER_RUNTIME:-copaw}\"\n while [ \"${elapsed}\" -lt \"${timeout}\" ]; do\n case \"${runtime}\" in\n copaw)\n if ${DOCKER_CMD} exec \"${container}\" curl -sf http://127.0.0.1:18799/api/agents 2>/dev/null | grep -q '\"agents\"'; then\n log \"$(msg install.wait_ready.ok)\"\n return 0\n fi\n ;;\n *)\n if ${DOCKER_CMD} exec \"${container}\" openclaw gateway health --json 2>/dev/null | grep -q '\"ok\"' 2>/dev/null; then\n log \"$(msg install.wait_ready.ok)\"\n return 0\n fi\n ;;\n esac\n sleep 5\n elapsed=$((elapsed + 5))\n printf \"\\r\\033[36m[AgentTeams]\\033[0m $(msg install.wait_ready.waiting \"${elapsed}\" \"${timeout}\")\"\n done\n\n echo \"\"\n die \"$(msg install.wait_ready.timeout \"${timeout}\" \"${container}\")\"\n}\n\nwait_matrix_ready() {\n local timeout=\"${AGENTTEAMS_READY_TIMEOUT:-300}\"\n local elapsed=0\n local container=\"${1:-agentteams-manager}\"\n\n log \"$(msg install.wait_matrix \"${timeout}\")\"\n\n while [ \"${elapsed}\" -lt \"${timeout}\" ]; do\n if ${DOCKER_CMD} exec \"${container}\" curl -sf http://127.0.0.1:6167/_tuwunel/server_version >/dev/null 2>&1; then\n log \"$(msg install.wait_matrix.ok)\"\n return 0\n fi\n sleep 5\n elapsed=$((elapsed + 5))\n printf \"\\r\\033[36m[AgentTeams]\\033[0m $(msg install.wait_matrix.waiting \"${elapsed}\" \"${timeout}\")\"\n done\n\n echo \"\"\n die \"$(msg install.wait_matrix.timeout \"${timeout}\" \"${container}\")\"\n}\n\n# Read KEY=value from /data/agentteams-secrets.env on a Docker volume (manager container not required).\n# Requires EMBEDDED_IMAGE (resolved earlier in install_manager). Uses ${DOCKER_CMD}.\nagentteams_read_secret_from_data_volume() {\n local _vol=\"$1\" _key=\"$2\"\n if [ -z \"${_vol}\" ] || [ -z \"${_key}\" ] || [ -z \"${EMBEDDED_IMAGE:-}\" ]; then\n echo \"\"\n return 0\n fi\n ${DOCKER_CMD} run --rm --entrypoint sh \\\n -v \"${_vol}:/data:ro\" \\\n \"${EMBEDDED_IMAGE}\" -c \"grep \\\"^${_key}=\\\" /data/agentteams-secrets.env 2>/dev/null | cut -d= -f2- | head -1 | tr -d '\\r'\" 2>/dev/null\n}\n\n# Read KEY=value from /data/worker-creds/<worker>.env on a Docker volume.\nagentteams_read_worker_creds_value_from_volume() {\n local _vol=\"$1\" _worker=\"$2\" _key=\"$3\"\n if [ -z \"${_vol}\" ] || [ -z \"${_worker}\" ] || [ -z \"${_key}\" ] || [ -z \"${EMBEDDED_IMAGE:-}\" ]; then\n echo \"\"\n return 0\n fi\n ${DOCKER_CMD} run --rm --entrypoint sh \\\n -v \"${_vol}:/data:ro\" \\\n \"${EMBEDDED_IMAGE}\" -c \"grep \\\"^${_key}=\\\" \\\"/data/worker-creds/${_worker}.env\\\" 2>/dev/null | cut -d= -f2- | head -1 | tr -d \\\"\\\\r\\\"\" 2>/dev/null\n}\n\n# Read admin_dm_room_id from host workspace state.json (fallback when Matrix API is unavailable).\nagentteams_read_admin_dm_room_from_workspace() {\n local _ws=\"$1\"\n local _f=\"${_ws}/state.json\"\n if [ ! -f \"${_f}\" ] || ! command -v jq >/dev/null 2>&1; then\n echo \"\"\n return 0\n fi\n jq -r '.admin_dm_room_id // empty | select(. != \"null\")' \"${_f}\" 2>/dev/null\n}\n\n# Read secret input with masked echo (shows * per keystroke, supports backspace)\n# Usage: read_secret \"prompt text: \"; value=\"${_RS_RESULT}\"\nread_secret() {\n local _rs_prompt=\"$1\"\n _RS_RESULT=\"\"\n local _rs_char=\"\"\n\n printf \"%s\" \"${_rs_prompt}\"\n\n while IFS= read -r -s -n 1 _rs_char; do\n if [[ -z \"${_rs_char}\" ]]; then\n break\n elif [[ \"${_rs_char}\" == $'\\177' ]] || [[ \"${_rs_char}\" == $'\\b' ]]; then\n if [ -n \"${_RS_RESULT}\" ]; then\n _RS_RESULT=\"${_RS_RESULT%?}\"\n printf \"\\b \\b\"\n fi\n else\n _RS_RESULT=\"${_RS_RESULT}${_rs_char}\"\n printf \"*\"\n fi\n done\n echo\n}\n\n# Load current parameter values from env file for upgrade mode display\nload_current_params_from_env() {\n local env_file=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n if [ -f \"${env_file}\" ]; then\n [ -z \"${AGENTTEAMS_LLM_PROVIDER:+x}\" ] && AGENTTEAMS_LLM_PROVIDER=\"$(grep '^AGENTTEAMS_LLM_PROVIDER=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_OPENAI_BASE_URL:+x}\" ] && AGENTTEAMS_OPENAI_BASE_URL=\"$(grep '^AGENTTEAMS_OPENAI_BASE_URL=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_DEFAULT_MODEL:+x}\" ] && AGENTTEAMS_DEFAULT_MODEL=\"$(grep '^AGENTTEAMS_DEFAULT_MODEL=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_EMBEDDING_MODEL:+x}\" ] && AGENTTEAMS_EMBEDDING_MODEL=\"$(grep '^AGENTTEAMS_EMBEDDING_MODEL=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_WORKSPACE_DIR:+x}\" ] && AGENTTEAMS_WORKSPACE_DIR=\"$(grep '^AGENTTEAMS_WORKSPACE_DIR=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_HOST_SHARE_DIR:+x}\" ] && AGENTTEAMS_HOST_SHARE_DIR=\"$(grep '^AGENTTEAMS_HOST_SHARE_DIR=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_PODMAN_AUTOSTART:+x}\" ] && AGENTTEAMS_PODMAN_AUTOSTART=\"$(grep '^AGENTTEAMS_PODMAN_AUTOSTART=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_DASHBOARD:+x}\" ] && AGENTTEAMS_DASHBOARD=\"$(grep '^AGENTTEAMS_DASHBOARD=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_DASHBOARD_VERSION:+x}\" ] && AGENTTEAMS_DASHBOARD_VERSION=\"$(grep '^AGENTTEAMS_DASHBOARD_VERSION=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_PORT_DASHBOARD:+x}\" ] && AGENTTEAMS_PORT_DASHBOARD=\"$(grep '^AGENTTEAMS_PORT_DASHBOARD=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_DASHBOARD_IMAGE:+x}\" ] && AGENTTEAMS_DASHBOARD_IMAGE=\"$(grep '^AGENTTEAMS_DASHBOARD_IMAGE=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL:+x}\" ] && AGENTTEAMS_AI_GATEWAY_ADMIN_URL=\"$(grep '^AGENTTEAMS_AI_GATEWAY_ADMIN_URL=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n fi\n}\n\n# In non-interactive mode, uses default or errors if required and no default.\n# Usage: prompt VAR_NAME \"Prompt text\" \"default\" [true=secret]\nprompt() {\n local var_name=\"$1\"\n local prompt_text=\"$2\"\n local default_value=\"$3\"\n local is_secret=\"${4:-false}\"\n\n # If the variable is already set in the environment, use it silently\n # In upgrade mode, show current value and let user change it\n eval \"local current_value=\\\"\\${${var_name}}\\\"\"\n if [ -n \"${current_value}\" ]; then\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_NON_INTERACTIVE}\" != \"1\" ]; then\n # Show masked value for secrets, full value otherwise\n local display_value=\"${current_value}\"\n if [ \"${is_secret}\" = \"true\" ]; then\n local len=${#current_value}\n if [ \"${len}\" -le 8 ]; then\n display_value=\"****\"\n else\n display_value=\"${current_value:0:4}****${current_value: -4}\"\n fi\n fi\n log \"$(msg prompt.upgrade_keep \"${prompt_text}\" \"${display_value}\")\"\n local new_value=\"\"\n if [ \"${is_secret}\" = \"true\" ]; then\n read_secret \"${prompt_text}: \"\n new_value=\"${_RS_RESULT}\"\n else\n read -e -p \"${prompt_text}: \" new_value\n if [ \"${new_value}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n fi\n if [ -n \"${new_value}\" ]; then\n eval \"export ${var_name}='${new_value}'\"\n fi\n return\n fi\n log \"$(msg prompt.preset \"${prompt_text}\")\"\n return\n fi\n\n # Non-interactive or quickstart: use default or error\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] || [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ]; then\n if [ -n \"${default_value}\" ]; then\n eval \"export ${var_name}='${default_value}'\"\n log \"$(msg prompt.default \"${prompt_text}\" \"${default_value}\")\"\n return\n elif [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n # Only hard-error in fully non-interactive mode, not quickstart\n die \"$(msg prompt.required \"${prompt_text}\")\"\n fi\n # quickstart + no default: fall through to interactive prompt below\n fi\n\n if [ -n \"${default_value}\" ]; then\n prompt_text=\"${prompt_text} [${default_value}]\"\n fi\n\n local value=\"\"\n if [ \"${is_secret}\" = \"true\" ]; then\n read_secret \"${prompt_text}: \"\n value=\"${_RS_RESULT}\"\n else\n read -e -p \"${prompt_text}: \" value\n if [ \"${value}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n fi\n\n value=\"${value:-${default_value}}\"\n if [ -z \"${value}\" ]; then\n die \"$(msg prompt.required_empty \"${prompt_text}\")\"\n fi\n\n eval \"export ${var_name}='${value}'\"\n}\n\n# Prompt for an optional value (empty string is acceptable)\n# Skips prompt if variable is already defined in environment (even if empty)\n# In upgrade mode, shows current value and lets user change it.\n# In non-interactive mode, defaults to empty string.\nprompt_optional() {\n local var_name=\"$1\"\n local prompt_text=\"$2\"\n local is_secret=\"${3:-false}\"\n\n # Check if variable is defined (even if set to empty string)\n eval \"local _chk=\\\"\\${${var_name}+x}\\\"\"\n if [ -n \"${_chk}\" ]; then\n # In upgrade mode, show current value and let user change it\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_NON_INTERACTIVE}\" != \"1\" ]; then\n eval \"local current_value=\\\"\\${${var_name}}\\\"\"\n local display_value=\"${current_value}\"\n if [ \"${is_secret}\" = \"true\" ] && [ -n \"${current_value}\" ]; then\n local len=${#current_value}\n if [ \"${len}\" -le 8 ]; then\n display_value=\"****\"\n else\n display_value=\"${current_value:0:4}****${current_value: -4}\"\n fi\n fi\n if [ -n \"${current_value}\" ]; then\n if [ \"${is_secret}\" = \"true\" ]; then\n log \"$(msg prompt.upgrade_keep_secret \"${prompt_text}\" \"${display_value}\")\"\n else\n log \"$(msg prompt.upgrade_keep \"${prompt_text}\" \"${display_value}\")\"\n fi\n else\n log \"$(msg prompt.upgrade_empty \"${prompt_text}\")\"\n fi\n local new_value=\"\"\n if [ \"${is_secret}\" = \"true\" ]; then\n read_secret \"${prompt_text}: \"\n new_value=\"${_RS_RESULT}\"\n else\n read -e -p \"${prompt_text}: \" new_value\n if [ \"${new_value}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n fi\n if [ -n \"${new_value}\" ]; then\n eval \"export ${var_name}='${new_value}'\"\n fi\n return\n fi\n log \"$(msg prompt.preset \"${prompt_text}\")\"\n return\n fi\n\n # Non-interactive or quickstart: skip, leave unset\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] || [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ]; then\n eval \"export ${var_name}=''\"\n return\n fi\n\n local value=\"\"\n if [ \"${is_secret}\" = \"true\" ]; then\n read_secret \"${prompt_text}: \"\n value=\"${_RS_RESULT}\"\n else\n read -e -p \"${prompt_text}: \" value\n if [ \"${value}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n fi\n\n eval \"export ${var_name}='${value}'\"\n}\n\ngenerate_key() {\n openssl rand -hex 32\n}\n\n# ============================================================\n# Detect container runtime socket on the host\n#\n# Dependency: Requires DOCKER_CMD to reflect the TRUE underlying runtime.\n# (Crucial for unmasking 'podman' disguised as 'docker'.\n# Must be strictly resolved prior, e.g., via check_container_runtime)\n# ============================================================\ndetect_socket() {\n local socket_path\n\n # 1. Respect explicitly defined DOCKER_HOST environment variable\n if [ -n \"${DOCKER_HOST}\" ]; then\n socket_path=$(echo \"${DOCKER_HOST}\" | sed 's|^unix://||')\n if [ -S \"${socket_path}\" ]; then\n echo \"${socket_path}\"\n return 0\n fi\n fi\n\n # 2. Route based on the resolved container runtime\n case \"${DOCKER_CMD}\" in\n docker)\n # Docker path handling\n socket_path=$(docker context ls --format '{{if .Current}}{{.DockerEndpoint}}{{end}}' 2>/dev/null | grep . | sed 's|^unix://||')\n if [ -n \"${socket_path}\" ] && [ -S \"${socket_path}\" ]; then\n echo \"${socket_path}\"\n return 0\n fi\n\n # Rootless Docker\n if [ -n \"${XDG_RUNTIME_DIR:-}\" ] && [ -S \"${XDG_RUNTIME_DIR}/docker.sock\" ]; then\n echo \"${XDG_RUNTIME_DIR}/docker.sock\"\n return 0\n fi\n\n # Rootless Docker (macOS fallback)\n if [ -S \"${HOME}/.docker/run/docker.sock\" ]; then\n echo \"${HOME}/.docker/run/docker.sock\"\n return 0\n fi\n\n # Root Docker\n if [ -S \"/var/run/docker.sock\" ]; then\n echo \"/var/run/docker.sock\"\n return 0\n fi\n ;;\n podman)\n # Podman path handling\n if [ \"$(id -u)\" -eq 0 ]; then\n socket_path=\"/run/podman/podman.sock\"\n else\n socket_path=\"${XDG_RUNTIME_DIR}/podman/podman.sock\"\n fi\n if [ -S \"${socket_path}\" ]; then\n echo \"${socket_path}\"\n return 0\n fi\n ;;\n esac\n\n # Return empty if no socket is found\n echo \"\"\n return 0\n}\n\n# Ensure Podman API socket is active (State mutation)\nensure_podman_socket() {\n local socket_path\n\n # Only applicable if runtime is podman and systemctl is available\n if [ \"${DOCKER_CMD:-}\" != \"podman\" ] || ! command -v systemctl >/dev/null 2>&1; then\n return 0\n fi\n\n echo \"Ensuring Podman API socket is active...\" >&2\n\n # Enable and start the socket based on user privileges\n if [ \"$(id -u)\" -eq 0 ]; then\n socket_path=\"/run/podman/podman.sock\"\n systemctl enable --now podman.socket >/dev/null 2>&1 || true\n else\n socket_path=\"${XDG_RUNTIME_DIR}/podman/podman.sock\"\n systemctl --user enable --now podman.socket >/dev/null 2>&1 || true\n fi\n\n # Give systemd a brief moment to assert the socket for root or rootless user\n local _i\n for _i in 1 2 3; do\n [ -S \"${socket_path}\" ] && break\n sleep 1\n done\n}\n\n# Detect local LAN IP address (cross-platform: macOS and Linux)\ndetect_lan_ip() {\n local ip=\"\"\n\n # macOS: try common Wi-Fi / Ethernet interfaces\n if command -v ipconfig >/dev/null 2>&1; then\n for iface in en0 en1 en2 en3 en4; do\n ip=$(ipconfig getifaddr \"${iface}\" 2>/dev/null)\n if [ -n \"${ip}\" ]; then\n echo \"${ip}\"\n return 0\n fi\n done\n fi\n\n # Linux: ip route — most reliable\n if command -v ip >/dev/null 2>&1; then\n ip=$(ip route get 1.1.1.1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i==\"src\"){print $(i+1); exit}}')\n if [ -n \"${ip}\" ]; then\n echo \"${ip}\"\n return 0\n fi\n fi\n\n # Linux fallback: hostname -I (space-separated list, take first non-loopback)\n if command -v hostname >/dev/null 2>&1; then\n ip=$(hostname -I 2>/dev/null | tr ' ' '\\n' | grep -v '^127\\.' | grep -v '^::' | head -1)\n if [ -n \"${ip}\" ]; then\n echo \"${ip}\"\n return 0\n fi\n fi\n\n # Last resort: ifconfig\n if command -v ifconfig >/dev/null 2>&1; then\n ip=$(ifconfig 2>/dev/null | awk '/inet /{if($2!~/^127\\./){print $2; exit}}')\n # Strip \"addr:\" prefix that some ifconfig versions add\n ip=\"${ip#addr:}\"\n if [ -n \"${ip}\" ]; then\n echo \"${ip}\"\n return 0\n fi\n fi\n\n echo \"\"\n}\n\n\n# ============================================================\n# Step-back navigation helpers\n# ============================================================\n\n# should_skip_step: returns 0 (skip) when the step is irrelevant in current mode\nshould_skip_step() {\n local step_fn=\"$1\"\n case \"${step_fn}\" in\n step_lang|step_mode)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n ;;\n step_version)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n ;;\n step_existing)\n local _env=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n [ ! -f \"${_env}\" ] && return 0\n ;;\n # Keep-All upgrade mode: skip all config steps (step_volume/step_workspace handled separately)\n step_llm|step_admin|step_network|step_ports|step_domains|step_github|step_skills|step_runtime|step_manager_runtime|step_dashboard|step_e2ee|step_docker_proxy|step_idle|step_hostshare)\n [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ] && return 0\n ;;\n step_volume|step_workspace)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ] && return 0\n [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ] && return 0\n ;;\n step_e2ee|step_idle)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE}\" != \"1\" ] && return 0\n ;;\n step_docker_proxy)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE}\" != \"1\" ] && return 0\n # Embedded mode handles docker access natively — skip this step\n [ \"${AGENTTEAMS_USE_EMBEDDED:-}\" = \"1\" ] && return 0\n ;;\n step_manager_runtime)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n ;;\n step_hostshare)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ] && return 0\n ;;\n step_podman_autostart)\n # Only relevant for Podman with systemd\n [ \"${DOCKER_CMD}\" != \"podman\" ] && return 0\n ! command -v systemctl >/dev/null 2>&1 && return 0\n\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE}\" != \"1\" ] && return 0\n [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ] && return 0\n ;;\n esac\n return 1\n}\n\n# clear_step_vars: unset variables set by a step so it will re-prompt on re-entry\nclear_step_vars() {\n local step_fn=\"$1\"\n case \"${step_fn}\" in\n step_mode) unset AGENTTEAMS_QUICKSTART ;;\n step_version) unset AGENTTEAMS_VERSION ;;\n step_existing) unset AGENTTEAMS_UPGRADE UPGRADE_EXISTING_WORKERS ;;\n step_llm)\n unset AGENTTEAMS_LLM_PROVIDER AGENTTEAMS_DEFAULT_MODEL AGENTTEAMS_OPENAI_BASE_URL\n unset AGENTTEAMS_LLM_API_KEY AGENTTEAMS_MODEL_CONTEXT_WINDOW AGENTTEAMS_MODEL_MAX_TOKENS\n unset AGENTTEAMS_MODEL_REASONING AGENTTEAMS_MODEL_VISION\n ;;\n step_admin) unset AGENTTEAMS_ADMIN_USER AGENTTEAMS_ADMIN_PASSWORD ;;\n step_network) unset AGENTTEAMS_LOCAL_ONLY ;;\n step_ports)\n unset AGENTTEAMS_PORT_GATEWAY AGENTTEAMS_PORT_CONSOLE\n unset AGENTTEAMS_PORT_ELEMENT_WEB AGENTTEAMS_PORT_MANAGER_CONSOLE\n ;;\n step_domains)\n unset AGENTTEAMS_MATRIX_DOMAIN AGENTTEAMS_MATRIX_CLIENT_DOMAIN\n unset AGENTTEAMS_AI_GATEWAY_DOMAIN AGENTTEAMS_FS_DOMAIN AGENTTEAMS_CONSOLE_DOMAIN\n ;;\n step_github) unset AGENTTEAMS_GITHUB_TOKEN ;;\n step_skills) unset AGENTTEAMS_SKILLS_API_URL ;;\n step_volume) unset AGENTTEAMS_DATA_DIR ;;\n step_workspace) unset AGENTTEAMS_WORKSPACE_DIR ;;\n step_dashboard) unset AGENTTEAMS_DASHBOARD AGENTTEAMS_DASHBOARD_VERSION AGENTTEAMS_PORT_DASHBOARD AGENTTEAMS_DASHBOARD_IMAGE AGENTTEAMS_AI_GATEWAY_ADMIN_URL ;;\n step_runtime) unset AGENTTEAMS_DEFAULT_WORKER_RUNTIME ;;\n step_manager_runtime) unset AGENTTEAMS_MANAGER_RUNTIME ;;\n step_e2ee) unset AGENTTEAMS_MATRIX_E2EE ;;\n step_docker_proxy) unset AGENTTEAMS_DOCKER_PROXY; unset AGENTTEAMS_PROXY_ALLOWED_REGISTRIES ;;\n step_idle) unset AGENTTEAMS_WORKER_IDLE_TIMEOUT ;;\n step_hostshare) unset AGENTTEAMS_HOST_SHARE_DIR ;;\n step_podman_autostart) unset AGENTTEAMS_PODMAN_AUTOSTART ;;\n esac\n}\n\n# ============================================================\n# Individual step functions\n# ============================================================\n\nstep_lang() {\n local lang_default_choice=\"2\"\n [ \"${AGENTTEAMS_LANGUAGE}\" = \"zh\" ] && lang_default_choice=\"1\"\n log \"$(msg lang.detected)\"\n log \"$(msg lang.switch_title)\"\n echo \"$(msg lang.option_zh)\"\n echo \"$(msg lang.option_en)\"\n echo \"\"\n local LANG_CHOICE\n read -e -p \"$(msg lang.prompt) [${lang_default_choice}]: \" LANG_CHOICE\n LANG_CHOICE=\"${LANG_CHOICE:-${lang_default_choice}}\"\n if [ \"${LANG_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n case \"${LANG_CHOICE}\" in\n 1) AGENTTEAMS_LANGUAGE=\"zh\" ;;\n 2) AGENTTEAMS_LANGUAGE=\"en\" ;;\n esac\n export AGENTTEAMS_LANGUAGE\n log \"\"\n}\n\nstep_mode() {\n log \"$(msg install.mode.title)\"\n echo \"\"\n echo \"$(msg install.mode.choose)\"\n echo \"$(msg install.mode.quickstart)\"\n echo \"$(msg install.mode.manual)\"\n echo \"\"\n local ONBOARDING_CHOICE\n read -e -p \"$(msg install.mode.prompt): \" ONBOARDING_CHOICE\n ONBOARDING_CHOICE=\"${ONBOARDING_CHOICE:-1}\"\n if [ \"${ONBOARDING_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n case \"${ONBOARDING_CHOICE}\" in\n 1|quick|quickstart)\n log \"$(msg install.mode.quickstart_selected)\"\n AGENTTEAMS_QUICKSTART=1\n ;;\n 2|manual)\n log \"$(msg install.mode.manual_selected)\"\n ;;\n *)\n log \"$(msg install.mode.invalid)\"\n AGENTTEAMS_QUICKSTART=1\n ;;\n esac\n log \"\"\n}\n\nstep_version() {\n # Skip if version already provided via env var\n if [ -n \"${AGENTTEAMS_VERSION}\" ]; then\n resolve_image_tags\n return 0\n fi\n # Try to fetch the latest stable release from GitHub\n log \"$(msg install.version.fetching)\"\n local _fetched\n _fetched=$(curl -sf --max-time 5 \\\n -H \"Accept: application/vnd.github+json\" \\\n \"https://api.github.com/repos/agentscope-ai/AgentTeams/releases/latest\" \\\n 2>/dev/null | grep '\"tag_name\"' | head -1 | sed 's/.*\"tag_name\": *\"\\([^\"]*\\)\".*/\\1/')\n if [ -n \"${_fetched}\" ]; then\n AGENTTEAMS_KNOWN_STABLE_VERSION=\"${_fetched}\"\n else\n log \"$(msg install.version.fetch_failed \"${AGENTTEAMS_KNOWN_STABLE_VERSION}\")\"\n fi\n log \"$(msg install.version.title)\"\n echo \"\"\n echo \"$(msg install.version.choose)\"\n echo \"$(msg install.version.option_latest)\"\n printf \"%s\\n\" \"$(msg install.version.option_stable \"${AGENTTEAMS_KNOWN_STABLE_VERSION}\")\"\n echo \"$(msg install.version.option_custom)\"\n echo \"\"\n local VERSION_CHOICE\n read -e -p \"$(msg install.version.prompt) [1]: \" VERSION_CHOICE\n VERSION_CHOICE=\"${VERSION_CHOICE:-1}\"\n if [ \"${VERSION_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n case \"${VERSION_CHOICE}\" in\n 1|latest)\n AGENTTEAMS_VERSION=\"latest\"\n log \"$(msg install.version.selected_latest)\"\n ;;\n 2|stable)\n AGENTTEAMS_VERSION=\"${AGENTTEAMS_KNOWN_STABLE_VERSION}\"\n log \"$(msg install.version.selected_stable \"${AGENTTEAMS_VERSION}\")\"\n ;;\n 3|custom)\n local CUSTOM_VERSION\n read -e -p \"$(msg install.version.custom_prompt): \" CUSTOM_VERSION\n AGENTTEAMS_VERSION=\"${CUSTOM_VERSION:-${AGENTTEAMS_KNOWN_STABLE_VERSION}}\"\n log \"$(msg install.version.selected_custom \"${AGENTTEAMS_VERSION}\")\"\n ;;\n *)\n AGENTTEAMS_VERSION=\"${AGENTTEAMS_KNOWN_STABLE_VERSION}\"\n log \"$(msg install.version.invalid \"${AGENTTEAMS_VERSION}\")\"\n ;;\n esac\n log \"\"\n resolve_image_tags\n}\n\nstep_existing() {\n local existing_env=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n log \"$(msg install.existing.detected \"${existing_env}\")\"\n local running_manager=\"\" running_workers=\"\" existing_workers=\"\"\n if ${DOCKER_CMD} ps --format '{{.Names}}' | grep -q \"^agentteams-manager$\"; then\n running_manager=\"agentteams-manager\"\n fi\n running_workers=$(${DOCKER_CMD} ps --format '{{.Names}}' | grep \"^agentteams-worker-\" || true)\n existing_workers=$(${DOCKER_CMD} ps -a --format '{{.Names}}' | grep \"^agentteams-worker-\" || true)\n local UPGRADE_CHOICE\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n log \"$(msg install.existing.upgrade_noninteractive)\"\n UPGRADE_CHOICE=\"upgrade\"\n else\n echo \"\"\n echo \"$(msg install.existing.choose)\"\n echo \"$(msg install.existing.upgrade)\"\n echo \"$(msg install.existing.reinstall)\"\n echo \"$(msg install.existing.cancel)\"\n echo \"\"\n read -e -p \"$(msg install.existing.prompt): \" UPGRADE_CHOICE\n UPGRADE_CHOICE=\"${UPGRADE_CHOICE:-1}\"\n if [ \"${UPGRADE_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n fi\n case \"${UPGRADE_CHOICE}\" in\n 1|upgrade)\n AGENTTEAMS_UPGRADE=1\n log \"$(msg install.existing.upgrading)\"\n # Show upgrade mode sub-menu (unless already set via env var)\n if [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL:-0}\" != \"1\" ]; then\n echo \"\"\n echo \"$(msg upgrade.mode.prompt)\"\n echo \"$(msg upgrade.mode.keep_all)\"\n echo \"$(msg upgrade.mode.confirm_each)\"\n echo \"$(msg upgrade.mode.back)\"\n echo \"\"\n local UPGRADE_MODE_CHOICE\n read -e -p \"$(msg install.existing.prompt): \" UPGRADE_MODE_CHOICE\n UPGRADE_MODE_CHOICE=\"${UPGRADE_MODE_CHOICE:-1}\"\n case \"${UPGRADE_MODE_CHOICE}\" in\n 1|keep)\n AGENTTEAMS_UPGRADE_KEEP_ALL=1\n ;;\n 2|confirm)\n AGENTTEAMS_UPGRADE_KEEP_ALL=0\n ;;\n 3|b)\n STEP_RESULT=\"back\"\n return 0\n ;;\n *)\n AGENTTEAMS_UPGRADE_KEEP_ALL=0\n ;;\n esac\n fi\n # Load current parameters for both Keep-All and confirm-each modes\n load_current_params_from_env\n if [ -n \"${running_manager}\" ] || [ -n \"${running_workers}\" ]; then\n echo \"\"\n echo -e \"\\033[33m$(msg install.existing.warn_manager_stop)\\033[0m\"\n if [ -n \"${existing_workers}\" ]; then\n echo -e \"\\033[33m$(msg install.existing.warn_worker_recreate)\\033[0m\"\n fi\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" != \"1\" ]; then\n echo \"\"\n local CONFIRM_STOP\n read -e -p \"$(msg install.existing.continue_prompt): \" CONFIRM_STOP\n if [ \"${CONFIRM_STOP}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n if [ \"${CONFIRM_STOP}\" != \"y\" ] && [ \"${CONFIRM_STOP}\" != \"Y\" ]; then\n log \"$(msg install.existing.cancelled)\"\n exit 0\n fi\n fi\n fi\n UPGRADE_EXISTING_WORKERS=\"${existing_workers}\"\n ;;\n 2|reinstall)\n log \"$(msg install.reinstall.performing)\"\n local existing_workspace=\"\"\n if [ -f \"${existing_env}\" ]; then\n existing_workspace=$(grep '^AGENTTEAMS_WORKSPACE_DIR=' \"${existing_env}\" 2>/dev/null | cut -d= -f2-)\n fi\n [ -z \"${existing_workspace}\" ] && existing_workspace=\"${HOME}/agentteams-manager\"\n echo \"\"\n echo -e \"\\033[33m$(msg install.reinstall.warn_stop)\\033[0m\"\n [ -n \"${running_manager}\" ] && echo -e \"\\033[33m - ${running_manager} (manager)\\033[0m\"\n for w in ${running_workers}; do\n echo -e \"\\033[33m - ${w} (worker)\\033[0m\"\n done\n echo \"\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_delete)\\033[0m\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_volume)\\033[0m\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_env \"${existing_env}\")\\033[0m\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_workspace \"${existing_workspace}\")\\033[0m\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_workers)\\033[0m\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_proxy)\\033[0m\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_network)\\033[0m\"\n echo \"\"\n echo -e \"\\033[31m$(msg install.reinstall.confirm_type)\\033[0m\"\n echo -e \"\\033[31m ${existing_workspace}\\033[0m\"\n echo \"\"\n local CONFIRM_PATH\n read -e -p \"$(msg install.reinstall.confirm_path): \" CONFIRM_PATH\n if [ \"${CONFIRM_PATH}\" != \"${existing_workspace}\" ]; then\n die \"$(msg install.reinstall.path_mismatch \"${CONFIRM_PATH}\" \"${existing_workspace}\")\"\n fi\n log \"$(msg install.reinstall.confirmed)\"\n ${DOCKER_CMD} stop agentteams-manager 2>/dev/null || true\n ${DOCKER_CMD} rm agentteams-manager 2>/dev/null || true\n for w in $(${DOCKER_CMD} ps -a --format '{{.Names}}' | grep \"^agentteams-worker-\" || true); do\n ${DOCKER_CMD} stop \"${w}\" 2>/dev/null || true\n ${DOCKER_CMD} rm \"${w}\" 2>/dev/null || true\n log \"$(msg install.reinstall.removed_worker \"${w}\")\"\n done\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' | grep -q \"^agentteams-controller$\"; then\n log \"$(msg install.reinstall.removing_proxy)\"\n ${DOCKER_CMD} stop agentteams-controller 2>/dev/null || true\n ${DOCKER_CMD} rm agentteams-controller 2>/dev/null || true\n fi\n if ${DOCKER_CMD} network ls --format '{{.Name}}' | grep -q \"^agentteams-net$\"; then\n log \"$(msg install.reinstall.removing_network)\"\n ${DOCKER_CMD} network rm agentteams-net 2>/dev/null || true\n fi\n if ${DOCKER_CMD} volume ls -q | grep -q \"^agentteams-data$\"; then\n log \"$(msg install.reinstall.removing_volume)\"\n ${DOCKER_CMD} volume rm agentteams-data 2>/dev/null || log \"$(msg install.reinstall.warn_volume_fail)\"\n fi\n if [ -d \"${existing_workspace}\" ]; then\n log \"$(msg install.reinstall.removing_workspace \"${existing_workspace}\")\"\n rm -rf \"${existing_workspace}\" || die \"$(msg install.reinstall.failed_rm_workspace)\"\n fi\n if [ -f \"${existing_env}\" ]; then\n log \"$(msg install.reinstall.removing_env \"${existing_env}\")\"\n rm -f \"${existing_env}\"\n fi\n log \"$(msg install.reinstall.cleanup_done)\"\n unset AGENTTEAMS_WORKSPACE_DIR\n return 0\n ;;\n 3|cancel|*)\n log \"$(msg install.existing.cancelled)\"\n exit 0\n ;;\n esac\n # Load existing env file as fallback (shell env vars take priority)\n if [ -f \"${existing_env}\" ]; then\n log \"$(msg install.loading_config \"${existing_env}\")\"\n while IFS='=' read -r key value; do\n case \"${key}\" in \\#*|\"\") continue ;; esac\n value=\"${value%%#*}\"\n value=\"${value#\"${value%%[![:space:]]*}\"}\"\n value=\"${value%\"${value##*[![:space:]]}\"}\"\n eval \"_existing_val=\\\"\\${${key}+x}\\\"\"\n if [ -z \"${_existing_val}\" ]; then export \"${key}=${value}\"; fi\n done < \"${existing_env}\"\n fi\n}\n\nstep_llm() {\n log \"$(msg llm.title)\"\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n if [ \"${AGENTTEAMS_LANGUAGE}\" = \"zh\" ]; then\n AGENTTEAMS_LLM_PROVIDER=\"${AGENTTEAMS_LLM_PROVIDER:-openai-compat}\"\n AGENTTEAMS_DEFAULT_MODEL=\"${AGENTTEAMS_DEFAULT_MODEL:-qwen3.6-plus}\"\n AGENTTEAMS_OPENAI_BASE_URL=\"${AGENTTEAMS_OPENAI_BASE_URL:-https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1}\"\n log \"$(msg llm.provider.label \"${AGENTTEAMS_LLM_PROVIDER}\")\"\n log \"$(msg llm.openai.base_url_label \"${AGENTTEAMS_OPENAI_BASE_URL}\")\"\n else\n AGENTTEAMS_LLM_PROVIDER=\"${AGENTTEAMS_LLM_PROVIDER:-qwen}\"\n AGENTTEAMS_DEFAULT_MODEL=\"${AGENTTEAMS_DEFAULT_MODEL:-qwen3.6-plus}\"\n AGENTTEAMS_OPENAI_BASE_URL=\"${AGENTTEAMS_OPENAI_BASE_URL:-}\"\n log \"$(msg llm.provider.qwen_default \"${AGENTTEAMS_LLM_PROVIDER}\")\"\n fi\n log \"$(msg llm.model.default \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n prompt AGENTTEAMS_LLM_API_KEY \"$(msg llm.apikey_prompt)\" \"\" \"true\"\n AGENTTEAMS_EMBEDDING_MODEL=\"${AGENTTEAMS_EMBEDDING_MODEL-text-embedding-v4}\"\n export AGENTTEAMS_LLM_PROVIDER AGENTTEAMS_DEFAULT_MODEL AGENTTEAMS_EMBEDDING_MODEL\n [ -n \"${AGENTTEAMS_OPENAI_BASE_URL+x}\" ] && export AGENTTEAMS_OPENAI_BASE_URL\n return 0\n fi\n\n # Upgrade Keep-All mode: skip provider selection, use loaded values\n local SKIP_TO_EMBEDDING=0\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ] && [ -n \"${AGENTTEAMS_LLM_PROVIDER}\" ]; then\n log \"$(msg llm.provider.label \"${AGENTTEAMS_LLM_PROVIDER}\")\"\n log \"$(msg llm.openai.base_url_label \"${AGENTTEAMS_OPENAI_BASE_URL}\")\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n PROVIDER_CHOICE=\"${AGENTTEAMS_LLM_PROVIDER}\"\n SKIP_TO_EMBEDDING=1\n else\n echo \"\"\n echo \"$(msg llm.providers_title)\"\n echo \"$(msg llm.provider.alibaba)\"\n echo \"$(msg llm.provider.openai_compat)\"\n echo \"\"\n local PROVIDER_CHOICE\n # If upgrade mode with loaded provider, show current as default\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_LLM_PROVIDER}\" ]; then\n local _prov_display\n case \"${AGENTTEAMS_LLM_PROVIDER}\" in\n openai-compat) _prov_display=\"2\" ;;\n *) _prov_display=\"1\" ;;\n esac\n read -e -p \"$(msg llm.provider.select) [${_prov_display}]: \" PROVIDER_CHOICE\n PROVIDER_CHOICE=\"${PROVIDER_CHOICE:-${_prov_display}}\"\n elif [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ]; then\n read -e -p \"$(msg llm.provider.select) [1]: \" PROVIDER_CHOICE\n PROVIDER_CHOICE=\"${PROVIDER_CHOICE:-1}\"\n else\n read -e -p \"$(msg llm.provider.select): \" PROVIDER_CHOICE\n PROVIDER_CHOICE=\"${PROVIDER_CHOICE:-1}\"\n fi\n if [ \"${PROVIDER_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n fi\n local ALIBABA_MODEL_CHOICE=\"\"\n local ALIBABA_ACCESS=\"\"\n case \"${PROVIDER_CHOICE}\" in\n 1|alibaba-cloud|openai-compat)\n if [ \"${AGENTTEAMS_LANGUAGE}\" = \"en\" ]; then\n AGENTTEAMS_LLM_PROVIDER=\"openai-compat\"\n AGENTTEAMS_OPENAI_BASE_URL=\"https://dashscope-intl.aliyuncs.com/compatible-mode/v1\"\n ALIBABA_ACCESS=\"tokenplan\"\n echo \"\"\n echo \"$(msg llm.codingplan.models_title)\"\n echo \"$(msg llm.codingplan.model.qwen36plus)\"\n echo \"$(msg llm.codingplan.model.glm5)\"\n echo \"$(msg llm.codingplan.model.kimi)\"\n echo \"$(msg llm.codingplan.model.minimax)\"\n echo \"\"\n local CODINGPLAN_MODEL_CHOICE\n # If upgrade with loaded model, show current as default\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_DEFAULT_MODEL}\" ]; then\n local _model_default\n case \"${AGENTTEAMS_DEFAULT_MODEL}\" in\n qwen3.6-plus) _model_default=\"1\" ;;\n glm-5) _model_default=\"2\" ;;\n kimi-k2.5) _model_default=\"3\" ;;\n MiniMax-M2.5) _model_default=\"4\" ;;\n *) _model_default=\"1\" ;;\n esac\n read -e -p \"$(msg llm.codingplan.model.select) [${_model_default}]: \" CODINGPLAN_MODEL_CHOICE\n CODINGPLAN_MODEL_CHOICE=\"${CODINGPLAN_MODEL_CHOICE:-${_model_default}}\"\n elif [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ]; then\n read -e -p \"$(msg llm.codingplan.model.select) [1]: \" CODINGPLAN_MODEL_CHOICE\n CODINGPLAN_MODEL_CHOICE=\"${CODINGPLAN_MODEL_CHOICE:-1}\"\n else\n read -e -p \"$(msg llm.codingplan.model.select): \" CODINGPLAN_MODEL_CHOICE\n CODINGPLAN_MODEL_CHOICE=\"${CODINGPLAN_MODEL_CHOICE:-1}\"\n fi\n if [ \"${CODINGPLAN_MODEL_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n case \"${CODINGPLAN_MODEL_CHOICE}\" in\n 1|qwen3.6-plus) AGENTTEAMS_DEFAULT_MODEL=\"qwen3.6-plus\" ;;\n 2|glm-5) AGENTTEAMS_DEFAULT_MODEL=\"glm-5\" ;;\n 3|kimi-k2.5) AGENTTEAMS_DEFAULT_MODEL=\"kimi-k2.5\" ;;\n 4|MiniMax-M2.5) AGENTTEAMS_DEFAULT_MODEL=\"MiniMax-M2.5\" ;;\n *) AGENTTEAMS_DEFAULT_MODEL=\"qwen3.6-plus\" ;;\n esac\n log \"$(msg llm.provider.selected_codingplan)\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n else\n echo \"\"\n echo \"$(msg llm.alibaba.models_title)\"\n echo \"$(msg llm.alibaba.model.tokenplan)\"\n echo \"$(msg llm.alibaba.model.bailian)\"\n echo \"$(msg llm.alibaba.model.codingplan_legacy)\"\n echo \"\"\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_OPENAI_BASE_URL}\" ]; then\n # Determine alibaba access type from loaded URL\n local _access_default=\"1\"\n if echo \"${AGENTTEAMS_OPENAI_BASE_URL}\" | grep -q \"token-plan\"; then\n _access_default=\"1\"\n elif echo \"${AGENTTEAMS_OPENAI_BASE_URL}\" | grep -q \"dashscope\"; then\n if echo \"${AGENTTEAMS_OPENAI_BASE_URL}\" | grep -q \"intl\"; then\n _access_default=\"3\"\n else\n _access_default=\"2\"\n fi\n fi\n read -e -p \"$(msg llm.alibaba.model.select) [${_access_default}]: \" ALIBABA_MODEL_CHOICE\n ALIBABA_MODEL_CHOICE=\"${ALIBABA_MODEL_CHOICE:-${_access_default}}\"\n elif [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ]; then\n read -e -p \"$(msg llm.alibaba.model.select) [1]: \" ALIBABA_MODEL_CHOICE\n ALIBABA_MODEL_CHOICE=\"${ALIBABA_MODEL_CHOICE:-1}\"\n else\n read -e -p \"$(msg llm.alibaba.model.select): \" ALIBABA_MODEL_CHOICE\n ALIBABA_MODEL_CHOICE=\"${ALIBABA_MODEL_CHOICE:-1}\"\n fi\n if [ \"${ALIBABA_MODEL_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n case \"${ALIBABA_MODEL_CHOICE}\" in\n 1|token-plan|tokenplan)\n ALIBABA_ACCESS=\"tokenplan\"\n AGENTTEAMS_LLM_PROVIDER=\"openai-compat\"\n AGENTTEAMS_OPENAI_BASE_URL=\"https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1\"\n echo \"\"\n echo \"$(msg llm.codingplan.models_title)\"\n echo \"$(msg llm.codingplan.model.qwen36plus)\"\n echo \"$(msg llm.codingplan.model.glm5)\"\n echo \"$(msg llm.codingplan.model.kimi)\"\n echo \"$(msg llm.codingplan.model.minimax)\"\n echo \"\"\n local CODINGPLAN_MODEL_CHOICE\n # If upgrade with loaded model, show current as default\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_DEFAULT_MODEL}\" ]; then\n local _model_default\n case \"${AGENTTEAMS_DEFAULT_MODEL}\" in\n qwen3.6-plus) _model_default=\"1\" ;;\n glm-5) _model_default=\"2\" ;;\n kimi-k2.5) _model_default=\"3\" ;;\n MiniMax-M2.5) _model_default=\"4\" ;;\n *) _model_default=\"1\" ;;\n esac\n read -e -p \"$(msg llm.codingplan.model.select) [${_model_default}]: \" CODINGPLAN_MODEL_CHOICE\n CODINGPLAN_MODEL_CHOICE=\"${CODINGPLAN_MODEL_CHOICE:-${_model_default}}\"\n elif [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ]; then\n read -e -p \"$(msg llm.codingplan.model.select) [1]: \" CODINGPLAN_MODEL_CHOICE\n CODINGPLAN_MODEL_CHOICE=\"${CODINGPLAN_MODEL_CHOICE:-1}\"\n else\n read -e -p \"$(msg llm.codingplan.model.select): \" CODINGPLAN_MODEL_CHOICE\n CODINGPLAN_MODEL_CHOICE=\"${CODINGPLAN_MODEL_CHOICE:-1}\"\n fi\n if [ \"${CODINGPLAN_MODEL_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n case \"${CODINGPLAN_MODEL_CHOICE}\" in\n 1|qwen3.6-plus) AGENTTEAMS_DEFAULT_MODEL=\"qwen3.6-plus\" ;;\n 2|glm-5) AGENTTEAMS_DEFAULT_MODEL=\"glm-5\" ;;\n 3|kimi-k2.5) AGENTTEAMS_DEFAULT_MODEL=\"kimi-k2.5\" ;;\n 4|MiniMax-M2.5) AGENTTEAMS_DEFAULT_MODEL=\"MiniMax-M2.5\" ;;\n *) AGENTTEAMS_DEFAULT_MODEL=\"qwen3.6-plus\" ;;\n esac\n log \"$(msg llm.provider.selected_tokenplan)\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n ;;\n 2|qwen|bailian)\n ALIBABA_ACCESS=\"bailian\"\n AGENTTEAMS_LLM_PROVIDER=\"qwen\"\n AGENTTEAMS_OPENAI_BASE_URL=\"\"\n echo \"\"\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_DEFAULT_MODEL}\" ]; then\n read -e -p \"$(msg llm.qwen.model_prompt) [${AGENTTEAMS_DEFAULT_MODEL}]: \" AGENTTEAMS_DEFAULT_MODEL\n else\n read -e -p \"$(msg llm.qwen.model_prompt) [qwen3.6-plus]: \" AGENTTEAMS_DEFAULT_MODEL\n fi\n if [ \"${AGENTTEAMS_DEFAULT_MODEL}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_DEFAULT_MODEL=\"${AGENTTEAMS_DEFAULT_MODEL:-qwen3.6-plus}\"\n log \"$(msg llm.provider.selected_qwen)\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n prompt_custom_model_params \"${AGENTTEAMS_DEFAULT_MODEL}\" || return 0\n ;;\n 3|coding-plan|codingplan)\n ALIBABA_ACCESS=\"codingplan_legacy\"\n AGENTTEAMS_LLM_PROVIDER=\"openai-compat\"\n AGENTTEAMS_OPENAI_BASE_URL=\"https://coding.dashscope.aliyuncs.com/v1\"\n echo \"\"\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_DEFAULT_MODEL}\" ]; then\n read -e -p \"$(msg llm.qwen.model_prompt) [${AGENTTEAMS_DEFAULT_MODEL}]: \" AGENTTEAMS_DEFAULT_MODEL\n else\n read -e -p \"$(msg llm.qwen.model_prompt): \" AGENTTEAMS_DEFAULT_MODEL\n fi\n if [ \"${AGENTTEAMS_DEFAULT_MODEL}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_DEFAULT_MODEL=\"${AGENTTEAMS_DEFAULT_MODEL:-qwen3.6-plus}\"\n log \"$(msg llm.provider.selected_codingplan_legacy)\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n prompt_custom_model_params \"${AGENTTEAMS_DEFAULT_MODEL}\" || return 0\n ;;\n *)\n die \"$(msg llm.alibaba.model.invalid \"${ALIBABA_MODEL_CHOICE}\")\"\n ;;\n esac\n fi\n if [ \"${ALIBABA_ACCESS}\" = \"bailian\" ]; then\n log \"$(msg llm.apikey_hint_bailian)\"\n log \"$(msg llm.apikey_url_bailian)\"\n elif [ \"${AGENTTEAMS_LANGUAGE}\" = \"en\" ]; then\n log \"$(msg llm.apikey_hint_qwencloud)\"\n log \"$(msg llm.apikey_url_qwencloud)\"\n elif [ \"${ALIBABA_ACCESS}\" = \"codingplan_legacy\" ]; then\n log \"$(msg llm.apikey_hint_codingplan)\"\n log \"$(msg llm.apikey_url_codingplan)\"\n else\n log \"$(msg llm.apikey_hint_tokenplan)\"\n log \"$(msg llm.apikey_url_tokenplan)\"\n fi\n log \"\"\n prompt AGENTTEAMS_LLM_API_KEY \"$(msg llm.apikey_prompt)\" \"\" \"true\" || return 0\n if [ \"${ALIBABA_ACCESS}\" = \"bailian\" ]; then\n test_llm_connectivity \"https://dashscope.aliyuncs.com/compatible-mode/v1\" \"${AGENTTEAMS_LLM_API_KEY}\" \"${AGENTTEAMS_DEFAULT_MODEL}\" || return 0\n elif [ \"${ALIBABA_ACCESS}\" = \"codingplan_legacy\" ]; then\n test_llm_connectivity \"https://coding.dashscope.aliyuncs.com/v1\" \"${AGENTTEAMS_LLM_API_KEY}\" \"${AGENTTEAMS_DEFAULT_MODEL}\" \"$(msg llm.openai.test.fail.codingplan_legacy)\" || return 0\n elif [ \"${AGENTTEAMS_LANGUAGE}\" = \"en\" ]; then\n test_llm_connectivity \"${AGENTTEAMS_OPENAI_BASE_URL}\" \"${AGENTTEAMS_LLM_API_KEY}\" \"${AGENTTEAMS_DEFAULT_MODEL}\" \"$(msg llm.openai.test.fail.codingplan)\" || return 0\n else\n test_llm_connectivity \"${AGENTTEAMS_OPENAI_BASE_URL}\" \"${AGENTTEAMS_LLM_API_KEY}\" \"${AGENTTEAMS_DEFAULT_MODEL}\" \"$(msg llm.openai.test.fail.tokenplan)\" || return 0\n fi\n ;;\n 2|openai-compat)\n AGENTTEAMS_LLM_PROVIDER=\"openai-compat\"\n log \"$(msg llm.provider.selected_openai \"${AGENTTEAMS_LLM_PROVIDER}\")\"\n echo \"\"\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg llm.openai.base_url_prompt)\" \"${AGENTTEAMS_OPENAI_BASE_URL}\")\"\n else\n _current=\"${AGENTTEAMS_OPENAI_BASE_URL}\"\n if [ -n \"${_current}\" ]; then\n read -e -p \"$(msg llm.openai.base_url_prompt) [${_current}]: \" AGENTTEAMS_OPENAI_BASE_URL\n else\n read -e -p \"$(msg llm.openai.base_url_prompt) [https://api.openai.com/v1]: \" AGENTTEAMS_OPENAI_BASE_URL\n fi\n if [ \"${AGENTTEAMS_OPENAI_BASE_URL}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_OPENAI_BASE_URL=\"${AGENTTEAMS_OPENAI_BASE_URL:-${_current}}\"\n [ -z \"${AGENTTEAMS_OPENAI_BASE_URL}\" ] && AGENTTEAMS_OPENAI_BASE_URL=\"https://api.openai.com/v1\"\n fi\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg llm.openai.model_prompt)\" \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n else\n _current=\"${AGENTTEAMS_DEFAULT_MODEL}\"\n if [ -n \"${_current}\" ]; then\n read -e -p \"$(msg llm.openai.model_prompt) [${_current}]: \" AGENTTEAMS_DEFAULT_MODEL\n else\n read -e -p \"$(msg llm.openai.model_prompt) [gpt-5.4]: \" AGENTTEAMS_DEFAULT_MODEL\n fi\n if [ \"${AGENTTEAMS_DEFAULT_MODEL}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_DEFAULT_MODEL=\"${AGENTTEAMS_DEFAULT_MODEL:-${_current}}\"\n [ -z \"${AGENTTEAMS_DEFAULT_MODEL}\" ] && AGENTTEAMS_DEFAULT_MODEL=\"gpt-5.4\"\n fi\n log \"$(msg llm.openai.base_url_label \"${AGENTTEAMS_OPENAI_BASE_URL}\")\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n prompt_custom_model_params \"${AGENTTEAMS_DEFAULT_MODEL}\" || return 0\n log \"\"\n prompt AGENTTEAMS_LLM_API_KEY \"$(msg llm.apikey_prompt)\" \"\" \"true\" || return 0\n test_llm_connectivity \"${AGENTTEAMS_OPENAI_BASE_URL}\" \"${AGENTTEAMS_LLM_API_KEY}\" \"${AGENTTEAMS_DEFAULT_MODEL}\" || return 0\n ;;\n *)\n die \"$(msg llm.provider.invalid \"${PROVIDER_CHOICE}\")\"\n ;;\n esac\n\n # Skip to embedding if Keep-All mode handled LLM params\n if [ \"${SKIP_TO_EMBEDDING}\" = \"1\" ]; then\n if [ -n \"${AGENTTEAMS_EMBEDDING_MODEL}\" ]; then\n log \"$(msg llm.embedding.title)\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_EMBEDDING_MODEL}\")\"\n log \"\"\n fi\n return 0\n fi\n\n # --- Embedding model (optional, auto-tested) ---\n echo \"\"\n log \"$(msg llm.embedding.title)\"\n log \"$(msg llm.embedding.hint)\"\n echo \"\"\n echo \"$(msg llm.embedding.option.default)\"\n echo \"$(msg llm.embedding.option.custom)\"\n echo \"$(msg llm.embedding.option.disable)\"\n echo \"\"\n local EMB_CHOICE\n read -e -p \"$(msg llm.embedding.select) [1]: \" EMB_CHOICE\n EMB_CHOICE=\"${EMB_CHOICE:-1}\"\n if [ \"${EMB_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n\n case \"${EMB_CHOICE}\" in\n 1)\n AGENTTEAMS_EMBEDDING_MODEL=\"text-embedding-v4\"\n ;;\n 2)\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg llm.embedding.custom_prompt)\" \"${AGENTTEAMS_EMBEDDING_MODEL}\")\"\n else\n _current=\"${AGENTTEAMS_EMBEDDING_MODEL}\"\n if [ -n \"${_current}\" ]; then\n read -e -p \"$(msg llm.embedding.custom_prompt) [${_current}]: \" AGENTTEAMS_EMBEDDING_MODEL\n else\n read -e -p \"$(msg llm.embedding.custom_prompt): \" AGENTTEAMS_EMBEDDING_MODEL\n fi\n if [ \"${AGENTTEAMS_EMBEDDING_MODEL}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_EMBEDDING_MODEL=\"${AGENTTEAMS_EMBEDDING_MODEL:-${_current}}\"\n fi\n ;;\n 3)\n AGENTTEAMS_EMBEDDING_MODEL=\"\"\n log \"$(msg llm.embedding.disabled)\"\n ;;\n *)\n AGENTTEAMS_EMBEDDING_MODEL=\"text-embedding-v4\"\n ;;\n esac\n\n if [ -n \"${AGENTTEAMS_EMBEDDING_MODEL}\" ]; then\n # Qwen provider uses dashscope directly; others use OPENAI_BASE_URL\n local EMB_BASE_URL=\"${AGENTTEAMS_OPENAI_BASE_URL}\"\n if [ \"${AGENTTEAMS_LLM_PROVIDER}\" = \"qwen\" ]; then\n EMB_BASE_URL=\"https://dashscope.aliyuncs.com/compatible-mode/v1\"\n fi\n if ! test_embedding_connectivity \"${EMB_BASE_URL}\" \"${AGENTTEAMS_LLM_API_KEY}\" \"${AGENTTEAMS_EMBEDDING_MODEL}\"; then\n AGENTTEAMS_EMBEDDING_MODEL=\"\"\n log \"$(msg llm.embedding.auto_disabled)\"\n fi\n fi\n\n export AGENTTEAMS_LLM_PROVIDER AGENTTEAMS_DEFAULT_MODEL\n [ -n \"${AGENTTEAMS_OPENAI_BASE_URL+x}\" ] && export AGENTTEAMS_OPENAI_BASE_URL\n log \"\"\n}\n\nstep_admin() {\n log \"$(msg admin.title)\"\n prompt AGENTTEAMS_ADMIN_USER \"$(msg admin.username_prompt)\" \"admin\" || return 0\n AGENTTEAMS_ADMIN_USER=\"$(printf '%s' \"${AGENTTEAMS_ADMIN_USER}\" | tr '[:upper:]' '[:lower:]')\"\n\n # Pre-set via env var: validate; in non-interactive mode fail fast,\n # in interactive mode warn and fall through to the retry prompt.\n if [ -n \"${AGENTTEAMS_ADMIN_PASSWORD:-}\" ]; then\n log \" $(msg prompt.preset \"$(msg admin.password_prompt)\")\"\n if [ ${#AGENTTEAMS_ADMIN_PASSWORD} -ge 8 ]; then\n log \"\"\n return 0\n fi\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n die \"$(msg admin.password_too_short \"${#AGENTTEAMS_ADMIN_PASSWORD}\")\"\n fi\n error \"$(msg admin.password_too_short \"${#AGENTTEAMS_ADMIN_PASSWORD}\")\"\n unset AGENTTEAMS_ADMIN_PASSWORD\n fi\n\n while true; do\n unset AGENTTEAMS_ADMIN_PASSWORD\n prompt_optional AGENTTEAMS_ADMIN_PASSWORD \"$(msg admin.password_prompt)\" \"true\" || return 0\n if [ -z \"${AGENTTEAMS_ADMIN_PASSWORD}\" ]; then\n AGENTTEAMS_ADMIN_PASSWORD=\"admin$(openssl rand -hex 6)\"\n log \"$(msg admin.password_generated)\"\n break\n fi\n if [ ${#AGENTTEAMS_ADMIN_PASSWORD} -ge 8 ]; then\n break\n fi\n error \"$(msg admin.password_too_short \"${#AGENTTEAMS_ADMIN_PASSWORD}\")\"\n done\n log \"\"\n}\n\nstep_network() {\n log \"$(msg port.local_only.title)\"\n echo \"\"\n echo \" 1) $(msg port.local_only.hint_yes)\"\n echo \" 2) $(msg port.local_only.hint_no)\"\n echo \"\"\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_LOCAL_ONLY=\"${AGENTTEAMS_LOCAL_ONLY:-1}\"\n elif [ -z \"${AGENTTEAMS_LOCAL_ONLY+x}\" ]; then\n local _local_choice\n read -e -p \"$(msg port.local_only.choice): \" _local_choice\n if [ \"${_local_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _local_choice=\"${_local_choice:-1}\"\n case \"${_local_choice}\" in\n 2|n|N|no|NO) AGENTTEAMS_LOCAL_ONLY=\"0\" ;;\n *) AGENTTEAMS_LOCAL_ONLY=\"1\" ;;\n esac\n fi\n export AGENTTEAMS_LOCAL_ONLY\n if [ \"${AGENTTEAMS_LOCAL_ONLY}\" = \"1\" ]; then\n log \"$(msg port.local_only.selected_local)\"\n else\n log \"$(msg port.local_only.selected_external)\"\n echo \"\"\n echo -e \"\\033[33m$(msg port.local_only.https_hint)\\033[0m\"\n fi\n}\n\nstep_ports() {\n log \"$(msg port.title)\"\n prompt AGENTTEAMS_PORT_GATEWAY \"$(msg port.gateway_prompt)\" \"18080\" || return 0\n prompt AGENTTEAMS_PORT_CONSOLE \"$(msg port.console_prompt)\" \"18001\" || return 0\n prompt AGENTTEAMS_PORT_ELEMENT_WEB \"$(msg port.element_prompt)\" \"18088\" || return 0\n prompt AGENTTEAMS_PORT_MANAGER_CONSOLE \"$(msg port.manager_console_prompt)\" \"18888\" || return 0\n log \"\"\n}\n\nstep_domains() {\n log \"$(msg domain.title)\"\n log \"$(msg domain.hint)\"\n prompt AGENTTEAMS_MATRIX_DOMAIN \"$(msg domain.matrix_prompt)\" \"matrix-local.agentteams.io:${AGENTTEAMS_PORT_GATEWAY}\" || return 0\n prompt AGENTTEAMS_MATRIX_CLIENT_DOMAIN \"$(msg domain.element_prompt)\" \"matrix-client-local.agentteams.io\" || return 0\n prompt AGENTTEAMS_AI_GATEWAY_DOMAIN \"$(msg domain.gateway_prompt)\" \"aigw-local.agentteams.io\" || return 0\n prompt AGENTTEAMS_FS_DOMAIN \"$(msg domain.fs_prompt)\" \"fs-local.agentteams.io\" || return 0\n if [ \"${AGENTTEAMS_MANAGER_RUNTIME}\" != \"copaw\" ]; then\n prompt AGENTTEAMS_CONSOLE_DOMAIN \"$(msg domain.console_prompt)\" \"console-local.agentteams.io\" || return 0\n fi\n log \"\"\n}\n\nstep_github() {\n log \"$(msg github.title)\"\n prompt_optional AGENTTEAMS_GITHUB_TOKEN \"$(msg github.token_prompt)\" \"true\" || return 0\n}\n\nstep_skills() {\n log \"\"\n log \"$(msg skills.title)\"\n prompt_optional AGENTTEAMS_SKILLS_API_URL \"$(msg skills.url_prompt)\" || return 0\n log \"\"\n}\n\nstep_volume() {\n log \"$(msg data.title)\"\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_DATA_DIR=\"${AGENTTEAMS_DATA_DIR:-agentteams-data}\"\n log \" $(msg data.volume_using \"${AGENTTEAMS_DATA_DIR}\") (non-interactive, skipped)\"\n export AGENTTEAMS_DATA_DIR\n return 0\n fi\n # ─────────────────────────────────────────────────────────────────\n if [ -z \"${AGENTTEAMS_DATA_DIR+x}\" ]; then\n local _input\n read -e -p \"$(msg data.volume_prompt): \" _input\n if [ \"${_input}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_DATA_DIR=\"${_input:-agentteams-data}\"\n export AGENTTEAMS_DATA_DIR\n fi\n AGENTTEAMS_DATA_DIR=\"${AGENTTEAMS_DATA_DIR:-agentteams-data}\"\n log \"$(msg data.volume_using \"${AGENTTEAMS_DATA_DIR}\")\"\n}\n\nstep_workspace() {\n log \"$(msg workspace.title)\"\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_WORKSPACE_DIR=\"${AGENTTEAMS_WORKSPACE_DIR:-${HOME}/agentteams-manager}\"\n AGENTTEAMS_WORKSPACE_DIR=\"$(cd \"${AGENTTEAMS_WORKSPACE_DIR}\" 2>/dev/null && pwd || echo \"${AGENTTEAMS_WORKSPACE_DIR}\")\"\n mkdir -p \"${AGENTTEAMS_WORKSPACE_DIR}\"\n log \" $(msg workspace.dir_label \"${AGENTTEAMS_WORKSPACE_DIR}\") (non-interactive, skipped)\"\n export AGENTTEAMS_WORKSPACE_DIR\n return 0\n fi\n # ─────────────────────────────────────────────────────────────────\n local _input\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg workspace.dir_prompt \"${HOME}/agentteams-manager\")\" \"${AGENTTEAMS_WORKSPACE_DIR}\")\"\n _input=\"${AGENTTEAMS_WORKSPACE_DIR}\"\n else\n local _current=\"${AGENTTEAMS_WORKSPACE_DIR}\"\n if [ -n \"${_current}\" ]; then\n read -e -p \"$(msg workspace.dir_prompt \"${HOME}/agentteams-manager\") [${_current}]: \" _input\n else\n read -e -p \"$(msg workspace.dir_prompt \"${HOME}/agentteams-manager\"): \" _input\n fi\n if [ \"${_input}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _input=\"${_input:-${_current}}\"\n fi\n AGENTTEAMS_WORKSPACE_DIR=\"${_input:-${HOME}/agentteams-manager}\"\n export AGENTTEAMS_WORKSPACE_DIR\n AGENTTEAMS_WORKSPACE_DIR=\"$(cd \"${AGENTTEAMS_WORKSPACE_DIR}\" 2>/dev/null && pwd || echo \"${AGENTTEAMS_WORKSPACE_DIR}\")\"\n mkdir -p \"${AGENTTEAMS_WORKSPACE_DIR}\"\n log \"$(msg workspace.dir_label \"${AGENTTEAMS_WORKSPACE_DIR}\")\"\n}\n\nstep_dashboard() {\n AGENTTEAMS_DASHBOARD=\"${AGENTTEAMS_DASHBOARD:-1}\"\n AGENTTEAMS_DASHBOARD_VERSION=\"${AGENTTEAMS_DASHBOARD_VERSION:-v1.2.0-beta.1}\"\n AGENTTEAMS_PORT_DASHBOARD=\"${AGENTTEAMS_PORT_DASHBOARD:-13000}\"\n AGENTTEAMS_AI_GATEWAY_ADMIN_URL=\"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL:-}\"\n\n # Compute the default image for the current version.\n # We track whether the current image matches the derived default so that\n # changing the version also updates the image (unless the user has\n # explicitly overridden it to a non-default value).\n _dashboard_default_image() {\n echo \"${AGENTTEAMS_REGISTRY}/agentteams/agentteams-dashboard:${1:-${AGENTTEAMS_DASHBOARD_VERSION}}\"\n }\n local _init_default\n _init_default=$(_dashboard_default_image)\n if [ -z \"${AGENTTEAMS_DASHBOARD_IMAGE:-}\" ]; then\n AGENTTEAMS_DASHBOARD_IMAGE=\"${_init_default}\"\n fi\n\n # Recompute default image when the version changes AND the current image\n # matches the default for the *previous* (env-file) version.\n # This handles non-interactive upgrades correctly:\n # - env file loads old default image + old version\n # - CLI passes new version\n # - image should follow (because it was auto-derived before)\n # But preserves truly custom images (same repo but custom tag like :canary)\n # because they won't match the old-version default exactly.\n local _old_saved_version=\"\"\n local _env_file=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n if [ -f \"${_env_file}\" ]; then\n _old_saved_version=\"$(grep '^AGENTTEAMS_DASHBOARD_VERSION=' \"${_env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n fi\n if [ -n \"${_old_saved_version}\" ] && \\\n [ \"${_old_saved_version}\" != \"${AGENTTEAMS_DASHBOARD_VERSION}\" ] && \\\n [ \"${AGENTTEAMS_DASHBOARD_IMAGE}\" = \"$(_dashboard_default_image \"${_old_saved_version}\")\" ]; then\n AGENTTEAMS_DASHBOARD_IMAGE=$(_dashboard_default_image)\n fi\n\n # Quick Start: accept all defaults without prompting. The dashboard is\n # installed by default (AGENTTEAMS_DASHBOARD=1); set AGENTTEAMS_DASHBOARD=0\n # to opt out.\n if [ \"${AGENTTEAMS_QUICKSTART:-0}\" = \"1\" ]; then\n if [ \"${AGENTTEAMS_DASHBOARD}\" = \"1\" ]; then\n log \"$(msg dash.summary \"${AGENTTEAMS_PORT_DASHBOARD}\" \"${AGENTTEAMS_DASHBOARD_IMAGE}\") (quick start)\"\n else\n log \"$(msg dash.skip) (quick start)\"\n fi\n export AGENTTEAMS_DASHBOARD AGENTTEAMS_DASHBOARD_VERSION AGENTTEAMS_PORT_DASHBOARD AGENTTEAMS_DASHBOARD_IMAGE AGENTTEAMS_AI_GATEWAY_ADMIN_URL\n return 0\n fi\n\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_DASHBOARD=\"${AGENTTEAMS_DASHBOARD:-1}\"\n if [ \"${AGENTTEAMS_DASHBOARD}\" = \"1\" ]; then\n log \"$(msg dash.summary \"${AGENTTEAMS_PORT_DASHBOARD}\" \"${AGENTTEAMS_DASHBOARD_IMAGE}\") (non-interactive)\"\n else\n log \"$(msg dash.skip) (non-interactive)\"\n fi\n export AGENTTEAMS_DASHBOARD AGENTTEAMS_DASHBOARD_VERSION AGENTTEAMS_PORT_DASHBOARD AGENTTEAMS_DASHBOARD_IMAGE AGENTTEAMS_AI_GATEWAY_ADMIN_URL\n return 0\n fi\n\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ]; then\n log \"$(msg prompt.upgrade_keep \"\" \"${AGENTTEAMS_DASHBOARD}\") dashboard=${AGENTTEAMS_DASHBOARD} version=${AGENTTEAMS_DASHBOARD_VERSION} port=${AGENTTEAMS_PORT_DASHBOARD}\"\n export AGENTTEAMS_DASHBOARD AGENTTEAMS_DASHBOARD_VERSION AGENTTEAMS_PORT_DASHBOARD AGENTTEAMS_DASHBOARD_IMAGE AGENTTEAMS_AI_GATEWAY_ADMIN_URL\n return 0\n fi\n\n local _input\n echo \"\"\n read -p \"$(msg dash.prompt) [Y/n]: \" _input\n case \"${_input:-}\" in\n [nN0]) AGENTTEAMS_DASHBOARD=0 ;;\n *) AGENTTEAMS_DASHBOARD=1 ;;\n esac\n if [ \"${_input}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n\n if [ \"${AGENTTEAMS_DASHBOARD}\" != \"1\" ]; then\n log \"$(msg dash.skip)\"\n export AGENTTEAMS_DASHBOARD AGENTTEAMS_DASHBOARD_VERSION AGENTTEAMS_PORT_DASHBOARD AGENTTEAMS_DASHBOARD_IMAGE AGENTTEAMS_AI_GATEWAY_ADMIN_URL\n return 0\n fi\n\n # Dashboard version\n local _current_version=\"${AGENTTEAMS_DASHBOARD_VERSION}\"\n local _old_version=\"${AGENTTEAMS_DASHBOARD_VERSION}\"\n local _old_default_image\n _old_default_image=$(_dashboard_default_image \"${_old_version}\")\n read -p \"$(msg dash.version_prompt) [${_current_version}]: \" _input\n AGENTTEAMS_DASHBOARD_VERSION=\"${_input:-${_current_version}}\"\n\n # Dashboard port\n local _current_port=\"${AGENTTEAMS_PORT_DASHBOARD}\"\n read -p \"$(msg dash.port_prompt) [${_current_port}]: \" _input\n AGENTTEAMS_PORT_DASHBOARD=\"${_input:-${_current_port}}\"\n\n # Dashboard image: if the current image matches the default derived from\n # the OLD version, recompute it from the NEW version. This handles both\n # fresh installs and upgrades where the previous image was the default.\n # Only a real user override (image != default for old version) is preserved.\n if [ \"${AGENTTEAMS_DASHBOARD_VERSION}\" != \"${_old_version}\" ] && \\\n [ \"${AGENTTEAMS_DASHBOARD_IMAGE}\" = \"${_old_default_image}\" ]; then\n AGENTTEAMS_DASHBOARD_IMAGE=$(_dashboard_default_image)\n fi\n local _current_image=\"${AGENTTEAMS_DASHBOARD_IMAGE}\"\n read -p \"$(msg dash.image_prompt) [${_current_image}]: \" _input\n if [ -n \"${_input}\" ]; then\n AGENTTEAMS_DASHBOARD_IMAGE=\"${_input}\"\n fi\n\n # Higress Console URL (explicit config takes priority; auto-detect as fallback)\n local _higress_url=\"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\"\n if [ -z \"${_higress_url}\" ]; then\n if ${DOCKER_CMD} ps --format '{{.Names}}' | grep -q \"^agentteams-controller$\"; then\n if ${DOCKER_CMD} exec agentteams-controller curl -sf --max-time 2 http://127.0.0.1:8001/ >/dev/null 2>&1; then\n _higress_url=\"http://agentteams-controller:8001\"\n log \"$(msg dash.auto_url): ${_higress_url}\"\n else\n log \"$(msg dash.auto_url_fail)\"\n fi\n fi\n fi\n read -p \"$(msg dash.gateway_prompt) [${_higress_url:-<auto>}]: \" _input\n AGENTTEAMS_AI_GATEWAY_ADMIN_URL=\"${_input:-${_higress_url}}\"\n\n # Normalize URL: add http:// prefix if missing\n if [ -n \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\" ]; then\n case \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\" in\n http://*|https://*) ;;\n *) AGENTTEAMS_AI_GATEWAY_ADMIN_URL=\"http://${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\" ;;\n esac\n fi\n\n # Verify the Higress Console URL is reachable (best-effort warning only)\n if [ -n \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\" ]; then\n local _gw_reachable=0\n # If the URL points to the controller container, test from inside the container\n if echo \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\" | grep -qE \"(agentteams-controller|hiclaw-controller|127\\.0\\.0\\.1|localhost)\"; then\n if ${DOCKER_CMD} ps --format '{{.Names}}' | grep -q \"^agentteams-controller$\"; then\n if ${DOCKER_CMD} exec agentteams-controller curl -sf --max-time 3 \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}/\" >/dev/null 2>&1; then\n _gw_reachable=1\n fi\n fi\n else\n # External URL — probe from inside the controller (same network as\n # the Dashboard container) when possible; fall back to the host.\n if ${DOCKER_CMD} ps --format '{{.Names}}' | grep -q \"^agentteams-controller$\"; then\n if ${DOCKER_CMD} exec agentteams-controller curl -sf --max-time 3 \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}/\" >/dev/null 2>&1; then\n _gw_reachable=1\n fi\n elif curl -sf --max-time 3 \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}/\" >/dev/null 2>&1; then\n _gw_reachable=1\n fi\n fi\n if [ \"${_gw_reachable}\" = \"0\" ]; then\n log \"WARNING: Higress Console URL may not be reachable: ${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\"\n log \" Dashboard will still work, but shared login via Higress Console may fail.\"\n log \" To fix: verify the URL is correct and the Higress Console service is running.\"\n fi\n fi\n\n log \"$(msg dash.summary \"${AGENTTEAMS_PORT_DASHBOARD}\" \"${AGENTTEAMS_DASHBOARD_IMAGE}\")\"\n export AGENTTEAMS_DASHBOARD AGENTTEAMS_DASHBOARD_VERSION AGENTTEAMS_PORT_DASHBOARD AGENTTEAMS_DASHBOARD_IMAGE AGENTTEAMS_AI_GATEWAY_ADMIN_URL\n}\n\nstep_runtime() {\n log \"$(msg worker_runtime.title)\"\n echo \"\"\n echo \" 1) $(msg worker_runtime.copaw)\"\n echo \" 2) $(msg worker_runtime.openclaw)\"\n if ! _ver_lt \"${AGENTTEAMS_VERSION}\" \"v1.1.0\"; then\n echo \" 3) $(msg worker_runtime.hermes)\"\n fi\n echo \"\"\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"${AGENTTEAMS_DEFAULT_WORKER_RUNTIME:-copaw}\"\n elif [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_DEFAULT_WORKER_RUNTIME}\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg worker_runtime.title_short)\" \"${AGENTTEAMS_DEFAULT_WORKER_RUNTIME}\")\"\n local _runtime_choice\n read -e -p \"$(msg worker_runtime.choice): \" _runtime_choice\n if [ \"${_runtime_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n if [ -n \"${_runtime_choice}\" ]; then\n case \"${_runtime_choice}\" in\n 2) AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"openclaw\" ;;\n 3) if ! _ver_lt \"${AGENTTEAMS_VERSION}\" \"v1.1.0\"; then\n AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"hermes\"\n else\n AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"copaw\"\n fi ;;\n *) AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"copaw\" ;;\n esac\n fi\n elif [ -z \"${AGENTTEAMS_DEFAULT_WORKER_RUNTIME+x}\" ]; then\n local _runtime_choice\n read -e -p \"$(msg worker_runtime.choice): \" _runtime_choice\n if [ \"${_runtime_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _runtime_choice=\"${_runtime_choice:-1}\"\n case \"${_runtime_choice}\" in\n 2) AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"openclaw\" ;;\n 3) if ! _ver_lt \"${AGENTTEAMS_VERSION}\" \"v1.1.0\"; then\n AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"hermes\"\n else\n AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"copaw\"\n fi ;;\n *) AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"copaw\" ;;\n esac\n fi\n export AGENTTEAMS_DEFAULT_WORKER_RUNTIME\n log \"$(msg worker_runtime.selected \"${AGENTTEAMS_DEFAULT_WORKER_RUNTIME}\")\"\n}\n\nstep_manager_runtime() {\n log \"$(msg manager_runtime.title)\"\n echo \"\"\n echo \" 1) $(msg manager_runtime.copaw)\"\n echo \" 2) $(msg manager_runtime.openclaw)\"\n echo \"\"\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_MANAGER_RUNTIME=\"${AGENTTEAMS_MANAGER_RUNTIME:-copaw}\"\n elif [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_MANAGER_RUNTIME}\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg manager_runtime.title_short)\" \"${AGENTTEAMS_MANAGER_RUNTIME}\")\"\n local _runtime_choice\n read -e -p \"$(msg manager_runtime.choice): \" _runtime_choice\n if [ \"${_runtime_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n if [ -n \"${_runtime_choice}\" ]; then\n case \"${_runtime_choice}\" in\n 2) AGENTTEAMS_MANAGER_RUNTIME=\"openclaw\" ;;\n *) AGENTTEAMS_MANAGER_RUNTIME=\"copaw\" ;;\n esac\n fi\n elif [ -z \"${AGENTTEAMS_MANAGER_RUNTIME+x}\" ]; then\n local _runtime_choice\n read -e -p \"$(msg manager_runtime.choice): \" _runtime_choice\n if [ \"${_runtime_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _runtime_choice=\"${_runtime_choice:-1}\"\n case \"${_runtime_choice}\" in\n 2) AGENTTEAMS_MANAGER_RUNTIME=\"openclaw\" ;;\n *) AGENTTEAMS_MANAGER_RUNTIME=\"copaw\" ;;\n esac\n fi\n export AGENTTEAMS_MANAGER_RUNTIME\n log \"$(msg manager_runtime.selected \"${AGENTTEAMS_MANAGER_RUNTIME}\")\"\n}\n\nstep_e2ee() {\n log \"\"\n log \"$(msg matrix_e2ee.title)\"\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_MATRIX_E2EE=\"${AGENTTEAMS_MATRIX_E2EE:-0}\"\n log \" $(msg matrix_e2ee.title_short) = ${AGENTTEAMS_MATRIX_E2EE} (non-interactive, skipped)\"\n export AGENTTEAMS_MATRIX_E2EE\n return 0\n fi\n # ─────────────────────────────────────────────────────────────────\n echo \"\"\n echo -e \" $(msg matrix_e2ee.desc)\"\n echo \"\"\n echo \" 1) $(msg matrix_e2ee.disable)\"\n echo \" 2) $(msg matrix_e2ee.enable)\"\n echo \"\"\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_MATRIX_E2EE}\" ]; then\n local _e2ee_display; if [ \"${AGENTTEAMS_MATRIX_E2EE}\" = \"1\" ]; then _e2ee_display=\"$(msg matrix_e2ee.val_enabled)\"; else _e2ee_display=\"$(msg matrix_e2ee.val_disabled)\"; fi\n log \"$(msg prompt.upgrade_keep \"$(msg matrix_e2ee.title_short)\" \"${_e2ee_display}\")\"\n local _e2ee_choice\n read -e -p \"$(msg matrix_e2ee.choice): \" _e2ee_choice\n if [ \"${_e2ee_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n if [ -n \"${_e2ee_choice}\" ]; then\n case \"${_e2ee_choice}\" in\n 2) AGENTTEAMS_MATRIX_E2EE=\"1\" ;;\n *) AGENTTEAMS_MATRIX_E2EE=\"0\" ;;\n esac\n fi\n elif [ -z \"${AGENTTEAMS_MATRIX_E2EE+x}\" ]; then\n local _e2ee_choice\n read -e -p \"$(msg matrix_e2ee.choice): \" _e2ee_choice\n if [ \"${_e2ee_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _e2ee_choice=\"${_e2ee_choice:-1}\"\n case \"${_e2ee_choice}\" in\n 2) AGENTTEAMS_MATRIX_E2EE=\"1\" ;;\n *) AGENTTEAMS_MATRIX_E2EE=\"0\" ;;\n esac\n fi\n AGENTTEAMS_MATRIX_E2EE=\"${AGENTTEAMS_MATRIX_E2EE:-0}\"\n export AGENTTEAMS_MATRIX_E2EE\n if [ \"${AGENTTEAMS_MATRIX_E2EE}\" = \"1\" ]; then\n log \"$(msg matrix_e2ee.selected_enabled)\"\n else\n log \"$(msg matrix_e2ee.selected_disabled)\"\n fi\n}\n\nstep_docker_proxy() {\n # Only relevant when socket mounting is enabled\n if [ \"${AGENTTEAMS_MOUNT_SOCKET}\" != \"1\" ]; then\n AGENTTEAMS_DOCKER_PROXY=\"0\"\n return 0\n fi\n\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_DOCKER_PROXY=\"${AGENTTEAMS_DOCKER_PROXY:-0}\"\n log \" $(msg docker_proxy.title_short) = ${AGENTTEAMS_DOCKER_PROXY} (non-interactive, skipped)\"\n export AGENTTEAMS_DOCKER_PROXY\n return 0\n fi\n # ─────────────────────────────────────────────────────────────────\n\n echo \"\"\n echo -e \" \\033[1m$(msg docker_proxy.title)\\033[0m\"\n echo \"\"\n echo -e \" $(msg docker_proxy.desc)\"\n echo \"\"\n echo \" 1) $(msg docker_proxy.enable)\"\n echo \" 2) $(msg docker_proxy.disable)\"\n echo \"\"\n\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_DOCKER_PROXY}\" ]; then\n local _proxy_display; if [ \"${AGENTTEAMS_DOCKER_PROXY}\" = \"1\" ]; then _proxy_display=\"$(msg docker_proxy.val_enabled)\"; else _proxy_display=\"$(msg docker_proxy.val_disabled)\"; fi\n log \"$(msg prompt.upgrade_keep \"$(msg docker_proxy.title_short)\" \"${_proxy_display}\")\"\n local _choice\n read -e -p \"$(msg docker_proxy.choice): \" _choice\n if [ \"${_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n if [ -n \"${_choice}\" ]; then\n case \"${_choice}\" in\n 2) AGENTTEAMS_DOCKER_PROXY=\"0\" ;;\n *) AGENTTEAMS_DOCKER_PROXY=\"1\" ;;\n esac\n fi\n elif [ -z \"${AGENTTEAMS_DOCKER_PROXY+x}\" ]; then\n local _choice\n read -e -p \"$(msg docker_proxy.choice): \" _choice\n if [ \"${_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _choice=\"${_choice:-1}\"\n case \"${_choice}\" in\n 2) AGENTTEAMS_DOCKER_PROXY=\"0\" ;;\n *) AGENTTEAMS_DOCKER_PROXY=\"1\" ;;\n esac\n fi\n AGENTTEAMS_DOCKER_PROXY=\"${AGENTTEAMS_DOCKER_PROXY:-1}\"\n export AGENTTEAMS_DOCKER_PROXY\n if [ \"${AGENTTEAMS_DOCKER_PROXY}\" = \"1\" ]; then\n log \"$(msg docker_proxy.selected_enabled)\"\n\n # Prompt for additional allowed image sources\n echo \"\"\n echo -e \" $(msg docker_proxy.registries_desc)\"\n echo \"\"\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_PROXY_ALLOWED_REGISTRIES}\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg docker_proxy.registries_label)\" \"${AGENTTEAMS_PROXY_ALLOWED_REGISTRIES}\")\"\n local _reg_input\n read -e -p \"$(msg docker_proxy.registries_prompt): \" _reg_input\n if [ \"${_reg_input}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n [ -n \"${_reg_input}\" ] && AGENTTEAMS_PROXY_ALLOWED_REGISTRIES=\"${_reg_input}\"\n elif [ -z \"${AGENTTEAMS_PROXY_ALLOWED_REGISTRIES+x}\" ]; then\n local _reg_input\n read -e -p \"$(msg docker_proxy.registries_prompt): \" _reg_input\n if [ \"${_reg_input}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_PROXY_ALLOWED_REGISTRIES=\"${_reg_input:-}\"\n fi\n export AGENTTEAMS_PROXY_ALLOWED_REGISTRIES\n else\n log \"$(msg docker_proxy.selected_disabled)\"\n fi\n}\n\nstep_idle() {\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_WORKER_IDLE_TIMEOUT=\"${AGENTTEAMS_WORKER_IDLE_TIMEOUT:-720}\"\n log \" $(msg idle_timeout.label) = ${AGENTTEAMS_WORKER_IDLE_TIMEOUT} (non-interactive, skipped)\"\n export AGENTTEAMS_WORKER_IDLE_TIMEOUT\n return 0\n fi\n # ─────────────────────────────────────────────────────────────────\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_WORKER_IDLE_TIMEOUT}\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg idle_timeout.label)\" \"${AGENTTEAMS_WORKER_IDLE_TIMEOUT}\")\"\n local _idle_timeout\n read -e -p \"$(msg idle_timeout.prompt): \" _idle_timeout\n if [ \"${_idle_timeout}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n [ -n \"${_idle_timeout}\" ] && AGENTTEAMS_WORKER_IDLE_TIMEOUT=\"${_idle_timeout}\"\n elif [ -z \"${AGENTTEAMS_WORKER_IDLE_TIMEOUT+x}\" ]; then\n local _idle_timeout\n read -e -p \"$(msg idle_timeout.prompt): \" _idle_timeout\n if [ \"${_idle_timeout}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_WORKER_IDLE_TIMEOUT=\"${_idle_timeout:-720}\"\n fi\n AGENTTEAMS_WORKER_IDLE_TIMEOUT=\"${AGENTTEAMS_WORKER_IDLE_TIMEOUT:-720}\"\n export AGENTTEAMS_WORKER_IDLE_TIMEOUT\n log \"$(msg idle_timeout.selected \"${AGENTTEAMS_WORKER_IDLE_TIMEOUT}\")\"\n}\n\nstep_hostshare() {\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_HOST_SHARE_DIR=\"${AGENTTEAMS_HOST_SHARE_DIR:-${HOME}}\"\n log \" $(msg host_share.label) = ${AGENTTEAMS_HOST_SHARE_DIR} (non-interactive, skipped)\"\n export AGENTTEAMS_HOST_SHARE_DIR\n return 0\n fi\n # ─────────────────────────────────────────────────────────────────\n local _current=\"${AGENTTEAMS_HOST_SHARE_DIR}\"\n local _share_dir\n if [ -n \"${_current}\" ]; then\n read -e -p \"$(msg host_share.prompt \"$HOME\") [${_current}]: \" _share_dir\n else\n read -e -p \"$(msg host_share.prompt \"$HOME\"): \" _share_dir\n fi\n if [ \"${_share_dir}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_HOST_SHARE_DIR=\"${_share_dir:-${_current:-$HOME}}\"\n export AGENTTEAMS_HOST_SHARE_DIR\n}\n\nstep_podman_autostart() {\n echo \"\"\n log \"$(msg podman.autostart.title)\"\n echo \"\"\n echo -e \" $(msg podman.autostart.desc)\"\n echo \"\"\n echo \" 1) $(msg podman.autostart.enable)\"\n echo \" 2) $(msg podman.autostart.disable)\"\n echo \"\"\n\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_PODMAN_AUTOSTART}\" ]; then\n local _disp; if [ \"${AGENTTEAMS_PODMAN_AUTOSTART}\" = \"1\" ]; then _disp=\"$(msg podman.autostart.val_enabled)\"; else _disp=\"$(msg podman.autostart.val_disabled)\"; fi\n log \"$(msg prompt.upgrade_keep \"$(msg podman.autostart.title_short)\" \"${_disp}\")\"\n local _choice\n read -e -p \"$(msg podman.autostart.choice): \" _choice\n if [ \"${_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n if [ -n \"${_choice}\" ]; then\n case \"${_choice}\" in\n 2) AGENTTEAMS_PODMAN_AUTOSTART=\"0\" ;;\n *) AGENTTEAMS_PODMAN_AUTOSTART=\"1\" ;;\n esac\n fi\n elif [ -z \"${AGENTTEAMS_PODMAN_AUTOSTART+x}\" ]; then\n local _choice\n read -e -p \"$(msg podman.autostart.choice) [1]: \" _choice\n if [ \"${_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _choice=\"${_choice:-1}\"\n case \"${_choice}\" in\n 2) AGENTTEAMS_PODMAN_AUTOSTART=\"0\" ;;\n *) AGENTTEAMS_PODMAN_AUTOSTART=\"1\" ;;\n esac\n fi\n AGENTTEAMS_PODMAN_AUTOSTART=\"${AGENTTEAMS_PODMAN_AUTOSTART:-1}\"\n export AGENTTEAMS_PODMAN_AUTOSTART\n if [ \"${AGENTTEAMS_PODMAN_AUTOSTART}\" = \"1\" ]; then\n log \"$(msg podman.autostart.selected_enabled)\"\n else\n log \"$(msg podman.autostart.selected_disabled)\"\n fi\n}\n\n# ============================================================\n# Setup dedicated Podman autostart service for AgentTeams\n# ============================================================\nsetup_podman_autostart() {\n # Only execute if using podman and autostart is enabled\n if [ \"${DOCKER_CMD:-docker}\" != \"podman\" ] || [ \"${AGENTTEAMS_PODMAN_AUTOSTART:-0}\" != \"1\" ]; then\n return 0\n fi\n\n log \"$(msg install.podman.autostart_title)\"\n\n local current_user\n current_user=\"$(whoami)\"\n\n # Define the dedicated AgentTeams service content\n # Strict scoping: Only manage containers matching '--filter name=agentteams'\n local _service_content=\"[Unit]\nDescription=AgentTeams Dedicated Podman Autostart Service\nDocumentation=https://github.com/agentscope-ai/AgentTeams\nWants=network-online.target\nAfter=network-online.target\n\n[Service]\nType=oneshot\nRemainAfterExit=true\nEnvironment=LOGGING=\\\"--log-level=info\\\"\nExecStart=/usr/bin/podman \\$LOGGING start --filter name=agentteams --filter restart-policy=always --filter restart-policy=unless-stopped\nExecStop=/usr/bin/podman \\$LOGGING stop --filter name=agentteams --filter restart-policy=always --filter restart-policy=unless-stopped\n\n[Install]\nWantedBy=default.target\"\n\n local _autostart_ok=0\n\n if [ \"${current_user}\" = \"root\" ]; then\n # Rootful Mode (System-wide dedicated service)\n log \"$(msg install.podman.root_setup)\"\n\n local _sys_dir=\"/etc/systemd/system\"\n mkdir -p \"${_sys_dir}\"\n echo \"${_service_content}\" > \"${_sys_dir}/agentteams-podman-restart.service\"\n\n systemctl daemon-reload >/dev/null 2>&1 || true\n\n if systemctl enable --now agentteams-podman-restart.service >/dev/null 2>&1; then\n log \"$(msg install.podman.root_success)\"\n _autostart_ok=1\n else\n log \"$(msg install.podman.root_fail)\"\n fi\n else\n # Rootless Mode (User-level dedicated service)\n log \"$(msg install.podman.user_setup \"${current_user}\")\"\n\n local _linger_ok=0\n\n # 1. Enable Linger for background execution (Graceful escalation strategy)\n if loginctl show-user \"${current_user}\" --property=Linger 2>/dev/null | grep -q \"Linger=yes\"; then\n _linger_ok=1\n else\n log \"$(msg install.podman.linger_enable)\"\n\n # Step A: Try to enable linger natively without sudo\n if loginctl enable-linger \"${current_user}\" >/dev/null 2>&1; then\n log \"Successfully enabled systemd linger natively.\"\n _linger_ok=1\n else\n # Step B: Fallback to sudo\n log \"Native linger enablement failed. Distro security policy requires privileges.\"\n if command -v sudo >/dev/null 2>&1; then\n log \"Attempting to enable systemd linger via sudo (enter password if prompted)...\"\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n sudo -n loginctl enable-linger \"${current_user}\" 2>/dev/null && _linger_ok=1 || log \"$(msg install.podman.linger_warn)\"\n else\n sudo loginctl enable-linger \"${current_user}\" && _linger_ok=1 || log \"$(msg install.podman.linger_warn)\"\n fi\n else\n log \"$(msg install.podman.linger_warn)\"\n fi\n fi\n fi\n\n # 2. Write dedicated service file to user directory\n local _user_dir=\"${HOME}/.config/systemd/user\"\n mkdir -p \"${_user_dir}\"\n echo \"${_service_content}\" > \"${_user_dir}/agentteams-podman-restart.service\"\n\n systemctl --user daemon-reload >/dev/null 2>&1 || true\n\n if systemctl --user enable --now agentteams-podman-restart.service >/dev/null 2>&1; then\n log \"$(msg install.podman.user_success)\"\n # Crucial: Only consider autostart fully successful if systemd linger is also enabled\n if [ \"${_linger_ok}\" = \"1\" ]; then\n _autostart_ok=1\n else\n log \"⚠️ Service enabled, but since systemd linger is not active, containers will not auto-recover on system reboot.\"\n fi\n else\n log \"$(msg install.podman.user_fail)\"\n fi\n fi\n\n # Only print final success banner if the service AND linger were actually enabled\n if [ \"${_autostart_ok}\" = \"1\" ]; then\n log \"$(msg install.podman.success)\"\n fi\n}\n\n# ============================================================\n# Dashboard startup (called after controller is ready)\n# ============================================================\n\n_start_dashboard() {\n local CTRL_CONTAINER=\"agentteams-controller\"\n local DASHBOARD_CONTAINER=\"agentteams-dashboard\"\n\n if [ \"${AGENTTEAMS_DASHBOARD:-0}\" != \"1\" ]; then\n # Stop and remove the existing dashboard container if present.\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -q \"^${DASHBOARD_CONTAINER}$\"; then\n log \"Stopping and removing ${DASHBOARD_CONTAINER}...\"\n ${DOCKER_CMD} stop \"${DASHBOARD_CONTAINER}\" >/dev/null 2>&1 || true\n ${DOCKER_CMD} rm -f \"${DASHBOARD_CONTAINER}\" >/dev/null 2>&1 || true\n log \"Dashboard stopped.\"\n fi\n return 0\n fi\n\n # Dashboard requires the embedded architecture (controller container).\n # In legacy all-in-one mode the controller API is not available.\n if [ \"${AGENTTEAMS_USE_EMBEDDED:-0}\" != \"1\" ]; then\n log \"Skipping Dashboard: requires embedded architecture (agentteams-controller), not available in legacy mode.\"\n log \"Use Quick Start (embedded) mode to enable agentteams-dashboard.\"\n return 0\n fi\n\n AGENTTEAMS_PORT_DASHBOARD=\"${AGENTTEAMS_PORT_DASHBOARD:-13000}\"\n AGENTTEAMS_DASHBOARD_VERSION=\"${AGENTTEAMS_DASHBOARD_VERSION:-v1.2.0-beta.1}\"\n AGENTTEAMS_DASHBOARD_IMAGE=\"${AGENTTEAMS_DASHBOARD_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-dashboard:${AGENTTEAMS_DASHBOARD_VERSION}}\"\n\n log \"\"\n log \"Starting agentteams-dashboard...\"\n\n local BIND_ADDR=\"127.0.0.1\"\n [ \"${AGENTTEAMS_LOCAL_ONLY:-1}\" = \"0\" ] && BIND_ADDR=\"0.0.0.0\"\n\n # Remove existing container\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' | grep -qx \"${DASHBOARD_CONTAINER}\"; then\n ${DOCKER_CMD} rm -f \"${DASHBOARD_CONTAINER}\" >/dev/null 2>&1 || true\n fi\n\n # Pull or use local image\n if ! ${DOCKER_CMD} image inspect \"${AGENTTEAMS_DASHBOARD_IMAGE}\" >/dev/null 2>&1; then\n log \"Pulling dashboard image: ${AGENTTEAMS_DASHBOARD_IMAGE}\"\n ${DOCKER_CMD} pull \"${AGENTTEAMS_DASHBOARD_IMAGE}\" 2>/dev/null || {\n log \"WARNING: Could not pull dashboard image. Skipping.\"\n return 0\n }\n fi\n\n # Build env args from controller container (MinIO/LLM/auth).\n local env_args=()\n env_args+=(-e AGENTTEAMS_CONTROLLER_URL=\"http://${CTRL_CONTAINER}:8090\")\n env_args+=(-e NEXT_PUBLIC_MATRIX_API_URL=\"http://${CTRL_CONTAINER}:6167\")\n env_args+=(-e MATRIX_HOMESERVER_ALLOWLIST=\"${CTRL_CONTAINER},matrix-local.agentteams.io,matrix.org\")\n\n if ${DOCKER_CMD} ps --format '{{.Names}}' | grep -qx \"${CTRL_CONTAINER}\"; then\n local env_out\n env_out=$(${DOCKER_CMD} inspect \"${CTRL_CONTAINER}\" --format='{{range .Config.Env}}{{.}}{{\"\\n\"}}{{end}}')\n\n _env() { echo \"${env_out}\" | sed -n \"s|^${1}=||p\" | head -n1; }\n\n local fs_endpoint; fs_endpoint=$(_env AGENTTEAMS_FS_ENDPOINT)\n [ -z \"${fs_endpoint}\" ] && fs_endpoint=$(_env AGENTTEAMS_MINIO_ENDPOINT)\n if [ -n \"${fs_endpoint}\" ]; then\n fs_endpoint=$(echo \"${fs_endpoint}\" | sed -e \"s|127\\.0\\.0\\.1|${CTRL_CONTAINER}|\" -e \"s|localhost|${CTRL_CONTAINER}|\")\n env_args+=(-e AGENTTEAMS_FS_ENDPOINT=\"${fs_endpoint}\")\n else\n env_args+=(-e AGENTTEAMS_FS_ENDPOINT=\"http://${CTRL_CONTAINER}:9000\")\n fi\n\n local fs_bucket; fs_bucket=$(_env AGENTTEAMS_FS_BUCKET)\n [ -z \"${fs_bucket}\" ] && fs_bucket=$(_env AGENTTEAMS_MINIO_BUCKET)\n [ -n \"${fs_bucket}\" ] && env_args+=(-e AGENTTEAMS_FS_BUCKET=\"${fs_bucket}\")\n\n local fs_user; fs_user=$(_env AGENTTEAMS_FS_ACCESS_KEY)\n [ -z \"${fs_user}\" ] && fs_user=$(_env AGENTTEAMS_MINIO_USER)\n [ -n \"${fs_user}\" ] && env_args+=(-e AGENTTEAMS_FS_ACCESS_KEY=\"${fs_user}\")\n\n local fs_pass; fs_pass=$(_env AGENTTEAMS_FS_SECRET_KEY)\n [ -z \"${fs_pass}\" ] && fs_pass=$(_env AGENTTEAMS_MINIO_PASSWORD)\n [ -n \"${fs_pass}\" ] && env_args+=(-e AGENTTEAMS_FS_SECRET_KEY=\"${fs_pass}\")\n\n for k in AGENTTEAMS_LLM_PROVIDER AGENTTEAMS_LLM_API_KEY AGENTTEAMS_OPENAI_BASE_URL AGENTTEAMS_DEFAULT_MODEL; do\n local v; v=$(_env \"${k}\")\n [ -n \"${v}\" ] && env_args+=(-e \"${k}=${v}\")\n done\n\n # The controller writes the CLI SA token to /var/run/agentteams/cli-token.\n # The token is generated asynchronously during the first reconcile loop,\n # so it may not be available immediately after the container starts.\n # We poll for up to 30s to handle both fresh installs (token not yet minted)\n # and older images that don't support cli-token at all.\n # For backward compatibility, also try the legacy /var/run/hiclaw/cli-token\n # path used by older HiClaw images.\n local auth_token=\"${AGENTTEAMS_AUTH_TOKEN:-}\"\n if [ -z \"${auth_token}\" ]; then\n local _token_wait=0\n local _token_max_wait=30\n while [ ${_token_wait} -lt ${_token_max_wait} ]; do\n auth_token=$(${DOCKER_CMD} exec \"${CTRL_CONTAINER}\" sh -c 'cat /var/run/agentteams/cli-token 2>/dev/null || cat /var/run/hiclaw/cli-token 2>/dev/null' | tr -d '\\n' || true)\n if [ -n \"${auth_token}\" ]; then\n break\n fi\n sleep 2\n _token_wait=$((_token_wait + 2))\n done\n fi\n if [ -n \"${auth_token}\" ]; then\n env_args+=(-e AGENTTEAMS_AUTH_TOKEN=\"${auth_token}\")\n else\n log \"WARNING: could not read controller SA token (timed out after ${_token_max_wait:-30}s)\"\n log \" This can happen with older AgentTeams/HiClaw images or if the controller is still starting.\"\n log \" Dashboard will still work but some API calls may fail until you log in via Higress Console.\"\n log \" To fix: upgrade AgentTeams to v1.2.0-beta.1+ or set AGENTTEAMS_AUTH_TOKEN manually.\"\n fi\n\n local admin_user; admin_user=$(_env AGENTTEAMS_ADMIN_USER)\n local admin_pass; admin_pass=$(_env AGENTTEAMS_ADMIN_PASSWORD)\n [ -n \"${admin_user}\" ] && env_args+=(-e AGENTTEAMS_ADMIN_USER=\"${admin_user}\")\n [ -n \"${admin_pass}\" ] && env_args+=(-e AGENTTEAMS_ADMIN_PASSWORD=\"${admin_pass}\")\n\n # Higress Console URL: explicit config takes priority, auto-detect as fallback.\n local _resolved_gw_url=\"\"\n if [ -n \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\" ]; then\n # Normalize URL: add http:// prefix if missing\n _resolved_gw_url=\"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\"\n case \"${_resolved_gw_url}\" in\n http://*|https://*) ;;\n *) _resolved_gw_url=\"http://${_resolved_gw_url}\" ;;\n esac\n env_args+=(-e AGENTTEAMS_AI_GATEWAY_ADMIN_URL=\"${_resolved_gw_url}\")\n elif ${DOCKER_CMD} exec \"${CTRL_CONTAINER}\" wget -q -O- --timeout=2 http://127.0.0.1:8001/ >/dev/null 2>&1; then\n _resolved_gw_url=\"http://${CTRL_CONTAINER}:8001\"\n env_args+=(-e AGENTTEAMS_AI_GATEWAY_ADMIN_URL=\"${_resolved_gw_url}\")\n fi\n\n # Verify Higress Console URL reachability (best-effort warning)\n if [ -n \"${_resolved_gw_url}\" ]; then\n if ! ${DOCKER_CMD} exec \"${CTRL_CONTAINER}\" curl -sf --max-time 3 \"${_resolved_gw_url}/\" >/dev/null 2>&1; then\n log \"WARNING: Higress Console URL may not be reachable: ${_resolved_gw_url}\"\n log \" Dashboard will still work, but shared login via Higress Console may fail.\"\n log \" To fix: verify the URL is correct and the Higress Console service is running.\"\n fi\n fi\n else\n log \"WARNING: ${CTRL_CONTAINER} not running; launching Dashboard without Controller/MinIO/LLM env.\"\n if [ -n \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\" ]; then\n local _gw_norm=\"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\"\n case \"${_gw_norm}\" in\n http://*|https://*) ;;\n *) _gw_norm=\"http://${_gw_norm}\" ;;\n esac\n env_args+=(-e AGENTTEAMS_AI_GATEWAY_ADMIN_URL=\"${_gw_norm}\")\n fi\n fi\n\n # Create persistent data volume (for future use)\n ${DOCKER_CMD} volume create agentteams-dashboard-data >/dev/null 2>&1 || true\n\n ${DOCKER_CMD} run -d \\\n --name \"${DASHBOARD_CONTAINER}\" \\\n --restart unless-stopped \\\n --network \"${AGENTTEAMS_NETWORK:-agentteams-net}\" \\\n --network-alias dashboard.agentteams.io \\\n -p \"${BIND_ADDR}:${AGENTTEAMS_PORT_DASHBOARD}:3000\" \\\n \"${env_args[@]}\" \\\n -v agentteams-dashboard-data:/app/db \\\n \"${AGENTTEAMS_DASHBOARD_IMAGE}\"\n\n # Wait for dashboard to be ready\n local max_wait=60\n local waited=0\n while [ $waited -lt $max_wait ]; do\n if curl -sf \"http://127.0.0.1:${AGENTTEAMS_PORT_DASHBOARD}/\" >/dev/null 2>&1; then\n log \"Dashboard ready on port ${AGENTTEAMS_PORT_DASHBOARD}\"\n return 0\n fi\n sleep 2\n waited=$((waited + 2))\n done\n log \"WARNING: Dashboard startup timed out after ${max_wait}s\"\n ${DOCKER_CMD} logs \"${DASHBOARD_CONTAINER}\" 2>&1 | tail -20\n return 0\n}\n\n# ============================================================\n# Manager Installation (Interactive)\n# ============================================================\n\ninstall_manager() {\n log \"$(msg install.title)\"\n log \"$(msg install.registry \"${AGENTTEAMS_REGISTRY}\")\"\n log \"\"\n log \"$(msg install.dir \"$(pwd)\")\"\n log \"$(msg install.dir_hint)\"\n log \"$(msg install.dir_hint2)\"\n log \"\"\n\n # Non-interactive fallback: resolve version immediately so image tags are available\n # before the step state machine runs. Interactive mode lets step_version handle it.\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_VERSION=\"${AGENTTEAMS_VERSION:-${AGENTTEAMS_KNOWN_STABLE_VERSION}}\"\n resolve_image_tags\n fi\n\n # Migrate legacy env file location before checks\n local existing_env=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n if [ ! -f \"${existing_env}\" ] && [ -f \"./agentteams-manager.env\" ]; then\n log \"Migrating agentteams-manager.env from current directory to ${existing_env}...\"\n mv \"./agentteams-manager.env\" \"${existing_env}\"\n fi\n\n # Orphan volume detection (only when no env file — step_existing handles the env-file case)\n if [ ! -f \"${existing_env}\" ]; then\n local data_vol=\"${AGENTTEAMS_DATA_DIR:-agentteams-data}\"\n if ${DOCKER_CMD} volume ls -q | grep -q \"^${data_vol}$\"; then\n echo \"\"\n log \"$(msg install.orphan_volume.detected \"${data_vol}\")\"\n log \"$(msg install.orphan_volume.warn)\"\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n log \"$(msg install.orphan_volume.clean_noninteractive)\"\n ${DOCKER_CMD} stop agentteams-manager 2>/dev/null || true\n ${DOCKER_CMD} rm agentteams-manager 2>/dev/null || true\n for w in $(${DOCKER_CMD} ps -a --format '{{.Names}}' | grep \"^agentteams-worker-\" || true); do\n ${DOCKER_CMD} stop \"${w}\" 2>/dev/null || true\n ${DOCKER_CMD} rm \"${w}\" 2>/dev/null || true\n done\n log \"$(msg install.orphan_volume.cleaning)\"\n ${DOCKER_CMD} volume rm \"${data_vol}\" 2>/dev/null || true\n log \"$(msg install.orphan_volume.cleaned)\"\n else\n echo \"\"\n echo \"$(msg install.orphan_volume.choose)\"\n echo \"$(msg install.orphan_volume.clean)\"\n echo \"$(msg install.orphan_volume.keep)\"\n echo \"\"\n local ORPHAN_CHOICE\n read -e -p \"$(msg install.orphan_volume.prompt): \" ORPHAN_CHOICE\n ORPHAN_CHOICE=\"${ORPHAN_CHOICE:-1}\"\n case \"${ORPHAN_CHOICE}\" in\n 1|clean)\n ${DOCKER_CMD} stop agentteams-manager 2>/dev/null || true\n ${DOCKER_CMD} rm agentteams-manager 2>/dev/null || true\n for w in $(${DOCKER_CMD} ps -a --format '{{.Names}}' | grep \"^agentteams-worker-\" || true); do\n ${DOCKER_CMD} stop \"${w}\" 2>/dev/null || true\n ${DOCKER_CMD} rm \"${w}\" 2>/dev/null || true\n done\n log \"$(msg install.orphan_volume.cleaning)\"\n ${DOCKER_CMD} volume rm \"${data_vol}\" 2>/dev/null || true\n log \"$(msg install.orphan_volume.cleaned)\"\n ;;\n 2|keep)\n log \"$(msg install.orphan_volume.keeping)\"\n ;;\n esac\n fi\n fi\n fi\n\n # ── State machine ─────────────────────────────────────────────────────────\n local _STEPS=( step_lang step_mode step_version step_existing step_llm step_manager_runtime step_runtime step_admin step_network\n step_ports step_domains step_github step_skills step_volume\n step_workspace step_dashboard step_e2ee step_docker_proxy step_idle step_hostshare step_podman_autostart )\n local _STEP_HISTORY=()\n local _step_idx=0\n while [ \"${_step_idx}\" -lt \"${#_STEPS[@]}\" ]; do\n local _step_fn=\"${_STEPS[$_step_idx]}\"\n if should_skip_step \"${_step_fn}\"; then\n _step_idx=$((_step_idx + 1))\n continue\n fi\n if [ \"${#_STEP_HISTORY[@]}\" -gt 0 ]; then\n log \"$(msg nav.back_hint)\"\n fi\n STEP_RESULT=\"\"\n \"${_step_fn}\"\n if [ \"${STEP_RESULT}\" = \"back\" ]; then\n if [ \"${#_STEP_HISTORY[@]}\" -gt 0 ]; then\n local _last=$(( ${#_STEP_HISTORY[@]} - 1 ))\n _step_idx=\"${_STEP_HISTORY[$_last]}\"\n _STEP_HISTORY=(\"${_STEP_HISTORY[@]:0:${_last}}\")\n clear_step_vars \"${_STEPS[$_step_idx]}\"\n fi\n # else: first step, ignore 'b'\n else\n _STEP_HISTORY+=(\"${_step_idx}\")\n _step_idx=$((_step_idx + 1))\n fi\n done\n # ── End state machine ──────────────────────────────────────────────────────\n\n # Post-machine defaults for any steps that were skipped\n AGENTTEAMS_DATA_DIR=\"${AGENTTEAMS_DATA_DIR:-agentteams-data}\"\n if [ -z \"${AGENTTEAMS_WORKSPACE_DIR+x}\" ] || [ -z \"${AGENTTEAMS_WORKSPACE_DIR}\" ]; then\n AGENTTEAMS_WORKSPACE_DIR=\"${HOME}/agentteams-manager\"\n export AGENTTEAMS_WORKSPACE_DIR\n fi\n AGENTTEAMS_WORKSPACE_DIR=\"$(cd \"${AGENTTEAMS_WORKSPACE_DIR}\" 2>/dev/null && pwd || echo \"${AGENTTEAMS_WORKSPACE_DIR}\")\"\n mkdir -p \"${AGENTTEAMS_WORKSPACE_DIR}\"\n AGENTTEAMS_MANAGER_RUNTIME=\"${AGENTTEAMS_MANAGER_RUNTIME:-copaw}\"\n export AGENTTEAMS_MANAGER_RUNTIME\n AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"${AGENTTEAMS_DEFAULT_WORKER_RUNTIME:-copaw}\"\n AGENTTEAMS_MATRIX_E2EE=\"${AGENTTEAMS_MATRIX_E2EE:-0}\"\n export AGENTTEAMS_MATRIX_E2EE\n AGENTTEAMS_WORKER_IDLE_TIMEOUT=\"${AGENTTEAMS_WORKER_IDLE_TIMEOUT:-720}\"\n export AGENTTEAMS_WORKER_IDLE_TIMEOUT\n AGENTTEAMS_HOST_SHARE_DIR=\"${AGENTTEAMS_HOST_SHARE_DIR:-$HOME}\"\n export AGENTTEAMS_HOST_SHARE_DIR\n\n log \"\"\n\n # Generate secrets (only if not already set)\n log \"$(msg install.generating_secrets)\"\n AGENTTEAMS_MANAGER_PASSWORD=\"${AGENTTEAMS_MANAGER_PASSWORD:-$(generate_key)}\"\n AGENTTEAMS_REGISTRATION_TOKEN=\"${AGENTTEAMS_REGISTRATION_TOKEN:-$(generate_key)}\"\n AGENTTEAMS_MINIO_USER=\"${AGENTTEAMS_MINIO_USER:-${AGENTTEAMS_ADMIN_USER}}\"\n AGENTTEAMS_MINIO_PASSWORD=\"${AGENTTEAMS_MINIO_PASSWORD:-${AGENTTEAMS_ADMIN_PASSWORD}}\"\n AGENTTEAMS_MANAGER_GATEWAY_KEY=\"${AGENTTEAMS_MANAGER_GATEWAY_KEY:-$(generate_key)}\"\n\n # Matrix AppService tokens — generate once during install/upgrade if not provided.\n # Persisted to env file so they survive controller restarts.\n AGENTTEAMS_MATRIX_APPSERVICE_ENABLED=\"${AGENTTEAMS_MATRIX_APPSERVICE_ENABLED:-true}\"\n export AGENTTEAMS_MATRIX_APPSERVICE_ENABLED\n if [ \"${AGENTTEAMS_MATRIX_APPSERVICE_ENABLED}\" != \"false\" ] && [ \"${AGENTTEAMS_MATRIX_APPSERVICE_ENABLED}\" != \"0\" ]; then\n if [ -z \"${AGENTTEAMS_MATRIX_APPSERVICE_AS_TOKEN}\" ]; then\n AGENTTEAMS_MATRIX_APPSERVICE_AS_TOKEN=\"$(openssl rand -hex 32)\"\n log \" Auto-generated AppService as_token (saved to env file)\"\n fi\n if [ -z \"${AGENTTEAMS_MATRIX_APPSERVICE_HS_TOKEN}\" ]; then\n AGENTTEAMS_MATRIX_APPSERVICE_HS_TOKEN=\"$(openssl rand -hex 32)\"\n log \" Auto-generated AppService hs_token (saved to env file)\"\n fi\n export AGENTTEAMS_MATRIX_APPSERVICE_AS_TOKEN\n export AGENTTEAMS_MATRIX_APPSERVICE_HS_TOKEN\n fi\n\n # Detect Apple Silicon (M1/M2/M3/M4) - need JVM fix for Higress Console\n # See: https://github.com/agentscope-ai/AgentTeams/issues/249\n if [ -z \"${JVM_ARGS:-}\" ] && [ \"$(uname -m)\" = \"arm64\" ] && [ \"$(uname -s)\" = \"Darwin\" ]; then\n log \"Apple Silicon detected - setting JVM_ARGS to fix Higress Console SIGILL issue\"\n JVM_ARGS=\"-XX:+UnlockDiagnosticVMOptions -XX:-UseAESCTRIntrinsics -XX:UseSVE=0\"\n fi\n\n # Write .env file\n ENV_FILE=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n cat > \"${ENV_FILE}\" << EOF\n# AgentTeams Manager Configuration\n# Generated by agentteams-install.sh on $(date)\n\n# Language\nAGENTTEAMS_LANGUAGE=${AGENTTEAMS_LANGUAGE}\n\n# LLM\nAGENTTEAMS_LLM_PROVIDER=${AGENTTEAMS_LLM_PROVIDER}\nAGENTTEAMS_DEFAULT_MODEL=${AGENTTEAMS_DEFAULT_MODEL}\nAGENTTEAMS_LLM_API_KEY=${AGENTTEAMS_LLM_API_KEY}\nAGENTTEAMS_OPENAI_BASE_URL=${AGENTTEAMS_OPENAI_BASE_URL:-}\nAGENTTEAMS_MODEL_CONTEXT_WINDOW=${AGENTTEAMS_MODEL_CONTEXT_WINDOW:-}\nAGENTTEAMS_MODEL_MAX_TOKENS=${AGENTTEAMS_MODEL_MAX_TOKENS:-}\nAGENTTEAMS_MODEL_REASONING=${AGENTTEAMS_MODEL_REASONING:-}\nAGENTTEAMS_MODEL_VISION=${AGENTTEAMS_MODEL_VISION:-}\n\n# Embedding model (empty = disabled, default: text-embedding-v4)\nAGENTTEAMS_EMBEDDING_MODEL=${AGENTTEAMS_EMBEDDING_MODEL}\n\n# Admin\nAGENTTEAMS_ADMIN_USER=${AGENTTEAMS_ADMIN_USER}\nAGENTTEAMS_ADMIN_PASSWORD=${AGENTTEAMS_ADMIN_PASSWORD}\n\n# Ports\nAGENTTEAMS_LOCAL_ONLY=${AGENTTEAMS_LOCAL_ONLY}\nAGENTTEAMS_PORT_GATEWAY=${AGENTTEAMS_PORT_GATEWAY}\nAGENTTEAMS_PORT_CONSOLE=${AGENTTEAMS_PORT_CONSOLE}\nAGENTTEAMS_PORT_ELEMENT_WEB=${AGENTTEAMS_PORT_ELEMENT_WEB}\nAGENTTEAMS_PORT_MANAGER_CONSOLE=${AGENTTEAMS_PORT_MANAGER_CONSOLE:-18888}\n\n# Manager runtime (openclaw | copaw)\nAGENTTEAMS_MANAGER_RUNTIME=${AGENTTEAMS_MANAGER_RUNTIME:-copaw}\n\n# Matrix\nAGENTTEAMS_MATRIX_DOMAIN=${AGENTTEAMS_MATRIX_DOMAIN}\nAGENTTEAMS_MATRIX_CLIENT_DOMAIN=${AGENTTEAMS_MATRIX_CLIENT_DOMAIN}\n\n# Gateway\nAGENTTEAMS_AI_GATEWAY_DOMAIN=${AGENTTEAMS_AI_GATEWAY_DOMAIN}\nAGENTTEAMS_MANAGER_GATEWAY_KEY=${AGENTTEAMS_MANAGER_GATEWAY_KEY}\n\n# File System\nAGENTTEAMS_FS_DOMAIN=${AGENTTEAMS_FS_DOMAIN}\nAGENTTEAMS_CONSOLE_DOMAIN=${AGENTTEAMS_CONSOLE_DOMAIN}\nAGENTTEAMS_MINIO_USER=${AGENTTEAMS_MINIO_USER}\nAGENTTEAMS_MINIO_PASSWORD=${AGENTTEAMS_MINIO_PASSWORD}\n\n# Internal\nAGENTTEAMS_MANAGER_PASSWORD=${AGENTTEAMS_MANAGER_PASSWORD}\nAGENTTEAMS_REGISTRATION_TOKEN=${AGENTTEAMS_REGISTRATION_TOKEN}\n\n# GitHub (optional)\nAGENTTEAMS_GITHUB_TOKEN=${AGENTTEAMS_GITHUB_TOKEN:-}\n\n# Nacos package import defaults\nAGENTTEAMS_NACOS_REGISTRY_URI=${AGENTTEAMS_NACOS_REGISTRY_URI:-nacos://market.agentteams.io:80/public}\nAGENTTEAMS_NACOS_USERNAME=${AGENTTEAMS_NACOS_USERNAME:-}\nAGENTTEAMS_NACOS_PASSWORD=${AGENTTEAMS_NACOS_PASSWORD:-}\nAGENTTEAMS_NACOS_TOKEN=${AGENTTEAMS_NACOS_TOKEN:-}\n\n# Skills Registry (optional, default: nacos://market.agentteams.io:80/public)\nAGENTTEAMS_SKILLS_API_URL=${AGENTTEAMS_SKILLS_API_URL:-nacos://market.agentteams.io:80/public}\n\n# OpenClaw CMS plugin (optional)\nAGENTTEAMS_CMS_TRACES_ENABLED=${AGENTTEAMS_CMS_TRACES_ENABLED:-false}\nAGENTTEAMS_CMS_ENDPOINT=${AGENTTEAMS_CMS_ENDPOINT:-}\nAGENTTEAMS_CMS_LICENSE_KEY=${AGENTTEAMS_CMS_LICENSE_KEY:-}\nAGENTTEAMS_CMS_PROJECT=${AGENTTEAMS_CMS_PROJECT:-}\nAGENTTEAMS_CMS_WORKSPACE=${AGENTTEAMS_CMS_WORKSPACE:-}\nAGENTTEAMS_CMS_SERVICE_NAME=${AGENTTEAMS_CMS_SERVICE_NAME:-agentteams-manager}\nAGENTTEAMS_CMS_METRICS_ENABLED=${AGENTTEAMS_CMS_METRICS_ENABLED:-false}\n\n# Worker images (for direct container creation)\nAGENTTEAMS_WORKER_IMAGE=${WORKER_IMAGE}\nAGENTTEAMS_COPAW_WORKER_IMAGE=${COPAW_WORKER_IMAGE}\nAGENTTEAMS_HERMES_WORKER_IMAGE=${HERMES_WORKER_IMAGE}\n\n# Default Worker runtime (openclaw | copaw | hermes)\nAGENTTEAMS_DEFAULT_WORKER_RUNTIME=${AGENTTEAMS_DEFAULT_WORKER_RUNTIME:-copaw}\n\n# Matrix E2EE (0=disabled, 1=enabled; default: 0)\nAGENTTEAMS_MATRIX_E2EE=${AGENTTEAMS_MATRIX_E2EE:-0}\n\n# Matrix AppService\nAGENTTEAMS_MATRIX_APPSERVICE_ENABLED=${AGENTTEAMS_MATRIX_APPSERVICE_ENABLED:-true}\nAGENTTEAMS_MATRIX_APPSERVICE_AS_TOKEN=${AGENTTEAMS_MATRIX_APPSERVICE_AS_TOKEN:-}\nAGENTTEAMS_MATRIX_APPSERVICE_HS_TOKEN=${AGENTTEAMS_MATRIX_APPSERVICE_HS_TOKEN:-}\n\n# Docker API proxy (0=disabled, 1=enabled; default: 1)\nAGENTTEAMS_DOCKER_PROXY=${AGENTTEAMS_DOCKER_PROXY:-1}\n\n# Docker API proxy: additional allowed image sources (comma-separated)\nAGENTTEAMS_PROXY_ALLOWED_REGISTRIES=${AGENTTEAMS_PROXY_ALLOWED_REGISTRIES:-}\n\n# Worker idle timeout in minutes (default: 720 = 12 hours)\nAGENTTEAMS_WORKER_IDLE_TIMEOUT=${AGENTTEAMS_WORKER_IDLE_TIMEOUT:-720}\n\n# Podman autostart via systemd (0=disabled, 1=enabled)\nAGENTTEAMS_PODMAN_AUTOSTART=${AGENTTEAMS_PODMAN_AUTOSTART:-0}\n\n# JVM Args for Higress Console (fixes SIGILL on Apple Silicon)\nJVM_ARGS=${JVM_ARGS:-}\n\n# Higress WASM plugin image registry (auto-selected by timezone)\nHIGRESS_ADMIN_WASM_PLUGIN_IMAGE_REGISTRY=${AGENTTEAMS_REGISTRY}\n\n# Data persistence\nAGENTTEAMS_DATA_DIR=${AGENTTEAMS_DATA_DIR:-agentteams-data}\n# Manager workspace (skills, memory, state — host-editable)\nAGENTTEAMS_WORKSPACE_DIR=${AGENTTEAMS_WORKSPACE_DIR:-}\n# Host directory sharing\nAGENTTEAMS_HOST_SHARE_DIR=${AGENTTEAMS_HOST_SHARE_DIR:-}\n\n# agentteams-dashboard (management UI)\nAGENTTEAMS_DASHBOARD=${AGENTTEAMS_DASHBOARD:-1}\nAGENTTEAMS_DASHBOARD_VERSION=${AGENTTEAMS_DASHBOARD_VERSION:-v1.2.0-beta.1}\nAGENTTEAMS_PORT_DASHBOARD=${AGENTTEAMS_PORT_DASHBOARD:-13000}\nAGENTTEAMS_DASHBOARD_IMAGE=${AGENTTEAMS_DASHBOARD_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-dashboard:${AGENTTEAMS_DASHBOARD_VERSION}}\nAGENTTEAMS_AI_GATEWAY_ADMIN_URL=${AGENTTEAMS_AI_GATEWAY_ADMIN_URL:-}\nEOF\n\n chmod 600 \"${ENV_FILE}\"\n log \"$(msg install.config_saved \"${ENV_FILE}\")\"\n\n # Detect container runtime socket\n SOCKET_MOUNT_ARGS=\"\"\n if [ \"${AGENTTEAMS_MOUNT_SOCKET}\" = \"1\" ]; then\n # Actively ensure the API socket is enabled and active before detection.\n if [ \"${DOCKER_CMD:-}\" = \"podman\" ]; then\n ensure_podman_socket\n fi\n CONTAINER_SOCK=$(detect_socket)\n if [ -n \"${CONTAINER_SOCK}\" ]; then\n log \"$(msg install.socket_detected \"${CONTAINER_SOCK}\")\"\n SOCKET_MOUNT_ARGS=\"-v ${CONTAINER_SOCK}:/var/run/docker.sock --security-opt label=disable\"\n else\n log \"$(msg install.socket_not_found)\"\n # Interactive confirmation when socket not found\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" != \"1\" ]; then\n echo \"\"\n echo -e \"\\033[33m$(msg install.socket_confirm.title)\\033[0m\"\n echo \"\"\n echo -e \"$(msg install.socket_confirm.message)\"\n echo \"\"\n read -p \"$(msg install.socket_confirm.prompt)\" SOCKET_CONFIRM\n if [ \"${SOCKET_CONFIRM}\" != \"y\" ] && [ \"${SOCKET_CONFIRM}\" != \"Y\" ]; then\n log \"$(msg install.socket_confirm.cancelled)\"\n exit 0\n fi\n fi\n fi\n fi\n\n # Create the data volume if it doesn't already exist (reuse on reinstall)\n if ! ${DOCKER_CMD} volume ls -q | grep -q \"^${AGENTTEAMS_DATA_DIR}$\"; then\n ${DOCKER_CMD} volume create \"${AGENTTEAMS_DATA_DIR}\" > /dev/null\n fi\n\n # Data mount: Docker volume\n DATA_MOUNT_ARGS=\"-v ${AGENTTEAMS_DATA_DIR}:/data\"\n\n # Manager workspace mount (always a host directory, defaulting to ~/agentteams-manager)\n WORKSPACE_MOUNT_ARGS=\"-v ${AGENTTEAMS_WORKSPACE_DIR}:/root/manager-workspace\"\n\n # Pass host timezone to container so date/time commands reflect local time\n TZ_ARGS=\"-e TZ=${AGENTTEAMS_TIMEZONE}\"\n\n # Host directory mount\n if [ -d \"${AGENTTEAMS_HOST_SHARE_DIR}\" ]; then\n HOST_SHARE_MOUNT_ARGS=\"-v ${AGENTTEAMS_HOST_SHARE_DIR}:/host-share\"\n log \"$(msg host_share.sharing \"${AGENTTEAMS_HOST_SHARE_DIR}\")\"\n else\n log \"$(msg host_share.not_exist \"${AGENTTEAMS_HOST_SHARE_DIR}\")\"\n HOST_SHARE_MOUNT_ARGS=\"-v ${AGENTTEAMS_HOST_SHARE_DIR}:/host-share\"\n fi\n\n # YOLO mode: pass through if set in environment (enables autonomous decisions)\n YOLO_ARGS=\"\"\n if [ \"${AGENTTEAMS_YOLO:-}\" = \"1\" ]; then\n YOLO_ARGS=\"-e AGENTTEAMS_YOLO=1\"\n log \"$(msg install.yolo)\"\n fi\n\n # Matrix-plugin debug tracing: pass through if AGENTTEAMS_MATRIX_DEBUG=1.\n # The container entrypoints translate this to OPENCLAW_MATRIX_DEBUG=1\n # so the openclaw matrix plugin emits structured INFO-level lifecycle\n # traces (sync state transitions, room.invite/join, message handler\n # arrival + filter outcomes). Used to diagnose worker/manager hangs.\n MATRIX_DEBUG_ARGS=\"\"\n if [ \"${AGENTTEAMS_MATRIX_DEBUG:-}\" = \"1\" ]; then\n MATRIX_DEBUG_ARGS=\"-e AGENTTEAMS_MATRIX_DEBUG=1\"\n fi\n\n # E2EE is already in the env file; but also pass explicitly in case env file is not the source\n # (AGENTTEAMS_MATRIX_E2EE is already written to ENV_FILE above via --env-file)\n\n # Pull images (manager based on runtime config; all worker runtimes always pulled)\n _is_local_image() {\n case \"$1\" in\n agentteams/*) return 0 ;;\n *) return 1 ;;\n esac\n }\n\n _local_image_exists() {\n local _img=\"$1\"\n [ -z \"${_img}\" ] && return 1\n _is_local_image \"${_img}\" || return 1\n ${DOCKER_CMD} image inspect \"${_img}\" >/dev/null 2>&1\n }\n\n # Helper: pull or skip a single image\n # Args: $1=image $2=exists_msg_key $3=pulling_msg_key\n _pull_image() {\n local _img=\"$1\" _exists_key=\"$2\" _pull_key=\"$3\"\n [ -z \"${_img}\" ] && return 0\n if _local_image_exists \"${_img}\"; then\n log \"$(msg \"${_exists_key}\" \"${_img}\")\"\n return 0\n fi\n log \"$(msg \"${_pull_key}\" \"${_img}\")\"\n local _attempt=1\n while [ $_attempt -le 3 ]; do\n if ${DOCKER_CMD} pull \"${_img}\"; then\n return 0\n fi\n if [ $_attempt -lt 3 ]; then\n log \"Pull failed (attempt ${_attempt}/3), retrying in 5s...\"\n sleep 5\n fi\n _attempt=$((_attempt + 1))\n done\n die \"Failed to pull ${_img} after 3 attempts\"\n return 1\n }\n\n # Embedded controller image (resolve versioned tag, fallback to latest)\n resolve_embedded_image\n if [ \"${AGENTTEAMS_USE_EMBEDDED}\" = \"1\" ]; then\n _local_image_exists \"${EMBEDDED_IMAGE}\" || true\n fi\n\n # Manager image is always required (select based on runtime)\n if [ \"${AGENTTEAMS_MANAGER_RUNTIME}\" = \"copaw\" ]; then\n _pull_image \"${MANAGER_COPAW_IMAGE}\" \"install.image.exists\" \"install.image.pulling_manager\"\n else\n _pull_image \"${MANAGER_IMAGE}\" \"install.image.exists\" \"install.image.pulling_manager\"\n fi\n\n # Pull all worker runtime images (workers may use any runtime regardless of the default)\n _pull_image \"${WORKER_IMAGE}\" \"install.image.worker_exists\" \"install.image.pulling_worker\"\n _pull_image \"${COPAW_WORKER_IMAGE}\" \"install.image.worker_exists\" \"install.image.pulling_worker\"\n _pull_image \"${HERMES_WORKER_IMAGE}\" \"install.image.worker_exists\" \"install.image.pulling_worker\"\n\n # --- Pre-upgrade: extract Matrix passwords from running old containers ---\n # Only needed when upgrading FROM old architecture (v1.0.9) TO embedded.\n # For new-arch-to-new-arch upgrades, credential files already exist with\n # correct room IDs — we must NOT overwrite them.\n _creds_tmp=\"\"\n if [ \"${AGENTTEAMS_UPGRADE:-0}\" = \"1\" ] && [ \"${AGENTTEAMS_USE_EMBEDDED}\" = \"1\" ]; then\n # Detect if upgrading from old arch: old arch has no agentteams-controller container\n # (or has it as a docker-proxy only, not embedded). Check if the existing\n # agentteams-controller is an embedded image (has supervisord) or just a proxy.\n _is_old_arch=0\n if ! ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -q \"^agentteams-controller$\"; then\n _is_old_arch=1\n elif ${DOCKER_CMD} ps -a --format '{{.Names}} {{.Image}}' 2>/dev/null | grep \"^agentteams-controller \" | grep -qv \"embedded\"; then\n _is_old_arch=1\n fi\n\n if [ \"${_is_old_arch}\" = \"1\" ]; then\n _creds_tmp=$(mktemp -d)\n\n # docker exec for Matrix/minio paths only works while agentteams-manager is running.\n _mgr_creds_tempstart=0\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -q '^agentteams-manager$'; then\n if ! ${DOCKER_CMD} ps --format '{{.Names}}' 2>/dev/null | grep -q '^agentteams-manager$'; then\n log \"agentteams-manager is stopped; starting it temporarily to extract Matrix credentials for upgrade...\"\n ${DOCKER_CMD} start agentteams-manager 2>/dev/null || true\n wait_matrix_ready \"agentteams-manager\"\n _mgr_creds_tempstart=1\n fi\n fi\n\n # Manager password (container Config.Env, then secrets file inside running manager, then data volume)\n _mgr_pw=$(${DOCKER_CMD} inspect agentteams-manager --format '{{range .Config.Env}}{{println .}}{{end}}' 2>/dev/null | grep '^AGENTTEAMS_MANAGER_PASSWORD=' | cut -d= -f2-)\n if [ -z \"${_mgr_pw}\" ] && ${DOCKER_CMD} ps --format '{{.Names}}' 2>/dev/null | grep -q '^agentteams-manager$'; then\n _mgr_pw=$(${DOCKER_CMD} exec agentteams-manager bash -c 'source /data/agentteams-secrets.env 2>/dev/null && echo \"${AGENTTEAMS_MANAGER_PASSWORD}\"' 2>/dev/null)\n fi\n if [ -z \"${_mgr_pw}\" ] && ${DOCKER_CMD} volume ls -q 2>/dev/null | grep -q \"^${AGENTTEAMS_DATA_DIR}$\"; then\n _mgr_pw=$(agentteams_read_secret_from_data_volume \"${AGENTTEAMS_DATA_DIR}\" AGENTTEAMS_MANAGER_PASSWORD)\n fi\n\n # Manager admin DM room ID: login as admin, find DM room with @manager\n _mgr_room=\"\"\n if [ -n \"${_mgr_pw}\" ]; then\n _admin_pw=$(grep AGENTTEAMS_ADMIN_PASSWORD \"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\" 2>/dev/null | cut -d= -f2-)\n _admin_user=$(grep AGENTTEAMS_ADMIN_USER \"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\" 2>/dev/null | cut -d= -f2-)\n _admin_user=\"${_admin_user:-admin}\"\n _matrix_domain=$(grep AGENTTEAMS_MATRIX_DOMAIN \"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\" 2>/dev/null | cut -d= -f2-)\n if [ -n \"${_admin_pw}\" ] && ${DOCKER_CMD} ps --format '{{.Names}}' 2>/dev/null | grep -q '^agentteams-manager$'; then\n _admin_token=$(${DOCKER_CMD} exec agentteams-manager curl -sf -X POST http://127.0.0.1:6167/_matrix/client/v3/login \\\n -H \"Content-Type: application/json\" \\\n -d '{\"type\":\"m.login.password\",\"identifier\":{\"type\":\"m.id.user\",\"user\":\"'\"${_admin_user}\"'\"},\"password\":\"'\"${_admin_pw}\"'\"}' 2>/dev/null | python3 -c \"import sys,json; print(json.load(sys.stdin).get('access_token',''))\" 2>/dev/null || true)\n if [ -n \"${_admin_token}\" ]; then\n _mgr_room=$(${DOCKER_CMD} exec agentteams-manager curl -sf -X GET \\\n -H \"Authorization: Bearer ${_admin_token}\" \\\n \"http://127.0.0.1:6167/_matrix/client/v3/joined_rooms\" 2>/dev/null | python3 -c \"\nimport sys,json,subprocess\nrooms = json.load(sys.stdin).get('joined_rooms',[])\nfor room_id in rooms:\n enc = room_id.replace('!','%21')\n members = json.loads(subprocess.check_output([\n 'docker','exec','agentteams-manager','curl','-sf','-X','GET',\n '-H','Authorization: Bearer ${_admin_token}',\n 'http://127.0.0.1:6167/_matrix/client/v3/rooms/'+enc+'/members'\n ]).decode()).get('chunk',[])\n member_ids = [m['state_key'] for m in members]\n if any('manager' in m and 'admin' not in m.split(':')[0] for m in member_ids):\n if len(member_ids) <= 3:\n print(room_id)\n break\n\" 2>/dev/null || true)\n fi\n fi\n if [ -z \"${_mgr_room}\" ]; then\n _mgr_room=$(agentteams_read_admin_dm_room_from_workspace \"${AGENTTEAMS_WORKSPACE_DIR}\")\n fi\n cat > \"${_creds_tmp}/default.env\" <<CREDEOF\nWORKER_PASSWORD=\"${_mgr_pw}\"\nWORKER_MINIO_PASSWORD=\"$(openssl rand -hex 24)\"\nWORKER_GATEWAY_KEY=\"${AGENTTEAMS_MANAGER_GATEWAY_KEY}\"\nWORKER_ROOM_ID=\"${_mgr_room}\"\nCREDEOF\n log \"Extracted Manager Matrix password${_mgr_room:+ and room ID}\"\n fi\n\n # Worker passwords and room IDs from workers-registry.json\n if [ -f \"${AGENTTEAMS_WORKSPACE_DIR}/workers-registry.json\" ]; then\n _worker_names=$(python3 -c \"import json; d=json.load(open('${AGENTTEAMS_WORKSPACE_DIR}/workers-registry.json')); print(' '.join(d.get('workers',{}).keys()))\" 2>/dev/null || true)\n for _wname in ${_worker_names}; do\n _wpw=\"\"\n if ${DOCKER_CMD} ps --format '{{.Names}}' 2>/dev/null | grep -q '^agentteams-manager$'; then\n _wpw=$(${DOCKER_CMD} exec agentteams-manager cat \"/root/agentteams-fs/agents/${_wname}/credentials/matrix/password\" 2>/dev/null || true)\n fi\n if [ -z \"${_wpw}\" ] && ${DOCKER_CMD} volume ls -q 2>/dev/null | grep -q \"^${AGENTTEAMS_DATA_DIR}$\"; then\n _wpw=$(agentteams_read_worker_creds_value_from_volume \"${AGENTTEAMS_DATA_DIR}\" \"${_wname}\" WORKER_PASSWORD)\n fi\n _wroom=$(python3 -c \"import json; d=json.load(open('${AGENTTEAMS_WORKSPACE_DIR}/workers-registry.json')); print(d.get('workers',{}).get('${_wname}',{}).get('room_id',''))\" 2>/dev/null || true)\n if [ -z \"${_wroom}\" ] && ${DOCKER_CMD} volume ls -q 2>/dev/null | grep -q \"^${AGENTTEAMS_DATA_DIR}$\"; then\n _wroom=$(agentteams_read_worker_creds_value_from_volume \"${AGENTTEAMS_DATA_DIR}\" \"${_wname}\" WORKER_ROOM_ID)\n fi\n if [ -n \"${_wpw}\" ]; then\n cat > \"${_creds_tmp}/${_wname}.env\" <<CREDEOF\nWORKER_PASSWORD=\"${_wpw}\"\nWORKER_MINIO_PASSWORD=\"$(openssl rand -hex 24)\"\nWORKER_GATEWAY_KEY=\"$(openssl rand -hex 32)\"\nWORKER_ROOM_ID=\"${_wroom}\"\nCREDEOF\n log \"Extracted ${_wname} Matrix password${_wroom:+ and room ID}\"\n fi\n done\n fi\n\n if [ \"${_mgr_creds_tempstart}\" = \"1\" ]; then\n log \"Stopping agentteams-manager after credential extraction (upgrade will recreate containers)...\"\n ${DOCKER_CMD} stop agentteams-manager 2>/dev/null || true\n fi\n fi # _is_old_arch\n fi\n\n # --- Stop and remove existing containers ---\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' | grep -q \"^agentteams-controller$\"; then\n ${DOCKER_CMD} stop agentteams-controller 2>/dev/null || true\n ${DOCKER_CMD} rm agentteams-controller 2>/dev/null || true\n fi\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' | grep -q \"^agentteams-manager$\"; then\n log \"$(msg install.removing_existing)\"\n ${DOCKER_CMD} stop agentteams-manager 2>/dev/null || true\n ${DOCKER_CMD} rm agentteams-manager 2>/dev/null || true\n fi\n\n # Stop and remove worker containers (controller will recreate via CR reconciliation)\n if [ -n \"${UPGRADE_EXISTING_WORKERS:-}\" ]; then\n log \"$(msg install.existing.stopping_workers)\"\n for w in ${UPGRADE_EXISTING_WORKERS}; do\n ${DOCKER_CMD} stop \"${w}\" 2>/dev/null || true\n ${DOCKER_CMD} rm \"${w}\" 2>/dev/null || true\n log \"$(msg install.existing.removed \"${w}\")\"\n done\n fi\n\n # Clean up legacy containers from v1.0.x and earlier\n for _legacy_name in agentteams-docker-proxy; do\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -qx \"${_legacy_name}\"; then\n log \"Removing legacy container: ${_legacy_name}\"\n ${DOCKER_CMD} stop \"${_legacy_name}\" 2>/dev/null || true\n ${DOCKER_CMD} rm -f \"${_legacy_name}\" 2>/dev/null || true\n fi\n done\n\n # --- Upgrade: inject extracted credentials into data volume ---\n # Only needed for old-arch upgrades (credential files were extracted above).\n if [ -n \"${_creds_tmp}\" ] && [ -d \"${_creds_tmp}\" ] && [ -n \"$(ls -A \"${_creds_tmp}\" 2>/dev/null)\" ]; then\n local _cleanup_ctr=\"agentteams-upgrade-cleanup\"\n ${DOCKER_CMD} rm -f \"${_cleanup_ctr}\" 2>/dev/null || true\n ${DOCKER_CMD} run --rm --name \"${_cleanup_ctr}\" \\\n --entrypoint sh \\\n -v \"${AGENTTEAMS_DATA_DIR}:/data\" \\\n -v \"${_creds_tmp}:/creds:ro\" \\\n \"${EMBEDDED_IMAGE}\" -c '\n rm -rf /data/worker-creds\n mkdir -p /data/worker-creds\n cp /creds/*.env /data/worker-creds/ 2>/dev/null || true\n chmod 600 /data/worker-creds/*.env 2>/dev/null || true\n ' 2>/dev/null && log \"Injected credentials for upgrade\" || log \"Warning: credential injection failed, continuing\"\n rm -rf \"${_creds_tmp}\"\n fi\n\n # --- Start containers ---\n log \"$(msg install.starting_manager)\"\n\n # Build port binding args\n if [ \"${AGENTTEAMS_LOCAL_ONLY:-1}\" = \"1\" ]; then\n _port_prefix=\"127.0.0.1:\"\n else\n _port_prefix=\"\"\n fi\n\n # Ensure agentteams-net Docker network exists\n ${DOCKER_CMD} network inspect agentteams-net >/dev/null 2>&1 || ${DOCKER_CMD} network create agentteams-net\n\n if [ \"${AGENTTEAMS_USE_EMBEDDED}\" != \"1\" ] && [ \"${AGENTTEAMS_UPGRADE:-0}\" = \"1\" ]; then\n # Check if current installation is embedded — downgrade to legacy is not supported\n if ${DOCKER_CMD} ps -a --format '{{.Names}} {{.Image}}' 2>/dev/null | grep \"^agentteams-controller \" | grep -q \"embedded\"; then\n error \"Downgrade from embedded architecture to legacy version (${AGENTTEAMS_VERSION}) is not supported.\"\n error \"Please use 'make uninstall-embedded' first, then do a clean install of the target version.\"\n exit 1\n fi\n fi\n\n if [ \"${AGENTTEAMS_USE_EMBEDDED}\" = \"1\" ]; then\n # ============================================================\n # New architecture: embedded controller + auto-created manager\n # ============================================================\n\n # Internal port: 8080 (Higress gateway inside the container).\n local _internal_gw_port=8080\n local _matrix_domain=\"${AGENTTEAMS_MATRIX_DOMAIN:-matrix-local.agentteams.io:${AGENTTEAMS_PORT_GATEWAY}}\"\n local _aigw_domain=\"${AGENTTEAMS_AI_GATEWAY_DOMAIN:-aigw-local.agentteams.io}\"\n # Ensure internal gateway port is present (container-internal traffic uses 8080)\n case \"${_aigw_domain}\" in *:*) ;; *) _aigw_domain=\"${_aigw_domain}:${_internal_gw_port}\" ;; esac\n local _fs_domain=\"${AGENTTEAMS_FS_DOMAIN:-fs-local.agentteams.io}\"\n case \"${_fs_domain}\" in *:*) ;; *) _fs_domain=\"${_fs_domain}:${_internal_gw_port}\" ;; esac\n\n # Controller env args\n local _ctrl_env_prefix\n _ctrl_env_prefix=\"$(_controller_env_prefix \"${AGENTTEAMS_VERSION}\")\"\n local _ctrl_env_args=(\n -e \"${_ctrl_env_prefix}ADMIN_USER=${AGENTTEAMS_ADMIN_USER}\"\n -e \"${_ctrl_env_prefix}ADMIN_PASSWORD=${AGENTTEAMS_ADMIN_PASSWORD}\"\n -e \"${_ctrl_env_prefix}MANAGER_PASSWORD=${AGENTTEAMS_MANAGER_PASSWORD}\"\n -e \"${_ctrl_env_prefix}REGISTRATION_TOKEN=${AGENTTEAMS_REGISTRATION_TOKEN}\"\n -e \"${_ctrl_env_prefix}MINIO_USER=${AGENTTEAMS_MINIO_USER}\"\n -e \"${_ctrl_env_prefix}MINIO_PASSWORD=${AGENTTEAMS_MINIO_PASSWORD}\"\n -e \"${_ctrl_env_prefix}LLM_PROVIDER=${AGENTTEAMS_LLM_PROVIDER}\"\n -e \"${_ctrl_env_prefix}LLM_API_KEY=${AGENTTEAMS_LLM_API_KEY}\"\n -e \"${_ctrl_env_prefix}DEFAULT_MODEL=${AGENTTEAMS_DEFAULT_MODEL}\"\n -e \"${_ctrl_env_prefix}MANAGER_GATEWAY_KEY=${AGENTTEAMS_MANAGER_GATEWAY_KEY}\"\n -e \"${_ctrl_env_prefix}MANAGER_RUNTIME=${AGENTTEAMS_MANAGER_RUNTIME:-copaw}\"\n -e \"${_ctrl_env_prefix}MANAGER_IMAGE=$([ \"${AGENTTEAMS_MANAGER_RUNTIME}\" = \"copaw\" ] && echo \"${MANAGER_COPAW_IMAGE}\" || echo \"${MANAGER_IMAGE}\")\"\n -e \"${_ctrl_env_prefix}DEFAULT_WORKER_RUNTIME=${AGENTTEAMS_DEFAULT_WORKER_RUNTIME:-copaw}\"\n -e \"${_ctrl_env_prefix}WORKER_IMAGE=${WORKER_IMAGE}\"\n -e \"${_ctrl_env_prefix}COPAW_WORKER_IMAGE=${COPAW_WORKER_IMAGE}\"\n -e \"${_ctrl_env_prefix}HERMES_WORKER_IMAGE=${HERMES_WORKER_IMAGE}\"\n -e \"${_ctrl_env_prefix}MATRIX_DOMAIN=${_matrix_domain}\"\n -e \"${_ctrl_env_prefix}ELEMENT_HOMESERVER_URL=http://127.0.0.1:${AGENTTEAMS_PORT_GATEWAY}\"\n -e \"${_ctrl_env_prefix}MATRIX_URL=http://127.0.0.1:6167\"\n -e \"${_ctrl_env_prefix}MATRIX_E2EE=${AGENTTEAMS_MATRIX_E2EE:-0}\"\n -e \"${_ctrl_env_prefix}MINIO_ENDPOINT=http://127.0.0.1:9000\"\n -e \"${_ctrl_env_prefix}MINIO_BUCKET=agentteams-storage\"\n -e \"${_ctrl_env_prefix}STORAGE_PREFIX=agentteams/agentteams-storage\"\n -e \"${_ctrl_env_prefix}FS_ENDPOINT=http://127.0.0.1:9000\"\n -e \"${_ctrl_env_prefix}AI_GATEWAY_URL=http://${_aigw_domain}\"\n -e \"${_ctrl_env_prefix}CONTROLLER_URL=http://agentteams-controller:8090\"\n -e \"${_ctrl_env_prefix}DOCKER_NETWORK=agentteams-net\"\n -e \"${_ctrl_env_prefix}WORKSPACE_DIR=${AGENTTEAMS_WORKSPACE_DIR}\"\n -e \"${_ctrl_env_prefix}HOST_SHARE_DIR=${AGENTTEAMS_HOST_SHARE_DIR}\"\n -e \"${_ctrl_env_prefix}MANAGER_ENABLED=true\"\n -e \"${_ctrl_env_prefix}PORT_MANAGER_CONSOLE=${AGENTTEAMS_PORT_MANAGER_CONSOLE:-18888}\"\n )\n if _use_legacy_image_env \"${AGENTTEAMS_VERSION}\"; then\n _ctrl_env_args+=(\n -e \"${_ctrl_env_prefix}FS_BUCKET=agentteams-storage\"\n -e \"${_ctrl_env_prefix}RESOURCE_PREFIX=agentteams-\"\n )\n else\n _ctrl_env_args+=(\n -e \"${_ctrl_env_prefix}MATRIX_APPSERVICE_ENABLED=${AGENTTEAMS_MATRIX_APPSERVICE_ENABLED:-true}\"\n -e \"${_ctrl_env_prefix}MATRIX_APPSERVICE_AS_TOKEN=${AGENTTEAMS_MATRIX_APPSERVICE_AS_TOKEN:-}\"\n -e \"${_ctrl_env_prefix}MATRIX_APPSERVICE_HS_TOKEN=${AGENTTEAMS_MATRIX_APPSERVICE_HS_TOKEN:-}\"\n )\n fi\n\n # Timezone\n if [ -n \"${AGENTTEAMS_TIMEZONE:-}\" ]; then\n _ctrl_env_args+=(-e \"TZ=${AGENTTEAMS_TIMEZONE}\")\n fi\n\n # Yolo mode\n if [ \"${AGENTTEAMS_YOLO:-}\" = \"1\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}YOLO=1\")\n fi\n\n # Matrix-plugin debug tracing — propagated to every manager + worker\n # the controller spawns, then translated to OPENCLAW_MATRIX_DEBUG=1\n # by the container entrypoints. Use this to diagnose\n # \"worker did not join\" / \"manager replied empty\" hangs.\n if [ \"${AGENTTEAMS_MATRIX_DEBUG:-}\" = \"1\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}MATRIX_DEBUG=1\")\n fi\n\n # Optional: GitHub token\n if [ -n \"${AGENTTEAMS_GITHUB_TOKEN:-}\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}GITHUB_TOKEN=${AGENTTEAMS_GITHUB_TOKEN}\")\n fi\n\n # Optional: embedding model\n if [ -n \"${AGENTTEAMS_EMBEDDING_MODEL:-}\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}EMBEDDING_MODEL=${AGENTTEAMS_EMBEDDING_MODEL}\")\n fi\n\n # Optional: OpenAI-compatible base URL\n if [ -n \"${AGENTTEAMS_OPENAI_BASE_URL:-}\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}OPENAI_BASE_URL=${AGENTTEAMS_OPENAI_BASE_URL}\")\n fi\n # Optional: language\n if [ -n \"${AGENTTEAMS_LANGUAGE:-}\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}LANGUAGE=${AGENTTEAMS_LANGUAGE}\")\n fi\n\n # Optional: CMS/ARMS observability. In embedded mode the controller\n # spawns the Manager and Workers, so it must receive these settings.\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}CMS_TRACES_ENABLED=${AGENTTEAMS_CMS_TRACES_ENABLED:-false}\")\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}CMS_SERVICE_NAME=${AGENTTEAMS_CMS_SERVICE_NAME:-agentteams-manager}\")\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}CMS_METRICS_ENABLED=${AGENTTEAMS_CMS_METRICS_ENABLED:-false}\")\n if [ -n \"${AGENTTEAMS_CMS_ENDPOINT:-}\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}CMS_ENDPOINT=${AGENTTEAMS_CMS_ENDPOINT}\")\n fi\n if [ -n \"${AGENTTEAMS_CMS_LICENSE_KEY:-}\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}CMS_LICENSE_KEY=${AGENTTEAMS_CMS_LICENSE_KEY}\")\n fi\n if [ -n \"${AGENTTEAMS_CMS_PROJECT:-}\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}CMS_PROJECT=${AGENTTEAMS_CMS_PROJECT}\")\n fi\n if [ -n \"${AGENTTEAMS_CMS_WORKSPACE:-}\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}CMS_WORKSPACE=${AGENTTEAMS_CMS_WORKSPACE}\")\n fi\n\n # shellcheck disable=SC2086\n ${DOCKER_CMD} run -d \\\n --name agentteams-controller \\\n --network agentteams-net \\\n --network-alias matrix-local.agentteams.io \\\n --network-alias aigw-local.agentteams.io \\\n --network-alias fs-local.agentteams.io \\\n \"${_ctrl_env_args[@]}\" \\\n -v \"${CONTAINER_SOCK}:/var/run/docker.sock\" \\\n --security-opt label=disable \\\n -v \"${AGENTTEAMS_DATA_DIR}:/data\" \\\n -v \"${AGENTTEAMS_WORKSPACE_DIR}:/root/agentteams-fs/agents/manager\" \\\n -p \"${_port_prefix}${AGENTTEAMS_PORT_GATEWAY}:8080\" \\\n -p \"${_port_prefix}${AGENTTEAMS_PORT_CONSOLE}:8001\" \\\n -p \"${_port_prefix}${AGENTTEAMS_PORT_ELEMENT_WEB:-18088}:8088\" \\\n --restart unless-stopped \\\n \"${EMBEDDED_IMAGE}\"\n\n log \"Embedded controller started: agentteams-controller\"\n\n # Wait for infrastructure inside the controller container\n _wait_for_url() {\n local url=\"$1\" ctr=\"$2\" max_wait=\"${3:-120}\" desc=\"${4:-service}\"\n local elapsed=0\n log \"Waiting for ${desc}...\"\n while [ $elapsed -lt $max_wait ]; do\n if ${DOCKER_CMD} exec \"${ctr}\" curl -sf \"${url}\" >/dev/null 2>&1; then\n log \"${desc} is ready (${elapsed}s)\"\n return 0\n fi\n sleep 2\n elapsed=$((elapsed + 2))\n done\n log \"ERROR: ${desc} not ready after ${max_wait}s\"\n return 1\n }\n\n _wait_for_url \"http://127.0.0.1:6167/_tuwunel/server_version\" agentteams-controller 120 \"Tuwunel (Matrix)\" || exit 1\n _wait_for_url \"http://127.0.0.1:9000/minio/health/live\" agentteams-controller 60 \"MinIO\" || exit 1\n _wait_for_url \"http://127.0.0.1:8080/status\" agentteams-controller 120 \"Higress Gateway\" || exit 1\n\n # Wait for controller to create Manager Agent container\n log \"Waiting for Manager Agent container...\"\n local _mgr_wait=0\n local _mgr_max=300\n while [ $_mgr_wait -lt $_mgr_max ]; do\n if ${DOCKER_CMD} ps --format '{{.Names}}' 2>/dev/null | grep -q \"^agentteams-manager$\"; then\n log \"Manager Agent container detected (${_mgr_wait}s)\"\n break\n fi\n sleep 3\n _mgr_wait=$((_mgr_wait + 3))\n done\n if [ $_mgr_wait -ge $_mgr_max ]; then\n log \"ERROR: Manager Agent container not created after ${_mgr_max}s\"\n log \"Controller logs:\"\n ${DOCKER_CMD} exec agentteams-controller tail -30 /var/log/agentteams/agentteams-controller-error.log 2>/dev/null || true\n exit 1\n fi\n\n # Wait for Manager Agent to be running\n log \"Waiting for Manager Agent to start...\"\n local _agent_wait=0\n while [ $_agent_wait -lt 120 ]; do\n local _state\n _state=$(${DOCKER_CMD} inspect --format '{{.State.Status}}' agentteams-manager 2>/dev/null || echo \"missing\")\n if [ \"${_state}\" = \"running\" ]; then\n log \"Manager Agent is running\"\n break\n fi\n sleep 2\n _agent_wait=$((_agent_wait + 2))\n done\n\n # Enable yolo mode in agent if requested\n if [ \"${AGENTTEAMS_YOLO:-}\" = \"1\" ]; then\n ${DOCKER_CMD} exec agentteams-manager touch /root/manager-workspace/yolo-mode 2>/dev/null || true\n fi\n\n # Wait for the controller to send the first-boot welcome message.\n # The controller gates this on (a) Manager joining the DM room and\n # (b) Higress WASM key-auth propagation actually clearing /v1/chat/completions\n # for the Manager's gateway key — typically ~45-90s on a fresh install.\n # We poll Manager CR Status.WelcomeSent via the in-container agt CLI,\n # exec'd inside agentteams-controller (the source-of-truth container — its\n # bundled CLI binary is always in lockstep with whatever controller\n # binary is currently serving the HTTP API, since they're the same\n # `go build` output. The agentteams-manager container's CLI may lag the\n # controller across image upgrades and silently drop the welcomeSent\n # field, leaving this loop hung). The controller container mints a\n # long-lived admin SA token at startup and writes it to\n # AGENTTEAMS_AUTH_TOKEN_FILE=/var/run/agentteams/cli-token (set as a Dockerfile\n # ENV default), so a bare `docker exec agentteams-controller agt …`\n # auto-discovers both the endpoint and the token. There is a brief\n # window after container start before bootstrapAdminCLIToken completes\n # where the file may be empty / absent — the loop's silent retry\n # handles that the same way it handles the manager-not-yet-running\n # case below.\n if ${DOCKER_CMD} exec agentteams-controller sh -c 'command -v agt' >/dev/null 2>&1; then\n log \"$(msg install.welcome_msg.waiting)\"\n local _welcome_wait=0\n local _welcome_max=\"${AGENTTEAMS_WELCOME_TIMEOUT:-300}\"\n local _welcome_done=0\n while [ $_welcome_wait -lt $_welcome_max ]; do\n # `tr -d` strips whitespace/CR so the grep stays robust to\n # any future change in go-json field ordering or formatting.\n local _wjson\n _wjson=$(${DOCKER_CMD} exec agentteams-controller \\\n agt get managers default -o json 2>/dev/null || true)\n if [ -n \"${_wjson}\" ] && printf '%s' \"${_wjson}\" | tr -d ' \\r\\n' | grep -q '\"welcomeSent\":true'; then\n log \"$(msg install.welcome_msg.confirmed \"${_welcome_wait}\")\"\n _welcome_done=1\n break\n fi\n sleep 3\n _welcome_wait=$((_welcome_wait + 3))\n done\n if [ $_welcome_done -ne 1 ]; then\n # Non-fatal: install is still good. Keep going to the success\n # banner so the admin can use Element Web to nudge Manager into\n # onboarding manually (one DM message is enough).\n log \"$(msg install.welcome_msg.timeout \"${_welcome_max}\")\"\n log \"$(msg install.welcome_msg.timeout_hint)\"\n log \"$(msg install.welcome_msg.timeout_inspect)\"\n fi\n else\n log \"$(msg install.welcome_msg.poll_unavailable)\"\n fi\n\n else\n # ============================================================\n # Legacy architecture: all-in-one manager container\n # ============================================================\n\n NETWORK_ARGS=\"--network agentteams-net\"\n NETWORK_ALIAS_ARGS=\"--network-alias matrix-local.agentteams.io --network-alias aigw-local.agentteams.io --network-alias fs-local.agentteams.io\"\n for _domain in \"${AGENTTEAMS_MATRIX_CLIENT_DOMAIN:-}\" \"${AGENTTEAMS_CONSOLE_DOMAIN:-}\"; do\n if [ -n \"${_domain}\" ] && [[ \"${_domain}\" == *-local.agentteams.io ]]; then\n NETWORK_ALIAS_ARGS=\"${NETWORK_ALIAS_ARGS} --network-alias ${_domain}\"\n fi\n done\n\n # Start Docker API proxy if enabled (security layer between Manager and Docker daemon)\n PROXY_ARGS=\"\"\n if [ \"${AGENTTEAMS_DOCKER_PROXY:-1}\" = \"1\" ] && [ -n \"${CONTAINER_SOCK:-}\" ]; then\n local _proxy_image=\"${AGENTTEAMS_REGISTRY}/higress/agentteams-docker-proxy:${AGENTTEAMS_VERSION}\"\n # Try versioned tag, fallback to latest\n if ! ${DOCKER_CMD} image inspect \"${_proxy_image}\" >/dev/null 2>&1; then\n ${DOCKER_CMD} pull \"${_proxy_image}\" 2>/dev/null || {\n _proxy_image=\"${AGENTTEAMS_REGISTRY}/higress/agentteams-docker-proxy:latest\"\n ${DOCKER_CMD} pull \"${_proxy_image}\" 2>/dev/null || true\n }\n fi\n if ${DOCKER_CMD} image inspect \"${_proxy_image}\" >/dev/null 2>&1; then\n log \"Starting Docker API proxy...\"\n ${DOCKER_CMD} run -d \\\n --name agentteams-docker-proxy \\\n --network agentteams-net \\\n -v \"${CONTAINER_SOCK}:/var/run/docker.sock\" \\\n --security-opt label=disable \\\n -e AGENTTEAMS_WORKER_IMAGE=\"${WORKER_IMAGE}\" \\\n -e AGENTTEAMS_COPAW_WORKER_IMAGE=\"${COPAW_WORKER_IMAGE}\" \\\n -e AGENTTEAMS_HERMES_WORKER_IMAGE=\"${HERMES_WORKER_IMAGE}\" \\\n ${AGENTTEAMS_PROXY_ALLOWED_REGISTRIES:+-e AGENTTEAMS_PROXY_ALLOWED_REGISTRIES=\"${AGENTTEAMS_PROXY_ALLOWED_REGISTRIES}\"} \\\n --restart unless-stopped \\\n \"${_proxy_image}\"\n PROXY_ARGS=\"-e AGENTTEAMS_CONTROLLER_URL=http://agentteams-docker-proxy:2375 -e AGENTTEAMS_CONTAINER_API=http://agentteams-docker-proxy:2375\"\n SOCKET_MOUNT_ARGS=\"\"\n fi\n fi\n\n # Pass host timezone to container\n TZ_ARGS=\"\"\n if [ -n \"${AGENTTEAMS_TIMEZONE:-}\" ]; then\n TZ_ARGS=\"-e TZ=${AGENTTEAMS_TIMEZONE}\"\n fi\n\n YOLO_ARGS=\"\"\n if [ \"${AGENTTEAMS_YOLO:-}\" = \"1\" ]; then\n YOLO_ARGS=\"-e AGENTTEAMS_YOLO=1\"\n fi\n\n MATRIX_DEBUG_ARGS=\"\"\n if [ \"${AGENTTEAMS_MATRIX_DEBUG:-}\" = \"1\" ]; then\n MATRIX_DEBUG_ARGS=\"-e AGENTTEAMS_MATRIX_DEBUG=1\"\n fi\n\n # shellcheck disable=SC2086\n ${DOCKER_CMD} run -d \\\n --name agentteams-manager \\\n --env-file \"${ENV_FILE}\" \\\n -e HOME=/root/manager-workspace \\\n -w /root/manager-workspace \\\n -e HOST_ORIGINAL_HOME=\"${AGENTTEAMS_HOST_SHARE_DIR}\" \\\n -e AGENTTEAMS_MANAGER_RUNTIME=\"${AGENTTEAMS_MANAGER_RUNTIME:-copaw}\" \\\n ${JVM_ARGS:+-e JVM_ARGS=\"${JVM_ARGS}\"} \\\n ${YOLO_ARGS} \\\n ${MATRIX_DEBUG_ARGS} \\\n ${TZ_ARGS} \\\n ${SOCKET_MOUNT_ARGS} \\\n ${NETWORK_ARGS} \\\n ${NETWORK_ALIAS_ARGS} \\\n ${PROXY_ARGS} \\\n -p \"${_port_prefix}${AGENTTEAMS_PORT_GATEWAY}:8080\" \\\n -p \"${_port_prefix}${AGENTTEAMS_PORT_CONSOLE}:8001\" \\\n -p \"${_port_prefix}${AGENTTEAMS_PORT_ELEMENT_WEB:-18088}:8088\" \\\n -p \"127.0.0.1:${AGENTTEAMS_PORT_MANAGER_CONSOLE:-18888}:18888\" \\\n ${DATA_MOUNT_ARGS} \\\n ${WORKSPACE_MOUNT_ARGS} \\\n ${HOST_SHARE_MOUNT_ARGS} \\\n --restart unless-stopped \\\n \"$([ \"${AGENTTEAMS_MANAGER_RUNTIME}\" = \"copaw\" ] && echo \"${MANAGER_COPAW_IMAGE}\" || echo \"${MANAGER_IMAGE}\")\"\n\n # Wait for Manager agent to be ready\n wait_manager_ready \"agentteams-manager\"\n\n # Wait for Matrix server to be ready\n wait_matrix_ready \"agentteams-manager\"\n\n # Post-install verification (non-fatal: warnings only)\n local _verify_script\n _verify_script=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)/agentteams-verify.sh\"\n if [ -f \"${_verify_script}\" ]; then\n bash \"${_verify_script}\" \"agentteams-manager\" || {\n log \"WARNING: Some post-install checks failed. Re-run: bash install/agentteams-verify.sh\"\n }\n fi\n fi\n unset _port_prefix\n\n # ── Start Dashboard (optional) ────────────────────────────────────\n _start_dashboard\n\n # Apply Podman autostart if selected and environment matches\n if [ \"${DOCKER_CMD}\" = \"podman\" ] && [ \"${AGENTTEAMS_PODMAN_AUTOSTART:-0}\" = \"1\" ]; then\n setup_podman_autostart\n fi\n\n log \"\"\n log \"$(msg success.title)\"\n log \"\"\n log \"$(msg success.domains_configured)\"\n log \" ${AGENTTEAMS_MATRIX_DOMAIN%%:*} ${AGENTTEAMS_MATRIX_CLIENT_DOMAIN} ${AGENTTEAMS_AI_GATEWAY_DOMAIN} ${AGENTTEAMS_FS_DOMAIN} ${AGENTTEAMS_CONSOLE_DOMAIN}\"\n log \"\"\n local lan_ip\n lan_ip=$(detect_lan_ip)\n echo -e \"\\033[33m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\033[0m\"\n echo -e \"\\033[33m $(msg success.open_url)\\033[0m\"\n echo -e \"\\033[33m \\033[0m\"\n echo -e \"\\033[1;36m http://127.0.0.1:${AGENTTEAMS_PORT_ELEMENT_WEB:-18088}/#/login\\033[0m\"\n echo -e \"\\033[33m \\033[0m\"\n echo -e \"\\033[33m $(msg success.login_with)\\033[0m\"\n echo -e \"\\033[33m $(msg success.username \"${AGENTTEAMS_ADMIN_USER}\")\\033[0m\"\n echo -e \"\\033[33m $(msg success.password \"${AGENTTEAMS_ADMIN_PASSWORD}\")\\033[0m\"\n echo -e \"\\033[33m \\033[0m\"\n echo -e \"\\033[33m $(msg success.after_login)\\033[0m\"\n echo -e \"\\033[33m $(msg success.tell_it)\\033[0m\"\n echo -e \"\\033[33m $(msg success.manager_auto)\\033[0m\"\n echo -e \"\\033[33m \\033[0m\"\n echo -e \"\\033[33m ───────────────────────────────────────────────────────────────────────────── \\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_title)\\033[0m\"\n echo -e \"\\033[33m \\033[0m\"\n if [ -n \"${lan_ip}\" ]; then\n echo -e \"\\033[33m $(msg success.mobile_step1)\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_step2 \"http://${lan_ip}:${AGENTTEAMS_PORT_GATEWAY}\")\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_step3)\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_username \"${AGENTTEAMS_ADMIN_USER}\")\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_password \"${AGENTTEAMS_ADMIN_PASSWORD}\")\\033[0m\"\n else\n echo -e \"\\033[33m $(msg success.mobile_step1)\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_step2_noip \"${AGENTTEAMS_PORT_GATEWAY}\")\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_noip_hint)\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_step3)\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_username \"${AGENTTEAMS_ADMIN_USER}\")\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_password \"${AGENTTEAMS_ADMIN_PASSWORD}\")\\033[0m\"\n fi\n echo -e \"\\033[33m \\033[0m\"\n echo -e \"\\033[33m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\033[0m\"\n log \"\"\n log \"$(msg success.other_consoles)\"\n log \"$(msg success.higress_console \"${AGENTTEAMS_PORT_CONSOLE}\" \"${AGENTTEAMS_ADMIN_USER}\" \"${AGENTTEAMS_ADMIN_PASSWORD}\")\"\n if [ \"${AGENTTEAMS_DASHBOARD:-0}\" = \"1\" ]; then\n log \"$(msg success.dashboard \"${AGENTTEAMS_PORT_DASHBOARD:-13000}\")\"\n fi\n if [ \"${AGENTTEAMS_USE_EMBEDDED}\" != \"1\" ]; then\n log \"$(msg success.manager_console \"${AGENTTEAMS_PORT_MANAGER_CONSOLE:-18888}\")\"\n log \"$(msg success.manager_console_gateway \"${AGENTTEAMS_ADMIN_USER}\" \"${AGENTTEAMS_ADMIN_PASSWORD}\")\"\n fi\n log \"\"\n log \"$(msg success.switch_llm.title)\"\n log \"$(msg success.switch_llm.hint)\"\n log \"$(msg success.switch_llm.docs)\"\n log \"$(msg success.switch_llm.url)\"\n log \"\"\n log \"$(msg success.tip)\"\n log \"\"\n if [ \"${AGENTTEAMS_LOCAL_ONLY:-1}\" != \"1\" ]; then\n echo -e \"\\033[33m$(msg port.local_only.https_hint)\\033[0m\"\n log \"\"\n fi\n log \"$(msg success.config_file \"${ENV_FILE}\")\"\n log \"$(msg success.data_volume \"${AGENTTEAMS_DATA_DIR}\")\"\n log \"$(msg success.workspace \"${AGENTTEAMS_WORKSPACE_DIR}\")\"\n}\n\n# ============================================================\n# Worker Installation (One-Click)\n# ============================================================\n\ninstall_worker() {\n local WORKER_NAME=\"\"\n local FS=\"\"\n local FS_KEY=\"\"\n local FS_SECRET=\"\"\n local RESET=false\n local SKILLS_API_URL=\"\"\n\n # Parse arguments\n while [ $# -gt 0 ]; do\n case $1 in\n --name) WORKER_NAME=\"$2\"; shift 2 ;;\n --fs) FS=\"$2\"; shift 2 ;;\n --fs-key) FS_KEY=\"$2\"; shift 2 ;;\n --fs-secret) FS_SECRET=\"$2\"; shift 2 ;;\n --skills-api-url) SKILLS_API_URL=\"$2\"; shift 2 ;;\n --reset) RESET=true; shift ;;\n *) die \"$(msg error.unknown_option \"$1\")\" ;;\n esac\n done\n\n # Validate required params\n [ -z \"${WORKER_NAME}\" ] && die \"$(msg error.name_required)\"\n [ -z \"${FS}\" ] && die \"$(msg error.fs_required)\"\n [ -z \"${FS_KEY}\" ] && die \"$(msg error.fs_key_required)\"\n [ -z \"${FS_SECRET}\" ] && die \"$(msg error.fs_secret_required)\"\n\n local CONTAINER_NAME=\"agentteams-worker-${WORKER_NAME}\"\n\n # Handle reset\n if [ \"${RESET}\" = true ]; then\n log \"$(msg worker.resetting \"${WORKER_NAME}\")\"\n ${DOCKER_CMD} stop \"${CONTAINER_NAME}\" 2>/dev/null || true\n ${DOCKER_CMD} rm \"${CONTAINER_NAME}\" 2>/dev/null || true\n fi\n\n # Check for existing container\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' | grep -q \"^${CONTAINER_NAME}$\"; then\n die \"$(msg worker.exists \"${CONTAINER_NAME}\")\"\n fi\n\n log \"$(msg worker.starting \"${WORKER_NAME}\")\"\n\n # Build docker run args\n local DOCKER_ENV=\"\"\n DOCKER_ENV=\"${DOCKER_ENV} -e HOME=/root/agentteams-fs/agents/${WORKER_NAME}\"\n DOCKER_ENV=\"${DOCKER_ENV} -w /root/agentteams-fs/agents/${WORKER_NAME}\"\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_WORKER_NAME=${WORKER_NAME}\"\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_FS_ENDPOINT=${FS}\"\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_FS_ACCESS_KEY=${FS_KEY}\"\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_FS_SECRET_KEY=${FS_SECRET}\"\n\n if [ -z \"${SKILLS_API_URL}\" ]; then\n if [ -n \"${AGENTTEAMS_SKILLS_API_URL:-}\" ]; then\n SKILLS_API_URL=\"${AGENTTEAMS_SKILLS_API_URL}\"\n else\n SKILLS_API_URL=\"nacos://market.agentteams.io:80/public\"\n fi\n fi\n\n # Add SKILLS_API_URL if specified\n DOCKER_ENV=\"${DOCKER_ENV} -e SKILLS_API_URL=${SKILLS_API_URL}\"\n log \"$(msg worker.skills_url \"${SKILLS_API_URL}\")\"\n if [ -n \"${AGENTTEAMS_NACOS_USERNAME:-}\" ]; then\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_NACOS_USERNAME=${AGENTTEAMS_NACOS_USERNAME}\"\n fi\n if [ -n \"${AGENTTEAMS_NACOS_PASSWORD:-}\" ]; then\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_NACOS_PASSWORD=${AGENTTEAMS_NACOS_PASSWORD}\"\n fi\n if [ -n \"${AGENTTEAMS_NACOS_TOKEN:-}\" ]; then\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_NACOS_TOKEN=${AGENTTEAMS_NACOS_TOKEN}\"\n fi\n\n # shellcheck disable=SC2086\n ${DOCKER_CMD} run -d \\\n --name \"${CONTAINER_NAME}\" \\\n ${DOCKER_ENV} \\\n --restart unless-stopped \\\n \"${WORKER_IMAGE}\"\n\n log \"\"\n log \"$(msg worker.started \"${WORKER_NAME}\")\"\n log \"$(msg worker.container \"${CONTAINER_NAME}\")\"\n log \"$(msg worker.view_logs \"${CONTAINER_NAME}\")\"\n}\n\n# ============================================================\n# Main\n# ============================================================\n\n# ============================================================\n# LLM API connectivity test\n# ============================================================\n\ntest_llm_connectivity() {\n local base_url=\"$1\"\n local api_key=\"$2\"\n local model=\"$3\"\n local hint=\"${4:-}\" # optional: extra hint shown on failure\n if ! command -v curl >/dev/null 2>&1; then\n echo -e \"\\033[33m$(msg llm.openai.test.no_curl)\\033[0m\"\n return\n fi\n log \"$(msg llm.openai.test.testing)\"\n local _body _http_code _tmpfile\n _tmpfile=$(mktemp)\n _http_code=$(curl -s -o \"${_tmpfile}\" -w \"%{http_code}\" \\\n -X POST \"${base_url%/}/chat/completions\" \\\n -H \"Authorization: Bearer ${api_key}\" \\\n -H \"Content-Type: application/json\" \\\n -H \"User-Agent: AgentTeams/${AGENTTEAMS_VERSION:-latest}\" \\\n --max-time 30 \\\n -d \"{\\\"model\\\":\\\"${model}\\\",\\\"messages\\\":[{\\\"role\\\":\\\"user\\\",\\\"content\\\":\\\"hi\\\"}]}\" \\\n 2>/dev/null)\n _body=$(cat \"${_tmpfile}\")\n rm -f \"${_tmpfile}\"\n if [ \"${_http_code}\" = \"200\" ] || [ \"${_http_code}\" = \"201\" ]; then\n log \"$(msg llm.openai.test.ok)\"\n else\n echo -e \"\\033[33m$(msg llm.openai.test.fail \"${_http_code}\" \"${_body}\")\\033[0m\"\n if [ -n \"${hint}\" ]; then\n echo -e \"\\033[33m${hint}\\033[0m\"\n fi\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" != \"1\" ]; then\n local _confirm\n read -e -p \"$(msg llm.openai.test.confirm)\" _confirm\n if [ \"${_confirm}\" = \"b\" ]; then\n STEP_RESULT=\"back\"\n return 1\n fi\n if [ \"${_confirm}\" != \"y\" ] && [ \"${_confirm}\" != \"Y\" ]; then\n log \"$(msg llm.openai.test.aborted)\"\n exit 1\n fi\n fi\n fi\n}\n\ntest_embedding_connectivity() {\n local base_url=\"$1\"\n local api_key=\"$2\"\n local model=\"$3\"\n if ! command -v curl >/dev/null 2>&1; then\n return 0\n fi\n log \"$(msg llm.embedding.test.testing)\"\n local _body _http_code _tmpfile\n _tmpfile=$(mktemp)\n _http_code=$(curl -s -o \"${_tmpfile}\" -w \"%{http_code}\" \\\n -X POST \"${base_url%/}/embeddings\" \\\n -H \"Authorization: Bearer ${api_key}\" \\\n -H \"Content-Type: application/json\" \\\n -H \"User-Agent: AgentTeams/${AGENTTEAMS_VERSION:-latest}\" \\\n --max-time 30 \\\n -d \"{\\\"model\\\":\\\"${model}\\\",\\\"input\\\":\\\"test\\\"}\" \\\n 2>/dev/null)\n _body=$(cat \"${_tmpfile}\")\n rm -f \"${_tmpfile}\"\n if [ \"${_http_code}\" = \"200\" ] || [ \"${_http_code}\" = \"201\" ]; then\n log \"$(msg llm.embedding.test.ok)\"\n return 0\n else\n echo -e \"\\033[33m$(msg llm.embedding.test.fail \"${_http_code}\" \"${_body}\")\\033[0m\"\n return 1\n fi\n}\n\n# ============================================================\n# Uninstall\n# ============================================================\n\nuninstall_agentteams() {\n log \"$(msg uninstall.title)\"\n\n # Safely disable and remove the dedicated AgentTeams Podman autostart service\n if [ \"${DOCKER_CMD}\" = \"podman\" ] && command -v systemctl >/dev/null 2>&1; then\n local _systemctl_cmd=\"systemctl\"\n local _service_dir=\"/etc/systemd/system\"\n\n if [ \"$(id -u)\" != \"0\" ]; then\n _systemctl_cmd=\"systemctl --user\"\n _service_dir=\"${HOME}/.config/systemd/user\"\n fi\n\n if [ -f \"${_service_dir}/agentteams-podman-restart.service\" ]; then\n log \"Disabling and removing dedicated AgentTeams Podman autostart service...\"\n ${_systemctl_cmd} disable --now agentteams-podman-restart.service 2>/dev/null || true\n rm -f \"${_service_dir}/agentteams-podman-restart.service\"\n ${_systemctl_cmd} daemon-reload 2>/dev/null || true\n fi\n # Note: Native podman-restart.service is strictly untouched per maintainer review.\n fi\n\n # Capture manager image before removing (needed for workspace cleanup on Linux)\n local manager_image=\"\"\n manager_image=$(${DOCKER_CMD} inspect agentteams-manager --format '{{.Config.Image}}' 2>/dev/null || true)\n\n # Stop and remove manager\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -q \"^agentteams-manager$\"; then\n log \"$(msg uninstall.stopping_manager)\"\n ${DOCKER_CMD} stop agentteams-manager >/dev/null 2>&1 || true\n ${DOCKER_CMD} rm agentteams-manager >/dev/null 2>&1 || true\n fi\n\n # Stop and remove workers\n local workers\n workers=$(${DOCKER_CMD} ps -a --filter \"name=agentteams-worker-\" --format '{{.Names}}' 2>/dev/null || true)\n if [ -n \"${workers}\" ]; then\n log \"$(msg uninstall.stopping_workers)\"\n echo \"${workers}\" | while read -r w; do\n ${DOCKER_CMD} rm -f \"${w}\" >/dev/null 2>&1 || true\n log \"$(msg uninstall.removed \"${w}\")\"\n done\n fi\n\n # Stop and remove the dashboard container (installed via step_dashboard)\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -q \"^agentteams-dashboard$\"; then\n log \"Stopping and removing agentteams-dashboard...\"\n ${DOCKER_CMD} stop agentteams-dashboard >/dev/null 2>&1 || true\n ${DOCKER_CMD} rm agentteams-dashboard >/dev/null 2>&1 || true\n fi\n if ${DOCKER_CMD} volume inspect agentteams-dashboard-data >/dev/null 2>&1; then\n log \"Removing dashboard data volume: agentteams-dashboard-data\"\n ${DOCKER_CMD} volume rm agentteams-dashboard-data >/dev/null 2>&1 || true\n fi\n\n # Stop and remove docker-proxy (legacy ≤ v1.0.x; current arch uses\n # agentteams-controller for the same role)\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -q \"^agentteams-docker-proxy$\"; then\n log \"$(msg uninstall.removing_proxy)\"\n ${DOCKER_CMD} stop agentteams-docker-proxy >/dev/null 2>&1 || true\n ${DOCKER_CMD} rm agentteams-docker-proxy >/dev/null 2>&1 || true\n fi\n\n # Stop and remove the embedded controller container. MUST happen\n # before the `docker volume rm agentteams-data` step below — in embedded\n # mode agentteams-controller mounts agentteams-data at /data (Tuwunel DB,\n # MinIO state, Higress state, all the room messages), and `volume\n # rm` against an in-use volume fails silently because of the trailing\n # `|| true`. Skipping this used to leave room/message history behind\n # across \"uninstall + reinstall\" cycles. See PR #692.\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -q \"^agentteams-controller$\"; then\n log \"$(msg uninstall.stopping_controller)\"\n ${DOCKER_CMD} stop agentteams-controller >/dev/null 2>&1 || true\n ${DOCKER_CMD} rm agentteams-controller >/dev/null 2>&1 || true\n fi\n\n # Read env file for data/workspace info before removing\n local env_file=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n [ ! -f \"${env_file}\" ] && [ -f \"./agentteams-manager.env\" ] && env_file=\"./agentteams-manager.env\"\n\n local data_dir=\"\" workspace_dir=\"\"\n if [ -f \"${env_file}\" ]; then\n data_dir=$(grep '^AGENTTEAMS_DATA_DIR=' \"${env_file}\" 2>/dev/null | cut -d= -f2- || true)\n workspace_dir=$(grep '^AGENTTEAMS_WORKSPACE_DIR=' \"${env_file}\" 2>/dev/null | cut -d= -f2- || true)\n fi\n\n # Remove Docker volume\n local vol=\"${data_dir:-agentteams-data}\"\n if ${DOCKER_CMD} volume inspect \"${vol}\" >/dev/null 2>&1; then\n log \"$(msg uninstall.removing_volume \"${vol}\")\"\n ${DOCKER_CMD} volume rm \"${vol}\" >/dev/null 2>&1 || true\n fi\n\n # Remove Docker network\n if ${DOCKER_CMD} network ls --format '{{.Name}}' 2>/dev/null | grep -q \"^agentteams-net$\"; then\n log \"$(msg uninstall.removing_network)\"\n ${DOCKER_CMD} network rm agentteams-net >/dev/null 2>&1 || true\n fi\n\n # Remove workspace directory\n if [ -n \"${workspace_dir}\" ] && [ -d \"${workspace_dir}\" ]; then\n log \"$(msg uninstall.removing_workspace \"${workspace_dir}\")\"\n rm -rf \"${workspace_dir}\" 2>/dev/null || true\n if [ -d \"${workspace_dir}\" ]; then\n log \"$(msg uninstall.removing_workspace_elevated)\"\n local _parent _base\n _parent=$(dirname \"${workspace_dir}\")\n _base=$(basename \"${workspace_dir}\")\n local _rm_image=\"${manager_image:-busybox}\"\n ${DOCKER_CMD} run --rm --entrypoint sh \\\n -v \"${_parent}:/host-parent\" \\\n \"${_rm_image}\" \\\n -c \"rm -rf /host-parent/${_base}\" 2>/dev/null || true\n fi\n fi\n\n # Remove env file\n if [ -f \"${env_file}\" ]; then\n log \"$(msg uninstall.removing_env \"${env_file}\")\"\n rm -f \"${env_file}\"\n fi\n\n # Remove install log\n if [ -f \"${HOME}/agentteams-install.log\" ]; then\n log \"$(msg uninstall.removing_log \"${HOME}/agentteams-install.log\")\"\n rm -f \"${HOME}/agentteams-install.log\"\n fi\n\n echo \"\"\n log \"$(msg uninstall.done)\"\n}\n\n# ============================================================\n# Check container runtime (docker or podman) and environment\n# ============================================================\ncheck_container_runtime() {\n # Ensure the rootless environment baseline (XDG_RUNTIME_DIR) is ready before engine detection.\n if [ \"$(id -u)\" -ne 0 ] && [ -z \"${XDG_RUNTIME_DIR:-}\" ]; then\n export XDG_RUNTIME_DIR=\"/run/user/$(id -u)\"\n fi\n\n if command -v docker >/dev/null 2>&1; then\n DOCKER_CMD=\"docker\"\n # Check for podman disguised as docker (alias or podman-docker package)\n if docker --version 2>/dev/null | grep -qi \"podman\"; then\n DOCKER_CMD=\"podman\"\n fi\n elif command -v podman >/dev/null 2>&1; then\n DOCKER_CMD=\"podman\"\n else\n echo -e \"\\033[31m[AgentTeams ERROR]\\033[0m $(msg error.docker_not_found)\" >&2\n exit 1\n fi\n\n # Unified health check (Validates runtime responsiveness)\n if ! ${DOCKER_CMD} ps >/dev/null 2>&1; then\n echo -e \"\\033[31m[AgentTeams ERROR]\\033[0m $(msg error.docker_not_running)\" >&2\n exit 1\n fi\n}\n\ncheck_container_runtime\n\ncase \"${1:-}\" in\n manager|\"\")\n install_manager\n ;;\n worker)\n shift\n install_worker \"$@\"\n ;;\n dashboard)\n check_container_runtime\n load_current_params_from_env\n AGENTTEAMS_DASHBOARD=\"${AGENTTEAMS_DASHBOARD:-1}\"\n AGENTTEAMS_DASHBOARD_VERSION=\"${AGENTTEAMS_DASHBOARD_VERSION:-v1.2.0-beta.1}\"\n AGENTTEAMS_PORT_DASHBOARD=\"${AGENTTEAMS_PORT_DASHBOARD:-13000}\"\n AGENTTEAMS_DASHBOARD_IMAGE=\"${AGENTTEAMS_DASHBOARD_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-dashboard:${AGENTTEAMS_DASHBOARD_VERSION}}\"\n AGENTTEAMS_USE_EMBEDDED=1\n _start_dashboard\n ;;\n uninstall)\n uninstall_agentteams\n ;;\n *)\n echo \"Usage: $0 [manager|worker [options]|dashboard|uninstall]\"\n echo \"\"\n echo \"Commands:\"\n echo \" manager Interactive Manager installation (default)\"\n echo \" Choose Quick Start (all defaults) or Manual mode\"\n echo \" worker Worker installation (requires --name and connection params)\"\n echo \" dashboard Start/stop agentteams-dashboard (use with AGENTTEAMS_DASHBOARD=0 to stop)\"\n echo \" uninstall Stop and remove Manager + all Worker containers\"\n echo \"\"\n echo \"Quick Start (fastest):\"\n echo \" $0\"\n echo \" # Then select '1' for Quick Start mode\"\n echo \"\"\n echo \"Non-interactive (for automation):\"\n echo \" AGENTTEAMS_NON_INTERACTIVE=1 AGENTTEAMS_LLM_API_KEY=sk-xxx $0\"\n echo \"\"\n echo \"Worker Options:\"\n echo \" --name <name> Worker name (required)\"\n echo \" --fs <url> MinIO endpoint URL (required)\"\n echo \" --fs-key <key> MinIO access key (required)\"\n echo \" --fs-secret <secret> MinIO secret key (required)\"\n echo \" --reset Remove existing Worker container before creating\"\n exit 1\n ;;\nesac\n"}, "785c2db56a02c0635a66bba490ad0f6f327c790a": {"t": 1785764745.8078957, "n": 244718, "h": "992eb531027a06cc", "c": "#!/bin/bash\n# agentteams-install.sh - One-click installation for AgentTeams Manager, Worker, and Dashboard\n#\n# Usage:\n# ./agentteams-install.sh # Interactive installation (choose Quick Start or Manual)\n# ./agentteams-install.sh manager # Same as above (explicit)\n# ./agentteams-install.sh worker --name <name> ... # Worker installation\n# ./agentteams-install.sh dashboard # Start/stop agentteams-dashboard (AGENTTEAMS_DASHBOARD=0 to stop)\n# ./agentteams-install.sh uninstall # Stop and remove Manager + all Workers\n#\n# NOTE: agentteams-dashboard is currently only supported via the Bash installer.\n# The PowerShell installer (agentteams-install.ps1) does not yet include dashboard support.\n#\n# Onboarding Modes:\n# Quick Start - Fast installation with all default values (recommended)\n# Manual - Customize each option step by step\n#\n# Environment variables (for automation):\n# AGENTTEAMS_NON_INTERACTIVE Skip all prompts, use defaults (default: 0)\n# AGENTTEAMS_LLM_PROVIDER LLM provider (default: openai-compat for zh non-interactive Token Plan; qwen for en)\n# AGENTTEAMS_DEFAULT_MODEL Default model (default: qwen3.6-plus for zh Token Plan and en non-interactive)\n# AGENTTEAMS_OPENAI_BASE_URL OpenAI-compatible base URL (default for zh non-interactive: Alibaba Token Plan endpoint)\n# AGENTTEAMS_LLM_API_KEY LLM API key (required)\n# AGENTTEAMS_ADMIN_USER Admin username (default: admin)\n# AGENTTEAMS_ADMIN_PASSWORD Admin password (auto-generated if not set, min 8 chars)\n# AGENTTEAMS_MATRIX_DOMAIN Matrix domain (default: matrix-local.agentteams.io:18080)\n# AGENTTEAMS_MOUNT_SOCKET Mount container runtime socket (default: 1)\n# AGENTTEAMS_DATA_DIR Docker volume name for persistent data (default: agentteams-data)\n# AGENTTEAMS_WORKSPACE_DIR Host directory for manager workspace (default: ~/agentteams-manager)\n# AGENTTEAMS_VERSION Image tag (default: latest)\n# AGENTTEAMS_REGISTRY Image registry (default: auto-detected by timezone)\n# AGENTTEAMS_INSTALL_MANAGER_IMAGE Override manager image (e.g., local build)\n# AGENTTEAMS_INSTALL_WORKER_IMAGE Override worker image (e.g., local build)\n# AGENTTEAMS_INSTALL_COPAW_WORKER_IMAGE Override copaw worker image (e.g., local build)\n# AGENTTEAMS_INSTALL_HERMES_WORKER_IMAGE Override hermes worker image (e.g., local build)\n# AGENTTEAMS_NACOS_REGISTRY_URI Default Nacos registry URI for Worker market search/import\n# (default: nacos://market.agentteams.io:80/public)\n# AGENTTEAMS_NACOS_USERNAME Default Nacos username for nacos:// package imports (optional)\n# AGENTTEAMS_NACOS_PASSWORD Default Nacos password for nacos:// package imports (optional)\n# AGENTTEAMS_CMS_TRACES_ENABLED Enable openclaw-cms-plugin traces for Manager AND all Workers (default: false)\n# AGENTTEAMS_CMS_ENDPOINT ARMS OTLP endpoint (required if traces enabled)\n# AGENTTEAMS_CMS_LICENSE_KEY CMS license key (required if traces enabled)\n# AGENTTEAMS_CMS_PROJECT CMS project name (optional)\n# AGENTTEAMS_CMS_WORKSPACE CMS workspace ID (required if traces enabled)\n# AGENTTEAMS_CMS_SERVICE_NAME Manager service name in ARMS (default: agentteams-manager)\n# Workers always report as agentteams-worker-<name> automatically\n# AGENTTEAMS_CMS_METRICS_ENABLED Enable diagnostics-otel metrics for Manager AND all Workers (default: false)\n# AGENTTEAMS_PORT_GATEWAY Host port for Higress gateway (default: 18080)\n# AGENTTEAMS_PORT_CONSOLE Host port for Higress console (default: 18001)\n# AGENTTEAMS_PORT_ELEMENT_WEB Host port for Element Web direct access (default: 18088)\n# AGENTTEAMS_PORT_MANAGER_CONSOLE Host port for Manager console (default: 18888)\n# AGENTTEAMS_WORKER_IDLE_TIMEOUT Worker idle timeout in minutes (default: 720, i.e. 12 hours)\n# AGENTTEAMS_DASHBOARD Install agentteams-dashboard management UI (default: 1)\n# AGENTTEAMS_DASHBOARD_VERSION Dashboard version (default: v1.2.0-beta.1, independent of AgentTeams version)\n# AGENTTEAMS_PORT_DASHBOARD Dashboard host port (default: 13000)\n# AGENTTEAMS_DASHBOARD_IMAGE Override dashboard image (default: <registry>/agentteams/agentteams-dashboard:<DASHBOARD_VERSION>)\n# AGENTTEAMS_AI_GATEWAY_ADMIN_URL Higress Console URL for shared auth (auto-detected)\n\nset -e\n\nAGENTTEAMS_VERSION=\"${AGENTTEAMS_VERSION:-}\"\nAGENTTEAMS_KNOWN_STABLE_VERSION=\"v1.1.2\" # fallback if GitHub API is unreachable\n\n_normalize_version() {\n local version=\"$1\"\n case \"${version}\" in\n [0-9]*.[0-9]*.[0-9]*) version=\"v${version}\" ;;\n esac\n case \"${version}\" in\n v[0-9]*.[0-9]*.[0-9]*.beta.[0-9]*) version=\"${version/.beta./-beta.}\" ;;\n esac\n printf '%s' \"${version}\"\n}\n\n# Returns 0 (true) if $1 < $2 using semver order; \"latest\" is treated as greatest\n_ver_lt() {\n [ \"$1\" = \"latest\" ] && return 1\n [ \"$2\" = \"latest\" ] && return 0\n [ \"$1\" = \"$2\" ] && return 1\n [ \"$(printf '%s\\n%s' \"$1\" \"$2\" | sort -V | head -1)\" = \"$1\" ]\n}\n\n_use_legacy_image_env() {\n if [ \"$1\" = \"latest\" ]; then\n _ver_lt \"${AGENTTEAMS_KNOWN_STABLE_VERSION}\" \"v1.2.0\"\n else\n _ver_lt \"$1\" \"v1.2.0\"\n fi\n}\n\n_controller_env_prefix() {\n if _use_legacy_image_env \"$1\"; then\n printf '%s%s' 'HIC' 'LAW_'\n else\n printf '%s' 'AGENTTEAMS_'\n fi\n}\nAGENTTEAMS_NON_INTERACTIVE=\"${AGENTTEAMS_NON_INTERACTIVE:-0}\"\nAGENTTEAMS_MOUNT_SOCKET=\"${AGENTTEAMS_MOUNT_SOCKET:-1}\"\nAGENTTEAMS_DOCKER_PROXY=\"${AGENTTEAMS_DOCKER_PROXY:-1}\"\nSTEP_RESULT=\"\" # Used by state machine to signal \"back\" navigation\n\n# ============================================================\n# Log all output to file\n# ============================================================\n\nAGENTTEAMS_LOG_FILE=\"${HOME}/agentteams-install.log\"\n\nif [ \"${1:-}\" != \"uninstall\" ]; then\n # Redirect all output (stdout and stderr) to both terminal and log file\n exec > >(tee -a \"${AGENTTEAMS_LOG_FILE}\") 2>&1\n\n echo \"\"\n echo \"========================================\"\n echo \"AgentTeams Installation Log\"\n echo \"Started: $(date)\"\n echo \"User: $(whoami)\"\n echo \"System: $(uname -a)\"\n echo \"Log file: ${AGENTTEAMS_LOG_FILE}\"\n echo \"========================================\"\n echo \"\"\nfi\n\n# ============================================================\n# Utility functions (needed early for timezone detection)\n# ============================================================\n\nlog() {\n echo -e \"\\033[36m[AgentTeams]\\033[0m $1\"\n}\n\nerror() {\n echo -e \"\\033[31m[AgentTeams ERROR]\\033[0m $1\" >&2\n}\n\ndie() {\n error \"$1\"\n exit 1\n}\n\n# ============================================================\n# Timezone detection (compatible with Linux and macOS)\n# ============================================================\n\ndetect_timezone() {\n local tz=\"\"\n\n # Try /etc/timezone (Debian/Ubuntu)\n if [ -f /etc/timezone ]; then\n tz=$(cat /etc/timezone 2>/dev/null | tr -d '[:space:]')\n fi\n\n # Try /etc/localtime symlink (macOS and some Linux)\n if [ -z \"${tz}\" ] && [ -L /etc/localtime ]; then\n tz=$(ls -l /etc/localtime 2>/dev/null | sed 's|.*/zoneinfo/||')\n fi\n\n # Try timedatectl (systemd)\n if [ -z \"${tz}\" ]; then\n tz=$(timedatectl show --value -p Timezone 2>/dev/null)\n fi\n\n # If still not detected, warn and prompt user. Diagnostics go to stderr —\n # this function returns its value via stdout to `$(detect_timezone)`, so\n # any echo here would leak into AGENTTEAMS_TIMEZONE and downstream image tags.\n if [ -z \"${tz}\" ]; then\n echo \"\" >&2\n echo -e \"\\033[33m[AgentTeams WARNING]\\033[0m Could not detect timezone automatically.\" >&2\n echo -e \"\\033[33m[AgentTeams]\\033[0m Please enter your timezone (e.g., Asia/Shanghai, America/New_York).\" >&2\n echo \"\" >&2\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n tz=\"Asia/Shanghai\"\n log \"Using default timezone: ${tz}\" >&2\n else\n read -e -p \"Timezone [Asia/Shanghai]: \" tz\n tz=\"${tz:-Asia/Shanghai}\"\n fi\n fi\n\n echo \"${tz}\"\n}\n\n# Detect timezone once at startup (used by registry selection and container TZ)\nAGENTTEAMS_TIMEZONE=\"${AGENTTEAMS_TIMEZONE:-$(detect_timezone)}\"\n\n# ============================================================\n# Language detection based on timezone\n# ============================================================\n\ndetect_language() {\n local tz=\"${AGENTTEAMS_TIMEZONE}\"\n case \"${tz}\" in\n Asia/Shanghai|Asia/Chongqing|Asia/Harbin|Asia/Urumqi|\\\n Asia/Taipei|Asia/Hong_Kong|Asia/Macau)\n echo \"zh\"\n ;;\n *)\n echo \"en\"\n ;;\n esac\n}\n\n# Language priority: env var > existing env file > timezone detection\nif [ -z \"${AGENTTEAMS_LANGUAGE}\" ]; then\n # Check existing env file for saved language preference (upgrade scenario)\n _env_file=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n # Migrate from legacy location (current directory) if needed\n if [ ! -f \"${_env_file}\" ] && [ -f \"./agentteams-manager.env\" ]; then\n mv \"./agentteams-manager.env\" \"${_env_file}\" 2>/dev/null || true\n fi\n if [ -f \"${_env_file}\" ]; then\n _saved_lang=$(grep '^AGENTTEAMS_LANGUAGE=' \"${_env_file}\" 2>/dev/null | cut -d= -f2-)\n if [ -n \"${_saved_lang}\" ]; then\n AGENTTEAMS_LANGUAGE=\"${_saved_lang}\"\n fi\n fi\n # Fall back to timezone-based detection\n if [ -z \"${AGENTTEAMS_LANGUAGE}\" ]; then\n AGENTTEAMS_LANGUAGE=\"$(detect_language)\"\n fi\n unset _env_file _saved_lang\nfi\nexport AGENTTEAMS_LANGUAGE\n\n# ============================================================\n# Centralized message dictionary and msg() function\n# Compatible with bash 3.2+ (macOS default) — uses case instead of declare -A\n# ============================================================\n\n# msg() function: look up message by key, with printf-style argument substitution\n# Falls back to English if the current language translation is missing.\nmsg() {\n local key=\"$1\"\n shift\n local lang=\"${AGENTTEAMS_LANGUAGE:-en}\"\n local text=\"\"\n case \"${key}.${lang}\" in\n # --- Timezone detection messages ---\n \"tz.warning.title.zh\") text=\"无法自动检测时区。\" ;;\n \"tz.warning.title.en\") text=\"Could not detect timezone automatically.\" ;;\n \"tz.warning.prompt.zh\") text=\"请输入您的时区(例如 Asia/Shanghai、America/New_York)。\" ;;\n \"tz.warning.prompt.en\") text=\"Please enter your timezone (e.g., Asia/Shanghai, America/New_York).\" ;;\n \"tz.default.zh\") text=\"使用默认时区: %s\" ;;\n \"tz.default.en\") text=\"Using default timezone: %s\" ;;\n \"tz.input_prompt.zh\") text=\"时区\" ;;\n \"tz.input_prompt.en\") text=\"Timezone\" ;;\n # --- Installation title and info ---\n \"install.title.zh\") text=\"=== AgentTeams Manager 安装 ===\" ;;\n \"install.title.en\") text=\"=== AgentTeams Manager Installation ===\" ;;\n \"install.registry.zh\") text=\"镜像仓库: %s\" ;;\n \"install.registry.en\") text=\"Registry: %s\" ;;\n \"install.dir.zh\") text=\"安装目录: %s\" ;;\n \"install.dir.en\") text=\"Installation directory: %s\" ;;\n \"install.dir_hint.zh\") text=\" (env 文件 'agentteams-manager.env' 将保存到 HOME 目录。)\" ;;\n \"install.dir_hint.en\") text=\" (The env file 'agentteams-manager.env' will be saved to your HOME directory.)\" ;;\n \"install.dir_hint2.zh\") text=\" (请从您希望管理此安装的目录运行此脚本。)\" ;;\n \"install.dir_hint2.en\") text=\" (Run this script from the directory where you want to manage this installation.)\" ;;\n # --- Onboarding mode ---\n \"install.mode.title.zh\") text=\"--- Onboarding 模式 ---\" ;;\n \"install.mode.title.en\") text=\"--- Onboarding Mode ---\" ;;\n \"install.mode.choose.zh\") text=\"选择安装模式:\" ;;\n \"install.mode.choose.en\") text=\"Choose your installation mode:\" ;;\n \"install.mode.quickstart.zh\") text=\" 1) 快速开始 - 使用阿里云通义 Token 套餐快速安装(推荐)\" ;;\n \"install.mode.quickstart.en\") text=\" 1) Quick Start - Fast installation with Qwen Cloud (recommended)\" ;;\n \"install.mode.manual.zh\") text=\" 2) 手动配置 - 选择 LLM 提供商并自定义选项\" ;;\n \"install.mode.manual.en\") text=\" 2) Manual - Choose LLM provider and customize options\" ;;\n \"install.mode.prompt.zh\") text=\"请选择 [1/2]\" ;;\n \"install.mode.prompt.en\") text=\"Enter choice [1/2]\" ;;\n \"install.mode.quickstart_selected.zh\") text=\"已选择快速开始模式 - 使用阿里云通义 Token 套餐\" ;;\n \"install.mode.quickstart_selected.en\") text=\"Quick Start mode selected - using Qwen Cloud\" ;;\n \"install.mode.manual_selected.zh\") text=\"已选择手动配置模式 - 您将选择 LLM 提供商并自定义选项\" ;;\n \"install.mode.manual_selected.en\") text=\"Manual mode selected - you will choose LLM provider and customize options\" ;;\n \"install.mode.invalid.zh\") text=\"无效选择,默认使用快速开始模式\" ;;\n \"install.mode.invalid.en\") text=\"Invalid choice, defaulting to Quick Start mode\" ;;\n # --- Upgrade mode sub-menu ---\n \"upgrade.mode.prompt.zh\") text=\"请选择升级方式:\" ;;\n \"upgrade.mode.prompt.en\") text=\"Select upgrade mode:\" ;;\n \"upgrade.mode.keep_all.zh\") text=\" 1) 保留所有参数,一键升级(推荐)\" ;;\n \"upgrade.mode.keep_all.en\") text=\" 1) Keep all parameters, quick upgrade (recommended)\" ;;\n \"upgrade.mode.confirm_each.zh\") text=\" 2) 逐个确认参数(可修改)\" ;;\n \"upgrade.mode.confirm_each.en\") text=\" 2) Confirm each parameter (can modify)\" ;;\n \"upgrade.mode.back.zh\") text=\" 3) 返回\" ;;\n \"upgrade.mode.back.en\") text=\" 3) Back\" ;;\n # --- Version selection ---\n \"install.version.title.zh\") text=\"--- 版本选择 ---\" ;;\n \"install.version.title.en\") text=\"--- Version Selection ---\" ;;\n \"install.version.choose.zh\") text=\"选择要安装的版本:\" ;;\n \"install.version.choose.en\") text=\"Choose the version to install:\" ;;\n \"install.version.option_latest.zh\") text=\" 1) latest - 最新版(默认)\" ;;\n \"install.version.option_latest.en\") text=\" 1) latest - Latest build (default)\" ;;\n \"install.version.option_stable.zh\") text=\" 2) %s - 最新稳定版\" ;;\n \"install.version.option_stable.en\") text=\" 2) %s - Latest stable release\" ;;\n \"install.version.fetching.zh\") text=\"正在查询最新稳定版本...\" ;;\n \"install.version.fetching.en\") text=\"Fetching latest stable release...\" ;;\n \"install.version.fetch_failed.zh\") text=\"无法查询 GitHub,使用内置版本 %s\" ;;\n \"install.version.fetch_failed.en\") text=\"Could not reach GitHub, using built-in version %s\" ;;\n \"install.version.option_custom.zh\") text=\" 3) 自定义 - 手动输入版本号(如 v1.0.5)\" ;;\n \"install.version.option_custom.en\") text=\" 3) Custom - Enter a specific version (e.g. v1.0.5)\" ;;\n \"install.version.prompt.zh\") text=\"请选择 [1/2/3]\" ;;\n \"install.version.prompt.en\") text=\"Enter choice [1/2/3]\" ;;\n \"install.version.custom_prompt.zh\") text=\"请输入版本号\" ;;\n \"install.version.custom_prompt.en\") text=\"Enter version tag\" ;;\n \"install.version.selected_latest.zh\") text=\"已选择最新版 (latest)\" ;;\n \"install.version.selected_latest.en\") text=\"Selected latest version\" ;;\n \"install.version.selected_stable.zh\") text=\"已选择最新稳定版 (%s)\" ;;\n \"install.version.selected_stable.en\") text=\"Selected latest stable version (%s)\" ;;\n \"install.version.selected_custom.zh\") text=\"已选择自定义版本 (%s)\" ;;\n \"install.version.selected_custom.en\") text=\"Selected custom version (%s)\" ;;\n \"install.version.invalid.zh\") text=\"无效选择,使用最新稳定版 (%s)\" ;;\n \"install.version.invalid.en\") text=\"Invalid choice, defaulting to latest stable version (%s)\" ;;\n # --- Existing installation detected ---\n \"install.existing.detected.zh\") text=\"检测到已有 Manager 安装(env 文件: %s)\" ;;\n \"install.existing.detected.en\") text=\"Existing Manager installation detected (env file: %s)\" ;;\n \"install.existing.choose.zh\") text=\"选择操作:\" ;;\n \"install.existing.choose.en\") text=\"Choose an action:\" ;;\n \"install.existing.upgrade.zh\") text=\" 1) 就地升级(保留数据、工作空间、env 文件)\" ;;\n \"install.existing.upgrade.en\") text=\" 1) In-place upgrade (keep data, workspace, env file)\" ;;\n \"install.existing.reinstall.zh\") text=\" 2) 全新重装(删除所有数据,重新开始)\" ;;\n \"install.existing.reinstall.en\") text=\" 2) Clean reinstall (remove all data, start fresh)\" ;;\n \"install.existing.cancel.zh\") text=\" 3) 取消\" ;;\n \"install.existing.cancel.en\") text=\" 3) Cancel\" ;;\n \"install.existing.prompt.zh\") text=\"请选择 [1/2/3]\" ;;\n \"install.existing.prompt.en\") text=\"Enter choice [1/2/3]\" ;;\n \"install.existing.upgrade_noninteractive.zh\") text=\"非交互模式: 执行就地升级...\" ;;\n \"install.existing.upgrade_noninteractive.en\") text=\"Non-interactive mode: performing in-place upgrade...\" ;;\n \"install.existing.upgrading.zh\") text=\"执行就地升级...\" ;;\n \"install.existing.upgrading.en\") text=\"Performing in-place upgrade...\" ;;\n \"install.existing.warn_manager_stop.zh\") text=\"⚠️ Manager 容器将被停止并重新创建。\" ;;\n \"install.existing.warn_manager_stop.en\") text=\"⚠️ Manager container will be stopped and recreated.\" ;;\n \"install.existing.warn_worker_recreate.zh\") text=\"⚠️ Worker 容器也将被重新创建(以更新 Manager IP)。\" ;;\n \"install.existing.warn_worker_recreate.en\") text=\"⚠️ Worker containers will also be recreated (to update Manager IP in hosts).\" ;;\n \"install.existing.continue_prompt.zh\") text=\"继续?[y/N]\" ;;\n \"install.existing.continue_prompt.en\") text=\"Continue? [y/N]\" ;;\n \"install.existing.cancelled.zh\") text=\"安装已取消。\" ;;\n \"install.existing.cancelled.en\") text=\"Installation cancelled.\" ;;\n \"install.existing.stopping_manager.zh\") text=\"停止并移除现有 manager 容器...\" ;;\n \"install.existing.stopping_manager.en\") text=\"Stopping and removing existing manager container...\" ;;\n \"install.existing.stopping_workers.zh\") text=\"停止并移除现有 worker 容器...\" ;;\n \"install.existing.stopping_workers.en\") text=\"Stopping and removing existing worker containers...\" ;;\n \"install.existing.removed.zh\") text=\" 已移除: %s\" ;;\n \"install.existing.removed.en\") text=\" Removed: %s\" ;;\n # --- Clean reinstall messages ---\n \"install.reinstall.performing.zh\") text=\"执行全新重装...\" ;;\n \"install.reinstall.performing.en\") text=\"Performing clean reinstall...\" ;;\n \"install.reinstall.warn_stop.zh\") text=\"⚠️ 以下运行中的容器将被停止:\" ;;\n \"install.reinstall.warn_stop.en\") text=\"⚠️ The following running containers will be stopped:\" ;;\n \"install.reinstall.warn_delete.zh\") text=\"⚠️ 警告: 以下内容将被删除:\" ;;\n \"install.reinstall.warn_delete.en\") text=\"⚠️ WARNING: This will DELETE the following:\" ;;\n \"install.reinstall.warn_volume.zh\") text=\" - Docker 卷: agentteams-data\" ;;\n \"install.reinstall.warn_volume.en\") text=\" - Docker volume: agentteams-data\" ;;\n \"install.reinstall.warn_env.zh\") text=\" - Env 文件: %s\" ;;\n \"install.reinstall.warn_env.en\") text=\" - Env file: %s\" ;;\n \"install.reinstall.warn_workspace.zh\") text=\" - Manager 工作空间: %s\" ;;\n \"install.reinstall.warn_workspace.en\") text=\" - Manager workspace: %s\" ;;\n \"install.reinstall.warn_workers.zh\") text=\" - 所有 worker 容器\" ;;\n \"install.reinstall.warn_workers.en\") text=\" - All worker containers\" ;;\n \"install.reinstall.warn_proxy.zh\") text=\" - Docker API 代理容器: agentteams-controller\" ;;\n \"install.reinstall.warn_proxy.en\") text=\" - Docker API proxy container: agentteams-controller\" ;;\n \"install.reinstall.warn_network.zh\") text=\" - Docker 网络: agentteams-net\" ;;\n \"install.reinstall.warn_network.en\") text=\" - Docker network: agentteams-net\" ;;\n \"install.reinstall.confirm_type.zh\") text=\"请输入工作空间路径以确认删除(或按 Ctrl+C 取消):\" ;;\n \"install.reinstall.confirm_type.en\") text=\"To confirm deletion, please type the workspace path:\" ;;\n \"install.reinstall.confirm_path.zh\") text=\"输入路径以确认(或按 Ctrl+C 取消)\" ;;\n \"install.reinstall.confirm_path.en\") text=\"Type the path to confirm (or press Ctrl+C to cancel)\" ;;\n \"install.reinstall.path_mismatch.zh\") text=\"路径不匹配。中止重装。输入: '%s',期望: '%s'\" ;;\n \"install.reinstall.path_mismatch.en\") text=\"Path mismatch. Aborting reinstall. Input: '%s', Expected: '%s'\" ;;\n \"install.reinstall.confirmed.zh\") text=\"已确认。正在清理...\" ;;\n \"install.reinstall.confirmed.en\") text=\"Confirmed. Cleaning up...\" ;;\n \"install.reinstall.removed_worker.zh\") text=\" 已移除 worker: %s\" ;;\n \"install.reinstall.removed_worker.en\") text=\" Removed worker: %s\" ;;\n \"install.reinstall.removing_volume.zh\") text=\"正在移除 Docker 卷: agentteams-data\" ;;\n \"install.reinstall.removing_volume.en\") text=\"Removing Docker volume: agentteams-data\" ;;\n \"install.reinstall.warn_volume_fail.zh\") text=\" 警告: 无法移除卷(可能有引用)\" ;;\n \"install.reinstall.warn_volume_fail.en\") text=\" Warning: Could not remove volume (may have references)\" ;;\n \"install.reinstall.removing_proxy.zh\") text=\"正在移除 Docker API 代理容器: agentteams-controller\" ;;\n \"install.reinstall.removing_proxy.en\") text=\"Removing Docker API proxy container: agentteams-controller\" ;;\n \"install.reinstall.removing_network.zh\") text=\"正在移除 Docker 网络: agentteams-net\" ;;\n \"install.reinstall.removing_network.en\") text=\"Removing Docker network: agentteams-net\" ;;\n \"install.reinstall.removing_workspace.zh\") text=\"正在移除工作空间目录: %s\" ;;\n \"install.reinstall.removing_workspace.en\") text=\"Removing workspace directory: %s\" ;;\n \"install.reinstall.removing_env.zh\") text=\"正在移除 env 文件: %s\" ;;\n \"install.reinstall.removing_env.en\") text=\"Removing env file: %s\" ;;\n \"install.reinstall.cleanup_done.zh\") text=\"清理完成。开始全新安装...\" ;;\n \"install.reinstall.cleanup_done.en\") text=\"Cleanup complete. Starting fresh installation...\" ;;\n \"install.reinstall.failed_rm_workspace.zh\") text=\"无法移除工作空间目录\" ;;\n \"install.reinstall.failed_rm_workspace.en\") text=\"Failed to remove workspace directory\" ;;\n # --- Orphan volume detection ---\n \"install.orphan_volume.detected.zh\") text=\"⚠️ 检测到残留数据卷 '%s',但未找到对应的 env 配置文件。\" ;;\n \"install.orphan_volume.detected.en\") text=\"⚠️ Found leftover data volume '%s' but no matching env config file.\" ;;\n \"install.orphan_volume.warn.zh\") text=\"这可能是之前安装的残留数据,会导致新安装出现异常(如密码冲突、服务启动失败)。\" ;;\n \"install.orphan_volume.warn.en\") text=\"This is likely leftover data from a previous installation and may cause issues (e.g., credential conflicts, service startup failures).\" ;;\n \"install.orphan_volume.choose.zh\") text=\"选择操作:\" ;;\n \"install.orphan_volume.choose.en\") text=\"Choose an action:\" ;;\n \"install.orphan_volume.clean.zh\") text=\" 1) 清理残留数据卷后继续安装(推荐)\" ;;\n \"install.orphan_volume.clean.en\") text=\" 1) Remove leftover volume and continue installation (recommended)\" ;;\n \"install.orphan_volume.keep.zh\") text=\" 2) 保留数据卷继续安装(可能出现异常)\" ;;\n \"install.orphan_volume.keep.en\") text=\" 2) Keep the volume and continue installation (may cause issues)\" ;;\n \"install.orphan_volume.prompt.zh\") text=\"请选择 [1/2]\" ;;\n \"install.orphan_volume.prompt.en\") text=\"Enter choice [1/2]\" ;;\n \"install.orphan_volume.cleaning.zh\") text=\"正在清理残留数据卷...\" ;;\n \"install.orphan_volume.cleaning.en\") text=\"Removing leftover data volume...\" ;;\n \"install.orphan_volume.cleaned.zh\") text=\"残留数据卷已清理。继续全新安装...\" ;;\n \"install.orphan_volume.cleaned.en\") text=\"Leftover volume removed. Continuing with fresh installation...\" ;;\n \"install.orphan_volume.keeping.zh\") text=\"保留数据卷,继续安装。如遇异常请选择全新重装。\" ;;\n \"install.orphan_volume.keeping.en\") text=\"Keeping existing volume. If you encounter issues, consider a clean reinstall.\" ;;\n \"install.orphan_volume.clean_noninteractive.zh\") text=\"非交互模式: 自动清理残留数据卷...\" ;;\n \"install.orphan_volume.clean_noninteractive.en\") text=\"Non-interactive mode: automatically removing leftover volume...\" ;;\n # --- Loading existing config ---\n \"install.loading_config.zh\") text=\"从 %s 加载已有配置(shell 环境变量优先)...\" ;;\n \"install.loading_config.en\") text=\"Loading existing config from %s (shell env vars take priority)...\" ;;\n # --- LLM Configuration ---\n \"llm.title.zh\") text=\"--- LLM 配置 ---\" ;;\n \"llm.title.en\") text=\"--- LLM Configuration ---\" ;;\n \"llm.provider.label.zh\") text=\" 提供商: %s\" ;;\n \"llm.provider.label.en\") text=\" Provider: %s\" ;;\n \"llm.model.label.zh\") text=\" 模型: %s\" ;;\n \"llm.model.label.en\") text=\" Model: %s\" ;;\n \"llm.provider.qwen.zh\") text=\" 提供商: qwen(阿里云百炼)\" ;;\n \"llm.provider.qwen.en\") text=\" Provider: qwen (Alibaba Cloud Bailian)\" ;;\n \"llm.provider.qwen_default.zh\") text=\" 提供商: %s(默认)\" ;;\n \"llm.provider.qwen_default.en\") text=\" Provider: %s (default)\" ;;\n \"llm.model.default.zh\") text=\" 模型: %s(默认)\" ;;\n \"llm.model.default.en\") text=\" Model: %s (default)\" ;;\n \"llm.apikey_hint_bailian.zh\") text=\" 💡 获取阿里云百炼(DashScope)API Key:\" ;;\n \"llm.apikey_hint_bailian.en\") text=\" 💡 Get your Alibaba Cloud Bailian (DashScope) API Key:\" ;;\n \"llm.apikey_url_bailian.zh\") text=\" https://www.aliyun.com/product/bailian\" ;;\n \"llm.apikey_url_bailian.en\") text=\" https://www.aliyun.com/product/bailian\" ;;\n \"llm.apikey_hint_qwencloud.zh\") text=\" 💡 从 Qwen Cloud(国际站)获取 DASHSCOPE_API_KEY:\" ;;\n \"llm.apikey_hint_qwencloud.en\") text=\" 💡 Get your DASHSCOPE_API_KEY for Qwen Cloud (international) from:\" ;;\n \"llm.apikey_url_qwencloud.zh\") text=\" https://home.qwencloud.com/api-keys (文档: https://docs.qwencloud.com/)\" ;;\n \"llm.apikey_url_qwencloud.en\") text=\" https://home.qwencloud.com/api-keys | Docs: https://docs.qwencloud.com/\" ;;\n \"llm.apikey_hint_tokenplan.zh\") text=\" 💡 获取 DashScope API Key 或开通通义 Token 套餐,请参考:\" ;;\n \"llm.apikey_hint_tokenplan.en\") text=\" 💡 Get your DashScope or Token Plan API key (Alibaba Model Studio):\" ;;\n \"llm.apikey_url_tokenplan.zh\") text=\" https://help.aliyun.com/zh/model-studio/token-plan-quickstart\" ;;\n \"llm.apikey_url_tokenplan.en\") text=\" https://common-buy.aliyun.com/token-plan/ | https://help.aliyun.com/zh/model-studio/token-plan-quickstart\" ;;\n \"llm.apikey_hint_codingplan.zh\") text=\" 💡 获取 DashScope API Key(Coding 套餐 / coding.dashscope 接口):\" ;;\n \"llm.apikey_hint_codingplan.en\") text=\" 💡 Get your DashScope API key for Coding Plan (coding.dashscope endpoint):\" ;;\n \"llm.apikey_url_codingplan.zh\") text=\" https://help.aliyun.com/zh/model-studio/get-api-key\" ;;\n \"llm.apikey_url_codingplan.en\") text=\" https://help.aliyun.com/zh/model-studio/get-api-key\" ;;\n \"llm.apikey_prompt.zh\") text=\"LLM API Key\" ;;\n \"llm.apikey_prompt.en\") text=\"LLM API Key\" ;;\n \"llm.providers_title.zh\") text=\"可用 LLM 提供商:\" ;;\n \"llm.providers_title.en\") text=\"Available LLM Providers:\" ;;\n \"llm.provider.alibaba.zh\") text=\" 1) 阿里云通义 Token 套餐 - 推荐中国用户使用\" ;;\n \"llm.provider.alibaba.en\") text=\" 1) Qwen Cloud - International (OpenAI-compatible API, recommended)\" ;;\n \"llm.provider.openai_compat.zh\") text=\" 2) OpenAI 兼容 API - 自定义 Base URL(OpenAI、DeepSeek 等)\" ;;\n \"llm.provider.openai_compat.en\") text=\" 2) OpenAI-compatible API - Custom Base URL (OpenAI, DeepSeek, etc.)\" ;;\n \"llm.provider.select.zh\") text=\"选择提供商 [1/2]\" ;;\n \"llm.provider.select.en\") text=\"Select provider [1/2]\" ;;\n \"llm.alibaba.models_title.zh\") text=\"选择阿里云模型接入方式:\" ;;\n \"llm.alibaba.models_title.en\") text=\"Select Alibaba Cloud model access:\" ;;\n \"llm.alibaba.model.tokenplan.zh\") text=\" 1) 阿里云通义 Token 套餐 - 兼容模式(推荐)\" ;;\n \"llm.alibaba.model.tokenplan.en\") text=\" 1) Alibaba Cloud Token Plan - compatible-mode (recommended)\" ;;\n \"llm.alibaba.model.bailian.zh\") text=\" 2) 阿里云百炼 - DashScope 通用兼容接口\" ;;\n \"llm.alibaba.model.bailian.en\") text=\" 2) Alibaba Cloud Bailian - DashScope compatible mode\" ;;\n \"llm.alibaba.model.codingplan_legacy.zh\") text=\" 3) 阿里云 Coding 套餐 - 旧版端点(兼容保留)\" ;;\n \"llm.alibaba.model.codingplan_legacy.en\") text=\" 3) Alibaba Cloud Coding Plan - legacy endpoint (backward compatible)\" ;;\n \"llm.alibaba.model.select.zh\") text=\"选择接入方式 [1/2/3]\" ;;\n \"llm.alibaba.model.select.en\") text=\"Select access option [1/2/3]\" ;;\n \"llm.alibaba.model.invalid.zh\") text=\"无效选择: %s(请输入 1、2 或 3)\" ;;\n \"llm.alibaba.model.invalid.en\") text=\"Invalid choice: %s (please enter 1, 2, or 3)\" ;;\n \"llm.codingplan.models_title.zh\") text=\"选择通义 Token 套餐默认模型:\" ;;\n \"llm.codingplan.models_title.en\") text=\"Select Qwen Cloud default model:\" ;;\n \"llm.codingplan.model.qwen36plus.zh\") text=\" 1) qwen3.6-plus - 千问 3.6(推荐)\" ;;\n \"llm.codingplan.model.qwen36plus.en\") text=\" 1) qwen3.6-plus - Qwen 3.6 (recommended)\" ;;\n \"llm.codingplan.model.glm5.zh\") text=\" 2) glm-5 - 智谱 GLM-5(编程推荐)\" ;;\n \"llm.codingplan.model.glm5.en\") text=\" 2) glm-5 - Zhipu GLM-5 (recommended for coding)\" ;;\n \"llm.codingplan.model.kimi.zh\") text=\" 3) kimi-k2.5 - Moonshot Kimi K2.5\" ;;\n \"llm.codingplan.model.kimi.en\") text=\" 3) kimi-k2.5 - Moonshot Kimi K2.5\" ;;\n \"llm.codingplan.model.minimax.zh\") text=\" 4) MiniMax-M2.5 - MiniMax M2.5\" ;;\n \"llm.codingplan.model.minimax.en\") text=\" 4) MiniMax-M2.5 - MiniMax M2.5\" ;;\n \"llm.codingplan.model.select.zh\") text=\"选择模型 [1/2/3/4]\" ;;\n \"llm.codingplan.model.select.en\") text=\"Select model [1/2/3/4]\" ;;\n \"llm.provider.selected_tokenplan.zh\") text=\" 提供商: 阿里云通义 Token 套餐(兼容模式)\" ;;\n \"llm.provider.selected_tokenplan.en\") text=\" Provider: Alibaba Cloud Token Plan (compatible mode)\" ;;\n \"llm.provider.selected_codingplan.zh\") text=\" 提供商: 阿里云通义 Token 套餐(alibaba-cloud)\" ;;\n \"llm.provider.selected_codingplan.en\") text=\" Provider: Qwen Cloud (international) (alibaba-cloud)\" ;;\n \"llm.provider.selected_codingplan_legacy.zh\") text=\" 提供商: 阿里云 Coding 套餐(coding.dashscope)\" ;;\n \"llm.provider.selected_codingplan_legacy.en\") text=\" Provider: Alibaba Cloud Coding Plan (coding.dashscope)\" ;;\n \"llm.provider.selected_qwen.zh\") text=\" 提供商: 阿里云百炼\" ;;\n \"llm.provider.selected_qwen.en\") text=\" Provider: Alibaba Cloud Bailian\" ;;\n \"llm.provider.selected_openai.zh\") text=\" 提供商: %s(OpenAI 兼容)\" ;;\n \"llm.provider.selected_openai.en\") text=\" Provider: %s (OpenAI-compatible)\" ;;\n \"llm.provider.invalid.zh\") text=\"无效选择: %s(请输入 1 或 2)\" ;;\n \"llm.provider.invalid.en\") text=\"Invalid choice: %s (please enter 1 or 2)\" ;;\n \"llm.qwen.model_prompt.zh\") text=\"默认模型 ID\" ;;\n \"llm.qwen.model_prompt.en\") text=\"Default Model ID\" ;;\n \"llm.openai.base_url_prompt.zh\") text=\"Base URL\" ;;\n \"llm.openai.base_url_prompt.en\") text=\"Base URL\" ;;\n \"llm.openai.model_prompt.zh\") text=\"默认模型 ID\" ;;\n \"llm.openai.model_prompt.en\") text=\"Default Model ID\" ;;\n \"llm.openai.base_url_label.zh\") text=\" Base URL: %s\" ;;\n \"llm.openai.base_url_label.en\") text=\" Base URL: %s\" ;;\n # --- Custom model parameters ---\n \"llm.custom_model.detected.zh\") text=\" ⚠️ 模型 '%s' 不在内置模型列表中,请配置模型参数:\" ;;\n \"llm.custom_model.detected.en\") text=\" ⚠️ Model '%s' is not in the built-in model list. Please configure model parameters:\" ;;\n \"llm.custom_model.context_prompt.zh\") text=\"最大上下文长度(token 数)[150000]\" ;;\n \"llm.custom_model.context_prompt.en\") text=\"Max context window (tokens) [150000]\" ;;\n \"llm.custom_model.max_tokens_prompt.zh\") text=\"最大输出长度(token 数)[128000]\" ;;\n \"llm.custom_model.max_tokens_prompt.en\") text=\"Max output tokens [128000]\" ;;\n \"llm.custom_model.reasoning_prompt.zh\") text=\"是否支持推理/思考模式?[Y/n]\" ;;\n \"llm.custom_model.reasoning_prompt.en\") text=\"Does it support reasoning/thinking mode? [Y/n]\" ;;\n \"llm.custom_model.vision_prompt.zh\") text=\"是否支持图片输入?[y/N]\" ;;\n \"llm.custom_model.vision_prompt.en\") text=\"Does it support image input? [y/N]\" ;;\n \"llm.custom_model.summary.zh\") text=\" 自定义模型参数: 上下文=%s, 最大输出=%s, 推理=%s, 图片=%s\" ;;\n \"llm.custom_model.summary.en\") text=\" Custom model params: context=%s, maxTokens=%s, reasoning=%s, vision=%s\" ;;\n # --- Admin Credentials ---\n \"admin.title.zh\") text=\"--- 管理员凭据 ---\" ;;\n \"admin.title.en\") text=\"--- Admin Credentials ---\" ;;\n \"admin.username_prompt.zh\") text=\"管理员用户名\" ;;\n \"admin.username_prompt.en\") text=\"Admin Username\" ;;\n \"admin.password_prompt.zh\") text=\"管理员密码(留空自动生成,最少 8 位)\" ;;\n \"admin.password_prompt.en\") text=\"Admin Password (leave empty to auto-generate, min 8 chars)\" ;;\n \"admin.password_generated.zh\") text=\" 已自动生成管理员密码\" ;;\n \"admin.password_generated.en\") text=\" Auto-generated admin password\" ;;\n \"admin.password_too_short.zh\") text=\"管理员密码至少需要 8 个字符(MinIO 要求)。当前长度: %s\" ;;\n \"admin.password_too_short.en\") text=\"Admin password must be at least 8 characters (MinIO requirement). Current length: %s\" ;;\n # --- Port Configuration ---\n \"port.title.zh\") text=\"--- 端口配置(按回车使用默认值)---\" ;;\n \"port.title.en\") text=\"--- Port Configuration (press Enter for defaults) ---\" ;;\n \"port.gateway_prompt.zh\") text=\"网关主机端口(容器内 8080)\" ;;\n \"port.gateway_prompt.en\") text=\"Host port for gateway (8080 inside container)\" ;;\n \"port.console_prompt.zh\") text=\"Higress 控制台主机端口(容器内 8001)\" ;;\n \"port.console_prompt.en\") text=\"Host port for Higress console (8001 inside container)\" ;;\n \"port.element_prompt.zh\") text=\"Element Web 直接访问主机端口(容器内 8088)\" ;;\n \"port.element_prompt.en\") text=\"Host port for Element Web direct access (8088 inside container)\" ;;\n \"port.manager_console_prompt.zh\") text=\"Manager 控制台主机端口(容器内 18888)\" ;;\n \"port.manager_console_prompt.en\") text=\"Host port for Manager console (18888 inside container)\" ;;\n \"port.copaw_app_prompt.zh\") text=\"QwenPaw App API 主机端口(容器内 18799)\" ;;\n \"port.copaw_app_prompt.en\") text=\"Host port for QwenPaw App API (18799 inside container)\" ;;\n # --- Local-only binding ---\n \"port.local_only.title.zh\") text=\"--- 网络访问模式 ---\" ;;\n \"port.local_only.title.en\") text=\"--- Network Access Mode ---\" ;;\n \"port.local_only.prompt.zh\") text=\"是否仅允许本机访问(端口绑定到 127.0.0.1)?\" ;;\n \"port.local_only.prompt.en\") text=\"Bind ports to localhost only (127.0.0.1)?\" ;;\n \"port.local_only.hint_yes.zh\") text=\" 仅本机使用,无需开放外部端口(推荐)\" ;;\n \"port.local_only.hint_yes.en\") text=\" Local use only, no external port exposure (recommended)\" ;;\n \"port.local_only.hint_no.zh\") text=\" 允许外部访问(局域网 / 公网)\" ;;\n \"port.local_only.hint_no.en\") text=\" Allow external access (LAN / public network)\" ;;\n \"port.local_only.choice.zh\") text=\"请选择 [1/2]\" ;;\n \"port.local_only.choice.en\") text=\"Enter choice [1/2]\" ;;\n \"port.local_only.selected_local.zh\") text=\"端口已绑定到 127.0.0.1(仅本机访问)\" ;;\n \"port.local_only.selected_local.en\") text=\"Ports bound to 127.0.0.1 (localhost only)\" ;;\n \"port.local_only.selected_external.zh\") text=\"端口已绑定到所有网络接口(0.0.0.0)\" ;;\n \"port.local_only.selected_external.en\") text=\"Ports bound to all interfaces (0.0.0.0)\" ;;\n \"port.local_only.https_hint.zh\") text=\"⚠️ 建议在 Higress 控制台配置 TLS 证书并启用 HTTPS,避免明文传输。\" ;;\n \"port.local_only.https_hint.en\") text=\"⚠️ It is recommended to configure TLS certificates and enable HTTPS in the Higress Console to avoid plaintext transmission.\" ;;\n \"port.local_only.https_docs.zh\") text=\"\" ;;\n \"port.local_only.https_docs.en\") text=\"\" ;;\n # --- Domain Configuration ---\n \"domain.title.zh\") text=\"--- 域名配置(按回车使用默认值)---\" ;;\n \"domain.title.en\") text=\"--- Domain Configuration (press Enter for defaults) ---\" ;;\n \"domain.hint.zh\") text=\"提示: 自定义域名前必须事先做好 DNS 解析。单机 ECS 部署时无需修改 aigw、fs 等域名;Element Web 和 Matrix Server 也可通过 IP 直接访问。\" ;;\n \"domain.hint.en\") text=\"Hint: Configure DNS resolution before customizing domains. For single ECS deployment, no need to change aigw, fs, etc.; Element Web and Matrix Server can also be accessed directly via IP.\" ;;\n \"domain.matrix_prompt.zh\") text=\"Matrix 域名\" ;;\n \"domain.matrix_prompt.en\") text=\"Matrix Domain\" ;;\n \"domain.element_prompt.zh\") text=\"Element Web 域名\" ;;\n \"domain.element_prompt.en\") text=\"Element Web Domain\" ;;\n \"domain.gateway_prompt.zh\") text=\"AI 网关域名\" ;;\n \"domain.gateway_prompt.en\") text=\"AI Gateway Domain\" ;;\n \"domain.fs_prompt.zh\") text=\"文件系统域名\" ;;\n \"domain.fs_prompt.en\") text=\"File System Domain\" ;;\n \"domain.console_prompt.zh\") text=\"Manager 控制台域名\" ;;\n \"domain.console_prompt.en\") text=\"Manager Console Domain\" ;;\n # --- GitHub Integration ---\n \"github.title.zh\") text=\"--- GitHub 集成(可选,按回车跳过)---\" ;;\n \"github.title.en\") text=\"--- GitHub Integration (optional, press Enter to skip) ---\" ;;\n \"github.token_prompt.zh\") text=\"GitHub 个人访问令牌(可选)\" ;;\n \"github.token_prompt.en\") text=\"GitHub Personal Access Token (optional)\" ;;\n # --- Skills Registry ---\n \"skills.title.zh\") text=\"--- Skills 注册中心(可选,按回车使用默认 nacos://market.agentteams.io:80/public)---\" ;;\n \"skills.title.en\") text=\"--- Skills Registry (optional, press Enter for default nacos://market.agentteams.io:80/public) ---\" ;;\n \"skills.url_prompt.zh\") text=\"Skills 注册中心 URL(留空使用默认 nacos://market.agentteams.io:80/public)\" ;;\n \"skills.url_prompt.en\") text=\"Skills Registry URL (leave empty for default nacos://market.agentteams.io:80/public)\" ;;\n # --- Data Persistence ---\n \"data.title.zh\") text=\"--- 数据持久化 ---\" ;;\n \"data.title.en\") text=\"--- Data Persistence ---\" ;;\n \"data.volume_prompt.zh\") text=\"Docker 卷名称 [agentteams-data]\" ;;\n \"data.volume_prompt.en\") text=\"Docker volume name for persistent data [agentteams-data]\" ;;\n \"data.volume_using.zh\") text=\" 使用 Docker 卷: %s\" ;;\n \"data.volume_using.en\") text=\" Using Docker volume: %s\" ;;\n # --- Manager Workspace ---\n \"workspace.title.zh\") text=\"--- Manager 工作空间 ---\" ;;\n \"workspace.title.en\") text=\"--- Manager Workspace ---\" ;;\n \"workspace.dir_prompt.zh\") text=\"Manager 工作空间目录 [%s]\" ;;\n \"workspace.dir_prompt.en\") text=\"Manager workspace directory [%s]\" ;;\n \"workspace.dir_label.zh\") text=\" Manager 工作空间: %s\" ;;\n \"workspace.dir_label.en\") text=\" Manager workspace: %s\" ;;\n # --- Host directory sharing ---\n \"host_share.prompt.zh\") text=\"与 Agent 共享的主机目录(默认: %s)\" ;;\n \"host_share.prompt.en\") text=\"Host directory to share with agents (default: %s)\" ;;\n \"host_share.sharing.zh\") text=\"共享主机目录: %s -> 容器内 /host-share\" ;;\n \"host_share.sharing.en\") text=\"Sharing host directory: %s -> /host-share in container\" ;;\n \"host_share.not_exist.zh\") text=\"警告: 主机目录 %s 不存在,跳过验证继续使用\" ;;\n \"host_share.not_exist.en\") text=\"WARNING: Host directory %s does not exist, using without validation\" ;;\n # --- Default worker runtime ---\n \"worker_runtime.title.zh\") text=\"--- 默认 Worker 运行时 ---\" ;;\n \"worker_runtime.title.en\") text=\"--- Default Worker Runtime ---\" ;;\n \"worker_runtime.openclaw.zh\") text=\"OpenClaw\" ;;\n \"worker_runtime.openclaw.en\") text=\"OpenClaw\" ;;\n \"worker_runtime.copaw.zh\") text=\"QwenPaw\" ;;\n \"worker_runtime.copaw.en\") text=\"QwenPaw\" ;;\n \"worker_runtime.hermes.zh\") text=\"Hermes\" ;;\n \"worker_runtime.hermes.en\") text=\"Hermes\" ;;\n \"worker_runtime.choice.zh\") text=\"请选择 [1/2/3]\" ;;\n \"worker_runtime.choice.en\") text=\"Enter choice [1/2/3]\" ;;\n \"worker_runtime.selected.zh\") text=\"默认 Worker 运行时: %s\" ;;\n \"worker_runtime.selected.en\") text=\"Default Worker runtime: %s\" ;;\n \"worker_runtime.title_short.zh\") text=\"默认 Worker 运行时\" ;;\n \"worker_runtime.title_short.en\") text=\"Default Worker Runtime\" ;;\n \"manager_runtime.title.zh\") text=\"--- Manager 运行时 ---\" ;;\n \"manager_runtime.title.en\") text=\"--- Manager Runtime ---\" ;;\n \"manager_runtime.openclaw.zh\") text=\"OpenClaw\" ;;\n \"manager_runtime.openclaw.en\") text=\"OpenClaw\" ;;\n \"manager_runtime.copaw.zh\") text=\"QwenPaw\" ;;\n \"manager_runtime.copaw.en\") text=\"QwenPaw\" ;;\n \"manager_runtime.choice.zh\") text=\"请选择 [1/2]\" ;;\n \"manager_runtime.choice.en\") text=\"Enter choice [1/2]\" ;;\n \"manager_runtime.selected.zh\") text=\"Manager 运行时: %s\" ;;\n \"manager_runtime.selected.en\") text=\"Manager runtime: %s\" ;;\n \"manager_runtime.title_short.zh\") text=\"Manager 运行时\" ;;\n \"manager_runtime.title_short.en\") text=\"Manager Runtime\" ;;\n # --- Secrets and config ---\n \"install.generating_secrets.zh\") text=\"正在生成密钥...\" ;;\n \"install.generating_secrets.en\") text=\"Generating secrets...\" ;;\n \"install.config_saved.zh\") text=\"配置已保存到 %s\" ;;\n \"install.config_saved.en\") text=\"Configuration saved to %s\" ;;\n # --- Container runtime socket ---\n \"install.socket_detected.zh\") text=\"容器运行时 socket: %s(已启用直接创建 Worker)\" ;;\n \"install.socket_detected.en\") text=\"Container runtime socket: %s (direct Worker creation enabled)\" ;;\n \"install.socket_not_found.zh\") text=\"未找到容器运行时 socket(Manager 无法直接创建 Worker 容器,需要你手动执行 docker 命令创建)\" ;;\n \"install.socket_not_found.en\") text=\"No container runtime socket found (Manager cannot create Worker containers directly, you will need to create them manually using docker commands)\" ;;\n \"install.socket_confirm.title.zh\") text=\"⚠️ 未检测到容器运行时 Socket\" ;;\n \"install.socket_confirm.title.en\") text=\"⚠️ Container Runtime Socket Not Detected\" ;;\n \"install.socket_confirm.message.zh\") text=\"未找到 Docker/Podman socket,Manager 将无法自动创建 Worker 容器。\\n你需要手动执行 docker run 命令来创建 Worker。\\n\\n是否继续安装?\" ;;\n \"install.socket_confirm.message.en\") text=\"Docker/Podman socket not found. Manager will not be able to create Worker containers automatically.\\nYou will need to manually run docker commands to create Workers.\\n\\nContinue installation?\" ;;\n \"install.socket_confirm.prompt.zh\") text=\"继续安装? [y/N]: \" ;;\n \"install.socket_confirm.prompt.en\") text=\"Continue? [y/N]: \" ;;\n \"install.socket_confirm.cancelled.zh\") text=\"安装已取消。如需启用 Worker 自动创建,请确保 Docker/Podman 正在运行,然后重新运行安装脚本。\" ;;\n \"install.socket_confirm.cancelled.en\") text=\"Installation cancelled. To enable automatic Worker creation, ensure Docker/Podman is running and re-run the installer.\" ;;\n # --- Container management ---\n \"install.removing_existing.zh\") text=\"正在移除现有 agentteams-manager 容器...\" ;;\n \"install.removing_existing.en\") text=\"Removing existing agentteams-manager container...\" ;;\n # --- Matrix E2EE ---\n \"matrix_e2ee.title.zh\") text=\"--- Matrix 端到端加密(E2EE)---\" ;;\n \"matrix_e2ee.title.en\") text=\"--- Matrix End-to-End Encryption (E2EE) ---\" ;;\n \"matrix_e2ee.desc.zh\") text=\"E2EE 会对 Manager 与 Worker 之间的 Matrix 消息进行端到端加密。\\n 启用后,即使 Matrix 服务器被入侵,消息内容也无法被窃取。\\n 但 E2EE 会增加首次握手耗时,且要求所有 Agent 都支持 matrix-sdk-crypto。\\n 如果不确定,建议保持禁用。\\n ⚠ 注意:禁用 E2EE 后,请勿在 Element 上创建默认启用加密的 Private 房间,\\n 否则 Agent 将无法读取该房间中的加密消息。请改用 Public 房间或关闭房间加密。\" ;;\n \"matrix_e2ee.desc.en\") text=\"E2EE encrypts Matrix messages between Manager and Workers end-to-end.\\n When enabled, message content stays private even if the Matrix server is compromised.\\n However, E2EE adds overhead to the initial handshake and requires all Agents\\n to support matrix-sdk-crypto. If unsure, keep it disabled.\\n ⚠ Note: When E2EE is disabled, do NOT create Private rooms in Element (which\\n enable encryption by default) — Agents cannot read encrypted messages without\\n E2EE support. Use Public rooms or turn off room encryption instead.\" ;;\n \"matrix_e2ee.enable.zh\") text=\"启用 E2EE\" ;;\n \"matrix_e2ee.enable.en\") text=\"Enable E2EE\" ;;\n \"matrix_e2ee.disable.zh\") text=\"禁用 E2EE(推荐)\" ;;\n \"matrix_e2ee.disable.en\") text=\"Disable E2EE (recommended)\" ;;\n \"matrix_e2ee.choice.zh\") text=\"请选择 [1/2]\" ;;\n \"matrix_e2ee.choice.en\") text=\"Enter choice [1/2]\" ;;\n \"matrix_e2ee.selected_enabled.zh\") text=\"Matrix E2EE: 已启用\" ;;\n \"matrix_e2ee.selected_enabled.en\") text=\"Matrix E2EE: enabled\" ;;\n \"matrix_e2ee.selected_disabled.zh\") text=\"Matrix E2EE: 已禁用(默认)\" ;;\n \"matrix_e2ee.selected_disabled.en\") text=\"Matrix E2EE: disabled (default)\" ;;\n \"matrix_e2ee.title_short.zh\") text=\"Matrix E2EE\" ;;\n \"matrix_e2ee.title_short.en\") text=\"Matrix E2EE\" ;;\n \"matrix_e2ee.val_enabled.zh\") text=\"已启用\" ;;\n \"matrix_e2ee.val_enabled.en\") text=\"enabled\" ;;\n \"matrix_e2ee.val_disabled.zh\") text=\"已禁用\" ;;\n \"matrix_e2ee.val_disabled.en\") text=\"disabled\" ;;\n # --- Docker API proxy ---\n \"docker_proxy.title.zh\") text=\"--- Docker API 安全代理 ---\" ;;\n \"docker_proxy.title.en\") text=\"--- Docker API Security Proxy ---\" ;;\n \"docker_proxy.desc.zh\") text=\"Docker API 代理可防止 AI Agent 通过 Docker API 越狱访问宿主机。\\n 启用后,Manager 不再直接持有 Docker socket,所有容器操作经过安全校验。\" ;;\n \"docker_proxy.desc.en\") text=\"Docker API proxy prevents AI Agents from escaping via Docker API to access the host.\\n When enabled, Manager no longer has direct Docker socket access; all container operations go through security validation.\" ;;\n \"docker_proxy.enable.zh\") text=\"启用(推荐)\" ;;\n \"docker_proxy.enable.en\") text=\"Enable (recommended)\" ;;\n \"docker_proxy.disable.zh\") text=\"禁用(直接挂载 Docker socket)\" ;;\n \"docker_proxy.disable.en\") text=\"Disable (mount Docker socket directly)\" ;;\n \"docker_proxy.choice.zh\") text=\"请选择 [1/2]\" ;;\n \"docker_proxy.choice.en\") text=\"Enter choice [1/2]\" ;;\n \"docker_proxy.selected_enabled.zh\") text=\"Docker API 代理: 已启用\" ;;\n \"docker_proxy.selected_enabled.en\") text=\"Docker API proxy: enabled\" ;;\n \"docker_proxy.selected_disabled.zh\") text=\"Docker API 代理: 已禁用\" ;;\n \"docker_proxy.selected_disabled.en\") text=\"Docker API proxy: disabled\" ;;\n \"docker_proxy.title_short.zh\") text=\"Docker API 代理\" ;;\n \"docker_proxy.title_short.en\") text=\"Docker API Proxy\" ;;\n \"docker_proxy.val_enabled.zh\") text=\"已启用\" ;;\n \"docker_proxy.val_enabled.en\") text=\"enabled\" ;;\n \"docker_proxy.val_disabled.zh\") text=\"已禁用\" ;;\n \"docker_proxy.val_disabled.en\") text=\"disabled\" ;;\n \"docker_proxy.registries_desc.zh\") text=\"默认放行的镜像来源:本地镜像、localhost、Higress 仓库(所有 region)。\\n 如需放行其他镜像仓库,请输入逗号分隔的地址前缀。\\n 示例: ghcr.io/myorg,registry.example.com/team\" ;;\n \"docker_proxy.registries_desc.en\") text=\"Default allowed image sources: local images, localhost, Higress registries (all regions).\\n To allow additional image sources, enter comma-separated address prefixes.\\n Example: ghcr.io/myorg,registry.example.com/team\" ;;\n \"docker_proxy.registries_prompt.zh\") text=\"额外放行的镜像来源(按回车跳过)\" ;;\n \"docker_proxy.registries_prompt.en\") text=\"Additional allowed image sources (press Enter to skip)\" ;;\n \"docker_proxy.registries_label.zh\") text=\"额外放行的镜像来源\" ;;\n \"docker_proxy.registries_label.en\") text=\"Additional allowed image sources\" ;;\n # --- Podman Autostart ---\n \"podman.autostart.title.zh\") text=\"--- Podman 驻留与开机自启 ---\" ;;\n \"podman.autostart.title.en\") text=\"--- Podman Linger & Autostart ---\" ;;\n \"podman.autostart.desc.zh\") text=\"配置为 systemd 服务,支持开机自动启动及后台驻留。\" ;;\n \"podman.autostart.desc.en\") text=\"Configure as a systemd service for automatic startup on boot.\" ;;\n \"podman.autostart.enable.zh\") text=\"启用(推荐)\" ;;\n \"podman.autostart.enable.en\") text=\"Enable (recommended)\" ;;\n \"podman.autostart.disable.zh\") text=\"不启用(容器随当前会话关闭而停止)\" ;;\n \"podman.autostart.disable.en\") text=\"Disable (containers stop when session ends)\" ;;\n \"podman.autostart.choice.zh\") text=\"请选择 [1/2]\" ;;\n \"podman.autostart.choice.en\") text=\"Enter choice [1/2]\" ;;\n \"podman.autostart.selected_enabled.zh\") text=\"Podman 开机自启: 已启用\" ;;\n \"podman.autostart.selected_enabled.en\") text=\"Podman Autostart: enabled\" ;;\n \"podman.autostart.selected_disabled.zh\") text=\"Podman 开机自启: 已禁用\" ;;\n \"podman.autostart.selected_disabled.en\") text=\"Podman Autostart: disabled\" ;;\n \"podman.autostart.title_short.zh\") text=\"Podman 开机自启\" ;;\n \"podman.autostart.title_short.en\") text=\"Podman Autostart\" ;;\n \"podman.autostart.val_enabled.zh\") text=\"已启用\" ;;\n \"podman.autostart.val_enabled.en\") text=\"enabled\" ;;\n \"podman.autostart.val_disabled.zh\") text=\"已禁用\" ;;\n \"podman.autostart.val_disabled.en\") text=\"disabled\" ;;\n \"install.podman.autostart_title.zh\") text=\"正在配置 Podman 容器开机自启...\" ;;\n \"install.podman.autostart_title.en\") text=\"Configuring Podman container autostart...\" ;;\n \"install.podman.linger_enable.zh\") text=\"尝试启用 systemd linger (如有提示请在此输入 sudo 密码)...\" ;;\n \"install.podman.linger_enable.en\") text=\"Attempting to enable systemd linger (enter sudo password if prompted)...\" ;;\n \"install.podman.linger_warn.zh\") text=\"警告: 无法自动启用 linger。驻留可能失败,请手动执行: sudo loginctl enable-linger \\$(whoami)\" ;;\n \"install.podman.linger_warn.en\") text=\"WARNING: Failed to auto-enable linger. Please run manually: sudo loginctl enable-linger \\$(whoami)\" ;;\n \"install.podman.root_setup.zh\") text=\"检测到 root 用户。正在配置系统级 agentteams-podman-restart 服务...\" ;;\n \"install.podman.root_setup.en\") text=\"Root user detected. Configuring system-wide agentteams-podman-restart service...\" ;;\n \"install.podman.root_success.zh\") text=\"✅ 系统级 agentteams-podman-restart 服务已成功启用并运行。\" ;;\n \"install.podman.root_success.en\") text=\"✅ System-wide agentteams-podman-restart service successfully enabled and running.\" ;;\n \"install.podman.root_fail.zh\") text=\"⚠️ 系统级 agentteams-podman-restart 服务启用失败,请稍后手动检查。\" ;;\n \"install.podman.root_fail.en\") text=\"⚠️ Failed to enable system-wide agentteams-podman-restart service. Please check manually later.\" ;;\n \"install.podman.user_setup.zh\") text=\"检测到普通用户 (%s)。正在配置用户级 agentteams-podman-restart 服务...\" ;;\n \"install.podman.user_setup.en\") text=\"Non-root user (%s) detected. Configuring user-level agentteams-podman-restart service...\" ;;\n \"install.podman.user_success.zh\") text=\"✅ 用户级 agentteams-podman-restart 服务已成功启用并运行。\" ;;\n \"install.podman.user_success.en\") text=\"✅ User-level agentteams-podman-restart service successfully enabled and running.\" ;;\n \"install.podman.user_fail.zh\") text=\"⚠️ 用户级 agentteams-podman-restart 服务启用失败。\\n提示:这通常是因为缺少 XDG_RUNTIME_DIR 或 dbus 没有运行。您可以尝试手动执行: systemctl --user enable --now agentteams-podman-restart.service\" ;;\n \"install.podman.user_fail.en\") text=\"⚠️ Failed to enable user-level agentteams-podman-restart service.\\nHint: This is usually due to missing XDG_RUNTIME_DIR or dbus not running. Try manually: systemctl --user enable --now agentteams-podman-restart.service\" ;;\n \"install.podman.success.zh\") text=\"Podman 开机自启配置完成。\" ;;\n \"install.podman.success.en\") text=\"Podman autostart successfully configured.\" ;;\n # --- Worker idle timeout ---\n \"idle_timeout.prompt.zh\") text=\"Worker 空闲自动停止超时(分钟)[720]\" ;;\n \"idle_timeout.prompt.en\") text=\"Worker idle auto-stop timeout in minutes [720]\" ;;\n \"idle_timeout.selected.zh\") text=\"Worker 空闲超时: %s 分钟\" ;;\n \"idle_timeout.selected.en\") text=\"Worker idle timeout: %s minutes\" ;;\n \"idle_timeout.label.zh\") text=\"Worker 空闲超时(分钟)\" ;;\n \"idle_timeout.label.en\") text=\"Worker idle timeout (min)\" ;;\n # --- YOLO mode ---\n \"install.yolo.zh\") text=\"YOLO 模式已启用(自主决策,无交互提示)\" ;;\n \"install.yolo.en\") text=\"YOLO mode enabled (autonomous decisions, no interactive prompts)\" ;;\n # --- Image pulling ---\n \"install.image.exists.zh\") text=\"Manager 镜像已存在: %s\" ;;\n \"install.image.exists.en\") text=\"Manager image already exists locally: %s\" ;;\n \"install.image.pulling_manager.zh\") text=\"正在拉取 Manager 镜像: %s\" ;;\n \"install.image.pulling_manager.en\") text=\"Pulling Manager image: %s\" ;;\n \"install.image.worker_exists.zh\") text=\"Worker 镜像已存在: %s\" ;;\n \"install.image.worker_exists.en\") text=\"Worker image already exists locally: %s\" ;;\n \"install.image.pulling_worker.zh\") text=\"正在拉取 Worker 镜像: %s\" ;;\n \"install.image.pulling_worker.en\") text=\"Pulling Worker image: %s\" ;;\n # --- Starting container ---\n \"install.starting_manager.zh\") text=\"正在启动 Manager 容器...\" ;;\n \"install.starting_manager.en\") text=\"Starting Manager container...\" ;;\n # --- Wait for Manager ready ---\n \"install.wait_ready.zh\") text=\"等待 Manager Agent 就绪(超时: %ss)...\" ;;\n \"install.wait_ready.en\") text=\"Waiting for Manager agent to be ready (timeout: %ss)...\" ;;\n \"install.wait_ready.ok.zh\") text=\"Manager Agent 已就绪!\" ;;\n \"install.wait_ready.ok.en\") text=\"Manager agent is ready!\" ;;\n \"install.wait_ready.waiting.zh\") text=\"等待中... (%ds/%ds)\" ;;\n \"install.wait_ready.waiting.en\") text=\"Waiting... (%ds/%ds)\" ;;\n \"install.wait_ready.timeout.zh\") text=\"Manager Agent 在 %ss 内未就绪。请检查: docker logs %s\" ;;\n \"install.wait_ready.timeout.en\") text=\"Manager agent did not become ready within %ss. Check: docker logs %s\" ;;\n # --- Wait for Matrix ready ---\n \"install.wait_matrix.zh\") text=\"等待 Matrix 服务就绪(超时: %ss)...\" ;;\n \"install.wait_matrix.en\") text=\"Waiting for Matrix server to be ready (timeout: %ss)...\" ;;\n \"install.wait_matrix.ok.zh\") text=\"Matrix 服务已就绪!\" ;;\n \"install.wait_matrix.ok.en\") text=\"Matrix server is ready!\" ;;\n \"install.wait_matrix.waiting.zh\") text=\"等待 Matrix 中... (%ds/%ds)\" ;;\n \"install.wait_matrix.waiting.en\") text=\"Waiting for Matrix... (%ds/%ds)\" ;;\n \"install.wait_matrix.timeout.zh\") text=\"Matrix 服务在 %ss 内未就绪。请检查: docker logs %s\" ;;\n \"install.wait_matrix.timeout.en\") text=\"Matrix server did not become ready within %ss. Check: docker logs %s\" ;;\n # --- OpenAI-compatible connectivity test ---\n \"llm.openai.test.testing.zh\") text=\"正在测试 API 联通性...\" ;;\n \"llm.openai.test.testing.en\") text=\"Testing API connectivity...\" ;;\n \"llm.openai.test.ok.zh\") text=\"✅ API 联通性测试通过\" ;;\n \"llm.openai.test.ok.en\") text=\"✅ API connectivity test passed\" ;;\n \"llm.openai.test.fail.zh\") text=\"⚠️ API 联通性测试失败(HTTP %s)。响应内容:\\n%s\\n请根据以上错误信息联系您的模型服务商解决。\" ;;\n \"llm.openai.test.fail.en\") text=\"⚠️ API connectivity test failed (HTTP %s). Response body:\\n%s\\nPlease contact your model provider to resolve the issue.\" ;;\n \"llm.openai.test.fail.tokenplan.zh\") text=\"⚠️ 提示: 请确认 API Key 有效且已开通通义 Token 套餐。文档: https://help.aliyun.com/zh/model-studio/token-plan-quickstart\" ;;\n \"llm.openai.test.fail.tokenplan.en\") text=\"⚠️ Hint: Verify your Token Plan API key and compatible-mode access. Docs: https://help.aliyun.com/zh/model-studio/token-plan-quickstart\" ;;\n \"llm.openai.test.fail.codingplan.zh\") text=\"⚠️ 提示: 请确认 API Key 有效且已开通通义 Token 套餐。文档: https://help.aliyun.com/zh/model-studio/token-plan-quickstart\" ;;\n \"llm.openai.test.fail.codingplan.en\") text=\"⚠️ Hint: Verify your DASHSCOPE_API_KEY for Qwen Cloud. API keys: https://home.qwencloud.com/api-keys Docs: https://docs.qwencloud.com/\" ;;\n \"llm.openai.test.fail.codingplan_legacy.zh\") text=\"⚠️ 提示: 请确认 API Key 有效且 Coding 套餐接口可用。文档: https://help.aliyun.com/zh/model-studio/get-api-key\" ;;\n \"llm.openai.test.fail.codingplan_legacy.en\") text=\"⚠️ Hint: Verify your DashScope API key and Coding Plan access. Docs: https://help.aliyun.com/zh/model-studio/get-api-key\" ;;\n \"llm.openai.test.no_curl.zh\") text=\"⚠️ 未找到 curl,跳过 API 联通性测试\" ;;\n \"llm.openai.test.no_curl.en\") text=\"⚠️ curl not found, skipping API connectivity test\" ;;\n \"llm.openai.test.confirm.zh\") text=\"是否仍要继续安装?[y/N/b] \" ;;\n \"llm.openai.test.confirm.en\") text=\"Continue with installation anyway? [y/N/b] \" ;;\n \"llm.embedding.title.zh\") text=\"📦 记忆搜索配置\" ;;\n \"llm.embedding.title.en\") text=\"📦 Memory Search Configuration\" ;;\n \"llm.embedding.hint.zh\") text=\" Embedding 模型可提升记忆搜索质量(语义匹配)。不启用也可正常使用记忆功能(关键词匹配)。\" ;;\n \"llm.embedding.hint.en\") text=\" Embedding model improves memory search quality (semantic matching). Memory still works without it (keyword matching).\" ;;\n \"llm.embedding.option.default.zh\") text=\" 1) text-embedding-v4(推荐)\" ;;\n \"llm.embedding.option.default.en\") text=\" 1) text-embedding-v4 (Recommended)\" ;;\n \"llm.embedding.option.custom.zh\") text=\" 2) 自定义 Embedding 模型\" ;;\n \"llm.embedding.option.custom.en\") text=\" 2) Custom embedding model\" ;;\n \"llm.embedding.option.disable.zh\") text=\" 3) 不启用\" ;;\n \"llm.embedding.option.disable.en\") text=\" 3) Do not enable\" ;;\n \"llm.embedding.select.zh\") text=\"选择\" ;;\n \"llm.embedding.select.en\") text=\"Select\" ;;\n \"llm.embedding.custom_prompt.zh\") text=\" Embedding 模型名称\" ;;\n \"llm.embedding.custom_prompt.en\") text=\" Embedding model name\" ;;\n \"llm.embedding.test.testing.zh\") text=\"正在测试 Embedding API 联通性...\" ;;\n \"llm.embedding.test.testing.en\") text=\"Testing Embedding API connectivity...\" ;;\n \"llm.embedding.test.ok.zh\") text=\"✅ Embedding API 联通性测试通过\" ;;\n \"llm.embedding.test.ok.en\") text=\"✅ Embedding API connectivity test passed\" ;;\n \"llm.embedding.test.fail.zh\") text=\"⚠️ Embedding API 测试失败(HTTP %s)。响应: %s\" ;;\n \"llm.embedding.test.fail.en\") text=\"⚠️ Embedding API test failed (HTTP %s). Response: %s\" ;;\n \"llm.embedding.auto_disabled.zh\") text=\"⚠️ Embedding 已自动禁用,记忆搜索将使用关键词匹配。您可以稍后在 agentteams-manager.env 中设置 AGENTTEAMS_EMBEDDING_MODEL 启用。\" ;;\n \"llm.embedding.auto_disabled.en\") text=\"⚠️ Embedding auto-disabled. Memory search will use keyword matching. You can enable it later in agentteams-manager.env by setting AGENTTEAMS_EMBEDDING_MODEL.\" ;;\n \"llm.embedding.disabled.zh\") text=\"ℹ️ Embedding 已禁用,记忆搜索将使用关键词匹配。\" ;;\n \"llm.embedding.disabled.en\") text=\"ℹ️ Embedding disabled. Memory search will use keyword matching.\" ;;\n \"llm.openai.test.aborted.zh\") text=\"安装已中止。\" ;;\n \"llm.openai.test.aborted.en\") text=\"Installation aborted.\" ;;\n \"nav.back_hint.zh\") text=\"(输入 b 返回上一步)\" ;;\n \"nav.back_hint.en\") text=\"(enter b to go back)\" ;;\n # --- OpenAI-compatible provider creation ---\n \"install.openai_compat.missing.zh\") text=\"警告: OpenAI Base URL 或 API Key 未设置,跳过提供商创建\" ;;\n \"install.openai_compat.missing.en\") text=\"WARNING: OpenAI Base URL or API Key not set, skipping provider creation\" ;;\n \"install.openai_compat.creating.zh\") text=\"正在创建 OpenAI 兼容提供商...\" ;;\n \"install.openai_compat.creating.en\") text=\"Creating OpenAI-compatible provider...\" ;;\n \"install.openai_compat.domain.zh\") text=\" 域名: %s\" ;;\n \"install.openai_compat.domain.en\") text=\" Domain: %s\" ;;\n \"install.openai_compat.port.zh\") text=\" 端口: %s\" ;;\n \"install.openai_compat.port.en\") text=\" Port: %s\" ;;\n \"install.openai_compat.protocol.zh\") text=\" 协议: %s\" ;;\n \"install.openai_compat.protocol.en\") text=\" Protocol: %s\" ;;\n \"install.openai_compat.service_fail.zh\") text=\"警告: 创建 DNS 服务源失败(可能已存在)\" ;;\n \"install.openai_compat.service_fail.en\") text=\"WARNING: Failed to create DNS service source (may already exist)\" ;;\n \"install.openai_compat.provider_fail.zh\") text=\"警告: 创建 AI 提供商失败(可能已存在)\" ;;\n \"install.openai_compat.provider_fail.en\") text=\"WARNING: Failed to create AI provider (may already exist)\" ;;\n \"install.openai_compat.success.zh\") text=\"OpenAI 兼容提供商创建成功\" ;;\n \"install.openai_compat.success.en\") text=\"OpenAI-compatible provider created successfully\" ;;\n # --- Welcome message ---\n \"install.welcome_msg.soul_configured.zh\") text=\"Soul 已配置(找到 soul-configured 标记),跳过 onboarding 消息\" ;;\n \"install.welcome_msg.soul_configured.en\") text=\"Soul already configured (soul-configured marker found), skipping onboarding message\" ;;\n \"install.welcome_msg.logging_in.zh\") text=\"正在以 %s 身份登录以发送欢迎消息...\" ;;\n \"install.welcome_msg.logging_in.en\") text=\"Logging in as %s to send welcome message...\" ;;\n \"install.welcome_msg.login_failed.zh\") text=\"警告: 以 %s 身份登录失败,跳过欢迎消息\" ;;\n \"install.welcome_msg.login_failed.en\") text=\"WARNING: Failed to login as %s, skipping welcome message\" ;;\n \"install.welcome_msg.finding_room.zh\") text=\"正在查找与 Manager 的 DM 房间...\" ;;\n \"install.welcome_msg.finding_room.en\") text=\"Finding DM room with Manager...\" ;;\n \"install.welcome_msg.creating_room.zh\") text=\"正在创建与 Manager 的 DM 房间...\" ;;\n \"install.welcome_msg.creating_room.en\") text=\"Creating DM room with Manager...\" ;;\n \"install.welcome_msg.no_room.zh\") text=\"警告: 无法找到或创建与 Manager 的 DM 房间\" ;;\n \"install.welcome_msg.no_room.en\") text=\"WARNING: Could not find or create DM room with Manager\" ;;\n \"install.welcome_msg.waiting_join.zh\") text=\"等待 Manager 加入房间...\" ;;\n \"install.welcome_msg.waiting_join.en\") text=\"Waiting for Manager to join the room...\" ;;\n \"install.welcome_msg.sending.zh\") text=\"正在向 Manager 发送欢迎消息...\" ;;\n \"install.welcome_msg.sending.en\") text=\"Sending welcome message to Manager...\" ;;\n \"install.welcome_msg.send_failed.zh\") text=\"警告: 发送欢迎消息失败\" ;;\n \"install.welcome_msg.send_failed.en\") text=\"WARNING: Failed to send welcome message\" ;;\n \"install.welcome_msg.sent.zh\") text=\"欢迎消息已发送给 Manager\" ;;\n \"install.welcome_msg.sent.en\") text=\"Welcome message sent to Manager\" ;;\n \"install.welcome_msg.waiting.zh\") text=\"等待 Manager 发送欢迎消息(Higress 路由授权 + LLM 探活,约 45-90s)...\" ;;\n \"install.welcome_msg.waiting.en\") text=\"Waiting for Manager to send the welcome message (Higress route auth + LLM probe, ~45-90s)...\" ;;\n \"install.welcome_msg.confirmed.zh\") text=\"Manager 已确认发送欢迎消息(status.welcomeSent=true,用时 %ss)\" ;;\n \"install.welcome_msg.confirmed.en\") text=\"Manager confirmed welcome message sent (status.welcomeSent=true, %ss elapsed)\" ;;\n \"install.welcome_msg.timeout.zh\") text=\"警告: 在 %ss 内未观察到 Manager 发送欢迎消息(status.welcomeSent=true)。安装仍然成功,所有服务已就绪——可继续按下方提示登录 Element Web。\" ;;\n \"install.welcome_msg.timeout.en\") text=\"WARNING: Did not observe the Manager sending its welcome message (status.welcomeSent=true) within %ss. Installation is still successful, all services are up — continue with the Element Web instructions below.\" ;;\n \"install.welcome_msg.timeout_hint.zh\") text=\"手动触发 onboarding: 登录 Element Web → 打开与 Manager 的 DM 房间 → 发送任意一句话(例如 \\\"hi\\\"),Manager 会接管对话并开始引导。\" ;;\n \"install.welcome_msg.timeout_hint.en\") text=\"Manual onboarding: log in to Element Web → open the DM with the Manager → send any message (e.g. \\\"hi\\\") and the Manager will take over and start the guided setup.\" ;;\n \"install.welcome_msg.timeout_inspect.zh\") text=\"排查命令: docker exec agentteams-controller agt get managers default\" ;;\n \"install.welcome_msg.timeout_inspect.en\") text=\"Inspect status: docker exec agentteams-controller agt get managers default\" ;;\n \"install.welcome_msg.poll_unavailable.zh\") text=\"提示: agentteams-manager 内未找到 AgentTeams CLI,跳过 welcome 等待(旧镜像?)\" ;;\n \"install.welcome_msg.poll_unavailable.en\") text=\"Note: AgentTeams CLI not found inside agentteams-manager; skipping welcome wait (old image?)\" ;;\n # --- Dashboard wizard ---\n \"dash.prompt.zh\") text=\"是否安装 agentteams-dashboard 管理面板?\" ;;\n \"dash.prompt.en\") text=\"Install agentteams-dashboard management UI?\" ;;\n \"dash.version_prompt.zh\") text=\"Dashboard 版本\" ;;\n \"dash.version_prompt.en\") text=\"Dashboard version\" ;;\n \"dash.port_prompt.zh\") text=\"Dashboard 端口号\" ;;\n \"dash.port_prompt.en\") text=\"Dashboard port\" ;;\n \"dash.image_prompt.zh\") text=\"Dashboard 镜像\" ;;\n \"dash.image_prompt.en\") text=\"Dashboard image\" ;;\n \"dash.gateway_prompt.zh\") text=\"Higress Console URL (用于共享登录)\" ;;\n \"dash.gateway_prompt.en\") text=\"Higress Console URL (for shared login)\" ;;\n \"dash.skip.zh\") text=\"跳过 Dashboard 安装\" ;;\n \"dash.skip.en\") text=\"Skipping Dashboard installation\" ;;\n \"dash.auto_url.zh\") text=\"自动检测到 Higress Console URL\" ;;\n \"dash.auto_url.en\") text=\"Auto-detected Higress Console URL\" ;;\n \"dash.auto_url_fail.zh\") text=\"无法自动检测 Higress Console URL,请手动输入\" ;;\n \"dash.auto_url_fail.en\") text=\"Cannot auto-detect Higress Console URL, please enter manually\" ;;\n \"dash.summary.zh\") text=\"Dashboard 配置: 端口=%s 镜像=%s\" ;;\n \"dash.summary.en\") text=\"Dashboard config: port=%s image=%s\" ;;\n # --- Dashboard success ---\n \"success.dashboard.zh\") text=\" agentteams-dashboard 管理面板: http://localhost:%s/\" ;;\n \"success.dashboard.en\") text=\" agentteams-dashboard: http://localhost:%s/\" ;;\n # --- Final output panel ---\n \"success.title.zh\") text=\"=== AgentTeams Manager 已启动!===\" ;;\n \"success.title.en\") text=\"=== AgentTeams Manager Started! ===\" ;;\n \"success.domains_configured.zh\") text=\"以下域名已配置解析到 127.0.0.1:\" ;;\n \"success.domains_configured.en\") text=\"The following domains are configured to resolve to 127.0.0.1:\" ;;\n \"success.open_url.zh\") text=\" ★ 在浏览器中打开以下 URL 开始使用: ★\" ;;\n \"success.open_url.en\") text=\" ★ Open the following URL in your browser to start: ★\" ;;\n \"success.login_with.zh\") text=\" 登录信息:\" ;;\n \"success.login_with.en\") text=\" Login with:\" ;;\n \"success.username.zh\") text=\" 用户名: %s\" ;;\n \"success.username.en\") text=\" Username: %s\" ;;\n \"success.password.zh\") text=\" 密码: %s\" ;;\n \"success.password.en\") text=\" Password: %s\" ;;\n \"success.after_login.zh\") text=\" 登录后,开始与 Manager 聊天!\" ;;\n \"success.after_login.en\") text=\" After login, start chatting with the Manager!\" ;;\n \"success.tell_it.zh\") text=\" 告诉它: \\\"创建一个名为 alice 的前端开发 Worker\\\"\" ;;\n \"success.tell_it.en\") text=\" Tell it: \\\"Create a Worker named alice for frontend dev\\\"\" ;;\n \"success.manager_auto.zh\") text=\" Manager 会自动处理一切。\" ;;\n \"success.manager_auto.en\") text=\" The Manager will handle everything automatically.\" ;;\n \"success.mobile_title.zh\") text=\" 📱 移动端访问(FluffyChat / Element Mobile):\" ;;\n \"success.mobile_title.en\") text=\" 📱 Mobile access (FluffyChat / Element Mobile):\" ;;\n \"success.mobile_step1.zh\") text=\" 1. 在手机上下载 FluffyChat 或 Element\" ;;\n \"success.mobile_step1.en\") text=\" 1. Download FluffyChat or Element on your phone\" ;;\n \"success.mobile_step2.zh\") text=\" 2. 设置 homeserver 为: %s\" ;;\n \"success.mobile_step2.en\") text=\" 2. Set homeserver to: %s\" ;;\n \"success.mobile_step2_noip.zh\") text=\" 2. 设置 homeserver 为: http://<本机局域网IP>:%s\" ;;\n \"success.mobile_step2_noip.en\") text=\" 2. Set homeserver to: http://<this-machine-LAN-IP>:%s\" ;;\n \"success.mobile_noip_hint.zh\") text=\" (无法自动检测局域网 IP — 请使用 ifconfig / ip addr 查看)\" ;;\n \"success.mobile_noip_hint.en\") text=\" (Could not detect LAN IP automatically — check with: ifconfig / ip addr)\" ;;\n \"success.mobile_step3.zh\") text=\" 3. 登录信息:\" ;;\n \"success.mobile_step3.en\") text=\" 3. Login with:\" ;;\n \"success.mobile_username.zh\") text=\" 用户名: %s\" ;;\n \"success.mobile_username.en\") text=\" Username: %s\" ;;\n \"success.mobile_password.zh\") text=\" 密码: %s\" ;;\n \"success.mobile_password.en\") text=\" Password: %s\" ;;\n # --- Other consoles and tips ---\n \"success.other_consoles.zh\") text=\"--- 其他控制台 ---\" ;;\n \"success.other_consoles.en\") text=\"--- Other Consoles ---\" ;;\n \"success.higress_console.zh\") text=\" Higress 控制台: http://localhost:%s(用户名: %s / 密码: %s)\" ;;\n \"success.higress_console.en\") text=\" Higress Console: http://localhost:%s (Username: %s / Password: %s)\" ;;\n \"success.manager_console.zh\") text=\" Manager 控制台(本地): http://localhost:%s(无需登录)\" ;;\n \"success.manager_console.en\") text=\" Manager Console (local): http://localhost:%s (no login required)\" ;;\n \"success.manager_console_gateway.zh\") text=\" Manager 控制台(网关): http://console-local.agentteams.io(用户名: %s / 密码: %s)\" ;;\n \"success.manager_console_gateway.en\") text=\" Manager Console (gateway): http://console-local.agentteams.io (Username: %s / Password: %s)\" ;;\n \"success.copaw_console.zh\") text=\" QwenPaw App API: http://localhost:%s(无需登录)\" ;;\n \"success.copaw_console.en\") text=\" QwenPaw App API: http://localhost:%s (no login required)\" ;;\n \"success.switch_llm.title.zh\") text=\"--- 切换 LLM 提供商 ---\" ;;\n \"success.switch_llm.title.en\") text=\"--- Switch LLM Providers ---\" ;;\n \"success.switch_llm.hint.zh\") text=\" 您可以通过 Higress 控制台切换到其他 LLM 提供商(OpenAI、Anthropic 等)。\" ;;\n \"success.switch_llm.hint.en\") text=\" You can switch to other LLM providers (OpenAI, Anthropic, etc.) via Higress Console.\" ;;\n \"success.switch_llm.docs.zh\") text=\" 详细说明请参阅:\" ;;\n \"success.switch_llm.docs.en\") text=\" For detailed instructions, see:\" ;;\n \"success.switch_llm.url.zh\") text=\" https://higress.ai/en/docs/ai/scene-guide/multi-proxy#console-configuration\" ;;\n \"success.switch_llm.url.en\") text=\" https://higress.ai/en/docs/ai/scene-guide/multi-proxy#console-configuration\" ;;\n \"success.tip.zh\") text=\"提示: 您也可以在聊天中让 Manager 为您配置 LLM 提供商。\" ;;\n \"success.tip.en\") text=\"Tip: You can also ask the Manager to configure LLM providers for you in the chat.\" ;;\n \"success.config_file.zh\") text=\"配置文件: %s\" ;;\n \"success.config_file.en\") text=\"Configuration file: %s\" ;;\n \"success.data_volume.zh\") text=\"数据卷: %s\" ;;\n \"success.data_volume.en\") text=\"Data volume: %s\" ;;\n \"success.workspace.zh\") text=\"Manager 工作空间: %s\" ;;\n \"success.workspace.en\") text=\"Manager workspace: %s\" ;;\n # --- Worker installation ---\n \"worker.resetting.zh\") text=\"正在重置 Worker: %s...\" ;;\n \"worker.resetting.en\") text=\"Resetting Worker: %s...\" ;;\n \"worker.exists.zh\") text=\"容器 '%s' 已存在。使用 --reset 重新创建。\" ;;\n \"worker.exists.en\") text=\"Container '%s' already exists. Use --reset to recreate.\" ;;\n \"worker.starting.zh\") text=\"正在启动 Worker: %s...\" ;;\n \"worker.starting.en\") text=\"Starting Worker: %s...\" ;;\n \"worker.skills_url.zh\") text=\" Skills API URL: %s\" ;;\n \"worker.skills_url.en\") text=\" Skills API URL: %s\" ;;\n \"worker.started.zh\") text=\"=== Worker %s 已启动!===\" ;;\n \"worker.started.en\") text=\"=== Worker %s Started! ===\" ;;\n \"worker.container.zh\") text=\"容器: %s\" ;;\n \"worker.container.en\") text=\"Container: %s\" ;;\n \"worker.view_logs.zh\") text=\"查看日志: docker logs -f %s\" ;;\n \"worker.view_logs.en\") text=\"View logs: docker logs -f %s\" ;;\n # --- Prompt function messages ---\n \"prompt.preset.zh\") text=\" %s = (已通过环境变量预设)\" ;;\n \"prompt.preset.en\") text=\" %s = (pre-set via env)\" ;;\n \"prompt.upgrade_keep.zh\") text=\" %s = %s(当前值,回车保留 / 输入新值覆盖)\" ;;\n \"prompt.upgrade_keep.en\") text=\" %s = %s (current value, press Enter to keep / type new value to change)\" ;;\n \"prompt.upgrade_keep_secret.zh\") text=\" %s = %s(当前值,回车保留 / 输入新值覆盖)\" ;;\n \"prompt.upgrade_keep_secret.en\") text=\" %s = %s (current value, press Enter to keep / type new value to change)\" ;;\n \"prompt.upgrade_empty.zh\") text=\" %s = (未设置,回车跳过 / 输入新值设置)\" ;;\n \"prompt.upgrade_empty.en\") text=\" %s = (not set, press Enter to skip / type new value to set)\" ;;\n \"prompt.default.zh\") text=\" %s = %s(默认)\" ;;\n \"prompt.default.en\") text=\" %s = %s (default)\" ;;\n \"prompt.required.zh\") text=\"%s 是必需的(在非交互模式下通过环境变量设置)\" ;;\n \"prompt.required.en\") text=\"%s is required (set via environment variable in non-interactive mode)\" ;;\n \"prompt.required_empty.zh\") text=\"%s 是必需的\" ;;\n \"prompt.required_empty.en\") text=\"%s is required\" ;;\n # --- Language switch prompt (bilingual by design) ---\n \"lang.detected.zh\") text=\"检测到语言 / Detected language: 中文\" ;;\n \"lang.detected.en\") text=\"检测到语言 / Detected language: English\" ;;\n \"lang.switch_title.zh\") text=\"切换语言 / Switch language:\" ;;\n \"lang.switch_title.en\") text=\"切换语言 / Switch language:\" ;;\n \"lang.option_zh.zh\") text=\" 1) 中文\" ;;\n \"lang.option_zh.en\") text=\" 1) 中文\" ;;\n \"lang.option_en.zh\") text=\" 2) English\" ;;\n \"lang.option_en.en\") text=\" 2) English\" ;;\n \"lang.prompt.zh\") text=\"请选择 / Enter choice\" ;;\n \"lang.prompt.en\") text=\"请选择 / Enter choice\" ;;\n # --- Uninstall messages ---\n \"uninstall.title.zh\") text=\"正在卸载 AgentTeams...\" ;;\n \"uninstall.title.en\") text=\"Uninstalling AgentTeams...\" ;;\n \"uninstall.stopping_manager.zh\") text=\"正在停止并移除 agentteams-manager...\" ;;\n \"uninstall.stopping_manager.en\") text=\"Stopping and removing agentteams-manager...\" ;;\n \"uninstall.stopping_workers.zh\") text=\"正在停止并移除 Worker 容器...\" ;;\n \"uninstall.stopping_workers.en\") text=\"Stopping and removing worker containers...\" ;;\n \"uninstall.removed.zh\") text=\" 已移除: %s\" ;;\n \"uninstall.removed.en\") text=\" Removed: %s\" ;;\n \"uninstall.removing_volume.zh\") text=\"正在移除 Docker 卷: %s\" ;;\n \"uninstall.removing_volume.en\") text=\"Removing Docker volume: %s\" ;;\n \"uninstall.removing_env.zh\") text=\"正在移除 env 文件: %s\" ;;\n \"uninstall.removing_env.en\") text=\"Removing env file: %s\" ;;\n \"uninstall.removing_proxy.zh\") text=\"正在停止并移除 Docker API 代理容器: agentteams-docker-proxy\" ;;\n \"uninstall.removing_proxy.en\") text=\"Stopping and removing Docker API proxy container: agentteams-docker-proxy\" ;;\n \"uninstall.stopping_controller.zh\") text=\"正在停止并移除 agentteams-controller (内嵌 Tuwunel/MinIO/Higress)...\" ;;\n \"uninstall.stopping_controller.en\") text=\"Stopping and removing agentteams-controller (embedded Tuwunel/MinIO/Higress)...\" ;;\n \"uninstall.removing_network.zh\") text=\"正在移除 Docker 网络: agentteams-net\" ;;\n \"uninstall.removing_network.en\") text=\"Removing Docker network: agentteams-net\" ;;\n \"uninstall.removing_workspace.zh\") text=\"正在移除工作空间目录: %s\" ;;\n \"uninstall.removing_workspace.en\") text=\"Removing workspace directory: %s\" ;;\n \"uninstall.removing_workspace_elevated.zh\") text=\" 工作空间包含 root 文件,通过容器清理...\" ;;\n \"uninstall.removing_workspace_elevated.en\") text=\" Workspace contains root-owned files, cleaning via container...\" ;;\n \"uninstall.removing_log.zh\") text=\"正在移除日志文件: %s\" ;;\n \"uninstall.removing_log.en\") text=\"Removing log file: %s\" ;;\n \"uninstall.done.zh\") text=\"AgentTeams 已卸载。\" ;;\n \"uninstall.done.en\") text=\"AgentTeams has been uninstalled.\" ;;\n # --- Error messages ---\n \"error.name_required.zh\") text=\"--name 是必需的\" ;;\n \"error.name_required.en\") text=\"--name is required\" ;;\n \"error.fs_required.zh\") text=\"--fs 是必需的\" ;;\n \"error.fs_required.en\") text=\"--fs is required\" ;;\n \"error.fs_key_required.zh\") text=\"--fs-key 是必需的\" ;;\n \"error.fs_key_required.en\") text=\"--fs-key is required\" ;;\n \"error.fs_secret_required.zh\") text=\"--fs-secret 是必需的\" ;;\n \"error.fs_secret_required.en\") text=\"--fs-secret is required\" ;;\n \"error.unknown_option.zh\") text=\"未知选项: %s\" ;;\n \"error.unknown_option.en\") text=\"Unknown option: %s\" ;;\n \"error.docker_not_found.zh\") text=\"未找到 docker 或 podman 命令。请先安装 Docker Desktop 或 Podman Desktop:\\n Docker Desktop: https://www.docker.com/products/docker-desktop/\\n Podman Desktop: https://podman-desktop.io/\" ;;\n \"error.docker_not_found.en\") text=\"docker or podman command not found. Please install Docker Desktop or Podman Desktop first:\\n Docker Desktop: https://www.docker.com/products/docker-desktop/\\n Podman Desktop: https://podman-desktop.io/\" ;;\n \"error.docker_not_running.zh\") text=\"Docker 未运行。请先启动 Docker Desktop 或 Podman Desktop。\" ;;\n \"error.docker_not_running.en\") text=\"Docker is not running. Please start Docker Desktop or Podman Desktop first.\" ;;\n # --- Fallback: try English for unknown lang ---\n *)\n case \"${key}.en\" in\n \"tz.warning.title.en\") text=\"Could not detect timezone automatically.\" ;;\n \"install.title.en\") text=\"=== AgentTeams Manager Installation ===\" ;;\n *) text=\"${key}\" ;;\n esac\n ;;\n esac\n if [ $# -gt 0 ]; then\n # shellcheck disable=SC2059\n printf \"${text}\\n\" \"$@\"\n else\n echo \"${text}\"\n fi\n}\n\n# ============================================================\n# Registry selection based on timezone\n# ============================================================\n\ndetect_registry() {\n local tz=\"${AGENTTEAMS_TIMEZONE}\"\n\n case \"${tz}\" in\n America/*)\n echo \"higress-registry.us-west-1.cr.aliyuncs.com\"\n ;;\n Asia/Singapore|Asia/Bangkok|Asia/Jakarta|Asia/Makassar|Asia/Jayapura|\\\n Asia/Kuala_Lumpur|Asia/Ho_Chi_Minh|Asia/Manila|Asia/Yangon|\\\n Asia/Vientiane|Asia/Phnom_Penh|Asia/Pontianak|Asia/Ujung_Pandang)\n echo \"higress-registry.ap-southeast-7.cr.aliyuncs.com\"\n ;;\n *)\n echo \"higress-registry.cn-hangzhou.cr.aliyuncs.com\"\n ;;\n esac\n}\n\nAGENTTEAMS_REGISTRY=\"${AGENTTEAMS_REGISTRY:-$(detect_registry)}\"\n# Backward compatibility: accept old env var names from previous versions\nAGENTTEAMS_INSTALL_CONTROLLER_IMAGE=\"${AGENTTEAMS_INSTALL_CONTROLLER_IMAGE:-${AGENTTEAMS_INSTALL_DOCKER_PROXY_IMAGE:-}}\"\n# Image variables are resolved after version selection in step_version().\n# These placeholders allow early code paths to reference them without errors.\nMANAGER_IMAGE=\"${AGENTTEAMS_INSTALL_MANAGER_IMAGE:-}\"\nMANAGER_COPAW_IMAGE=\"${AGENTTEAMS_INSTALL_MANAGER_COPAW_IMAGE:-}\"\nWORKER_IMAGE=\"${AGENTTEAMS_INSTALL_WORKER_IMAGE:-}\"\nCOPAW_WORKER_IMAGE=\"${AGENTTEAMS_INSTALL_COPAW_WORKER_IMAGE:-}\"\nHERMES_WORKER_IMAGE=\"${AGENTTEAMS_INSTALL_HERMES_WORKER_IMAGE:-}\"\nCONTROLLER_IMAGE=\"${AGENTTEAMS_INSTALL_CONTROLLER_IMAGE:-}\"\n\nresolve_image_tags() {\n AGENTTEAMS_VERSION=\"$(_normalize_version \"${AGENTTEAMS_VERSION}\")\"\n MANAGER_IMAGE=\"${AGENTTEAMS_INSTALL_MANAGER_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-manager:${AGENTTEAMS_VERSION}}\"\n MANAGER_COPAW_IMAGE=\"${AGENTTEAMS_INSTALL_MANAGER_COPAW_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-manager-copaw:${AGENTTEAMS_VERSION}}\"\n WORKER_IMAGE=\"${AGENTTEAMS_INSTALL_WORKER_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-worker:${AGENTTEAMS_VERSION}}\"\n COPAW_WORKER_IMAGE=\"${AGENTTEAMS_INSTALL_COPAW_WORKER_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-copaw-worker:${AGENTTEAMS_VERSION}}\"\n HERMES_WORKER_IMAGE=\"${AGENTTEAMS_INSTALL_HERMES_WORKER_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-hermes-worker:${AGENTTEAMS_VERSION}}\"\n EMBEDDED_IMAGE=\"${AGENTTEAMS_INSTALL_EMBEDDED_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-embedded:${AGENTTEAMS_VERSION}}\"\n # CoPaw Worker introduced in v1.0.4; Hermes Worker introduced in v1.1.0\n if [ -z \"${AGENTTEAMS_INSTALL_COPAW_WORKER_IMAGE:-}\" ] && _ver_lt \"${AGENTTEAMS_VERSION}\" \"v1.0.4\"; then\n COPAW_WORKER_IMAGE=\"\"\n fi\n if [ -z \"${AGENTTEAMS_INSTALL_HERMES_WORKER_IMAGE:-}\" ] && _ver_lt \"${AGENTTEAMS_VERSION}\" \"v1.1.0\"; then\n HERMES_WORKER_IMAGE=\"\"\n fi\n}\n\n# Resolve the embedded controller image. Embedded mode is the only supported\n# architecture since PR #616 (manager image no longer bundles Higress/Tuwunel/MinIO).\n# If the embedded image is unavailable for the requested version, fail fast with an\n# actionable error rather than silently falling back to the legacy single-container\n# path — that path is permanently broken with the slim manager image and would just\n# leave the user with a manager container looping on \"Waiting for Higress Gateway\".\n# Sets EMBEDDED_IMAGE and AGENTTEAMS_USE_EMBEDDED.\nresolve_embedded_image() {\n AGENTTEAMS_USE_EMBEDDED=1\n\n # If the user explicitly overrode the image (e.g. `make install-embedded` passes\n # a locally-built tag), respect it as-is without any registry probe.\n if [ -n \"${AGENTTEAMS_INSTALL_EMBEDDED_IMAGE:-}\" ]; then\n EMBEDDED_IMAGE=\"${AGENTTEAMS_INSTALL_EMBEDDED_IMAGE}\"\n return 0\n fi\n\n local _versioned=\"${AGENTTEAMS_REGISTRY}/agentteams/agentteams-embedded:${AGENTTEAMS_VERSION}\"\n local _latest=\"${AGENTTEAMS_REGISTRY}/agentteams/agentteams-embedded:latest\"\n\n # Skip probe when AGENTTEAMS_VERSION is \"latest\" — no point trying the same tag twice.\n if [ \"${AGENTTEAMS_VERSION}\" = \"latest\" ]; then\n EMBEDDED_IMAGE=\"${_latest}\"\n return 0\n fi\n\n if ${DOCKER_CMD} pull \"${_versioned}\" >/dev/null 2>&1; then\n EMBEDDED_IMAGE=\"${_versioned}\"\n return 0\n fi\n\n # Versions before v1.1.0 predate agentteams-embedded entirely — their manager image\n # bundled all infrastructure. Falling back to agentteams-embedded:latest would\n # silently swap in the v1.1.0 architecture (embedded kube-apiserver) which\n # crashes under QEMU on Apple Silicon. Auto-activate legacy mode instead.\n if _ver_lt \"${AGENTTEAMS_VERSION}\" \"v1.1.0\"; then\n log \"INFO: ${AGENTTEAMS_VERSION} predates agentteams-embedded; switching to legacy all-in-one manager architecture.\"\n log \"WARNING: Legacy all-in-one mode requires AGENTTEAMS_VERSION <= v1.0.9 (older bundled manager image).\"\n log \"WARNING: Newer slim manager images will hang on 'Waiting for Higress Gateway'.\"\n AGENTTEAMS_USE_EMBEDDED=0\n return 0\n fi\n\n if ${DOCKER_CMD} pull \"${_latest}\" >/dev/null 2>&1; then\n log \"embedded ${AGENTTEAMS_VERSION} not found, using latest\"\n EMBEDDED_IMAGE=\"${_latest}\"\n return 0\n fi\n\n # Explicit escape hatch — still honoured for edge cases.\n if [ \"${AGENTTEAMS_FORCE_LEGACY:-0}\" = \"1\" ]; then\n log \"WARNING: AGENTTEAMS_FORCE_LEGACY=1 — using legacy all-in-one manager architecture.\"\n log \"WARNING: This requires AGENTTEAMS_VERSION <= v1.0.9 (older bundled manager image).\"\n log \"WARNING: Newer slim manager images will hang on 'Waiting for Higress Gateway'.\"\n AGENTTEAMS_USE_EMBEDDED=0\n return 0\n fi\n\n error \"Embedded controller image is not available in the registry:\"\n error \" - tried: ${_versioned}\"\n error \" - tried: ${_latest}\"\n error \"\"\n error \"Embedded mode is the only supported architecture since PR #616.\"\n error \"How to resolve:\"\n error \" 1) Pin to a AGENTTEAMS_VERSION whose embedded image has been published, or\"\n error \" wait for the release pipeline to publish it.\"\n error \" 2) For a local build, run: make install-embedded\"\n error \" (builds and uses the local embedded image without touching the registry).\"\n error \" 3) Override with a custom image: AGENTTEAMS_INSTALL_EMBEDDED_IMAGE=... ./agentteams-install.sh\"\n exit 1\n}\n\n# ============================================================\n# Known models list — used to detect custom models during install\n# ============================================================\nKNOWN_MODELS=\"gpt-5.4 gpt-5.3-codex gpt-5-mini gpt-5-nano claude-opus-4-6 claude-sonnet-4-6 claude-haiku-4-5 qwen3.6-plus qwen3.5-plus deepseek-chat deepseek-reasoner kimi-k2.5 glm-5 MiniMax-M2.7 MiniMax-M2.7-highspeed MiniMax-M2.5\"\n\nis_known_model() {\n local model=\"$1\"\n for m in ${KNOWN_MODELS}; do\n [ \"${m}\" = \"${model}\" ] && return 0\n done\n return 1\n}\n\n# Prompt user for custom model parameters when model is not in the known list.\n# Sets: AGENTTEAMS_MODEL_CONTEXT_WINDOW, AGENTTEAMS_MODEL_MAX_TOKENS, AGENTTEAMS_MODEL_REASONING, AGENTTEAMS_MODEL_VISION\nprompt_custom_model_params() {\n local model=\"$1\"\n if is_known_model \"${model}\"; then\n # Clear any stale custom params for known models\n AGENTTEAMS_MODEL_CONTEXT_WINDOW=\"\"\n AGENTTEAMS_MODEL_MAX_TOKENS=\"\"\n AGENTTEAMS_MODEL_REASONING=\"\"\n AGENTTEAMS_MODEL_VISION=\"\"\n return\n fi\n echo \"\"\n log \"$(msg llm.custom_model.detected \"${model}\")\"\n echo \"\"\n read -e -p \" $(msg llm.custom_model.context_prompt): \" AGENTTEAMS_MODEL_CONTEXT_WINDOW\n if [ \"${AGENTTEAMS_MODEL_CONTEXT_WINDOW}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n AGENTTEAMS_MODEL_CONTEXT_WINDOW=\"${AGENTTEAMS_MODEL_CONTEXT_WINDOW:-150000}\"\n read -e -p \" $(msg llm.custom_model.max_tokens_prompt): \" AGENTTEAMS_MODEL_MAX_TOKENS\n if [ \"${AGENTTEAMS_MODEL_MAX_TOKENS}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n AGENTTEAMS_MODEL_MAX_TOKENS=\"${AGENTTEAMS_MODEL_MAX_TOKENS:-128000}\"\n read -e -p \" $(msg llm.custom_model.reasoning_prompt): \" _reasoning\n if [ \"${_reasoning}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n case \"${_reasoning}\" in\n n|N|no|NO) AGENTTEAMS_MODEL_REASONING=\"false\" ;;\n *) AGENTTEAMS_MODEL_REASONING=\"true\" ;;\n esac\n read -e -p \" $(msg llm.custom_model.vision_prompt): \" _vision\n if [ \"${_vision}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n case \"${_vision}\" in\n y|Y|yes|YES) AGENTTEAMS_MODEL_VISION=\"true\" ;;\n *) AGENTTEAMS_MODEL_VISION=\"false\" ;;\n esac\n log \"$(msg llm.custom_model.summary \"${AGENTTEAMS_MODEL_CONTEXT_WINDOW}\" \"${AGENTTEAMS_MODEL_MAX_TOKENS}\" \"${AGENTTEAMS_MODEL_REASONING}\" \"${AGENTTEAMS_MODEL_VISION}\")\"\n}\n\n# ============================================================\n# Wait for Manager agent to be ready\n# Uses `openclaw gateway health` inside the container to confirm the gateway is running\n# ============================================================\n\nwait_manager_ready() {\n local timeout=\"${AGENTTEAMS_READY_TIMEOUT:-300}\"\n local elapsed=0\n local container=\"${1:-agentteams-manager}\"\n\n log \"$(msg install.wait_ready \"${timeout}\")\"\n\n # Wait for Manager agent to be healthy inside the container\n local runtime=\"${AGENTTEAMS_MANAGER_RUNTIME:-copaw}\"\n while [ \"${elapsed}\" -lt \"${timeout}\" ]; do\n case \"${runtime}\" in\n copaw)\n if ${DOCKER_CMD} exec \"${container}\" curl -sf http://127.0.0.1:18799/api/agents 2>/dev/null | grep -q '\"agents\"'; then\n log \"$(msg install.wait_ready.ok)\"\n return 0\n fi\n ;;\n *)\n if ${DOCKER_CMD} exec \"${container}\" openclaw gateway health --json 2>/dev/null | grep -q '\"ok\"' 2>/dev/null; then\n log \"$(msg install.wait_ready.ok)\"\n return 0\n fi\n ;;\n esac\n sleep 5\n elapsed=$((elapsed + 5))\n printf \"\\r\\033[36m[AgentTeams]\\033[0m $(msg install.wait_ready.waiting \"${elapsed}\" \"${timeout}\")\"\n done\n\n echo \"\"\n die \"$(msg install.wait_ready.timeout \"${timeout}\" \"${container}\")\"\n}\n\nwait_matrix_ready() {\n local timeout=\"${AGENTTEAMS_READY_TIMEOUT:-300}\"\n local elapsed=0\n local container=\"${1:-agentteams-manager}\"\n\n log \"$(msg install.wait_matrix \"${timeout}\")\"\n\n while [ \"${elapsed}\" -lt \"${timeout}\" ]; do\n if ${DOCKER_CMD} exec \"${container}\" curl -sf http://127.0.0.1:6167/_tuwunel/server_version >/dev/null 2>&1; then\n log \"$(msg install.wait_matrix.ok)\"\n return 0\n fi\n sleep 5\n elapsed=$((elapsed + 5))\n printf \"\\r\\033[36m[AgentTeams]\\033[0m $(msg install.wait_matrix.waiting \"${elapsed}\" \"${timeout}\")\"\n done\n\n echo \"\"\n die \"$(msg install.wait_matrix.timeout \"${timeout}\" \"${container}\")\"\n}\n\n# Read KEY=value from /data/agentteams-secrets.env on a Docker volume (manager container not required).\n# Requires EMBEDDED_IMAGE (resolved earlier in install_manager). Uses ${DOCKER_CMD}.\nagentteams_read_secret_from_data_volume() {\n local _vol=\"$1\" _key=\"$2\"\n if [ -z \"${_vol}\" ] || [ -z \"${_key}\" ] || [ -z \"${EMBEDDED_IMAGE:-}\" ]; then\n echo \"\"\n return 0\n fi\n ${DOCKER_CMD} run --rm --entrypoint sh \\\n -v \"${_vol}:/data:ro\" \\\n \"${EMBEDDED_IMAGE}\" -c \"grep \\\"^${_key}=\\\" /data/agentteams-secrets.env 2>/dev/null | cut -d= -f2- | head -1 | tr -d '\\r'\" 2>/dev/null\n}\n\n# Read KEY=value from /data/worker-creds/<worker>.env on a Docker volume.\nagentteams_read_worker_creds_value_from_volume() {\n local _vol=\"$1\" _worker=\"$2\" _key=\"$3\"\n if [ -z \"${_vol}\" ] || [ -z \"${_worker}\" ] || [ -z \"${_key}\" ] || [ -z \"${EMBEDDED_IMAGE:-}\" ]; then\n echo \"\"\n return 0\n fi\n ${DOCKER_CMD} run --rm --entrypoint sh \\\n -v \"${_vol}:/data:ro\" \\\n \"${EMBEDDED_IMAGE}\" -c \"grep \\\"^${_key}=\\\" \\\"/data/worker-creds/${_worker}.env\\\" 2>/dev/null | cut -d= -f2- | head -1 | tr -d \\\"\\\\r\\\"\" 2>/dev/null\n}\n\n# Read admin_dm_room_id from host workspace state.json (fallback when Matrix API is unavailable).\nagentteams_read_admin_dm_room_from_workspace() {\n local _ws=\"$1\"\n local _f=\"${_ws}/state.json\"\n if [ ! -f \"${_f}\" ] || ! command -v jq >/dev/null 2>&1; then\n echo \"\"\n return 0\n fi\n jq -r '.admin_dm_room_id // empty | select(. != \"null\")' \"${_f}\" 2>/dev/null\n}\n\n# Read secret input with masked echo (shows * per keystroke, supports backspace)\n# Usage: read_secret \"prompt text: \"; value=\"${_RS_RESULT}\"\nread_secret() {\n local _rs_prompt=\"$1\"\n _RS_RESULT=\"\"\n local _rs_char=\"\"\n\n printf \"%s\" \"${_rs_prompt}\"\n\n while IFS= read -r -s -n 1 _rs_char; do\n if [[ -z \"${_rs_char}\" ]]; then\n break\n elif [[ \"${_rs_char}\" == $'\\177' ]] || [[ \"${_rs_char}\" == $'\\b' ]]; then\n if [ -n \"${_RS_RESULT}\" ]; then\n _RS_RESULT=\"${_RS_RESULT%?}\"\n printf \"\\b \\b\"\n fi\n else\n _RS_RESULT=\"${_RS_RESULT}${_rs_char}\"\n printf \"*\"\n fi\n done\n echo\n}\n\n# Load current parameter values from env file for upgrade mode display\nload_current_params_from_env() {\n local env_file=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n if [ -f \"${env_file}\" ]; then\n [ -z \"${AGENTTEAMS_LLM_PROVIDER:+x}\" ] && AGENTTEAMS_LLM_PROVIDER=\"$(grep '^AGENTTEAMS_LLM_PROVIDER=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_OPENAI_BASE_URL:+x}\" ] && AGENTTEAMS_OPENAI_BASE_URL=\"$(grep '^AGENTTEAMS_OPENAI_BASE_URL=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_DEFAULT_MODEL:+x}\" ] && AGENTTEAMS_DEFAULT_MODEL=\"$(grep '^AGENTTEAMS_DEFAULT_MODEL=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_EMBEDDING_MODEL:+x}\" ] && AGENTTEAMS_EMBEDDING_MODEL=\"$(grep '^AGENTTEAMS_EMBEDDING_MODEL=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_WORKSPACE_DIR:+x}\" ] && AGENTTEAMS_WORKSPACE_DIR=\"$(grep '^AGENTTEAMS_WORKSPACE_DIR=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_HOST_SHARE_DIR:+x}\" ] && AGENTTEAMS_HOST_SHARE_DIR=\"$(grep '^AGENTTEAMS_HOST_SHARE_DIR=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_PODMAN_AUTOSTART:+x}\" ] && AGENTTEAMS_PODMAN_AUTOSTART=\"$(grep '^AGENTTEAMS_PODMAN_AUTOSTART=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_DASHBOARD:+x}\" ] && AGENTTEAMS_DASHBOARD=\"$(grep '^AGENTTEAMS_DASHBOARD=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_DASHBOARD_VERSION:+x}\" ] && AGENTTEAMS_DASHBOARD_VERSION=\"$(grep '^AGENTTEAMS_DASHBOARD_VERSION=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_PORT_DASHBOARD:+x}\" ] && AGENTTEAMS_PORT_DASHBOARD=\"$(grep '^AGENTTEAMS_PORT_DASHBOARD=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_DASHBOARD_IMAGE:+x}\" ] && AGENTTEAMS_DASHBOARD_IMAGE=\"$(grep '^AGENTTEAMS_DASHBOARD_IMAGE=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL:+x}\" ] && AGENTTEAMS_AI_GATEWAY_ADMIN_URL=\"$(grep '^AGENTTEAMS_AI_GATEWAY_ADMIN_URL=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n fi\n}\n\n# In non-interactive mode, uses default or errors if required and no default.\n# Usage: prompt VAR_NAME \"Prompt text\" \"default\" [true=secret]\nprompt() {\n local var_name=\"$1\"\n local prompt_text=\"$2\"\n local default_value=\"$3\"\n local is_secret=\"${4:-false}\"\n\n # If the variable is already set in the environment, use it silently\n # In upgrade mode, show current value and let user change it\n eval \"local current_value=\\\"\\${${var_name}}\\\"\"\n if [ -n \"${current_value}\" ]; then\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_NON_INTERACTIVE}\" != \"1\" ]; then\n # Show masked value for secrets, full value otherwise\n local display_value=\"${current_value}\"\n if [ \"${is_secret}\" = \"true\" ]; then\n local len=${#current_value}\n if [ \"${len}\" -le 8 ]; then\n display_value=\"****\"\n else\n display_value=\"${current_value:0:4}****${current_value: -4}\"\n fi\n fi\n log \"$(msg prompt.upgrade_keep \"${prompt_text}\" \"${display_value}\")\"\n local new_value=\"\"\n if [ \"${is_secret}\" = \"true\" ]; then\n read_secret \"${prompt_text}: \"\n new_value=\"${_RS_RESULT}\"\n else\n read -e -p \"${prompt_text}: \" new_value\n if [ \"${new_value}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n fi\n if [ -n \"${new_value}\" ]; then\n eval \"export ${var_name}='${new_value}'\"\n fi\n return\n fi\n log \"$(msg prompt.preset \"${prompt_text}\")\"\n return\n fi\n\n # Non-interactive or quickstart: use default or error\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] || [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ]; then\n if [ -n \"${default_value}\" ]; then\n eval \"export ${var_name}='${default_value}'\"\n log \"$(msg prompt.default \"${prompt_text}\" \"${default_value}\")\"\n return\n elif [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n # Only hard-error in fully non-interactive mode, not quickstart\n die \"$(msg prompt.required \"${prompt_text}\")\"\n fi\n # quickstart + no default: fall through to interactive prompt below\n fi\n\n if [ -n \"${default_value}\" ]; then\n prompt_text=\"${prompt_text} [${default_value}]\"\n fi\n\n local value=\"\"\n if [ \"${is_secret}\" = \"true\" ]; then\n read_secret \"${prompt_text}: \"\n value=\"${_RS_RESULT}\"\n else\n read -e -p \"${prompt_text}: \" value\n if [ \"${value}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n fi\n\n value=\"${value:-${default_value}}\"\n if [ -z \"${value}\" ]; then\n die \"$(msg prompt.required_empty \"${prompt_text}\")\"\n fi\n\n eval \"export ${var_name}='${value}'\"\n}\n\n# Prompt for an optional value (empty string is acceptable)\n# Skips prompt if variable is already defined in environment (even if empty)\n# In upgrade mode, shows current value and lets user change it.\n# In non-interactive mode, defaults to empty string.\nprompt_optional() {\n local var_name=\"$1\"\n local prompt_text=\"$2\"\n local is_secret=\"${3:-false}\"\n\n # Check if variable is defined (even if set to empty string)\n eval \"local _chk=\\\"\\${${var_name}+x}\\\"\"\n if [ -n \"${_chk}\" ]; then\n # In upgrade mode, show current value and let user change it\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_NON_INTERACTIVE}\" != \"1\" ]; then\n eval \"local current_value=\\\"\\${${var_name}}\\\"\"\n local display_value=\"${current_value}\"\n if [ \"${is_secret}\" = \"true\" ] && [ -n \"${current_value}\" ]; then\n local len=${#current_value}\n if [ \"${len}\" -le 8 ]; then\n display_value=\"****\"\n else\n display_value=\"${current_value:0:4}****${current_value: -4}\"\n fi\n fi\n if [ -n \"${current_value}\" ]; then\n if [ \"${is_secret}\" = \"true\" ]; then\n log \"$(msg prompt.upgrade_keep_secret \"${prompt_text}\" \"${display_value}\")\"\n else\n log \"$(msg prompt.upgrade_keep \"${prompt_text}\" \"${display_value}\")\"\n fi\n else\n log \"$(msg prompt.upgrade_empty \"${prompt_text}\")\"\n fi\n local new_value=\"\"\n if [ \"${is_secret}\" = \"true\" ]; then\n read_secret \"${prompt_text}: \"\n new_value=\"${_RS_RESULT}\"\n else\n read -e -p \"${prompt_text}: \" new_value\n if [ \"${new_value}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n fi\n if [ -n \"${new_value}\" ]; then\n eval \"export ${var_name}='${new_value}'\"\n fi\n return\n fi\n log \"$(msg prompt.preset \"${prompt_text}\")\"\n return\n fi\n\n # Non-interactive or quickstart: skip, leave unset\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] || [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ]; then\n eval \"export ${var_name}=''\"\n return\n fi\n\n local value=\"\"\n if [ \"${is_secret}\" = \"true\" ]; then\n read_secret \"${prompt_text}: \"\n value=\"${_RS_RESULT}\"\n else\n read -e -p \"${prompt_text}: \" value\n if [ \"${value}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n fi\n\n eval \"export ${var_name}='${value}'\"\n}\n\ngenerate_key() {\n openssl rand -hex 32\n}\n\n# ============================================================\n# Detect container runtime socket on the host\n#\n# Dependency: Requires DOCKER_CMD to reflect the TRUE underlying runtime.\n# (Crucial for unmasking 'podman' disguised as 'docker'.\n# Must be strictly resolved prior, e.g., via check_container_runtime)\n# ============================================================\ndetect_socket() {\n local socket_path\n\n # 1. Respect explicitly defined DOCKER_HOST environment variable\n if [ -n \"${DOCKER_HOST}\" ]; then\n socket_path=$(echo \"${DOCKER_HOST}\" | sed 's|^unix://||')\n if [ -S \"${socket_path}\" ]; then\n echo \"${socket_path}\"\n return 0\n fi\n fi\n\n # 2. Route based on the resolved container runtime\n case \"${DOCKER_CMD}\" in\n docker)\n # Docker path handling\n socket_path=$(docker context ls --format '{{if .Current}}{{.DockerEndpoint}}{{end}}' 2>/dev/null | grep . | sed 's|^unix://||')\n if [ -n \"${socket_path}\" ] && [ -S \"${socket_path}\" ]; then\n echo \"${socket_path}\"\n return 0\n fi\n\n # Rootless Docker\n if [ -n \"${XDG_RUNTIME_DIR:-}\" ] && [ -S \"${XDG_RUNTIME_DIR}/docker.sock\" ]; then\n echo \"${XDG_RUNTIME_DIR}/docker.sock\"\n return 0\n fi\n\n # Rootless Docker (macOS fallback)\n if [ -S \"${HOME}/.docker/run/docker.sock\" ]; then\n echo \"${HOME}/.docker/run/docker.sock\"\n return 0\n fi\n\n # Root Docker\n if [ -S \"/var/run/docker.sock\" ]; then\n echo \"/var/run/docker.sock\"\n return 0\n fi\n ;;\n podman)\n # Podman path handling\n if [ \"$(id -u)\" -eq 0 ]; then\n socket_path=\"/run/podman/podman.sock\"\n else\n socket_path=\"${XDG_RUNTIME_DIR}/podman/podman.sock\"\n fi\n if [ -S \"${socket_path}\" ]; then\n echo \"${socket_path}\"\n return 0\n fi\n ;;\n esac\n\n # Return empty if no socket is found\n echo \"\"\n return 0\n}\n\n# Ensure Podman API socket is active (State mutation)\nensure_podman_socket() {\n local socket_path\n\n # Only applicable if runtime is podman and systemctl is available\n if [ \"${DOCKER_CMD:-}\" != \"podman\" ] || ! command -v systemctl >/dev/null 2>&1; then\n return 0\n fi\n\n echo \"Ensuring Podman API socket is active...\" >&2\n\n # Enable and start the socket based on user privileges\n if [ \"$(id -u)\" -eq 0 ]; then\n socket_path=\"/run/podman/podman.sock\"\n systemctl enable --now podman.socket >/dev/null 2>&1 || true\n else\n socket_path=\"${XDG_RUNTIME_DIR}/podman/podman.sock\"\n systemctl --user enable --now podman.socket >/dev/null 2>&1 || true\n fi\n\n # Give systemd a brief moment to assert the socket for root or rootless user\n local _i\n for _i in 1 2 3; do\n [ -S \"${socket_path}\" ] && break\n sleep 1\n done\n}\n\n# Detect local LAN IP address (cross-platform: macOS and Linux)\ndetect_lan_ip() {\n local ip=\"\"\n\n # macOS: try common Wi-Fi / Ethernet interfaces\n if command -v ipconfig >/dev/null 2>&1; then\n for iface in en0 en1 en2 en3 en4; do\n ip=$(ipconfig getifaddr \"${iface}\" 2>/dev/null)\n if [ -n \"${ip}\" ]; then\n echo \"${ip}\"\n return 0\n fi\n done\n fi\n\n # Linux: ip route — most reliable\n if command -v ip >/dev/null 2>&1; then\n ip=$(ip route get 1.1.1.1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i==\"src\"){print $(i+1); exit}}')\n if [ -n \"${ip}\" ]; then\n echo \"${ip}\"\n return 0\n fi\n fi\n\n # Linux fallback: hostname -I (space-separated list, take first non-loopback)\n if command -v hostname >/dev/null 2>&1; then\n ip=$(hostname -I 2>/dev/null | tr ' ' '\\n' | grep -v '^127\\.' | grep -v '^::' | head -1)\n if [ -n \"${ip}\" ]; then\n echo \"${ip}\"\n return 0\n fi\n fi\n\n # Last resort: ifconfig\n if command -v ifconfig >/dev/null 2>&1; then\n ip=$(ifconfig 2>/dev/null | awk '/inet /{if($2!~/^127\\./){print $2; exit}}')\n # Strip \"addr:\" prefix that some ifconfig versions add\n ip=\"${ip#addr:}\"\n if [ -n \"${ip}\" ]; then\n echo \"${ip}\"\n return 0\n fi\n fi\n\n echo \"\"\n}\n\n\n# ============================================================\n# Step-back navigation helpers\n# ============================================================\n\n# should_skip_step: returns 0 (skip) when the step is irrelevant in current mode\nshould_skip_step() {\n local step_fn=\"$1\"\n case \"${step_fn}\" in\n step_lang|step_mode)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n ;;\n step_version)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n ;;\n step_existing)\n local _env=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n [ ! -f \"${_env}\" ] && return 0\n ;;\n # Keep-All upgrade mode: skip all config steps (step_volume/step_workspace handled separately)\n step_llm|step_admin|step_network|step_ports|step_domains|step_github|step_skills|step_runtime|step_manager_runtime|step_dashboard|step_e2ee|step_docker_proxy|step_idle|step_hostshare)\n [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ] && return 0\n ;;\n step_volume|step_workspace)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ] && return 0\n [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ] && return 0\n ;;\n step_e2ee|step_idle)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE}\" != \"1\" ] && return 0\n ;;\n step_docker_proxy)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE}\" != \"1\" ] && return 0\n # Embedded mode handles docker access natively — skip this step\n [ \"${AGENTTEAMS_USE_EMBEDDED:-}\" = \"1\" ] && return 0\n ;;\n step_manager_runtime)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n ;;\n step_hostshare)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ] && return 0\n ;;\n step_podman_autostart)\n # Only relevant for Podman with systemd\n [ \"${DOCKER_CMD}\" != \"podman\" ] && return 0\n ! command -v systemctl >/dev/null 2>&1 && return 0\n\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE}\" != \"1\" ] && return 0\n [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ] && return 0\n ;;\n esac\n return 1\n}\n\n# clear_step_vars: unset variables set by a step so it will re-prompt on re-entry\nclear_step_vars() {\n local step_fn=\"$1\"\n case \"${step_fn}\" in\n step_mode) unset AGENTTEAMS_QUICKSTART ;;\n step_version) unset AGENTTEAMS_VERSION ;;\n step_existing) unset AGENTTEAMS_UPGRADE UPGRADE_EXISTING_WORKERS ;;\n step_llm)\n unset AGENTTEAMS_LLM_PROVIDER AGENTTEAMS_DEFAULT_MODEL AGENTTEAMS_OPENAI_BASE_URL\n unset AGENTTEAMS_LLM_API_KEY AGENTTEAMS_MODEL_CONTEXT_WINDOW AGENTTEAMS_MODEL_MAX_TOKENS\n unset AGENTTEAMS_MODEL_REASONING AGENTTEAMS_MODEL_VISION\n ;;\n step_admin) unset AGENTTEAMS_ADMIN_USER AGENTTEAMS_ADMIN_PASSWORD ;;\n step_network) unset AGENTTEAMS_LOCAL_ONLY ;;\n step_ports)\n unset AGENTTEAMS_PORT_GATEWAY AGENTTEAMS_PORT_CONSOLE\n unset AGENTTEAMS_PORT_ELEMENT_WEB AGENTTEAMS_PORT_MANAGER_CONSOLE\n ;;\n step_domains)\n unset AGENTTEAMS_MATRIX_DOMAIN AGENTTEAMS_MATRIX_CLIENT_DOMAIN\n unset AGENTTEAMS_AI_GATEWAY_DOMAIN AGENTTEAMS_FS_DOMAIN AGENTTEAMS_CONSOLE_DOMAIN\n ;;\n step_github) unset AGENTTEAMS_GITHUB_TOKEN ;;\n step_skills) unset AGENTTEAMS_SKILLS_API_URL ;;\n step_volume) unset AGENTTEAMS_DATA_DIR ;;\n step_workspace) unset AGENTTEAMS_WORKSPACE_DIR ;;\n step_dashboard) unset AGENTTEAMS_DASHBOARD AGENTTEAMS_DASHBOARD_VERSION AGENTTEAMS_PORT_DASHBOARD AGENTTEAMS_DASHBOARD_IMAGE AGENTTEAMS_AI_GATEWAY_ADMIN_URL ;;\n step_runtime) unset AGENTTEAMS_DEFAULT_WORKER_RUNTIME ;;\n step_manager_runtime) unset AGENTTEAMS_MANAGER_RUNTIME ;;\n step_e2ee) unset AGENTTEAMS_MATRIX_E2EE ;;\n step_docker_proxy) unset AGENTTEAMS_DOCKER_PROXY; unset AGENTTEAMS_PROXY_ALLOWED_REGISTRIES ;;\n step_idle) unset AGENTTEAMS_WORKER_IDLE_TIMEOUT ;;\n step_hostshare) unset AGENTTEAMS_HOST_SHARE_DIR ;;\n step_podman_autostart) unset AGENTTEAMS_PODMAN_AUTOSTART ;;\n esac\n}\n\n# ============================================================\n# Individual step functions\n# ============================================================\n\nstep_lang() {\n local lang_default_choice=\"2\"\n [ \"${AGENTTEAMS_LANGUAGE}\" = \"zh\" ] && lang_default_choice=\"1\"\n log \"$(msg lang.detected)\"\n log \"$(msg lang.switch_title)\"\n echo \"$(msg lang.option_zh)\"\n echo \"$(msg lang.option_en)\"\n echo \"\"\n local LANG_CHOICE\n read -e -p \"$(msg lang.prompt) [${lang_default_choice}]: \" LANG_CHOICE\n LANG_CHOICE=\"${LANG_CHOICE:-${lang_default_choice}}\"\n if [ \"${LANG_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n case \"${LANG_CHOICE}\" in\n 1) AGENTTEAMS_LANGUAGE=\"zh\" ;;\n 2) AGENTTEAMS_LANGUAGE=\"en\" ;;\n esac\n export AGENTTEAMS_LANGUAGE\n log \"\"\n}\n\nstep_mode() {\n log \"$(msg install.mode.title)\"\n echo \"\"\n echo \"$(msg install.mode.choose)\"\n echo \"$(msg install.mode.quickstart)\"\n echo \"$(msg install.mode.manual)\"\n echo \"\"\n local ONBOARDING_CHOICE\n read -e -p \"$(msg install.mode.prompt): \" ONBOARDING_CHOICE\n ONBOARDING_CHOICE=\"${ONBOARDING_CHOICE:-1}\"\n if [ \"${ONBOARDING_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n case \"${ONBOARDING_CHOICE}\" in\n 1|quick|quickstart)\n log \"$(msg install.mode.quickstart_selected)\"\n AGENTTEAMS_QUICKSTART=1\n ;;\n 2|manual)\n log \"$(msg install.mode.manual_selected)\"\n ;;\n *)\n log \"$(msg install.mode.invalid)\"\n AGENTTEAMS_QUICKSTART=1\n ;;\n esac\n log \"\"\n}\n\nstep_version() {\n # Skip if version already provided via env var\n if [ -n \"${AGENTTEAMS_VERSION}\" ]; then\n resolve_image_tags\n return 0\n fi\n # Try to fetch the latest stable release from GitHub\n log \"$(msg install.version.fetching)\"\n local _fetched\n _fetched=$(curl -sf --max-time 5 \\\n -H \"Accept: application/vnd.github+json\" \\\n \"https://api.github.com/repos/agentscope-ai/AgentTeams/releases/latest\" \\\n 2>/dev/null | grep '\"tag_name\"' | head -1 | sed 's/.*\"tag_name\": *\"\\([^\"]*\\)\".*/\\1/')\n if [ -n \"${_fetched}\" ]; then\n AGENTTEAMS_KNOWN_STABLE_VERSION=\"${_fetched}\"\n else\n log \"$(msg install.version.fetch_failed \"${AGENTTEAMS_KNOWN_STABLE_VERSION}\")\"\n fi\n log \"$(msg install.version.title)\"\n echo \"\"\n echo \"$(msg install.version.choose)\"\n echo \"$(msg install.version.option_latest)\"\n printf \"%s\\n\" \"$(msg install.version.option_stable \"${AGENTTEAMS_KNOWN_STABLE_VERSION}\")\"\n echo \"$(msg install.version.option_custom)\"\n echo \"\"\n local VERSION_CHOICE\n read -e -p \"$(msg install.version.prompt) [1]: \" VERSION_CHOICE\n VERSION_CHOICE=\"${VERSION_CHOICE:-1}\"\n if [ \"${VERSION_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n case \"${VERSION_CHOICE}\" in\n 1|latest)\n AGENTTEAMS_VERSION=\"latest\"\n log \"$(msg install.version.selected_latest)\"\n ;;\n 2|stable)\n AGENTTEAMS_VERSION=\"${AGENTTEAMS_KNOWN_STABLE_VERSION}\"\n log \"$(msg install.version.selected_stable \"${AGENTTEAMS_VERSION}\")\"\n ;;\n 3|custom)\n local CUSTOM_VERSION\n read -e -p \"$(msg install.version.custom_prompt): \" CUSTOM_VERSION\n AGENTTEAMS_VERSION=\"${CUSTOM_VERSION:-${AGENTTEAMS_KNOWN_STABLE_VERSION}}\"\n log \"$(msg install.version.selected_custom \"${AGENTTEAMS_VERSION}\")\"\n ;;\n *)\n AGENTTEAMS_VERSION=\"${AGENTTEAMS_KNOWN_STABLE_VERSION}\"\n log \"$(msg install.version.invalid \"${AGENTTEAMS_VERSION}\")\"\n ;;\n esac\n log \"\"\n resolve_image_tags\n}\n\nstep_existing() {\n local existing_env=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n log \"$(msg install.existing.detected \"${existing_env}\")\"\n local running_manager=\"\" running_workers=\"\" existing_workers=\"\"\n if ${DOCKER_CMD} ps --format '{{.Names}}' | grep -q \"^agentteams-manager$\"; then\n running_manager=\"agentteams-manager\"\n fi\n running_workers=$(${DOCKER_CMD} ps --format '{{.Names}}' | grep \"^agentteams-worker-\" || true)\n existing_workers=$(${DOCKER_CMD} ps -a --format '{{.Names}}' | grep \"^agentteams-worker-\" || true)\n local UPGRADE_CHOICE\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n log \"$(msg install.existing.upgrade_noninteractive)\"\n UPGRADE_CHOICE=\"upgrade\"\n else\n echo \"\"\n echo \"$(msg install.existing.choose)\"\n echo \"$(msg install.existing.upgrade)\"\n echo \"$(msg install.existing.reinstall)\"\n echo \"$(msg install.existing.cancel)\"\n echo \"\"\n read -e -p \"$(msg install.existing.prompt): \" UPGRADE_CHOICE\n UPGRADE_CHOICE=\"${UPGRADE_CHOICE:-1}\"\n if [ \"${UPGRADE_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n fi\n case \"${UPGRADE_CHOICE}\" in\n 1|upgrade)\n AGENTTEAMS_UPGRADE=1\n log \"$(msg install.existing.upgrading)\"\n # Show upgrade mode sub-menu (unless already set via env var)\n if [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL:-0}\" != \"1\" ]; then\n echo \"\"\n echo \"$(msg upgrade.mode.prompt)\"\n echo \"$(msg upgrade.mode.keep_all)\"\n echo \"$(msg upgrade.mode.confirm_each)\"\n echo \"$(msg upgrade.mode.back)\"\n echo \"\"\n local UPGRADE_MODE_CHOICE\n read -e -p \"$(msg install.existing.prompt): \" UPGRADE_MODE_CHOICE\n UPGRADE_MODE_CHOICE=\"${UPGRADE_MODE_CHOICE:-1}\"\n case \"${UPGRADE_MODE_CHOICE}\" in\n 1|keep)\n AGENTTEAMS_UPGRADE_KEEP_ALL=1\n ;;\n 2|confirm)\n AGENTTEAMS_UPGRADE_KEEP_ALL=0\n ;;\n 3|b)\n STEP_RESULT=\"back\"\n return 0\n ;;\n *)\n AGENTTEAMS_UPGRADE_KEEP_ALL=0\n ;;\n esac\n fi\n # Load current parameters for both Keep-All and confirm-each modes\n load_current_params_from_env\n if [ -n \"${running_manager}\" ] || [ -n \"${running_workers}\" ]; then\n echo \"\"\n echo -e \"\\033[33m$(msg install.existing.warn_manager_stop)\\033[0m\"\n if [ -n \"${existing_workers}\" ]; then\n echo -e \"\\033[33m$(msg install.existing.warn_worker_recreate)\\033[0m\"\n fi\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" != \"1\" ]; then\n echo \"\"\n local CONFIRM_STOP\n read -e -p \"$(msg install.existing.continue_prompt): \" CONFIRM_STOP\n if [ \"${CONFIRM_STOP}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n if [ \"${CONFIRM_STOP}\" != \"y\" ] && [ \"${CONFIRM_STOP}\" != \"Y\" ]; then\n log \"$(msg install.existing.cancelled)\"\n exit 0\n fi\n fi\n fi\n UPGRADE_EXISTING_WORKERS=\"${existing_workers}\"\n ;;\n 2|reinstall)\n log \"$(msg install.reinstall.performing)\"\n local existing_workspace=\"\"\n if [ -f \"${existing_env}\" ]; then\n existing_workspace=$(grep '^AGENTTEAMS_WORKSPACE_DIR=' \"${existing_env}\" 2>/dev/null | cut -d= -f2-)\n fi\n [ -z \"${existing_workspace}\" ] && existing_workspace=\"${HOME}/agentteams-manager\"\n echo \"\"\n echo -e \"\\033[33m$(msg install.reinstall.warn_stop)\\033[0m\"\n [ -n \"${running_manager}\" ] && echo -e \"\\033[33m - ${running_manager} (manager)\\033[0m\"\n for w in ${running_workers}; do\n echo -e \"\\033[33m - ${w} (worker)\\033[0m\"\n done\n echo \"\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_delete)\\033[0m\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_volume)\\033[0m\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_env \"${existing_env}\")\\033[0m\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_workspace \"${existing_workspace}\")\\033[0m\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_workers)\\033[0m\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_proxy)\\033[0m\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_network)\\033[0m\"\n echo \"\"\n echo -e \"\\033[31m$(msg install.reinstall.confirm_type)\\033[0m\"\n echo -e \"\\033[31m ${existing_workspace}\\033[0m\"\n echo \"\"\n local CONFIRM_PATH\n read -e -p \"$(msg install.reinstall.confirm_path): \" CONFIRM_PATH\n if [ \"${CONFIRM_PATH}\" != \"${existing_workspace}\" ]; then\n die \"$(msg install.reinstall.path_mismatch \"${CONFIRM_PATH}\" \"${existing_workspace}\")\"\n fi\n log \"$(msg install.reinstall.confirmed)\"\n ${DOCKER_CMD} stop agentteams-manager 2>/dev/null || true\n ${DOCKER_CMD} rm agentteams-manager 2>/dev/null || true\n for w in $(${DOCKER_CMD} ps -a --format '{{.Names}}' | grep \"^agentteams-worker-\" || true); do\n ${DOCKER_CMD} stop \"${w}\" 2>/dev/null || true\n ${DOCKER_CMD} rm \"${w}\" 2>/dev/null || true\n log \"$(msg install.reinstall.removed_worker \"${w}\")\"\n done\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' | grep -q \"^agentteams-controller$\"; then\n log \"$(msg install.reinstall.removing_proxy)\"\n ${DOCKER_CMD} stop agentteams-controller 2>/dev/null || true\n ${DOCKER_CMD} rm agentteams-controller 2>/dev/null || true\n fi\n if ${DOCKER_CMD} network ls --format '{{.Name}}' | grep -q \"^agentteams-net$\"; then\n log \"$(msg install.reinstall.removing_network)\"\n ${DOCKER_CMD} network rm agentteams-net 2>/dev/null || true\n fi\n if ${DOCKER_CMD} volume ls -q | grep -q \"^agentteams-data$\"; then\n log \"$(msg install.reinstall.removing_volume)\"\n ${DOCKER_CMD} volume rm agentteams-data 2>/dev/null || log \"$(msg install.reinstall.warn_volume_fail)\"\n fi\n if [ -d \"${existing_workspace}\" ]; then\n log \"$(msg install.reinstall.removing_workspace \"${existing_workspace}\")\"\n rm -rf \"${existing_workspace}\" || die \"$(msg install.reinstall.failed_rm_workspace)\"\n fi\n if [ -f \"${existing_env}\" ]; then\n log \"$(msg install.reinstall.removing_env \"${existing_env}\")\"\n rm -f \"${existing_env}\"\n fi\n log \"$(msg install.reinstall.cleanup_done)\"\n unset AGENTTEAMS_WORKSPACE_DIR\n return 0\n ;;\n 3|cancel|*)\n log \"$(msg install.existing.cancelled)\"\n exit 0\n ;;\n esac\n # Load existing env file as fallback (shell env vars take priority)\n if [ -f \"${existing_env}\" ]; then\n log \"$(msg install.loading_config \"${existing_env}\")\"\n while IFS='=' read -r key value; do\n case \"${key}\" in \\#*|\"\") continue ;; esac\n value=\"${value%%#*}\"\n value=\"${value#\"${value%%[![:space:]]*}\"}\"\n value=\"${value%\"${value##*[![:space:]]}\"}\"\n eval \"_existing_val=\\\"\\${${key}+x}\\\"\"\n if [ -z \"${_existing_val}\" ]; then export \"${key}=${value}\"; fi\n done < \"${existing_env}\"\n fi\n}\n\nstep_llm() {\n log \"$(msg llm.title)\"\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n if [ \"${AGENTTEAMS_LANGUAGE}\" = \"zh\" ]; then\n AGENTTEAMS_LLM_PROVIDER=\"${AGENTTEAMS_LLM_PROVIDER:-openai-compat}\"\n AGENTTEAMS_DEFAULT_MODEL=\"${AGENTTEAMS_DEFAULT_MODEL:-qwen3.6-plus}\"\n AGENTTEAMS_OPENAI_BASE_URL=\"${AGENTTEAMS_OPENAI_BASE_URL:-https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1}\"\n log \"$(msg llm.provider.label \"${AGENTTEAMS_LLM_PROVIDER}\")\"\n log \"$(msg llm.openai.base_url_label \"${AGENTTEAMS_OPENAI_BASE_URL}\")\"\n else\n AGENTTEAMS_LLM_PROVIDER=\"${AGENTTEAMS_LLM_PROVIDER:-qwen}\"\n AGENTTEAMS_DEFAULT_MODEL=\"${AGENTTEAMS_DEFAULT_MODEL:-qwen3.6-plus}\"\n AGENTTEAMS_OPENAI_BASE_URL=\"${AGENTTEAMS_OPENAI_BASE_URL:-}\"\n log \"$(msg llm.provider.qwen_default \"${AGENTTEAMS_LLM_PROVIDER}\")\"\n fi\n log \"$(msg llm.model.default \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n prompt AGENTTEAMS_LLM_API_KEY \"$(msg llm.apikey_prompt)\" \"\" \"true\"\n AGENTTEAMS_EMBEDDING_MODEL=\"${AGENTTEAMS_EMBEDDING_MODEL-text-embedding-v4}\"\n export AGENTTEAMS_LLM_PROVIDER AGENTTEAMS_DEFAULT_MODEL AGENTTEAMS_EMBEDDING_MODEL\n [ -n \"${AGENTTEAMS_OPENAI_BASE_URL+x}\" ] && export AGENTTEAMS_OPENAI_BASE_URL\n return 0\n fi\n\n # Upgrade Keep-All mode: skip provider selection, use loaded values\n local SKIP_TO_EMBEDDING=0\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ] && [ -n \"${AGENTTEAMS_LLM_PROVIDER}\" ]; then\n log \"$(msg llm.provider.label \"${AGENTTEAMS_LLM_PROVIDER}\")\"\n log \"$(msg llm.openai.base_url_label \"${AGENTTEAMS_OPENAI_BASE_URL}\")\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n PROVIDER_CHOICE=\"${AGENTTEAMS_LLM_PROVIDER}\"\n SKIP_TO_EMBEDDING=1\n else\n echo \"\"\n echo \"$(msg llm.providers_title)\"\n echo \"$(msg llm.provider.alibaba)\"\n echo \"$(msg llm.provider.openai_compat)\"\n echo \"\"\n local PROVIDER_CHOICE\n # If upgrade mode with loaded provider, show current as default\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_LLM_PROVIDER}\" ]; then\n local _prov_display\n case \"${AGENTTEAMS_LLM_PROVIDER}\" in\n openai-compat) _prov_display=\"2\" ;;\n *) _prov_display=\"1\" ;;\n esac\n read -e -p \"$(msg llm.provider.select) [${_prov_display}]: \" PROVIDER_CHOICE\n PROVIDER_CHOICE=\"${PROVIDER_CHOICE:-${_prov_display}}\"\n elif [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ]; then\n read -e -p \"$(msg llm.provider.select) [1]: \" PROVIDER_CHOICE\n PROVIDER_CHOICE=\"${PROVIDER_CHOICE:-1}\"\n else\n read -e -p \"$(msg llm.provider.select): \" PROVIDER_CHOICE\n PROVIDER_CHOICE=\"${PROVIDER_CHOICE:-1}\"\n fi\n if [ \"${PROVIDER_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n fi\n local ALIBABA_MODEL_CHOICE=\"\"\n local ALIBABA_ACCESS=\"\"\n case \"${PROVIDER_CHOICE}\" in\n 1|alibaba-cloud|openai-compat)\n if [ \"${AGENTTEAMS_LANGUAGE}\" = \"en\" ]; then\n AGENTTEAMS_LLM_PROVIDER=\"openai-compat\"\n AGENTTEAMS_OPENAI_BASE_URL=\"https://dashscope-intl.aliyuncs.com/compatible-mode/v1\"\n ALIBABA_ACCESS=\"tokenplan\"\n echo \"\"\n echo \"$(msg llm.codingplan.models_title)\"\n echo \"$(msg llm.codingplan.model.qwen36plus)\"\n echo \"$(msg llm.codingplan.model.glm5)\"\n echo \"$(msg llm.codingplan.model.kimi)\"\n echo \"$(msg llm.codingplan.model.minimax)\"\n echo \"\"\n local CODINGPLAN_MODEL_CHOICE\n # If upgrade with loaded model, show current as default\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_DEFAULT_MODEL}\" ]; then\n local _model_default\n case \"${AGENTTEAMS_DEFAULT_MODEL}\" in\n qwen3.6-plus) _model_default=\"1\" ;;\n glm-5) _model_default=\"2\" ;;\n kimi-k2.5) _model_default=\"3\" ;;\n MiniMax-M2.5) _model_default=\"4\" ;;\n *) _model_default=\"1\" ;;\n esac\n read -e -p \"$(msg llm.codingplan.model.select) [${_model_default}]: \" CODINGPLAN_MODEL_CHOICE\n CODINGPLAN_MODEL_CHOICE=\"${CODINGPLAN_MODEL_CHOICE:-${_model_default}}\"\n elif [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ]; then\n read -e -p \"$(msg llm.codingplan.model.select) [1]: \" CODINGPLAN_MODEL_CHOICE\n CODINGPLAN_MODEL_CHOICE=\"${CODINGPLAN_MODEL_CHOICE:-1}\"\n else\n read -e -p \"$(msg llm.codingplan.model.select): \" CODINGPLAN_MODEL_CHOICE\n CODINGPLAN_MODEL_CHOICE=\"${CODINGPLAN_MODEL_CHOICE:-1}\"\n fi\n if [ \"${CODINGPLAN_MODEL_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n case \"${CODINGPLAN_MODEL_CHOICE}\" in\n 1|qwen3.6-plus) AGENTTEAMS_DEFAULT_MODEL=\"qwen3.6-plus\" ;;\n 2|glm-5) AGENTTEAMS_DEFAULT_MODEL=\"glm-5\" ;;\n 3|kimi-k2.5) AGENTTEAMS_DEFAULT_MODEL=\"kimi-k2.5\" ;;\n 4|MiniMax-M2.5) AGENTTEAMS_DEFAULT_MODEL=\"MiniMax-M2.5\" ;;\n *) AGENTTEAMS_DEFAULT_MODEL=\"qwen3.6-plus\" ;;\n esac\n log \"$(msg llm.provider.selected_codingplan)\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n else\n echo \"\"\n echo \"$(msg llm.alibaba.models_title)\"\n echo \"$(msg llm.alibaba.model.tokenplan)\"\n echo \"$(msg llm.alibaba.model.bailian)\"\n echo \"$(msg llm.alibaba.model.codingplan_legacy)\"\n echo \"\"\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_OPENAI_BASE_URL}\" ]; then\n # Determine alibaba access type from loaded URL\n local _access_default=\"1\"\n if echo \"${AGENTTEAMS_OPENAI_BASE_URL}\" | grep -q \"token-plan\"; then\n _access_default=\"1\"\n elif echo \"${AGENTTEAMS_OPENAI_BASE_URL}\" | grep -q \"dashscope\"; then\n if echo \"${AGENTTEAMS_OPENAI_BASE_URL}\" | grep -q \"intl\"; then\n _access_default=\"3\"\n else\n _access_default=\"2\"\n fi\n fi\n read -e -p \"$(msg llm.alibaba.model.select) [${_access_default}]: \" ALIBABA_MODEL_CHOICE\n ALIBABA_MODEL_CHOICE=\"${ALIBABA_MODEL_CHOICE:-${_access_default}}\"\n elif [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ]; then\n read -e -p \"$(msg llm.alibaba.model.select) [1]: \" ALIBABA_MODEL_CHOICE\n ALIBABA_MODEL_CHOICE=\"${ALIBABA_MODEL_CHOICE:-1}\"\n else\n read -e -p \"$(msg llm.alibaba.model.select): \" ALIBABA_MODEL_CHOICE\n ALIBABA_MODEL_CHOICE=\"${ALIBABA_MODEL_CHOICE:-1}\"\n fi\n if [ \"${ALIBABA_MODEL_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n case \"${ALIBABA_MODEL_CHOICE}\" in\n 1|token-plan|tokenplan)\n ALIBABA_ACCESS=\"tokenplan\"\n AGENTTEAMS_LLM_PROVIDER=\"openai-compat\"\n AGENTTEAMS_OPENAI_BASE_URL=\"https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1\"\n echo \"\"\n echo \"$(msg llm.codingplan.models_title)\"\n echo \"$(msg llm.codingplan.model.qwen36plus)\"\n echo \"$(msg llm.codingplan.model.glm5)\"\n echo \"$(msg llm.codingplan.model.kimi)\"\n echo \"$(msg llm.codingplan.model.minimax)\"\n echo \"\"\n local CODINGPLAN_MODEL_CHOICE\n # If upgrade with loaded model, show current as default\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_DEFAULT_MODEL}\" ]; then\n local _model_default\n case \"${AGENTTEAMS_DEFAULT_MODEL}\" in\n qwen3.6-plus) _model_default=\"1\" ;;\n glm-5) _model_default=\"2\" ;;\n kimi-k2.5) _model_default=\"3\" ;;\n MiniMax-M2.5) _model_default=\"4\" ;;\n *) _model_default=\"1\" ;;\n esac\n read -e -p \"$(msg llm.codingplan.model.select) [${_model_default}]: \" CODINGPLAN_MODEL_CHOICE\n CODINGPLAN_MODEL_CHOICE=\"${CODINGPLAN_MODEL_CHOICE:-${_model_default}}\"\n elif [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ]; then\n read -e -p \"$(msg llm.codingplan.model.select) [1]: \" CODINGPLAN_MODEL_CHOICE\n CODINGPLAN_MODEL_CHOICE=\"${CODINGPLAN_MODEL_CHOICE:-1}\"\n else\n read -e -p \"$(msg llm.codingplan.model.select): \" CODINGPLAN_MODEL_CHOICE\n CODINGPLAN_MODEL_CHOICE=\"${CODINGPLAN_MODEL_CHOICE:-1}\"\n fi\n if [ \"${CODINGPLAN_MODEL_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n case \"${CODINGPLAN_MODEL_CHOICE}\" in\n 1|qwen3.6-plus) AGENTTEAMS_DEFAULT_MODEL=\"qwen3.6-plus\" ;;\n 2|glm-5) AGENTTEAMS_DEFAULT_MODEL=\"glm-5\" ;;\n 3|kimi-k2.5) AGENTTEAMS_DEFAULT_MODEL=\"kimi-k2.5\" ;;\n 4|MiniMax-M2.5) AGENTTEAMS_DEFAULT_MODEL=\"MiniMax-M2.5\" ;;\n *) AGENTTEAMS_DEFAULT_MODEL=\"qwen3.6-plus\" ;;\n esac\n log \"$(msg llm.provider.selected_tokenplan)\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n ;;\n 2|qwen|bailian)\n ALIBABA_ACCESS=\"bailian\"\n AGENTTEAMS_LLM_PROVIDER=\"qwen\"\n AGENTTEAMS_OPENAI_BASE_URL=\"\"\n echo \"\"\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_DEFAULT_MODEL}\" ]; then\n read -e -p \"$(msg llm.qwen.model_prompt) [${AGENTTEAMS_DEFAULT_MODEL}]: \" AGENTTEAMS_DEFAULT_MODEL\n else\n read -e -p \"$(msg llm.qwen.model_prompt) [qwen3.6-plus]: \" AGENTTEAMS_DEFAULT_MODEL\n fi\n if [ \"${AGENTTEAMS_DEFAULT_MODEL}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_DEFAULT_MODEL=\"${AGENTTEAMS_DEFAULT_MODEL:-qwen3.6-plus}\"\n log \"$(msg llm.provider.selected_qwen)\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n prompt_custom_model_params \"${AGENTTEAMS_DEFAULT_MODEL}\" || return 0\n ;;\n 3|coding-plan|codingplan)\n ALIBABA_ACCESS=\"codingplan_legacy\"\n AGENTTEAMS_LLM_PROVIDER=\"openai-compat\"\n AGENTTEAMS_OPENAI_BASE_URL=\"https://coding.dashscope.aliyuncs.com/v1\"\n echo \"\"\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_DEFAULT_MODEL}\" ]; then\n read -e -p \"$(msg llm.qwen.model_prompt) [${AGENTTEAMS_DEFAULT_MODEL}]: \" AGENTTEAMS_DEFAULT_MODEL\n else\n read -e -p \"$(msg llm.qwen.model_prompt): \" AGENTTEAMS_DEFAULT_MODEL\n fi\n if [ \"${AGENTTEAMS_DEFAULT_MODEL}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_DEFAULT_MODEL=\"${AGENTTEAMS_DEFAULT_MODEL:-qwen3.6-plus}\"\n log \"$(msg llm.provider.selected_codingplan_legacy)\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n prompt_custom_model_params \"${AGENTTEAMS_DEFAULT_MODEL}\" || return 0\n ;;\n *)\n die \"$(msg llm.alibaba.model.invalid \"${ALIBABA_MODEL_CHOICE}\")\"\n ;;\n esac\n fi\n if [ \"${ALIBABA_ACCESS}\" = \"bailian\" ]; then\n log \"$(msg llm.apikey_hint_bailian)\"\n log \"$(msg llm.apikey_url_bailian)\"\n elif [ \"${AGENTTEAMS_LANGUAGE}\" = \"en\" ]; then\n log \"$(msg llm.apikey_hint_qwencloud)\"\n log \"$(msg llm.apikey_url_qwencloud)\"\n elif [ \"${ALIBABA_ACCESS}\" = \"codingplan_legacy\" ]; then\n log \"$(msg llm.apikey_hint_codingplan)\"\n log \"$(msg llm.apikey_url_codingplan)\"\n else\n log \"$(msg llm.apikey_hint_tokenplan)\"\n log \"$(msg llm.apikey_url_tokenplan)\"\n fi\n log \"\"\n prompt AGENTTEAMS_LLM_API_KEY \"$(msg llm.apikey_prompt)\" \"\" \"true\" || return 0\n if [ \"${ALIBABA_ACCESS}\" = \"bailian\" ]; then\n test_llm_connectivity \"https://dashscope.aliyuncs.com/compatible-mode/v1\" \"${AGENTTEAMS_LLM_API_KEY}\" \"${AGENTTEAMS_DEFAULT_MODEL}\" || return 0\n elif [ \"${ALIBABA_ACCESS}\" = \"codingplan_legacy\" ]; then\n test_llm_connectivity \"https://coding.dashscope.aliyuncs.com/v1\" \"${AGENTTEAMS_LLM_API_KEY}\" \"${AGENTTEAMS_DEFAULT_MODEL}\" \"$(msg llm.openai.test.fail.codingplan_legacy)\" || return 0\n elif [ \"${AGENTTEAMS_LANGUAGE}\" = \"en\" ]; then\n test_llm_connectivity \"${AGENTTEAMS_OPENAI_BASE_URL}\" \"${AGENTTEAMS_LLM_API_KEY}\" \"${AGENTTEAMS_DEFAULT_MODEL}\" \"$(msg llm.openai.test.fail.codingplan)\" || return 0\n else\n test_llm_connectivity \"${AGENTTEAMS_OPENAI_BASE_URL}\" \"${AGENTTEAMS_LLM_API_KEY}\" \"${AGENTTEAMS_DEFAULT_MODEL}\" \"$(msg llm.openai.test.fail.tokenplan)\" || return 0\n fi\n ;;\n 2|openai-compat)\n AGENTTEAMS_LLM_PROVIDER=\"openai-compat\"\n log \"$(msg llm.provider.selected_openai \"${AGENTTEAMS_LLM_PROVIDER}\")\"\n echo \"\"\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg llm.openai.base_url_prompt)\" \"${AGENTTEAMS_OPENAI_BASE_URL}\")\"\n else\n _current=\"${AGENTTEAMS_OPENAI_BASE_URL}\"\n if [ -n \"${_current}\" ]; then\n read -e -p \"$(msg llm.openai.base_url_prompt) [${_current}]: \" AGENTTEAMS_OPENAI_BASE_URL\n else\n read -e -p \"$(msg llm.openai.base_url_prompt) [https://api.openai.com/v1]: \" AGENTTEAMS_OPENAI_BASE_URL\n fi\n if [ \"${AGENTTEAMS_OPENAI_BASE_URL}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_OPENAI_BASE_URL=\"${AGENTTEAMS_OPENAI_BASE_URL:-${_current}}\"\n [ -z \"${AGENTTEAMS_OPENAI_BASE_URL}\" ] && AGENTTEAMS_OPENAI_BASE_URL=\"https://api.openai.com/v1\"\n fi\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg llm.openai.model_prompt)\" \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n else\n _current=\"${AGENTTEAMS_DEFAULT_MODEL}\"\n if [ -n \"${_current}\" ]; then\n read -e -p \"$(msg llm.openai.model_prompt) [${_current}]: \" AGENTTEAMS_DEFAULT_MODEL\n else\n read -e -p \"$(msg llm.openai.model_prompt) [gpt-5.4]: \" AGENTTEAMS_DEFAULT_MODEL\n fi\n if [ \"${AGENTTEAMS_DEFAULT_MODEL}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_DEFAULT_MODEL=\"${AGENTTEAMS_DEFAULT_MODEL:-${_current}}\"\n [ -z \"${AGENTTEAMS_DEFAULT_MODEL}\" ] && AGENTTEAMS_DEFAULT_MODEL=\"gpt-5.4\"\n fi\n log \"$(msg llm.openai.base_url_label \"${AGENTTEAMS_OPENAI_BASE_URL}\")\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n prompt_custom_model_params \"${AGENTTEAMS_DEFAULT_MODEL}\" || return 0\n log \"\"\n prompt AGENTTEAMS_LLM_API_KEY \"$(msg llm.apikey_prompt)\" \"\" \"true\" || return 0\n test_llm_connectivity \"${AGENTTEAMS_OPENAI_BASE_URL}\" \"${AGENTTEAMS_LLM_API_KEY}\" \"${AGENTTEAMS_DEFAULT_MODEL}\" || return 0\n ;;\n *)\n die \"$(msg llm.provider.invalid \"${PROVIDER_CHOICE}\")\"\n ;;\n esac\n\n # Skip to embedding if Keep-All mode handled LLM params\n if [ \"${SKIP_TO_EMBEDDING}\" = \"1\" ]; then\n if [ -n \"${AGENTTEAMS_EMBEDDING_MODEL}\" ]; then\n log \"$(msg llm.embedding.title)\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_EMBEDDING_MODEL}\")\"\n log \"\"\n fi\n return 0\n fi\n\n # --- Embedding model (optional, auto-tested) ---\n echo \"\"\n log \"$(msg llm.embedding.title)\"\n log \"$(msg llm.embedding.hint)\"\n echo \"\"\n echo \"$(msg llm.embedding.option.default)\"\n echo \"$(msg llm.embedding.option.custom)\"\n echo \"$(msg llm.embedding.option.disable)\"\n echo \"\"\n local EMB_CHOICE\n read -e -p \"$(msg llm.embedding.select) [1]: \" EMB_CHOICE\n EMB_CHOICE=\"${EMB_CHOICE:-1}\"\n if [ \"${EMB_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n\n case \"${EMB_CHOICE}\" in\n 1)\n AGENTTEAMS_EMBEDDING_MODEL=\"text-embedding-v4\"\n ;;\n 2)\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg llm.embedding.custom_prompt)\" \"${AGENTTEAMS_EMBEDDING_MODEL}\")\"\n else\n _current=\"${AGENTTEAMS_EMBEDDING_MODEL}\"\n if [ -n \"${_current}\" ]; then\n read -e -p \"$(msg llm.embedding.custom_prompt) [${_current}]: \" AGENTTEAMS_EMBEDDING_MODEL\n else\n read -e -p \"$(msg llm.embedding.custom_prompt): \" AGENTTEAMS_EMBEDDING_MODEL\n fi\n if [ \"${AGENTTEAMS_EMBEDDING_MODEL}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_EMBEDDING_MODEL=\"${AGENTTEAMS_EMBEDDING_MODEL:-${_current}}\"\n fi\n ;;\n 3)\n AGENTTEAMS_EMBEDDING_MODEL=\"\"\n log \"$(msg llm.embedding.disabled)\"\n ;;\n *)\n AGENTTEAMS_EMBEDDING_MODEL=\"text-embedding-v4\"\n ;;\n esac\n\n if [ -n \"${AGENTTEAMS_EMBEDDING_MODEL}\" ]; then\n # Qwen provider uses dashscope directly; others use OPENAI_BASE_URL\n local EMB_BASE_URL=\"${AGENTTEAMS_OPENAI_BASE_URL}\"\n if [ \"${AGENTTEAMS_LLM_PROVIDER}\" = \"qwen\" ]; then\n EMB_BASE_URL=\"https://dashscope.aliyuncs.com/compatible-mode/v1\"\n fi\n if ! test_embedding_connectivity \"${EMB_BASE_URL}\" \"${AGENTTEAMS_LLM_API_KEY}\" \"${AGENTTEAMS_EMBEDDING_MODEL}\"; then\n AGENTTEAMS_EMBEDDING_MODEL=\"\"\n log \"$(msg llm.embedding.auto_disabled)\"\n fi\n fi\n\n export AGENTTEAMS_LLM_PROVIDER AGENTTEAMS_DEFAULT_MODEL\n [ -n \"${AGENTTEAMS_OPENAI_BASE_URL+x}\" ] && export AGENTTEAMS_OPENAI_BASE_URL\n log \"\"\n}\n\nstep_admin() {\n log \"$(msg admin.title)\"\n prompt AGENTTEAMS_ADMIN_USER \"$(msg admin.username_prompt)\" \"admin\" || return 0\n AGENTTEAMS_ADMIN_USER=\"$(printf '%s' \"${AGENTTEAMS_ADMIN_USER}\" | tr '[:upper:]' '[:lower:]')\"\n\n # Pre-set via env var: validate; in non-interactive mode fail fast,\n # in interactive mode warn and fall through to the retry prompt.\n if [ -n \"${AGENTTEAMS_ADMIN_PASSWORD:-}\" ]; then\n log \" $(msg prompt.preset \"$(msg admin.password_prompt)\")\"\n if [ ${#AGENTTEAMS_ADMIN_PASSWORD} -ge 8 ]; then\n log \"\"\n return 0\n fi\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n die \"$(msg admin.password_too_short \"${#AGENTTEAMS_ADMIN_PASSWORD}\")\"\n fi\n error \"$(msg admin.password_too_short \"${#AGENTTEAMS_ADMIN_PASSWORD}\")\"\n unset AGENTTEAMS_ADMIN_PASSWORD\n fi\n\n while true; do\n unset AGENTTEAMS_ADMIN_PASSWORD\n prompt_optional AGENTTEAMS_ADMIN_PASSWORD \"$(msg admin.password_prompt)\" \"true\" || return 0\n if [ -z \"${AGENTTEAMS_ADMIN_PASSWORD}\" ]; then\n AGENTTEAMS_ADMIN_PASSWORD=\"admin$(openssl rand -hex 6)\"\n log \"$(msg admin.password_generated)\"\n break\n fi\n if [ ${#AGENTTEAMS_ADMIN_PASSWORD} -ge 8 ]; then\n break\n fi\n error \"$(msg admin.password_too_short \"${#AGENTTEAMS_ADMIN_PASSWORD}\")\"\n done\n log \"\"\n}\n\nstep_network() {\n log \"$(msg port.local_only.title)\"\n echo \"\"\n echo \" 1) $(msg port.local_only.hint_yes)\"\n echo \" 2) $(msg port.local_only.hint_no)\"\n echo \"\"\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_LOCAL_ONLY=\"${AGENTTEAMS_LOCAL_ONLY:-1}\"\n elif [ -z \"${AGENTTEAMS_LOCAL_ONLY+x}\" ]; then\n local _local_choice\n read -e -p \"$(msg port.local_only.choice): \" _local_choice\n if [ \"${_local_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _local_choice=\"${_local_choice:-1}\"\n case \"${_local_choice}\" in\n 2|n|N|no|NO) AGENTTEAMS_LOCAL_ONLY=\"0\" ;;\n *) AGENTTEAMS_LOCAL_ONLY=\"1\" ;;\n esac\n fi\n export AGENTTEAMS_LOCAL_ONLY\n if [ \"${AGENTTEAMS_LOCAL_ONLY}\" = \"1\" ]; then\n log \"$(msg port.local_only.selected_local)\"\n else\n log \"$(msg port.local_only.selected_external)\"\n echo \"\"\n echo -e \"\\033[33m$(msg port.local_only.https_hint)\\033[0m\"\n fi\n}\n\nstep_ports() {\n log \"$(msg port.title)\"\n prompt AGENTTEAMS_PORT_GATEWAY \"$(msg port.gateway_prompt)\" \"18080\" || return 0\n prompt AGENTTEAMS_PORT_CONSOLE \"$(msg port.console_prompt)\" \"18001\" || return 0\n prompt AGENTTEAMS_PORT_ELEMENT_WEB \"$(msg port.element_prompt)\" \"18088\" || return 0\n prompt AGENTTEAMS_PORT_MANAGER_CONSOLE \"$(msg port.manager_console_prompt)\" \"18888\" || return 0\n log \"\"\n}\n\nstep_domains() {\n log \"$(msg domain.title)\"\n log \"$(msg domain.hint)\"\n prompt AGENTTEAMS_MATRIX_DOMAIN \"$(msg domain.matrix_prompt)\" \"matrix-local.agentteams.io:${AGENTTEAMS_PORT_GATEWAY}\" || return 0\n prompt AGENTTEAMS_MATRIX_CLIENT_DOMAIN \"$(msg domain.element_prompt)\" \"matrix-client-local.agentteams.io\" || return 0\n prompt AGENTTEAMS_AI_GATEWAY_DOMAIN \"$(msg domain.gateway_prompt)\" \"aigw-local.agentteams.io\" || return 0\n prompt AGENTTEAMS_FS_DOMAIN \"$(msg domain.fs_prompt)\" \"fs-local.agentteams.io\" || return 0\n if [ \"${AGENTTEAMS_MANAGER_RUNTIME}\" != \"copaw\" ]; then\n prompt AGENTTEAMS_CONSOLE_DOMAIN \"$(msg domain.console_prompt)\" \"console-local.agentteams.io\" || return 0\n fi\n log \"\"\n}\n\nstep_github() {\n log \"$(msg github.title)\"\n prompt_optional AGENTTEAMS_GITHUB_TOKEN \"$(msg github.token_prompt)\" \"true\" || return 0\n}\n\nstep_skills() {\n log \"\"\n log \"$(msg skills.title)\"\n prompt_optional AGENTTEAMS_SKILLS_API_URL \"$(msg skills.url_prompt)\" || return 0\n log \"\"\n}\n\nstep_volume() {\n log \"$(msg data.title)\"\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_DATA_DIR=\"${AGENTTEAMS_DATA_DIR:-agentteams-data}\"\n log \" $(msg data.volume_using \"${AGENTTEAMS_DATA_DIR}\") (non-interactive, skipped)\"\n export AGENTTEAMS_DATA_DIR\n return 0\n fi\n # ─────────────────────────────────────────────────────────────────\n if [ -z \"${AGENTTEAMS_DATA_DIR+x}\" ]; then\n local _input\n read -e -p \"$(msg data.volume_prompt): \" _input\n if [ \"${_input}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_DATA_DIR=\"${_input:-agentteams-data}\"\n export AGENTTEAMS_DATA_DIR\n fi\n AGENTTEAMS_DATA_DIR=\"${AGENTTEAMS_DATA_DIR:-agentteams-data}\"\n log \"$(msg data.volume_using \"${AGENTTEAMS_DATA_DIR}\")\"\n}\n\nstep_workspace() {\n log \"$(msg workspace.title)\"\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_WORKSPACE_DIR=\"${AGENTTEAMS_WORKSPACE_DIR:-${HOME}/agentteams-manager}\"\n AGENTTEAMS_WORKSPACE_DIR=\"$(cd \"${AGENTTEAMS_WORKSPACE_DIR}\" 2>/dev/null && pwd || echo \"${AGENTTEAMS_WORKSPACE_DIR}\")\"\n mkdir -p \"${AGENTTEAMS_WORKSPACE_DIR}\"\n log \" $(msg workspace.dir_label \"${AGENTTEAMS_WORKSPACE_DIR}\") (non-interactive, skipped)\"\n export AGENTTEAMS_WORKSPACE_DIR\n return 0\n fi\n # ─────────────────────────────────────────────────────────────────\n local _input\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg workspace.dir_prompt \"${HOME}/agentteams-manager\")\" \"${AGENTTEAMS_WORKSPACE_DIR}\")\"\n _input=\"${AGENTTEAMS_WORKSPACE_DIR}\"\n else\n local _current=\"${AGENTTEAMS_WORKSPACE_DIR}\"\n if [ -n \"${_current}\" ]; then\n read -e -p \"$(msg workspace.dir_prompt \"${HOME}/agentteams-manager\") [${_current}]: \" _input\n else\n read -e -p \"$(msg workspace.dir_prompt \"${HOME}/agentteams-manager\"): \" _input\n fi\n if [ \"${_input}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _input=\"${_input:-${_current}}\"\n fi\n AGENTTEAMS_WORKSPACE_DIR=\"${_input:-${HOME}/agentteams-manager}\"\n export AGENTTEAMS_WORKSPACE_DIR\n AGENTTEAMS_WORKSPACE_DIR=\"$(cd \"${AGENTTEAMS_WORKSPACE_DIR}\" 2>/dev/null && pwd || echo \"${AGENTTEAMS_WORKSPACE_DIR}\")\"\n mkdir -p \"${AGENTTEAMS_WORKSPACE_DIR}\"\n log \"$(msg workspace.dir_label \"${AGENTTEAMS_WORKSPACE_DIR}\")\"\n}\n\nstep_dashboard() {\n AGENTTEAMS_DASHBOARD=\"${AGENTTEAMS_DASHBOARD:-1}\"\n AGENTTEAMS_DASHBOARD_VERSION=\"${AGENTTEAMS_DASHBOARD_VERSION:-v1.2.0-beta.1}\"\n AGENTTEAMS_PORT_DASHBOARD=\"${AGENTTEAMS_PORT_DASHBOARD:-13000}\"\n AGENTTEAMS_AI_GATEWAY_ADMIN_URL=\"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL:-}\"\n\n # Compute the default image for the current version.\n # We track whether the current image matches the derived default so that\n # changing the version also updates the image (unless the user has\n # explicitly overridden it to a non-default value).\n _dashboard_default_image() {\n echo \"${AGENTTEAMS_REGISTRY}/agentteams/agentteams-dashboard:${1:-${AGENTTEAMS_DASHBOARD_VERSION}}\"\n }\n local _init_default\n _init_default=$(_dashboard_default_image)\n if [ -z \"${AGENTTEAMS_DASHBOARD_IMAGE:-}\" ]; then\n AGENTTEAMS_DASHBOARD_IMAGE=\"${_init_default}\"\n fi\n\n # Recompute default image when the version changes AND the current image\n # matches the default for the *previous* (env-file) version.\n # This handles non-interactive upgrades correctly:\n # - env file loads old default image + old version\n # - CLI passes new version\n # - image should follow (because it was auto-derived before)\n # But preserves truly custom images (same repo but custom tag like :canary)\n # because they won't match the old-version default exactly.\n local _old_saved_version=\"\"\n local _env_file=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n if [ -f \"${_env_file}\" ]; then\n _old_saved_version=\"$(grep '^AGENTTEAMS_DASHBOARD_VERSION=' \"${_env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n fi\n if [ -n \"${_old_saved_version}\" ] && \\\n [ \"${_old_saved_version}\" != \"${AGENTTEAMS_DASHBOARD_VERSION}\" ] && \\\n [ \"${AGENTTEAMS_DASHBOARD_IMAGE}\" = \"$(_dashboard_default_image \"${_old_saved_version}\")\" ]; then\n AGENTTEAMS_DASHBOARD_IMAGE=$(_dashboard_default_image)\n fi\n\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_DASHBOARD=\"${AGENTTEAMS_DASHBOARD:-1}\"\n if [ \"${AGENTTEAMS_DASHBOARD}\" = \"1\" ]; then\n log \"$(msg dash.summary \"${AGENTTEAMS_PORT_DASHBOARD}\" \"${AGENTTEAMS_DASHBOARD_IMAGE}\") (non-interactive)\"\n else\n log \"$(msg dash.skip) (non-interactive)\"\n fi\n export AGENTTEAMS_DASHBOARD AGENTTEAMS_DASHBOARD_VERSION AGENTTEAMS_PORT_DASHBOARD AGENTTEAMS_DASHBOARD_IMAGE AGENTTEAMS_AI_GATEWAY_ADMIN_URL\n return 0\n fi\n\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ]; then\n log \"$(msg prompt.upgrade_keep \"\" \"${AGENTTEAMS_DASHBOARD}\") dashboard=${AGENTTEAMS_DASHBOARD} version=${AGENTTEAMS_DASHBOARD_VERSION} port=${AGENTTEAMS_PORT_DASHBOARD}\"\n export AGENTTEAMS_DASHBOARD AGENTTEAMS_DASHBOARD_VERSION AGENTTEAMS_PORT_DASHBOARD AGENTTEAMS_DASHBOARD_IMAGE AGENTTEAMS_AI_GATEWAY_ADMIN_URL\n return 0\n fi\n\n local _input\n echo \"\"\n read -p \"$(msg dash.prompt) [Y/n]: \" _input\n case \"${_input:-}\" in\n [nN0]) AGENTTEAMS_DASHBOARD=0 ;;\n *) AGENTTEAMS_DASHBOARD=1 ;;\n esac\n if [ \"${_input}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n\n if [ \"${AGENTTEAMS_DASHBOARD}\" != \"1\" ]; then\n log \"$(msg dash.skip)\"\n export AGENTTEAMS_DASHBOARD AGENTTEAMS_DASHBOARD_VERSION AGENTTEAMS_PORT_DASHBOARD AGENTTEAMS_DASHBOARD_IMAGE AGENTTEAMS_AI_GATEWAY_ADMIN_URL\n return 0\n fi\n\n # Dashboard version\n local _current_version=\"${AGENTTEAMS_DASHBOARD_VERSION}\"\n local _old_version=\"${AGENTTEAMS_DASHBOARD_VERSION}\"\n local _old_default_image\n _old_default_image=$(_dashboard_default_image \"${_old_version}\")\n read -p \"$(msg dash.version_prompt) [${_current_version}]: \" _input\n AGENTTEAMS_DASHBOARD_VERSION=\"${_input:-${_current_version}}\"\n\n # Dashboard port\n local _current_port=\"${AGENTTEAMS_PORT_DASHBOARD}\"\n read -p \"$(msg dash.port_prompt) [${_current_port}]: \" _input\n AGENTTEAMS_PORT_DASHBOARD=\"${_input:-${_current_port}}\"\n\n # Dashboard image: if the current image matches the default derived from\n # the OLD version, recompute it from the NEW version. This handles both\n # fresh installs and upgrades where the previous image was the default.\n # Only a real user override (image != default for old version) is preserved.\n if [ \"${AGENTTEAMS_DASHBOARD_VERSION}\" != \"${_old_version}\" ] && \\\n [ \"${AGENTTEAMS_DASHBOARD_IMAGE}\" = \"${_old_default_image}\" ]; then\n AGENTTEAMS_DASHBOARD_IMAGE=$(_dashboard_default_image)\n fi\n local _current_image=\"${AGENTTEAMS_DASHBOARD_IMAGE}\"\n read -p \"$(msg dash.image_prompt) [${_current_image}]: \" _input\n if [ -n \"${_input}\" ]; then\n AGENTTEAMS_DASHBOARD_IMAGE=\"${_input}\"\n fi\n\n # Higress Console URL (explicit config takes priority; auto-detect as fallback)\n local _higress_url=\"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\"\n if [ -z \"${_higress_url}\" ]; then\n if ${DOCKER_CMD} ps --format '{{.Names}}' | grep -q \"^agentteams-controller$\"; then\n if ${DOCKER_CMD} exec agentteams-controller curl -sf --max-time 2 http://127.0.0.1:8001/ >/dev/null 2>&1; then\n _higress_url=\"http://agentteams-controller:8001\"\n log \"$(msg dash.auto_url): ${_higress_url}\"\n fi\n fi\n fi\n read -p \"$(msg dash.gateway_prompt) [${_higress_url:-<skip>}]: \" _input\n AGENTTEAMS_AI_GATEWAY_ADMIN_URL=\"${_input:-${_higress_url}}\"\n\n # Normalize URL: add http:// prefix if missing\n if [ -n \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\" ]; then\n case \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\" in\n http://*|https://*) ;;\n *) AGENTTEAMS_AI_GATEWAY_ADMIN_URL=\"http://${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\" ;;\n esac\n fi\n\n # Verify the Higress Console URL is reachable (best-effort warning only)\n if [ -n \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\" ]; then\n local _gw_reachable=0\n # If the URL points to the controller container, test from inside the container\n if echo \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\" | grep -qE \"(agentteams-controller|hiclaw-controller|127\\.0\\.0\\.1|localhost)\"; then\n if ${DOCKER_CMD} ps --format '{{.Names}}' | grep -q \"^agentteams-controller$\"; then\n if ${DOCKER_CMD} exec agentteams-controller curl -sf --max-time 3 \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}/\" >/dev/null 2>&1; then\n _gw_reachable=1\n fi\n fi\n else\n # External URL — test from host\n if curl -sf --max-time 3 \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}/\" >/dev/null 2>&1; then\n _gw_reachable=1\n fi\n fi\n if [ \"${_gw_reachable}\" = \"0\" ]; then\n log \"WARNING: Higress Console URL may not be reachable: ${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\"\n log \" Dashboard will still work, but shared login via Higress Console may fail.\"\n log \" To fix: verify the URL is correct and the Higress Console service is running.\"\n fi\n fi\n\n log \"$(msg dash.summary \"${AGENTTEAMS_PORT_DASHBOARD}\" \"${AGENTTEAMS_DASHBOARD_IMAGE}\")\"\n export AGENTTEAMS_DASHBOARD AGENTTEAMS_DASHBOARD_VERSION AGENTTEAMS_PORT_DASHBOARD AGENTTEAMS_DASHBOARD_IMAGE AGENTTEAMS_AI_GATEWAY_ADMIN_URL\n}\n\nstep_runtime() {\n log \"$(msg worker_runtime.title)\"\n echo \"\"\n echo \" 1) $(msg worker_runtime.copaw)\"\n echo \" 2) $(msg worker_runtime.openclaw)\"\n if ! _ver_lt \"${AGENTTEAMS_VERSION}\" \"v1.1.0\"; then\n echo \" 3) $(msg worker_runtime.hermes)\"\n fi\n echo \"\"\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"${AGENTTEAMS_DEFAULT_WORKER_RUNTIME:-copaw}\"\n elif [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_DEFAULT_WORKER_RUNTIME}\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg worker_runtime.title_short)\" \"${AGENTTEAMS_DEFAULT_WORKER_RUNTIME}\")\"\n local _runtime_choice\n read -e -p \"$(msg worker_runtime.choice): \" _runtime_choice\n if [ \"${_runtime_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n if [ -n \"${_runtime_choice}\" ]; then\n case \"${_runtime_choice}\" in\n 2) AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"openclaw\" ;;\n 3) if ! _ver_lt \"${AGENTTEAMS_VERSION}\" \"v1.1.0\"; then\n AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"hermes\"\n else\n AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"copaw\"\n fi ;;\n *) AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"copaw\" ;;\n esac\n fi\n elif [ -z \"${AGENTTEAMS_DEFAULT_WORKER_RUNTIME+x}\" ]; then\n local _runtime_choice\n read -e -p \"$(msg worker_runtime.choice): \" _runtime_choice\n if [ \"${_runtime_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _runtime_choice=\"${_runtime_choice:-1}\"\n case \"${_runtime_choice}\" in\n 2) AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"openclaw\" ;;\n 3) if ! _ver_lt \"${AGENTTEAMS_VERSION}\" \"v1.1.0\"; then\n AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"hermes\"\n else\n AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"copaw\"\n fi ;;\n *) AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"copaw\" ;;\n esac\n fi\n export AGENTTEAMS_DEFAULT_WORKER_RUNTIME\n log \"$(msg worker_runtime.selected \"${AGENTTEAMS_DEFAULT_WORKER_RUNTIME}\")\"\n}\n\nstep_manager_runtime() {\n log \"$(msg manager_runtime.title)\"\n echo \"\"\n echo \" 1) $(msg manager_runtime.copaw)\"\n echo \" 2) $(msg manager_runtime.openclaw)\"\n echo \"\"\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_MANAGER_RUNTIME=\"${AGENTTEAMS_MANAGER_RUNTIME:-copaw}\"\n elif [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_MANAGER_RUNTIME}\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg manager_runtime.title_short)\" \"${AGENTTEAMS_MANAGER_RUNTIME}\")\"\n local _runtime_choice\n read -e -p \"$(msg manager_runtime.choice): \" _runtime_choice\n if [ \"${_runtime_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n if [ -n \"${_runtime_choice}\" ]; then\n case \"${_runtime_choice}\" in\n 2) AGENTTEAMS_MANAGER_RUNTIME=\"openclaw\" ;;\n *) AGENTTEAMS_MANAGER_RUNTIME=\"copaw\" ;;\n esac\n fi\n elif [ -z \"${AGENTTEAMS_MANAGER_RUNTIME+x}\" ]; then\n local _runtime_choice\n read -e -p \"$(msg manager_runtime.choice): \" _runtime_choice\n if [ \"${_runtime_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _runtime_choice=\"${_runtime_choice:-1}\"\n case \"${_runtime_choice}\" in\n 2) AGENTTEAMS_MANAGER_RUNTIME=\"openclaw\" ;;\n *) AGENTTEAMS_MANAGER_RUNTIME=\"copaw\" ;;\n esac\n fi\n export AGENTTEAMS_MANAGER_RUNTIME\n log \"$(msg manager_runtime.selected \"${AGENTTEAMS_MANAGER_RUNTIME}\")\"\n}\n\nstep_e2ee() {\n log \"\"\n log \"$(msg matrix_e2ee.title)\"\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_MATRIX_E2EE=\"${AGENTTEAMS_MATRIX_E2EE:-0}\"\n log \" $(msg matrix_e2ee.title_short) = ${AGENTTEAMS_MATRIX_E2EE} (non-interactive, skipped)\"\n export AGENTTEAMS_MATRIX_E2EE\n return 0\n fi\n # ─────────────────────────────────────────────────────────────────\n echo \"\"\n echo -e \" $(msg matrix_e2ee.desc)\"\n echo \"\"\n echo \" 1) $(msg matrix_e2ee.disable)\"\n echo \" 2) $(msg matrix_e2ee.enable)\"\n echo \"\"\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_MATRIX_E2EE}\" ]; then\n local _e2ee_display; if [ \"${AGENTTEAMS_MATRIX_E2EE}\" = \"1\" ]; then _e2ee_display=\"$(msg matrix_e2ee.val_enabled)\"; else _e2ee_display=\"$(msg matrix_e2ee.val_disabled)\"; fi\n log \"$(msg prompt.upgrade_keep \"$(msg matrix_e2ee.title_short)\" \"${_e2ee_display}\")\"\n local _e2ee_choice\n read -e -p \"$(msg matrix_e2ee.choice): \" _e2ee_choice\n if [ \"${_e2ee_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n if [ -n \"${_e2ee_choice}\" ]; then\n case \"${_e2ee_choice}\" in\n 2) AGENTTEAMS_MATRIX_E2EE=\"1\" ;;\n *) AGENTTEAMS_MATRIX_E2EE=\"0\" ;;\n esac\n fi\n elif [ -z \"${AGENTTEAMS_MATRIX_E2EE+x}\" ]; then\n local _e2ee_choice\n read -e -p \"$(msg matrix_e2ee.choice): \" _e2ee_choice\n if [ \"${_e2ee_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _e2ee_choice=\"${_e2ee_choice:-1}\"\n case \"${_e2ee_choice}\" in\n 2) AGENTTEAMS_MATRIX_E2EE=\"1\" ;;\n *) AGENTTEAMS_MATRIX_E2EE=\"0\" ;;\n esac\n fi\n AGENTTEAMS_MATRIX_E2EE=\"${AGENTTEAMS_MATRIX_E2EE:-0}\"\n export AGENTTEAMS_MATRIX_E2EE\n if [ \"${AGENTTEAMS_MATRIX_E2EE}\" = \"1\" ]; then\n log \"$(msg matrix_e2ee.selected_enabled)\"\n else\n log \"$(msg matrix_e2ee.selected_disabled)\"\n fi\n}\n\nstep_docker_proxy() {\n # Only relevant when socket mounting is enabled\n if [ \"${AGENTTEAMS_MOUNT_SOCKET}\" != \"1\" ]; then\n AGENTTEAMS_DOCKER_PROXY=\"0\"\n return 0\n fi\n\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_DOCKER_PROXY=\"${AGENTTEAMS_DOCKER_PROXY:-0}\"\n log \" $(msg docker_proxy.title_short) = ${AGENTTEAMS_DOCKER_PROXY} (non-interactive, skipped)\"\n export AGENTTEAMS_DOCKER_PROXY\n return 0\n fi\n # ─────────────────────────────────────────────────────────────────\n\n echo \"\"\n echo -e \" \\033[1m$(msg docker_proxy.title)\\033[0m\"\n echo \"\"\n echo -e \" $(msg docker_proxy.desc)\"\n echo \"\"\n echo \" 1) $(msg docker_proxy.enable)\"\n echo \" 2) $(msg docker_proxy.disable)\"\n echo \"\"\n\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_DOCKER_PROXY}\" ]; then\n local _proxy_display; if [ \"${AGENTTEAMS_DOCKER_PROXY}\" = \"1\" ]; then _proxy_display=\"$(msg docker_proxy.val_enabled)\"; else _proxy_display=\"$(msg docker_proxy.val_disabled)\"; fi\n log \"$(msg prompt.upgrade_keep \"$(msg docker_proxy.title_short)\" \"${_proxy_display}\")\"\n local _choice\n read -e -p \"$(msg docker_proxy.choice): \" _choice\n if [ \"${_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n if [ -n \"${_choice}\" ]; then\n case \"${_choice}\" in\n 2) AGENTTEAMS_DOCKER_PROXY=\"0\" ;;\n *) AGENTTEAMS_DOCKER_PROXY=\"1\" ;;\n esac\n fi\n elif [ -z \"${AGENTTEAMS_DOCKER_PROXY+x}\" ]; then\n local _choice\n read -e -p \"$(msg docker_proxy.choice): \" _choice\n if [ \"${_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _choice=\"${_choice:-1}\"\n case \"${_choice}\" in\n 2) AGENTTEAMS_DOCKER_PROXY=\"0\" ;;\n *) AGENTTEAMS_DOCKER_PROXY=\"1\" ;;\n esac\n fi\n AGENTTEAMS_DOCKER_PROXY=\"${AGENTTEAMS_DOCKER_PROXY:-1}\"\n export AGENTTEAMS_DOCKER_PROXY\n if [ \"${AGENTTEAMS_DOCKER_PROXY}\" = \"1\" ]; then\n log \"$(msg docker_proxy.selected_enabled)\"\n\n # Prompt for additional allowed image sources\n echo \"\"\n echo -e \" $(msg docker_proxy.registries_desc)\"\n echo \"\"\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_PROXY_ALLOWED_REGISTRIES}\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg docker_proxy.registries_label)\" \"${AGENTTEAMS_PROXY_ALLOWED_REGISTRIES}\")\"\n local _reg_input\n read -e -p \"$(msg docker_proxy.registries_prompt): \" _reg_input\n if [ \"${_reg_input}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n [ -n \"${_reg_input}\" ] && AGENTTEAMS_PROXY_ALLOWED_REGISTRIES=\"${_reg_input}\"\n elif [ -z \"${AGENTTEAMS_PROXY_ALLOWED_REGISTRIES+x}\" ]; then\n local _reg_input\n read -e -p \"$(msg docker_proxy.registries_prompt): \" _reg_input\n if [ \"${_reg_input}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_PROXY_ALLOWED_REGISTRIES=\"${_reg_input:-}\"\n fi\n export AGENTTEAMS_PROXY_ALLOWED_REGISTRIES\n else\n log \"$(msg docker_proxy.selected_disabled)\"\n fi\n}\n\nstep_idle() {\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_WORKER_IDLE_TIMEOUT=\"${AGENTTEAMS_WORKER_IDLE_TIMEOUT:-720}\"\n log \" $(msg idle_timeout.label) = ${AGENTTEAMS_WORKER_IDLE_TIMEOUT} (non-interactive, skipped)\"\n export AGENTTEAMS_WORKER_IDLE_TIMEOUT\n return 0\n fi\n # ─────────────────────────────────────────────────────────────────\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_WORKER_IDLE_TIMEOUT}\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg idle_timeout.label)\" \"${AGENTTEAMS_WORKER_IDLE_TIMEOUT}\")\"\n local _idle_timeout\n read -e -p \"$(msg idle_timeout.prompt): \" _idle_timeout\n if [ \"${_idle_timeout}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n [ -n \"${_idle_timeout}\" ] && AGENTTEAMS_WORKER_IDLE_TIMEOUT=\"${_idle_timeout}\"\n elif [ -z \"${AGENTTEAMS_WORKER_IDLE_TIMEOUT+x}\" ]; then\n local _idle_timeout\n read -e -p \"$(msg idle_timeout.prompt): \" _idle_timeout\n if [ \"${_idle_timeout}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_WORKER_IDLE_TIMEOUT=\"${_idle_timeout:-720}\"\n fi\n AGENTTEAMS_WORKER_IDLE_TIMEOUT=\"${AGENTTEAMS_WORKER_IDLE_TIMEOUT:-720}\"\n export AGENTTEAMS_WORKER_IDLE_TIMEOUT\n log \"$(msg idle_timeout.selected \"${AGENTTEAMS_WORKER_IDLE_TIMEOUT}\")\"\n}\n\nstep_hostshare() {\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_HOST_SHARE_DIR=\"${AGENTTEAMS_HOST_SHARE_DIR:-${HOME}}\"\n log \" $(msg host_share.label) = ${AGENTTEAMS_HOST_SHARE_DIR} (non-interactive, skipped)\"\n export AGENTTEAMS_HOST_SHARE_DIR\n return 0\n fi\n # ─────────────────────────────────────────────────────────────────\n local _current=\"${AGENTTEAMS_HOST_SHARE_DIR}\"\n local _share_dir\n if [ -n \"${_current}\" ]; then\n read -e -p \"$(msg host_share.prompt \"$HOME\") [${_current}]: \" _share_dir\n else\n read -e -p \"$(msg host_share.prompt \"$HOME\"): \" _share_dir\n fi\n if [ \"${_share_dir}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_HOST_SHARE_DIR=\"${_share_dir:-${_current:-$HOME}}\"\n export AGENTTEAMS_HOST_SHARE_DIR\n}\n\nstep_podman_autostart() {\n echo \"\"\n log \"$(msg podman.autostart.title)\"\n echo \"\"\n echo -e \" $(msg podman.autostart.desc)\"\n echo \"\"\n echo \" 1) $(msg podman.autostart.enable)\"\n echo \" 2) $(msg podman.autostart.disable)\"\n echo \"\"\n\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_PODMAN_AUTOSTART}\" ]; then\n local _disp; if [ \"${AGENTTEAMS_PODMAN_AUTOSTART}\" = \"1\" ]; then _disp=\"$(msg podman.autostart.val_enabled)\"; else _disp=\"$(msg podman.autostart.val_disabled)\"; fi\n log \"$(msg prompt.upgrade_keep \"$(msg podman.autostart.title_short)\" \"${_disp}\")\"\n local _choice\n read -e -p \"$(msg podman.autostart.choice): \" _choice\n if [ \"${_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n if [ -n \"${_choice}\" ]; then\n case \"${_choice}\" in\n 2) AGENTTEAMS_PODMAN_AUTOSTART=\"0\" ;;\n *) AGENTTEAMS_PODMAN_AUTOSTART=\"1\" ;;\n esac\n fi\n elif [ -z \"${AGENTTEAMS_PODMAN_AUTOSTART+x}\" ]; then\n local _choice\n read -e -p \"$(msg podman.autostart.choice) [1]: \" _choice\n if [ \"${_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _choice=\"${_choice:-1}\"\n case \"${_choice}\" in\n 2) AGENTTEAMS_PODMAN_AUTOSTART=\"0\" ;;\n *) AGENTTEAMS_PODMAN_AUTOSTART=\"1\" ;;\n esac\n fi\n AGENTTEAMS_PODMAN_AUTOSTART=\"${AGENTTEAMS_PODMAN_AUTOSTART:-1}\"\n export AGENTTEAMS_PODMAN_AUTOSTART\n if [ \"${AGENTTEAMS_PODMAN_AUTOSTART}\" = \"1\" ]; then\n log \"$(msg podman.autostart.selected_enabled)\"\n else\n log \"$(msg podman.autostart.selected_disabled)\"\n fi\n}\n\n# ============================================================\n# Setup dedicated Podman autostart service for AgentTeams\n# ============================================================\nsetup_podman_autostart() {\n # Only execute if using podman and autostart is enabled\n if [ \"${DOCKER_CMD:-docker}\" != \"podman\" ] || [ \"${AGENTTEAMS_PODMAN_AUTOSTART:-0}\" != \"1\" ]; then\n return 0\n fi\n\n log \"$(msg install.podman.autostart_title)\"\n\n local current_user\n current_user=\"$(whoami)\"\n\n # Define the dedicated AgentTeams service content\n # Strict scoping: Only manage containers matching '--filter name=agentteams'\n local _service_content=\"[Unit]\nDescription=AgentTeams Dedicated Podman Autostart Service\nDocumentation=https://github.com/agentscope-ai/AgentTeams\nWants=network-online.target\nAfter=network-online.target\n\n[Service]\nType=oneshot\nRemainAfterExit=true\nEnvironment=LOGGING=\\\"--log-level=info\\\"\nExecStart=/usr/bin/podman \\$LOGGING start --filter name=agentteams --filter restart-policy=always --filter restart-policy=unless-stopped\nExecStop=/usr/bin/podman \\$LOGGING stop --filter name=agentteams --filter restart-policy=always --filter restart-policy=unless-stopped\n\n[Install]\nWantedBy=default.target\"\n\n local _autostart_ok=0\n\n if [ \"${current_user}\" = \"root\" ]; then\n # Rootful Mode (System-wide dedicated service)\n log \"$(msg install.podman.root_setup)\"\n\n local _sys_dir=\"/etc/systemd/system\"\n mkdir -p \"${_sys_dir}\"\n echo \"${_service_content}\" > \"${_sys_dir}/agentteams-podman-restart.service\"\n\n systemctl daemon-reload >/dev/null 2>&1 || true\n\n if systemctl enable --now agentteams-podman-restart.service >/dev/null 2>&1; then\n log \"$(msg install.podman.root_success)\"\n _autostart_ok=1\n else\n log \"$(msg install.podman.root_fail)\"\n fi\n else\n # Rootless Mode (User-level dedicated service)\n log \"$(msg install.podman.user_setup \"${current_user}\")\"\n\n local _linger_ok=0\n\n # 1. Enable Linger for background execution (Graceful escalation strategy)\n if loginctl show-user \"${current_user}\" --property=Linger 2>/dev/null | grep -q \"Linger=yes\"; then\n _linger_ok=1\n else\n log \"$(msg install.podman.linger_enable)\"\n\n # Step A: Try to enable linger natively without sudo\n if loginctl enable-linger \"${current_user}\" >/dev/null 2>&1; then\n log \"Successfully enabled systemd linger natively.\"\n _linger_ok=1\n else\n # Step B: Fallback to sudo\n log \"Native linger enablement failed. Distro security policy requires privileges.\"\n if command -v sudo >/dev/null 2>&1; then\n log \"Attempting to enable systemd linger via sudo (enter password if prompted)...\"\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n sudo -n loginctl enable-linger \"${current_user}\" 2>/dev/null && _linger_ok=1 || log \"$(msg install.podman.linger_warn)\"\n else\n sudo loginctl enable-linger \"${current_user}\" && _linger_ok=1 || log \"$(msg install.podman.linger_warn)\"\n fi\n else\n log \"$(msg install.podman.linger_warn)\"\n fi\n fi\n fi\n\n # 2. Write dedicated service file to user directory\n local _user_dir=\"${HOME}/.config/systemd/user\"\n mkdir -p \"${_user_dir}\"\n echo \"${_service_content}\" > \"${_user_dir}/agentteams-podman-restart.service\"\n\n systemctl --user daemon-reload >/dev/null 2>&1 || true\n\n if systemctl --user enable --now agentteams-podman-restart.service >/dev/null 2>&1; then\n log \"$(msg install.podman.user_success)\"\n # Crucial: Only consider autostart fully successful if systemd linger is also enabled\n if [ \"${_linger_ok}\" = \"1\" ]; then\n _autostart_ok=1\n else\n log \"⚠️ Service enabled, but since systemd linger is not active, containers will not auto-recover on system reboot.\"\n fi\n else\n log \"$(msg install.podman.user_fail)\"\n fi\n fi\n\n # Only print final success banner if the service AND linger were actually enabled\n if [ \"${_autostart_ok}\" = \"1\" ]; then\n log \"$(msg install.podman.success)\"\n fi\n}\n\n# ============================================================\n# Dashboard startup (called after controller is ready)\n# ============================================================\n\n_start_dashboard() {\n local CTRL_CONTAINER=\"agentteams-controller\"\n local DASHBOARD_CONTAINER=\"agentteams-dashboard\"\n\n if [ \"${AGENTTEAMS_DASHBOARD:-0}\" != \"1\" ]; then\n # Stop and remove the existing dashboard container if present.\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -q \"^${DASHBOARD_CONTAINER}$\"; then\n log \"Stopping and removing ${DASHBOARD_CONTAINER}...\"\n ${DOCKER_CMD} stop \"${DASHBOARD_CONTAINER}\" >/dev/null 2>&1 || true\n ${DOCKER_CMD} rm -f \"${DASHBOARD_CONTAINER}\" >/dev/null 2>&1 || true\n log \"Dashboard stopped.\"\n fi\n return 0\n fi\n\n # Dashboard requires the embedded architecture (controller container).\n # In legacy all-in-one mode the controller API is not available.\n if [ \"${AGENTTEAMS_USE_EMBEDDED:-0}\" != \"1\" ]; then\n log \"Skipping Dashboard: requires embedded architecture (agentteams-controller), not available in legacy mode.\"\n log \"Use Quick Start (embedded) mode to enable agentteams-dashboard.\"\n return 0\n fi\n\n AGENTTEAMS_PORT_DASHBOARD=\"${AGENTTEAMS_PORT_DASHBOARD:-13000}\"\n AGENTTEAMS_DASHBOARD_VERSION=\"${AGENTTEAMS_DASHBOARD_VERSION:-v1.2.0-beta.1}\"\n AGENTTEAMS_DASHBOARD_IMAGE=\"${AGENTTEAMS_DASHBOARD_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-dashboard:${AGENTTEAMS_DASHBOARD_VERSION}}\"\n\n log \"\"\n log \"Starting agentteams-dashboard...\"\n\n local BIND_ADDR=\"127.0.0.1\"\n [ \"${AGENTTEAMS_LOCAL_ONLY:-1}\" = \"0\" ] && BIND_ADDR=\"0.0.0.0\"\n\n # Remove existing container\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' | grep -qx \"${DASHBOARD_CONTAINER}\"; then\n ${DOCKER_CMD} rm -f \"${DASHBOARD_CONTAINER}\" >/dev/null 2>&1 || true\n fi\n\n # Pull or use local image\n if ! ${DOCKER_CMD} image inspect \"${AGENTTEAMS_DASHBOARD_IMAGE}\" >/dev/null 2>&1; then\n log \"Pulling dashboard image: ${AGENTTEAMS_DASHBOARD_IMAGE}\"\n ${DOCKER_CMD} pull \"${AGENTTEAMS_DASHBOARD_IMAGE}\" 2>/dev/null || {\n log \"WARNING: Could not pull dashboard image. Skipping.\"\n return 0\n }\n fi\n\n # Build env args from controller container (MinIO/LLM/auth).\n local env_args=()\n env_args+=(-e AGENTTEAMS_CONTROLLER_URL=\"http://${CTRL_CONTAINER}:8090\")\n env_args+=(-e NEXT_PUBLIC_MATRIX_API_URL=\"http://${CTRL_CONTAINER}:6167\")\n env_args+=(-e MATRIX_HOMESERVER_ALLOWLIST=\"${CTRL_CONTAINER},matrix-local.agentteams.io,matrix.org\")\n\n if ${DOCKER_CMD} ps --format '{{.Names}}' | grep -qx \"${CTRL_CONTAINER}\"; then\n local env_out\n env_out=$(${DOCKER_CMD} inspect \"${CTRL_CONTAINER}\" --format='{{range .Config.Env}}{{.}}{{\"\\n\"}}{{end}}')\n\n _env() { echo \"${env_out}\" | sed -n \"s|^${1}=||p\" | head -n1; }\n\n local fs_endpoint; fs_endpoint=$(_env AGENTTEAMS_FS_ENDPOINT)\n [ -z \"${fs_endpoint}\" ] && fs_endpoint=$(_env AGENTTEAMS_MINIO_ENDPOINT)\n if [ -n \"${fs_endpoint}\" ]; then\n fs_endpoint=$(echo \"${fs_endpoint}\" | sed -e \"s|127\\.0\\.0\\.1|${CTRL_CONTAINER}|\" -e \"s|localhost|${CTRL_CONTAINER}|\")\n env_args+=(-e AGENTTEAMS_FS_ENDPOINT=\"${fs_endpoint}\")\n else\n env_args+=(-e AGENTTEAMS_FS_ENDPOINT=\"http://${CTRL_CONTAINER}:9000\")\n fi\n\n local fs_bucket; fs_bucket=$(_env AGENTTEAMS_FS_BUCKET)\n [ -z \"${fs_bucket}\" ] && fs_bucket=$(_env AGENTTEAMS_MINIO_BUCKET)\n [ -n \"${fs_bucket}\" ] && env_args+=(-e AGENTTEAMS_FS_BUCKET=\"${fs_bucket}\")\n\n local fs_user; fs_user=$(_env AGENTTEAMS_FS_ACCESS_KEY)\n [ -z \"${fs_user}\" ] && fs_user=$(_env AGENTTEAMS_MINIO_USER)\n [ -n \"${fs_user}\" ] && env_args+=(-e AGENTTEAMS_FS_ACCESS_KEY=\"${fs_user}\")\n\n local fs_pass; fs_pass=$(_env AGENTTEAMS_FS_SECRET_KEY)\n [ -z \"${fs_pass}\" ] && fs_pass=$(_env AGENTTEAMS_MINIO_PASSWORD)\n [ -n \"${fs_pass}\" ] && env_args+=(-e AGENTTEAMS_FS_SECRET_KEY=\"${fs_pass}\")\n\n for k in AGENTTEAMS_LLM_PROVIDER AGENTTEAMS_LLM_API_KEY AGENTTEAMS_OPENAI_BASE_URL AGENTTEAMS_DEFAULT_MODEL; do\n local v; v=$(_env \"${k}\")\n [ -n \"${v}\" ] && env_args+=(-e \"${k}=${v}\")\n done\n\n # The controller writes the CLI SA token to /var/run/agentteams/cli-token.\n # The token is generated asynchronously during the first reconcile loop,\n # so it may not be available immediately after the container starts.\n # We poll for up to 30s to handle both fresh installs (token not yet minted)\n # and older images that don't support cli-token at all.\n # For backward compatibility, also try the legacy /var/run/hiclaw/cli-token\n # path used by older HiClaw images.\n local auth_token=\"${AGENTTEAMS_AUTH_TOKEN:-}\"\n if [ -z \"${auth_token}\" ]; then\n local _token_wait=0\n local _token_max_wait=30\n while [ ${_token_wait} -lt ${_token_max_wait} ]; do\n auth_token=$(${DOCKER_CMD} exec \"${CTRL_CONTAINER}\" sh -c 'cat /var/run/agentteams/cli-token 2>/dev/null || cat /var/run/hiclaw/cli-token 2>/dev/null' | tr -d '\\n' || true)\n if [ -n \"${auth_token}\" ]; then\n break\n fi\n sleep 2\n _token_wait=$((_token_wait + 2))\n done\n fi\n if [ -n \"${auth_token}\" ]; then\n env_args+=(-e AGENTTEAMS_AUTH_TOKEN=\"${auth_token}\")\n else\n log \"WARNING: could not read controller SA token (timed out after ${_token_max_wait:-30}s)\"\n log \" This can happen with older AgentTeams/HiClaw images or if the controller is still starting.\"\n log \" Dashboard will still work but some API calls may fail until you log in via Higress Console.\"\n log \" To fix: upgrade AgentTeams to v1.2.0-beta.1+ or set AGENTTEAMS_AUTH_TOKEN manually.\"\n fi\n\n local admin_user; admin_user=$(_env AGENTTEAMS_ADMIN_USER)\n local admin_pass; admin_pass=$(_env AGENTTEAMS_ADMIN_PASSWORD)\n [ -n \"${admin_user}\" ] && env_args+=(-e AGENTTEAMS_ADMIN_USER=\"${admin_user}\")\n [ -n \"${admin_pass}\" ] && env_args+=(-e AGENTTEAMS_ADMIN_PASSWORD=\"${admin_pass}\")\n\n # Higress Console URL: explicit config takes priority, auto-detect as fallback.\n local _resolved_gw_url=\"\"\n if [ -n \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\" ]; then\n # Normalize URL: add http:// prefix if missing\n _resolved_gw_url=\"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\"\n case \"${_resolved_gw_url}\" in\n http://*|https://*) ;;\n *) _resolved_gw_url=\"http://${_resolved_gw_url}\" ;;\n esac\n env_args+=(-e AGENTTEAMS_AI_GATEWAY_ADMIN_URL=\"${_resolved_gw_url}\")\n elif ${DOCKER_CMD} exec \"${CTRL_CONTAINER}\" wget -q -O- --timeout=2 http://127.0.0.1:8001/ >/dev/null 2>&1; then\n _resolved_gw_url=\"http://${CTRL_CONTAINER}:8001\"\n env_args+=(-e AGENTTEAMS_AI_GATEWAY_ADMIN_URL=\"${_resolved_gw_url}\")\n fi\n\n # Verify Higress Console URL reachability (best-effort warning)\n if [ -n \"${_resolved_gw_url}\" ]; then\n if ! ${DOCKER_CMD} exec \"${CTRL_CONTAINER}\" curl -sf --max-time 3 \"${_resolved_gw_url}/\" >/dev/null 2>&1; then\n log \"WARNING: Higress Console URL may not be reachable: ${_resolved_gw_url}\"\n log \" Dashboard will still work, but shared login via Higress Console may fail.\"\n log \" To fix: verify the URL is correct and the Higress Console service is running.\"\n fi\n fi\n else\n log \"WARNING: ${CTRL_CONTAINER} not running; launching Dashboard without Controller/MinIO/LLM env.\"\n if [ -n \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\" ]; then\n local _gw_norm=\"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\"\n case \"${_gw_norm}\" in\n http://*|https://*) ;;\n *) _gw_norm=\"http://${_gw_norm}\" ;;\n esac\n env_args+=(-e AGENTTEAMS_AI_GATEWAY_ADMIN_URL=\"${_gw_norm}\")\n fi\n fi\n\n # Create persistent data volume (for future use)\n ${DOCKER_CMD} volume create agentteams-dashboard-data >/dev/null 2>&1 || true\n\n ${DOCKER_CMD} run -d \\\n --name \"${DASHBOARD_CONTAINER}\" \\\n --restart unless-stopped \\\n --network \"${AGENTTEAMS_NETWORK:-agentteams-net}\" \\\n --network-alias dashboard.agentteams.io \\\n -p \"${BIND_ADDR}:${AGENTTEAMS_PORT_DASHBOARD}:3000\" \\\n \"${env_args[@]}\" \\\n -v agentteams-dashboard-data:/app/db \\\n \"${AGENTTEAMS_DASHBOARD_IMAGE}\"\n\n # Wait for dashboard to be ready\n local max_wait=60\n local waited=0\n while [ $waited -lt $max_wait ]; do\n if curl -sf \"http://127.0.0.1:${AGENTTEAMS_PORT_DASHBOARD}/\" >/dev/null 2>&1; then\n log \"Dashboard ready on port ${AGENTTEAMS_PORT_DASHBOARD}\"\n return 0\n fi\n sleep 2\n waited=$((waited + 2))\n done\n log \"WARNING: Dashboard startup timed out after ${max_wait}s\"\n ${DOCKER_CMD} logs \"${DASHBOARD_CONTAINER}\" 2>&1 | tail -20\n return 0\n}\n\n# ============================================================\n# Manager Installation (Interactive)\n# ============================================================\n\ninstall_manager() {\n log \"$(msg install.title)\"\n log \"$(msg install.registry \"${AGENTTEAMS_REGISTRY}\")\"\n log \"\"\n log \"$(msg install.dir \"$(pwd)\")\"\n log \"$(msg install.dir_hint)\"\n log \"$(msg install.dir_hint2)\"\n log \"\"\n\n # Non-interactive fallback: resolve version immediately so image tags are available\n # before the step state machine runs. Interactive mode lets step_version handle it.\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_VERSION=\"${AGENTTEAMS_VERSION:-${AGENTTEAMS_KNOWN_STABLE_VERSION}}\"\n resolve_image_tags\n fi\n\n # Migrate legacy env file location before checks\n local existing_env=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n if [ ! -f \"${existing_env}\" ] && [ -f \"./agentteams-manager.env\" ]; then\n log \"Migrating agentteams-manager.env from current directory to ${existing_env}...\"\n mv \"./agentteams-manager.env\" \"${existing_env}\"\n fi\n\n # Orphan volume detection (only when no env file — step_existing handles the env-file case)\n if [ ! -f \"${existing_env}\" ]; then\n local data_vol=\"${AGENTTEAMS_DATA_DIR:-agentteams-data}\"\n if ${DOCKER_CMD} volume ls -q | grep -q \"^${data_vol}$\"; then\n echo \"\"\n log \"$(msg install.orphan_volume.detected \"${data_vol}\")\"\n log \"$(msg install.orphan_volume.warn)\"\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n log \"$(msg install.orphan_volume.clean_noninteractive)\"\n ${DOCKER_CMD} stop agentteams-manager 2>/dev/null || true\n ${DOCKER_CMD} rm agentteams-manager 2>/dev/null || true\n for w in $(${DOCKER_CMD} ps -a --format '{{.Names}}' | grep \"^agentteams-worker-\" || true); do\n ${DOCKER_CMD} stop \"${w}\" 2>/dev/null || true\n ${DOCKER_CMD} rm \"${w}\" 2>/dev/null || true\n done\n log \"$(msg install.orphan_volume.cleaning)\"\n ${DOCKER_CMD} volume rm \"${data_vol}\" 2>/dev/null || true\n log \"$(msg install.orphan_volume.cleaned)\"\n else\n echo \"\"\n echo \"$(msg install.orphan_volume.choose)\"\n echo \"$(msg install.orphan_volume.clean)\"\n echo \"$(msg install.orphan_volume.keep)\"\n echo \"\"\n local ORPHAN_CHOICE\n read -e -p \"$(msg install.orphan_volume.prompt): \" ORPHAN_CHOICE\n ORPHAN_CHOICE=\"${ORPHAN_CHOICE:-1}\"\n case \"${ORPHAN_CHOICE}\" in\n 1|clean)\n ${DOCKER_CMD} stop agentteams-manager 2>/dev/null || true\n ${DOCKER_CMD} rm agentteams-manager 2>/dev/null || true\n for w in $(${DOCKER_CMD} ps -a --format '{{.Names}}' | grep \"^agentteams-worker-\" || true); do\n ${DOCKER_CMD} stop \"${w}\" 2>/dev/null || true\n ${DOCKER_CMD} rm \"${w}\" 2>/dev/null || true\n done\n log \"$(msg install.orphan_volume.cleaning)\"\n ${DOCKER_CMD} volume rm \"${data_vol}\" 2>/dev/null || true\n log \"$(msg install.orphan_volume.cleaned)\"\n ;;\n 2|keep)\n log \"$(msg install.orphan_volume.keeping)\"\n ;;\n esac\n fi\n fi\n fi\n\n # ── State machine ─────────────────────────────────────────────────────────\n local _STEPS=( step_lang step_mode step_version step_existing step_llm step_manager_runtime step_runtime step_admin step_network\n step_ports step_domains step_github step_skills step_volume\n step_workspace step_dashboard step_e2ee step_docker_proxy step_idle step_hostshare step_podman_autostart )\n local _STEP_HISTORY=()\n local _step_idx=0\n while [ \"${_step_idx}\" -lt \"${#_STEPS[@]}\" ]; do\n local _step_fn=\"${_STEPS[$_step_idx]}\"\n if should_skip_step \"${_step_fn}\"; then\n _step_idx=$((_step_idx + 1))\n continue\n fi\n if [ \"${#_STEP_HISTORY[@]}\" -gt 0 ]; then\n log \"$(msg nav.back_hint)\"\n fi\n STEP_RESULT=\"\"\n \"${_step_fn}\"\n if [ \"${STEP_RESULT}\" = \"back\" ]; then\n if [ \"${#_STEP_HISTORY[@]}\" -gt 0 ]; then\n local _last=$(( ${#_STEP_HISTORY[@]} - 1 ))\n _step_idx=\"${_STEP_HISTORY[$_last]}\"\n _STEP_HISTORY=(\"${_STEP_HISTORY[@]:0:${_last}}\")\n clear_step_vars \"${_STEPS[$_step_idx]}\"\n fi\n # else: first step, ignore 'b'\n else\n _STEP_HISTORY+=(\"${_step_idx}\")\n _step_idx=$((_step_idx + 1))\n fi\n done\n # ── End state machine ──────────────────────────────────────────────────────\n\n # Post-machine defaults for any steps that were skipped\n AGENTTEAMS_DATA_DIR=\"${AGENTTEAMS_DATA_DIR:-agentteams-data}\"\n if [ -z \"${AGENTTEAMS_WORKSPACE_DIR+x}\" ] || [ -z \"${AGENTTEAMS_WORKSPACE_DIR}\" ]; then\n AGENTTEAMS_WORKSPACE_DIR=\"${HOME}/agentteams-manager\"\n export AGENTTEAMS_WORKSPACE_DIR\n fi\n AGENTTEAMS_WORKSPACE_DIR=\"$(cd \"${AGENTTEAMS_WORKSPACE_DIR}\" 2>/dev/null && pwd || echo \"${AGENTTEAMS_WORKSPACE_DIR}\")\"\n mkdir -p \"${AGENTTEAMS_WORKSPACE_DIR}\"\n AGENTTEAMS_MANAGER_RUNTIME=\"${AGENTTEAMS_MANAGER_RUNTIME:-copaw}\"\n export AGENTTEAMS_MANAGER_RUNTIME\n AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"${AGENTTEAMS_DEFAULT_WORKER_RUNTIME:-copaw}\"\n AGENTTEAMS_MATRIX_E2EE=\"${AGENTTEAMS_MATRIX_E2EE:-0}\"\n export AGENTTEAMS_MATRIX_E2EE\n AGENTTEAMS_WORKER_IDLE_TIMEOUT=\"${AGENTTEAMS_WORKER_IDLE_TIMEOUT:-720}\"\n export AGENTTEAMS_WORKER_IDLE_TIMEOUT\n AGENTTEAMS_HOST_SHARE_DIR=\"${AGENTTEAMS_HOST_SHARE_DIR:-$HOME}\"\n export AGENTTEAMS_HOST_SHARE_DIR\n\n log \"\"\n\n # Generate secrets (only if not already set)\n log \"$(msg install.generating_secrets)\"\n AGENTTEAMS_MANAGER_PASSWORD=\"${AGENTTEAMS_MANAGER_PASSWORD:-$(generate_key)}\"\n AGENTTEAMS_REGISTRATION_TOKEN=\"${AGENTTEAMS_REGISTRATION_TOKEN:-$(generate_key)}\"\n AGENTTEAMS_MINIO_USER=\"${AGENTTEAMS_MINIO_USER:-${AGENTTEAMS_ADMIN_USER}}\"\n AGENTTEAMS_MINIO_PASSWORD=\"${AGENTTEAMS_MINIO_PASSWORD:-${AGENTTEAMS_ADMIN_PASSWORD}}\"\n AGENTTEAMS_MANAGER_GATEWAY_KEY=\"${AGENTTEAMS_MANAGER_GATEWAY_KEY:-$(generate_key)}\"\n\n # Matrix AppService tokens — generate once during install/upgrade if not provided.\n # Persisted to env file so they survive controller restarts.\n AGENTTEAMS_MATRIX_APPSERVICE_ENABLED=\"${AGENTTEAMS_MATRIX_APPSERVICE_ENABLED:-true}\"\n export AGENTTEAMS_MATRIX_APPSERVICE_ENABLED\n if [ \"${AGENTTEAMS_MATRIX_APPSERVICE_ENABLED}\" != \"false\" ] && [ \"${AGENTTEAMS_MATRIX_APPSERVICE_ENABLED}\" != \"0\" ]; then\n if [ -z \"${AGENTTEAMS_MATRIX_APPSERVICE_AS_TOKEN}\" ]; then\n AGENTTEAMS_MATRIX_APPSERVICE_AS_TOKEN=\"$(openssl rand -hex 32)\"\n log \" Auto-generated AppService as_token (saved to env file)\"\n fi\n if [ -z \"${AGENTTEAMS_MATRIX_APPSERVICE_HS_TOKEN}\" ]; then\n AGENTTEAMS_MATRIX_APPSERVICE_HS_TOKEN=\"$(openssl rand -hex 32)\"\n log \" Auto-generated AppService hs_token (saved to env file)\"\n fi\n export AGENTTEAMS_MATRIX_APPSERVICE_AS_TOKEN\n export AGENTTEAMS_MATRIX_APPSERVICE_HS_TOKEN\n fi\n\n # Detect Apple Silicon (M1/M2/M3/M4) - need JVM fix for Higress Console\n # See: https://github.com/agentscope-ai/AgentTeams/issues/249\n if [ -z \"${JVM_ARGS:-}\" ] && [ \"$(uname -m)\" = \"arm64\" ] && [ \"$(uname -s)\" = \"Darwin\" ]; then\n log \"Apple Silicon detected - setting JVM_ARGS to fix Higress Console SIGILL issue\"\n JVM_ARGS=\"-XX:+UnlockDiagnosticVMOptions -XX:-UseAESCTRIntrinsics -XX:UseSVE=0\"\n fi\n\n # Write .env file\n ENV_FILE=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n cat > \"${ENV_FILE}\" << EOF\n# AgentTeams Manager Configuration\n# Generated by agentteams-install.sh on $(date)\n\n# Language\nAGENTTEAMS_LANGUAGE=${AGENTTEAMS_LANGUAGE}\n\n# LLM\nAGENTTEAMS_LLM_PROVIDER=${AGENTTEAMS_LLM_PROVIDER}\nAGENTTEAMS_DEFAULT_MODEL=${AGENTTEAMS_DEFAULT_MODEL}\nAGENTTEAMS_LLM_API_KEY=${AGENTTEAMS_LLM_API_KEY}\nAGENTTEAMS_OPENAI_BASE_URL=${AGENTTEAMS_OPENAI_BASE_URL:-}\nAGENTTEAMS_MODEL_CONTEXT_WINDOW=${AGENTTEAMS_MODEL_CONTEXT_WINDOW:-}\nAGENTTEAMS_MODEL_MAX_TOKENS=${AGENTTEAMS_MODEL_MAX_TOKENS:-}\nAGENTTEAMS_MODEL_REASONING=${AGENTTEAMS_MODEL_REASONING:-}\nAGENTTEAMS_MODEL_VISION=${AGENTTEAMS_MODEL_VISION:-}\n\n# Embedding model (empty = disabled, default: text-embedding-v4)\nAGENTTEAMS_EMBEDDING_MODEL=${AGENTTEAMS_EMBEDDING_MODEL}\n\n# Admin\nAGENTTEAMS_ADMIN_USER=${AGENTTEAMS_ADMIN_USER}\nAGENTTEAMS_ADMIN_PASSWORD=${AGENTTEAMS_ADMIN_PASSWORD}\n\n# Ports\nAGENTTEAMS_LOCAL_ONLY=${AGENTTEAMS_LOCAL_ONLY}\nAGENTTEAMS_PORT_GATEWAY=${AGENTTEAMS_PORT_GATEWAY}\nAGENTTEAMS_PORT_CONSOLE=${AGENTTEAMS_PORT_CONSOLE}\nAGENTTEAMS_PORT_ELEMENT_WEB=${AGENTTEAMS_PORT_ELEMENT_WEB}\nAGENTTEAMS_PORT_MANAGER_CONSOLE=${AGENTTEAMS_PORT_MANAGER_CONSOLE:-18888}\n\n# Manager runtime (openclaw | copaw)\nAGENTTEAMS_MANAGER_RUNTIME=${AGENTTEAMS_MANAGER_RUNTIME:-copaw}\n\n# Matrix\nAGENTTEAMS_MATRIX_DOMAIN=${AGENTTEAMS_MATRIX_DOMAIN}\nAGENTTEAMS_MATRIX_CLIENT_DOMAIN=${AGENTTEAMS_MATRIX_CLIENT_DOMAIN}\n\n# Gateway\nAGENTTEAMS_AI_GATEWAY_DOMAIN=${AGENTTEAMS_AI_GATEWAY_DOMAIN}\nAGENTTEAMS_MANAGER_GATEWAY_KEY=${AGENTTEAMS_MANAGER_GATEWAY_KEY}\n\n# File System\nAGENTTEAMS_FS_DOMAIN=${AGENTTEAMS_FS_DOMAIN}\nAGENTTEAMS_CONSOLE_DOMAIN=${AGENTTEAMS_CONSOLE_DOMAIN}\nAGENTTEAMS_MINIO_USER=${AGENTTEAMS_MINIO_USER}\nAGENTTEAMS_MINIO_PASSWORD=${AGENTTEAMS_MINIO_PASSWORD}\n\n# Internal\nAGENTTEAMS_MANAGER_PASSWORD=${AGENTTEAMS_MANAGER_PASSWORD}\nAGENTTEAMS_REGISTRATION_TOKEN=${AGENTTEAMS_REGISTRATION_TOKEN}\n\n# GitHub (optional)\nAGENTTEAMS_GITHUB_TOKEN=${AGENTTEAMS_GITHUB_TOKEN:-}\n\n# Nacos package import defaults\nAGENTTEAMS_NACOS_REGISTRY_URI=${AGENTTEAMS_NACOS_REGISTRY_URI:-nacos://market.agentteams.io:80/public}\nAGENTTEAMS_NACOS_USERNAME=${AGENTTEAMS_NACOS_USERNAME:-}\nAGENTTEAMS_NACOS_PASSWORD=${AGENTTEAMS_NACOS_PASSWORD:-}\nAGENTTEAMS_NACOS_TOKEN=${AGENTTEAMS_NACOS_TOKEN:-}\n\n# Skills Registry (optional, default: nacos://market.agentteams.io:80/public)\nAGENTTEAMS_SKILLS_API_URL=${AGENTTEAMS_SKILLS_API_URL:-nacos://market.agentteams.io:80/public}\n\n# OpenClaw CMS plugin (optional)\nAGENTTEAMS_CMS_TRACES_ENABLED=${AGENTTEAMS_CMS_TRACES_ENABLED:-false}\nAGENTTEAMS_CMS_ENDPOINT=${AGENTTEAMS_CMS_ENDPOINT:-}\nAGENTTEAMS_CMS_LICENSE_KEY=${AGENTTEAMS_CMS_LICENSE_KEY:-}\nAGENTTEAMS_CMS_PROJECT=${AGENTTEAMS_CMS_PROJECT:-}\nAGENTTEAMS_CMS_WORKSPACE=${AGENTTEAMS_CMS_WORKSPACE:-}\nAGENTTEAMS_CMS_SERVICE_NAME=${AGENTTEAMS_CMS_SERVICE_NAME:-agentteams-manager}\nAGENTTEAMS_CMS_METRICS_ENABLED=${AGENTTEAMS_CMS_METRICS_ENABLED:-false}\n\n# Worker images (for direct container creation)\nAGENTTEAMS_WORKER_IMAGE=${WORKER_IMAGE}\nAGENTTEAMS_COPAW_WORKER_IMAGE=${COPAW_WORKER_IMAGE}\nAGENTTEAMS_HERMES_WORKER_IMAGE=${HERMES_WORKER_IMAGE}\n\n# Default Worker runtime (openclaw | copaw | hermes)\nAGENTTEAMS_DEFAULT_WORKER_RUNTIME=${AGENTTEAMS_DEFAULT_WORKER_RUNTIME:-copaw}\n\n# Matrix E2EE (0=disabled, 1=enabled; default: 0)\nAGENTTEAMS_MATRIX_E2EE=${AGENTTEAMS_MATRIX_E2EE:-0}\n\n# Matrix AppService\nAGENTTEAMS_MATRIX_APPSERVICE_ENABLED=${AGENTTEAMS_MATRIX_APPSERVICE_ENABLED:-true}\nAGENTTEAMS_MATRIX_APPSERVICE_AS_TOKEN=${AGENTTEAMS_MATRIX_APPSERVICE_AS_TOKEN:-}\nAGENTTEAMS_MATRIX_APPSERVICE_HS_TOKEN=${AGENTTEAMS_MATRIX_APPSERVICE_HS_TOKEN:-}\n\n# Docker API proxy (0=disabled, 1=enabled; default: 1)\nAGENTTEAMS_DOCKER_PROXY=${AGENTTEAMS_DOCKER_PROXY:-1}\n\n# Docker API proxy: additional allowed image sources (comma-separated)\nAGENTTEAMS_PROXY_ALLOWED_REGISTRIES=${AGENTTEAMS_PROXY_ALLOWED_REGISTRIES:-}\n\n# Worker idle timeout in minutes (default: 720 = 12 hours)\nAGENTTEAMS_WORKER_IDLE_TIMEOUT=${AGENTTEAMS_WORKER_IDLE_TIMEOUT:-720}\n\n# Podman autostart via systemd (0=disabled, 1=enabled)\nAGENTTEAMS_PODMAN_AUTOSTART=${AGENTTEAMS_PODMAN_AUTOSTART:-0}\n\n# JVM Args for Higress Console (fixes SIGILL on Apple Silicon)\nJVM_ARGS=${JVM_ARGS:-}\n\n# Higress WASM plugin image registry (auto-selected by timezone)\nHIGRESS_ADMIN_WASM_PLUGIN_IMAGE_REGISTRY=${AGENTTEAMS_REGISTRY}\n\n# Data persistence\nAGENTTEAMS_DATA_DIR=${AGENTTEAMS_DATA_DIR:-agentteams-data}\n# Manager workspace (skills, memory, state — host-editable)\nAGENTTEAMS_WORKSPACE_DIR=${AGENTTEAMS_WORKSPACE_DIR:-}\n# Host directory sharing\nAGENTTEAMS_HOST_SHARE_DIR=${AGENTTEAMS_HOST_SHARE_DIR:-}\n\n# agentteams-dashboard (management UI)\nAGENTTEAMS_DASHBOARD=${AGENTTEAMS_DASHBOARD:-1}\nAGENTTEAMS_DASHBOARD_VERSION=${AGENTTEAMS_DASHBOARD_VERSION:-v1.2.0-beta.1}\nAGENTTEAMS_PORT_DASHBOARD=${AGENTTEAMS_PORT_DASHBOARD:-13000}\nAGENTTEAMS_DASHBOARD_IMAGE=${AGENTTEAMS_DASHBOARD_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-dashboard:${AGENTTEAMS_DASHBOARD_VERSION}}\nAGENTTEAMS_AI_GATEWAY_ADMIN_URL=${AGENTTEAMS_AI_GATEWAY_ADMIN_URL:-}\nEOF\n\n chmod 600 \"${ENV_FILE}\"\n log \"$(msg install.config_saved \"${ENV_FILE}\")\"\n\n # Detect container runtime socket\n SOCKET_MOUNT_ARGS=\"\"\n if [ \"${AGENTTEAMS_MOUNT_SOCKET}\" = \"1\" ]; then\n # Actively ensure the API socket is enabled and active before detection.\n if [ \"${DOCKER_CMD:-}\" = \"podman\" ]; then\n ensure_podman_socket\n fi\n CONTAINER_SOCK=$(detect_socket)\n if [ -n \"${CONTAINER_SOCK}\" ]; then\n log \"$(msg install.socket_detected \"${CONTAINER_SOCK}\")\"\n SOCKET_MOUNT_ARGS=\"-v ${CONTAINER_SOCK}:/var/run/docker.sock --security-opt label=disable\"\n else\n log \"$(msg install.socket_not_found)\"\n # Interactive confirmation when socket not found\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" != \"1\" ]; then\n echo \"\"\n echo -e \"\\033[33m$(msg install.socket_confirm.title)\\033[0m\"\n echo \"\"\n echo -e \"$(msg install.socket_confirm.message)\"\n echo \"\"\n read -p \"$(msg install.socket_confirm.prompt)\" SOCKET_CONFIRM\n if [ \"${SOCKET_CONFIRM}\" != \"y\" ] && [ \"${SOCKET_CONFIRM}\" != \"Y\" ]; then\n log \"$(msg install.socket_confirm.cancelled)\"\n exit 0\n fi\n fi\n fi\n fi\n\n # Create the data volume if it doesn't already exist (reuse on reinstall)\n if ! ${DOCKER_CMD} volume ls -q | grep -q \"^${AGENTTEAMS_DATA_DIR}$\"; then\n ${DOCKER_CMD} volume create \"${AGENTTEAMS_DATA_DIR}\" > /dev/null\n fi\n\n # Data mount: Docker volume\n DATA_MOUNT_ARGS=\"-v ${AGENTTEAMS_DATA_DIR}:/data\"\n\n # Manager workspace mount (always a host directory, defaulting to ~/agentteams-manager)\n WORKSPACE_MOUNT_ARGS=\"-v ${AGENTTEAMS_WORKSPACE_DIR}:/root/manager-workspace\"\n\n # Pass host timezone to container so date/time commands reflect local time\n TZ_ARGS=\"-e TZ=${AGENTTEAMS_TIMEZONE}\"\n\n # Host directory mount\n if [ -d \"${AGENTTEAMS_HOST_SHARE_DIR}\" ]; then\n HOST_SHARE_MOUNT_ARGS=\"-v ${AGENTTEAMS_HOST_SHARE_DIR}:/host-share\"\n log \"$(msg host_share.sharing \"${AGENTTEAMS_HOST_SHARE_DIR}\")\"\n else\n log \"$(msg host_share.not_exist \"${AGENTTEAMS_HOST_SHARE_DIR}\")\"\n HOST_SHARE_MOUNT_ARGS=\"-v ${AGENTTEAMS_HOST_SHARE_DIR}:/host-share\"\n fi\n\n # YOLO mode: pass through if set in environment (enables autonomous decisions)\n YOLO_ARGS=\"\"\n if [ \"${AGENTTEAMS_YOLO:-}\" = \"1\" ]; then\n YOLO_ARGS=\"-e AGENTTEAMS_YOLO=1\"\n log \"$(msg install.yolo)\"\n fi\n\n # Matrix-plugin debug tracing: pass through if AGENTTEAMS_MATRIX_DEBUG=1.\n # The container entrypoints translate this to OPENCLAW_MATRIX_DEBUG=1\n # so the openclaw matrix plugin emits structured INFO-level lifecycle\n # traces (sync state transitions, room.invite/join, message handler\n # arrival + filter outcomes). Used to diagnose worker/manager hangs.\n MATRIX_DEBUG_ARGS=\"\"\n if [ \"${AGENTTEAMS_MATRIX_DEBUG:-}\" = \"1\" ]; then\n MATRIX_DEBUG_ARGS=\"-e AGENTTEAMS_MATRIX_DEBUG=1\"\n fi\n\n # E2EE is already in the env file; but also pass explicitly in case env file is not the source\n # (AGENTTEAMS_MATRIX_E2EE is already written to ENV_FILE above via --env-file)\n\n # Pull images (manager based on runtime config; all worker runtimes always pulled)\n _is_local_image() {\n case \"$1\" in\n agentteams/*) return 0 ;;\n *) return 1 ;;\n esac\n }\n\n _local_image_exists() {\n local _img=\"$1\"\n [ -z \"${_img}\" ] && return 1\n _is_local_image \"${_img}\" || return 1\n ${DOCKER_CMD} image inspect \"${_img}\" >/dev/null 2>&1\n }\n\n # Helper: pull or skip a single image\n # Args: $1=image $2=exists_msg_key $3=pulling_msg_key\n _pull_image() {\n local _img=\"$1\" _exists_key=\"$2\" _pull_key=\"$3\"\n [ -z \"${_img}\" ] && return 0\n if _local_image_exists \"${_img}\"; then\n log \"$(msg \"${_exists_key}\" \"${_img}\")\"\n return 0\n fi\n log \"$(msg \"${_pull_key}\" \"${_img}\")\"\n local _attempt=1\n while [ $_attempt -le 3 ]; do\n if ${DOCKER_CMD} pull \"${_img}\"; then\n return 0\n fi\n if [ $_attempt -lt 3 ]; then\n log \"Pull failed (attempt ${_attempt}/3), retrying in 5s...\"\n sleep 5\n fi\n _attempt=$((_attempt + 1))\n done\n die \"Failed to pull ${_img} after 3 attempts\"\n return 1\n }\n\n # Embedded controller image (resolve versioned tag, fallback to latest)\n resolve_embedded_image\n if [ \"${AGENTTEAMS_USE_EMBEDDED}\" = \"1\" ]; then\n _local_image_exists \"${EMBEDDED_IMAGE}\" || true\n fi\n\n # Manager image is always required (select based on runtime)\n if [ \"${AGENTTEAMS_MANAGER_RUNTIME}\" = \"copaw\" ]; then\n _pull_image \"${MANAGER_COPAW_IMAGE}\" \"install.image.exists\" \"install.image.pulling_manager\"\n else\n _pull_image \"${MANAGER_IMAGE}\" \"install.image.exists\" \"install.image.pulling_manager\"\n fi\n\n # Pull all worker runtime images (workers may use any runtime regardless of the default)\n _pull_image \"${WORKER_IMAGE}\" \"install.image.worker_exists\" \"install.image.pulling_worker\"\n _pull_image \"${COPAW_WORKER_IMAGE}\" \"install.image.worker_exists\" \"install.image.pulling_worker\"\n _pull_image \"${HERMES_WORKER_IMAGE}\" \"install.image.worker_exists\" \"install.image.pulling_worker\"\n\n # --- Pre-upgrade: extract Matrix passwords from running old containers ---\n # Only needed when upgrading FROM old architecture (v1.0.9) TO embedded.\n # For new-arch-to-new-arch upgrades, credential files already exist with\n # correct room IDs — we must NOT overwrite them.\n _creds_tmp=\"\"\n if [ \"${AGENTTEAMS_UPGRADE:-0}\" = \"1\" ] && [ \"${AGENTTEAMS_USE_EMBEDDED}\" = \"1\" ]; then\n # Detect if upgrading from old arch: old arch has no agentteams-controller container\n # (or has it as a docker-proxy only, not embedded). Check if the existing\n # agentteams-controller is an embedded image (has supervisord) or just a proxy.\n _is_old_arch=0\n if ! ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -q \"^agentteams-controller$\"; then\n _is_old_arch=1\n elif ${DOCKER_CMD} ps -a --format '{{.Names}} {{.Image}}' 2>/dev/null | grep \"^agentteams-controller \" | grep -qv \"embedded\"; then\n _is_old_arch=1\n fi\n\n if [ \"${_is_old_arch}\" = \"1\" ]; then\n _creds_tmp=$(mktemp -d)\n\n # docker exec for Matrix/minio paths only works while agentteams-manager is running.\n _mgr_creds_tempstart=0\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -q '^agentteams-manager$'; then\n if ! ${DOCKER_CMD} ps --format '{{.Names}}' 2>/dev/null | grep -q '^agentteams-manager$'; then\n log \"agentteams-manager is stopped; starting it temporarily to extract Matrix credentials for upgrade...\"\n ${DOCKER_CMD} start agentteams-manager 2>/dev/null || true\n wait_matrix_ready \"agentteams-manager\"\n _mgr_creds_tempstart=1\n fi\n fi\n\n # Manager password (container Config.Env, then secrets file inside running manager, then data volume)\n _mgr_pw=$(${DOCKER_CMD} inspect agentteams-manager --format '{{range .Config.Env}}{{println .}}{{end}}' 2>/dev/null | grep '^AGENTTEAMS_MANAGER_PASSWORD=' | cut -d= -f2-)\n if [ -z \"${_mgr_pw}\" ] && ${DOCKER_CMD} ps --format '{{.Names}}' 2>/dev/null | grep -q '^agentteams-manager$'; then\n _mgr_pw=$(${DOCKER_CMD} exec agentteams-manager bash -c 'source /data/agentteams-secrets.env 2>/dev/null && echo \"${AGENTTEAMS_MANAGER_PASSWORD}\"' 2>/dev/null)\n fi\n if [ -z \"${_mgr_pw}\" ] && ${DOCKER_CMD} volume ls -q 2>/dev/null | grep -q \"^${AGENTTEAMS_DATA_DIR}$\"; then\n _mgr_pw=$(agentteams_read_secret_from_data_volume \"${AGENTTEAMS_DATA_DIR}\" AGENTTEAMS_MANAGER_PASSWORD)\n fi\n\n # Manager admin DM room ID: login as admin, find DM room with @manager\n _mgr_room=\"\"\n if [ -n \"${_mgr_pw}\" ]; then\n _admin_pw=$(grep AGENTTEAMS_ADMIN_PASSWORD \"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\" 2>/dev/null | cut -d= -f2-)\n _admin_user=$(grep AGENTTEAMS_ADMIN_USER \"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\" 2>/dev/null | cut -d= -f2-)\n _admin_user=\"${_admin_user:-admin}\"\n _matrix_domain=$(grep AGENTTEAMS_MATRIX_DOMAIN \"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\" 2>/dev/null | cut -d= -f2-)\n if [ -n \"${_admin_pw}\" ] && ${DOCKER_CMD} ps --format '{{.Names}}' 2>/dev/null | grep -q '^agentteams-manager$'; then\n _admin_token=$(${DOCKER_CMD} exec agentteams-manager curl -sf -X POST http://127.0.0.1:6167/_matrix/client/v3/login \\\n -H \"Content-Type: application/json\" \\\n -d '{\"type\":\"m.login.password\",\"identifier\":{\"type\":\"m.id.user\",\"user\":\"'\"${_admin_user}\"'\"},\"password\":\"'\"${_admin_pw}\"'\"}' 2>/dev/null | python3 -c \"import sys,json; print(json.load(sys.stdin).get('access_token',''))\" 2>/dev/null || true)\n if [ -n \"${_admin_token}\" ]; then\n _mgr_room=$(${DOCKER_CMD} exec agentteams-manager curl -sf -X GET \\\n -H \"Authorization: Bearer ${_admin_token}\" \\\n \"http://127.0.0.1:6167/_matrix/client/v3/joined_rooms\" 2>/dev/null | python3 -c \"\nimport sys,json,subprocess\nrooms = json.load(sys.stdin).get('joined_rooms',[])\nfor room_id in rooms:\n enc = room_id.replace('!','%21')\n members = json.loads(subprocess.check_output([\n 'docker','exec','agentteams-manager','curl','-sf','-X','GET',\n '-H','Authorization: Bearer ${_admin_token}',\n 'http://127.0.0.1:6167/_matrix/client/v3/rooms/'+enc+'/members'\n ]).decode()).get('chunk',[])\n member_ids = [m['state_key'] for m in members]\n if any('manager' in m and 'admin' not in m.split(':')[0] for m in member_ids):\n if len(member_ids) <= 3:\n print(room_id)\n break\n\" 2>/dev/null || true)\n fi\n fi\n if [ -z \"${_mgr_room}\" ]; then\n _mgr_room=$(agentteams_read_admin_dm_room_from_workspace \"${AGENTTEAMS_WORKSPACE_DIR}\")\n fi\n cat > \"${_creds_tmp}/default.env\" <<CREDEOF\nWORKER_PASSWORD=\"${_mgr_pw}\"\nWORKER_MINIO_PASSWORD=\"$(openssl rand -hex 24)\"\nWORKER_GATEWAY_KEY=\"${AGENTTEAMS_MANAGER_GATEWAY_KEY}\"\nWORKER_ROOM_ID=\"${_mgr_room}\"\nCREDEOF\n log \"Extracted Manager Matrix password${_mgr_room:+ and room ID}\"\n fi\n\n if [ \"${_mgr_creds_tempstart}\" = \"1\" ]; then\n log \"Stopping agentteams-manager after credential extraction (upgrade will recreate containers)...\"\n ${DOCKER_CMD} stop agentteams-manager 2>/dev/null || true\n fi\n fi # _is_old_arch\n fi\n\n # --- Stop and remove existing containers ---\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' | grep -q \"^agentteams-controller$\"; then\n ${DOCKER_CMD} stop agentteams-controller 2>/dev/null || true\n ${DOCKER_CMD} rm agentteams-controller 2>/dev/null || true\n fi\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' | grep -q \"^agentteams-manager$\"; then\n log \"$(msg install.removing_existing)\"\n ${DOCKER_CMD} stop agentteams-manager 2>/dev/null || true\n ${DOCKER_CMD} rm agentteams-manager 2>/dev/null || true\n fi\n\n # Stop and remove worker containers (controller will recreate via CR reconciliation)\n if [ -n \"${UPGRADE_EXISTING_WORKERS:-}\" ]; then\n log \"$(msg install.existing.stopping_workers)\"\n for w in ${UPGRADE_EXISTING_WORKERS}; do\n ${DOCKER_CMD} stop \"${w}\" 2>/dev/null || true\n ${DOCKER_CMD} rm \"${w}\" 2>/dev/null || true\n log \"$(msg install.existing.removed \"${w}\")\"\n done\n fi\n\n # Clean up legacy containers from v1.0.x and earlier\n for _legacy_name in agentteams-docker-proxy; do\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -qx \"${_legacy_name}\"; then\n log \"Removing legacy container: ${_legacy_name}\"\n ${DOCKER_CMD} stop \"${_legacy_name}\" 2>/dev/null || true\n ${DOCKER_CMD} rm -f \"${_legacy_name}\" 2>/dev/null || true\n fi\n done\n\n # --- Upgrade: inject extracted credentials into data volume ---\n # Only needed for old-arch upgrades (credential files were extracted above).\n if [ -n \"${_creds_tmp}\" ] && [ -d \"${_creds_tmp}\" ] && [ -n \"$(ls -A \"${_creds_tmp}\" 2>/dev/null)\" ]; then\n local _cleanup_ctr=\"agentteams-upgrade-cleanup\"\n ${DOCKER_CMD} rm -f \"${_cleanup_ctr}\" 2>/dev/null || true\n ${DOCKER_CMD} run --rm --name \"${_cleanup_ctr}\" \\\n --entrypoint sh \\\n -v \"${AGENTTEAMS_DATA_DIR}:/data\" \\\n -v \"${_creds_tmp}:/creds:ro\" \\\n \"${EMBEDDED_IMAGE}\" -c '\n rm -rf /data/worker-creds\n mkdir -p /data/worker-creds\n cp /creds/*.env /data/worker-creds/ 2>/dev/null || true\n chmod 600 /data/worker-creds/*.env 2>/dev/null || true\n ' 2>/dev/null && log \"Injected credentials for upgrade\" || log \"Warning: credential injection failed, continuing\"\n rm -rf \"${_creds_tmp}\"\n fi\n\n # --- Start containers ---\n log \"$(msg install.starting_manager)\"\n\n # Build port binding args\n if [ \"${AGENTTEAMS_LOCAL_ONLY:-1}\" = \"1\" ]; then\n _port_prefix=\"127.0.0.1:\"\n else\n _port_prefix=\"\"\n fi\n\n # Ensure agentteams-net Docker network exists\n ${DOCKER_CMD} network inspect agentteams-net >/dev/null 2>&1 || ${DOCKER_CMD} network create agentteams-net\n\n if [ \"${AGENTTEAMS_USE_EMBEDDED}\" != \"1\" ] && [ \"${AGENTTEAMS_UPGRADE:-0}\" = \"1\" ]; then\n # Check if current installation is embedded — downgrade to legacy is not supported\n if ${DOCKER_CMD} ps -a --format '{{.Names}} {{.Image}}' 2>/dev/null | grep \"^agentteams-controller \" | grep -q \"embedded\"; then\n error \"Downgrade from embedded architecture to legacy version (${AGENTTEAMS_VERSION}) is not supported.\"\n error \"Please use 'make uninstall-embedded' first, then do a clean install of the target version.\"\n exit 1\n fi\n fi\n\n if [ \"${AGENTTEAMS_USE_EMBEDDED}\" = \"1\" ]; then\n # ============================================================\n # New architecture: embedded controller + auto-created manager\n # ============================================================\n\n # Internal port: 8080 (Higress gateway inside the container).\n local _internal_gw_port=8080\n local _matrix_domain=\"${AGENTTEAMS_MATRIX_DOMAIN:-matrix-local.agentteams.io:${AGENTTEAMS_PORT_GATEWAY}}\"\n local _aigw_domain=\"${AGENTTEAMS_AI_GATEWAY_DOMAIN:-aigw-local.agentteams.io}\"\n # Ensure internal gateway port is present (container-internal traffic uses 8080)\n case \"${_aigw_domain}\" in *:*) ;; *) _aigw_domain=\"${_aigw_domain}:${_internal_gw_port}\" ;; esac\n local _fs_domain=\"${AGENTTEAMS_FS_DOMAIN:-fs-local.agentteams.io}\"\n case \"${_fs_domain}\" in *:*) ;; *) _fs_domain=\"${_fs_domain}:${_internal_gw_port}\" ;; esac\n\n # Controller env args\n local _ctrl_env_prefix\n _ctrl_env_prefix=\"$(_controller_env_prefix \"${AGENTTEAMS_VERSION}\")\"\n local _ctrl_env_args=(\n -e \"${_ctrl_env_prefix}ADMIN_USER=${AGENTTEAMS_ADMIN_USER}\"\n -e \"${_ctrl_env_prefix}ADMIN_PASSWORD=${AGENTTEAMS_ADMIN_PASSWORD}\"\n -e \"${_ctrl_env_prefix}MANAGER_PASSWORD=${AGENTTEAMS_MANAGER_PASSWORD}\"\n -e \"${_ctrl_env_prefix}REGISTRATION_TOKEN=${AGENTTEAMS_REGISTRATION_TOKEN}\"\n -e \"${_ctrl_env_prefix}MINIO_USER=${AGENTTEAMS_MINIO_USER}\"\n -e \"${_ctrl_env_prefix}MINIO_PASSWORD=${AGENTTEAMS_MINIO_PASSWORD}\"\n -e \"${_ctrl_env_prefix}LLM_PROVIDER=${AGENTTEAMS_LLM_PROVIDER}\"\n -e \"${_ctrl_env_prefix}LLM_API_KEY=${AGENTTEAMS_LLM_API_KEY}\"\n -e \"${_ctrl_env_prefix}DEFAULT_MODEL=${AGENTTEAMS_DEFAULT_MODEL}\"\n -e \"${_ctrl_env_prefix}MANAGER_GATEWAY_KEY=${AGENTTEAMS_MANAGER_GATEWAY_KEY}\"\n -e \"${_ctrl_env_prefix}MANAGER_RUNTIME=${AGENTTEAMS_MANAGER_RUNTIME:-copaw}\"\n -e \"${_ctrl_env_prefix}MANAGER_IMAGE=$([ \"${AGENTTEAMS_MANAGER_RUNTIME}\" = \"copaw\" ] && echo \"${MANAGER_COPAW_IMAGE}\" || echo \"${MANAGER_IMAGE}\")\"\n -e \"${_ctrl_env_prefix}DEFAULT_WORKER_RUNTIME=${AGENTTEAMS_DEFAULT_WORKER_RUNTIME:-copaw}\"\n -e \"${_ctrl_env_prefix}WORKER_IMAGE=${WORKER_IMAGE}\"\n -e \"${_ctrl_env_prefix}COPAW_WORKER_IMAGE=${COPAW_WORKER_IMAGE}\"\n -e \"${_ctrl_env_prefix}HERMES_WORKER_IMAGE=${HERMES_WORKER_IMAGE}\"\n -e \"${_ctrl_env_prefix}MATRIX_DOMAIN=${_matrix_domain}\"\n -e \"${_ctrl_env_prefix}ELEMENT_HOMESERVER_URL=http://127.0.0.1:${AGENTTEAMS_PORT_GATEWAY}\"\n -e \"${_ctrl_env_prefix}MATRIX_URL=http://127.0.0.1:6167\"\n -e \"${_ctrl_env_prefix}MATRIX_E2EE=${AGENTTEAMS_MATRIX_E2EE:-0}\"\n -e \"${_ctrl_env_prefix}MINIO_ENDPOINT=http://127.0.0.1:9000\"\n -e \"${_ctrl_env_prefix}MINIO_BUCKET=agentteams-storage\"\n -e \"${_ctrl_env_prefix}STORAGE_PREFIX=agentteams/agentteams-storage\"\n -e \"${_ctrl_env_prefix}FS_ENDPOINT=http://127.0.0.1:9000\"\n -e \"${_ctrl_env_prefix}AI_GATEWAY_URL=http://${_aigw_domain}\"\n -e \"${_ctrl_env_prefix}CONTROLLER_URL=http://agentteams-controller:8090\"\n -e \"${_ctrl_env_prefix}DOCKER_NETWORK=agentteams-net\"\n -e \"${_ctrl_env_prefix}WORKSPACE_DIR=${AGENTTEAMS_WORKSPACE_DIR}\"\n -e \"${_ctrl_env_prefix}HOST_SHARE_DIR=${AGENTTEAMS_HOST_SHARE_DIR}\"\n -e \"${_ctrl_env_prefix}MANAGER_ENABLED=true\"\n -e \"${_ctrl_env_prefix}PORT_MANAGER_CONSOLE=${AGENTTEAMS_PORT_MANAGER_CONSOLE:-18888}\"\n )\n if _use_legacy_image_env \"${AGENTTEAMS_VERSION}\"; then\n _ctrl_env_args+=(\n -e \"${_ctrl_env_prefix}FS_BUCKET=agentteams-storage\"\n -e \"${_ctrl_env_prefix}RESOURCE_PREFIX=agentteams-\"\n )\n else\n _ctrl_env_args+=(\n -e \"${_ctrl_env_prefix}MATRIX_APPSERVICE_ENABLED=${AGENTTEAMS_MATRIX_APPSERVICE_ENABLED:-true}\"\n -e \"${_ctrl_env_prefix}MATRIX_APPSERVICE_AS_TOKEN=${AGENTTEAMS_MATRIX_APPSERVICE_AS_TOKEN:-}\"\n -e \"${_ctrl_env_prefix}MATRIX_APPSERVICE_HS_TOKEN=${AGENTTEAMS_MATRIX_APPSERVICE_HS_TOKEN:-}\"\n )\n fi\n\n # Timezone\n if [ -n \"${AGENTTEAMS_TIMEZONE:-}\" ]; then\n _ctrl_env_args+=(-e \"TZ=${AGENTTEAMS_TIMEZONE}\")\n fi\n\n # Yolo mode\n if [ \"${AGENTTEAMS_YOLO:-}\" = \"1\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}YOLO=1\")\n fi\n\n # Matrix-plugin debug tracing — propagated to every manager + worker\n # the controller spawns, then translated to OPENCLAW_MATRIX_DEBUG=1\n # by the container entrypoints. Use this to diagnose\n # \"worker did not join\" / \"manager replied empty\" hangs.\n if [ \"${AGENTTEAMS_MATRIX_DEBUG:-}\" = \"1\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}MATRIX_DEBUG=1\")\n fi\n\n # Optional: GitHub token\n if [ -n \"${AGENTTEAMS_GITHUB_TOKEN:-}\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}GITHUB_TOKEN=${AGENTTEAMS_GITHUB_TOKEN}\")\n fi\n\n # Optional: embedding model\n if [ -n \"${AGENTTEAMS_EMBEDDING_MODEL:-}\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}EMBEDDING_MODEL=${AGENTTEAMS_EMBEDDING_MODEL}\")\n fi\n\n # Optional: OpenAI-compatible base URL\n if [ -n \"${AGENTTEAMS_OPENAI_BASE_URL:-}\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}OPENAI_BASE_URL=${AGENTTEAMS_OPENAI_BASE_URL}\")\n fi\n # Optional: language\n if [ -n \"${AGENTTEAMS_LANGUAGE:-}\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}LANGUAGE=${AGENTTEAMS_LANGUAGE}\")\n fi\n\n # Optional: CMS/ARMS observability. In embedded mode the controller\n # spawns the Manager and Workers, so it must receive these settings.\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}CMS_TRACES_ENABLED=${AGENTTEAMS_CMS_TRACES_ENABLED:-false}\")\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}CMS_SERVICE_NAME=${AGENTTEAMS_CMS_SERVICE_NAME:-agentteams-manager}\")\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}CMS_METRICS_ENABLED=${AGENTTEAMS_CMS_METRICS_ENABLED:-false}\")\n if [ -n \"${AGENTTEAMS_CMS_ENDPOINT:-}\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}CMS_ENDPOINT=${AGENTTEAMS_CMS_ENDPOINT}\")\n fi\n if [ -n \"${AGENTTEAMS_CMS_LICENSE_KEY:-}\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}CMS_LICENSE_KEY=${AGENTTEAMS_CMS_LICENSE_KEY}\")\n fi\n if [ -n \"${AGENTTEAMS_CMS_PROJECT:-}\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}CMS_PROJECT=${AGENTTEAMS_CMS_PROJECT}\")\n fi\n if [ -n \"${AGENTTEAMS_CMS_WORKSPACE:-}\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}CMS_WORKSPACE=${AGENTTEAMS_CMS_WORKSPACE}\")\n fi\n\n # shellcheck disable=SC2086\n ${DOCKER_CMD} run -d \\\n --name agentteams-controller \\\n --network agentteams-net \\\n --network-alias matrix-local.agentteams.io \\\n --network-alias aigw-local.agentteams.io \\\n --network-alias fs-local.agentteams.io \\\n \"${_ctrl_env_args[@]}\" \\\n -v \"${CONTAINER_SOCK}:/var/run/docker.sock\" \\\n --security-opt label=disable \\\n -v \"${AGENTTEAMS_DATA_DIR}:/data\" \\\n -v \"${AGENTTEAMS_WORKSPACE_DIR}:/root/agentteams-fs/agents/manager\" \\\n -p \"${_port_prefix}${AGENTTEAMS_PORT_GATEWAY}:8080\" \\\n -p \"${_port_prefix}${AGENTTEAMS_PORT_CONSOLE}:8001\" \\\n -p \"${_port_prefix}${AGENTTEAMS_PORT_ELEMENT_WEB:-18088}:8088\" \\\n --restart unless-stopped \\\n \"${EMBEDDED_IMAGE}\"\n\n log \"Embedded controller started: agentteams-controller\"\n\n # Wait for infrastructure inside the controller container\n _wait_for_url() {\n local url=\"$1\" ctr=\"$2\" max_wait=\"${3:-120}\" desc=\"${4:-service}\"\n local elapsed=0\n log \"Waiting for ${desc}...\"\n while [ $elapsed -lt $max_wait ]; do\n if ${DOCKER_CMD} exec \"${ctr}\" curl -sf \"${url}\" >/dev/null 2>&1; then\n log \"${desc} is ready (${elapsed}s)\"\n return 0\n fi\n sleep 2\n elapsed=$((elapsed + 2))\n done\n log \"ERROR: ${desc} not ready after ${max_wait}s\"\n return 1\n }\n\n _wait_for_url \"http://127.0.0.1:6167/_tuwunel/server_version\" agentteams-controller 120 \"Tuwunel (Matrix)\" || exit 1\n _wait_for_url \"http://127.0.0.1:9000/minio/health/live\" agentteams-controller 60 \"MinIO\" || exit 1\n _wait_for_url \"http://127.0.0.1:8080/status\" agentteams-controller 120 \"Higress Gateway\" || exit 1\n\n # Wait for controller to create Manager Agent container\n log \"Waiting for Manager Agent container...\"\n local _mgr_wait=0\n local _mgr_max=300\n while [ $_mgr_wait -lt $_mgr_max ]; do\n if ${DOCKER_CMD} ps --format '{{.Names}}' 2>/dev/null | grep -q \"^agentteams-manager$\"; then\n log \"Manager Agent container detected (${_mgr_wait}s)\"\n break\n fi\n sleep 3\n _mgr_wait=$((_mgr_wait + 3))\n done\n if [ $_mgr_wait -ge $_mgr_max ]; then\n log \"ERROR: Manager Agent container not created after ${_mgr_max}s\"\n log \"Controller logs:\"\n ${DOCKER_CMD} exec agentteams-controller tail -30 /var/log/agentteams/agentteams-controller-error.log 2>/dev/null || true\n exit 1\n fi\n\n # Wait for Manager Agent to be running\n log \"Waiting for Manager Agent to start...\"\n local _agent_wait=0\n while [ $_agent_wait -lt 120 ]; do\n local _state\n _state=$(${DOCKER_CMD} inspect --format '{{.State.Status}}' agentteams-manager 2>/dev/null || echo \"missing\")\n if [ \"${_state}\" = \"running\" ]; then\n log \"Manager Agent is running\"\n break\n fi\n sleep 2\n _agent_wait=$((_agent_wait + 2))\n done\n\n # Enable yolo mode in agent if requested\n if [ \"${AGENTTEAMS_YOLO:-}\" = \"1\" ]; then\n ${DOCKER_CMD} exec agentteams-manager touch /root/manager-workspace/yolo-mode 2>/dev/null || true\n fi\n\n # Wait for the controller to send the first-boot welcome message.\n # The controller gates this on (a) Manager joining the DM room and\n # (b) Higress WASM key-auth propagation actually clearing /v1/chat/completions\n # for the Manager's gateway key — typically ~45-90s on a fresh install.\n # We poll Manager CR Status.WelcomeSent via the in-container agt CLI,\n # exec'd inside agentteams-controller (the source-of-truth container — its\n # bundled CLI binary is always in lockstep with whatever controller\n # binary is currently serving the HTTP API, since they're the same\n # `go build` output. The agentteams-manager container's CLI may lag the\n # controller across image upgrades and silently drop the welcomeSent\n # field, leaving this loop hung). The controller container mints a\n # long-lived admin SA token at startup and writes it to\n # AGENTTEAMS_AUTH_TOKEN_FILE=/var/run/agentteams/cli-token (set as a Dockerfile\n # ENV default), so a bare `docker exec agentteams-controller agt …`\n # auto-discovers both the endpoint and the token. There is a brief\n # window after container start before bootstrapAdminCLIToken completes\n # where the file may be empty / absent — the loop's silent retry\n # handles that the same way it handles the manager-not-yet-running\n # case below.\n if ${DOCKER_CMD} exec agentteams-controller sh -c 'command -v agt' >/dev/null 2>&1; then\n log \"$(msg install.welcome_msg.waiting)\"\n local _welcome_wait=0\n local _welcome_max=\"${AGENTTEAMS_WELCOME_TIMEOUT:-300}\"\n local _welcome_done=0\n while [ $_welcome_wait -lt $_welcome_max ]; do\n # `tr -d` strips whitespace/CR so the grep stays robust to\n # any future change in go-json field ordering or formatting.\n local _wjson\n _wjson=$(${DOCKER_CMD} exec agentteams-controller \\\n agt get managers default -o json 2>/dev/null || true)\n if [ -n \"${_wjson}\" ] && printf '%s' \"${_wjson}\" | tr -d ' \\r\\n' | grep -q '\"welcomeSent\":true'; then\n log \"$(msg install.welcome_msg.confirmed \"${_welcome_wait}\")\"\n _welcome_done=1\n break\n fi\n sleep 3\n _welcome_wait=$((_welcome_wait + 3))\n done\n if [ $_welcome_done -ne 1 ]; then\n # Non-fatal: install is still good. Keep going to the success\n # banner so the admin can use Element Web to nudge Manager into\n # onboarding manually (one DM message is enough).\n log \"$(msg install.welcome_msg.timeout \"${_welcome_max}\")\"\n log \"$(msg install.welcome_msg.timeout_hint)\"\n log \"$(msg install.welcome_msg.timeout_inspect)\"\n fi\n else\n log \"$(msg install.welcome_msg.poll_unavailable)\"\n fi\n\n else\n # ============================================================\n # Legacy architecture: all-in-one manager container\n # ============================================================\n\n NETWORK_ARGS=\"--network agentteams-net\"\n NETWORK_ALIAS_ARGS=\"--network-alias matrix-local.agentteams.io --network-alias aigw-local.agentteams.io --network-alias fs-local.agentteams.io\"\n for _domain in \"${AGENTTEAMS_MATRIX_CLIENT_DOMAIN:-}\" \"${AGENTTEAMS_CONSOLE_DOMAIN:-}\"; do\n if [ -n \"${_domain}\" ] && [[ \"${_domain}\" == *-local.agentteams.io ]]; then\n NETWORK_ALIAS_ARGS=\"${NETWORK_ALIAS_ARGS} --network-alias ${_domain}\"\n fi\n done\n\n # Start Docker API proxy if enabled (security layer between Manager and Docker daemon)\n PROXY_ARGS=\"\"\n if [ \"${AGENTTEAMS_DOCKER_PROXY:-1}\" = \"1\" ] && [ -n \"${CONTAINER_SOCK:-}\" ]; then\n local _proxy_image=\"${AGENTTEAMS_REGISTRY}/higress/agentteams-docker-proxy:${AGENTTEAMS_VERSION}\"\n # Try versioned tag, fallback to latest\n if ! ${DOCKER_CMD} image inspect \"${_proxy_image}\" >/dev/null 2>&1; then\n ${DOCKER_CMD} pull \"${_proxy_image}\" 2>/dev/null || {\n _proxy_image=\"${AGENTTEAMS_REGISTRY}/higress/agentteams-docker-proxy:latest\"\n ${DOCKER_CMD} pull \"${_proxy_image}\" 2>/dev/null || true\n }\n fi\n if ${DOCKER_CMD} image inspect \"${_proxy_image}\" >/dev/null 2>&1; then\n log \"Starting Docker API proxy...\"\n ${DOCKER_CMD} run -d \\\n --name agentteams-docker-proxy \\\n --network agentteams-net \\\n -v \"${CONTAINER_SOCK}:/var/run/docker.sock\" \\\n --security-opt label=disable \\\n -e AGENTTEAMS_WORKER_IMAGE=\"${WORKER_IMAGE}\" \\\n -e AGENTTEAMS_COPAW_WORKER_IMAGE=\"${COPAW_WORKER_IMAGE}\" \\\n -e AGENTTEAMS_HERMES_WORKER_IMAGE=\"${HERMES_WORKER_IMAGE}\" \\\n ${AGENTTEAMS_PROXY_ALLOWED_REGISTRIES:+-e AGENTTEAMS_PROXY_ALLOWED_REGISTRIES=\"${AGENTTEAMS_PROXY_ALLOWED_REGISTRIES}\"} \\\n --restart unless-stopped \\\n \"${_proxy_image}\"\n PROXY_ARGS=\"-e AGENTTEAMS_CONTROLLER_URL=http://agentteams-docker-proxy:2375 -e AGENTTEAMS_CONTAINER_API=http://agentteams-docker-proxy:2375\"\n SOCKET_MOUNT_ARGS=\"\"\n fi\n fi\n\n # Pass host timezone to container\n TZ_ARGS=\"\"\n if [ -n \"${AGENTTEAMS_TIMEZONE:-}\" ]; then\n TZ_ARGS=\"-e TZ=${AGENTTEAMS_TIMEZONE}\"\n fi\n\n YOLO_ARGS=\"\"\n if [ \"${AGENTTEAMS_YOLO:-}\" = \"1\" ]; then\n YOLO_ARGS=\"-e AGENTTEAMS_YOLO=1\"\n fi\n\n MATRIX_DEBUG_ARGS=\"\"\n if [ \"${AGENTTEAMS_MATRIX_DEBUG:-}\" = \"1\" ]; then\n MATRIX_DEBUG_ARGS=\"-e AGENTTEAMS_MATRIX_DEBUG=1\"\n fi\n\n # shellcheck disable=SC2086\n ${DOCKER_CMD} run -d \\\n --name agentteams-manager \\\n --env-file \"${ENV_FILE}\" \\\n -e HOME=/root/manager-workspace \\\n -w /root/manager-workspace \\\n -e HOST_ORIGINAL_HOME=\"${AGENTTEAMS_HOST_SHARE_DIR}\" \\\n -e AGENTTEAMS_MANAGER_RUNTIME=\"${AGENTTEAMS_MANAGER_RUNTIME:-copaw}\" \\\n ${JVM_ARGS:+-e JVM_ARGS=\"${JVM_ARGS}\"} \\\n ${YOLO_ARGS} \\\n ${MATRIX_DEBUG_ARGS} \\\n ${TZ_ARGS} \\\n ${SOCKET_MOUNT_ARGS} \\\n ${NETWORK_ARGS} \\\n ${NETWORK_ALIAS_ARGS} \\\n ${PROXY_ARGS} \\\n -p \"${_port_prefix}${AGENTTEAMS_PORT_GATEWAY}:8080\" \\\n -p \"${_port_prefix}${AGENTTEAMS_PORT_CONSOLE}:8001\" \\\n -p \"${_port_prefix}${AGENTTEAMS_PORT_ELEMENT_WEB:-18088}:8088\" \\\n -p \"127.0.0.1:${AGENTTEAMS_PORT_MANAGER_CONSOLE:-18888}:18888\" \\\n ${DATA_MOUNT_ARGS} \\\n ${WORKSPACE_MOUNT_ARGS} \\\n ${HOST_SHARE_MOUNT_ARGS} \\\n --restart unless-stopped \\\n \"$([ \"${AGENTTEAMS_MANAGER_RUNTIME}\" = \"copaw\" ] && echo \"${MANAGER_COPAW_IMAGE}\" || echo \"${MANAGER_IMAGE}\")\"\n\n # Wait for Manager agent to be ready\n wait_manager_ready \"agentteams-manager\"\n\n # Wait for Matrix server to be ready\n wait_matrix_ready \"agentteams-manager\"\n\n # Post-install verification (non-fatal: warnings only)\n local _verify_script\n _verify_script=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)/agentteams-verify.sh\"\n if [ -f \"${_verify_script}\" ]; then\n bash \"${_verify_script}\" \"agentteams-manager\" || {\n log \"WARNING: Some post-install checks failed. Re-run: bash install/agentteams-verify.sh\"\n }\n fi\n fi\n unset _port_prefix\n\n # ── Start Dashboard (optional) ────────────────────────────────────\n _start_dashboard\n\n # Apply Podman autostart if selected and environment matches\n if [ \"${DOCKER_CMD}\" = \"podman\" ] && [ \"${AGENTTEAMS_PODMAN_AUTOSTART:-0}\" = \"1\" ]; then\n setup_podman_autostart\n fi\n\n log \"\"\n log \"$(msg success.title)\"\n log \"\"\n log \"$(msg success.domains_configured)\"\n log \" ${AGENTTEAMS_MATRIX_DOMAIN%%:*} ${AGENTTEAMS_MATRIX_CLIENT_DOMAIN} ${AGENTTEAMS_AI_GATEWAY_DOMAIN} ${AGENTTEAMS_FS_DOMAIN} ${AGENTTEAMS_CONSOLE_DOMAIN}\"\n log \"\"\n local lan_ip\n lan_ip=$(detect_lan_ip)\n echo -e \"\\033[33m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\033[0m\"\n echo -e \"\\033[33m $(msg success.open_url)\\033[0m\"\n echo -e \"\\033[33m \\033[0m\"\n echo -e \"\\033[1;36m http://127.0.0.1:${AGENTTEAMS_PORT_ELEMENT_WEB:-18088}/#/login\\033[0m\"\n echo -e \"\\033[33m \\033[0m\"\n echo -e \"\\033[33m $(msg success.login_with)\\033[0m\"\n echo -e \"\\033[33m $(msg success.username \"${AGENTTEAMS_ADMIN_USER}\")\\033[0m\"\n echo -e \"\\033[33m $(msg success.password \"${AGENTTEAMS_ADMIN_PASSWORD}\")\\033[0m\"\n echo -e \"\\033[33m \\033[0m\"\n echo -e \"\\033[33m $(msg success.after_login)\\033[0m\"\n echo -e \"\\033[33m $(msg success.tell_it)\\033[0m\"\n echo -e \"\\033[33m $(msg success.manager_auto)\\033[0m\"\n echo -e \"\\033[33m \\033[0m\"\n echo -e \"\\033[33m ───────────────────────────────────────────────────────────────────────────── \\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_title)\\033[0m\"\n echo -e \"\\033[33m \\033[0m\"\n if [ -n \"${lan_ip}\" ]; then\n echo -e \"\\033[33m $(msg success.mobile_step1)\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_step2 \"http://${lan_ip}:${AGENTTEAMS_PORT_GATEWAY}\")\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_step3)\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_username \"${AGENTTEAMS_ADMIN_USER}\")\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_password \"${AGENTTEAMS_ADMIN_PASSWORD}\")\\033[0m\"\n else\n echo -e \"\\033[33m $(msg success.mobile_step1)\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_step2_noip \"${AGENTTEAMS_PORT_GATEWAY}\")\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_noip_hint)\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_step3)\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_username \"${AGENTTEAMS_ADMIN_USER}\")\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_password \"${AGENTTEAMS_ADMIN_PASSWORD}\")\\033[0m\"\n fi\n echo -e \"\\033[33m \\033[0m\"\n echo -e \"\\033[33m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\033[0m\"\n log \"\"\n log \"$(msg success.other_consoles)\"\n log \"$(msg success.higress_console \"${AGENTTEAMS_PORT_CONSOLE}\" \"${AGENTTEAMS_ADMIN_USER}\" \"${AGENTTEAMS_ADMIN_PASSWORD}\")\"\n if [ \"${AGENTTEAMS_DASHBOARD:-0}\" = \"1\" ]; then\n log \"$(msg success.dashboard \"${AGENTTEAMS_PORT_DASHBOARD:-13000}\")\"\n fi\n if [ \"${AGENTTEAMS_USE_EMBEDDED}\" != \"1\" ]; then\n log \"$(msg success.manager_console \"${AGENTTEAMS_PORT_MANAGER_CONSOLE:-18888}\")\"\n log \"$(msg success.manager_console_gateway \"${AGENTTEAMS_ADMIN_USER}\" \"${AGENTTEAMS_ADMIN_PASSWORD}\")\"\n fi\n log \"\"\n log \"$(msg success.switch_llm.title)\"\n log \"$(msg success.switch_llm.hint)\"\n log \"$(msg success.switch_llm.docs)\"\n log \"$(msg success.switch_llm.url)\"\n log \"\"\n log \"$(msg success.tip)\"\n log \"\"\n if [ \"${AGENTTEAMS_LOCAL_ONLY:-1}\" != \"1\" ]; then\n echo -e \"\\033[33m$(msg port.local_only.https_hint)\\033[0m\"\n log \"\"\n fi\n log \"$(msg success.config_file \"${ENV_FILE}\")\"\n log \"$(msg success.data_volume \"${AGENTTEAMS_DATA_DIR}\")\"\n log \"$(msg success.workspace \"${AGENTTEAMS_WORKSPACE_DIR}\")\"\n}\n\n# ============================================================\n# Worker Installation (One-Click)\n# ============================================================\n\ninstall_worker() {\n local WORKER_NAME=\"\"\n local FS=\"\"\n local FS_KEY=\"\"\n local FS_SECRET=\"\"\n local RESET=false\n local SKILLS_API_URL=\"\"\n\n # Parse arguments\n while [ $# -gt 0 ]; do\n case $1 in\n --name) WORKER_NAME=\"$2\"; shift 2 ;;\n --fs) FS=\"$2\"; shift 2 ;;\n --fs-key) FS_KEY=\"$2\"; shift 2 ;;\n --fs-secret) FS_SECRET=\"$2\"; shift 2 ;;\n --skills-api-url) SKILLS_API_URL=\"$2\"; shift 2 ;;\n --reset) RESET=true; shift ;;\n *) die \"$(msg error.unknown_option \"$1\")\" ;;\n esac\n done\n\n # Validate required params\n [ -z \"${WORKER_NAME}\" ] && die \"$(msg error.name_required)\"\n [ -z \"${FS}\" ] && die \"$(msg error.fs_required)\"\n [ -z \"${FS_KEY}\" ] && die \"$(msg error.fs_key_required)\"\n [ -z \"${FS_SECRET}\" ] && die \"$(msg error.fs_secret_required)\"\n\n local CONTAINER_NAME=\"agentteams-worker-${WORKER_NAME}\"\n\n # Handle reset\n if [ \"${RESET}\" = true ]; then\n log \"$(msg worker.resetting \"${WORKER_NAME}\")\"\n ${DOCKER_CMD} stop \"${CONTAINER_NAME}\" 2>/dev/null || true\n ${DOCKER_CMD} rm \"${CONTAINER_NAME}\" 2>/dev/null || true\n fi\n\n # Check for existing container\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' | grep -q \"^${CONTAINER_NAME}$\"; then\n die \"$(msg worker.exists \"${CONTAINER_NAME}\")\"\n fi\n\n log \"$(msg worker.starting \"${WORKER_NAME}\")\"\n\n # Build docker run args\n local DOCKER_ENV=\"\"\n DOCKER_ENV=\"${DOCKER_ENV} -e HOME=/root/agentteams-fs/agents/${WORKER_NAME}\"\n DOCKER_ENV=\"${DOCKER_ENV} -w /root/agentteams-fs/agents/${WORKER_NAME}\"\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_WORKER_NAME=${WORKER_NAME}\"\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_FS_ENDPOINT=${FS}\"\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_FS_ACCESS_KEY=${FS_KEY}\"\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_FS_SECRET_KEY=${FS_SECRET}\"\n\n if [ -z \"${SKILLS_API_URL}\" ]; then\n if [ -n \"${AGENTTEAMS_SKILLS_API_URL:-}\" ]; then\n SKILLS_API_URL=\"${AGENTTEAMS_SKILLS_API_URL}\"\n else\n SKILLS_API_URL=\"nacos://market.agentteams.io:80/public\"\n fi\n fi\n\n # Add SKILLS_API_URL if specified\n DOCKER_ENV=\"${DOCKER_ENV} -e SKILLS_API_URL=${SKILLS_API_URL}\"\n log \"$(msg worker.skills_url \"${SKILLS_API_URL}\")\"\n if [ -n \"${AGENTTEAMS_NACOS_USERNAME:-}\" ]; then\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_NACOS_USERNAME=${AGENTTEAMS_NACOS_USERNAME}\"\n fi\n if [ -n \"${AGENTTEAMS_NACOS_PASSWORD:-}\" ]; then\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_NACOS_PASSWORD=${AGENTTEAMS_NACOS_PASSWORD}\"\n fi\n if [ -n \"${AGENTTEAMS_NACOS_TOKEN:-}\" ]; then\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_NACOS_TOKEN=${AGENTTEAMS_NACOS_TOKEN}\"\n fi\n\n # shellcheck disable=SC2086\n ${DOCKER_CMD} run -d \\\n --name \"${CONTAINER_NAME}\" \\\n ${DOCKER_ENV} \\\n --restart unless-stopped \\\n \"${WORKER_IMAGE}\"\n\n log \"\"\n log \"$(msg worker.started \"${WORKER_NAME}\")\"\n log \"$(msg worker.container \"${CONTAINER_NAME}\")\"\n log \"$(msg worker.view_logs \"${CONTAINER_NAME}\")\"\n}\n\n# ============================================================\n# Main\n# ============================================================\n\n# ============================================================\n# LLM API connectivity test\n# ============================================================\n\ntest_llm_connectivity() {\n local base_url=\"$1\"\n local api_key=\"$2\"\n local model=\"$3\"\n local hint=\"${4:-}\" # optional: extra hint shown on failure\n if ! command -v curl >/dev/null 2>&1; then\n echo -e \"\\033[33m$(msg llm.openai.test.no_curl)\\033[0m\"\n return\n fi\n log \"$(msg llm.openai.test.testing)\"\n local _body _http_code _tmpfile\n _tmpfile=$(mktemp)\n _http_code=$(curl -s -o \"${_tmpfile}\" -w \"%{http_code}\" \\\n -X POST \"${base_url%/}/chat/completions\" \\\n -H \"Authorization: Bearer ${api_key}\" \\\n -H \"Content-Type: application/json\" \\\n -H \"User-Agent: AgentTeams/${AGENTTEAMS_VERSION:-latest}\" \\\n --max-time 30 \\\n -d \"{\\\"model\\\":\\\"${model}\\\",\\\"messages\\\":[{\\\"role\\\":\\\"user\\\",\\\"content\\\":\\\"hi\\\"}]}\" \\\n 2>/dev/null)\n _body=$(cat \"${_tmpfile}\")\n rm -f \"${_tmpfile}\"\n if [ \"${_http_code}\" = \"200\" ] || [ \"${_http_code}\" = \"201\" ]; then\n log \"$(msg llm.openai.test.ok)\"\n else\n echo -e \"\\033[33m$(msg llm.openai.test.fail \"${_http_code}\" \"${_body}\")\\033[0m\"\n if [ -n \"${hint}\" ]; then\n echo -e \"\\033[33m${hint}\\033[0m\"\n fi\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" != \"1\" ]; then\n local _confirm\n read -e -p \"$(msg llm.openai.test.confirm)\" _confirm\n if [ \"${_confirm}\" = \"b\" ]; then\n STEP_RESULT=\"back\"\n return 1\n fi\n if [ \"${_confirm}\" != \"y\" ] && [ \"${_confirm}\" != \"Y\" ]; then\n log \"$(msg llm.openai.test.aborted)\"\n exit 1\n fi\n fi\n fi\n}\n\ntest_embedding_connectivity() {\n local base_url=\"$1\"\n local api_key=\"$2\"\n local model=\"$3\"\n if ! command -v curl >/dev/null 2>&1; then\n return 0\n fi\n log \"$(msg llm.embedding.test.testing)\"\n local _body _http_code _tmpfile\n _tmpfile=$(mktemp)\n _http_code=$(curl -s -o \"${_tmpfile}\" -w \"%{http_code}\" \\\n -X POST \"${base_url%/}/embeddings\" \\\n -H \"Authorization: Bearer ${api_key}\" \\\n -H \"Content-Type: application/json\" \\\n -H \"User-Agent: AgentTeams/${AGENTTEAMS_VERSION:-latest}\" \\\n --max-time 30 \\\n -d \"{\\\"model\\\":\\\"${model}\\\",\\\"input\\\":\\\"test\\\"}\" \\\n 2>/dev/null)\n _body=$(cat \"${_tmpfile}\")\n rm -f \"${_tmpfile}\"\n if [ \"${_http_code}\" = \"200\" ] || [ \"${_http_code}\" = \"201\" ]; then\n log \"$(msg llm.embedding.test.ok)\"\n return 0\n else\n echo -e \"\\033[33m$(msg llm.embedding.test.fail \"${_http_code}\" \"${_body}\")\\033[0m\"\n return 1\n fi\n}\n\n# ============================================================\n# Uninstall\n# ============================================================\n\nuninstall_agentteams() {\n log \"$(msg uninstall.title)\"\n\n # Safely disable and remove the dedicated AgentTeams Podman autostart service\n if [ \"${DOCKER_CMD}\" = \"podman\" ] && command -v systemctl >/dev/null 2>&1; then\n local _systemctl_cmd=\"systemctl\"\n local _service_dir=\"/etc/systemd/system\"\n\n if [ \"$(id -u)\" != \"0\" ]; then\n _systemctl_cmd=\"systemctl --user\"\n _service_dir=\"${HOME}/.config/systemd/user\"\n fi\n\n if [ -f \"${_service_dir}/agentteams-podman-restart.service\" ]; then\n log \"Disabling and removing dedicated AgentTeams Podman autostart service...\"\n ${_systemctl_cmd} disable --now agentteams-podman-restart.service 2>/dev/null || true\n rm -f \"${_service_dir}/agentteams-podman-restart.service\"\n ${_systemctl_cmd} daemon-reload 2>/dev/null || true\n fi\n # Note: Native podman-restart.service is strictly untouched per maintainer review.\n fi\n\n # Capture manager image before removing (needed for workspace cleanup on Linux)\n local manager_image=\"\"\n manager_image=$(${DOCKER_CMD} inspect agentteams-manager --format '{{.Config.Image}}' 2>/dev/null || true)\n\n # Stop and remove manager\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -q \"^agentteams-manager$\"; then\n log \"$(msg uninstall.stopping_manager)\"\n ${DOCKER_CMD} stop agentteams-manager >/dev/null 2>&1 || true\n ${DOCKER_CMD} rm agentteams-manager >/dev/null 2>&1 || true\n fi\n\n # Stop and remove workers\n local workers\n workers=$(${DOCKER_CMD} ps -a --filter \"name=agentteams-worker-\" --format '{{.Names}}' 2>/dev/null || true)\n if [ -n \"${workers}\" ]; then\n log \"$(msg uninstall.stopping_workers)\"\n echo \"${workers}\" | while read -r w; do\n ${DOCKER_CMD} rm -f \"${w}\" >/dev/null 2>&1 || true\n log \"$(msg uninstall.removed \"${w}\")\"\n done\n fi\n\n # Stop and remove the dashboard container (installed via step_dashboard)\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -q \"^agentteams-dashboard$\"; then\n log \"Stopping and removing agentteams-dashboard...\"\n ${DOCKER_CMD} stop agentteams-dashboard >/dev/null 2>&1 || true\n ${DOCKER_CMD} rm agentteams-dashboard >/dev/null 2>&1 || true\n fi\n\n # Stop and remove docker-proxy (legacy ≤ v1.0.x; current arch uses\n # agentteams-controller for the same role)\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -q \"^agentteams-docker-proxy$\"; then\n log \"$(msg uninstall.removing_proxy)\"\n ${DOCKER_CMD} stop agentteams-docker-proxy >/dev/null 2>&1 || true\n ${DOCKER_CMD} rm agentteams-docker-proxy >/dev/null 2>&1 || true\n fi\n\n # Stop and remove the embedded controller container. MUST happen\n # before the `docker volume rm agentteams-data` step below — in embedded\n # mode agentteams-controller mounts agentteams-data at /data (Tuwunel DB,\n # MinIO state, Higress state, all the room messages), and `volume\n # rm` against an in-use volume fails silently because of the trailing\n # `|| true`. Skipping this used to leave room/message history behind\n # across \"uninstall + reinstall\" cycles. See PR #692.\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -q \"^agentteams-controller$\"; then\n log \"$(msg uninstall.stopping_controller)\"\n ${DOCKER_CMD} stop agentteams-controller >/dev/null 2>&1 || true\n ${DOCKER_CMD} rm agentteams-controller >/dev/null 2>&1 || true\n fi\n\n # Read env file for data/workspace info before removing\n local env_file=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n [ ! -f \"${env_file}\" ] && [ -f \"./agentteams-manager.env\" ] && env_file=\"./agentteams-manager.env\"\n\n local data_dir=\"\" workspace_dir=\"\"\n if [ -f \"${env_file}\" ]; then\n data_dir=$(grep '^AGENTTEAMS_DATA_DIR=' \"${env_file}\" 2>/dev/null | cut -d= -f2- || true)\n workspace_dir=$(grep '^AGENTTEAMS_WORKSPACE_DIR=' \"${env_file}\" 2>/dev/null | cut -d= -f2- || true)\n fi\n\n # Remove Docker volume\n local vol=\"${data_dir:-agentteams-data}\"\n if ${DOCKER_CMD} volume inspect \"${vol}\" >/dev/null 2>&1; then\n log \"$(msg uninstall.removing_volume \"${vol}\")\"\n ${DOCKER_CMD} volume rm \"${vol}\" >/dev/null 2>&1 || true\n fi\n\n # Remove Docker network\n if ${DOCKER_CMD} network ls --format '{{.Name}}' 2>/dev/null | grep -q \"^agentteams-net$\"; then\n log \"$(msg uninstall.removing_network)\"\n ${DOCKER_CMD} network rm agentteams-net >/dev/null 2>&1 || true\n fi\n\n # Remove workspace directory\n if [ -n \"${workspace_dir}\" ] && [ -d \"${workspace_dir}\" ]; then\n log \"$(msg uninstall.removing_workspace \"${workspace_dir}\")\"\n rm -rf \"${workspace_dir}\" 2>/dev/null || true\n if [ -d \"${workspace_dir}\" ]; then\n log \"$(msg uninstall.removing_workspace_elevated)\"\n local _parent _base\n _parent=$(dirname \"${workspace_dir}\")\n _base=$(basename \"${workspace_dir}\")\n local _rm_image=\"${manager_image:-busybox}\"\n ${DOCKER_CMD} run --rm --entrypoint sh \\\n -v \"${_parent}:/host-parent\" \\\n \"${_rm_image}\" \\\n -c \"rm -rf /host-parent/${_base}\" 2>/dev/null || true\n fi\n fi\n\n # Remove env file\n if [ -f \"${env_file}\" ]; then\n log \"$(msg uninstall.removing_env \"${env_file}\")\"\n rm -f \"${env_file}\"\n fi\n\n # Remove install log\n if [ -f \"${HOME}/agentteams-install.log\" ]; then\n log \"$(msg uninstall.removing_log \"${HOME}/agentteams-install.log\")\"\n rm -f \"${HOME}/agentteams-install.log\"\n fi\n\n echo \"\"\n log \"$(msg uninstall.done)\"\n}\n\n# ============================================================\n# Check container runtime (docker or podman) and environment\n# ============================================================\ncheck_container_runtime() {\n # Ensure the rootless environment baseline (XDG_RUNTIME_DIR) is ready before engine detection.\n if [ \"$(id -u)\" -ne 0 ] && [ -z \"${XDG_RUNTIME_DIR:-}\" ]; then\n export XDG_RUNTIME_DIR=\"/run/user/$(id -u)\"\n fi\n\n if command -v docker >/dev/null 2>&1; then\n DOCKER_CMD=\"docker\"\n # Check for podman disguised as docker (alias or podman-docker package)\n if docker --version 2>/dev/null | grep -qi \"podman\"; then\n DOCKER_CMD=\"podman\"\n fi\n elif command -v podman >/dev/null 2>&1; then\n DOCKER_CMD=\"podman\"\n else\n echo -e \"\\033[31m[AgentTeams ERROR]\\033[0m $(msg error.docker_not_found)\" >&2\n exit 1\n fi\n\n # Unified health check (Validates runtime responsiveness)\n if ! ${DOCKER_CMD} ps >/dev/null 2>&1; then\n echo -e \"\\033[31m[AgentTeams ERROR]\\033[0m $(msg error.docker_not_running)\" >&2\n exit 1\n fi\n}\n\ncheck_container_runtime\n\ncase \"${1:-}\" in\n manager|\"\")\n install_manager\n ;;\n worker)\n shift\n install_worker \"$@\"\n ;;\n dashboard)\n check_container_runtime\n load_current_params_from_env\n AGENTTEAMS_DASHBOARD=\"${AGENTTEAMS_DASHBOARD:-1}\"\n AGENTTEAMS_DASHBOARD_VERSION=\"${AGENTTEAMS_DASHBOARD_VERSION:-v1.2.0-beta.1}\"\n AGENTTEAMS_PORT_DASHBOARD=\"${AGENTTEAMS_PORT_DASHBOARD:-13000}\"\n AGENTTEAMS_DASHBOARD_IMAGE=\"${AGENTTEAMS_DASHBOARD_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-dashboard:${AGENTTEAMS_DASHBOARD_VERSION}}\"\n AGENTTEAMS_USE_EMBEDDED=1\n _start_dashboard\n ;;\n uninstall)\n uninstall_agentteams\n ;;\n *)\n echo \"Usage: $0 [manager|worker [options]|dashboard|uninstall]\"\n echo \"\"\n echo \"Commands:\"\n echo \" manager Interactive Manager installation (default)\"\n echo \" Choose Quick Start (all defaults) or Manual mode\"\n echo \" worker Worker installation (requires --name and connection params)\"\n echo \" dashboard Start/stop agentteams-dashboard (use with AGENTTEAMS_DASHBOARD=0 to stop)\"\n echo \" uninstall Stop and remove Manager + all Worker containers\"\n echo \"\"\n echo \"Quick Start (fastest):\"\n echo \" $0\"\n echo \" # Then select '1' for Quick Start mode\"\n echo \"\"\n echo \"Non-interactive (for automation):\"\n echo \" AGENTTEAMS_NON_INTERACTIVE=1 AGENTTEAMS_LLM_API_KEY=sk-xxx $0\"\n echo \"\"\n echo \"Worker Options:\"\n echo \" --name <name> Worker name (required)\"\n echo \" --fs <url> MinIO endpoint URL (required)\"\n echo \" --fs-key <key> MinIO access key (required)\"\n echo \" --fs-secret <secret> MinIO secret key (required)\"\n echo \" --reset Remove existing Worker container before creating\"\n exit 1\n ;;\nesac\n"}, "82cbd5fe78d294b1018fc8a037e4f91879dce9e7": {"t": 1785764748.087161, "n": 243232, "h": "c4816e92cd6f46f7", "c": "#!/bin/bash\n# agentteams-install.sh - One-click installation for AgentTeams Manager, Worker, and Dashboard\n#\n# Usage:\n# ./agentteams-install.sh # Interactive installation (choose Quick Start or Manual)\n# ./agentteams-install.sh manager # Same as above (explicit)\n# ./agentteams-install.sh worker --name <name> ... # Worker installation\n# ./agentteams-install.sh dashboard # Start/stop agentteams-dashboard (AGENTTEAMS_DASHBOARD=0 to stop)\n# ./agentteams-install.sh uninstall # Stop and remove Manager + all Workers\n#\n# NOTE: agentteams-dashboard is currently only supported via the Bash installer.\n# The PowerShell installer (agentteams-install.ps1) does not yet include dashboard support.\n#\n# Onboarding Modes:\n# Quick Start - Fast installation with all default values (recommended)\n# Manual - Customize each option step by step\n#\n# Environment variables (for automation):\n# AGENTTEAMS_NON_INTERACTIVE Skip all prompts, use defaults (default: 0)\n# AGENTTEAMS_LLM_PROVIDER LLM provider (default: openai-compat for zh non-interactive Token Plan; qwen for en)\n# AGENTTEAMS_DEFAULT_MODEL Default model (default: qwen3.6-plus for zh Token Plan and en non-interactive)\n# AGENTTEAMS_OPENAI_BASE_URL OpenAI-compatible base URL (default for zh non-interactive: Alibaba Token Plan endpoint)\n# AGENTTEAMS_LLM_API_KEY LLM API key (required)\n# AGENTTEAMS_ADMIN_USER Admin username (default: admin)\n# AGENTTEAMS_ADMIN_PASSWORD Admin password (auto-generated if not set, min 8 chars)\n# AGENTTEAMS_MATRIX_DOMAIN Matrix domain (default: matrix-local.agentteams.io:18080)\n# AGENTTEAMS_MOUNT_SOCKET Mount container runtime socket (default: 1)\n# AGENTTEAMS_DATA_DIR Docker volume name for persistent data (default: agentteams-data)\n# AGENTTEAMS_WORKSPACE_DIR Host directory for manager workspace (default: ~/agentteams-manager)\n# AGENTTEAMS_VERSION Image tag (default: latest)\n# AGENTTEAMS_REGISTRY Image registry (default: auto-detected by timezone)\n# AGENTTEAMS_INSTALL_MANAGER_IMAGE Override manager image (e.g., local build)\n# AGENTTEAMS_INSTALL_WORKER_IMAGE Override worker image (e.g., local build)\n# AGENTTEAMS_INSTALL_COPAW_WORKER_IMAGE Override copaw worker image (e.g., local build)\n# AGENTTEAMS_INSTALL_HERMES_WORKER_IMAGE Override hermes worker image (e.g., local build)\n# AGENTTEAMS_NACOS_REGISTRY_URI Default Nacos registry URI for Worker market search/import\n# (default: nacos://market.agentteams.io:80/public)\n# AGENTTEAMS_NACOS_USERNAME Default Nacos username for nacos:// package imports (optional)\n# AGENTTEAMS_NACOS_PASSWORD Default Nacos password for nacos:// package imports (optional)\n# AGENTTEAMS_CMS_TRACES_ENABLED Enable openclaw-cms-plugin traces for Manager AND all Workers (default: false)\n# AGENTTEAMS_CMS_ENDPOINT ARMS OTLP endpoint (required if traces enabled)\n# AGENTTEAMS_CMS_LICENSE_KEY CMS license key (required if traces enabled)\n# AGENTTEAMS_CMS_PROJECT CMS project name (optional)\n# AGENTTEAMS_CMS_WORKSPACE CMS workspace ID (required if traces enabled)\n# AGENTTEAMS_CMS_SERVICE_NAME Manager service name in ARMS (default: agentteams-manager)\n# Workers always report as agentteams-worker-<name> automatically\n# AGENTTEAMS_CMS_METRICS_ENABLED Enable diagnostics-otel metrics for Manager AND all Workers (default: false)\n# AGENTTEAMS_PORT_GATEWAY Host port for Higress gateway (default: 18080)\n# AGENTTEAMS_PORT_CONSOLE Host port for Higress console (default: 18001)\n# AGENTTEAMS_PORT_ELEMENT_WEB Host port for Element Web direct access (default: 18088)\n# AGENTTEAMS_PORT_MANAGER_CONSOLE Host port for Manager console (default: 18888)\n# AGENTTEAMS_WORKER_IDLE_TIMEOUT Worker idle timeout in minutes (default: 720, i.e. 12 hours)\n# AGENTTEAMS_DASHBOARD Install agentteams-dashboard management UI (default: 1)\n# AGENTTEAMS_DASHBOARD_VERSION Dashboard version (default: v1.2.0-beta.1, independent of AgentTeams version)\n# AGENTTEAMS_PORT_DASHBOARD Dashboard host port (default: 13000)\n# AGENTTEAMS_DASHBOARD_IMAGE Override dashboard image (default: <registry>/agentteams/agentteams-dashboard:<DASHBOARD_VERSION>)\n# AGENTTEAMS_AI_GATEWAY_ADMIN_URL Higress Console URL for shared auth (auto-detected)\n\nset -e\n\nAGENTTEAMS_VERSION=\"${AGENTTEAMS_VERSION:-}\"\nAGENTTEAMS_KNOWN_STABLE_VERSION=\"v1.1.2\" # fallback if GitHub API is unreachable\n\n# Returns 0 (true) if $1 < $2 using semver order; \"latest\" is treated as greatest\n_ver_lt() {\n [ \"$1\" = \"latest\" ] && return 1\n [ \"$2\" = \"latest\" ] && return 0\n [ \"$1\" = \"$2\" ] && return 1\n [ \"$(printf '%s\\n%s' \"$1\" \"$2\" | sort -V | head -1)\" = \"$1\" ]\n}\nAGENTTEAMS_NON_INTERACTIVE=\"${AGENTTEAMS_NON_INTERACTIVE:-0}\"\nAGENTTEAMS_MOUNT_SOCKET=\"${AGENTTEAMS_MOUNT_SOCKET:-1}\"\nAGENTTEAMS_DOCKER_PROXY=\"${AGENTTEAMS_DOCKER_PROXY:-1}\"\nSTEP_RESULT=\"\" # Used by state machine to signal \"back\" navigation\n\n# ============================================================\n# Log all output to file\n# ============================================================\n\nAGENTTEAMS_LOG_FILE=\"${HOME}/agentteams-install.log\"\n\nif [ \"${1:-}\" != \"uninstall\" ]; then\n # Redirect all output (stdout and stderr) to both terminal and log file\n exec > >(tee -a \"${AGENTTEAMS_LOG_FILE}\") 2>&1\n\n echo \"\"\n echo \"========================================\"\n echo \"AgentTeams Installation Log\"\n echo \"Started: $(date)\"\n echo \"User: $(whoami)\"\n echo \"System: $(uname -a)\"\n echo \"Log file: ${AGENTTEAMS_LOG_FILE}\"\n echo \"========================================\"\n echo \"\"\nfi\n\n# ============================================================\n# Utility functions (needed early for timezone detection)\n# ============================================================\n\nlog() {\n echo -e \"\\033[36m[AgentTeams]\\033[0m $1\"\n}\n\nerror() {\n echo -e \"\\033[31m[AgentTeams ERROR]\\033[0m $1\" >&2\n}\n\ndie() {\n error \"$1\"\n exit 1\n}\n\n# ============================================================\n# Timezone detection (compatible with Linux and macOS)\n# ============================================================\n\ndetect_timezone() {\n local tz=\"\"\n\n # Try /etc/timezone (Debian/Ubuntu)\n if [ -f /etc/timezone ]; then\n tz=$(cat /etc/timezone 2>/dev/null | tr -d '[:space:]')\n fi\n\n # Try /etc/localtime symlink (macOS and some Linux)\n if [ -z \"${tz}\" ] && [ -L /etc/localtime ]; then\n tz=$(ls -l /etc/localtime 2>/dev/null | sed 's|.*/zoneinfo/||')\n fi\n\n # Try timedatectl (systemd)\n if [ -z \"${tz}\" ]; then\n tz=$(timedatectl show --value -p Timezone 2>/dev/null)\n fi\n\n # If still not detected, warn and prompt user. Diagnostics go to stderr —\n # this function returns its value via stdout to `$(detect_timezone)`, so\n # any echo here would leak into AGENTTEAMS_TIMEZONE and downstream image tags.\n if [ -z \"${tz}\" ]; then\n echo \"\" >&2\n echo -e \"\\033[33m[AgentTeams WARNING]\\033[0m Could not detect timezone automatically.\" >&2\n echo -e \"\\033[33m[AgentTeams]\\033[0m Please enter your timezone (e.g., Asia/Shanghai, America/New_York).\" >&2\n echo \"\" >&2\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n tz=\"Asia/Shanghai\"\n log \"Using default timezone: ${tz}\" >&2\n else\n read -e -p \"Timezone [Asia/Shanghai]: \" tz\n tz=\"${tz:-Asia/Shanghai}\"\n fi\n fi\n\n echo \"${tz}\"\n}\n\n# Detect timezone once at startup (used by registry selection and container TZ)\nAGENTTEAMS_TIMEZONE=\"${AGENTTEAMS_TIMEZONE:-$(detect_timezone)}\"\n\n# ============================================================\n# Language detection based on timezone\n# ============================================================\n\ndetect_language() {\n local tz=\"${AGENTTEAMS_TIMEZONE}\"\n case \"${tz}\" in\n Asia/Shanghai|Asia/Chongqing|Asia/Harbin|Asia/Urumqi|\\\n Asia/Taipei|Asia/Hong_Kong|Asia/Macau)\n echo \"zh\"\n ;;\n *)\n echo \"en\"\n ;;\n esac\n}\n\n# Language priority: env var > existing env file > timezone detection\nif [ -z \"${AGENTTEAMS_LANGUAGE}\" ]; then\n # Check existing env file for saved language preference (upgrade scenario)\n _env_file=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n # Migrate from legacy location (current directory) if needed\n if [ ! -f \"${_env_file}\" ] && [ -f \"./agentteams-manager.env\" ]; then\n mv \"./agentteams-manager.env\" \"${_env_file}\" 2>/dev/null || true\n fi\n if [ -f \"${_env_file}\" ]; then\n _saved_lang=$(grep '^AGENTTEAMS_LANGUAGE=' \"${_env_file}\" 2>/dev/null | cut -d= -f2-)\n if [ -n \"${_saved_lang}\" ]; then\n AGENTTEAMS_LANGUAGE=\"${_saved_lang}\"\n fi\n fi\n # Fall back to timezone-based detection\n if [ -z \"${AGENTTEAMS_LANGUAGE}\" ]; then\n AGENTTEAMS_LANGUAGE=\"$(detect_language)\"\n fi\n unset _env_file _saved_lang\nfi\nexport AGENTTEAMS_LANGUAGE\n\n# ============================================================\n# Centralized message dictionary and msg() function\n# Compatible with bash 3.2+ (macOS default) — uses case instead of declare -A\n# ============================================================\n\n# msg() function: look up message by key, with printf-style argument substitution\n# Falls back to English if the current language translation is missing.\nmsg() {\n local key=\"$1\"\n shift\n local lang=\"${AGENTTEAMS_LANGUAGE:-en}\"\n local text=\"\"\n case \"${key}.${lang}\" in\n # --- Timezone detection messages ---\n \"tz.warning.title.zh\") text=\"无法自动检测时区。\" ;;\n \"tz.warning.title.en\") text=\"Could not detect timezone automatically.\" ;;\n \"tz.warning.prompt.zh\") text=\"请输入您的时区(例如 Asia/Shanghai、America/New_York)。\" ;;\n \"tz.warning.prompt.en\") text=\"Please enter your timezone (e.g., Asia/Shanghai, America/New_York).\" ;;\n \"tz.default.zh\") text=\"使用默认时区: %s\" ;;\n \"tz.default.en\") text=\"Using default timezone: %s\" ;;\n \"tz.input_prompt.zh\") text=\"时区\" ;;\n \"tz.input_prompt.en\") text=\"Timezone\" ;;\n # --- Installation title and info ---\n \"install.title.zh\") text=\"=== AgentTeams Manager 安装 ===\" ;;\n \"install.title.en\") text=\"=== AgentTeams Manager Installation ===\" ;;\n \"install.registry.zh\") text=\"镜像仓库: %s\" ;;\n \"install.registry.en\") text=\"Registry: %s\" ;;\n \"install.dir.zh\") text=\"安装目录: %s\" ;;\n \"install.dir.en\") text=\"Installation directory: %s\" ;;\n \"install.dir_hint.zh\") text=\" (env 文件 'agentteams-manager.env' 将保存到 HOME 目录。)\" ;;\n \"install.dir_hint.en\") text=\" (The env file 'agentteams-manager.env' will be saved to your HOME directory.)\" ;;\n \"install.dir_hint2.zh\") text=\" (请从您希望管理此安装的目录运行此脚本。)\" ;;\n \"install.dir_hint2.en\") text=\" (Run this script from the directory where you want to manage this installation.)\" ;;\n # --- Onboarding mode ---\n \"install.mode.title.zh\") text=\"--- Onboarding 模式 ---\" ;;\n \"install.mode.title.en\") text=\"--- Onboarding Mode ---\" ;;\n \"install.mode.choose.zh\") text=\"选择安装模式:\" ;;\n \"install.mode.choose.en\") text=\"Choose your installation mode:\" ;;\n \"install.mode.quickstart.zh\") text=\" 1) 快速开始 - 使用阿里云通义 Token 套餐快速安装(推荐)\" ;;\n \"install.mode.quickstart.en\") text=\" 1) Quick Start - Fast installation with Qwen Cloud (recommended)\" ;;\n \"install.mode.manual.zh\") text=\" 2) 手动配置 - 选择 LLM 提供商并自定义选项\" ;;\n \"install.mode.manual.en\") text=\" 2) Manual - Choose LLM provider and customize options\" ;;\n \"install.mode.prompt.zh\") text=\"请选择 [1/2]\" ;;\n \"install.mode.prompt.en\") text=\"Enter choice [1/2]\" ;;\n \"install.mode.quickstart_selected.zh\") text=\"已选择快速开始模式 - 使用阿里云通义 Token 套餐\" ;;\n \"install.mode.quickstart_selected.en\") text=\"Quick Start mode selected - using Qwen Cloud\" ;;\n \"install.mode.manual_selected.zh\") text=\"已选择手动配置模式 - 您将选择 LLM 提供商并自定义选项\" ;;\n \"install.mode.manual_selected.en\") text=\"Manual mode selected - you will choose LLM provider and customize options\" ;;\n \"install.mode.invalid.zh\") text=\"无效选择,默认使用快速开始模式\" ;;\n \"install.mode.invalid.en\") text=\"Invalid choice, defaulting to Quick Start mode\" ;;\n # --- Upgrade mode sub-menu ---\n \"upgrade.mode.prompt.zh\") text=\"请选择升级方式:\" ;;\n \"upgrade.mode.prompt.en\") text=\"Select upgrade mode:\" ;;\n \"upgrade.mode.keep_all.zh\") text=\" 1) 保留所有参数,一键升级(推荐)\" ;;\n \"upgrade.mode.keep_all.en\") text=\" 1) Keep all parameters, quick upgrade (recommended)\" ;;\n \"upgrade.mode.confirm_each.zh\") text=\" 2) 逐个确认参数(可修改)\" ;;\n \"upgrade.mode.confirm_each.en\") text=\" 2) Confirm each parameter (can modify)\" ;;\n \"upgrade.mode.back.zh\") text=\" 3) 返回\" ;;\n \"upgrade.mode.back.en\") text=\" 3) Back\" ;;\n # --- Version selection ---\n \"install.version.title.zh\") text=\"--- 版本选择 ---\" ;;\n \"install.version.title.en\") text=\"--- Version Selection ---\" ;;\n \"install.version.choose.zh\") text=\"选择要安装的版本:\" ;;\n \"install.version.choose.en\") text=\"Choose the version to install:\" ;;\n \"install.version.option_latest.zh\") text=\" 1) latest - 最新版(默认)\" ;;\n \"install.version.option_latest.en\") text=\" 1) latest - Latest build (default)\" ;;\n \"install.version.option_stable.zh\") text=\" 2) %s - 最新稳定版\" ;;\n \"install.version.option_stable.en\") text=\" 2) %s - Latest stable release\" ;;\n \"install.version.fetching.zh\") text=\"正在查询最新稳定版本...\" ;;\n \"install.version.fetching.en\") text=\"Fetching latest stable release...\" ;;\n \"install.version.fetch_failed.zh\") text=\"无法查询 GitHub,使用内置版本 %s\" ;;\n \"install.version.fetch_failed.en\") text=\"Could not reach GitHub, using built-in version %s\" ;;\n \"install.version.option_custom.zh\") text=\" 3) 自定义 - 手动输入版本号(如 v1.0.5)\" ;;\n \"install.version.option_custom.en\") text=\" 3) Custom - Enter a specific version (e.g. v1.0.5)\" ;;\n \"install.version.prompt.zh\") text=\"请选择 [1/2/3]\" ;;\n \"install.version.prompt.en\") text=\"Enter choice [1/2/3]\" ;;\n \"install.version.custom_prompt.zh\") text=\"请输入版本号\" ;;\n \"install.version.custom_prompt.en\") text=\"Enter version tag\" ;;\n \"install.version.selected_latest.zh\") text=\"已选择最新版 (latest)\" ;;\n \"install.version.selected_latest.en\") text=\"Selected latest version\" ;;\n \"install.version.selected_stable.zh\") text=\"已选择最新稳定版 (%s)\" ;;\n \"install.version.selected_stable.en\") text=\"Selected latest stable version (%s)\" ;;\n \"install.version.selected_custom.zh\") text=\"已选择自定义版本 (%s)\" ;;\n \"install.version.selected_custom.en\") text=\"Selected custom version (%s)\" ;;\n \"install.version.invalid.zh\") text=\"无效选择,使用最新稳定版 (%s)\" ;;\n \"install.version.invalid.en\") text=\"Invalid choice, defaulting to latest stable version (%s)\" ;;\n # --- Existing installation detected ---\n \"install.existing.detected.zh\") text=\"检测到已有 Manager 安装(env 文件: %s)\" ;;\n \"install.existing.detected.en\") text=\"Existing Manager installation detected (env file: %s)\" ;;\n \"install.existing.choose.zh\") text=\"选择操作:\" ;;\n \"install.existing.choose.en\") text=\"Choose an action:\" ;;\n \"install.existing.upgrade.zh\") text=\" 1) 就地升级(保留数据、工作空间、env 文件)\" ;;\n \"install.existing.upgrade.en\") text=\" 1) In-place upgrade (keep data, workspace, env file)\" ;;\n \"install.existing.reinstall.zh\") text=\" 2) 全新重装(删除所有数据,重新开始)\" ;;\n \"install.existing.reinstall.en\") text=\" 2) Clean reinstall (remove all data, start fresh)\" ;;\n \"install.existing.cancel.zh\") text=\" 3) 取消\" ;;\n \"install.existing.cancel.en\") text=\" 3) Cancel\" ;;\n \"install.existing.prompt.zh\") text=\"请选择 [1/2/3]\" ;;\n \"install.existing.prompt.en\") text=\"Enter choice [1/2/3]\" ;;\n \"install.existing.upgrade_noninteractive.zh\") text=\"非交互模式: 执行就地升级...\" ;;\n \"install.existing.upgrade_noninteractive.en\") text=\"Non-interactive mode: performing in-place upgrade...\" ;;\n \"install.existing.upgrading.zh\") text=\"执行就地升级...\" ;;\n \"install.existing.upgrading.en\") text=\"Performing in-place upgrade...\" ;;\n \"install.existing.warn_manager_stop.zh\") text=\"⚠️ Manager 容器将被停止并重新创建。\" ;;\n \"install.existing.warn_manager_stop.en\") text=\"⚠️ Manager container will be stopped and recreated.\" ;;\n \"install.existing.warn_worker_recreate.zh\") text=\"⚠️ Worker 容器也将被重新创建(以更新 Manager IP)。\" ;;\n \"install.existing.warn_worker_recreate.en\") text=\"⚠️ Worker containers will also be recreated (to update Manager IP in hosts).\" ;;\n \"install.existing.continue_prompt.zh\") text=\"继续?[y/N]\" ;;\n \"install.existing.continue_prompt.en\") text=\"Continue? [y/N]\" ;;\n \"install.existing.cancelled.zh\") text=\"安装已取消。\" ;;\n \"install.existing.cancelled.en\") text=\"Installation cancelled.\" ;;\n \"install.existing.stopping_manager.zh\") text=\"停止并移除现有 manager 容器...\" ;;\n \"install.existing.stopping_manager.en\") text=\"Stopping and removing existing manager container...\" ;;\n \"install.existing.stopping_workers.zh\") text=\"停止并移除现有 worker 容器...\" ;;\n \"install.existing.stopping_workers.en\") text=\"Stopping and removing existing worker containers...\" ;;\n \"install.existing.removed.zh\") text=\" 已移除: %s\" ;;\n \"install.existing.removed.en\") text=\" Removed: %s\" ;;\n # --- Clean reinstall messages ---\n \"install.reinstall.performing.zh\") text=\"执行全新重装...\" ;;\n \"install.reinstall.performing.en\") text=\"Performing clean reinstall...\" ;;\n \"install.reinstall.warn_stop.zh\") text=\"⚠️ 以下运行中的容器将被停止:\" ;;\n \"install.reinstall.warn_stop.en\") text=\"⚠️ The following running containers will be stopped:\" ;;\n \"install.reinstall.warn_delete.zh\") text=\"⚠️ 警告: 以下内容将被删除:\" ;;\n \"install.reinstall.warn_delete.en\") text=\"⚠️ WARNING: This will DELETE the following:\" ;;\n \"install.reinstall.warn_volume.zh\") text=\" - Docker 卷: agentteams-data\" ;;\n \"install.reinstall.warn_volume.en\") text=\" - Docker volume: agentteams-data\" ;;\n \"install.reinstall.warn_env.zh\") text=\" - Env 文件: %s\" ;;\n \"install.reinstall.warn_env.en\") text=\" - Env file: %s\" ;;\n \"install.reinstall.warn_workspace.zh\") text=\" - Manager 工作空间: %s\" ;;\n \"install.reinstall.warn_workspace.en\") text=\" - Manager workspace: %s\" ;;\n \"install.reinstall.warn_workers.zh\") text=\" - 所有 worker 容器\" ;;\n \"install.reinstall.warn_workers.en\") text=\" - All worker containers\" ;;\n \"install.reinstall.warn_proxy.zh\") text=\" - Docker API 代理容器: agentteams-controller\" ;;\n \"install.reinstall.warn_proxy.en\") text=\" - Docker API proxy container: agentteams-controller\" ;;\n \"install.reinstall.warn_network.zh\") text=\" - Docker 网络: agentteams-net\" ;;\n \"install.reinstall.warn_network.en\") text=\" - Docker network: agentteams-net\" ;;\n \"install.reinstall.confirm_type.zh\") text=\"请输入工作空间路径以确认删除(或按 Ctrl+C 取消):\" ;;\n \"install.reinstall.confirm_type.en\") text=\"To confirm deletion, please type the workspace path:\" ;;\n \"install.reinstall.confirm_path.zh\") text=\"输入路径以确认(或按 Ctrl+C 取消)\" ;;\n \"install.reinstall.confirm_path.en\") text=\"Type the path to confirm (or press Ctrl+C to cancel)\" ;;\n \"install.reinstall.path_mismatch.zh\") text=\"路径不匹配。中止重装。输入: '%s',期望: '%s'\" ;;\n \"install.reinstall.path_mismatch.en\") text=\"Path mismatch. Aborting reinstall. Input: '%s', Expected: '%s'\" ;;\n \"install.reinstall.confirmed.zh\") text=\"已确认。正在清理...\" ;;\n \"install.reinstall.confirmed.en\") text=\"Confirmed. Cleaning up...\" ;;\n \"install.reinstall.removed_worker.zh\") text=\" 已移除 worker: %s\" ;;\n \"install.reinstall.removed_worker.en\") text=\" Removed worker: %s\" ;;\n \"install.reinstall.removing_volume.zh\") text=\"正在移除 Docker 卷: agentteams-data\" ;;\n \"install.reinstall.removing_volume.en\") text=\"Removing Docker volume: agentteams-data\" ;;\n \"install.reinstall.warn_volume_fail.zh\") text=\" 警告: 无法移除卷(可能有引用)\" ;;\n \"install.reinstall.warn_volume_fail.en\") text=\" Warning: Could not remove volume (may have references)\" ;;\n \"install.reinstall.removing_proxy.zh\") text=\"正在移除 Docker API 代理容器: agentteams-controller\" ;;\n \"install.reinstall.removing_proxy.en\") text=\"Removing Docker API proxy container: agentteams-controller\" ;;\n \"install.reinstall.removing_network.zh\") text=\"正在移除 Docker 网络: agentteams-net\" ;;\n \"install.reinstall.removing_network.en\") text=\"Removing Docker network: agentteams-net\" ;;\n \"install.reinstall.removing_workspace.zh\") text=\"正在移除工作空间目录: %s\" ;;\n \"install.reinstall.removing_workspace.en\") text=\"Removing workspace directory: %s\" ;;\n \"install.reinstall.removing_env.zh\") text=\"正在移除 env 文件: %s\" ;;\n \"install.reinstall.removing_env.en\") text=\"Removing env file: %s\" ;;\n \"install.reinstall.cleanup_done.zh\") text=\"清理完成。开始全新安装...\" ;;\n \"install.reinstall.cleanup_done.en\") text=\"Cleanup complete. Starting fresh installation...\" ;;\n \"install.reinstall.failed_rm_workspace.zh\") text=\"无法移除工作空间目录\" ;;\n \"install.reinstall.failed_rm_workspace.en\") text=\"Failed to remove workspace directory\" ;;\n # --- Orphan volume detection ---\n \"install.orphan_volume.detected.zh\") text=\"⚠️ 检测到残留数据卷 '%s',但未找到对应的 env 配置文件。\" ;;\n \"install.orphan_volume.detected.en\") text=\"⚠️ Found leftover data volume '%s' but no matching env config file.\" ;;\n \"install.orphan_volume.warn.zh\") text=\"这可能是之前安装的残留数据,会导致新安装出现异常(如密码冲突、服务启动失败)。\" ;;\n \"install.orphan_volume.warn.en\") text=\"This is likely leftover data from a previous installation and may cause issues (e.g., credential conflicts, service startup failures).\" ;;\n \"install.orphan_volume.choose.zh\") text=\"选择操作:\" ;;\n \"install.orphan_volume.choose.en\") text=\"Choose an action:\" ;;\n \"install.orphan_volume.clean.zh\") text=\" 1) 清理残留数据卷后继续安装(推荐)\" ;;\n \"install.orphan_volume.clean.en\") text=\" 1) Remove leftover volume and continue installation (recommended)\" ;;\n \"install.orphan_volume.keep.zh\") text=\" 2) 保留数据卷继续安装(可能出现异常)\" ;;\n \"install.orphan_volume.keep.en\") text=\" 2) Keep the volume and continue installation (may cause issues)\" ;;\n \"install.orphan_volume.prompt.zh\") text=\"请选择 [1/2]\" ;;\n \"install.orphan_volume.prompt.en\") text=\"Enter choice [1/2]\" ;;\n \"install.orphan_volume.cleaning.zh\") text=\"正在清理残留数据卷...\" ;;\n \"install.orphan_volume.cleaning.en\") text=\"Removing leftover data volume...\" ;;\n \"install.orphan_volume.cleaned.zh\") text=\"残留数据卷已清理。继续全新安装...\" ;;\n \"install.orphan_volume.cleaned.en\") text=\"Leftover volume removed. Continuing with fresh installation...\" ;;\n \"install.orphan_volume.keeping.zh\") text=\"保留数据卷,继续安装。如遇异常请选择全新重装。\" ;;\n \"install.orphan_volume.keeping.en\") text=\"Keeping existing volume. If you encounter issues, consider a clean reinstall.\" ;;\n \"install.orphan_volume.clean_noninteractive.zh\") text=\"非交互模式: 自动清理残留数据卷...\" ;;\n \"install.orphan_volume.clean_noninteractive.en\") text=\"Non-interactive mode: automatically removing leftover volume...\" ;;\n # --- Loading existing config ---\n \"install.loading_config.zh\") text=\"从 %s 加载已有配置(shell 环境变量优先)...\" ;;\n \"install.loading_config.en\") text=\"Loading existing config from %s (shell env vars take priority)...\" ;;\n # --- LLM Configuration ---\n \"llm.title.zh\") text=\"--- LLM 配置 ---\" ;;\n \"llm.title.en\") text=\"--- LLM Configuration ---\" ;;\n \"llm.provider.label.zh\") text=\" 提供商: %s\" ;;\n \"llm.provider.label.en\") text=\" Provider: %s\" ;;\n \"llm.model.label.zh\") text=\" 模型: %s\" ;;\n \"llm.model.label.en\") text=\" Model: %s\" ;;\n \"llm.provider.qwen.zh\") text=\" 提供商: qwen(阿里云百炼)\" ;;\n \"llm.provider.qwen.en\") text=\" Provider: qwen (Alibaba Cloud Bailian)\" ;;\n \"llm.provider.qwen_default.zh\") text=\" 提供商: %s(默认)\" ;;\n \"llm.provider.qwen_default.en\") text=\" Provider: %s (default)\" ;;\n \"llm.model.default.zh\") text=\" 模型: %s(默认)\" ;;\n \"llm.model.default.en\") text=\" Model: %s (default)\" ;;\n \"llm.apikey_hint_bailian.zh\") text=\" 💡 获取阿里云百炼(DashScope)API Key:\" ;;\n \"llm.apikey_hint_bailian.en\") text=\" 💡 Get your Alibaba Cloud Bailian (DashScope) API Key:\" ;;\n \"llm.apikey_url_bailian.zh\") text=\" https://www.aliyun.com/product/bailian\" ;;\n \"llm.apikey_url_bailian.en\") text=\" https://www.aliyun.com/product/bailian\" ;;\n \"llm.apikey_hint_qwencloud.zh\") text=\" 💡 从 Qwen Cloud(国际站)获取 DASHSCOPE_API_KEY:\" ;;\n \"llm.apikey_hint_qwencloud.en\") text=\" 💡 Get your DASHSCOPE_API_KEY for Qwen Cloud (international) from:\" ;;\n \"llm.apikey_url_qwencloud.zh\") text=\" https://home.qwencloud.com/api-keys (文档: https://docs.qwencloud.com/)\" ;;\n \"llm.apikey_url_qwencloud.en\") text=\" https://home.qwencloud.com/api-keys | Docs: https://docs.qwencloud.com/\" ;;\n \"llm.apikey_hint_tokenplan.zh\") text=\" 💡 获取 DashScope API Key 或开通通义 Token 套餐,请参考:\" ;;\n \"llm.apikey_hint_tokenplan.en\") text=\" 💡 Get your DashScope or Token Plan API key (Alibaba Model Studio):\" ;;\n \"llm.apikey_url_tokenplan.zh\") text=\" https://help.aliyun.com/zh/model-studio/token-plan-quickstart\" ;;\n \"llm.apikey_url_tokenplan.en\") text=\" https://common-buy.aliyun.com/token-plan/ | https://help.aliyun.com/zh/model-studio/token-plan-quickstart\" ;;\n \"llm.apikey_hint_codingplan.zh\") text=\" 💡 获取 DashScope API Key(Coding 套餐 / coding.dashscope 接口):\" ;;\n \"llm.apikey_hint_codingplan.en\") text=\" 💡 Get your DashScope API key for Coding Plan (coding.dashscope endpoint):\" ;;\n \"llm.apikey_url_codingplan.zh\") text=\" https://help.aliyun.com/zh/model-studio/get-api-key\" ;;\n \"llm.apikey_url_codingplan.en\") text=\" https://help.aliyun.com/zh/model-studio/get-api-key\" ;;\n \"llm.apikey_prompt.zh\") text=\"LLM API Key\" ;;\n \"llm.apikey_prompt.en\") text=\"LLM API Key\" ;;\n \"llm.providers_title.zh\") text=\"可用 LLM 提供商:\" ;;\n \"llm.providers_title.en\") text=\"Available LLM Providers:\" ;;\n \"llm.provider.alibaba.zh\") text=\" 1) 阿里云通义 Token 套餐 - 推荐中国用户使用\" ;;\n \"llm.provider.alibaba.en\") text=\" 1) Qwen Cloud - International (OpenAI-compatible API, recommended)\" ;;\n \"llm.provider.openai_compat.zh\") text=\" 2) OpenAI 兼容 API - 自定义 Base URL(OpenAI、DeepSeek 等)\" ;;\n \"llm.provider.openai_compat.en\") text=\" 2) OpenAI-compatible API - Custom Base URL (OpenAI, DeepSeek, etc.)\" ;;\n \"llm.provider.select.zh\") text=\"选择提供商 [1/2]\" ;;\n \"llm.provider.select.en\") text=\"Select provider [1/2]\" ;;\n \"llm.alibaba.models_title.zh\") text=\"选择阿里云模型接入方式:\" ;;\n \"llm.alibaba.models_title.en\") text=\"Select Alibaba Cloud model access:\" ;;\n \"llm.alibaba.model.tokenplan.zh\") text=\" 1) 阿里云通义 Token 套餐 - 兼容模式(推荐)\" ;;\n \"llm.alibaba.model.tokenplan.en\") text=\" 1) Alibaba Cloud Token Plan - compatible-mode (recommended)\" ;;\n \"llm.alibaba.model.bailian.zh\") text=\" 2) 阿里云百炼 - DashScope 通用兼容接口\" ;;\n \"llm.alibaba.model.bailian.en\") text=\" 2) Alibaba Cloud Bailian - DashScope compatible mode\" ;;\n \"llm.alibaba.model.codingplan_legacy.zh\") text=\" 3) 阿里云 Coding 套餐 - 旧版端点(兼容保留)\" ;;\n \"llm.alibaba.model.codingplan_legacy.en\") text=\" 3) Alibaba Cloud Coding Plan - legacy endpoint (backward compatible)\" ;;\n \"llm.alibaba.model.select.zh\") text=\"选择接入方式 [1/2/3]\" ;;\n \"llm.alibaba.model.select.en\") text=\"Select access option [1/2/3]\" ;;\n \"llm.alibaba.model.invalid.zh\") text=\"无效选择: %s(请输入 1、2 或 3)\" ;;\n \"llm.alibaba.model.invalid.en\") text=\"Invalid choice: %s (please enter 1, 2, or 3)\" ;;\n \"llm.codingplan.models_title.zh\") text=\"选择通义 Token 套餐默认模型:\" ;;\n \"llm.codingplan.models_title.en\") text=\"Select Qwen Cloud default model:\" ;;\n \"llm.codingplan.model.qwen36plus.zh\") text=\" 1) qwen3.6-plus - 千问 3.6(推荐)\" ;;\n \"llm.codingplan.model.qwen36plus.en\") text=\" 1) qwen3.6-plus - Qwen 3.6 (recommended)\" ;;\n \"llm.codingplan.model.glm5.zh\") text=\" 2) glm-5 - 智谱 GLM-5(编程推荐)\" ;;\n \"llm.codingplan.model.glm5.en\") text=\" 2) glm-5 - Zhipu GLM-5 (recommended for coding)\" ;;\n \"llm.codingplan.model.kimi.zh\") text=\" 3) kimi-k2.5 - Moonshot Kimi K2.5\" ;;\n \"llm.codingplan.model.kimi.en\") text=\" 3) kimi-k2.5 - Moonshot Kimi K2.5\" ;;\n \"llm.codingplan.model.minimax.zh\") text=\" 4) MiniMax-M2.5 - MiniMax M2.5\" ;;\n \"llm.codingplan.model.minimax.en\") text=\" 4) MiniMax-M2.5 - MiniMax M2.5\" ;;\n \"llm.codingplan.model.select.zh\") text=\"选择模型 [1/2/3/4]\" ;;\n \"llm.codingplan.model.select.en\") text=\"Select model [1/2/3/4]\" ;;\n \"llm.provider.selected_tokenplan.zh\") text=\" 提供商: 阿里云通义 Token 套餐(兼容模式)\" ;;\n \"llm.provider.selected_tokenplan.en\") text=\" Provider: Alibaba Cloud Token Plan (compatible mode)\" ;;\n \"llm.provider.selected_codingplan.zh\") text=\" 提供商: 阿里云通义 Token 套餐(alibaba-cloud)\" ;;\n \"llm.provider.selected_codingplan.en\") text=\" Provider: Qwen Cloud (international) (alibaba-cloud)\" ;;\n \"llm.provider.selected_codingplan_legacy.zh\") text=\" 提供商: 阿里云 Coding 套餐(coding.dashscope)\" ;;\n \"llm.provider.selected_codingplan_legacy.en\") text=\" Provider: Alibaba Cloud Coding Plan (coding.dashscope)\" ;;\n \"llm.provider.selected_qwen.zh\") text=\" 提供商: 阿里云百炼\" ;;\n \"llm.provider.selected_qwen.en\") text=\" Provider: Alibaba Cloud Bailian\" ;;\n \"llm.provider.selected_openai.zh\") text=\" 提供商: %s(OpenAI 兼容)\" ;;\n \"llm.provider.selected_openai.en\") text=\" Provider: %s (OpenAI-compatible)\" ;;\n \"llm.provider.invalid.zh\") text=\"无效选择: %s(请输入 1 或 2)\" ;;\n \"llm.provider.invalid.en\") text=\"Invalid choice: %s (please enter 1 or 2)\" ;;\n \"llm.qwen.model_prompt.zh\") text=\"默认模型 ID\" ;;\n \"llm.qwen.model_prompt.en\") text=\"Default Model ID\" ;;\n \"llm.openai.base_url_prompt.zh\") text=\"Base URL\" ;;\n \"llm.openai.base_url_prompt.en\") text=\"Base URL\" ;;\n \"llm.openai.model_prompt.zh\") text=\"默认模型 ID\" ;;\n \"llm.openai.model_prompt.en\") text=\"Default Model ID\" ;;\n \"llm.openai.base_url_label.zh\") text=\" Base URL: %s\" ;;\n \"llm.openai.base_url_label.en\") text=\" Base URL: %s\" ;;\n # --- Custom model parameters ---\n \"llm.custom_model.detected.zh\") text=\" ⚠️ 模型 '%s' 不在内置模型列表中,请配置模型参数:\" ;;\n \"llm.custom_model.detected.en\") text=\" ⚠️ Model '%s' is not in the built-in model list. Please configure model parameters:\" ;;\n \"llm.custom_model.context_prompt.zh\") text=\"最大上下文长度(token 数)[150000]\" ;;\n \"llm.custom_model.context_prompt.en\") text=\"Max context window (tokens) [150000]\" ;;\n \"llm.custom_model.max_tokens_prompt.zh\") text=\"最大输出长度(token 数)[128000]\" ;;\n \"llm.custom_model.max_tokens_prompt.en\") text=\"Max output tokens [128000]\" ;;\n \"llm.custom_model.reasoning_prompt.zh\") text=\"是否支持推理/思考模式?[Y/n]\" ;;\n \"llm.custom_model.reasoning_prompt.en\") text=\"Does it support reasoning/thinking mode? [Y/n]\" ;;\n \"llm.custom_model.vision_prompt.zh\") text=\"是否支持图片输入?[y/N]\" ;;\n \"llm.custom_model.vision_prompt.en\") text=\"Does it support image input? [y/N]\" ;;\n \"llm.custom_model.summary.zh\") text=\" 自定义模型参数: 上下文=%s, 最大输出=%s, 推理=%s, 图片=%s\" ;;\n \"llm.custom_model.summary.en\") text=\" Custom model params: context=%s, maxTokens=%s, reasoning=%s, vision=%s\" ;;\n # --- Admin Credentials ---\n \"admin.title.zh\") text=\"--- 管理员凭据 ---\" ;;\n \"admin.title.en\") text=\"--- Admin Credentials ---\" ;;\n \"admin.username_prompt.zh\") text=\"管理员用户名\" ;;\n \"admin.username_prompt.en\") text=\"Admin Username\" ;;\n \"admin.password_prompt.zh\") text=\"管理员密码(留空自动生成,最少 8 位)\" ;;\n \"admin.password_prompt.en\") text=\"Admin Password (leave empty to auto-generate, min 8 chars)\" ;;\n \"admin.password_generated.zh\") text=\" 已自动生成管理员密码\" ;;\n \"admin.password_generated.en\") text=\" Auto-generated admin password\" ;;\n \"admin.password_too_short.zh\") text=\"管理员密码至少需要 8 个字符(MinIO 要求)。当前长度: %s\" ;;\n \"admin.password_too_short.en\") text=\"Admin password must be at least 8 characters (MinIO requirement). Current length: %s\" ;;\n # --- Port Configuration ---\n \"port.title.zh\") text=\"--- 端口配置(按回车使用默认值)---\" ;;\n \"port.title.en\") text=\"--- Port Configuration (press Enter for defaults) ---\" ;;\n \"port.gateway_prompt.zh\") text=\"网关主机端口(容器内 8080)\" ;;\n \"port.gateway_prompt.en\") text=\"Host port for gateway (8080 inside container)\" ;;\n \"port.console_prompt.zh\") text=\"Higress 控制台主机端口(容器内 8001)\" ;;\n \"port.console_prompt.en\") text=\"Host port for Higress console (8001 inside container)\" ;;\n \"port.element_prompt.zh\") text=\"Element Web 直接访问主机端口(容器内 8088)\" ;;\n \"port.element_prompt.en\") text=\"Host port for Element Web direct access (8088 inside container)\" ;;\n \"port.manager_console_prompt.zh\") text=\"Manager 控制台主机端口(容器内 18888)\" ;;\n \"port.manager_console_prompt.en\") text=\"Host port for Manager console (18888 inside container)\" ;;\n \"port.copaw_app_prompt.zh\") text=\"QwenPaw App API 主机端口(容器内 18799)\" ;;\n \"port.copaw_app_prompt.en\") text=\"Host port for QwenPaw App API (18799 inside container)\" ;;\n # --- Local-only binding ---\n \"port.local_only.title.zh\") text=\"--- 网络访问模式 ---\" ;;\n \"port.local_only.title.en\") text=\"--- Network Access Mode ---\" ;;\n \"port.local_only.prompt.zh\") text=\"是否仅允许本机访问(端口绑定到 127.0.0.1)?\" ;;\n \"port.local_only.prompt.en\") text=\"Bind ports to localhost only (127.0.0.1)?\" ;;\n \"port.local_only.hint_yes.zh\") text=\" 仅本机使用,无需开放外部端口(推荐)\" ;;\n \"port.local_only.hint_yes.en\") text=\" Local use only, no external port exposure (recommended)\" ;;\n \"port.local_only.hint_no.zh\") text=\" 允许外部访问(局域网 / 公网)\" ;;\n \"port.local_only.hint_no.en\") text=\" Allow external access (LAN / public network)\" ;;\n \"port.local_only.choice.zh\") text=\"请选择 [1/2]\" ;;\n \"port.local_only.choice.en\") text=\"Enter choice [1/2]\" ;;\n \"port.local_only.selected_local.zh\") text=\"端口已绑定到 127.0.0.1(仅本机访问)\" ;;\n \"port.local_only.selected_local.en\") text=\"Ports bound to 127.0.0.1 (localhost only)\" ;;\n \"port.local_only.selected_external.zh\") text=\"端口已绑定到所有网络接口(0.0.0.0)\" ;;\n \"port.local_only.selected_external.en\") text=\"Ports bound to all interfaces (0.0.0.0)\" ;;\n \"port.local_only.https_hint.zh\") text=\"⚠️ 建议在 Higress 控制台配置 TLS 证书并启用 HTTPS,避免明文传输。\" ;;\n \"port.local_only.https_hint.en\") text=\"⚠️ It is recommended to configure TLS certificates and enable HTTPS in the Higress Console to avoid plaintext transmission.\" ;;\n \"port.local_only.https_docs.zh\") text=\"\" ;;\n \"port.local_only.https_docs.en\") text=\"\" ;;\n # --- Domain Configuration ---\n \"domain.title.zh\") text=\"--- 域名配置(按回车使用默认值)---\" ;;\n \"domain.title.en\") text=\"--- Domain Configuration (press Enter for defaults) ---\" ;;\n \"domain.hint.zh\") text=\"提示: 自定义域名前必须事先做好 DNS 解析。单机 ECS 部署时无需修改 aigw、fs 等域名;Element Web 和 Matrix Server 也可通过 IP 直接访问。\" ;;\n \"domain.hint.en\") text=\"Hint: Configure DNS resolution before customizing domains. For single ECS deployment, no need to change aigw, fs, etc.; Element Web and Matrix Server can also be accessed directly via IP.\" ;;\n \"domain.matrix_prompt.zh\") text=\"Matrix 域名\" ;;\n \"domain.matrix_prompt.en\") text=\"Matrix Domain\" ;;\n \"domain.element_prompt.zh\") text=\"Element Web 域名\" ;;\n \"domain.element_prompt.en\") text=\"Element Web Domain\" ;;\n \"domain.gateway_prompt.zh\") text=\"AI 网关域名\" ;;\n \"domain.gateway_prompt.en\") text=\"AI Gateway Domain\" ;;\n \"domain.fs_prompt.zh\") text=\"文件系统域名\" ;;\n \"domain.fs_prompt.en\") text=\"File System Domain\" ;;\n \"domain.console_prompt.zh\") text=\"Manager 控制台域名\" ;;\n \"domain.console_prompt.en\") text=\"Manager Console Domain\" ;;\n # --- GitHub Integration ---\n \"github.title.zh\") text=\"--- GitHub 集成(可选,按回车跳过)---\" ;;\n \"github.title.en\") text=\"--- GitHub Integration (optional, press Enter to skip) ---\" ;;\n \"github.token_prompt.zh\") text=\"GitHub 个人访问令牌(可选)\" ;;\n \"github.token_prompt.en\") text=\"GitHub Personal Access Token (optional)\" ;;\n # --- Skills Registry ---\n \"skills.title.zh\") text=\"--- Skills 注册中心(可选,按回车使用默认 nacos://market.agentteams.io:80/public)---\" ;;\n \"skills.title.en\") text=\"--- Skills Registry (optional, press Enter for default nacos://market.agentteams.io:80/public) ---\" ;;\n \"skills.url_prompt.zh\") text=\"Skills 注册中心 URL(留空使用默认 nacos://market.agentteams.io:80/public)\" ;;\n \"skills.url_prompt.en\") text=\"Skills Registry URL (leave empty for default nacos://market.agentteams.io:80/public)\" ;;\n # --- Data Persistence ---\n \"data.title.zh\") text=\"--- 数据持久化 ---\" ;;\n \"data.title.en\") text=\"--- Data Persistence ---\" ;;\n \"data.volume_prompt.zh\") text=\"Docker 卷名称 [agentteams-data]\" ;;\n \"data.volume_prompt.en\") text=\"Docker volume name for persistent data [agentteams-data]\" ;;\n \"data.volume_using.zh\") text=\" 使用 Docker 卷: %s\" ;;\n \"data.volume_using.en\") text=\" Using Docker volume: %s\" ;;\n # --- Manager Workspace ---\n \"workspace.title.zh\") text=\"--- Manager 工作空间 ---\" ;;\n \"workspace.title.en\") text=\"--- Manager Workspace ---\" ;;\n \"workspace.dir_prompt.zh\") text=\"Manager 工作空间目录 [%s]\" ;;\n \"workspace.dir_prompt.en\") text=\"Manager workspace directory [%s]\" ;;\n \"workspace.dir_label.zh\") text=\" Manager 工作空间: %s\" ;;\n \"workspace.dir_label.en\") text=\" Manager workspace: %s\" ;;\n # --- Host directory sharing ---\n \"host_share.prompt.zh\") text=\"与 Agent 共享的主机目录(默认: %s)\" ;;\n \"host_share.prompt.en\") text=\"Host directory to share with agents (default: %s)\" ;;\n \"host_share.sharing.zh\") text=\"共享主机目录: %s -> 容器内 /host-share\" ;;\n \"host_share.sharing.en\") text=\"Sharing host directory: %s -> /host-share in container\" ;;\n \"host_share.not_exist.zh\") text=\"警告: 主机目录 %s 不存在,跳过验证继续使用\" ;;\n \"host_share.not_exist.en\") text=\"WARNING: Host directory %s does not exist, using without validation\" ;;\n # --- Default worker runtime ---\n \"worker_runtime.title.zh\") text=\"--- 默认 Worker 运行时 ---\" ;;\n \"worker_runtime.title.en\") text=\"--- Default Worker Runtime ---\" ;;\n \"worker_runtime.openclaw.zh\") text=\"OpenClaw\" ;;\n \"worker_runtime.openclaw.en\") text=\"OpenClaw\" ;;\n \"worker_runtime.copaw.zh\") text=\"QwenPaw\" ;;\n \"worker_runtime.copaw.en\") text=\"QwenPaw\" ;;\n \"worker_runtime.hermes.zh\") text=\"Hermes\" ;;\n \"worker_runtime.hermes.en\") text=\"Hermes\" ;;\n \"worker_runtime.choice.zh\") text=\"请选择 [1/2/3]\" ;;\n \"worker_runtime.choice.en\") text=\"Enter choice [1/2/3]\" ;;\n \"worker_runtime.selected.zh\") text=\"默认 Worker 运行时: %s\" ;;\n \"worker_runtime.selected.en\") text=\"Default Worker runtime: %s\" ;;\n \"worker_runtime.title_short.zh\") text=\"默认 Worker 运行时\" ;;\n \"worker_runtime.title_short.en\") text=\"Default Worker Runtime\" ;;\n \"manager_runtime.title.zh\") text=\"--- Manager 运行时 ---\" ;;\n \"manager_runtime.title.en\") text=\"--- Manager Runtime ---\" ;;\n \"manager_runtime.openclaw.zh\") text=\"OpenClaw\" ;;\n \"manager_runtime.openclaw.en\") text=\"OpenClaw\" ;;\n \"manager_runtime.copaw.zh\") text=\"QwenPaw\" ;;\n \"manager_runtime.copaw.en\") text=\"QwenPaw\" ;;\n \"manager_runtime.choice.zh\") text=\"请选择 [1/2]\" ;;\n \"manager_runtime.choice.en\") text=\"Enter choice [1/2]\" ;;\n \"manager_runtime.selected.zh\") text=\"Manager 运行时: %s\" ;;\n \"manager_runtime.selected.en\") text=\"Manager runtime: %s\" ;;\n \"manager_runtime.title_short.zh\") text=\"Manager 运行时\" ;;\n \"manager_runtime.title_short.en\") text=\"Manager Runtime\" ;;\n # --- Secrets and config ---\n \"install.generating_secrets.zh\") text=\"正在生成密钥...\" ;;\n \"install.generating_secrets.en\") text=\"Generating secrets...\" ;;\n \"install.config_saved.zh\") text=\"配置已保存到 %s\" ;;\n \"install.config_saved.en\") text=\"Configuration saved to %s\" ;;\n # --- Container runtime socket ---\n \"install.socket_detected.zh\") text=\"容器运行时 socket: %s(已启用直接创建 Worker)\" ;;\n \"install.socket_detected.en\") text=\"Container runtime socket: %s (direct Worker creation enabled)\" ;;\n \"install.socket_not_found.zh\") text=\"未找到容器运行时 socket(Manager 无法直接创建 Worker 容器,需要你手动执行 docker 命令创建)\" ;;\n \"install.socket_not_found.en\") text=\"No container runtime socket found (Manager cannot create Worker containers directly, you will need to create them manually using docker commands)\" ;;\n \"install.socket_confirm.title.zh\") text=\"⚠️ 未检测到容器运行时 Socket\" ;;\n \"install.socket_confirm.title.en\") text=\"⚠️ Container Runtime Socket Not Detected\" ;;\n \"install.socket_confirm.message.zh\") text=\"未找到 Docker/Podman socket,Manager 将无法自动创建 Worker 容器。\\n你需要手动执行 docker run 命令来创建 Worker。\\n\\n是否继续安装?\" ;;\n \"install.socket_confirm.message.en\") text=\"Docker/Podman socket not found. Manager will not be able to create Worker containers automatically.\\nYou will need to manually run docker commands to create Workers.\\n\\nContinue installation?\" ;;\n \"install.socket_confirm.prompt.zh\") text=\"继续安装? [y/N]: \" ;;\n \"install.socket_confirm.prompt.en\") text=\"Continue? [y/N]: \" ;;\n \"install.socket_confirm.cancelled.zh\") text=\"安装已取消。如需启用 Worker 自动创建,请确保 Docker/Podman 正在运行,然后重新运行安装脚本。\" ;;\n \"install.socket_confirm.cancelled.en\") text=\"Installation cancelled. To enable automatic Worker creation, ensure Docker/Podman is running and re-run the installer.\" ;;\n # --- Container management ---\n \"install.removing_existing.zh\") text=\"正在移除现有 agentteams-manager 容器...\" ;;\n \"install.removing_existing.en\") text=\"Removing existing agentteams-manager container...\" ;;\n # --- Matrix E2EE ---\n \"matrix_e2ee.title.zh\") text=\"--- Matrix 端到端加密(E2EE)---\" ;;\n \"matrix_e2ee.title.en\") text=\"--- Matrix End-to-End Encryption (E2EE) ---\" ;;\n \"matrix_e2ee.desc.zh\") text=\"E2EE 会对 Manager 与 Worker 之间的 Matrix 消息进行端到端加密。\\n 启用后,即使 Matrix 服务器被入侵,消息内容也无法被窃取。\\n 但 E2EE 会增加首次握手耗时,且要求所有 Agent 都支持 matrix-sdk-crypto。\\n 如果不确定,建议保持禁用。\\n ⚠ 注意:禁用 E2EE 后,请勿在 Element 上创建默认启用加密的 Private 房间,\\n 否则 Agent 将无法读取该房间中的加密消息。请改用 Public 房间或关闭房间加密。\" ;;\n \"matrix_e2ee.desc.en\") text=\"E2EE encrypts Matrix messages between Manager and Workers end-to-end.\\n When enabled, message content stays private even if the Matrix server is compromised.\\n However, E2EE adds overhead to the initial handshake and requires all Agents\\n to support matrix-sdk-crypto. If unsure, keep it disabled.\\n ⚠ Note: When E2EE is disabled, do NOT create Private rooms in Element (which\\n enable encryption by default) — Agents cannot read encrypted messages without\\n E2EE support. Use Public rooms or turn off room encryption instead.\" ;;\n \"matrix_e2ee.enable.zh\") text=\"启用 E2EE\" ;;\n \"matrix_e2ee.enable.en\") text=\"Enable E2EE\" ;;\n \"matrix_e2ee.disable.zh\") text=\"禁用 E2EE(推荐)\" ;;\n \"matrix_e2ee.disable.en\") text=\"Disable E2EE (recommended)\" ;;\n \"matrix_e2ee.choice.zh\") text=\"请选择 [1/2]\" ;;\n \"matrix_e2ee.choice.en\") text=\"Enter choice [1/2]\" ;;\n \"matrix_e2ee.selected_enabled.zh\") text=\"Matrix E2EE: 已启用\" ;;\n \"matrix_e2ee.selected_enabled.en\") text=\"Matrix E2EE: enabled\" ;;\n \"matrix_e2ee.selected_disabled.zh\") text=\"Matrix E2EE: 已禁用(默认)\" ;;\n \"matrix_e2ee.selected_disabled.en\") text=\"Matrix E2EE: disabled (default)\" ;;\n \"matrix_e2ee.title_short.zh\") text=\"Matrix E2EE\" ;;\n \"matrix_e2ee.title_short.en\") text=\"Matrix E2EE\" ;;\n \"matrix_e2ee.val_enabled.zh\") text=\"已启用\" ;;\n \"matrix_e2ee.val_enabled.en\") text=\"enabled\" ;;\n \"matrix_e2ee.val_disabled.zh\") text=\"已禁用\" ;;\n \"matrix_e2ee.val_disabled.en\") text=\"disabled\" ;;\n # --- Docker API proxy ---\n \"docker_proxy.title.zh\") text=\"--- Docker API 安全代理 ---\" ;;\n \"docker_proxy.title.en\") text=\"--- Docker API Security Proxy ---\" ;;\n \"docker_proxy.desc.zh\") text=\"Docker API 代理可防止 AI Agent 通过 Docker API 越狱访问宿主机。\\n 启用后,Manager 不再直接持有 Docker socket,所有容器操作经过安全校验。\" ;;\n \"docker_proxy.desc.en\") text=\"Docker API proxy prevents AI Agents from escaping via Docker API to access the host.\\n When enabled, Manager no longer has direct Docker socket access; all container operations go through security validation.\" ;;\n \"docker_proxy.enable.zh\") text=\"启用(推荐)\" ;;\n \"docker_proxy.enable.en\") text=\"Enable (recommended)\" ;;\n \"docker_proxy.disable.zh\") text=\"禁用(直接挂载 Docker socket)\" ;;\n \"docker_proxy.disable.en\") text=\"Disable (mount Docker socket directly)\" ;;\n \"docker_proxy.choice.zh\") text=\"请选择 [1/2]\" ;;\n \"docker_proxy.choice.en\") text=\"Enter choice [1/2]\" ;;\n \"docker_proxy.selected_enabled.zh\") text=\"Docker API 代理: 已启用\" ;;\n \"docker_proxy.selected_enabled.en\") text=\"Docker API proxy: enabled\" ;;\n \"docker_proxy.selected_disabled.zh\") text=\"Docker API 代理: 已禁用\" ;;\n \"docker_proxy.selected_disabled.en\") text=\"Docker API proxy: disabled\" ;;\n \"docker_proxy.title_short.zh\") text=\"Docker API 代理\" ;;\n \"docker_proxy.title_short.en\") text=\"Docker API Proxy\" ;;\n \"docker_proxy.val_enabled.zh\") text=\"已启用\" ;;\n \"docker_proxy.val_enabled.en\") text=\"enabled\" ;;\n \"docker_proxy.val_disabled.zh\") text=\"已禁用\" ;;\n \"docker_proxy.val_disabled.en\") text=\"disabled\" ;;\n \"docker_proxy.registries_desc.zh\") text=\"默认放行的镜像来源:本地镜像、localhost、Higress 仓库(所有 region)。\\n 如需放行其他镜像仓库,请输入逗号分隔的地址前缀。\\n 示例: ghcr.io/myorg,registry.example.com/team\" ;;\n \"docker_proxy.registries_desc.en\") text=\"Default allowed image sources: local images, localhost, Higress registries (all regions).\\n To allow additional image sources, enter comma-separated address prefixes.\\n Example: ghcr.io/myorg,registry.example.com/team\" ;;\n \"docker_proxy.registries_prompt.zh\") text=\"额外放行的镜像来源(按回车跳过)\" ;;\n \"docker_proxy.registries_prompt.en\") text=\"Additional allowed image sources (press Enter to skip)\" ;;\n \"docker_proxy.registries_label.zh\") text=\"额外放行的镜像来源\" ;;\n \"docker_proxy.registries_label.en\") text=\"Additional allowed image sources\" ;;\n # --- Podman Autostart ---\n \"podman.autostart.title.zh\") text=\"--- Podman 驻留与开机自启 ---\" ;;\n \"podman.autostart.title.en\") text=\"--- Podman Linger & Autostart ---\" ;;\n \"podman.autostart.desc.zh\") text=\"配置为 systemd 服务,支持开机自动启动及后台驻留。\" ;;\n \"podman.autostart.desc.en\") text=\"Configure as a systemd service for automatic startup on boot.\" ;;\n \"podman.autostart.enable.zh\") text=\"启用(推荐)\" ;;\n \"podman.autostart.enable.en\") text=\"Enable (recommended)\" ;;\n \"podman.autostart.disable.zh\") text=\"不启用(容器随当前会话关闭而停止)\" ;;\n \"podman.autostart.disable.en\") text=\"Disable (containers stop when session ends)\" ;;\n \"podman.autostart.choice.zh\") text=\"请选择 [1/2]\" ;;\n \"podman.autostart.choice.en\") text=\"Enter choice [1/2]\" ;;\n \"podman.autostart.selected_enabled.zh\") text=\"Podman 开机自启: 已启用\" ;;\n \"podman.autostart.selected_enabled.en\") text=\"Podman Autostart: enabled\" ;;\n \"podman.autostart.selected_disabled.zh\") text=\"Podman 开机自启: 已禁用\" ;;\n \"podman.autostart.selected_disabled.en\") text=\"Podman Autostart: disabled\" ;;\n \"podman.autostart.title_short.zh\") text=\"Podman 开机自启\" ;;\n \"podman.autostart.title_short.en\") text=\"Podman Autostart\" ;;\n \"podman.autostart.val_enabled.zh\") text=\"已启用\" ;;\n \"podman.autostart.val_enabled.en\") text=\"enabled\" ;;\n \"podman.autostart.val_disabled.zh\") text=\"已禁用\" ;;\n \"podman.autostart.val_disabled.en\") text=\"disabled\" ;;\n \"install.podman.autostart_title.zh\") text=\"正在配置 Podman 容器开机自启...\" ;;\n \"install.podman.autostart_title.en\") text=\"Configuring Podman container autostart...\" ;;\n \"install.podman.linger_enable.zh\") text=\"尝试启用 systemd linger (如有提示请在此输入 sudo 密码)...\" ;;\n \"install.podman.linger_enable.en\") text=\"Attempting to enable systemd linger (enter sudo password if prompted)...\" ;;\n \"install.podman.linger_warn.zh\") text=\"警告: 无法自动启用 linger。驻留可能失败,请手动执行: sudo loginctl enable-linger \\$(whoami)\" ;;\n \"install.podman.linger_warn.en\") text=\"WARNING: Failed to auto-enable linger. Please run manually: sudo loginctl enable-linger \\$(whoami)\" ;;\n \"install.podman.root_setup.zh\") text=\"检测到 root 用户。正在配置系统级 agentteams-podman-restart 服务...\" ;;\n \"install.podman.root_setup.en\") text=\"Root user detected. Configuring system-wide agentteams-podman-restart service...\" ;;\n \"install.podman.root_success.zh\") text=\"✅ 系统级 agentteams-podman-restart 服务已成功启用并运行。\" ;;\n \"install.podman.root_success.en\") text=\"✅ System-wide agentteams-podman-restart service successfully enabled and running.\" ;;\n \"install.podman.root_fail.zh\") text=\"⚠️ 系统级 agentteams-podman-restart 服务启用失败,请稍后手动检查。\" ;;\n \"install.podman.root_fail.en\") text=\"⚠️ Failed to enable system-wide agentteams-podman-restart service. Please check manually later.\" ;;\n \"install.podman.user_setup.zh\") text=\"检测到普通用户 (%s)。正在配置用户级 agentteams-podman-restart 服务...\" ;;\n \"install.podman.user_setup.en\") text=\"Non-root user (%s) detected. Configuring user-level agentteams-podman-restart service...\" ;;\n \"install.podman.user_success.zh\") text=\"✅ 用户级 agentteams-podman-restart 服务已成功启用并运行。\" ;;\n \"install.podman.user_success.en\") text=\"✅ User-level agentteams-podman-restart service successfully enabled and running.\" ;;\n \"install.podman.user_fail.zh\") text=\"⚠️ 用户级 agentteams-podman-restart 服务启用失败。\\n提示:这通常是因为缺少 XDG_RUNTIME_DIR 或 dbus 没有运行。您可以尝试手动执行: systemctl --user enable --now agentteams-podman-restart.service\" ;;\n \"install.podman.user_fail.en\") text=\"⚠️ Failed to enable user-level agentteams-podman-restart service.\\nHint: This is usually due to missing XDG_RUNTIME_DIR or dbus not running. Try manually: systemctl --user enable --now agentteams-podman-restart.service\" ;;\n \"install.podman.success.zh\") text=\"Podman 开机自启配置完成。\" ;;\n \"install.podman.success.en\") text=\"Podman autostart successfully configured.\" ;;\n # --- Worker idle timeout ---\n \"idle_timeout.prompt.zh\") text=\"Worker 空闲自动停止超时(分钟)[720]\" ;;\n \"idle_timeout.prompt.en\") text=\"Worker idle auto-stop timeout in minutes [720]\" ;;\n \"idle_timeout.selected.zh\") text=\"Worker 空闲超时: %s 分钟\" ;;\n \"idle_timeout.selected.en\") text=\"Worker idle timeout: %s minutes\" ;;\n \"idle_timeout.label.zh\") text=\"Worker 空闲超时(分钟)\" ;;\n \"idle_timeout.label.en\") text=\"Worker idle timeout (min)\" ;;\n # --- YOLO mode ---\n \"install.yolo.zh\") text=\"YOLO 模式已启用(自主决策,无交互提示)\" ;;\n \"install.yolo.en\") text=\"YOLO mode enabled (autonomous decisions, no interactive prompts)\" ;;\n # --- Image pulling ---\n \"install.image.exists.zh\") text=\"Manager 镜像已存在: %s\" ;;\n \"install.image.exists.en\") text=\"Manager image already exists locally: %s\" ;;\n \"install.image.pulling_manager.zh\") text=\"正在拉取 Manager 镜像: %s\" ;;\n \"install.image.pulling_manager.en\") text=\"Pulling Manager image: %s\" ;;\n \"install.image.worker_exists.zh\") text=\"Worker 镜像已存在: %s\" ;;\n \"install.image.worker_exists.en\") text=\"Worker image already exists locally: %s\" ;;\n \"install.image.pulling_worker.zh\") text=\"正在拉取 Worker 镜像: %s\" ;;\n \"install.image.pulling_worker.en\") text=\"Pulling Worker image: %s\" ;;\n # --- Starting container ---\n \"install.starting_manager.zh\") text=\"正在启动 Manager 容器...\" ;;\n \"install.starting_manager.en\") text=\"Starting Manager container...\" ;;\n # --- Wait for Manager ready ---\n \"install.wait_ready.zh\") text=\"等待 Manager Agent 就绪(超时: %ss)...\" ;;\n \"install.wait_ready.en\") text=\"Waiting for Manager agent to be ready (timeout: %ss)...\" ;;\n \"install.wait_ready.ok.zh\") text=\"Manager Agent 已就绪!\" ;;\n \"install.wait_ready.ok.en\") text=\"Manager agent is ready!\" ;;\n \"install.wait_ready.waiting.zh\") text=\"等待中... (%ds/%ds)\" ;;\n \"install.wait_ready.waiting.en\") text=\"Waiting... (%ds/%ds)\" ;;\n \"install.wait_ready.timeout.zh\") text=\"Manager Agent 在 %ss 内未就绪。请检查: docker logs %s\" ;;\n \"install.wait_ready.timeout.en\") text=\"Manager agent did not become ready within %ss. Check: docker logs %s\" ;;\n # --- Wait for Matrix ready ---\n \"install.wait_matrix.zh\") text=\"等待 Matrix 服务就绪(超时: %ss)...\" ;;\n \"install.wait_matrix.en\") text=\"Waiting for Matrix server to be ready (timeout: %ss)...\" ;;\n \"install.wait_matrix.ok.zh\") text=\"Matrix 服务已就绪!\" ;;\n \"install.wait_matrix.ok.en\") text=\"Matrix server is ready!\" ;;\n \"install.wait_matrix.waiting.zh\") text=\"等待 Matrix 中... (%ds/%ds)\" ;;\n \"install.wait_matrix.waiting.en\") text=\"Waiting for Matrix... (%ds/%ds)\" ;;\n \"install.wait_matrix.timeout.zh\") text=\"Matrix 服务在 %ss 内未就绪。请检查: docker logs %s\" ;;\n \"install.wait_matrix.timeout.en\") text=\"Matrix server did not become ready within %ss. Check: docker logs %s\" ;;\n # --- OpenAI-compatible connectivity test ---\n \"llm.openai.test.testing.zh\") text=\"正在测试 API 联通性...\" ;;\n \"llm.openai.test.testing.en\") text=\"Testing API connectivity...\" ;;\n \"llm.openai.test.ok.zh\") text=\"✅ API 联通性测试通过\" ;;\n \"llm.openai.test.ok.en\") text=\"✅ API connectivity test passed\" ;;\n \"llm.openai.test.fail.zh\") text=\"⚠️ API 联通性测试失败(HTTP %s)。响应内容:\\n%s\\n请根据以上错误信息联系您的模型服务商解决。\" ;;\n \"llm.openai.test.fail.en\") text=\"⚠️ API connectivity test failed (HTTP %s). Response body:\\n%s\\nPlease contact your model provider to resolve the issue.\" ;;\n \"llm.openai.test.fail.tokenplan.zh\") text=\"⚠️ 提示: 请确认 API Key 有效且已开通通义 Token 套餐。文档: https://help.aliyun.com/zh/model-studio/token-plan-quickstart\" ;;\n \"llm.openai.test.fail.tokenplan.en\") text=\"⚠️ Hint: Verify your Token Plan API key and compatible-mode access. Docs: https://help.aliyun.com/zh/model-studio/token-plan-quickstart\" ;;\n \"llm.openai.test.fail.codingplan.zh\") text=\"⚠️ 提示: 请确认 API Key 有效且已开通通义 Token 套餐。文档: https://help.aliyun.com/zh/model-studio/token-plan-quickstart\" ;;\n \"llm.openai.test.fail.codingplan.en\") text=\"⚠️ Hint: Verify your DASHSCOPE_API_KEY for Qwen Cloud. API keys: https://home.qwencloud.com/api-keys Docs: https://docs.qwencloud.com/\" ;;\n \"llm.openai.test.fail.codingplan_legacy.zh\") text=\"⚠️ 提示: 请确认 API Key 有效且 Coding 套餐接口可用。文档: https://help.aliyun.com/zh/model-studio/get-api-key\" ;;\n \"llm.openai.test.fail.codingplan_legacy.en\") text=\"⚠️ Hint: Verify your DashScope API key and Coding Plan access. Docs: https://help.aliyun.com/zh/model-studio/get-api-key\" ;;\n \"llm.openai.test.no_curl.zh\") text=\"⚠️ 未找到 curl,跳过 API 联通性测试\" ;;\n \"llm.openai.test.no_curl.en\") text=\"⚠️ curl not found, skipping API connectivity test\" ;;\n \"llm.openai.test.confirm.zh\") text=\"是否仍要继续安装?[y/N/b] \" ;;\n \"llm.openai.test.confirm.en\") text=\"Continue with installation anyway? [y/N/b] \" ;;\n \"llm.embedding.title.zh\") text=\"📦 记忆搜索配置\" ;;\n \"llm.embedding.title.en\") text=\"📦 Memory Search Configuration\" ;;\n \"llm.embedding.hint.zh\") text=\" Embedding 模型可提升记忆搜索质量(语义匹配)。不启用也可正常使用记忆功能(关键词匹配)。\" ;;\n \"llm.embedding.hint.en\") text=\" Embedding model improves memory search quality (semantic matching). Memory still works without it (keyword matching).\" ;;\n \"llm.embedding.option.default.zh\") text=\" 1) text-embedding-v4(推荐)\" ;;\n \"llm.embedding.option.default.en\") text=\" 1) text-embedding-v4 (Recommended)\" ;;\n \"llm.embedding.option.custom.zh\") text=\" 2) 自定义 Embedding 模型\" ;;\n \"llm.embedding.option.custom.en\") text=\" 2) Custom embedding model\" ;;\n \"llm.embedding.option.disable.zh\") text=\" 3) 不启用\" ;;\n \"llm.embedding.option.disable.en\") text=\" 3) Do not enable\" ;;\n \"llm.embedding.select.zh\") text=\"选择\" ;;\n \"llm.embedding.select.en\") text=\"Select\" ;;\n \"llm.embedding.custom_prompt.zh\") text=\" Embedding 模型名称\" ;;\n \"llm.embedding.custom_prompt.en\") text=\" Embedding model name\" ;;\n \"llm.embedding.test.testing.zh\") text=\"正在测试 Embedding API 联通性...\" ;;\n \"llm.embedding.test.testing.en\") text=\"Testing Embedding API connectivity...\" ;;\n \"llm.embedding.test.ok.zh\") text=\"✅ Embedding API 联通性测试通过\" ;;\n \"llm.embedding.test.ok.en\") text=\"✅ Embedding API connectivity test passed\" ;;\n \"llm.embedding.test.fail.zh\") text=\"⚠️ Embedding API 测试失败(HTTP %s)。响应: %s\" ;;\n \"llm.embedding.test.fail.en\") text=\"⚠️ Embedding API test failed (HTTP %s). Response: %s\" ;;\n \"llm.embedding.auto_disabled.zh\") text=\"⚠️ Embedding 已自动禁用,记忆搜索将使用关键词匹配。您可以稍后在 agentteams-manager.env 中设置 AGENTTEAMS_EMBEDDING_MODEL 启用。\" ;;\n \"llm.embedding.auto_disabled.en\") text=\"⚠️ Embedding auto-disabled. Memory search will use keyword matching. You can enable it later in agentteams-manager.env by setting AGENTTEAMS_EMBEDDING_MODEL.\" ;;\n \"llm.embedding.disabled.zh\") text=\"ℹ️ Embedding 已禁用,记忆搜索将使用关键词匹配。\" ;;\n \"llm.embedding.disabled.en\") text=\"ℹ️ Embedding disabled. Memory search will use keyword matching.\" ;;\n \"llm.openai.test.aborted.zh\") text=\"安装已中止。\" ;;\n \"llm.openai.test.aborted.en\") text=\"Installation aborted.\" ;;\n \"nav.back_hint.zh\") text=\"(输入 b 返回上一步)\" ;;\n \"nav.back_hint.en\") text=\"(enter b to go back)\" ;;\n # --- OpenAI-compatible provider creation ---\n \"install.openai_compat.missing.zh\") text=\"警告: OpenAI Base URL 或 API Key 未设置,跳过提供商创建\" ;;\n \"install.openai_compat.missing.en\") text=\"WARNING: OpenAI Base URL or API Key not set, skipping provider creation\" ;;\n \"install.openai_compat.creating.zh\") text=\"正在创建 OpenAI 兼容提供商...\" ;;\n \"install.openai_compat.creating.en\") text=\"Creating OpenAI-compatible provider...\" ;;\n \"install.openai_compat.domain.zh\") text=\" 域名: %s\" ;;\n \"install.openai_compat.domain.en\") text=\" Domain: %s\" ;;\n \"install.openai_compat.port.zh\") text=\" 端口: %s\" ;;\n \"install.openai_compat.port.en\") text=\" Port: %s\" ;;\n \"install.openai_compat.protocol.zh\") text=\" 协议: %s\" ;;\n \"install.openai_compat.protocol.en\") text=\" Protocol: %s\" ;;\n \"install.openai_compat.service_fail.zh\") text=\"警告: 创建 DNS 服务源失败(可能已存在)\" ;;\n \"install.openai_compat.service_fail.en\") text=\"WARNING: Failed to create DNS service source (may already exist)\" ;;\n \"install.openai_compat.provider_fail.zh\") text=\"警告: 创建 AI 提供商失败(可能已存在)\" ;;\n \"install.openai_compat.provider_fail.en\") text=\"WARNING: Failed to create AI provider (may already exist)\" ;;\n \"install.openai_compat.success.zh\") text=\"OpenAI 兼容提供商创建成功\" ;;\n \"install.openai_compat.success.en\") text=\"OpenAI-compatible provider created successfully\" ;;\n # --- Welcome message ---\n \"install.welcome_msg.soul_configured.zh\") text=\"Soul 已配置(找到 soul-configured 标记),跳过 onboarding 消息\" ;;\n \"install.welcome_msg.soul_configured.en\") text=\"Soul already configured (soul-configured marker found), skipping onboarding message\" ;;\n \"install.welcome_msg.logging_in.zh\") text=\"正在以 %s 身份登录以发送欢迎消息...\" ;;\n \"install.welcome_msg.logging_in.en\") text=\"Logging in as %s to send welcome message...\" ;;\n \"install.welcome_msg.login_failed.zh\") text=\"警告: 以 %s 身份登录失败,跳过欢迎消息\" ;;\n \"install.welcome_msg.login_failed.en\") text=\"WARNING: Failed to login as %s, skipping welcome message\" ;;\n \"install.welcome_msg.finding_room.zh\") text=\"正在查找与 Manager 的 DM 房间...\" ;;\n \"install.welcome_msg.finding_room.en\") text=\"Finding DM room with Manager...\" ;;\n \"install.welcome_msg.creating_room.zh\") text=\"正在创建与 Manager 的 DM 房间...\" ;;\n \"install.welcome_msg.creating_room.en\") text=\"Creating DM room with Manager...\" ;;\n \"install.welcome_msg.no_room.zh\") text=\"警告: 无法找到或创建与 Manager 的 DM 房间\" ;;\n \"install.welcome_msg.no_room.en\") text=\"WARNING: Could not find or create DM room with Manager\" ;;\n \"install.welcome_msg.waiting_join.zh\") text=\"等待 Manager 加入房间...\" ;;\n \"install.welcome_msg.waiting_join.en\") text=\"Waiting for Manager to join the room...\" ;;\n \"install.welcome_msg.sending.zh\") text=\"正在向 Manager 发送欢迎消息...\" ;;\n \"install.welcome_msg.sending.en\") text=\"Sending welcome message to Manager...\" ;;\n \"install.welcome_msg.send_failed.zh\") text=\"警告: 发送欢迎消息失败\" ;;\n \"install.welcome_msg.send_failed.en\") text=\"WARNING: Failed to send welcome message\" ;;\n \"install.welcome_msg.sent.zh\") text=\"欢迎消息已发送给 Manager\" ;;\n \"install.welcome_msg.sent.en\") text=\"Welcome message sent to Manager\" ;;\n \"install.welcome_msg.waiting.zh\") text=\"等待 Manager 发送欢迎消息(Higress 路由授权 + LLM 探活,约 45-90s)...\" ;;\n \"install.welcome_msg.waiting.en\") text=\"Waiting for Manager to send the welcome message (Higress route auth + LLM probe, ~45-90s)...\" ;;\n \"install.welcome_msg.confirmed.zh\") text=\"Manager 已确认发送欢迎消息(status.welcomeSent=true,用时 %ss)\" ;;\n \"install.welcome_msg.confirmed.en\") text=\"Manager confirmed welcome message sent (status.welcomeSent=true, %ss elapsed)\" ;;\n \"install.welcome_msg.timeout.zh\") text=\"警告: 在 %ss 内未观察到 Manager 发送欢迎消息(status.welcomeSent=true)。安装仍然成功,所有服务已就绪——可继续按下方提示登录 Element Web。\" ;;\n \"install.welcome_msg.timeout.en\") text=\"WARNING: Did not observe the Manager sending its welcome message (status.welcomeSent=true) within %ss. Installation is still successful, all services are up — continue with the Element Web instructions below.\" ;;\n \"install.welcome_msg.timeout_hint.zh\") text=\"手动触发 onboarding: 登录 Element Web → 打开与 Manager 的 DM 房间 → 发送任意一句话(例如 \\\"hi\\\"),Manager 会接管对话并开始引导。\" ;;\n \"install.welcome_msg.timeout_hint.en\") text=\"Manual onboarding: log in to Element Web → open the DM with the Manager → send any message (e.g. \\\"hi\\\") and the Manager will take over and start the guided setup.\" ;;\n \"install.welcome_msg.timeout_inspect.zh\") text=\"排查命令: docker exec agentteams-controller agt get managers default\" ;;\n \"install.welcome_msg.timeout_inspect.en\") text=\"Inspect status: docker exec agentteams-controller agt get managers default\" ;;\n \"install.welcome_msg.poll_unavailable.zh\") text=\"提示: agentteams-manager 内未找到 AgentTeams CLI,跳过 welcome 等待(旧镜像?)\" ;;\n \"install.welcome_msg.poll_unavailable.en\") text=\"Note: AgentTeams CLI not found inside agentteams-manager; skipping welcome wait (old image?)\" ;;\n # --- Dashboard wizard ---\n \"dash.prompt.zh\") text=\"是否安装 agentteams-dashboard 管理面板?\" ;;\n \"dash.prompt.en\") text=\"Install agentteams-dashboard management UI?\" ;;\n \"dash.version_prompt.zh\") text=\"Dashboard 版本\" ;;\n \"dash.version_prompt.en\") text=\"Dashboard version\" ;;\n \"dash.port_prompt.zh\") text=\"Dashboard 端口号\" ;;\n \"dash.port_prompt.en\") text=\"Dashboard port\" ;;\n \"dash.image_prompt.zh\") text=\"Dashboard 镜像\" ;;\n \"dash.image_prompt.en\") text=\"Dashboard image\" ;;\n \"dash.gateway_prompt.zh\") text=\"Higress Console URL (用于共享登录)\" ;;\n \"dash.gateway_prompt.en\") text=\"Higress Console URL (for shared login)\" ;;\n \"dash.skip.zh\") text=\"跳过 Dashboard 安装\" ;;\n \"dash.skip.en\") text=\"Skipping Dashboard installation\" ;;\n \"dash.auto_url.zh\") text=\"自动检测到 Higress Console URL\" ;;\n \"dash.auto_url.en\") text=\"Auto-detected Higress Console URL\" ;;\n \"dash.auto_url_fail.zh\") text=\"无法自动检测 Higress Console URL,请手动输入\" ;;\n \"dash.auto_url_fail.en\") text=\"Cannot auto-detect Higress Console URL, please enter manually\" ;;\n \"dash.summary.zh\") text=\"Dashboard 配置: 端口=%s 镜像=%s\" ;;\n \"dash.summary.en\") text=\"Dashboard config: port=%s image=%s\" ;;\n # --- Dashboard success ---\n \"success.dashboard.zh\") text=\" agentteams-dashboard 管理面板: http://localhost:%s/\" ;;\n \"success.dashboard.en\") text=\" agentteams-dashboard: http://localhost:%s/\" ;;\n # --- Final output panel ---\n \"success.title.zh\") text=\"=== AgentTeams Manager 已启动!===\" ;;\n \"success.title.en\") text=\"=== AgentTeams Manager Started! ===\" ;;\n \"success.domains_configured.zh\") text=\"以下域名已配置解析到 127.0.0.1:\" ;;\n \"success.domains_configured.en\") text=\"The following domains are configured to resolve to 127.0.0.1:\" ;;\n \"success.open_url.zh\") text=\" ★ 在浏览器中打开以下 URL 开始使用: ★\" ;;\n \"success.open_url.en\") text=\" ★ Open the following URL in your browser to start: ★\" ;;\n \"success.login_with.zh\") text=\" 登录信息:\" ;;\n \"success.login_with.en\") text=\" Login with:\" ;;\n \"success.username.zh\") text=\" 用户名: %s\" ;;\n \"success.username.en\") text=\" Username: %s\" ;;\n \"success.password.zh\") text=\" 密码: %s\" ;;\n \"success.password.en\") text=\" Password: %s\" ;;\n \"success.after_login.zh\") text=\" 登录后,开始与 Manager 聊天!\" ;;\n \"success.after_login.en\") text=\" After login, start chatting with the Manager!\" ;;\n \"success.tell_it.zh\") text=\" 告诉它: \\\"创建一个名为 alice 的前端开发 Worker\\\"\" ;;\n \"success.tell_it.en\") text=\" Tell it: \\\"Create a Worker named alice for frontend dev\\\"\" ;;\n \"success.manager_auto.zh\") text=\" Manager 会自动处理一切。\" ;;\n \"success.manager_auto.en\") text=\" The Manager will handle everything automatically.\" ;;\n \"success.mobile_title.zh\") text=\" 📱 移动端访问(FluffyChat / Element Mobile):\" ;;\n \"success.mobile_title.en\") text=\" 📱 Mobile access (FluffyChat / Element Mobile):\" ;;\n \"success.mobile_step1.zh\") text=\" 1. 在手机上下载 FluffyChat 或 Element\" ;;\n \"success.mobile_step1.en\") text=\" 1. Download FluffyChat or Element on your phone\" ;;\n \"success.mobile_step2.zh\") text=\" 2. 设置 homeserver 为: %s\" ;;\n \"success.mobile_step2.en\") text=\" 2. Set homeserver to: %s\" ;;\n \"success.mobile_step2_noip.zh\") text=\" 2. 设置 homeserver 为: http://<本机局域网IP>:%s\" ;;\n \"success.mobile_step2_noip.en\") text=\" 2. Set homeserver to: http://<this-machine-LAN-IP>:%s\" ;;\n \"success.mobile_noip_hint.zh\") text=\" (无法自动检测局域网 IP — 请使用 ifconfig / ip addr 查看)\" ;;\n \"success.mobile_noip_hint.en\") text=\" (Could not detect LAN IP automatically — check with: ifconfig / ip addr)\" ;;\n \"success.mobile_step3.zh\") text=\" 3. 登录信息:\" ;;\n \"success.mobile_step3.en\") text=\" 3. Login with:\" ;;\n \"success.mobile_username.zh\") text=\" 用户名: %s\" ;;\n \"success.mobile_username.en\") text=\" Username: %s\" ;;\n \"success.mobile_password.zh\") text=\" 密码: %s\" ;;\n \"success.mobile_password.en\") text=\" Password: %s\" ;;\n # --- Other consoles and tips ---\n \"success.other_consoles.zh\") text=\"--- 其他控制台 ---\" ;;\n \"success.other_consoles.en\") text=\"--- Other Consoles ---\" ;;\n \"success.higress_console.zh\") text=\" Higress 控制台: http://localhost:%s(用户名: %s / 密码: %s)\" ;;\n \"success.higress_console.en\") text=\" Higress Console: http://localhost:%s (Username: %s / Password: %s)\" ;;\n \"success.manager_console.zh\") text=\" Manager 控制台(本地): http://localhost:%s(无需登录)\" ;;\n \"success.manager_console.en\") text=\" Manager Console (local): http://localhost:%s (no login required)\" ;;\n \"success.manager_console_gateway.zh\") text=\" Manager 控制台(网关): http://console-local.agentteams.io(用户名: %s / 密码: %s)\" ;;\n \"success.manager_console_gateway.en\") text=\" Manager Console (gateway): http://console-local.agentteams.io (Username: %s / Password: %s)\" ;;\n \"success.copaw_console.zh\") text=\" QwenPaw App API: http://localhost:%s(无需登录)\" ;;\n \"success.copaw_console.en\") text=\" QwenPaw App API: http://localhost:%s (no login required)\" ;;\n \"success.switch_llm.title.zh\") text=\"--- 切换 LLM 提供商 ---\" ;;\n \"success.switch_llm.title.en\") text=\"--- Switch LLM Providers ---\" ;;\n \"success.switch_llm.hint.zh\") text=\" 您可以通过 Higress 控制台切换到其他 LLM 提供商(OpenAI、Anthropic 等)。\" ;;\n \"success.switch_llm.hint.en\") text=\" You can switch to other LLM providers (OpenAI, Anthropic, etc.) via Higress Console.\" ;;\n \"success.switch_llm.docs.zh\") text=\" 详细说明请参阅:\" ;;\n \"success.switch_llm.docs.en\") text=\" For detailed instructions, see:\" ;;\n \"success.switch_llm.url.zh\") text=\" https://higress.ai/en/docs/ai/scene-guide/multi-proxy#console-configuration\" ;;\n \"success.switch_llm.url.en\") text=\" https://higress.ai/en/docs/ai/scene-guide/multi-proxy#console-configuration\" ;;\n \"success.tip.zh\") text=\"提示: 您也可以在聊天中让 Manager 为您配置 LLM 提供商。\" ;;\n \"success.tip.en\") text=\"Tip: You can also ask the Manager to configure LLM providers for you in the chat.\" ;;\n \"success.config_file.zh\") text=\"配置文件: %s\" ;;\n \"success.config_file.en\") text=\"Configuration file: %s\" ;;\n \"success.data_volume.zh\") text=\"数据卷: %s\" ;;\n \"success.data_volume.en\") text=\"Data volume: %s\" ;;\n \"success.workspace.zh\") text=\"Manager 工作空间: %s\" ;;\n \"success.workspace.en\") text=\"Manager workspace: %s\" ;;\n # --- Worker installation ---\n \"worker.resetting.zh\") text=\"正在重置 Worker: %s...\" ;;\n \"worker.resetting.en\") text=\"Resetting Worker: %s...\" ;;\n \"worker.exists.zh\") text=\"容器 '%s' 已存在。使用 --reset 重新创建。\" ;;\n \"worker.exists.en\") text=\"Container '%s' already exists. Use --reset to recreate.\" ;;\n \"worker.starting.zh\") text=\"正在启动 Worker: %s...\" ;;\n \"worker.starting.en\") text=\"Starting Worker: %s...\" ;;\n \"worker.skills_url.zh\") text=\" Skills API URL: %s\" ;;\n \"worker.skills_url.en\") text=\" Skills API URL: %s\" ;;\n \"worker.started.zh\") text=\"=== Worker %s 已启动!===\" ;;\n \"worker.started.en\") text=\"=== Worker %s Started! ===\" ;;\n \"worker.container.zh\") text=\"容器: %s\" ;;\n \"worker.container.en\") text=\"Container: %s\" ;;\n \"worker.view_logs.zh\") text=\"查看日志: docker logs -f %s\" ;;\n \"worker.view_logs.en\") text=\"View logs: docker logs -f %s\" ;;\n # --- Prompt function messages ---\n \"prompt.preset.zh\") text=\" %s = (已通过环境变量预设)\" ;;\n \"prompt.preset.en\") text=\" %s = (pre-set via env)\" ;;\n \"prompt.upgrade_keep.zh\") text=\" %s = %s(当前值,回车保留 / 输入新值覆盖)\" ;;\n \"prompt.upgrade_keep.en\") text=\" %s = %s (current value, press Enter to keep / type new value to change)\" ;;\n \"prompt.upgrade_keep_secret.zh\") text=\" %s = %s(当前值,回车保留 / 输入新值覆盖)\" ;;\n \"prompt.upgrade_keep_secret.en\") text=\" %s = %s (current value, press Enter to keep / type new value to change)\" ;;\n \"prompt.upgrade_empty.zh\") text=\" %s = (未设置,回车跳过 / 输入新值设置)\" ;;\n \"prompt.upgrade_empty.en\") text=\" %s = (not set, press Enter to skip / type new value to set)\" ;;\n \"prompt.default.zh\") text=\" %s = %s(默认)\" ;;\n \"prompt.default.en\") text=\" %s = %s (default)\" ;;\n \"prompt.required.zh\") text=\"%s 是必需的(在非交互模式下通过环境变量设置)\" ;;\n \"prompt.required.en\") text=\"%s is required (set via environment variable in non-interactive mode)\" ;;\n \"prompt.required_empty.zh\") text=\"%s 是必需的\" ;;\n \"prompt.required_empty.en\") text=\"%s is required\" ;;\n # --- Language switch prompt (bilingual by design) ---\n \"lang.detected.zh\") text=\"检测到语言 / Detected language: 中文\" ;;\n \"lang.detected.en\") text=\"检测到语言 / Detected language: English\" ;;\n \"lang.switch_title.zh\") text=\"切换语言 / Switch language:\" ;;\n \"lang.switch_title.en\") text=\"切换语言 / Switch language:\" ;;\n \"lang.option_zh.zh\") text=\" 1) 中文\" ;;\n \"lang.option_zh.en\") text=\" 1) 中文\" ;;\n \"lang.option_en.zh\") text=\" 2) English\" ;;\n \"lang.option_en.en\") text=\" 2) English\" ;;\n \"lang.prompt.zh\") text=\"请选择 / Enter choice\" ;;\n \"lang.prompt.en\") text=\"请选择 / Enter choice\" ;;\n # --- Uninstall messages ---\n \"uninstall.title.zh\") text=\"正在卸载 AgentTeams...\" ;;\n \"uninstall.title.en\") text=\"Uninstalling AgentTeams...\" ;;\n \"uninstall.stopping_manager.zh\") text=\"正在停止并移除 agentteams-manager...\" ;;\n \"uninstall.stopping_manager.en\") text=\"Stopping and removing agentteams-manager...\" ;;\n \"uninstall.stopping_workers.zh\") text=\"正在停止并移除 Worker 容器...\" ;;\n \"uninstall.stopping_workers.en\") text=\"Stopping and removing worker containers...\" ;;\n \"uninstall.removed.zh\") text=\" 已移除: %s\" ;;\n \"uninstall.removed.en\") text=\" Removed: %s\" ;;\n \"uninstall.removing_volume.zh\") text=\"正在移除 Docker 卷: %s\" ;;\n \"uninstall.removing_volume.en\") text=\"Removing Docker volume: %s\" ;;\n \"uninstall.removing_env.zh\") text=\"正在移除 env 文件: %s\" ;;\n \"uninstall.removing_env.en\") text=\"Removing env file: %s\" ;;\n \"uninstall.removing_proxy.zh\") text=\"正在停止并移除 Docker API 代理容器: agentteams-docker-proxy\" ;;\n \"uninstall.removing_proxy.en\") text=\"Stopping and removing Docker API proxy container: agentteams-docker-proxy\" ;;\n \"uninstall.stopping_controller.zh\") text=\"正在停止并移除 agentteams-controller (内嵌 Tuwunel/MinIO/Higress)...\" ;;\n \"uninstall.stopping_controller.en\") text=\"Stopping and removing agentteams-controller (embedded Tuwunel/MinIO/Higress)...\" ;;\n \"uninstall.removing_network.zh\") text=\"正在移除 Docker 网络: agentteams-net\" ;;\n \"uninstall.removing_network.en\") text=\"Removing Docker network: agentteams-net\" ;;\n \"uninstall.removing_workspace.zh\") text=\"正在移除工作空间目录: %s\" ;;\n \"uninstall.removing_workspace.en\") text=\"Removing workspace directory: %s\" ;;\n \"uninstall.removing_workspace_elevated.zh\") text=\" 工作空间包含 root 文件,通过容器清理...\" ;;\n \"uninstall.removing_workspace_elevated.en\") text=\" Workspace contains root-owned files, cleaning via container...\" ;;\n \"uninstall.removing_log.zh\") text=\"正在移除日志文件: %s\" ;;\n \"uninstall.removing_log.en\") text=\"Removing log file: %s\" ;;\n \"uninstall.done.zh\") text=\"AgentTeams 已卸载。\" ;;\n \"uninstall.done.en\") text=\"AgentTeams has been uninstalled.\" ;;\n # --- Error messages ---\n \"error.name_required.zh\") text=\"--name 是必需的\" ;;\n \"error.name_required.en\") text=\"--name is required\" ;;\n \"error.fs_required.zh\") text=\"--fs 是必需的\" ;;\n \"error.fs_required.en\") text=\"--fs is required\" ;;\n \"error.fs_key_required.zh\") text=\"--fs-key 是必需的\" ;;\n \"error.fs_key_required.en\") text=\"--fs-key is required\" ;;\n \"error.fs_secret_required.zh\") text=\"--fs-secret 是必需的\" ;;\n \"error.fs_secret_required.en\") text=\"--fs-secret is required\" ;;\n \"error.unknown_option.zh\") text=\"未知选项: %s\" ;;\n \"error.unknown_option.en\") text=\"Unknown option: %s\" ;;\n \"error.docker_not_found.zh\") text=\"未找到 docker 或 podman 命令。请先安装 Docker Desktop 或 Podman Desktop:\\n Docker Desktop: https://www.docker.com/products/docker-desktop/\\n Podman Desktop: https://podman-desktop.io/\" ;;\n \"error.docker_not_found.en\") text=\"docker or podman command not found. Please install Docker Desktop or Podman Desktop first:\\n Docker Desktop: https://www.docker.com/products/docker-desktop/\\n Podman Desktop: https://podman-desktop.io/\" ;;\n \"error.docker_not_running.zh\") text=\"Docker 未运行。请先启动 Docker Desktop 或 Podman Desktop。\" ;;\n \"error.docker_not_running.en\") text=\"Docker is not running. Please start Docker Desktop or Podman Desktop first.\" ;;\n # --- Fallback: try English for unknown lang ---\n *)\n case \"${key}.en\" in\n \"tz.warning.title.en\") text=\"Could not detect timezone automatically.\" ;;\n \"install.title.en\") text=\"=== AgentTeams Manager Installation ===\" ;;\n *) text=\"${key}\" ;;\n esac\n ;;\n esac\n if [ $# -gt 0 ]; then\n # shellcheck disable=SC2059\n printf \"${text}\\n\" \"$@\"\n else\n echo \"${text}\"\n fi\n}\n\n# ============================================================\n# Registry selection based on timezone\n# ============================================================\n\ndetect_registry() {\n local tz=\"${AGENTTEAMS_TIMEZONE}\"\n\n case \"${tz}\" in\n America/*)\n echo \"higress-registry.us-west-1.cr.aliyuncs.com\"\n ;;\n Asia/Singapore|Asia/Bangkok|Asia/Jakarta|Asia/Makassar|Asia/Jayapura|\\\n Asia/Kuala_Lumpur|Asia/Ho_Chi_Minh|Asia/Manila|Asia/Yangon|\\\n Asia/Vientiane|Asia/Phnom_Penh|Asia/Pontianak|Asia/Ujung_Pandang)\n echo \"higress-registry.ap-southeast-7.cr.aliyuncs.com\"\n ;;\n *)\n echo \"higress-registry.cn-hangzhou.cr.aliyuncs.com\"\n ;;\n esac\n}\n\nAGENTTEAMS_REGISTRY=\"${AGENTTEAMS_REGISTRY:-$(detect_registry)}\"\n# Backward compatibility: accept old env var names from previous versions\nAGENTTEAMS_INSTALL_CONTROLLER_IMAGE=\"${AGENTTEAMS_INSTALL_CONTROLLER_IMAGE:-${AGENTTEAMS_INSTALL_DOCKER_PROXY_IMAGE:-}}\"\n# Image variables are resolved after version selection in step_version().\n# These placeholders allow early code paths to reference them without errors.\nMANAGER_IMAGE=\"${AGENTTEAMS_INSTALL_MANAGER_IMAGE:-}\"\nMANAGER_COPAW_IMAGE=\"${AGENTTEAMS_INSTALL_MANAGER_COPAW_IMAGE:-}\"\nWORKER_IMAGE=\"${AGENTTEAMS_INSTALL_WORKER_IMAGE:-}\"\nCOPAW_WORKER_IMAGE=\"${AGENTTEAMS_INSTALL_COPAW_WORKER_IMAGE:-}\"\nHERMES_WORKER_IMAGE=\"${AGENTTEAMS_INSTALL_HERMES_WORKER_IMAGE:-}\"\nCONTROLLER_IMAGE=\"${AGENTTEAMS_INSTALL_CONTROLLER_IMAGE:-}\"\n\nresolve_image_tags() {\n MANAGER_IMAGE=\"${AGENTTEAMS_INSTALL_MANAGER_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-manager:${AGENTTEAMS_VERSION}}\"\n MANAGER_COPAW_IMAGE=\"${AGENTTEAMS_INSTALL_MANAGER_COPAW_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-manager-copaw:${AGENTTEAMS_VERSION}}\"\n WORKER_IMAGE=\"${AGENTTEAMS_INSTALL_WORKER_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-worker:${AGENTTEAMS_VERSION}}\"\n COPAW_WORKER_IMAGE=\"${AGENTTEAMS_INSTALL_COPAW_WORKER_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-copaw-worker:${AGENTTEAMS_VERSION}}\"\n HERMES_WORKER_IMAGE=\"${AGENTTEAMS_INSTALL_HERMES_WORKER_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-hermes-worker:${AGENTTEAMS_VERSION}}\"\n EMBEDDED_IMAGE=\"${AGENTTEAMS_INSTALL_EMBEDDED_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-embedded:${AGENTTEAMS_VERSION}}\"\n # CoPaw Worker introduced in v1.0.4; Hermes Worker introduced in v1.1.0\n if [ -z \"${AGENTTEAMS_INSTALL_COPAW_WORKER_IMAGE:-}\" ] && _ver_lt \"${AGENTTEAMS_VERSION}\" \"v1.0.4\"; then\n COPAW_WORKER_IMAGE=\"\"\n fi\n if [ -z \"${AGENTTEAMS_INSTALL_HERMES_WORKER_IMAGE:-}\" ] && _ver_lt \"${AGENTTEAMS_VERSION}\" \"v1.1.0\"; then\n HERMES_WORKER_IMAGE=\"\"\n fi\n}\n\n# Resolve the embedded controller image. Embedded mode is the only supported\n# architecture since PR #616 (manager image no longer bundles Higress/Tuwunel/MinIO).\n# If the embedded image is unavailable for the requested version, fail fast with an\n# actionable error rather than silently falling back to the legacy single-container\n# path — that path is permanently broken with the slim manager image and would just\n# leave the user with a manager container looping on \"Waiting for Higress Gateway\".\n# Sets EMBEDDED_IMAGE and AGENTTEAMS_USE_EMBEDDED.\nresolve_embedded_image() {\n AGENTTEAMS_USE_EMBEDDED=1\n\n # If the user explicitly overrode the image (e.g. `make install-embedded` passes\n # a locally-built tag), respect it as-is without any registry probe.\n if [ -n \"${AGENTTEAMS_INSTALL_EMBEDDED_IMAGE:-}\" ]; then\n EMBEDDED_IMAGE=\"${AGENTTEAMS_INSTALL_EMBEDDED_IMAGE}\"\n return 0\n fi\n\n local _versioned=\"${AGENTTEAMS_REGISTRY}/agentteams/agentteams-embedded:${AGENTTEAMS_VERSION}\"\n local _latest=\"${AGENTTEAMS_REGISTRY}/agentteams/agentteams-embedded:latest\"\n\n # Skip probe when AGENTTEAMS_VERSION is \"latest\" — no point trying the same tag twice.\n if [ \"${AGENTTEAMS_VERSION}\" = \"latest\" ]; then\n EMBEDDED_IMAGE=\"${_latest}\"\n return 0\n fi\n\n if ${DOCKER_CMD} pull \"${_versioned}\" >/dev/null 2>&1; then\n EMBEDDED_IMAGE=\"${_versioned}\"\n return 0\n fi\n\n # Versions before v1.1.0 predate agentteams-embedded entirely — their manager image\n # bundled all infrastructure. Falling back to agentteams-embedded:latest would\n # silently swap in the v1.1.0 architecture (embedded kube-apiserver) which\n # crashes under QEMU on Apple Silicon. Auto-activate legacy mode instead.\n if _ver_lt \"${AGENTTEAMS_VERSION}\" \"v1.1.0\"; then\n log \"INFO: ${AGENTTEAMS_VERSION} predates agentteams-embedded; switching to legacy all-in-one manager architecture.\"\n log \"WARNING: Legacy all-in-one mode requires AGENTTEAMS_VERSION <= v1.0.9 (older bundled manager image).\"\n log \"WARNING: Newer slim manager images will hang on 'Waiting for Higress Gateway'.\"\n AGENTTEAMS_USE_EMBEDDED=0\n return 0\n fi\n\n if ${DOCKER_CMD} pull \"${_latest}\" >/dev/null 2>&1; then\n log \"embedded ${AGENTTEAMS_VERSION} not found, using latest\"\n EMBEDDED_IMAGE=\"${_latest}\"\n return 0\n fi\n\n # Explicit escape hatch — still honoured for edge cases.\n if [ \"${AGENTTEAMS_FORCE_LEGACY:-0}\" = \"1\" ]; then\n log \"WARNING: AGENTTEAMS_FORCE_LEGACY=1 — using legacy all-in-one manager architecture.\"\n log \"WARNING: This requires AGENTTEAMS_VERSION <= v1.0.9 (older bundled manager image).\"\n log \"WARNING: Newer slim manager images will hang on 'Waiting for Higress Gateway'.\"\n AGENTTEAMS_USE_EMBEDDED=0\n return 0\n fi\n\n error \"Embedded controller image is not available in the registry:\"\n error \" - tried: ${_versioned}\"\n error \" - tried: ${_latest}\"\n error \"\"\n error \"Embedded mode is the only supported architecture since PR #616.\"\n error \"How to resolve:\"\n error \" 1) Pin to a AGENTTEAMS_VERSION whose embedded image has been published, or\"\n error \" wait for the release pipeline to publish it.\"\n error \" 2) For a local build, run: make install-embedded\"\n error \" (builds and uses the local embedded image without touching the registry).\"\n error \" 3) Override with a custom image: AGENTTEAMS_INSTALL_EMBEDDED_IMAGE=... ./agentteams-install.sh\"\n exit 1\n}\n\n# ============================================================\n# Known models list — used to detect custom models during install\n# ============================================================\nKNOWN_MODELS=\"gpt-5.4 gpt-5.3-codex gpt-5-mini gpt-5-nano claude-opus-4-6 claude-sonnet-4-6 claude-haiku-4-5 qwen3.6-plus qwen3.5-plus deepseek-chat deepseek-reasoner kimi-k2.5 glm-5 MiniMax-M2.7 MiniMax-M2.7-highspeed MiniMax-M2.5\"\n\nis_known_model() {\n local model=\"$1\"\n for m in ${KNOWN_MODELS}; do\n [ \"${m}\" = \"${model}\" ] && return 0\n done\n return 1\n}\n\n# Prompt user for custom model parameters when model is not in the known list.\n# Sets: AGENTTEAMS_MODEL_CONTEXT_WINDOW, AGENTTEAMS_MODEL_MAX_TOKENS, AGENTTEAMS_MODEL_REASONING, AGENTTEAMS_MODEL_VISION\nprompt_custom_model_params() {\n local model=\"$1\"\n if is_known_model \"${model}\"; then\n # Clear any stale custom params for known models\n AGENTTEAMS_MODEL_CONTEXT_WINDOW=\"\"\n AGENTTEAMS_MODEL_MAX_TOKENS=\"\"\n AGENTTEAMS_MODEL_REASONING=\"\"\n AGENTTEAMS_MODEL_VISION=\"\"\n return\n fi\n echo \"\"\n log \"$(msg llm.custom_model.detected \"${model}\")\"\n echo \"\"\n read -e -p \" $(msg llm.custom_model.context_prompt): \" AGENTTEAMS_MODEL_CONTEXT_WINDOW\n if [ \"${AGENTTEAMS_MODEL_CONTEXT_WINDOW}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n AGENTTEAMS_MODEL_CONTEXT_WINDOW=\"${AGENTTEAMS_MODEL_CONTEXT_WINDOW:-150000}\"\n read -e -p \" $(msg llm.custom_model.max_tokens_prompt): \" AGENTTEAMS_MODEL_MAX_TOKENS\n if [ \"${AGENTTEAMS_MODEL_MAX_TOKENS}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n AGENTTEAMS_MODEL_MAX_TOKENS=\"${AGENTTEAMS_MODEL_MAX_TOKENS:-128000}\"\n read -e -p \" $(msg llm.custom_model.reasoning_prompt): \" _reasoning\n if [ \"${_reasoning}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n case \"${_reasoning}\" in\n n|N|no|NO) AGENTTEAMS_MODEL_REASONING=\"false\" ;;\n *) AGENTTEAMS_MODEL_REASONING=\"true\" ;;\n esac\n read -e -p \" $(msg llm.custom_model.vision_prompt): \" _vision\n if [ \"${_vision}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n case \"${_vision}\" in\n y|Y|yes|YES) AGENTTEAMS_MODEL_VISION=\"true\" ;;\n *) AGENTTEAMS_MODEL_VISION=\"false\" ;;\n esac\n log \"$(msg llm.custom_model.summary \"${AGENTTEAMS_MODEL_CONTEXT_WINDOW}\" \"${AGENTTEAMS_MODEL_MAX_TOKENS}\" \"${AGENTTEAMS_MODEL_REASONING}\" \"${AGENTTEAMS_MODEL_VISION}\")\"\n}\n\n# ============================================================\n# Wait for Manager agent to be ready\n# Uses `openclaw gateway health` inside the container to confirm the gateway is running\n# ============================================================\n\nwait_manager_ready() {\n local timeout=\"${AGENTTEAMS_READY_TIMEOUT:-300}\"\n local elapsed=0\n local container=\"${1:-agentteams-manager}\"\n\n log \"$(msg install.wait_ready \"${timeout}\")\"\n\n # Wait for Manager agent to be healthy inside the container\n local runtime=\"${AGENTTEAMS_MANAGER_RUNTIME:-copaw}\"\n while [ \"${elapsed}\" -lt \"${timeout}\" ]; do\n case \"${runtime}\" in\n copaw)\n if ${DOCKER_CMD} exec \"${container}\" curl -sf http://127.0.0.1:18799/api/agents 2>/dev/null | grep -q '\"agents\"'; then\n log \"$(msg install.wait_ready.ok)\"\n return 0\n fi\n ;;\n *)\n if ${DOCKER_CMD} exec \"${container}\" openclaw gateway health --json 2>/dev/null | grep -q '\"ok\"' 2>/dev/null; then\n log \"$(msg install.wait_ready.ok)\"\n return 0\n fi\n ;;\n esac\n sleep 5\n elapsed=$((elapsed + 5))\n printf \"\\r\\033[36m[AgentTeams]\\033[0m $(msg install.wait_ready.waiting \"${elapsed}\" \"${timeout}\")\"\n done\n\n echo \"\"\n die \"$(msg install.wait_ready.timeout \"${timeout}\" \"${container}\")\"\n}\n\nwait_matrix_ready() {\n local timeout=\"${AGENTTEAMS_READY_TIMEOUT:-300}\"\n local elapsed=0\n local container=\"${1:-agentteams-manager}\"\n\n log \"$(msg install.wait_matrix \"${timeout}\")\"\n\n while [ \"${elapsed}\" -lt \"${timeout}\" ]; do\n if ${DOCKER_CMD} exec \"${container}\" curl -sf http://127.0.0.1:6167/_tuwunel/server_version >/dev/null 2>&1; then\n log \"$(msg install.wait_matrix.ok)\"\n return 0\n fi\n sleep 5\n elapsed=$((elapsed + 5))\n printf \"\\r\\033[36m[AgentTeams]\\033[0m $(msg install.wait_matrix.waiting \"${elapsed}\" \"${timeout}\")\"\n done\n\n echo \"\"\n die \"$(msg install.wait_matrix.timeout \"${timeout}\" \"${container}\")\"\n}\n\n# Read KEY=value from /data/agentteams-secrets.env on a Docker volume (manager container not required).\n# Requires EMBEDDED_IMAGE (resolved earlier in install_manager). Uses ${DOCKER_CMD}.\nagentteams_read_secret_from_data_volume() {\n local _vol=\"$1\" _key=\"$2\"\n if [ -z \"${_vol}\" ] || [ -z \"${_key}\" ] || [ -z \"${EMBEDDED_IMAGE:-}\" ]; then\n echo \"\"\n return 0\n fi\n ${DOCKER_CMD} run --rm --entrypoint sh \\\n -v \"${_vol}:/data:ro\" \\\n \"${EMBEDDED_IMAGE}\" -c \"grep \\\"^${_key}=\\\" /data/agentteams-secrets.env 2>/dev/null | cut -d= -f2- | head -1 | tr -d '\\r'\" 2>/dev/null\n}\n\n# Read KEY=value from /data/worker-creds/<worker>.env on a Docker volume.\nagentteams_read_worker_creds_value_from_volume() {\n local _vol=\"$1\" _worker=\"$2\" _key=\"$3\"\n if [ -z \"${_vol}\" ] || [ -z \"${_worker}\" ] || [ -z \"${_key}\" ] || [ -z \"${EMBEDDED_IMAGE:-}\" ]; then\n echo \"\"\n return 0\n fi\n ${DOCKER_CMD} run --rm --entrypoint sh \\\n -v \"${_vol}:/data:ro\" \\\n \"${EMBEDDED_IMAGE}\" -c \"grep \\\"^${_key}=\\\" \\\"/data/worker-creds/${_worker}.env\\\" 2>/dev/null | cut -d= -f2- | head -1 | tr -d \\\"\\\\r\\\"\" 2>/dev/null\n}\n\n# Read admin_dm_room_id from host workspace state.json (fallback when Matrix API is unavailable).\nagentteams_read_admin_dm_room_from_workspace() {\n local _ws=\"$1\"\n local _f=\"${_ws}/state.json\"\n if [ ! -f \"${_f}\" ] || ! command -v jq >/dev/null 2>&1; then\n echo \"\"\n return 0\n fi\n jq -r '.admin_dm_room_id // empty | select(. != \"null\")' \"${_f}\" 2>/dev/null\n}\n\n# Read secret input with masked echo (shows * per keystroke, supports backspace)\n# Usage: read_secret \"prompt text: \"; value=\"${_RS_RESULT}\"\nread_secret() {\n local _rs_prompt=\"$1\"\n _RS_RESULT=\"\"\n local _rs_char=\"\"\n\n printf \"%s\" \"${_rs_prompt}\"\n\n while IFS= read -r -s -n 1 _rs_char; do\n if [[ -z \"${_rs_char}\" ]]; then\n break\n elif [[ \"${_rs_char}\" == $'\\177' ]] || [[ \"${_rs_char}\" == $'\\b' ]]; then\n if [ -n \"${_RS_RESULT}\" ]; then\n _RS_RESULT=\"${_RS_RESULT%?}\"\n printf \"\\b \\b\"\n fi\n else\n _RS_RESULT=\"${_RS_RESULT}${_rs_char}\"\n printf \"*\"\n fi\n done\n echo\n}\n\n# Load current parameter values from env file for upgrade mode display\nload_current_params_from_env() {\n local env_file=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n if [ -f \"${env_file}\" ]; then\n [ -z \"${AGENTTEAMS_LLM_PROVIDER:+x}\" ] && AGENTTEAMS_LLM_PROVIDER=\"$(grep '^AGENTTEAMS_LLM_PROVIDER=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_OPENAI_BASE_URL:+x}\" ] && AGENTTEAMS_OPENAI_BASE_URL=\"$(grep '^AGENTTEAMS_OPENAI_BASE_URL=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_DEFAULT_MODEL:+x}\" ] && AGENTTEAMS_DEFAULT_MODEL=\"$(grep '^AGENTTEAMS_DEFAULT_MODEL=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_EMBEDDING_MODEL:+x}\" ] && AGENTTEAMS_EMBEDDING_MODEL=\"$(grep '^AGENTTEAMS_EMBEDDING_MODEL=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_WORKSPACE_DIR:+x}\" ] && AGENTTEAMS_WORKSPACE_DIR=\"$(grep '^AGENTTEAMS_WORKSPACE_DIR=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_HOST_SHARE_DIR:+x}\" ] && AGENTTEAMS_HOST_SHARE_DIR=\"$(grep '^AGENTTEAMS_HOST_SHARE_DIR=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_PODMAN_AUTOSTART:+x}\" ] && AGENTTEAMS_PODMAN_AUTOSTART=\"$(grep '^AGENTTEAMS_PODMAN_AUTOSTART=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_DASHBOARD:+x}\" ] && AGENTTEAMS_DASHBOARD=\"$(grep '^AGENTTEAMS_DASHBOARD=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_DASHBOARD_VERSION:+x}\" ] && AGENTTEAMS_DASHBOARD_VERSION=\"$(grep '^AGENTTEAMS_DASHBOARD_VERSION=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_PORT_DASHBOARD:+x}\" ] && AGENTTEAMS_PORT_DASHBOARD=\"$(grep '^AGENTTEAMS_PORT_DASHBOARD=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_DASHBOARD_IMAGE:+x}\" ] && AGENTTEAMS_DASHBOARD_IMAGE=\"$(grep '^AGENTTEAMS_DASHBOARD_IMAGE=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL:+x}\" ] && AGENTTEAMS_AI_GATEWAY_ADMIN_URL=\"$(grep '^AGENTTEAMS_AI_GATEWAY_ADMIN_URL=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n fi\n}\n\n# In non-interactive mode, uses default or errors if required and no default.\n# Usage: prompt VAR_NAME \"Prompt text\" \"default\" [true=secret]\nprompt() {\n local var_name=\"$1\"\n local prompt_text=\"$2\"\n local default_value=\"$3\"\n local is_secret=\"${4:-false}\"\n\n # If the variable is already set in the environment, use it silently\n # In upgrade mode, show current value and let user change it\n eval \"local current_value=\\\"\\${${var_name}}\\\"\"\n if [ -n \"${current_value}\" ]; then\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_NON_INTERACTIVE}\" != \"1\" ]; then\n # Show masked value for secrets, full value otherwise\n local display_value=\"${current_value}\"\n if [ \"${is_secret}\" = \"true\" ]; then\n local len=${#current_value}\n if [ \"${len}\" -le 8 ]; then\n display_value=\"****\"\n else\n display_value=\"${current_value:0:4}****${current_value: -4}\"\n fi\n fi\n log \"$(msg prompt.upgrade_keep \"${prompt_text}\" \"${display_value}\")\"\n local new_value=\"\"\n if [ \"${is_secret}\" = \"true\" ]; then\n read_secret \"${prompt_text}: \"\n new_value=\"${_RS_RESULT}\"\n else\n read -e -p \"${prompt_text}: \" new_value\n if [ \"${new_value}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n fi\n if [ -n \"${new_value}\" ]; then\n eval \"export ${var_name}='${new_value}'\"\n fi\n return\n fi\n log \"$(msg prompt.preset \"${prompt_text}\")\"\n return\n fi\n\n # Non-interactive or quickstart: use default or error\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] || [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ]; then\n if [ -n \"${default_value}\" ]; then\n eval \"export ${var_name}='${default_value}'\"\n log \"$(msg prompt.default \"${prompt_text}\" \"${default_value}\")\"\n return\n elif [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n # Only hard-error in fully non-interactive mode, not quickstart\n die \"$(msg prompt.required \"${prompt_text}\")\"\n fi\n # quickstart + no default: fall through to interactive prompt below\n fi\n\n if [ -n \"${default_value}\" ]; then\n prompt_text=\"${prompt_text} [${default_value}]\"\n fi\n\n local value=\"\"\n if [ \"${is_secret}\" = \"true\" ]; then\n read_secret \"${prompt_text}: \"\n value=\"${_RS_RESULT}\"\n else\n read -e -p \"${prompt_text}: \" value\n if [ \"${value}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n fi\n\n value=\"${value:-${default_value}}\"\n if [ -z \"${value}\" ]; then\n die \"$(msg prompt.required_empty \"${prompt_text}\")\"\n fi\n\n eval \"export ${var_name}='${value}'\"\n}\n\n# Prompt for an optional value (empty string is acceptable)\n# Skips prompt if variable is already defined in environment (even if empty)\n# In upgrade mode, shows current value and lets user change it.\n# In non-interactive mode, defaults to empty string.\nprompt_optional() {\n local var_name=\"$1\"\n local prompt_text=\"$2\"\n local is_secret=\"${3:-false}\"\n\n # Check if variable is defined (even if set to empty string)\n eval \"local _chk=\\\"\\${${var_name}+x}\\\"\"\n if [ -n \"${_chk}\" ]; then\n # In upgrade mode, show current value and let user change it\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_NON_INTERACTIVE}\" != \"1\" ]; then\n eval \"local current_value=\\\"\\${${var_name}}\\\"\"\n local display_value=\"${current_value}\"\n if [ \"${is_secret}\" = \"true\" ] && [ -n \"${current_value}\" ]; then\n local len=${#current_value}\n if [ \"${len}\" -le 8 ]; then\n display_value=\"****\"\n else\n display_value=\"${current_value:0:4}****${current_value: -4}\"\n fi\n fi\n if [ -n \"${current_value}\" ]; then\n if [ \"${is_secret}\" = \"true\" ]; then\n log \"$(msg prompt.upgrade_keep_secret \"${prompt_text}\" \"${display_value}\")\"\n else\n log \"$(msg prompt.upgrade_keep \"${prompt_text}\" \"${display_value}\")\"\n fi\n else\n log \"$(msg prompt.upgrade_empty \"${prompt_text}\")\"\n fi\n local new_value=\"\"\n if [ \"${is_secret}\" = \"true\" ]; then\n read_secret \"${prompt_text}: \"\n new_value=\"${_RS_RESULT}\"\n else\n read -e -p \"${prompt_text}: \" new_value\n if [ \"${new_value}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n fi\n if [ -n \"${new_value}\" ]; then\n eval \"export ${var_name}='${new_value}'\"\n fi\n return\n fi\n log \"$(msg prompt.preset \"${prompt_text}\")\"\n return\n fi\n\n # Non-interactive or quickstart: skip, leave unset\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] || [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ]; then\n eval \"export ${var_name}=''\"\n return\n fi\n\n local value=\"\"\n if [ \"${is_secret}\" = \"true\" ]; then\n read_secret \"${prompt_text}: \"\n value=\"${_RS_RESULT}\"\n else\n read -e -p \"${prompt_text}: \" value\n if [ \"${value}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n fi\n\n eval \"export ${var_name}='${value}'\"\n}\n\ngenerate_key() {\n openssl rand -hex 32\n}\n\n# ============================================================\n# Detect container runtime socket on the host\n#\n# Dependency: Requires DOCKER_CMD to reflect the TRUE underlying runtime.\n# (Crucial for unmasking 'podman' disguised as 'docker'.\n# Must be strictly resolved prior, e.g., via check_container_runtime)\n# ============================================================\ndetect_socket() {\n local socket_path\n\n # 1. Respect explicitly defined DOCKER_HOST environment variable\n if [ -n \"${DOCKER_HOST}\" ]; then\n socket_path=$(echo \"${DOCKER_HOST}\" | sed 's|^unix://||')\n if [ -S \"${socket_path}\" ]; then\n echo \"${socket_path}\"\n return 0\n fi\n fi\n\n # 2. Route based on the resolved container runtime\n case \"${DOCKER_CMD}\" in\n docker)\n # Docker path handling\n socket_path=$(docker context ls --format '{{if .Current}}{{.DockerEndpoint}}{{end}}' 2>/dev/null | grep . | sed 's|^unix://||')\n if [ -n \"${socket_path}\" ] && [ -S \"${socket_path}\" ]; then\n echo \"${socket_path}\"\n return 0\n fi\n\n # Rootless Docker\n if [ -n \"${XDG_RUNTIME_DIR:-}\" ] && [ -S \"${XDG_RUNTIME_DIR}/docker.sock\" ]; then\n echo \"${XDG_RUNTIME_DIR}/docker.sock\"\n return 0\n fi\n\n # Rootless Docker (macOS fallback)\n if [ -S \"${HOME}/.docker/run/docker.sock\" ]; then\n echo \"${HOME}/.docker/run/docker.sock\"\n return 0\n fi\n\n # Root Docker\n if [ -S \"/var/run/docker.sock\" ]; then\n echo \"/var/run/docker.sock\"\n return 0\n fi\n ;;\n podman)\n # Podman path handling\n if [ \"$(id -u)\" -eq 0 ]; then\n socket_path=\"/run/podman/podman.sock\"\n else\n socket_path=\"${XDG_RUNTIME_DIR}/podman/podman.sock\"\n fi\n if [ -S \"${socket_path}\" ]; then\n echo \"${socket_path}\"\n return 0\n fi\n ;;\n esac\n\n # Return empty if no socket is found\n echo \"\"\n return 0\n}\n\n# Ensure Podman API socket is active (State mutation)\nensure_podman_socket() {\n local socket_path\n\n # Only applicable if runtime is podman and systemctl is available\n if [ \"${DOCKER_CMD:-}\" != \"podman\" ] || ! command -v systemctl >/dev/null 2>&1; then\n return 0\n fi\n\n echo \"Ensuring Podman API socket is active...\" >&2\n\n # Enable and start the socket based on user privileges\n if [ \"$(id -u)\" -eq 0 ]; then\n socket_path=\"/run/podman/podman.sock\"\n systemctl enable --now podman.socket >/dev/null 2>&1 || true\n else\n socket_path=\"${XDG_RUNTIME_DIR}/podman/podman.sock\"\n systemctl --user enable --now podman.socket >/dev/null 2>&1 || true\n fi\n\n # Give systemd a brief moment to assert the socket for root or rootless user\n local _i\n for _i in 1 2 3; do\n [ -S \"${socket_path}\" ] && break\n sleep 1\n done\n}\n\n# Detect local LAN IP address (cross-platform: macOS and Linux)\ndetect_lan_ip() {\n local ip=\"\"\n\n # macOS: try common Wi-Fi / Ethernet interfaces\n if command -v ipconfig >/dev/null 2>&1; then\n for iface in en0 en1 en2 en3 en4; do\n ip=$(ipconfig getifaddr \"${iface}\" 2>/dev/null)\n if [ -n \"${ip}\" ]; then\n echo \"${ip}\"\n return 0\n fi\n done\n fi\n\n # Linux: ip route — most reliable\n if command -v ip >/dev/null 2>&1; then\n ip=$(ip route get 1.1.1.1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i==\"src\"){print $(i+1); exit}}')\n if [ -n \"${ip}\" ]; then\n echo \"${ip}\"\n return 0\n fi\n fi\n\n # Linux fallback: hostname -I (space-separated list, take first non-loopback)\n if command -v hostname >/dev/null 2>&1; then\n ip=$(hostname -I 2>/dev/null | tr ' ' '\\n' | grep -v '^127\\.' | grep -v '^::' | head -1)\n if [ -n \"${ip}\" ]; then\n echo \"${ip}\"\n return 0\n fi\n fi\n\n # Last resort: ifconfig\n if command -v ifconfig >/dev/null 2>&1; then\n ip=$(ifconfig 2>/dev/null | awk '/inet /{if($2!~/^127\\./){print $2; exit}}')\n # Strip \"addr:\" prefix that some ifconfig versions add\n ip=\"${ip#addr:}\"\n if [ -n \"${ip}\" ]; then\n echo \"${ip}\"\n return 0\n fi\n fi\n\n echo \"\"\n}\n\n\n# ============================================================\n# Step-back navigation helpers\n# ============================================================\n\n# should_skip_step: returns 0 (skip) when the step is irrelevant in current mode\nshould_skip_step() {\n local step_fn=\"$1\"\n case \"${step_fn}\" in\n step_lang|step_mode)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n ;;\n step_version)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n ;;\n step_existing)\n local _env=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n [ ! -f \"${_env}\" ] && return 0\n ;;\n # Keep-All upgrade mode: skip all config steps (step_volume/step_workspace handled separately)\n step_llm|step_admin|step_network|step_ports|step_domains|step_github|step_skills|step_runtime|step_manager_runtime|step_dashboard|step_e2ee|step_docker_proxy|step_idle|step_hostshare)\n [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ] && return 0\n ;;\n step_volume|step_workspace)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ] && return 0\n [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ] && return 0\n ;;\n step_e2ee|step_idle)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE}\" != \"1\" ] && return 0\n ;;\n step_docker_proxy)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE}\" != \"1\" ] && return 0\n # Embedded mode handles docker access natively — skip this step\n [ \"${AGENTTEAMS_USE_EMBEDDED:-}\" = \"1\" ] && return 0\n ;;\n step_manager_runtime)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n ;;\n step_hostshare)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ] && return 0\n ;;\n step_podman_autostart)\n # Only relevant for Podman with systemd\n [ \"${DOCKER_CMD}\" != \"podman\" ] && return 0\n ! command -v systemctl >/dev/null 2>&1 && return 0\n\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE}\" != \"1\" ] && return 0\n [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ] && return 0\n ;;\n esac\n return 1\n}\n\n# clear_step_vars: unset variables set by a step so it will re-prompt on re-entry\nclear_step_vars() {\n local step_fn=\"$1\"\n case \"${step_fn}\" in\n step_mode) unset AGENTTEAMS_QUICKSTART ;;\n step_version) unset AGENTTEAMS_VERSION ;;\n step_existing) unset AGENTTEAMS_UPGRADE UPGRADE_EXISTING_WORKERS ;;\n step_llm)\n unset AGENTTEAMS_LLM_PROVIDER AGENTTEAMS_DEFAULT_MODEL AGENTTEAMS_OPENAI_BASE_URL\n unset AGENTTEAMS_LLM_API_KEY AGENTTEAMS_MODEL_CONTEXT_WINDOW AGENTTEAMS_MODEL_MAX_TOKENS\n unset AGENTTEAMS_MODEL_REASONING AGENTTEAMS_MODEL_VISION\n ;;\n step_admin) unset AGENTTEAMS_ADMIN_USER AGENTTEAMS_ADMIN_PASSWORD ;;\n step_network) unset AGENTTEAMS_LOCAL_ONLY ;;\n step_ports)\n unset AGENTTEAMS_PORT_GATEWAY AGENTTEAMS_PORT_CONSOLE\n unset AGENTTEAMS_PORT_ELEMENT_WEB AGENTTEAMS_PORT_MANAGER_CONSOLE\n ;;\n step_domains)\n unset AGENTTEAMS_MATRIX_DOMAIN AGENTTEAMS_MATRIX_CLIENT_DOMAIN\n unset AGENTTEAMS_AI_GATEWAY_DOMAIN AGENTTEAMS_FS_DOMAIN AGENTTEAMS_CONSOLE_DOMAIN\n ;;\n step_github) unset AGENTTEAMS_GITHUB_TOKEN ;;\n step_skills) unset AGENTTEAMS_SKILLS_API_URL ;;\n step_volume) unset AGENTTEAMS_DATA_DIR ;;\n step_workspace) unset AGENTTEAMS_WORKSPACE_DIR ;;\n step_dashboard) unset AGENTTEAMS_DASHBOARD AGENTTEAMS_DASHBOARD_VERSION AGENTTEAMS_PORT_DASHBOARD AGENTTEAMS_DASHBOARD_IMAGE AGENTTEAMS_AI_GATEWAY_ADMIN_URL ;;\n step_runtime) unset AGENTTEAMS_DEFAULT_WORKER_RUNTIME ;;\n step_manager_runtime) unset AGENTTEAMS_MANAGER_RUNTIME ;;\n step_e2ee) unset AGENTTEAMS_MATRIX_E2EE ;;\n step_docker_proxy) unset AGENTTEAMS_DOCKER_PROXY; unset AGENTTEAMS_PROXY_ALLOWED_REGISTRIES ;;\n step_idle) unset AGENTTEAMS_WORKER_IDLE_TIMEOUT ;;\n step_hostshare) unset AGENTTEAMS_HOST_SHARE_DIR ;;\n step_podman_autostart) unset AGENTTEAMS_PODMAN_AUTOSTART ;;\n esac\n}\n\n# ============================================================\n# Individual step functions\n# ============================================================\n\nstep_lang() {\n local lang_default_choice=\"2\"\n [ \"${AGENTTEAMS_LANGUAGE}\" = \"zh\" ] && lang_default_choice=\"1\"\n log \"$(msg lang.detected)\"\n log \"$(msg lang.switch_title)\"\n echo \"$(msg lang.option_zh)\"\n echo \"$(msg lang.option_en)\"\n echo \"\"\n local LANG_CHOICE\n read -e -p \"$(msg lang.prompt) [${lang_default_choice}]: \" LANG_CHOICE\n LANG_CHOICE=\"${LANG_CHOICE:-${lang_default_choice}}\"\n if [ \"${LANG_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n case \"${LANG_CHOICE}\" in\n 1) AGENTTEAMS_LANGUAGE=\"zh\" ;;\n 2) AGENTTEAMS_LANGUAGE=\"en\" ;;\n esac\n export AGENTTEAMS_LANGUAGE\n log \"\"\n}\n\nstep_mode() {\n log \"$(msg install.mode.title)\"\n echo \"\"\n echo \"$(msg install.mode.choose)\"\n echo \"$(msg install.mode.quickstart)\"\n echo \"$(msg install.mode.manual)\"\n echo \"\"\n local ONBOARDING_CHOICE\n read -e -p \"$(msg install.mode.prompt): \" ONBOARDING_CHOICE\n ONBOARDING_CHOICE=\"${ONBOARDING_CHOICE:-1}\"\n if [ \"${ONBOARDING_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n case \"${ONBOARDING_CHOICE}\" in\n 1|quick|quickstart)\n log \"$(msg install.mode.quickstart_selected)\"\n AGENTTEAMS_QUICKSTART=1\n ;;\n 2|manual)\n log \"$(msg install.mode.manual_selected)\"\n ;;\n *)\n log \"$(msg install.mode.invalid)\"\n AGENTTEAMS_QUICKSTART=1\n ;;\n esac\n log \"\"\n}\n\nstep_version() {\n # Skip if version already provided via env var\n if [ -n \"${AGENTTEAMS_VERSION}\" ]; then\n resolve_image_tags\n return 0\n fi\n # Try to fetch the latest stable release from GitHub\n log \"$(msg install.version.fetching)\"\n local _fetched\n _fetched=$(curl -sf --max-time 5 \\\n -H \"Accept: application/vnd.github+json\" \\\n \"https://api.github.com/repos/agentscope-ai/AgentTeams/releases/latest\" \\\n 2>/dev/null | grep '\"tag_name\"' | head -1 | sed 's/.*\"tag_name\": *\"\\([^\"]*\\)\".*/\\1/')\n if [ -n \"${_fetched}\" ]; then\n AGENTTEAMS_KNOWN_STABLE_VERSION=\"${_fetched}\"\n else\n log \"$(msg install.version.fetch_failed \"${AGENTTEAMS_KNOWN_STABLE_VERSION}\")\"\n fi\n log \"$(msg install.version.title)\"\n echo \"\"\n echo \"$(msg install.version.choose)\"\n echo \"$(msg install.version.option_latest)\"\n printf \"%s\\n\" \"$(msg install.version.option_stable \"${AGENTTEAMS_KNOWN_STABLE_VERSION}\")\"\n echo \"$(msg install.version.option_custom)\"\n echo \"\"\n local VERSION_CHOICE\n read -e -p \"$(msg install.version.prompt) [1]: \" VERSION_CHOICE\n VERSION_CHOICE=\"${VERSION_CHOICE:-1}\"\n if [ \"${VERSION_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n case \"${VERSION_CHOICE}\" in\n 1|latest)\n AGENTTEAMS_VERSION=\"latest\"\n log \"$(msg install.version.selected_latest)\"\n ;;\n 2|stable)\n AGENTTEAMS_VERSION=\"${AGENTTEAMS_KNOWN_STABLE_VERSION}\"\n log \"$(msg install.version.selected_stable \"${AGENTTEAMS_VERSION}\")\"\n ;;\n 3|custom)\n local CUSTOM_VERSION\n read -e -p \"$(msg install.version.custom_prompt): \" CUSTOM_VERSION\n AGENTTEAMS_VERSION=\"${CUSTOM_VERSION:-${AGENTTEAMS_KNOWN_STABLE_VERSION}}\"\n log \"$(msg install.version.selected_custom \"${AGENTTEAMS_VERSION}\")\"\n ;;\n *)\n AGENTTEAMS_VERSION=\"${AGENTTEAMS_KNOWN_STABLE_VERSION}\"\n log \"$(msg install.version.invalid \"${AGENTTEAMS_VERSION}\")\"\n ;;\n esac\n log \"\"\n resolve_image_tags\n}\n\nstep_existing() {\n local existing_env=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n log \"$(msg install.existing.detected \"${existing_env}\")\"\n local running_manager=\"\" running_workers=\"\" existing_workers=\"\"\n if ${DOCKER_CMD} ps --format '{{.Names}}' | grep -q \"^agentteams-manager$\"; then\n running_manager=\"agentteams-manager\"\n fi\n running_workers=$(${DOCKER_CMD} ps --format '{{.Names}}' | grep \"^agentteams-worker-\" || true)\n existing_workers=$(${DOCKER_CMD} ps -a --format '{{.Names}}' | grep \"^agentteams-worker-\" || true)\n local UPGRADE_CHOICE\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n log \"$(msg install.existing.upgrade_noninteractive)\"\n UPGRADE_CHOICE=\"upgrade\"\n else\n echo \"\"\n echo \"$(msg install.existing.choose)\"\n echo \"$(msg install.existing.upgrade)\"\n echo \"$(msg install.existing.reinstall)\"\n echo \"$(msg install.existing.cancel)\"\n echo \"\"\n read -e -p \"$(msg install.existing.prompt): \" UPGRADE_CHOICE\n UPGRADE_CHOICE=\"${UPGRADE_CHOICE:-1}\"\n if [ \"${UPGRADE_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n fi\n case \"${UPGRADE_CHOICE}\" in\n 1|upgrade)\n AGENTTEAMS_UPGRADE=1\n log \"$(msg install.existing.upgrading)\"\n # Show upgrade mode sub-menu (unless already set via env var)\n if [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL:-0}\" != \"1\" ]; then\n echo \"\"\n echo \"$(msg upgrade.mode.prompt)\"\n echo \"$(msg upgrade.mode.keep_all)\"\n echo \"$(msg upgrade.mode.confirm_each)\"\n echo \"$(msg upgrade.mode.back)\"\n echo \"\"\n local UPGRADE_MODE_CHOICE\n read -e -p \"$(msg install.existing.prompt): \" UPGRADE_MODE_CHOICE\n UPGRADE_MODE_CHOICE=\"${UPGRADE_MODE_CHOICE:-1}\"\n case \"${UPGRADE_MODE_CHOICE}\" in\n 1|keep)\n AGENTTEAMS_UPGRADE_KEEP_ALL=1\n ;;\n 2|confirm)\n AGENTTEAMS_UPGRADE_KEEP_ALL=0\n ;;\n 3|b)\n STEP_RESULT=\"back\"\n return 0\n ;;\n *)\n AGENTTEAMS_UPGRADE_KEEP_ALL=0\n ;;\n esac\n fi\n # Load current parameters for both Keep-All and confirm-each modes\n load_current_params_from_env\n if [ -n \"${running_manager}\" ] || [ -n \"${running_workers}\" ]; then\n echo \"\"\n echo -e \"\\033[33m$(msg install.existing.warn_manager_stop)\\033[0m\"\n if [ -n \"${existing_workers}\" ]; then\n echo -e \"\\033[33m$(msg install.existing.warn_worker_recreate)\\033[0m\"\n fi\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" != \"1\" ]; then\n echo \"\"\n local CONFIRM_STOP\n read -e -p \"$(msg install.existing.continue_prompt): \" CONFIRM_STOP\n if [ \"${CONFIRM_STOP}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n if [ \"${CONFIRM_STOP}\" != \"y\" ] && [ \"${CONFIRM_STOP}\" != \"Y\" ]; then\n log \"$(msg install.existing.cancelled)\"\n exit 0\n fi\n fi\n fi\n UPGRADE_EXISTING_WORKERS=\"${existing_workers}\"\n ;;\n 2|reinstall)\n log \"$(msg install.reinstall.performing)\"\n local existing_workspace=\"\"\n if [ -f \"${existing_env}\" ]; then\n existing_workspace=$(grep '^AGENTTEAMS_WORKSPACE_DIR=' \"${existing_env}\" 2>/dev/null | cut -d= -f2-)\n fi\n [ -z \"${existing_workspace}\" ] && existing_workspace=\"${HOME}/agentteams-manager\"\n echo \"\"\n echo -e \"\\033[33m$(msg install.reinstall.warn_stop)\\033[0m\"\n [ -n \"${running_manager}\" ] && echo -e \"\\033[33m - ${running_manager} (manager)\\033[0m\"\n for w in ${running_workers}; do\n echo -e \"\\033[33m - ${w} (worker)\\033[0m\"\n done\n echo \"\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_delete)\\033[0m\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_volume)\\033[0m\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_env \"${existing_env}\")\\033[0m\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_workspace \"${existing_workspace}\")\\033[0m\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_workers)\\033[0m\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_proxy)\\033[0m\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_network)\\033[0m\"\n echo \"\"\n echo -e \"\\033[31m$(msg install.reinstall.confirm_type)\\033[0m\"\n echo -e \"\\033[31m ${existing_workspace}\\033[0m\"\n echo \"\"\n local CONFIRM_PATH\n read -e -p \"$(msg install.reinstall.confirm_path): \" CONFIRM_PATH\n if [ \"${CONFIRM_PATH}\" != \"${existing_workspace}\" ]; then\n die \"$(msg install.reinstall.path_mismatch \"${CONFIRM_PATH}\" \"${existing_workspace}\")\"\n fi\n log \"$(msg install.reinstall.confirmed)\"\n ${DOCKER_CMD} stop agentteams-manager 2>/dev/null || true\n ${DOCKER_CMD} rm agentteams-manager 2>/dev/null || true\n for w in $(${DOCKER_CMD} ps -a --format '{{.Names}}' | grep \"^agentteams-worker-\" || true); do\n ${DOCKER_CMD} stop \"${w}\" 2>/dev/null || true\n ${DOCKER_CMD} rm \"${w}\" 2>/dev/null || true\n log \"$(msg install.reinstall.removed_worker \"${w}\")\"\n done\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' | grep -q \"^agentteams-controller$\"; then\n log \"$(msg install.reinstall.removing_proxy)\"\n ${DOCKER_CMD} stop agentteams-controller 2>/dev/null || true\n ${DOCKER_CMD} rm agentteams-controller 2>/dev/null || true\n fi\n if ${DOCKER_CMD} network ls --format '{{.Name}}' | grep -q \"^agentteams-net$\"; then\n log \"$(msg install.reinstall.removing_network)\"\n ${DOCKER_CMD} network rm agentteams-net 2>/dev/null || true\n fi\n if ${DOCKER_CMD} volume ls -q | grep -q \"^agentteams-data$\"; then\n log \"$(msg install.reinstall.removing_volume)\"\n ${DOCKER_CMD} volume rm agentteams-data 2>/dev/null || log \"$(msg install.reinstall.warn_volume_fail)\"\n fi\n if [ -d \"${existing_workspace}\" ]; then\n log \"$(msg install.reinstall.removing_workspace \"${existing_workspace}\")\"\n rm -rf \"${existing_workspace}\" || die \"$(msg install.reinstall.failed_rm_workspace)\"\n fi\n if [ -f \"${existing_env}\" ]; then\n log \"$(msg install.reinstall.removing_env \"${existing_env}\")\"\n rm -f \"${existing_env}\"\n fi\n log \"$(msg install.reinstall.cleanup_done)\"\n unset AGENTTEAMS_WORKSPACE_DIR\n return 0\n ;;\n 3|cancel|*)\n log \"$(msg install.existing.cancelled)\"\n exit 0\n ;;\n esac\n # Load existing env file as fallback (shell env vars take priority)\n if [ -f \"${existing_env}\" ]; then\n log \"$(msg install.loading_config \"${existing_env}\")\"\n while IFS='=' read -r key value; do\n case \"${key}\" in \\#*|\"\") continue ;; esac\n value=\"${value%%#*}\"\n value=\"${value#\"${value%%[![:space:]]*}\"}\"\n value=\"${value%\"${value##*[![:space:]]}\"}\"\n eval \"_existing_val=\\\"\\${${key}+x}\\\"\"\n if [ -z \"${_existing_val}\" ]; then export \"${key}=${value}\"; fi\n done < \"${existing_env}\"\n fi\n}\n\nstep_llm() {\n log \"$(msg llm.title)\"\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n if [ \"${AGENTTEAMS_LANGUAGE}\" = \"zh\" ]; then\n AGENTTEAMS_LLM_PROVIDER=\"${AGENTTEAMS_LLM_PROVIDER:-openai-compat}\"\n AGENTTEAMS_DEFAULT_MODEL=\"${AGENTTEAMS_DEFAULT_MODEL:-qwen3.6-plus}\"\n AGENTTEAMS_OPENAI_BASE_URL=\"${AGENTTEAMS_OPENAI_BASE_URL:-https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1}\"\n log \"$(msg llm.provider.label \"${AGENTTEAMS_LLM_PROVIDER}\")\"\n log \"$(msg llm.openai.base_url_label \"${AGENTTEAMS_OPENAI_BASE_URL}\")\"\n else\n AGENTTEAMS_LLM_PROVIDER=\"${AGENTTEAMS_LLM_PROVIDER:-qwen}\"\n AGENTTEAMS_DEFAULT_MODEL=\"${AGENTTEAMS_DEFAULT_MODEL:-qwen3.6-plus}\"\n AGENTTEAMS_OPENAI_BASE_URL=\"${AGENTTEAMS_OPENAI_BASE_URL:-}\"\n log \"$(msg llm.provider.qwen_default \"${AGENTTEAMS_LLM_PROVIDER}\")\"\n fi\n log \"$(msg llm.model.default \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n prompt AGENTTEAMS_LLM_API_KEY \"$(msg llm.apikey_prompt)\" \"\" \"true\"\n AGENTTEAMS_EMBEDDING_MODEL=\"${AGENTTEAMS_EMBEDDING_MODEL-text-embedding-v4}\"\n export AGENTTEAMS_LLM_PROVIDER AGENTTEAMS_DEFAULT_MODEL AGENTTEAMS_EMBEDDING_MODEL\n [ -n \"${AGENTTEAMS_OPENAI_BASE_URL+x}\" ] && export AGENTTEAMS_OPENAI_BASE_URL\n return 0\n fi\n\n # Upgrade Keep-All mode: skip provider selection, use loaded values\n local SKIP_TO_EMBEDDING=0\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ] && [ -n \"${AGENTTEAMS_LLM_PROVIDER}\" ]; then\n log \"$(msg llm.provider.label \"${AGENTTEAMS_LLM_PROVIDER}\")\"\n log \"$(msg llm.openai.base_url_label \"${AGENTTEAMS_OPENAI_BASE_URL}\")\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n PROVIDER_CHOICE=\"${AGENTTEAMS_LLM_PROVIDER}\"\n SKIP_TO_EMBEDDING=1\n else\n echo \"\"\n echo \"$(msg llm.providers_title)\"\n echo \"$(msg llm.provider.alibaba)\"\n echo \"$(msg llm.provider.openai_compat)\"\n echo \"\"\n local PROVIDER_CHOICE\n # If upgrade mode with loaded provider, show current as default\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_LLM_PROVIDER}\" ]; then\n local _prov_display\n case \"${AGENTTEAMS_LLM_PROVIDER}\" in\n openai-compat) _prov_display=\"2\" ;;\n *) _prov_display=\"1\" ;;\n esac\n read -e -p \"$(msg llm.provider.select) [${_prov_display}]: \" PROVIDER_CHOICE\n PROVIDER_CHOICE=\"${PROVIDER_CHOICE:-${_prov_display}}\"\n elif [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ]; then\n read -e -p \"$(msg llm.provider.select) [1]: \" PROVIDER_CHOICE\n PROVIDER_CHOICE=\"${PROVIDER_CHOICE:-1}\"\n else\n read -e -p \"$(msg llm.provider.select): \" PROVIDER_CHOICE\n PROVIDER_CHOICE=\"${PROVIDER_CHOICE:-1}\"\n fi\n if [ \"${PROVIDER_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n fi\n local ALIBABA_MODEL_CHOICE=\"\"\n local ALIBABA_ACCESS=\"\"\n case \"${PROVIDER_CHOICE}\" in\n 1|alibaba-cloud|openai-compat)\n if [ \"${AGENTTEAMS_LANGUAGE}\" = \"en\" ]; then\n AGENTTEAMS_LLM_PROVIDER=\"openai-compat\"\n AGENTTEAMS_OPENAI_BASE_URL=\"https://dashscope-intl.aliyuncs.com/compatible-mode/v1\"\n ALIBABA_ACCESS=\"tokenplan\"\n echo \"\"\n echo \"$(msg llm.codingplan.models_title)\"\n echo \"$(msg llm.codingplan.model.qwen36plus)\"\n echo \"$(msg llm.codingplan.model.glm5)\"\n echo \"$(msg llm.codingplan.model.kimi)\"\n echo \"$(msg llm.codingplan.model.minimax)\"\n echo \"\"\n local CODINGPLAN_MODEL_CHOICE\n # If upgrade with loaded model, show current as default\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_DEFAULT_MODEL}\" ]; then\n local _model_default\n case \"${AGENTTEAMS_DEFAULT_MODEL}\" in\n qwen3.6-plus) _model_default=\"1\" ;;\n glm-5) _model_default=\"2\" ;;\n kimi-k2.5) _model_default=\"3\" ;;\n MiniMax-M2.5) _model_default=\"4\" ;;\n *) _model_default=\"1\" ;;\n esac\n read -e -p \"$(msg llm.codingplan.model.select) [${_model_default}]: \" CODINGPLAN_MODEL_CHOICE\n CODINGPLAN_MODEL_CHOICE=\"${CODINGPLAN_MODEL_CHOICE:-${_model_default}}\"\n elif [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ]; then\n read -e -p \"$(msg llm.codingplan.model.select) [1]: \" CODINGPLAN_MODEL_CHOICE\n CODINGPLAN_MODEL_CHOICE=\"${CODINGPLAN_MODEL_CHOICE:-1}\"\n else\n read -e -p \"$(msg llm.codingplan.model.select): \" CODINGPLAN_MODEL_CHOICE\n CODINGPLAN_MODEL_CHOICE=\"${CODINGPLAN_MODEL_CHOICE:-1}\"\n fi\n if [ \"${CODINGPLAN_MODEL_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n case \"${CODINGPLAN_MODEL_CHOICE}\" in\n 1|qwen3.6-plus) AGENTTEAMS_DEFAULT_MODEL=\"qwen3.6-plus\" ;;\n 2|glm-5) AGENTTEAMS_DEFAULT_MODEL=\"glm-5\" ;;\n 3|kimi-k2.5) AGENTTEAMS_DEFAULT_MODEL=\"kimi-k2.5\" ;;\n 4|MiniMax-M2.5) AGENTTEAMS_DEFAULT_MODEL=\"MiniMax-M2.5\" ;;\n *) AGENTTEAMS_DEFAULT_MODEL=\"qwen3.6-plus\" ;;\n esac\n log \"$(msg llm.provider.selected_codingplan)\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n else\n echo \"\"\n echo \"$(msg llm.alibaba.models_title)\"\n echo \"$(msg llm.alibaba.model.tokenplan)\"\n echo \"$(msg llm.alibaba.model.bailian)\"\n echo \"$(msg llm.alibaba.model.codingplan_legacy)\"\n echo \"\"\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_OPENAI_BASE_URL}\" ]; then\n # Determine alibaba access type from loaded URL\n local _access_default=\"1\"\n if echo \"${AGENTTEAMS_OPENAI_BASE_URL}\" | grep -q \"token-plan\"; then\n _access_default=\"1\"\n elif echo \"${AGENTTEAMS_OPENAI_BASE_URL}\" | grep -q \"dashscope\"; then\n if echo \"${AGENTTEAMS_OPENAI_BASE_URL}\" | grep -q \"intl\"; then\n _access_default=\"3\"\n else\n _access_default=\"2\"\n fi\n fi\n read -e -p \"$(msg llm.alibaba.model.select) [${_access_default}]: \" ALIBABA_MODEL_CHOICE\n ALIBABA_MODEL_CHOICE=\"${ALIBABA_MODEL_CHOICE:-${_access_default}}\"\n elif [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ]; then\n read -e -p \"$(msg llm.alibaba.model.select) [1]: \" ALIBABA_MODEL_CHOICE\n ALIBABA_MODEL_CHOICE=\"${ALIBABA_MODEL_CHOICE:-1}\"\n else\n read -e -p \"$(msg llm.alibaba.model.select): \" ALIBABA_MODEL_CHOICE\n ALIBABA_MODEL_CHOICE=\"${ALIBABA_MODEL_CHOICE:-1}\"\n fi\n if [ \"${ALIBABA_MODEL_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n case \"${ALIBABA_MODEL_CHOICE}\" in\n 1|token-plan|tokenplan)\n ALIBABA_ACCESS=\"tokenplan\"\n AGENTTEAMS_LLM_PROVIDER=\"openai-compat\"\n AGENTTEAMS_OPENAI_BASE_URL=\"https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1\"\n echo \"\"\n echo \"$(msg llm.codingplan.models_title)\"\n echo \"$(msg llm.codingplan.model.qwen36plus)\"\n echo \"$(msg llm.codingplan.model.glm5)\"\n echo \"$(msg llm.codingplan.model.kimi)\"\n echo \"$(msg llm.codingplan.model.minimax)\"\n echo \"\"\n local CODINGPLAN_MODEL_CHOICE\n # If upgrade with loaded model, show current as default\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_DEFAULT_MODEL}\" ]; then\n local _model_default\n case \"${AGENTTEAMS_DEFAULT_MODEL}\" in\n qwen3.6-plus) _model_default=\"1\" ;;\n glm-5) _model_default=\"2\" ;;\n kimi-k2.5) _model_default=\"3\" ;;\n MiniMax-M2.5) _model_default=\"4\" ;;\n *) _model_default=\"1\" ;;\n esac\n read -e -p \"$(msg llm.codingplan.model.select) [${_model_default}]: \" CODINGPLAN_MODEL_CHOICE\n CODINGPLAN_MODEL_CHOICE=\"${CODINGPLAN_MODEL_CHOICE:-${_model_default}}\"\n elif [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ]; then\n read -e -p \"$(msg llm.codingplan.model.select) [1]: \" CODINGPLAN_MODEL_CHOICE\n CODINGPLAN_MODEL_CHOICE=\"${CODINGPLAN_MODEL_CHOICE:-1}\"\n else\n read -e -p \"$(msg llm.codingplan.model.select): \" CODINGPLAN_MODEL_CHOICE\n CODINGPLAN_MODEL_CHOICE=\"${CODINGPLAN_MODEL_CHOICE:-1}\"\n fi\n if [ \"${CODINGPLAN_MODEL_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n case \"${CODINGPLAN_MODEL_CHOICE}\" in\n 1|qwen3.6-plus) AGENTTEAMS_DEFAULT_MODEL=\"qwen3.6-plus\" ;;\n 2|glm-5) AGENTTEAMS_DEFAULT_MODEL=\"glm-5\" ;;\n 3|kimi-k2.5) AGENTTEAMS_DEFAULT_MODEL=\"kimi-k2.5\" ;;\n 4|MiniMax-M2.5) AGENTTEAMS_DEFAULT_MODEL=\"MiniMax-M2.5\" ;;\n *) AGENTTEAMS_DEFAULT_MODEL=\"qwen3.6-plus\" ;;\n esac\n log \"$(msg llm.provider.selected_tokenplan)\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n ;;\n 2|qwen|bailian)\n ALIBABA_ACCESS=\"bailian\"\n AGENTTEAMS_LLM_PROVIDER=\"qwen\"\n AGENTTEAMS_OPENAI_BASE_URL=\"\"\n echo \"\"\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_DEFAULT_MODEL}\" ]; then\n read -e -p \"$(msg llm.qwen.model_prompt) [${AGENTTEAMS_DEFAULT_MODEL}]: \" AGENTTEAMS_DEFAULT_MODEL\n else\n read -e -p \"$(msg llm.qwen.model_prompt) [qwen3.6-plus]: \" AGENTTEAMS_DEFAULT_MODEL\n fi\n if [ \"${AGENTTEAMS_DEFAULT_MODEL}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_DEFAULT_MODEL=\"${AGENTTEAMS_DEFAULT_MODEL:-qwen3.6-plus}\"\n log \"$(msg llm.provider.selected_qwen)\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n prompt_custom_model_params \"${AGENTTEAMS_DEFAULT_MODEL}\" || return 0\n ;;\n 3|coding-plan|codingplan)\n ALIBABA_ACCESS=\"codingplan_legacy\"\n AGENTTEAMS_LLM_PROVIDER=\"openai-compat\"\n AGENTTEAMS_OPENAI_BASE_URL=\"https://coding.dashscope.aliyuncs.com/v1\"\n echo \"\"\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_DEFAULT_MODEL}\" ]; then\n read -e -p \"$(msg llm.qwen.model_prompt) [${AGENTTEAMS_DEFAULT_MODEL}]: \" AGENTTEAMS_DEFAULT_MODEL\n else\n read -e -p \"$(msg llm.qwen.model_prompt): \" AGENTTEAMS_DEFAULT_MODEL\n fi\n if [ \"${AGENTTEAMS_DEFAULT_MODEL}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_DEFAULT_MODEL=\"${AGENTTEAMS_DEFAULT_MODEL:-qwen3.6-plus}\"\n log \"$(msg llm.provider.selected_codingplan_legacy)\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n prompt_custom_model_params \"${AGENTTEAMS_DEFAULT_MODEL}\" || return 0\n ;;\n *)\n die \"$(msg llm.alibaba.model.invalid \"${ALIBABA_MODEL_CHOICE}\")\"\n ;;\n esac\n fi\n if [ \"${ALIBABA_ACCESS}\" = \"bailian\" ]; then\n log \"$(msg llm.apikey_hint_bailian)\"\n log \"$(msg llm.apikey_url_bailian)\"\n elif [ \"${AGENTTEAMS_LANGUAGE}\" = \"en\" ]; then\n log \"$(msg llm.apikey_hint_qwencloud)\"\n log \"$(msg llm.apikey_url_qwencloud)\"\n elif [ \"${ALIBABA_ACCESS}\" = \"codingplan_legacy\" ]; then\n log \"$(msg llm.apikey_hint_codingplan)\"\n log \"$(msg llm.apikey_url_codingplan)\"\n else\n log \"$(msg llm.apikey_hint_tokenplan)\"\n log \"$(msg llm.apikey_url_tokenplan)\"\n fi\n log \"\"\n prompt AGENTTEAMS_LLM_API_KEY \"$(msg llm.apikey_prompt)\" \"\" \"true\" || return 0\n if [ \"${ALIBABA_ACCESS}\" = \"bailian\" ]; then\n test_llm_connectivity \"https://dashscope.aliyuncs.com/compatible-mode/v1\" \"${AGENTTEAMS_LLM_API_KEY}\" \"${AGENTTEAMS_DEFAULT_MODEL}\" || return 0\n elif [ \"${ALIBABA_ACCESS}\" = \"codingplan_legacy\" ]; then\n test_llm_connectivity \"https://coding.dashscope.aliyuncs.com/v1\" \"${AGENTTEAMS_LLM_API_KEY}\" \"${AGENTTEAMS_DEFAULT_MODEL}\" \"$(msg llm.openai.test.fail.codingplan_legacy)\" || return 0\n elif [ \"${AGENTTEAMS_LANGUAGE}\" = \"en\" ]; then\n test_llm_connectivity \"${AGENTTEAMS_OPENAI_BASE_URL}\" \"${AGENTTEAMS_LLM_API_KEY}\" \"${AGENTTEAMS_DEFAULT_MODEL}\" \"$(msg llm.openai.test.fail.codingplan)\" || return 0\n else\n test_llm_connectivity \"${AGENTTEAMS_OPENAI_BASE_URL}\" \"${AGENTTEAMS_LLM_API_KEY}\" \"${AGENTTEAMS_DEFAULT_MODEL}\" \"$(msg llm.openai.test.fail.tokenplan)\" || return 0\n fi\n ;;\n 2|openai-compat)\n AGENTTEAMS_LLM_PROVIDER=\"openai-compat\"\n log \"$(msg llm.provider.selected_openai \"${AGENTTEAMS_LLM_PROVIDER}\")\"\n echo \"\"\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg llm.openai.base_url_prompt)\" \"${AGENTTEAMS_OPENAI_BASE_URL}\")\"\n else\n _current=\"${AGENTTEAMS_OPENAI_BASE_URL}\"\n if [ -n \"${_current}\" ]; then\n read -e -p \"$(msg llm.openai.base_url_prompt) [${_current}]: \" AGENTTEAMS_OPENAI_BASE_URL\n else\n read -e -p \"$(msg llm.openai.base_url_prompt) [https://api.openai.com/v1]: \" AGENTTEAMS_OPENAI_BASE_URL\n fi\n if [ \"${AGENTTEAMS_OPENAI_BASE_URL}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_OPENAI_BASE_URL=\"${AGENTTEAMS_OPENAI_BASE_URL:-${_current}}\"\n [ -z \"${AGENTTEAMS_OPENAI_BASE_URL}\" ] && AGENTTEAMS_OPENAI_BASE_URL=\"https://api.openai.com/v1\"\n fi\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg llm.openai.model_prompt)\" \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n else\n _current=\"${AGENTTEAMS_DEFAULT_MODEL}\"\n if [ -n \"${_current}\" ]; then\n read -e -p \"$(msg llm.openai.model_prompt) [${_current}]: \" AGENTTEAMS_DEFAULT_MODEL\n else\n read -e -p \"$(msg llm.openai.model_prompt) [gpt-5.4]: \" AGENTTEAMS_DEFAULT_MODEL\n fi\n if [ \"${AGENTTEAMS_DEFAULT_MODEL}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_DEFAULT_MODEL=\"${AGENTTEAMS_DEFAULT_MODEL:-${_current}}\"\n [ -z \"${AGENTTEAMS_DEFAULT_MODEL}\" ] && AGENTTEAMS_DEFAULT_MODEL=\"gpt-5.4\"\n fi\n log \"$(msg llm.openai.base_url_label \"${AGENTTEAMS_OPENAI_BASE_URL}\")\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n prompt_custom_model_params \"${AGENTTEAMS_DEFAULT_MODEL}\" || return 0\n log \"\"\n prompt AGENTTEAMS_LLM_API_KEY \"$(msg llm.apikey_prompt)\" \"\" \"true\" || return 0\n test_llm_connectivity \"${AGENTTEAMS_OPENAI_BASE_URL}\" \"${AGENTTEAMS_LLM_API_KEY}\" \"${AGENTTEAMS_DEFAULT_MODEL}\" || return 0\n ;;\n *)\n die \"$(msg llm.provider.invalid \"${PROVIDER_CHOICE}\")\"\n ;;\n esac\n\n # Skip to embedding if Keep-All mode handled LLM params\n if [ \"${SKIP_TO_EMBEDDING}\" = \"1\" ]; then\n if [ -n \"${AGENTTEAMS_EMBEDDING_MODEL}\" ]; then\n log \"$(msg llm.embedding.title)\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_EMBEDDING_MODEL}\")\"\n log \"\"\n fi\n return 0\n fi\n\n # --- Embedding model (optional, auto-tested) ---\n echo \"\"\n log \"$(msg llm.embedding.title)\"\n log \"$(msg llm.embedding.hint)\"\n echo \"\"\n echo \"$(msg llm.embedding.option.default)\"\n echo \"$(msg llm.embedding.option.custom)\"\n echo \"$(msg llm.embedding.option.disable)\"\n echo \"\"\n local EMB_CHOICE\n read -e -p \"$(msg llm.embedding.select) [1]: \" EMB_CHOICE\n EMB_CHOICE=\"${EMB_CHOICE:-1}\"\n if [ \"${EMB_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n\n case \"${EMB_CHOICE}\" in\n 1)\n AGENTTEAMS_EMBEDDING_MODEL=\"text-embedding-v4\"\n ;;\n 2)\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg llm.embedding.custom_prompt)\" \"${AGENTTEAMS_EMBEDDING_MODEL}\")\"\n else\n _current=\"${AGENTTEAMS_EMBEDDING_MODEL}\"\n if [ -n \"${_current}\" ]; then\n read -e -p \"$(msg llm.embedding.custom_prompt) [${_current}]: \" AGENTTEAMS_EMBEDDING_MODEL\n else\n read -e -p \"$(msg llm.embedding.custom_prompt): \" AGENTTEAMS_EMBEDDING_MODEL\n fi\n if [ \"${AGENTTEAMS_EMBEDDING_MODEL}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_EMBEDDING_MODEL=\"${AGENTTEAMS_EMBEDDING_MODEL:-${_current}}\"\n fi\n ;;\n 3)\n AGENTTEAMS_EMBEDDING_MODEL=\"\"\n log \"$(msg llm.embedding.disabled)\"\n ;;\n *)\n AGENTTEAMS_EMBEDDING_MODEL=\"text-embedding-v4\"\n ;;\n esac\n\n if [ -n \"${AGENTTEAMS_EMBEDDING_MODEL}\" ]; then\n # Qwen provider uses dashscope directly; others use OPENAI_BASE_URL\n local EMB_BASE_URL=\"${AGENTTEAMS_OPENAI_BASE_URL}\"\n if [ \"${AGENTTEAMS_LLM_PROVIDER}\" = \"qwen\" ]; then\n EMB_BASE_URL=\"https://dashscope.aliyuncs.com/compatible-mode/v1\"\n fi\n if ! test_embedding_connectivity \"${EMB_BASE_URL}\" \"${AGENTTEAMS_LLM_API_KEY}\" \"${AGENTTEAMS_EMBEDDING_MODEL}\"; then\n AGENTTEAMS_EMBEDDING_MODEL=\"\"\n log \"$(msg llm.embedding.auto_disabled)\"\n fi\n fi\n\n export AGENTTEAMS_LLM_PROVIDER AGENTTEAMS_DEFAULT_MODEL\n [ -n \"${AGENTTEAMS_OPENAI_BASE_URL+x}\" ] && export AGENTTEAMS_OPENAI_BASE_URL\n log \"\"\n}\n\nstep_admin() {\n log \"$(msg admin.title)\"\n prompt AGENTTEAMS_ADMIN_USER \"$(msg admin.username_prompt)\" \"admin\" || return 0\n AGENTTEAMS_ADMIN_USER=\"$(printf '%s' \"${AGENTTEAMS_ADMIN_USER}\" | tr '[:upper:]' '[:lower:]')\"\n\n # Pre-set via env var: validate; in non-interactive mode fail fast,\n # in interactive mode warn and fall through to the retry prompt.\n if [ -n \"${AGENTTEAMS_ADMIN_PASSWORD:-}\" ]; then\n log \" $(msg prompt.preset \"$(msg admin.password_prompt)\")\"\n if [ ${#AGENTTEAMS_ADMIN_PASSWORD} -ge 8 ]; then\n log \"\"\n return 0\n fi\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n die \"$(msg admin.password_too_short \"${#AGENTTEAMS_ADMIN_PASSWORD}\")\"\n fi\n error \"$(msg admin.password_too_short \"${#AGENTTEAMS_ADMIN_PASSWORD}\")\"\n unset AGENTTEAMS_ADMIN_PASSWORD\n fi\n\n while true; do\n unset AGENTTEAMS_ADMIN_PASSWORD\n prompt_optional AGENTTEAMS_ADMIN_PASSWORD \"$(msg admin.password_prompt)\" \"true\" || return 0\n if [ -z \"${AGENTTEAMS_ADMIN_PASSWORD}\" ]; then\n AGENTTEAMS_ADMIN_PASSWORD=\"admin$(openssl rand -hex 6)\"\n log \"$(msg admin.password_generated)\"\n break\n fi\n if [ ${#AGENTTEAMS_ADMIN_PASSWORD} -ge 8 ]; then\n break\n fi\n error \"$(msg admin.password_too_short \"${#AGENTTEAMS_ADMIN_PASSWORD}\")\"\n done\n log \"\"\n}\n\nstep_network() {\n log \"$(msg port.local_only.title)\"\n echo \"\"\n echo \" 1) $(msg port.local_only.hint_yes)\"\n echo \" 2) $(msg port.local_only.hint_no)\"\n echo \"\"\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_LOCAL_ONLY=\"${AGENTTEAMS_LOCAL_ONLY:-1}\"\n elif [ -z \"${AGENTTEAMS_LOCAL_ONLY+x}\" ]; then\n local _local_choice\n read -e -p \"$(msg port.local_only.choice): \" _local_choice\n if [ \"${_local_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _local_choice=\"${_local_choice:-1}\"\n case \"${_local_choice}\" in\n 2|n|N|no|NO) AGENTTEAMS_LOCAL_ONLY=\"0\" ;;\n *) AGENTTEAMS_LOCAL_ONLY=\"1\" ;;\n esac\n fi\n export AGENTTEAMS_LOCAL_ONLY\n if [ \"${AGENTTEAMS_LOCAL_ONLY}\" = \"1\" ]; then\n log \"$(msg port.local_only.selected_local)\"\n else\n log \"$(msg port.local_only.selected_external)\"\n echo \"\"\n echo -e \"\\033[33m$(msg port.local_only.https_hint)\\033[0m\"\n fi\n}\n\nstep_ports() {\n log \"$(msg port.title)\"\n prompt AGENTTEAMS_PORT_GATEWAY \"$(msg port.gateway_prompt)\" \"18080\" || return 0\n prompt AGENTTEAMS_PORT_CONSOLE \"$(msg port.console_prompt)\" \"18001\" || return 0\n prompt AGENTTEAMS_PORT_ELEMENT_WEB \"$(msg port.element_prompt)\" \"18088\" || return 0\n prompt AGENTTEAMS_PORT_MANAGER_CONSOLE \"$(msg port.manager_console_prompt)\" \"18888\" || return 0\n log \"\"\n}\n\nstep_domains() {\n log \"$(msg domain.title)\"\n log \"$(msg domain.hint)\"\n prompt AGENTTEAMS_MATRIX_DOMAIN \"$(msg domain.matrix_prompt)\" \"matrix-local.agentteams.io:${AGENTTEAMS_PORT_GATEWAY}\" || return 0\n prompt AGENTTEAMS_MATRIX_CLIENT_DOMAIN \"$(msg domain.element_prompt)\" \"matrix-client-local.agentteams.io\" || return 0\n prompt AGENTTEAMS_AI_GATEWAY_DOMAIN \"$(msg domain.gateway_prompt)\" \"aigw-local.agentteams.io\" || return 0\n prompt AGENTTEAMS_FS_DOMAIN \"$(msg domain.fs_prompt)\" \"fs-local.agentteams.io\" || return 0\n if [ \"${AGENTTEAMS_MANAGER_RUNTIME}\" != \"copaw\" ]; then\n prompt AGENTTEAMS_CONSOLE_DOMAIN \"$(msg domain.console_prompt)\" \"console-local.agentteams.io\" || return 0\n fi\n log \"\"\n}\n\nstep_github() {\n log \"$(msg github.title)\"\n prompt_optional AGENTTEAMS_GITHUB_TOKEN \"$(msg github.token_prompt)\" \"true\" || return 0\n}\n\nstep_skills() {\n log \"\"\n log \"$(msg skills.title)\"\n prompt_optional AGENTTEAMS_SKILLS_API_URL \"$(msg skills.url_prompt)\" || return 0\n log \"\"\n}\n\nstep_volume() {\n log \"$(msg data.title)\"\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_DATA_DIR=\"${AGENTTEAMS_DATA_DIR:-agentteams-data}\"\n log \" $(msg data.volume_using \"${AGENTTEAMS_DATA_DIR}\") (non-interactive, skipped)\"\n export AGENTTEAMS_DATA_DIR\n return 0\n fi\n # ─────────────────────────────────────────────────────────────────\n if [ -z \"${AGENTTEAMS_DATA_DIR+x}\" ]; then\n local _input\n read -e -p \"$(msg data.volume_prompt): \" _input\n if [ \"${_input}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_DATA_DIR=\"${_input:-agentteams-data}\"\n export AGENTTEAMS_DATA_DIR\n fi\n AGENTTEAMS_DATA_DIR=\"${AGENTTEAMS_DATA_DIR:-agentteams-data}\"\n log \"$(msg data.volume_using \"${AGENTTEAMS_DATA_DIR}\")\"\n}\n\nstep_workspace() {\n log \"$(msg workspace.title)\"\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_WORKSPACE_DIR=\"${AGENTTEAMS_WORKSPACE_DIR:-${HOME}/agentteams-manager}\"\n AGENTTEAMS_WORKSPACE_DIR=\"$(cd \"${AGENTTEAMS_WORKSPACE_DIR}\" 2>/dev/null && pwd || echo \"${AGENTTEAMS_WORKSPACE_DIR}\")\"\n mkdir -p \"${AGENTTEAMS_WORKSPACE_DIR}\"\n log \" $(msg workspace.dir_label \"${AGENTTEAMS_WORKSPACE_DIR}\") (non-interactive, skipped)\"\n export AGENTTEAMS_WORKSPACE_DIR\n return 0\n fi\n # ─────────────────────────────────────────────────────────────────\n local _input\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg workspace.dir_prompt \"${HOME}/agentteams-manager\")\" \"${AGENTTEAMS_WORKSPACE_DIR}\")\"\n _input=\"${AGENTTEAMS_WORKSPACE_DIR}\"\n else\n local _current=\"${AGENTTEAMS_WORKSPACE_DIR}\"\n if [ -n \"${_current}\" ]; then\n read -e -p \"$(msg workspace.dir_prompt \"${HOME}/agentteams-manager\") [${_current}]: \" _input\n else\n read -e -p \"$(msg workspace.dir_prompt \"${HOME}/agentteams-manager\"): \" _input\n fi\n if [ \"${_input}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _input=\"${_input:-${_current}}\"\n fi\n AGENTTEAMS_WORKSPACE_DIR=\"${_input:-${HOME}/agentteams-manager}\"\n export AGENTTEAMS_WORKSPACE_DIR\n AGENTTEAMS_WORKSPACE_DIR=\"$(cd \"${AGENTTEAMS_WORKSPACE_DIR}\" 2>/dev/null && pwd || echo \"${AGENTTEAMS_WORKSPACE_DIR}\")\"\n mkdir -p \"${AGENTTEAMS_WORKSPACE_DIR}\"\n log \"$(msg workspace.dir_label \"${AGENTTEAMS_WORKSPACE_DIR}\")\"\n}\n\nstep_dashboard() {\n AGENTTEAMS_DASHBOARD=\"${AGENTTEAMS_DASHBOARD:-1}\"\n AGENTTEAMS_DASHBOARD_VERSION=\"${AGENTTEAMS_DASHBOARD_VERSION:-v1.2.0-beta.1}\"\n AGENTTEAMS_PORT_DASHBOARD=\"${AGENTTEAMS_PORT_DASHBOARD:-13000}\"\n AGENTTEAMS_AI_GATEWAY_ADMIN_URL=\"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL:-}\"\n\n # Compute the default image for the current version.\n # We track whether the current image matches the derived default so that\n # changing the version also updates the image (unless the user has\n # explicitly overridden it to a non-default value).\n _dashboard_default_image() {\n echo \"${AGENTTEAMS_REGISTRY}/agentteams/agentteams-dashboard:${1:-${AGENTTEAMS_DASHBOARD_VERSION}}\"\n }\n local _init_default\n _init_default=$(_dashboard_default_image)\n if [ -z \"${AGENTTEAMS_DASHBOARD_IMAGE:-}\" ]; then\n AGENTTEAMS_DASHBOARD_IMAGE=\"${_init_default}\"\n fi\n\n # Recompute default image when the version changes AND the current image\n # matches the default for the *previous* (env-file) version.\n # This handles non-interactive upgrades correctly:\n # - env file loads old default image + old version\n # - CLI passes new version\n # - image should follow (because it was auto-derived before)\n # But preserves truly custom images (same repo but custom tag like :canary)\n # because they won't match the old-version default exactly.\n local _old_saved_version=\"\"\n local _env_file=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n if [ -f \"${_env_file}\" ]; then\n _old_saved_version=\"$(grep '^AGENTTEAMS_DASHBOARD_VERSION=' \"${_env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n fi\n if [ -n \"${_old_saved_version}\" ] && \\\n [ \"${_old_saved_version}\" != \"${AGENTTEAMS_DASHBOARD_VERSION}\" ] && \\\n [ \"${AGENTTEAMS_DASHBOARD_IMAGE}\" = \"$(_dashboard_default_image \"${_old_saved_version}\")\" ]; then\n AGENTTEAMS_DASHBOARD_IMAGE=$(_dashboard_default_image)\n fi\n\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_DASHBOARD=\"${AGENTTEAMS_DASHBOARD:-1}\"\n if [ \"${AGENTTEAMS_DASHBOARD}\" = \"1\" ]; then\n log \"$(msg dash.summary \"${AGENTTEAMS_PORT_DASHBOARD}\" \"${AGENTTEAMS_DASHBOARD_IMAGE}\") (non-interactive)\"\n else\n log \"$(msg dash.skip) (non-interactive)\"\n fi\n export AGENTTEAMS_DASHBOARD AGENTTEAMS_DASHBOARD_VERSION AGENTTEAMS_PORT_DASHBOARD AGENTTEAMS_DASHBOARD_IMAGE AGENTTEAMS_AI_GATEWAY_ADMIN_URL\n return 0\n fi\n\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ]; then\n log \"$(msg prompt.upgrade_keep \"\" \"${AGENTTEAMS_DASHBOARD}\") dashboard=${AGENTTEAMS_DASHBOARD} version=${AGENTTEAMS_DASHBOARD_VERSION} port=${AGENTTEAMS_PORT_DASHBOARD}\"\n export AGENTTEAMS_DASHBOARD AGENTTEAMS_DASHBOARD_VERSION AGENTTEAMS_PORT_DASHBOARD AGENTTEAMS_DASHBOARD_IMAGE AGENTTEAMS_AI_GATEWAY_ADMIN_URL\n return 0\n fi\n\n local _input\n echo \"\"\n read -p \"$(msg dash.prompt) [Y/n]: \" _input\n case \"${_input:-}\" in\n [nN0]) AGENTTEAMS_DASHBOARD=0 ;;\n *) AGENTTEAMS_DASHBOARD=1 ;;\n esac\n if [ \"${_input}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n\n if [ \"${AGENTTEAMS_DASHBOARD}\" != \"1\" ]; then\n log \"$(msg dash.skip)\"\n export AGENTTEAMS_DASHBOARD AGENTTEAMS_DASHBOARD_VERSION AGENTTEAMS_PORT_DASHBOARD AGENTTEAMS_DASHBOARD_IMAGE AGENTTEAMS_AI_GATEWAY_ADMIN_URL\n return 0\n fi\n\n # Dashboard version\n local _current_version=\"${AGENTTEAMS_DASHBOARD_VERSION}\"\n local _old_version=\"${AGENTTEAMS_DASHBOARD_VERSION}\"\n local _old_default_image\n _old_default_image=$(_dashboard_default_image \"${_old_version}\")\n read -p \"$(msg dash.version_prompt) [${_current_version}]: \" _input\n AGENTTEAMS_DASHBOARD_VERSION=\"${_input:-${_current_version}}\"\n\n # Dashboard port\n local _current_port=\"${AGENTTEAMS_PORT_DASHBOARD}\"\n read -p \"$(msg dash.port_prompt) [${_current_port}]: \" _input\n AGENTTEAMS_PORT_DASHBOARD=\"${_input:-${_current_port}}\"\n\n # Dashboard image: if the current image matches the default derived from\n # the OLD version, recompute it from the NEW version. This handles both\n # fresh installs and upgrades where the previous image was the default.\n # Only a real user override (image != default for old version) is preserved.\n if [ \"${AGENTTEAMS_DASHBOARD_VERSION}\" != \"${_old_version}\" ] && \\\n [ \"${AGENTTEAMS_DASHBOARD_IMAGE}\" = \"${_old_default_image}\" ]; then\n AGENTTEAMS_DASHBOARD_IMAGE=$(_dashboard_default_image)\n fi\n local _current_image=\"${AGENTTEAMS_DASHBOARD_IMAGE}\"\n read -p \"$(msg dash.image_prompt) [${_current_image}]: \" _input\n if [ -n \"${_input}\" ]; then\n AGENTTEAMS_DASHBOARD_IMAGE=\"${_input}\"\n fi\n\n # Higress Console URL (explicit config takes priority; auto-detect as fallback)\n local _higress_url=\"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\"\n if [ -z \"${_higress_url}\" ]; then\n if ${DOCKER_CMD} ps --format '{{.Names}}' | grep -q \"^agentteams-controller$\"; then\n if ${DOCKER_CMD} exec agentteams-controller curl -sf --max-time 2 http://127.0.0.1:8001/ >/dev/null 2>&1; then\n _higress_url=\"http://agentteams-controller:8001\"\n log \"$(msg dash.auto_url): ${_higress_url}\"\n fi\n fi\n fi\n read -p \"$(msg dash.gateway_prompt) [${_higress_url:-<skip>}]: \" _input\n AGENTTEAMS_AI_GATEWAY_ADMIN_URL=\"${_input:-${_higress_url}}\"\n\n # Normalize URL: add http:// prefix if missing\n if [ -n \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\" ]; then\n case \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\" in\n http://*|https://*) ;;\n *) AGENTTEAMS_AI_GATEWAY_ADMIN_URL=\"http://${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\" ;;\n esac\n fi\n\n # Verify the Higress Console URL is reachable (best-effort warning only)\n if [ -n \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\" ]; then\n local _gw_reachable=0\n # If the URL points to the controller container, test from inside the container\n if echo \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\" | grep -qE \"(agentteams-controller|hiclaw-controller|127\\.0\\.0\\.1|localhost)\"; then\n if ${DOCKER_CMD} ps --format '{{.Names}}' | grep -q \"^agentteams-controller$\"; then\n if ${DOCKER_CMD} exec agentteams-controller curl -sf --max-time 3 \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}/\" >/dev/null 2>&1; then\n _gw_reachable=1\n fi\n fi\n else\n # External URL — test from host\n if curl -sf --max-time 3 \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}/\" >/dev/null 2>&1; then\n _gw_reachable=1\n fi\n fi\n if [ \"${_gw_reachable}\" = \"0\" ]; then\n log \"WARNING: Higress Console URL may not be reachable: ${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\"\n log \" Dashboard will still work, but shared login via Higress Console may fail.\"\n log \" To fix: verify the URL is correct and the Higress Console service is running.\"\n fi\n fi\n\n log \"$(msg dash.summary \"${AGENTTEAMS_PORT_DASHBOARD}\" \"${AGENTTEAMS_DASHBOARD_IMAGE}\")\"\n export AGENTTEAMS_DASHBOARD AGENTTEAMS_DASHBOARD_VERSION AGENTTEAMS_PORT_DASHBOARD AGENTTEAMS_DASHBOARD_IMAGE AGENTTEAMS_AI_GATEWAY_ADMIN_URL\n}\n\nstep_runtime() {\n log \"$(msg worker_runtime.title)\"\n echo \"\"\n echo \" 1) $(msg worker_runtime.copaw)\"\n echo \" 2) $(msg worker_runtime.openclaw)\"\n if ! _ver_lt \"${AGENTTEAMS_VERSION}\" \"v1.1.0\"; then\n echo \" 3) $(msg worker_runtime.hermes)\"\n fi\n echo \"\"\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"${AGENTTEAMS_DEFAULT_WORKER_RUNTIME:-copaw}\"\n elif [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_DEFAULT_WORKER_RUNTIME}\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg worker_runtime.title_short)\" \"${AGENTTEAMS_DEFAULT_WORKER_RUNTIME}\")\"\n local _runtime_choice\n read -e -p \"$(msg worker_runtime.choice): \" _runtime_choice\n if [ \"${_runtime_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n if [ -n \"${_runtime_choice}\" ]; then\n case \"${_runtime_choice}\" in\n 2) AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"openclaw\" ;;\n 3) if ! _ver_lt \"${AGENTTEAMS_VERSION}\" \"v1.1.0\"; then\n AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"hermes\"\n else\n AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"copaw\"\n fi ;;\n *) AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"copaw\" ;;\n esac\n fi\n elif [ -z \"${AGENTTEAMS_DEFAULT_WORKER_RUNTIME+x}\" ]; then\n local _runtime_choice\n read -e -p \"$(msg worker_runtime.choice): \" _runtime_choice\n if [ \"${_runtime_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _runtime_choice=\"${_runtime_choice:-1}\"\n case \"${_runtime_choice}\" in\n 2) AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"openclaw\" ;;\n 3) if ! _ver_lt \"${AGENTTEAMS_VERSION}\" \"v1.1.0\"; then\n AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"hermes\"\n else\n AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"copaw\"\n fi ;;\n *) AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"copaw\" ;;\n esac\n fi\n export AGENTTEAMS_DEFAULT_WORKER_RUNTIME\n log \"$(msg worker_runtime.selected \"${AGENTTEAMS_DEFAULT_WORKER_RUNTIME}\")\"\n}\n\nstep_manager_runtime() {\n log \"$(msg manager_runtime.title)\"\n echo \"\"\n echo \" 1) $(msg manager_runtime.copaw)\"\n echo \" 2) $(msg manager_runtime.openclaw)\"\n echo \"\"\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_MANAGER_RUNTIME=\"${AGENTTEAMS_MANAGER_RUNTIME:-copaw}\"\n elif [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_MANAGER_RUNTIME}\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg manager_runtime.title_short)\" \"${AGENTTEAMS_MANAGER_RUNTIME}\")\"\n local _runtime_choice\n read -e -p \"$(msg manager_runtime.choice): \" _runtime_choice\n if [ \"${_runtime_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n if [ -n \"${_runtime_choice}\" ]; then\n case \"${_runtime_choice}\" in\n 2) AGENTTEAMS_MANAGER_RUNTIME=\"openclaw\" ;;\n *) AGENTTEAMS_MANAGER_RUNTIME=\"copaw\" ;;\n esac\n fi\n elif [ -z \"${AGENTTEAMS_MANAGER_RUNTIME+x}\" ]; then\n local _runtime_choice\n read -e -p \"$(msg manager_runtime.choice): \" _runtime_choice\n if [ \"${_runtime_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _runtime_choice=\"${_runtime_choice:-1}\"\n case \"${_runtime_choice}\" in\n 2) AGENTTEAMS_MANAGER_RUNTIME=\"openclaw\" ;;\n *) AGENTTEAMS_MANAGER_RUNTIME=\"copaw\" ;;\n esac\n fi\n export AGENTTEAMS_MANAGER_RUNTIME\n log \"$(msg manager_runtime.selected \"${AGENTTEAMS_MANAGER_RUNTIME}\")\"\n}\n\nstep_e2ee() {\n log \"\"\n log \"$(msg matrix_e2ee.title)\"\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_MATRIX_E2EE=\"${AGENTTEAMS_MATRIX_E2EE:-0}\"\n log \" $(msg matrix_e2ee.title_short) = ${AGENTTEAMS_MATRIX_E2EE} (non-interactive, skipped)\"\n export AGENTTEAMS_MATRIX_E2EE\n return 0\n fi\n # ─────────────────────────────────────────────────────────────────\n echo \"\"\n echo -e \" $(msg matrix_e2ee.desc)\"\n echo \"\"\n echo \" 1) $(msg matrix_e2ee.disable)\"\n echo \" 2) $(msg matrix_e2ee.enable)\"\n echo \"\"\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_MATRIX_E2EE}\" ]; then\n local _e2ee_display; if [ \"${AGENTTEAMS_MATRIX_E2EE}\" = \"1\" ]; then _e2ee_display=\"$(msg matrix_e2ee.val_enabled)\"; else _e2ee_display=\"$(msg matrix_e2ee.val_disabled)\"; fi\n log \"$(msg prompt.upgrade_keep \"$(msg matrix_e2ee.title_short)\" \"${_e2ee_display}\")\"\n local _e2ee_choice\n read -e -p \"$(msg matrix_e2ee.choice): \" _e2ee_choice\n if [ \"${_e2ee_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n if [ -n \"${_e2ee_choice}\" ]; then\n case \"${_e2ee_choice}\" in\n 2) AGENTTEAMS_MATRIX_E2EE=\"1\" ;;\n *) AGENTTEAMS_MATRIX_E2EE=\"0\" ;;\n esac\n fi\n elif [ -z \"${AGENTTEAMS_MATRIX_E2EE+x}\" ]; then\n local _e2ee_choice\n read -e -p \"$(msg matrix_e2ee.choice): \" _e2ee_choice\n if [ \"${_e2ee_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _e2ee_choice=\"${_e2ee_choice:-1}\"\n case \"${_e2ee_choice}\" in\n 2) AGENTTEAMS_MATRIX_E2EE=\"1\" ;;\n *) AGENTTEAMS_MATRIX_E2EE=\"0\" ;;\n esac\n fi\n AGENTTEAMS_MATRIX_E2EE=\"${AGENTTEAMS_MATRIX_E2EE:-0}\"\n export AGENTTEAMS_MATRIX_E2EE\n if [ \"${AGENTTEAMS_MATRIX_E2EE}\" = \"1\" ]; then\n log \"$(msg matrix_e2ee.selected_enabled)\"\n else\n log \"$(msg matrix_e2ee.selected_disabled)\"\n fi\n}\n\nstep_docker_proxy() {\n # Only relevant when socket mounting is enabled\n if [ \"${AGENTTEAMS_MOUNT_SOCKET}\" != \"1\" ]; then\n AGENTTEAMS_DOCKER_PROXY=\"0\"\n return 0\n fi\n\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_DOCKER_PROXY=\"${AGENTTEAMS_DOCKER_PROXY:-0}\"\n log \" $(msg docker_proxy.title_short) = ${AGENTTEAMS_DOCKER_PROXY} (non-interactive, skipped)\"\n export AGENTTEAMS_DOCKER_PROXY\n return 0\n fi\n # ─────────────────────────────────────────────────────────────────\n\n echo \"\"\n echo -e \" \\033[1m$(msg docker_proxy.title)\\033[0m\"\n echo \"\"\n echo -e \" $(msg docker_proxy.desc)\"\n echo \"\"\n echo \" 1) $(msg docker_proxy.enable)\"\n echo \" 2) $(msg docker_proxy.disable)\"\n echo \"\"\n\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_DOCKER_PROXY}\" ]; then\n local _proxy_display; if [ \"${AGENTTEAMS_DOCKER_PROXY}\" = \"1\" ]; then _proxy_display=\"$(msg docker_proxy.val_enabled)\"; else _proxy_display=\"$(msg docker_proxy.val_disabled)\"; fi\n log \"$(msg prompt.upgrade_keep \"$(msg docker_proxy.title_short)\" \"${_proxy_display}\")\"\n local _choice\n read -e -p \"$(msg docker_proxy.choice): \" _choice\n if [ \"${_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n if [ -n \"${_choice}\" ]; then\n case \"${_choice}\" in\n 2) AGENTTEAMS_DOCKER_PROXY=\"0\" ;;\n *) AGENTTEAMS_DOCKER_PROXY=\"1\" ;;\n esac\n fi\n elif [ -z \"${AGENTTEAMS_DOCKER_PROXY+x}\" ]; then\n local _choice\n read -e -p \"$(msg docker_proxy.choice): \" _choice\n if [ \"${_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _choice=\"${_choice:-1}\"\n case \"${_choice}\" in\n 2) AGENTTEAMS_DOCKER_PROXY=\"0\" ;;\n *) AGENTTEAMS_DOCKER_PROXY=\"1\" ;;\n esac\n fi\n AGENTTEAMS_DOCKER_PROXY=\"${AGENTTEAMS_DOCKER_PROXY:-1}\"\n export AGENTTEAMS_DOCKER_PROXY\n if [ \"${AGENTTEAMS_DOCKER_PROXY}\" = \"1\" ]; then\n log \"$(msg docker_proxy.selected_enabled)\"\n\n # Prompt for additional allowed image sources\n echo \"\"\n echo -e \" $(msg docker_proxy.registries_desc)\"\n echo \"\"\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_PROXY_ALLOWED_REGISTRIES}\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg docker_proxy.registries_label)\" \"${AGENTTEAMS_PROXY_ALLOWED_REGISTRIES}\")\"\n local _reg_input\n read -e -p \"$(msg docker_proxy.registries_prompt): \" _reg_input\n if [ \"${_reg_input}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n [ -n \"${_reg_input}\" ] && AGENTTEAMS_PROXY_ALLOWED_REGISTRIES=\"${_reg_input}\"\n elif [ -z \"${AGENTTEAMS_PROXY_ALLOWED_REGISTRIES+x}\" ]; then\n local _reg_input\n read -e -p \"$(msg docker_proxy.registries_prompt): \" _reg_input\n if [ \"${_reg_input}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_PROXY_ALLOWED_REGISTRIES=\"${_reg_input:-}\"\n fi\n export AGENTTEAMS_PROXY_ALLOWED_REGISTRIES\n else\n log \"$(msg docker_proxy.selected_disabled)\"\n fi\n}\n\nstep_idle() {\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_WORKER_IDLE_TIMEOUT=\"${AGENTTEAMS_WORKER_IDLE_TIMEOUT:-720}\"\n log \" $(msg idle_timeout.label) = ${AGENTTEAMS_WORKER_IDLE_TIMEOUT} (non-interactive, skipped)\"\n export AGENTTEAMS_WORKER_IDLE_TIMEOUT\n return 0\n fi\n # ─────────────────────────────────────────────────────────────────\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_WORKER_IDLE_TIMEOUT}\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg idle_timeout.label)\" \"${AGENTTEAMS_WORKER_IDLE_TIMEOUT}\")\"\n local _idle_timeout\n read -e -p \"$(msg idle_timeout.prompt): \" _idle_timeout\n if [ \"${_idle_timeout}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n [ -n \"${_idle_timeout}\" ] && AGENTTEAMS_WORKER_IDLE_TIMEOUT=\"${_idle_timeout}\"\n elif [ -z \"${AGENTTEAMS_WORKER_IDLE_TIMEOUT+x}\" ]; then\n local _idle_timeout\n read -e -p \"$(msg idle_timeout.prompt): \" _idle_timeout\n if [ \"${_idle_timeout}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_WORKER_IDLE_TIMEOUT=\"${_idle_timeout:-720}\"\n fi\n AGENTTEAMS_WORKER_IDLE_TIMEOUT=\"${AGENTTEAMS_WORKER_IDLE_TIMEOUT:-720}\"\n export AGENTTEAMS_WORKER_IDLE_TIMEOUT\n log \"$(msg idle_timeout.selected \"${AGENTTEAMS_WORKER_IDLE_TIMEOUT}\")\"\n}\n\nstep_hostshare() {\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_HOST_SHARE_DIR=\"${AGENTTEAMS_HOST_SHARE_DIR:-${HOME}}\"\n log \" $(msg host_share.label) = ${AGENTTEAMS_HOST_SHARE_DIR} (non-interactive, skipped)\"\n export AGENTTEAMS_HOST_SHARE_DIR\n return 0\n fi\n # ─────────────────────────────────────────────────────────────────\n local _current=\"${AGENTTEAMS_HOST_SHARE_DIR}\"\n local _share_dir\n if [ -n \"${_current}\" ]; then\n read -e -p \"$(msg host_share.prompt \"$HOME\") [${_current}]: \" _share_dir\n else\n read -e -p \"$(msg host_share.prompt \"$HOME\"): \" _share_dir\n fi\n if [ \"${_share_dir}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_HOST_SHARE_DIR=\"${_share_dir:-${_current:-$HOME}}\"\n export AGENTTEAMS_HOST_SHARE_DIR\n}\n\nstep_podman_autostart() {\n echo \"\"\n log \"$(msg podman.autostart.title)\"\n echo \"\"\n echo -e \" $(msg podman.autostart.desc)\"\n echo \"\"\n echo \" 1) $(msg podman.autostart.enable)\"\n echo \" 2) $(msg podman.autostart.disable)\"\n echo \"\"\n\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_PODMAN_AUTOSTART}\" ]; then\n local _disp; if [ \"${AGENTTEAMS_PODMAN_AUTOSTART}\" = \"1\" ]; then _disp=\"$(msg podman.autostart.val_enabled)\"; else _disp=\"$(msg podman.autostart.val_disabled)\"; fi\n log \"$(msg prompt.upgrade_keep \"$(msg podman.autostart.title_short)\" \"${_disp}\")\"\n local _choice\n read -e -p \"$(msg podman.autostart.choice): \" _choice\n if [ \"${_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n if [ -n \"${_choice}\" ]; then\n case \"${_choice}\" in\n 2) AGENTTEAMS_PODMAN_AUTOSTART=\"0\" ;;\n *) AGENTTEAMS_PODMAN_AUTOSTART=\"1\" ;;\n esac\n fi\n elif [ -z \"${AGENTTEAMS_PODMAN_AUTOSTART+x}\" ]; then\n local _choice\n read -e -p \"$(msg podman.autostart.choice) [1]: \" _choice\n if [ \"${_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _choice=\"${_choice:-1}\"\n case \"${_choice}\" in\n 2) AGENTTEAMS_PODMAN_AUTOSTART=\"0\" ;;\n *) AGENTTEAMS_PODMAN_AUTOSTART=\"1\" ;;\n esac\n fi\n AGENTTEAMS_PODMAN_AUTOSTART=\"${AGENTTEAMS_PODMAN_AUTOSTART:-1}\"\n export AGENTTEAMS_PODMAN_AUTOSTART\n if [ \"${AGENTTEAMS_PODMAN_AUTOSTART}\" = \"1\" ]; then\n log \"$(msg podman.autostart.selected_enabled)\"\n else\n log \"$(msg podman.autostart.selected_disabled)\"\n fi\n}\n\n# ============================================================\n# Setup dedicated Podman autostart service for AgentTeams\n# ============================================================\nsetup_podman_autostart() {\n # Only execute if using podman and autostart is enabled\n if [ \"${DOCKER_CMD:-docker}\" != \"podman\" ] || [ \"${AGENTTEAMS_PODMAN_AUTOSTART:-0}\" != \"1\" ]; then\n return 0\n fi\n\n log \"$(msg install.podman.autostart_title)\"\n\n local current_user\n current_user=\"$(whoami)\"\n\n # Define the dedicated AgentTeams service content\n # Strict scoping: Only manage containers matching '--filter name=agentteams'\n local _service_content=\"[Unit]\nDescription=AgentTeams Dedicated Podman Autostart Service\nDocumentation=https://github.com/agentscope-ai/AgentTeams\nWants=network-online.target\nAfter=network-online.target\n\n[Service]\nType=oneshot\nRemainAfterExit=true\nEnvironment=LOGGING=\\\"--log-level=info\\\"\nExecStart=/usr/bin/podman \\$LOGGING start --filter name=agentteams --filter restart-policy=always --filter restart-policy=unless-stopped\nExecStop=/usr/bin/podman \\$LOGGING stop --filter name=agentteams --filter restart-policy=always --filter restart-policy=unless-stopped\n\n[Install]\nWantedBy=default.target\"\n\n local _autostart_ok=0\n\n if [ \"${current_user}\" = \"root\" ]; then\n # Rootful Mode (System-wide dedicated service)\n log \"$(msg install.podman.root_setup)\"\n\n local _sys_dir=\"/etc/systemd/system\"\n mkdir -p \"${_sys_dir}\"\n echo \"${_service_content}\" > \"${_sys_dir}/agentteams-podman-restart.service\"\n\n systemctl daemon-reload >/dev/null 2>&1 || true\n\n if systemctl enable --now agentteams-podman-restart.service >/dev/null 2>&1; then\n log \"$(msg install.podman.root_success)\"\n _autostart_ok=1\n else\n log \"$(msg install.podman.root_fail)\"\n fi\n else\n # Rootless Mode (User-level dedicated service)\n log \"$(msg install.podman.user_setup \"${current_user}\")\"\n\n local _linger_ok=0\n\n # 1. Enable Linger for background execution (Graceful escalation strategy)\n if loginctl show-user \"${current_user}\" --property=Linger 2>/dev/null | grep -q \"Linger=yes\"; then\n _linger_ok=1\n else\n log \"$(msg install.podman.linger_enable)\"\n\n # Step A: Try to enable linger natively without sudo\n if loginctl enable-linger \"${current_user}\" >/dev/null 2>&1; then\n log \"Successfully enabled systemd linger natively.\"\n _linger_ok=1\n else\n # Step B: Fallback to sudo\n log \"Native linger enablement failed. Distro security policy requires privileges.\"\n if command -v sudo >/dev/null 2>&1; then\n log \"Attempting to enable systemd linger via sudo (enter password if prompted)...\"\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n sudo -n loginctl enable-linger \"${current_user}\" 2>/dev/null && _linger_ok=1 || log \"$(msg install.podman.linger_warn)\"\n else\n sudo loginctl enable-linger \"${current_user}\" && _linger_ok=1 || log \"$(msg install.podman.linger_warn)\"\n fi\n else\n log \"$(msg install.podman.linger_warn)\"\n fi\n fi\n fi\n\n # 2. Write dedicated service file to user directory\n local _user_dir=\"${HOME}/.config/systemd/user\"\n mkdir -p \"${_user_dir}\"\n echo \"${_service_content}\" > \"${_user_dir}/agentteams-podman-restart.service\"\n\n systemctl --user daemon-reload >/dev/null 2>&1 || true\n\n if systemctl --user enable --now agentteams-podman-restart.service >/dev/null 2>&1; then\n log \"$(msg install.podman.user_success)\"\n # Crucial: Only consider autostart fully successful if systemd linger is also enabled\n if [ \"${_linger_ok}\" = \"1\" ]; then\n _autostart_ok=1\n else\n log \"⚠️ Service enabled, but since systemd linger is not active, containers will not auto-recover on system reboot.\"\n fi\n else\n log \"$(msg install.podman.user_fail)\"\n fi\n fi\n\n # Only print final success banner if the service AND linger were actually enabled\n if [ \"${_autostart_ok}\" = \"1\" ]; then\n log \"$(msg install.podman.success)\"\n fi\n}\n\n# ============================================================\n# Dashboard startup (called after controller is ready)\n# ============================================================\n\n_start_dashboard() {\n local CTRL_CONTAINER=\"agentteams-controller\"\n local DASHBOARD_CONTAINER=\"agentteams-dashboard\"\n\n if [ \"${AGENTTEAMS_DASHBOARD:-0}\" != \"1\" ]; then\n # Stop and remove the existing dashboard container if present.\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -q \"^${DASHBOARD_CONTAINER}$\"; then\n log \"Stopping and removing ${DASHBOARD_CONTAINER}...\"\n ${DOCKER_CMD} stop \"${DASHBOARD_CONTAINER}\" >/dev/null 2>&1 || true\n ${DOCKER_CMD} rm -f \"${DASHBOARD_CONTAINER}\" >/dev/null 2>&1 || true\n log \"Dashboard stopped.\"\n fi\n return 0\n fi\n\n # Dashboard requires the embedded architecture (controller container).\n # In legacy all-in-one mode the controller API is not available.\n if [ \"${AGENTTEAMS_USE_EMBEDDED:-0}\" != \"1\" ]; then\n log \"Skipping Dashboard: requires embedded architecture (agentteams-controller), not available in legacy mode.\"\n log \"Use Quick Start (embedded) mode to enable agentteams-dashboard.\"\n return 0\n fi\n\n AGENTTEAMS_PORT_DASHBOARD=\"${AGENTTEAMS_PORT_DASHBOARD:-13000}\"\n AGENTTEAMS_DASHBOARD_VERSION=\"${AGENTTEAMS_DASHBOARD_VERSION:-v1.2.0-beta.1}\"\n AGENTTEAMS_DASHBOARD_IMAGE=\"${AGENTTEAMS_DASHBOARD_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-dashboard:${AGENTTEAMS_DASHBOARD_VERSION}}\"\n\n log \"\"\n log \"Starting agentteams-dashboard...\"\n\n local BIND_ADDR=\"127.0.0.1\"\n [ \"${AGENTTEAMS_LOCAL_ONLY:-1}\" = \"0\" ] && BIND_ADDR=\"0.0.0.0\"\n\n # Remove existing container\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' | grep -qx \"${DASHBOARD_CONTAINER}\"; then\n ${DOCKER_CMD} rm -f \"${DASHBOARD_CONTAINER}\" >/dev/null 2>&1 || true\n fi\n\n # Pull or use local image\n if ! ${DOCKER_CMD} image inspect \"${AGENTTEAMS_DASHBOARD_IMAGE}\" >/dev/null 2>&1; then\n log \"Pulling dashboard image: ${AGENTTEAMS_DASHBOARD_IMAGE}\"\n ${DOCKER_CMD} pull \"${AGENTTEAMS_DASHBOARD_IMAGE}\" 2>/dev/null || {\n log \"WARNING: Could not pull dashboard image. Skipping.\"\n return 0\n }\n fi\n\n # Build env args from controller container (MinIO/LLM/auth).\n local env_args=()\n env_args+=(-e AGENTTEAMS_CONTROLLER_URL=\"http://${CTRL_CONTAINER}:8090\")\n env_args+=(-e NEXT_PUBLIC_MATRIX_API_URL=\"http://${CTRL_CONTAINER}:6167\")\n env_args+=(-e MATRIX_HOMESERVER_ALLOWLIST=\"${CTRL_CONTAINER},matrix-local.agentteams.io,matrix.org\")\n\n if ${DOCKER_CMD} ps --format '{{.Names}}' | grep -qx \"${CTRL_CONTAINER}\"; then\n local env_out\n env_out=$(${DOCKER_CMD} inspect \"${CTRL_CONTAINER}\" --format='{{range .Config.Env}}{{.}}{{\"\\n\"}}{{end}}')\n\n _env() { echo \"${env_out}\" | sed -n \"s|^${1}=||p\" | head -n1; }\n\n local fs_endpoint; fs_endpoint=$(_env AGENTTEAMS_FS_ENDPOINT)\n [ -z \"${fs_endpoint}\" ] && fs_endpoint=$(_env AGENTTEAMS_MINIO_ENDPOINT)\n if [ -n \"${fs_endpoint}\" ]; then\n fs_endpoint=$(echo \"${fs_endpoint}\" | sed -e \"s|127\\.0\\.0\\.1|${CTRL_CONTAINER}|\" -e \"s|localhost|${CTRL_CONTAINER}|\")\n env_args+=(-e AGENTTEAMS_FS_ENDPOINT=\"${fs_endpoint}\")\n else\n env_args+=(-e AGENTTEAMS_FS_ENDPOINT=\"http://${CTRL_CONTAINER}:9000\")\n fi\n\n local fs_bucket; fs_bucket=$(_env AGENTTEAMS_FS_BUCKET)\n [ -z \"${fs_bucket}\" ] && fs_bucket=$(_env AGENTTEAMS_MINIO_BUCKET)\n [ -n \"${fs_bucket}\" ] && env_args+=(-e AGENTTEAMS_FS_BUCKET=\"${fs_bucket}\")\n\n local fs_user; fs_user=$(_env AGENTTEAMS_FS_ACCESS_KEY)\n [ -z \"${fs_user}\" ] && fs_user=$(_env AGENTTEAMS_MINIO_USER)\n [ -n \"${fs_user}\" ] && env_args+=(-e AGENTTEAMS_FS_ACCESS_KEY=\"${fs_user}\")\n\n local fs_pass; fs_pass=$(_env AGENTTEAMS_FS_SECRET_KEY)\n [ -z \"${fs_pass}\" ] && fs_pass=$(_env AGENTTEAMS_MINIO_PASSWORD)\n [ -n \"${fs_pass}\" ] && env_args+=(-e AGENTTEAMS_FS_SECRET_KEY=\"${fs_pass}\")\n\n for k in AGENTTEAMS_LLM_PROVIDER AGENTTEAMS_LLM_API_KEY AGENTTEAMS_OPENAI_BASE_URL AGENTTEAMS_DEFAULT_MODEL; do\n local v; v=$(_env \"${k}\")\n [ -n \"${v}\" ] && env_args+=(-e \"${k}=${v}\")\n done\n\n # The controller writes the CLI SA token to /var/run/agentteams/cli-token.\n # The token is generated asynchronously during the first reconcile loop,\n # so it may not be available immediately after the container starts.\n # We poll for up to 30s to handle both fresh installs (token not yet minted)\n # and older images that don't support cli-token at all.\n # For backward compatibility, also try the legacy /var/run/hiclaw/cli-token\n # path used by older HiClaw images.\n local auth_token=\"${AGENTTEAMS_AUTH_TOKEN:-}\"\n if [ -z \"${auth_token}\" ]; then\n local _token_wait=0\n local _token_max_wait=30\n while [ ${_token_wait} -lt ${_token_max_wait} ]; do\n auth_token=$(${DOCKER_CMD} exec \"${CTRL_CONTAINER}\" sh -c 'cat /var/run/agentteams/cli-token 2>/dev/null || cat /var/run/hiclaw/cli-token 2>/dev/null' | tr -d '\\n' || true)\n if [ -n \"${auth_token}\" ]; then\n break\n fi\n sleep 2\n _token_wait=$((_token_wait + 2))\n done\n fi\n if [ -n \"${auth_token}\" ]; then\n env_args+=(-e AGENTTEAMS_AUTH_TOKEN=\"${auth_token}\")\n else\n log \"WARNING: could not read controller SA token (timed out after ${_token_max_wait:-30}s)\"\n log \" This can happen with older AgentTeams/HiClaw images or if the controller is still starting.\"\n log \" Dashboard will still work but some API calls may fail until you log in via Higress Console.\"\n log \" To fix: upgrade AgentTeams to v1.2.0-beta.1+ or set AGENTTEAMS_AUTH_TOKEN manually.\"\n fi\n\n local admin_user; admin_user=$(_env AGENTTEAMS_ADMIN_USER)\n local admin_pass; admin_pass=$(_env AGENTTEAMS_ADMIN_PASSWORD)\n [ -n \"${admin_user}\" ] && env_args+=(-e AGENTTEAMS_ADMIN_USER=\"${admin_user}\")\n [ -n \"${admin_pass}\" ] && env_args+=(-e AGENTTEAMS_ADMIN_PASSWORD=\"${admin_pass}\")\n\n # Higress Console URL: explicit config takes priority, auto-detect as fallback.\n local _resolved_gw_url=\"\"\n if [ -n \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\" ]; then\n # Normalize URL: add http:// prefix if missing\n _resolved_gw_url=\"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\"\n case \"${_resolved_gw_url}\" in\n http://*|https://*) ;;\n *) _resolved_gw_url=\"http://${_resolved_gw_url}\" ;;\n esac\n env_args+=(-e AGENTTEAMS_AI_GATEWAY_ADMIN_URL=\"${_resolved_gw_url}\")\n elif ${DOCKER_CMD} exec \"${CTRL_CONTAINER}\" wget -q -O- --timeout=2 http://127.0.0.1:8001/ >/dev/null 2>&1; then\n _resolved_gw_url=\"http://${CTRL_CONTAINER}:8001\"\n env_args+=(-e AGENTTEAMS_AI_GATEWAY_ADMIN_URL=\"${_resolved_gw_url}\")\n fi\n\n # Verify Higress Console URL reachability (best-effort warning)\n if [ -n \"${_resolved_gw_url}\" ]; then\n if ! ${DOCKER_CMD} exec \"${CTRL_CONTAINER}\" curl -sf --max-time 3 \"${_resolved_gw_url}/\" >/dev/null 2>&1; then\n log \"WARNING: Higress Console URL may not be reachable: ${_resolved_gw_url}\"\n log \" Dashboard will still work, but shared login via Higress Console may fail.\"\n log \" To fix: verify the URL is correct and the Higress Console service is running.\"\n fi\n fi\n else\n log \"WARNING: ${CTRL_CONTAINER} not running; launching Dashboard without Controller/MinIO/LLM env.\"\n if [ -n \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\" ]; then\n local _gw_norm=\"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\"\n case \"${_gw_norm}\" in\n http://*|https://*) ;;\n *) _gw_norm=\"http://${_gw_norm}\" ;;\n esac\n env_args+=(-e AGENTTEAMS_AI_GATEWAY_ADMIN_URL=\"${_gw_norm}\")\n fi\n fi\n\n # Create persistent data volume (for future use)\n ${DOCKER_CMD} volume create agentteams-dashboard-data >/dev/null 2>&1 || true\n\n ${DOCKER_CMD} run -d \\\n --name \"${DASHBOARD_CONTAINER}\" \\\n --restart unless-stopped \\\n --network \"${AGENTTEAMS_NETWORK:-agentteams-net}\" \\\n --network-alias dashboard.agentteams.io \\\n -p \"${BIND_ADDR}:${AGENTTEAMS_PORT_DASHBOARD}:3000\" \\\n \"${env_args[@]}\" \\\n -v agentteams-dashboard-data:/app/db \\\n \"${AGENTTEAMS_DASHBOARD_IMAGE}\"\n\n # Wait for dashboard to be ready\n local max_wait=60\n local waited=0\n while [ $waited -lt $max_wait ]; do\n if curl -sf \"http://127.0.0.1:${AGENTTEAMS_PORT_DASHBOARD}/\" >/dev/null 2>&1; then\n log \"Dashboard ready on port ${AGENTTEAMS_PORT_DASHBOARD}\"\n return 0\n fi\n sleep 2\n waited=$((waited + 2))\n done\n log \"WARNING: Dashboard startup timed out after ${max_wait}s\"\n ${DOCKER_CMD} logs \"${DASHBOARD_CONTAINER}\" 2>&1 | tail -20\n return 0\n}\n\n# ============================================================\n# Manager Installation (Interactive)\n# ============================================================\n\ninstall_manager() {\n log \"$(msg install.title)\"\n log \"$(msg install.registry \"${AGENTTEAMS_REGISTRY}\")\"\n log \"\"\n log \"$(msg install.dir \"$(pwd)\")\"\n log \"$(msg install.dir_hint)\"\n log \"$(msg install.dir_hint2)\"\n log \"\"\n\n # Non-interactive fallback: resolve version immediately so image tags are available\n # before the step state machine runs. Interactive mode lets step_version handle it.\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_VERSION=\"${AGENTTEAMS_VERSION:-${AGENTTEAMS_KNOWN_STABLE_VERSION}}\"\n resolve_image_tags\n fi\n\n # Migrate legacy env file location before checks\n local existing_env=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n if [ ! -f \"${existing_env}\" ] && [ -f \"./agentteams-manager.env\" ]; then\n log \"Migrating agentteams-manager.env from current directory to ${existing_env}...\"\n mv \"./agentteams-manager.env\" \"${existing_env}\"\n fi\n\n # Orphan volume detection (only when no env file — step_existing handles the env-file case)\n if [ ! -f \"${existing_env}\" ]; then\n local data_vol=\"${AGENTTEAMS_DATA_DIR:-agentteams-data}\"\n if ${DOCKER_CMD} volume ls -q | grep -q \"^${data_vol}$\"; then\n echo \"\"\n log \"$(msg install.orphan_volume.detected \"${data_vol}\")\"\n log \"$(msg install.orphan_volume.warn)\"\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n log \"$(msg install.orphan_volume.clean_noninteractive)\"\n ${DOCKER_CMD} stop agentteams-manager 2>/dev/null || true\n ${DOCKER_CMD} rm agentteams-manager 2>/dev/null || true\n for w in $(${DOCKER_CMD} ps -a --format '{{.Names}}' | grep \"^agentteams-worker-\" || true); do\n ${DOCKER_CMD} stop \"${w}\" 2>/dev/null || true\n ${DOCKER_CMD} rm \"${w}\" 2>/dev/null || true\n done\n log \"$(msg install.orphan_volume.cleaning)\"\n ${DOCKER_CMD} volume rm \"${data_vol}\" 2>/dev/null || true\n log \"$(msg install.orphan_volume.cleaned)\"\n else\n echo \"\"\n echo \"$(msg install.orphan_volume.choose)\"\n echo \"$(msg install.orphan_volume.clean)\"\n echo \"$(msg install.orphan_volume.keep)\"\n echo \"\"\n local ORPHAN_CHOICE\n read -e -p \"$(msg install.orphan_volume.prompt): \" ORPHAN_CHOICE\n ORPHAN_CHOICE=\"${ORPHAN_CHOICE:-1}\"\n case \"${ORPHAN_CHOICE}\" in\n 1|clean)\n ${DOCKER_CMD} stop agentteams-manager 2>/dev/null || true\n ${DOCKER_CMD} rm agentteams-manager 2>/dev/null || true\n for w in $(${DOCKER_CMD} ps -a --format '{{.Names}}' | grep \"^agentteams-worker-\" || true); do\n ${DOCKER_CMD} stop \"${w}\" 2>/dev/null || true\n ${DOCKER_CMD} rm \"${w}\" 2>/dev/null || true\n done\n log \"$(msg install.orphan_volume.cleaning)\"\n ${DOCKER_CMD} volume rm \"${data_vol}\" 2>/dev/null || true\n log \"$(msg install.orphan_volume.cleaned)\"\n ;;\n 2|keep)\n log \"$(msg install.orphan_volume.keeping)\"\n ;;\n esac\n fi\n fi\n fi\n\n # ── State machine ─────────────────────────────────────────────────────────\n local _STEPS=( step_lang step_mode step_version step_existing step_llm step_manager_runtime step_runtime step_admin step_network\n step_ports step_domains step_github step_skills step_volume\n step_workspace step_dashboard step_e2ee step_docker_proxy step_idle step_hostshare step_podman_autostart )\n local _STEP_HISTORY=()\n local _step_idx=0\n while [ \"${_step_idx}\" -lt \"${#_STEPS[@]}\" ]; do\n local _step_fn=\"${_STEPS[$_step_idx]}\"\n if should_skip_step \"${_step_fn}\"; then\n _step_idx=$((_step_idx + 1))\n continue\n fi\n if [ \"${#_STEP_HISTORY[@]}\" -gt 0 ]; then\n log \"$(msg nav.back_hint)\"\n fi\n STEP_RESULT=\"\"\n \"${_step_fn}\"\n if [ \"${STEP_RESULT}\" = \"back\" ]; then\n if [ \"${#_STEP_HISTORY[@]}\" -gt 0 ]; then\n local _last=$(( ${#_STEP_HISTORY[@]} - 1 ))\n _step_idx=\"${_STEP_HISTORY[$_last]}\"\n _STEP_HISTORY=(\"${_STEP_HISTORY[@]:0:${_last}}\")\n clear_step_vars \"${_STEPS[$_step_idx]}\"\n fi\n # else: first step, ignore 'b'\n else\n _STEP_HISTORY+=(\"${_step_idx}\")\n _step_idx=$((_step_idx + 1))\n fi\n done\n # ── End state machine ──────────────────────────────────────────────────────\n\n # Post-machine defaults for any steps that were skipped\n AGENTTEAMS_DATA_DIR=\"${AGENTTEAMS_DATA_DIR:-agentteams-data}\"\n if [ -z \"${AGENTTEAMS_WORKSPACE_DIR+x}\" ] || [ -z \"${AGENTTEAMS_WORKSPACE_DIR}\" ]; then\n AGENTTEAMS_WORKSPACE_DIR=\"${HOME}/agentteams-manager\"\n export AGENTTEAMS_WORKSPACE_DIR\n fi\n AGENTTEAMS_WORKSPACE_DIR=\"$(cd \"${AGENTTEAMS_WORKSPACE_DIR}\" 2>/dev/null && pwd || echo \"${AGENTTEAMS_WORKSPACE_DIR}\")\"\n mkdir -p \"${AGENTTEAMS_WORKSPACE_DIR}\"\n AGENTTEAMS_MANAGER_RUNTIME=\"${AGENTTEAMS_MANAGER_RUNTIME:-copaw}\"\n export AGENTTEAMS_MANAGER_RUNTIME\n AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"${AGENTTEAMS_DEFAULT_WORKER_RUNTIME:-copaw}\"\n AGENTTEAMS_MATRIX_E2EE=\"${AGENTTEAMS_MATRIX_E2EE:-0}\"\n export AGENTTEAMS_MATRIX_E2EE\n AGENTTEAMS_WORKER_IDLE_TIMEOUT=\"${AGENTTEAMS_WORKER_IDLE_TIMEOUT:-720}\"\n export AGENTTEAMS_WORKER_IDLE_TIMEOUT\n AGENTTEAMS_HOST_SHARE_DIR=\"${AGENTTEAMS_HOST_SHARE_DIR:-$HOME}\"\n export AGENTTEAMS_HOST_SHARE_DIR\n\n log \"\"\n\n # Generate secrets (only if not already set)\n log \"$(msg install.generating_secrets)\"\n AGENTTEAMS_MANAGER_PASSWORD=\"${AGENTTEAMS_MANAGER_PASSWORD:-$(generate_key)}\"\n AGENTTEAMS_REGISTRATION_TOKEN=\"${AGENTTEAMS_REGISTRATION_TOKEN:-$(generate_key)}\"\n AGENTTEAMS_MINIO_USER=\"${AGENTTEAMS_MINIO_USER:-${AGENTTEAMS_ADMIN_USER}}\"\n AGENTTEAMS_MINIO_PASSWORD=\"${AGENTTEAMS_MINIO_PASSWORD:-${AGENTTEAMS_ADMIN_PASSWORD}}\"\n AGENTTEAMS_MANAGER_GATEWAY_KEY=\"${AGENTTEAMS_MANAGER_GATEWAY_KEY:-$(generate_key)}\"\n\n # Matrix AppService tokens — generate once during install/upgrade if not provided.\n # Persisted to env file so they survive controller restarts.\n AGENTTEAMS_MATRIX_APPSERVICE_ENABLED=\"${AGENTTEAMS_MATRIX_APPSERVICE_ENABLED:-true}\"\n export AGENTTEAMS_MATRIX_APPSERVICE_ENABLED\n if [ \"${AGENTTEAMS_MATRIX_APPSERVICE_ENABLED}\" != \"false\" ] && [ \"${AGENTTEAMS_MATRIX_APPSERVICE_ENABLED}\" != \"0\" ]; then\n if [ -z \"${AGENTTEAMS_MATRIX_APPSERVICE_AS_TOKEN}\" ]; then\n AGENTTEAMS_MATRIX_APPSERVICE_AS_TOKEN=\"$(openssl rand -hex 32)\"\n log \" Auto-generated AppService as_token (saved to env file)\"\n fi\n if [ -z \"${AGENTTEAMS_MATRIX_APPSERVICE_HS_TOKEN}\" ]; then\n AGENTTEAMS_MATRIX_APPSERVICE_HS_TOKEN=\"$(openssl rand -hex 32)\"\n log \" Auto-generated AppService hs_token (saved to env file)\"\n fi\n export AGENTTEAMS_MATRIX_APPSERVICE_AS_TOKEN\n export AGENTTEAMS_MATRIX_APPSERVICE_HS_TOKEN\n fi\n\n # Detect Apple Silicon (M1/M2/M3/M4) - need JVM fix for Higress Console\n # See: https://github.com/agentscope-ai/AgentTeams/issues/249\n if [ -z \"${JVM_ARGS:-}\" ] && [ \"$(uname -m)\" = \"arm64\" ] && [ \"$(uname -s)\" = \"Darwin\" ]; then\n log \"Apple Silicon detected - setting JVM_ARGS to fix Higress Console SIGILL issue\"\n JVM_ARGS=\"-XX:+UnlockDiagnosticVMOptions -XX:-UseAESCTRIntrinsics -XX:UseSVE=0\"\n fi\n\n # Write .env file\n ENV_FILE=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n cat > \"${ENV_FILE}\" << EOF\n# AgentTeams Manager Configuration\n# Generated by agentteams-install.sh on $(date)\n\n# Language\nAGENTTEAMS_LANGUAGE=${AGENTTEAMS_LANGUAGE}\n\n# LLM\nAGENTTEAMS_LLM_PROVIDER=${AGENTTEAMS_LLM_PROVIDER}\nAGENTTEAMS_DEFAULT_MODEL=${AGENTTEAMS_DEFAULT_MODEL}\nAGENTTEAMS_LLM_API_KEY=${AGENTTEAMS_LLM_API_KEY}\nAGENTTEAMS_OPENAI_BASE_URL=${AGENTTEAMS_OPENAI_BASE_URL:-}\nAGENTTEAMS_MODEL_CONTEXT_WINDOW=${AGENTTEAMS_MODEL_CONTEXT_WINDOW:-}\nAGENTTEAMS_MODEL_MAX_TOKENS=${AGENTTEAMS_MODEL_MAX_TOKENS:-}\nAGENTTEAMS_MODEL_REASONING=${AGENTTEAMS_MODEL_REASONING:-}\nAGENTTEAMS_MODEL_VISION=${AGENTTEAMS_MODEL_VISION:-}\n\n# Embedding model (empty = disabled, default: text-embedding-v4)\nAGENTTEAMS_EMBEDDING_MODEL=${AGENTTEAMS_EMBEDDING_MODEL}\n\n# Admin\nAGENTTEAMS_ADMIN_USER=${AGENTTEAMS_ADMIN_USER}\nAGENTTEAMS_ADMIN_PASSWORD=${AGENTTEAMS_ADMIN_PASSWORD}\n\n# Ports\nAGENTTEAMS_LOCAL_ONLY=${AGENTTEAMS_LOCAL_ONLY}\nAGENTTEAMS_PORT_GATEWAY=${AGENTTEAMS_PORT_GATEWAY}\nAGENTTEAMS_PORT_CONSOLE=${AGENTTEAMS_PORT_CONSOLE}\nAGENTTEAMS_PORT_ELEMENT_WEB=${AGENTTEAMS_PORT_ELEMENT_WEB}\nAGENTTEAMS_PORT_MANAGER_CONSOLE=${AGENTTEAMS_PORT_MANAGER_CONSOLE:-18888}\n\n# Manager runtime (openclaw | copaw)\nAGENTTEAMS_MANAGER_RUNTIME=${AGENTTEAMS_MANAGER_RUNTIME:-copaw}\n\n# Matrix\nAGENTTEAMS_MATRIX_DOMAIN=${AGENTTEAMS_MATRIX_DOMAIN}\nAGENTTEAMS_MATRIX_CLIENT_DOMAIN=${AGENTTEAMS_MATRIX_CLIENT_DOMAIN}\n\n# Gateway\nAGENTTEAMS_AI_GATEWAY_DOMAIN=${AGENTTEAMS_AI_GATEWAY_DOMAIN}\nAGENTTEAMS_MANAGER_GATEWAY_KEY=${AGENTTEAMS_MANAGER_GATEWAY_KEY}\n\n# File System\nAGENTTEAMS_FS_DOMAIN=${AGENTTEAMS_FS_DOMAIN}\nAGENTTEAMS_CONSOLE_DOMAIN=${AGENTTEAMS_CONSOLE_DOMAIN}\nAGENTTEAMS_MINIO_USER=${AGENTTEAMS_MINIO_USER}\nAGENTTEAMS_MINIO_PASSWORD=${AGENTTEAMS_MINIO_PASSWORD}\n\n# Internal\nAGENTTEAMS_MANAGER_PASSWORD=${AGENTTEAMS_MANAGER_PASSWORD}\nAGENTTEAMS_REGISTRATION_TOKEN=${AGENTTEAMS_REGISTRATION_TOKEN}\n\n# GitHub (optional)\nAGENTTEAMS_GITHUB_TOKEN=${AGENTTEAMS_GITHUB_TOKEN:-}\n\n# Nacos package import defaults\nAGENTTEAMS_NACOS_REGISTRY_URI=${AGENTTEAMS_NACOS_REGISTRY_URI:-nacos://market.agentteams.io:80/public}\nAGENTTEAMS_NACOS_USERNAME=${AGENTTEAMS_NACOS_USERNAME:-}\nAGENTTEAMS_NACOS_PASSWORD=${AGENTTEAMS_NACOS_PASSWORD:-}\nAGENTTEAMS_NACOS_TOKEN=${AGENTTEAMS_NACOS_TOKEN:-}\n\n# Skills Registry (optional, default: nacos://market.agentteams.io:80/public)\nAGENTTEAMS_SKILLS_API_URL=${AGENTTEAMS_SKILLS_API_URL:-nacos://market.agentteams.io:80/public}\n\n# OpenClaw CMS plugin (optional)\nAGENTTEAMS_CMS_TRACES_ENABLED=${AGENTTEAMS_CMS_TRACES_ENABLED:-false}\nAGENTTEAMS_CMS_ENDPOINT=${AGENTTEAMS_CMS_ENDPOINT:-}\nAGENTTEAMS_CMS_LICENSE_KEY=${AGENTTEAMS_CMS_LICENSE_KEY:-}\nAGENTTEAMS_CMS_PROJECT=${AGENTTEAMS_CMS_PROJECT:-}\nAGENTTEAMS_CMS_WORKSPACE=${AGENTTEAMS_CMS_WORKSPACE:-}\nAGENTTEAMS_CMS_SERVICE_NAME=${AGENTTEAMS_CMS_SERVICE_NAME:-agentteams-manager}\nAGENTTEAMS_CMS_METRICS_ENABLED=${AGENTTEAMS_CMS_METRICS_ENABLED:-false}\n\n# Worker images (for direct container creation)\nAGENTTEAMS_WORKER_IMAGE=${WORKER_IMAGE}\nAGENTTEAMS_COPAW_WORKER_IMAGE=${COPAW_WORKER_IMAGE}\nAGENTTEAMS_HERMES_WORKER_IMAGE=${HERMES_WORKER_IMAGE}\n\n# Default Worker runtime (openclaw | copaw | hermes)\nAGENTTEAMS_DEFAULT_WORKER_RUNTIME=${AGENTTEAMS_DEFAULT_WORKER_RUNTIME:-copaw}\n\n# Matrix E2EE (0=disabled, 1=enabled; default: 0)\nAGENTTEAMS_MATRIX_E2EE=${AGENTTEAMS_MATRIX_E2EE:-0}\n\n# Matrix AppService\nAGENTTEAMS_MATRIX_APPSERVICE_ENABLED=${AGENTTEAMS_MATRIX_APPSERVICE_ENABLED:-true}\nAGENTTEAMS_MATRIX_APPSERVICE_AS_TOKEN=${AGENTTEAMS_MATRIX_APPSERVICE_AS_TOKEN:-}\nAGENTTEAMS_MATRIX_APPSERVICE_HS_TOKEN=${AGENTTEAMS_MATRIX_APPSERVICE_HS_TOKEN:-}\n\n# Docker API proxy (0=disabled, 1=enabled; default: 1)\nAGENTTEAMS_DOCKER_PROXY=${AGENTTEAMS_DOCKER_PROXY:-1}\n\n# Docker API proxy: additional allowed image sources (comma-separated)\nAGENTTEAMS_PROXY_ALLOWED_REGISTRIES=${AGENTTEAMS_PROXY_ALLOWED_REGISTRIES:-}\n\n# Worker idle timeout in minutes (default: 720 = 12 hours)\nAGENTTEAMS_WORKER_IDLE_TIMEOUT=${AGENTTEAMS_WORKER_IDLE_TIMEOUT:-720}\n\n# Podman autostart via systemd (0=disabled, 1=enabled)\nAGENTTEAMS_PODMAN_AUTOSTART=${AGENTTEAMS_PODMAN_AUTOSTART:-0}\n\n# JVM Args for Higress Console (fixes SIGILL on Apple Silicon)\nJVM_ARGS=${JVM_ARGS:-}\n\n# Higress WASM plugin image registry (auto-selected by timezone)\nHIGRESS_ADMIN_WASM_PLUGIN_IMAGE_REGISTRY=${AGENTTEAMS_REGISTRY}\n\n# Data persistence\nAGENTTEAMS_DATA_DIR=${AGENTTEAMS_DATA_DIR:-agentteams-data}\n# Manager workspace (skills, memory, state — host-editable)\nAGENTTEAMS_WORKSPACE_DIR=${AGENTTEAMS_WORKSPACE_DIR:-}\n# Host directory sharing\nAGENTTEAMS_HOST_SHARE_DIR=${AGENTTEAMS_HOST_SHARE_DIR:-}\n\n# agentteams-dashboard (management UI)\nAGENTTEAMS_DASHBOARD=${AGENTTEAMS_DASHBOARD:-1}\nAGENTTEAMS_DASHBOARD_VERSION=${AGENTTEAMS_DASHBOARD_VERSION:-v1.2.0-beta.1}\nAGENTTEAMS_PORT_DASHBOARD=${AGENTTEAMS_PORT_DASHBOARD:-13000}\nAGENTTEAMS_DASHBOARD_IMAGE=${AGENTTEAMS_DASHBOARD_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-dashboard:${AGENTTEAMS_DASHBOARD_VERSION}}\nAGENTTEAMS_AI_GATEWAY_ADMIN_URL=${AGENTTEAMS_AI_GATEWAY_ADMIN_URL:-}\nEOF\n\n chmod 600 \"${ENV_FILE}\"\n log \"$(msg install.config_saved \"${ENV_FILE}\")\"\n\n # Detect container runtime socket\n SOCKET_MOUNT_ARGS=\"\"\n if [ \"${AGENTTEAMS_MOUNT_SOCKET}\" = \"1\" ]; then\n # Actively ensure the API socket is enabled and active before detection.\n if [ \"${DOCKER_CMD:-}\" = \"podman\" ]; then\n ensure_podman_socket\n fi\n CONTAINER_SOCK=$(detect_socket)\n if [ -n \"${CONTAINER_SOCK}\" ]; then\n log \"$(msg install.socket_detected \"${CONTAINER_SOCK}\")\"\n SOCKET_MOUNT_ARGS=\"-v ${CONTAINER_SOCK}:/var/run/docker.sock --security-opt label=disable\"\n else\n log \"$(msg install.socket_not_found)\"\n # Interactive confirmation when socket not found\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" != \"1\" ]; then\n echo \"\"\n echo -e \"\\033[33m$(msg install.socket_confirm.title)\\033[0m\"\n echo \"\"\n echo -e \"$(msg install.socket_confirm.message)\"\n echo \"\"\n read -p \"$(msg install.socket_confirm.prompt)\" SOCKET_CONFIRM\n if [ \"${SOCKET_CONFIRM}\" != \"y\" ] && [ \"${SOCKET_CONFIRM}\" != \"Y\" ]; then\n log \"$(msg install.socket_confirm.cancelled)\"\n exit 0\n fi\n fi\n fi\n fi\n\n # Create the data volume if it doesn't already exist (reuse on reinstall)\n if ! ${DOCKER_CMD} volume ls -q | grep -q \"^${AGENTTEAMS_DATA_DIR}$\"; then\n ${DOCKER_CMD} volume create \"${AGENTTEAMS_DATA_DIR}\" > /dev/null\n fi\n\n # Data mount: Docker volume\n DATA_MOUNT_ARGS=\"-v ${AGENTTEAMS_DATA_DIR}:/data\"\n\n # Manager workspace mount (always a host directory, defaulting to ~/agentteams-manager)\n WORKSPACE_MOUNT_ARGS=\"-v ${AGENTTEAMS_WORKSPACE_DIR}:/root/manager-workspace\"\n\n # Pass host timezone to container so date/time commands reflect local time\n TZ_ARGS=\"-e TZ=${AGENTTEAMS_TIMEZONE}\"\n\n # Host directory mount\n if [ -d \"${AGENTTEAMS_HOST_SHARE_DIR}\" ]; then\n HOST_SHARE_MOUNT_ARGS=\"-v ${AGENTTEAMS_HOST_SHARE_DIR}:/host-share\"\n log \"$(msg host_share.sharing \"${AGENTTEAMS_HOST_SHARE_DIR}\")\"\n else\n log \"$(msg host_share.not_exist \"${AGENTTEAMS_HOST_SHARE_DIR}\")\"\n HOST_SHARE_MOUNT_ARGS=\"-v ${AGENTTEAMS_HOST_SHARE_DIR}:/host-share\"\n fi\n\n # YOLO mode: pass through if set in environment (enables autonomous decisions)\n YOLO_ARGS=\"\"\n if [ \"${AGENTTEAMS_YOLO:-}\" = \"1\" ]; then\n YOLO_ARGS=\"-e AGENTTEAMS_YOLO=1\"\n log \"$(msg install.yolo)\"\n fi\n\n # Matrix-plugin debug tracing: pass through if AGENTTEAMS_MATRIX_DEBUG=1.\n # The container entrypoints translate this to OPENCLAW_MATRIX_DEBUG=1\n # so the openclaw matrix plugin emits structured INFO-level lifecycle\n # traces (sync state transitions, room.invite/join, message handler\n # arrival + filter outcomes). Used to diagnose worker/manager hangs.\n MATRIX_DEBUG_ARGS=\"\"\n if [ \"${AGENTTEAMS_MATRIX_DEBUG:-}\" = \"1\" ]; then\n MATRIX_DEBUG_ARGS=\"-e AGENTTEAMS_MATRIX_DEBUG=1\"\n fi\n\n # E2EE is already in the env file; but also pass explicitly in case env file is not the source\n # (AGENTTEAMS_MATRIX_E2EE is already written to ENV_FILE above via --env-file)\n\n # Pull images (manager based on runtime config; all worker runtimes always pulled)\n _is_local_image() {\n case \"$1\" in\n agentteams/*) return 0 ;;\n *) return 1 ;;\n esac\n }\n\n _local_image_exists() {\n local _img=\"$1\"\n [ -z \"${_img}\" ] && return 1\n _is_local_image \"${_img}\" || return 1\n ${DOCKER_CMD} image inspect \"${_img}\" >/dev/null 2>&1\n }\n\n # Helper: pull or skip a single image\n # Args: $1=image $2=exists_msg_key $3=pulling_msg_key\n _pull_image() {\n local _img=\"$1\" _exists_key=\"$2\" _pull_key=\"$3\"\n [ -z \"${_img}\" ] && return 0\n if _local_image_exists \"${_img}\"; then\n log \"$(msg \"${_exists_key}\" \"${_img}\")\"\n return 0\n fi\n log \"$(msg \"${_pull_key}\" \"${_img}\")\"\n local _attempt=1\n while [ $_attempt -le 3 ]; do\n if ${DOCKER_CMD} pull \"${_img}\"; then\n return 0\n fi\n if [ $_attempt -lt 3 ]; then\n log \"Pull failed (attempt ${_attempt}/3), retrying in 5s...\"\n sleep 5\n fi\n _attempt=$((_attempt + 1))\n done\n die \"Failed to pull ${_img} after 3 attempts\"\n return 1\n }\n\n # Embedded controller image (resolve versioned tag, fallback to latest)\n resolve_embedded_image\n if [ \"${AGENTTEAMS_USE_EMBEDDED}\" = \"1\" ]; then\n _local_image_exists \"${EMBEDDED_IMAGE}\" || true\n fi\n\n # Manager image is always required (select based on runtime)\n if [ \"${AGENTTEAMS_MANAGER_RUNTIME}\" = \"copaw\" ]; then\n _pull_image \"${MANAGER_COPAW_IMAGE}\" \"install.image.exists\" \"install.image.pulling_manager\"\n else\n _pull_image \"${MANAGER_IMAGE}\" \"install.image.exists\" \"install.image.pulling_manager\"\n fi\n\n # Pull all worker runtime images (workers may use any runtime regardless of the default)\n _pull_image \"${WORKER_IMAGE}\" \"install.image.worker_exists\" \"install.image.pulling_worker\"\n _pull_image \"${COPAW_WORKER_IMAGE}\" \"install.image.worker_exists\" \"install.image.pulling_worker\"\n _pull_image \"${HERMES_WORKER_IMAGE}\" \"install.image.worker_exists\" \"install.image.pulling_worker\"\n\n # --- Pre-upgrade: extract Matrix passwords from running old containers ---\n # Only needed when upgrading FROM old architecture (v1.0.9) TO embedded.\n # For new-arch-to-new-arch upgrades, credential files already exist with\n # correct room IDs — we must NOT overwrite them.\n _creds_tmp=\"\"\n if [ \"${AGENTTEAMS_UPGRADE:-0}\" = \"1\" ] && [ \"${AGENTTEAMS_USE_EMBEDDED}\" = \"1\" ]; then\n # Detect if upgrading from old arch: old arch has no agentteams-controller container\n # (or has it as a docker-proxy only, not embedded). Check if the existing\n # agentteams-controller is an embedded image (has supervisord) or just a proxy.\n _is_old_arch=0\n if ! ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -q \"^agentteams-controller$\"; then\n _is_old_arch=1\n elif ${DOCKER_CMD} ps -a --format '{{.Names}} {{.Image}}' 2>/dev/null | grep \"^agentteams-controller \" | grep -qv \"embedded\"; then\n _is_old_arch=1\n fi\n\n if [ \"${_is_old_arch}\" = \"1\" ]; then\n _creds_tmp=$(mktemp -d)\n\n # docker exec for Matrix/minio paths only works while agentteams-manager is running.\n _mgr_creds_tempstart=0\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -q '^agentteams-manager$'; then\n if ! ${DOCKER_CMD} ps --format '{{.Names}}' 2>/dev/null | grep -q '^agentteams-manager$'; then\n log \"agentteams-manager is stopped; starting it temporarily to extract Matrix credentials for upgrade...\"\n ${DOCKER_CMD} start agentteams-manager 2>/dev/null || true\n wait_matrix_ready \"agentteams-manager\"\n _mgr_creds_tempstart=1\n fi\n fi\n\n # Manager password (container Config.Env, then secrets file inside running manager, then data volume)\n _mgr_pw=$(${DOCKER_CMD} inspect agentteams-manager --format '{{range .Config.Env}}{{println .}}{{end}}' 2>/dev/null | grep '^AGENTTEAMS_MANAGER_PASSWORD=' | cut -d= -f2-)\n if [ -z \"${_mgr_pw}\" ] && ${DOCKER_CMD} ps --format '{{.Names}}' 2>/dev/null | grep -q '^agentteams-manager$'; then\n _mgr_pw=$(${DOCKER_CMD} exec agentteams-manager bash -c 'source /data/agentteams-secrets.env 2>/dev/null && echo \"${AGENTTEAMS_MANAGER_PASSWORD}\"' 2>/dev/null)\n fi\n if [ -z \"${_mgr_pw}\" ] && ${DOCKER_CMD} volume ls -q 2>/dev/null | grep -q \"^${AGENTTEAMS_DATA_DIR}$\"; then\n _mgr_pw=$(agentteams_read_secret_from_data_volume \"${AGENTTEAMS_DATA_DIR}\" AGENTTEAMS_MANAGER_PASSWORD)\n fi\n\n # Manager admin DM room ID: login as admin, find DM room with @manager\n _mgr_room=\"\"\n if [ -n \"${_mgr_pw}\" ]; then\n _admin_pw=$(grep AGENTTEAMS_ADMIN_PASSWORD \"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\" 2>/dev/null | cut -d= -f2-)\n _admin_user=$(grep AGENTTEAMS_ADMIN_USER \"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\" 2>/dev/null | cut -d= -f2-)\n _admin_user=\"${_admin_user:-admin}\"\n _matrix_domain=$(grep AGENTTEAMS_MATRIX_DOMAIN \"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\" 2>/dev/null | cut -d= -f2-)\n if [ -n \"${_admin_pw}\" ] && ${DOCKER_CMD} ps --format '{{.Names}}' 2>/dev/null | grep -q '^agentteams-manager$'; then\n _admin_token=$(${DOCKER_CMD} exec agentteams-manager curl -sf -X POST http://127.0.0.1:6167/_matrix/client/v3/login \\\n -H \"Content-Type: application/json\" \\\n -d '{\"type\":\"m.login.password\",\"identifier\":{\"type\":\"m.id.user\",\"user\":\"'\"${_admin_user}\"'\"},\"password\":\"'\"${_admin_pw}\"'\"}' 2>/dev/null | python3 -c \"import sys,json; print(json.load(sys.stdin).get('access_token',''))\" 2>/dev/null || true)\n if [ -n \"${_admin_token}\" ]; then\n _mgr_room=$(${DOCKER_CMD} exec agentteams-manager curl -sf -X GET \\\n -H \"Authorization: Bearer ${_admin_token}\" \\\n \"http://127.0.0.1:6167/_matrix/client/v3/joined_rooms\" 2>/dev/null | python3 -c \"\nimport sys,json,subprocess\nrooms = json.load(sys.stdin).get('joined_rooms',[])\nfor room_id in rooms:\n enc = room_id.replace('!','%21')\n members = json.loads(subprocess.check_output([\n 'docker','exec','agentteams-manager','curl','-sf','-X','GET',\n '-H','Authorization: Bearer ${_admin_token}',\n 'http://127.0.0.1:6167/_matrix/client/v3/rooms/'+enc+'/members'\n ]).decode()).get('chunk',[])\n member_ids = [m['state_key'] for m in members]\n if any('manager' in m and 'admin' not in m.split(':')[0] for m in member_ids):\n if len(member_ids) <= 3:\n print(room_id)\n break\n\" 2>/dev/null || true)\n fi\n fi\n if [ -z \"${_mgr_room}\" ]; then\n _mgr_room=$(agentteams_read_admin_dm_room_from_workspace \"${AGENTTEAMS_WORKSPACE_DIR}\")\n fi\n cat > \"${_creds_tmp}/default.env\" <<CREDEOF\nWORKER_PASSWORD=\"${_mgr_pw}\"\nWORKER_MINIO_PASSWORD=\"$(openssl rand -hex 24)\"\nWORKER_GATEWAY_KEY=\"${AGENTTEAMS_MANAGER_GATEWAY_KEY}\"\nWORKER_ROOM_ID=\"${_mgr_room}\"\nCREDEOF\n log \"Extracted Manager Matrix password${_mgr_room:+ and room ID}\"\n fi\n\n if [ \"${_mgr_creds_tempstart}\" = \"1\" ]; then\n log \"Stopping agentteams-manager after credential extraction (upgrade will recreate containers)...\"\n ${DOCKER_CMD} stop agentteams-manager 2>/dev/null || true\n fi\n fi # _is_old_arch\n fi\n\n # --- Stop and remove existing containers ---\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' | grep -q \"^agentteams-controller$\"; then\n ${DOCKER_CMD} stop agentteams-controller 2>/dev/null || true\n ${DOCKER_CMD} rm agentteams-controller 2>/dev/null || true\n fi\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' | grep -q \"^agentteams-manager$\"; then\n log \"$(msg install.removing_existing)\"\n ${DOCKER_CMD} stop agentteams-manager 2>/dev/null || true\n ${DOCKER_CMD} rm agentteams-manager 2>/dev/null || true\n fi\n\n # Stop and remove worker containers (controller will recreate via CR reconciliation)\n if [ -n \"${UPGRADE_EXISTING_WORKERS:-}\" ]; then\n log \"$(msg install.existing.stopping_workers)\"\n for w in ${UPGRADE_EXISTING_WORKERS}; do\n ${DOCKER_CMD} stop \"${w}\" 2>/dev/null || true\n ${DOCKER_CMD} rm \"${w}\" 2>/dev/null || true\n log \"$(msg install.existing.removed \"${w}\")\"\n done\n fi\n\n # Clean up legacy containers from v1.0.x and earlier\n for _legacy_name in agentteams-docker-proxy; do\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -qx \"${_legacy_name}\"; then\n log \"Removing legacy container: ${_legacy_name}\"\n ${DOCKER_CMD} stop \"${_legacy_name}\" 2>/dev/null || true\n ${DOCKER_CMD} rm -f \"${_legacy_name}\" 2>/dev/null || true\n fi\n done\n\n # --- Upgrade: inject extracted credentials into data volume ---\n # Only needed for old-arch upgrades (credential files were extracted above).\n if [ -n \"${_creds_tmp}\" ] && [ -d \"${_creds_tmp}\" ] && [ -n \"$(ls -A \"${_creds_tmp}\" 2>/dev/null)\" ]; then\n local _cleanup_ctr=\"agentteams-upgrade-cleanup\"\n ${DOCKER_CMD} rm -f \"${_cleanup_ctr}\" 2>/dev/null || true\n ${DOCKER_CMD} run --rm --name \"${_cleanup_ctr}\" \\\n --entrypoint sh \\\n -v \"${AGENTTEAMS_DATA_DIR}:/data\" \\\n -v \"${_creds_tmp}:/creds:ro\" \\\n \"${EMBEDDED_IMAGE}\" -c '\n rm -rf /data/worker-creds\n mkdir -p /data/worker-creds\n cp /creds/*.env /data/worker-creds/ 2>/dev/null || true\n chmod 600 /data/worker-creds/*.env 2>/dev/null || true\n ' 2>/dev/null && log \"Injected credentials for upgrade\" || log \"Warning: credential injection failed, continuing\"\n rm -rf \"${_creds_tmp}\"\n fi\n\n # --- Start containers ---\n log \"$(msg install.starting_manager)\"\n\n # Build port binding args\n if [ \"${AGENTTEAMS_LOCAL_ONLY:-1}\" = \"1\" ]; then\n _port_prefix=\"127.0.0.1:\"\n else\n _port_prefix=\"\"\n fi\n\n # Ensure agentteams-net Docker network exists\n ${DOCKER_CMD} network inspect agentteams-net >/dev/null 2>&1 || ${DOCKER_CMD} network create agentteams-net\n\n if [ \"${AGENTTEAMS_USE_EMBEDDED}\" != \"1\" ] && [ \"${AGENTTEAMS_UPGRADE:-0}\" = \"1\" ]; then\n # Check if current installation is embedded — downgrade to legacy is not supported\n if ${DOCKER_CMD} ps -a --format '{{.Names}} {{.Image}}' 2>/dev/null | grep \"^agentteams-controller \" | grep -q \"embedded\"; then\n error \"Downgrade from embedded architecture to legacy version (${AGENTTEAMS_VERSION}) is not supported.\"\n error \"Please use 'make uninstall-embedded' first, then do a clean install of the target version.\"\n exit 1\n fi\n fi\n\n if [ \"${AGENTTEAMS_USE_EMBEDDED}\" = \"1\" ]; then\n # ============================================================\n # New architecture: embedded controller + auto-created manager\n # ============================================================\n\n # Internal port: 8080 (Higress gateway inside the container).\n local _internal_gw_port=8080\n local _matrix_domain=\"${AGENTTEAMS_MATRIX_DOMAIN:-matrix-local.agentteams.io:${AGENTTEAMS_PORT_GATEWAY}}\"\n local _aigw_domain=\"${AGENTTEAMS_AI_GATEWAY_DOMAIN:-aigw-local.agentteams.io}\"\n # Ensure internal gateway port is present (container-internal traffic uses 8080)\n case \"${_aigw_domain}\" in *:*) ;; *) _aigw_domain=\"${_aigw_domain}:${_internal_gw_port}\" ;; esac\n local _fs_domain=\"${AGENTTEAMS_FS_DOMAIN:-fs-local.agentteams.io}\"\n case \"${_fs_domain}\" in *:*) ;; *) _fs_domain=\"${_fs_domain}:${_internal_gw_port}\" ;; esac\n\n # Controller env args\n local _ctrl_env_args=(\n -e \"AGENTTEAMS_ADMIN_USER=${AGENTTEAMS_ADMIN_USER}\"\n -e \"AGENTTEAMS_ADMIN_PASSWORD=${AGENTTEAMS_ADMIN_PASSWORD}\"\n -e \"AGENTTEAMS_MANAGER_PASSWORD=${AGENTTEAMS_MANAGER_PASSWORD}\"\n -e \"AGENTTEAMS_REGISTRATION_TOKEN=${AGENTTEAMS_REGISTRATION_TOKEN}\"\n -e \"AGENTTEAMS_MINIO_USER=${AGENTTEAMS_MINIO_USER}\"\n -e \"AGENTTEAMS_MINIO_PASSWORD=${AGENTTEAMS_MINIO_PASSWORD}\"\n -e \"AGENTTEAMS_LLM_PROVIDER=${AGENTTEAMS_LLM_PROVIDER}\"\n -e \"AGENTTEAMS_LLM_API_KEY=${AGENTTEAMS_LLM_API_KEY}\"\n -e \"AGENTTEAMS_DEFAULT_MODEL=${AGENTTEAMS_DEFAULT_MODEL}\"\n -e \"AGENTTEAMS_MANAGER_GATEWAY_KEY=${AGENTTEAMS_MANAGER_GATEWAY_KEY}\"\n -e \"AGENTTEAMS_MANAGER_RUNTIME=${AGENTTEAMS_MANAGER_RUNTIME:-copaw}\"\n -e \"AGENTTEAMS_MANAGER_IMAGE=$([ \"${AGENTTEAMS_MANAGER_RUNTIME}\" = \"copaw\" ] && echo \"${MANAGER_COPAW_IMAGE}\" || echo \"${MANAGER_IMAGE}\")\"\n -e \"AGENTTEAMS_DEFAULT_WORKER_RUNTIME=${AGENTTEAMS_DEFAULT_WORKER_RUNTIME:-copaw}\"\n -e \"AGENTTEAMS_WORKER_IMAGE=${WORKER_IMAGE}\"\n -e \"AGENTTEAMS_COPAW_WORKER_IMAGE=${COPAW_WORKER_IMAGE}\"\n -e \"AGENTTEAMS_HERMES_WORKER_IMAGE=${HERMES_WORKER_IMAGE}\"\n -e \"AGENTTEAMS_MATRIX_DOMAIN=${_matrix_domain}\"\n -e \"AGENTTEAMS_ELEMENT_HOMESERVER_URL=http://127.0.0.1:${AGENTTEAMS_PORT_GATEWAY}\"\n -e \"AGENTTEAMS_MATRIX_URL=http://127.0.0.1:6167\"\n -e \"AGENTTEAMS_MATRIX_E2EE=${AGENTTEAMS_MATRIX_E2EE:-0}\"\n -e \"AGENTTEAMS_MATRIX_APPSERVICE_ENABLED=${AGENTTEAMS_MATRIX_APPSERVICE_ENABLED:-true}\"\n -e \"AGENTTEAMS_MATRIX_APPSERVICE_AS_TOKEN=${AGENTTEAMS_MATRIX_APPSERVICE_AS_TOKEN:-}\"\n -e \"AGENTTEAMS_MATRIX_APPSERVICE_HS_TOKEN=${AGENTTEAMS_MATRIX_APPSERVICE_HS_TOKEN:-}\"\n -e \"AGENTTEAMS_MINIO_ENDPOINT=http://127.0.0.1:9000\"\n -e \"AGENTTEAMS_MINIO_BUCKET=agentteams-storage\"\n -e \"AGENTTEAMS_STORAGE_PREFIX=agentteams/agentteams-storage\"\n -e \"AGENTTEAMS_FS_ENDPOINT=http://127.0.0.1:9000\"\n -e \"AGENTTEAMS_AI_GATEWAY_URL=http://${_aigw_domain}\"\n -e \"AGENTTEAMS_CONTROLLER_URL=http://agentteams-controller:8090\"\n -e \"AGENTTEAMS_DOCKER_NETWORK=agentteams-net\"\n -e \"AGENTTEAMS_WORKSPACE_DIR=${AGENTTEAMS_WORKSPACE_DIR}\"\n -e \"AGENTTEAMS_HOST_SHARE_DIR=${AGENTTEAMS_HOST_SHARE_DIR}\"\n -e \"AGENTTEAMS_MANAGER_ENABLED=true\"\n -e \"AGENTTEAMS_PORT_MANAGER_CONSOLE=${AGENTTEAMS_PORT_MANAGER_CONSOLE:-18888}\"\n )\n\n # Timezone\n if [ -n \"${AGENTTEAMS_TIMEZONE:-}\" ]; then\n _ctrl_env_args+=(-e \"TZ=${AGENTTEAMS_TIMEZONE}\")\n fi\n\n # Yolo mode\n if [ \"${AGENTTEAMS_YOLO:-}\" = \"1\" ]; then\n _ctrl_env_args+=(-e \"AGENTTEAMS_YOLO=1\")\n fi\n\n # Matrix-plugin debug tracing — propagated to every manager + worker\n # the controller spawns, then translated to OPENCLAW_MATRIX_DEBUG=1\n # by the container entrypoints. Use this to diagnose\n # \"worker did not join\" / \"manager replied empty\" hangs.\n if [ \"${AGENTTEAMS_MATRIX_DEBUG:-}\" = \"1\" ]; then\n _ctrl_env_args+=(-e \"AGENTTEAMS_MATRIX_DEBUG=1\")\n fi\n\n # Optional: GitHub token\n if [ -n \"${AGENTTEAMS_GITHUB_TOKEN:-}\" ]; then\n _ctrl_env_args+=(-e \"AGENTTEAMS_GITHUB_TOKEN=${AGENTTEAMS_GITHUB_TOKEN}\")\n fi\n\n # Optional: embedding model\n if [ -n \"${AGENTTEAMS_EMBEDDING_MODEL:-}\" ]; then\n _ctrl_env_args+=(-e \"AGENTTEAMS_EMBEDDING_MODEL=${AGENTTEAMS_EMBEDDING_MODEL}\")\n fi\n\n # Optional: OpenAI-compatible base URL\n if [ -n \"${AGENTTEAMS_OPENAI_BASE_URL:-}\" ]; then\n _ctrl_env_args+=(-e \"AGENTTEAMS_OPENAI_BASE_URL=${AGENTTEAMS_OPENAI_BASE_URL}\")\n fi\n # Optional: language\n if [ -n \"${AGENTTEAMS_LANGUAGE:-}\" ]; then\n _ctrl_env_args+=(-e \"AGENTTEAMS_LANGUAGE=${AGENTTEAMS_LANGUAGE}\")\n fi\n\n # Optional: CMS/ARMS observability. In embedded mode the controller\n # spawns the Manager and Workers, so it must receive these settings.\n _ctrl_env_args+=(-e \"AGENTTEAMS_CMS_TRACES_ENABLED=${AGENTTEAMS_CMS_TRACES_ENABLED:-false}\")\n _ctrl_env_args+=(-e \"AGENTTEAMS_CMS_SERVICE_NAME=${AGENTTEAMS_CMS_SERVICE_NAME:-agentteams-manager}\")\n _ctrl_env_args+=(-e \"AGENTTEAMS_CMS_METRICS_ENABLED=${AGENTTEAMS_CMS_METRICS_ENABLED:-false}\")\n if [ -n \"${AGENTTEAMS_CMS_ENDPOINT:-}\" ]; then\n _ctrl_env_args+=(-e \"AGENTTEAMS_CMS_ENDPOINT=${AGENTTEAMS_CMS_ENDPOINT}\")\n fi\n if [ -n \"${AGENTTEAMS_CMS_LICENSE_KEY:-}\" ]; then\n _ctrl_env_args+=(-e \"AGENTTEAMS_CMS_LICENSE_KEY=${AGENTTEAMS_CMS_LICENSE_KEY}\")\n fi\n if [ -n \"${AGENTTEAMS_CMS_PROJECT:-}\" ]; then\n _ctrl_env_args+=(-e \"AGENTTEAMS_CMS_PROJECT=${AGENTTEAMS_CMS_PROJECT}\")\n fi\n if [ -n \"${AGENTTEAMS_CMS_WORKSPACE:-}\" ]; then\n _ctrl_env_args+=(-e \"AGENTTEAMS_CMS_WORKSPACE=${AGENTTEAMS_CMS_WORKSPACE}\")\n fi\n\n # shellcheck disable=SC2086\n ${DOCKER_CMD} run -d \\\n --name agentteams-controller \\\n --network agentteams-net \\\n --network-alias matrix-local.agentteams.io \\\n --network-alias aigw-local.agentteams.io \\\n --network-alias fs-local.agentteams.io \\\n \"${_ctrl_env_args[@]}\" \\\n -v \"${CONTAINER_SOCK}:/var/run/docker.sock\" \\\n --security-opt label=disable \\\n -v \"${AGENTTEAMS_DATA_DIR}:/data\" \\\n -v \"${AGENTTEAMS_WORKSPACE_DIR}:/root/agentteams-fs/agents/manager\" \\\n -p \"${_port_prefix}${AGENTTEAMS_PORT_GATEWAY}:8080\" \\\n -p \"${_port_prefix}${AGENTTEAMS_PORT_CONSOLE}:8001\" \\\n -p \"${_port_prefix}${AGENTTEAMS_PORT_ELEMENT_WEB:-18088}:8088\" \\\n --restart unless-stopped \\\n \"${EMBEDDED_IMAGE}\"\n\n log \"Embedded controller started: agentteams-controller\"\n\n # Wait for infrastructure inside the controller container\n _wait_for_url() {\n local url=\"$1\" ctr=\"$2\" max_wait=\"${3:-120}\" desc=\"${4:-service}\"\n local elapsed=0\n log \"Waiting for ${desc}...\"\n while [ $elapsed -lt $max_wait ]; do\n if ${DOCKER_CMD} exec \"${ctr}\" curl -sf \"${url}\" >/dev/null 2>&1; then\n log \"${desc} is ready (${elapsed}s)\"\n return 0\n fi\n sleep 2\n elapsed=$((elapsed + 2))\n done\n log \"ERROR: ${desc} not ready after ${max_wait}s\"\n return 1\n }\n\n _wait_for_url \"http://127.0.0.1:6167/_tuwunel/server_version\" agentteams-controller 120 \"Tuwunel (Matrix)\" || exit 1\n _wait_for_url \"http://127.0.0.1:9000/minio/health/live\" agentteams-controller 60 \"MinIO\" || exit 1\n _wait_for_url \"http://127.0.0.1:8080/status\" agentteams-controller 120 \"Higress Gateway\" || exit 1\n\n # Wait for controller to create Manager Agent container\n log \"Waiting for Manager Agent container...\"\n local _mgr_wait=0\n local _mgr_max=300\n while [ $_mgr_wait -lt $_mgr_max ]; do\n if ${DOCKER_CMD} ps --format '{{.Names}}' 2>/dev/null | grep -q \"^agentteams-manager$\"; then\n log \"Manager Agent container detected (${_mgr_wait}s)\"\n break\n fi\n sleep 3\n _mgr_wait=$((_mgr_wait + 3))\n done\n if [ $_mgr_wait -ge $_mgr_max ]; then\n log \"ERROR: Manager Agent container not created after ${_mgr_max}s\"\n log \"Controller logs:\"\n ${DOCKER_CMD} exec agentteams-controller tail -30 /var/log/agentteams/agentteams-controller-error.log 2>/dev/null || true\n exit 1\n fi\n\n # Wait for Manager Agent to be running\n log \"Waiting for Manager Agent to start...\"\n local _agent_wait=0\n while [ $_agent_wait -lt 120 ]; do\n local _state\n _state=$(${DOCKER_CMD} inspect --format '{{.State.Status}}' agentteams-manager 2>/dev/null || echo \"missing\")\n if [ \"${_state}\" = \"running\" ]; then\n log \"Manager Agent is running\"\n break\n fi\n sleep 2\n _agent_wait=$((_agent_wait + 2))\n done\n\n # Enable yolo mode in agent if requested\n if [ \"${AGENTTEAMS_YOLO:-}\" = \"1\" ]; then\n ${DOCKER_CMD} exec agentteams-manager touch /root/manager-workspace/yolo-mode 2>/dev/null || true\n fi\n\n # Wait for the controller to send the first-boot welcome message.\n # The controller gates this on (a) Manager joining the DM room and\n # (b) Higress WASM key-auth propagation actually clearing /v1/chat/completions\n # for the Manager's gateway key — typically ~45-90s on a fresh install.\n # We poll Manager CR Status.WelcomeSent via the in-container agt CLI,\n # exec'd inside agentteams-controller (the source-of-truth container — its\n # bundled CLI binary is always in lockstep with whatever controller\n # binary is currently serving the HTTP API, since they're the same\n # `go build` output. The agentteams-manager container's CLI may lag the\n # controller across image upgrades and silently drop the welcomeSent\n # field, leaving this loop hung). The controller container mints a\n # long-lived admin SA token at startup and writes it to\n # AGENTTEAMS_AUTH_TOKEN_FILE=/var/run/agentteams/cli-token (set as a Dockerfile\n # ENV default), so a bare `docker exec agentteams-controller agt …`\n # auto-discovers both the endpoint and the token. There is a brief\n # window after container start before bootstrapAdminCLIToken completes\n # where the file may be empty / absent — the loop's silent retry\n # handles that the same way it handles the manager-not-yet-running\n # case below.\n if ${DOCKER_CMD} exec agentteams-controller sh -c 'command -v agt' >/dev/null 2>&1; then\n log \"$(msg install.welcome_msg.waiting)\"\n local _welcome_wait=0\n local _welcome_max=\"${AGENTTEAMS_WELCOME_TIMEOUT:-300}\"\n local _welcome_done=0\n while [ $_welcome_wait -lt $_welcome_max ]; do\n # `tr -d` strips whitespace/CR so the grep stays robust to\n # any future change in go-json field ordering or formatting.\n local _wjson\n _wjson=$(${DOCKER_CMD} exec agentteams-controller \\\n agt get managers default -o json 2>/dev/null || true)\n if [ -n \"${_wjson}\" ] && printf '%s' \"${_wjson}\" | tr -d ' \\r\\n' | grep -q '\"welcomeSent\":true'; then\n log \"$(msg install.welcome_msg.confirmed \"${_welcome_wait}\")\"\n _welcome_done=1\n break\n fi\n sleep 3\n _welcome_wait=$((_welcome_wait + 3))\n done\n if [ $_welcome_done -ne 1 ]; then\n # Non-fatal: install is still good. Keep going to the success\n # banner so the admin can use Element Web to nudge Manager into\n # onboarding manually (one DM message is enough).\n log \"$(msg install.welcome_msg.timeout \"${_welcome_max}\")\"\n log \"$(msg install.welcome_msg.timeout_hint)\"\n log \"$(msg install.welcome_msg.timeout_inspect)\"\n fi\n else\n log \"$(msg install.welcome_msg.poll_unavailable)\"\n fi\n\n else\n # ============================================================\n # Legacy architecture: all-in-one manager container\n # ============================================================\n\n NETWORK_ARGS=\"--network agentteams-net\"\n NETWORK_ALIAS_ARGS=\"--network-alias matrix-local.agentteams.io --network-alias aigw-local.agentteams.io --network-alias fs-local.agentteams.io\"\n for _domain in \"${AGENTTEAMS_MATRIX_CLIENT_DOMAIN:-}\" \"${AGENTTEAMS_CONSOLE_DOMAIN:-}\"; do\n if [ -n \"${_domain}\" ] && [[ \"${_domain}\" == *-local.agentteams.io ]]; then\n NETWORK_ALIAS_ARGS=\"${NETWORK_ALIAS_ARGS} --network-alias ${_domain}\"\n fi\n done\n\n # Start Docker API proxy if enabled (security layer between Manager and Docker daemon)\n PROXY_ARGS=\"\"\n if [ \"${AGENTTEAMS_DOCKER_PROXY:-1}\" = \"1\" ] && [ -n \"${CONTAINER_SOCK:-}\" ]; then\n local _proxy_image=\"${AGENTTEAMS_REGISTRY}/higress/agentteams-docker-proxy:${AGENTTEAMS_VERSION}\"\n # Try versioned tag, fallback to latest\n if ! ${DOCKER_CMD} image inspect \"${_proxy_image}\" >/dev/null 2>&1; then\n ${DOCKER_CMD} pull \"${_proxy_image}\" 2>/dev/null || {\n _proxy_image=\"${AGENTTEAMS_REGISTRY}/higress/agentteams-docker-proxy:latest\"\n ${DOCKER_CMD} pull \"${_proxy_image}\" 2>/dev/null || true\n }\n fi\n if ${DOCKER_CMD} image inspect \"${_proxy_image}\" >/dev/null 2>&1; then\n log \"Starting Docker API proxy...\"\n ${DOCKER_CMD} run -d \\\n --name agentteams-docker-proxy \\\n --network agentteams-net \\\n -v \"${CONTAINER_SOCK}:/var/run/docker.sock\" \\\n --security-opt label=disable \\\n -e AGENTTEAMS_WORKER_IMAGE=\"${WORKER_IMAGE}\" \\\n -e AGENTTEAMS_COPAW_WORKER_IMAGE=\"${COPAW_WORKER_IMAGE}\" \\\n -e AGENTTEAMS_HERMES_WORKER_IMAGE=\"${HERMES_WORKER_IMAGE}\" \\\n ${AGENTTEAMS_PROXY_ALLOWED_REGISTRIES:+-e AGENTTEAMS_PROXY_ALLOWED_REGISTRIES=\"${AGENTTEAMS_PROXY_ALLOWED_REGISTRIES}\"} \\\n --restart unless-stopped \\\n \"${_proxy_image}\"\n PROXY_ARGS=\"-e AGENTTEAMS_CONTROLLER_URL=http://agentteams-docker-proxy:2375 -e AGENTTEAMS_CONTAINER_API=http://agentteams-docker-proxy:2375\"\n SOCKET_MOUNT_ARGS=\"\"\n fi\n fi\n\n # Pass host timezone to container\n TZ_ARGS=\"\"\n if [ -n \"${AGENTTEAMS_TIMEZONE:-}\" ]; then\n TZ_ARGS=\"-e TZ=${AGENTTEAMS_TIMEZONE}\"\n fi\n\n YOLO_ARGS=\"\"\n if [ \"${AGENTTEAMS_YOLO:-}\" = \"1\" ]; then\n YOLO_ARGS=\"-e AGENTTEAMS_YOLO=1\"\n fi\n\n MATRIX_DEBUG_ARGS=\"\"\n if [ \"${AGENTTEAMS_MATRIX_DEBUG:-}\" = \"1\" ]; then\n MATRIX_DEBUG_ARGS=\"-e AGENTTEAMS_MATRIX_DEBUG=1\"\n fi\n\n # shellcheck disable=SC2086\n ${DOCKER_CMD} run -d \\\n --name agentteams-manager \\\n --env-file \"${ENV_FILE}\" \\\n -e HOME=/root/manager-workspace \\\n -w /root/manager-workspace \\\n -e HOST_ORIGINAL_HOME=\"${AGENTTEAMS_HOST_SHARE_DIR}\" \\\n -e AGENTTEAMS_MANAGER_RUNTIME=\"${AGENTTEAMS_MANAGER_RUNTIME:-copaw}\" \\\n ${JVM_ARGS:+-e JVM_ARGS=\"${JVM_ARGS}\"} \\\n ${YOLO_ARGS} \\\n ${MATRIX_DEBUG_ARGS} \\\n ${TZ_ARGS} \\\n ${SOCKET_MOUNT_ARGS} \\\n ${NETWORK_ARGS} \\\n ${NETWORK_ALIAS_ARGS} \\\n ${PROXY_ARGS} \\\n -p \"${_port_prefix}${AGENTTEAMS_PORT_GATEWAY}:8080\" \\\n -p \"${_port_prefix}${AGENTTEAMS_PORT_CONSOLE}:8001\" \\\n -p \"${_port_prefix}${AGENTTEAMS_PORT_ELEMENT_WEB:-18088}:8088\" \\\n -p \"127.0.0.1:${AGENTTEAMS_PORT_MANAGER_CONSOLE:-18888}:18888\" \\\n ${DATA_MOUNT_ARGS} \\\n ${WORKSPACE_MOUNT_ARGS} \\\n ${HOST_SHARE_MOUNT_ARGS} \\\n --restart unless-stopped \\\n \"$([ \"${AGENTTEAMS_MANAGER_RUNTIME}\" = \"copaw\" ] && echo \"${MANAGER_COPAW_IMAGE}\" || echo \"${MANAGER_IMAGE}\")\"\n\n # Wait for Manager agent to be ready\n wait_manager_ready \"agentteams-manager\"\n\n # Wait for Matrix server to be ready\n wait_matrix_ready \"agentteams-manager\"\n\n # Post-install verification (non-fatal: warnings only)\n local _verify_script\n _verify_script=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)/agentteams-verify.sh\"\n if [ -f \"${_verify_script}\" ]; then\n bash \"${_verify_script}\" \"agentteams-manager\" || {\n log \"WARNING: Some post-install checks failed. Re-run: bash install/agentteams-verify.sh\"\n }\n fi\n fi\n unset _port_prefix\n\n # ── Start Dashboard (optional) ────────────────────────────────────\n _start_dashboard\n\n # Apply Podman autostart if selected and environment matches\n if [ \"${DOCKER_CMD}\" = \"podman\" ] && [ \"${AGENTTEAMS_PODMAN_AUTOSTART:-0}\" = \"1\" ]; then\n setup_podman_autostart\n fi\n\n log \"\"\n log \"$(msg success.title)\"\n log \"\"\n log \"$(msg success.domains_configured)\"\n log \" ${AGENTTEAMS_MATRIX_DOMAIN%%:*} ${AGENTTEAMS_MATRIX_CLIENT_DOMAIN} ${AGENTTEAMS_AI_GATEWAY_DOMAIN} ${AGENTTEAMS_FS_DOMAIN} ${AGENTTEAMS_CONSOLE_DOMAIN}\"\n log \"\"\n local lan_ip\n lan_ip=$(detect_lan_ip)\n echo -e \"\\033[33m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\033[0m\"\n echo -e \"\\033[33m $(msg success.open_url)\\033[0m\"\n echo -e \"\\033[33m \\033[0m\"\n echo -e \"\\033[1;36m http://127.0.0.1:${AGENTTEAMS_PORT_ELEMENT_WEB:-18088}/#/login\\033[0m\"\n echo -e \"\\033[33m \\033[0m\"\n echo -e \"\\033[33m $(msg success.login_with)\\033[0m\"\n echo -e \"\\033[33m $(msg success.username \"${AGENTTEAMS_ADMIN_USER}\")\\033[0m\"\n echo -e \"\\033[33m $(msg success.password \"${AGENTTEAMS_ADMIN_PASSWORD}\")\\033[0m\"\n echo -e \"\\033[33m \\033[0m\"\n echo -e \"\\033[33m $(msg success.after_login)\\033[0m\"\n echo -e \"\\033[33m $(msg success.tell_it)\\033[0m\"\n echo -e \"\\033[33m $(msg success.manager_auto)\\033[0m\"\n echo -e \"\\033[33m \\033[0m\"\n echo -e \"\\033[33m ───────────────────────────────────────────────────────────────────────────── \\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_title)\\033[0m\"\n echo -e \"\\033[33m \\033[0m\"\n if [ -n \"${lan_ip}\" ]; then\n echo -e \"\\033[33m $(msg success.mobile_step1)\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_step2 \"http://${lan_ip}:${AGENTTEAMS_PORT_GATEWAY}\")\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_step3)\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_username \"${AGENTTEAMS_ADMIN_USER}\")\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_password \"${AGENTTEAMS_ADMIN_PASSWORD}\")\\033[0m\"\n else\n echo -e \"\\033[33m $(msg success.mobile_step1)\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_step2_noip \"${AGENTTEAMS_PORT_GATEWAY}\")\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_noip_hint)\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_step3)\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_username \"${AGENTTEAMS_ADMIN_USER}\")\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_password \"${AGENTTEAMS_ADMIN_PASSWORD}\")\\033[0m\"\n fi\n echo -e \"\\033[33m \\033[0m\"\n echo -e \"\\033[33m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\033[0m\"\n log \"\"\n log \"$(msg success.other_consoles)\"\n log \"$(msg success.higress_console \"${AGENTTEAMS_PORT_CONSOLE}\" \"${AGENTTEAMS_ADMIN_USER}\" \"${AGENTTEAMS_ADMIN_PASSWORD}\")\"\n if [ \"${AGENTTEAMS_DASHBOARD:-0}\" = \"1\" ]; then\n log \"$(msg success.dashboard \"${AGENTTEAMS_PORT_DASHBOARD:-13000}\")\"\n fi\n if [ \"${AGENTTEAMS_USE_EMBEDDED}\" != \"1\" ]; then\n log \"$(msg success.manager_console \"${AGENTTEAMS_PORT_MANAGER_CONSOLE:-18888}\")\"\n log \"$(msg success.manager_console_gateway \"${AGENTTEAMS_ADMIN_USER}\" \"${AGENTTEAMS_ADMIN_PASSWORD}\")\"\n fi\n log \"\"\n log \"$(msg success.switch_llm.title)\"\n log \"$(msg success.switch_llm.hint)\"\n log \"$(msg success.switch_llm.docs)\"\n log \"$(msg success.switch_llm.url)\"\n log \"\"\n log \"$(msg success.tip)\"\n log \"\"\n if [ \"${AGENTTEAMS_LOCAL_ONLY:-1}\" != \"1\" ]; then\n echo -e \"\\033[33m$(msg port.local_only.https_hint)\\033[0m\"\n log \"\"\n fi\n log \"$(msg success.config_file \"${ENV_FILE}\")\"\n log \"$(msg success.data_volume \"${AGENTTEAMS_DATA_DIR}\")\"\n log \"$(msg success.workspace \"${AGENTTEAMS_WORKSPACE_DIR}\")\"\n}\n\n# ============================================================\n# Worker Installation (One-Click)\n# ============================================================\n\ninstall_worker() {\n local WORKER_NAME=\"\"\n local FS=\"\"\n local FS_KEY=\"\"\n local FS_SECRET=\"\"\n local RESET=false\n local SKILLS_API_URL=\"\"\n\n # Parse arguments\n while [ $# -gt 0 ]; do\n case $1 in\n --name) WORKER_NAME=\"$2\"; shift 2 ;;\n --fs) FS=\"$2\"; shift 2 ;;\n --fs-key) FS_KEY=\"$2\"; shift 2 ;;\n --fs-secret) FS_SECRET=\"$2\"; shift 2 ;;\n --skills-api-url) SKILLS_API_URL=\"$2\"; shift 2 ;;\n --reset) RESET=true; shift ;;\n *) die \"$(msg error.unknown_option \"$1\")\" ;;\n esac\n done\n\n # Validate required params\n [ -z \"${WORKER_NAME}\" ] && die \"$(msg error.name_required)\"\n [ -z \"${FS}\" ] && die \"$(msg error.fs_required)\"\n [ -z \"${FS_KEY}\" ] && die \"$(msg error.fs_key_required)\"\n [ -z \"${FS_SECRET}\" ] && die \"$(msg error.fs_secret_required)\"\n\n local CONTAINER_NAME=\"agentteams-worker-${WORKER_NAME}\"\n\n # Handle reset\n if [ \"${RESET}\" = true ]; then\n log \"$(msg worker.resetting \"${WORKER_NAME}\")\"\n ${DOCKER_CMD} stop \"${CONTAINER_NAME}\" 2>/dev/null || true\n ${DOCKER_CMD} rm \"${CONTAINER_NAME}\" 2>/dev/null || true\n fi\n\n # Check for existing container\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' | grep -q \"^${CONTAINER_NAME}$\"; then\n die \"$(msg worker.exists \"${CONTAINER_NAME}\")\"\n fi\n\n log \"$(msg worker.starting \"${WORKER_NAME}\")\"\n\n # Build docker run args\n local DOCKER_ENV=\"\"\n DOCKER_ENV=\"${DOCKER_ENV} -e HOME=/root/agentteams-fs/agents/${WORKER_NAME}\"\n DOCKER_ENV=\"${DOCKER_ENV} -w /root/agentteams-fs/agents/${WORKER_NAME}\"\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_WORKER_NAME=${WORKER_NAME}\"\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_FS_ENDPOINT=${FS}\"\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_FS_ACCESS_KEY=${FS_KEY}\"\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_FS_SECRET_KEY=${FS_SECRET}\"\n\n if [ -z \"${SKILLS_API_URL}\" ]; then\n if [ -n \"${AGENTTEAMS_SKILLS_API_URL:-}\" ]; then\n SKILLS_API_URL=\"${AGENTTEAMS_SKILLS_API_URL}\"\n else\n SKILLS_API_URL=\"nacos://market.agentteams.io:80/public\"\n fi\n fi\n\n # Add SKILLS_API_URL if specified\n DOCKER_ENV=\"${DOCKER_ENV} -e SKILLS_API_URL=${SKILLS_API_URL}\"\n log \"$(msg worker.skills_url \"${SKILLS_API_URL}\")\"\n if [ -n \"${AGENTTEAMS_NACOS_USERNAME:-}\" ]; then\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_NACOS_USERNAME=${AGENTTEAMS_NACOS_USERNAME}\"\n fi\n if [ -n \"${AGENTTEAMS_NACOS_PASSWORD:-}\" ]; then\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_NACOS_PASSWORD=${AGENTTEAMS_NACOS_PASSWORD}\"\n fi\n if [ -n \"${AGENTTEAMS_NACOS_TOKEN:-}\" ]; then\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_NACOS_TOKEN=${AGENTTEAMS_NACOS_TOKEN}\"\n fi\n\n # shellcheck disable=SC2086\n ${DOCKER_CMD} run -d \\\n --name \"${CONTAINER_NAME}\" \\\n ${DOCKER_ENV} \\\n --restart unless-stopped \\\n \"${WORKER_IMAGE}\"\n\n log \"\"\n log \"$(msg worker.started \"${WORKER_NAME}\")\"\n log \"$(msg worker.container \"${CONTAINER_NAME}\")\"\n log \"$(msg worker.view_logs \"${CONTAINER_NAME}\")\"\n}\n\n# ============================================================\n# Main\n# ============================================================\n\n# ============================================================\n# LLM API connectivity test\n# ============================================================\n\ntest_llm_connectivity() {\n local base_url=\"$1\"\n local api_key=\"$2\"\n local model=\"$3\"\n local hint=\"${4:-}\" # optional: extra hint shown on failure\n if ! command -v curl >/dev/null 2>&1; then\n echo -e \"\\033[33m$(msg llm.openai.test.no_curl)\\033[0m\"\n return\n fi\n log \"$(msg llm.openai.test.testing)\"\n local _body _http_code _tmpfile\n _tmpfile=$(mktemp)\n _http_code=$(curl -s -o \"${_tmpfile}\" -w \"%{http_code}\" \\\n -X POST \"${base_url%/}/chat/completions\" \\\n -H \"Authorization: Bearer ${api_key}\" \\\n -H \"Content-Type: application/json\" \\\n -H \"User-Agent: AgentTeams/${AGENTTEAMS_VERSION:-latest}\" \\\n --max-time 30 \\\n -d \"{\\\"model\\\":\\\"${model}\\\",\\\"messages\\\":[{\\\"role\\\":\\\"user\\\",\\\"content\\\":\\\"hi\\\"}]}\" \\\n 2>/dev/null)\n _body=$(cat \"${_tmpfile}\")\n rm -f \"${_tmpfile}\"\n if [ \"${_http_code}\" = \"200\" ] || [ \"${_http_code}\" = \"201\" ]; then\n log \"$(msg llm.openai.test.ok)\"\n else\n echo -e \"\\033[33m$(msg llm.openai.test.fail \"${_http_code}\" \"${_body}\")\\033[0m\"\n if [ -n \"${hint}\" ]; then\n echo -e \"\\033[33m${hint}\\033[0m\"\n fi\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" != \"1\" ]; then\n local _confirm\n read -e -p \"$(msg llm.openai.test.confirm)\" _confirm\n if [ \"${_confirm}\" = \"b\" ]; then\n STEP_RESULT=\"back\"\n return 1\n fi\n if [ \"${_confirm}\" != \"y\" ] && [ \"${_confirm}\" != \"Y\" ]; then\n log \"$(msg llm.openai.test.aborted)\"\n exit 1\n fi\n fi\n fi\n}\n\ntest_embedding_connectivity() {\n local base_url=\"$1\"\n local api_key=\"$2\"\n local model=\"$3\"\n if ! command -v curl >/dev/null 2>&1; then\n return 0\n fi\n log \"$(msg llm.embedding.test.testing)\"\n local _body _http_code _tmpfile\n _tmpfile=$(mktemp)\n _http_code=$(curl -s -o \"${_tmpfile}\" -w \"%{http_code}\" \\\n -X POST \"${base_url%/}/embeddings\" \\\n -H \"Authorization: Bearer ${api_key}\" \\\n -H \"Content-Type: application/json\" \\\n -H \"User-Agent: AgentTeams/${AGENTTEAMS_VERSION:-latest}\" \\\n --max-time 30 \\\n -d \"{\\\"model\\\":\\\"${model}\\\",\\\"input\\\":\\\"test\\\"}\" \\\n 2>/dev/null)\n _body=$(cat \"${_tmpfile}\")\n rm -f \"${_tmpfile}\"\n if [ \"${_http_code}\" = \"200\" ] || [ \"${_http_code}\" = \"201\" ]; then\n log \"$(msg llm.embedding.test.ok)\"\n return 0\n else\n echo -e \"\\033[33m$(msg llm.embedding.test.fail \"${_http_code}\" \"${_body}\")\\033[0m\"\n return 1\n fi\n}\n\n# ============================================================\n# Uninstall\n# ============================================================\n\nuninstall_agentteams() {\n log \"$(msg uninstall.title)\"\n\n # Safely disable and remove the dedicated AgentTeams Podman autostart service\n if [ \"${DOCKER_CMD}\" = \"podman\" ] && command -v systemctl >/dev/null 2>&1; then\n local _systemctl_cmd=\"systemctl\"\n local _service_dir=\"/etc/systemd/system\"\n\n if [ \"$(id -u)\" != \"0\" ]; then\n _systemctl_cmd=\"systemctl --user\"\n _service_dir=\"${HOME}/.config/systemd/user\"\n fi\n\n if [ -f \"${_service_dir}/agentteams-podman-restart.service\" ]; then\n log \"Disabling and removing dedicated AgentTeams Podman autostart service...\"\n ${_systemctl_cmd} disable --now agentteams-podman-restart.service 2>/dev/null || true\n rm -f \"${_service_dir}/agentteams-podman-restart.service\"\n ${_systemctl_cmd} daemon-reload 2>/dev/null || true\n fi\n # Note: Native podman-restart.service is strictly untouched per maintainer review.\n fi\n\n # Capture manager image before removing (needed for workspace cleanup on Linux)\n local manager_image=\"\"\n manager_image=$(${DOCKER_CMD} inspect agentteams-manager --format '{{.Config.Image}}' 2>/dev/null || true)\n\n # Stop and remove manager\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -q \"^agentteams-manager$\"; then\n log \"$(msg uninstall.stopping_manager)\"\n ${DOCKER_CMD} stop agentteams-manager >/dev/null 2>&1 || true\n ${DOCKER_CMD} rm agentteams-manager >/dev/null 2>&1 || true\n fi\n\n # Stop and remove workers\n local workers\n workers=$(${DOCKER_CMD} ps -a --filter \"name=agentteams-worker-\" --format '{{.Names}}' 2>/dev/null || true)\n if [ -n \"${workers}\" ]; then\n log \"$(msg uninstall.stopping_workers)\"\n echo \"${workers}\" | while read -r w; do\n ${DOCKER_CMD} rm -f \"${w}\" >/dev/null 2>&1 || true\n log \"$(msg uninstall.removed \"${w}\")\"\n done\n fi\n\n # Stop and remove the dashboard container (installed via step_dashboard)\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -q \"^agentteams-dashboard$\"; then\n log \"Stopping and removing agentteams-dashboard...\"\n ${DOCKER_CMD} stop agentteams-dashboard >/dev/null 2>&1 || true\n ${DOCKER_CMD} rm agentteams-dashboard >/dev/null 2>&1 || true\n fi\n\n # Stop and remove docker-proxy (legacy ≤ v1.0.x; current arch uses\n # agentteams-controller for the same role)\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -q \"^agentteams-docker-proxy$\"; then\n log \"$(msg uninstall.removing_proxy)\"\n ${DOCKER_CMD} stop agentteams-docker-proxy >/dev/null 2>&1 || true\n ${DOCKER_CMD} rm agentteams-docker-proxy >/dev/null 2>&1 || true\n fi\n\n # Stop and remove the embedded controller container. MUST happen\n # before the `docker volume rm agentteams-data` step below — in embedded\n # mode agentteams-controller mounts agentteams-data at /data (Tuwunel DB,\n # MinIO state, Higress state, all the room messages), and `volume\n # rm` against an in-use volume fails silently because of the trailing\n # `|| true`. Skipping this used to leave room/message history behind\n # across \"uninstall + reinstall\" cycles. See PR #692.\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -q \"^agentteams-controller$\"; then\n log \"$(msg uninstall.stopping_controller)\"\n ${DOCKER_CMD} stop agentteams-controller >/dev/null 2>&1 || true\n ${DOCKER_CMD} rm agentteams-controller >/dev/null 2>&1 || true\n fi\n\n # Read env file for data/workspace info before removing\n local env_file=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n [ ! -f \"${env_file}\" ] && [ -f \"./agentteams-manager.env\" ] && env_file=\"./agentteams-manager.env\"\n\n local data_dir=\"\" workspace_dir=\"\"\n if [ -f \"${env_file}\" ]; then\n data_dir=$(grep '^AGENTTEAMS_DATA_DIR=' \"${env_file}\" 2>/dev/null | cut -d= -f2- || true)\n workspace_dir=$(grep '^AGENTTEAMS_WORKSPACE_DIR=' \"${env_file}\" 2>/dev/null | cut -d= -f2- || true)\n fi\n\n # Remove Docker volume\n local vol=\"${data_dir:-agentteams-data}\"\n if ${DOCKER_CMD} volume inspect \"${vol}\" >/dev/null 2>&1; then\n log \"$(msg uninstall.removing_volume \"${vol}\")\"\n ${DOCKER_CMD} volume rm \"${vol}\" >/dev/null 2>&1 || true\n fi\n\n # Remove Docker network\n if ${DOCKER_CMD} network ls --format '{{.Name}}' 2>/dev/null | grep -q \"^agentteams-net$\"; then\n log \"$(msg uninstall.removing_network)\"\n ${DOCKER_CMD} network rm agentteams-net >/dev/null 2>&1 || true\n fi\n\n # Remove workspace directory\n if [ -n \"${workspace_dir}\" ] && [ -d \"${workspace_dir}\" ]; then\n log \"$(msg uninstall.removing_workspace \"${workspace_dir}\")\"\n rm -rf \"${workspace_dir}\" 2>/dev/null || true\n if [ -d \"${workspace_dir}\" ]; then\n log \"$(msg uninstall.removing_workspace_elevated)\"\n local _parent _base\n _parent=$(dirname \"${workspace_dir}\")\n _base=$(basename \"${workspace_dir}\")\n local _rm_image=\"${manager_image:-busybox}\"\n ${DOCKER_CMD} run --rm --entrypoint sh \\\n -v \"${_parent}:/host-parent\" \\\n \"${_rm_image}\" \\\n -c \"rm -rf /host-parent/${_base}\" 2>/dev/null || true\n fi\n fi\n\n # Remove env file\n if [ -f \"${env_file}\" ]; then\n log \"$(msg uninstall.removing_env \"${env_file}\")\"\n rm -f \"${env_file}\"\n fi\n\n # Remove install log\n if [ -f \"${HOME}/agentteams-install.log\" ]; then\n log \"$(msg uninstall.removing_log \"${HOME}/agentteams-install.log\")\"\n rm -f \"${HOME}/agentteams-install.log\"\n fi\n\n echo \"\"\n log \"$(msg uninstall.done)\"\n}\n\n# ============================================================\n# Check container runtime (docker or podman) and environment\n# ============================================================\ncheck_container_runtime() {\n # Ensure the rootless environment baseline (XDG_RUNTIME_DIR) is ready before engine detection.\n if [ \"$(id -u)\" -ne 0 ] && [ -z \"${XDG_RUNTIME_DIR:-}\" ]; then\n export XDG_RUNTIME_DIR=\"/run/user/$(id -u)\"\n fi\n\n if command -v docker >/dev/null 2>&1; then\n DOCKER_CMD=\"docker\"\n # Check for podman disguised as docker (alias or podman-docker package)\n if docker --version 2>/dev/null | grep -qi \"podman\"; then\n DOCKER_CMD=\"podman\"\n fi\n elif command -v podman >/dev/null 2>&1; then\n DOCKER_CMD=\"podman\"\n else\n echo -e \"\\033[31m[AgentTeams ERROR]\\033[0m $(msg error.docker_not_found)\" >&2\n exit 1\n fi\n\n # Unified health check (Validates runtime responsiveness)\n if ! ${DOCKER_CMD} ps >/dev/null 2>&1; then\n echo -e \"\\033[31m[AgentTeams ERROR]\\033[0m $(msg error.docker_not_running)\" >&2\n exit 1\n fi\n}\n\ncheck_container_runtime\n\ncase \"${1:-}\" in\n manager|\"\")\n install_manager\n ;;\n worker)\n shift\n install_worker \"$@\"\n ;;\n dashboard)\n check_container_runtime\n load_current_params_from_env\n AGENTTEAMS_DASHBOARD=\"${AGENTTEAMS_DASHBOARD:-1}\"\n AGENTTEAMS_DASHBOARD_VERSION=\"${AGENTTEAMS_DASHBOARD_VERSION:-v1.2.0-beta.1}\"\n AGENTTEAMS_PORT_DASHBOARD=\"${AGENTTEAMS_PORT_DASHBOARD:-13000}\"\n AGENTTEAMS_DASHBOARD_IMAGE=\"${AGENTTEAMS_DASHBOARD_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-dashboard:${AGENTTEAMS_DASHBOARD_VERSION}}\"\n AGENTTEAMS_USE_EMBEDDED=1\n _start_dashboard\n ;;\n uninstall)\n uninstall_agentteams\n ;;\n *)\n echo \"Usage: $0 [manager|worker [options]|dashboard|uninstall]\"\n echo \"\"\n echo \"Commands:\"\n echo \" manager Interactive Manager installation (default)\"\n echo \" Choose Quick Start (all defaults) or Manual mode\"\n echo \" worker Worker installation (requires --name and connection params)\"\n echo \" dashboard Start/stop agentteams-dashboard (use with AGENTTEAMS_DASHBOARD=0 to stop)\"\n echo \" uninstall Stop and remove Manager + all Worker containers\"\n echo \"\"\n echo \"Quick Start (fastest):\"\n echo \" $0\"\n echo \" # Then select '1' for Quick Start mode\"\n echo \"\"\n echo \"Non-interactive (for automation):\"\n echo \" AGENTTEAMS_NON_INTERACTIVE=1 AGENTTEAMS_LLM_API_KEY=sk-xxx $0\"\n echo \"\"\n echo \"Worker Options:\"\n echo \" --name <name> Worker name (required)\"\n echo \" --fs <url> MinIO endpoint URL (required)\"\n echo \" --fs-key <key> MinIO access key (required)\"\n echo \" --fs-secret <secret> MinIO secret key (required)\"\n echo \" --reset Remove existing Worker container before creating\"\n exit 1\n ;;\nesac\n"}, "37c31b77d4e88ca87a1270c61a1e6f659e8023e1": {"t": 1785764750.3606665, "n": 221298, "h": "3146f2ea9a74f365", "c": "#!/bin/bash\n# agentteams-install.sh - One-click installation for AgentTeams Manager and Worker\n#\n# Usage:\n# ./agentteams-install.sh # Interactive installation (choose Quick Start or Manual)\n# ./agentteams-install.sh manager # Same as above (explicit)\n# ./agentteams-install.sh worker --name <name> ... # Worker installation\n# ./agentteams-install.sh uninstall # Stop and remove Manager + all Workers\n#\n# Onboarding Modes:\n# Quick Start - Fast installation with all default values (recommended)\n# Manual - Customize each option step by step\n#\n# Environment variables (for automation):\n# AGENTTEAMS_NON_INTERACTIVE Skip all prompts, use defaults (default: 0)\n# AGENTTEAMS_LLM_PROVIDER LLM provider (default: openai-compat for zh non-interactive Token Plan; qwen for en)\n# AGENTTEAMS_DEFAULT_MODEL Default model (default: qwen3.6-plus for zh Token Plan and en non-interactive)\n# AGENTTEAMS_OPENAI_BASE_URL OpenAI-compatible base URL (default for zh non-interactive: Alibaba Token Plan endpoint)\n# AGENTTEAMS_LLM_API_KEY LLM API key (required)\n# AGENTTEAMS_ADMIN_USER Admin username (default: admin)\n# AGENTTEAMS_ADMIN_PASSWORD Admin password (auto-generated if not set, min 8 chars)\n# AGENTTEAMS_MATRIX_DOMAIN Matrix domain (default: matrix-local.agentteams.io:18080)\n# AGENTTEAMS_MOUNT_SOCKET Mount container runtime socket (default: 1)\n# AGENTTEAMS_DATA_DIR Docker volume name for persistent data (default: agentteams-data)\n# AGENTTEAMS_WORKSPACE_DIR Host directory for manager workspace (default: ~/agentteams-manager)\n# AGENTTEAMS_VERSION Image tag (default: latest)\n# AGENTTEAMS_REGISTRY Image registry (default: auto-detected by timezone)\n# AGENTTEAMS_INSTALL_MANAGER_IMAGE Override manager image (e.g., local build)\n# AGENTTEAMS_INSTALL_WORKER_IMAGE Override worker image (e.g., local build)\n# AGENTTEAMS_INSTALL_COPAW_WORKER_IMAGE Override copaw worker image (e.g., local build)\n# AGENTTEAMS_INSTALL_HERMES_WORKER_IMAGE Override hermes worker image (e.g., local build)\n# AGENTTEAMS_NACOS_REGISTRY_URI Default Nacos registry URI for Worker market search/import\n# (default: nacos://market.agentteams.io:80/public)\n# AGENTTEAMS_NACOS_USERNAME Default Nacos username for nacos:// package imports (optional)\n# AGENTTEAMS_NACOS_PASSWORD Default Nacos password for nacos:// package imports (optional)\n# AGENTTEAMS_CMS_TRACES_ENABLED Enable openclaw-cms-plugin traces for Manager AND all Workers (default: false)\n# AGENTTEAMS_CMS_ENDPOINT ARMS OTLP endpoint (required if traces enabled)\n# AGENTTEAMS_CMS_LICENSE_KEY CMS license key (required if traces enabled)\n# AGENTTEAMS_CMS_PROJECT CMS project name (optional)\n# AGENTTEAMS_CMS_WORKSPACE CMS workspace ID (required if traces enabled)\n# AGENTTEAMS_CMS_SERVICE_NAME Manager service name in ARMS (default: agentteams-manager)\n# Workers always report as agentteams-worker-<name> automatically\n# AGENTTEAMS_CMS_METRICS_ENABLED Enable diagnostics-otel metrics for Manager AND all Workers (default: false)\n# AGENTTEAMS_PORT_GATEWAY Host port for Higress gateway (default: 18080)\n# AGENTTEAMS_PORT_CONSOLE Host port for Higress console (default: 18001)\n# AGENTTEAMS_PORT_ELEMENT_WEB Host port for Element Web direct access (default: 18088)\n# AGENTTEAMS_PORT_MANAGER_CONSOLE Host port for Manager console (default: 18888)\n# AGENTTEAMS_WORKER_IDLE_TIMEOUT Worker idle timeout in minutes (default: 720, i.e. 12 hours)\n\nset -e\n\nAGENTTEAMS_VERSION=\"${AGENTTEAMS_VERSION:-}\"\nAGENTTEAMS_KNOWN_STABLE_VERSION=\"v1.1.2\" # fallback if GitHub API is unreachable\n\n# Returns 0 (true) if $1 < $2 using semver order; \"latest\" is treated as greatest\n_ver_lt() {\n [ \"$1\" = \"latest\" ] && return 1\n [ \"$2\" = \"latest\" ] && return 0\n [ \"$1\" = \"$2\" ] && return 1\n [ \"$(printf '%s\\n%s' \"$1\" \"$2\" | sort -V | head -1)\" = \"$1\" ]\n}\nAGENTTEAMS_NON_INTERACTIVE=\"${AGENTTEAMS_NON_INTERACTIVE:-0}\"\nAGENTTEAMS_MOUNT_SOCKET=\"${AGENTTEAMS_MOUNT_SOCKET:-1}\"\nAGENTTEAMS_DOCKER_PROXY=\"${AGENTTEAMS_DOCKER_PROXY:-1}\"\nSTEP_RESULT=\"\" # Used by state machine to signal \"back\" navigation\n\n# ============================================================\n# Log all output to file\n# ============================================================\n\nAGENTTEAMS_LOG_FILE=\"${HOME}/agentteams-install.log\"\n\nif [ \"${1:-}\" != \"uninstall\" ]; then\n # Redirect all output (stdout and stderr) to both terminal and log file\n exec > >(tee -a \"${AGENTTEAMS_LOG_FILE}\") 2>&1\n\n echo \"\"\n echo \"========================================\"\n echo \"AgentTeams Installation Log\"\n echo \"Started: $(date)\"\n echo \"User: $(whoami)\"\n echo \"System: $(uname -a)\"\n echo \"Log file: ${AGENTTEAMS_LOG_FILE}\"\n echo \"========================================\"\n echo \"\"\nfi\n\n# ============================================================\n# Utility functions (needed early for timezone detection)\n# ============================================================\n\nlog() {\n echo -e \"\\033[36m[AgentTeams]\\033[0m $1\"\n}\n\nerror() {\n echo -e \"\\033[31m[AgentTeams ERROR]\\033[0m $1\" >&2\n}\n\ndie() {\n error \"$1\"\n exit 1\n}\n\n# ============================================================\n# Timezone detection (compatible with Linux and macOS)\n# ============================================================\n\ndetect_timezone() {\n local tz=\"\"\n\n # Try /etc/timezone (Debian/Ubuntu)\n if [ -f /etc/timezone ]; then\n tz=$(cat /etc/timezone 2>/dev/null | tr -d '[:space:]')\n fi\n\n # Try /etc/localtime symlink (macOS and some Linux)\n if [ -z \"${tz}\" ] && [ -L /etc/localtime ]; then\n tz=$(ls -l /etc/localtime 2>/dev/null | sed 's|.*/zoneinfo/||')\n fi\n\n # Try timedatectl (systemd)\n if [ -z \"${tz}\" ]; then\n tz=$(timedatectl show --value -p Timezone 2>/dev/null)\n fi\n\n # If still not detected, warn and prompt user. Diagnostics go to stderr —\n # this function returns its value via stdout to `$(detect_timezone)`, so\n # any echo here would leak into AGENTTEAMS_TIMEZONE and downstream image tags.\n if [ -z \"${tz}\" ]; then\n echo \"\" >&2\n echo -e \"\\033[33m[AgentTeams WARNING]\\033[0m Could not detect timezone automatically.\" >&2\n echo -e \"\\033[33m[AgentTeams]\\033[0m Please enter your timezone (e.g., Asia/Shanghai, America/New_York).\" >&2\n echo \"\" >&2\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n tz=\"Asia/Shanghai\"\n log \"Using default timezone: ${tz}\" >&2\n else\n read -e -p \"Timezone [Asia/Shanghai]: \" tz\n tz=\"${tz:-Asia/Shanghai}\"\n fi\n fi\n\n echo \"${tz}\"\n}\n\n# Detect timezone once at startup (used by registry selection and container TZ)\nAGENTTEAMS_TIMEZONE=\"${AGENTTEAMS_TIMEZONE:-$(detect_timezone)}\"\n\n# ============================================================\n# Language detection based on timezone\n# ============================================================\n\ndetect_language() {\n local tz=\"${AGENTTEAMS_TIMEZONE}\"\n case \"${tz}\" in\n Asia/Shanghai|Asia/Chongqing|Asia/Harbin|Asia/Urumqi|\\\n Asia/Taipei|Asia/Hong_Kong|Asia/Macau)\n echo \"zh\"\n ;;\n *)\n echo \"en\"\n ;;\n esac\n}\n\n# Language priority: env var > existing env file > timezone detection\nif [ -z \"${AGENTTEAMS_LANGUAGE}\" ]; then\n # Check existing env file for saved language preference (upgrade scenario)\n _env_file=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n # Migrate from legacy location (current directory) if needed\n if [ ! -f \"${_env_file}\" ] && [ -f \"./agentteams-manager.env\" ]; then\n mv \"./agentteams-manager.env\" \"${_env_file}\" 2>/dev/null || true\n fi\n if [ -f \"${_env_file}\" ]; then\n _saved_lang=$(grep '^AGENTTEAMS_LANGUAGE=' \"${_env_file}\" 2>/dev/null | cut -d= -f2-)\n if [ -n \"${_saved_lang}\" ]; then\n AGENTTEAMS_LANGUAGE=\"${_saved_lang}\"\n fi\n fi\n # Fall back to timezone-based detection\n if [ -z \"${AGENTTEAMS_LANGUAGE}\" ]; then\n AGENTTEAMS_LANGUAGE=\"$(detect_language)\"\n fi\n unset _env_file _saved_lang\nfi\nexport AGENTTEAMS_LANGUAGE\n\n# ============================================================\n# Centralized message dictionary and msg() function\n# Compatible with bash 3.2+ (macOS default) — uses case instead of declare -A\n# ============================================================\n\n# msg() function: look up message by key, with printf-style argument substitution\n# Falls back to English if the current language translation is missing.\nmsg() {\n local key=\"$1\"\n shift\n local lang=\"${AGENTTEAMS_LANGUAGE:-en}\"\n local text=\"\"\n case \"${key}.${lang}\" in\n # --- Timezone detection messages ---\n \"tz.warning.title.zh\") text=\"无法自动检测时区。\" ;;\n \"tz.warning.title.en\") text=\"Could not detect timezone automatically.\" ;;\n \"tz.warning.prompt.zh\") text=\"请输入您的时区(例如 Asia/Shanghai、America/New_York)。\" ;;\n \"tz.warning.prompt.en\") text=\"Please enter your timezone (e.g., Asia/Shanghai, America/New_York).\" ;;\n \"tz.default.zh\") text=\"使用默认时区: %s\" ;;\n \"tz.default.en\") text=\"Using default timezone: %s\" ;;\n \"tz.input_prompt.zh\") text=\"时区\" ;;\n \"tz.input_prompt.en\") text=\"Timezone\" ;;\n # --- Installation title and info ---\n \"install.title.zh\") text=\"=== AgentTeams Manager 安装 ===\" ;;\n \"install.title.en\") text=\"=== AgentTeams Manager Installation ===\" ;;\n \"install.registry.zh\") text=\"镜像仓库: %s\" ;;\n \"install.registry.en\") text=\"Registry: %s\" ;;\n \"install.dir.zh\") text=\"安装目录: %s\" ;;\n \"install.dir.en\") text=\"Installation directory: %s\" ;;\n \"install.dir_hint.zh\") text=\" (env 文件 'agentteams-manager.env' 将保存到 HOME 目录。)\" ;;\n \"install.dir_hint.en\") text=\" (The env file 'agentteams-manager.env' will be saved to your HOME directory.)\" ;;\n \"install.dir_hint2.zh\") text=\" (请从您希望管理此安装的目录运行此脚本。)\" ;;\n \"install.dir_hint2.en\") text=\" (Run this script from the directory where you want to manage this installation.)\" ;;\n # --- Onboarding mode ---\n \"install.mode.title.zh\") text=\"--- Onboarding 模式 ---\" ;;\n \"install.mode.title.en\") text=\"--- Onboarding Mode ---\" ;;\n \"install.mode.choose.zh\") text=\"选择安装模式:\" ;;\n \"install.mode.choose.en\") text=\"Choose your installation mode:\" ;;\n \"install.mode.quickstart.zh\") text=\" 1) 快速开始 - 使用阿里云通义 Token 套餐快速安装(推荐)\" ;;\n \"install.mode.quickstart.en\") text=\" 1) Quick Start - Fast installation with Qwen Cloud (recommended)\" ;;\n \"install.mode.manual.zh\") text=\" 2) 手动配置 - 选择 LLM 提供商并自定义选项\" ;;\n \"install.mode.manual.en\") text=\" 2) Manual - Choose LLM provider and customize options\" ;;\n \"install.mode.prompt.zh\") text=\"请选择 [1/2]\" ;;\n \"install.mode.prompt.en\") text=\"Enter choice [1/2]\" ;;\n \"install.mode.quickstart_selected.zh\") text=\"已选择快速开始模式 - 使用阿里云通义 Token 套餐\" ;;\n \"install.mode.quickstart_selected.en\") text=\"Quick Start mode selected - using Qwen Cloud\" ;;\n \"install.mode.manual_selected.zh\") text=\"已选择手动配置模式 - 您将选择 LLM 提供商并自定义选项\" ;;\n \"install.mode.manual_selected.en\") text=\"Manual mode selected - you will choose LLM provider and customize options\" ;;\n \"install.mode.invalid.zh\") text=\"无效选择,默认使用快速开始模式\" ;;\n \"install.mode.invalid.en\") text=\"Invalid choice, defaulting to Quick Start mode\" ;;\n # --- Upgrade mode sub-menu ---\n \"upgrade.mode.prompt.zh\") text=\"请选择升级方式:\" ;;\n \"upgrade.mode.prompt.en\") text=\"Select upgrade mode:\" ;;\n \"upgrade.mode.keep_all.zh\") text=\" 1) 保留所有参数,一键升级(推荐)\" ;;\n \"upgrade.mode.keep_all.en\") text=\" 1) Keep all parameters, quick upgrade (recommended)\" ;;\n \"upgrade.mode.confirm_each.zh\") text=\" 2) 逐个确认参数(可修改)\" ;;\n \"upgrade.mode.confirm_each.en\") text=\" 2) Confirm each parameter (can modify)\" ;;\n \"upgrade.mode.back.zh\") text=\" 3) 返回\" ;;\n \"upgrade.mode.back.en\") text=\" 3) Back\" ;;\n # --- Version selection ---\n \"install.version.title.zh\") text=\"--- 版本选择 ---\" ;;\n \"install.version.title.en\") text=\"--- Version Selection ---\" ;;\n \"install.version.choose.zh\") text=\"选择要安装的版本:\" ;;\n \"install.version.choose.en\") text=\"Choose the version to install:\" ;;\n \"install.version.option_latest.zh\") text=\" 1) latest - 最新版(默认)\" ;;\n \"install.version.option_latest.en\") text=\" 1) latest - Latest build (default)\" ;;\n \"install.version.option_stable.zh\") text=\" 2) %s - 最新稳定版\" ;;\n \"install.version.option_stable.en\") text=\" 2) %s - Latest stable release\" ;;\n \"install.version.fetching.zh\") text=\"正在查询最新稳定版本...\" ;;\n \"install.version.fetching.en\") text=\"Fetching latest stable release...\" ;;\n \"install.version.fetch_failed.zh\") text=\"无法查询 GitHub,使用内置版本 %s\" ;;\n \"install.version.fetch_failed.en\") text=\"Could not reach GitHub, using built-in version %s\" ;;\n \"install.version.option_custom.zh\") text=\" 3) 自定义 - 手动输入版本号(如 v1.0.5)\" ;;\n \"install.version.option_custom.en\") text=\" 3) Custom - Enter a specific version (e.g. v1.0.5)\" ;;\n \"install.version.prompt.zh\") text=\"请选择 [1/2/3]\" ;;\n \"install.version.prompt.en\") text=\"Enter choice [1/2/3]\" ;;\n \"install.version.custom_prompt.zh\") text=\"请输入版本号\" ;;\n \"install.version.custom_prompt.en\") text=\"Enter version tag\" ;;\n \"install.version.selected_latest.zh\") text=\"已选择最新版 (latest)\" ;;\n \"install.version.selected_latest.en\") text=\"Selected latest version\" ;;\n \"install.version.selected_stable.zh\") text=\"已选择最新稳定版 (%s)\" ;;\n \"install.version.selected_stable.en\") text=\"Selected latest stable version (%s)\" ;;\n \"install.version.selected_custom.zh\") text=\"已选择自定义版本 (%s)\" ;;\n \"install.version.selected_custom.en\") text=\"Selected custom version (%s)\" ;;\n \"install.version.invalid.zh\") text=\"无效选择,使用最新稳定版 (%s)\" ;;\n \"install.version.invalid.en\") text=\"Invalid choice, defaulting to latest stable version (%s)\" ;;\n # --- Existing installation detected ---\n \"install.existing.detected.zh\") text=\"检测到已有 Manager 安装(env 文件: %s)\" ;;\n \"install.existing.detected.en\") text=\"Existing Manager installation detected (env file: %s)\" ;;\n \"install.existing.choose.zh\") text=\"选择操作:\" ;;\n \"install.existing.choose.en\") text=\"Choose an action:\" ;;\n \"install.existing.upgrade.zh\") text=\" 1) 就地升级(保留数据、工作空间、env 文件)\" ;;\n \"install.existing.upgrade.en\") text=\" 1) In-place upgrade (keep data, workspace, env file)\" ;;\n \"install.existing.reinstall.zh\") text=\" 2) 全新重装(删除所有数据,重新开始)\" ;;\n \"install.existing.reinstall.en\") text=\" 2) Clean reinstall (remove all data, start fresh)\" ;;\n \"install.existing.cancel.zh\") text=\" 3) 取消\" ;;\n \"install.existing.cancel.en\") text=\" 3) Cancel\" ;;\n \"install.existing.prompt.zh\") text=\"请选择 [1/2/3]\" ;;\n \"install.existing.prompt.en\") text=\"Enter choice [1/2/3]\" ;;\n \"install.existing.upgrade_noninteractive.zh\") text=\"非交互模式: 执行就地升级...\" ;;\n \"install.existing.upgrade_noninteractive.en\") text=\"Non-interactive mode: performing in-place upgrade...\" ;;\n \"install.existing.upgrading.zh\") text=\"执行就地升级...\" ;;\n \"install.existing.upgrading.en\") text=\"Performing in-place upgrade...\" ;;\n \"install.existing.warn_manager_stop.zh\") text=\"⚠️ Manager 容器将被停止并重新创建。\" ;;\n \"install.existing.warn_manager_stop.en\") text=\"⚠️ Manager container will be stopped and recreated.\" ;;\n \"install.existing.warn_worker_recreate.zh\") text=\"⚠️ Worker 容器也将被重新创建(以更新 Manager IP)。\" ;;\n \"install.existing.warn_worker_recreate.en\") text=\"⚠️ Worker containers will also be recreated (to update Manager IP in hosts).\" ;;\n \"install.existing.continue_prompt.zh\") text=\"继续?[y/N]\" ;;\n \"install.existing.continue_prompt.en\") text=\"Continue? [y/N]\" ;;\n \"install.existing.cancelled.zh\") text=\"安装已取消。\" ;;\n \"install.existing.cancelled.en\") text=\"Installation cancelled.\" ;;\n \"install.existing.stopping_manager.zh\") text=\"停止并移除现有 manager 容器...\" ;;\n \"install.existing.stopping_manager.en\") text=\"Stopping and removing existing manager container...\" ;;\n \"install.existing.stopping_workers.zh\") text=\"停止并移除现有 worker 容器...\" ;;\n \"install.existing.stopping_workers.en\") text=\"Stopping and removing existing worker containers...\" ;;\n \"install.existing.removed.zh\") text=\" 已移除: %s\" ;;\n \"install.existing.removed.en\") text=\" Removed: %s\" ;;\n # --- Clean reinstall messages ---\n \"install.reinstall.performing.zh\") text=\"执行全新重装...\" ;;\n \"install.reinstall.performing.en\") text=\"Performing clean reinstall...\" ;;\n \"install.reinstall.warn_stop.zh\") text=\"⚠️ 以下运行中的容器将被停止:\" ;;\n \"install.reinstall.warn_stop.en\") text=\"⚠️ The following running containers will be stopped:\" ;;\n \"install.reinstall.warn_delete.zh\") text=\"⚠️ 警告: 以下内容将被删除:\" ;;\n \"install.reinstall.warn_delete.en\") text=\"⚠️ WARNING: This will DELETE the following:\" ;;\n \"install.reinstall.warn_volume.zh\") text=\" - Docker 卷: agentteams-data\" ;;\n \"install.reinstall.warn_volume.en\") text=\" - Docker volume: agentteams-data\" ;;\n \"install.reinstall.warn_env.zh\") text=\" - Env 文件: %s\" ;;\n \"install.reinstall.warn_env.en\") text=\" - Env file: %s\" ;;\n \"install.reinstall.warn_workspace.zh\") text=\" - Manager 工作空间: %s\" ;;\n \"install.reinstall.warn_workspace.en\") text=\" - Manager workspace: %s\" ;;\n \"install.reinstall.warn_workers.zh\") text=\" - 所有 worker 容器\" ;;\n \"install.reinstall.warn_workers.en\") text=\" - All worker containers\" ;;\n \"install.reinstall.warn_proxy.zh\") text=\" - Docker API 代理容器: agentteams-controller\" ;;\n \"install.reinstall.warn_proxy.en\") text=\" - Docker API proxy container: agentteams-controller\" ;;\n \"install.reinstall.warn_network.zh\") text=\" - Docker 网络: agentteams-net\" ;;\n \"install.reinstall.warn_network.en\") text=\" - Docker network: agentteams-net\" ;;\n \"install.reinstall.confirm_type.zh\") text=\"请输入工作空间路径以确认删除(或按 Ctrl+C 取消):\" ;;\n \"install.reinstall.confirm_type.en\") text=\"To confirm deletion, please type the workspace path:\" ;;\n \"install.reinstall.confirm_path.zh\") text=\"输入路径以确认(或按 Ctrl+C 取消)\" ;;\n \"install.reinstall.confirm_path.en\") text=\"Type the path to confirm (or press Ctrl+C to cancel)\" ;;\n \"install.reinstall.path_mismatch.zh\") text=\"路径不匹配。中止重装。输入: '%s',期望: '%s'\" ;;\n \"install.reinstall.path_mismatch.en\") text=\"Path mismatch. Aborting reinstall. Input: '%s', Expected: '%s'\" ;;\n \"install.reinstall.confirmed.zh\") text=\"已确认。正在清理...\" ;;\n \"install.reinstall.confirmed.en\") text=\"Confirmed. Cleaning up...\" ;;\n \"install.reinstall.removed_worker.zh\") text=\" 已移除 worker: %s\" ;;\n \"install.reinstall.removed_worker.en\") text=\" Removed worker: %s\" ;;\n \"install.reinstall.removing_volume.zh\") text=\"正在移除 Docker 卷: agentteams-data\" ;;\n \"install.reinstall.removing_volume.en\") text=\"Removing Docker volume: agentteams-data\" ;;\n \"install.reinstall.warn_volume_fail.zh\") text=\" 警告: 无法移除卷(可能有引用)\" ;;\n \"install.reinstall.warn_volume_fail.en\") text=\" Warning: Could not remove volume (may have references)\" ;;\n \"install.reinstall.removing_proxy.zh\") text=\"正在移除 Docker API 代理容器: agentteams-controller\" ;;\n \"install.reinstall.removing_proxy.en\") text=\"Removing Docker API proxy container: agentteams-controller\" ;;\n \"install.reinstall.removing_network.zh\") text=\"正在移除 Docker 网络: agentteams-net\" ;;\n \"install.reinstall.removing_network.en\") text=\"Removing Docker network: agentteams-net\" ;;\n \"install.reinstall.removing_workspace.zh\") text=\"正在移除工作空间目录: %s\" ;;\n \"install.reinstall.removing_workspace.en\") text=\"Removing workspace directory: %s\" ;;\n \"install.reinstall.removing_env.zh\") text=\"正在移除 env 文件: %s\" ;;\n \"install.reinstall.removing_env.en\") text=\"Removing env file: %s\" ;;\n \"install.reinstall.cleanup_done.zh\") text=\"清理完成。开始全新安装...\" ;;\n \"install.reinstall.cleanup_done.en\") text=\"Cleanup complete. Starting fresh installation...\" ;;\n \"install.reinstall.failed_rm_workspace.zh\") text=\"无法移除工作空间目录\" ;;\n \"install.reinstall.failed_rm_workspace.en\") text=\"Failed to remove workspace directory\" ;;\n # --- Orphan volume detection ---\n \"install.orphan_volume.detected.zh\") text=\"⚠️ 检测到残留数据卷 '%s',但未找到对应的 env 配置文件。\" ;;\n \"install.orphan_volume.detected.en\") text=\"⚠️ Found leftover data volume '%s' but no matching env config file.\" ;;\n \"install.orphan_volume.warn.zh\") text=\"这可能是之前安装的残留数据,会导致新安装出现异常(如密码冲突、服务启动失败)。\" ;;\n \"install.orphan_volume.warn.en\") text=\"This is likely leftover data from a previous installation and may cause issues (e.g., credential conflicts, service startup failures).\" ;;\n \"install.orphan_volume.choose.zh\") text=\"选择操作:\" ;;\n \"install.orphan_volume.choose.en\") text=\"Choose an action:\" ;;\n \"install.orphan_volume.clean.zh\") text=\" 1) 清理残留数据卷后继续安装(推荐)\" ;;\n \"install.orphan_volume.clean.en\") text=\" 1) Remove leftover volume and continue installation (recommended)\" ;;\n \"install.orphan_volume.keep.zh\") text=\" 2) 保留数据卷继续安装(可能出现异常)\" ;;\n \"install.orphan_volume.keep.en\") text=\" 2) Keep the volume and continue installation (may cause issues)\" ;;\n \"install.orphan_volume.prompt.zh\") text=\"请选择 [1/2]\" ;;\n \"install.orphan_volume.prompt.en\") text=\"Enter choice [1/2]\" ;;\n \"install.orphan_volume.cleaning.zh\") text=\"正在清理残留数据卷...\" ;;\n \"install.orphan_volume.cleaning.en\") text=\"Removing leftover data volume...\" ;;\n \"install.orphan_volume.cleaned.zh\") text=\"残留数据卷已清理。继续全新安装...\" ;;\n \"install.orphan_volume.cleaned.en\") text=\"Leftover volume removed. Continuing with fresh installation...\" ;;\n \"install.orphan_volume.keeping.zh\") text=\"保留数据卷,继续安装。如遇异常请选择全新重装。\" ;;\n \"install.orphan_volume.keeping.en\") text=\"Keeping existing volume. If you encounter issues, consider a clean reinstall.\" ;;\n \"install.orphan_volume.clean_noninteractive.zh\") text=\"非交互模式: 自动清理残留数据卷...\" ;;\n \"install.orphan_volume.clean_noninteractive.en\") text=\"Non-interactive mode: automatically removing leftover volume...\" ;;\n # --- Loading existing config ---\n \"install.loading_config.zh\") text=\"从 %s 加载已有配置(shell 环境变量优先)...\" ;;\n \"install.loading_config.en\") text=\"Loading existing config from %s (shell env vars take priority)...\" ;;\n # --- LLM Configuration ---\n \"llm.title.zh\") text=\"--- LLM 配置 ---\" ;;\n \"llm.title.en\") text=\"--- LLM Configuration ---\" ;;\n \"llm.provider.label.zh\") text=\" 提供商: %s\" ;;\n \"llm.provider.label.en\") text=\" Provider: %s\" ;;\n \"llm.model.label.zh\") text=\" 模型: %s\" ;;\n \"llm.model.label.en\") text=\" Model: %s\" ;;\n \"llm.provider.qwen.zh\") text=\" 提供商: qwen(阿里云百炼)\" ;;\n \"llm.provider.qwen.en\") text=\" Provider: qwen (Alibaba Cloud Bailian)\" ;;\n \"llm.provider.qwen_default.zh\") text=\" 提供商: %s(默认)\" ;;\n \"llm.provider.qwen_default.en\") text=\" Provider: %s (default)\" ;;\n \"llm.model.default.zh\") text=\" 模型: %s(默认)\" ;;\n \"llm.model.default.en\") text=\" Model: %s (default)\" ;;\n \"llm.apikey_hint_bailian.zh\") text=\" 💡 获取阿里云百炼(DashScope)API Key:\" ;;\n \"llm.apikey_hint_bailian.en\") text=\" 💡 Get your Alibaba Cloud Bailian (DashScope) API Key:\" ;;\n \"llm.apikey_url_bailian.zh\") text=\" https://www.aliyun.com/product/bailian\" ;;\n \"llm.apikey_url_bailian.en\") text=\" https://www.aliyun.com/product/bailian\" ;;\n \"llm.apikey_hint_qwencloud.zh\") text=\" 💡 从 Qwen Cloud(国际站)获取 DASHSCOPE_API_KEY:\" ;;\n \"llm.apikey_hint_qwencloud.en\") text=\" 💡 Get your DASHSCOPE_API_KEY for Qwen Cloud (international) from:\" ;;\n \"llm.apikey_url_qwencloud.zh\") text=\" https://home.qwencloud.com/api-keys (文档: https://docs.qwencloud.com/)\" ;;\n \"llm.apikey_url_qwencloud.en\") text=\" https://home.qwencloud.com/api-keys | Docs: https://docs.qwencloud.com/\" ;;\n \"llm.apikey_hint_tokenplan.zh\") text=\" 💡 获取 DashScope API Key 或开通通义 Token 套餐,请参考:\" ;;\n \"llm.apikey_hint_tokenplan.en\") text=\" 💡 Get your DashScope or Token Plan API key (Alibaba Model Studio):\" ;;\n \"llm.apikey_url_tokenplan.zh\") text=\" https://help.aliyun.com/zh/model-studio/token-plan-quickstart\" ;;\n \"llm.apikey_url_tokenplan.en\") text=\" https://common-buy.aliyun.com/token-plan/ | https://help.aliyun.com/zh/model-studio/token-plan-quickstart\" ;;\n \"llm.apikey_hint_codingplan.zh\") text=\" 💡 获取 DashScope API Key(Coding 套餐 / coding.dashscope 接口):\" ;;\n \"llm.apikey_hint_codingplan.en\") text=\" 💡 Get your DashScope API key for Coding Plan (coding.dashscope endpoint):\" ;;\n \"llm.apikey_url_codingplan.zh\") text=\" https://help.aliyun.com/zh/model-studio/get-api-key\" ;;\n \"llm.apikey_url_codingplan.en\") text=\" https://help.aliyun.com/zh/model-studio/get-api-key\" ;;\n \"llm.apikey_prompt.zh\") text=\"LLM API Key\" ;;\n \"llm.apikey_prompt.en\") text=\"LLM API Key\" ;;\n \"llm.providers_title.zh\") text=\"可用 LLM 提供商:\" ;;\n \"llm.providers_title.en\") text=\"Available LLM Providers:\" ;;\n \"llm.provider.alibaba.zh\") text=\" 1) 阿里云通义 Token 套餐 - 推荐中国用户使用\" ;;\n \"llm.provider.alibaba.en\") text=\" 1) Qwen Cloud - International (OpenAI-compatible API, recommended)\" ;;\n \"llm.provider.openai_compat.zh\") text=\" 2) OpenAI 兼容 API - 自定义 Base URL(OpenAI、DeepSeek 等)\" ;;\n \"llm.provider.openai_compat.en\") text=\" 2) OpenAI-compatible API - Custom Base URL (OpenAI, DeepSeek, etc.)\" ;;\n \"llm.provider.select.zh\") text=\"选择提供商 [1/2]\" ;;\n \"llm.provider.select.en\") text=\"Select provider [1/2]\" ;;\n \"llm.alibaba.models_title.zh\") text=\"选择阿里云模型接入方式:\" ;;\n \"llm.alibaba.models_title.en\") text=\"Select Alibaba Cloud model access:\" ;;\n \"llm.alibaba.model.tokenplan.zh\") text=\" 1) 阿里云通义 Token 套餐 - 兼容模式(推荐)\" ;;\n \"llm.alibaba.model.tokenplan.en\") text=\" 1) Alibaba Cloud Token Plan - compatible-mode (recommended)\" ;;\n \"llm.alibaba.model.bailian.zh\") text=\" 2) 阿里云百炼 - DashScope 通用兼容接口\" ;;\n \"llm.alibaba.model.bailian.en\") text=\" 2) Alibaba Cloud Bailian - DashScope compatible mode\" ;;\n \"llm.alibaba.model.codingplan_legacy.zh\") text=\" 3) 阿里云 Coding 套餐 - 旧版端点(兼容保留)\" ;;\n \"llm.alibaba.model.codingplan_legacy.en\") text=\" 3) Alibaba Cloud Coding Plan - legacy endpoint (backward compatible)\" ;;\n \"llm.alibaba.model.select.zh\") text=\"选择接入方式 [1/2/3]\" ;;\n \"llm.alibaba.model.select.en\") text=\"Select access option [1/2/3]\" ;;\n \"llm.alibaba.model.invalid.zh\") text=\"无效选择: %s(请输入 1、2 或 3)\" ;;\n \"llm.alibaba.model.invalid.en\") text=\"Invalid choice: %s (please enter 1, 2, or 3)\" ;;\n \"llm.codingplan.models_title.zh\") text=\"选择通义 Token 套餐默认模型:\" ;;\n \"llm.codingplan.models_title.en\") text=\"Select Qwen Cloud default model:\" ;;\n \"llm.codingplan.model.qwen36plus.zh\") text=\" 1) qwen3.6-plus - 千问 3.6(推荐)\" ;;\n \"llm.codingplan.model.qwen36plus.en\") text=\" 1) qwen3.6-plus - Qwen 3.6 (recommended)\" ;;\n \"llm.codingplan.model.glm5.zh\") text=\" 2) glm-5 - 智谱 GLM-5(编程推荐)\" ;;\n \"llm.codingplan.model.glm5.en\") text=\" 2) glm-5 - Zhipu GLM-5 (recommended for coding)\" ;;\n \"llm.codingplan.model.kimi.zh\") text=\" 3) kimi-k2.5 - Moonshot Kimi K2.5\" ;;\n \"llm.codingplan.model.kimi.en\") text=\" 3) kimi-k2.5 - Moonshot Kimi K2.5\" ;;\n \"llm.codingplan.model.minimax.zh\") text=\" 4) MiniMax-M2.5 - MiniMax M2.5\" ;;\n \"llm.codingplan.model.minimax.en\") text=\" 4) MiniMax-M2.5 - MiniMax M2.5\" ;;\n \"llm.codingplan.model.select.zh\") text=\"选择模型 [1/2/3/4]\" ;;\n \"llm.codingplan.model.select.en\") text=\"Select model [1/2/3/4]\" ;;\n \"llm.provider.selected_tokenplan.zh\") text=\" 提供商: 阿里云通义 Token 套餐(兼容模式)\" ;;\n \"llm.provider.selected_tokenplan.en\") text=\" Provider: Alibaba Cloud Token Plan (compatible mode)\" ;;\n \"llm.provider.selected_codingplan.zh\") text=\" 提供商: 阿里云通义 Token 套餐(alibaba-cloud)\" ;;\n \"llm.provider.selected_codingplan.en\") text=\" Provider: Qwen Cloud (international) (alibaba-cloud)\" ;;\n \"llm.provider.selected_codingplan_legacy.zh\") text=\" 提供商: 阿里云 Coding 套餐(coding.dashscope)\" ;;\n \"llm.provider.selected_codingplan_legacy.en\") text=\" Provider: Alibaba Cloud Coding Plan (coding.dashscope)\" ;;\n \"llm.provider.selected_qwen.zh\") text=\" 提供商: 阿里云百炼\" ;;\n \"llm.provider.selected_qwen.en\") text=\" Provider: Alibaba Cloud Bailian\" ;;\n \"llm.provider.selected_openai.zh\") text=\" 提供商: %s(OpenAI 兼容)\" ;;\n \"llm.provider.selected_openai.en\") text=\" Provider: %s (OpenAI-compatible)\" ;;\n \"llm.provider.invalid.zh\") text=\"无效选择: %s(请输入 1 或 2)\" ;;\n \"llm.provider.invalid.en\") text=\"Invalid choice: %s (please enter 1 or 2)\" ;;\n \"llm.qwen.model_prompt.zh\") text=\"默认模型 ID\" ;;\n \"llm.qwen.model_prompt.en\") text=\"Default Model ID\" ;;\n \"llm.openai.base_url_prompt.zh\") text=\"Base URL\" ;;\n \"llm.openai.base_url_prompt.en\") text=\"Base URL\" ;;\n \"llm.openai.model_prompt.zh\") text=\"默认模型 ID\" ;;\n \"llm.openai.model_prompt.en\") text=\"Default Model ID\" ;;\n \"llm.openai.base_url_label.zh\") text=\" Base URL: %s\" ;;\n \"llm.openai.base_url_label.en\") text=\" Base URL: %s\" ;;\n # --- Custom model parameters ---\n \"llm.custom_model.detected.zh\") text=\" ⚠️ 模型 '%s' 不在内置模型列表中,请配置模型参数:\" ;;\n \"llm.custom_model.detected.en\") text=\" ⚠️ Model '%s' is not in the built-in model list. Please configure model parameters:\" ;;\n \"llm.custom_model.context_prompt.zh\") text=\"最大上下文长度(token 数)[150000]\" ;;\n \"llm.custom_model.context_prompt.en\") text=\"Max context window (tokens) [150000]\" ;;\n \"llm.custom_model.max_tokens_prompt.zh\") text=\"最大输出长度(token 数)[128000]\" ;;\n \"llm.custom_model.max_tokens_prompt.en\") text=\"Max output tokens [128000]\" ;;\n \"llm.custom_model.reasoning_prompt.zh\") text=\"是否支持推理/思考模式?[Y/n]\" ;;\n \"llm.custom_model.reasoning_prompt.en\") text=\"Does it support reasoning/thinking mode? [Y/n]\" ;;\n \"llm.custom_model.vision_prompt.zh\") text=\"是否支持图片输入?[y/N]\" ;;\n \"llm.custom_model.vision_prompt.en\") text=\"Does it support image input? [y/N]\" ;;\n \"llm.custom_model.summary.zh\") text=\" 自定义模型参数: 上下文=%s, 最大输出=%s, 推理=%s, 图片=%s\" ;;\n \"llm.custom_model.summary.en\") text=\" Custom model params: context=%s, maxTokens=%s, reasoning=%s, vision=%s\" ;;\n # --- Admin Credentials ---\n \"admin.title.zh\") text=\"--- 管理员凭据 ---\" ;;\n \"admin.title.en\") text=\"--- Admin Credentials ---\" ;;\n \"admin.username_prompt.zh\") text=\"管理员用户名\" ;;\n \"admin.username_prompt.en\") text=\"Admin Username\" ;;\n \"admin.password_prompt.zh\") text=\"管理员密码(留空自动生成,最少 8 位)\" ;;\n \"admin.password_prompt.en\") text=\"Admin Password (leave empty to auto-generate, min 8 chars)\" ;;\n \"admin.password_generated.zh\") text=\" 已自动生成管理员密码\" ;;\n \"admin.password_generated.en\") text=\" Auto-generated admin password\" ;;\n \"admin.password_too_short.zh\") text=\"管理员密码至少需要 8 个字符(MinIO 要求)。当前长度: %s\" ;;\n \"admin.password_too_short.en\") text=\"Admin password must be at least 8 characters (MinIO requirement). Current length: %s\" ;;\n # --- Port Configuration ---\n \"port.title.zh\") text=\"--- 端口配置(按回车使用默认值)---\" ;;\n \"port.title.en\") text=\"--- Port Configuration (press Enter for defaults) ---\" ;;\n \"port.gateway_prompt.zh\") text=\"网关主机端口(容器内 8080)\" ;;\n \"port.gateway_prompt.en\") text=\"Host port for gateway (8080 inside container)\" ;;\n \"port.console_prompt.zh\") text=\"Higress 控制台主机端口(容器内 8001)\" ;;\n \"port.console_prompt.en\") text=\"Host port for Higress console (8001 inside container)\" ;;\n \"port.element_prompt.zh\") text=\"Element Web 直接访问主机端口(容器内 8088)\" ;;\n \"port.element_prompt.en\") text=\"Host port for Element Web direct access (8088 inside container)\" ;;\n \"port.manager_console_prompt.zh\") text=\"Manager 控制台主机端口(容器内 18888)\" ;;\n \"port.manager_console_prompt.en\") text=\"Host port for Manager console (18888 inside container)\" ;;\n \"port.copaw_app_prompt.zh\") text=\"QwenPaw App API 主机端口(容器内 18799)\" ;;\n \"port.copaw_app_prompt.en\") text=\"Host port for QwenPaw App API (18799 inside container)\" ;;\n # --- Local-only binding ---\n \"port.local_only.title.zh\") text=\"--- 网络访问模式 ---\" ;;\n \"port.local_only.title.en\") text=\"--- Network Access Mode ---\" ;;\n \"port.local_only.prompt.zh\") text=\"是否仅允许本机访问(端口绑定到 127.0.0.1)?\" ;;\n \"port.local_only.prompt.en\") text=\"Bind ports to localhost only (127.0.0.1)?\" ;;\n \"port.local_only.hint_yes.zh\") text=\" 仅本机使用,无需开放外部端口(推荐)\" ;;\n \"port.local_only.hint_yes.en\") text=\" Local use only, no external port exposure (recommended)\" ;;\n \"port.local_only.hint_no.zh\") text=\" 允许外部访问(局域网 / 公网)\" ;;\n \"port.local_only.hint_no.en\") text=\" Allow external access (LAN / public network)\" ;;\n \"port.local_only.choice.zh\") text=\"请选择 [1/2]\" ;;\n \"port.local_only.choice.en\") text=\"Enter choice [1/2]\" ;;\n \"port.local_only.selected_local.zh\") text=\"端口已绑定到 127.0.0.1(仅本机访问)\" ;;\n \"port.local_only.selected_local.en\") text=\"Ports bound to 127.0.0.1 (localhost only)\" ;;\n \"port.local_only.selected_external.zh\") text=\"端口已绑定到所有网络接口(0.0.0.0)\" ;;\n \"port.local_only.selected_external.en\") text=\"Ports bound to all interfaces (0.0.0.0)\" ;;\n \"port.local_only.https_hint.zh\") text=\"⚠️ 建议在 Higress 控制台配置 TLS 证书并启用 HTTPS,避免明文传输。\" ;;\n \"port.local_only.https_hint.en\") text=\"⚠️ It is recommended to configure TLS certificates and enable HTTPS in the Higress Console to avoid plaintext transmission.\" ;;\n \"port.local_only.https_docs.zh\") text=\"\" ;;\n \"port.local_only.https_docs.en\") text=\"\" ;;\n # --- Domain Configuration ---\n \"domain.title.zh\") text=\"--- 域名配置(按回车使用默认值)---\" ;;\n \"domain.title.en\") text=\"--- Domain Configuration (press Enter for defaults) ---\" ;;\n \"domain.hint.zh\") text=\"提示: 自定义域名前必须事先做好 DNS 解析。单机 ECS 部署时无需修改 aigw、fs 等域名;Element Web 和 Matrix Server 也可通过 IP 直接访问。\" ;;\n \"domain.hint.en\") text=\"Hint: Configure DNS resolution before customizing domains. For single ECS deployment, no need to change aigw, fs, etc.; Element Web and Matrix Server can also be accessed directly via IP.\" ;;\n \"domain.matrix_prompt.zh\") text=\"Matrix 域名\" ;;\n \"domain.matrix_prompt.en\") text=\"Matrix Domain\" ;;\n \"domain.element_prompt.zh\") text=\"Element Web 域名\" ;;\n \"domain.element_prompt.en\") text=\"Element Web Domain\" ;;\n \"domain.gateway_prompt.zh\") text=\"AI 网关域名\" ;;\n \"domain.gateway_prompt.en\") text=\"AI Gateway Domain\" ;;\n \"domain.fs_prompt.zh\") text=\"文件系统域名\" ;;\n \"domain.fs_prompt.en\") text=\"File System Domain\" ;;\n \"domain.console_prompt.zh\") text=\"Manager 控制台域名\" ;;\n \"domain.console_prompt.en\") text=\"Manager Console Domain\" ;;\n # --- GitHub Integration ---\n \"github.title.zh\") text=\"--- GitHub 集成(可选,按回车跳过)---\" ;;\n \"github.title.en\") text=\"--- GitHub Integration (optional, press Enter to skip) ---\" ;;\n \"github.token_prompt.zh\") text=\"GitHub 个人访问令牌(可选)\" ;;\n \"github.token_prompt.en\") text=\"GitHub Personal Access Token (optional)\" ;;\n # --- Skills Registry ---\n \"skills.title.zh\") text=\"--- Skills 注册中心(可选,按回车使用默认 nacos://market.agentteams.io:80/public)---\" ;;\n \"skills.title.en\") text=\"--- Skills Registry (optional, press Enter for default nacos://market.agentteams.io:80/public) ---\" ;;\n \"skills.url_prompt.zh\") text=\"Skills 注册中心 URL(留空使用默认 nacos://market.agentteams.io:80/public)\" ;;\n \"skills.url_prompt.en\") text=\"Skills Registry URL (leave empty for default nacos://market.agentteams.io:80/public)\" ;;\n # --- Data Persistence ---\n \"data.title.zh\") text=\"--- 数据持久化 ---\" ;;\n \"data.title.en\") text=\"--- Data Persistence ---\" ;;\n \"data.volume_prompt.zh\") text=\"Docker 卷名称 [agentteams-data]\" ;;\n \"data.volume_prompt.en\") text=\"Docker volume name for persistent data [agentteams-data]\" ;;\n \"data.volume_using.zh\") text=\" 使用 Docker 卷: %s\" ;;\n \"data.volume_using.en\") text=\" Using Docker volume: %s\" ;;\n # --- Manager Workspace ---\n \"workspace.title.zh\") text=\"--- Manager 工作空间 ---\" ;;\n \"workspace.title.en\") text=\"--- Manager Workspace ---\" ;;\n \"workspace.dir_prompt.zh\") text=\"Manager 工作空间目录 [%s]\" ;;\n \"workspace.dir_prompt.en\") text=\"Manager workspace directory [%s]\" ;;\n \"workspace.dir_label.zh\") text=\" Manager 工作空间: %s\" ;;\n \"workspace.dir_label.en\") text=\" Manager workspace: %s\" ;;\n # --- Host directory sharing ---\n \"host_share.prompt.zh\") text=\"与 Agent 共享的主机目录(默认: %s)\" ;;\n \"host_share.prompt.en\") text=\"Host directory to share with agents (default: %s)\" ;;\n \"host_share.sharing.zh\") text=\"共享主机目录: %s -> 容器内 /host-share\" ;;\n \"host_share.sharing.en\") text=\"Sharing host directory: %s -> /host-share in container\" ;;\n \"host_share.not_exist.zh\") text=\"警告: 主机目录 %s 不存在,跳过验证继续使用\" ;;\n \"host_share.not_exist.en\") text=\"WARNING: Host directory %s does not exist, using without validation\" ;;\n # --- Default worker runtime ---\n \"worker_runtime.title.zh\") text=\"--- 默认 Worker 运行时 ---\" ;;\n \"worker_runtime.title.en\") text=\"--- Default Worker Runtime ---\" ;;\n \"worker_runtime.openclaw.zh\") text=\"OpenClaw\" ;;\n \"worker_runtime.openclaw.en\") text=\"OpenClaw\" ;;\n \"worker_runtime.copaw.zh\") text=\"QwenPaw\" ;;\n \"worker_runtime.copaw.en\") text=\"QwenPaw\" ;;\n \"worker_runtime.hermes.zh\") text=\"Hermes\" ;;\n \"worker_runtime.hermes.en\") text=\"Hermes\" ;;\n \"worker_runtime.choice.zh\") text=\"请选择 [1/2/3]\" ;;\n \"worker_runtime.choice.en\") text=\"Enter choice [1/2/3]\" ;;\n \"worker_runtime.selected.zh\") text=\"默认 Worker 运行时: %s\" ;;\n \"worker_runtime.selected.en\") text=\"Default Worker runtime: %s\" ;;\n \"worker_runtime.title_short.zh\") text=\"默认 Worker 运行时\" ;;\n \"worker_runtime.title_short.en\") text=\"Default Worker Runtime\" ;;\n \"manager_runtime.title.zh\") text=\"--- Manager 运行时 ---\" ;;\n \"manager_runtime.title.en\") text=\"--- Manager Runtime ---\" ;;\n \"manager_runtime.openclaw.zh\") text=\"OpenClaw\" ;;\n \"manager_runtime.openclaw.en\") text=\"OpenClaw\" ;;\n \"manager_runtime.copaw.zh\") text=\"QwenPaw\" ;;\n \"manager_runtime.copaw.en\") text=\"QwenPaw\" ;;\n \"manager_runtime.choice.zh\") text=\"请选择 [1/2]\" ;;\n \"manager_runtime.choice.en\") text=\"Enter choice [1/2]\" ;;\n \"manager_runtime.selected.zh\") text=\"Manager 运行时: %s\" ;;\n \"manager_runtime.selected.en\") text=\"Manager runtime: %s\" ;;\n \"manager_runtime.title_short.zh\") text=\"Manager 运行时\" ;;\n \"manager_runtime.title_short.en\") text=\"Manager Runtime\" ;;\n # --- Secrets and config ---\n \"install.generating_secrets.zh\") text=\"正在生成密钥...\" ;;\n \"install.generating_secrets.en\") text=\"Generating secrets...\" ;;\n \"install.config_saved.zh\") text=\"配置已保存到 %s\" ;;\n \"install.config_saved.en\") text=\"Configuration saved to %s\" ;;\n # --- Container runtime socket ---\n \"install.socket_detected.zh\") text=\"容器运行时 socket: %s(已启用直接创建 Worker)\" ;;\n \"install.socket_detected.en\") text=\"Container runtime socket: %s (direct Worker creation enabled)\" ;;\n \"install.socket_not_found.zh\") text=\"未找到容器运行时 socket(Manager 无法直接创建 Worker 容器,需要你手动执行 docker 命令创建)\" ;;\n \"install.socket_not_found.en\") text=\"No container runtime socket found (Manager cannot create Worker containers directly, you will need to create them manually using docker commands)\" ;;\n \"install.socket_confirm.title.zh\") text=\"⚠️ 未检测到容器运行时 Socket\" ;;\n \"install.socket_confirm.title.en\") text=\"⚠️ Container Runtime Socket Not Detected\" ;;\n \"install.socket_confirm.message.zh\") text=\"未找到 Docker/Podman socket,Manager 将无法自动创建 Worker 容器。\\n你需要手动执行 docker run 命令来创建 Worker。\\n\\n是否继续安装?\" ;;\n \"install.socket_confirm.message.en\") text=\"Docker/Podman socket not found. Manager will not be able to create Worker containers automatically.\\nYou will need to manually run docker commands to create Workers.\\n\\nContinue installation?\" ;;\n \"install.socket_confirm.prompt.zh\") text=\"继续安装? [y/N]: \" ;;\n \"install.socket_confirm.prompt.en\") text=\"Continue? [y/N]: \" ;;\n \"install.socket_confirm.cancelled.zh\") text=\"安装已取消。如需启用 Worker 自动创建,请确保 Docker/Podman 正在运行,然后重新运行安装脚本。\" ;;\n \"install.socket_confirm.cancelled.en\") text=\"Installation cancelled. To enable automatic Worker creation, ensure Docker/Podman is running and re-run the installer.\" ;;\n # --- Container management ---\n \"install.removing_existing.zh\") text=\"正在移除现有 agentteams-manager 容器...\" ;;\n \"install.removing_existing.en\") text=\"Removing existing agentteams-manager container...\" ;;\n # --- Matrix E2EE ---\n \"matrix_e2ee.title.zh\") text=\"--- Matrix 端到端加密(E2EE)---\" ;;\n \"matrix_e2ee.title.en\") text=\"--- Matrix End-to-End Encryption (E2EE) ---\" ;;\n \"matrix_e2ee.desc.zh\") text=\"E2EE 会对 Manager 与 Worker 之间的 Matrix 消息进行端到端加密。\\n 启用后,即使 Matrix 服务器被入侵,消息内容也无法被窃取。\\n 但 E2EE 会增加首次握手耗时,且要求所有 Agent 都支持 matrix-sdk-crypto。\\n 如果不确定,建议保持禁用。\\n ⚠ 注意:禁用 E2EE 后,请勿在 Element 上创建默认启用加密的 Private 房间,\\n 否则 Agent 将无法读取该房间中的加密消息。请改用 Public 房间或关闭房间加密。\" ;;\n \"matrix_e2ee.desc.en\") text=\"E2EE encrypts Matrix messages between Manager and Workers end-to-end.\\n When enabled, message content stays private even if the Matrix server is compromised.\\n However, E2EE adds overhead to the initial handshake and requires all Agents\\n to support matrix-sdk-crypto. If unsure, keep it disabled.\\n ⚠ Note: When E2EE is disabled, do NOT create Private rooms in Element (which\\n enable encryption by default) — Agents cannot read encrypted messages without\\n E2EE support. Use Public rooms or turn off room encryption instead.\" ;;\n \"matrix_e2ee.enable.zh\") text=\"启用 E2EE\" ;;\n \"matrix_e2ee.enable.en\") text=\"Enable E2EE\" ;;\n \"matrix_e2ee.disable.zh\") text=\"禁用 E2EE(推荐)\" ;;\n \"matrix_e2ee.disable.en\") text=\"Disable E2EE (recommended)\" ;;\n \"matrix_e2ee.choice.zh\") text=\"请选择 [1/2]\" ;;\n \"matrix_e2ee.choice.en\") text=\"Enter choice [1/2]\" ;;\n \"matrix_e2ee.selected_enabled.zh\") text=\"Matrix E2EE: 已启用\" ;;\n \"matrix_e2ee.selected_enabled.en\") text=\"Matrix E2EE: enabled\" ;;\n \"matrix_e2ee.selected_disabled.zh\") text=\"Matrix E2EE: 已禁用(默认)\" ;;\n \"matrix_e2ee.selected_disabled.en\") text=\"Matrix E2EE: disabled (default)\" ;;\n \"matrix_e2ee.title_short.zh\") text=\"Matrix E2EE\" ;;\n \"matrix_e2ee.title_short.en\") text=\"Matrix E2EE\" ;;\n \"matrix_e2ee.val_enabled.zh\") text=\"已启用\" ;;\n \"matrix_e2ee.val_enabled.en\") text=\"enabled\" ;;\n \"matrix_e2ee.val_disabled.zh\") text=\"已禁用\" ;;\n \"matrix_e2ee.val_disabled.en\") text=\"disabled\" ;;\n # --- Docker API proxy ---\n \"docker_proxy.title.zh\") text=\"--- Docker API 安全代理 ---\" ;;\n \"docker_proxy.title.en\") text=\"--- Docker API Security Proxy ---\" ;;\n \"docker_proxy.desc.zh\") text=\"Docker API 代理可防止 AI Agent 通过 Docker API 越狱访问宿主机。\\n 启用后,Manager 不再直接持有 Docker socket,所有容器操作经过安全校验。\" ;;\n \"docker_proxy.desc.en\") text=\"Docker API proxy prevents AI Agents from escaping via Docker API to access the host.\\n When enabled, Manager no longer has direct Docker socket access; all container operations go through security validation.\" ;;\n \"docker_proxy.enable.zh\") text=\"启用(推荐)\" ;;\n \"docker_proxy.enable.en\") text=\"Enable (recommended)\" ;;\n \"docker_proxy.disable.zh\") text=\"禁用(直接挂载 Docker socket)\" ;;\n \"docker_proxy.disable.en\") text=\"Disable (mount Docker socket directly)\" ;;\n \"docker_proxy.choice.zh\") text=\"请选择 [1/2]\" ;;\n \"docker_proxy.choice.en\") text=\"Enter choice [1/2]\" ;;\n \"docker_proxy.selected_enabled.zh\") text=\"Docker API 代理: 已启用\" ;;\n \"docker_proxy.selected_enabled.en\") text=\"Docker API proxy: enabled\" ;;\n \"docker_proxy.selected_disabled.zh\") text=\"Docker API 代理: 已禁用\" ;;\n \"docker_proxy.selected_disabled.en\") text=\"Docker API proxy: disabled\" ;;\n \"docker_proxy.title_short.zh\") text=\"Docker API 代理\" ;;\n \"docker_proxy.title_short.en\") text=\"Docker API Proxy\" ;;\n \"docker_proxy.val_enabled.zh\") text=\"已启用\" ;;\n \"docker_proxy.val_enabled.en\") text=\"enabled\" ;;\n \"docker_proxy.val_disabled.zh\") text=\"已禁用\" ;;\n \"docker_proxy.val_disabled.en\") text=\"disabled\" ;;\n \"docker_proxy.registries_desc.zh\") text=\"默认放行的镜像来源:本地镜像、localhost、Higress 仓库(所有 region)。\\n 如需放行其他镜像仓库,请输入逗号分隔的地址前缀。\\n 示例: ghcr.io/myorg,registry.example.com/team\" ;;\n \"docker_proxy.registries_desc.en\") text=\"Default allowed image sources: local images, localhost, Higress registries (all regions).\\n To allow additional image sources, enter comma-separated address prefixes.\\n Example: ghcr.io/myorg,registry.example.com/team\" ;;\n \"docker_proxy.registries_prompt.zh\") text=\"额外放行的镜像来源(按回车跳过)\" ;;\n \"docker_proxy.registries_prompt.en\") text=\"Additional allowed image sources (press Enter to skip)\" ;;\n \"docker_proxy.registries_label.zh\") text=\"额外放行的镜像来源\" ;;\n \"docker_proxy.registries_label.en\") text=\"Additional allowed image sources\" ;;\n # --- Podman Autostart ---\n \"podman.autostart.title.zh\") text=\"--- Podman 驻留与开机自启 ---\" ;;\n \"podman.autostart.title.en\") text=\"--- Podman Linger & Autostart ---\" ;;\n \"podman.autostart.desc.zh\") text=\"配置为 systemd 服务,支持开机自动启动及后台驻留。\" ;;\n \"podman.autostart.desc.en\") text=\"Configure as a systemd service for automatic startup on boot.\" ;;\n \"podman.autostart.enable.zh\") text=\"启用(推荐)\" ;;\n \"podman.autostart.enable.en\") text=\"Enable (recommended)\" ;;\n \"podman.autostart.disable.zh\") text=\"不启用(容器随当前会话关闭而停止)\" ;;\n \"podman.autostart.disable.en\") text=\"Disable (containers stop when session ends)\" ;;\n \"podman.autostart.choice.zh\") text=\"请选择 [1/2]\" ;;\n \"podman.autostart.choice.en\") text=\"Enter choice [1/2]\" ;;\n \"podman.autostart.selected_enabled.zh\") text=\"Podman 开机自启: 已启用\" ;;\n \"podman.autostart.selected_enabled.en\") text=\"Podman Autostart: enabled\" ;;\n \"podman.autostart.selected_disabled.zh\") text=\"Podman 开机自启: 已禁用\" ;;\n \"podman.autostart.selected_disabled.en\") text=\"Podman Autostart: disabled\" ;;\n \"podman.autostart.title_short.zh\") text=\"Podman 开机自启\" ;;\n \"podman.autostart.title_short.en\") text=\"Podman Autostart\" ;;\n \"podman.autostart.val_enabled.zh\") text=\"已启用\" ;;\n \"podman.autostart.val_enabled.en\") text=\"enabled\" ;;\n \"podman.autostart.val_disabled.zh\") text=\"已禁用\" ;;\n \"podman.autostart.val_disabled.en\") text=\"disabled\" ;;\n \"install.podman.autostart_title.zh\") text=\"正在配置 Podman 容器开机自启...\" ;;\n \"install.podman.autostart_title.en\") text=\"Configuring Podman container autostart...\" ;;\n \"install.podman.linger_enable.zh\") text=\"尝试启用 systemd linger (如有提示请在此输入 sudo 密码)...\" ;;\n \"install.podman.linger_enable.en\") text=\"Attempting to enable systemd linger (enter sudo password if prompted)...\" ;;\n \"install.podman.linger_warn.zh\") text=\"警告: 无法自动启用 linger。驻留可能失败,请手动执行: sudo loginctl enable-linger \\$(whoami)\" ;;\n \"install.podman.linger_warn.en\") text=\"WARNING: Failed to auto-enable linger. Please run manually: sudo loginctl enable-linger \\$(whoami)\" ;;\n \"install.podman.root_setup.zh\") text=\"检测到 root 用户。正在配置系统级 agentteams-podman-restart 服务...\" ;;\n \"install.podman.root_setup.en\") text=\"Root user detected. Configuring system-wide agentteams-podman-restart service...\" ;;\n \"install.podman.root_success.zh\") text=\"✅ 系统级 agentteams-podman-restart 服务已成功启用并运行。\" ;;\n \"install.podman.root_success.en\") text=\"✅ System-wide agentteams-podman-restart service successfully enabled and running.\" ;;\n \"install.podman.root_fail.zh\") text=\"⚠️ 系统级 agentteams-podman-restart 服务启用失败,请稍后手动检查。\" ;;\n \"install.podman.root_fail.en\") text=\"⚠️ Failed to enable system-wide agentteams-podman-restart service. Please check manually later.\" ;;\n \"install.podman.user_setup.zh\") text=\"检测到普通用户 (%s)。正在配置用户级 agentteams-podman-restart 服务...\" ;;\n \"install.podman.user_setup.en\") text=\"Non-root user (%s) detected. Configuring user-level agentteams-podman-restart service...\" ;;\n \"install.podman.user_success.zh\") text=\"✅ 用户级 agentteams-podman-restart 服务已成功启用并运行。\" ;;\n \"install.podman.user_success.en\") text=\"✅ User-level agentteams-podman-restart service successfully enabled and running.\" ;;\n \"install.podman.user_fail.zh\") text=\"⚠️ 用户级 agentteams-podman-restart 服务启用失败。\\n提示:这通常是因为缺少 XDG_RUNTIME_DIR 或 dbus 没有运行。您可以尝试手动执行: systemctl --user enable --now agentteams-podman-restart.service\" ;;\n \"install.podman.user_fail.en\") text=\"⚠️ Failed to enable user-level agentteams-podman-restart service.\\nHint: This is usually due to missing XDG_RUNTIME_DIR or dbus not running. Try manually: systemctl --user enable --now agentteams-podman-restart.service\" ;;\n \"install.podman.success.zh\") text=\"Podman 开机自启配置完成。\" ;;\n \"install.podman.success.en\") text=\"Podman autostart successfully configured.\" ;;\n # --- Worker idle timeout ---\n \"idle_timeout.prompt.zh\") text=\"Worker 空闲自动停止超时(分钟)[720]\" ;;\n \"idle_timeout.prompt.en\") text=\"Worker idle auto-stop timeout in minutes [720]\" ;;\n \"idle_timeout.selected.zh\") text=\"Worker 空闲超时: %s 分钟\" ;;\n \"idle_timeout.selected.en\") text=\"Worker idle timeout: %s minutes\" ;;\n \"idle_timeout.label.zh\") text=\"Worker 空闲超时(分钟)\" ;;\n \"idle_timeout.label.en\") text=\"Worker idle timeout (min)\" ;;\n # --- YOLO mode ---\n \"install.yolo.zh\") text=\"YOLO 模式已启用(自主决策,无交互提示)\" ;;\n \"install.yolo.en\") text=\"YOLO mode enabled (autonomous decisions, no interactive prompts)\" ;;\n # --- Image pulling ---\n \"install.image.exists.zh\") text=\"Manager 镜像已存在: %s\" ;;\n \"install.image.exists.en\") text=\"Manager image already exists locally: %s\" ;;\n \"install.image.pulling_manager.zh\") text=\"正在拉取 Manager 镜像: %s\" ;;\n \"install.image.pulling_manager.en\") text=\"Pulling Manager image: %s\" ;;\n \"install.image.worker_exists.zh\") text=\"Worker 镜像已存在: %s\" ;;\n \"install.image.worker_exists.en\") text=\"Worker image already exists locally: %s\" ;;\n \"install.image.pulling_worker.zh\") text=\"正在拉取 Worker 镜像: %s\" ;;\n \"install.image.pulling_worker.en\") text=\"Pulling Worker image: %s\" ;;\n # --- Starting container ---\n \"install.starting_manager.zh\") text=\"正在启动 Manager 容器...\" ;;\n \"install.starting_manager.en\") text=\"Starting Manager container...\" ;;\n # --- Wait for Manager ready ---\n \"install.wait_ready.zh\") text=\"等待 Manager Agent 就绪(超时: %ss)...\" ;;\n \"install.wait_ready.en\") text=\"Waiting for Manager agent to be ready (timeout: %ss)...\" ;;\n \"install.wait_ready.ok.zh\") text=\"Manager Agent 已就绪!\" ;;\n \"install.wait_ready.ok.en\") text=\"Manager agent is ready!\" ;;\n \"install.wait_ready.waiting.zh\") text=\"等待中... (%ds/%ds)\" ;;\n \"install.wait_ready.waiting.en\") text=\"Waiting... (%ds/%ds)\" ;;\n \"install.wait_ready.timeout.zh\") text=\"Manager Agent 在 %ss 内未就绪。请检查: docker logs %s\" ;;\n \"install.wait_ready.timeout.en\") text=\"Manager agent did not become ready within %ss. Check: docker logs %s\" ;;\n # --- Wait for Matrix ready ---\n \"install.wait_matrix.zh\") text=\"等待 Matrix 服务就绪(超时: %ss)...\" ;;\n \"install.wait_matrix.en\") text=\"Waiting for Matrix server to be ready (timeout: %ss)...\" ;;\n \"install.wait_matrix.ok.zh\") text=\"Matrix 服务已就绪!\" ;;\n \"install.wait_matrix.ok.en\") text=\"Matrix server is ready!\" ;;\n \"install.wait_matrix.waiting.zh\") text=\"等待 Matrix 中... (%ds/%ds)\" ;;\n \"install.wait_matrix.waiting.en\") text=\"Waiting for Matrix... (%ds/%ds)\" ;;\n \"install.wait_matrix.timeout.zh\") text=\"Matrix 服务在 %ss 内未就绪。请检查: docker logs %s\" ;;\n \"install.wait_matrix.timeout.en\") text=\"Matrix server did not become ready within %ss. Check: docker logs %s\" ;;\n # --- OpenAI-compatible connectivity test ---\n \"llm.openai.test.testing.zh\") text=\"正在测试 API 联通性...\" ;;\n \"llm.openai.test.testing.en\") text=\"Testing API connectivity...\" ;;\n \"llm.openai.test.ok.zh\") text=\"✅ API 联通性测试通过\" ;;\n \"llm.openai.test.ok.en\") text=\"✅ API connectivity test passed\" ;;\n \"llm.openai.test.fail.zh\") text=\"⚠️ API 联通性测试失败(HTTP %s)。响应内容:\\n%s\\n请根据以上错误信息联系您的模型服务商解决。\" ;;\n \"llm.openai.test.fail.en\") text=\"⚠️ API connectivity test failed (HTTP %s). Response body:\\n%s\\nPlease contact your model provider to resolve the issue.\" ;;\n \"llm.openai.test.fail.tokenplan.zh\") text=\"⚠️ 提示: 请确认 API Key 有效且已开通通义 Token 套餐。文档: https://help.aliyun.com/zh/model-studio/token-plan-quickstart\" ;;\n \"llm.openai.test.fail.tokenplan.en\") text=\"⚠️ Hint: Verify your Token Plan API key and compatible-mode access. Docs: https://help.aliyun.com/zh/model-studio/token-plan-quickstart\" ;;\n \"llm.openai.test.fail.codingplan.zh\") text=\"⚠️ 提示: 请确认 API Key 有效且已开通通义 Token 套餐。文档: https://help.aliyun.com/zh/model-studio/token-plan-quickstart\" ;;\n \"llm.openai.test.fail.codingplan.en\") text=\"⚠️ Hint: Verify your DASHSCOPE_API_KEY for Qwen Cloud. API keys: https://home.qwencloud.com/api-keys Docs: https://docs.qwencloud.com/\" ;;\n \"llm.openai.test.fail.codingplan_legacy.zh\") text=\"⚠️ 提示: 请确认 API Key 有效且 Coding 套餐接口可用。文档: https://help.aliyun.com/zh/model-studio/get-api-key\" ;;\n \"llm.openai.test.fail.codingplan_legacy.en\") text=\"⚠️ Hint: Verify your DashScope API key and Coding Plan access. Docs: https://help.aliyun.com/zh/model-studio/get-api-key\" ;;\n \"llm.openai.test.no_curl.zh\") text=\"⚠️ 未找到 curl,跳过 API 联通性测试\" ;;\n \"llm.openai.test.no_curl.en\") text=\"⚠️ curl not found, skipping API connectivity test\" ;;\n \"llm.openai.test.confirm.zh\") text=\"是否仍要继续安装?[y/N/b] \" ;;\n \"llm.openai.test.confirm.en\") text=\"Continue with installation anyway? [y/N/b] \" ;;\n \"llm.embedding.title.zh\") text=\"📦 记忆搜索配置\" ;;\n \"llm.embedding.title.en\") text=\"📦 Memory Search Configuration\" ;;\n \"llm.embedding.hint.zh\") text=\" Embedding 模型可提升记忆搜索质量(语义匹配)。不启用也可正常使用记忆功能(关键词匹配)。\" ;;\n \"llm.embedding.hint.en\") text=\" Embedding model improves memory search quality (semantic matching). Memory still works without it (keyword matching).\" ;;\n \"llm.embedding.option.default.zh\") text=\" 1) text-embedding-v4(推荐)\" ;;\n \"llm.embedding.option.default.en\") text=\" 1) text-embedding-v4 (Recommended)\" ;;\n \"llm.embedding.option.custom.zh\") text=\" 2) 自定义 Embedding 模型\" ;;\n \"llm.embedding.option.custom.en\") text=\" 2) Custom embedding model\" ;;\n \"llm.embedding.option.disable.zh\") text=\" 3) 不启用\" ;;\n \"llm.embedding.option.disable.en\") text=\" 3) Do not enable\" ;;\n \"llm.embedding.select.zh\") text=\"选择\" ;;\n \"llm.embedding.select.en\") text=\"Select\" ;;\n \"llm.embedding.custom_prompt.zh\") text=\" Embedding 模型名称\" ;;\n \"llm.embedding.custom_prompt.en\") text=\" Embedding model name\" ;;\n \"llm.embedding.test.testing.zh\") text=\"正在测试 Embedding API 联通性...\" ;;\n \"llm.embedding.test.testing.en\") text=\"Testing Embedding API connectivity...\" ;;\n \"llm.embedding.test.ok.zh\") text=\"✅ Embedding API 联通性测试通过\" ;;\n \"llm.embedding.test.ok.en\") text=\"✅ Embedding API connectivity test passed\" ;;\n \"llm.embedding.test.fail.zh\") text=\"⚠️ Embedding API 测试失败(HTTP %s)。响应: %s\" ;;\n \"llm.embedding.test.fail.en\") text=\"⚠️ Embedding API test failed (HTTP %s). Response: %s\" ;;\n \"llm.embedding.auto_disabled.zh\") text=\"⚠️ Embedding 已自动禁用,记忆搜索将使用关键词匹配。您可以稍后在 agentteams-manager.env 中设置 AGENTTEAMS_EMBEDDING_MODEL 启用。\" ;;\n \"llm.embedding.auto_disabled.en\") text=\"⚠️ Embedding auto-disabled. Memory search will use keyword matching. You can enable it later in agentteams-manager.env by setting AGENTTEAMS_EMBEDDING_MODEL.\" ;;\n \"llm.embedding.disabled.zh\") text=\"ℹ️ Embedding 已禁用,记忆搜索将使用关键词匹配。\" ;;\n \"llm.embedding.disabled.en\") text=\"ℹ️ Embedding disabled. Memory search will use keyword matching.\" ;;\n \"llm.openai.test.aborted.zh\") text=\"安装已中止。\" ;;\n \"llm.openai.test.aborted.en\") text=\"Installation aborted.\" ;;\n \"nav.back_hint.zh\") text=\"(输入 b 返回上一步)\" ;;\n \"nav.back_hint.en\") text=\"(enter b to go back)\" ;;\n # --- OpenAI-compatible provider creation ---\n \"install.openai_compat.missing.zh\") text=\"警告: OpenAI Base URL 或 API Key 未设置,跳过提供商创建\" ;;\n \"install.openai_compat.missing.en\") text=\"WARNING: OpenAI Base URL or API Key not set, skipping provider creation\" ;;\n \"install.openai_compat.creating.zh\") text=\"正在创建 OpenAI 兼容提供商...\" ;;\n \"install.openai_compat.creating.en\") text=\"Creating OpenAI-compatible provider...\" ;;\n \"install.openai_compat.domain.zh\") text=\" 域名: %s\" ;;\n \"install.openai_compat.domain.en\") text=\" Domain: %s\" ;;\n \"install.openai_compat.port.zh\") text=\" 端口: %s\" ;;\n \"install.openai_compat.port.en\") text=\" Port: %s\" ;;\n \"install.openai_compat.protocol.zh\") text=\" 协议: %s\" ;;\n \"install.openai_compat.protocol.en\") text=\" Protocol: %s\" ;;\n \"install.openai_compat.service_fail.zh\") text=\"警告: 创建 DNS 服务源失败(可能已存在)\" ;;\n \"install.openai_compat.service_fail.en\") text=\"WARNING: Failed to create DNS service source (may already exist)\" ;;\n \"install.openai_compat.provider_fail.zh\") text=\"警告: 创建 AI 提供商失败(可能已存在)\" ;;\n \"install.openai_compat.provider_fail.en\") text=\"WARNING: Failed to create AI provider (may already exist)\" ;;\n \"install.openai_compat.success.zh\") text=\"OpenAI 兼容提供商创建成功\" ;;\n \"install.openai_compat.success.en\") text=\"OpenAI-compatible provider created successfully\" ;;\n # --- Welcome message ---\n \"install.welcome_msg.soul_configured.zh\") text=\"Soul 已配置(找到 soul-configured 标记),跳过 onboarding 消息\" ;;\n \"install.welcome_msg.soul_configured.en\") text=\"Soul already configured (soul-configured marker found), skipping onboarding message\" ;;\n \"install.welcome_msg.logging_in.zh\") text=\"正在以 %s 身份登录以发送欢迎消息...\" ;;\n \"install.welcome_msg.logging_in.en\") text=\"Logging in as %s to send welcome message...\" ;;\n \"install.welcome_msg.login_failed.zh\") text=\"警告: 以 %s 身份登录失败,跳过欢迎消息\" ;;\n \"install.welcome_msg.login_failed.en\") text=\"WARNING: Failed to login as %s, skipping welcome message\" ;;\n \"install.welcome_msg.finding_room.zh\") text=\"正在查找与 Manager 的 DM 房间...\" ;;\n \"install.welcome_msg.finding_room.en\") text=\"Finding DM room with Manager...\" ;;\n \"install.welcome_msg.creating_room.zh\") text=\"正在创建与 Manager 的 DM 房间...\" ;;\n \"install.welcome_msg.creating_room.en\") text=\"Creating DM room with Manager...\" ;;\n \"install.welcome_msg.no_room.zh\") text=\"警告: 无法找到或创建与 Manager 的 DM 房间\" ;;\n \"install.welcome_msg.no_room.en\") text=\"WARNING: Could not find or create DM room with Manager\" ;;\n \"install.welcome_msg.waiting_join.zh\") text=\"等待 Manager 加入房间...\" ;;\n \"install.welcome_msg.waiting_join.en\") text=\"Waiting for Manager to join the room...\" ;;\n \"install.welcome_msg.sending.zh\") text=\"正在向 Manager 发送欢迎消息...\" ;;\n \"install.welcome_msg.sending.en\") text=\"Sending welcome message to Manager...\" ;;\n \"install.welcome_msg.send_failed.zh\") text=\"警告: 发送欢迎消息失败\" ;;\n \"install.welcome_msg.send_failed.en\") text=\"WARNING: Failed to send welcome message\" ;;\n \"install.welcome_msg.sent.zh\") text=\"欢迎消息已发送给 Manager\" ;;\n \"install.welcome_msg.sent.en\") text=\"Welcome message sent to Manager\" ;;\n \"install.welcome_msg.waiting.zh\") text=\"等待 Manager 发送欢迎消息(Higress 路由授权 + LLM 探活,约 45-90s)...\" ;;\n \"install.welcome_msg.waiting.en\") text=\"Waiting for Manager to send the welcome message (Higress route auth + LLM probe, ~45-90s)...\" ;;\n \"install.welcome_msg.confirmed.zh\") text=\"Manager 已确认发送欢迎消息(status.welcomeSent=true,用时 %ss)\" ;;\n \"install.welcome_msg.confirmed.en\") text=\"Manager confirmed welcome message sent (status.welcomeSent=true, %ss elapsed)\" ;;\n \"install.welcome_msg.timeout.zh\") text=\"警告: 在 %ss 内未观察到 Manager 发送欢迎消息(status.welcomeSent=true)。安装仍然成功,所有服务已就绪——可继续按下方提示登录 Element Web。\" ;;\n \"install.welcome_msg.timeout.en\") text=\"WARNING: Did not observe the Manager sending its welcome message (status.welcomeSent=true) within %ss. Installation is still successful, all services are up — continue with the Element Web instructions below.\" ;;\n \"install.welcome_msg.timeout_hint.zh\") text=\"手动触发 onboarding: 登录 Element Web → 打开与 Manager 的 DM 房间 → 发送任意一句话(例如 \\\"hi\\\"),Manager 会接管对话并开始引导。\" ;;\n \"install.welcome_msg.timeout_hint.en\") text=\"Manual onboarding: log in to Element Web → open the DM with the Manager → send any message (e.g. \\\"hi\\\") and the Manager will take over and start the guided setup.\" ;;\n \"install.welcome_msg.timeout_inspect.zh\") text=\"排查命令: docker exec agentteams-controller agt get managers default\" ;;\n \"install.welcome_msg.timeout_inspect.en\") text=\"Inspect status: docker exec agentteams-controller agt get managers default\" ;;\n \"install.welcome_msg.poll_unavailable.zh\") text=\"提示: agentteams-manager 内未找到 AgentTeams CLI,跳过 welcome 等待(旧镜像?)\" ;;\n \"install.welcome_msg.poll_unavailable.en\") text=\"Note: AgentTeams CLI not found inside agentteams-manager; skipping welcome wait (old image?)\" ;;\n # --- Final output panel ---\n \"success.title.zh\") text=\"=== AgentTeams Manager 已启动!===\" ;;\n \"success.title.en\") text=\"=== AgentTeams Manager Started! ===\" ;;\n \"success.domains_configured.zh\") text=\"以下域名已配置解析到 127.0.0.1:\" ;;\n \"success.domains_configured.en\") text=\"The following domains are configured to resolve to 127.0.0.1:\" ;;\n \"success.open_url.zh\") text=\" ★ 在浏览器中打开以下 URL 开始使用: ★\" ;;\n \"success.open_url.en\") text=\" ★ Open the following URL in your browser to start: ★\" ;;\n \"success.login_with.zh\") text=\" 登录信息:\" ;;\n \"success.login_with.en\") text=\" Login with:\" ;;\n \"success.username.zh\") text=\" 用户名: %s\" ;;\n \"success.username.en\") text=\" Username: %s\" ;;\n \"success.password.zh\") text=\" 密码: %s\" ;;\n \"success.password.en\") text=\" Password: %s\" ;;\n \"success.after_login.zh\") text=\" 登录后,开始与 Manager 聊天!\" ;;\n \"success.after_login.en\") text=\" After login, start chatting with the Manager!\" ;;\n \"success.tell_it.zh\") text=\" 告诉它: \\\"创建一个名为 alice 的前端开发 Worker\\\"\" ;;\n \"success.tell_it.en\") text=\" Tell it: \\\"Create a Worker named alice for frontend dev\\\"\" ;;\n \"success.manager_auto.zh\") text=\" Manager 会自动处理一切。\" ;;\n \"success.manager_auto.en\") text=\" The Manager will handle everything automatically.\" ;;\n \"success.mobile_title.zh\") text=\" 📱 移动端访问(FluffyChat / Element Mobile):\" ;;\n \"success.mobile_title.en\") text=\" 📱 Mobile access (FluffyChat / Element Mobile):\" ;;\n \"success.mobile_step1.zh\") text=\" 1. 在手机上下载 FluffyChat 或 Element\" ;;\n \"success.mobile_step1.en\") text=\" 1. Download FluffyChat or Element on your phone\" ;;\n \"success.mobile_step2.zh\") text=\" 2. 设置 homeserver 为: %s\" ;;\n \"success.mobile_step2.en\") text=\" 2. Set homeserver to: %s\" ;;\n \"success.mobile_step2_noip.zh\") text=\" 2. 设置 homeserver 为: http://<本机局域网IP>:%s\" ;;\n \"success.mobile_step2_noip.en\") text=\" 2. Set homeserver to: http://<this-machine-LAN-IP>:%s\" ;;\n \"success.mobile_noip_hint.zh\") text=\" (无法自动检测局域网 IP — 请使用 ifconfig / ip addr 查看)\" ;;\n \"success.mobile_noip_hint.en\") text=\" (Could not detect LAN IP automatically — check with: ifconfig / ip addr)\" ;;\n \"success.mobile_step3.zh\") text=\" 3. 登录信息:\" ;;\n \"success.mobile_step3.en\") text=\" 3. Login with:\" ;;\n \"success.mobile_username.zh\") text=\" 用户名: %s\" ;;\n \"success.mobile_username.en\") text=\" Username: %s\" ;;\n \"success.mobile_password.zh\") text=\" 密码: %s\" ;;\n \"success.mobile_password.en\") text=\" Password: %s\" ;;\n # --- Other consoles and tips ---\n \"success.other_consoles.zh\") text=\"--- 其他控制台 ---\" ;;\n \"success.other_consoles.en\") text=\"--- Other Consoles ---\" ;;\n \"success.higress_console.zh\") text=\" Higress 控制台: http://localhost:%s(用户名: %s / 密码: %s)\" ;;\n \"success.higress_console.en\") text=\" Higress Console: http://localhost:%s (Username: %s / Password: %s)\" ;;\n \"success.manager_console.zh\") text=\" Manager 控制台(本地): http://localhost:%s(无需登录)\" ;;\n \"success.manager_console.en\") text=\" Manager Console (local): http://localhost:%s (no login required)\" ;;\n \"success.manager_console_gateway.zh\") text=\" Manager 控制台(网关): http://console-local.agentteams.io(用户名: %s / 密码: %s)\" ;;\n \"success.manager_console_gateway.en\") text=\" Manager Console (gateway): http://console-local.agentteams.io (Username: %s / Password: %s)\" ;;\n \"success.copaw_console.zh\") text=\" QwenPaw App API: http://localhost:%s(无需登录)\" ;;\n \"success.copaw_console.en\") text=\" QwenPaw App API: http://localhost:%s (no login required)\" ;;\n \"success.switch_llm.title.zh\") text=\"--- 切换 LLM 提供商 ---\" ;;\n \"success.switch_llm.title.en\") text=\"--- Switch LLM Providers ---\" ;;\n \"success.switch_llm.hint.zh\") text=\" 您可以通过 Higress 控制台切换到其他 LLM 提供商(OpenAI、Anthropic 等)。\" ;;\n \"success.switch_llm.hint.en\") text=\" You can switch to other LLM providers (OpenAI, Anthropic, etc.) via Higress Console.\" ;;\n \"success.switch_llm.docs.zh\") text=\" 详细说明请参阅:\" ;;\n \"success.switch_llm.docs.en\") text=\" For detailed instructions, see:\" ;;\n \"success.switch_llm.url.zh\") text=\" https://higress.ai/en/docs/ai/scene-guide/multi-proxy#console-configuration\" ;;\n \"success.switch_llm.url.en\") text=\" https://higress.ai/en/docs/ai/scene-guide/multi-proxy#console-configuration\" ;;\n \"success.tip.zh\") text=\"提示: 您也可以在聊天中让 Manager 为您配置 LLM 提供商。\" ;;\n \"success.tip.en\") text=\"Tip: You can also ask the Manager to configure LLM providers for you in the chat.\" ;;\n \"success.config_file.zh\") text=\"配置文件: %s\" ;;\n \"success.config_file.en\") text=\"Configuration file: %s\" ;;\n \"success.data_volume.zh\") text=\"数据卷: %s\" ;;\n \"success.data_volume.en\") text=\"Data volume: %s\" ;;\n \"success.workspace.zh\") text=\"Manager 工作空间: %s\" ;;\n \"success.workspace.en\") text=\"Manager workspace: %s\" ;;\n # --- Worker installation ---\n \"worker.resetting.zh\") text=\"正在重置 Worker: %s...\" ;;\n \"worker.resetting.en\") text=\"Resetting Worker: %s...\" ;;\n \"worker.exists.zh\") text=\"容器 '%s' 已存在。使用 --reset 重新创建。\" ;;\n \"worker.exists.en\") text=\"Container '%s' already exists. Use --reset to recreate.\" ;;\n \"worker.starting.zh\") text=\"正在启动 Worker: %s...\" ;;\n \"worker.starting.en\") text=\"Starting Worker: %s...\" ;;\n \"worker.skills_url.zh\") text=\" Skills API URL: %s\" ;;\n \"worker.skills_url.en\") text=\" Skills API URL: %s\" ;;\n \"worker.started.zh\") text=\"=== Worker %s 已启动!===\" ;;\n \"worker.started.en\") text=\"=== Worker %s Started! ===\" ;;\n \"worker.container.zh\") text=\"容器: %s\" ;;\n \"worker.container.en\") text=\"Container: %s\" ;;\n \"worker.view_logs.zh\") text=\"查看日志: docker logs -f %s\" ;;\n \"worker.view_logs.en\") text=\"View logs: docker logs -f %s\" ;;\n # --- Prompt function messages ---\n \"prompt.preset.zh\") text=\" %s = (已通过环境变量预设)\" ;;\n \"prompt.preset.en\") text=\" %s = (pre-set via env)\" ;;\n \"prompt.upgrade_keep.zh\") text=\" %s = %s(当前值,回车保留 / 输入新值覆盖)\" ;;\n \"prompt.upgrade_keep.en\") text=\" %s = %s (current value, press Enter to keep / type new value to change)\" ;;\n \"prompt.upgrade_keep_secret.zh\") text=\" %s = %s(当前值,回车保留 / 输入新值覆盖)\" ;;\n \"prompt.upgrade_keep_secret.en\") text=\" %s = %s (current value, press Enter to keep / type new value to change)\" ;;\n \"prompt.upgrade_empty.zh\") text=\" %s = (未设置,回车跳过 / 输入新值设置)\" ;;\n \"prompt.upgrade_empty.en\") text=\" %s = (not set, press Enter to skip / type new value to set)\" ;;\n \"prompt.default.zh\") text=\" %s = %s(默认)\" ;;\n \"prompt.default.en\") text=\" %s = %s (default)\" ;;\n \"prompt.required.zh\") text=\"%s 是必需的(在非交互模式下通过环境变量设置)\" ;;\n \"prompt.required.en\") text=\"%s is required (set via environment variable in non-interactive mode)\" ;;\n \"prompt.required_empty.zh\") text=\"%s 是必需的\" ;;\n \"prompt.required_empty.en\") text=\"%s is required\" ;;\n # --- Language switch prompt (bilingual by design) ---\n \"lang.detected.zh\") text=\"检测到语言 / Detected language: 中文\" ;;\n \"lang.detected.en\") text=\"检测到语言 / Detected language: English\" ;;\n \"lang.switch_title.zh\") text=\"切换语言 / Switch language:\" ;;\n \"lang.switch_title.en\") text=\"切换语言 / Switch language:\" ;;\n \"lang.option_zh.zh\") text=\" 1) 中文\" ;;\n \"lang.option_zh.en\") text=\" 1) 中文\" ;;\n \"lang.option_en.zh\") text=\" 2) English\" ;;\n \"lang.option_en.en\") text=\" 2) English\" ;;\n \"lang.prompt.zh\") text=\"请选择 / Enter choice\" ;;\n \"lang.prompt.en\") text=\"请选择 / Enter choice\" ;;\n # --- Uninstall messages ---\n \"uninstall.title.zh\") text=\"正在卸载 AgentTeams...\" ;;\n \"uninstall.title.en\") text=\"Uninstalling AgentTeams...\" ;;\n \"uninstall.stopping_manager.zh\") text=\"正在停止并移除 agentteams-manager...\" ;;\n \"uninstall.stopping_manager.en\") text=\"Stopping and removing agentteams-manager...\" ;;\n \"uninstall.stopping_workers.zh\") text=\"正在停止并移除 Worker 容器...\" ;;\n \"uninstall.stopping_workers.en\") text=\"Stopping and removing worker containers...\" ;;\n \"uninstall.removed.zh\") text=\" 已移除: %s\" ;;\n \"uninstall.removed.en\") text=\" Removed: %s\" ;;\n \"uninstall.removing_volume.zh\") text=\"正在移除 Docker 卷: %s\" ;;\n \"uninstall.removing_volume.en\") text=\"Removing Docker volume: %s\" ;;\n \"uninstall.removing_env.zh\") text=\"正在移除 env 文件: %s\" ;;\n \"uninstall.removing_env.en\") text=\"Removing env file: %s\" ;;\n \"uninstall.removing_proxy.zh\") text=\"正在停止并移除 Docker API 代理容器: agentteams-docker-proxy\" ;;\n \"uninstall.removing_proxy.en\") text=\"Stopping and removing Docker API proxy container: agentteams-docker-proxy\" ;;\n \"uninstall.stopping_controller.zh\") text=\"正在停止并移除 agentteams-controller (内嵌 Tuwunel/MinIO/Higress)...\" ;;\n \"uninstall.stopping_controller.en\") text=\"Stopping and removing agentteams-controller (embedded Tuwunel/MinIO/Higress)...\" ;;\n \"uninstall.removing_network.zh\") text=\"正在移除 Docker 网络: agentteams-net\" ;;\n \"uninstall.removing_network.en\") text=\"Removing Docker network: agentteams-net\" ;;\n \"uninstall.removing_workspace.zh\") text=\"正在移除工作空间目录: %s\" ;;\n \"uninstall.removing_workspace.en\") text=\"Removing workspace directory: %s\" ;;\n \"uninstall.removing_workspace_elevated.zh\") text=\" 工作空间包含 root 文件,通过容器清理...\" ;;\n \"uninstall.removing_workspace_elevated.en\") text=\" Workspace contains root-owned files, cleaning via container...\" ;;\n \"uninstall.removing_log.zh\") text=\"正在移除日志文件: %s\" ;;\n \"uninstall.removing_log.en\") text=\"Removing log file: %s\" ;;\n \"uninstall.done.zh\") text=\"AgentTeams 已卸载。\" ;;\n \"uninstall.done.en\") text=\"AgentTeams has been uninstalled.\" ;;\n # --- Error messages ---\n \"error.name_required.zh\") text=\"--name 是必需的\" ;;\n \"error.name_required.en\") text=\"--name is required\" ;;\n \"error.fs_required.zh\") text=\"--fs 是必需的\" ;;\n \"error.fs_required.en\") text=\"--fs is required\" ;;\n \"error.fs_key_required.zh\") text=\"--fs-key 是必需的\" ;;\n \"error.fs_key_required.en\") text=\"--fs-key is required\" ;;\n \"error.fs_secret_required.zh\") text=\"--fs-secret 是必需的\" ;;\n \"error.fs_secret_required.en\") text=\"--fs-secret is required\" ;;\n \"error.unknown_option.zh\") text=\"未知选项: %s\" ;;\n \"error.unknown_option.en\") text=\"Unknown option: %s\" ;;\n \"error.docker_not_found.zh\") text=\"未找到 docker 或 podman 命令。请先安装 Docker Desktop 或 Podman Desktop:\\n Docker Desktop: https://www.docker.com/products/docker-desktop/\\n Podman Desktop: https://podman-desktop.io/\" ;;\n \"error.docker_not_found.en\") text=\"docker or podman command not found. Please install Docker Desktop or Podman Desktop first:\\n Docker Desktop: https://www.docker.com/products/docker-desktop/\\n Podman Desktop: https://podman-desktop.io/\" ;;\n \"error.docker_not_running.zh\") text=\"Docker 未运行。请先启动 Docker Desktop 或 Podman Desktop。\" ;;\n \"error.docker_not_running.en\") text=\"Docker is not running. Please start Docker Desktop or Podman Desktop first.\" ;;\n # --- Fallback: try English for unknown lang ---\n *)\n case \"${key}.en\" in\n \"tz.warning.title.en\") text=\"Could not detect timezone automatically.\" ;;\n \"install.title.en\") text=\"=== AgentTeams Manager Installation ===\" ;;\n *) text=\"${key}\" ;;\n esac\n ;;\n esac\n if [ $# -gt 0 ]; then\n # shellcheck disable=SC2059\n printf \"${text}\\n\" \"$@\"\n else\n echo \"${text}\"\n fi\n}\n\n# ============================================================\n# Registry selection based on timezone\n# ============================================================\n\ndetect_registry() {\n local tz=\"${AGENTTEAMS_TIMEZONE}\"\n\n case \"${tz}\" in\n America/*)\n echo \"higress-registry.us-west-1.cr.aliyuncs.com\"\n ;;\n Asia/Singapore|Asia/Bangkok|Asia/Jakarta|Asia/Makassar|Asia/Jayapura|\\\n Asia/Kuala_Lumpur|Asia/Ho_Chi_Minh|Asia/Manila|Asia/Yangon|\\\n Asia/Vientiane|Asia/Phnom_Penh|Asia/Pontianak|Asia/Ujung_Pandang)\n echo \"higress-registry.ap-southeast-7.cr.aliyuncs.com\"\n ;;\n *)\n echo \"higress-registry.cn-hangzhou.cr.aliyuncs.com\"\n ;;\n esac\n}\n\nAGENTTEAMS_REGISTRY=\"${AGENTTEAMS_REGISTRY:-$(detect_registry)}\"\n# Image variables are resolved after version selection in step_version().\n# These placeholders allow early code paths to reference them without errors.\nMANAGER_IMAGE=\"${AGENTTEAMS_INSTALL_MANAGER_IMAGE:-}\"\nMANAGER_COPAW_IMAGE=\"${AGENTTEAMS_INSTALL_MANAGER_COPAW_IMAGE:-}\"\nWORKER_IMAGE=\"${AGENTTEAMS_INSTALL_WORKER_IMAGE:-}\"\nCOPAW_WORKER_IMAGE=\"${AGENTTEAMS_INSTALL_COPAW_WORKER_IMAGE:-}\"\nHERMES_WORKER_IMAGE=\"${AGENTTEAMS_INSTALL_HERMES_WORKER_IMAGE:-}\"\nCONTROLLER_IMAGE=\"${AGENTTEAMS_INSTALL_CONTROLLER_IMAGE:-}\"\n\nresolve_image_tags() {\n MANAGER_IMAGE=\"${AGENTTEAMS_INSTALL_MANAGER_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-manager:${AGENTTEAMS_VERSION}}\"\n MANAGER_COPAW_IMAGE=\"${AGENTTEAMS_INSTALL_MANAGER_COPAW_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-manager-copaw:${AGENTTEAMS_VERSION}}\"\n WORKER_IMAGE=\"${AGENTTEAMS_INSTALL_WORKER_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-worker:${AGENTTEAMS_VERSION}}\"\n COPAW_WORKER_IMAGE=\"${AGENTTEAMS_INSTALL_COPAW_WORKER_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-copaw-worker:${AGENTTEAMS_VERSION}}\"\n HERMES_WORKER_IMAGE=\"${AGENTTEAMS_INSTALL_HERMES_WORKER_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-hermes-worker:${AGENTTEAMS_VERSION}}\"\n EMBEDDED_IMAGE=\"${AGENTTEAMS_INSTALL_EMBEDDED_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-embedded:${AGENTTEAMS_VERSION}}\"\n # CoPaw Worker introduced in v1.0.4; Hermes Worker introduced in v1.1.0\n if [ -z \"${AGENTTEAMS_INSTALL_COPAW_WORKER_IMAGE:-}\" ] && _ver_lt \"${AGENTTEAMS_VERSION}\" \"v1.0.4\"; then\n COPAW_WORKER_IMAGE=\"\"\n fi\n if [ -z \"${AGENTTEAMS_INSTALL_HERMES_WORKER_IMAGE:-}\" ] && _ver_lt \"${AGENTTEAMS_VERSION}\" \"v1.1.0\"; then\n HERMES_WORKER_IMAGE=\"\"\n fi\n}\n\n# Resolve the embedded controller image. Embedded mode is the only supported\n# architecture since PR #616 (manager image no longer bundles Higress/Tuwunel/MinIO).\n# If the embedded image is unavailable for the requested version, fail fast with an\n# actionable error rather than silently falling back to the legacy single-container\n# path — that path is permanently broken with the slim manager image and would just\n# leave the user with a manager container looping on \"Waiting for Higress Gateway\".\n# Sets EMBEDDED_IMAGE and AGENTTEAMS_USE_EMBEDDED.\nresolve_embedded_image() {\n AGENTTEAMS_USE_EMBEDDED=1\n\n # If the user explicitly overrode the image (e.g. `make install-embedded` passes\n # a locally-built tag), respect it as-is without any registry probe.\n if [ -n \"${AGENTTEAMS_INSTALL_EMBEDDED_IMAGE:-}\" ]; then\n EMBEDDED_IMAGE=\"${AGENTTEAMS_INSTALL_EMBEDDED_IMAGE}\"\n return 0\n fi\n\n local _versioned=\"${AGENTTEAMS_REGISTRY}/agentteams/agentteams-embedded:${AGENTTEAMS_VERSION}\"\n local _latest=\"${AGENTTEAMS_REGISTRY}/agentteams/agentteams-embedded:latest\"\n\n # Skip probe when AGENTTEAMS_VERSION is \"latest\" — no point trying the same tag twice.\n if [ \"${AGENTTEAMS_VERSION}\" = \"latest\" ]; then\n EMBEDDED_IMAGE=\"${_latest}\"\n return 0\n fi\n\n if ${DOCKER_CMD} pull \"${_versioned}\" >/dev/null 2>&1; then\n EMBEDDED_IMAGE=\"${_versioned}\"\n return 0\n fi\n\n # Versions before v1.1.0 predate agentteams-embedded entirely — their manager image\n # bundled all infrastructure. Falling back to agentteams-embedded:latest would\n # silently swap in the v1.1.0 architecture (embedded kube-apiserver) which\n # crashes under QEMU on Apple Silicon. Auto-activate legacy mode instead.\n if _ver_lt \"${AGENTTEAMS_VERSION}\" \"v1.1.0\"; then\n log \"INFO: ${AGENTTEAMS_VERSION} predates agentteams-embedded; switching to legacy all-in-one manager architecture.\"\n log \"WARNING: Legacy all-in-one mode requires AGENTTEAMS_VERSION <= v1.0.9 (older bundled manager image).\"\n log \"WARNING: Newer slim manager images will hang on 'Waiting for Higress Gateway'.\"\n AGENTTEAMS_USE_EMBEDDED=0\n return 0\n fi\n\n if ${DOCKER_CMD} pull \"${_latest}\" >/dev/null 2>&1; then\n log \"embedded ${AGENTTEAMS_VERSION} not found, using latest\"\n EMBEDDED_IMAGE=\"${_latest}\"\n return 0\n fi\n\n # Explicit escape hatch — still honoured for edge cases.\n if [ \"${AGENTTEAMS_FORCE_LEGACY:-0}\" = \"1\" ]; then\n log \"WARNING: AGENTTEAMS_FORCE_LEGACY=1 — using legacy all-in-one manager architecture.\"\n log \"WARNING: This requires AGENTTEAMS_VERSION <= v1.0.9 (older bundled manager image).\"\n log \"WARNING: Newer slim manager images will hang on 'Waiting for Higress Gateway'.\"\n AGENTTEAMS_USE_EMBEDDED=0\n return 0\n fi\n\n error \"Embedded controller image is not available in the registry:\"\n error \" - tried: ${_versioned}\"\n error \" - tried: ${_latest}\"\n error \"\"\n error \"Embedded mode is the only supported architecture since PR #616.\"\n error \"How to resolve:\"\n error \" 1) Pin to a AGENTTEAMS_VERSION whose embedded image has been published, or\"\n error \" wait for the release pipeline to publish it.\"\n error \" 2) For a local build, run: make install-embedded\"\n error \" (builds and uses the local embedded image without touching the registry).\"\n error \" 3) Override with a custom image: AGENTTEAMS_INSTALL_EMBEDDED_IMAGE=... ./agentteams-install.sh\"\n exit 1\n}\n\n# ============================================================\n# Known models list — used to detect custom models during install\n# ============================================================\nKNOWN_MODELS=\"gpt-5.4 gpt-5.3-codex gpt-5-mini gpt-5-nano claude-opus-4-6 claude-sonnet-4-6 claude-haiku-4-5 qwen3.6-plus qwen3.5-plus deepseek-chat deepseek-reasoner kimi-k2.5 glm-5 MiniMax-M2.7 MiniMax-M2.7-highspeed MiniMax-M2.5\"\n\nis_known_model() {\n local model=\"$1\"\n for m in ${KNOWN_MODELS}; do\n [ \"${m}\" = \"${model}\" ] && return 0\n done\n return 1\n}\n\n# Prompt user for custom model parameters when model is not in the known list.\n# Sets: AGENTTEAMS_MODEL_CONTEXT_WINDOW, AGENTTEAMS_MODEL_MAX_TOKENS, AGENTTEAMS_MODEL_REASONING, AGENTTEAMS_MODEL_VISION\nprompt_custom_model_params() {\n local model=\"$1\"\n if is_known_model \"${model}\"; then\n # Clear any stale custom params for known models\n AGENTTEAMS_MODEL_CONTEXT_WINDOW=\"\"\n AGENTTEAMS_MODEL_MAX_TOKENS=\"\"\n AGENTTEAMS_MODEL_REASONING=\"\"\n AGENTTEAMS_MODEL_VISION=\"\"\n return\n fi\n echo \"\"\n log \"$(msg llm.custom_model.detected \"${model}\")\"\n echo \"\"\n read -e -p \" $(msg llm.custom_model.context_prompt): \" AGENTTEAMS_MODEL_CONTEXT_WINDOW\n if [ \"${AGENTTEAMS_MODEL_CONTEXT_WINDOW}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n AGENTTEAMS_MODEL_CONTEXT_WINDOW=\"${AGENTTEAMS_MODEL_CONTEXT_WINDOW:-150000}\"\n read -e -p \" $(msg llm.custom_model.max_tokens_prompt): \" AGENTTEAMS_MODEL_MAX_TOKENS\n if [ \"${AGENTTEAMS_MODEL_MAX_TOKENS}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n AGENTTEAMS_MODEL_MAX_TOKENS=\"${AGENTTEAMS_MODEL_MAX_TOKENS:-128000}\"\n read -e -p \" $(msg llm.custom_model.reasoning_prompt): \" _reasoning\n if [ \"${_reasoning}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n case \"${_reasoning}\" in\n n|N|no|NO) AGENTTEAMS_MODEL_REASONING=\"false\" ;;\n *) AGENTTEAMS_MODEL_REASONING=\"true\" ;;\n esac\n read -e -p \" $(msg llm.custom_model.vision_prompt): \" _vision\n if [ \"${_vision}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n case \"${_vision}\" in\n y|Y|yes|YES) AGENTTEAMS_MODEL_VISION=\"true\" ;;\n *) AGENTTEAMS_MODEL_VISION=\"false\" ;;\n esac\n log \"$(msg llm.custom_model.summary \"${AGENTTEAMS_MODEL_CONTEXT_WINDOW}\" \"${AGENTTEAMS_MODEL_MAX_TOKENS}\" \"${AGENTTEAMS_MODEL_REASONING}\" \"${AGENTTEAMS_MODEL_VISION}\")\"\n}\n\n# ============================================================\n# Wait for Manager agent to be ready\n# Uses `openclaw gateway health` inside the container to confirm the gateway is running\n# ============================================================\n\nwait_manager_ready() {\n local timeout=\"${AGENTTEAMS_READY_TIMEOUT:-300}\"\n local elapsed=0\n local container=\"${1:-agentteams-manager}\"\n\n log \"$(msg install.wait_ready \"${timeout}\")\"\n\n # Wait for Manager agent to be healthy inside the container\n local runtime=\"${AGENTTEAMS_MANAGER_RUNTIME:-copaw}\"\n while [ \"${elapsed}\" -lt \"${timeout}\" ]; do\n case \"${runtime}\" in\n copaw)\n if ${DOCKER_CMD} exec \"${container}\" curl -sf http://127.0.0.1:18799/api/agents 2>/dev/null | grep -q '\"agents\"'; then\n log \"$(msg install.wait_ready.ok)\"\n return 0\n fi\n ;;\n *)\n if ${DOCKER_CMD} exec \"${container}\" openclaw gateway health --json 2>/dev/null | grep -q '\"ok\"' 2>/dev/null; then\n log \"$(msg install.wait_ready.ok)\"\n return 0\n fi\n ;;\n esac\n sleep 5\n elapsed=$((elapsed + 5))\n printf \"\\r\\033[36m[AgentTeams]\\033[0m $(msg install.wait_ready.waiting \"${elapsed}\" \"${timeout}\")\"\n done\n\n echo \"\"\n die \"$(msg install.wait_ready.timeout \"${timeout}\" \"${container}\")\"\n}\n\nwait_matrix_ready() {\n local timeout=\"${AGENTTEAMS_READY_TIMEOUT:-300}\"\n local elapsed=0\n local container=\"${1:-agentteams-manager}\"\n\n log \"$(msg install.wait_matrix \"${timeout}\")\"\n\n while [ \"${elapsed}\" -lt \"${timeout}\" ]; do\n if ${DOCKER_CMD} exec \"${container}\" curl -sf http://127.0.0.1:6167/_tuwunel/server_version >/dev/null 2>&1; then\n log \"$(msg install.wait_matrix.ok)\"\n return 0\n fi\n sleep 5\n elapsed=$((elapsed + 5))\n printf \"\\r\\033[36m[AgentTeams]\\033[0m $(msg install.wait_matrix.waiting \"${elapsed}\" \"${timeout}\")\"\n done\n\n echo \"\"\n die \"$(msg install.wait_matrix.timeout \"${timeout}\" \"${container}\")\"\n}\n\n# Read KEY=value from /data/agentteams-secrets.env on a Docker volume (manager container not required).\n# Requires EMBEDDED_IMAGE (resolved earlier in install_manager). Uses ${DOCKER_CMD}.\nagentteams_read_secret_from_data_volume() {\n local _vol=\"$1\" _key=\"$2\"\n if [ -z \"${_vol}\" ] || [ -z \"${_key}\" ] || [ -z \"${EMBEDDED_IMAGE:-}\" ]; then\n echo \"\"\n return 0\n fi\n ${DOCKER_CMD} run --rm --entrypoint sh \\\n -v \"${_vol}:/data:ro\" \\\n \"${EMBEDDED_IMAGE}\" -c \"grep \\\"^${_key}=\\\" /data/agentteams-secrets.env 2>/dev/null | cut -d= -f2- | head -1 | tr -d '\\r'\" 2>/dev/null\n}\n\n# Read KEY=value from /data/worker-creds/<worker>.env on a Docker volume.\nagentteams_read_worker_creds_value_from_volume() {\n local _vol=\"$1\" _worker=\"$2\" _key=\"$3\"\n if [ -z \"${_vol}\" ] || [ -z \"${_worker}\" ] || [ -z \"${_key}\" ] || [ -z \"${EMBEDDED_IMAGE:-}\" ]; then\n echo \"\"\n return 0\n fi\n ${DOCKER_CMD} run --rm --entrypoint sh \\\n -v \"${_vol}:/data:ro\" \\\n \"${EMBEDDED_IMAGE}\" -c \"grep \\\"^${_key}=\\\" \\\"/data/worker-creds/${_worker}.env\\\" 2>/dev/null | cut -d= -f2- | head -1 | tr -d \\\"\\\\r\\\"\" 2>/dev/null\n}\n\n# Read admin_dm_room_id from host workspace state.json (fallback when Matrix API is unavailable).\nagentteams_read_admin_dm_room_from_workspace() {\n local _ws=\"$1\"\n local _f=\"${_ws}/state.json\"\n if [ ! -f \"${_f}\" ] || ! command -v jq >/dev/null 2>&1; then\n echo \"\"\n return 0\n fi\n jq -r '.admin_dm_room_id // empty | select(. != \"null\")' \"${_f}\" 2>/dev/null\n}\n\n# Read secret input with masked echo (shows * per keystroke, supports backspace)\n# Usage: read_secret \"prompt text: \"; value=\"${_RS_RESULT}\"\nread_secret() {\n local _rs_prompt=\"$1\"\n _RS_RESULT=\"\"\n local _rs_char=\"\"\n\n printf \"%s\" \"${_rs_prompt}\"\n\n while IFS= read -r -s -n 1 _rs_char; do\n if [[ -z \"${_rs_char}\" ]]; then\n break\n elif [[ \"${_rs_char}\" == $'\\177' ]] || [[ \"${_rs_char}\" == $'\\b' ]]; then\n if [ -n \"${_RS_RESULT}\" ]; then\n _RS_RESULT=\"${_RS_RESULT%?}\"\n printf \"\\b \\b\"\n fi\n else\n _RS_RESULT=\"${_RS_RESULT}${_rs_char}\"\n printf \"*\"\n fi\n done\n echo\n}\n\n# Load current parameter values from env file for upgrade mode display\nload_current_params_from_env() {\n local env_file=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n if [ -f \"${env_file}\" ]; then\n [ -z \"${AGENTTEAMS_LLM_PROVIDER:+x}\" ] && AGENTTEAMS_LLM_PROVIDER=\"$(grep '^AGENTTEAMS_LLM_PROVIDER=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_OPENAI_BASE_URL:+x}\" ] && AGENTTEAMS_OPENAI_BASE_URL=\"$(grep '^AGENTTEAMS_OPENAI_BASE_URL=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_DEFAULT_MODEL:+x}\" ] && AGENTTEAMS_DEFAULT_MODEL=\"$(grep '^AGENTTEAMS_DEFAULT_MODEL=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_EMBEDDING_MODEL:+x}\" ] && AGENTTEAMS_EMBEDDING_MODEL=\"$(grep '^AGENTTEAMS_EMBEDDING_MODEL=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_WORKSPACE_DIR:+x}\" ] && AGENTTEAMS_WORKSPACE_DIR=\"$(grep '^AGENTTEAMS_WORKSPACE_DIR=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_HOST_SHARE_DIR:+x}\" ] && AGENTTEAMS_HOST_SHARE_DIR=\"$(grep '^AGENTTEAMS_HOST_SHARE_DIR=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_PODMAN_AUTOSTART:+x}\" ] && AGENTTEAMS_PODMAN_AUTOSTART=\"$(grep '^AGENTTEAMS_PODMAN_AUTOSTART=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n fi\n}\n\n# In non-interactive mode, uses default or errors if required and no default.\n# Usage: prompt VAR_NAME \"Prompt text\" \"default\" [true=secret]\nprompt() {\n local var_name=\"$1\"\n local prompt_text=\"$2\"\n local default_value=\"$3\"\n local is_secret=\"${4:-false}\"\n\n # If the variable is already set in the environment, use it silently\n # In upgrade mode, show current value and let user change it\n eval \"local current_value=\\\"\\${${var_name}}\\\"\"\n if [ -n \"${current_value}\" ]; then\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_NON_INTERACTIVE}\" != \"1\" ]; then\n # Show masked value for secrets, full value otherwise\n local display_value=\"${current_value}\"\n if [ \"${is_secret}\" = \"true\" ]; then\n local len=${#current_value}\n if [ \"${len}\" -le 8 ]; then\n display_value=\"****\"\n else\n display_value=\"${current_value:0:4}****${current_value: -4}\"\n fi\n fi\n log \"$(msg prompt.upgrade_keep \"${prompt_text}\" \"${display_value}\")\"\n local new_value=\"\"\n if [ \"${is_secret}\" = \"true\" ]; then\n read_secret \"${prompt_text}: \"\n new_value=\"${_RS_RESULT}\"\n else\n read -e -p \"${prompt_text}: \" new_value\n if [ \"${new_value}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n fi\n if [ -n \"${new_value}\" ]; then\n eval \"export ${var_name}='${new_value}'\"\n fi\n return\n fi\n log \"$(msg prompt.preset \"${prompt_text}\")\"\n return\n fi\n\n # Non-interactive or quickstart: use default or error\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] || [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ]; then\n if [ -n \"${default_value}\" ]; then\n eval \"export ${var_name}='${default_value}'\"\n log \"$(msg prompt.default \"${prompt_text}\" \"${default_value}\")\"\n return\n elif [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n # Only hard-error in fully non-interactive mode, not quickstart\n die \"$(msg prompt.required \"${prompt_text}\")\"\n fi\n # quickstart + no default: fall through to interactive prompt below\n fi\n\n if [ -n \"${default_value}\" ]; then\n prompt_text=\"${prompt_text} [${default_value}]\"\n fi\n\n local value=\"\"\n if [ \"${is_secret}\" = \"true\" ]; then\n read_secret \"${prompt_text}: \"\n value=\"${_RS_RESULT}\"\n else\n read -e -p \"${prompt_text}: \" value\n if [ \"${value}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n fi\n\n value=\"${value:-${default_value}}\"\n if [ -z \"${value}\" ]; then\n die \"$(msg prompt.required_empty \"${prompt_text}\")\"\n fi\n\n eval \"export ${var_name}='${value}'\"\n}\n\n# Prompt for an optional value (empty string is acceptable)\n# Skips prompt if variable is already defined in environment (even if empty)\n# In upgrade mode, shows current value and lets user change it.\n# In non-interactive mode, defaults to empty string.\nprompt_optional() {\n local var_name=\"$1\"\n local prompt_text=\"$2\"\n local is_secret=\"${3:-false}\"\n\n # Check if variable is defined (even if set to empty string)\n eval \"local _chk=\\\"\\${${var_name}+x}\\\"\"\n if [ -n \"${_chk}\" ]; then\n # In upgrade mode, show current value and let user change it\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_NON_INTERACTIVE}\" != \"1\" ]; then\n eval \"local current_value=\\\"\\${${var_name}}\\\"\"\n local display_value=\"${current_value}\"\n if [ \"${is_secret}\" = \"true\" ] && [ -n \"${current_value}\" ]; then\n local len=${#current_value}\n if [ \"${len}\" -le 8 ]; then\n display_value=\"****\"\n else\n display_value=\"${current_value:0:4}****${current_value: -4}\"\n fi\n fi\n if [ -n \"${current_value}\" ]; then\n if [ \"${is_secret}\" = \"true\" ]; then\n log \"$(msg prompt.upgrade_keep_secret \"${prompt_text}\" \"${display_value}\")\"\n else\n log \"$(msg prompt.upgrade_keep \"${prompt_text}\" \"${display_value}\")\"\n fi\n else\n log \"$(msg prompt.upgrade_empty \"${prompt_text}\")\"\n fi\n local new_value=\"\"\n if [ \"${is_secret}\" = \"true\" ]; then\n read_secret \"${prompt_text}: \"\n new_value=\"${_RS_RESULT}\"\n else\n read -e -p \"${prompt_text}: \" new_value\n if [ \"${new_value}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n fi\n if [ -n \"${new_value}\" ]; then\n eval \"export ${var_name}='${new_value}'\"\n fi\n return\n fi\n log \"$(msg prompt.preset \"${prompt_text}\")\"\n return\n fi\n\n # Non-interactive or quickstart: skip, leave unset\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] || [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ]; then\n eval \"export ${var_name}=''\"\n return\n fi\n\n local value=\"\"\n if [ \"${is_secret}\" = \"true\" ]; then\n read_secret \"${prompt_text}: \"\n value=\"${_RS_RESULT}\"\n else\n read -e -p \"${prompt_text}: \" value\n if [ \"${value}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n fi\n\n eval \"export ${var_name}='${value}'\"\n}\n\ngenerate_key() {\n openssl rand -hex 32\n}\n\n# ============================================================\n# Detect container runtime socket on the host\n#\n# Dependency: Requires DOCKER_CMD to reflect the TRUE underlying runtime.\n# (Crucial for unmasking 'podman' disguised as 'docker'.\n# Must be strictly resolved prior, e.g., via check_container_runtime)\n# ============================================================\ndetect_socket() {\n local socket_path\n\n # 1. Respect explicitly defined DOCKER_HOST environment variable\n if [ -n \"${DOCKER_HOST}\" ]; then\n socket_path=$(echo \"${DOCKER_HOST}\" | sed 's|^unix://||')\n if [ -S \"${socket_path}\" ]; then\n echo \"${socket_path}\"\n return 0\n fi\n fi\n\n # 2. Route based on the resolved container runtime\n case \"${DOCKER_CMD}\" in\n docker)\n # Docker path handling\n socket_path=$(docker context ls --format '{{if .Current}}{{.DockerEndpoint}}{{end}}' 2>/dev/null | grep . | sed 's|^unix://||')\n if [ -n \"${socket_path}\" ] && [ -S \"${socket_path}\" ]; then\n echo \"${socket_path}\"\n return 0\n fi\n\n # Rootless Docker\n if [ -n \"${XDG_RUNTIME_DIR:-}\" ] && [ -S \"${XDG_RUNTIME_DIR}/docker.sock\" ]; then\n echo \"${XDG_RUNTIME_DIR}/docker.sock\"\n return 0\n fi\n\n # Rootless Docker (macOS fallback)\n if [ -S \"${HOME}/.docker/run/docker.sock\" ]; then\n echo \"${HOME}/.docker/run/docker.sock\"\n return 0\n fi\n\n # Root Docker\n if [ -S \"/var/run/docker.sock\" ]; then\n echo \"/var/run/docker.sock\"\n return 0\n fi\n ;;\n podman)\n # Podman path handling\n if [ \"$(id -u)\" -eq 0 ]; then\n socket_path=\"/run/podman/podman.sock\"\n else\n socket_path=\"${XDG_RUNTIME_DIR}/podman/podman.sock\"\n fi\n if [ -S \"${socket_path}\" ]; then\n echo \"${socket_path}\"\n return 0\n fi\n ;;\n esac\n\n # Return empty if no socket is found\n echo \"\"\n return 0\n}\n\n# Ensure Podman API socket is active (State mutation)\nensure_podman_socket() {\n local socket_path\n\n # Only applicable if runtime is podman and systemctl is available\n if [ \"${DOCKER_CMD:-}\" != \"podman\" ] || ! command -v systemctl >/dev/null 2>&1; then\n return 0\n fi\n\n echo \"Ensuring Podman API socket is active...\" >&2\n\n # Enable and start the socket based on user privileges\n if [ \"$(id -u)\" -eq 0 ]; then\n socket_path=\"/run/podman/podman.sock\"\n systemctl enable --now podman.socket >/dev/null 2>&1 || true\n else\n socket_path=\"${XDG_RUNTIME_DIR}/podman/podman.sock\"\n systemctl --user enable --now podman.socket >/dev/null 2>&1 || true\n fi\n\n # Give systemd a brief moment to assert the socket for root or rootless user\n local _i\n for _i in 1 2 3; do\n [ -S \"${socket_path}\" ] && break\n sleep 1\n done\n}\n\n# Detect local LAN IP address (cross-platform: macOS and Linux)\ndetect_lan_ip() {\n local ip=\"\"\n\n # macOS: try common Wi-Fi / Ethernet interfaces\n if command -v ipconfig >/dev/null 2>&1; then\n for iface in en0 en1 en2 en3 en4; do\n ip=$(ipconfig getifaddr \"${iface}\" 2>/dev/null)\n if [ -n \"${ip}\" ]; then\n echo \"${ip}\"\n return 0\n fi\n done\n fi\n\n # Linux: ip route — most reliable\n if command -v ip >/dev/null 2>&1; then\n ip=$(ip route get 1.1.1.1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i==\"src\"){print $(i+1); exit}}')\n if [ -n \"${ip}\" ]; then\n echo \"${ip}\"\n return 0\n fi\n fi\n\n # Linux fallback: hostname -I (space-separated list, take first non-loopback)\n if command -v hostname >/dev/null 2>&1; then\n ip=$(hostname -I 2>/dev/null | tr ' ' '\\n' | grep -v '^127\\.' | grep -v '^::' | head -1)\n if [ -n \"${ip}\" ]; then\n echo \"${ip}\"\n return 0\n fi\n fi\n\n # Last resort: ifconfig\n if command -v ifconfig >/dev/null 2>&1; then\n ip=$(ifconfig 2>/dev/null | awk '/inet /{if($2!~/^127\\./){print $2; exit}}')\n # Strip \"addr:\" prefix that some ifconfig versions add\n ip=\"${ip#addr:}\"\n if [ -n \"${ip}\" ]; then\n echo \"${ip}\"\n return 0\n fi\n fi\n\n echo \"\"\n}\n\n\n# ============================================================\n# Step-back navigation helpers\n# ============================================================\n\n# should_skip_step: returns 0 (skip) when the step is irrelevant in current mode\nshould_skip_step() {\n local step_fn=\"$1\"\n case \"${step_fn}\" in\n step_lang|step_mode)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n ;;\n step_version)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n ;;\n step_existing)\n local _env=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n [ ! -f \"${_env}\" ] && return 0\n ;;\n # Keep-All upgrade mode: skip all config steps (step_volume/step_workspace handled separately)\n step_llm|step_admin|step_network|step_ports|step_domains|step_github|step_skills|step_runtime|step_manager_runtime|step_e2ee|step_docker_proxy|step_idle|step_hostshare)\n [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ] && return 0\n ;;\n step_volume|step_workspace)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ] && return 0\n [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ] && return 0\n ;;\n step_e2ee|step_idle)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE}\" != \"1\" ] && return 0\n ;;\n step_docker_proxy)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE}\" != \"1\" ] && return 0\n # Embedded mode handles docker access natively — skip this step\n [ \"${AGENTTEAMS_USE_EMBEDDED:-}\" = \"1\" ] && return 0\n ;;\n step_manager_runtime)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n ;;\n step_hostshare)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ] && return 0\n ;;\n step_podman_autostart)\n # Only relevant for Podman with systemd\n [ \"${DOCKER_CMD}\" != \"podman\" ] && return 0\n ! command -v systemctl >/dev/null 2>&1 && return 0\n\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE}\" != \"1\" ] && return 0\n [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ] && return 0\n ;;\n esac\n return 1\n}\n\n# clear_step_vars: unset variables set by a step so it will re-prompt on re-entry\nclear_step_vars() {\n local step_fn=\"$1\"\n case \"${step_fn}\" in\n step_mode) unset AGENTTEAMS_QUICKSTART ;;\n step_version) unset AGENTTEAMS_VERSION ;;\n step_existing) unset AGENTTEAMS_UPGRADE UPGRADE_EXISTING_WORKERS ;;\n step_llm)\n unset AGENTTEAMS_LLM_PROVIDER AGENTTEAMS_DEFAULT_MODEL AGENTTEAMS_OPENAI_BASE_URL\n unset AGENTTEAMS_LLM_API_KEY AGENTTEAMS_MODEL_CONTEXT_WINDOW AGENTTEAMS_MODEL_MAX_TOKENS\n unset AGENTTEAMS_MODEL_REASONING AGENTTEAMS_MODEL_VISION\n ;;\n step_admin) unset AGENTTEAMS_ADMIN_USER AGENTTEAMS_ADMIN_PASSWORD ;;\n step_network) unset AGENTTEAMS_LOCAL_ONLY ;;\n step_ports)\n unset AGENTTEAMS_PORT_GATEWAY AGENTTEAMS_PORT_CONSOLE\n unset AGENTTEAMS_PORT_ELEMENT_WEB AGENTTEAMS_PORT_MANAGER_CONSOLE\n ;;\n step_domains)\n unset AGENTTEAMS_MATRIX_DOMAIN AGENTTEAMS_MATRIX_CLIENT_DOMAIN\n unset AGENTTEAMS_AI_GATEWAY_DOMAIN AGENTTEAMS_FS_DOMAIN AGENTTEAMS_CONSOLE_DOMAIN\n ;;\n step_github) unset AGENTTEAMS_GITHUB_TOKEN ;;\n step_skills) unset AGENTTEAMS_SKILLS_API_URL ;;\n step_volume) unset AGENTTEAMS_DATA_DIR ;;\n step_workspace) unset AGENTTEAMS_WORKSPACE_DIR ;;\n step_runtime) unset AGENTTEAMS_DEFAULT_WORKER_RUNTIME ;;\n step_manager_runtime) unset AGENTTEAMS_MANAGER_RUNTIME ;;\n step_e2ee) unset AGENTTEAMS_MATRIX_E2EE ;;\n step_docker_proxy) unset AGENTTEAMS_DOCKER_PROXY; unset AGENTTEAMS_PROXY_ALLOWED_REGISTRIES ;;\n step_idle) unset AGENTTEAMS_WORKER_IDLE_TIMEOUT ;;\n step_hostshare) unset AGENTTEAMS_HOST_SHARE_DIR ;;\n step_podman_autostart) unset AGENTTEAMS_PODMAN_AUTOSTART ;;\n esac\n}\n\n# ============================================================\n# Individual step functions\n# ============================================================\n\nstep_lang() {\n local lang_default_choice=\"2\"\n [ \"${AGENTTEAMS_LANGUAGE}\" = \"zh\" ] && lang_default_choice=\"1\"\n log \"$(msg lang.detected)\"\n log \"$(msg lang.switch_title)\"\n echo \"$(msg lang.option_zh)\"\n echo \"$(msg lang.option_en)\"\n echo \"\"\n local LANG_CHOICE\n read -e -p \"$(msg lang.prompt) [${lang_default_choice}]: \" LANG_CHOICE\n LANG_CHOICE=\"${LANG_CHOICE:-${lang_default_choice}}\"\n if [ \"${LANG_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n case \"${LANG_CHOICE}\" in\n 1) AGENTTEAMS_LANGUAGE=\"zh\" ;;\n 2) AGENTTEAMS_LANGUAGE=\"en\" ;;\n esac\n export AGENTTEAMS_LANGUAGE\n log \"\"\n}\n\nstep_mode() {\n log \"$(msg install.mode.title)\"\n echo \"\"\n echo \"$(msg install.mode.choose)\"\n echo \"$(msg install.mode.quickstart)\"\n echo \"$(msg install.mode.manual)\"\n echo \"\"\n local ONBOARDING_CHOICE\n read -e -p \"$(msg install.mode.prompt): \" ONBOARDING_CHOICE\n ONBOARDING_CHOICE=\"${ONBOARDING_CHOICE:-1}\"\n if [ \"${ONBOARDING_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n case \"${ONBOARDING_CHOICE}\" in\n 1|quick|quickstart)\n log \"$(msg install.mode.quickstart_selected)\"\n AGENTTEAMS_QUICKSTART=1\n ;;\n 2|manual)\n log \"$(msg install.mode.manual_selected)\"\n ;;\n *)\n log \"$(msg install.mode.invalid)\"\n AGENTTEAMS_QUICKSTART=1\n ;;\n esac\n log \"\"\n}\n\nstep_version() {\n # Skip if version already provided via env var\n if [ -n \"${AGENTTEAMS_VERSION}\" ]; then\n resolve_image_tags\n return 0\n fi\n # Try to fetch the latest stable release from GitHub\n log \"$(msg install.version.fetching)\"\n local _fetched\n _fetched=$(curl -sf --max-time 5 \\\n -H \"Accept: application/vnd.github+json\" \\\n \"https://api.github.com/repos/agentscope-ai/AgentTeams/releases/latest\" \\\n 2>/dev/null | grep '\"tag_name\"' | head -1 | sed 's/.*\"tag_name\": *\"\\([^\"]*\\)\".*/\\1/')\n if [ -n \"${_fetched}\" ]; then\n AGENTTEAMS_KNOWN_STABLE_VERSION=\"${_fetched}\"\n else\n log \"$(msg install.version.fetch_failed \"${AGENTTEAMS_KNOWN_STABLE_VERSION}\")\"\n fi\n log \"$(msg install.version.title)\"\n echo \"\"\n echo \"$(msg install.version.choose)\"\n echo \"$(msg install.version.option_latest)\"\n printf \"%s\\n\" \"$(msg install.version.option_stable \"${AGENTTEAMS_KNOWN_STABLE_VERSION}\")\"\n echo \"$(msg install.version.option_custom)\"\n echo \"\"\n local VERSION_CHOICE\n read -e -p \"$(msg install.version.prompt) [1]: \" VERSION_CHOICE\n VERSION_CHOICE=\"${VERSION_CHOICE:-1}\"\n if [ \"${VERSION_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n case \"${VERSION_CHOICE}\" in\n 1|latest)\n AGENTTEAMS_VERSION=\"latest\"\n log \"$(msg install.version.selected_latest)\"\n ;;\n 2|stable)\n AGENTTEAMS_VERSION=\"${AGENTTEAMS_KNOWN_STABLE_VERSION}\"\n log \"$(msg install.version.selected_stable \"${AGENTTEAMS_VERSION}\")\"\n ;;\n 3|custom)\n local CUSTOM_VERSION\n read -e -p \"$(msg install.version.custom_prompt): \" CUSTOM_VERSION\n AGENTTEAMS_VERSION=\"${CUSTOM_VERSION:-${AGENTTEAMS_KNOWN_STABLE_VERSION}}\"\n log \"$(msg install.version.selected_custom \"${AGENTTEAMS_VERSION}\")\"\n ;;\n *)\n AGENTTEAMS_VERSION=\"${AGENTTEAMS_KNOWN_STABLE_VERSION}\"\n log \"$(msg install.version.invalid \"${AGENTTEAMS_VERSION}\")\"\n ;;\n esac\n log \"\"\n resolve_image_tags\n}\n\nstep_existing() {\n local existing_env=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n log \"$(msg install.existing.detected \"${existing_env}\")\"\n local running_manager=\"\" running_workers=\"\" existing_workers=\"\"\n if ${DOCKER_CMD} ps --format '{{.Names}}' | grep -q \"^agentteams-manager$\"; then\n running_manager=\"agentteams-manager\"\n fi\n running_workers=$(${DOCKER_CMD} ps --format '{{.Names}}' | grep \"^agentteams-worker-\" || true)\n existing_workers=$(${DOCKER_CMD} ps -a --format '{{.Names}}' | grep \"^agentteams-worker-\" || true)\n local UPGRADE_CHOICE\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n log \"$(msg install.existing.upgrade_noninteractive)\"\n UPGRADE_CHOICE=\"upgrade\"\n else\n echo \"\"\n echo \"$(msg install.existing.choose)\"\n echo \"$(msg install.existing.upgrade)\"\n echo \"$(msg install.existing.reinstall)\"\n echo \"$(msg install.existing.cancel)\"\n echo \"\"\n read -e -p \"$(msg install.existing.prompt): \" UPGRADE_CHOICE\n UPGRADE_CHOICE=\"${UPGRADE_CHOICE:-1}\"\n if [ \"${UPGRADE_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n fi\n case \"${UPGRADE_CHOICE}\" in\n 1|upgrade)\n AGENTTEAMS_UPGRADE=1\n log \"$(msg install.existing.upgrading)\"\n # Show upgrade mode sub-menu (unless already set via env var)\n if [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL:-0}\" != \"1\" ]; then\n echo \"\"\n echo \"$(msg upgrade.mode.prompt)\"\n echo \"$(msg upgrade.mode.keep_all)\"\n echo \"$(msg upgrade.mode.confirm_each)\"\n echo \"$(msg upgrade.mode.back)\"\n echo \"\"\n local UPGRADE_MODE_CHOICE\n read -e -p \"$(msg install.existing.prompt): \" UPGRADE_MODE_CHOICE\n UPGRADE_MODE_CHOICE=\"${UPGRADE_MODE_CHOICE:-1}\"\n case \"${UPGRADE_MODE_CHOICE}\" in\n 1|keep)\n AGENTTEAMS_UPGRADE_KEEP_ALL=1\n ;;\n 2|confirm)\n AGENTTEAMS_UPGRADE_KEEP_ALL=0\n ;;\n 3|b)\n STEP_RESULT=\"back\"\n return 0\n ;;\n *)\n AGENTTEAMS_UPGRADE_KEEP_ALL=0\n ;;\n esac\n fi\n # Load current parameters for both Keep-All and confirm-each modes\n load_current_params_from_env\n if [ -n \"${running_manager}\" ] || [ -n \"${running_workers}\" ]; then\n echo \"\"\n echo -e \"\\033[33m$(msg install.existing.warn_manager_stop)\\033[0m\"\n if [ -n \"${existing_workers}\" ]; then\n echo -e \"\\033[33m$(msg install.existing.warn_worker_recreate)\\033[0m\"\n fi\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" != \"1\" ]; then\n echo \"\"\n local CONFIRM_STOP\n read -e -p \"$(msg install.existing.continue_prompt): \" CONFIRM_STOP\n if [ \"${CONFIRM_STOP}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n if [ \"${CONFIRM_STOP}\" != \"y\" ] && [ \"${CONFIRM_STOP}\" != \"Y\" ]; then\n log \"$(msg install.existing.cancelled)\"\n exit 0\n fi\n fi\n fi\n UPGRADE_EXISTING_WORKERS=\"${existing_workers}\"\n ;;\n 2|reinstall)\n log \"$(msg install.reinstall.performing)\"\n local existing_workspace=\"\"\n if [ -f \"${existing_env}\" ]; then\n existing_workspace=$(grep '^AGENTTEAMS_WORKSPACE_DIR=' \"${existing_env}\" 2>/dev/null | cut -d= -f2-)\n fi\n [ -z \"${existing_workspace}\" ] && existing_workspace=\"${HOME}/agentteams-manager\"\n echo \"\"\n echo -e \"\\033[33m$(msg install.reinstall.warn_stop)\\033[0m\"\n [ -n \"${running_manager}\" ] && echo -e \"\\033[33m - ${running_manager} (manager)\\033[0m\"\n for w in ${running_workers}; do\n echo -e \"\\033[33m - ${w} (worker)\\033[0m\"\n done\n echo \"\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_delete)\\033[0m\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_volume)\\033[0m\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_env \"${existing_env}\")\\033[0m\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_workspace \"${existing_workspace}\")\\033[0m\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_workers)\\033[0m\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_proxy)\\033[0m\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_network)\\033[0m\"\n echo \"\"\n echo -e \"\\033[31m$(msg install.reinstall.confirm_type)\\033[0m\"\n echo -e \"\\033[31m ${existing_workspace}\\033[0m\"\n echo \"\"\n local CONFIRM_PATH\n read -e -p \"$(msg install.reinstall.confirm_path): \" CONFIRM_PATH\n if [ \"${CONFIRM_PATH}\" != \"${existing_workspace}\" ]; then\n die \"$(msg install.reinstall.path_mismatch \"${CONFIRM_PATH}\" \"${existing_workspace}\")\"\n fi\n log \"$(msg install.reinstall.confirmed)\"\n ${DOCKER_CMD} stop agentteams-manager 2>/dev/null || true\n ${DOCKER_CMD} rm agentteams-manager 2>/dev/null || true\n for w in $(${DOCKER_CMD} ps -a --format '{{.Names}}' | grep \"^agentteams-worker-\" || true); do\n ${DOCKER_CMD} stop \"${w}\" 2>/dev/null || true\n ${DOCKER_CMD} rm \"${w}\" 2>/dev/null || true\n log \"$(msg install.reinstall.removed_worker \"${w}\")\"\n done\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' | grep -q \"^agentteams-controller$\"; then\n log \"$(msg install.reinstall.removing_proxy)\"\n ${DOCKER_CMD} stop agentteams-controller 2>/dev/null || true\n ${DOCKER_CMD} rm agentteams-controller 2>/dev/null || true\n fi\n if ${DOCKER_CMD} network ls --format '{{.Name}}' | grep -q \"^agentteams-net$\"; then\n log \"$(msg install.reinstall.removing_network)\"\n ${DOCKER_CMD} network rm agentteams-net 2>/dev/null || true\n fi\n if ${DOCKER_CMD} volume ls -q | grep -q \"^agentteams-data$\"; then\n log \"$(msg install.reinstall.removing_volume)\"\n ${DOCKER_CMD} volume rm agentteams-data 2>/dev/null || log \"$(msg install.reinstall.warn_volume_fail)\"\n fi\n if [ -d \"${existing_workspace}\" ]; then\n log \"$(msg install.reinstall.removing_workspace \"${existing_workspace}\")\"\n rm -rf \"${existing_workspace}\" || die \"$(msg install.reinstall.failed_rm_workspace)\"\n fi\n if [ -f \"${existing_env}\" ]; then\n log \"$(msg install.reinstall.removing_env \"${existing_env}\")\"\n rm -f \"${existing_env}\"\n fi\n log \"$(msg install.reinstall.cleanup_done)\"\n unset AGENTTEAMS_WORKSPACE_DIR\n return 0\n ;;\n 3|cancel|*)\n log \"$(msg install.existing.cancelled)\"\n exit 0\n ;;\n esac\n # Load existing env file as fallback (shell env vars take priority)\n if [ -f \"${existing_env}\" ]; then\n log \"$(msg install.loading_config \"${existing_env}\")\"\n while IFS='=' read -r key value; do\n case \"${key}\" in \\#*|\"\") continue ;; esac\n value=\"${value%%#*}\"\n value=\"${value#\"${value%%[![:space:]]*}\"}\"\n value=\"${value%\"${value##*[![:space:]]}\"}\"\n eval \"_existing_val=\\\"\\${${key}+x}\\\"\"\n if [ -z \"${_existing_val}\" ]; then export \"${key}=${value}\"; fi\n done < \"${existing_env}\"\n fi\n}\n\nstep_llm() {\n log \"$(msg llm.title)\"\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n if [ \"${AGENTTEAMS_LANGUAGE}\" = \"zh\" ]; then\n AGENTTEAMS_LLM_PROVIDER=\"${AGENTTEAMS_LLM_PROVIDER:-openai-compat}\"\n AGENTTEAMS_DEFAULT_MODEL=\"${AGENTTEAMS_DEFAULT_MODEL:-qwen3.6-plus}\"\n AGENTTEAMS_OPENAI_BASE_URL=\"${AGENTTEAMS_OPENAI_BASE_URL:-https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1}\"\n log \"$(msg llm.provider.label \"${AGENTTEAMS_LLM_PROVIDER}\")\"\n log \"$(msg llm.openai.base_url_label \"${AGENTTEAMS_OPENAI_BASE_URL}\")\"\n else\n AGENTTEAMS_LLM_PROVIDER=\"${AGENTTEAMS_LLM_PROVIDER:-qwen}\"\n AGENTTEAMS_DEFAULT_MODEL=\"${AGENTTEAMS_DEFAULT_MODEL:-qwen3.6-plus}\"\n AGENTTEAMS_OPENAI_BASE_URL=\"${AGENTTEAMS_OPENAI_BASE_URL:-}\"\n log \"$(msg llm.provider.qwen_default \"${AGENTTEAMS_LLM_PROVIDER}\")\"\n fi\n log \"$(msg llm.model.default \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n prompt AGENTTEAMS_LLM_API_KEY \"$(msg llm.apikey_prompt)\" \"\" \"true\"\n AGENTTEAMS_EMBEDDING_MODEL=\"${AGENTTEAMS_EMBEDDING_MODEL-text-embedding-v4}\"\n export AGENTTEAMS_LLM_PROVIDER AGENTTEAMS_DEFAULT_MODEL AGENTTEAMS_EMBEDDING_MODEL\n [ -n \"${AGENTTEAMS_OPENAI_BASE_URL+x}\" ] && export AGENTTEAMS_OPENAI_BASE_URL\n return 0\n fi\n\n # Upgrade Keep-All mode: skip provider selection, use loaded values\n local SKIP_TO_EMBEDDING=0\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ] && [ -n \"${AGENTTEAMS_LLM_PROVIDER}\" ]; then\n log \"$(msg llm.provider.label \"${AGENTTEAMS_LLM_PROVIDER}\")\"\n log \"$(msg llm.openai.base_url_label \"${AGENTTEAMS_OPENAI_BASE_URL}\")\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n PROVIDER_CHOICE=\"${AGENTTEAMS_LLM_PROVIDER}\"\n SKIP_TO_EMBEDDING=1\n else\n echo \"\"\n echo \"$(msg llm.providers_title)\"\n echo \"$(msg llm.provider.alibaba)\"\n echo \"$(msg llm.provider.openai_compat)\"\n echo \"\"\n local PROVIDER_CHOICE\n # If upgrade mode with loaded provider, show current as default\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_LLM_PROVIDER}\" ]; then\n local _prov_display\n case \"${AGENTTEAMS_LLM_PROVIDER}\" in\n openai-compat) _prov_display=\"2\" ;;\n *) _prov_display=\"1\" ;;\n esac\n read -e -p \"$(msg llm.provider.select) [${_prov_display}]: \" PROVIDER_CHOICE\n PROVIDER_CHOICE=\"${PROVIDER_CHOICE:-${_prov_display}}\"\n elif [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ]; then\n read -e -p \"$(msg llm.provider.select) [1]: \" PROVIDER_CHOICE\n PROVIDER_CHOICE=\"${PROVIDER_CHOICE:-1}\"\n else\n read -e -p \"$(msg llm.provider.select): \" PROVIDER_CHOICE\n PROVIDER_CHOICE=\"${PROVIDER_CHOICE:-1}\"\n fi\n if [ \"${PROVIDER_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n fi\n local ALIBABA_MODEL_CHOICE=\"\"\n local ALIBABA_ACCESS=\"\"\n case \"${PROVIDER_CHOICE}\" in\n 1|alibaba-cloud|openai-compat)\n if [ \"${AGENTTEAMS_LANGUAGE}\" = \"en\" ]; then\n AGENTTEAMS_LLM_PROVIDER=\"openai-compat\"\n AGENTTEAMS_OPENAI_BASE_URL=\"https://dashscope-intl.aliyuncs.com/compatible-mode/v1\"\n ALIBABA_ACCESS=\"tokenplan\"\n echo \"\"\n echo \"$(msg llm.codingplan.models_title)\"\n echo \"$(msg llm.codingplan.model.qwen36plus)\"\n echo \"$(msg llm.codingplan.model.glm5)\"\n echo \"$(msg llm.codingplan.model.kimi)\"\n echo \"$(msg llm.codingplan.model.minimax)\"\n echo \"\"\n local CODINGPLAN_MODEL_CHOICE\n # If upgrade with loaded model, show current as default\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_DEFAULT_MODEL}\" ]; then\n local _model_default\n case \"${AGENTTEAMS_DEFAULT_MODEL}\" in\n qwen3.6-plus) _model_default=\"1\" ;;\n glm-5) _model_default=\"2\" ;;\n kimi-k2.5) _model_default=\"3\" ;;\n MiniMax-M2.5) _model_default=\"4\" ;;\n *) _model_default=\"1\" ;;\n esac\n read -e -p \"$(msg llm.codingplan.model.select) [${_model_default}]: \" CODINGPLAN_MODEL_CHOICE\n CODINGPLAN_MODEL_CHOICE=\"${CODINGPLAN_MODEL_CHOICE:-${_model_default}}\"\n elif [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ]; then\n read -e -p \"$(msg llm.codingplan.model.select) [1]: \" CODINGPLAN_MODEL_CHOICE\n CODINGPLAN_MODEL_CHOICE=\"${CODINGPLAN_MODEL_CHOICE:-1}\"\n else\n read -e -p \"$(msg llm.codingplan.model.select): \" CODINGPLAN_MODEL_CHOICE\n CODINGPLAN_MODEL_CHOICE=\"${CODINGPLAN_MODEL_CHOICE:-1}\"\n fi\n if [ \"${CODINGPLAN_MODEL_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n case \"${CODINGPLAN_MODEL_CHOICE}\" in\n 1|qwen3.6-plus) AGENTTEAMS_DEFAULT_MODEL=\"qwen3.6-plus\" ;;\n 2|glm-5) AGENTTEAMS_DEFAULT_MODEL=\"glm-5\" ;;\n 3|kimi-k2.5) AGENTTEAMS_DEFAULT_MODEL=\"kimi-k2.5\" ;;\n 4|MiniMax-M2.5) AGENTTEAMS_DEFAULT_MODEL=\"MiniMax-M2.5\" ;;\n *) AGENTTEAMS_DEFAULT_MODEL=\"qwen3.6-plus\" ;;\n esac\n log \"$(msg llm.provider.selected_codingplan)\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n else\n echo \"\"\n echo \"$(msg llm.alibaba.models_title)\"\n echo \"$(msg llm.alibaba.model.tokenplan)\"\n echo \"$(msg llm.alibaba.model.bailian)\"\n echo \"$(msg llm.alibaba.model.codingplan_legacy)\"\n echo \"\"\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_OPENAI_BASE_URL}\" ]; then\n # Determine alibaba access type from loaded URL\n local _access_default=\"1\"\n if echo \"${AGENTTEAMS_OPENAI_BASE_URL}\" | grep -q \"token-plan\"; then\n _access_default=\"1\"\n elif echo \"${AGENTTEAMS_OPENAI_BASE_URL}\" | grep -q \"dashscope\"; then\n if echo \"${AGENTTEAMS_OPENAI_BASE_URL}\" | grep -q \"intl\"; then\n _access_default=\"3\"\n else\n _access_default=\"2\"\n fi\n fi\n read -e -p \"$(msg llm.alibaba.model.select) [${_access_default}]: \" ALIBABA_MODEL_CHOICE\n ALIBABA_MODEL_CHOICE=\"${ALIBABA_MODEL_CHOICE:-${_access_default}}\"\n elif [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ]; then\n read -e -p \"$(msg llm.alibaba.model.select) [1]: \" ALIBABA_MODEL_CHOICE\n ALIBABA_MODEL_CHOICE=\"${ALIBABA_MODEL_CHOICE:-1}\"\n else\n read -e -p \"$(msg llm.alibaba.model.select): \" ALIBABA_MODEL_CHOICE\n ALIBABA_MODEL_CHOICE=\"${ALIBABA_MODEL_CHOICE:-1}\"\n fi\n if [ \"${ALIBABA_MODEL_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n case \"${ALIBABA_MODEL_CHOICE}\" in\n 1|token-plan|tokenplan)\n ALIBABA_ACCESS=\"tokenplan\"\n AGENTTEAMS_LLM_PROVIDER=\"openai-compat\"\n AGENTTEAMS_OPENAI_BASE_URL=\"https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1\"\n echo \"\"\n echo \"$(msg llm.codingplan.models_title)\"\n echo \"$(msg llm.codingplan.model.qwen36plus)\"\n echo \"$(msg llm.codingplan.model.glm5)\"\n echo \"$(msg llm.codingplan.model.kimi)\"\n echo \"$(msg llm.codingplan.model.minimax)\"\n echo \"\"\n local CODINGPLAN_MODEL_CHOICE\n # If upgrade with loaded model, show current as default\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_DEFAULT_MODEL}\" ]; then\n local _model_default\n case \"${AGENTTEAMS_DEFAULT_MODEL}\" in\n qwen3.6-plus) _model_default=\"1\" ;;\n glm-5) _model_default=\"2\" ;;\n kimi-k2.5) _model_default=\"3\" ;;\n MiniMax-M2.5) _model_default=\"4\" ;;\n *) _model_default=\"1\" ;;\n esac\n read -e -p \"$(msg llm.codingplan.model.select) [${_model_default}]: \" CODINGPLAN_MODEL_CHOICE\n CODINGPLAN_MODEL_CHOICE=\"${CODINGPLAN_MODEL_CHOICE:-${_model_default}}\"\n elif [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ]; then\n read -e -p \"$(msg llm.codingplan.model.select) [1]: \" CODINGPLAN_MODEL_CHOICE\n CODINGPLAN_MODEL_CHOICE=\"${CODINGPLAN_MODEL_CHOICE:-1}\"\n else\n read -e -p \"$(msg llm.codingplan.model.select): \" CODINGPLAN_MODEL_CHOICE\n CODINGPLAN_MODEL_CHOICE=\"${CODINGPLAN_MODEL_CHOICE:-1}\"\n fi\n if [ \"${CODINGPLAN_MODEL_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n case \"${CODINGPLAN_MODEL_CHOICE}\" in\n 1|qwen3.6-plus) AGENTTEAMS_DEFAULT_MODEL=\"qwen3.6-plus\" ;;\n 2|glm-5) AGENTTEAMS_DEFAULT_MODEL=\"glm-5\" ;;\n 3|kimi-k2.5) AGENTTEAMS_DEFAULT_MODEL=\"kimi-k2.5\" ;;\n 4|MiniMax-M2.5) AGENTTEAMS_DEFAULT_MODEL=\"MiniMax-M2.5\" ;;\n *) AGENTTEAMS_DEFAULT_MODEL=\"qwen3.6-plus\" ;;\n esac\n log \"$(msg llm.provider.selected_tokenplan)\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n ;;\n 2|qwen|bailian)\n ALIBABA_ACCESS=\"bailian\"\n AGENTTEAMS_LLM_PROVIDER=\"qwen\"\n AGENTTEAMS_OPENAI_BASE_URL=\"\"\n echo \"\"\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_DEFAULT_MODEL}\" ]; then\n read -e -p \"$(msg llm.qwen.model_prompt) [${AGENTTEAMS_DEFAULT_MODEL}]: \" AGENTTEAMS_DEFAULT_MODEL\n else\n read -e -p \"$(msg llm.qwen.model_prompt) [qwen3.6-plus]: \" AGENTTEAMS_DEFAULT_MODEL\n fi\n if [ \"${AGENTTEAMS_DEFAULT_MODEL}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_DEFAULT_MODEL=\"${AGENTTEAMS_DEFAULT_MODEL:-qwen3.6-plus}\"\n log \"$(msg llm.provider.selected_qwen)\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n prompt_custom_model_params \"${AGENTTEAMS_DEFAULT_MODEL}\" || return 0\n ;;\n 3|coding-plan|codingplan)\n ALIBABA_ACCESS=\"codingplan_legacy\"\n AGENTTEAMS_LLM_PROVIDER=\"openai-compat\"\n AGENTTEAMS_OPENAI_BASE_URL=\"https://coding.dashscope.aliyuncs.com/v1\"\n echo \"\"\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_DEFAULT_MODEL}\" ]; then\n read -e -p \"$(msg llm.qwen.model_prompt) [${AGENTTEAMS_DEFAULT_MODEL}]: \" AGENTTEAMS_DEFAULT_MODEL\n else\n read -e -p \"$(msg llm.qwen.model_prompt): \" AGENTTEAMS_DEFAULT_MODEL\n fi\n if [ \"${AGENTTEAMS_DEFAULT_MODEL}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_DEFAULT_MODEL=\"${AGENTTEAMS_DEFAULT_MODEL:-qwen3.6-plus}\"\n log \"$(msg llm.provider.selected_codingplan_legacy)\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n prompt_custom_model_params \"${AGENTTEAMS_DEFAULT_MODEL}\" || return 0\n ;;\n *)\n die \"$(msg llm.alibaba.model.invalid \"${ALIBABA_MODEL_CHOICE}\")\"\n ;;\n esac\n fi\n if [ \"${ALIBABA_ACCESS}\" = \"bailian\" ]; then\n log \"$(msg llm.apikey_hint_bailian)\"\n log \"$(msg llm.apikey_url_bailian)\"\n elif [ \"${AGENTTEAMS_LANGUAGE}\" = \"en\" ]; then\n log \"$(msg llm.apikey_hint_qwencloud)\"\n log \"$(msg llm.apikey_url_qwencloud)\"\n elif [ \"${ALIBABA_ACCESS}\" = \"codingplan_legacy\" ]; then\n log \"$(msg llm.apikey_hint_codingplan)\"\n log \"$(msg llm.apikey_url_codingplan)\"\n else\n log \"$(msg llm.apikey_hint_tokenplan)\"\n log \"$(msg llm.apikey_url_tokenplan)\"\n fi\n log \"\"\n prompt AGENTTEAMS_LLM_API_KEY \"$(msg llm.apikey_prompt)\" \"\" \"true\" || return 0\n if [ \"${ALIBABA_ACCESS}\" = \"bailian\" ]; then\n test_llm_connectivity \"https://dashscope.aliyuncs.com/compatible-mode/v1\" \"${AGENTTEAMS_LLM_API_KEY}\" \"${AGENTTEAMS_DEFAULT_MODEL}\" || return 0\n elif [ \"${ALIBABA_ACCESS}\" = \"codingplan_legacy\" ]; then\n test_llm_connectivity \"https://coding.dashscope.aliyuncs.com/v1\" \"${AGENTTEAMS_LLM_API_KEY}\" \"${AGENTTEAMS_DEFAULT_MODEL}\" \"$(msg llm.openai.test.fail.codingplan_legacy)\" || return 0\n elif [ \"${AGENTTEAMS_LANGUAGE}\" = \"en\" ]; then\n test_llm_connectivity \"${AGENTTEAMS_OPENAI_BASE_URL}\" \"${AGENTTEAMS_LLM_API_KEY}\" \"${AGENTTEAMS_DEFAULT_MODEL}\" \"$(msg llm.openai.test.fail.codingplan)\" || return 0\n else\n test_llm_connectivity \"${AGENTTEAMS_OPENAI_BASE_URL}\" \"${AGENTTEAMS_LLM_API_KEY}\" \"${AGENTTEAMS_DEFAULT_MODEL}\" \"$(msg llm.openai.test.fail.tokenplan)\" || return 0\n fi\n ;;\n 2|openai-compat)\n AGENTTEAMS_LLM_PROVIDER=\"openai-compat\"\n log \"$(msg llm.provider.selected_openai \"${AGENTTEAMS_LLM_PROVIDER}\")\"\n echo \"\"\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg llm.openai.base_url_prompt)\" \"${AGENTTEAMS_OPENAI_BASE_URL}\")\"\n else\n _current=\"${AGENTTEAMS_OPENAI_BASE_URL}\"\n if [ -n \"${_current}\" ]; then\n read -e -p \"$(msg llm.openai.base_url_prompt) [${_current}]: \" AGENTTEAMS_OPENAI_BASE_URL\n else\n read -e -p \"$(msg llm.openai.base_url_prompt) [https://api.openai.com/v1]: \" AGENTTEAMS_OPENAI_BASE_URL\n fi\n if [ \"${AGENTTEAMS_OPENAI_BASE_URL}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_OPENAI_BASE_URL=\"${AGENTTEAMS_OPENAI_BASE_URL:-${_current}}\"\n [ -z \"${AGENTTEAMS_OPENAI_BASE_URL}\" ] && AGENTTEAMS_OPENAI_BASE_URL=\"https://api.openai.com/v1\"\n fi\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg llm.openai.model_prompt)\" \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n else\n _current=\"${AGENTTEAMS_DEFAULT_MODEL}\"\n if [ -n \"${_current}\" ]; then\n read -e -p \"$(msg llm.openai.model_prompt) [${_current}]: \" AGENTTEAMS_DEFAULT_MODEL\n else\n read -e -p \"$(msg llm.openai.model_prompt) [gpt-5.4]: \" AGENTTEAMS_DEFAULT_MODEL\n fi\n if [ \"${AGENTTEAMS_DEFAULT_MODEL}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_DEFAULT_MODEL=\"${AGENTTEAMS_DEFAULT_MODEL:-${_current}}\"\n [ -z \"${AGENTTEAMS_DEFAULT_MODEL}\" ] && AGENTTEAMS_DEFAULT_MODEL=\"gpt-5.4\"\n fi\n log \"$(msg llm.openai.base_url_label \"${AGENTTEAMS_OPENAI_BASE_URL}\")\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n prompt_custom_model_params \"${AGENTTEAMS_DEFAULT_MODEL}\" || return 0\n log \"\"\n prompt AGENTTEAMS_LLM_API_KEY \"$(msg llm.apikey_prompt)\" \"\" \"true\" || return 0\n test_llm_connectivity \"${AGENTTEAMS_OPENAI_BASE_URL}\" \"${AGENTTEAMS_LLM_API_KEY}\" \"${AGENTTEAMS_DEFAULT_MODEL}\" || return 0\n ;;\n *)\n die \"$(msg llm.provider.invalid \"${PROVIDER_CHOICE}\")\"\n ;;\n esac\n\n # Skip to embedding if Keep-All mode handled LLM params\n if [ \"${SKIP_TO_EMBEDDING}\" = \"1\" ]; then\n if [ -n \"${AGENTTEAMS_EMBEDDING_MODEL}\" ]; then\n log \"$(msg llm.embedding.title)\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_EMBEDDING_MODEL}\")\"\n log \"\"\n fi\n return 0\n fi\n\n # --- Embedding model (optional, auto-tested) ---\n echo \"\"\n log \"$(msg llm.embedding.title)\"\n log \"$(msg llm.embedding.hint)\"\n echo \"\"\n echo \"$(msg llm.embedding.option.default)\"\n echo \"$(msg llm.embedding.option.custom)\"\n echo \"$(msg llm.embedding.option.disable)\"\n echo \"\"\n local EMB_CHOICE\n read -e -p \"$(msg llm.embedding.select) [1]: \" EMB_CHOICE\n EMB_CHOICE=\"${EMB_CHOICE:-1}\"\n if [ \"${EMB_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n\n case \"${EMB_CHOICE}\" in\n 1)\n AGENTTEAMS_EMBEDDING_MODEL=\"text-embedding-v4\"\n ;;\n 2)\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg llm.embedding.custom_prompt)\" \"${AGENTTEAMS_EMBEDDING_MODEL}\")\"\n else\n _current=\"${AGENTTEAMS_EMBEDDING_MODEL}\"\n if [ -n \"${_current}\" ]; then\n read -e -p \"$(msg llm.embedding.custom_prompt) [${_current}]: \" AGENTTEAMS_EMBEDDING_MODEL\n else\n read -e -p \"$(msg llm.embedding.custom_prompt): \" AGENTTEAMS_EMBEDDING_MODEL\n fi\n if [ \"${AGENTTEAMS_EMBEDDING_MODEL}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_EMBEDDING_MODEL=\"${AGENTTEAMS_EMBEDDING_MODEL:-${_current}}\"\n fi\n ;;\n 3)\n AGENTTEAMS_EMBEDDING_MODEL=\"\"\n log \"$(msg llm.embedding.disabled)\"\n ;;\n *)\n AGENTTEAMS_EMBEDDING_MODEL=\"text-embedding-v4\"\n ;;\n esac\n\n if [ -n \"${AGENTTEAMS_EMBEDDING_MODEL}\" ]; then\n # Qwen provider uses dashscope directly; others use OPENAI_BASE_URL\n local EMB_BASE_URL=\"${AGENTTEAMS_OPENAI_BASE_URL}\"\n if [ \"${AGENTTEAMS_LLM_PROVIDER}\" = \"qwen\" ]; then\n EMB_BASE_URL=\"https://dashscope.aliyuncs.com/compatible-mode/v1\"\n fi\n if ! test_embedding_connectivity \"${EMB_BASE_URL}\" \"${AGENTTEAMS_LLM_API_KEY}\" \"${AGENTTEAMS_EMBEDDING_MODEL}\"; then\n AGENTTEAMS_EMBEDDING_MODEL=\"\"\n log \"$(msg llm.embedding.auto_disabled)\"\n fi\n fi\n\n export AGENTTEAMS_LLM_PROVIDER AGENTTEAMS_DEFAULT_MODEL\n [ -n \"${AGENTTEAMS_OPENAI_BASE_URL+x}\" ] && export AGENTTEAMS_OPENAI_BASE_URL\n log \"\"\n}\n\nstep_admin() {\n log \"$(msg admin.title)\"\n prompt AGENTTEAMS_ADMIN_USER \"$(msg admin.username_prompt)\" \"admin\" || return 0\n AGENTTEAMS_ADMIN_USER=\"$(printf '%s' \"${AGENTTEAMS_ADMIN_USER}\" | tr '[:upper:]' '[:lower:]')\"\n\n # Pre-set via env var: validate; in non-interactive mode fail fast,\n # in interactive mode warn and fall through to the retry prompt.\n if [ -n \"${AGENTTEAMS_ADMIN_PASSWORD:-}\" ]; then\n log \" $(msg prompt.preset \"$(msg admin.password_prompt)\")\"\n if [ ${#AGENTTEAMS_ADMIN_PASSWORD} -ge 8 ]; then\n log \"\"\n return 0\n fi\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n die \"$(msg admin.password_too_short \"${#AGENTTEAMS_ADMIN_PASSWORD}\")\"\n fi\n error \"$(msg admin.password_too_short \"${#AGENTTEAMS_ADMIN_PASSWORD}\")\"\n unset AGENTTEAMS_ADMIN_PASSWORD\n fi\n\n while true; do\n unset AGENTTEAMS_ADMIN_PASSWORD\n prompt_optional AGENTTEAMS_ADMIN_PASSWORD \"$(msg admin.password_prompt)\" \"true\" || return 0\n if [ -z \"${AGENTTEAMS_ADMIN_PASSWORD}\" ]; then\n AGENTTEAMS_ADMIN_PASSWORD=\"admin$(openssl rand -hex 6)\"\n log \"$(msg admin.password_generated)\"\n break\n fi\n if [ ${#AGENTTEAMS_ADMIN_PASSWORD} -ge 8 ]; then\n break\n fi\n error \"$(msg admin.password_too_short \"${#AGENTTEAMS_ADMIN_PASSWORD}\")\"\n done\n log \"\"\n}\n\nstep_network() {\n log \"$(msg port.local_only.title)\"\n echo \"\"\n echo \" 1) $(msg port.local_only.hint_yes)\"\n echo \" 2) $(msg port.local_only.hint_no)\"\n echo \"\"\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_LOCAL_ONLY=\"${AGENTTEAMS_LOCAL_ONLY:-1}\"\n elif [ -z \"${AGENTTEAMS_LOCAL_ONLY+x}\" ]; then\n local _local_choice\n read -e -p \"$(msg port.local_only.choice): \" _local_choice\n if [ \"${_local_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _local_choice=\"${_local_choice:-1}\"\n case \"${_local_choice}\" in\n 2|n|N|no|NO) AGENTTEAMS_LOCAL_ONLY=\"0\" ;;\n *) AGENTTEAMS_LOCAL_ONLY=\"1\" ;;\n esac\n fi\n export AGENTTEAMS_LOCAL_ONLY\n if [ \"${AGENTTEAMS_LOCAL_ONLY}\" = \"1\" ]; then\n log \"$(msg port.local_only.selected_local)\"\n else\n log \"$(msg port.local_only.selected_external)\"\n echo \"\"\n echo -e \"\\033[33m$(msg port.local_only.https_hint)\\033[0m\"\n fi\n}\n\nstep_ports() {\n log \"$(msg port.title)\"\n prompt AGENTTEAMS_PORT_GATEWAY \"$(msg port.gateway_prompt)\" \"18080\" || return 0\n prompt AGENTTEAMS_PORT_CONSOLE \"$(msg port.console_prompt)\" \"18001\" || return 0\n prompt AGENTTEAMS_PORT_ELEMENT_WEB \"$(msg port.element_prompt)\" \"18088\" || return 0\n prompt AGENTTEAMS_PORT_MANAGER_CONSOLE \"$(msg port.manager_console_prompt)\" \"18888\" || return 0\n log \"\"\n}\n\nstep_domains() {\n log \"$(msg domain.title)\"\n log \"$(msg domain.hint)\"\n prompt AGENTTEAMS_MATRIX_DOMAIN \"$(msg domain.matrix_prompt)\" \"matrix-local.agentteams.io:${AGENTTEAMS_PORT_GATEWAY}\" || return 0\n prompt AGENTTEAMS_MATRIX_CLIENT_DOMAIN \"$(msg domain.element_prompt)\" \"matrix-client-local.agentteams.io\" || return 0\n prompt AGENTTEAMS_AI_GATEWAY_DOMAIN \"$(msg domain.gateway_prompt)\" \"aigw-local.agentteams.io\" || return 0\n prompt AGENTTEAMS_FS_DOMAIN \"$(msg domain.fs_prompt)\" \"fs-local.agentteams.io\" || return 0\n if [ \"${AGENTTEAMS_MANAGER_RUNTIME}\" != \"copaw\" ]; then\n prompt AGENTTEAMS_CONSOLE_DOMAIN \"$(msg domain.console_prompt)\" \"console-local.agentteams.io\" || return 0\n fi\n log \"\"\n}\n\nstep_github() {\n log \"$(msg github.title)\"\n prompt_optional AGENTTEAMS_GITHUB_TOKEN \"$(msg github.token_prompt)\" \"true\" || return 0\n}\n\nstep_skills() {\n log \"\"\n log \"$(msg skills.title)\"\n prompt_optional AGENTTEAMS_SKILLS_API_URL \"$(msg skills.url_prompt)\" || return 0\n log \"\"\n}\n\nstep_volume() {\n log \"$(msg data.title)\"\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_DATA_DIR=\"${AGENTTEAMS_DATA_DIR:-agentteams-data}\"\n log \" $(msg data.volume_using \"${AGENTTEAMS_DATA_DIR}\") (non-interactive, skipped)\"\n export AGENTTEAMS_DATA_DIR\n return 0\n fi\n # ─────────────────────────────────────────────────────────────────\n if [ -z \"${AGENTTEAMS_DATA_DIR+x}\" ]; then\n local _input\n read -e -p \"$(msg data.volume_prompt): \" _input\n if [ \"${_input}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_DATA_DIR=\"${_input:-agentteams-data}\"\n export AGENTTEAMS_DATA_DIR\n fi\n AGENTTEAMS_DATA_DIR=\"${AGENTTEAMS_DATA_DIR:-agentteams-data}\"\n log \"$(msg data.volume_using \"${AGENTTEAMS_DATA_DIR}\")\"\n}\n\nstep_workspace() {\n log \"$(msg workspace.title)\"\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_WORKSPACE_DIR=\"${AGENTTEAMS_WORKSPACE_DIR:-${HOME}/agentteams-manager}\"\n AGENTTEAMS_WORKSPACE_DIR=\"$(cd \"${AGENTTEAMS_WORKSPACE_DIR}\" 2>/dev/null && pwd || echo \"${AGENTTEAMS_WORKSPACE_DIR}\")\"\n mkdir -p \"${AGENTTEAMS_WORKSPACE_DIR}\"\n log \" $(msg workspace.dir_label \"${AGENTTEAMS_WORKSPACE_DIR}\") (non-interactive, skipped)\"\n export AGENTTEAMS_WORKSPACE_DIR\n return 0\n fi\n # ─────────────────────────────────────────────────────────────────\n local _input\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg workspace.dir_prompt \"${HOME}/agentteams-manager\")\" \"${AGENTTEAMS_WORKSPACE_DIR}\")\"\n _input=\"${AGENTTEAMS_WORKSPACE_DIR}\"\n else\n local _current=\"${AGENTTEAMS_WORKSPACE_DIR}\"\n if [ -n \"${_current}\" ]; then\n read -e -p \"$(msg workspace.dir_prompt \"${HOME}/agentteams-manager\") [${_current}]: \" _input\n else\n read -e -p \"$(msg workspace.dir_prompt \"${HOME}/agentteams-manager\"): \" _input\n fi\n if [ \"${_input}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _input=\"${_input:-${_current}}\"\n fi\n AGENTTEAMS_WORKSPACE_DIR=\"${_input:-${HOME}/agentteams-manager}\"\n export AGENTTEAMS_WORKSPACE_DIR\n AGENTTEAMS_WORKSPACE_DIR=\"$(cd \"${AGENTTEAMS_WORKSPACE_DIR}\" 2>/dev/null && pwd || echo \"${AGENTTEAMS_WORKSPACE_DIR}\")\"\n mkdir -p \"${AGENTTEAMS_WORKSPACE_DIR}\"\n log \"$(msg workspace.dir_label \"${AGENTTEAMS_WORKSPACE_DIR}\")\"\n}\n\nstep_runtime() {\n log \"$(msg worker_runtime.title)\"\n echo \"\"\n echo \" 1) $(msg worker_runtime.copaw)\"\n echo \" 2) $(msg worker_runtime.openclaw)\"\n if ! _ver_lt \"${AGENTTEAMS_VERSION}\" \"v1.1.0\"; then\n echo \" 3) $(msg worker_runtime.hermes)\"\n fi\n echo \"\"\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"${AGENTTEAMS_DEFAULT_WORKER_RUNTIME:-copaw}\"\n elif [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_DEFAULT_WORKER_RUNTIME}\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg worker_runtime.title_short)\" \"${AGENTTEAMS_DEFAULT_WORKER_RUNTIME}\")\"\n local _runtime_choice\n read -e -p \"$(msg worker_runtime.choice): \" _runtime_choice\n if [ \"${_runtime_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n if [ -n \"${_runtime_choice}\" ]; then\n case \"${_runtime_choice}\" in\n 2) AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"openclaw\" ;;\n 3) if ! _ver_lt \"${AGENTTEAMS_VERSION}\" \"v1.1.0\"; then\n AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"hermes\"\n else\n AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"copaw\"\n fi ;;\n *) AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"copaw\" ;;\n esac\n fi\n elif [ -z \"${AGENTTEAMS_DEFAULT_WORKER_RUNTIME+x}\" ]; then\n local _runtime_choice\n read -e -p \"$(msg worker_runtime.choice): \" _runtime_choice\n if [ \"${_runtime_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _runtime_choice=\"${_runtime_choice:-1}\"\n case \"${_runtime_choice}\" in\n 2) AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"openclaw\" ;;\n 3) if ! _ver_lt \"${AGENTTEAMS_VERSION}\" \"v1.1.0\"; then\n AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"hermes\"\n else\n AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"copaw\"\n fi ;;\n *) AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"copaw\" ;;\n esac\n fi\n export AGENTTEAMS_DEFAULT_WORKER_RUNTIME\n log \"$(msg worker_runtime.selected \"${AGENTTEAMS_DEFAULT_WORKER_RUNTIME}\")\"\n}\n\nstep_manager_runtime() {\n log \"$(msg manager_runtime.title)\"\n echo \"\"\n echo \" 1) $(msg manager_runtime.copaw)\"\n echo \" 2) $(msg manager_runtime.openclaw)\"\n echo \"\"\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_MANAGER_RUNTIME=\"${AGENTTEAMS_MANAGER_RUNTIME:-copaw}\"\n elif [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_MANAGER_RUNTIME}\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg manager_runtime.title_short)\" \"${AGENTTEAMS_MANAGER_RUNTIME}\")\"\n local _runtime_choice\n read -e -p \"$(msg manager_runtime.choice): \" _runtime_choice\n if [ \"${_runtime_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n if [ -n \"${_runtime_choice}\" ]; then\n case \"${_runtime_choice}\" in\n 2) AGENTTEAMS_MANAGER_RUNTIME=\"openclaw\" ;;\n *) AGENTTEAMS_MANAGER_RUNTIME=\"copaw\" ;;\n esac\n fi\n elif [ -z \"${AGENTTEAMS_MANAGER_RUNTIME+x}\" ]; then\n local _runtime_choice\n read -e -p \"$(msg manager_runtime.choice): \" _runtime_choice\n if [ \"${_runtime_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _runtime_choice=\"${_runtime_choice:-1}\"\n case \"${_runtime_choice}\" in\n 2) AGENTTEAMS_MANAGER_RUNTIME=\"openclaw\" ;;\n *) AGENTTEAMS_MANAGER_RUNTIME=\"copaw\" ;;\n esac\n fi\n export AGENTTEAMS_MANAGER_RUNTIME\n log \"$(msg manager_runtime.selected \"${AGENTTEAMS_MANAGER_RUNTIME}\")\"\n}\n\nstep_e2ee() {\n log \"\"\n log \"$(msg matrix_e2ee.title)\"\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_MATRIX_E2EE=\"${AGENTTEAMS_MATRIX_E2EE:-0}\"\n log \" $(msg matrix_e2ee.title_short) = ${AGENTTEAMS_MATRIX_E2EE} (non-interactive, skipped)\"\n export AGENTTEAMS_MATRIX_E2EE\n return 0\n fi\n # ─────────────────────────────────────────────────────────────────\n echo \"\"\n echo -e \" $(msg matrix_e2ee.desc)\"\n echo \"\"\n echo \" 1) $(msg matrix_e2ee.disable)\"\n echo \" 2) $(msg matrix_e2ee.enable)\"\n echo \"\"\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_MATRIX_E2EE}\" ]; then\n local _e2ee_display; if [ \"${AGENTTEAMS_MATRIX_E2EE}\" = \"1\" ]; then _e2ee_display=\"$(msg matrix_e2ee.val_enabled)\"; else _e2ee_display=\"$(msg matrix_e2ee.val_disabled)\"; fi\n log \"$(msg prompt.upgrade_keep \"$(msg matrix_e2ee.title_short)\" \"${_e2ee_display}\")\"\n local _e2ee_choice\n read -e -p \"$(msg matrix_e2ee.choice): \" _e2ee_choice\n if [ \"${_e2ee_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n if [ -n \"${_e2ee_choice}\" ]; then\n case \"${_e2ee_choice}\" in\n 2) AGENTTEAMS_MATRIX_E2EE=\"1\" ;;\n *) AGENTTEAMS_MATRIX_E2EE=\"0\" ;;\n esac\n fi\n elif [ -z \"${AGENTTEAMS_MATRIX_E2EE+x}\" ]; then\n local _e2ee_choice\n read -e -p \"$(msg matrix_e2ee.choice): \" _e2ee_choice\n if [ \"${_e2ee_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _e2ee_choice=\"${_e2ee_choice:-1}\"\n case \"${_e2ee_choice}\" in\n 2) AGENTTEAMS_MATRIX_E2EE=\"1\" ;;\n *) AGENTTEAMS_MATRIX_E2EE=\"0\" ;;\n esac\n fi\n AGENTTEAMS_MATRIX_E2EE=\"${AGENTTEAMS_MATRIX_E2EE:-0}\"\n export AGENTTEAMS_MATRIX_E2EE\n if [ \"${AGENTTEAMS_MATRIX_E2EE}\" = \"1\" ]; then\n log \"$(msg matrix_e2ee.selected_enabled)\"\n else\n log \"$(msg matrix_e2ee.selected_disabled)\"\n fi\n}\n\nstep_docker_proxy() {\n # Only relevant when socket mounting is enabled\n if [ \"${AGENTTEAMS_MOUNT_SOCKET}\" != \"1\" ]; then\n AGENTTEAMS_DOCKER_PROXY=\"0\"\n return 0\n fi\n\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_DOCKER_PROXY=\"${AGENTTEAMS_DOCKER_PROXY:-0}\"\n log \" $(msg docker_proxy.title_short) = ${AGENTTEAMS_DOCKER_PROXY} (non-interactive, skipped)\"\n export AGENTTEAMS_DOCKER_PROXY\n return 0\n fi\n # ─────────────────────────────────────────────────────────────────\n\n echo \"\"\n echo -e \" \\033[1m$(msg docker_proxy.title)\\033[0m\"\n echo \"\"\n echo -e \" $(msg docker_proxy.desc)\"\n echo \"\"\n echo \" 1) $(msg docker_proxy.enable)\"\n echo \" 2) $(msg docker_proxy.disable)\"\n echo \"\"\n\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_DOCKER_PROXY}\" ]; then\n local _proxy_display; if [ \"${AGENTTEAMS_DOCKER_PROXY}\" = \"1\" ]; then _proxy_display=\"$(msg docker_proxy.val_enabled)\"; else _proxy_display=\"$(msg docker_proxy.val_disabled)\"; fi\n log \"$(msg prompt.upgrade_keep \"$(msg docker_proxy.title_short)\" \"${_proxy_display}\")\"\n local _choice\n read -e -p \"$(msg docker_proxy.choice): \" _choice\n if [ \"${_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n if [ -n \"${_choice}\" ]; then\n case \"${_choice}\" in\n 2) AGENTTEAMS_DOCKER_PROXY=\"0\" ;;\n *) AGENTTEAMS_DOCKER_PROXY=\"1\" ;;\n esac\n fi\n elif [ -z \"${AGENTTEAMS_DOCKER_PROXY+x}\" ]; then\n local _choice\n read -e -p \"$(msg docker_proxy.choice): \" _choice\n if [ \"${_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _choice=\"${_choice:-1}\"\n case \"${_choice}\" in\n 2) AGENTTEAMS_DOCKER_PROXY=\"0\" ;;\n *) AGENTTEAMS_DOCKER_PROXY=\"1\" ;;\n esac\n fi\n AGENTTEAMS_DOCKER_PROXY=\"${AGENTTEAMS_DOCKER_PROXY:-1}\"\n export AGENTTEAMS_DOCKER_PROXY\n if [ \"${AGENTTEAMS_DOCKER_PROXY}\" = \"1\" ]; then\n log \"$(msg docker_proxy.selected_enabled)\"\n\n # Prompt for additional allowed image sources\n echo \"\"\n echo -e \" $(msg docker_proxy.registries_desc)\"\n echo \"\"\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_PROXY_ALLOWED_REGISTRIES}\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg docker_proxy.registries_label)\" \"${AGENTTEAMS_PROXY_ALLOWED_REGISTRIES}\")\"\n local _reg_input\n read -e -p \"$(msg docker_proxy.registries_prompt): \" _reg_input\n if [ \"${_reg_input}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n [ -n \"${_reg_input}\" ] && AGENTTEAMS_PROXY_ALLOWED_REGISTRIES=\"${_reg_input}\"\n elif [ -z \"${AGENTTEAMS_PROXY_ALLOWED_REGISTRIES+x}\" ]; then\n local _reg_input\n read -e -p \"$(msg docker_proxy.registries_prompt): \" _reg_input\n if [ \"${_reg_input}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_PROXY_ALLOWED_REGISTRIES=\"${_reg_input:-}\"\n fi\n export AGENTTEAMS_PROXY_ALLOWED_REGISTRIES\n else\n log \"$(msg docker_proxy.selected_disabled)\"\n fi\n}\n\nstep_idle() {\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_WORKER_IDLE_TIMEOUT=\"${AGENTTEAMS_WORKER_IDLE_TIMEOUT:-720}\"\n log \" $(msg idle_timeout.label) = ${AGENTTEAMS_WORKER_IDLE_TIMEOUT} (non-interactive, skipped)\"\n export AGENTTEAMS_WORKER_IDLE_TIMEOUT\n return 0\n fi\n # ─────────────────────────────────────────────────────────────────\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_WORKER_IDLE_TIMEOUT}\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg idle_timeout.label)\" \"${AGENTTEAMS_WORKER_IDLE_TIMEOUT}\")\"\n local _idle_timeout\n read -e -p \"$(msg idle_timeout.prompt): \" _idle_timeout\n if [ \"${_idle_timeout}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n [ -n \"${_idle_timeout}\" ] && AGENTTEAMS_WORKER_IDLE_TIMEOUT=\"${_idle_timeout}\"\n elif [ -z \"${AGENTTEAMS_WORKER_IDLE_TIMEOUT+x}\" ]; then\n local _idle_timeout\n read -e -p \"$(msg idle_timeout.prompt): \" _idle_timeout\n if [ \"${_idle_timeout}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_WORKER_IDLE_TIMEOUT=\"${_idle_timeout:-720}\"\n fi\n AGENTTEAMS_WORKER_IDLE_TIMEOUT=\"${AGENTTEAMS_WORKER_IDLE_TIMEOUT:-720}\"\n export AGENTTEAMS_WORKER_IDLE_TIMEOUT\n log \"$(msg idle_timeout.selected \"${AGENTTEAMS_WORKER_IDLE_TIMEOUT}\")\"\n}\n\nstep_hostshare() {\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_HOST_SHARE_DIR=\"${AGENTTEAMS_HOST_SHARE_DIR:-${HOME}}\"\n log \" $(msg host_share.label) = ${AGENTTEAMS_HOST_SHARE_DIR} (non-interactive, skipped)\"\n export AGENTTEAMS_HOST_SHARE_DIR\n return 0\n fi\n # ─────────────────────────────────────────────────────────────────\n local _current=\"${AGENTTEAMS_HOST_SHARE_DIR}\"\n local _share_dir\n if [ -n \"${_current}\" ]; then\n read -e -p \"$(msg host_share.prompt \"$HOME\") [${_current}]: \" _share_dir\n else\n read -e -p \"$(msg host_share.prompt \"$HOME\"): \" _share_dir\n fi\n if [ \"${_share_dir}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_HOST_SHARE_DIR=\"${_share_dir:-${_current:-$HOME}}\"\n export AGENTTEAMS_HOST_SHARE_DIR\n}\n\nstep_podman_autostart() {\n echo \"\"\n log \"$(msg podman.autostart.title)\"\n echo \"\"\n echo -e \" $(msg podman.autostart.desc)\"\n echo \"\"\n echo \" 1) $(msg podman.autostart.enable)\"\n echo \" 2) $(msg podman.autostart.disable)\"\n echo \"\"\n\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_PODMAN_AUTOSTART}\" ]; then\n local _disp; if [ \"${AGENTTEAMS_PODMAN_AUTOSTART}\" = \"1\" ]; then _disp=\"$(msg podman.autostart.val_enabled)\"; else _disp=\"$(msg podman.autostart.val_disabled)\"; fi\n log \"$(msg prompt.upgrade_keep \"$(msg podman.autostart.title_short)\" \"${_disp}\")\"\n local _choice\n read -e -p \"$(msg podman.autostart.choice): \" _choice\n if [ \"${_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n if [ -n \"${_choice}\" ]; then\n case \"${_choice}\" in\n 2) AGENTTEAMS_PODMAN_AUTOSTART=\"0\" ;;\n *) AGENTTEAMS_PODMAN_AUTOSTART=\"1\" ;;\n esac\n fi\n elif [ -z \"${AGENTTEAMS_PODMAN_AUTOSTART+x}\" ]; then\n local _choice\n read -e -p \"$(msg podman.autostart.choice) [1]: \" _choice\n if [ \"${_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _choice=\"${_choice:-1}\"\n case \"${_choice}\" in\n 2) AGENTTEAMS_PODMAN_AUTOSTART=\"0\" ;;\n *) AGENTTEAMS_PODMAN_AUTOSTART=\"1\" ;;\n esac\n fi\n AGENTTEAMS_PODMAN_AUTOSTART=\"${AGENTTEAMS_PODMAN_AUTOSTART:-1}\"\n export AGENTTEAMS_PODMAN_AUTOSTART\n if [ \"${AGENTTEAMS_PODMAN_AUTOSTART}\" = \"1\" ]; then\n log \"$(msg podman.autostart.selected_enabled)\"\n else\n log \"$(msg podman.autostart.selected_disabled)\"\n fi\n}\n\n# ============================================================\n# Setup dedicated Podman autostart service for AgentTeams\n# ============================================================\nsetup_podman_autostart() {\n # Only execute if using podman and autostart is enabled\n if [ \"${DOCKER_CMD:-docker}\" != \"podman\" ] || [ \"${AGENTTEAMS_PODMAN_AUTOSTART:-0}\" != \"1\" ]; then\n return 0\n fi\n\n log \"$(msg install.podman.autostart_title)\"\n\n local current_user\n current_user=\"$(whoami)\"\n\n # Define the dedicated AgentTeams service content\n # Strict scoping: only manage AgentTeams containers.\n local _service_content=\"[Unit]\nDescription=AgentTeams Dedicated Podman Autostart Service\nDocumentation=https://github.com/agentscope-ai/AgentTeams\nWants=network-online.target\nAfter=network-online.target\n\n[Service]\nType=oneshot\nRemainAfterExit=true\nEnvironment=LOGGING=\\\"--log-level=info\\\"\nExecStart=/usr/bin/podman \\$LOGGING start --filter name=agentteams --filter restart-policy=always --filter restart-policy=unless-stopped\nExecStop=/usr/bin/podman \\$LOGGING stop --filter name=agentteams --filter restart-policy=always --filter restart-policy=unless-stopped\n\n[Install]\nWantedBy=default.target\"\n\n local _autostart_ok=0\n\n if [ \"${current_user}\" = \"root\" ]; then\n # Rootful Mode (System-wide dedicated service)\n log \"$(msg install.podman.root_setup)\"\n\n local _sys_dir=\"/etc/systemd/system\"\n mkdir -p \"${_sys_dir}\"\n echo \"${_service_content}\" > \"${_sys_dir}/agentteams-podman-restart.service\"\n\n systemctl daemon-reload >/dev/null 2>&1 || true\n\n if systemctl enable --now agentteams-podman-restart.service >/dev/null 2>&1; then\n log \"$(msg install.podman.root_success)\"\n _autostart_ok=1\n else\n log \"$(msg install.podman.root_fail)\"\n fi\n else\n # Rootless Mode (User-level dedicated service)\n log \"$(msg install.podman.user_setup \"${current_user}\")\"\n\n local _linger_ok=0\n\n # 1. Enable Linger for background execution (Graceful escalation strategy)\n if loginctl show-user \"${current_user}\" --property=Linger 2>/dev/null | grep -q \"Linger=yes\"; then\n _linger_ok=1\n else\n log \"$(msg install.podman.linger_enable)\"\n\n # Step A: Try to enable linger natively without sudo\n if loginctl enable-linger \"${current_user}\" >/dev/null 2>&1; then\n log \"Successfully enabled systemd linger natively.\"\n _linger_ok=1\n else\n # Step B: Fallback to sudo\n log \"Native linger enablement failed. Distro security policy requires privileges.\"\n if command -v sudo >/dev/null 2>&1; then\n log \"Attempting to enable systemd linger via sudo (enter password if prompted)...\"\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n sudo -n loginctl enable-linger \"${current_user}\" 2>/dev/null && _linger_ok=1 || log \"$(msg install.podman.linger_warn)\"\n else\n sudo loginctl enable-linger \"${current_user}\" && _linger_ok=1 || log \"$(msg install.podman.linger_warn)\"\n fi\n else\n log \"$(msg install.podman.linger_warn)\"\n fi\n fi\n fi\n\n # 2. Write dedicated service file to user directory\n local _user_dir=\"${HOME}/.config/systemd/user\"\n mkdir -p \"${_user_dir}\"\n echo \"${_service_content}\" > \"${_user_dir}/agentteams-podman-restart.service\"\n\n systemctl --user daemon-reload >/dev/null 2>&1 || true\n\n if systemctl --user enable --now agentteams-podman-restart.service >/dev/null 2>&1; then\n log \"$(msg install.podman.user_success)\"\n # Crucial: Only consider autostart fully successful if systemd linger is also enabled\n if [ \"${_linger_ok}\" = \"1\" ]; then\n _autostart_ok=1\n else\n log \"⚠️ Service enabled, but since systemd linger is not active, containers will not auto-recover on system reboot.\"\n fi\n else\n log \"$(msg install.podman.user_fail)\"\n fi\n fi\n\n # Only print final success banner if the service AND linger were actually enabled\n if [ \"${_autostart_ok}\" = \"1\" ]; then\n log \"$(msg install.podman.success)\"\n fi\n}\n\n# ============================================================\n# Manager Installation (Interactive)\n# ============================================================\n\ninstall_manager() {\n log \"$(msg install.title)\"\n log \"$(msg install.registry \"${AGENTTEAMS_REGISTRY}\")\"\n log \"\"\n log \"$(msg install.dir \"$(pwd)\")\"\n log \"$(msg install.dir_hint)\"\n log \"$(msg install.dir_hint2)\"\n log \"\"\n\n # Non-interactive fallback: resolve version immediately so image tags are available\n # before the step state machine runs. Interactive mode lets step_version handle it.\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_VERSION=\"${AGENTTEAMS_VERSION:-${AGENTTEAMS_KNOWN_STABLE_VERSION}}\"\n resolve_image_tags\n fi\n\n # Migrate legacy env file location before checks\n local existing_env=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n if [ ! -f \"${existing_env}\" ] && [ -f \"./agentteams-manager.env\" ]; then\n log \"Migrating agentteams-manager.env from current directory to ${existing_env}...\"\n mv \"./agentteams-manager.env\" \"${existing_env}\"\n fi\n\n # Orphan volume detection (only when no env file — step_existing handles the env-file case)\n if [ ! -f \"${existing_env}\" ]; then\n local data_vol=\"${AGENTTEAMS_DATA_DIR:-agentteams-data}\"\n if ${DOCKER_CMD} volume ls -q | grep -q \"^${data_vol}$\"; then\n echo \"\"\n log \"$(msg install.orphan_volume.detected \"${data_vol}\")\"\n log \"$(msg install.orphan_volume.warn)\"\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n log \"$(msg install.orphan_volume.clean_noninteractive)\"\n ${DOCKER_CMD} stop agentteams-manager 2>/dev/null || true\n ${DOCKER_CMD} rm agentteams-manager 2>/dev/null || true\n for w in $(${DOCKER_CMD} ps -a --format '{{.Names}}' | grep \"^agentteams-worker-\" || true); do\n ${DOCKER_CMD} stop \"${w}\" 2>/dev/null || true\n ${DOCKER_CMD} rm \"${w}\" 2>/dev/null || true\n done\n log \"$(msg install.orphan_volume.cleaning)\"\n ${DOCKER_CMD} volume rm \"${data_vol}\" 2>/dev/null || true\n log \"$(msg install.orphan_volume.cleaned)\"\n else\n echo \"\"\n echo \"$(msg install.orphan_volume.choose)\"\n echo \"$(msg install.orphan_volume.clean)\"\n echo \"$(msg install.orphan_volume.keep)\"\n echo \"\"\n local ORPHAN_CHOICE\n read -e -p \"$(msg install.orphan_volume.prompt): \" ORPHAN_CHOICE\n ORPHAN_CHOICE=\"${ORPHAN_CHOICE:-1}\"\n case \"${ORPHAN_CHOICE}\" in\n 1|clean)\n ${DOCKER_CMD} stop agentteams-manager 2>/dev/null || true\n ${DOCKER_CMD} rm agentteams-manager 2>/dev/null || true\n for w in $(${DOCKER_CMD} ps -a --format '{{.Names}}' | grep \"^agentteams-worker-\" || true); do\n ${DOCKER_CMD} stop \"${w}\" 2>/dev/null || true\n ${DOCKER_CMD} rm \"${w}\" 2>/dev/null || true\n done\n log \"$(msg install.orphan_volume.cleaning)\"\n ${DOCKER_CMD} volume rm \"${data_vol}\" 2>/dev/null || true\n log \"$(msg install.orphan_volume.cleaned)\"\n ;;\n 2|keep)\n log \"$(msg install.orphan_volume.keeping)\"\n ;;\n esac\n fi\n fi\n fi\n\n # ── State machine ─────────────────────────────────────────────────────────\n local _STEPS=( step_lang step_mode step_version step_existing step_llm step_manager_runtime step_runtime step_admin step_network\n step_ports step_domains step_github step_skills step_volume\n step_workspace step_e2ee step_docker_proxy step_idle step_hostshare step_podman_autostart )\n local _STEP_HISTORY=()\n local _step_idx=0\n while [ \"${_step_idx}\" -lt \"${#_STEPS[@]}\" ]; do\n local _step_fn=\"${_STEPS[$_step_idx]}\"\n if should_skip_step \"${_step_fn}\"; then\n _step_idx=$((_step_idx + 1))\n continue\n fi\n if [ \"${#_STEP_HISTORY[@]}\" -gt 0 ]; then\n log \"$(msg nav.back_hint)\"\n fi\n STEP_RESULT=\"\"\n \"${_step_fn}\"\n if [ \"${STEP_RESULT}\" = \"back\" ]; then\n if [ \"${#_STEP_HISTORY[@]}\" -gt 0 ]; then\n local _last=$(( ${#_STEP_HISTORY[@]} - 1 ))\n _step_idx=\"${_STEP_HISTORY[$_last]}\"\n _STEP_HISTORY=(\"${_STEP_HISTORY[@]:0:${_last}}\")\n clear_step_vars \"${_STEPS[$_step_idx]}\"\n fi\n # else: first step, ignore 'b'\n else\n _STEP_HISTORY+=(\"${_step_idx}\")\n _step_idx=$((_step_idx + 1))\n fi\n done\n # ── End state machine ──────────────────────────────────────────────────────\n\n # Post-machine defaults for any steps that were skipped\n AGENTTEAMS_DATA_DIR=\"${AGENTTEAMS_DATA_DIR:-agentteams-data}\"\n if [ -z \"${AGENTTEAMS_WORKSPACE_DIR+x}\" ] || [ -z \"${AGENTTEAMS_WORKSPACE_DIR}\" ]; then\n AGENTTEAMS_WORKSPACE_DIR=\"${HOME}/agentteams-manager\"\n export AGENTTEAMS_WORKSPACE_DIR\n fi\n AGENTTEAMS_WORKSPACE_DIR=\"$(cd \"${AGENTTEAMS_WORKSPACE_DIR}\" 2>/dev/null && pwd || echo \"${AGENTTEAMS_WORKSPACE_DIR}\")\"\n mkdir -p \"${AGENTTEAMS_WORKSPACE_DIR}\"\n AGENTTEAMS_MANAGER_RUNTIME=\"${AGENTTEAMS_MANAGER_RUNTIME:-copaw}\"\n export AGENTTEAMS_MANAGER_RUNTIME\n AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"${AGENTTEAMS_DEFAULT_WORKER_RUNTIME:-copaw}\"\n AGENTTEAMS_MATRIX_E2EE=\"${AGENTTEAMS_MATRIX_E2EE:-0}\"\n export AGENTTEAMS_MATRIX_E2EE\n AGENTTEAMS_WORKER_IDLE_TIMEOUT=\"${AGENTTEAMS_WORKER_IDLE_TIMEOUT:-720}\"\n export AGENTTEAMS_WORKER_IDLE_TIMEOUT\n AGENTTEAMS_HOST_SHARE_DIR=\"${AGENTTEAMS_HOST_SHARE_DIR:-$HOME}\"\n export AGENTTEAMS_HOST_SHARE_DIR\n\n log \"\"\n\n # Generate secrets (only if not already set)\n log \"$(msg install.generating_secrets)\"\n AGENTTEAMS_MANAGER_PASSWORD=\"${AGENTTEAMS_MANAGER_PASSWORD:-$(generate_key)}\"\n AGENTTEAMS_REGISTRATION_TOKEN=\"${AGENTTEAMS_REGISTRATION_TOKEN:-$(generate_key)}\"\n AGENTTEAMS_MINIO_USER=\"${AGENTTEAMS_MINIO_USER:-${AGENTTEAMS_ADMIN_USER}}\"\n AGENTTEAMS_MINIO_PASSWORD=\"${AGENTTEAMS_MINIO_PASSWORD:-${AGENTTEAMS_ADMIN_PASSWORD}}\"\n AGENTTEAMS_MANAGER_GATEWAY_KEY=\"${AGENTTEAMS_MANAGER_GATEWAY_KEY:-$(generate_key)}\"\n\n # Matrix AppService tokens — generate once during install/upgrade if not provided.\n # Persisted to env file so they survive controller restarts.\n AGENTTEAMS_MATRIX_APPSERVICE_ENABLED=\"${AGENTTEAMS_MATRIX_APPSERVICE_ENABLED:-true}\"\n export AGENTTEAMS_MATRIX_APPSERVICE_ENABLED\n if [ \"${AGENTTEAMS_MATRIX_APPSERVICE_ENABLED}\" != \"false\" ] && [ \"${AGENTTEAMS_MATRIX_APPSERVICE_ENABLED}\" != \"0\" ]; then\n if [ -z \"${AGENTTEAMS_MATRIX_APPSERVICE_AS_TOKEN}\" ]; then\n AGENTTEAMS_MATRIX_APPSERVICE_AS_TOKEN=\"$(openssl rand -hex 32)\"\n log \" Auto-generated AppService as_token (saved to env file)\"\n fi\n if [ -z \"${AGENTTEAMS_MATRIX_APPSERVICE_HS_TOKEN}\" ]; then\n AGENTTEAMS_MATRIX_APPSERVICE_HS_TOKEN=\"$(openssl rand -hex 32)\"\n log \" Auto-generated AppService hs_token (saved to env file)\"\n fi\n export AGENTTEAMS_MATRIX_APPSERVICE_AS_TOKEN\n export AGENTTEAMS_MATRIX_APPSERVICE_HS_TOKEN\n fi\n\n # Detect Apple Silicon (M1/M2/M3/M4) - need JVM fix for Higress Console\n # See: https://github.com/agentscope-ai/AgentTeams/issues/249\n if [ -z \"${JVM_ARGS:-}\" ] && [ \"$(uname -m)\" = \"arm64\" ] && [ \"$(uname -s)\" = \"Darwin\" ]; then\n log \"Apple Silicon detected - setting JVM_ARGS to fix Higress Console SIGILL issue\"\n JVM_ARGS=\"-XX:+UnlockDiagnosticVMOptions -XX:-UseAESCTRIntrinsics -XX:UseSVE=0\"\n fi\n\n # Write .env file\n ENV_FILE=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n cat > \"${ENV_FILE}\" << EOF\n# AgentTeams Manager Configuration\n# Generated by agentteams-install.sh on $(date)\n\n# Language\nAGENTTEAMS_LANGUAGE=${AGENTTEAMS_LANGUAGE}\n\n# LLM\nAGENTTEAMS_LLM_PROVIDER=${AGENTTEAMS_LLM_PROVIDER}\nAGENTTEAMS_DEFAULT_MODEL=${AGENTTEAMS_DEFAULT_MODEL}\nAGENTTEAMS_LLM_API_KEY=${AGENTTEAMS_LLM_API_KEY}\nAGENTTEAMS_OPENAI_BASE_URL=${AGENTTEAMS_OPENAI_BASE_URL:-}\nAGENTTEAMS_MODEL_CONTEXT_WINDOW=${AGENTTEAMS_MODEL_CONTEXT_WINDOW:-}\nAGENTTEAMS_MODEL_MAX_TOKENS=${AGENTTEAMS_MODEL_MAX_TOKENS:-}\nAGENTTEAMS_MODEL_REASONING=${AGENTTEAMS_MODEL_REASONING:-}\nAGENTTEAMS_MODEL_VISION=${AGENTTEAMS_MODEL_VISION:-}\n\n# Embedding model (empty = disabled, default: text-embedding-v4)\nAGENTTEAMS_EMBEDDING_MODEL=${AGENTTEAMS_EMBEDDING_MODEL}\n\n# Admin\nAGENTTEAMS_ADMIN_USER=${AGENTTEAMS_ADMIN_USER}\nAGENTTEAMS_ADMIN_PASSWORD=${AGENTTEAMS_ADMIN_PASSWORD}\n\n# Ports\nAGENTTEAMS_LOCAL_ONLY=${AGENTTEAMS_LOCAL_ONLY}\nAGENTTEAMS_PORT_GATEWAY=${AGENTTEAMS_PORT_GATEWAY}\nAGENTTEAMS_PORT_CONSOLE=${AGENTTEAMS_PORT_CONSOLE}\nAGENTTEAMS_PORT_ELEMENT_WEB=${AGENTTEAMS_PORT_ELEMENT_WEB}\nAGENTTEAMS_PORT_MANAGER_CONSOLE=${AGENTTEAMS_PORT_MANAGER_CONSOLE:-18888}\n\n# Manager runtime (openclaw | copaw)\nAGENTTEAMS_MANAGER_RUNTIME=${AGENTTEAMS_MANAGER_RUNTIME:-copaw}\n\n# Matrix\nAGENTTEAMS_MATRIX_DOMAIN=${AGENTTEAMS_MATRIX_DOMAIN}\nAGENTTEAMS_MATRIX_CLIENT_DOMAIN=${AGENTTEAMS_MATRIX_CLIENT_DOMAIN}\n\n# Gateway\nAGENTTEAMS_AI_GATEWAY_DOMAIN=${AGENTTEAMS_AI_GATEWAY_DOMAIN}\nAGENTTEAMS_MANAGER_GATEWAY_KEY=${AGENTTEAMS_MANAGER_GATEWAY_KEY}\n\n# File System\nAGENTTEAMS_FS_DOMAIN=${AGENTTEAMS_FS_DOMAIN}\nAGENTTEAMS_CONSOLE_DOMAIN=${AGENTTEAMS_CONSOLE_DOMAIN}\nAGENTTEAMS_MINIO_USER=${AGENTTEAMS_MINIO_USER}\nAGENTTEAMS_MINIO_PASSWORD=${AGENTTEAMS_MINIO_PASSWORD}\n\n# Internal\nAGENTTEAMS_MANAGER_PASSWORD=${AGENTTEAMS_MANAGER_PASSWORD}\nAGENTTEAMS_REGISTRATION_TOKEN=${AGENTTEAMS_REGISTRATION_TOKEN}\n\n# GitHub (optional)\nAGENTTEAMS_GITHUB_TOKEN=${AGENTTEAMS_GITHUB_TOKEN:-}\n\n# Nacos package import defaults\nAGENTTEAMS_NACOS_REGISTRY_URI=${AGENTTEAMS_NACOS_REGISTRY_URI:-nacos://market.agentteams.io:80/public}\nAGENTTEAMS_NACOS_USERNAME=${AGENTTEAMS_NACOS_USERNAME:-}\nAGENTTEAMS_NACOS_PASSWORD=${AGENTTEAMS_NACOS_PASSWORD:-}\nAGENTTEAMS_NACOS_TOKEN=${AGENTTEAMS_NACOS_TOKEN:-}\n\n# Skills Registry (optional, default: nacos://market.agentteams.io:80/public)\nAGENTTEAMS_SKILLS_API_URL=${AGENTTEAMS_SKILLS_API_URL:-nacos://market.agentteams.io:80/public}\n\n# OpenClaw CMS plugin (optional)\nAGENTTEAMS_CMS_TRACES_ENABLED=${AGENTTEAMS_CMS_TRACES_ENABLED:-false}\nAGENTTEAMS_CMS_ENDPOINT=${AGENTTEAMS_CMS_ENDPOINT:-}\nAGENTTEAMS_CMS_LICENSE_KEY=${AGENTTEAMS_CMS_LICENSE_KEY:-}\nAGENTTEAMS_CMS_PROJECT=${AGENTTEAMS_CMS_PROJECT:-}\nAGENTTEAMS_CMS_WORKSPACE=${AGENTTEAMS_CMS_WORKSPACE:-}\nAGENTTEAMS_CMS_SERVICE_NAME=${AGENTTEAMS_CMS_SERVICE_NAME:-agentteams-manager}\nAGENTTEAMS_CMS_METRICS_ENABLED=${AGENTTEAMS_CMS_METRICS_ENABLED:-false}\n\n# Worker images (for direct container creation)\nAGENTTEAMS_WORKER_IMAGE=${WORKER_IMAGE}\nAGENTTEAMS_COPAW_WORKER_IMAGE=${COPAW_WORKER_IMAGE}\nAGENTTEAMS_HERMES_WORKER_IMAGE=${HERMES_WORKER_IMAGE}\n\n# Default Worker runtime (openclaw | copaw | hermes)\nAGENTTEAMS_DEFAULT_WORKER_RUNTIME=${AGENTTEAMS_DEFAULT_WORKER_RUNTIME:-copaw}\n\n# Matrix E2EE (0=disabled, 1=enabled; default: 0)\nAGENTTEAMS_MATRIX_E2EE=${AGENTTEAMS_MATRIX_E2EE:-0}\n\n# Matrix AppService\nAGENTTEAMS_MATRIX_APPSERVICE_ENABLED=${AGENTTEAMS_MATRIX_APPSERVICE_ENABLED:-true}\nAGENTTEAMS_MATRIX_APPSERVICE_AS_TOKEN=${AGENTTEAMS_MATRIX_APPSERVICE_AS_TOKEN:-}\nAGENTTEAMS_MATRIX_APPSERVICE_HS_TOKEN=${AGENTTEAMS_MATRIX_APPSERVICE_HS_TOKEN:-}\n\n# Docker API proxy (0=disabled, 1=enabled; default: 1)\nAGENTTEAMS_DOCKER_PROXY=${AGENTTEAMS_DOCKER_PROXY:-1}\n\n# Docker API proxy: additional allowed image sources (comma-separated)\nAGENTTEAMS_PROXY_ALLOWED_REGISTRIES=${AGENTTEAMS_PROXY_ALLOWED_REGISTRIES:-}\n\n# Worker idle timeout in minutes (default: 720 = 12 hours)\nAGENTTEAMS_WORKER_IDLE_TIMEOUT=${AGENTTEAMS_WORKER_IDLE_TIMEOUT:-720}\n\n# Podman autostart via systemd (0=disabled, 1=enabled)\nAGENTTEAMS_PODMAN_AUTOSTART=${AGENTTEAMS_PODMAN_AUTOSTART:-0}\n\n# JVM Args for Higress Console (fixes SIGILL on Apple Silicon)\nJVM_ARGS=${JVM_ARGS:-}\n\n# Higress WASM plugin image registry (auto-selected by timezone)\nHIGRESS_ADMIN_WASM_PLUGIN_IMAGE_REGISTRY=${AGENTTEAMS_REGISTRY}\n\n# Data persistence\nAGENTTEAMS_DATA_DIR=${AGENTTEAMS_DATA_DIR:-agentteams-data}\n# Manager workspace (skills, memory, state — host-editable)\nAGENTTEAMS_WORKSPACE_DIR=${AGENTTEAMS_WORKSPACE_DIR:-}\n# Host directory sharing\nAGENTTEAMS_HOST_SHARE_DIR=${AGENTTEAMS_HOST_SHARE_DIR:-}\nEOF\n\n chmod 600 \"${ENV_FILE}\"\n log \"$(msg install.config_saved \"${ENV_FILE}\")\"\n\n # Detect container runtime socket\n SOCKET_MOUNT_ARGS=\"\"\n if [ \"${AGENTTEAMS_MOUNT_SOCKET}\" = \"1\" ]; then\n # Actively ensure the API socket is enabled and active before detection.\n if [ \"${DOCKER_CMD:-}\" = \"podman\" ]; then\n ensure_podman_socket\n fi\n if [ -z \"${CONTAINER_SOCK}\" ]; then\n CONTAINER_SOCK=$(detect_socket)\n fi\n if [ -n \"${CONTAINER_SOCK}\" ]; then\n log \"$(msg install.socket_detected \"${CONTAINER_SOCK}\")\"\n SOCKET_MOUNT_ARGS=\"-v ${CONTAINER_SOCK}:/var/run/docker.sock --security-opt label=disable\"\n else\n log \"$(msg install.socket_not_found)\"\n # Interactive confirmation when socket not found\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" != \"1\" ]; then\n echo \"\"\n echo -e \"\\033[33m$(msg install.socket_confirm.title)\\033[0m\"\n echo \"\"\n echo -e \"$(msg install.socket_confirm.message)\"\n echo \"\"\n read -p \"$(msg install.socket_confirm.prompt)\" SOCKET_CONFIRM\n if [ \"${SOCKET_CONFIRM}\" != \"y\" ] && [ \"${SOCKET_CONFIRM}\" != \"Y\" ]; then\n log \"$(msg install.socket_confirm.cancelled)\"\n exit 0\n fi\n fi\n fi\n fi\n\n # Create the data volume if it doesn't already exist (reuse on reinstall)\n if ! ${DOCKER_CMD} volume ls -q | grep -q \"^${AGENTTEAMS_DATA_DIR}$\"; then\n ${DOCKER_CMD} volume create \"${AGENTTEAMS_DATA_DIR}\" > /dev/null\n fi\n\n # Data mount: Docker volume\n DATA_MOUNT_ARGS=\"-v ${AGENTTEAMS_DATA_DIR}:/data\"\n\n # Manager workspace mount (always a host directory, defaulting to ~/agentteams-manager)\n WORKSPACE_MOUNT_ARGS=\"-v ${AGENTTEAMS_WORKSPACE_DIR}:/root/manager-workspace\"\n\n # Pass host timezone to container so date/time commands reflect local time\n TZ_ARGS=\"-e TZ=${AGENTTEAMS_TIMEZONE}\"\n\n # Host directory mount\n if [ -d \"${AGENTTEAMS_HOST_SHARE_DIR}\" ]; then\n HOST_SHARE_MOUNT_ARGS=\"-v ${AGENTTEAMS_HOST_SHARE_DIR}:/host-share\"\n log \"$(msg host_share.sharing \"${AGENTTEAMS_HOST_SHARE_DIR}\")\"\n else\n log \"$(msg host_share.not_exist \"${AGENTTEAMS_HOST_SHARE_DIR}\")\"\n HOST_SHARE_MOUNT_ARGS=\"-v ${AGENTTEAMS_HOST_SHARE_DIR}:/host-share\"\n fi\n\n # YOLO mode: pass through if set in environment (enables autonomous decisions)\n YOLO_ARGS=\"\"\n if [ \"${AGENTTEAMS_YOLO:-}\" = \"1\" ]; then\n YOLO_ARGS=\"-e AGENTTEAMS_YOLO=1\"\n log \"$(msg install.yolo)\"\n fi\n\n # Matrix-plugin debug tracing: pass through if AGENTTEAMS_MATRIX_DEBUG=1.\n # The container entrypoints translate this to OPENCLAW_MATRIX_DEBUG=1\n # so the openclaw matrix plugin emits structured INFO-level lifecycle\n # traces (sync state transitions, room.invite/join, message handler\n # arrival + filter outcomes). Used to diagnose worker/manager hangs.\n MATRIX_DEBUG_ARGS=\"\"\n if [ \"${AGENTTEAMS_MATRIX_DEBUG:-}\" = \"1\" ]; then\n MATRIX_DEBUG_ARGS=\"-e AGENTTEAMS_MATRIX_DEBUG=1\"\n fi\n\n # E2EE is already in the env file; but also pass explicitly in case env file is not the source\n # (AGENTTEAMS_MATRIX_E2EE is already written to ENV_FILE above via --env-file)\n\n # Pull images (manager based on runtime config; all worker runtimes always pulled)\n _is_local_image() {\n case \"$1\" in\n agentteams/*) return 0 ;;\n *) return 1 ;;\n esac\n }\n\n _local_image_exists() {\n local _img=\"$1\"\n [ -z \"${_img}\" ] && return 1\n _is_local_image \"${_img}\" || return 1\n ${DOCKER_CMD} image inspect \"${_img}\" >/dev/null 2>&1\n }\n\n # Helper: pull or skip a single image\n # Args: $1=image $2=exists_msg_key $3=pulling_msg_key\n _pull_image() {\n local _img=\"$1\" _exists_key=\"$2\" _pull_key=\"$3\"\n [ -z \"${_img}\" ] && return 0\n if _local_image_exists \"${_img}\"; then\n log \"$(msg \"${_exists_key}\" \"${_img}\")\"\n return 0\n fi\n log \"$(msg \"${_pull_key}\" \"${_img}\")\"\n local _attempt=1\n while [ $_attempt -le 3 ]; do\n if ${DOCKER_CMD} pull \"${_img}\"; then\n return 0\n fi\n if [ $_attempt -lt 3 ]; then\n log \"Pull failed (attempt ${_attempt}/3), retrying in 5s...\"\n sleep 5\n fi\n _attempt=$((_attempt + 1))\n done\n die \"Failed to pull ${_img} after 3 attempts\"\n return 1\n }\n\n # Embedded controller image (resolve versioned tag, fallback to latest)\n resolve_embedded_image\n if [ \"${AGENTTEAMS_USE_EMBEDDED}\" = \"1\" ]; then\n _local_image_exists \"${EMBEDDED_IMAGE}\" || true\n fi\n\n # Manager image is always required (select based on runtime)\n if [ \"${AGENTTEAMS_MANAGER_RUNTIME}\" = \"copaw\" ]; then\n _pull_image \"${MANAGER_COPAW_IMAGE}\" \"install.image.exists\" \"install.image.pulling_manager\"\n else\n _pull_image \"${MANAGER_IMAGE}\" \"install.image.exists\" \"install.image.pulling_manager\"\n fi\n\n # Pull all worker runtime images (workers may use any runtime regardless of the default)\n _pull_image \"${WORKER_IMAGE}\" \"install.image.worker_exists\" \"install.image.pulling_worker\"\n _pull_image \"${COPAW_WORKER_IMAGE}\" \"install.image.worker_exists\" \"install.image.pulling_worker\"\n _pull_image \"${HERMES_WORKER_IMAGE}\" \"install.image.worker_exists\" \"install.image.pulling_worker\"\n\n # --- Pre-upgrade: extract Matrix passwords from running old containers ---\n # Only needed when upgrading FROM old architecture (v1.0.9) TO embedded.\n # For new-arch-to-new-arch upgrades, credential files already exist with\n # correct room IDs — we must NOT overwrite them.\n _creds_tmp=\"\"\n if [ \"${AGENTTEAMS_UPGRADE:-0}\" = \"1\" ] && [ \"${AGENTTEAMS_USE_EMBEDDED}\" = \"1\" ]; then\n # Detect if upgrading from old arch: old arch has no agentteams-controller container\n # (or has it as a docker-proxy only, not embedded). Check if the existing\n # agentteams-controller is an embedded image (has supervisord) or just a proxy.\n _is_old_arch=0\n if ! ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -q \"^agentteams-controller$\"; then\n _is_old_arch=1\n elif ${DOCKER_CMD} ps -a --format '{{.Names}} {{.Image}}' 2>/dev/null | grep \"^agentteams-controller \" | grep -qv \"embedded\"; then\n _is_old_arch=1\n fi\n\n if [ \"${_is_old_arch}\" = \"1\" ]; then\n _creds_tmp=$(mktemp -d)\n\n # docker exec for Matrix/minio paths only works while agentteams-manager is running.\n _mgr_creds_tempstart=0\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -q '^agentteams-manager$'; then\n if ! ${DOCKER_CMD} ps --format '{{.Names}}' 2>/dev/null | grep -q '^agentteams-manager$'; then\n log \"agentteams-manager is stopped; starting it temporarily to extract Matrix credentials for upgrade...\"\n ${DOCKER_CMD} start agentteams-manager 2>/dev/null || true\n wait_matrix_ready \"agentteams-manager\"\n _mgr_creds_tempstart=1\n fi\n fi\n\n # Manager password (container Config.Env, then secrets file inside running manager, then data volume)\n _mgr_pw=$(${DOCKER_CMD} inspect agentteams-manager --format '{{range .Config.Env}}{{println .}}{{end}}' 2>/dev/null | grep '^AGENTTEAMS_MANAGER_PASSWORD=' | cut -d= -f2-)\n if [ -z \"${_mgr_pw}\" ] && ${DOCKER_CMD} ps --format '{{.Names}}' 2>/dev/null | grep -q '^agentteams-manager$'; then\n _mgr_pw=$(${DOCKER_CMD} exec agentteams-manager bash -c 'source /data/agentteams-secrets.env 2>/dev/null && echo \"${AGENTTEAMS_MANAGER_PASSWORD}\"' 2>/dev/null)\n fi\n if [ -z \"${_mgr_pw}\" ] && ${DOCKER_CMD} volume ls -q 2>/dev/null | grep -q \"^${AGENTTEAMS_DATA_DIR}$\"; then\n _mgr_pw=$(agentteams_read_secret_from_data_volume \"${AGENTTEAMS_DATA_DIR}\" AGENTTEAMS_MANAGER_PASSWORD)\n fi\n\n # Manager admin DM room ID: login as admin, find DM room with @manager\n _mgr_room=\"\"\n if [ -n \"${_mgr_pw}\" ]; then\n _admin_pw=$(grep AGENTTEAMS_ADMIN_PASSWORD \"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\" 2>/dev/null | cut -d= -f2-)\n _admin_user=$(grep AGENTTEAMS_ADMIN_USER \"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\" 2>/dev/null | cut -d= -f2-)\n _admin_user=\"${_admin_user:-admin}\"\n _matrix_domain=$(grep AGENTTEAMS_MATRIX_DOMAIN \"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\" 2>/dev/null | cut -d= -f2-)\n if [ -n \"${_admin_pw}\" ] && ${DOCKER_CMD} ps --format '{{.Names}}' 2>/dev/null | grep -q '^agentteams-manager$'; then\n _admin_token=$(${DOCKER_CMD} exec agentteams-manager curl -sf -X POST http://127.0.0.1:6167/_matrix/client/v3/login \\\n -H \"Content-Type: application/json\" \\\n -d '{\"type\":\"m.login.password\",\"identifier\":{\"type\":\"m.id.user\",\"user\":\"'\"${_admin_user}\"'\"},\"password\":\"'\"${_admin_pw}\"'\"}' 2>/dev/null | python3 -c \"import sys,json; print(json.load(sys.stdin).get('access_token',''))\" 2>/dev/null || true)\n if [ -n \"${_admin_token}\" ]; then\n _mgr_room=$(${DOCKER_CMD} exec agentteams-manager curl -sf -X GET \\\n -H \"Authorization: Bearer ${_admin_token}\" \\\n \"http://127.0.0.1:6167/_matrix/client/v3/joined_rooms\" 2>/dev/null | python3 -c \"\nimport sys,json,subprocess\nrooms = json.load(sys.stdin).get('joined_rooms',[])\nfor room_id in rooms:\n enc = room_id.replace('!','%21')\n members = json.loads(subprocess.check_output([\n 'docker','exec','agentteams-manager','curl','-sf','-X','GET',\n '-H','Authorization: Bearer ${_admin_token}',\n 'http://127.0.0.1:6167/_matrix/client/v3/rooms/'+enc+'/members'\n ]).decode()).get('chunk',[])\n member_ids = [m['state_key'] for m in members]\n if any('manager' in m and 'admin' not in m.split(':')[0] for m in member_ids):\n if len(member_ids) <= 3:\n print(room_id)\n break\n\" 2>/dev/null || true)\n fi\n fi\n if [ -z \"${_mgr_room}\" ]; then\n _mgr_room=$(agentteams_read_admin_dm_room_from_workspace \"${AGENTTEAMS_WORKSPACE_DIR}\")\n fi\n cat > \"${_creds_tmp}/default.env\" <<CREDEOF\nWORKER_PASSWORD=\"${_mgr_pw}\"\nWORKER_MINIO_PASSWORD=\"$(openssl rand -hex 24)\"\nWORKER_GATEWAY_KEY=\"${AGENTTEAMS_MANAGER_GATEWAY_KEY}\"\nWORKER_ROOM_ID=\"${_mgr_room}\"\nCREDEOF\n log \"Extracted Manager Matrix password${_mgr_room:+ and room ID}\"\n fi\n\n if [ \"${_mgr_creds_tempstart}\" = \"1\" ]; then\n log \"Stopping agentteams-manager after credential extraction (upgrade will recreate containers)...\"\n ${DOCKER_CMD} stop agentteams-manager 2>/dev/null || true\n fi\n fi # _is_old_arch\n fi\n\n # --- Stop and remove existing containers ---\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' | grep -q \"^agentteams-controller$\"; then\n ${DOCKER_CMD} stop agentteams-controller 2>/dev/null || true\n ${DOCKER_CMD} rm agentteams-controller 2>/dev/null || true\n fi\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' | grep -q \"^agentteams-manager$\"; then\n log \"$(msg install.removing_existing)\"\n ${DOCKER_CMD} stop agentteams-manager 2>/dev/null || true\n ${DOCKER_CMD} rm agentteams-manager 2>/dev/null || true\n fi\n\n # Stop and remove worker containers (controller will recreate via CR reconciliation)\n if [ -n \"${UPGRADE_EXISTING_WORKERS:-}\" ]; then\n log \"$(msg install.existing.stopping_workers)\"\n for w in ${UPGRADE_EXISTING_WORKERS}; do\n ${DOCKER_CMD} stop \"${w}\" 2>/dev/null || true\n ${DOCKER_CMD} rm \"${w}\" 2>/dev/null || true\n log \"$(msg install.existing.removed \"${w}\")\"\n done\n fi\n\n # --- Upgrade: inject extracted credentials into data volume ---\n # Only needed for old-arch upgrades (credential files were extracted above).\n if [ -n \"${_creds_tmp}\" ] && [ -d \"${_creds_tmp}\" ] && [ -n \"$(ls -A \"${_creds_tmp}\" 2>/dev/null)\" ]; then\n local _cleanup_ctr=\"agentteams-upgrade-cleanup\"\n ${DOCKER_CMD} rm -f \"${_cleanup_ctr}\" 2>/dev/null || true\n ${DOCKER_CMD} run --rm --name \"${_cleanup_ctr}\" \\\n --entrypoint sh \\\n -v \"${AGENTTEAMS_DATA_DIR}:/data\" \\\n -v \"${_creds_tmp}:/creds:ro\" \\\n \"${EMBEDDED_IMAGE}\" -c '\n rm -rf /data/worker-creds\n mkdir -p /data/worker-creds\n cp /creds/*.env /data/worker-creds/ 2>/dev/null || true\n chmod 600 /data/worker-creds/*.env 2>/dev/null || true\n ' 2>/dev/null && log \"Injected credentials for upgrade\" || log \"Warning: credential injection failed, continuing\"\n rm -rf \"${_creds_tmp}\"\n fi\n\n # --- Start containers ---\n log \"$(msg install.starting_manager)\"\n\n # Build port binding args\n if [ \"${AGENTTEAMS_LOCAL_ONLY:-1}\" = \"1\" ]; then\n _port_prefix=\"127.0.0.1:\"\n else\n _port_prefix=\"\"\n fi\n\n # Ensure agentteams-net Docker network exists\n ${DOCKER_CMD} network inspect agentteams-net >/dev/null 2>&1 || ${DOCKER_CMD} network create agentteams-net\n\n if [ \"${AGENTTEAMS_USE_EMBEDDED}\" != \"1\" ] && [ \"${AGENTTEAMS_UPGRADE:-0}\" = \"1\" ]; then\n # Check if current installation is embedded — downgrade to legacy is not supported\n if ${DOCKER_CMD} ps -a --format '{{.Names}} {{.Image}}' 2>/dev/null | grep \"^agentteams-controller \" | grep -q \"embedded\"; then\n error \"Downgrade from embedded architecture to legacy version (${AGENTTEAMS_VERSION}) is not supported.\"\n error \"Please use 'make uninstall-embedded' first, then do a clean install of the target version.\"\n exit 1\n fi\n fi\n\n if [ \"${AGENTTEAMS_USE_EMBEDDED}\" = \"1\" ]; then\n # ============================================================\n # New architecture: embedded controller + auto-created manager\n # ============================================================\n\n # Internal port: 8080 (Higress gateway inside the container).\n local _internal_gw_port=8080\n local _matrix_domain=\"${AGENTTEAMS_MATRIX_DOMAIN:-matrix-local.agentteams.io:${AGENTTEAMS_PORT_GATEWAY}}\"\n local _aigw_domain=\"${AGENTTEAMS_AI_GATEWAY_DOMAIN:-aigw-local.agentteams.io}\"\n # Ensure internal gateway port is present (container-internal traffic uses 8080)\n case \"${_aigw_domain}\" in *:*) ;; *) _aigw_domain=\"${_aigw_domain}:${_internal_gw_port}\" ;; esac\n local _fs_domain=\"${AGENTTEAMS_FS_DOMAIN:-fs-local.agentteams.io}\"\n case \"${_fs_domain}\" in *:*) ;; *) _fs_domain=\"${_fs_domain}:${_internal_gw_port}\" ;; esac\n\n # Controller env args\n local _ctrl_env_args=(\n -e \"AGENTTEAMS_ADMIN_USER=${AGENTTEAMS_ADMIN_USER}\"\n -e \"AGENTTEAMS_ADMIN_PASSWORD=${AGENTTEAMS_ADMIN_PASSWORD}\"\n -e \"AGENTTEAMS_MANAGER_PASSWORD=${AGENTTEAMS_MANAGER_PASSWORD}\"\n -e \"AGENTTEAMS_REGISTRATION_TOKEN=${AGENTTEAMS_REGISTRATION_TOKEN}\"\n -e \"AGENTTEAMS_MINIO_USER=${AGENTTEAMS_MINIO_USER}\"\n -e \"AGENTTEAMS_MINIO_PASSWORD=${AGENTTEAMS_MINIO_PASSWORD}\"\n -e \"AGENTTEAMS_LLM_PROVIDER=${AGENTTEAMS_LLM_PROVIDER}\"\n -e \"AGENTTEAMS_LLM_API_KEY=${AGENTTEAMS_LLM_API_KEY}\"\n -e \"AGENTTEAMS_DEFAULT_MODEL=${AGENTTEAMS_DEFAULT_MODEL}\"\n -e \"AGENTTEAMS_MANAGER_GATEWAY_KEY=${AGENTTEAMS_MANAGER_GATEWAY_KEY}\"\n -e \"AGENTTEAMS_MANAGER_RUNTIME=${AGENTTEAMS_MANAGER_RUNTIME:-copaw}\"\n -e \"AGENTTEAMS_MANAGER_IMAGE=$([ \"${AGENTTEAMS_MANAGER_RUNTIME}\" = \"copaw\" ] && echo \"${MANAGER_COPAW_IMAGE}\" || echo \"${MANAGER_IMAGE}\")\"\n -e \"AGENTTEAMS_DEFAULT_WORKER_RUNTIME=${AGENTTEAMS_DEFAULT_WORKER_RUNTIME:-copaw}\"\n -e \"AGENTTEAMS_WORKER_IMAGE=${WORKER_IMAGE}\"\n -e \"AGENTTEAMS_COPAW_WORKER_IMAGE=${COPAW_WORKER_IMAGE}\"\n -e \"AGENTTEAMS_HERMES_WORKER_IMAGE=${HERMES_WORKER_IMAGE}\"\n -e \"AGENTTEAMS_MATRIX_DOMAIN=${_matrix_domain}\"\n -e \"AGENTTEAMS_ELEMENT_HOMESERVER_URL=http://127.0.0.1:${AGENTTEAMS_PORT_GATEWAY}\"\n -e \"AGENTTEAMS_MATRIX_URL=http://127.0.0.1:6167\"\n -e \"AGENTTEAMS_MATRIX_E2EE=${AGENTTEAMS_MATRIX_E2EE:-0}\"\n -e \"AGENTTEAMS_MATRIX_APPSERVICE_ENABLED=${AGENTTEAMS_MATRIX_APPSERVICE_ENABLED:-true}\"\n -e \"AGENTTEAMS_MATRIX_APPSERVICE_AS_TOKEN=${AGENTTEAMS_MATRIX_APPSERVICE_AS_TOKEN:-}\"\n -e \"AGENTTEAMS_MATRIX_APPSERVICE_HS_TOKEN=${AGENTTEAMS_MATRIX_APPSERVICE_HS_TOKEN:-}\"\n -e \"AGENTTEAMS_MINIO_ENDPOINT=http://127.0.0.1:9000\"\n -e \"AGENTTEAMS_MINIO_BUCKET=agentteams-storage\"\n -e \"AGENTTEAMS_STORAGE_PREFIX=agentteams/agentteams-storage\"\n -e \"AGENTTEAMS_FS_ENDPOINT=http://127.0.0.1:9000\"\n -e \"AGENTTEAMS_AI_GATEWAY_URL=http://${_aigw_domain}\"\n -e \"AGENTTEAMS_CONTROLLER_URL=http://agentteams-controller:8090\"\n -e \"AGENTTEAMS_DOCKER_NETWORK=agentteams-net\"\n -e \"AGENTTEAMS_WORKSPACE_DIR=${AGENTTEAMS_WORKSPACE_DIR}\"\n -e \"AGENTTEAMS_HOST_SHARE_DIR=${AGENTTEAMS_HOST_SHARE_DIR}\"\n -e \"AGENTTEAMS_MANAGER_ENABLED=true\"\n -e \"AGENTTEAMS_PORT_MANAGER_CONSOLE=${AGENTTEAMS_PORT_MANAGER_CONSOLE:-18888}\"\n )\n\n # Timezone\n if [ -n \"${AGENTTEAMS_TIMEZONE:-}\" ]; then\n _ctrl_env_args+=(-e \"TZ=${AGENTTEAMS_TIMEZONE}\")\n fi\n\n # Yolo mode\n if [ \"${AGENTTEAMS_YOLO:-}\" = \"1\" ]; then\n _ctrl_env_args+=(-e \"AGENTTEAMS_YOLO=1\")\n fi\n\n # Matrix-plugin debug tracing — propagated to every manager + worker\n # the controller spawns, then translated to OPENCLAW_MATRIX_DEBUG=1\n # by the container entrypoints. Use this to diagnose\n # \"worker did not join\" / \"manager replied empty\" hangs.\n if [ \"${AGENTTEAMS_MATRIX_DEBUG:-}\" = \"1\" ]; then\n _ctrl_env_args+=(-e \"AGENTTEAMS_MATRIX_DEBUG=1\")\n fi\n\n # Optional: GitHub token\n if [ -n \"${AGENTTEAMS_GITHUB_TOKEN:-}\" ]; then\n _ctrl_env_args+=(-e \"AGENTTEAMS_GITHUB_TOKEN=${AGENTTEAMS_GITHUB_TOKEN}\")\n fi\n\n # Optional: embedding model\n if [ -n \"${AGENTTEAMS_EMBEDDING_MODEL:-}\" ]; then\n _ctrl_env_args+=(-e \"AGENTTEAMS_EMBEDDING_MODEL=${AGENTTEAMS_EMBEDDING_MODEL}\")\n fi\n\n # Optional: OpenAI-compatible base URL\n if [ -n \"${AGENTTEAMS_OPENAI_BASE_URL:-}\" ]; then\n _ctrl_env_args+=(-e \"AGENTTEAMS_OPENAI_BASE_URL=${AGENTTEAMS_OPENAI_BASE_URL}\")\n fi\n # Optional: language\n if [ -n \"${AGENTTEAMS_LANGUAGE:-}\" ]; then\n _ctrl_env_args+=(-e \"AGENTTEAMS_LANGUAGE=${AGENTTEAMS_LANGUAGE}\")\n fi\n\n # Optional: CMS/ARMS observability. In embedded mode the controller\n # spawns the Manager and Workers, so it must receive these settings.\n _ctrl_env_args+=(-e \"AGENTTEAMS_CMS_TRACES_ENABLED=${AGENTTEAMS_CMS_TRACES_ENABLED:-false}\")\n _ctrl_env_args+=(-e \"AGENTTEAMS_CMS_SERVICE_NAME=${AGENTTEAMS_CMS_SERVICE_NAME:-agentteams-manager}\")\n _ctrl_env_args+=(-e \"AGENTTEAMS_CMS_METRICS_ENABLED=${AGENTTEAMS_CMS_METRICS_ENABLED:-false}\")\n if [ -n \"${AGENTTEAMS_CMS_ENDPOINT:-}\" ]; then\n _ctrl_env_args+=(-e \"AGENTTEAMS_CMS_ENDPOINT=${AGENTTEAMS_CMS_ENDPOINT}\")\n fi\n if [ -n \"${AGENTTEAMS_CMS_LICENSE_KEY:-}\" ]; then\n _ctrl_env_args+=(-e \"AGENTTEAMS_CMS_LICENSE_KEY=${AGENTTEAMS_CMS_LICENSE_KEY}\")\n fi\n if [ -n \"${AGENTTEAMS_CMS_PROJECT:-}\" ]; then\n _ctrl_env_args+=(-e \"AGENTTEAMS_CMS_PROJECT=${AGENTTEAMS_CMS_PROJECT}\")\n fi\n if [ -n \"${AGENTTEAMS_CMS_WORKSPACE:-}\" ]; then\n _ctrl_env_args+=(-e \"AGENTTEAMS_CMS_WORKSPACE=${AGENTTEAMS_CMS_WORKSPACE}\")\n fi\n\n # shellcheck disable=SC2086\n ${DOCKER_CMD} run -d \\\n --name agentteams-controller \\\n --network agentteams-net \\\n --network-alias matrix-local.agentteams.io \\\n --network-alias aigw-local.agentteams.io \\\n --network-alias fs-local.agentteams.io \\\n \"${_ctrl_env_args[@]}\" \\\n -v \"${CONTAINER_SOCK}:/var/run/docker.sock\" \\\n --security-opt label=disable \\\n -v \"${AGENTTEAMS_DATA_DIR}:/data\" \\\n -v \"${AGENTTEAMS_WORKSPACE_DIR}:/root/agentteams-fs/agents/manager\" \\\n -p \"${_port_prefix}${AGENTTEAMS_PORT_GATEWAY}:8080\" \\\n -p \"${_port_prefix}${AGENTTEAMS_PORT_CONSOLE}:8001\" \\\n -p \"${_port_prefix}${AGENTTEAMS_PORT_ELEMENT_WEB:-18088}:8088\" \\\n --restart unless-stopped \\\n \"${EMBEDDED_IMAGE}\"\n\n log \"Embedded controller started: agentteams-controller\"\n\n # Wait for infrastructure inside the controller container\n _wait_for_url() {\n local url=\"$1\" ctr=\"$2\" max_wait=\"${3:-120}\" desc=\"${4:-service}\"\n local elapsed=0\n log \"Waiting for ${desc}...\"\n while [ $elapsed -lt $max_wait ]; do\n if ${DOCKER_CMD} exec \"${ctr}\" curl -sf \"${url}\" >/dev/null 2>&1; then\n log \"${desc} is ready (${elapsed}s)\"\n return 0\n fi\n sleep 2\n elapsed=$((elapsed + 2))\n done\n log \"ERROR: ${desc} not ready after ${max_wait}s\"\n return 1\n }\n\n _wait_for_url \"http://127.0.0.1:6167/_tuwunel/server_version\" agentteams-controller 120 \"Tuwunel (Matrix)\" || exit 1\n _wait_for_url \"http://127.0.0.1:9000/minio/health/live\" agentteams-controller 60 \"MinIO\" || exit 1\n _wait_for_url \"http://127.0.0.1:8080/status\" agentteams-controller 120 \"Higress Gateway\" || exit 1\n\n # Wait for controller to create Manager Agent container\n log \"Waiting for Manager Agent container...\"\n local _mgr_wait=0\n local _mgr_max=300\n while [ $_mgr_wait -lt $_mgr_max ]; do\n if ${DOCKER_CMD} ps --format '{{.Names}}' 2>/dev/null | grep -q \"^agentteams-manager$\"; then\n log \"Manager Agent container detected (${_mgr_wait}s)\"\n break\n fi\n sleep 3\n _mgr_wait=$((_mgr_wait + 3))\n done\n if [ $_mgr_wait -ge $_mgr_max ]; then\n log \"ERROR: Manager Agent container not created after ${_mgr_max}s\"\n log \"Controller logs:\"\n ${DOCKER_CMD} exec agentteams-controller tail -30 /var/log/agentteams/agentteams-controller-error.log 2>/dev/null || true\n exit 1\n fi\n\n # Wait for Manager Agent to be running\n log \"Waiting for Manager Agent to start...\"\n local _agent_wait=0\n while [ $_agent_wait -lt 120 ]; do\n local _state\n _state=$(${DOCKER_CMD} inspect --format '{{.State.Status}}' agentteams-manager 2>/dev/null || echo \"missing\")\n if [ \"${_state}\" = \"running\" ]; then\n log \"Manager Agent is running\"\n break\n fi\n sleep 2\n _agent_wait=$((_agent_wait + 2))\n done\n\n # Enable yolo mode in agent if requested\n if [ \"${AGENTTEAMS_YOLO:-}\" = \"1\" ]; then\n ${DOCKER_CMD} exec agentteams-manager touch /root/manager-workspace/yolo-mode 2>/dev/null || true\n fi\n\n # Wait for the controller to send the first-boot welcome message.\n # The controller gates this on (a) Manager joining the DM room and\n # (b) Higress WASM key-auth propagation actually clearing /v1/chat/completions\n # for the Manager's gateway key — typically ~45-90s on a fresh install.\n # We poll Manager CR Status.WelcomeSent via the in-container AgentTeams CLI,\n # exec'd inside agentteams-controller (the source-of-truth container — its\n # bundled CLI binary is always in lockstep with whatever controller\n # binary is currently serving the HTTP API, since they're the same\n # `go build` output. The agentteams-manager container's CLI may lag the\n # controller across image upgrades and silently drop the welcomeSent\n # field, leaving this loop hung). The controller container mints a\n # long-lived admin SA token at startup and writes it to\n # AGENTTEAMS_AUTH_TOKEN_FILE=/var/run/agentteams/cli-token (set as a Dockerfile\n # ENV default), so a bare `docker exec agentteams-controller agt …`\n # auto-discovers both the endpoint and the token. There is a brief\n # window after container start before bootstrapAdminCLIToken completes\n # where the file may be empty / absent — the loop's silent retry\n # handles that the same way it handles the manager-not-yet-running\n # case below.\n if ${DOCKER_CMD} exec agentteams-controller sh -c 'command -v agt' >/dev/null 2>&1; then\n log \"$(msg install.welcome_msg.waiting)\"\n local _welcome_wait=0\n local _welcome_max=\"${AGENTTEAMS_WELCOME_TIMEOUT:-300}\"\n local _welcome_done=0\n while [ $_welcome_wait -lt $_welcome_max ]; do\n # `tr -d` strips whitespace/CR so the grep stays robust to\n # any future change in go-json field ordering or formatting.\n local _wjson\n _wjson=$(${DOCKER_CMD} exec agentteams-controller \\\n agt get managers default -o json 2>/dev/null || true)\n if [ -n \"${_wjson}\" ] && printf '%s' \"${_wjson}\" | tr -d ' \\r\\n' | grep -q '\"welcomeSent\":true'; then\n log \"$(msg install.welcome_msg.confirmed \"${_welcome_wait}\")\"\n _welcome_done=1\n break\n fi\n sleep 3\n _welcome_wait=$((_welcome_wait + 3))\n done\n if [ $_welcome_done -ne 1 ]; then\n # Non-fatal: install is still good. Keep going to the success\n # banner so the admin can use Element Web to nudge Manager into\n # onboarding manually (one DM message is enough).\n log \"$(msg install.welcome_msg.timeout \"${_welcome_max}\")\"\n log \"$(msg install.welcome_msg.timeout_hint)\"\n log \"$(msg install.welcome_msg.timeout_inspect)\"\n fi\n else\n log \"$(msg install.welcome_msg.poll_unavailable)\"\n fi\n\n else\n # ============================================================\n # Legacy architecture: all-in-one manager container\n # ============================================================\n\n NETWORK_ARGS=\"--network agentteams-net\"\n NETWORK_ALIAS_ARGS=\"--network-alias matrix-local.agentteams.io --network-alias aigw-local.agentteams.io --network-alias fs-local.agentteams.io\"\n for _domain in \"${AGENTTEAMS_MATRIX_CLIENT_DOMAIN:-}\" \"${AGENTTEAMS_CONSOLE_DOMAIN:-}\"; do\n if [ -n \"${_domain}\" ] && [[ \"${_domain}\" == *-local.agentteams.io ]]; then\n NETWORK_ALIAS_ARGS=\"${NETWORK_ALIAS_ARGS} --network-alias ${_domain}\"\n fi\n done\n\n # Start Docker API proxy if enabled (security layer between Manager and Docker daemon)\n PROXY_ARGS=\"\"\n if [ \"${AGENTTEAMS_DOCKER_PROXY:-1}\" = \"1\" ] && [ -n \"${CONTAINER_SOCK:-}\" ]; then\n local _proxy_image=\"${AGENTTEAMS_REGISTRY}/higress/agentteams-docker-proxy:${AGENTTEAMS_VERSION}\"\n # Try versioned tag, fallback to latest\n if ! ${DOCKER_CMD} image inspect \"${_proxy_image}\" >/dev/null 2>&1; then\n ${DOCKER_CMD} pull \"${_proxy_image}\" 2>/dev/null || {\n _proxy_image=\"${AGENTTEAMS_REGISTRY}/higress/agentteams-docker-proxy:latest\"\n ${DOCKER_CMD} pull \"${_proxy_image}\" 2>/dev/null || true\n }\n fi\n if ${DOCKER_CMD} image inspect \"${_proxy_image}\" >/dev/null 2>&1; then\n log \"Starting Docker API proxy...\"\n ${DOCKER_CMD} run -d \\\n --name agentteams-docker-proxy \\\n --network agentteams-net \\\n -v \"${CONTAINER_SOCK}:/var/run/docker.sock\" \\\n --security-opt label=disable \\\n -e AGENTTEAMS_WORKER_IMAGE=\"${WORKER_IMAGE}\" \\\n -e AGENTTEAMS_COPAW_WORKER_IMAGE=\"${COPAW_WORKER_IMAGE}\" \\\n -e AGENTTEAMS_HERMES_WORKER_IMAGE=\"${HERMES_WORKER_IMAGE}\" \\\n ${AGENTTEAMS_PROXY_ALLOWED_REGISTRIES:+-e AGENTTEAMS_PROXY_ALLOWED_REGISTRIES=\"${AGENTTEAMS_PROXY_ALLOWED_REGISTRIES}\"} \\\n --restart unless-stopped \\\n \"${_proxy_image}\"\n PROXY_ARGS=\"-e AGENTTEAMS_CONTROLLER_URL=http://agentteams-docker-proxy:2375 -e AGENTTEAMS_CONTAINER_API=http://agentteams-docker-proxy:2375\"\n SOCKET_MOUNT_ARGS=\"\"\n fi\n fi\n\n # Pass host timezone to container\n TZ_ARGS=\"\"\n if [ -n \"${AGENTTEAMS_TIMEZONE:-}\" ]; then\n TZ_ARGS=\"-e TZ=${AGENTTEAMS_TIMEZONE}\"\n fi\n\n YOLO_ARGS=\"\"\n if [ \"${AGENTTEAMS_YOLO:-}\" = \"1\" ]; then\n YOLO_ARGS=\"-e AGENTTEAMS_YOLO=1\"\n fi\n\n MATRIX_DEBUG_ARGS=\"\"\n if [ \"${AGENTTEAMS_MATRIX_DEBUG:-}\" = \"1\" ]; then\n MATRIX_DEBUG_ARGS=\"-e AGENTTEAMS_MATRIX_DEBUG=1\"\n fi\n\n # shellcheck disable=SC2086\n ${DOCKER_CMD} run -d \\\n --name agentteams-manager \\\n --env-file \"${ENV_FILE}\" \\\n -e HOME=/root/manager-workspace \\\n -w /root/manager-workspace \\\n -e HOST_ORIGINAL_HOME=\"${AGENTTEAMS_HOST_SHARE_DIR}\" \\\n -e AGENTTEAMS_MANAGER_RUNTIME=\"${AGENTTEAMS_MANAGER_RUNTIME:-copaw}\" \\\n ${JVM_ARGS:+-e JVM_ARGS=\"${JVM_ARGS}\"} \\\n ${YOLO_ARGS} \\\n ${MATRIX_DEBUG_ARGS} \\\n ${TZ_ARGS} \\\n ${SOCKET_MOUNT_ARGS} \\\n ${NETWORK_ARGS} \\\n ${NETWORK_ALIAS_ARGS} \\\n ${PROXY_ARGS} \\\n -p \"${_port_prefix}${AGENTTEAMS_PORT_GATEWAY}:8080\" \\\n -p \"${_port_prefix}${AGENTTEAMS_PORT_CONSOLE}:8001\" \\\n -p \"${_port_prefix}${AGENTTEAMS_PORT_ELEMENT_WEB:-18088}:8088\" \\\n -p \"127.0.0.1:${AGENTTEAMS_PORT_MANAGER_CONSOLE:-18888}:18888\" \\\n ${DATA_MOUNT_ARGS} \\\n ${WORKSPACE_MOUNT_ARGS} \\\n ${HOST_SHARE_MOUNT_ARGS} \\\n --restart unless-stopped \\\n \"$([ \"${AGENTTEAMS_MANAGER_RUNTIME}\" = \"copaw\" ] && echo \"${MANAGER_COPAW_IMAGE}\" || echo \"${MANAGER_IMAGE}\")\"\n\n # Wait for Manager agent to be ready\n wait_manager_ready \"agentteams-manager\"\n\n # Wait for Matrix server to be ready\n wait_matrix_ready \"agentteams-manager\"\n\n # Post-install verification (non-fatal: warnings only)\n local _verify_script\n _verify_script=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)/agentteams-verify.sh\"\n if [ -f \"${_verify_script}\" ]; then\n bash \"${_verify_script}\" \"agentteams-manager\" || {\n log \"WARNING: Some post-install checks failed. Re-run: bash install/agentteams-verify.sh\"\n }\n fi\n fi\n unset _port_prefix\n\n # Apply Podman autostart if selected and environment matches\n if [ \"${DOCKER_CMD}\" = \"podman\" ] && [ \"${AGENTTEAMS_PODMAN_AUTOSTART:-0}\" = \"1\" ]; then\n setup_podman_autostart\n fi\n\n log \"\"\n log \"$(msg success.title)\"\n log \"\"\n log \"$(msg success.domains_configured)\"\n log \" ${AGENTTEAMS_MATRIX_DOMAIN%%:*} ${AGENTTEAMS_MATRIX_CLIENT_DOMAIN} ${AGENTTEAMS_AI_GATEWAY_DOMAIN} ${AGENTTEAMS_FS_DOMAIN} ${AGENTTEAMS_CONSOLE_DOMAIN}\"\n log \"\"\n local lan_ip\n lan_ip=$(detect_lan_ip)\n echo -e \"\\033[33m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\033[0m\"\n echo -e \"\\033[33m $(msg success.open_url)\\033[0m\"\n echo -e \"\\033[33m \\033[0m\"\n echo -e \"\\033[1;36m http://127.0.0.1:${AGENTTEAMS_PORT_ELEMENT_WEB:-18088}/#/login\\033[0m\"\n echo -e \"\\033[33m \\033[0m\"\n echo -e \"\\033[33m $(msg success.login_with)\\033[0m\"\n echo -e \"\\033[33m $(msg success.username \"${AGENTTEAMS_ADMIN_USER}\")\\033[0m\"\n echo -e \"\\033[33m $(msg success.password \"${AGENTTEAMS_ADMIN_PASSWORD}\")\\033[0m\"\n echo -e \"\\033[33m \\033[0m\"\n echo -e \"\\033[33m $(msg success.after_login)\\033[0m\"\n echo -e \"\\033[33m $(msg success.tell_it)\\033[0m\"\n echo -e \"\\033[33m $(msg success.manager_auto)\\033[0m\"\n echo -e \"\\033[33m \\033[0m\"\n echo -e \"\\033[33m ───────────────────────────────────────────────────────────────────────────── \\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_title)\\033[0m\"\n echo -e \"\\033[33m \\033[0m\"\n if [ -n \"${lan_ip}\" ]; then\n echo -e \"\\033[33m $(msg success.mobile_step1)\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_step2 \"http://${lan_ip}:${AGENTTEAMS_PORT_GATEWAY}\")\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_step3)\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_username \"${AGENTTEAMS_ADMIN_USER}\")\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_password \"${AGENTTEAMS_ADMIN_PASSWORD}\")\\033[0m\"\n else\n echo -e \"\\033[33m $(msg success.mobile_step1)\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_step2_noip \"${AGENTTEAMS_PORT_GATEWAY}\")\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_noip_hint)\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_step3)\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_username \"${AGENTTEAMS_ADMIN_USER}\")\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_password \"${AGENTTEAMS_ADMIN_PASSWORD}\")\\033[0m\"\n fi\n echo -e \"\\033[33m \\033[0m\"\n echo -e \"\\033[33m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\033[0m\"\n log \"\"\n log \"$(msg success.other_consoles)\"\n log \"$(msg success.higress_console \"${AGENTTEAMS_PORT_CONSOLE}\" \"${AGENTTEAMS_ADMIN_USER}\" \"${AGENTTEAMS_ADMIN_PASSWORD}\")\"\n if [ \"${AGENTTEAMS_USE_EMBEDDED}\" != \"1\" ]; then\n log \"$(msg success.manager_console \"${AGENTTEAMS_PORT_MANAGER_CONSOLE:-18888}\")\"\n log \"$(msg success.manager_console_gateway \"${AGENTTEAMS_ADMIN_USER}\" \"${AGENTTEAMS_ADMIN_PASSWORD}\")\"\n fi\n log \"\"\n log \"$(msg success.switch_llm.title)\"\n log \"$(msg success.switch_llm.hint)\"\n log \"$(msg success.switch_llm.docs)\"\n log \"$(msg success.switch_llm.url)\"\n log \"\"\n log \"$(msg success.tip)\"\n log \"\"\n if [ \"${AGENTTEAMS_LOCAL_ONLY:-1}\" != \"1\" ]; then\n echo -e \"\\033[33m$(msg port.local_only.https_hint)\\033[0m\"\n log \"\"\n fi\n log \"$(msg success.config_file \"${ENV_FILE}\")\"\n log \"$(msg success.data_volume \"${AGENTTEAMS_DATA_DIR}\")\"\n log \"$(msg success.workspace \"${AGENTTEAMS_WORKSPACE_DIR}\")\"\n}\n\n# ============================================================\n# Worker Installation (One-Click)\n# ============================================================\n\ninstall_worker() {\n local WORKER_NAME=\"\"\n local FS=\"\"\n local FS_KEY=\"\"\n local FS_SECRET=\"\"\n local RESET=false\n local SKILLS_API_URL=\"\"\n\n # Parse arguments\n while [ $# -gt 0 ]; do\n case $1 in\n --name) WORKER_NAME=\"$2\"; shift 2 ;;\n --fs) FS=\"$2\"; shift 2 ;;\n --fs-key) FS_KEY=\"$2\"; shift 2 ;;\n --fs-secret) FS_SECRET=\"$2\"; shift 2 ;;\n --skills-api-url) SKILLS_API_URL=\"$2\"; shift 2 ;;\n --reset) RESET=true; shift ;;\n *) die \"$(msg error.unknown_option \"$1\")\" ;;\n esac\n done\n\n # Validate required params\n [ -z \"${WORKER_NAME}\" ] && die \"$(msg error.name_required)\"\n [ -z \"${FS}\" ] && die \"$(msg error.fs_required)\"\n [ -z \"${FS_KEY}\" ] && die \"$(msg error.fs_key_required)\"\n [ -z \"${FS_SECRET}\" ] && die \"$(msg error.fs_secret_required)\"\n\n local CONTAINER_NAME=\"agentteams-worker-${WORKER_NAME}\"\n\n # Handle reset\n if [ \"${RESET}\" = true ]; then\n log \"$(msg worker.resetting \"${WORKER_NAME}\")\"\n ${DOCKER_CMD} stop \"${CONTAINER_NAME}\" 2>/dev/null || true\n ${DOCKER_CMD} rm \"${CONTAINER_NAME}\" 2>/dev/null || true\n fi\n\n # Check for existing container\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' | grep -q \"^${CONTAINER_NAME}$\"; then\n die \"$(msg worker.exists \"${CONTAINER_NAME}\")\"\n fi\n\n log \"$(msg worker.starting \"${WORKER_NAME}\")\"\n\n # Build docker run args\n local DOCKER_ENV=\"\"\n DOCKER_ENV=\"${DOCKER_ENV} -e HOME=/root/agentteams-fs/agents/${WORKER_NAME}\"\n DOCKER_ENV=\"${DOCKER_ENV} -w /root/agentteams-fs/agents/${WORKER_NAME}\"\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_WORKER_NAME=${WORKER_NAME}\"\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_FS_ENDPOINT=${FS}\"\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_FS_ACCESS_KEY=${FS_KEY}\"\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_FS_SECRET_KEY=${FS_SECRET}\"\n\n if [ -z \"${SKILLS_API_URL}\" ]; then\n if [ -n \"${AGENTTEAMS_SKILLS_API_URL:-}\" ]; then\n SKILLS_API_URL=\"${AGENTTEAMS_SKILLS_API_URL}\"\n else\n SKILLS_API_URL=\"nacos://market.agentteams.io:80/public\"\n fi\n fi\n\n # Add SKILLS_API_URL if specified\n DOCKER_ENV=\"${DOCKER_ENV} -e SKILLS_API_URL=${SKILLS_API_URL}\"\n log \"$(msg worker.skills_url \"${SKILLS_API_URL}\")\"\n if [ -n \"${AGENTTEAMS_NACOS_USERNAME:-}\" ]; then\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_NACOS_USERNAME=${AGENTTEAMS_NACOS_USERNAME}\"\n fi\n if [ -n \"${AGENTTEAMS_NACOS_PASSWORD:-}\" ]; then\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_NACOS_PASSWORD=${AGENTTEAMS_NACOS_PASSWORD}\"\n fi\n if [ -n \"${AGENTTEAMS_NACOS_TOKEN:-}\" ]; then\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_NACOS_TOKEN=${AGENTTEAMS_NACOS_TOKEN}\"\n fi\n\n # shellcheck disable=SC2086\n ${DOCKER_CMD} run -d \\\n --name \"${CONTAINER_NAME}\" \\\n ${DOCKER_ENV} \\\n --restart unless-stopped \\\n \"${WORKER_IMAGE}\"\n\n log \"\"\n log \"$(msg worker.started \"${WORKER_NAME}\")\"\n log \"$(msg worker.container \"${CONTAINER_NAME}\")\"\n log \"$(msg worker.view_logs \"${CONTAINER_NAME}\")\"\n}\n\n# ============================================================\n# Main\n# ============================================================\n\n# ============================================================\n# LLM API connectivity test\n# ============================================================\n\ntest_llm_connectivity() {\n local base_url=\"$1\"\n local api_key=\"$2\"\n local model=\"$3\"\n local hint=\"${4:-}\" # optional: extra hint shown on failure\n if ! command -v curl >/dev/null 2>&1; then\n echo -e \"\\033[33m$(msg llm.openai.test.no_curl)\\033[0m\"\n return\n fi\n log \"$(msg llm.openai.test.testing)\"\n local _body _http_code _tmpfile\n _tmpfile=$(mktemp)\n _http_code=$(curl -s -o \"${_tmpfile}\" -w \"%{http_code}\" \\\n -X POST \"${base_url%/}/chat/completions\" \\\n -H \"Authorization: Bearer ${api_key}\" \\\n -H \"Content-Type: application/json\" \\\n -H \"User-Agent: AgentTeams/${AGENTTEAMS_VERSION:-latest}\" \\\n --max-time 30 \\\n -d \"{\\\"model\\\":\\\"${model}\\\",\\\"messages\\\":[{\\\"role\\\":\\\"user\\\",\\\"content\\\":\\\"hi\\\"}]}\" \\\n 2>/dev/null)\n _body=$(cat \"${_tmpfile}\")\n rm -f \"${_tmpfile}\"\n if [ \"${_http_code}\" = \"200\" ] || [ \"${_http_code}\" = \"201\" ]; then\n log \"$(msg llm.openai.test.ok)\"\n else\n echo -e \"\\033[33m$(msg llm.openai.test.fail \"${_http_code}\" \"${_body}\")\\033[0m\"\n if [ -n \"${hint}\" ]; then\n echo -e \"\\033[33m${hint}\\033[0m\"\n fi\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" != \"1\" ]; then\n local _confirm\n read -e -p \"$(msg llm.openai.test.confirm)\" _confirm\n if [ \"${_confirm}\" = \"b\" ]; then\n STEP_RESULT=\"back\"\n return 1\n fi\n if [ \"${_confirm}\" != \"y\" ] && [ \"${_confirm}\" != \"Y\" ]; then\n log \"$(msg llm.openai.test.aborted)\"\n exit 1\n fi\n fi\n fi\n}\n\ntest_embedding_connectivity() {\n local base_url=\"$1\"\n local api_key=\"$2\"\n local model=\"$3\"\n if ! command -v curl >/dev/null 2>&1; then\n return 0\n fi\n log \"$(msg llm.embedding.test.testing)\"\n local _body _http_code _tmpfile\n _tmpfile=$(mktemp)\n _http_code=$(curl -s -o \"${_tmpfile}\" -w \"%{http_code}\" \\\n -X POST \"${base_url%/}/embeddings\" \\\n -H \"Authorization: Bearer ${api_key}\" \\\n -H \"Content-Type: application/json\" \\\n -H \"User-Agent: AgentTeams/${AGENTTEAMS_VERSION:-latest}\" \\\n --max-time 30 \\\n -d \"{\\\"model\\\":\\\"${model}\\\",\\\"input\\\":\\\"test\\\"}\" \\\n 2>/dev/null)\n _body=$(cat \"${_tmpfile}\")\n rm -f \"${_tmpfile}\"\n if [ \"${_http_code}\" = \"200\" ] || [ \"${_http_code}\" = \"201\" ]; then\n log \"$(msg llm.embedding.test.ok)\"\n return 0\n else\n echo -e \"\\033[33m$(msg llm.embedding.test.fail \"${_http_code}\" \"${_body}\")\\033[0m\"\n return 1\n fi\n}\n\n# ============================================================\n# Uninstall\n# ============================================================\n\nuninstall_agentteams() {\n log \"$(msg uninstall.title)\"\n\n # Safely disable and remove the dedicated AgentTeams Podman autostart service\n if [ \"${DOCKER_CMD}\" = \"podman\" ] && command -v systemctl >/dev/null 2>&1; then\n local _systemctl_cmd=\"systemctl\"\n local _service_dir=\"/etc/systemd/system\"\n\n if [ \"$(id -u)\" != \"0\" ]; then\n _systemctl_cmd=\"systemctl --user\"\n _service_dir=\"${HOME}/.config/systemd/user\"\n fi\n\n if [ -f \"${_service_dir}/agentteams-podman-restart.service\" ]; then\n log \"Disabling and removing dedicated AgentTeams Podman autostart service...\"\n ${_systemctl_cmd} disable --now agentteams-podman-restart.service 2>/dev/null || true\n rm -f \"${_service_dir}/agentteams-podman-restart.service\"\n ${_systemctl_cmd} daemon-reload 2>/dev/null || true\n fi\n if [ -f \"${_service_dir}/agentteams-podman-restart.service\" ]; then\n ${_systemctl_cmd} disable --now agentteams-podman-restart.service 2>/dev/null || true\n rm -f \"${_service_dir}/agentteams-podman-restart.service\"\n ${_systemctl_cmd} daemon-reload 2>/dev/null || true\n fi\n # Note: Native podman-restart.service is strictly untouched per maintainer review.\n fi\n\n # Capture manager image before removing (needed for workspace cleanup on Linux)\n local manager_image=\"\"\n manager_image=$(${DOCKER_CMD} inspect agentteams-manager --format '{{.Config.Image}}' 2>/dev/null || true)\n\n # Stop and remove manager\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -q \"^agentteams-manager$\"; then\n log \"$(msg uninstall.stopping_manager)\"\n ${DOCKER_CMD} stop agentteams-manager >/dev/null 2>&1 || true\n ${DOCKER_CMD} rm agentteams-manager >/dev/null 2>&1 || true\n fi\n\n # Stop and remove workers\n local workers\n workers=$(${DOCKER_CMD} ps -a --filter \"name=agentteams-worker-\" --format '{{.Names}}' 2>/dev/null || true)\n if [ -n \"${workers}\" ]; then\n log \"$(msg uninstall.stopping_workers)\"\n echo \"${workers}\" | while read -r w; do\n ${DOCKER_CMD} rm -f \"${w}\" >/dev/null 2>&1 || true\n log \"$(msg uninstall.removed \"${w}\")\"\n done\n fi\n\n # Stop and remove docker-proxy (legacy ≤ v1.0.x; current arch uses\n # agentteams-controller for the same role)\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -q \"^agentteams-docker-proxy$\"; then\n log \"$(msg uninstall.removing_proxy)\"\n ${DOCKER_CMD} stop agentteams-docker-proxy >/dev/null 2>&1 || true\n ${DOCKER_CMD} rm agentteams-docker-proxy >/dev/null 2>&1 || true\n fi\n\n # Stop and remove the embedded controller container. MUST happen\n # before the `docker volume rm agentteams-data` step below — in embedded\n # mode agentteams-controller mounts agentteams-data at /data (Tuwunel DB,\n # MinIO state, Higress state, all the room messages), and `volume\n # rm` against an in-use volume fails silently because of the trailing\n # `|| true`. Skipping this used to leave room/message history behind\n # across \"uninstall + reinstall\" cycles. See PR #692.\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -q \"^agentteams-controller$\"; then\n log \"$(msg uninstall.stopping_controller)\"\n ${DOCKER_CMD} stop agentteams-controller >/dev/null 2>&1 || true\n ${DOCKER_CMD} rm agentteams-controller >/dev/null 2>&1 || true\n fi\n\n # Read env file for data/workspace info before removing\n local env_file=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n [ ! -f \"${env_file}\" ] && [ -f \"./agentteams-manager.env\" ] && env_file=\"./agentteams-manager.env\"\n\n local data_dir=\"\" workspace_dir=\"\"\n if [ -f \"${env_file}\" ]; then\n data_dir=$(grep '^AGENTTEAMS_DATA_DIR=' \"${env_file}\" 2>/dev/null | cut -d= -f2- || true)\n workspace_dir=$(grep '^AGENTTEAMS_WORKSPACE_DIR=' \"${env_file}\" 2>/dev/null | cut -d= -f2- || true)\n fi\n\n # Remove Docker volume\n local vol=\"${data_dir:-agentteams-data}\"\n if ${DOCKER_CMD} volume inspect \"${vol}\" >/dev/null 2>&1; then\n log \"$(msg uninstall.removing_volume \"${vol}\")\"\n ${DOCKER_CMD} volume rm \"${vol}\" >/dev/null 2>&1 || true\n fi\n\n # Remove Docker network\n if ${DOCKER_CMD} network ls --format '{{.Name}}' 2>/dev/null | grep -q \"^agentteams-net$\"; then\n log \"$(msg uninstall.removing_network)\"\n ${DOCKER_CMD} network rm agentteams-net >/dev/null 2>&1 || true\n fi\n\n # Remove workspace directory\n if [ -n \"${workspace_dir}\" ] && [ -d \"${workspace_dir}\" ]; then\n log \"$(msg uninstall.removing_workspace \"${workspace_dir}\")\"\n rm -rf \"${workspace_dir}\" 2>/dev/null || true\n if [ -d \"${workspace_dir}\" ]; then\n log \"$(msg uninstall.removing_workspace_elevated)\"\n local _parent _base\n _parent=$(dirname \"${workspace_dir}\")\n _base=$(basename \"${workspace_dir}\")\n local _rm_image=\"${manager_image:-busybox}\"\n ${DOCKER_CMD} run --rm --entrypoint sh \\\n -v \"${_parent}:/host-parent\" \\\n \"${_rm_image}\" \\\n -c \"rm -rf /host-parent/${_base}\" 2>/dev/null || true\n fi\n fi\n\n # Remove env file\n if [ -f \"${env_file}\" ]; then\n log \"$(msg uninstall.removing_env \"${env_file}\")\"\n rm -f \"${env_file}\"\n fi\n\n # Remove install log\n if [ -f \"${HOME}/agentteams-install.log\" ]; then\n log \"$(msg uninstall.removing_log \"${HOME}/agentteams-install.log\")\"\n rm -f \"${HOME}/agentteams-install.log\"\n fi\n\n echo \"\"\n log \"$(msg uninstall.done)\"\n}\n\n# ============================================================\n# Check container runtime (docker or podman) and environment\n# ============================================================\ncheck_container_runtime() {\n # Ensure the rootless environment baseline (XDG_RUNTIME_DIR) is ready before engine detection.\n if [ \"$(id -u)\" -ne 0 ] && [ -z \"${XDG_RUNTIME_DIR:-}\" ]; then\n export XDG_RUNTIME_DIR=\"/run/user/$(id -u)\"\n fi\n\n if command -v docker >/dev/null 2>&1; then\n DOCKER_CMD=\"docker\"\n # Check for podman disguised as docker (alias or podman-docker package)\n if docker --version 2>/dev/null | grep -qi \"podman\"; then\n DOCKER_CMD=\"podman\"\n fi\n elif command -v podman >/dev/null 2>&1; then\n DOCKER_CMD=\"podman\"\n else\n echo -e \"\\033[31m[AgentTeams ERROR]\\033[0m $(msg error.docker_not_found)\" >&2\n exit 1\n fi\n\n # Unified health check (Validates runtime responsiveness)\n if ! ${DOCKER_CMD} ps >/dev/null 2>&1; then\n echo -e \"\\033[31m[AgentTeams ERROR]\\033[0m $(msg error.docker_not_running)\" >&2\n exit 1\n fi\n}\n\ncheck_container_runtime\n\ncase \"${1:-}\" in\n manager|\"\")\n install_manager\n ;;\n worker)\n shift\n install_worker \"$@\"\n ;;\n uninstall)\n uninstall_agentteams\n ;;\n *)\n echo \"Usage: $0 [manager|worker [options]|uninstall]\"\n echo \"\"\n echo \"Commands:\"\n echo \" manager Interactive Manager installation (default)\"\n echo \" Choose Quick Start (all defaults) or Manual mode\"\n echo \" worker Worker installation (requires --name and connection params)\"\n echo \" uninstall Stop and remove Manager + all Worker containers\"\n echo \"\"\n echo \"Quick Start (fastest):\"\n echo \" $0\"\n echo \" # Then select '1' for Quick Start mode\"\n echo \"\"\n echo \"Non-interactive (for automation):\"\n echo \" AGENTTEAMS_NON_INTERACTIVE=1 AGENTTEAMS_LLM_API_KEY=sk-xxx $0\"\n echo \"\"\n echo \"Worker Options:\"\n echo \" --name <name> Worker name (required)\"\n echo \" --fs <url> MinIO endpoint URL (required)\"\n echo \" --fs-key <key> MinIO access key (required)\"\n echo \" --fs-secret <secret> MinIO secret key (required)\"\n echo \" --reset Remove existing Worker container before creating\"\n exit 1\n ;;\nesac\n"}, "687b6d94ba04e894f5982150293c188158189d3e": {"t": 1785764752.3850355, "n": 223131, "h": "2daed5b4318b2631", "c": "#!/bin/bash\n# agentteams-install.sh - One-click installation for AgentTeams Manager and Worker\n#\n# Usage:\n# ./agentteams-install.sh # Interactive installation (choose Quick Start or Manual)\n# ./agentteams-install.sh manager # Same as above (explicit)\n# ./agentteams-install.sh worker --name <name> ... # Worker installation\n# ./agentteams-install.sh uninstall # Stop and remove Manager + all Workers\n#\n# Onboarding Modes:\n# Quick Start - Fast installation with all default values (recommended)\n# Manual - Customize each option step by step\n#\n# Environment variables (for automation):\n# AGENTTEAMS_NON_INTERACTIVE Skip all prompts, use defaults (default: 0)\n# AGENTTEAMS_LLM_PROVIDER LLM provider (default: openai-compat for zh non-interactive Token Plan; qwen for en)\n# AGENTTEAMS_DEFAULT_MODEL Default model (default: qwen3.6-plus for zh Token Plan and en non-interactive)\n# AGENTTEAMS_OPENAI_BASE_URL OpenAI-compatible base URL (default for zh non-interactive: Alibaba Token Plan endpoint)\n# AGENTTEAMS_LLM_API_KEY LLM API key (required)\n# AGENTTEAMS_ADMIN_USER Admin username (default: admin)\n# AGENTTEAMS_ADMIN_PASSWORD Admin password (auto-generated if not set, min 8 chars)\n# AGENTTEAMS_MATRIX_DOMAIN Matrix domain (default: matrix-local.agentteams.io:18080)\n# AGENTTEAMS_MOUNT_SOCKET Mount container runtime socket (default: 1)\n# AGENTTEAMS_DATA_DIR Docker volume name for persistent data (default: agentteams-data)\n# AGENTTEAMS_WORKSPACE_DIR Host directory for manager workspace (default: ~/agentteams-manager)\n# AGENTTEAMS_VERSION Image tag (default: latest)\n# AGENTTEAMS_REGISTRY Image registry (default: auto-detected by timezone)\n# AGENTTEAMS_INSTALL_MANAGER_IMAGE Override manager image (e.g., local build)\n# AGENTTEAMS_INSTALL_WORKER_IMAGE Override worker image (e.g., local build)\n# AGENTTEAMS_INSTALL_COPAW_WORKER_IMAGE Override copaw worker image (e.g., local build)\n# AGENTTEAMS_INSTALL_HERMES_WORKER_IMAGE Override hermes worker image (e.g., local build)\n# AGENTTEAMS_NACOS_REGISTRY_URI Default Nacos registry URI for Worker market search/import\n# (default: nacos://market.agentteams.io:80/public)\n# AGENTTEAMS_NACOS_USERNAME Default Nacos username for nacos:// package imports (optional)\n# AGENTTEAMS_NACOS_PASSWORD Default Nacos password for nacos:// package imports (optional)\n# AGENTTEAMS_CMS_TRACES_ENABLED Enable openclaw-cms-plugin traces for Manager AND all Workers (default: false)\n# AGENTTEAMS_CMS_ENDPOINT ARMS OTLP endpoint (required if traces enabled)\n# AGENTTEAMS_CMS_LICENSE_KEY CMS license key (required if traces enabled)\n# AGENTTEAMS_CMS_PROJECT CMS project name (optional)\n# AGENTTEAMS_CMS_WORKSPACE CMS workspace ID (required if traces enabled)\n# AGENTTEAMS_CMS_SERVICE_NAME Manager service name in ARMS (default: agentteams-manager)\n# Workers always report as agentteams-worker-<name> automatically\n# AGENTTEAMS_CMS_METRICS_ENABLED Enable diagnostics-otel metrics for Manager AND all Workers (default: false)\n# AGENTTEAMS_PORT_GATEWAY Host port for Higress gateway (default: 18080)\n# AGENTTEAMS_PORT_CONSOLE Host port for Higress console (default: 18001)\n# AGENTTEAMS_PORT_ELEMENT_WEB Host port for Element Web direct access (default: 18088)\n# AGENTTEAMS_PORT_MANAGER_CONSOLE Host port for Manager console (default: 18888)\n# AGENTTEAMS_WORKER_IDLE_TIMEOUT Worker idle timeout in minutes (default: 720, i.e. 12 hours)\n\nset -e\n\nAGENTTEAMS_VERSION=\"${AGENTTEAMS_VERSION:-}\"\nAGENTTEAMS_KNOWN_STABLE_VERSION=\"v1.1.2\" # fallback if GitHub API is unreachable\n\n# Returns 0 (true) if $1 < $2 using semver order; \"latest\" is treated as greatest\n_ver_lt() {\n [ \"$1\" = \"latest\" ] && return 1\n [ \"$2\" = \"latest\" ] && return 0\n [ \"$1\" = \"$2\" ] && return 1\n [ \"$(printf '%s\\n%s' \"$1\" \"$2\" | sort -V | head -1)\" = \"$1\" ]\n}\nAGENTTEAMS_NON_INTERACTIVE=\"${AGENTTEAMS_NON_INTERACTIVE:-0}\"\nAGENTTEAMS_MOUNT_SOCKET=\"${AGENTTEAMS_MOUNT_SOCKET:-1}\"\nAGENTTEAMS_DOCKER_PROXY=\"${AGENTTEAMS_DOCKER_PROXY:-1}\"\nSTEP_RESULT=\"\" # Used by state machine to signal \"back\" navigation\n\n# ============================================================\n# Log all output to file\n# ============================================================\n\nAGENTTEAMS_LOG_FILE=\"${HOME}/agentteams-install.log\"\n\nif [ \"${1:-}\" != \"uninstall\" ]; then\n # Redirect all output (stdout and stderr) to both terminal and log file\n exec > >(tee -a \"${AGENTTEAMS_LOG_FILE}\") 2>&1\n\n echo \"\"\n echo \"========================================\"\n echo \"AgentTeams Installation Log\"\n echo \"Started: $(date)\"\n echo \"User: $(whoami)\"\n echo \"System: $(uname -a)\"\n echo \"Log file: ${AGENTTEAMS_LOG_FILE}\"\n echo \"========================================\"\n echo \"\"\nfi\n\n# ============================================================\n# Utility functions (needed early for timezone detection)\n# ============================================================\n\nlog() {\n echo -e \"\\033[36m[AgentTeams]\\033[0m $1\"\n}\n\nerror() {\n echo -e \"\\033[31m[AgentTeams ERROR]\\033[0m $1\" >&2\n}\n\ndie() {\n error \"$1\"\n exit 1\n}\n\n# ============================================================\n# Timezone detection (compatible with Linux and macOS)\n# ============================================================\n\ndetect_timezone() {\n local tz=\"\"\n\n # Try /etc/timezone (Debian/Ubuntu)\n if [ -f /etc/timezone ]; then\n tz=$(cat /etc/timezone 2>/dev/null | tr -d '[:space:]')\n fi\n\n # Try /etc/localtime symlink (macOS and some Linux)\n if [ -z \"${tz}\" ] && [ -L /etc/localtime ]; then\n tz=$(ls -l /etc/localtime 2>/dev/null | sed 's|.*/zoneinfo/||')\n fi\n\n # Try timedatectl (systemd)\n if [ -z \"${tz}\" ]; then\n tz=$(timedatectl show --value -p Timezone 2>/dev/null)\n fi\n\n # If still not detected, warn and prompt user. Diagnostics go to stderr —\n # this function returns its value via stdout to `$(detect_timezone)`, so\n # any echo here would leak into AGENTTEAMS_TIMEZONE and downstream image tags.\n if [ -z \"${tz}\" ]; then\n echo \"\" >&2\n echo -e \"\\033[33m[AgentTeams WARNING]\\033[0m Could not detect timezone automatically.\" >&2\n echo -e \"\\033[33m[AgentTeams]\\033[0m Please enter your timezone (e.g., Asia/Shanghai, America/New_York).\" >&2\n echo \"\" >&2\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n tz=\"Asia/Shanghai\"\n log \"Using default timezone: ${tz}\" >&2\n else\n read -e -p \"Timezone [Asia/Shanghai]: \" tz\n tz=\"${tz:-Asia/Shanghai}\"\n fi\n fi\n\n echo \"${tz}\"\n}\n\n# Detect timezone once at startup (used by registry selection and container TZ)\nAGENTTEAMS_TIMEZONE=\"${AGENTTEAMS_TIMEZONE:-$(detect_timezone)}\"\n\n# ============================================================\n# Language detection based on timezone\n# ============================================================\n\ndetect_language() {\n local tz=\"${AGENTTEAMS_TIMEZONE}\"\n case \"${tz}\" in\n Asia/Shanghai|Asia/Chongqing|Asia/Harbin|Asia/Urumqi|\\\n Asia/Taipei|Asia/Hong_Kong|Asia/Macau)\n echo \"zh\"\n ;;\n *)\n echo \"en\"\n ;;\n esac\n}\n\n# Language priority: env var > existing env file > timezone detection\nif [ -z \"${AGENTTEAMS_LANGUAGE}\" ]; then\n # Check existing env file for saved language preference (upgrade scenario)\n _env_file=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n # Migrate from legacy location (current directory) if needed\n if [ ! -f \"${_env_file}\" ] && [ -f \"./agentteams-manager.env\" ]; then\n mv \"./agentteams-manager.env\" \"${_env_file}\" 2>/dev/null || true\n fi\n if [ -f \"${_env_file}\" ]; then\n _saved_lang=$(grep '^AGENTTEAMS_LANGUAGE=' \"${_env_file}\" 2>/dev/null | cut -d= -f2-)\n if [ -n \"${_saved_lang}\" ]; then\n AGENTTEAMS_LANGUAGE=\"${_saved_lang}\"\n fi\n fi\n # Fall back to timezone-based detection\n if [ -z \"${AGENTTEAMS_LANGUAGE}\" ]; then\n AGENTTEAMS_LANGUAGE=\"$(detect_language)\"\n fi\n unset _env_file _saved_lang\nfi\nexport AGENTTEAMS_LANGUAGE\n\n# ============================================================\n# Centralized message dictionary and msg() function\n# Compatible with bash 3.2+ (macOS default) — uses case instead of declare -A\n# ============================================================\n\n# msg() function: look up message by key, with printf-style argument substitution\n# Falls back to English if the current language translation is missing.\nmsg() {\n local key=\"$1\"\n shift\n local lang=\"${AGENTTEAMS_LANGUAGE:-en}\"\n local text=\"\"\n case \"${key}.${lang}\" in\n # --- Timezone detection messages ---\n \"tz.warning.title.zh\") text=\"无法自动检测时区。\" ;;\n \"tz.warning.title.en\") text=\"Could not detect timezone automatically.\" ;;\n \"tz.warning.prompt.zh\") text=\"请输入您的时区(例如 Asia/Shanghai、America/New_York)。\" ;;\n \"tz.warning.prompt.en\") text=\"Please enter your timezone (e.g., Asia/Shanghai, America/New_York).\" ;;\n \"tz.default.zh\") text=\"使用默认时区: %s\" ;;\n \"tz.default.en\") text=\"Using default timezone: %s\" ;;\n \"tz.input_prompt.zh\") text=\"时区\" ;;\n \"tz.input_prompt.en\") text=\"Timezone\" ;;\n # --- Installation title and info ---\n \"install.title.zh\") text=\"=== AgentTeams Manager 安装 ===\" ;;\n \"install.title.en\") text=\"=== AgentTeams Manager Installation ===\" ;;\n \"install.registry.zh\") text=\"镜像仓库: %s\" ;;\n \"install.registry.en\") text=\"Registry: %s\" ;;\n \"install.dir.zh\") text=\"安装目录: %s\" ;;\n \"install.dir.en\") text=\"Installation directory: %s\" ;;\n \"install.dir_hint.zh\") text=\" (env 文件 'agentteams-manager.env' 将保存到 HOME 目录。)\" ;;\n \"install.dir_hint.en\") text=\" (The env file 'agentteams-manager.env' will be saved to your HOME directory.)\" ;;\n \"install.dir_hint2.zh\") text=\" (请从您希望管理此安装的目录运行此脚本。)\" ;;\n \"install.dir_hint2.en\") text=\" (Run this script from the directory where you want to manage this installation.)\" ;;\n # --- Onboarding mode ---\n \"install.mode.title.zh\") text=\"--- Onboarding 模式 ---\" ;;\n \"install.mode.title.en\") text=\"--- Onboarding Mode ---\" ;;\n \"install.mode.choose.zh\") text=\"选择安装模式:\" ;;\n \"install.mode.choose.en\") text=\"Choose your installation mode:\" ;;\n \"install.mode.quickstart.zh\") text=\" 1) 快速开始 - 使用阿里云通义 Token 套餐快速安装(推荐)\" ;;\n \"install.mode.quickstart.en\") text=\" 1) Quick Start - Fast installation with Qwen Cloud (recommended)\" ;;\n \"install.mode.manual.zh\") text=\" 2) 手动配置 - 选择 LLM 提供商并自定义选项\" ;;\n \"install.mode.manual.en\") text=\" 2) Manual - Choose LLM provider and customize options\" ;;\n \"install.mode.prompt.zh\") text=\"请选择 [1/2]\" ;;\n \"install.mode.prompt.en\") text=\"Enter choice [1/2]\" ;;\n \"install.mode.quickstart_selected.zh\") text=\"已选择快速开始模式 - 使用阿里云通义 Token 套餐\" ;;\n \"install.mode.quickstart_selected.en\") text=\"Quick Start mode selected - using Qwen Cloud\" ;;\n \"install.mode.manual_selected.zh\") text=\"已选择手动配置模式 - 您将选择 LLM 提供商并自定义选项\" ;;\n \"install.mode.manual_selected.en\") text=\"Manual mode selected - you will choose LLM provider and customize options\" ;;\n \"install.mode.invalid.zh\") text=\"无效选择,默认使用快速开始模式\" ;;\n \"install.mode.invalid.en\") text=\"Invalid choice, defaulting to Quick Start mode\" ;;\n # --- Upgrade mode sub-menu ---\n \"upgrade.mode.prompt.zh\") text=\"请选择升级方式:\" ;;\n \"upgrade.mode.prompt.en\") text=\"Select upgrade mode:\" ;;\n \"upgrade.mode.keep_all.zh\") text=\" 1) 保留所有参数,一键升级(推荐)\" ;;\n \"upgrade.mode.keep_all.en\") text=\" 1) Keep all parameters, quick upgrade (recommended)\" ;;\n \"upgrade.mode.confirm_each.zh\") text=\" 2) 逐个确认参数(可修改)\" ;;\n \"upgrade.mode.confirm_each.en\") text=\" 2) Confirm each parameter (can modify)\" ;;\n \"upgrade.mode.back.zh\") text=\" 3) 返回\" ;;\n \"upgrade.mode.back.en\") text=\" 3) Back\" ;;\n # --- Version selection ---\n \"install.version.title.zh\") text=\"--- 版本选择 ---\" ;;\n \"install.version.title.en\") text=\"--- Version Selection ---\" ;;\n \"install.version.choose.zh\") text=\"选择要安装的版本:\" ;;\n \"install.version.choose.en\") text=\"Choose the version to install:\" ;;\n \"install.version.option_latest.zh\") text=\" 1) latest - 最新版(默认)\" ;;\n \"install.version.option_latest.en\") text=\" 1) latest - Latest build (default)\" ;;\n \"install.version.option_stable.zh\") text=\" 2) %s - 最新稳定版\" ;;\n \"install.version.option_stable.en\") text=\" 2) %s - Latest stable release\" ;;\n \"install.version.fetching.zh\") text=\"正在查询最新稳定版本...\" ;;\n \"install.version.fetching.en\") text=\"Fetching latest stable release...\" ;;\n \"install.version.fetch_failed.zh\") text=\"无法查询 GitHub,使用内置版本 %s\" ;;\n \"install.version.fetch_failed.en\") text=\"Could not reach GitHub, using built-in version %s\" ;;\n \"install.version.option_custom.zh\") text=\" 3) 自定义 - 手动输入版本号(如 v1.0.5)\" ;;\n \"install.version.option_custom.en\") text=\" 3) Custom - Enter a specific version (e.g. v1.0.5)\" ;;\n \"install.version.prompt.zh\") text=\"请选择 [1/2/3]\" ;;\n \"install.version.prompt.en\") text=\"Enter choice [1/2/3]\" ;;\n \"install.version.custom_prompt.zh\") text=\"请输入版本号\" ;;\n \"install.version.custom_prompt.en\") text=\"Enter version tag\" ;;\n \"install.version.selected_latest.zh\") text=\"已选择最新版 (latest)\" ;;\n \"install.version.selected_latest.en\") text=\"Selected latest version\" ;;\n \"install.version.selected_stable.zh\") text=\"已选择最新稳定版 (%s)\" ;;\n \"install.version.selected_stable.en\") text=\"Selected latest stable version (%s)\" ;;\n \"install.version.selected_custom.zh\") text=\"已选择自定义版本 (%s)\" ;;\n \"install.version.selected_custom.en\") text=\"Selected custom version (%s)\" ;;\n \"install.version.invalid.zh\") text=\"无效选择,使用最新稳定版 (%s)\" ;;\n \"install.version.invalid.en\") text=\"Invalid choice, defaulting to latest stable version (%s)\" ;;\n # --- Existing installation detected ---\n \"install.existing.detected.zh\") text=\"检测到已有 Manager 安装(env 文件: %s)\" ;;\n \"install.existing.detected.en\") text=\"Existing Manager installation detected (env file: %s)\" ;;\n \"install.existing.choose.zh\") text=\"选择操作:\" ;;\n \"install.existing.choose.en\") text=\"Choose an action:\" ;;\n \"install.existing.upgrade.zh\") text=\" 1) 就地升级(保留数据、工作空间、env 文件)\" ;;\n \"install.existing.upgrade.en\") text=\" 1) In-place upgrade (keep data, workspace, env file)\" ;;\n \"install.existing.reinstall.zh\") text=\" 2) 全新重装(删除所有数据,重新开始)\" ;;\n \"install.existing.reinstall.en\") text=\" 2) Clean reinstall (remove all data, start fresh)\" ;;\n \"install.existing.cancel.zh\") text=\" 3) 取消\" ;;\n \"install.existing.cancel.en\") text=\" 3) Cancel\" ;;\n \"install.existing.prompt.zh\") text=\"请选择 [1/2/3]\" ;;\n \"install.existing.prompt.en\") text=\"Enter choice [1/2/3]\" ;;\n \"install.existing.upgrade_noninteractive.zh\") text=\"非交互模式: 执行就地升级...\" ;;\n \"install.existing.upgrade_noninteractive.en\") text=\"Non-interactive mode: performing in-place upgrade...\" ;;\n \"install.existing.upgrading.zh\") text=\"执行就地升级...\" ;;\n \"install.existing.upgrading.en\") text=\"Performing in-place upgrade...\" ;;\n \"install.existing.warn_manager_stop.zh\") text=\"⚠️ Manager 容器将被停止并重新创建。\" ;;\n \"install.existing.warn_manager_stop.en\") text=\"⚠️ Manager container will be stopped and recreated.\" ;;\n \"install.existing.warn_worker_recreate.zh\") text=\"⚠️ Worker 容器也将被重新创建(以更新 Manager IP)。\" ;;\n \"install.existing.warn_worker_recreate.en\") text=\"⚠️ Worker containers will also be recreated (to update Manager IP in hosts).\" ;;\n \"install.existing.continue_prompt.zh\") text=\"继续?[y/N]\" ;;\n \"install.existing.continue_prompt.en\") text=\"Continue? [y/N]\" ;;\n \"install.existing.cancelled.zh\") text=\"安装已取消。\" ;;\n \"install.existing.cancelled.en\") text=\"Installation cancelled.\" ;;\n \"install.existing.stopping_manager.zh\") text=\"停止并移除现有 manager 容器...\" ;;\n \"install.existing.stopping_manager.en\") text=\"Stopping and removing existing manager container...\" ;;\n \"install.existing.stopping_workers.zh\") text=\"停止并移除现有 worker 容器...\" ;;\n \"install.existing.stopping_workers.en\") text=\"Stopping and removing existing worker containers...\" ;;\n \"install.existing.removed.zh\") text=\" 已移除: %s\" ;;\n \"install.existing.removed.en\") text=\" Removed: %s\" ;;\n # --- Clean reinstall messages ---\n \"install.reinstall.performing.zh\") text=\"执行全新重装...\" ;;\n \"install.reinstall.performing.en\") text=\"Performing clean reinstall...\" ;;\n \"install.reinstall.warn_stop.zh\") text=\"⚠️ 以下运行中的容器将被停止:\" ;;\n \"install.reinstall.warn_stop.en\") text=\"⚠️ The following running containers will be stopped:\" ;;\n \"install.reinstall.warn_delete.zh\") text=\"⚠️ 警告: 以下内容将被删除:\" ;;\n \"install.reinstall.warn_delete.en\") text=\"⚠️ WARNING: This will DELETE the following:\" ;;\n \"install.reinstall.warn_volume.zh\") text=\" - Docker 卷: agentteams-data\" ;;\n \"install.reinstall.warn_volume.en\") text=\" - Docker volume: agentteams-data\" ;;\n \"install.reinstall.warn_env.zh\") text=\" - Env 文件: %s\" ;;\n \"install.reinstall.warn_env.en\") text=\" - Env file: %s\" ;;\n \"install.reinstall.warn_workspace.zh\") text=\" - Manager 工作空间: %s\" ;;\n \"install.reinstall.warn_workspace.en\") text=\" - Manager workspace: %s\" ;;\n \"install.reinstall.warn_workers.zh\") text=\" - 所有 worker 容器\" ;;\n \"install.reinstall.warn_workers.en\") text=\" - All worker containers\" ;;\n \"install.reinstall.warn_proxy.zh\") text=\" - Docker API 代理容器: agentteams-controller\" ;;\n \"install.reinstall.warn_proxy.en\") text=\" - Docker API proxy container: agentteams-controller\" ;;\n \"install.reinstall.warn_network.zh\") text=\" - Docker 网络: agentteams-net\" ;;\n \"install.reinstall.warn_network.en\") text=\" - Docker network: agentteams-net\" ;;\n \"install.reinstall.confirm_type.zh\") text=\"请输入工作空间路径以确认删除(或按 Ctrl+C 取消):\" ;;\n \"install.reinstall.confirm_type.en\") text=\"To confirm deletion, please type the workspace path:\" ;;\n \"install.reinstall.confirm_path.zh\") text=\"输入路径以确认(或按 Ctrl+C 取消)\" ;;\n \"install.reinstall.confirm_path.en\") text=\"Type the path to confirm (or press Ctrl+C to cancel)\" ;;\n \"install.reinstall.path_mismatch.zh\") text=\"路径不匹配。中止重装。输入: '%s',期望: '%s'\" ;;\n \"install.reinstall.path_mismatch.en\") text=\"Path mismatch. Aborting reinstall. Input: '%s', Expected: '%s'\" ;;\n \"install.reinstall.confirmed.zh\") text=\"已确认。正在清理...\" ;;\n \"install.reinstall.confirmed.en\") text=\"Confirmed. Cleaning up...\" ;;\n \"install.reinstall.removed_worker.zh\") text=\" 已移除 worker: %s\" ;;\n \"install.reinstall.removed_worker.en\") text=\" Removed worker: %s\" ;;\n \"install.reinstall.removing_volume.zh\") text=\"正在移除 Docker 卷: agentteams-data\" ;;\n \"install.reinstall.removing_volume.en\") text=\"Removing Docker volume: agentteams-data\" ;;\n \"install.reinstall.warn_volume_fail.zh\") text=\" 警告: 无法移除卷(可能有引用)\" ;;\n \"install.reinstall.warn_volume_fail.en\") text=\" Warning: Could not remove volume (may have references)\" ;;\n \"install.reinstall.removing_proxy.zh\") text=\"正在移除 Docker API 代理容器: agentteams-controller\" ;;\n \"install.reinstall.removing_proxy.en\") text=\"Removing Docker API proxy container: agentteams-controller\" ;;\n \"install.reinstall.removing_network.zh\") text=\"正在移除 Docker 网络: agentteams-net\" ;;\n \"install.reinstall.removing_network.en\") text=\"Removing Docker network: agentteams-net\" ;;\n \"install.reinstall.removing_workspace.zh\") text=\"正在移除工作空间目录: %s\" ;;\n \"install.reinstall.removing_workspace.en\") text=\"Removing workspace directory: %s\" ;;\n \"install.reinstall.removing_env.zh\") text=\"正在移除 env 文件: %s\" ;;\n \"install.reinstall.removing_env.en\") text=\"Removing env file: %s\" ;;\n \"install.reinstall.cleanup_done.zh\") text=\"清理完成。开始全新安装...\" ;;\n \"install.reinstall.cleanup_done.en\") text=\"Cleanup complete. Starting fresh installation...\" ;;\n \"install.reinstall.failed_rm_workspace.zh\") text=\"无法移除工作空间目录\" ;;\n \"install.reinstall.failed_rm_workspace.en\") text=\"Failed to remove workspace directory\" ;;\n # --- Orphan volume detection ---\n \"install.orphan_volume.detected.zh\") text=\"⚠️ 检测到残留数据卷 '%s',但未找到对应的 env 配置文件。\" ;;\n \"install.orphan_volume.detected.en\") text=\"⚠️ Found leftover data volume '%s' but no matching env config file.\" ;;\n \"install.orphan_volume.warn.zh\") text=\"这可能是之前安装的残留数据,会导致新安装出现异常(如密码冲突、服务启动失败)。\" ;;\n \"install.orphan_volume.warn.en\") text=\"This is likely leftover data from a previous installation and may cause issues (e.g., credential conflicts, service startup failures).\" ;;\n \"install.orphan_volume.choose.zh\") text=\"选择操作:\" ;;\n \"install.orphan_volume.choose.en\") text=\"Choose an action:\" ;;\n \"install.orphan_volume.clean.zh\") text=\" 1) 清理残留数据卷后继续安装(推荐)\" ;;\n \"install.orphan_volume.clean.en\") text=\" 1) Remove leftover volume and continue installation (recommended)\" ;;\n \"install.orphan_volume.keep.zh\") text=\" 2) 保留数据卷继续安装(可能出现异常)\" ;;\n \"install.orphan_volume.keep.en\") text=\" 2) Keep the volume and continue installation (may cause issues)\" ;;\n \"install.orphan_volume.prompt.zh\") text=\"请选择 [1/2]\" ;;\n \"install.orphan_volume.prompt.en\") text=\"Enter choice [1/2]\" ;;\n \"install.orphan_volume.cleaning.zh\") text=\"正在清理残留数据卷...\" ;;\n \"install.orphan_volume.cleaning.en\") text=\"Removing leftover data volume...\" ;;\n \"install.orphan_volume.cleaned.zh\") text=\"残留数据卷已清理。继续全新安装...\" ;;\n \"install.orphan_volume.cleaned.en\") text=\"Leftover volume removed. Continuing with fresh installation...\" ;;\n \"install.orphan_volume.keeping.zh\") text=\"保留数据卷,继续安装。如遇异常请选择全新重装。\" ;;\n \"install.orphan_volume.keeping.en\") text=\"Keeping existing volume. If you encounter issues, consider a clean reinstall.\" ;;\n \"install.orphan_volume.clean_noninteractive.zh\") text=\"非交互模式: 自动清理残留数据卷...\" ;;\n \"install.orphan_volume.clean_noninteractive.en\") text=\"Non-interactive mode: automatically removing leftover volume...\" ;;\n # --- Loading existing config ---\n \"install.loading_config.zh\") text=\"从 %s 加载已有配置(shell 环境变量优先)...\" ;;\n \"install.loading_config.en\") text=\"Loading existing config from %s (shell env vars take priority)...\" ;;\n # --- LLM Configuration ---\n \"llm.title.zh\") text=\"--- LLM 配置 ---\" ;;\n \"llm.title.en\") text=\"--- LLM Configuration ---\" ;;\n \"llm.provider.label.zh\") text=\" 提供商: %s\" ;;\n \"llm.provider.label.en\") text=\" Provider: %s\" ;;\n \"llm.model.label.zh\") text=\" 模型: %s\" ;;\n \"llm.model.label.en\") text=\" Model: %s\" ;;\n \"llm.provider.qwen.zh\") text=\" 提供商: qwen(阿里云百炼)\" ;;\n \"llm.provider.qwen.en\") text=\" Provider: qwen (Alibaba Cloud Bailian)\" ;;\n \"llm.provider.qwen_default.zh\") text=\" 提供商: %s(默认)\" ;;\n \"llm.provider.qwen_default.en\") text=\" Provider: %s (default)\" ;;\n \"llm.model.default.zh\") text=\" 模型: %s(默认)\" ;;\n \"llm.model.default.en\") text=\" Model: %s (default)\" ;;\n \"llm.apikey_hint_bailian.zh\") text=\" 💡 获取阿里云百炼(DashScope)API Key:\" ;;\n \"llm.apikey_hint_bailian.en\") text=\" 💡 Get your Alibaba Cloud Bailian (DashScope) API Key:\" ;;\n \"llm.apikey_url_bailian.zh\") text=\" https://www.aliyun.com/product/bailian\" ;;\n \"llm.apikey_url_bailian.en\") text=\" https://www.aliyun.com/product/bailian\" ;;\n \"llm.apikey_hint_qwencloud.zh\") text=\" 💡 从 Qwen Cloud(国际站)获取 DASHSCOPE_API_KEY:\" ;;\n \"llm.apikey_hint_qwencloud.en\") text=\" 💡 Get your DASHSCOPE_API_KEY for Qwen Cloud (international) from:\" ;;\n \"llm.apikey_url_qwencloud.zh\") text=\" https://home.qwencloud.com/api-keys (文档: https://docs.qwencloud.com/)\" ;;\n \"llm.apikey_url_qwencloud.en\") text=\" https://home.qwencloud.com/api-keys | Docs: https://docs.qwencloud.com/\" ;;\n \"llm.apikey_hint_tokenplan.zh\") text=\" 💡 获取 DashScope API Key 或开通通义 Token 套餐,请参考:\" ;;\n \"llm.apikey_hint_tokenplan.en\") text=\" 💡 Get your DashScope or Token Plan API key (Alibaba Model Studio):\" ;;\n \"llm.apikey_url_tokenplan.zh\") text=\" https://help.aliyun.com/zh/model-studio/token-plan-quickstart\" ;;\n \"llm.apikey_url_tokenplan.en\") text=\" https://common-buy.aliyun.com/token-plan/ | https://help.aliyun.com/zh/model-studio/token-plan-quickstart\" ;;\n \"llm.apikey_hint_codingplan.zh\") text=\" 💡 获取 DashScope API Key(Coding 套餐 / coding.dashscope 接口):\" ;;\n \"llm.apikey_hint_codingplan.en\") text=\" 💡 Get your DashScope API key for Coding Plan (coding.dashscope endpoint):\" ;;\n \"llm.apikey_url_codingplan.zh\") text=\" https://help.aliyun.com/zh/model-studio/get-api-key\" ;;\n \"llm.apikey_url_codingplan.en\") text=\" https://help.aliyun.com/zh/model-studio/get-api-key\" ;;\n \"llm.apikey_prompt.zh\") text=\"LLM API Key\" ;;\n \"llm.apikey_prompt.en\") text=\"LLM API Key\" ;;\n \"llm.providers_title.zh\") text=\"可用 LLM 提供商:\" ;;\n \"llm.providers_title.en\") text=\"Available LLM Providers:\" ;;\n \"llm.provider.alibaba.zh\") text=\" 1) 阿里云通义 Token 套餐 - 推荐中国用户使用\" ;;\n \"llm.provider.alibaba.en\") text=\" 1) Qwen Cloud - International (OpenAI-compatible API, recommended)\" ;;\n \"llm.provider.openai_compat.zh\") text=\" 2) OpenAI 兼容 API - 自定义 Base URL(OpenAI、DeepSeek 等)\" ;;\n \"llm.provider.openai_compat.en\") text=\" 2) OpenAI-compatible API - Custom Base URL (OpenAI, DeepSeek, etc.)\" ;;\n \"llm.provider.select.zh\") text=\"选择提供商 [1/2]\" ;;\n \"llm.provider.select.en\") text=\"Select provider [1/2]\" ;;\n \"llm.alibaba.models_title.zh\") text=\"选择阿里云模型接入方式:\" ;;\n \"llm.alibaba.models_title.en\") text=\"Select Alibaba Cloud model access:\" ;;\n \"llm.alibaba.model.tokenplan.zh\") text=\" 1) 阿里云通义 Token 套餐 - 兼容模式(推荐)\" ;;\n \"llm.alibaba.model.tokenplan.en\") text=\" 1) Alibaba Cloud Token Plan - compatible-mode (recommended)\" ;;\n \"llm.alibaba.model.bailian.zh\") text=\" 2) 阿里云百炼 - DashScope 通用兼容接口\" ;;\n \"llm.alibaba.model.bailian.en\") text=\" 2) Alibaba Cloud Bailian - DashScope compatible mode\" ;;\n \"llm.alibaba.model.codingplan_legacy.zh\") text=\" 3) 阿里云 Coding 套餐 - 旧版端点(兼容保留)\" ;;\n \"llm.alibaba.model.codingplan_legacy.en\") text=\" 3) Alibaba Cloud Coding Plan - legacy endpoint (backward compatible)\" ;;\n \"llm.alibaba.model.select.zh\") text=\"选择接入方式 [1/2/3]\" ;;\n \"llm.alibaba.model.select.en\") text=\"Select access option [1/2/3]\" ;;\n \"llm.alibaba.model.invalid.zh\") text=\"无效选择: %s(请输入 1、2 或 3)\" ;;\n \"llm.alibaba.model.invalid.en\") text=\"Invalid choice: %s (please enter 1, 2, or 3)\" ;;\n \"llm.codingplan.models_title.zh\") text=\"选择通义 Token 套餐默认模型:\" ;;\n \"llm.codingplan.models_title.en\") text=\"Select Qwen Cloud default model:\" ;;\n \"llm.codingplan.model.qwen36plus.zh\") text=\" 1) qwen3.6-plus - 千问 3.6(推荐)\" ;;\n \"llm.codingplan.model.qwen36plus.en\") text=\" 1) qwen3.6-plus - Qwen 3.6 (recommended)\" ;;\n \"llm.codingplan.model.glm5.zh\") text=\" 2) glm-5 - 智谱 GLM-5(编程推荐)\" ;;\n \"llm.codingplan.model.glm5.en\") text=\" 2) glm-5 - Zhipu GLM-5 (recommended for coding)\" ;;\n \"llm.codingplan.model.kimi.zh\") text=\" 3) kimi-k2.5 - Moonshot Kimi K2.5\" ;;\n \"llm.codingplan.model.kimi.en\") text=\" 3) kimi-k2.5 - Moonshot Kimi K2.5\" ;;\n \"llm.codingplan.model.minimax.zh\") text=\" 4) MiniMax-M2.5 - MiniMax M2.5\" ;;\n \"llm.codingplan.model.minimax.en\") text=\" 4) MiniMax-M2.5 - MiniMax M2.5\" ;;\n \"llm.codingplan.model.select.zh\") text=\"选择模型 [1/2/3/4]\" ;;\n \"llm.codingplan.model.select.en\") text=\"Select model [1/2/3/4]\" ;;\n \"llm.provider.selected_tokenplan.zh\") text=\" 提供商: 阿里云通义 Token 套餐(兼容模式)\" ;;\n \"llm.provider.selected_tokenplan.en\") text=\" Provider: Alibaba Cloud Token Plan (compatible mode)\" ;;\n \"llm.provider.selected_codingplan.zh\") text=\" 提供商: 阿里云通义 Token 套餐(alibaba-cloud)\" ;;\n \"llm.provider.selected_codingplan.en\") text=\" Provider: Qwen Cloud (international) (alibaba-cloud)\" ;;\n \"llm.provider.selected_codingplan_legacy.zh\") text=\" 提供商: 阿里云 Coding 套餐(coding.dashscope)\" ;;\n \"llm.provider.selected_codingplan_legacy.en\") text=\" Provider: Alibaba Cloud Coding Plan (coding.dashscope)\" ;;\n \"llm.provider.selected_qwen.zh\") text=\" 提供商: 阿里云百炼\" ;;\n \"llm.provider.selected_qwen.en\") text=\" Provider: Alibaba Cloud Bailian\" ;;\n \"llm.provider.selected_openai.zh\") text=\" 提供商: %s(OpenAI 兼容)\" ;;\n \"llm.provider.selected_openai.en\") text=\" Provider: %s (OpenAI-compatible)\" ;;\n \"llm.provider.invalid.zh\") text=\"无效选择: %s(请输入 1 或 2)\" ;;\n \"llm.provider.invalid.en\") text=\"Invalid choice: %s (please enter 1 or 2)\" ;;\n \"llm.qwen.model_prompt.zh\") text=\"默认模型 ID\" ;;\n \"llm.qwen.model_prompt.en\") text=\"Default Model ID\" ;;\n \"llm.openai.base_url_prompt.zh\") text=\"Base URL\" ;;\n \"llm.openai.base_url_prompt.en\") text=\"Base URL\" ;;\n \"llm.openai.model_prompt.zh\") text=\"默认模型 ID\" ;;\n \"llm.openai.model_prompt.en\") text=\"Default Model ID\" ;;\n \"llm.openai.base_url_label.zh\") text=\" Base URL: %s\" ;;\n \"llm.openai.base_url_label.en\") text=\" Base URL: %s\" ;;\n # --- Custom model parameters ---\n \"llm.custom_model.detected.zh\") text=\" ⚠️ 模型 '%s' 不在内置模型列表中,请配置模型参数:\" ;;\n \"llm.custom_model.detected.en\") text=\" ⚠️ Model '%s' is not in the built-in model list. Please configure model parameters:\" ;;\n \"llm.custom_model.context_prompt.zh\") text=\"最大上下文长度(token 数)[150000]\" ;;\n \"llm.custom_model.context_prompt.en\") text=\"Max context window (tokens) [150000]\" ;;\n \"llm.custom_model.max_tokens_prompt.zh\") text=\"最大输出长度(token 数)[128000]\" ;;\n \"llm.custom_model.max_tokens_prompt.en\") text=\"Max output tokens [128000]\" ;;\n \"llm.custom_model.reasoning_prompt.zh\") text=\"是否支持推理/思考模式?[Y/n]\" ;;\n \"llm.custom_model.reasoning_prompt.en\") text=\"Does it support reasoning/thinking mode? [Y/n]\" ;;\n \"llm.custom_model.vision_prompt.zh\") text=\"是否支持图片输入?[y/N]\" ;;\n \"llm.custom_model.vision_prompt.en\") text=\"Does it support image input? [y/N]\" ;;\n \"llm.custom_model.summary.zh\") text=\" 自定义模型参数: 上下文=%s, 最大输出=%s, 推理=%s, 图片=%s\" ;;\n \"llm.custom_model.summary.en\") text=\" Custom model params: context=%s, maxTokens=%s, reasoning=%s, vision=%s\" ;;\n # --- Admin Credentials ---\n \"admin.title.zh\") text=\"--- 管理员凭据 ---\" ;;\n \"admin.title.en\") text=\"--- Admin Credentials ---\" ;;\n \"admin.username_prompt.zh\") text=\"管理员用户名\" ;;\n \"admin.username_prompt.en\") text=\"Admin Username\" ;;\n \"admin.password_prompt.zh\") text=\"管理员密码(留空自动生成,最少 8 位)\" ;;\n \"admin.password_prompt.en\") text=\"Admin Password (leave empty to auto-generate, min 8 chars)\" ;;\n \"admin.password_generated.zh\") text=\" 已自动生成管理员密码\" ;;\n \"admin.password_generated.en\") text=\" Auto-generated admin password\" ;;\n \"admin.password_too_short.zh\") text=\"管理员密码至少需要 8 个字符(MinIO 要求)。当前长度: %s\" ;;\n \"admin.password_too_short.en\") text=\"Admin password must be at least 8 characters (MinIO requirement). Current length: %s\" ;;\n # --- Port Configuration ---\n \"port.title.zh\") text=\"--- 端口配置(按回车使用默认值)---\" ;;\n \"port.title.en\") text=\"--- Port Configuration (press Enter for defaults) ---\" ;;\n \"port.gateway_prompt.zh\") text=\"网关主机端口(容器内 8080)\" ;;\n \"port.gateway_prompt.en\") text=\"Host port for gateway (8080 inside container)\" ;;\n \"port.console_prompt.zh\") text=\"Higress 控制台主机端口(容器内 8001)\" ;;\n \"port.console_prompt.en\") text=\"Host port for Higress console (8001 inside container)\" ;;\n \"port.element_prompt.zh\") text=\"Element Web 直接访问主机端口(容器内 8088)\" ;;\n \"port.element_prompt.en\") text=\"Host port for Element Web direct access (8088 inside container)\" ;;\n \"port.manager_console_prompt.zh\") text=\"Manager 控制台主机端口(容器内 18888)\" ;;\n \"port.manager_console_prompt.en\") text=\"Host port for Manager console (18888 inside container)\" ;;\n \"port.copaw_app_prompt.zh\") text=\"QwenPaw App API 主机端口(容器内 18799)\" ;;\n \"port.copaw_app_prompt.en\") text=\"Host port for QwenPaw App API (18799 inside container)\" ;;\n # --- Local-only binding ---\n \"port.local_only.title.zh\") text=\"--- 网络访问模式 ---\" ;;\n \"port.local_only.title.en\") text=\"--- Network Access Mode ---\" ;;\n \"port.local_only.prompt.zh\") text=\"是否仅允许本机访问(端口绑定到 127.0.0.1)?\" ;;\n \"port.local_only.prompt.en\") text=\"Bind ports to localhost only (127.0.0.1)?\" ;;\n \"port.local_only.hint_yes.zh\") text=\" 仅本机使用,无需开放外部端口(推荐)\" ;;\n \"port.local_only.hint_yes.en\") text=\" Local use only, no external port exposure (recommended)\" ;;\n \"port.local_only.hint_no.zh\") text=\" 允许外部访问(局域网 / 公网)\" ;;\n \"port.local_only.hint_no.en\") text=\" Allow external access (LAN / public network)\" ;;\n \"port.local_only.choice.zh\") text=\"请选择 [1/2]\" ;;\n \"port.local_only.choice.en\") text=\"Enter choice [1/2]\" ;;\n \"port.local_only.selected_local.zh\") text=\"端口已绑定到 127.0.0.1(仅本机访问)\" ;;\n \"port.local_only.selected_local.en\") text=\"Ports bound to 127.0.0.1 (localhost only)\" ;;\n \"port.local_only.selected_external.zh\") text=\"端口已绑定到所有网络接口(0.0.0.0)\" ;;\n \"port.local_only.selected_external.en\") text=\"Ports bound to all interfaces (0.0.0.0)\" ;;\n \"port.local_only.https_hint.zh\") text=\"⚠️ 建议在 Higress 控制台配置 TLS 证书并启用 HTTPS,避免明文传输。\" ;;\n \"port.local_only.https_hint.en\") text=\"⚠️ It is recommended to configure TLS certificates and enable HTTPS in the Higress Console to avoid plaintext transmission.\" ;;\n \"port.local_only.https_docs.zh\") text=\"\" ;;\n \"port.local_only.https_docs.en\") text=\"\" ;;\n # --- Domain Configuration ---\n \"domain.title.zh\") text=\"--- 域名配置(按回车使用默认值)---\" ;;\n \"domain.title.en\") text=\"--- Domain Configuration (press Enter for defaults) ---\" ;;\n \"domain.hint.zh\") text=\"提示: 自定义域名前必须事先做好 DNS 解析。单机 ECS 部署时无需修改 aigw、fs 等域名;Element Web 和 Matrix Server 也可通过 IP 直接访问。\" ;;\n \"domain.hint.en\") text=\"Hint: Configure DNS resolution before customizing domains. For single ECS deployment, no need to change aigw, fs, etc.; Element Web and Matrix Server can also be accessed directly via IP.\" ;;\n \"domain.matrix_prompt.zh\") text=\"Matrix 域名\" ;;\n \"domain.matrix_prompt.en\") text=\"Matrix Domain\" ;;\n \"domain.element_prompt.zh\") text=\"Element Web 域名\" ;;\n \"domain.element_prompt.en\") text=\"Element Web Domain\" ;;\n \"domain.gateway_prompt.zh\") text=\"AI 网关域名\" ;;\n \"domain.gateway_prompt.en\") text=\"AI Gateway Domain\" ;;\n \"domain.fs_prompt.zh\") text=\"文件系统域名\" ;;\n \"domain.fs_prompt.en\") text=\"File System Domain\" ;;\n \"domain.console_prompt.zh\") text=\"Manager 控制台域名\" ;;\n \"domain.console_prompt.en\") text=\"Manager Console Domain\" ;;\n # --- GitHub Integration ---\n \"github.title.zh\") text=\"--- GitHub 集成(可选,按回车跳过)---\" ;;\n \"github.title.en\") text=\"--- GitHub Integration (optional, press Enter to skip) ---\" ;;\n \"github.token_prompt.zh\") text=\"GitHub 个人访问令牌(可选)\" ;;\n \"github.token_prompt.en\") text=\"GitHub Personal Access Token (optional)\" ;;\n # --- Skills Registry ---\n \"skills.title.zh\") text=\"--- Skills 注册中心(可选,按回车使用默认 nacos://market.agentteams.io:80/public)---\" ;;\n \"skills.title.en\") text=\"--- Skills Registry (optional, press Enter for default nacos://market.agentteams.io:80/public) ---\" ;;\n \"skills.url_prompt.zh\") text=\"Skills 注册中心 URL(留空使用默认 nacos://market.agentteams.io:80/public)\" ;;\n \"skills.url_prompt.en\") text=\"Skills Registry URL (leave empty for default nacos://market.agentteams.io:80/public)\" ;;\n # --- Data Persistence ---\n \"data.title.zh\") text=\"--- 数据持久化 ---\" ;;\n \"data.title.en\") text=\"--- Data Persistence ---\" ;;\n \"data.volume_prompt.zh\") text=\"Docker 卷名称 [agentteams-data]\" ;;\n \"data.volume_prompt.en\") text=\"Docker volume name for persistent data [agentteams-data]\" ;;\n \"data.volume_using.zh\") text=\" 使用 Docker 卷: %s\" ;;\n \"data.volume_using.en\") text=\" Using Docker volume: %s\" ;;\n # --- Manager Workspace ---\n \"workspace.title.zh\") text=\"--- Manager 工作空间 ---\" ;;\n \"workspace.title.en\") text=\"--- Manager Workspace ---\" ;;\n \"workspace.dir_prompt.zh\") text=\"Manager 工作空间目录 [%s]\" ;;\n \"workspace.dir_prompt.en\") text=\"Manager workspace directory [%s]\" ;;\n \"workspace.dir_label.zh\") text=\" Manager 工作空间: %s\" ;;\n \"workspace.dir_label.en\") text=\" Manager workspace: %s\" ;;\n # --- Host directory sharing ---\n \"host_share.prompt.zh\") text=\"与 Agent 共享的主机目录(默认: %s)\" ;;\n \"host_share.prompt.en\") text=\"Host directory to share with agents (default: %s)\" ;;\n \"host_share.sharing.zh\") text=\"共享主机目录: %s -> 容器内 /host-share\" ;;\n \"host_share.sharing.en\") text=\"Sharing host directory: %s -> /host-share in container\" ;;\n \"host_share.not_exist.zh\") text=\"警告: 主机目录 %s 不存在,跳过验证继续使用\" ;;\n \"host_share.not_exist.en\") text=\"WARNING: Host directory %s does not exist, using without validation\" ;;\n # --- Default worker runtime ---\n \"worker_runtime.title.zh\") text=\"--- 默认 Worker 运行时 ---\" ;;\n \"worker_runtime.title.en\") text=\"--- Default Worker Runtime ---\" ;;\n \"worker_runtime.openclaw.zh\") text=\"OpenClaw\" ;;\n \"worker_runtime.openclaw.en\") text=\"OpenClaw\" ;;\n \"worker_runtime.copaw.zh\") text=\"QwenPaw\" ;;\n \"worker_runtime.copaw.en\") text=\"QwenPaw\" ;;\n \"worker_runtime.hermes.zh\") text=\"Hermes\" ;;\n \"worker_runtime.hermes.en\") text=\"Hermes\" ;;\n \"worker_runtime.choice.zh\") text=\"请选择 [1/2/3]\" ;;\n \"worker_runtime.choice.en\") text=\"Enter choice [1/2/3]\" ;;\n \"worker_runtime.selected.zh\") text=\"默认 Worker 运行时: %s\" ;;\n \"worker_runtime.selected.en\") text=\"Default Worker runtime: %s\" ;;\n \"worker_runtime.title_short.zh\") text=\"默认 Worker 运行时\" ;;\n \"worker_runtime.title_short.en\") text=\"Default Worker Runtime\" ;;\n \"manager_runtime.title.zh\") text=\"--- Manager 运行时 ---\" ;;\n \"manager_runtime.title.en\") text=\"--- Manager Runtime ---\" ;;\n \"manager_runtime.openclaw.zh\") text=\"OpenClaw\" ;;\n \"manager_runtime.openclaw.en\") text=\"OpenClaw\" ;;\n \"manager_runtime.copaw.zh\") text=\"QwenPaw\" ;;\n \"manager_runtime.copaw.en\") text=\"QwenPaw\" ;;\n \"manager_runtime.choice.zh\") text=\"请选择 [1/2]\" ;;\n \"manager_runtime.choice.en\") text=\"Enter choice [1/2]\" ;;\n \"manager_runtime.selected.zh\") text=\"Manager 运行时: %s\" ;;\n \"manager_runtime.selected.en\") text=\"Manager runtime: %s\" ;;\n \"manager_runtime.title_short.zh\") text=\"Manager 运行时\" ;;\n \"manager_runtime.title_short.en\") text=\"Manager Runtime\" ;;\n # --- Secrets and config ---\n \"install.generating_secrets.zh\") text=\"正在生成密钥...\" ;;\n \"install.generating_secrets.en\") text=\"Generating secrets...\" ;;\n \"install.config_saved.zh\") text=\"配置已保存到 %s\" ;;\n \"install.config_saved.en\") text=\"Configuration saved to %s\" ;;\n # --- Container runtime socket ---\n \"install.socket_detected.zh\") text=\"容器运行时 socket: %s(已启用直接创建 Worker)\" ;;\n \"install.socket_detected.en\") text=\"Container runtime socket: %s (direct Worker creation enabled)\" ;;\n \"install.socket_not_found.zh\") text=\"未找到容器运行时 socket(Manager 无法直接创建 Worker 容器,需要你手动执行 docker 命令创建)\" ;;\n \"install.socket_not_found.en\") text=\"No container runtime socket found (Manager cannot create Worker containers directly, you will need to create them manually using docker commands)\" ;;\n \"install.socket_confirm.title.zh\") text=\"⚠️ 未检测到容器运行时 Socket\" ;;\n \"install.socket_confirm.title.en\") text=\"⚠️ Container Runtime Socket Not Detected\" ;;\n \"install.socket_confirm.message.zh\") text=\"未找到 Docker/Podman socket,Manager 将无法自动创建 Worker 容器。\\n你需要手动执行 docker run 命令来创建 Worker。\\n\\n是否继续安装?\" ;;\n \"install.socket_confirm.message.en\") text=\"Docker/Podman socket not found. Manager will not be able to create Worker containers automatically.\\nYou will need to manually run docker commands to create Workers.\\n\\nContinue installation?\" ;;\n \"install.socket_confirm.prompt.zh\") text=\"继续安装? [y/N]: \" ;;\n \"install.socket_confirm.prompt.en\") text=\"Continue? [y/N]: \" ;;\n \"install.socket_confirm.cancelled.zh\") text=\"安装已取消。如需启用 Worker 自动创建,请确保 Docker/Podman 正在运行,然后重新运行安装脚本。\" ;;\n \"install.socket_confirm.cancelled.en\") text=\"Installation cancelled. To enable automatic Worker creation, ensure Docker/Podman is running and re-run the installer.\" ;;\n # --- Container management ---\n \"install.removing_existing.zh\") text=\"正在移除现有 agentteams-manager 容器...\" ;;\n \"install.removing_existing.en\") text=\"Removing existing agentteams-manager container...\" ;;\n # --- Matrix E2EE ---\n \"matrix_e2ee.title.zh\") text=\"--- Matrix 端到端加密(E2EE)---\" ;;\n \"matrix_e2ee.title.en\") text=\"--- Matrix End-to-End Encryption (E2EE) ---\" ;;\n \"matrix_e2ee.desc.zh\") text=\"E2EE 会对 Manager 与 Worker 之间的 Matrix 消息进行端到端加密。\\n 启用后,即使 Matrix 服务器被入侵,消息内容也无法被窃取。\\n 但 E2EE 会增加首次握手耗时,且要求所有 Agent 都支持 matrix-sdk-crypto。\\n 如果不确定,建议保持禁用。\\n ⚠ 注意:禁用 E2EE 后,请勿在 Element 上创建默认启用加密的 Private 房间,\\n 否则 Agent 将无法读取该房间中的加密消息。请改用 Public 房间或关闭房间加密。\" ;;\n \"matrix_e2ee.desc.en\") text=\"E2EE encrypts Matrix messages between Manager and Workers end-to-end.\\n When enabled, message content stays private even if the Matrix server is compromised.\\n However, E2EE adds overhead to the initial handshake and requires all Agents\\n to support matrix-sdk-crypto. If unsure, keep it disabled.\\n ⚠ Note: When E2EE is disabled, do NOT create Private rooms in Element (which\\n enable encryption by default) — Agents cannot read encrypted messages without\\n E2EE support. Use Public rooms or turn off room encryption instead.\" ;;\n \"matrix_e2ee.enable.zh\") text=\"启用 E2EE\" ;;\n \"matrix_e2ee.enable.en\") text=\"Enable E2EE\" ;;\n \"matrix_e2ee.disable.zh\") text=\"禁用 E2EE(推荐)\" ;;\n \"matrix_e2ee.disable.en\") text=\"Disable E2EE (recommended)\" ;;\n \"matrix_e2ee.choice.zh\") text=\"请选择 [1/2]\" ;;\n \"matrix_e2ee.choice.en\") text=\"Enter choice [1/2]\" ;;\n \"matrix_e2ee.selected_enabled.zh\") text=\"Matrix E2EE: 已启用\" ;;\n \"matrix_e2ee.selected_enabled.en\") text=\"Matrix E2EE: enabled\" ;;\n \"matrix_e2ee.selected_disabled.zh\") text=\"Matrix E2EE: 已禁用(默认)\" ;;\n \"matrix_e2ee.selected_disabled.en\") text=\"Matrix E2EE: disabled (default)\" ;;\n \"matrix_e2ee.title_short.zh\") text=\"Matrix E2EE\" ;;\n \"matrix_e2ee.title_short.en\") text=\"Matrix E2EE\" ;;\n \"matrix_e2ee.val_enabled.zh\") text=\"已启用\" ;;\n \"matrix_e2ee.val_enabled.en\") text=\"enabled\" ;;\n \"matrix_e2ee.val_disabled.zh\") text=\"已禁用\" ;;\n \"matrix_e2ee.val_disabled.en\") text=\"disabled\" ;;\n # --- Docker API proxy ---\n \"docker_proxy.title.zh\") text=\"--- Docker API 安全代理 ---\" ;;\n \"docker_proxy.title.en\") text=\"--- Docker API Security Proxy ---\" ;;\n \"docker_proxy.desc.zh\") text=\"Docker API 代理可防止 AI Agent 通过 Docker API 越狱访问宿主机。\\n 启用后,Manager 不再直接持有 Docker socket,所有容器操作经过安全校验。\" ;;\n \"docker_proxy.desc.en\") text=\"Docker API proxy prevents AI Agents from escaping via Docker API to access the host.\\n When enabled, Manager no longer has direct Docker socket access; all container operations go through security validation.\" ;;\n \"docker_proxy.enable.zh\") text=\"启用(推荐)\" ;;\n \"docker_proxy.enable.en\") text=\"Enable (recommended)\" ;;\n \"docker_proxy.disable.zh\") text=\"禁用(直接挂载 Docker socket)\" ;;\n \"docker_proxy.disable.en\") text=\"Disable (mount Docker socket directly)\" ;;\n \"docker_proxy.choice.zh\") text=\"请选择 [1/2]\" ;;\n \"docker_proxy.choice.en\") text=\"Enter choice [1/2]\" ;;\n \"docker_proxy.selected_enabled.zh\") text=\"Docker API 代理: 已启用\" ;;\n \"docker_proxy.selected_enabled.en\") text=\"Docker API proxy: enabled\" ;;\n \"docker_proxy.selected_disabled.zh\") text=\"Docker API 代理: 已禁用\" ;;\n \"docker_proxy.selected_disabled.en\") text=\"Docker API proxy: disabled\" ;;\n \"docker_proxy.title_short.zh\") text=\"Docker API 代理\" ;;\n \"docker_proxy.title_short.en\") text=\"Docker API Proxy\" ;;\n \"docker_proxy.val_enabled.zh\") text=\"已启用\" ;;\n \"docker_proxy.val_enabled.en\") text=\"enabled\" ;;\n \"docker_proxy.val_disabled.zh\") text=\"已禁用\" ;;\n \"docker_proxy.val_disabled.en\") text=\"disabled\" ;;\n \"docker_proxy.registries_desc.zh\") text=\"默认放行的镜像来源:本地镜像、localhost、Higress 仓库(所有 region)。\\n 如需放行其他镜像仓库,请输入逗号分隔的地址前缀。\\n 示例: ghcr.io/myorg,registry.example.com/team\" ;;\n \"docker_proxy.registries_desc.en\") text=\"Default allowed image sources: local images, localhost, Higress registries (all regions).\\n To allow additional image sources, enter comma-separated address prefixes.\\n Example: ghcr.io/myorg,registry.example.com/team\" ;;\n \"docker_proxy.registries_prompt.zh\") text=\"额外放行的镜像来源(按回车跳过)\" ;;\n \"docker_proxy.registries_prompt.en\") text=\"Additional allowed image sources (press Enter to skip)\" ;;\n \"docker_proxy.registries_label.zh\") text=\"额外放行的镜像来源\" ;;\n \"docker_proxy.registries_label.en\") text=\"Additional allowed image sources\" ;;\n # --- Podman Autostart ---\n \"podman.autostart.title.zh\") text=\"--- Podman 驻留与开机自启 ---\" ;;\n \"podman.autostart.title.en\") text=\"--- Podman Linger & Autostart ---\" ;;\n \"podman.autostart.desc.zh\") text=\"配置为 systemd 服务,支持开机自动启动及后台驻留。\" ;;\n \"podman.autostart.desc.en\") text=\"Configure as a systemd service for automatic startup on boot.\" ;;\n \"podman.autostart.enable.zh\") text=\"启用(推荐)\" ;;\n \"podman.autostart.enable.en\") text=\"Enable (recommended)\" ;;\n \"podman.autostart.disable.zh\") text=\"不启用(容器随当前会话关闭而停止)\" ;;\n \"podman.autostart.disable.en\") text=\"Disable (containers stop when session ends)\" ;;\n \"podman.autostart.choice.zh\") text=\"请选择 [1/2]\" ;;\n \"podman.autostart.choice.en\") text=\"Enter choice [1/2]\" ;;\n \"podman.autostart.selected_enabled.zh\") text=\"Podman 开机自启: 已启用\" ;;\n \"podman.autostart.selected_enabled.en\") text=\"Podman Autostart: enabled\" ;;\n \"podman.autostart.selected_disabled.zh\") text=\"Podman 开机自启: 已禁用\" ;;\n \"podman.autostart.selected_disabled.en\") text=\"Podman Autostart: disabled\" ;;\n \"podman.autostart.title_short.zh\") text=\"Podman 开机自启\" ;;\n \"podman.autostart.title_short.en\") text=\"Podman Autostart\" ;;\n \"podman.autostart.val_enabled.zh\") text=\"已启用\" ;;\n \"podman.autostart.val_enabled.en\") text=\"enabled\" ;;\n \"podman.autostart.val_disabled.zh\") text=\"已禁用\" ;;\n \"podman.autostart.val_disabled.en\") text=\"disabled\" ;;\n \"install.podman.autostart_title.zh\") text=\"正在配置 Podman 容器开机自启...\" ;;\n \"install.podman.autostart_title.en\") text=\"Configuring Podman container autostart...\" ;;\n \"install.podman.linger_enable.zh\") text=\"尝试启用 systemd linger (如有提示请在此输入 sudo 密码)...\" ;;\n \"install.podman.linger_enable.en\") text=\"Attempting to enable systemd linger (enter sudo password if prompted)...\" ;;\n \"install.podman.linger_warn.zh\") text=\"警告: 无法自动启用 linger。驻留可能失败,请手动执行: sudo loginctl enable-linger \\$(whoami)\" ;;\n \"install.podman.linger_warn.en\") text=\"WARNING: Failed to auto-enable linger. Please run manually: sudo loginctl enable-linger \\$(whoami)\" ;;\n \"install.podman.root_setup.zh\") text=\"检测到 root 用户。正在配置系统级 agentteams-podman-restart 服务...\" ;;\n \"install.podman.root_setup.en\") text=\"Root user detected. Configuring system-wide agentteams-podman-restart service...\" ;;\n \"install.podman.root_success.zh\") text=\"✅ 系统级 agentteams-podman-restart 服务已成功启用并运行。\" ;;\n \"install.podman.root_success.en\") text=\"✅ System-wide agentteams-podman-restart service successfully enabled and running.\" ;;\n \"install.podman.root_fail.zh\") text=\"⚠️ 系统级 agentteams-podman-restart 服务启用失败,请稍后手动检查。\" ;;\n \"install.podman.root_fail.en\") text=\"⚠️ Failed to enable system-wide agentteams-podman-restart service. Please check manually later.\" ;;\n \"install.podman.user_setup.zh\") text=\"检测到普通用户 (%s)。正在配置用户级 agentteams-podman-restart 服务...\" ;;\n \"install.podman.user_setup.en\") text=\"Non-root user (%s) detected. Configuring user-level agentteams-podman-restart service...\" ;;\n \"install.podman.user_success.zh\") text=\"✅ 用户级 agentteams-podman-restart 服务已成功启用并运行。\" ;;\n \"install.podman.user_success.en\") text=\"✅ User-level agentteams-podman-restart service successfully enabled and running.\" ;;\n \"install.podman.user_fail.zh\") text=\"⚠️ 用户级 agentteams-podman-restart 服务启用失败。\\n提示:这通常是因为缺少 XDG_RUNTIME_DIR 或 dbus 没有运行。您可以尝试手动执行: systemctl --user enable --now agentteams-podman-restart.service\" ;;\n \"install.podman.user_fail.en\") text=\"⚠️ Failed to enable user-level agentteams-podman-restart service.\\nHint: This is usually due to missing XDG_RUNTIME_DIR or dbus not running. Try manually: systemctl --user enable --now agentteams-podman-restart.service\" ;;\n \"install.podman.success.zh\") text=\"Podman 开机自启配置完成。\" ;;\n \"install.podman.success.en\") text=\"Podman autostart successfully configured.\" ;;\n # --- Worker idle timeout ---\n \"idle_timeout.prompt.zh\") text=\"Worker 空闲自动停止超时(分钟)[720]\" ;;\n \"idle_timeout.prompt.en\") text=\"Worker idle auto-stop timeout in minutes [720]\" ;;\n \"idle_timeout.selected.zh\") text=\"Worker 空闲超时: %s 分钟\" ;;\n \"idle_timeout.selected.en\") text=\"Worker idle timeout: %s minutes\" ;;\n \"idle_timeout.label.zh\") text=\"Worker 空闲超时(分钟)\" ;;\n \"idle_timeout.label.en\") text=\"Worker idle timeout (min)\" ;;\n # --- YOLO mode ---\n \"install.yolo.zh\") text=\"YOLO 模式已启用(自主决策,无交互提示)\" ;;\n \"install.yolo.en\") text=\"YOLO mode enabled (autonomous decisions, no interactive prompts)\" ;;\n # --- Image pulling ---\n \"install.image.exists.zh\") text=\"Manager 镜像已存在: %s\" ;;\n \"install.image.exists.en\") text=\"Manager image already exists locally: %s\" ;;\n \"install.image.pulling_manager.zh\") text=\"正在拉取 Manager 镜像: %s\" ;;\n \"install.image.pulling_manager.en\") text=\"Pulling Manager image: %s\" ;;\n \"install.image.worker_exists.zh\") text=\"Worker 镜像已存在: %s\" ;;\n \"install.image.worker_exists.en\") text=\"Worker image already exists locally: %s\" ;;\n \"install.image.pulling_worker.zh\") text=\"正在拉取 Worker 镜像: %s\" ;;\n \"install.image.pulling_worker.en\") text=\"Pulling Worker image: %s\" ;;\n # --- Starting container ---\n \"install.starting_manager.zh\") text=\"正在启动 Manager 容器...\" ;;\n \"install.starting_manager.en\") text=\"Starting Manager container...\" ;;\n # --- Wait for Manager ready ---\n \"install.wait_ready.zh\") text=\"等待 Manager Agent 就绪(超时: %ss)...\" ;;\n \"install.wait_ready.en\") text=\"Waiting for Manager agent to be ready (timeout: %ss)...\" ;;\n \"install.wait_ready.ok.zh\") text=\"Manager Agent 已就绪!\" ;;\n \"install.wait_ready.ok.en\") text=\"Manager agent is ready!\" ;;\n \"install.wait_ready.waiting.zh\") text=\"等待中... (%ds/%ds)\" ;;\n \"install.wait_ready.waiting.en\") text=\"Waiting... (%ds/%ds)\" ;;\n \"install.wait_ready.timeout.zh\") text=\"Manager Agent 在 %ss 内未就绪。请检查: docker logs %s\" ;;\n \"install.wait_ready.timeout.en\") text=\"Manager agent did not become ready within %ss. Check: docker logs %s\" ;;\n # --- Wait for Matrix ready ---\n \"install.wait_matrix.zh\") text=\"等待 Matrix 服务就绪(超时: %ss)...\" ;;\n \"install.wait_matrix.en\") text=\"Waiting for Matrix server to be ready (timeout: %ss)...\" ;;\n \"install.wait_matrix.ok.zh\") text=\"Matrix 服务已就绪!\" ;;\n \"install.wait_matrix.ok.en\") text=\"Matrix server is ready!\" ;;\n \"install.wait_matrix.waiting.zh\") text=\"等待 Matrix 中... (%ds/%ds)\" ;;\n \"install.wait_matrix.waiting.en\") text=\"Waiting for Matrix... (%ds/%ds)\" ;;\n \"install.wait_matrix.timeout.zh\") text=\"Matrix 服务在 %ss 内未就绪。请检查: docker logs %s\" ;;\n \"install.wait_matrix.timeout.en\") text=\"Matrix server did not become ready within %ss. Check: docker logs %s\" ;;\n # --- OpenAI-compatible connectivity test ---\n \"llm.openai.test.testing.zh\") text=\"正在测试 API 联通性...\" ;;\n \"llm.openai.test.testing.en\") text=\"Testing API connectivity...\" ;;\n \"llm.openai.test.ok.zh\") text=\"✅ API 联通性测试通过\" ;;\n \"llm.openai.test.ok.en\") text=\"✅ API connectivity test passed\" ;;\n \"llm.openai.test.fail.zh\") text=\"⚠️ API 联通性测试失败(HTTP %s)。响应内容:\\n%s\\n请根据以上错误信息联系您的模型服务商解决。\" ;;\n \"llm.openai.test.fail.en\") text=\"⚠️ API connectivity test failed (HTTP %s). Response body:\\n%s\\nPlease contact your model provider to resolve the issue.\" ;;\n \"llm.openai.test.fail.tokenplan.zh\") text=\"⚠️ 提示: 请确认 API Key 有效且已开通通义 Token 套餐。文档: https://help.aliyun.com/zh/model-studio/token-plan-quickstart\" ;;\n \"llm.openai.test.fail.tokenplan.en\") text=\"⚠️ Hint: Verify your Token Plan API key and compatible-mode access. Docs: https://help.aliyun.com/zh/model-studio/token-plan-quickstart\" ;;\n \"llm.openai.test.fail.codingplan.zh\") text=\"⚠️ 提示: 请确认 API Key 有效且已开通通义 Token 套餐。文档: https://help.aliyun.com/zh/model-studio/token-plan-quickstart\" ;;\n \"llm.openai.test.fail.codingplan.en\") text=\"⚠️ Hint: Verify your DASHSCOPE_API_KEY for Qwen Cloud. API keys: https://home.qwencloud.com/api-keys Docs: https://docs.qwencloud.com/\" ;;\n \"llm.openai.test.fail.codingplan_legacy.zh\") text=\"⚠️ 提示: 请确认 API Key 有效且 Coding 套餐接口可用。文档: https://help.aliyun.com/zh/model-studio/get-api-key\" ;;\n \"llm.openai.test.fail.codingplan_legacy.en\") text=\"⚠️ Hint: Verify your DashScope API key and Coding Plan access. Docs: https://help.aliyun.com/zh/model-studio/get-api-key\" ;;\n \"llm.openai.test.no_curl.zh\") text=\"⚠️ 未找到 curl,跳过 API 联通性测试\" ;;\n \"llm.openai.test.no_curl.en\") text=\"⚠️ curl not found, skipping API connectivity test\" ;;\n \"llm.openai.test.confirm.zh\") text=\"是否仍要继续安装?[y/N/b] \" ;;\n \"llm.openai.test.confirm.en\") text=\"Continue with installation anyway? [y/N/b] \" ;;\n \"llm.embedding.title.zh\") text=\"📦 记忆搜索配置\" ;;\n \"llm.embedding.title.en\") text=\"📦 Memory Search Configuration\" ;;\n \"llm.embedding.hint.zh\") text=\" Embedding 模型可提升记忆搜索质量(语义匹配)。不启用也可正常使用记忆功能(关键词匹配)。\" ;;\n \"llm.embedding.hint.en\") text=\" Embedding model improves memory search quality (semantic matching). Memory still works without it (keyword matching).\" ;;\n \"llm.embedding.option.default.zh\") text=\" 1) text-embedding-v4(推荐)\" ;;\n \"llm.embedding.option.default.en\") text=\" 1) text-embedding-v4 (Recommended)\" ;;\n \"llm.embedding.option.custom.zh\") text=\" 2) 自定义 Embedding 模型\" ;;\n \"llm.embedding.option.custom.en\") text=\" 2) Custom embedding model\" ;;\n \"llm.embedding.option.disable.zh\") text=\" 3) 不启用\" ;;\n \"llm.embedding.option.disable.en\") text=\" 3) Do not enable\" ;;\n \"llm.embedding.select.zh\") text=\"选择\" ;;\n \"llm.embedding.select.en\") text=\"Select\" ;;\n \"llm.embedding.custom_prompt.zh\") text=\" Embedding 模型名称\" ;;\n \"llm.embedding.custom_prompt.en\") text=\" Embedding model name\" ;;\n \"llm.embedding.test.testing.zh\") text=\"正在测试 Embedding API 联通性...\" ;;\n \"llm.embedding.test.testing.en\") text=\"Testing Embedding API connectivity...\" ;;\n \"llm.embedding.test.ok.zh\") text=\"✅ Embedding API 联通性测试通过\" ;;\n \"llm.embedding.test.ok.en\") text=\"✅ Embedding API connectivity test passed\" ;;\n \"llm.embedding.test.fail.zh\") text=\"⚠️ Embedding API 测试失败(HTTP %s)。响应: %s\" ;;\n \"llm.embedding.test.fail.en\") text=\"⚠️ Embedding API test failed (HTTP %s). Response: %s\" ;;\n \"llm.embedding.auto_disabled.zh\") text=\"⚠️ Embedding 已自动禁用,记忆搜索将使用关键词匹配。您可以稍后在 agentteams-manager.env 中设置 AGENTTEAMS_EMBEDDING_MODEL 启用。\" ;;\n \"llm.embedding.auto_disabled.en\") text=\"⚠️ Embedding auto-disabled. Memory search will use keyword matching. You can enable it later in agentteams-manager.env by setting AGENTTEAMS_EMBEDDING_MODEL.\" ;;\n \"llm.embedding.disabled.zh\") text=\"ℹ️ Embedding 已禁用,记忆搜索将使用关键词匹配。\" ;;\n \"llm.embedding.disabled.en\") text=\"ℹ️ Embedding disabled. Memory search will use keyword matching.\" ;;\n \"llm.openai.test.aborted.zh\") text=\"安装已中止。\" ;;\n \"llm.openai.test.aborted.en\") text=\"Installation aborted.\" ;;\n \"nav.back_hint.zh\") text=\"(输入 b 返回上一步)\" ;;\n \"nav.back_hint.en\") text=\"(enter b to go back)\" ;;\n # --- OpenAI-compatible provider creation ---\n \"install.openai_compat.missing.zh\") text=\"警告: OpenAI Base URL 或 API Key 未设置,跳过提供商创建\" ;;\n \"install.openai_compat.missing.en\") text=\"WARNING: OpenAI Base URL or API Key not set, skipping provider creation\" ;;\n \"install.openai_compat.creating.zh\") text=\"正在创建 OpenAI 兼容提供商...\" ;;\n \"install.openai_compat.creating.en\") text=\"Creating OpenAI-compatible provider...\" ;;\n \"install.openai_compat.domain.zh\") text=\" 域名: %s\" ;;\n \"install.openai_compat.domain.en\") text=\" Domain: %s\" ;;\n \"install.openai_compat.port.zh\") text=\" 端口: %s\" ;;\n \"install.openai_compat.port.en\") text=\" Port: %s\" ;;\n \"install.openai_compat.protocol.zh\") text=\" 协议: %s\" ;;\n \"install.openai_compat.protocol.en\") text=\" Protocol: %s\" ;;\n \"install.openai_compat.service_fail.zh\") text=\"警告: 创建 DNS 服务源失败(可能已存在)\" ;;\n \"install.openai_compat.service_fail.en\") text=\"WARNING: Failed to create DNS service source (may already exist)\" ;;\n \"install.openai_compat.provider_fail.zh\") text=\"警告: 创建 AI 提供商失败(可能已存在)\" ;;\n \"install.openai_compat.provider_fail.en\") text=\"WARNING: Failed to create AI provider (may already exist)\" ;;\n \"install.openai_compat.success.zh\") text=\"OpenAI 兼容提供商创建成功\" ;;\n \"install.openai_compat.success.en\") text=\"OpenAI-compatible provider created successfully\" ;;\n # --- Welcome message ---\n \"install.welcome_msg.soul_configured.zh\") text=\"Soul 已配置(找到 soul-configured 标记),跳过 onboarding 消息\" ;;\n \"install.welcome_msg.soul_configured.en\") text=\"Soul already configured (soul-configured marker found), skipping onboarding message\" ;;\n \"install.welcome_msg.logging_in.zh\") text=\"正在以 %s 身份登录以发送欢迎消息...\" ;;\n \"install.welcome_msg.logging_in.en\") text=\"Logging in as %s to send welcome message...\" ;;\n \"install.welcome_msg.login_failed.zh\") text=\"警告: 以 %s 身份登录失败,跳过欢迎消息\" ;;\n \"install.welcome_msg.login_failed.en\") text=\"WARNING: Failed to login as %s, skipping welcome message\" ;;\n \"install.welcome_msg.finding_room.zh\") text=\"正在查找与 Manager 的 DM 房间...\" ;;\n \"install.welcome_msg.finding_room.en\") text=\"Finding DM room with Manager...\" ;;\n \"install.welcome_msg.creating_room.zh\") text=\"正在创建与 Manager 的 DM 房间...\" ;;\n \"install.welcome_msg.creating_room.en\") text=\"Creating DM room with Manager...\" ;;\n \"install.welcome_msg.no_room.zh\") text=\"警告: 无法找到或创建与 Manager 的 DM 房间\" ;;\n \"install.welcome_msg.no_room.en\") text=\"WARNING: Could not find or create DM room with Manager\" ;;\n \"install.welcome_msg.waiting_join.zh\") text=\"等待 Manager 加入房间...\" ;;\n \"install.welcome_msg.waiting_join.en\") text=\"Waiting for Manager to join the room...\" ;;\n \"install.welcome_msg.sending.zh\") text=\"正在向 Manager 发送欢迎消息...\" ;;\n \"install.welcome_msg.sending.en\") text=\"Sending welcome message to Manager...\" ;;\n \"install.welcome_msg.send_failed.zh\") text=\"警告: 发送欢迎消息失败\" ;;\n \"install.welcome_msg.send_failed.en\") text=\"WARNING: Failed to send welcome message\" ;;\n \"install.welcome_msg.sent.zh\") text=\"欢迎消息已发送给 Manager\" ;;\n \"install.welcome_msg.sent.en\") text=\"Welcome message sent to Manager\" ;;\n \"install.welcome_msg.waiting.zh\") text=\"等待 Manager 发送欢迎消息(Higress 路由授权 + LLM 探活,约 45-90s)...\" ;;\n \"install.welcome_msg.waiting.en\") text=\"Waiting for Manager to send the welcome message (Higress route auth + LLM probe, ~45-90s)...\" ;;\n \"install.welcome_msg.confirmed.zh\") text=\"Manager 已确认发送欢迎消息(status.welcomeSent=true,用时 %ss)\" ;;\n \"install.welcome_msg.confirmed.en\") text=\"Manager confirmed welcome message sent (status.welcomeSent=true, %ss elapsed)\" ;;\n \"install.welcome_msg.timeout.zh\") text=\"警告: 在 %ss 内未观察到 Manager 发送欢迎消息(status.welcomeSent=true)。安装仍然成功,所有服务已就绪——可继续按下方提示登录 Element Web。\" ;;\n \"install.welcome_msg.timeout.en\") text=\"WARNING: Did not observe the Manager sending its welcome message (status.welcomeSent=true) within %ss. Installation is still successful, all services are up — continue with the Element Web instructions below.\" ;;\n \"install.welcome_msg.timeout_hint.zh\") text=\"手动触发 onboarding: 登录 Element Web → 打开与 Manager 的 DM 房间 → 发送任意一句话(例如 \\\"hi\\\"),Manager 会接管对话并开始引导。\" ;;\n \"install.welcome_msg.timeout_hint.en\") text=\"Manual onboarding: log in to Element Web → open the DM with the Manager → send any message (e.g. \\\"hi\\\") and the Manager will take over and start the guided setup.\" ;;\n \"install.welcome_msg.timeout_inspect.zh\") text=\"排查命令: docker exec agentteams-controller agt get managers default\" ;;\n \"install.welcome_msg.timeout_inspect.en\") text=\"Inspect status: docker exec agentteams-controller agt get managers default\" ;;\n \"install.welcome_msg.poll_unavailable.zh\") text=\"提示: agentteams-manager 内未找到 AgentTeams CLI,跳过 welcome 等待(旧镜像?)\" ;;\n \"install.welcome_msg.poll_unavailable.en\") text=\"Note: AgentTeams CLI not found inside agentteams-manager; skipping welcome wait (old image?)\" ;;\n # --- Final output panel ---\n \"success.title.zh\") text=\"=== AgentTeams Manager 已启动!===\" ;;\n \"success.title.en\") text=\"=== AgentTeams Manager Started! ===\" ;;\n \"success.domains_configured.zh\") text=\"以下域名已配置解析到 127.0.0.1:\" ;;\n \"success.domains_configured.en\") text=\"The following domains are configured to resolve to 127.0.0.1:\" ;;\n \"success.open_url.zh\") text=\" ★ 在浏览器中打开以下 URL 开始使用: ★\" ;;\n \"success.open_url.en\") text=\" ★ Open the following URL in your browser to start: ★\" ;;\n \"success.login_with.zh\") text=\" 登录信息:\" ;;\n \"success.login_with.en\") text=\" Login with:\" ;;\n \"success.username.zh\") text=\" 用户名: %s\" ;;\n \"success.username.en\") text=\" Username: %s\" ;;\n \"success.password.zh\") text=\" 密码: %s\" ;;\n \"success.password.en\") text=\" Password: %s\" ;;\n \"success.after_login.zh\") text=\" 登录后,开始与 Manager 聊天!\" ;;\n \"success.after_login.en\") text=\" After login, start chatting with the Manager!\" ;;\n \"success.tell_it.zh\") text=\" 告诉它: \\\"创建一个名为 alice 的前端开发 Worker\\\"\" ;;\n \"success.tell_it.en\") text=\" Tell it: \\\"Create a Worker named alice for frontend dev\\\"\" ;;\n \"success.manager_auto.zh\") text=\" Manager 会自动处理一切。\" ;;\n \"success.manager_auto.en\") text=\" The Manager will handle everything automatically.\" ;;\n \"success.mobile_title.zh\") text=\" 📱 移动端访问(FluffyChat / Element Mobile):\" ;;\n \"success.mobile_title.en\") text=\" 📱 Mobile access (FluffyChat / Element Mobile):\" ;;\n \"success.mobile_step1.zh\") text=\" 1. 在手机上下载 FluffyChat 或 Element\" ;;\n \"success.mobile_step1.en\") text=\" 1. Download FluffyChat or Element on your phone\" ;;\n \"success.mobile_step2.zh\") text=\" 2. 设置 homeserver 为: %s\" ;;\n \"success.mobile_step2.en\") text=\" 2. Set homeserver to: %s\" ;;\n \"success.mobile_step2_noip.zh\") text=\" 2. 设置 homeserver 为: http://<本机局域网IP>:%s\" ;;\n \"success.mobile_step2_noip.en\") text=\" 2. Set homeserver to: http://<this-machine-LAN-IP>:%s\" ;;\n \"success.mobile_noip_hint.zh\") text=\" (无法自动检测局域网 IP — 请使用 ifconfig / ip addr 查看)\" ;;\n \"success.mobile_noip_hint.en\") text=\" (Could not detect LAN IP automatically — check with: ifconfig / ip addr)\" ;;\n \"success.mobile_step3.zh\") text=\" 3. 登录信息:\" ;;\n \"success.mobile_step3.en\") text=\" 3. Login with:\" ;;\n \"success.mobile_username.zh\") text=\" 用户名: %s\" ;;\n \"success.mobile_username.en\") text=\" Username: %s\" ;;\n \"success.mobile_password.zh\") text=\" 密码: %s\" ;;\n \"success.mobile_password.en\") text=\" Password: %s\" ;;\n # --- Other consoles and tips ---\n \"success.other_consoles.zh\") text=\"--- 其他控制台 ---\" ;;\n \"success.other_consoles.en\") text=\"--- Other Consoles ---\" ;;\n \"success.higress_console.zh\") text=\" Higress 控制台: http://localhost:%s(用户名: %s / 密码: %s)\" ;;\n \"success.higress_console.en\") text=\" Higress Console: http://localhost:%s (Username: %s / Password: %s)\" ;;\n \"success.manager_console.zh\") text=\" Manager 控制台(本地): http://localhost:%s(无需登录)\" ;;\n \"success.manager_console.en\") text=\" Manager Console (local): http://localhost:%s (no login required)\" ;;\n \"success.manager_console_gateway.zh\") text=\" Manager 控制台(网关): http://console-local.agentteams.io(用户名: %s / 密码: %s)\" ;;\n \"success.manager_console_gateway.en\") text=\" Manager Console (gateway): http://console-local.agentteams.io (Username: %s / Password: %s)\" ;;\n \"success.copaw_console.zh\") text=\" QwenPaw App API: http://localhost:%s(无需登录)\" ;;\n \"success.copaw_console.en\") text=\" QwenPaw App API: http://localhost:%s (no login required)\" ;;\n \"success.switch_llm.title.zh\") text=\"--- 切换 LLM 提供商 ---\" ;;\n \"success.switch_llm.title.en\") text=\"--- Switch LLM Providers ---\" ;;\n \"success.switch_llm.hint.zh\") text=\" 您可以通过 Higress 控制台切换到其他 LLM 提供商(OpenAI、Anthropic 等)。\" ;;\n \"success.switch_llm.hint.en\") text=\" You can switch to other LLM providers (OpenAI, Anthropic, etc.) via Higress Console.\" ;;\n \"success.switch_llm.docs.zh\") text=\" 详细说明请参阅:\" ;;\n \"success.switch_llm.docs.en\") text=\" For detailed instructions, see:\" ;;\n \"success.switch_llm.url.zh\") text=\" https://higress.ai/en/docs/ai/scene-guide/multi-proxy#console-configuration\" ;;\n \"success.switch_llm.url.en\") text=\" https://higress.ai/en/docs/ai/scene-guide/multi-proxy#console-configuration\" ;;\n \"success.tip.zh\") text=\"提示: 您也可以在聊天中让 Manager 为您配置 LLM 提供商。\" ;;\n \"success.tip.en\") text=\"Tip: You can also ask the Manager to configure LLM providers for you in the chat.\" ;;\n \"success.config_file.zh\") text=\"配置文件: %s\" ;;\n \"success.config_file.en\") text=\"Configuration file: %s\" ;;\n \"success.data_volume.zh\") text=\"数据卷: %s\" ;;\n \"success.data_volume.en\") text=\"Data volume: %s\" ;;\n \"success.workspace.zh\") text=\"Manager 工作空间: %s\" ;;\n \"success.workspace.en\") text=\"Manager workspace: %s\" ;;\n # --- Worker installation ---\n \"worker.resetting.zh\") text=\"正在重置 Worker: %s...\" ;;\n \"worker.resetting.en\") text=\"Resetting Worker: %s...\" ;;\n \"worker.exists.zh\") text=\"容器 '%s' 已存在。使用 --reset 重新创建。\" ;;\n \"worker.exists.en\") text=\"Container '%s' already exists. Use --reset to recreate.\" ;;\n \"worker.starting.zh\") text=\"正在启动 Worker: %s...\" ;;\n \"worker.starting.en\") text=\"Starting Worker: %s...\" ;;\n \"worker.skills_url.zh\") text=\" Skills API URL: %s\" ;;\n \"worker.skills_url.en\") text=\" Skills API URL: %s\" ;;\n \"worker.started.zh\") text=\"=== Worker %s 已启动!===\" ;;\n \"worker.started.en\") text=\"=== Worker %s Started! ===\" ;;\n \"worker.container.zh\") text=\"容器: %s\" ;;\n \"worker.container.en\") text=\"Container: %s\" ;;\n \"worker.view_logs.zh\") text=\"查看日志: docker logs -f %s\" ;;\n \"worker.view_logs.en\") text=\"View logs: docker logs -f %s\" ;;\n # --- Prompt function messages ---\n \"prompt.preset.zh\") text=\" %s = (已通过环境变量预设)\" ;;\n \"prompt.preset.en\") text=\" %s = (pre-set via env)\" ;;\n \"prompt.upgrade_keep.zh\") text=\" %s = %s(当前值,回车保留 / 输入新值覆盖)\" ;;\n \"prompt.upgrade_keep.en\") text=\" %s = %s (current value, press Enter to keep / type new value to change)\" ;;\n \"prompt.upgrade_keep_secret.zh\") text=\" %s = %s(当前值,回车保留 / 输入新值覆盖)\" ;;\n \"prompt.upgrade_keep_secret.en\") text=\" %s = %s (current value, press Enter to keep / type new value to change)\" ;;\n \"prompt.upgrade_empty.zh\") text=\" %s = (未设置,回车跳过 / 输入新值设置)\" ;;\n \"prompt.upgrade_empty.en\") text=\" %s = (not set, press Enter to skip / type new value to set)\" ;;\n \"prompt.default.zh\") text=\" %s = %s(默认)\" ;;\n \"prompt.default.en\") text=\" %s = %s (default)\" ;;\n \"prompt.required.zh\") text=\"%s 是必需的(在非交互模式下通过环境变量设置)\" ;;\n \"prompt.required.en\") text=\"%s is required (set via environment variable in non-interactive mode)\" ;;\n \"prompt.required_empty.zh\") text=\"%s 是必需的\" ;;\n \"prompt.required_empty.en\") text=\"%s is required\" ;;\n # --- Language switch prompt (bilingual by design) ---\n \"lang.detected.zh\") text=\"检测到语言 / Detected language: 中文\" ;;\n \"lang.detected.en\") text=\"检测到语言 / Detected language: English\" ;;\n \"lang.switch_title.zh\") text=\"切换语言 / Switch language:\" ;;\n \"lang.switch_title.en\") text=\"切换语言 / Switch language:\" ;;\n \"lang.option_zh.zh\") text=\" 1) 中文\" ;;\n \"lang.option_zh.en\") text=\" 1) 中文\" ;;\n \"lang.option_en.zh\") text=\" 2) English\" ;;\n \"lang.option_en.en\") text=\" 2) English\" ;;\n \"lang.prompt.zh\") text=\"请选择 / Enter choice\" ;;\n \"lang.prompt.en\") text=\"请选择 / Enter choice\" ;;\n # --- Uninstall messages ---\n \"uninstall.title.zh\") text=\"正在卸载 AgentTeams...\" ;;\n \"uninstall.title.en\") text=\"Uninstalling AgentTeams...\" ;;\n \"uninstall.stopping_manager.zh\") text=\"正在停止并移除 agentteams-manager...\" ;;\n \"uninstall.stopping_manager.en\") text=\"Stopping and removing agentteams-manager...\" ;;\n \"uninstall.stopping_workers.zh\") text=\"正在停止并移除 Worker 容器...\" ;;\n \"uninstall.stopping_workers.en\") text=\"Stopping and removing worker containers...\" ;;\n \"uninstall.removed.zh\") text=\" 已移除: %s\" ;;\n \"uninstall.removed.en\") text=\" Removed: %s\" ;;\n \"uninstall.removing_volume.zh\") text=\"正在移除 Docker 卷: %s\" ;;\n \"uninstall.removing_volume.en\") text=\"Removing Docker volume: %s\" ;;\n \"uninstall.removing_env.zh\") text=\"正在移除 env 文件: %s\" ;;\n \"uninstall.removing_env.en\") text=\"Removing env file: %s\" ;;\n \"uninstall.removing_proxy.zh\") text=\"正在停止并移除 Docker API 代理容器: agentteams-docker-proxy\" ;;\n \"uninstall.removing_proxy.en\") text=\"Stopping and removing Docker API proxy container: agentteams-docker-proxy\" ;;\n \"uninstall.stopping_controller.zh\") text=\"正在停止并移除 agentteams-controller (内嵌 Tuwunel/MinIO/Higress)...\" ;;\n \"uninstall.stopping_controller.en\") text=\"Stopping and removing agentteams-controller (embedded Tuwunel/MinIO/Higress)...\" ;;\n \"uninstall.removing_network.zh\") text=\"正在移除 Docker 网络: agentteams-net\" ;;\n \"uninstall.removing_network.en\") text=\"Removing Docker network: agentteams-net\" ;;\n \"uninstall.removing_workspace.zh\") text=\"正在移除工作空间目录: %s\" ;;\n \"uninstall.removing_workspace.en\") text=\"Removing workspace directory: %s\" ;;\n \"uninstall.removing_workspace_elevated.zh\") text=\" 工作空间包含 root 文件,通过容器清理...\" ;;\n \"uninstall.removing_workspace_elevated.en\") text=\" Workspace contains root-owned files, cleaning via container...\" ;;\n \"uninstall.removing_log.zh\") text=\"正在移除日志文件: %s\" ;;\n \"uninstall.removing_log.en\") text=\"Removing log file: %s\" ;;\n \"uninstall.done.zh\") text=\"AgentTeams 已卸载。\" ;;\n \"uninstall.done.en\") text=\"AgentTeams has been uninstalled.\" ;;\n # --- Error messages ---\n \"error.name_required.zh\") text=\"--name 是必需的\" ;;\n \"error.name_required.en\") text=\"--name is required\" ;;\n \"error.fs_required.zh\") text=\"--fs 是必需的\" ;;\n \"error.fs_required.en\") text=\"--fs is required\" ;;\n \"error.fs_key_required.zh\") text=\"--fs-key 是必需的\" ;;\n \"error.fs_key_required.en\") text=\"--fs-key is required\" ;;\n \"error.fs_secret_required.zh\") text=\"--fs-secret 是必需的\" ;;\n \"error.fs_secret_required.en\") text=\"--fs-secret is required\" ;;\n \"error.unknown_option.zh\") text=\"未知选项: %s\" ;;\n \"error.unknown_option.en\") text=\"Unknown option: %s\" ;;\n \"error.docker_not_found.zh\") text=\"未找到 docker 或 podman 命令。请先安装 Docker Desktop 或 Podman Desktop:\\n Docker Desktop: https://www.docker.com/products/docker-desktop/\\n Podman Desktop: https://podman-desktop.io/\" ;;\n \"error.docker_not_found.en\") text=\"docker or podman command not found. Please install Docker Desktop or Podman Desktop first:\\n Docker Desktop: https://www.docker.com/products/docker-desktop/\\n Podman Desktop: https://podman-desktop.io/\" ;;\n \"error.docker_not_running.zh\") text=\"Docker 未运行。请先启动 Docker Desktop 或 Podman Desktop。\" ;;\n \"error.docker_not_running.en\") text=\"Docker is not running. Please start Docker Desktop or Podman Desktop first.\" ;;\n # --- Fallback: try English for unknown lang ---\n *)\n case \"${key}.en\" in\n \"tz.warning.title.en\") text=\"Could not detect timezone automatically.\" ;;\n \"install.title.en\") text=\"=== AgentTeams Manager Installation ===\" ;;\n *) text=\"${key}\" ;;\n esac\n ;;\n esac\n if [ $# -gt 0 ]; then\n # shellcheck disable=SC2059\n printf \"${text}\\n\" \"$@\"\n else\n echo \"${text}\"\n fi\n}\n\n# ============================================================\n# Registry selection based on timezone\n# ============================================================\n\ndetect_registry() {\n local tz=\"${AGENTTEAMS_TIMEZONE}\"\n\n case \"${tz}\" in\n America/*)\n echo \"higress-registry.us-west-1.cr.aliyuncs.com\"\n ;;\n Asia/Singapore|Asia/Bangkok|Asia/Jakarta|Asia/Makassar|Asia/Jayapura|\\\n Asia/Kuala_Lumpur|Asia/Ho_Chi_Minh|Asia/Manila|Asia/Yangon|\\\n Asia/Vientiane|Asia/Phnom_Penh|Asia/Pontianak|Asia/Ujung_Pandang)\n echo \"higress-registry.ap-southeast-7.cr.aliyuncs.com\"\n ;;\n *)\n echo \"higress-registry.cn-hangzhou.cr.aliyuncs.com\"\n ;;\n esac\n}\n\nAGENTTEAMS_REGISTRY=\"${AGENTTEAMS_REGISTRY:-$(detect_registry)}\"\n# Image variables are resolved after version selection in step_version().\n# These placeholders allow early code paths to reference them without errors.\nMANAGER_IMAGE=\"${AGENTTEAMS_INSTALL_MANAGER_IMAGE:-}\"\nMANAGER_COPAW_IMAGE=\"${AGENTTEAMS_INSTALL_MANAGER_COPAW_IMAGE:-}\"\nWORKER_IMAGE=\"${AGENTTEAMS_INSTALL_WORKER_IMAGE:-}\"\nCOPAW_WORKER_IMAGE=\"${AGENTTEAMS_INSTALL_COPAW_WORKER_IMAGE:-}\"\nHERMES_WORKER_IMAGE=\"${AGENTTEAMS_INSTALL_HERMES_WORKER_IMAGE:-}\"\nCONTROLLER_IMAGE=\"${AGENTTEAMS_INSTALL_CONTROLLER_IMAGE:-}\"\n\nresolve_image_tags() {\n MANAGER_IMAGE=\"${AGENTTEAMS_INSTALL_MANAGER_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-manager:${AGENTTEAMS_VERSION}}\"\n MANAGER_COPAW_IMAGE=\"${AGENTTEAMS_INSTALL_MANAGER_COPAW_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-manager-copaw:${AGENTTEAMS_VERSION}}\"\n WORKER_IMAGE=\"${AGENTTEAMS_INSTALL_WORKER_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-worker:${AGENTTEAMS_VERSION}}\"\n COPAW_WORKER_IMAGE=\"${AGENTTEAMS_INSTALL_COPAW_WORKER_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-copaw-worker:${AGENTTEAMS_VERSION}}\"\n HERMES_WORKER_IMAGE=\"${AGENTTEAMS_INSTALL_HERMES_WORKER_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-hermes-worker:${AGENTTEAMS_VERSION}}\"\n EMBEDDED_IMAGE=\"${AGENTTEAMS_INSTALL_EMBEDDED_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-embedded:${AGENTTEAMS_VERSION}}\"\n # CoPaw Worker introduced in v1.0.4; Hermes Worker introduced in v1.1.0\n if [ -z \"${AGENTTEAMS_INSTALL_COPAW_WORKER_IMAGE:-}\" ] && _ver_lt \"${AGENTTEAMS_VERSION}\" \"v1.0.4\"; then\n COPAW_WORKER_IMAGE=\"\"\n fi\n if [ -z \"${AGENTTEAMS_INSTALL_HERMES_WORKER_IMAGE:-}\" ] && _ver_lt \"${AGENTTEAMS_VERSION}\" \"v1.1.0\"; then\n HERMES_WORKER_IMAGE=\"\"\n fi\n}\n\n# Resolve the embedded controller image. Embedded mode is the only supported\n# architecture since PR #616 (manager image no longer bundles Higress/Tuwunel/MinIO).\n# If the embedded image is unavailable for the requested version, fail fast with an\n# actionable error rather than silently falling back to the legacy single-container\n# path — that path is permanently broken with the slim manager image and would just\n# leave the user with a manager container looping on \"Waiting for Higress Gateway\".\n# Sets EMBEDDED_IMAGE and AGENTTEAMS_USE_EMBEDDED.\nresolve_embedded_image() {\n AGENTTEAMS_USE_EMBEDDED=1\n\n # If the user explicitly overrode the image (e.g. `make install-embedded` passes\n # a locally-built tag), respect it as-is without any registry probe.\n if [ -n \"${AGENTTEAMS_INSTALL_EMBEDDED_IMAGE:-}\" ]; then\n EMBEDDED_IMAGE=\"${AGENTTEAMS_INSTALL_EMBEDDED_IMAGE}\"\n return 0\n fi\n\n local _versioned=\"${AGENTTEAMS_REGISTRY}/agentteams/agentteams-embedded:${AGENTTEAMS_VERSION}\"\n local _latest=\"${AGENTTEAMS_REGISTRY}/agentteams/agentteams-embedded:latest\"\n\n # Skip probe when AGENTTEAMS_VERSION is \"latest\" — no point trying the same tag twice.\n if [ \"${AGENTTEAMS_VERSION}\" = \"latest\" ]; then\n EMBEDDED_IMAGE=\"${_latest}\"\n return 0\n fi\n\n if ${DOCKER_CMD} pull \"${_versioned}\" >/dev/null 2>&1; then\n EMBEDDED_IMAGE=\"${_versioned}\"\n return 0\n fi\n\n # Versions before v1.1.0 predate agentteams-embedded entirely — their manager image\n # bundled all infrastructure. Falling back to agentteams-embedded:latest would\n # silently swap in the v1.1.0 architecture (embedded kube-apiserver) which\n # crashes under QEMU on Apple Silicon. Auto-activate legacy mode instead.\n if _ver_lt \"${AGENTTEAMS_VERSION}\" \"v1.1.0\"; then\n log \"INFO: ${AGENTTEAMS_VERSION} predates agentteams-embedded; switching to legacy all-in-one manager architecture.\"\n log \"WARNING: Legacy all-in-one mode requires AGENTTEAMS_VERSION <= v1.0.9 (older bundled manager image).\"\n log \"WARNING: Newer slim manager images will hang on 'Waiting for Higress Gateway'.\"\n AGENTTEAMS_USE_EMBEDDED=0\n return 0\n fi\n\n if ${DOCKER_CMD} pull \"${_latest}\" >/dev/null 2>&1; then\n log \"embedded ${AGENTTEAMS_VERSION} not found, using latest\"\n EMBEDDED_IMAGE=\"${_latest}\"\n return 0\n fi\n\n # Explicit escape hatch — still honoured for edge cases.\n if [ \"${AGENTTEAMS_FORCE_LEGACY:-0}\" = \"1\" ]; then\n log \"WARNING: AGENTTEAMS_FORCE_LEGACY=1 — using legacy all-in-one manager architecture.\"\n log \"WARNING: This requires AGENTTEAMS_VERSION <= v1.0.9 (older bundled manager image).\"\n log \"WARNING: Newer slim manager images will hang on 'Waiting for Higress Gateway'.\"\n AGENTTEAMS_USE_EMBEDDED=0\n return 0\n fi\n\n error \"Embedded controller image is not available in the registry:\"\n error \" - tried: ${_versioned}\"\n error \" - tried: ${_latest}\"\n error \"\"\n error \"Embedded mode is the only supported architecture since PR #616.\"\n error \"How to resolve:\"\n error \" 1) Pin to a AGENTTEAMS_VERSION whose embedded image has been published, or\"\n error \" wait for the release pipeline to publish it.\"\n error \" 2) For a local build, run: make install-embedded\"\n error \" (builds and uses the local embedded image without touching the registry).\"\n error \" 3) Override with a custom image: AGENTTEAMS_INSTALL_EMBEDDED_IMAGE=... ./agentteams-install.sh\"\n exit 1\n}\n\n# ============================================================\n# Known models list — used to detect custom models during install\n# ============================================================\nKNOWN_MODELS=\"gpt-5.4 gpt-5.3-codex gpt-5-mini gpt-5-nano claude-opus-4-6 claude-sonnet-4-6 claude-haiku-4-5 qwen3.6-plus qwen3.5-plus deepseek-chat deepseek-reasoner kimi-k2.5 glm-5 MiniMax-M2.7 MiniMax-M2.7-highspeed MiniMax-M2.5\"\n\nis_known_model() {\n local model=\"$1\"\n for m in ${KNOWN_MODELS}; do\n [ \"${m}\" = \"${model}\" ] && return 0\n done\n return 1\n}\n\n# Prompt user for custom model parameters when model is not in the known list.\n# Sets: AGENTTEAMS_MODEL_CONTEXT_WINDOW, AGENTTEAMS_MODEL_MAX_TOKENS, AGENTTEAMS_MODEL_REASONING, AGENTTEAMS_MODEL_VISION\nprompt_custom_model_params() {\n local model=\"$1\"\n if is_known_model \"${model}\"; then\n # Clear any stale custom params for known models\n AGENTTEAMS_MODEL_CONTEXT_WINDOW=\"\"\n AGENTTEAMS_MODEL_MAX_TOKENS=\"\"\n AGENTTEAMS_MODEL_REASONING=\"\"\n AGENTTEAMS_MODEL_VISION=\"\"\n return\n fi\n echo \"\"\n log \"$(msg llm.custom_model.detected \"${model}\")\"\n echo \"\"\n read -e -p \" $(msg llm.custom_model.context_prompt): \" AGENTTEAMS_MODEL_CONTEXT_WINDOW\n if [ \"${AGENTTEAMS_MODEL_CONTEXT_WINDOW}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n AGENTTEAMS_MODEL_CONTEXT_WINDOW=\"${AGENTTEAMS_MODEL_CONTEXT_WINDOW:-150000}\"\n read -e -p \" $(msg llm.custom_model.max_tokens_prompt): \" AGENTTEAMS_MODEL_MAX_TOKENS\n if [ \"${AGENTTEAMS_MODEL_MAX_TOKENS}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n AGENTTEAMS_MODEL_MAX_TOKENS=\"${AGENTTEAMS_MODEL_MAX_TOKENS:-128000}\"\n read -e -p \" $(msg llm.custom_model.reasoning_prompt): \" _reasoning\n if [ \"${_reasoning}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n case \"${_reasoning}\" in\n n|N|no|NO) AGENTTEAMS_MODEL_REASONING=\"false\" ;;\n *) AGENTTEAMS_MODEL_REASONING=\"true\" ;;\n esac\n read -e -p \" $(msg llm.custom_model.vision_prompt): \" _vision\n if [ \"${_vision}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n case \"${_vision}\" in\n y|Y|yes|YES) AGENTTEAMS_MODEL_VISION=\"true\" ;;\n *) AGENTTEAMS_MODEL_VISION=\"false\" ;;\n esac\n log \"$(msg llm.custom_model.summary \"${AGENTTEAMS_MODEL_CONTEXT_WINDOW}\" \"${AGENTTEAMS_MODEL_MAX_TOKENS}\" \"${AGENTTEAMS_MODEL_REASONING}\" \"${AGENTTEAMS_MODEL_VISION}\")\"\n}\n\n# ============================================================\n# Wait for Manager agent to be ready\n# Uses `openclaw gateway health` inside the container to confirm the gateway is running\n# ============================================================\n\nwait_manager_ready() {\n local timeout=\"${AGENTTEAMS_READY_TIMEOUT:-300}\"\n local elapsed=0\n local container=\"${1:-agentteams-manager}\"\n\n log \"$(msg install.wait_ready \"${timeout}\")\"\n\n # Wait for Manager agent to be healthy inside the container\n local runtime=\"${AGENTTEAMS_MANAGER_RUNTIME:-copaw}\"\n while [ \"${elapsed}\" -lt \"${timeout}\" ]; do\n case \"${runtime}\" in\n copaw)\n if ${DOCKER_CMD} exec \"${container}\" curl -sf http://127.0.0.1:18799/api/agents 2>/dev/null | grep -q '\"agents\"'; then\n log \"$(msg install.wait_ready.ok)\"\n return 0\n fi\n ;;\n *)\n if ${DOCKER_CMD} exec \"${container}\" openclaw gateway health --json 2>/dev/null | grep -q '\"ok\"' 2>/dev/null; then\n log \"$(msg install.wait_ready.ok)\"\n return 0\n fi\n ;;\n esac\n sleep 5\n elapsed=$((elapsed + 5))\n printf \"\\r\\033[36m[AgentTeams]\\033[0m $(msg install.wait_ready.waiting \"${elapsed}\" \"${timeout}\")\"\n done\n\n echo \"\"\n die \"$(msg install.wait_ready.timeout \"${timeout}\" \"${container}\")\"\n}\n\nwait_matrix_ready() {\n local timeout=\"${AGENTTEAMS_READY_TIMEOUT:-300}\"\n local elapsed=0\n local container=\"${1:-agentteams-manager}\"\n\n log \"$(msg install.wait_matrix \"${timeout}\")\"\n\n while [ \"${elapsed}\" -lt \"${timeout}\" ]; do\n if ${DOCKER_CMD} exec \"${container}\" curl -sf http://127.0.0.1:6167/_tuwunel/server_version >/dev/null 2>&1; then\n log \"$(msg install.wait_matrix.ok)\"\n return 0\n fi\n sleep 5\n elapsed=$((elapsed + 5))\n printf \"\\r\\033[36m[AgentTeams]\\033[0m $(msg install.wait_matrix.waiting \"${elapsed}\" \"${timeout}\")\"\n done\n\n echo \"\"\n die \"$(msg install.wait_matrix.timeout \"${timeout}\" \"${container}\")\"\n}\n\n# Read KEY=value from /data/agentteams-secrets.env on a Docker volume (manager container not required).\n# Requires EMBEDDED_IMAGE (resolved earlier in install_manager). Uses ${DOCKER_CMD}.\nagentteams_read_secret_from_data_volume() {\n local _vol=\"$1\" _key=\"$2\"\n if [ -z \"${_vol}\" ] || [ -z \"${_key}\" ] || [ -z \"${EMBEDDED_IMAGE:-}\" ]; then\n echo \"\"\n return 0\n fi\n ${DOCKER_CMD} run --rm --entrypoint sh \\\n -v \"${_vol}:/data:ro\" \\\n \"${EMBEDDED_IMAGE}\" -c \"grep \\\"^${_key}=\\\" /data/agentteams-secrets.env 2>/dev/null | cut -d= -f2- | head -1 | tr -d '\\r'\" 2>/dev/null\n}\n\n# Read KEY=value from /data/worker-creds/<worker>.env on a Docker volume.\nagentteams_read_worker_creds_value_from_volume() {\n local _vol=\"$1\" _worker=\"$2\" _key=\"$3\"\n if [ -z \"${_vol}\" ] || [ -z \"${_worker}\" ] || [ -z \"${_key}\" ] || [ -z \"${EMBEDDED_IMAGE:-}\" ]; then\n echo \"\"\n return 0\n fi\n ${DOCKER_CMD} run --rm --entrypoint sh \\\n -v \"${_vol}:/data:ro\" \\\n \"${EMBEDDED_IMAGE}\" -c \"grep \\\"^${_key}=\\\" \\\"/data/worker-creds/${_worker}.env\\\" 2>/dev/null | cut -d= -f2- | head -1 | tr -d \\\"\\\\r\\\"\" 2>/dev/null\n}\n\n# Read admin_dm_room_id from host workspace state.json (fallback when Matrix API is unavailable).\nagentteams_read_admin_dm_room_from_workspace() {\n local _ws=\"$1\"\n local _f=\"${_ws}/state.json\"\n if [ ! -f \"${_f}\" ] || ! command -v jq >/dev/null 2>&1; then\n echo \"\"\n return 0\n fi\n jq -r '.admin_dm_room_id // empty | select(. != \"null\")' \"${_f}\" 2>/dev/null\n}\n\n# Read secret input with masked echo (shows * per keystroke, supports backspace)\n# Usage: read_secret \"prompt text: \"; value=\"${_RS_RESULT}\"\nread_secret() {\n local _rs_prompt=\"$1\"\n _RS_RESULT=\"\"\n local _rs_char=\"\"\n\n printf \"%s\" \"${_rs_prompt}\"\n\n while IFS= read -r -s -n 1 _rs_char; do\n if [[ -z \"${_rs_char}\" ]]; then\n break\n elif [[ \"${_rs_char}\" == $'\\177' ]] || [[ \"${_rs_char}\" == $'\\b' ]]; then\n if [ -n \"${_RS_RESULT}\" ]; then\n _RS_RESULT=\"${_RS_RESULT%?}\"\n printf \"\\b \\b\"\n fi\n else\n _RS_RESULT=\"${_RS_RESULT}${_rs_char}\"\n printf \"*\"\n fi\n done\n echo\n}\n\n# Load current parameter values from env file for upgrade mode display\nload_current_params_from_env() {\n local env_file=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n if [ -f \"${env_file}\" ]; then\n [ -z \"${AGENTTEAMS_LLM_PROVIDER:+x}\" ] && AGENTTEAMS_LLM_PROVIDER=\"$(grep '^AGENTTEAMS_LLM_PROVIDER=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_OPENAI_BASE_URL:+x}\" ] && AGENTTEAMS_OPENAI_BASE_URL=\"$(grep '^AGENTTEAMS_OPENAI_BASE_URL=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_DEFAULT_MODEL:+x}\" ] && AGENTTEAMS_DEFAULT_MODEL=\"$(grep '^AGENTTEAMS_DEFAULT_MODEL=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_EMBEDDING_MODEL:+x}\" ] && AGENTTEAMS_EMBEDDING_MODEL=\"$(grep '^AGENTTEAMS_EMBEDDING_MODEL=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_WORKSPACE_DIR:+x}\" ] && AGENTTEAMS_WORKSPACE_DIR=\"$(grep '^AGENTTEAMS_WORKSPACE_DIR=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_HOST_SHARE_DIR:+x}\" ] && AGENTTEAMS_HOST_SHARE_DIR=\"$(grep '^AGENTTEAMS_HOST_SHARE_DIR=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_PODMAN_AUTOSTART:+x}\" ] && AGENTTEAMS_PODMAN_AUTOSTART=\"$(grep '^AGENTTEAMS_PODMAN_AUTOSTART=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n fi\n}\n\n# In non-interactive mode, uses default or errors if required and no default.\n# Usage: prompt VAR_NAME \"Prompt text\" \"default\" [true=secret]\nprompt() {\n local var_name=\"$1\"\n local prompt_text=\"$2\"\n local default_value=\"$3\"\n local is_secret=\"${4:-false}\"\n\n # If the variable is already set in the environment, use it silently\n # In upgrade mode, show current value and let user change it\n eval \"local current_value=\\\"\\${${var_name}}\\\"\"\n if [ -n \"${current_value}\" ]; then\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_NON_INTERACTIVE}\" != \"1\" ]; then\n # Show masked value for secrets, full value otherwise\n local display_value=\"${current_value}\"\n if [ \"${is_secret}\" = \"true\" ]; then\n local len=${#current_value}\n if [ \"${len}\" -le 8 ]; then\n display_value=\"****\"\n else\n display_value=\"${current_value:0:4}****${current_value: -4}\"\n fi\n fi\n log \"$(msg prompt.upgrade_keep \"${prompt_text}\" \"${display_value}\")\"\n local new_value=\"\"\n if [ \"${is_secret}\" = \"true\" ]; then\n read_secret \"${prompt_text}: \"\n new_value=\"${_RS_RESULT}\"\n else\n read -e -p \"${prompt_text}: \" new_value\n if [ \"${new_value}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n fi\n if [ -n \"${new_value}\" ]; then\n eval \"export ${var_name}='${new_value}'\"\n fi\n return\n fi\n log \"$(msg prompt.preset \"${prompt_text}\")\"\n return\n fi\n\n # Non-interactive or quickstart: use default or error\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] || [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ]; then\n if [ -n \"${default_value}\" ]; then\n eval \"export ${var_name}='${default_value}'\"\n log \"$(msg prompt.default \"${prompt_text}\" \"${default_value}\")\"\n return\n elif [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n # Only hard-error in fully non-interactive mode, not quickstart\n die \"$(msg prompt.required \"${prompt_text}\")\"\n fi\n # quickstart + no default: fall through to interactive prompt below\n fi\n\n if [ -n \"${default_value}\" ]; then\n prompt_text=\"${prompt_text} [${default_value}]\"\n fi\n\n local value=\"\"\n if [ \"${is_secret}\" = \"true\" ]; then\n read_secret \"${prompt_text}: \"\n value=\"${_RS_RESULT}\"\n else\n read -e -p \"${prompt_text}: \" value\n if [ \"${value}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n fi\n\n value=\"${value:-${default_value}}\"\n if [ -z \"${value}\" ]; then\n die \"$(msg prompt.required_empty \"${prompt_text}\")\"\n fi\n\n eval \"export ${var_name}='${value}'\"\n}\n\n# Prompt for an optional value (empty string is acceptable)\n# Skips prompt if variable is already defined in environment (even if empty)\n# In upgrade mode, shows current value and lets user change it.\n# In non-interactive mode, defaults to empty string.\nprompt_optional() {\n local var_name=\"$1\"\n local prompt_text=\"$2\"\n local is_secret=\"${3:-false}\"\n\n # Check if variable is defined (even if set to empty string)\n eval \"local _chk=\\\"\\${${var_name}+x}\\\"\"\n if [ -n \"${_chk}\" ]; then\n # In upgrade mode, show current value and let user change it\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_NON_INTERACTIVE}\" != \"1\" ]; then\n eval \"local current_value=\\\"\\${${var_name}}\\\"\"\n local display_value=\"${current_value}\"\n if [ \"${is_secret}\" = \"true\" ] && [ -n \"${current_value}\" ]; then\n local len=${#current_value}\n if [ \"${len}\" -le 8 ]; then\n display_value=\"****\"\n else\n display_value=\"${current_value:0:4}****${current_value: -4}\"\n fi\n fi\n if [ -n \"${current_value}\" ]; then\n if [ \"${is_secret}\" = \"true\" ]; then\n log \"$(msg prompt.upgrade_keep_secret \"${prompt_text}\" \"${display_value}\")\"\n else\n log \"$(msg prompt.upgrade_keep \"${prompt_text}\" \"${display_value}\")\"\n fi\n else\n log \"$(msg prompt.upgrade_empty \"${prompt_text}\")\"\n fi\n local new_value=\"\"\n if [ \"${is_secret}\" = \"true\" ]; then\n read_secret \"${prompt_text}: \"\n new_value=\"${_RS_RESULT}\"\n else\n read -e -p \"${prompt_text}: \" new_value\n if [ \"${new_value}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n fi\n if [ -n \"${new_value}\" ]; then\n eval \"export ${var_name}='${new_value}'\"\n fi\n return\n fi\n log \"$(msg prompt.preset \"${prompt_text}\")\"\n return\n fi\n\n # Non-interactive or quickstart: skip, leave unset\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] || [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ]; then\n eval \"export ${var_name}=''\"\n return\n fi\n\n local value=\"\"\n if [ \"${is_secret}\" = \"true\" ]; then\n read_secret \"${prompt_text}: \"\n value=\"${_RS_RESULT}\"\n else\n read -e -p \"${prompt_text}: \" value\n if [ \"${value}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n fi\n\n eval \"export ${var_name}='${value}'\"\n}\n\ngenerate_key() {\n openssl rand -hex 32\n}\n\n# ============================================================\n# Detect container runtime socket on the host\n#\n# Dependency: Requires DOCKER_CMD to reflect the TRUE underlying runtime.\n# (Crucial for unmasking 'podman' disguised as 'docker'.\n# Must be strictly resolved prior, e.g., via check_container_runtime)\n# ============================================================\ndetect_socket() {\n local socket_path\n\n # 1. Respect explicitly defined DOCKER_HOST environment variable\n if [ -n \"${DOCKER_HOST}\" ]; then\n socket_path=$(echo \"${DOCKER_HOST}\" | sed 's|^unix://||')\n if [ -S \"${socket_path}\" ]; then\n echo \"${socket_path}\"\n return 0\n fi\n fi\n\n # 2. Route based on the resolved container runtime\n case \"${DOCKER_CMD}\" in\n docker)\n # Docker path handling\n socket_path=$(docker context ls --format '{{if .Current}}{{.DockerEndpoint}}{{end}}' 2>/dev/null | grep . | sed 's|^unix://||')\n if [ -n \"${socket_path}\" ] && [ -S \"${socket_path}\" ]; then\n echo \"${socket_path}\"\n return 0\n fi\n\n # Rootless Docker\n if [ -n \"${XDG_RUNTIME_DIR:-}\" ] && [ -S \"${XDG_RUNTIME_DIR}/docker.sock\" ]; then\n echo \"${XDG_RUNTIME_DIR}/docker.sock\"\n return 0\n fi\n\n # Rootless Docker (macOS fallback)\n if [ -S \"${HOME}/.docker/run/docker.sock\" ]; then\n echo \"${HOME}/.docker/run/docker.sock\"\n return 0\n fi\n\n # Root Docker\n if [ -S \"/var/run/docker.sock\" ]; then\n echo \"/var/run/docker.sock\"\n return 0\n fi\n ;;\n podman)\n # Podman path handling\n if [ \"$(id -u)\" -eq 0 ]; then\n socket_path=\"/run/podman/podman.sock\"\n else\n socket_path=\"${XDG_RUNTIME_DIR}/podman/podman.sock\"\n fi\n if [ -S \"${socket_path}\" ]; then\n echo \"${socket_path}\"\n return 0\n fi\n ;;\n esac\n\n # Return empty if no socket is found\n echo \"\"\n return 0\n}\n\n# Ensure Podman API socket is active (State mutation)\nensure_podman_socket() {\n local socket_path\n\n # Only applicable if runtime is podman and systemctl is available\n if [ \"${DOCKER_CMD:-}\" != \"podman\" ] || ! command -v systemctl >/dev/null 2>&1; then\n return 0\n fi\n\n echo \"Ensuring Podman API socket is active...\" >&2\n\n # Enable and start the socket based on user privileges\n if [ \"$(id -u)\" -eq 0 ]; then\n socket_path=\"/run/podman/podman.sock\"\n systemctl enable --now podman.socket >/dev/null 2>&1 || true\n else\n socket_path=\"${XDG_RUNTIME_DIR}/podman/podman.sock\"\n systemctl --user enable --now podman.socket >/dev/null 2>&1 || true\n fi\n\n # Give systemd a brief moment to assert the socket for root or rootless user\n local _i\n for _i in 1 2 3; do\n [ -S \"${socket_path}\" ] && break\n sleep 1\n done\n}\n\n# Detect local LAN IP address (cross-platform: macOS and Linux)\ndetect_lan_ip() {\n local ip=\"\"\n\n # macOS: try common Wi-Fi / Ethernet interfaces\n if command -v ipconfig >/dev/null 2>&1; then\n for iface in en0 en1 en2 en3 en4; do\n ip=$(ipconfig getifaddr \"${iface}\" 2>/dev/null)\n if [ -n \"${ip}\" ]; then\n echo \"${ip}\"\n return 0\n fi\n done\n fi\n\n # Linux: ip route — most reliable\n if command -v ip >/dev/null 2>&1; then\n ip=$(ip route get 1.1.1.1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i==\"src\"){print $(i+1); exit}}')\n if [ -n \"${ip}\" ]; then\n echo \"${ip}\"\n return 0\n fi\n fi\n\n # Linux fallback: hostname -I (space-separated list, take first non-loopback)\n if command -v hostname >/dev/null 2>&1; then\n ip=$(hostname -I 2>/dev/null | tr ' ' '\\n' | grep -v '^127\\.' | grep -v '^::' | head -1)\n if [ -n \"${ip}\" ]; then\n echo \"${ip}\"\n return 0\n fi\n fi\n\n # Last resort: ifconfig\n if command -v ifconfig >/dev/null 2>&1; then\n ip=$(ifconfig 2>/dev/null | awk '/inet /{if($2!~/^127\\./){print $2; exit}}')\n # Strip \"addr:\" prefix that some ifconfig versions add\n ip=\"${ip#addr:}\"\n if [ -n \"${ip}\" ]; then\n echo \"${ip}\"\n return 0\n fi\n fi\n\n echo \"\"\n}\n\n\n# ============================================================\n# Step-back navigation helpers\n# ============================================================\n\n# should_skip_step: returns 0 (skip) when the step is irrelevant in current mode\nshould_skip_step() {\n local step_fn=\"$1\"\n case \"${step_fn}\" in\n step_lang|step_mode)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n ;;\n step_version)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n ;;\n step_existing)\n local _env=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n [ ! -f \"${_env}\" ] && return 0\n ;;\n # Keep-All upgrade mode: skip all config steps (step_volume/step_workspace handled separately)\n step_llm|step_admin|step_network|step_ports|step_domains|step_github|step_skills|step_runtime|step_manager_runtime|step_e2ee|step_docker_proxy|step_idle|step_hostshare)\n [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ] && return 0\n ;;\n step_volume|step_workspace)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ] && return 0\n [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ] && return 0\n ;;\n step_e2ee|step_idle)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE}\" != \"1\" ] && return 0\n ;;\n step_docker_proxy)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE}\" != \"1\" ] && return 0\n # Embedded mode handles docker access natively — skip this step\n [ \"${AGENTTEAMS_USE_EMBEDDED:-}\" = \"1\" ] && return 0\n ;;\n step_manager_runtime)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n ;;\n step_hostshare)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ] && return 0\n ;;\n step_podman_autostart)\n # Only relevant for Podman with systemd\n [ \"${DOCKER_CMD}\" != \"podman\" ] && return 0\n ! command -v systemctl >/dev/null 2>&1 && return 0\n\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE}\" != \"1\" ] && return 0\n [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ] && return 0\n ;;\n esac\n return 1\n}\n\n# clear_step_vars: unset variables set by a step so it will re-prompt on re-entry\nclear_step_vars() {\n local step_fn=\"$1\"\n case \"${step_fn}\" in\n step_mode) unset AGENTTEAMS_QUICKSTART ;;\n step_version) unset AGENTTEAMS_VERSION ;;\n step_existing) unset AGENTTEAMS_UPGRADE UPGRADE_EXISTING_WORKERS ;;\n step_llm)\n unset AGENTTEAMS_LLM_PROVIDER AGENTTEAMS_DEFAULT_MODEL AGENTTEAMS_OPENAI_BASE_URL\n unset AGENTTEAMS_LLM_API_KEY AGENTTEAMS_MODEL_CONTEXT_WINDOW AGENTTEAMS_MODEL_MAX_TOKENS\n unset AGENTTEAMS_MODEL_REASONING AGENTTEAMS_MODEL_VISION\n ;;\n step_admin) unset AGENTTEAMS_ADMIN_USER AGENTTEAMS_ADMIN_PASSWORD ;;\n step_network) unset AGENTTEAMS_LOCAL_ONLY ;;\n step_ports)\n unset AGENTTEAMS_PORT_GATEWAY AGENTTEAMS_PORT_CONSOLE\n unset AGENTTEAMS_PORT_ELEMENT_WEB AGENTTEAMS_PORT_MANAGER_CONSOLE\n ;;\n step_domains)\n unset AGENTTEAMS_MATRIX_DOMAIN AGENTTEAMS_MATRIX_CLIENT_DOMAIN\n unset AGENTTEAMS_AI_GATEWAY_DOMAIN AGENTTEAMS_FS_DOMAIN AGENTTEAMS_CONSOLE_DOMAIN\n ;;\n step_github) unset AGENTTEAMS_GITHUB_TOKEN ;;\n step_skills) unset AGENTTEAMS_SKILLS_API_URL ;;\n step_volume) unset AGENTTEAMS_DATA_DIR ;;\n step_workspace) unset AGENTTEAMS_WORKSPACE_DIR ;;\n step_runtime) unset AGENTTEAMS_DEFAULT_WORKER_RUNTIME ;;\n step_manager_runtime) unset AGENTTEAMS_MANAGER_RUNTIME ;;\n step_e2ee) unset AGENTTEAMS_MATRIX_E2EE ;;\n step_docker_proxy) unset AGENTTEAMS_DOCKER_PROXY; unset AGENTTEAMS_PROXY_ALLOWED_REGISTRIES ;;\n step_idle) unset AGENTTEAMS_WORKER_IDLE_TIMEOUT ;;\n step_hostshare) unset AGENTTEAMS_HOST_SHARE_DIR ;;\n step_podman_autostart) unset AGENTTEAMS_PODMAN_AUTOSTART ;;\n esac\n}\n\n# ============================================================\n# Individual step functions\n# ============================================================\n\nstep_lang() {\n local lang_default_choice=\"2\"\n [ \"${AGENTTEAMS_LANGUAGE}\" = \"zh\" ] && lang_default_choice=\"1\"\n log \"$(msg lang.detected)\"\n log \"$(msg lang.switch_title)\"\n echo \"$(msg lang.option_zh)\"\n echo \"$(msg lang.option_en)\"\n echo \"\"\n local LANG_CHOICE\n read -e -p \"$(msg lang.prompt) [${lang_default_choice}]: \" LANG_CHOICE\n LANG_CHOICE=\"${LANG_CHOICE:-${lang_default_choice}}\"\n if [ \"${LANG_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n case \"${LANG_CHOICE}\" in\n 1) AGENTTEAMS_LANGUAGE=\"zh\" ;;\n 2) AGENTTEAMS_LANGUAGE=\"en\" ;;\n esac\n export AGENTTEAMS_LANGUAGE\n log \"\"\n}\n\nstep_mode() {\n log \"$(msg install.mode.title)\"\n echo \"\"\n echo \"$(msg install.mode.choose)\"\n echo \"$(msg install.mode.quickstart)\"\n echo \"$(msg install.mode.manual)\"\n echo \"\"\n local ONBOARDING_CHOICE\n read -e -p \"$(msg install.mode.prompt): \" ONBOARDING_CHOICE\n ONBOARDING_CHOICE=\"${ONBOARDING_CHOICE:-1}\"\n if [ \"${ONBOARDING_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n case \"${ONBOARDING_CHOICE}\" in\n 1|quick|quickstart)\n log \"$(msg install.mode.quickstart_selected)\"\n AGENTTEAMS_QUICKSTART=1\n ;;\n 2|manual)\n log \"$(msg install.mode.manual_selected)\"\n ;;\n *)\n log \"$(msg install.mode.invalid)\"\n AGENTTEAMS_QUICKSTART=1\n ;;\n esac\n log \"\"\n}\n\nstep_version() {\n # Skip if version already provided via env var\n if [ -n \"${AGENTTEAMS_VERSION}\" ]; then\n resolve_image_tags\n return 0\n fi\n # Try to fetch the latest stable release from GitHub\n log \"$(msg install.version.fetching)\"\n local _fetched\n _fetched=$(curl -sf --max-time 5 \\\n -H \"Accept: application/vnd.github+json\" \\\n \"https://api.github.com/repos/agentscope-ai/AgentTeams/releases/latest\" \\\n 2>/dev/null | grep '\"tag_name\"' | head -1 | sed 's/.*\"tag_name\": *\"\\([^\"]*\\)\".*/\\1/')\n if [ -n \"${_fetched}\" ]; then\n AGENTTEAMS_KNOWN_STABLE_VERSION=\"${_fetched}\"\n else\n log \"$(msg install.version.fetch_failed \"${AGENTTEAMS_KNOWN_STABLE_VERSION}\")\"\n fi\n log \"$(msg install.version.title)\"\n echo \"\"\n echo \"$(msg install.version.choose)\"\n echo \"$(msg install.version.option_latest)\"\n printf \"%s\\n\" \"$(msg install.version.option_stable \"${AGENTTEAMS_KNOWN_STABLE_VERSION}\")\"\n echo \"$(msg install.version.option_custom)\"\n echo \"\"\n local VERSION_CHOICE\n read -e -p \"$(msg install.version.prompt) [1]: \" VERSION_CHOICE\n VERSION_CHOICE=\"${VERSION_CHOICE:-1}\"\n if [ \"${VERSION_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n case \"${VERSION_CHOICE}\" in\n 1|latest)\n AGENTTEAMS_VERSION=\"latest\"\n log \"$(msg install.version.selected_latest)\"\n ;;\n 2|stable)\n AGENTTEAMS_VERSION=\"${AGENTTEAMS_KNOWN_STABLE_VERSION}\"\n log \"$(msg install.version.selected_stable \"${AGENTTEAMS_VERSION}\")\"\n ;;\n 3|custom)\n local CUSTOM_VERSION\n read -e -p \"$(msg install.version.custom_prompt): \" CUSTOM_VERSION\n AGENTTEAMS_VERSION=\"${CUSTOM_VERSION:-${AGENTTEAMS_KNOWN_STABLE_VERSION}}\"\n log \"$(msg install.version.selected_custom \"${AGENTTEAMS_VERSION}\")\"\n ;;\n *)\n AGENTTEAMS_VERSION=\"${AGENTTEAMS_KNOWN_STABLE_VERSION}\"\n log \"$(msg install.version.invalid \"${AGENTTEAMS_VERSION}\")\"\n ;;\n esac\n log \"\"\n resolve_image_tags\n}\n\nstep_existing() {\n local existing_env=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n log \"$(msg install.existing.detected \"${existing_env}\")\"\n local running_manager=\"\" running_workers=\"\" existing_workers=\"\"\n if ${DOCKER_CMD} ps --format '{{.Names}}' | grep -q \"^agentteams-manager$\"; then\n running_manager=\"agentteams-manager\"\n fi\n running_workers=$(${DOCKER_CMD} ps --format '{{.Names}}' | grep \"^agentteams-worker-\" || true)\n existing_workers=$(${DOCKER_CMD} ps -a --format '{{.Names}}' | grep \"^agentteams-worker-\" || true)\n local UPGRADE_CHOICE\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n log \"$(msg install.existing.upgrade_noninteractive)\"\n UPGRADE_CHOICE=\"upgrade\"\n else\n echo \"\"\n echo \"$(msg install.existing.choose)\"\n echo \"$(msg install.existing.upgrade)\"\n echo \"$(msg install.existing.reinstall)\"\n echo \"$(msg install.existing.cancel)\"\n echo \"\"\n read -e -p \"$(msg install.existing.prompt): \" UPGRADE_CHOICE\n UPGRADE_CHOICE=\"${UPGRADE_CHOICE:-1}\"\n if [ \"${UPGRADE_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n fi\n case \"${UPGRADE_CHOICE}\" in\n 1|upgrade)\n AGENTTEAMS_UPGRADE=1\n log \"$(msg install.existing.upgrading)\"\n # Show upgrade mode sub-menu (unless already set via env var)\n if [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL:-0}\" != \"1\" ]; then\n echo \"\"\n echo \"$(msg upgrade.mode.prompt)\"\n echo \"$(msg upgrade.mode.keep_all)\"\n echo \"$(msg upgrade.mode.confirm_each)\"\n echo \"$(msg upgrade.mode.back)\"\n echo \"\"\n local UPGRADE_MODE_CHOICE\n read -e -p \"$(msg install.existing.prompt): \" UPGRADE_MODE_CHOICE\n UPGRADE_MODE_CHOICE=\"${UPGRADE_MODE_CHOICE:-1}\"\n case \"${UPGRADE_MODE_CHOICE}\" in\n 1|keep)\n AGENTTEAMS_UPGRADE_KEEP_ALL=1\n ;;\n 2|confirm)\n AGENTTEAMS_UPGRADE_KEEP_ALL=0\n ;;\n 3|b)\n STEP_RESULT=\"back\"\n return 0\n ;;\n *)\n AGENTTEAMS_UPGRADE_KEEP_ALL=0\n ;;\n esac\n fi\n # Load current parameters for both Keep-All and confirm-each modes\n load_current_params_from_env\n if [ -n \"${running_manager}\" ] || [ -n \"${running_workers}\" ]; then\n echo \"\"\n echo -e \"\\033[33m$(msg install.existing.warn_manager_stop)\\033[0m\"\n if [ -n \"${existing_workers}\" ]; then\n echo -e \"\\033[33m$(msg install.existing.warn_worker_recreate)\\033[0m\"\n fi\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" != \"1\" ]; then\n echo \"\"\n local CONFIRM_STOP\n read -e -p \"$(msg install.existing.continue_prompt): \" CONFIRM_STOP\n if [ \"${CONFIRM_STOP}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n if [ \"${CONFIRM_STOP}\" != \"y\" ] && [ \"${CONFIRM_STOP}\" != \"Y\" ]; then\n log \"$(msg install.existing.cancelled)\"\n exit 0\n fi\n fi\n fi\n UPGRADE_EXISTING_WORKERS=\"${existing_workers}\"\n ;;\n 2|reinstall)\n log \"$(msg install.reinstall.performing)\"\n local existing_workspace=\"\"\n if [ -f \"${existing_env}\" ]; then\n existing_workspace=$(grep '^AGENTTEAMS_WORKSPACE_DIR=' \"${existing_env}\" 2>/dev/null | cut -d= -f2-)\n fi\n [ -z \"${existing_workspace}\" ] && existing_workspace=\"${HOME}/agentteams-manager\"\n echo \"\"\n echo -e \"\\033[33m$(msg install.reinstall.warn_stop)\\033[0m\"\n [ -n \"${running_manager}\" ] && echo -e \"\\033[33m - ${running_manager} (manager)\\033[0m\"\n for w in ${running_workers}; do\n echo -e \"\\033[33m - ${w} (worker)\\033[0m\"\n done\n echo \"\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_delete)\\033[0m\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_volume)\\033[0m\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_env \"${existing_env}\")\\033[0m\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_workspace \"${existing_workspace}\")\\033[0m\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_workers)\\033[0m\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_proxy)\\033[0m\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_network)\\033[0m\"\n echo \"\"\n echo -e \"\\033[31m$(msg install.reinstall.confirm_type)\\033[0m\"\n echo -e \"\\033[31m ${existing_workspace}\\033[0m\"\n echo \"\"\n local CONFIRM_PATH\n read -e -p \"$(msg install.reinstall.confirm_path): \" CONFIRM_PATH\n if [ \"${CONFIRM_PATH}\" != \"${existing_workspace}\" ]; then\n die \"$(msg install.reinstall.path_mismatch \"${CONFIRM_PATH}\" \"${existing_workspace}\")\"\n fi\n log \"$(msg install.reinstall.confirmed)\"\n ${DOCKER_CMD} stop agentteams-manager 2>/dev/null || true\n ${DOCKER_CMD} rm agentteams-manager 2>/dev/null || true\n for w in $(${DOCKER_CMD} ps -a --format '{{.Names}}' | grep \"^agentteams-worker-\" || true); do\n ${DOCKER_CMD} stop \"${w}\" 2>/dev/null || true\n ${DOCKER_CMD} rm \"${w}\" 2>/dev/null || true\n log \"$(msg install.reinstall.removed_worker \"${w}\")\"\n done\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' | grep -q \"^agentteams-controller$\"; then\n log \"$(msg install.reinstall.removing_proxy)\"\n ${DOCKER_CMD} stop agentteams-controller 2>/dev/null || true\n ${DOCKER_CMD} rm agentteams-controller 2>/dev/null || true\n fi\n if ${DOCKER_CMD} network ls --format '{{.Name}}' | grep -q \"^agentteams-net$\"; then\n log \"$(msg install.reinstall.removing_network)\"\n ${DOCKER_CMD} network rm agentteams-net 2>/dev/null || true\n fi\n if ${DOCKER_CMD} volume ls -q | grep -q \"^agentteams-data$\"; then\n log \"$(msg install.reinstall.removing_volume)\"\n ${DOCKER_CMD} volume rm agentteams-data 2>/dev/null || log \"$(msg install.reinstall.warn_volume_fail)\"\n fi\n if [ -d \"${existing_workspace}\" ]; then\n log \"$(msg install.reinstall.removing_workspace \"${existing_workspace}\")\"\n rm -rf \"${existing_workspace}\" || die \"$(msg install.reinstall.failed_rm_workspace)\"\n fi\n if [ -f \"${existing_env}\" ]; then\n log \"$(msg install.reinstall.removing_env \"${existing_env}\")\"\n rm -f \"${existing_env}\"\n fi\n log \"$(msg install.reinstall.cleanup_done)\"\n unset AGENTTEAMS_WORKSPACE_DIR\n return 0\n ;;\n 3|cancel|*)\n log \"$(msg install.existing.cancelled)\"\n exit 0\n ;;\n esac\n # Load existing env file as fallback (shell env vars take priority)\n if [ -f \"${existing_env}\" ]; then\n log \"$(msg install.loading_config \"${existing_env}\")\"\n while IFS='=' read -r key value; do\n case \"${key}\" in \\#*|\"\") continue ;; esac\n value=\"${value%%#*}\"\n value=\"${value#\"${value%%[![:space:]]*}\"}\"\n value=\"${value%\"${value##*[![:space:]]}\"}\"\n eval \"_existing_val=\\\"\\${${key}+x}\\\"\"\n if [ -z \"${_existing_val}\" ]; then export \"${key}=${value}\"; fi\n done < \"${existing_env}\"\n fi\n}\n\nstep_llm() {\n log \"$(msg llm.title)\"\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n if [ \"${AGENTTEAMS_LANGUAGE}\" = \"zh\" ]; then\n AGENTTEAMS_LLM_PROVIDER=\"${AGENTTEAMS_LLM_PROVIDER:-openai-compat}\"\n AGENTTEAMS_DEFAULT_MODEL=\"${AGENTTEAMS_DEFAULT_MODEL:-qwen3.6-plus}\"\n AGENTTEAMS_OPENAI_BASE_URL=\"${AGENTTEAMS_OPENAI_BASE_URL:-https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1}\"\n log \"$(msg llm.provider.label \"${AGENTTEAMS_LLM_PROVIDER}\")\"\n log \"$(msg llm.openai.base_url_label \"${AGENTTEAMS_OPENAI_BASE_URL}\")\"\n else\n AGENTTEAMS_LLM_PROVIDER=\"${AGENTTEAMS_LLM_PROVIDER:-qwen}\"\n AGENTTEAMS_DEFAULT_MODEL=\"${AGENTTEAMS_DEFAULT_MODEL:-qwen3.6-plus}\"\n AGENTTEAMS_OPENAI_BASE_URL=\"${AGENTTEAMS_OPENAI_BASE_URL:-}\"\n log \"$(msg llm.provider.qwen_default \"${AGENTTEAMS_LLM_PROVIDER}\")\"\n fi\n log \"$(msg llm.model.default \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n prompt AGENTTEAMS_LLM_API_KEY \"$(msg llm.apikey_prompt)\" \"\" \"true\"\n AGENTTEAMS_EMBEDDING_MODEL=\"${AGENTTEAMS_EMBEDDING_MODEL-text-embedding-v4}\"\n export AGENTTEAMS_LLM_PROVIDER AGENTTEAMS_DEFAULT_MODEL AGENTTEAMS_EMBEDDING_MODEL\n [ -n \"${AGENTTEAMS_OPENAI_BASE_URL+x}\" ] && export AGENTTEAMS_OPENAI_BASE_URL\n return 0\n fi\n\n # Upgrade Keep-All mode: skip provider selection, use loaded values\n local SKIP_TO_EMBEDDING=0\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ] && [ -n \"${AGENTTEAMS_LLM_PROVIDER}\" ]; then\n log \"$(msg llm.provider.label \"${AGENTTEAMS_LLM_PROVIDER}\")\"\n log \"$(msg llm.openai.base_url_label \"${AGENTTEAMS_OPENAI_BASE_URL}\")\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n PROVIDER_CHOICE=\"${AGENTTEAMS_LLM_PROVIDER}\"\n SKIP_TO_EMBEDDING=1\n else\n echo \"\"\n echo \"$(msg llm.providers_title)\"\n echo \"$(msg llm.provider.alibaba)\"\n echo \"$(msg llm.provider.openai_compat)\"\n echo \"\"\n local PROVIDER_CHOICE\n # If upgrade mode with loaded provider, show current as default\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_LLM_PROVIDER}\" ]; then\n local _prov_display\n case \"${AGENTTEAMS_LLM_PROVIDER}\" in\n openai-compat) _prov_display=\"2\" ;;\n *) _prov_display=\"1\" ;;\n esac\n read -e -p \"$(msg llm.provider.select) [${_prov_display}]: \" PROVIDER_CHOICE\n PROVIDER_CHOICE=\"${PROVIDER_CHOICE:-${_prov_display}}\"\n elif [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ]; then\n read -e -p \"$(msg llm.provider.select) [1]: \" PROVIDER_CHOICE\n PROVIDER_CHOICE=\"${PROVIDER_CHOICE:-1}\"\n else\n read -e -p \"$(msg llm.provider.select): \" PROVIDER_CHOICE\n PROVIDER_CHOICE=\"${PROVIDER_CHOICE:-1}\"\n fi\n if [ \"${PROVIDER_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n fi\n local ALIBABA_MODEL_CHOICE=\"\"\n local ALIBABA_ACCESS=\"\"\n case \"${PROVIDER_CHOICE}\" in\n 1|alibaba-cloud|openai-compat)\n if [ \"${AGENTTEAMS_LANGUAGE}\" = \"en\" ]; then\n AGENTTEAMS_LLM_PROVIDER=\"openai-compat\"\n AGENTTEAMS_OPENAI_BASE_URL=\"https://dashscope-intl.aliyuncs.com/compatible-mode/v1\"\n ALIBABA_ACCESS=\"tokenplan\"\n echo \"\"\n echo \"$(msg llm.codingplan.models_title)\"\n echo \"$(msg llm.codingplan.model.qwen36plus)\"\n echo \"$(msg llm.codingplan.model.glm5)\"\n echo \"$(msg llm.codingplan.model.kimi)\"\n echo \"$(msg llm.codingplan.model.minimax)\"\n echo \"\"\n local CODINGPLAN_MODEL_CHOICE\n # If upgrade with loaded model, show current as default\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_DEFAULT_MODEL}\" ]; then\n local _model_default\n case \"${AGENTTEAMS_DEFAULT_MODEL}\" in\n qwen3.6-plus) _model_default=\"1\" ;;\n glm-5) _model_default=\"2\" ;;\n kimi-k2.5) _model_default=\"3\" ;;\n MiniMax-M2.5) _model_default=\"4\" ;;\n *) _model_default=\"1\" ;;\n esac\n read -e -p \"$(msg llm.codingplan.model.select) [${_model_default}]: \" CODINGPLAN_MODEL_CHOICE\n CODINGPLAN_MODEL_CHOICE=\"${CODINGPLAN_MODEL_CHOICE:-${_model_default}}\"\n elif [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ]; then\n read -e -p \"$(msg llm.codingplan.model.select) [1]: \" CODINGPLAN_MODEL_CHOICE\n CODINGPLAN_MODEL_CHOICE=\"${CODINGPLAN_MODEL_CHOICE:-1}\"\n else\n read -e -p \"$(msg llm.codingplan.model.select): \" CODINGPLAN_MODEL_CHOICE\n CODINGPLAN_MODEL_CHOICE=\"${CODINGPLAN_MODEL_CHOICE:-1}\"\n fi\n if [ \"${CODINGPLAN_MODEL_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n case \"${CODINGPLAN_MODEL_CHOICE}\" in\n 1|qwen3.6-plus) AGENTTEAMS_DEFAULT_MODEL=\"qwen3.6-plus\" ;;\n 2|glm-5) AGENTTEAMS_DEFAULT_MODEL=\"glm-5\" ;;\n 3|kimi-k2.5) AGENTTEAMS_DEFAULT_MODEL=\"kimi-k2.5\" ;;\n 4|MiniMax-M2.5) AGENTTEAMS_DEFAULT_MODEL=\"MiniMax-M2.5\" ;;\n *) AGENTTEAMS_DEFAULT_MODEL=\"qwen3.6-plus\" ;;\n esac\n log \"$(msg llm.provider.selected_codingplan)\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n else\n echo \"\"\n echo \"$(msg llm.alibaba.models_title)\"\n echo \"$(msg llm.alibaba.model.tokenplan)\"\n echo \"$(msg llm.alibaba.model.bailian)\"\n echo \"$(msg llm.alibaba.model.codingplan_legacy)\"\n echo \"\"\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_OPENAI_BASE_URL}\" ]; then\n # Determine alibaba access type from loaded URL\n local _access_default=\"1\"\n if echo \"${AGENTTEAMS_OPENAI_BASE_URL}\" | grep -q \"token-plan\"; then\n _access_default=\"1\"\n elif echo \"${AGENTTEAMS_OPENAI_BASE_URL}\" | grep -q \"dashscope\"; then\n if echo \"${AGENTTEAMS_OPENAI_BASE_URL}\" | grep -q \"intl\"; then\n _access_default=\"3\"\n else\n _access_default=\"2\"\n fi\n fi\n read -e -p \"$(msg llm.alibaba.model.select) [${_access_default}]: \" ALIBABA_MODEL_CHOICE\n ALIBABA_MODEL_CHOICE=\"${ALIBABA_MODEL_CHOICE:-${_access_default}}\"\n elif [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ]; then\n read -e -p \"$(msg llm.alibaba.model.select) [1]: \" ALIBABA_MODEL_CHOICE\n ALIBABA_MODEL_CHOICE=\"${ALIBABA_MODEL_CHOICE:-1}\"\n else\n read -e -p \"$(msg llm.alibaba.model.select): \" ALIBABA_MODEL_CHOICE\n ALIBABA_MODEL_CHOICE=\"${ALIBABA_MODEL_CHOICE:-1}\"\n fi\n if [ \"${ALIBABA_MODEL_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n case \"${ALIBABA_MODEL_CHOICE}\" in\n 1|token-plan|tokenplan)\n ALIBABA_ACCESS=\"tokenplan\"\n AGENTTEAMS_LLM_PROVIDER=\"openai-compat\"\n AGENTTEAMS_OPENAI_BASE_URL=\"https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1\"\n echo \"\"\n echo \"$(msg llm.codingplan.models_title)\"\n echo \"$(msg llm.codingplan.model.qwen36plus)\"\n echo \"$(msg llm.codingplan.model.glm5)\"\n echo \"$(msg llm.codingplan.model.kimi)\"\n echo \"$(msg llm.codingplan.model.minimax)\"\n echo \"\"\n local CODINGPLAN_MODEL_CHOICE\n # If upgrade with loaded model, show current as default\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_DEFAULT_MODEL}\" ]; then\n local _model_default\n case \"${AGENTTEAMS_DEFAULT_MODEL}\" in\n qwen3.6-plus) _model_default=\"1\" ;;\n glm-5) _model_default=\"2\" ;;\n kimi-k2.5) _model_default=\"3\" ;;\n MiniMax-M2.5) _model_default=\"4\" ;;\n *) _model_default=\"1\" ;;\n esac\n read -e -p \"$(msg llm.codingplan.model.select) [${_model_default}]: \" CODINGPLAN_MODEL_CHOICE\n CODINGPLAN_MODEL_CHOICE=\"${CODINGPLAN_MODEL_CHOICE:-${_model_default}}\"\n elif [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ]; then\n read -e -p \"$(msg llm.codingplan.model.select) [1]: \" CODINGPLAN_MODEL_CHOICE\n CODINGPLAN_MODEL_CHOICE=\"${CODINGPLAN_MODEL_CHOICE:-1}\"\n else\n read -e -p \"$(msg llm.codingplan.model.select): \" CODINGPLAN_MODEL_CHOICE\n CODINGPLAN_MODEL_CHOICE=\"${CODINGPLAN_MODEL_CHOICE:-1}\"\n fi\n if [ \"${CODINGPLAN_MODEL_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n case \"${CODINGPLAN_MODEL_CHOICE}\" in\n 1|qwen3.6-plus) AGENTTEAMS_DEFAULT_MODEL=\"qwen3.6-plus\" ;;\n 2|glm-5) AGENTTEAMS_DEFAULT_MODEL=\"glm-5\" ;;\n 3|kimi-k2.5) AGENTTEAMS_DEFAULT_MODEL=\"kimi-k2.5\" ;;\n 4|MiniMax-M2.5) AGENTTEAMS_DEFAULT_MODEL=\"MiniMax-M2.5\" ;;\n *) AGENTTEAMS_DEFAULT_MODEL=\"qwen3.6-plus\" ;;\n esac\n log \"$(msg llm.provider.selected_tokenplan)\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n ;;\n 2|qwen|bailian)\n ALIBABA_ACCESS=\"bailian\"\n AGENTTEAMS_LLM_PROVIDER=\"qwen\"\n AGENTTEAMS_OPENAI_BASE_URL=\"\"\n echo \"\"\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_DEFAULT_MODEL}\" ]; then\n read -e -p \"$(msg llm.qwen.model_prompt) [${AGENTTEAMS_DEFAULT_MODEL}]: \" AGENTTEAMS_DEFAULT_MODEL\n else\n read -e -p \"$(msg llm.qwen.model_prompt) [qwen3.6-plus]: \" AGENTTEAMS_DEFAULT_MODEL\n fi\n if [ \"${AGENTTEAMS_DEFAULT_MODEL}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_DEFAULT_MODEL=\"${AGENTTEAMS_DEFAULT_MODEL:-qwen3.6-plus}\"\n log \"$(msg llm.provider.selected_qwen)\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n prompt_custom_model_params \"${AGENTTEAMS_DEFAULT_MODEL}\" || return 0\n ;;\n 3|coding-plan|codingplan)\n ALIBABA_ACCESS=\"codingplan_legacy\"\n AGENTTEAMS_LLM_PROVIDER=\"openai-compat\"\n AGENTTEAMS_OPENAI_BASE_URL=\"https://coding.dashscope.aliyuncs.com/v1\"\n echo \"\"\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_DEFAULT_MODEL}\" ]; then\n read -e -p \"$(msg llm.qwen.model_prompt) [${AGENTTEAMS_DEFAULT_MODEL}]: \" AGENTTEAMS_DEFAULT_MODEL\n else\n read -e -p \"$(msg llm.qwen.model_prompt): \" AGENTTEAMS_DEFAULT_MODEL\n fi\n if [ \"${AGENTTEAMS_DEFAULT_MODEL}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_DEFAULT_MODEL=\"${AGENTTEAMS_DEFAULT_MODEL:-qwen3.6-plus}\"\n log \"$(msg llm.provider.selected_codingplan_legacy)\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n prompt_custom_model_params \"${AGENTTEAMS_DEFAULT_MODEL}\" || return 0\n ;;\n *)\n die \"$(msg llm.alibaba.model.invalid \"${ALIBABA_MODEL_CHOICE}\")\"\n ;;\n esac\n fi\n if [ \"${ALIBABA_ACCESS}\" = \"bailian\" ]; then\n log \"$(msg llm.apikey_hint_bailian)\"\n log \"$(msg llm.apikey_url_bailian)\"\n elif [ \"${AGENTTEAMS_LANGUAGE}\" = \"en\" ]; then\n log \"$(msg llm.apikey_hint_qwencloud)\"\n log \"$(msg llm.apikey_url_qwencloud)\"\n elif [ \"${ALIBABA_ACCESS}\" = \"codingplan_legacy\" ]; then\n log \"$(msg llm.apikey_hint_codingplan)\"\n log \"$(msg llm.apikey_url_codingplan)\"\n else\n log \"$(msg llm.apikey_hint_tokenplan)\"\n log \"$(msg llm.apikey_url_tokenplan)\"\n fi\n log \"\"\n prompt AGENTTEAMS_LLM_API_KEY \"$(msg llm.apikey_prompt)\" \"\" \"true\" || return 0\n if [ \"${ALIBABA_ACCESS}\" = \"bailian\" ]; then\n test_llm_connectivity \"https://dashscope.aliyuncs.com/compatible-mode/v1\" \"${AGENTTEAMS_LLM_API_KEY}\" \"${AGENTTEAMS_DEFAULT_MODEL}\" || return 0\n elif [ \"${ALIBABA_ACCESS}\" = \"codingplan_legacy\" ]; then\n test_llm_connectivity \"https://coding.dashscope.aliyuncs.com/v1\" \"${AGENTTEAMS_LLM_API_KEY}\" \"${AGENTTEAMS_DEFAULT_MODEL}\" \"$(msg llm.openai.test.fail.codingplan_legacy)\" || return 0\n elif [ \"${AGENTTEAMS_LANGUAGE}\" = \"en\" ]; then\n test_llm_connectivity \"${AGENTTEAMS_OPENAI_BASE_URL}\" \"${AGENTTEAMS_LLM_API_KEY}\" \"${AGENTTEAMS_DEFAULT_MODEL}\" \"$(msg llm.openai.test.fail.codingplan)\" || return 0\n else\n test_llm_connectivity \"${AGENTTEAMS_OPENAI_BASE_URL}\" \"${AGENTTEAMS_LLM_API_KEY}\" \"${AGENTTEAMS_DEFAULT_MODEL}\" \"$(msg llm.openai.test.fail.tokenplan)\" || return 0\n fi\n ;;\n 2|openai-compat)\n AGENTTEAMS_LLM_PROVIDER=\"openai-compat\"\n log \"$(msg llm.provider.selected_openai \"${AGENTTEAMS_LLM_PROVIDER}\")\"\n echo \"\"\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg llm.openai.base_url_prompt)\" \"${AGENTTEAMS_OPENAI_BASE_URL}\")\"\n else\n _current=\"${AGENTTEAMS_OPENAI_BASE_URL}\"\n if [ -n \"${_current}\" ]; then\n read -e -p \"$(msg llm.openai.base_url_prompt) [${_current}]: \" AGENTTEAMS_OPENAI_BASE_URL\n else\n read -e -p \"$(msg llm.openai.base_url_prompt) [https://api.openai.com/v1]: \" AGENTTEAMS_OPENAI_BASE_URL\n fi\n if [ \"${AGENTTEAMS_OPENAI_BASE_URL}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_OPENAI_BASE_URL=\"${AGENTTEAMS_OPENAI_BASE_URL:-${_current}}\"\n [ -z \"${AGENTTEAMS_OPENAI_BASE_URL}\" ] && AGENTTEAMS_OPENAI_BASE_URL=\"https://api.openai.com/v1\"\n fi\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg llm.openai.model_prompt)\" \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n else\n _current=\"${AGENTTEAMS_DEFAULT_MODEL}\"\n if [ -n \"${_current}\" ]; then\n read -e -p \"$(msg llm.openai.model_prompt) [${_current}]: \" AGENTTEAMS_DEFAULT_MODEL\n else\n read -e -p \"$(msg llm.openai.model_prompt) [gpt-5.4]: \" AGENTTEAMS_DEFAULT_MODEL\n fi\n if [ \"${AGENTTEAMS_DEFAULT_MODEL}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_DEFAULT_MODEL=\"${AGENTTEAMS_DEFAULT_MODEL:-${_current}}\"\n [ -z \"${AGENTTEAMS_DEFAULT_MODEL}\" ] && AGENTTEAMS_DEFAULT_MODEL=\"gpt-5.4\"\n fi\n log \"$(msg llm.openai.base_url_label \"${AGENTTEAMS_OPENAI_BASE_URL}\")\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n prompt_custom_model_params \"${AGENTTEAMS_DEFAULT_MODEL}\" || return 0\n log \"\"\n prompt AGENTTEAMS_LLM_API_KEY \"$(msg llm.apikey_prompt)\" \"\" \"true\" || return 0\n test_llm_connectivity \"${AGENTTEAMS_OPENAI_BASE_URL}\" \"${AGENTTEAMS_LLM_API_KEY}\" \"${AGENTTEAMS_DEFAULT_MODEL}\" || return 0\n ;;\n *)\n die \"$(msg llm.provider.invalid \"${PROVIDER_CHOICE}\")\"\n ;;\n esac\n\n # Skip to embedding if Keep-All mode handled LLM params\n if [ \"${SKIP_TO_EMBEDDING}\" = \"1\" ]; then\n if [ -n \"${AGENTTEAMS_EMBEDDING_MODEL}\" ]; then\n log \"$(msg llm.embedding.title)\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_EMBEDDING_MODEL}\")\"\n log \"\"\n fi\n return 0\n fi\n\n # --- Embedding model (optional, auto-tested) ---\n echo \"\"\n log \"$(msg llm.embedding.title)\"\n log \"$(msg llm.embedding.hint)\"\n echo \"\"\n echo \"$(msg llm.embedding.option.default)\"\n echo \"$(msg llm.embedding.option.custom)\"\n echo \"$(msg llm.embedding.option.disable)\"\n echo \"\"\n local EMB_CHOICE\n read -e -p \"$(msg llm.embedding.select) [1]: \" EMB_CHOICE\n EMB_CHOICE=\"${EMB_CHOICE:-1}\"\n if [ \"${EMB_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n\n case \"${EMB_CHOICE}\" in\n 1)\n AGENTTEAMS_EMBEDDING_MODEL=\"text-embedding-v4\"\n ;;\n 2)\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg llm.embedding.custom_prompt)\" \"${AGENTTEAMS_EMBEDDING_MODEL}\")\"\n else\n _current=\"${AGENTTEAMS_EMBEDDING_MODEL}\"\n if [ -n \"${_current}\" ]; then\n read -e -p \"$(msg llm.embedding.custom_prompt) [${_current}]: \" AGENTTEAMS_EMBEDDING_MODEL\n else\n read -e -p \"$(msg llm.embedding.custom_prompt): \" AGENTTEAMS_EMBEDDING_MODEL\n fi\n if [ \"${AGENTTEAMS_EMBEDDING_MODEL}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_EMBEDDING_MODEL=\"${AGENTTEAMS_EMBEDDING_MODEL:-${_current}}\"\n fi\n ;;\n 3)\n AGENTTEAMS_EMBEDDING_MODEL=\"\"\n log \"$(msg llm.embedding.disabled)\"\n ;;\n *)\n AGENTTEAMS_EMBEDDING_MODEL=\"text-embedding-v4\"\n ;;\n esac\n\n if [ -n \"${AGENTTEAMS_EMBEDDING_MODEL}\" ]; then\n # Qwen provider uses dashscope directly; others use OPENAI_BASE_URL\n local EMB_BASE_URL=\"${AGENTTEAMS_OPENAI_BASE_URL}\"\n if [ \"${AGENTTEAMS_LLM_PROVIDER}\" = \"qwen\" ]; then\n EMB_BASE_URL=\"https://dashscope.aliyuncs.com/compatible-mode/v1\"\n fi\n if ! test_embedding_connectivity \"${EMB_BASE_URL}\" \"${AGENTTEAMS_LLM_API_KEY}\" \"${AGENTTEAMS_EMBEDDING_MODEL}\"; then\n AGENTTEAMS_EMBEDDING_MODEL=\"\"\n log \"$(msg llm.embedding.auto_disabled)\"\n fi\n fi\n\n export AGENTTEAMS_LLM_PROVIDER AGENTTEAMS_DEFAULT_MODEL\n [ -n \"${AGENTTEAMS_OPENAI_BASE_URL+x}\" ] && export AGENTTEAMS_OPENAI_BASE_URL\n log \"\"\n}\n\nstep_admin() {\n log \"$(msg admin.title)\"\n prompt AGENTTEAMS_ADMIN_USER \"$(msg admin.username_prompt)\" \"admin\" || return 0\n AGENTTEAMS_ADMIN_USER=\"$(printf '%s' \"${AGENTTEAMS_ADMIN_USER}\" | tr '[:upper:]' '[:lower:]')\"\n\n # Pre-set via env var: validate; in non-interactive mode fail fast,\n # in interactive mode warn and fall through to the retry prompt.\n if [ -n \"${AGENTTEAMS_ADMIN_PASSWORD:-}\" ]; then\n log \" $(msg prompt.preset \"$(msg admin.password_prompt)\")\"\n if [ ${#AGENTTEAMS_ADMIN_PASSWORD} -ge 8 ]; then\n log \"\"\n return 0\n fi\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n die \"$(msg admin.password_too_short \"${#AGENTTEAMS_ADMIN_PASSWORD}\")\"\n fi\n error \"$(msg admin.password_too_short \"${#AGENTTEAMS_ADMIN_PASSWORD}\")\"\n unset AGENTTEAMS_ADMIN_PASSWORD\n fi\n\n while true; do\n unset AGENTTEAMS_ADMIN_PASSWORD\n prompt_optional AGENTTEAMS_ADMIN_PASSWORD \"$(msg admin.password_prompt)\" \"true\" || return 0\n if [ -z \"${AGENTTEAMS_ADMIN_PASSWORD}\" ]; then\n AGENTTEAMS_ADMIN_PASSWORD=\"admin$(openssl rand -hex 6)\"\n log \"$(msg admin.password_generated)\"\n break\n fi\n if [ ${#AGENTTEAMS_ADMIN_PASSWORD} -ge 8 ]; then\n break\n fi\n error \"$(msg admin.password_too_short \"${#AGENTTEAMS_ADMIN_PASSWORD}\")\"\n done\n log \"\"\n}\n\nstep_network() {\n log \"$(msg port.local_only.title)\"\n echo \"\"\n echo \" 1) $(msg port.local_only.hint_yes)\"\n echo \" 2) $(msg port.local_only.hint_no)\"\n echo \"\"\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_LOCAL_ONLY=\"${AGENTTEAMS_LOCAL_ONLY:-1}\"\n elif [ -z \"${AGENTTEAMS_LOCAL_ONLY+x}\" ]; then\n local _local_choice\n read -e -p \"$(msg port.local_only.choice): \" _local_choice\n if [ \"${_local_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _local_choice=\"${_local_choice:-1}\"\n case \"${_local_choice}\" in\n 2|n|N|no|NO) AGENTTEAMS_LOCAL_ONLY=\"0\" ;;\n *) AGENTTEAMS_LOCAL_ONLY=\"1\" ;;\n esac\n fi\n export AGENTTEAMS_LOCAL_ONLY\n if [ \"${AGENTTEAMS_LOCAL_ONLY}\" = \"1\" ]; then\n log \"$(msg port.local_only.selected_local)\"\n else\n log \"$(msg port.local_only.selected_external)\"\n echo \"\"\n echo -e \"\\033[33m$(msg port.local_only.https_hint)\\033[0m\"\n fi\n}\n\nstep_ports() {\n log \"$(msg port.title)\"\n prompt AGENTTEAMS_PORT_GATEWAY \"$(msg port.gateway_prompt)\" \"18080\" || return 0\n prompt AGENTTEAMS_PORT_CONSOLE \"$(msg port.console_prompt)\" \"18001\" || return 0\n prompt AGENTTEAMS_PORT_ELEMENT_WEB \"$(msg port.element_prompt)\" \"18088\" || return 0\n prompt AGENTTEAMS_PORT_MANAGER_CONSOLE \"$(msg port.manager_console_prompt)\" \"18888\" || return 0\n log \"\"\n}\n\nstep_domains() {\n log \"$(msg domain.title)\"\n log \"$(msg domain.hint)\"\n prompt AGENTTEAMS_MATRIX_DOMAIN \"$(msg domain.matrix_prompt)\" \"matrix-local.agentteams.io:${AGENTTEAMS_PORT_GATEWAY}\" || return 0\n prompt AGENTTEAMS_MATRIX_CLIENT_DOMAIN \"$(msg domain.element_prompt)\" \"matrix-client-local.agentteams.io\" || return 0\n prompt AGENTTEAMS_AI_GATEWAY_DOMAIN \"$(msg domain.gateway_prompt)\" \"aigw-local.agentteams.io\" || return 0\n prompt AGENTTEAMS_FS_DOMAIN \"$(msg domain.fs_prompt)\" \"fs-local.agentteams.io\" || return 0\n if [ \"${AGENTTEAMS_MANAGER_RUNTIME}\" != \"copaw\" ]; then\n prompt AGENTTEAMS_CONSOLE_DOMAIN \"$(msg domain.console_prompt)\" \"console-local.agentteams.io\" || return 0\n fi\n log \"\"\n}\n\nstep_github() {\n log \"$(msg github.title)\"\n prompt_optional AGENTTEAMS_GITHUB_TOKEN \"$(msg github.token_prompt)\" \"true\" || return 0\n}\n\nstep_skills() {\n log \"\"\n log \"$(msg skills.title)\"\n prompt_optional AGENTTEAMS_SKILLS_API_URL \"$(msg skills.url_prompt)\" || return 0\n log \"\"\n}\n\nstep_volume() {\n log \"$(msg data.title)\"\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_DATA_DIR=\"${AGENTTEAMS_DATA_DIR:-agentteams-data}\"\n log \" $(msg data.volume_using \"${AGENTTEAMS_DATA_DIR}\") (non-interactive, skipped)\"\n export AGENTTEAMS_DATA_DIR\n return 0\n fi\n # ─────────────────────────────────────────────────────────────────\n if [ -z \"${AGENTTEAMS_DATA_DIR+x}\" ]; then\n local _input\n read -e -p \"$(msg data.volume_prompt): \" _input\n if [ \"${_input}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_DATA_DIR=\"${_input:-agentteams-data}\"\n export AGENTTEAMS_DATA_DIR\n fi\n AGENTTEAMS_DATA_DIR=\"${AGENTTEAMS_DATA_DIR:-agentteams-data}\"\n log \"$(msg data.volume_using \"${AGENTTEAMS_DATA_DIR}\")\"\n}\n\nstep_workspace() {\n log \"$(msg workspace.title)\"\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_WORKSPACE_DIR=\"${AGENTTEAMS_WORKSPACE_DIR:-${HOME}/agentteams-manager}\"\n AGENTTEAMS_WORKSPACE_DIR=\"$(cd \"${AGENTTEAMS_WORKSPACE_DIR}\" 2>/dev/null && pwd || echo \"${AGENTTEAMS_WORKSPACE_DIR}\")\"\n mkdir -p \"${AGENTTEAMS_WORKSPACE_DIR}\"\n log \" $(msg workspace.dir_label \"${AGENTTEAMS_WORKSPACE_DIR}\") (non-interactive, skipped)\"\n export AGENTTEAMS_WORKSPACE_DIR\n return 0\n fi\n # ─────────────────────────────────────────────────────────────────\n local _input\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg workspace.dir_prompt \"${HOME}/agentteams-manager\")\" \"${AGENTTEAMS_WORKSPACE_DIR}\")\"\n _input=\"${AGENTTEAMS_WORKSPACE_DIR}\"\n else\n local _current=\"${AGENTTEAMS_WORKSPACE_DIR}\"\n if [ -n \"${_current}\" ]; then\n read -e -p \"$(msg workspace.dir_prompt \"${HOME}/agentteams-manager\") [${_current}]: \" _input\n else\n read -e -p \"$(msg workspace.dir_prompt \"${HOME}/agentteams-manager\"): \" _input\n fi\n if [ \"${_input}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _input=\"${_input:-${_current}}\"\n fi\n AGENTTEAMS_WORKSPACE_DIR=\"${_input:-${HOME}/agentteams-manager}\"\n export AGENTTEAMS_WORKSPACE_DIR\n AGENTTEAMS_WORKSPACE_DIR=\"$(cd \"${AGENTTEAMS_WORKSPACE_DIR}\" 2>/dev/null && pwd || echo \"${AGENTTEAMS_WORKSPACE_DIR}\")\"\n mkdir -p \"${AGENTTEAMS_WORKSPACE_DIR}\"\n log \"$(msg workspace.dir_label \"${AGENTTEAMS_WORKSPACE_DIR}\")\"\n}\n\nstep_runtime() {\n log \"$(msg worker_runtime.title)\"\n echo \"\"\n echo \" 1) $(msg worker_runtime.copaw)\"\n echo \" 2) $(msg worker_runtime.openclaw)\"\n if ! _ver_lt \"${AGENTTEAMS_VERSION}\" \"v1.1.0\"; then\n echo \" 3) $(msg worker_runtime.hermes)\"\n fi\n echo \"\"\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"${AGENTTEAMS_DEFAULT_WORKER_RUNTIME:-copaw}\"\n elif [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_DEFAULT_WORKER_RUNTIME}\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg worker_runtime.title_short)\" \"${AGENTTEAMS_DEFAULT_WORKER_RUNTIME}\")\"\n local _runtime_choice\n read -e -p \"$(msg worker_runtime.choice): \" _runtime_choice\n if [ \"${_runtime_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n if [ -n \"${_runtime_choice}\" ]; then\n case \"${_runtime_choice}\" in\n 2) AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"openclaw\" ;;\n 3) if ! _ver_lt \"${AGENTTEAMS_VERSION}\" \"v1.1.0\"; then\n AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"hermes\"\n else\n AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"copaw\"\n fi ;;\n *) AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"copaw\" ;;\n esac\n fi\n elif [ -z \"${AGENTTEAMS_DEFAULT_WORKER_RUNTIME+x}\" ]; then\n local _runtime_choice\n read -e -p \"$(msg worker_runtime.choice): \" _runtime_choice\n if [ \"${_runtime_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _runtime_choice=\"${_runtime_choice:-1}\"\n case \"${_runtime_choice}\" in\n 2) AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"openclaw\" ;;\n 3) if ! _ver_lt \"${AGENTTEAMS_VERSION}\" \"v1.1.0\"; then\n AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"hermes\"\n else\n AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"copaw\"\n fi ;;\n *) AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"copaw\" ;;\n esac\n fi\n export AGENTTEAMS_DEFAULT_WORKER_RUNTIME\n log \"$(msg worker_runtime.selected \"${AGENTTEAMS_DEFAULT_WORKER_RUNTIME}\")\"\n}\n\nstep_manager_runtime() {\n log \"$(msg manager_runtime.title)\"\n echo \"\"\n echo \" 1) $(msg manager_runtime.copaw)\"\n echo \" 2) $(msg manager_runtime.openclaw)\"\n echo \"\"\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_MANAGER_RUNTIME=\"${AGENTTEAMS_MANAGER_RUNTIME:-copaw}\"\n elif [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_MANAGER_RUNTIME}\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg manager_runtime.title_short)\" \"${AGENTTEAMS_MANAGER_RUNTIME}\")\"\n local _runtime_choice\n read -e -p \"$(msg manager_runtime.choice): \" _runtime_choice\n if [ \"${_runtime_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n if [ -n \"${_runtime_choice}\" ]; then\n case \"${_runtime_choice}\" in\n 2) AGENTTEAMS_MANAGER_RUNTIME=\"openclaw\" ;;\n *) AGENTTEAMS_MANAGER_RUNTIME=\"copaw\" ;;\n esac\n fi\n elif [ -z \"${AGENTTEAMS_MANAGER_RUNTIME+x}\" ]; then\n local _runtime_choice\n read -e -p \"$(msg manager_runtime.choice): \" _runtime_choice\n if [ \"${_runtime_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _runtime_choice=\"${_runtime_choice:-1}\"\n case \"${_runtime_choice}\" in\n 2) AGENTTEAMS_MANAGER_RUNTIME=\"openclaw\" ;;\n *) AGENTTEAMS_MANAGER_RUNTIME=\"copaw\" ;;\n esac\n fi\n export AGENTTEAMS_MANAGER_RUNTIME\n log \"$(msg manager_runtime.selected \"${AGENTTEAMS_MANAGER_RUNTIME}\")\"\n}\n\nstep_e2ee() {\n log \"\"\n log \"$(msg matrix_e2ee.title)\"\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_MATRIX_E2EE=\"${AGENTTEAMS_MATRIX_E2EE:-0}\"\n log \" $(msg matrix_e2ee.title_short) = ${AGENTTEAMS_MATRIX_E2EE} (non-interactive, skipped)\"\n export AGENTTEAMS_MATRIX_E2EE\n return 0\n fi\n # ─────────────────────────────────────────────────────────────────\n echo \"\"\n echo -e \" $(msg matrix_e2ee.desc)\"\n echo \"\"\n echo \" 1) $(msg matrix_e2ee.disable)\"\n echo \" 2) $(msg matrix_e2ee.enable)\"\n echo \"\"\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_MATRIX_E2EE}\" ]; then\n local _e2ee_display; if [ \"${AGENTTEAMS_MATRIX_E2EE}\" = \"1\" ]; then _e2ee_display=\"$(msg matrix_e2ee.val_enabled)\"; else _e2ee_display=\"$(msg matrix_e2ee.val_disabled)\"; fi\n log \"$(msg prompt.upgrade_keep \"$(msg matrix_e2ee.title_short)\" \"${_e2ee_display}\")\"\n local _e2ee_choice\n read -e -p \"$(msg matrix_e2ee.choice): \" _e2ee_choice\n if [ \"${_e2ee_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n if [ -n \"${_e2ee_choice}\" ]; then\n case \"${_e2ee_choice}\" in\n 2) AGENTTEAMS_MATRIX_E2EE=\"1\" ;;\n *) AGENTTEAMS_MATRIX_E2EE=\"0\" ;;\n esac\n fi\n elif [ -z \"${AGENTTEAMS_MATRIX_E2EE+x}\" ]; then\n local _e2ee_choice\n read -e -p \"$(msg matrix_e2ee.choice): \" _e2ee_choice\n if [ \"${_e2ee_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _e2ee_choice=\"${_e2ee_choice:-1}\"\n case \"${_e2ee_choice}\" in\n 2) AGENTTEAMS_MATRIX_E2EE=\"1\" ;;\n *) AGENTTEAMS_MATRIX_E2EE=\"0\" ;;\n esac\n fi\n AGENTTEAMS_MATRIX_E2EE=\"${AGENTTEAMS_MATRIX_E2EE:-0}\"\n export AGENTTEAMS_MATRIX_E2EE\n if [ \"${AGENTTEAMS_MATRIX_E2EE}\" = \"1\" ]; then\n log \"$(msg matrix_e2ee.selected_enabled)\"\n else\n log \"$(msg matrix_e2ee.selected_disabled)\"\n fi\n}\n\nstep_docker_proxy() {\n # Only relevant when socket mounting is enabled\n if [ \"${AGENTTEAMS_MOUNT_SOCKET}\" != \"1\" ]; then\n AGENTTEAMS_DOCKER_PROXY=\"0\"\n return 0\n fi\n\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_DOCKER_PROXY=\"${AGENTTEAMS_DOCKER_PROXY:-0}\"\n log \" $(msg docker_proxy.title_short) = ${AGENTTEAMS_DOCKER_PROXY} (non-interactive, skipped)\"\n export AGENTTEAMS_DOCKER_PROXY\n return 0\n fi\n # ─────────────────────────────────────────────────────────────────\n\n echo \"\"\n echo -e \" \\033[1m$(msg docker_proxy.title)\\033[0m\"\n echo \"\"\n echo -e \" $(msg docker_proxy.desc)\"\n echo \"\"\n echo \" 1) $(msg docker_proxy.enable)\"\n echo \" 2) $(msg docker_proxy.disable)\"\n echo \"\"\n\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_DOCKER_PROXY}\" ]; then\n local _proxy_display; if [ \"${AGENTTEAMS_DOCKER_PROXY}\" = \"1\" ]; then _proxy_display=\"$(msg docker_proxy.val_enabled)\"; else _proxy_display=\"$(msg docker_proxy.val_disabled)\"; fi\n log \"$(msg prompt.upgrade_keep \"$(msg docker_proxy.title_short)\" \"${_proxy_display}\")\"\n local _choice\n read -e -p \"$(msg docker_proxy.choice): \" _choice\n if [ \"${_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n if [ -n \"${_choice}\" ]; then\n case \"${_choice}\" in\n 2) AGENTTEAMS_DOCKER_PROXY=\"0\" ;;\n *) AGENTTEAMS_DOCKER_PROXY=\"1\" ;;\n esac\n fi\n elif [ -z \"${AGENTTEAMS_DOCKER_PROXY+x}\" ]; then\n local _choice\n read -e -p \"$(msg docker_proxy.choice): \" _choice\n if [ \"${_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _choice=\"${_choice:-1}\"\n case \"${_choice}\" in\n 2) AGENTTEAMS_DOCKER_PROXY=\"0\" ;;\n *) AGENTTEAMS_DOCKER_PROXY=\"1\" ;;\n esac\n fi\n AGENTTEAMS_DOCKER_PROXY=\"${AGENTTEAMS_DOCKER_PROXY:-1}\"\n export AGENTTEAMS_DOCKER_PROXY\n if [ \"${AGENTTEAMS_DOCKER_PROXY}\" = \"1\" ]; then\n log \"$(msg docker_proxy.selected_enabled)\"\n\n # Prompt for additional allowed image sources\n echo \"\"\n echo -e \" $(msg docker_proxy.registries_desc)\"\n echo \"\"\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_PROXY_ALLOWED_REGISTRIES}\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg docker_proxy.registries_label)\" \"${AGENTTEAMS_PROXY_ALLOWED_REGISTRIES}\")\"\n local _reg_input\n read -e -p \"$(msg docker_proxy.registries_prompt): \" _reg_input\n if [ \"${_reg_input}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n [ -n \"${_reg_input}\" ] && AGENTTEAMS_PROXY_ALLOWED_REGISTRIES=\"${_reg_input}\"\n elif [ -z \"${AGENTTEAMS_PROXY_ALLOWED_REGISTRIES+x}\" ]; then\n local _reg_input\n read -e -p \"$(msg docker_proxy.registries_prompt): \" _reg_input\n if [ \"${_reg_input}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_PROXY_ALLOWED_REGISTRIES=\"${_reg_input:-}\"\n fi\n export AGENTTEAMS_PROXY_ALLOWED_REGISTRIES\n else\n log \"$(msg docker_proxy.selected_disabled)\"\n fi\n}\n\nstep_idle() {\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_WORKER_IDLE_TIMEOUT=\"${AGENTTEAMS_WORKER_IDLE_TIMEOUT:-720}\"\n log \" $(msg idle_timeout.label) = ${AGENTTEAMS_WORKER_IDLE_TIMEOUT} (non-interactive, skipped)\"\n export AGENTTEAMS_WORKER_IDLE_TIMEOUT\n return 0\n fi\n # ─────────────────────────────────────────────────────────────────\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_WORKER_IDLE_TIMEOUT}\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg idle_timeout.label)\" \"${AGENTTEAMS_WORKER_IDLE_TIMEOUT}\")\"\n local _idle_timeout\n read -e -p \"$(msg idle_timeout.prompt): \" _idle_timeout\n if [ \"${_idle_timeout}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n [ -n \"${_idle_timeout}\" ] && AGENTTEAMS_WORKER_IDLE_TIMEOUT=\"${_idle_timeout}\"\n elif [ -z \"${AGENTTEAMS_WORKER_IDLE_TIMEOUT+x}\" ]; then\n local _idle_timeout\n read -e -p \"$(msg idle_timeout.prompt): \" _idle_timeout\n if [ \"${_idle_timeout}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_WORKER_IDLE_TIMEOUT=\"${_idle_timeout:-720}\"\n fi\n AGENTTEAMS_WORKER_IDLE_TIMEOUT=\"${AGENTTEAMS_WORKER_IDLE_TIMEOUT:-720}\"\n export AGENTTEAMS_WORKER_IDLE_TIMEOUT\n log \"$(msg idle_timeout.selected \"${AGENTTEAMS_WORKER_IDLE_TIMEOUT}\")\"\n}\n\nstep_hostshare() {\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_HOST_SHARE_DIR=\"${AGENTTEAMS_HOST_SHARE_DIR:-${HOME}}\"\n log \" $(msg host_share.label) = ${AGENTTEAMS_HOST_SHARE_DIR} (non-interactive, skipped)\"\n export AGENTTEAMS_HOST_SHARE_DIR\n return 0\n fi\n # ─────────────────────────────────────────────────────────────────\n local _current=\"${AGENTTEAMS_HOST_SHARE_DIR}\"\n local _share_dir\n if [ -n \"${_current}\" ]; then\n read -e -p \"$(msg host_share.prompt \"$HOME\") [${_current}]: \" _share_dir\n else\n read -e -p \"$(msg host_share.prompt \"$HOME\"): \" _share_dir\n fi\n if [ \"${_share_dir}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_HOST_SHARE_DIR=\"${_share_dir:-${_current:-$HOME}}\"\n export AGENTTEAMS_HOST_SHARE_DIR\n}\n\nstep_podman_autostart() {\n echo \"\"\n log \"$(msg podman.autostart.title)\"\n echo \"\"\n echo -e \" $(msg podman.autostart.desc)\"\n echo \"\"\n echo \" 1) $(msg podman.autostart.enable)\"\n echo \" 2) $(msg podman.autostart.disable)\"\n echo \"\"\n\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_PODMAN_AUTOSTART}\" ]; then\n local _disp; if [ \"${AGENTTEAMS_PODMAN_AUTOSTART}\" = \"1\" ]; then _disp=\"$(msg podman.autostart.val_enabled)\"; else _disp=\"$(msg podman.autostart.val_disabled)\"; fi\n log \"$(msg prompt.upgrade_keep \"$(msg podman.autostart.title_short)\" \"${_disp}\")\"\n local _choice\n read -e -p \"$(msg podman.autostart.choice): \" _choice\n if [ \"${_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n if [ -n \"${_choice}\" ]; then\n case \"${_choice}\" in\n 2) AGENTTEAMS_PODMAN_AUTOSTART=\"0\" ;;\n *) AGENTTEAMS_PODMAN_AUTOSTART=\"1\" ;;\n esac\n fi\n elif [ -z \"${AGENTTEAMS_PODMAN_AUTOSTART+x}\" ]; then\n local _choice\n read -e -p \"$(msg podman.autostart.choice) [1]: \" _choice\n if [ \"${_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _choice=\"${_choice:-1}\"\n case \"${_choice}\" in\n 2) AGENTTEAMS_PODMAN_AUTOSTART=\"0\" ;;\n *) AGENTTEAMS_PODMAN_AUTOSTART=\"1\" ;;\n esac\n fi\n AGENTTEAMS_PODMAN_AUTOSTART=\"${AGENTTEAMS_PODMAN_AUTOSTART:-1}\"\n export AGENTTEAMS_PODMAN_AUTOSTART\n if [ \"${AGENTTEAMS_PODMAN_AUTOSTART}\" = \"1\" ]; then\n log \"$(msg podman.autostart.selected_enabled)\"\n else\n log \"$(msg podman.autostart.selected_disabled)\"\n fi\n}\n\n# ============================================================\n# Setup dedicated Podman autostart service for AgentTeams\n# ============================================================\nsetup_podman_autostart() {\n # Only execute if using podman and autostart is enabled\n if [ \"${DOCKER_CMD:-docker}\" != \"podman\" ] || [ \"${AGENTTEAMS_PODMAN_AUTOSTART:-0}\" != \"1\" ]; then\n return 0\n fi\n\n log \"$(msg install.podman.autostart_title)\"\n\n local current_user\n current_user=\"$(whoami)\"\n\n # Define the dedicated AgentTeams service content\n # Strict scoping: only manage AgentTeams containers.\n local _service_content=\"[Unit]\nDescription=AgentTeams Dedicated Podman Autostart Service\nDocumentation=https://github.com/agentscope-ai/AgentTeams\nWants=network-online.target\nAfter=network-online.target\n\n[Service]\nType=oneshot\nRemainAfterExit=true\nEnvironment=LOGGING=\\\"--log-level=info\\\"\nExecStart=/usr/bin/podman \\$LOGGING start --filter name=agentteams --filter restart-policy=always --filter restart-policy=unless-stopped\nExecStop=/usr/bin/podman \\$LOGGING stop --filter name=agentteams --filter restart-policy=always --filter restart-policy=unless-stopped\n\n[Install]\nWantedBy=default.target\"\n\n local _autostart_ok=0\n\n if [ \"${current_user}\" = \"root\" ]; then\n # Rootful Mode (System-wide dedicated service)\n log \"$(msg install.podman.root_setup)\"\n\n local _sys_dir=\"/etc/systemd/system\"\n mkdir -p \"${_sys_dir}\"\n echo \"${_service_content}\" > \"${_sys_dir}/agentteams-podman-restart.service\"\n\n systemctl daemon-reload >/dev/null 2>&1 || true\n\n if systemctl enable --now agentteams-podman-restart.service >/dev/null 2>&1; then\n log \"$(msg install.podman.root_success)\"\n _autostart_ok=1\n else\n log \"$(msg install.podman.root_fail)\"\n fi\n else\n # Rootless Mode (User-level dedicated service)\n log \"$(msg install.podman.user_setup \"${current_user}\")\"\n\n local _linger_ok=0\n\n # 1. Enable Linger for background execution (Graceful escalation strategy)\n if loginctl show-user \"${current_user}\" --property=Linger 2>/dev/null | grep -q \"Linger=yes\"; then\n _linger_ok=1\n else\n log \"$(msg install.podman.linger_enable)\"\n\n # Step A: Try to enable linger natively without sudo\n if loginctl enable-linger \"${current_user}\" >/dev/null 2>&1; then\n log \"Successfully enabled systemd linger natively.\"\n _linger_ok=1\n else\n # Step B: Fallback to sudo\n log \"Native linger enablement failed. Distro security policy requires privileges.\"\n if command -v sudo >/dev/null 2>&1; then\n log \"Attempting to enable systemd linger via sudo (enter password if prompted)...\"\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n sudo -n loginctl enable-linger \"${current_user}\" 2>/dev/null && _linger_ok=1 || log \"$(msg install.podman.linger_warn)\"\n else\n sudo loginctl enable-linger \"${current_user}\" && _linger_ok=1 || log \"$(msg install.podman.linger_warn)\"\n fi\n else\n log \"$(msg install.podman.linger_warn)\"\n fi\n fi\n fi\n\n # 2. Write dedicated service file to user directory\n local _user_dir=\"${HOME}/.config/systemd/user\"\n mkdir -p \"${_user_dir}\"\n echo \"${_service_content}\" > \"${_user_dir}/agentteams-podman-restart.service\"\n\n systemctl --user daemon-reload >/dev/null 2>&1 || true\n\n if systemctl --user enable --now agentteams-podman-restart.service >/dev/null 2>&1; then\n log \"$(msg install.podman.user_success)\"\n # Crucial: Only consider autostart fully successful if systemd linger is also enabled\n if [ \"${_linger_ok}\" = \"1\" ]; then\n _autostart_ok=1\n else\n log \"⚠️ Service enabled, but since systemd linger is not active, containers will not auto-recover on system reboot.\"\n fi\n else\n log \"$(msg install.podman.user_fail)\"\n fi\n fi\n\n # Only print final success banner if the service AND linger were actually enabled\n if [ \"${_autostart_ok}\" = \"1\" ]; then\n log \"$(msg install.podman.success)\"\n fi\n}\n\n# ============================================================\n# Manager Installation (Interactive)\n# ============================================================\n\ninstall_manager() {\n log \"$(msg install.title)\"\n log \"$(msg install.registry \"${AGENTTEAMS_REGISTRY}\")\"\n log \"\"\n log \"$(msg install.dir \"$(pwd)\")\"\n log \"$(msg install.dir_hint)\"\n log \"$(msg install.dir_hint2)\"\n log \"\"\n\n # Non-interactive fallback: resolve version immediately so image tags are available\n # before the step state machine runs. Interactive mode lets step_version handle it.\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_VERSION=\"${AGENTTEAMS_VERSION:-${AGENTTEAMS_KNOWN_STABLE_VERSION}}\"\n resolve_image_tags\n fi\n\n # Migrate legacy env file location before checks\n local existing_env=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n if [ ! -f \"${existing_env}\" ] && [ -f \"./agentteams-manager.env\" ]; then\n log \"Migrating agentteams-manager.env from current directory to ${existing_env}...\"\n mv \"./agentteams-manager.env\" \"${existing_env}\"\n fi\n\n # Orphan volume detection (only when no env file — step_existing handles the env-file case)\n if [ ! -f \"${existing_env}\" ]; then\n local data_vol=\"${AGENTTEAMS_DATA_DIR:-agentteams-data}\"\n if ${DOCKER_CMD} volume ls -q | grep -q \"^${data_vol}$\"; then\n echo \"\"\n log \"$(msg install.orphan_volume.detected \"${data_vol}\")\"\n log \"$(msg install.orphan_volume.warn)\"\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n log \"$(msg install.orphan_volume.clean_noninteractive)\"\n ${DOCKER_CMD} stop agentteams-manager 2>/dev/null || true\n ${DOCKER_CMD} rm agentteams-manager 2>/dev/null || true\n for w in $(${DOCKER_CMD} ps -a --format '{{.Names}}' | grep \"^agentteams-worker-\" || true); do\n ${DOCKER_CMD} stop \"${w}\" 2>/dev/null || true\n ${DOCKER_CMD} rm \"${w}\" 2>/dev/null || true\n done\n log \"$(msg install.orphan_volume.cleaning)\"\n ${DOCKER_CMD} volume rm \"${data_vol}\" 2>/dev/null || true\n log \"$(msg install.orphan_volume.cleaned)\"\n else\n echo \"\"\n echo \"$(msg install.orphan_volume.choose)\"\n echo \"$(msg install.orphan_volume.clean)\"\n echo \"$(msg install.orphan_volume.keep)\"\n echo \"\"\n local ORPHAN_CHOICE\n read -e -p \"$(msg install.orphan_volume.prompt): \" ORPHAN_CHOICE\n ORPHAN_CHOICE=\"${ORPHAN_CHOICE:-1}\"\n case \"${ORPHAN_CHOICE}\" in\n 1|clean)\n ${DOCKER_CMD} stop agentteams-manager 2>/dev/null || true\n ${DOCKER_CMD} rm agentteams-manager 2>/dev/null || true\n for w in $(${DOCKER_CMD} ps -a --format '{{.Names}}' | grep \"^agentteams-worker-\" || true); do\n ${DOCKER_CMD} stop \"${w}\" 2>/dev/null || true\n ${DOCKER_CMD} rm \"${w}\" 2>/dev/null || true\n done\n log \"$(msg install.orphan_volume.cleaning)\"\n ${DOCKER_CMD} volume rm \"${data_vol}\" 2>/dev/null || true\n log \"$(msg install.orphan_volume.cleaned)\"\n ;;\n 2|keep)\n log \"$(msg install.orphan_volume.keeping)\"\n ;;\n esac\n fi\n fi\n fi\n\n # ── State machine ─────────────────────────────────────────────────────────\n local _STEPS=( step_lang step_mode step_version step_existing step_llm step_manager_runtime step_runtime step_admin step_network\n step_ports step_domains step_github step_skills step_volume\n step_workspace step_e2ee step_docker_proxy step_idle step_hostshare step_podman_autostart )\n local _STEP_HISTORY=()\n local _step_idx=0\n while [ \"${_step_idx}\" -lt \"${#_STEPS[@]}\" ]; do\n local _step_fn=\"${_STEPS[$_step_idx]}\"\n if should_skip_step \"${_step_fn}\"; then\n _step_idx=$((_step_idx + 1))\n continue\n fi\n if [ \"${#_STEP_HISTORY[@]}\" -gt 0 ]; then\n log \"$(msg nav.back_hint)\"\n fi\n STEP_RESULT=\"\"\n \"${_step_fn}\"\n if [ \"${STEP_RESULT}\" = \"back\" ]; then\n if [ \"${#_STEP_HISTORY[@]}\" -gt 0 ]; then\n local _last=$(( ${#_STEP_HISTORY[@]} - 1 ))\n _step_idx=\"${_STEP_HISTORY[$_last]}\"\n _STEP_HISTORY=(\"${_STEP_HISTORY[@]:0:${_last}}\")\n clear_step_vars \"${_STEPS[$_step_idx]}\"\n fi\n # else: first step, ignore 'b'\n else\n _STEP_HISTORY+=(\"${_step_idx}\")\n _step_idx=$((_step_idx + 1))\n fi\n done\n # ── End state machine ──────────────────────────────────────────────────────\n\n # Post-machine defaults for any steps that were skipped\n AGENTTEAMS_DATA_DIR=\"${AGENTTEAMS_DATA_DIR:-agentteams-data}\"\n if [ -z \"${AGENTTEAMS_WORKSPACE_DIR+x}\" ] || [ -z \"${AGENTTEAMS_WORKSPACE_DIR}\" ]; then\n AGENTTEAMS_WORKSPACE_DIR=\"${HOME}/agentteams-manager\"\n export AGENTTEAMS_WORKSPACE_DIR\n fi\n AGENTTEAMS_WORKSPACE_DIR=\"$(cd \"${AGENTTEAMS_WORKSPACE_DIR}\" 2>/dev/null && pwd || echo \"${AGENTTEAMS_WORKSPACE_DIR}\")\"\n mkdir -p \"${AGENTTEAMS_WORKSPACE_DIR}\"\n AGENTTEAMS_MANAGER_RUNTIME=\"${AGENTTEAMS_MANAGER_RUNTIME:-copaw}\"\n export AGENTTEAMS_MANAGER_RUNTIME\n AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"${AGENTTEAMS_DEFAULT_WORKER_RUNTIME:-copaw}\"\n AGENTTEAMS_MATRIX_E2EE=\"${AGENTTEAMS_MATRIX_E2EE:-0}\"\n export AGENTTEAMS_MATRIX_E2EE\n AGENTTEAMS_WORKER_IDLE_TIMEOUT=\"${AGENTTEAMS_WORKER_IDLE_TIMEOUT:-720}\"\n export AGENTTEAMS_WORKER_IDLE_TIMEOUT\n AGENTTEAMS_HOST_SHARE_DIR=\"${AGENTTEAMS_HOST_SHARE_DIR:-$HOME}\"\n export AGENTTEAMS_HOST_SHARE_DIR\n\n log \"\"\n\n # Generate secrets (only if not already set)\n log \"$(msg install.generating_secrets)\"\n AGENTTEAMS_MANAGER_PASSWORD=\"${AGENTTEAMS_MANAGER_PASSWORD:-$(generate_key)}\"\n AGENTTEAMS_REGISTRATION_TOKEN=\"${AGENTTEAMS_REGISTRATION_TOKEN:-$(generate_key)}\"\n AGENTTEAMS_MINIO_USER=\"${AGENTTEAMS_MINIO_USER:-${AGENTTEAMS_ADMIN_USER}}\"\n AGENTTEAMS_MINIO_PASSWORD=\"${AGENTTEAMS_MINIO_PASSWORD:-${AGENTTEAMS_ADMIN_PASSWORD}}\"\n AGENTTEAMS_MANAGER_GATEWAY_KEY=\"${AGENTTEAMS_MANAGER_GATEWAY_KEY:-$(generate_key)}\"\n\n # Matrix AppService tokens — generate once during install/upgrade if not provided.\n # Persisted to env file so they survive controller restarts.\n AGENTTEAMS_MATRIX_APPSERVICE_ENABLED=\"${AGENTTEAMS_MATRIX_APPSERVICE_ENABLED:-true}\"\n export AGENTTEAMS_MATRIX_APPSERVICE_ENABLED\n if [ \"${AGENTTEAMS_MATRIX_APPSERVICE_ENABLED}\" != \"false\" ] && [ \"${AGENTTEAMS_MATRIX_APPSERVICE_ENABLED}\" != \"0\" ]; then\n if [ -z \"${AGENTTEAMS_MATRIX_APPSERVICE_AS_TOKEN}\" ]; then\n AGENTTEAMS_MATRIX_APPSERVICE_AS_TOKEN=\"$(openssl rand -hex 32)\"\n log \" Auto-generated AppService as_token (saved to env file)\"\n fi\n if [ -z \"${AGENTTEAMS_MATRIX_APPSERVICE_HS_TOKEN}\" ]; then\n AGENTTEAMS_MATRIX_APPSERVICE_HS_TOKEN=\"$(openssl rand -hex 32)\"\n log \" Auto-generated AppService hs_token (saved to env file)\"\n fi\n export AGENTTEAMS_MATRIX_APPSERVICE_AS_TOKEN\n export AGENTTEAMS_MATRIX_APPSERVICE_HS_TOKEN\n fi\n\n # Detect Apple Silicon (M1/M2/M3/M4) - need JVM fix for Higress Console\n # See: https://github.com/agentscope-ai/AgentTeams/issues/249\n if [ -z \"${JVM_ARGS:-}\" ] && [ \"$(uname -m)\" = \"arm64\" ] && [ \"$(uname -s)\" = \"Darwin\" ]; then\n log \"Apple Silicon detected - setting JVM_ARGS to fix Higress Console SIGILL issue\"\n JVM_ARGS=\"-XX:+UnlockDiagnosticVMOptions -XX:-UseAESCTRIntrinsics -XX:UseSVE=0\"\n fi\n\n # Write .env file\n ENV_FILE=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n cat > \"${ENV_FILE}\" << EOF\n# AgentTeams Manager Configuration\n# Generated by agentteams-install.sh on $(date)\n\n# Language\nAGENTTEAMS_LANGUAGE=${AGENTTEAMS_LANGUAGE}\n\n# LLM\nAGENTTEAMS_LLM_PROVIDER=${AGENTTEAMS_LLM_PROVIDER}\nAGENTTEAMS_DEFAULT_MODEL=${AGENTTEAMS_DEFAULT_MODEL}\nAGENTTEAMS_LLM_API_KEY=${AGENTTEAMS_LLM_API_KEY}\nAGENTTEAMS_OPENAI_BASE_URL=${AGENTTEAMS_OPENAI_BASE_URL:-}\nAGENTTEAMS_MODEL_CONTEXT_WINDOW=${AGENTTEAMS_MODEL_CONTEXT_WINDOW:-}\nAGENTTEAMS_MODEL_MAX_TOKENS=${AGENTTEAMS_MODEL_MAX_TOKENS:-}\nAGENTTEAMS_MODEL_REASONING=${AGENTTEAMS_MODEL_REASONING:-}\nAGENTTEAMS_MODEL_VISION=${AGENTTEAMS_MODEL_VISION:-}\n\n# Embedding model (empty = disabled, default: text-embedding-v4)\nAGENTTEAMS_EMBEDDING_MODEL=${AGENTTEAMS_EMBEDDING_MODEL}\n\n# Admin\nAGENTTEAMS_ADMIN_USER=${AGENTTEAMS_ADMIN_USER}\nAGENTTEAMS_ADMIN_PASSWORD=${AGENTTEAMS_ADMIN_PASSWORD}\n\n# Ports\nAGENTTEAMS_LOCAL_ONLY=${AGENTTEAMS_LOCAL_ONLY}\nAGENTTEAMS_PORT_GATEWAY=${AGENTTEAMS_PORT_GATEWAY}\nAGENTTEAMS_PORT_CONSOLE=${AGENTTEAMS_PORT_CONSOLE}\nAGENTTEAMS_PORT_ELEMENT_WEB=${AGENTTEAMS_PORT_ELEMENT_WEB}\nAGENTTEAMS_PORT_MANAGER_CONSOLE=${AGENTTEAMS_PORT_MANAGER_CONSOLE:-18888}\n\n# Manager runtime (openclaw | copaw)\nAGENTTEAMS_MANAGER_RUNTIME=${AGENTTEAMS_MANAGER_RUNTIME:-copaw}\n\n# Matrix\nAGENTTEAMS_MATRIX_DOMAIN=${AGENTTEAMS_MATRIX_DOMAIN}\nAGENTTEAMS_MATRIX_CLIENT_DOMAIN=${AGENTTEAMS_MATRIX_CLIENT_DOMAIN}\n\n# Gateway\nAGENTTEAMS_AI_GATEWAY_DOMAIN=${AGENTTEAMS_AI_GATEWAY_DOMAIN}\nAGENTTEAMS_MANAGER_GATEWAY_KEY=${AGENTTEAMS_MANAGER_GATEWAY_KEY}\n\n# File System\nAGENTTEAMS_FS_DOMAIN=${AGENTTEAMS_FS_DOMAIN}\nAGENTTEAMS_CONSOLE_DOMAIN=${AGENTTEAMS_CONSOLE_DOMAIN}\nAGENTTEAMS_MINIO_USER=${AGENTTEAMS_MINIO_USER}\nAGENTTEAMS_MINIO_PASSWORD=${AGENTTEAMS_MINIO_PASSWORD}\n\n# Internal\nAGENTTEAMS_MANAGER_PASSWORD=${AGENTTEAMS_MANAGER_PASSWORD}\nAGENTTEAMS_REGISTRATION_TOKEN=${AGENTTEAMS_REGISTRATION_TOKEN}\n\n# GitHub (optional)\nAGENTTEAMS_GITHUB_TOKEN=${AGENTTEAMS_GITHUB_TOKEN:-}\n\n# Nacos package import defaults\nAGENTTEAMS_NACOS_REGISTRY_URI=${AGENTTEAMS_NACOS_REGISTRY_URI:-nacos://market.agentteams.io:80/public}\nAGENTTEAMS_NACOS_USERNAME=${AGENTTEAMS_NACOS_USERNAME:-}\nAGENTTEAMS_NACOS_PASSWORD=${AGENTTEAMS_NACOS_PASSWORD:-}\nAGENTTEAMS_NACOS_TOKEN=${AGENTTEAMS_NACOS_TOKEN:-}\n\n# Skills Registry (optional, default: nacos://market.agentteams.io:80/public)\nAGENTTEAMS_SKILLS_API_URL=${AGENTTEAMS_SKILLS_API_URL:-nacos://market.agentteams.io:80/public}\n\n# OpenClaw CMS plugin (optional)\nAGENTTEAMS_CMS_TRACES_ENABLED=${AGENTTEAMS_CMS_TRACES_ENABLED:-false}\nAGENTTEAMS_CMS_ENDPOINT=${AGENTTEAMS_CMS_ENDPOINT:-}\nAGENTTEAMS_CMS_LICENSE_KEY=${AGENTTEAMS_CMS_LICENSE_KEY:-}\nAGENTTEAMS_CMS_PROJECT=${AGENTTEAMS_CMS_PROJECT:-}\nAGENTTEAMS_CMS_WORKSPACE=${AGENTTEAMS_CMS_WORKSPACE:-}\nAGENTTEAMS_CMS_SERVICE_NAME=${AGENTTEAMS_CMS_SERVICE_NAME:-agentteams-manager}\nAGENTTEAMS_CMS_METRICS_ENABLED=${AGENTTEAMS_CMS_METRICS_ENABLED:-false}\n\n# Worker images (for direct container creation)\nAGENTTEAMS_WORKER_IMAGE=${WORKER_IMAGE}\nAGENTTEAMS_COPAW_WORKER_IMAGE=${COPAW_WORKER_IMAGE}\nAGENTTEAMS_HERMES_WORKER_IMAGE=${HERMES_WORKER_IMAGE}\n\n# Default Worker runtime (openclaw | copaw | hermes)\nAGENTTEAMS_DEFAULT_WORKER_RUNTIME=${AGENTTEAMS_DEFAULT_WORKER_RUNTIME:-copaw}\n\n# Matrix E2EE (0=disabled, 1=enabled; default: 0)\nAGENTTEAMS_MATRIX_E2EE=${AGENTTEAMS_MATRIX_E2EE:-0}\n\n# Matrix AppService\nAGENTTEAMS_MATRIX_APPSERVICE_ENABLED=${AGENTTEAMS_MATRIX_APPSERVICE_ENABLED:-true}\nAGENTTEAMS_MATRIX_APPSERVICE_AS_TOKEN=${AGENTTEAMS_MATRIX_APPSERVICE_AS_TOKEN:-}\nAGENTTEAMS_MATRIX_APPSERVICE_HS_TOKEN=${AGENTTEAMS_MATRIX_APPSERVICE_HS_TOKEN:-}\n\n# Docker API proxy (0=disabled, 1=enabled; default: 1)\nAGENTTEAMS_DOCKER_PROXY=${AGENTTEAMS_DOCKER_PROXY:-1}\n\n# Docker API proxy: additional allowed image sources (comma-separated)\nAGENTTEAMS_PROXY_ALLOWED_REGISTRIES=${AGENTTEAMS_PROXY_ALLOWED_REGISTRIES:-}\n\n# Worker idle timeout in minutes (default: 720 = 12 hours)\nAGENTTEAMS_WORKER_IDLE_TIMEOUT=${AGENTTEAMS_WORKER_IDLE_TIMEOUT:-720}\n\n# Podman autostart via systemd (0=disabled, 1=enabled)\nAGENTTEAMS_PODMAN_AUTOSTART=${AGENTTEAMS_PODMAN_AUTOSTART:-0}\n\n# JVM Args for Higress Console (fixes SIGILL on Apple Silicon)\nJVM_ARGS=${JVM_ARGS:-}\n\n# Higress WASM plugin image registry (auto-selected by timezone)\nHIGRESS_ADMIN_WASM_PLUGIN_IMAGE_REGISTRY=${AGENTTEAMS_REGISTRY}\n\n# Data persistence\nAGENTTEAMS_DATA_DIR=${AGENTTEAMS_DATA_DIR:-agentteams-data}\n# Manager workspace (skills, memory, state — host-editable)\nAGENTTEAMS_WORKSPACE_DIR=${AGENTTEAMS_WORKSPACE_DIR:-}\n# Host directory sharing\nAGENTTEAMS_HOST_SHARE_DIR=${AGENTTEAMS_HOST_SHARE_DIR:-}\nEOF\n\n chmod 600 \"${ENV_FILE}\"\n log \"$(msg install.config_saved \"${ENV_FILE}\")\"\n\n # Detect container runtime socket\n SOCKET_MOUNT_ARGS=\"\"\n if [ \"${AGENTTEAMS_MOUNT_SOCKET}\" = \"1\" ]; then\n # Actively ensure the API socket is enabled and active before detection.\n if [ \"${DOCKER_CMD:-}\" = \"podman\" ]; then\n ensure_podman_socket\n fi\n if [ -z \"${CONTAINER_SOCK}\" ]; then\n CONTAINER_SOCK=$(detect_socket)\n fi\n if [ -n \"${CONTAINER_SOCK}\" ]; then\n log \"$(msg install.socket_detected \"${CONTAINER_SOCK}\")\"\n SOCKET_MOUNT_ARGS=\"-v ${CONTAINER_SOCK}:/var/run/docker.sock --security-opt label=disable\"\n else\n log \"$(msg install.socket_not_found)\"\n # Interactive confirmation when socket not found\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" != \"1\" ]; then\n echo \"\"\n echo -e \"\\033[33m$(msg install.socket_confirm.title)\\033[0m\"\n echo \"\"\n echo -e \"$(msg install.socket_confirm.message)\"\n echo \"\"\n read -p \"$(msg install.socket_confirm.prompt)\" SOCKET_CONFIRM\n if [ \"${SOCKET_CONFIRM}\" != \"y\" ] && [ \"${SOCKET_CONFIRM}\" != \"Y\" ]; then\n log \"$(msg install.socket_confirm.cancelled)\"\n exit 0\n fi\n fi\n fi\n fi\n\n # Create the data volume if it doesn't already exist (reuse on reinstall)\n if ! ${DOCKER_CMD} volume ls -q | grep -q \"^${AGENTTEAMS_DATA_DIR}$\"; then\n ${DOCKER_CMD} volume create \"${AGENTTEAMS_DATA_DIR}\" > /dev/null\n fi\n\n # Data mount: Docker volume\n DATA_MOUNT_ARGS=\"-v ${AGENTTEAMS_DATA_DIR}:/data\"\n\n # Manager workspace mount (always a host directory, defaulting to ~/agentteams-manager)\n WORKSPACE_MOUNT_ARGS=\"-v ${AGENTTEAMS_WORKSPACE_DIR}:/root/manager-workspace\"\n\n # Pass host timezone to container so date/time commands reflect local time\n TZ_ARGS=\"-e TZ=${AGENTTEAMS_TIMEZONE}\"\n\n # Host directory mount\n if [ -d \"${AGENTTEAMS_HOST_SHARE_DIR}\" ]; then\n HOST_SHARE_MOUNT_ARGS=\"-v ${AGENTTEAMS_HOST_SHARE_DIR}:/host-share\"\n log \"$(msg host_share.sharing \"${AGENTTEAMS_HOST_SHARE_DIR}\")\"\n else\n log \"$(msg host_share.not_exist \"${AGENTTEAMS_HOST_SHARE_DIR}\")\"\n HOST_SHARE_MOUNT_ARGS=\"-v ${AGENTTEAMS_HOST_SHARE_DIR}:/host-share\"\n fi\n\n # YOLO mode: pass through if set in environment (enables autonomous decisions)\n YOLO_ARGS=\"\"\n if [ \"${AGENTTEAMS_YOLO:-}\" = \"1\" ]; then\n YOLO_ARGS=\"-e AGENTTEAMS_YOLO=1\"\n log \"$(msg install.yolo)\"\n fi\n\n # Matrix-plugin debug tracing: pass through if AGENTTEAMS_MATRIX_DEBUG=1.\n # The container entrypoints translate this to OPENCLAW_MATRIX_DEBUG=1\n # so the openclaw matrix plugin emits structured INFO-level lifecycle\n # traces (sync state transitions, room.invite/join, message handler\n # arrival + filter outcomes). Used to diagnose worker/manager hangs.\n MATRIX_DEBUG_ARGS=\"\"\n if [ \"${AGENTTEAMS_MATRIX_DEBUG:-}\" = \"1\" ]; then\n MATRIX_DEBUG_ARGS=\"-e AGENTTEAMS_MATRIX_DEBUG=1\"\n fi\n\n # E2EE is already in the env file; but also pass explicitly in case env file is not the source\n # (AGENTTEAMS_MATRIX_E2EE is already written to ENV_FILE above via --env-file)\n\n # Pull images (manager based on runtime config; all worker runtimes always pulled)\n _is_local_image() {\n case \"$1\" in\n agentteams/*) return 0 ;;\n *) return 1 ;;\n esac\n }\n\n _local_image_exists() {\n local _img=\"$1\"\n [ -z \"${_img}\" ] && return 1\n _is_local_image \"${_img}\" || return 1\n ${DOCKER_CMD} image inspect \"${_img}\" >/dev/null 2>&1\n }\n\n # Helper: pull or skip a single image\n # Args: $1=image $2=exists_msg_key $3=pulling_msg_key\n _pull_image() {\n local _img=\"$1\" _exists_key=\"$2\" _pull_key=\"$3\"\n [ -z \"${_img}\" ] && return 0\n if _local_image_exists \"${_img}\"; then\n log \"$(msg \"${_exists_key}\" \"${_img}\")\"\n return 0\n fi\n log \"$(msg \"${_pull_key}\" \"${_img}\")\"\n local _attempt=1\n while [ $_attempt -le 3 ]; do\n if ${DOCKER_CMD} pull \"${_img}\"; then\n return 0\n fi\n if [ $_attempt -lt 3 ]; then\n log \"Pull failed (attempt ${_attempt}/3), retrying in 5s...\"\n sleep 5\n fi\n _attempt=$((_attempt + 1))\n done\n die \"Failed to pull ${_img} after 3 attempts\"\n return 1\n }\n\n # Embedded controller image (resolve versioned tag, fallback to latest)\n resolve_embedded_image\n if [ \"${AGENTTEAMS_USE_EMBEDDED}\" = \"1\" ]; then\n _local_image_exists \"${EMBEDDED_IMAGE}\" || true\n fi\n\n # Manager image is always required (select based on runtime)\n if [ \"${AGENTTEAMS_MANAGER_RUNTIME}\" = \"copaw\" ]; then\n _pull_image \"${MANAGER_COPAW_IMAGE}\" \"install.image.exists\" \"install.image.pulling_manager\"\n else\n _pull_image \"${MANAGER_IMAGE}\" \"install.image.exists\" \"install.image.pulling_manager\"\n fi\n\n # Pull all worker runtime images (workers may use any runtime regardless of the default)\n _pull_image \"${WORKER_IMAGE}\" \"install.image.worker_exists\" \"install.image.pulling_worker\"\n _pull_image \"${COPAW_WORKER_IMAGE}\" \"install.image.worker_exists\" \"install.image.pulling_worker\"\n _pull_image \"${HERMES_WORKER_IMAGE}\" \"install.image.worker_exists\" \"install.image.pulling_worker\"\n\n # --- Pre-upgrade: extract Matrix passwords from running old containers ---\n # Only needed when upgrading FROM old architecture (v1.0.9) TO embedded.\n # For new-arch-to-new-arch upgrades, credential files already exist with\n # correct room IDs — we must NOT overwrite them.\n _creds_tmp=\"\"\n if [ \"${AGENTTEAMS_UPGRADE:-0}\" = \"1\" ] && [ \"${AGENTTEAMS_USE_EMBEDDED}\" = \"1\" ]; then\n # Detect if upgrading from old arch: old arch has no agentteams-controller container\n # (or has it as a docker-proxy only, not embedded). Check if the existing\n # agentteams-controller is an embedded image (has supervisord) or just a proxy.\n _is_old_arch=0\n if ! ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -q \"^agentteams-controller$\"; then\n _is_old_arch=1\n elif ${DOCKER_CMD} ps -a --format '{{.Names}} {{.Image}}' 2>/dev/null | grep \"^agentteams-controller \" | grep -qv \"embedded\"; then\n _is_old_arch=1\n fi\n\n if [ \"${_is_old_arch}\" = \"1\" ]; then\n _creds_tmp=$(mktemp -d)\n\n # docker exec for Matrix/minio paths only works while agentteams-manager is running.\n _mgr_creds_tempstart=0\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -q '^agentteams-manager$'; then\n if ! ${DOCKER_CMD} ps --format '{{.Names}}' 2>/dev/null | grep -q '^agentteams-manager$'; then\n log \"agentteams-manager is stopped; starting it temporarily to extract Matrix credentials for upgrade...\"\n ${DOCKER_CMD} start agentteams-manager 2>/dev/null || true\n wait_matrix_ready \"agentteams-manager\"\n _mgr_creds_tempstart=1\n fi\n fi\n\n # Manager password (container Config.Env, then secrets file inside running manager, then data volume)\n _mgr_pw=$(${DOCKER_CMD} inspect agentteams-manager --format '{{range .Config.Env}}{{println .}}{{end}}' 2>/dev/null | grep '^AGENTTEAMS_MANAGER_PASSWORD=' | cut -d= -f2-)\n if [ -z \"${_mgr_pw}\" ] && ${DOCKER_CMD} ps --format '{{.Names}}' 2>/dev/null | grep -q '^agentteams-manager$'; then\n _mgr_pw=$(${DOCKER_CMD} exec agentteams-manager bash -c 'source /data/agentteams-secrets.env 2>/dev/null && echo \"${AGENTTEAMS_MANAGER_PASSWORD}\"' 2>/dev/null)\n fi\n if [ -z \"${_mgr_pw}\" ] && ${DOCKER_CMD} volume ls -q 2>/dev/null | grep -q \"^${AGENTTEAMS_DATA_DIR}$\"; then\n _mgr_pw=$(agentteams_read_secret_from_data_volume \"${AGENTTEAMS_DATA_DIR}\" AGENTTEAMS_MANAGER_PASSWORD)\n fi\n\n # Manager admin DM room ID: login as admin, find DM room with @manager\n _mgr_room=\"\"\n if [ -n \"${_mgr_pw}\" ]; then\n _admin_pw=$(grep AGENTTEAMS_ADMIN_PASSWORD \"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\" 2>/dev/null | cut -d= -f2-)\n _admin_user=$(grep AGENTTEAMS_ADMIN_USER \"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\" 2>/dev/null | cut -d= -f2-)\n _admin_user=\"${_admin_user:-admin}\"\n _matrix_domain=$(grep AGENTTEAMS_MATRIX_DOMAIN \"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\" 2>/dev/null | cut -d= -f2-)\n if [ -n \"${_admin_pw}\" ] && ${DOCKER_CMD} ps --format '{{.Names}}' 2>/dev/null | grep -q '^agentteams-manager$'; then\n _admin_token=$(${DOCKER_CMD} exec agentteams-manager curl -sf -X POST http://127.0.0.1:6167/_matrix/client/v3/login \\\n -H \"Content-Type: application/json\" \\\n -d '{\"type\":\"m.login.password\",\"identifier\":{\"type\":\"m.id.user\",\"user\":\"'\"${_admin_user}\"'\"},\"password\":\"'\"${_admin_pw}\"'\"}' 2>/dev/null | python3 -c \"import sys,json; print(json.load(sys.stdin).get('access_token',''))\" 2>/dev/null || true)\n if [ -n \"${_admin_token}\" ]; then\n _mgr_room=$(${DOCKER_CMD} exec agentteams-manager curl -sf -X GET \\\n -H \"Authorization: Bearer ${_admin_token}\" \\\n \"http://127.0.0.1:6167/_matrix/client/v3/joined_rooms\" 2>/dev/null | python3 -c \"\nimport sys,json,subprocess\nrooms = json.load(sys.stdin).get('joined_rooms',[])\nfor room_id in rooms:\n enc = room_id.replace('!','%21')\n members = json.loads(subprocess.check_output([\n 'docker','exec','agentteams-manager','curl','-sf','-X','GET',\n '-H','Authorization: Bearer ${_admin_token}',\n 'http://127.0.0.1:6167/_matrix/client/v3/rooms/'+enc+'/members'\n ]).decode()).get('chunk',[])\n member_ids = [m['state_key'] for m in members]\n if any('manager' in m and 'admin' not in m.split(':')[0] for m in member_ids):\n if len(member_ids) <= 3:\n print(room_id)\n break\n\" 2>/dev/null || true)\n fi\n fi\n if [ -z \"${_mgr_room}\" ]; then\n _mgr_room=$(agentteams_read_admin_dm_room_from_workspace \"${AGENTTEAMS_WORKSPACE_DIR}\")\n fi\n cat > \"${_creds_tmp}/default.env\" <<CREDEOF\nWORKER_PASSWORD=\"${_mgr_pw}\"\nWORKER_MINIO_PASSWORD=\"$(openssl rand -hex 24)\"\nWORKER_GATEWAY_KEY=\"${AGENTTEAMS_MANAGER_GATEWAY_KEY}\"\nWORKER_ROOM_ID=\"${_mgr_room}\"\nCREDEOF\n log \"Extracted Manager Matrix password${_mgr_room:+ and room ID}\"\n fi\n\n # Worker passwords and room IDs from workers-registry.json\n if [ -f \"${AGENTTEAMS_WORKSPACE_DIR}/workers-registry.json\" ]; then\n _worker_names=$(python3 -c \"import json; d=json.load(open('${AGENTTEAMS_WORKSPACE_DIR}/workers-registry.json')); print(' '.join(d.get('workers',{}).keys()))\" 2>/dev/null || true)\n for _wname in ${_worker_names}; do\n _wpw=\"\"\n if ${DOCKER_CMD} ps --format '{{.Names}}' 2>/dev/null | grep -q '^agentteams-manager$'; then\n _wpw=$(${DOCKER_CMD} exec agentteams-manager cat \"/root/agentteams-fs/agents/${_wname}/credentials/matrix/password\" 2>/dev/null || true)\n fi\n if [ -z \"${_wpw}\" ] && ${DOCKER_CMD} volume ls -q 2>/dev/null | grep -q \"^${AGENTTEAMS_DATA_DIR}$\"; then\n _wpw=$(agentteams_read_worker_creds_value_from_volume \"${AGENTTEAMS_DATA_DIR}\" \"${_wname}\" WORKER_PASSWORD)\n fi\n _wroom=$(python3 -c \"import json; d=json.load(open('${AGENTTEAMS_WORKSPACE_DIR}/workers-registry.json')); print(d.get('workers',{}).get('${_wname}',{}).get('room_id',''))\" 2>/dev/null || true)\n if [ -z \"${_wroom}\" ] && ${DOCKER_CMD} volume ls -q 2>/dev/null | grep -q \"^${AGENTTEAMS_DATA_DIR}$\"; then\n _wroom=$(agentteams_read_worker_creds_value_from_volume \"${AGENTTEAMS_DATA_DIR}\" \"${_wname}\" WORKER_ROOM_ID)\n fi\n if [ -n \"${_wpw}\" ]; then\n cat > \"${_creds_tmp}/${_wname}.env\" <<CREDEOF\nWORKER_PASSWORD=\"${_wpw}\"\nWORKER_MINIO_PASSWORD=\"$(openssl rand -hex 24)\"\nWORKER_GATEWAY_KEY=\"$(openssl rand -hex 32)\"\nWORKER_ROOM_ID=\"${_wroom}\"\nCREDEOF\n log \"Extracted ${_wname} Matrix password${_wroom:+ and room ID}\"\n fi\n done\n fi\n\n if [ \"${_mgr_creds_tempstart}\" = \"1\" ]; then\n log \"Stopping agentteams-manager after credential extraction (upgrade will recreate containers)...\"\n ${DOCKER_CMD} stop agentteams-manager 2>/dev/null || true\n fi\n fi # _is_old_arch\n fi\n\n # --- Stop and remove existing containers ---\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' | grep -q \"^agentteams-controller$\"; then\n ${DOCKER_CMD} stop agentteams-controller 2>/dev/null || true\n ${DOCKER_CMD} rm agentteams-controller 2>/dev/null || true\n fi\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' | grep -q \"^agentteams-manager$\"; then\n log \"$(msg install.removing_existing)\"\n ${DOCKER_CMD} stop agentteams-manager 2>/dev/null || true\n ${DOCKER_CMD} rm agentteams-manager 2>/dev/null || true\n fi\n\n # Stop and remove worker containers (controller will recreate via CR reconciliation)\n if [ -n \"${UPGRADE_EXISTING_WORKERS:-}\" ]; then\n log \"$(msg install.existing.stopping_workers)\"\n for w in ${UPGRADE_EXISTING_WORKERS}; do\n ${DOCKER_CMD} stop \"${w}\" 2>/dev/null || true\n ${DOCKER_CMD} rm \"${w}\" 2>/dev/null || true\n log \"$(msg install.existing.removed \"${w}\")\"\n done\n fi\n\n # --- Upgrade: inject extracted credentials into data volume ---\n # Only needed for old-arch upgrades (credential files were extracted above).\n if [ -n \"${_creds_tmp}\" ] && [ -d \"${_creds_tmp}\" ] && [ -n \"$(ls -A \"${_creds_tmp}\" 2>/dev/null)\" ]; then\n local _cleanup_ctr=\"agentteams-upgrade-cleanup\"\n ${DOCKER_CMD} rm -f \"${_cleanup_ctr}\" 2>/dev/null || true\n ${DOCKER_CMD} run --rm --name \"${_cleanup_ctr}\" \\\n --entrypoint sh \\\n -v \"${AGENTTEAMS_DATA_DIR}:/data\" \\\n -v \"${_creds_tmp}:/creds:ro\" \\\n \"${EMBEDDED_IMAGE}\" -c '\n rm -rf /data/worker-creds\n mkdir -p /data/worker-creds\n cp /creds/*.env /data/worker-creds/ 2>/dev/null || true\n chmod 600 /data/worker-creds/*.env 2>/dev/null || true\n ' 2>/dev/null && log \"Injected credentials for upgrade\" || log \"Warning: credential injection failed, continuing\"\n rm -rf \"${_creds_tmp}\"\n fi\n\n # --- Start containers ---\n log \"$(msg install.starting_manager)\"\n\n # Build port binding args\n if [ \"${AGENTTEAMS_LOCAL_ONLY:-1}\" = \"1\" ]; then\n _port_prefix=\"127.0.0.1:\"\n else\n _port_prefix=\"\"\n fi\n\n # Ensure agentteams-net Docker network exists\n ${DOCKER_CMD} network inspect agentteams-net >/dev/null 2>&1 || ${DOCKER_CMD} network create agentteams-net\n\n if [ \"${AGENTTEAMS_USE_EMBEDDED}\" != \"1\" ] && [ \"${AGENTTEAMS_UPGRADE:-0}\" = \"1\" ]; then\n # Check if current installation is embedded — downgrade to legacy is not supported\n if ${DOCKER_CMD} ps -a --format '{{.Names}} {{.Image}}' 2>/dev/null | grep \"^agentteams-controller \" | grep -q \"embedded\"; then\n error \"Downgrade from embedded architecture to legacy version (${AGENTTEAMS_VERSION}) is not supported.\"\n error \"Please use 'make uninstall-embedded' first, then do a clean install of the target version.\"\n exit 1\n fi\n fi\n\n if [ \"${AGENTTEAMS_USE_EMBEDDED}\" = \"1\" ]; then\n # ============================================================\n # New architecture: embedded controller + auto-created manager\n # ============================================================\n\n # Internal port: 8080 (Higress gateway inside the container).\n local _internal_gw_port=8080\n local _matrix_domain=\"${AGENTTEAMS_MATRIX_DOMAIN:-matrix-local.agentteams.io:${AGENTTEAMS_PORT_GATEWAY}}\"\n local _aigw_domain=\"${AGENTTEAMS_AI_GATEWAY_DOMAIN:-aigw-local.agentteams.io}\"\n # Ensure internal gateway port is present (container-internal traffic uses 8080)\n case \"${_aigw_domain}\" in *:*) ;; *) _aigw_domain=\"${_aigw_domain}:${_internal_gw_port}\" ;; esac\n local _fs_domain=\"${AGENTTEAMS_FS_DOMAIN:-fs-local.agentteams.io}\"\n case \"${_fs_domain}\" in *:*) ;; *) _fs_domain=\"${_fs_domain}:${_internal_gw_port}\" ;; esac\n\n # Controller env args\n local _ctrl_env_args=(\n -e \"AGENTTEAMS_ADMIN_USER=${AGENTTEAMS_ADMIN_USER}\"\n -e \"AGENTTEAMS_ADMIN_PASSWORD=${AGENTTEAMS_ADMIN_PASSWORD}\"\n -e \"AGENTTEAMS_MANAGER_PASSWORD=${AGENTTEAMS_MANAGER_PASSWORD}\"\n -e \"AGENTTEAMS_REGISTRATION_TOKEN=${AGENTTEAMS_REGISTRATION_TOKEN}\"\n -e \"AGENTTEAMS_MINIO_USER=${AGENTTEAMS_MINIO_USER}\"\n -e \"AGENTTEAMS_MINIO_PASSWORD=${AGENTTEAMS_MINIO_PASSWORD}\"\n -e \"AGENTTEAMS_LLM_PROVIDER=${AGENTTEAMS_LLM_PROVIDER}\"\n -e \"AGENTTEAMS_LLM_API_KEY=${AGENTTEAMS_LLM_API_KEY}\"\n -e \"AGENTTEAMS_DEFAULT_MODEL=${AGENTTEAMS_DEFAULT_MODEL}\"\n -e \"AGENTTEAMS_MANAGER_GATEWAY_KEY=${AGENTTEAMS_MANAGER_GATEWAY_KEY}\"\n -e \"AGENTTEAMS_MANAGER_RUNTIME=${AGENTTEAMS_MANAGER_RUNTIME:-copaw}\"\n -e \"AGENTTEAMS_MANAGER_IMAGE=$([ \"${AGENTTEAMS_MANAGER_RUNTIME}\" = \"copaw\" ] && echo \"${MANAGER_COPAW_IMAGE}\" || echo \"${MANAGER_IMAGE}\")\"\n -e \"AGENTTEAMS_DEFAULT_WORKER_RUNTIME=${AGENTTEAMS_DEFAULT_WORKER_RUNTIME:-copaw}\"\n -e \"AGENTTEAMS_WORKER_IMAGE=${WORKER_IMAGE}\"\n -e \"AGENTTEAMS_COPAW_WORKER_IMAGE=${COPAW_WORKER_IMAGE}\"\n -e \"AGENTTEAMS_HERMES_WORKER_IMAGE=${HERMES_WORKER_IMAGE}\"\n -e \"AGENTTEAMS_MATRIX_DOMAIN=${_matrix_domain}\"\n -e \"AGENTTEAMS_ELEMENT_HOMESERVER_URL=http://127.0.0.1:${AGENTTEAMS_PORT_GATEWAY}\"\n -e \"AGENTTEAMS_MATRIX_URL=http://127.0.0.1:6167\"\n -e \"AGENTTEAMS_MATRIX_E2EE=${AGENTTEAMS_MATRIX_E2EE:-0}\"\n -e \"AGENTTEAMS_MATRIX_APPSERVICE_ENABLED=${AGENTTEAMS_MATRIX_APPSERVICE_ENABLED:-true}\"\n -e \"AGENTTEAMS_MATRIX_APPSERVICE_AS_TOKEN=${AGENTTEAMS_MATRIX_APPSERVICE_AS_TOKEN:-}\"\n -e \"AGENTTEAMS_MATRIX_APPSERVICE_HS_TOKEN=${AGENTTEAMS_MATRIX_APPSERVICE_HS_TOKEN:-}\"\n -e \"AGENTTEAMS_MINIO_ENDPOINT=http://127.0.0.1:9000\"\n -e \"AGENTTEAMS_MINIO_BUCKET=agentteams-storage\"\n -e \"AGENTTEAMS_STORAGE_PREFIX=agentteams/agentteams-storage\"\n -e \"AGENTTEAMS_FS_ENDPOINT=http://127.0.0.1:9000\"\n -e \"AGENTTEAMS_AI_GATEWAY_URL=http://${_aigw_domain}\"\n -e \"AGENTTEAMS_CONTROLLER_URL=http://agentteams-controller:8090\"\n -e \"AGENTTEAMS_DOCKER_NETWORK=agentteams-net\"\n -e \"AGENTTEAMS_WORKSPACE_DIR=${AGENTTEAMS_WORKSPACE_DIR}\"\n -e \"AGENTTEAMS_HOST_SHARE_DIR=${AGENTTEAMS_HOST_SHARE_DIR}\"\n -e \"AGENTTEAMS_MANAGER_ENABLED=true\"\n -e \"AGENTTEAMS_PORT_MANAGER_CONSOLE=${AGENTTEAMS_PORT_MANAGER_CONSOLE:-18888}\"\n )\n\n # Timezone\n if [ -n \"${AGENTTEAMS_TIMEZONE:-}\" ]; then\n _ctrl_env_args+=(-e \"TZ=${AGENTTEAMS_TIMEZONE}\")\n fi\n\n # Yolo mode\n if [ \"${AGENTTEAMS_YOLO:-}\" = \"1\" ]; then\n _ctrl_env_args+=(-e \"AGENTTEAMS_YOLO=1\")\n fi\n\n # Matrix-plugin debug tracing — propagated to every manager + worker\n # the controller spawns, then translated to OPENCLAW_MATRIX_DEBUG=1\n # by the container entrypoints. Use this to diagnose\n # \"worker did not join\" / \"manager replied empty\" hangs.\n if [ \"${AGENTTEAMS_MATRIX_DEBUG:-}\" = \"1\" ]; then\n _ctrl_env_args+=(-e \"AGENTTEAMS_MATRIX_DEBUG=1\")\n fi\n\n # Optional: GitHub token\n if [ -n \"${AGENTTEAMS_GITHUB_TOKEN:-}\" ]; then\n _ctrl_env_args+=(-e \"AGENTTEAMS_GITHUB_TOKEN=${AGENTTEAMS_GITHUB_TOKEN}\")\n fi\n\n # Optional: embedding model\n if [ -n \"${AGENTTEAMS_EMBEDDING_MODEL:-}\" ]; then\n _ctrl_env_args+=(-e \"AGENTTEAMS_EMBEDDING_MODEL=${AGENTTEAMS_EMBEDDING_MODEL}\")\n fi\n\n # Optional: OpenAI-compatible base URL\n if [ -n \"${AGENTTEAMS_OPENAI_BASE_URL:-}\" ]; then\n _ctrl_env_args+=(-e \"AGENTTEAMS_OPENAI_BASE_URL=${AGENTTEAMS_OPENAI_BASE_URL}\")\n fi\n # Optional: language\n if [ -n \"${AGENTTEAMS_LANGUAGE:-}\" ]; then\n _ctrl_env_args+=(-e \"AGENTTEAMS_LANGUAGE=${AGENTTEAMS_LANGUAGE}\")\n fi\n\n # Optional: CMS/ARMS observability. In embedded mode the controller\n # spawns the Manager and Workers, so it must receive these settings.\n _ctrl_env_args+=(-e \"AGENTTEAMS_CMS_TRACES_ENABLED=${AGENTTEAMS_CMS_TRACES_ENABLED:-false}\")\n _ctrl_env_args+=(-e \"AGENTTEAMS_CMS_SERVICE_NAME=${AGENTTEAMS_CMS_SERVICE_NAME:-agentteams-manager}\")\n _ctrl_env_args+=(-e \"AGENTTEAMS_CMS_METRICS_ENABLED=${AGENTTEAMS_CMS_METRICS_ENABLED:-false}\")\n if [ -n \"${AGENTTEAMS_CMS_ENDPOINT:-}\" ]; then\n _ctrl_env_args+=(-e \"AGENTTEAMS_CMS_ENDPOINT=${AGENTTEAMS_CMS_ENDPOINT}\")\n fi\n if [ -n \"${AGENTTEAMS_CMS_LICENSE_KEY:-}\" ]; then\n _ctrl_env_args+=(-e \"AGENTTEAMS_CMS_LICENSE_KEY=${AGENTTEAMS_CMS_LICENSE_KEY}\")\n fi\n if [ -n \"${AGENTTEAMS_CMS_PROJECT:-}\" ]; then\n _ctrl_env_args+=(-e \"AGENTTEAMS_CMS_PROJECT=${AGENTTEAMS_CMS_PROJECT}\")\n fi\n if [ -n \"${AGENTTEAMS_CMS_WORKSPACE:-}\" ]; then\n _ctrl_env_args+=(-e \"AGENTTEAMS_CMS_WORKSPACE=${AGENTTEAMS_CMS_WORKSPACE}\")\n fi\n\n # shellcheck disable=SC2086\n ${DOCKER_CMD} run -d \\\n --name agentteams-controller \\\n --network agentteams-net \\\n --network-alias matrix-local.agentteams.io \\\n --network-alias aigw-local.agentteams.io \\\n --network-alias fs-local.agentteams.io \\\n \"${_ctrl_env_args[@]}\" \\\n -v \"${CONTAINER_SOCK}:/var/run/docker.sock\" \\\n --security-opt label=disable \\\n -v \"${AGENTTEAMS_DATA_DIR}:/data\" \\\n -v \"${AGENTTEAMS_WORKSPACE_DIR}:/root/agentteams-fs/agents/manager\" \\\n -p \"${_port_prefix}${AGENTTEAMS_PORT_GATEWAY}:8080\" \\\n -p \"${_port_prefix}${AGENTTEAMS_PORT_CONSOLE}:8001\" \\\n -p \"${_port_prefix}${AGENTTEAMS_PORT_ELEMENT_WEB:-18088}:8088\" \\\n --restart unless-stopped \\\n \"${EMBEDDED_IMAGE}\"\n\n log \"Embedded controller started: agentteams-controller\"\n\n # Wait for infrastructure inside the controller container\n _wait_for_url() {\n local url=\"$1\" ctr=\"$2\" max_wait=\"${3:-120}\" desc=\"${4:-service}\"\n local elapsed=0\n log \"Waiting for ${desc}...\"\n while [ $elapsed -lt $max_wait ]; do\n if ${DOCKER_CMD} exec \"${ctr}\" curl -sf \"${url}\" >/dev/null 2>&1; then\n log \"${desc} is ready (${elapsed}s)\"\n return 0\n fi\n sleep 2\n elapsed=$((elapsed + 2))\n done\n log \"ERROR: ${desc} not ready after ${max_wait}s\"\n return 1\n }\n\n _wait_for_url \"http://127.0.0.1:6167/_tuwunel/server_version\" agentteams-controller 120 \"Tuwunel (Matrix)\" || exit 1\n _wait_for_url \"http://127.0.0.1:9000/minio/health/live\" agentteams-controller 60 \"MinIO\" || exit 1\n _wait_for_url \"http://127.0.0.1:8080/status\" agentteams-controller 120 \"Higress Gateway\" || exit 1\n\n # Wait for controller to create Manager Agent container\n log \"Waiting for Manager Agent container...\"\n local _mgr_wait=0\n local _mgr_max=300\n while [ $_mgr_wait -lt $_mgr_max ]; do\n if ${DOCKER_CMD} ps --format '{{.Names}}' 2>/dev/null | grep -q \"^agentteams-manager$\"; then\n log \"Manager Agent container detected (${_mgr_wait}s)\"\n break\n fi\n sleep 3\n _mgr_wait=$((_mgr_wait + 3))\n done\n if [ $_mgr_wait -ge $_mgr_max ]; then\n log \"ERROR: Manager Agent container not created after ${_mgr_max}s\"\n log \"Controller logs:\"\n ${DOCKER_CMD} exec agentteams-controller tail -30 /var/log/agentteams/agentteams-controller-error.log 2>/dev/null || true\n exit 1\n fi\n\n # Wait for Manager Agent to be running\n log \"Waiting for Manager Agent to start...\"\n local _agent_wait=0\n while [ $_agent_wait -lt 120 ]; do\n local _state\n _state=$(${DOCKER_CMD} inspect --format '{{.State.Status}}' agentteams-manager 2>/dev/null || echo \"missing\")\n if [ \"${_state}\" = \"running\" ]; then\n log \"Manager Agent is running\"\n break\n fi\n sleep 2\n _agent_wait=$((_agent_wait + 2))\n done\n\n # Enable yolo mode in agent if requested\n if [ \"${AGENTTEAMS_YOLO:-}\" = \"1\" ]; then\n ${DOCKER_CMD} exec agentteams-manager touch /root/manager-workspace/yolo-mode 2>/dev/null || true\n fi\n\n # Wait for the controller to send the first-boot welcome message.\n # The controller gates this on (a) Manager joining the DM room and\n # (b) Higress WASM key-auth propagation actually clearing /v1/chat/completions\n # for the Manager's gateway key — typically ~45-90s on a fresh install.\n # We poll Manager CR Status.WelcomeSent via the in-container AgentTeams CLI,\n # exec'd inside agentteams-controller (the source-of-truth container — its\n # bundled CLI binary is always in lockstep with whatever controller\n # binary is currently serving the HTTP API, since they're the same\n # `go build` output. The agentteams-manager container's CLI may lag the\n # controller across image upgrades and silently drop the welcomeSent\n # field, leaving this loop hung). The controller container mints a\n # long-lived admin SA token at startup and writes it to\n # AGENTTEAMS_AUTH_TOKEN_FILE=/var/run/agentteams/cli-token (set as a Dockerfile\n # ENV default), so a bare `docker exec agentteams-controller agt …`\n # auto-discovers both the endpoint and the token. There is a brief\n # window after container start before bootstrapAdminCLIToken completes\n # where the file may be empty / absent — the loop's silent retry\n # handles that the same way it handles the manager-not-yet-running\n # case below.\n if ${DOCKER_CMD} exec agentteams-controller sh -c 'command -v agt' >/dev/null 2>&1; then\n log \"$(msg install.welcome_msg.waiting)\"\n local _welcome_wait=0\n local _welcome_max=\"${AGENTTEAMS_WELCOME_TIMEOUT:-300}\"\n local _welcome_done=0\n while [ $_welcome_wait -lt $_welcome_max ]; do\n # `tr -d` strips whitespace/CR so the grep stays robust to\n # any future change in go-json field ordering or formatting.\n local _wjson\n _wjson=$(${DOCKER_CMD} exec agentteams-controller \\\n agt get managers default -o json 2>/dev/null || true)\n if [ -n \"${_wjson}\" ] && printf '%s' \"${_wjson}\" | tr -d ' \\r\\n' | grep -q '\"welcomeSent\":true'; then\n log \"$(msg install.welcome_msg.confirmed \"${_welcome_wait}\")\"\n _welcome_done=1\n break\n fi\n sleep 3\n _welcome_wait=$((_welcome_wait + 3))\n done\n if [ $_welcome_done -ne 1 ]; then\n # Non-fatal: install is still good. Keep going to the success\n # banner so the admin can use Element Web to nudge Manager into\n # onboarding manually (one DM message is enough).\n log \"$(msg install.welcome_msg.timeout \"${_welcome_max}\")\"\n log \"$(msg install.welcome_msg.timeout_hint)\"\n log \"$(msg install.welcome_msg.timeout_inspect)\"\n fi\n else\n log \"$(msg install.welcome_msg.poll_unavailable)\"\n fi\n\n else\n # ============================================================\n # Legacy architecture: all-in-one manager container\n # ============================================================\n\n NETWORK_ARGS=\"--network agentteams-net\"\n NETWORK_ALIAS_ARGS=\"--network-alias matrix-local.agentteams.io --network-alias aigw-local.agentteams.io --network-alias fs-local.agentteams.io\"\n for _domain in \"${AGENTTEAMS_MATRIX_CLIENT_DOMAIN:-}\" \"${AGENTTEAMS_CONSOLE_DOMAIN:-}\"; do\n if [ -n \"${_domain}\" ] && [[ \"${_domain}\" == *-local.agentteams.io ]]; then\n NETWORK_ALIAS_ARGS=\"${NETWORK_ALIAS_ARGS} --network-alias ${_domain}\"\n fi\n done\n\n # Start Docker API proxy if enabled (security layer between Manager and Docker daemon)\n PROXY_ARGS=\"\"\n if [ \"${AGENTTEAMS_DOCKER_PROXY:-1}\" = \"1\" ] && [ -n \"${CONTAINER_SOCK:-}\" ]; then\n local _proxy_image=\"${AGENTTEAMS_REGISTRY}/higress/agentteams-docker-proxy:${AGENTTEAMS_VERSION}\"\n # Try versioned tag, fallback to latest\n if ! ${DOCKER_CMD} image inspect \"${_proxy_image}\" >/dev/null 2>&1; then\n ${DOCKER_CMD} pull \"${_proxy_image}\" 2>/dev/null || {\n _proxy_image=\"${AGENTTEAMS_REGISTRY}/higress/agentteams-docker-proxy:latest\"\n ${DOCKER_CMD} pull \"${_proxy_image}\" 2>/dev/null || true\n }\n fi\n if ${DOCKER_CMD} image inspect \"${_proxy_image}\" >/dev/null 2>&1; then\n log \"Starting Docker API proxy...\"\n ${DOCKER_CMD} run -d \\\n --name agentteams-docker-proxy \\\n --network agentteams-net \\\n -v \"${CONTAINER_SOCK}:/var/run/docker.sock\" \\\n --security-opt label=disable \\\n -e AGENTTEAMS_WORKER_IMAGE=\"${WORKER_IMAGE}\" \\\n -e AGENTTEAMS_COPAW_WORKER_IMAGE=\"${COPAW_WORKER_IMAGE}\" \\\n -e AGENTTEAMS_HERMES_WORKER_IMAGE=\"${HERMES_WORKER_IMAGE}\" \\\n ${AGENTTEAMS_PROXY_ALLOWED_REGISTRIES:+-e AGENTTEAMS_PROXY_ALLOWED_REGISTRIES=\"${AGENTTEAMS_PROXY_ALLOWED_REGISTRIES}\"} \\\n --restart unless-stopped \\\n \"${_proxy_image}\"\n PROXY_ARGS=\"-e AGENTTEAMS_CONTROLLER_URL=http://agentteams-docker-proxy:2375 -e AGENTTEAMS_CONTAINER_API=http://agentteams-docker-proxy:2375\"\n SOCKET_MOUNT_ARGS=\"\"\n fi\n fi\n\n # Pass host timezone to container\n TZ_ARGS=\"\"\n if [ -n \"${AGENTTEAMS_TIMEZONE:-}\" ]; then\n TZ_ARGS=\"-e TZ=${AGENTTEAMS_TIMEZONE}\"\n fi\n\n YOLO_ARGS=\"\"\n if [ \"${AGENTTEAMS_YOLO:-}\" = \"1\" ]; then\n YOLO_ARGS=\"-e AGENTTEAMS_YOLO=1\"\n fi\n\n MATRIX_DEBUG_ARGS=\"\"\n if [ \"${AGENTTEAMS_MATRIX_DEBUG:-}\" = \"1\" ]; then\n MATRIX_DEBUG_ARGS=\"-e AGENTTEAMS_MATRIX_DEBUG=1\"\n fi\n\n # shellcheck disable=SC2086\n ${DOCKER_CMD} run -d \\\n --name agentteams-manager \\\n --env-file \"${ENV_FILE}\" \\\n -e HOME=/root/manager-workspace \\\n -w /root/manager-workspace \\\n -e HOST_ORIGINAL_HOME=\"${AGENTTEAMS_HOST_SHARE_DIR}\" \\\n -e AGENTTEAMS_MANAGER_RUNTIME=\"${AGENTTEAMS_MANAGER_RUNTIME:-copaw}\" \\\n ${JVM_ARGS:+-e JVM_ARGS=\"${JVM_ARGS}\"} \\\n ${YOLO_ARGS} \\\n ${MATRIX_DEBUG_ARGS} \\\n ${TZ_ARGS} \\\n ${SOCKET_MOUNT_ARGS} \\\n ${NETWORK_ARGS} \\\n ${NETWORK_ALIAS_ARGS} \\\n ${PROXY_ARGS} \\\n -p \"${_port_prefix}${AGENTTEAMS_PORT_GATEWAY}:8080\" \\\n -p \"${_port_prefix}${AGENTTEAMS_PORT_CONSOLE}:8001\" \\\n -p \"${_port_prefix}${AGENTTEAMS_PORT_ELEMENT_WEB:-18088}:8088\" \\\n -p \"127.0.0.1:${AGENTTEAMS_PORT_MANAGER_CONSOLE:-18888}:18888\" \\\n ${DATA_MOUNT_ARGS} \\\n ${WORKSPACE_MOUNT_ARGS} \\\n ${HOST_SHARE_MOUNT_ARGS} \\\n --restart unless-stopped \\\n \"$([ \"${AGENTTEAMS_MANAGER_RUNTIME}\" = \"copaw\" ] && echo \"${MANAGER_COPAW_IMAGE}\" || echo \"${MANAGER_IMAGE}\")\"\n\n # Wait for Manager agent to be ready\n wait_manager_ready \"agentteams-manager\"\n\n # Wait for Matrix server to be ready\n wait_matrix_ready \"agentteams-manager\"\n\n # Post-install verification (non-fatal: warnings only)\n local _verify_script\n _verify_script=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)/agentteams-verify.sh\"\n if [ -f \"${_verify_script}\" ]; then\n bash \"${_verify_script}\" \"agentteams-manager\" || {\n log \"WARNING: Some post-install checks failed. Re-run: bash install/agentteams-verify.sh\"\n }\n fi\n fi\n unset _port_prefix\n\n # Apply Podman autostart if selected and environment matches\n if [ \"${DOCKER_CMD}\" = \"podman\" ] && [ \"${AGENTTEAMS_PODMAN_AUTOSTART:-0}\" = \"1\" ]; then\n setup_podman_autostart\n fi\n\n log \"\"\n log \"$(msg success.title)\"\n log \"\"\n log \"$(msg success.domains_configured)\"\n log \" ${AGENTTEAMS_MATRIX_DOMAIN%%:*} ${AGENTTEAMS_MATRIX_CLIENT_DOMAIN} ${AGENTTEAMS_AI_GATEWAY_DOMAIN} ${AGENTTEAMS_FS_DOMAIN} ${AGENTTEAMS_CONSOLE_DOMAIN}\"\n log \"\"\n local lan_ip\n lan_ip=$(detect_lan_ip)\n echo -e \"\\033[33m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\033[0m\"\n echo -e \"\\033[33m $(msg success.open_url)\\033[0m\"\n echo -e \"\\033[33m \\033[0m\"\n echo -e \"\\033[1;36m http://127.0.0.1:${AGENTTEAMS_PORT_ELEMENT_WEB:-18088}/#/login\\033[0m\"\n echo -e \"\\033[33m \\033[0m\"\n echo -e \"\\033[33m $(msg success.login_with)\\033[0m\"\n echo -e \"\\033[33m $(msg success.username \"${AGENTTEAMS_ADMIN_USER}\")\\033[0m\"\n echo -e \"\\033[33m $(msg success.password \"${AGENTTEAMS_ADMIN_PASSWORD}\")\\033[0m\"\n echo -e \"\\033[33m \\033[0m\"\n echo -e \"\\033[33m $(msg success.after_login)\\033[0m\"\n echo -e \"\\033[33m $(msg success.tell_it)\\033[0m\"\n echo -e \"\\033[33m $(msg success.manager_auto)\\033[0m\"\n echo -e \"\\033[33m \\033[0m\"\n echo -e \"\\033[33m ───────────────────────────────────────────────────────────────────────────── \\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_title)\\033[0m\"\n echo -e \"\\033[33m \\033[0m\"\n if [ -n \"${lan_ip}\" ]; then\n echo -e \"\\033[33m $(msg success.mobile_step1)\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_step2 \"http://${lan_ip}:${AGENTTEAMS_PORT_GATEWAY}\")\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_step3)\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_username \"${AGENTTEAMS_ADMIN_USER}\")\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_password \"${AGENTTEAMS_ADMIN_PASSWORD}\")\\033[0m\"\n else\n echo -e \"\\033[33m $(msg success.mobile_step1)\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_step2_noip \"${AGENTTEAMS_PORT_GATEWAY}\")\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_noip_hint)\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_step3)\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_username \"${AGENTTEAMS_ADMIN_USER}\")\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_password \"${AGENTTEAMS_ADMIN_PASSWORD}\")\\033[0m\"\n fi\n echo -e \"\\033[33m \\033[0m\"\n echo -e \"\\033[33m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\033[0m\"\n log \"\"\n log \"$(msg success.other_consoles)\"\n log \"$(msg success.higress_console \"${AGENTTEAMS_PORT_CONSOLE}\" \"${AGENTTEAMS_ADMIN_USER}\" \"${AGENTTEAMS_ADMIN_PASSWORD}\")\"\n if [ \"${AGENTTEAMS_USE_EMBEDDED}\" != \"1\" ]; then\n log \"$(msg success.manager_console \"${AGENTTEAMS_PORT_MANAGER_CONSOLE:-18888}\")\"\n log \"$(msg success.manager_console_gateway \"${AGENTTEAMS_ADMIN_USER}\" \"${AGENTTEAMS_ADMIN_PASSWORD}\")\"\n fi\n log \"\"\n log \"$(msg success.switch_llm.title)\"\n log \"$(msg success.switch_llm.hint)\"\n log \"$(msg success.switch_llm.docs)\"\n log \"$(msg success.switch_llm.url)\"\n log \"\"\n log \"$(msg success.tip)\"\n log \"\"\n if [ \"${AGENTTEAMS_LOCAL_ONLY:-1}\" != \"1\" ]; then\n echo -e \"\\033[33m$(msg port.local_only.https_hint)\\033[0m\"\n log \"\"\n fi\n log \"$(msg success.config_file \"${ENV_FILE}\")\"\n log \"$(msg success.data_volume \"${AGENTTEAMS_DATA_DIR}\")\"\n log \"$(msg success.workspace \"${AGENTTEAMS_WORKSPACE_DIR}\")\"\n}\n\n# ============================================================\n# Worker Installation (One-Click)\n# ============================================================\n\ninstall_worker() {\n local WORKER_NAME=\"\"\n local FS=\"\"\n local FS_KEY=\"\"\n local FS_SECRET=\"\"\n local RESET=false\n local SKILLS_API_URL=\"\"\n\n # Parse arguments\n while [ $# -gt 0 ]; do\n case $1 in\n --name) WORKER_NAME=\"$2\"; shift 2 ;;\n --fs) FS=\"$2\"; shift 2 ;;\n --fs-key) FS_KEY=\"$2\"; shift 2 ;;\n --fs-secret) FS_SECRET=\"$2\"; shift 2 ;;\n --skills-api-url) SKILLS_API_URL=\"$2\"; shift 2 ;;\n --reset) RESET=true; shift ;;\n *) die \"$(msg error.unknown_option \"$1\")\" ;;\n esac\n done\n\n # Validate required params\n [ -z \"${WORKER_NAME}\" ] && die \"$(msg error.name_required)\"\n [ -z \"${FS}\" ] && die \"$(msg error.fs_required)\"\n [ -z \"${FS_KEY}\" ] && die \"$(msg error.fs_key_required)\"\n [ -z \"${FS_SECRET}\" ] && die \"$(msg error.fs_secret_required)\"\n\n local CONTAINER_NAME=\"agentteams-worker-${WORKER_NAME}\"\n\n # Handle reset\n if [ \"${RESET}\" = true ]; then\n log \"$(msg worker.resetting \"${WORKER_NAME}\")\"\n ${DOCKER_CMD} stop \"${CONTAINER_NAME}\" 2>/dev/null || true\n ${DOCKER_CMD} rm \"${CONTAINER_NAME}\" 2>/dev/null || true\n fi\n\n # Check for existing container\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' | grep -q \"^${CONTAINER_NAME}$\"; then\n die \"$(msg worker.exists \"${CONTAINER_NAME}\")\"\n fi\n\n log \"$(msg worker.starting \"${WORKER_NAME}\")\"\n\n # Build docker run args\n local DOCKER_ENV=\"\"\n DOCKER_ENV=\"${DOCKER_ENV} -e HOME=/root/agentteams-fs/agents/${WORKER_NAME}\"\n DOCKER_ENV=\"${DOCKER_ENV} -w /root/agentteams-fs/agents/${WORKER_NAME}\"\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_WORKER_NAME=${WORKER_NAME}\"\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_FS_ENDPOINT=${FS}\"\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_FS_ACCESS_KEY=${FS_KEY}\"\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_FS_SECRET_KEY=${FS_SECRET}\"\n\n if [ -z \"${SKILLS_API_URL}\" ]; then\n if [ -n \"${AGENTTEAMS_SKILLS_API_URL:-}\" ]; then\n SKILLS_API_URL=\"${AGENTTEAMS_SKILLS_API_URL}\"\n else\n SKILLS_API_URL=\"nacos://market.agentteams.io:80/public\"\n fi\n fi\n\n # Add SKILLS_API_URL if specified\n DOCKER_ENV=\"${DOCKER_ENV} -e SKILLS_API_URL=${SKILLS_API_URL}\"\n log \"$(msg worker.skills_url \"${SKILLS_API_URL}\")\"\n if [ -n \"${AGENTTEAMS_NACOS_USERNAME:-}\" ]; then\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_NACOS_USERNAME=${AGENTTEAMS_NACOS_USERNAME}\"\n fi\n if [ -n \"${AGENTTEAMS_NACOS_PASSWORD:-}\" ]; then\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_NACOS_PASSWORD=${AGENTTEAMS_NACOS_PASSWORD}\"\n fi\n if [ -n \"${AGENTTEAMS_NACOS_TOKEN:-}\" ]; then\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_NACOS_TOKEN=${AGENTTEAMS_NACOS_TOKEN}\"\n fi\n\n # shellcheck disable=SC2086\n ${DOCKER_CMD} run -d \\\n --name \"${CONTAINER_NAME}\" \\\n ${DOCKER_ENV} \\\n --restart unless-stopped \\\n \"${WORKER_IMAGE}\"\n\n log \"\"\n log \"$(msg worker.started \"${WORKER_NAME}\")\"\n log \"$(msg worker.container \"${CONTAINER_NAME}\")\"\n log \"$(msg worker.view_logs \"${CONTAINER_NAME}\")\"\n}\n\n# ============================================================\n# Main\n# ============================================================\n\n# ============================================================\n# LLM API connectivity test\n# ============================================================\n\ntest_llm_connectivity() {\n local base_url=\"$1\"\n local api_key=\"$2\"\n local model=\"$3\"\n local hint=\"${4:-}\" # optional: extra hint shown on failure\n if ! command -v curl >/dev/null 2>&1; then\n echo -e \"\\033[33m$(msg llm.openai.test.no_curl)\\033[0m\"\n return\n fi\n log \"$(msg llm.openai.test.testing)\"\n local _body _http_code _tmpfile\n _tmpfile=$(mktemp)\n _http_code=$(curl -s -o \"${_tmpfile}\" -w \"%{http_code}\" \\\n -X POST \"${base_url%/}/chat/completions\" \\\n -H \"Authorization: Bearer ${api_key}\" \\\n -H \"Content-Type: application/json\" \\\n -H \"User-Agent: AgentTeams/${AGENTTEAMS_VERSION:-latest}\" \\\n --max-time 30 \\\n -d \"{\\\"model\\\":\\\"${model}\\\",\\\"messages\\\":[{\\\"role\\\":\\\"user\\\",\\\"content\\\":\\\"hi\\\"}]}\" \\\n 2>/dev/null)\n _body=$(cat \"${_tmpfile}\")\n rm -f \"${_tmpfile}\"\n if [ \"${_http_code}\" = \"200\" ] || [ \"${_http_code}\" = \"201\" ]; then\n log \"$(msg llm.openai.test.ok)\"\n else\n echo -e \"\\033[33m$(msg llm.openai.test.fail \"${_http_code}\" \"${_body}\")\\033[0m\"\n if [ -n \"${hint}\" ]; then\n echo -e \"\\033[33m${hint}\\033[0m\"\n fi\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" != \"1\" ]; then\n local _confirm\n read -e -p \"$(msg llm.openai.test.confirm)\" _confirm\n if [ \"${_confirm}\" = \"b\" ]; then\n STEP_RESULT=\"back\"\n return 1\n fi\n if [ \"${_confirm}\" != \"y\" ] && [ \"${_confirm}\" != \"Y\" ]; then\n log \"$(msg llm.openai.test.aborted)\"\n exit 1\n fi\n fi\n fi\n}\n\ntest_embedding_connectivity() {\n local base_url=\"$1\"\n local api_key=\"$2\"\n local model=\"$3\"\n if ! command -v curl >/dev/null 2>&1; then\n return 0\n fi\n log \"$(msg llm.embedding.test.testing)\"\n local _body _http_code _tmpfile\n _tmpfile=$(mktemp)\n _http_code=$(curl -s -o \"${_tmpfile}\" -w \"%{http_code}\" \\\n -X POST \"${base_url%/}/embeddings\" \\\n -H \"Authorization: Bearer ${api_key}\" \\\n -H \"Content-Type: application/json\" \\\n -H \"User-Agent: AgentTeams/${AGENTTEAMS_VERSION:-latest}\" \\\n --max-time 30 \\\n -d \"{\\\"model\\\":\\\"${model}\\\",\\\"input\\\":\\\"test\\\"}\" \\\n 2>/dev/null)\n _body=$(cat \"${_tmpfile}\")\n rm -f \"${_tmpfile}\"\n if [ \"${_http_code}\" = \"200\" ] || [ \"${_http_code}\" = \"201\" ]; then\n log \"$(msg llm.embedding.test.ok)\"\n return 0\n else\n echo -e \"\\033[33m$(msg llm.embedding.test.fail \"${_http_code}\" \"${_body}\")\\033[0m\"\n return 1\n fi\n}\n\n# ============================================================\n# Uninstall\n# ============================================================\n\nuninstall_agentteams() {\n log \"$(msg uninstall.title)\"\n\n # Safely disable and remove the dedicated AgentTeams Podman autostart service\n if [ \"${DOCKER_CMD}\" = \"podman\" ] && command -v systemctl >/dev/null 2>&1; then\n local _systemctl_cmd=\"systemctl\"\n local _service_dir=\"/etc/systemd/system\"\n\n if [ \"$(id -u)\" != \"0\" ]; then\n _systemctl_cmd=\"systemctl --user\"\n _service_dir=\"${HOME}/.config/systemd/user\"\n fi\n\n if [ -f \"${_service_dir}/agentteams-podman-restart.service\" ]; then\n log \"Disabling and removing dedicated AgentTeams Podman autostart service...\"\n ${_systemctl_cmd} disable --now agentteams-podman-restart.service 2>/dev/null || true\n rm -f \"${_service_dir}/agentteams-podman-restart.service\"\n ${_systemctl_cmd} daemon-reload 2>/dev/null || true\n fi\n if [ -f \"${_service_dir}/agentteams-podman-restart.service\" ]; then\n ${_systemctl_cmd} disable --now agentteams-podman-restart.service 2>/dev/null || true\n rm -f \"${_service_dir}/agentteams-podman-restart.service\"\n ${_systemctl_cmd} daemon-reload 2>/dev/null || true\n fi\n # Note: Native podman-restart.service is strictly untouched per maintainer review.\n fi\n\n # Capture manager image before removing (needed for workspace cleanup on Linux)\n local manager_image=\"\"\n manager_image=$(${DOCKER_CMD} inspect agentteams-manager --format '{{.Config.Image}}' 2>/dev/null || true)\n\n # Stop and remove manager\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -q \"^agentteams-manager$\"; then\n log \"$(msg uninstall.stopping_manager)\"\n ${DOCKER_CMD} stop agentteams-manager >/dev/null 2>&1 || true\n ${DOCKER_CMD} rm agentteams-manager >/dev/null 2>&1 || true\n fi\n\n # Stop and remove workers\n local workers\n workers=$(${DOCKER_CMD} ps -a --filter \"name=agentteams-worker-\" --format '{{.Names}}' 2>/dev/null || true)\n if [ -n \"${workers}\" ]; then\n log \"$(msg uninstall.stopping_workers)\"\n echo \"${workers}\" | while read -r w; do\n ${DOCKER_CMD} rm -f \"${w}\" >/dev/null 2>&1 || true\n log \"$(msg uninstall.removed \"${w}\")\"\n done\n fi\n\n # Stop and remove docker-proxy (legacy ≤ v1.0.x; current arch uses\n # agentteams-controller for the same role)\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -q \"^agentteams-docker-proxy$\"; then\n log \"$(msg uninstall.removing_proxy)\"\n ${DOCKER_CMD} stop agentteams-docker-proxy >/dev/null 2>&1 || true\n ${DOCKER_CMD} rm agentteams-docker-proxy >/dev/null 2>&1 || true\n fi\n\n # Stop and remove the embedded controller container. MUST happen\n # before the `docker volume rm agentteams-data` step below — in embedded\n # mode agentteams-controller mounts agentteams-data at /data (Tuwunel DB,\n # MinIO state, Higress state, all the room messages), and `volume\n # rm` against an in-use volume fails silently because of the trailing\n # `|| true`. Skipping this used to leave room/message history behind\n # across \"uninstall + reinstall\" cycles. See PR #692.\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -q \"^agentteams-controller$\"; then\n log \"$(msg uninstall.stopping_controller)\"\n ${DOCKER_CMD} stop agentteams-controller >/dev/null 2>&1 || true\n ${DOCKER_CMD} rm agentteams-controller >/dev/null 2>&1 || true\n fi\n\n # Read env file for data/workspace info before removing\n local env_file=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n [ ! -f \"${env_file}\" ] && [ -f \"./agentteams-manager.env\" ] && env_file=\"./agentteams-manager.env\"\n\n local data_dir=\"\" workspace_dir=\"\"\n if [ -f \"${env_file}\" ]; then\n data_dir=$(grep '^AGENTTEAMS_DATA_DIR=' \"${env_file}\" 2>/dev/null | cut -d= -f2- || true)\n workspace_dir=$(grep '^AGENTTEAMS_WORKSPACE_DIR=' \"${env_file}\" 2>/dev/null | cut -d= -f2- || true)\n fi\n\n # Remove Docker volume\n local vol=\"${data_dir:-agentteams-data}\"\n if ${DOCKER_CMD} volume inspect \"${vol}\" >/dev/null 2>&1; then\n log \"$(msg uninstall.removing_volume \"${vol}\")\"\n ${DOCKER_CMD} volume rm \"${vol}\" >/dev/null 2>&1 || true\n fi\n\n # Remove Docker network\n if ${DOCKER_CMD} network ls --format '{{.Name}}' 2>/dev/null | grep -q \"^agentteams-net$\"; then\n log \"$(msg uninstall.removing_network)\"\n ${DOCKER_CMD} network rm agentteams-net >/dev/null 2>&1 || true\n fi\n\n # Remove workspace directory\n if [ -n \"${workspace_dir}\" ] && [ -d \"${workspace_dir}\" ]; then\n log \"$(msg uninstall.removing_workspace \"${workspace_dir}\")\"\n rm -rf \"${workspace_dir}\" 2>/dev/null || true\n if [ -d \"${workspace_dir}\" ]; then\n log \"$(msg uninstall.removing_workspace_elevated)\"\n local _parent _base\n _parent=$(dirname \"${workspace_dir}\")\n _base=$(basename \"${workspace_dir}\")\n local _rm_image=\"${manager_image:-busybox}\"\n ${DOCKER_CMD} run --rm --entrypoint sh \\\n -v \"${_parent}:/host-parent\" \\\n \"${_rm_image}\" \\\n -c \"rm -rf /host-parent/${_base}\" 2>/dev/null || true\n fi\n fi\n\n # Remove env file\n if [ -f \"${env_file}\" ]; then\n log \"$(msg uninstall.removing_env \"${env_file}\")\"\n rm -f \"${env_file}\"\n fi\n\n # Remove install log\n if [ -f \"${HOME}/agentteams-install.log\" ]; then\n log \"$(msg uninstall.removing_log \"${HOME}/agentteams-install.log\")\"\n rm -f \"${HOME}/agentteams-install.log\"\n fi\n\n echo \"\"\n log \"$(msg uninstall.done)\"\n}\n\n# ============================================================\n# Check container runtime (docker or podman) and environment\n# ============================================================\ncheck_container_runtime() {\n # Ensure the rootless environment baseline (XDG_RUNTIME_DIR) is ready before engine detection.\n if [ \"$(id -u)\" -ne 0 ] && [ -z \"${XDG_RUNTIME_DIR:-}\" ]; then\n export XDG_RUNTIME_DIR=\"/run/user/$(id -u)\"\n fi\n\n if command -v docker >/dev/null 2>&1; then\n DOCKER_CMD=\"docker\"\n # Check for podman disguised as docker (alias or podman-docker package)\n if docker --version 2>/dev/null | grep -qi \"podman\"; then\n DOCKER_CMD=\"podman\"\n fi\n elif command -v podman >/dev/null 2>&1; then\n DOCKER_CMD=\"podman\"\n else\n echo -e \"\\033[31m[AgentTeams ERROR]\\033[0m $(msg error.docker_not_found)\" >&2\n exit 1\n fi\n\n # Unified health check (Validates runtime responsiveness)\n if ! ${DOCKER_CMD} ps >/dev/null 2>&1; then\n echo -e \"\\033[31m[AgentTeams ERROR]\\033[0m $(msg error.docker_not_running)\" >&2\n exit 1\n fi\n}\n\ncheck_container_runtime\n\ncase \"${1:-}\" in\n manager|\"\")\n install_manager\n ;;\n worker)\n shift\n install_worker \"$@\"\n ;;\n uninstall)\n uninstall_agentteams\n ;;\n *)\n echo \"Usage: $0 [manager|worker [options]|uninstall]\"\n echo \"\"\n echo \"Commands:\"\n echo \" manager Interactive Manager installation (default)\"\n echo \" Choose Quick Start (all defaults) or Manual mode\"\n echo \" worker Worker installation (requires --name and connection params)\"\n echo \" uninstall Stop and remove Manager + all Worker containers\"\n echo \"\"\n echo \"Quick Start (fastest):\"\n echo \" $0\"\n echo \" # Then select '1' for Quick Start mode\"\n echo \"\"\n echo \"Non-interactive (for automation):\"\n echo \" AGENTTEAMS_NON_INTERACTIVE=1 AGENTTEAMS_LLM_API_KEY=sk-xxx $0\"\n echo \"\"\n echo \"Worker Options:\"\n echo \" --name <name> Worker name (required)\"\n echo \" --fs <url> MinIO endpoint URL (required)\"\n echo \" --fs-key <key> MinIO access key (required)\"\n echo \" --fs-secret <secret> MinIO secret key (required)\"\n echo \" --reset Remove existing Worker container before creating\"\n exit 1\n ;;\nesac\n"}, "e6fa64c0cb7b14600c6abbf9b4a25f47bf544c1f": {"t": 1785764754.1573792, "n": 225619, "h": "fab10245db46ec19", "c": "#!/bin/bash\n# agentteams-install.sh - One-click installation for AgentTeams Manager and Worker\n#\n# Usage:\n# ./agentteams-install.sh # Interactive installation (choose Quick Start or Manual)\n# ./agentteams-install.sh manager # Same as above (explicit)\n# ./agentteams-install.sh worker --name <name> ... # Worker installation\n# ./agentteams-install.sh uninstall # Stop and remove Manager + all Workers\n#\n# Onboarding Modes:\n# Quick Start - Fast installation with all default values (recommended)\n# Manual - Customize each option step by step\n#\n# Environment variables (for automation):\n# AGENTTEAMS_NON_INTERACTIVE Skip all prompts, use defaults (default: 0)\n# AGENTTEAMS_LLM_PROVIDER LLM provider (default: openai-compat for zh non-interactive Token Plan; qwen for en)\n# AGENTTEAMS_DEFAULT_MODEL Default model (default: qwen3.6-plus for zh Token Plan and en non-interactive)\n# AGENTTEAMS_OPENAI_BASE_URL OpenAI-compatible base URL (default for zh non-interactive: Alibaba Token Plan endpoint)\n# AGENTTEAMS_LLM_API_KEY LLM API key (required)\n# AGENTTEAMS_ADMIN_USER Admin username (default: admin)\n# AGENTTEAMS_ADMIN_PASSWORD Admin password (auto-generated if not set, min 8 chars)\n# AGENTTEAMS_MATRIX_DOMAIN Matrix domain (default: matrix-local.agentteams.io:18080)\n# AGENTTEAMS_MOUNT_SOCKET Mount container runtime socket (default: 1)\n# AGENTTEAMS_DATA_DIR Docker volume name for persistent data (default: agentteams-data)\n# AGENTTEAMS_WORKSPACE_DIR Host directory for manager workspace (default: ~/agentteams-manager)\n# AGENTTEAMS_VERSION Image tag (default: latest)\n# AGENTTEAMS_REGISTRY Image registry (default: auto-detected by timezone)\n# AGENTTEAMS_INSTALL_MANAGER_IMAGE Override manager image (e.g., local build)\n# AGENTTEAMS_INSTALL_WORKER_IMAGE Override worker image (e.g., local build)\n# AGENTTEAMS_INSTALL_COPAW_WORKER_IMAGE Override copaw worker image (e.g., local build)\n# AGENTTEAMS_INSTALL_HERMES_WORKER_IMAGE Override hermes worker image (e.g., local build)\n# AGENTTEAMS_NACOS_REGISTRY_URI Default Nacos registry URI for Worker market search/import\n# (default: nacos://market.agentteams.io:80/public)\n# AGENTTEAMS_NACOS_USERNAME Default Nacos username for nacos:// package imports (optional)\n# AGENTTEAMS_NACOS_PASSWORD Default Nacos password for nacos:// package imports (optional)\n# AGENTTEAMS_CMS_TRACES_ENABLED Enable openclaw-cms-plugin traces for Manager AND all Workers (default: false)\n# AGENTTEAMS_CMS_ENDPOINT ARMS OTLP endpoint (required if traces enabled)\n# AGENTTEAMS_CMS_LICENSE_KEY CMS license key (required if traces enabled)\n# AGENTTEAMS_CMS_PROJECT CMS project name (optional)\n# AGENTTEAMS_CMS_WORKSPACE CMS workspace ID (required if traces enabled)\n# AGENTTEAMS_CMS_SERVICE_NAME Manager service name in ARMS (default: agentteams-manager)\n# Workers always report as agentteams-worker-<name> automatically\n# AGENTTEAMS_CMS_METRICS_ENABLED Enable diagnostics-otel metrics for Manager AND all Workers (default: false)\n# AGENTTEAMS_PORT_GATEWAY Host port for Higress gateway (default: 18080)\n# AGENTTEAMS_PORT_CONSOLE Host port for Higress console (default: 18001)\n# AGENTTEAMS_PORT_ELEMENT_WEB Host port for Element Web direct access (default: 18088)\n# AGENTTEAMS_PORT_MANAGER_CONSOLE Host port for Manager console (default: 18888)\n# AGENTTEAMS_WORKER_IDLE_TIMEOUT Worker idle timeout in minutes (default: 720, i.e. 12 hours)\n\nset -e\n\nAGENTTEAMS_VERSION=\"${AGENTTEAMS_VERSION:-}\"\nAGENTTEAMS_KNOWN_STABLE_VERSION=\"v1.1.2\" # fallback if GitHub API is unreachable\n\n# Returns 0 (true) if $1 < $2 using semver order; \"latest\" is treated as greatest\n_ver_lt() {\n [ \"$1\" = \"latest\" ] && return 1\n [ \"$2\" = \"latest\" ] && return 0\n [ \"$1\" = \"$2\" ] && return 1\n [ \"$(printf '%s\\n%s' \"$1\" \"$2\" | sort -V | head -1)\" = \"$1\" ]\n}\nAGENTTEAMS_NON_INTERACTIVE=\"${AGENTTEAMS_NON_INTERACTIVE:-0}\"\nAGENTTEAMS_MOUNT_SOCKET=\"${AGENTTEAMS_MOUNT_SOCKET:-1}\"\nAGENTTEAMS_DOCKER_PROXY=\"${AGENTTEAMS_DOCKER_PROXY:-1}\"\nSTEP_RESULT=\"\" # Used by state machine to signal \"back\" navigation\n\n# ============================================================\n# Log all output to file\n# ============================================================\n\nAGENTTEAMS_LOG_FILE=\"${HOME}/agentteams-install.log\"\n\nif [ \"${1:-}\" != \"uninstall\" ]; then\n # Redirect all output (stdout and stderr) to both terminal and log file\n exec > >(tee -a \"${AGENTTEAMS_LOG_FILE}\") 2>&1\n\n echo \"\"\n echo \"========================================\"\n echo \"AgentTeams Installation Log\"\n echo \"Started: $(date)\"\n echo \"User: $(whoami)\"\n echo \"System: $(uname -a)\"\n echo \"Log file: ${AGENTTEAMS_LOG_FILE}\"\n echo \"========================================\"\n echo \"\"\nfi\n\n# ============================================================\n# Utility functions (needed early for timezone detection)\n# ============================================================\n\nlog() {\n echo -e \"\\033[36m[AgentTeams]\\033[0m $1\"\n}\n\nerror() {\n echo -e \"\\033[31m[AgentTeams ERROR]\\033[0m $1\" >&2\n}\n\ndie() {\n error \"$1\"\n exit 1\n}\n\nmigrate_legacy_env_file() {\n [ -n \"${AGENTTEAMS_ENV_FILE:-}\" ] && return 0\n\n local current=\"${HOME}/agentteams-manager.env\"\n local legacy=\"\"\n for candidate in \"${HOME}/hiclaw-manager.env\" \"./hiclaw-manager.env\" \"./agentteams-manager.env\"; do\n if [ ! -f \"${current}\" ] && [ -f \"${candidate}\" ]; then\n legacy=\"${candidate}\"\n break\n fi\n done\n\n if [ -n \"${legacy}\" ]; then\n log \"Migrating legacy env file ${legacy} to ${current}...\"\n if mv \"${legacy}\" \"${current}\"; then\n case \"${legacy}\" in\n \"${HOME}/hiclaw-manager.env\") ln -s \"${current}\" \"${legacy}\" 2>/dev/null || true ;;\n esac\n fi\n fi\n}\n\n# ============================================================\n# Timezone detection (compatible with Linux and macOS)\n# ============================================================\n\ndetect_timezone() {\n local tz=\"\"\n\n # Try /etc/timezone (Debian/Ubuntu)\n if [ -f /etc/timezone ]; then\n tz=$(cat /etc/timezone 2>/dev/null | tr -d '[:space:]')\n fi\n\n # Try /etc/localtime symlink (macOS and some Linux)\n if [ -z \"${tz}\" ] && [ -L /etc/localtime ]; then\n tz=$(ls -l /etc/localtime 2>/dev/null | sed 's|.*/zoneinfo/||')\n fi\n\n # Try timedatectl (systemd)\n if [ -z \"${tz}\" ]; then\n tz=$(timedatectl show --value -p Timezone 2>/dev/null)\n fi\n\n # If still not detected, warn and prompt user. Diagnostics go to stderr —\n # this function returns its value via stdout to `$(detect_timezone)`, so\n # any echo here would leak into AGENTTEAMS_TIMEZONE and downstream image tags.\n if [ -z \"${tz}\" ]; then\n echo \"\" >&2\n echo -e \"\\033[33m[AgentTeams WARNING]\\033[0m Could not detect timezone automatically.\" >&2\n echo -e \"\\033[33m[AgentTeams]\\033[0m Please enter your timezone (e.g., Asia/Shanghai, America/New_York).\" >&2\n echo \"\" >&2\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n tz=\"Asia/Shanghai\"\n log \"Using default timezone: ${tz}\" >&2\n else\n read -e -p \"Timezone [Asia/Shanghai]: \" tz\n tz=\"${tz:-Asia/Shanghai}\"\n fi\n fi\n\n echo \"${tz}\"\n}\n\n# Detect timezone once at startup (used by registry selection and container TZ)\nAGENTTEAMS_TIMEZONE=\"${AGENTTEAMS_TIMEZONE:-$(detect_timezone)}\"\n\n# ============================================================\n# Language detection based on timezone\n# ============================================================\n\ndetect_language() {\n local tz=\"${AGENTTEAMS_TIMEZONE}\"\n case \"${tz}\" in\n Asia/Shanghai|Asia/Chongqing|Asia/Harbin|Asia/Urumqi|\\\n Asia/Taipei|Asia/Hong_Kong|Asia/Macau)\n echo \"zh\"\n ;;\n *)\n echo \"en\"\n ;;\n esac\n}\n\n# Language priority: env var > existing env file > timezone detection\nmigrate_legacy_env_file\nif [ -z \"${AGENTTEAMS_LANGUAGE}\" ]; then\n # Check existing env file for saved language preference (upgrade scenario)\n _env_file=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n # Migrate from legacy location (current directory) if needed\n if [ ! -f \"${_env_file}\" ] && [ -f \"./agentteams-manager.env\" ]; then\n mv \"./agentteams-manager.env\" \"${_env_file}\" 2>/dev/null || true\n fi\n if [ -f \"${_env_file}\" ]; then\n _saved_lang=$(grep '^AGENTTEAMS_LANGUAGE=' \"${_env_file}\" 2>/dev/null | cut -d= -f2-)\n if [ -n \"${_saved_lang}\" ]; then\n AGENTTEAMS_LANGUAGE=\"${_saved_lang}\"\n fi\n fi\n # Fall back to timezone-based detection\n if [ -z \"${AGENTTEAMS_LANGUAGE}\" ]; then\n AGENTTEAMS_LANGUAGE=\"$(detect_language)\"\n fi\n unset _env_file _saved_lang\nfi\nexport AGENTTEAMS_LANGUAGE\n\n# ============================================================\n# Centralized message dictionary and msg() function\n# Compatible with bash 3.2+ (macOS default) — uses case instead of declare -A\n# ============================================================\n\n# msg() function: look up message by key, with printf-style argument substitution\n# Falls back to English if the current language translation is missing.\nmsg() {\n local key=\"$1\"\n shift\n local lang=\"${AGENTTEAMS_LANGUAGE:-en}\"\n local text=\"\"\n case \"${key}.${lang}\" in\n # --- Timezone detection messages ---\n \"tz.warning.title.zh\") text=\"无法自动检测时区。\" ;;\n \"tz.warning.title.en\") text=\"Could not detect timezone automatically.\" ;;\n \"tz.warning.prompt.zh\") text=\"请输入您的时区(例如 Asia/Shanghai、America/New_York)。\" ;;\n \"tz.warning.prompt.en\") text=\"Please enter your timezone (e.g., Asia/Shanghai, America/New_York).\" ;;\n \"tz.default.zh\") text=\"使用默认时区: %s\" ;;\n \"tz.default.en\") text=\"Using default timezone: %s\" ;;\n \"tz.input_prompt.zh\") text=\"时区\" ;;\n \"tz.input_prompt.en\") text=\"Timezone\" ;;\n # --- Installation title and info ---\n \"install.title.zh\") text=\"=== AgentTeams Manager 安装 ===\" ;;\n \"install.title.en\") text=\"=== AgentTeams Manager Installation ===\" ;;\n \"install.registry.zh\") text=\"镜像仓库: %s\" ;;\n \"install.registry.en\") text=\"Registry: %s\" ;;\n \"install.dir.zh\") text=\"安装目录: %s\" ;;\n \"install.dir.en\") text=\"Installation directory: %s\" ;;\n \"install.dir_hint.zh\") text=\" (env 文件 'agentteams-manager.env' 将保存到 HOME 目录。)\" ;;\n \"install.dir_hint.en\") text=\" (The env file 'agentteams-manager.env' will be saved to your HOME directory.)\" ;;\n \"install.dir_hint2.zh\") text=\" (请从您希望管理此安装的目录运行此脚本。)\" ;;\n \"install.dir_hint2.en\") text=\" (Run this script from the directory where you want to manage this installation.)\" ;;\n # --- Onboarding mode ---\n \"install.mode.title.zh\") text=\"--- Onboarding 模式 ---\" ;;\n \"install.mode.title.en\") text=\"--- Onboarding Mode ---\" ;;\n \"install.mode.choose.zh\") text=\"选择安装模式:\" ;;\n \"install.mode.choose.en\") text=\"Choose your installation mode:\" ;;\n \"install.mode.quickstart.zh\") text=\" 1) 快速开始 - 使用阿里云通义 Token 套餐快速安装(推荐)\" ;;\n \"install.mode.quickstart.en\") text=\" 1) Quick Start - Fast installation with Qwen Cloud (recommended)\" ;;\n \"install.mode.manual.zh\") text=\" 2) 手动配置 - 选择 LLM 提供商并自定义选项\" ;;\n \"install.mode.manual.en\") text=\" 2) Manual - Choose LLM provider and customize options\" ;;\n \"install.mode.prompt.zh\") text=\"请选择 [1/2]\" ;;\n \"install.mode.prompt.en\") text=\"Enter choice [1/2]\" ;;\n \"install.mode.quickstart_selected.zh\") text=\"已选择快速开始模式 - 使用阿里云通义 Token 套餐\" ;;\n \"install.mode.quickstart_selected.en\") text=\"Quick Start mode selected - using Qwen Cloud\" ;;\n \"install.mode.manual_selected.zh\") text=\"已选择手动配置模式 - 您将选择 LLM 提供商并自定义选项\" ;;\n \"install.mode.manual_selected.en\") text=\"Manual mode selected - you will choose LLM provider and customize options\" ;;\n \"install.mode.invalid.zh\") text=\"无效选择,默认使用快速开始模式\" ;;\n \"install.mode.invalid.en\") text=\"Invalid choice, defaulting to Quick Start mode\" ;;\n # --- Upgrade mode sub-menu ---\n \"upgrade.mode.prompt.zh\") text=\"请选择升级方式:\" ;;\n \"upgrade.mode.prompt.en\") text=\"Select upgrade mode:\" ;;\n \"upgrade.mode.keep_all.zh\") text=\" 1) 保留所有参数,一键升级(推荐)\" ;;\n \"upgrade.mode.keep_all.en\") text=\" 1) Keep all parameters, quick upgrade (recommended)\" ;;\n \"upgrade.mode.confirm_each.zh\") text=\" 2) 逐个确认参数(可修改)\" ;;\n \"upgrade.mode.confirm_each.en\") text=\" 2) Confirm each parameter (can modify)\" ;;\n \"upgrade.mode.back.zh\") text=\" 3) 返回\" ;;\n \"upgrade.mode.back.en\") text=\" 3) Back\" ;;\n # --- Version selection ---\n \"install.version.title.zh\") text=\"--- 版本选择 ---\" ;;\n \"install.version.title.en\") text=\"--- Version Selection ---\" ;;\n \"install.version.choose.zh\") text=\"选择要安装的版本:\" ;;\n \"install.version.choose.en\") text=\"Choose the version to install:\" ;;\n \"install.version.option_latest.zh\") text=\" 1) latest - 最新版(默认)\" ;;\n \"install.version.option_latest.en\") text=\" 1) latest - Latest build (default)\" ;;\n \"install.version.option_stable.zh\") text=\" 2) %s - 最新稳定版\" ;;\n \"install.version.option_stable.en\") text=\" 2) %s - Latest stable release\" ;;\n \"install.version.fetching.zh\") text=\"正在查询最新稳定版本...\" ;;\n \"install.version.fetching.en\") text=\"Fetching latest stable release...\" ;;\n \"install.version.fetch_failed.zh\") text=\"无法查询 GitHub,使用内置版本 %s\" ;;\n \"install.version.fetch_failed.en\") text=\"Could not reach GitHub, using built-in version %s\" ;;\n \"install.version.option_custom.zh\") text=\" 3) 自定义 - 手动输入版本号(如 v1.0.5)\" ;;\n \"install.version.option_custom.en\") text=\" 3) Custom - Enter a specific version (e.g. v1.0.5)\" ;;\n \"install.version.prompt.zh\") text=\"请选择 [1/2/3]\" ;;\n \"install.version.prompt.en\") text=\"Enter choice [1/2/3]\" ;;\n \"install.version.custom_prompt.zh\") text=\"请输入版本号\" ;;\n \"install.version.custom_prompt.en\") text=\"Enter version tag\" ;;\n \"install.version.selected_latest.zh\") text=\"已选择最新版 (latest)\" ;;\n \"install.version.selected_latest.en\") text=\"Selected latest version\" ;;\n \"install.version.selected_stable.zh\") text=\"已选择最新稳定版 (%s)\" ;;\n \"install.version.selected_stable.en\") text=\"Selected latest stable version (%s)\" ;;\n \"install.version.selected_custom.zh\") text=\"已选择自定义版本 (%s)\" ;;\n \"install.version.selected_custom.en\") text=\"Selected custom version (%s)\" ;;\n \"install.version.invalid.zh\") text=\"无效选择,使用最新稳定版 (%s)\" ;;\n \"install.version.invalid.en\") text=\"Invalid choice, defaulting to latest stable version (%s)\" ;;\n # --- Existing installation detected ---\n \"install.existing.detected.zh\") text=\"检测到已有 Manager 安装(env 文件: %s)\" ;;\n \"install.existing.detected.en\") text=\"Existing Manager installation detected (env file: %s)\" ;;\n \"install.existing.choose.zh\") text=\"选择操作:\" ;;\n \"install.existing.choose.en\") text=\"Choose an action:\" ;;\n \"install.existing.upgrade.zh\") text=\" 1) 就地升级(保留数据、工作空间、env 文件)\" ;;\n \"install.existing.upgrade.en\") text=\" 1) In-place upgrade (keep data, workspace, env file)\" ;;\n \"install.existing.reinstall.zh\") text=\" 2) 全新重装(删除所有数据,重新开始)\" ;;\n \"install.existing.reinstall.en\") text=\" 2) Clean reinstall (remove all data, start fresh)\" ;;\n \"install.existing.cancel.zh\") text=\" 3) 取消\" ;;\n \"install.existing.cancel.en\") text=\" 3) Cancel\" ;;\n \"install.existing.prompt.zh\") text=\"请选择 [1/2/3]\" ;;\n \"install.existing.prompt.en\") text=\"Enter choice [1/2/3]\" ;;\n \"install.existing.upgrade_noninteractive.zh\") text=\"非交互模式: 执行就地升级...\" ;;\n \"install.existing.upgrade_noninteractive.en\") text=\"Non-interactive mode: performing in-place upgrade...\" ;;\n \"install.existing.upgrading.zh\") text=\"执行就地升级...\" ;;\n \"install.existing.upgrading.en\") text=\"Performing in-place upgrade...\" ;;\n \"install.existing.warn_manager_stop.zh\") text=\"⚠️ Manager 容器将被停止并重新创建。\" ;;\n \"install.existing.warn_manager_stop.en\") text=\"⚠️ Manager container will be stopped and recreated.\" ;;\n \"install.existing.warn_worker_recreate.zh\") text=\"⚠️ Worker 容器也将被重新创建(以更新 Manager IP)。\" ;;\n \"install.existing.warn_worker_recreate.en\") text=\"⚠️ Worker containers will also be recreated (to update Manager IP in hosts).\" ;;\n \"install.existing.continue_prompt.zh\") text=\"继续?[y/N]\" ;;\n \"install.existing.continue_prompt.en\") text=\"Continue? [y/N]\" ;;\n \"install.existing.cancelled.zh\") text=\"安装已取消。\" ;;\n \"install.existing.cancelled.en\") text=\"Installation cancelled.\" ;;\n \"install.existing.stopping_manager.zh\") text=\"停止并移除现有 manager 容器...\" ;;\n \"install.existing.stopping_manager.en\") text=\"Stopping and removing existing manager container...\" ;;\n \"install.existing.stopping_workers.zh\") text=\"停止并移除现有 worker 容器...\" ;;\n \"install.existing.stopping_workers.en\") text=\"Stopping and removing existing worker containers...\" ;;\n \"install.existing.removed.zh\") text=\" 已移除: %s\" ;;\n \"install.existing.removed.en\") text=\" Removed: %s\" ;;\n # --- Clean reinstall messages ---\n \"install.reinstall.performing.zh\") text=\"执行全新重装...\" ;;\n \"install.reinstall.performing.en\") text=\"Performing clean reinstall...\" ;;\n \"install.reinstall.warn_stop.zh\") text=\"⚠️ 以下运行中的容器将被停止:\" ;;\n \"install.reinstall.warn_stop.en\") text=\"⚠️ The following running containers will be stopped:\" ;;\n \"install.reinstall.warn_delete.zh\") text=\"⚠️ 警告: 以下内容将被删除:\" ;;\n \"install.reinstall.warn_delete.en\") text=\"⚠️ WARNING: This will DELETE the following:\" ;;\n \"install.reinstall.warn_volume.zh\") text=\" - Docker 卷: agentteams-data\" ;;\n \"install.reinstall.warn_volume.en\") text=\" - Docker volume: agentteams-data\" ;;\n \"install.reinstall.warn_env.zh\") text=\" - Env 文件: %s\" ;;\n \"install.reinstall.warn_env.en\") text=\" - Env file: %s\" ;;\n \"install.reinstall.warn_workspace.zh\") text=\" - Manager 工作空间: %s\" ;;\n \"install.reinstall.warn_workspace.en\") text=\" - Manager workspace: %s\" ;;\n \"install.reinstall.warn_workers.zh\") text=\" - 所有 worker 容器\" ;;\n \"install.reinstall.warn_workers.en\") text=\" - All worker containers\" ;;\n \"install.reinstall.warn_proxy.zh\") text=\" - Docker API 代理容器: agentteams-controller\" ;;\n \"install.reinstall.warn_proxy.en\") text=\" - Docker API proxy container: agentteams-controller\" ;;\n \"install.reinstall.warn_network.zh\") text=\" - Docker 网络: agentteams-net\" ;;\n \"install.reinstall.warn_network.en\") text=\" - Docker network: agentteams-net\" ;;\n \"install.reinstall.confirm_type.zh\") text=\"请输入工作空间路径以确认删除(或按 Ctrl+C 取消):\" ;;\n \"install.reinstall.confirm_type.en\") text=\"To confirm deletion, please type the workspace path:\" ;;\n \"install.reinstall.confirm_path.zh\") text=\"输入路径以确认(或按 Ctrl+C 取消)\" ;;\n \"install.reinstall.confirm_path.en\") text=\"Type the path to confirm (or press Ctrl+C to cancel)\" ;;\n \"install.reinstall.path_mismatch.zh\") text=\"路径不匹配。中止重装。输入: '%s',期望: '%s'\" ;;\n \"install.reinstall.path_mismatch.en\") text=\"Path mismatch. Aborting reinstall. Input: '%s', Expected: '%s'\" ;;\n \"install.reinstall.confirmed.zh\") text=\"已确认。正在清理...\" ;;\n \"install.reinstall.confirmed.en\") text=\"Confirmed. Cleaning up...\" ;;\n \"install.reinstall.removed_worker.zh\") text=\" 已移除 worker: %s\" ;;\n \"install.reinstall.removed_worker.en\") text=\" Removed worker: %s\" ;;\n \"install.reinstall.removing_volume.zh\") text=\"正在移除 Docker 卷: agentteams-data\" ;;\n \"install.reinstall.removing_volume.en\") text=\"Removing Docker volume: agentteams-data\" ;;\n \"install.reinstall.warn_volume_fail.zh\") text=\" 警告: 无法移除卷(可能有引用)\" ;;\n \"install.reinstall.warn_volume_fail.en\") text=\" Warning: Could not remove volume (may have references)\" ;;\n \"install.reinstall.removing_proxy.zh\") text=\"正在移除 Docker API 代理容器: agentteams-controller\" ;;\n \"install.reinstall.removing_proxy.en\") text=\"Removing Docker API proxy container: agentteams-controller\" ;;\n \"install.reinstall.removing_network.zh\") text=\"正在移除 Docker 网络: agentteams-net\" ;;\n \"install.reinstall.removing_network.en\") text=\"Removing Docker network: agentteams-net\" ;;\n \"install.reinstall.removing_workspace.zh\") text=\"正在移除工作空间目录: %s\" ;;\n \"install.reinstall.removing_workspace.en\") text=\"Removing workspace directory: %s\" ;;\n \"install.reinstall.removing_env.zh\") text=\"正在移除 env 文件: %s\" ;;\n \"install.reinstall.removing_env.en\") text=\"Removing env file: %s\" ;;\n \"install.reinstall.cleanup_done.zh\") text=\"清理完成。开始全新安装...\" ;;\n \"install.reinstall.cleanup_done.en\") text=\"Cleanup complete. Starting fresh installation...\" ;;\n \"install.reinstall.failed_rm_workspace.zh\") text=\"无法移除工作空间目录\" ;;\n \"install.reinstall.failed_rm_workspace.en\") text=\"Failed to remove workspace directory\" ;;\n # --- Orphan volume detection ---\n \"install.orphan_volume.detected.zh\") text=\"⚠️ 检测到残留数据卷 '%s',但未找到对应的 env 配置文件。\" ;;\n \"install.orphan_volume.detected.en\") text=\"⚠️ Found leftover data volume '%s' but no matching env config file.\" ;;\n \"install.orphan_volume.warn.zh\") text=\"这可能是之前安装的残留数据,会导致新安装出现异常(如密码冲突、服务启动失败)。\" ;;\n \"install.orphan_volume.warn.en\") text=\"This is likely leftover data from a previous installation and may cause issues (e.g., credential conflicts, service startup failures).\" ;;\n \"install.orphan_volume.choose.zh\") text=\"选择操作:\" ;;\n \"install.orphan_volume.choose.en\") text=\"Choose an action:\" ;;\n \"install.orphan_volume.clean.zh\") text=\" 1) 清理残留数据卷后继续安装(推荐)\" ;;\n \"install.orphan_volume.clean.en\") text=\" 1) Remove leftover volume and continue installation (recommended)\" ;;\n \"install.orphan_volume.keep.zh\") text=\" 2) 保留数据卷继续安装(可能出现异常)\" ;;\n \"install.orphan_volume.keep.en\") text=\" 2) Keep the volume and continue installation (may cause issues)\" ;;\n \"install.orphan_volume.prompt.zh\") text=\"请选择 [1/2]\" ;;\n \"install.orphan_volume.prompt.en\") text=\"Enter choice [1/2]\" ;;\n \"install.orphan_volume.cleaning.zh\") text=\"正在清理残留数据卷...\" ;;\n \"install.orphan_volume.cleaning.en\") text=\"Removing leftover data volume...\" ;;\n \"install.orphan_volume.cleaned.zh\") text=\"残留数据卷已清理。继续全新安装...\" ;;\n \"install.orphan_volume.cleaned.en\") text=\"Leftover volume removed. Continuing with fresh installation...\" ;;\n \"install.orphan_volume.keeping.zh\") text=\"保留数据卷,继续安装。如遇异常请选择全新重装。\" ;;\n \"install.orphan_volume.keeping.en\") text=\"Keeping existing volume. If you encounter issues, consider a clean reinstall.\" ;;\n \"install.orphan_volume.clean_noninteractive.zh\") text=\"非交互模式: 自动清理残留数据卷...\" ;;\n \"install.orphan_volume.clean_noninteractive.en\") text=\"Non-interactive mode: automatically removing leftover volume...\" ;;\n # --- Loading existing config ---\n \"install.loading_config.zh\") text=\"从 %s 加载已有配置(shell 环境变量优先)...\" ;;\n \"install.loading_config.en\") text=\"Loading existing config from %s (shell env vars take priority)...\" ;;\n # --- LLM Configuration ---\n \"llm.title.zh\") text=\"--- LLM 配置 ---\" ;;\n \"llm.title.en\") text=\"--- LLM Configuration ---\" ;;\n \"llm.provider.label.zh\") text=\" 提供商: %s\" ;;\n \"llm.provider.label.en\") text=\" Provider: %s\" ;;\n \"llm.model.label.zh\") text=\" 模型: %s\" ;;\n \"llm.model.label.en\") text=\" Model: %s\" ;;\n \"llm.provider.qwen.zh\") text=\" 提供商: qwen(阿里云百炼)\" ;;\n \"llm.provider.qwen.en\") text=\" Provider: qwen (Alibaba Cloud Bailian)\" ;;\n \"llm.provider.qwen_default.zh\") text=\" 提供商: %s(默认)\" ;;\n \"llm.provider.qwen_default.en\") text=\" Provider: %s (default)\" ;;\n \"llm.model.default.zh\") text=\" 模型: %s(默认)\" ;;\n \"llm.model.default.en\") text=\" Model: %s (default)\" ;;\n \"llm.apikey_hint_bailian.zh\") text=\" 💡 获取阿里云百炼(DashScope)API Key:\" ;;\n \"llm.apikey_hint_bailian.en\") text=\" 💡 Get your Alibaba Cloud Bailian (DashScope) API Key:\" ;;\n \"llm.apikey_url_bailian.zh\") text=\" https://www.aliyun.com/product/bailian\" ;;\n \"llm.apikey_url_bailian.en\") text=\" https://www.aliyun.com/product/bailian\" ;;\n \"llm.apikey_hint_qwencloud.zh\") text=\" 💡 从 Qwen Cloud(国际站)获取 DASHSCOPE_API_KEY:\" ;;\n \"llm.apikey_hint_qwencloud.en\") text=\" 💡 Get your DASHSCOPE_API_KEY for Qwen Cloud (international) from:\" ;;\n \"llm.apikey_url_qwencloud.zh\") text=\" https://home.qwencloud.com/api-keys (文档: https://docs.qwencloud.com/)\" ;;\n \"llm.apikey_url_qwencloud.en\") text=\" https://home.qwencloud.com/api-keys | Docs: https://docs.qwencloud.com/\" ;;\n \"llm.apikey_hint_tokenplan.zh\") text=\" 💡 获取 DashScope API Key 或开通通义 Token 套餐,请参考:\" ;;\n \"llm.apikey_hint_tokenplan.en\") text=\" 💡 Get your DashScope or Token Plan API key (Alibaba Model Studio):\" ;;\n \"llm.apikey_url_tokenplan.zh\") text=\" https://help.aliyun.com/zh/model-studio/token-plan-quickstart\" ;;\n \"llm.apikey_url_tokenplan.en\") text=\" https://common-buy.aliyun.com/token-plan/ | https://help.aliyun.com/zh/model-studio/token-plan-quickstart\" ;;\n \"llm.apikey_hint_codingplan.zh\") text=\" 💡 获取 DashScope API Key(Coding 套餐 / coding.dashscope 接口):\" ;;\n \"llm.apikey_hint_codingplan.en\") text=\" 💡 Get your DashScope API key for Coding Plan (coding.dashscope endpoint):\" ;;\n \"llm.apikey_url_codingplan.zh\") text=\" https://help.aliyun.com/zh/model-studio/get-api-key\" ;;\n \"llm.apikey_url_codingplan.en\") text=\" https://help.aliyun.com/zh/model-studio/get-api-key\" ;;\n \"llm.apikey_prompt.zh\") text=\"LLM API Key\" ;;\n \"llm.apikey_prompt.en\") text=\"LLM API Key\" ;;\n \"llm.providers_title.zh\") text=\"可用 LLM 提供商:\" ;;\n \"llm.providers_title.en\") text=\"Available LLM Providers:\" ;;\n \"llm.provider.alibaba.zh\") text=\" 1) 阿里云通义 Token 套餐 - 推荐中国用户使用\" ;;\n \"llm.provider.alibaba.en\") text=\" 1) Qwen Cloud - International (OpenAI-compatible API, recommended)\" ;;\n \"llm.provider.openai_compat.zh\") text=\" 2) OpenAI 兼容 API - 自定义 Base URL(OpenAI、DeepSeek 等)\" ;;\n \"llm.provider.openai_compat.en\") text=\" 2) OpenAI-compatible API - Custom Base URL (OpenAI, DeepSeek, etc.)\" ;;\n \"llm.provider.select.zh\") text=\"选择提供商 [1/2]\" ;;\n \"llm.provider.select.en\") text=\"Select provider [1/2]\" ;;\n \"llm.alibaba.models_title.zh\") text=\"选择阿里云模型接入方式:\" ;;\n \"llm.alibaba.models_title.en\") text=\"Select Alibaba Cloud model access:\" ;;\n \"llm.alibaba.model.tokenplan.zh\") text=\" 1) 阿里云通义 Token 套餐 - 兼容模式(推荐)\" ;;\n \"llm.alibaba.model.tokenplan.en\") text=\" 1) Alibaba Cloud Token Plan - compatible-mode (recommended)\" ;;\n \"llm.alibaba.model.bailian.zh\") text=\" 2) 阿里云百炼 - DashScope 通用兼容接口\" ;;\n \"llm.alibaba.model.bailian.en\") text=\" 2) Alibaba Cloud Bailian - DashScope compatible mode\" ;;\n \"llm.alibaba.model.codingplan_legacy.zh\") text=\" 3) 阿里云 Coding 套餐 - 旧版端点(兼容保留)\" ;;\n \"llm.alibaba.model.codingplan_legacy.en\") text=\" 3) Alibaba Cloud Coding Plan - legacy endpoint (backward compatible)\" ;;\n \"llm.alibaba.model.select.zh\") text=\"选择接入方式 [1/2/3]\" ;;\n \"llm.alibaba.model.select.en\") text=\"Select access option [1/2/3]\" ;;\n \"llm.alibaba.model.invalid.zh\") text=\"无效选择: %s(请输入 1、2 或 3)\" ;;\n \"llm.alibaba.model.invalid.en\") text=\"Invalid choice: %s (please enter 1, 2, or 3)\" ;;\n \"llm.codingplan.models_title.zh\") text=\"选择通义 Token 套餐默认模型:\" ;;\n \"llm.codingplan.models_title.en\") text=\"Select Qwen Cloud default model:\" ;;\n \"llm.codingplan.model.qwen36plus.zh\") text=\" 1) qwen3.6-plus - 千问 3.6(推荐)\" ;;\n \"llm.codingplan.model.qwen36plus.en\") text=\" 1) qwen3.6-plus - Qwen 3.6 (recommended)\" ;;\n \"llm.codingplan.model.glm5.zh\") text=\" 2) glm-5 - 智谱 GLM-5(编程推荐)\" ;;\n \"llm.codingplan.model.glm5.en\") text=\" 2) glm-5 - Zhipu GLM-5 (recommended for coding)\" ;;\n \"llm.codingplan.model.kimi.zh\") text=\" 3) kimi-k2.5 - Moonshot Kimi K2.5\" ;;\n \"llm.codingplan.model.kimi.en\") text=\" 3) kimi-k2.5 - Moonshot Kimi K2.5\" ;;\n \"llm.codingplan.model.minimax.zh\") text=\" 4) MiniMax-M2.5 - MiniMax M2.5\" ;;\n \"llm.codingplan.model.minimax.en\") text=\" 4) MiniMax-M2.5 - MiniMax M2.5\" ;;\n \"llm.codingplan.model.select.zh\") text=\"选择模型 [1/2/3/4]\" ;;\n \"llm.codingplan.model.select.en\") text=\"Select model [1/2/3/4]\" ;;\n \"llm.provider.selected_tokenplan.zh\") text=\" 提供商: 阿里云通义 Token 套餐(兼容模式)\" ;;\n \"llm.provider.selected_tokenplan.en\") text=\" Provider: Alibaba Cloud Token Plan (compatible mode)\" ;;\n \"llm.provider.selected_codingplan.zh\") text=\" 提供商: 阿里云通义 Token 套餐(alibaba-cloud)\" ;;\n \"llm.provider.selected_codingplan.en\") text=\" Provider: Qwen Cloud (international) (alibaba-cloud)\" ;;\n \"llm.provider.selected_codingplan_legacy.zh\") text=\" 提供商: 阿里云 Coding 套餐(coding.dashscope)\" ;;\n \"llm.provider.selected_codingplan_legacy.en\") text=\" Provider: Alibaba Cloud Coding Plan (coding.dashscope)\" ;;\n \"llm.provider.selected_qwen.zh\") text=\" 提供商: 阿里云百炼\" ;;\n \"llm.provider.selected_qwen.en\") text=\" Provider: Alibaba Cloud Bailian\" ;;\n \"llm.provider.selected_openai.zh\") text=\" 提供商: %s(OpenAI 兼容)\" ;;\n \"llm.provider.selected_openai.en\") text=\" Provider: %s (OpenAI-compatible)\" ;;\n \"llm.provider.invalid.zh\") text=\"无效选择: %s(请输入 1 或 2)\" ;;\n \"llm.provider.invalid.en\") text=\"Invalid choice: %s (please enter 1 or 2)\" ;;\n \"llm.qwen.model_prompt.zh\") text=\"默认模型 ID\" ;;\n \"llm.qwen.model_prompt.en\") text=\"Default Model ID\" ;;\n \"llm.openai.base_url_prompt.zh\") text=\"Base URL\" ;;\n \"llm.openai.base_url_prompt.en\") text=\"Base URL\" ;;\n \"llm.openai.model_prompt.zh\") text=\"默认模型 ID\" ;;\n \"llm.openai.model_prompt.en\") text=\"Default Model ID\" ;;\n \"llm.openai.base_url_label.zh\") text=\" Base URL: %s\" ;;\n \"llm.openai.base_url_label.en\") text=\" Base URL: %s\" ;;\n # --- Custom model parameters ---\n \"llm.custom_model.detected.zh\") text=\" ⚠️ 模型 '%s' 不在内置模型列表中,请配置模型参数:\" ;;\n \"llm.custom_model.detected.en\") text=\" ⚠️ Model '%s' is not in the built-in model list. Please configure model parameters:\" ;;\n \"llm.custom_model.context_prompt.zh\") text=\"最大上下文长度(token 数)[150000]\" ;;\n \"llm.custom_model.context_prompt.en\") text=\"Max context window (tokens) [150000]\" ;;\n \"llm.custom_model.max_tokens_prompt.zh\") text=\"最大输出长度(token 数)[128000]\" ;;\n \"llm.custom_model.max_tokens_prompt.en\") text=\"Max output tokens [128000]\" ;;\n \"llm.custom_model.reasoning_prompt.zh\") text=\"是否支持推理/思考模式?[Y/n]\" ;;\n \"llm.custom_model.reasoning_prompt.en\") text=\"Does it support reasoning/thinking mode? [Y/n]\" ;;\n \"llm.custom_model.vision_prompt.zh\") text=\"是否支持图片输入?[y/N]\" ;;\n \"llm.custom_model.vision_prompt.en\") text=\"Does it support image input? [y/N]\" ;;\n \"llm.custom_model.summary.zh\") text=\" 自定义模型参数: 上下文=%s, 最大输出=%s, 推理=%s, 图片=%s\" ;;\n \"llm.custom_model.summary.en\") text=\" Custom model params: context=%s, maxTokens=%s, reasoning=%s, vision=%s\" ;;\n # --- Admin Credentials ---\n \"admin.title.zh\") text=\"--- 管理员凭据 ---\" ;;\n \"admin.title.en\") text=\"--- Admin Credentials ---\" ;;\n \"admin.username_prompt.zh\") text=\"管理员用户名\" ;;\n \"admin.username_prompt.en\") text=\"Admin Username\" ;;\n \"admin.password_prompt.zh\") text=\"管理员密码(留空自动生成,最少 8 位)\" ;;\n \"admin.password_prompt.en\") text=\"Admin Password (leave empty to auto-generate, min 8 chars)\" ;;\n \"admin.password_generated.zh\") text=\" 已自动生成管理员密码\" ;;\n \"admin.password_generated.en\") text=\" Auto-generated admin password\" ;;\n \"admin.password_too_short.zh\") text=\"管理员密码至少需要 8 个字符(MinIO 要求)。当前长度: %s\" ;;\n \"admin.password_too_short.en\") text=\"Admin password must be at least 8 characters (MinIO requirement). Current length: %s\" ;;\n # --- Port Configuration ---\n \"port.title.zh\") text=\"--- 端口配置(按回车使用默认值)---\" ;;\n \"port.title.en\") text=\"--- Port Configuration (press Enter for defaults) ---\" ;;\n \"port.gateway_prompt.zh\") text=\"网关主机端口(容器内 8080)\" ;;\n \"port.gateway_prompt.en\") text=\"Host port for gateway (8080 inside container)\" ;;\n \"port.console_prompt.zh\") text=\"Higress 控制台主机端口(容器内 8001)\" ;;\n \"port.console_prompt.en\") text=\"Host port for Higress console (8001 inside container)\" ;;\n \"port.element_prompt.zh\") text=\"Element Web 直接访问主机端口(容器内 8088)\" ;;\n \"port.element_prompt.en\") text=\"Host port for Element Web direct access (8088 inside container)\" ;;\n \"port.manager_console_prompt.zh\") text=\"Manager 控制台主机端口(容器内 18888)\" ;;\n \"port.manager_console_prompt.en\") text=\"Host port for Manager console (18888 inside container)\" ;;\n \"port.copaw_app_prompt.zh\") text=\"QwenPaw App API 主机端口(容器内 18799)\" ;;\n \"port.copaw_app_prompt.en\") text=\"Host port for QwenPaw App API (18799 inside container)\" ;;\n # --- Local-only binding ---\n \"port.local_only.title.zh\") text=\"--- 网络访问模式 ---\" ;;\n \"port.local_only.title.en\") text=\"--- Network Access Mode ---\" ;;\n \"port.local_only.prompt.zh\") text=\"是否仅允许本机访问(端口绑定到 127.0.0.1)?\" ;;\n \"port.local_only.prompt.en\") text=\"Bind ports to localhost only (127.0.0.1)?\" ;;\n \"port.local_only.hint_yes.zh\") text=\" 仅本机使用,无需开放外部端口(推荐)\" ;;\n \"port.local_only.hint_yes.en\") text=\" Local use only, no external port exposure (recommended)\" ;;\n \"port.local_only.hint_no.zh\") text=\" 允许外部访问(局域网 / 公网)\" ;;\n \"port.local_only.hint_no.en\") text=\" Allow external access (LAN / public network)\" ;;\n \"port.local_only.choice.zh\") text=\"请选择 [1/2]\" ;;\n \"port.local_only.choice.en\") text=\"Enter choice [1/2]\" ;;\n \"port.local_only.selected_local.zh\") text=\"端口已绑定到 127.0.0.1(仅本机访问)\" ;;\n \"port.local_only.selected_local.en\") text=\"Ports bound to 127.0.0.1 (localhost only)\" ;;\n \"port.local_only.selected_external.zh\") text=\"端口已绑定到所有网络接口(0.0.0.0)\" ;;\n \"port.local_only.selected_external.en\") text=\"Ports bound to all interfaces (0.0.0.0)\" ;;\n \"port.local_only.https_hint.zh\") text=\"⚠️ 建议在 Higress 控制台配置 TLS 证书并启用 HTTPS,避免明文传输。\" ;;\n \"port.local_only.https_hint.en\") text=\"⚠️ It is recommended to configure TLS certificates and enable HTTPS in the Higress Console to avoid plaintext transmission.\" ;;\n \"port.local_only.https_docs.zh\") text=\"\" ;;\n \"port.local_only.https_docs.en\") text=\"\" ;;\n # --- Domain Configuration ---\n \"domain.title.zh\") text=\"--- 域名配置(按回车使用默认值)---\" ;;\n \"domain.title.en\") text=\"--- Domain Configuration (press Enter for defaults) ---\" ;;\n \"domain.hint.zh\") text=\"提示: 自定义域名前必须事先做好 DNS 解析。单机 ECS 部署时无需修改 aigw、fs 等域名;Element Web 和 Matrix Server 也可通过 IP 直接访问。\" ;;\n \"domain.hint.en\") text=\"Hint: Configure DNS resolution before customizing domains. For single ECS deployment, no need to change aigw, fs, etc.; Element Web and Matrix Server can also be accessed directly via IP.\" ;;\n \"domain.matrix_prompt.zh\") text=\"Matrix 域名\" ;;\n \"domain.matrix_prompt.en\") text=\"Matrix Domain\" ;;\n \"domain.element_prompt.zh\") text=\"Element Web 域名\" ;;\n \"domain.element_prompt.en\") text=\"Element Web Domain\" ;;\n \"domain.gateway_prompt.zh\") text=\"AI 网关域名\" ;;\n \"domain.gateway_prompt.en\") text=\"AI Gateway Domain\" ;;\n \"domain.fs_prompt.zh\") text=\"文件系统域名\" ;;\n \"domain.fs_prompt.en\") text=\"File System Domain\" ;;\n \"domain.console_prompt.zh\") text=\"Manager 控制台域名\" ;;\n \"domain.console_prompt.en\") text=\"Manager Console Domain\" ;;\n # --- GitHub Integration ---\n \"github.title.zh\") text=\"--- GitHub 集成(可选,按回车跳过)---\" ;;\n \"github.title.en\") text=\"--- GitHub Integration (optional, press Enter to skip) ---\" ;;\n \"github.token_prompt.zh\") text=\"GitHub 个人访问令牌(可选)\" ;;\n \"github.token_prompt.en\") text=\"GitHub Personal Access Token (optional)\" ;;\n # --- Skills Registry ---\n \"skills.title.zh\") text=\"--- Skills 注册中心(可选,按回车使用默认 nacos://market.agentteams.io:80/public)---\" ;;\n \"skills.title.en\") text=\"--- Skills Registry (optional, press Enter for default nacos://market.agentteams.io:80/public) ---\" ;;\n \"skills.url_prompt.zh\") text=\"Skills 注册中心 URL(留空使用默认 nacos://market.agentteams.io:80/public)\" ;;\n \"skills.url_prompt.en\") text=\"Skills Registry URL (leave empty for default nacos://market.agentteams.io:80/public)\" ;;\n # --- Data Persistence ---\n \"data.title.zh\") text=\"--- 数据持久化 ---\" ;;\n \"data.title.en\") text=\"--- Data Persistence ---\" ;;\n \"data.volume_prompt.zh\") text=\"Docker 卷名称 [agentteams-data]\" ;;\n \"data.volume_prompt.en\") text=\"Docker volume name for persistent data [agentteams-data]\" ;;\n \"data.volume_using.zh\") text=\" 使用 Docker 卷: %s\" ;;\n \"data.volume_using.en\") text=\" Using Docker volume: %s\" ;;\n # --- Manager Workspace ---\n \"workspace.title.zh\") text=\"--- Manager 工作空间 ---\" ;;\n \"workspace.title.en\") text=\"--- Manager Workspace ---\" ;;\n \"workspace.dir_prompt.zh\") text=\"Manager 工作空间目录 [%s]\" ;;\n \"workspace.dir_prompt.en\") text=\"Manager workspace directory [%s]\" ;;\n \"workspace.dir_label.zh\") text=\" Manager 工作空间: %s\" ;;\n \"workspace.dir_label.en\") text=\" Manager workspace: %s\" ;;\n # --- Host directory sharing ---\n \"host_share.prompt.zh\") text=\"与 Agent 共享的主机目录(默认: %s)\" ;;\n \"host_share.prompt.en\") text=\"Host directory to share with agents (default: %s)\" ;;\n \"host_share.sharing.zh\") text=\"共享主机目录: %s -> 容器内 /host-share\" ;;\n \"host_share.sharing.en\") text=\"Sharing host directory: %s -> /host-share in container\" ;;\n \"host_share.not_exist.zh\") text=\"警告: 主机目录 %s 不存在,跳过验证继续使用\" ;;\n \"host_share.not_exist.en\") text=\"WARNING: Host directory %s does not exist, using without validation\" ;;\n # --- Default worker runtime ---\n \"worker_runtime.title.zh\") text=\"--- 默认 Worker 运行时 ---\" ;;\n \"worker_runtime.title.en\") text=\"--- Default Worker Runtime ---\" ;;\n \"worker_runtime.openclaw.zh\") text=\"OpenClaw\" ;;\n \"worker_runtime.openclaw.en\") text=\"OpenClaw\" ;;\n \"worker_runtime.copaw.zh\") text=\"QwenPaw\" ;;\n \"worker_runtime.copaw.en\") text=\"QwenPaw\" ;;\n \"worker_runtime.hermes.zh\") text=\"Hermes\" ;;\n \"worker_runtime.hermes.en\") text=\"Hermes\" ;;\n \"worker_runtime.choice.zh\") text=\"请选择 [1/2/3]\" ;;\n \"worker_runtime.choice.en\") text=\"Enter choice [1/2/3]\" ;;\n \"worker_runtime.selected.zh\") text=\"默认 Worker 运行时: %s\" ;;\n \"worker_runtime.selected.en\") text=\"Default Worker runtime: %s\" ;;\n \"worker_runtime.title_short.zh\") text=\"默认 Worker 运行时\" ;;\n \"worker_runtime.title_short.en\") text=\"Default Worker Runtime\" ;;\n \"manager_runtime.title.zh\") text=\"--- Manager 运行时 ---\" ;;\n \"manager_runtime.title.en\") text=\"--- Manager Runtime ---\" ;;\n \"manager_runtime.openclaw.zh\") text=\"OpenClaw\" ;;\n \"manager_runtime.openclaw.en\") text=\"OpenClaw\" ;;\n \"manager_runtime.copaw.zh\") text=\"QwenPaw\" ;;\n \"manager_runtime.copaw.en\") text=\"QwenPaw\" ;;\n \"manager_runtime.choice.zh\") text=\"请选择 [1/2]\" ;;\n \"manager_runtime.choice.en\") text=\"Enter choice [1/2]\" ;;\n \"manager_runtime.selected.zh\") text=\"Manager 运行时: %s\" ;;\n \"manager_runtime.selected.en\") text=\"Manager runtime: %s\" ;;\n \"manager_runtime.title_short.zh\") text=\"Manager 运行时\" ;;\n \"manager_runtime.title_short.en\") text=\"Manager Runtime\" ;;\n # --- Secrets and config ---\n \"install.generating_secrets.zh\") text=\"正在生成密钥...\" ;;\n \"install.generating_secrets.en\") text=\"Generating secrets...\" ;;\n \"install.config_saved.zh\") text=\"配置已保存到 %s\" ;;\n \"install.config_saved.en\") text=\"Configuration saved to %s\" ;;\n # --- Container runtime socket ---\n \"install.socket_detected.zh\") text=\"容器运行时 socket: %s(已启用直接创建 Worker)\" ;;\n \"install.socket_detected.en\") text=\"Container runtime socket: %s (direct Worker creation enabled)\" ;;\n \"install.socket_not_found.zh\") text=\"未找到容器运行时 socket(Manager 无法直接创建 Worker 容器,需要你手动执行 docker 命令创建)\" ;;\n \"install.socket_not_found.en\") text=\"No container runtime socket found (Manager cannot create Worker containers directly, you will need to create them manually using docker commands)\" ;;\n \"install.socket_confirm.title.zh\") text=\"⚠️ 未检测到容器运行时 Socket\" ;;\n \"install.socket_confirm.title.en\") text=\"⚠️ Container Runtime Socket Not Detected\" ;;\n \"install.socket_confirm.message.zh\") text=\"未找到 Docker/Podman socket,Manager 将无法自动创建 Worker 容器。\\n你需要手动执行 docker run 命令来创建 Worker。\\n\\n是否继续安装?\" ;;\n \"install.socket_confirm.message.en\") text=\"Docker/Podman socket not found. Manager will not be able to create Worker containers automatically.\\nYou will need to manually run docker commands to create Workers.\\n\\nContinue installation?\" ;;\n \"install.socket_confirm.prompt.zh\") text=\"继续安装? [y/N]: \" ;;\n \"install.socket_confirm.prompt.en\") text=\"Continue? [y/N]: \" ;;\n \"install.socket_confirm.cancelled.zh\") text=\"安装已取消。如需启用 Worker 自动创建,请确保 Docker/Podman 正在运行,然后重新运行安装脚本。\" ;;\n \"install.socket_confirm.cancelled.en\") text=\"Installation cancelled. To enable automatic Worker creation, ensure Docker/Podman is running and re-run the installer.\" ;;\n # --- Container management ---\n \"install.removing_existing.zh\") text=\"正在移除现有 agentteams-manager 容器...\" ;;\n \"install.removing_existing.en\") text=\"Removing existing agentteams-manager container...\" ;;\n # --- Matrix E2EE ---\n \"matrix_e2ee.title.zh\") text=\"--- Matrix 端到端加密(E2EE)---\" ;;\n \"matrix_e2ee.title.en\") text=\"--- Matrix End-to-End Encryption (E2EE) ---\" ;;\n \"matrix_e2ee.desc.zh\") text=\"E2EE 会对 Manager 与 Worker 之间的 Matrix 消息进行端到端加密。\\n 启用后,即使 Matrix 服务器被入侵,消息内容也无法被窃取。\\n 但 E2EE 会增加首次握手耗时,且要求所有 Agent 都支持 matrix-sdk-crypto。\\n 如果不确定,建议保持禁用。\\n ⚠ 注意:禁用 E2EE 后,请勿在 Element 上创建默认启用加密的 Private 房间,\\n 否则 Agent 将无法读取该房间中的加密消息。请改用 Public 房间或关闭房间加密。\" ;;\n \"matrix_e2ee.desc.en\") text=\"E2EE encrypts Matrix messages between Manager and Workers end-to-end.\\n When enabled, message content stays private even if the Matrix server is compromised.\\n However, E2EE adds overhead to the initial handshake and requires all Agents\\n to support matrix-sdk-crypto. If unsure, keep it disabled.\\n ⚠ Note: When E2EE is disabled, do NOT create Private rooms in Element (which\\n enable encryption by default) — Agents cannot read encrypted messages without\\n E2EE support. Use Public rooms or turn off room encryption instead.\" ;;\n \"matrix_e2ee.enable.zh\") text=\"启用 E2EE\" ;;\n \"matrix_e2ee.enable.en\") text=\"Enable E2EE\" ;;\n \"matrix_e2ee.disable.zh\") text=\"禁用 E2EE(推荐)\" ;;\n \"matrix_e2ee.disable.en\") text=\"Disable E2EE (recommended)\" ;;\n \"matrix_e2ee.choice.zh\") text=\"请选择 [1/2]\" ;;\n \"matrix_e2ee.choice.en\") text=\"Enter choice [1/2]\" ;;\n \"matrix_e2ee.selected_enabled.zh\") text=\"Matrix E2EE: 已启用\" ;;\n \"matrix_e2ee.selected_enabled.en\") text=\"Matrix E2EE: enabled\" ;;\n \"matrix_e2ee.selected_disabled.zh\") text=\"Matrix E2EE: 已禁用(默认)\" ;;\n \"matrix_e2ee.selected_disabled.en\") text=\"Matrix E2EE: disabled (default)\" ;;\n \"matrix_e2ee.title_short.zh\") text=\"Matrix E2EE\" ;;\n \"matrix_e2ee.title_short.en\") text=\"Matrix E2EE\" ;;\n \"matrix_e2ee.val_enabled.zh\") text=\"已启用\" ;;\n \"matrix_e2ee.val_enabled.en\") text=\"enabled\" ;;\n \"matrix_e2ee.val_disabled.zh\") text=\"已禁用\" ;;\n \"matrix_e2ee.val_disabled.en\") text=\"disabled\" ;;\n # --- Docker API proxy ---\n \"docker_proxy.title.zh\") text=\"--- Docker API 安全代理 ---\" ;;\n \"docker_proxy.title.en\") text=\"--- Docker API Security Proxy ---\" ;;\n \"docker_proxy.desc.zh\") text=\"Docker API 代理可防止 AI Agent 通过 Docker API 越狱访问宿主机。\\n 启用后,Manager 不再直接持有 Docker socket,所有容器操作经过安全校验。\" ;;\n \"docker_proxy.desc.en\") text=\"Docker API proxy prevents AI Agents from escaping via Docker API to access the host.\\n When enabled, Manager no longer has direct Docker socket access; all container operations go through security validation.\" ;;\n \"docker_proxy.enable.zh\") text=\"启用(推荐)\" ;;\n \"docker_proxy.enable.en\") text=\"Enable (recommended)\" ;;\n \"docker_proxy.disable.zh\") text=\"禁用(直接挂载 Docker socket)\" ;;\n \"docker_proxy.disable.en\") text=\"Disable (mount Docker socket directly)\" ;;\n \"docker_proxy.choice.zh\") text=\"请选择 [1/2]\" ;;\n \"docker_proxy.choice.en\") text=\"Enter choice [1/2]\" ;;\n \"docker_proxy.selected_enabled.zh\") text=\"Docker API 代理: 已启用\" ;;\n \"docker_proxy.selected_enabled.en\") text=\"Docker API proxy: enabled\" ;;\n \"docker_proxy.selected_disabled.zh\") text=\"Docker API 代理: 已禁用\" ;;\n \"docker_proxy.selected_disabled.en\") text=\"Docker API proxy: disabled\" ;;\n \"docker_proxy.title_short.zh\") text=\"Docker API 代理\" ;;\n \"docker_proxy.title_short.en\") text=\"Docker API Proxy\" ;;\n \"docker_proxy.val_enabled.zh\") text=\"已启用\" ;;\n \"docker_proxy.val_enabled.en\") text=\"enabled\" ;;\n \"docker_proxy.val_disabled.zh\") text=\"已禁用\" ;;\n \"docker_proxy.val_disabled.en\") text=\"disabled\" ;;\n \"docker_proxy.registries_desc.zh\") text=\"默认放行的镜像来源:本地镜像、localhost、Higress 仓库(所有 region)。\\n 如需放行其他镜像仓库,请输入逗号分隔的地址前缀。\\n 示例: ghcr.io/myorg,registry.example.com/team\" ;;\n \"docker_proxy.registries_desc.en\") text=\"Default allowed image sources: local images, localhost, Higress registries (all regions).\\n To allow additional image sources, enter comma-separated address prefixes.\\n Example: ghcr.io/myorg,registry.example.com/team\" ;;\n \"docker_proxy.registries_prompt.zh\") text=\"额外放行的镜像来源(按回车跳过)\" ;;\n \"docker_proxy.registries_prompt.en\") text=\"Additional allowed image sources (press Enter to skip)\" ;;\n \"docker_proxy.registries_label.zh\") text=\"额外放行的镜像来源\" ;;\n \"docker_proxy.registries_label.en\") text=\"Additional allowed image sources\" ;;\n # --- Podman Autostart ---\n \"podman.autostart.title.zh\") text=\"--- Podman 驻留与开机自启 ---\" ;;\n \"podman.autostart.title.en\") text=\"--- Podman Linger & Autostart ---\" ;;\n \"podman.autostart.desc.zh\") text=\"配置为 systemd 服务,支持开机自动启动及后台驻留。\" ;;\n \"podman.autostart.desc.en\") text=\"Configure as a systemd service for automatic startup on boot.\" ;;\n \"podman.autostart.enable.zh\") text=\"启用(推荐)\" ;;\n \"podman.autostart.enable.en\") text=\"Enable (recommended)\" ;;\n \"podman.autostart.disable.zh\") text=\"不启用(容器随当前会话关闭而停止)\" ;;\n \"podman.autostart.disable.en\") text=\"Disable (containers stop when session ends)\" ;;\n \"podman.autostart.choice.zh\") text=\"请选择 [1/2]\" ;;\n \"podman.autostart.choice.en\") text=\"Enter choice [1/2]\" ;;\n \"podman.autostart.selected_enabled.zh\") text=\"Podman 开机自启: 已启用\" ;;\n \"podman.autostart.selected_enabled.en\") text=\"Podman Autostart: enabled\" ;;\n \"podman.autostart.selected_disabled.zh\") text=\"Podman 开机自启: 已禁用\" ;;\n \"podman.autostart.selected_disabled.en\") text=\"Podman Autostart: disabled\" ;;\n \"podman.autostart.title_short.zh\") text=\"Podman 开机自启\" ;;\n \"podman.autostart.title_short.en\") text=\"Podman Autostart\" ;;\n \"podman.autostart.val_enabled.zh\") text=\"已启用\" ;;\n \"podman.autostart.val_enabled.en\") text=\"enabled\" ;;\n \"podman.autostart.val_disabled.zh\") text=\"已禁用\" ;;\n \"podman.autostart.val_disabled.en\") text=\"disabled\" ;;\n \"install.podman.autostart_title.zh\") text=\"正在配置 Podman 容器开机自启...\" ;;\n \"install.podman.autostart_title.en\") text=\"Configuring Podman container autostart...\" ;;\n \"install.podman.linger_enable.zh\") text=\"尝试启用 systemd linger (如有提示请在此输入 sudo 密码)...\" ;;\n \"install.podman.linger_enable.en\") text=\"Attempting to enable systemd linger (enter sudo password if prompted)...\" ;;\n \"install.podman.linger_warn.zh\") text=\"警告: 无法自动启用 linger。驻留可能失败,请手动执行: sudo loginctl enable-linger \\$(whoami)\" ;;\n \"install.podman.linger_warn.en\") text=\"WARNING: Failed to auto-enable linger. Please run manually: sudo loginctl enable-linger \\$(whoami)\" ;;\n \"install.podman.root_setup.zh\") text=\"检测到 root 用户。正在配置系统级 agentteams-podman-restart 服务...\" ;;\n \"install.podman.root_setup.en\") text=\"Root user detected. Configuring system-wide agentteams-podman-restart service...\" ;;\n \"install.podman.root_success.zh\") text=\"✅ 系统级 agentteams-podman-restart 服务已成功启用并运行。\" ;;\n \"install.podman.root_success.en\") text=\"✅ System-wide agentteams-podman-restart service successfully enabled and running.\" ;;\n \"install.podman.root_fail.zh\") text=\"⚠️ 系统级 agentteams-podman-restart 服务启用失败,请稍后手动检查。\" ;;\n \"install.podman.root_fail.en\") text=\"⚠️ Failed to enable system-wide agentteams-podman-restart service. Please check manually later.\" ;;\n \"install.podman.user_setup.zh\") text=\"检测到普通用户 (%s)。正在配置用户级 agentteams-podman-restart 服务...\" ;;\n \"install.podman.user_setup.en\") text=\"Non-root user (%s) detected. Configuring user-level agentteams-podman-restart service...\" ;;\n \"install.podman.user_success.zh\") text=\"✅ 用户级 agentteams-podman-restart 服务已成功启用并运行。\" ;;\n \"install.podman.user_success.en\") text=\"✅ User-level agentteams-podman-restart service successfully enabled and running.\" ;;\n \"install.podman.user_fail.zh\") text=\"⚠️ 用户级 agentteams-podman-restart 服务启用失败。\\n提示:这通常是因为缺少 XDG_RUNTIME_DIR 或 dbus 没有运行。您可以尝试手动执行: systemctl --user enable --now agentteams-podman-restart.service\" ;;\n \"install.podman.user_fail.en\") text=\"⚠️ Failed to enable user-level agentteams-podman-restart service.\\nHint: This is usually due to missing XDG_RUNTIME_DIR or dbus not running. Try manually: systemctl --user enable --now agentteams-podman-restart.service\" ;;\n \"install.podman.success.zh\") text=\"Podman 开机自启配置完成。\" ;;\n \"install.podman.success.en\") text=\"Podman autostart successfully configured.\" ;;\n # --- Worker idle timeout ---\n \"idle_timeout.prompt.zh\") text=\"Worker 空闲自动停止超时(分钟)[720]\" ;;\n \"idle_timeout.prompt.en\") text=\"Worker idle auto-stop timeout in minutes [720]\" ;;\n \"idle_timeout.selected.zh\") text=\"Worker 空闲超时: %s 分钟\" ;;\n \"idle_timeout.selected.en\") text=\"Worker idle timeout: %s minutes\" ;;\n \"idle_timeout.label.zh\") text=\"Worker 空闲超时(分钟)\" ;;\n \"idle_timeout.label.en\") text=\"Worker idle timeout (min)\" ;;\n # --- YOLO mode ---\n \"install.yolo.zh\") text=\"YOLO 模式已启用(自主决策,无交互提示)\" ;;\n \"install.yolo.en\") text=\"YOLO mode enabled (autonomous decisions, no interactive prompts)\" ;;\n # --- Image pulling ---\n \"install.image.exists.zh\") text=\"Manager 镜像已存在: %s\" ;;\n \"install.image.exists.en\") text=\"Manager image already exists locally: %s\" ;;\n \"install.image.pulling_manager.zh\") text=\"正在拉取 Manager 镜像: %s\" ;;\n \"install.image.pulling_manager.en\") text=\"Pulling Manager image: %s\" ;;\n \"install.image.worker_exists.zh\") text=\"Worker 镜像已存在: %s\" ;;\n \"install.image.worker_exists.en\") text=\"Worker image already exists locally: %s\" ;;\n \"install.image.pulling_worker.zh\") text=\"正在拉取 Worker 镜像: %s\" ;;\n \"install.image.pulling_worker.en\") text=\"Pulling Worker image: %s\" ;;\n # --- Starting container ---\n \"install.starting_manager.zh\") text=\"正在启动 Manager 容器...\" ;;\n \"install.starting_manager.en\") text=\"Starting Manager container...\" ;;\n # --- Wait for Manager ready ---\n \"install.wait_ready.zh\") text=\"等待 Manager Agent 就绪(超时: %ss)...\" ;;\n \"install.wait_ready.en\") text=\"Waiting for Manager agent to be ready (timeout: %ss)...\" ;;\n \"install.wait_ready.ok.zh\") text=\"Manager Agent 已就绪!\" ;;\n \"install.wait_ready.ok.en\") text=\"Manager agent is ready!\" ;;\n \"install.wait_ready.waiting.zh\") text=\"等待中... (%ds/%ds)\" ;;\n \"install.wait_ready.waiting.en\") text=\"Waiting... (%ds/%ds)\" ;;\n \"install.wait_ready.timeout.zh\") text=\"Manager Agent 在 %ss 内未就绪。请检查: docker logs %s\" ;;\n \"install.wait_ready.timeout.en\") text=\"Manager agent did not become ready within %ss. Check: docker logs %s\" ;;\n # --- Wait for Matrix ready ---\n \"install.wait_matrix.zh\") text=\"等待 Matrix 服务就绪(超时: %ss)...\" ;;\n \"install.wait_matrix.en\") text=\"Waiting for Matrix server to be ready (timeout: %ss)...\" ;;\n \"install.wait_matrix.ok.zh\") text=\"Matrix 服务已就绪!\" ;;\n \"install.wait_matrix.ok.en\") text=\"Matrix server is ready!\" ;;\n \"install.wait_matrix.waiting.zh\") text=\"等待 Matrix 中... (%ds/%ds)\" ;;\n \"install.wait_matrix.waiting.en\") text=\"Waiting for Matrix... (%ds/%ds)\" ;;\n \"install.wait_matrix.timeout.zh\") text=\"Matrix 服务在 %ss 内未就绪。请检查: docker logs %s\" ;;\n \"install.wait_matrix.timeout.en\") text=\"Matrix server did not become ready within %ss. Check: docker logs %s\" ;;\n # --- OpenAI-compatible connectivity test ---\n \"llm.openai.test.testing.zh\") text=\"正在测试 API 联通性...\" ;;\n \"llm.openai.test.testing.en\") text=\"Testing API connectivity...\" ;;\n \"llm.openai.test.ok.zh\") text=\"✅ API 联通性测试通过\" ;;\n \"llm.openai.test.ok.en\") text=\"✅ API connectivity test passed\" ;;\n \"llm.openai.test.fail.zh\") text=\"⚠️ API 联通性测试失败(HTTP %s)。响应内容:\\n%s\\n请根据以上错误信息联系您的模型服务商解决。\" ;;\n \"llm.openai.test.fail.en\") text=\"⚠️ API connectivity test failed (HTTP %s). Response body:\\n%s\\nPlease contact your model provider to resolve the issue.\" ;;\n \"llm.openai.test.fail.tokenplan.zh\") text=\"⚠️ 提示: 请确认 API Key 有效且已开通通义 Token 套餐。文档: https://help.aliyun.com/zh/model-studio/token-plan-quickstart\" ;;\n \"llm.openai.test.fail.tokenplan.en\") text=\"⚠️ Hint: Verify your Token Plan API key and compatible-mode access. Docs: https://help.aliyun.com/zh/model-studio/token-plan-quickstart\" ;;\n \"llm.openai.test.fail.codingplan.zh\") text=\"⚠️ 提示: 请确认 API Key 有效且已开通通义 Token 套餐。文档: https://help.aliyun.com/zh/model-studio/token-plan-quickstart\" ;;\n \"llm.openai.test.fail.codingplan.en\") text=\"⚠️ Hint: Verify your DASHSCOPE_API_KEY for Qwen Cloud. API keys: https://home.qwencloud.com/api-keys Docs: https://docs.qwencloud.com/\" ;;\n \"llm.openai.test.fail.codingplan_legacy.zh\") text=\"⚠️ 提示: 请确认 API Key 有效且 Coding 套餐接口可用。文档: https://help.aliyun.com/zh/model-studio/get-api-key\" ;;\n \"llm.openai.test.fail.codingplan_legacy.en\") text=\"⚠️ Hint: Verify your DashScope API key and Coding Plan access. Docs: https://help.aliyun.com/zh/model-studio/get-api-key\" ;;\n \"llm.openai.test.no_curl.zh\") text=\"⚠️ 未找到 curl,跳过 API 联通性测试\" ;;\n \"llm.openai.test.no_curl.en\") text=\"⚠️ curl not found, skipping API connectivity test\" ;;\n \"llm.openai.test.confirm.zh\") text=\"是否仍要继续安装?[y/N/b] \" ;;\n \"llm.openai.test.confirm.en\") text=\"Continue with installation anyway? [y/N/b] \" ;;\n \"llm.embedding.title.zh\") text=\"📦 记忆搜索配置\" ;;\n \"llm.embedding.title.en\") text=\"📦 Memory Search Configuration\" ;;\n \"llm.embedding.hint.zh\") text=\" Embedding 模型可提升记忆搜索质量(语义匹配)。不启用也可正常使用记忆功能(关键词匹配)。\" ;;\n \"llm.embedding.hint.en\") text=\" Embedding model improves memory search quality (semantic matching). Memory still works without it (keyword matching).\" ;;\n \"llm.embedding.option.default.zh\") text=\" 1) text-embedding-v4(推荐)\" ;;\n \"llm.embedding.option.default.en\") text=\" 1) text-embedding-v4 (Recommended)\" ;;\n \"llm.embedding.option.custom.zh\") text=\" 2) 自定义 Embedding 模型\" ;;\n \"llm.embedding.option.custom.en\") text=\" 2) Custom embedding model\" ;;\n \"llm.embedding.option.disable.zh\") text=\" 3) 不启用\" ;;\n \"llm.embedding.option.disable.en\") text=\" 3) Do not enable\" ;;\n \"llm.embedding.select.zh\") text=\"选择\" ;;\n \"llm.embedding.select.en\") text=\"Select\" ;;\n \"llm.embedding.custom_prompt.zh\") text=\" Embedding 模型名称\" ;;\n \"llm.embedding.custom_prompt.en\") text=\" Embedding model name\" ;;\n \"llm.embedding.test.testing.zh\") text=\"正在测试 Embedding API 联通性...\" ;;\n \"llm.embedding.test.testing.en\") text=\"Testing Embedding API connectivity...\" ;;\n \"llm.embedding.test.ok.zh\") text=\"✅ Embedding API 联通性测试通过\" ;;\n \"llm.embedding.test.ok.en\") text=\"✅ Embedding API connectivity test passed\" ;;\n \"llm.embedding.test.fail.zh\") text=\"⚠️ Embedding API 测试失败(HTTP %s)。响应: %s\" ;;\n \"llm.embedding.test.fail.en\") text=\"⚠️ Embedding API test failed (HTTP %s). Response: %s\" ;;\n \"llm.embedding.auto_disabled.zh\") text=\"⚠️ Embedding 已自动禁用,记忆搜索将使用关键词匹配。您可以稍后在 agentteams-manager.env 中设置 AGENTTEAMS_EMBEDDING_MODEL 启用。\" ;;\n \"llm.embedding.auto_disabled.en\") text=\"⚠️ Embedding auto-disabled. Memory search will use keyword matching. You can enable it later in agentteams-manager.env by setting AGENTTEAMS_EMBEDDING_MODEL.\" ;;\n \"llm.embedding.disabled.zh\") text=\"ℹ️ Embedding 已禁用,记忆搜索将使用关键词匹配。\" ;;\n \"llm.embedding.disabled.en\") text=\"ℹ️ Embedding disabled. Memory search will use keyword matching.\" ;;\n \"llm.openai.test.aborted.zh\") text=\"安装已中止。\" ;;\n \"llm.openai.test.aborted.en\") text=\"Installation aborted.\" ;;\n \"nav.back_hint.zh\") text=\"(输入 b 返回上一步)\" ;;\n \"nav.back_hint.en\") text=\"(enter b to go back)\" ;;\n # --- OpenAI-compatible provider creation ---\n \"install.openai_compat.missing.zh\") text=\"警告: OpenAI Base URL 或 API Key 未设置,跳过提供商创建\" ;;\n \"install.openai_compat.missing.en\") text=\"WARNING: OpenAI Base URL or API Key not set, skipping provider creation\" ;;\n \"install.openai_compat.creating.zh\") text=\"正在创建 OpenAI 兼容提供商...\" ;;\n \"install.openai_compat.creating.en\") text=\"Creating OpenAI-compatible provider...\" ;;\n \"install.openai_compat.domain.zh\") text=\" 域名: %s\" ;;\n \"install.openai_compat.domain.en\") text=\" Domain: %s\" ;;\n \"install.openai_compat.port.zh\") text=\" 端口: %s\" ;;\n \"install.openai_compat.port.en\") text=\" Port: %s\" ;;\n \"install.openai_compat.protocol.zh\") text=\" 协议: %s\" ;;\n \"install.openai_compat.protocol.en\") text=\" Protocol: %s\" ;;\n \"install.openai_compat.service_fail.zh\") text=\"警告: 创建 DNS 服务源失败(可能已存在)\" ;;\n \"install.openai_compat.service_fail.en\") text=\"WARNING: Failed to create DNS service source (may already exist)\" ;;\n \"install.openai_compat.provider_fail.zh\") text=\"警告: 创建 AI 提供商失败(可能已存在)\" ;;\n \"install.openai_compat.provider_fail.en\") text=\"WARNING: Failed to create AI provider (may already exist)\" ;;\n \"install.openai_compat.success.zh\") text=\"OpenAI 兼容提供商创建成功\" ;;\n \"install.openai_compat.success.en\") text=\"OpenAI-compatible provider created successfully\" ;;\n # --- Welcome message ---\n \"install.welcome_msg.soul_configured.zh\") text=\"Soul 已配置(找到 soul-configured 标记),跳过 onboarding 消息\" ;;\n \"install.welcome_msg.soul_configured.en\") text=\"Soul already configured (soul-configured marker found), skipping onboarding message\" ;;\n \"install.welcome_msg.logging_in.zh\") text=\"正在以 %s 身份登录以发送欢迎消息...\" ;;\n \"install.welcome_msg.logging_in.en\") text=\"Logging in as %s to send welcome message...\" ;;\n \"install.welcome_msg.login_failed.zh\") text=\"警告: 以 %s 身份登录失败,跳过欢迎消息\" ;;\n \"install.welcome_msg.login_failed.en\") text=\"WARNING: Failed to login as %s, skipping welcome message\" ;;\n \"install.welcome_msg.finding_room.zh\") text=\"正在查找与 Manager 的 DM 房间...\" ;;\n \"install.welcome_msg.finding_room.en\") text=\"Finding DM room with Manager...\" ;;\n \"install.welcome_msg.creating_room.zh\") text=\"正在创建与 Manager 的 DM 房间...\" ;;\n \"install.welcome_msg.creating_room.en\") text=\"Creating DM room with Manager...\" ;;\n \"install.welcome_msg.no_room.zh\") text=\"警告: 无法找到或创建与 Manager 的 DM 房间\" ;;\n \"install.welcome_msg.no_room.en\") text=\"WARNING: Could not find or create DM room with Manager\" ;;\n \"install.welcome_msg.waiting_join.zh\") text=\"等待 Manager 加入房间...\" ;;\n \"install.welcome_msg.waiting_join.en\") text=\"Waiting for Manager to join the room...\" ;;\n \"install.welcome_msg.sending.zh\") text=\"正在向 Manager 发送欢迎消息...\" ;;\n \"install.welcome_msg.sending.en\") text=\"Sending welcome message to Manager...\" ;;\n \"install.welcome_msg.send_failed.zh\") text=\"警告: 发送欢迎消息失败\" ;;\n \"install.welcome_msg.send_failed.en\") text=\"WARNING: Failed to send welcome message\" ;;\n \"install.welcome_msg.sent.zh\") text=\"欢迎消息已发送给 Manager\" ;;\n \"install.welcome_msg.sent.en\") text=\"Welcome message sent to Manager\" ;;\n \"install.welcome_msg.waiting.zh\") text=\"等待 Manager 发送欢迎消息(Higress 路由授权 + LLM 探活,约 45-90s)...\" ;;\n \"install.welcome_msg.waiting.en\") text=\"Waiting for Manager to send the welcome message (Higress route auth + LLM probe, ~45-90s)...\" ;;\n \"install.welcome_msg.confirmed.zh\") text=\"Manager 已确认发送欢迎消息(status.welcomeSent=true,用时 %ss)\" ;;\n \"install.welcome_msg.confirmed.en\") text=\"Manager confirmed welcome message sent (status.welcomeSent=true, %ss elapsed)\" ;;\n \"install.welcome_msg.timeout.zh\") text=\"警告: 在 %ss 内未观察到 Manager 发送欢迎消息(status.welcomeSent=true)。安装仍然成功,所有服务已就绪——可继续按下方提示登录 Element Web。\" ;;\n \"install.welcome_msg.timeout.en\") text=\"WARNING: Did not observe the Manager sending its welcome message (status.welcomeSent=true) within %ss. Installation is still successful, all services are up — continue with the Element Web instructions below.\" ;;\n \"install.welcome_msg.timeout_hint.zh\") text=\"手动触发 onboarding: 登录 Element Web → 打开与 Manager 的 DM 房间 → 发送任意一句话(例如 \\\"hi\\\"),Manager 会接管对话并开始引导。\" ;;\n \"install.welcome_msg.timeout_hint.en\") text=\"Manual onboarding: log in to Element Web → open the DM with the Manager → send any message (e.g. \\\"hi\\\") and the Manager will take over and start the guided setup.\" ;;\n \"install.welcome_msg.timeout_inspect.zh\") text=\"排查命令: docker exec agentteams-controller agt get managers default\" ;;\n \"install.welcome_msg.timeout_inspect.en\") text=\"Inspect status: docker exec agentteams-controller agt get managers default\" ;;\n \"install.welcome_msg.poll_unavailable.zh\") text=\"提示: agentteams-manager 内未找到 AgentTeams CLI,跳过 welcome 等待(旧镜像?)\" ;;\n \"install.welcome_msg.poll_unavailable.en\") text=\"Note: AgentTeams CLI not found inside agentteams-manager; skipping welcome wait (old image?)\" ;;\n # --- Final output panel ---\n \"success.title.zh\") text=\"=== AgentTeams Manager 已启动!===\" ;;\n \"success.title.en\") text=\"=== AgentTeams Manager Started! ===\" ;;\n \"success.domains_configured.zh\") text=\"以下域名已配置解析到 127.0.0.1:\" ;;\n \"success.domains_configured.en\") text=\"The following domains are configured to resolve to 127.0.0.1:\" ;;\n \"success.open_url.zh\") text=\" ★ 在浏览器中打开以下 URL 开始使用: ★\" ;;\n \"success.open_url.en\") text=\" ★ Open the following URL in your browser to start: ★\" ;;\n \"success.login_with.zh\") text=\" 登录信息:\" ;;\n \"success.login_with.en\") text=\" Login with:\" ;;\n \"success.username.zh\") text=\" 用户名: %s\" ;;\n \"success.username.en\") text=\" Username: %s\" ;;\n \"success.password.zh\") text=\" 密码: %s\" ;;\n \"success.password.en\") text=\" Password: %s\" ;;\n \"success.after_login.zh\") text=\" 登录后,开始与 Manager 聊天!\" ;;\n \"success.after_login.en\") text=\" After login, start chatting with the Manager!\" ;;\n \"success.tell_it.zh\") text=\" 告诉它: \\\"创建一个名为 alice 的前端开发 Worker\\\"\" ;;\n \"success.tell_it.en\") text=\" Tell it: \\\"Create a Worker named alice for frontend dev\\\"\" ;;\n \"success.manager_auto.zh\") text=\" Manager 会自动处理一切。\" ;;\n \"success.manager_auto.en\") text=\" The Manager will handle everything automatically.\" ;;\n \"success.mobile_title.zh\") text=\" 📱 移动端访问(FluffyChat / Element Mobile):\" ;;\n \"success.mobile_title.en\") text=\" 📱 Mobile access (FluffyChat / Element Mobile):\" ;;\n \"success.mobile_step1.zh\") text=\" 1. 在手机上下载 FluffyChat 或 Element\" ;;\n \"success.mobile_step1.en\") text=\" 1. Download FluffyChat or Element on your phone\" ;;\n \"success.mobile_step2.zh\") text=\" 2. 设置 homeserver 为: %s\" ;;\n \"success.mobile_step2.en\") text=\" 2. Set homeserver to: %s\" ;;\n \"success.mobile_step2_noip.zh\") text=\" 2. 设置 homeserver 为: http://<本机局域网IP>:%s\" ;;\n \"success.mobile_step2_noip.en\") text=\" 2. Set homeserver to: http://<this-machine-LAN-IP>:%s\" ;;\n \"success.mobile_noip_hint.zh\") text=\" (无法自动检测局域网 IP — 请使用 ifconfig / ip addr 查看)\" ;;\n \"success.mobile_noip_hint.en\") text=\" (Could not detect LAN IP automatically — check with: ifconfig / ip addr)\" ;;\n \"success.mobile_step3.zh\") text=\" 3. 登录信息:\" ;;\n \"success.mobile_step3.en\") text=\" 3. Login with:\" ;;\n \"success.mobile_username.zh\") text=\" 用户名: %s\" ;;\n \"success.mobile_username.en\") text=\" Username: %s\" ;;\n \"success.mobile_password.zh\") text=\" 密码: %s\" ;;\n \"success.mobile_password.en\") text=\" Password: %s\" ;;\n # --- Other consoles and tips ---\n \"success.other_consoles.zh\") text=\"--- 其他控制台 ---\" ;;\n \"success.other_consoles.en\") text=\"--- Other Consoles ---\" ;;\n \"success.higress_console.zh\") text=\" Higress 控制台: http://localhost:%s(用户名: %s / 密码: %s)\" ;;\n \"success.higress_console.en\") text=\" Higress Console: http://localhost:%s (Username: %s / Password: %s)\" ;;\n \"success.manager_console.zh\") text=\" Manager 控制台(本地): http://localhost:%s(无需登录)\" ;;\n \"success.manager_console.en\") text=\" Manager Console (local): http://localhost:%s (no login required)\" ;;\n \"success.manager_console_gateway.zh\") text=\" Manager 控制台(网关): http://console-local.agentteams.io(用户名: %s / 密码: %s)\" ;;\n \"success.manager_console_gateway.en\") text=\" Manager Console (gateway): http://console-local.agentteams.io (Username: %s / Password: %s)\" ;;\n \"success.copaw_console.zh\") text=\" QwenPaw App API: http://localhost:%s(无需登录)\" ;;\n \"success.copaw_console.en\") text=\" QwenPaw App API: http://localhost:%s (no login required)\" ;;\n \"success.switch_llm.title.zh\") text=\"--- 切换 LLM 提供商 ---\" ;;\n \"success.switch_llm.title.en\") text=\"--- Switch LLM Providers ---\" ;;\n \"success.switch_llm.hint.zh\") text=\" 您可以通过 Higress 控制台切换到其他 LLM 提供商(OpenAI、Anthropic 等)。\" ;;\n \"success.switch_llm.hint.en\") text=\" You can switch to other LLM providers (OpenAI, Anthropic, etc.) via Higress Console.\" ;;\n \"success.switch_llm.docs.zh\") text=\" 详细说明请参阅:\" ;;\n \"success.switch_llm.docs.en\") text=\" For detailed instructions, see:\" ;;\n \"success.switch_llm.url.zh\") text=\" https://higress.ai/en/docs/ai/scene-guide/multi-proxy#console-configuration\" ;;\n \"success.switch_llm.url.en\") text=\" https://higress.ai/en/docs/ai/scene-guide/multi-proxy#console-configuration\" ;;\n \"success.tip.zh\") text=\"提示: 您也可以在聊天中让 Manager 为您配置 LLM 提供商。\" ;;\n \"success.tip.en\") text=\"Tip: You can also ask the Manager to configure LLM providers for you in the chat.\" ;;\n \"success.config_file.zh\") text=\"配置文件: %s\" ;;\n \"success.config_file.en\") text=\"Configuration file: %s\" ;;\n \"success.data_volume.zh\") text=\"数据卷: %s\" ;;\n \"success.data_volume.en\") text=\"Data volume: %s\" ;;\n \"success.workspace.zh\") text=\"Manager 工作空间: %s\" ;;\n \"success.workspace.en\") text=\"Manager workspace: %s\" ;;\n # --- Worker installation ---\n \"worker.resetting.zh\") text=\"正在重置 Worker: %s...\" ;;\n \"worker.resetting.en\") text=\"Resetting Worker: %s...\" ;;\n \"worker.exists.zh\") text=\"容器 '%s' 已存在。使用 --reset 重新创建。\" ;;\n \"worker.exists.en\") text=\"Container '%s' already exists. Use --reset to recreate.\" ;;\n \"worker.starting.zh\") text=\"正在启动 Worker: %s...\" ;;\n \"worker.starting.en\") text=\"Starting Worker: %s...\" ;;\n \"worker.skills_url.zh\") text=\" Skills API URL: %s\" ;;\n \"worker.skills_url.en\") text=\" Skills API URL: %s\" ;;\n \"worker.started.zh\") text=\"=== Worker %s 已启动!===\" ;;\n \"worker.started.en\") text=\"=== Worker %s Started! ===\" ;;\n \"worker.container.zh\") text=\"容器: %s\" ;;\n \"worker.container.en\") text=\"Container: %s\" ;;\n \"worker.view_logs.zh\") text=\"查看日志: docker logs -f %s\" ;;\n \"worker.view_logs.en\") text=\"View logs: docker logs -f %s\" ;;\n # --- Prompt function messages ---\n \"prompt.preset.zh\") text=\" %s = (已通过环境变量预设)\" ;;\n \"prompt.preset.en\") text=\" %s = (pre-set via env)\" ;;\n \"prompt.upgrade_keep.zh\") text=\" %s = %s(当前值,回车保留 / 输入新值覆盖)\" ;;\n \"prompt.upgrade_keep.en\") text=\" %s = %s (current value, press Enter to keep / type new value to change)\" ;;\n \"prompt.upgrade_keep_secret.zh\") text=\" %s = %s(当前值,回车保留 / 输入新值覆盖)\" ;;\n \"prompt.upgrade_keep_secret.en\") text=\" %s = %s (current value, press Enter to keep / type new value to change)\" ;;\n \"prompt.upgrade_empty.zh\") text=\" %s = (未设置,回车跳过 / 输入新值设置)\" ;;\n \"prompt.upgrade_empty.en\") text=\" %s = (not set, press Enter to skip / type new value to set)\" ;;\n \"prompt.default.zh\") text=\" %s = %s(默认)\" ;;\n \"prompt.default.en\") text=\" %s = %s (default)\" ;;\n \"prompt.required.zh\") text=\"%s 是必需的(在非交互模式下通过环境变量设置)\" ;;\n \"prompt.required.en\") text=\"%s is required (set via environment variable in non-interactive mode)\" ;;\n \"prompt.required_empty.zh\") text=\"%s 是必需的\" ;;\n \"prompt.required_empty.en\") text=\"%s is required\" ;;\n # --- Language switch prompt (bilingual by design) ---\n \"lang.detected.zh\") text=\"检测到语言 / Detected language: 中文\" ;;\n \"lang.detected.en\") text=\"检测到语言 / Detected language: English\" ;;\n \"lang.switch_title.zh\") text=\"切换语言 / Switch language:\" ;;\n \"lang.switch_title.en\") text=\"切换语言 / Switch language:\" ;;\n \"lang.option_zh.zh\") text=\" 1) 中文\" ;;\n \"lang.option_zh.en\") text=\" 1) 中文\" ;;\n \"lang.option_en.zh\") text=\" 2) English\" ;;\n \"lang.option_en.en\") text=\" 2) English\" ;;\n \"lang.prompt.zh\") text=\"请选择 / Enter choice\" ;;\n \"lang.prompt.en\") text=\"请选择 / Enter choice\" ;;\n # --- Uninstall messages ---\n \"uninstall.title.zh\") text=\"正在卸载 AgentTeams...\" ;;\n \"uninstall.title.en\") text=\"Uninstalling AgentTeams...\" ;;\n \"uninstall.stopping_manager.zh\") text=\"正在停止并移除 agentteams-manager...\" ;;\n \"uninstall.stopping_manager.en\") text=\"Stopping and removing agentteams-manager...\" ;;\n \"uninstall.stopping_workers.zh\") text=\"正在停止并移除 Worker 容器...\" ;;\n \"uninstall.stopping_workers.en\") text=\"Stopping and removing worker containers...\" ;;\n \"uninstall.removed.zh\") text=\" 已移除: %s\" ;;\n \"uninstall.removed.en\") text=\" Removed: %s\" ;;\n \"uninstall.removing_volume.zh\") text=\"正在移除 Docker 卷: %s\" ;;\n \"uninstall.removing_volume.en\") text=\"Removing Docker volume: %s\" ;;\n \"uninstall.removing_env.zh\") text=\"正在移除 env 文件: %s\" ;;\n \"uninstall.removing_env.en\") text=\"Removing env file: %s\" ;;\n \"uninstall.removing_proxy.zh\") text=\"正在停止并移除 Docker API 代理容器: hiclaw-docker-proxy\" ;;\n \"uninstall.removing_proxy.en\") text=\"Stopping and removing Docker API proxy container: hiclaw-docker-proxy\" ;;\n \"uninstall.stopping_controller.zh\") text=\"正在停止并移除 agentteams-controller (内嵌 Tuwunel/MinIO/Higress)...\" ;;\n \"uninstall.stopping_controller.en\") text=\"Stopping and removing agentteams-controller (embedded Tuwunel/MinIO/Higress)...\" ;;\n \"uninstall.removing_network.zh\") text=\"正在移除 Docker 网络: agentteams-net\" ;;\n \"uninstall.removing_network.en\") text=\"Removing Docker network: agentteams-net\" ;;\n \"uninstall.removing_workspace.zh\") text=\"正在移除工作空间目录: %s\" ;;\n \"uninstall.removing_workspace.en\") text=\"Removing workspace directory: %s\" ;;\n \"uninstall.removing_workspace_elevated.zh\") text=\" 工作空间包含 root 文件,通过容器清理...\" ;;\n \"uninstall.removing_workspace_elevated.en\") text=\" Workspace contains root-owned files, cleaning via container...\" ;;\n \"uninstall.removing_log.zh\") text=\"正在移除日志文件: %s\" ;;\n \"uninstall.removing_log.en\") text=\"Removing log file: %s\" ;;\n \"uninstall.done.zh\") text=\"AgentTeams 已卸载。\" ;;\n \"uninstall.done.en\") text=\"AgentTeams has been uninstalled.\" ;;\n # --- Error messages ---\n \"error.name_required.zh\") text=\"--name 是必需的\" ;;\n \"error.name_required.en\") text=\"--name is required\" ;;\n \"error.fs_required.zh\") text=\"--fs 是必需的\" ;;\n \"error.fs_required.en\") text=\"--fs is required\" ;;\n \"error.fs_key_required.zh\") text=\"--fs-key 是必需的\" ;;\n \"error.fs_key_required.en\") text=\"--fs-key is required\" ;;\n \"error.fs_secret_required.zh\") text=\"--fs-secret 是必需的\" ;;\n \"error.fs_secret_required.en\") text=\"--fs-secret is required\" ;;\n \"error.unknown_option.zh\") text=\"未知选项: %s\" ;;\n \"error.unknown_option.en\") text=\"Unknown option: %s\" ;;\n \"error.docker_not_found.zh\") text=\"未找到 docker 或 podman 命令。请先安装 Docker Desktop 或 Podman Desktop:\\n Docker Desktop: https://www.docker.com/products/docker-desktop/\\n Podman Desktop: https://podman-desktop.io/\" ;;\n \"error.docker_not_found.en\") text=\"docker or podman command not found. Please install Docker Desktop or Podman Desktop first:\\n Docker Desktop: https://www.docker.com/products/docker-desktop/\\n Podman Desktop: https://podman-desktop.io/\" ;;\n \"error.docker_not_running.zh\") text=\"Docker 未运行。请先启动 Docker Desktop 或 Podman Desktop。\" ;;\n \"error.docker_not_running.en\") text=\"Docker is not running. Please start Docker Desktop or Podman Desktop first.\" ;;\n # --- Fallback: try English for unknown lang ---\n *)\n case \"${key}.en\" in\n \"tz.warning.title.en\") text=\"Could not detect timezone automatically.\" ;;\n \"install.title.en\") text=\"=== AgentTeams Manager Installation ===\" ;;\n *) text=\"${key}\" ;;\n esac\n ;;\n esac\n if [ $# -gt 0 ]; then\n # shellcheck disable=SC2059\n printf \"${text}\\n\" \"$@\"\n else\n echo \"${text}\"\n fi\n}\n\n# ============================================================\n# Registry selection based on timezone\n# ============================================================\n\ndetect_registry() {\n local tz=\"${AGENTTEAMS_TIMEZONE}\"\n\n case \"${tz}\" in\n America/*)\n echo \"higress-registry.us-west-1.cr.aliyuncs.com\"\n ;;\n Asia/Singapore|Asia/Bangkok|Asia/Jakarta|Asia/Makassar|Asia/Jayapura|\\\n Asia/Kuala_Lumpur|Asia/Ho_Chi_Minh|Asia/Manila|Asia/Yangon|\\\n Asia/Vientiane|Asia/Phnom_Penh|Asia/Pontianak|Asia/Ujung_Pandang)\n echo \"higress-registry.ap-southeast-7.cr.aliyuncs.com\"\n ;;\n *)\n echo \"higress-registry.cn-hangzhou.cr.aliyuncs.com\"\n ;;\n esac\n}\n\nAGENTTEAMS_REGISTRY=\"${AGENTTEAMS_REGISTRY:-$(detect_registry)}\"\n# Backward compatibility: accept old env var names from previous versions\nAGENTTEAMS_INSTALL_CONTROLLER_IMAGE=\"${AGENTTEAMS_INSTALL_CONTROLLER_IMAGE:-${AGENTTEAMS_INSTALL_DOCKER_PROXY_IMAGE:-}}\"\n# Image variables are resolved after version selection in step_version().\n# These placeholders allow early code paths to reference them without errors.\nMANAGER_IMAGE=\"${AGENTTEAMS_INSTALL_MANAGER_IMAGE:-}\"\nMANAGER_COPAW_IMAGE=\"${AGENTTEAMS_INSTALL_MANAGER_COPAW_IMAGE:-}\"\nWORKER_IMAGE=\"${AGENTTEAMS_INSTALL_WORKER_IMAGE:-}\"\nCOPAW_WORKER_IMAGE=\"${AGENTTEAMS_INSTALL_COPAW_WORKER_IMAGE:-}\"\nHERMES_WORKER_IMAGE=\"${AGENTTEAMS_INSTALL_HERMES_WORKER_IMAGE:-}\"\nCONTROLLER_IMAGE=\"${AGENTTEAMS_INSTALL_CONTROLLER_IMAGE:-}\"\n\nresolve_image_tags() {\n MANAGER_IMAGE=\"${AGENTTEAMS_INSTALL_MANAGER_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-manager:${AGENTTEAMS_VERSION}}\"\n MANAGER_COPAW_IMAGE=\"${AGENTTEAMS_INSTALL_MANAGER_COPAW_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-manager-copaw:${AGENTTEAMS_VERSION}}\"\n WORKER_IMAGE=\"${AGENTTEAMS_INSTALL_WORKER_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-worker:${AGENTTEAMS_VERSION}}\"\n COPAW_WORKER_IMAGE=\"${AGENTTEAMS_INSTALL_COPAW_WORKER_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-copaw-worker:${AGENTTEAMS_VERSION}}\"\n HERMES_WORKER_IMAGE=\"${AGENTTEAMS_INSTALL_HERMES_WORKER_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-hermes-worker:${AGENTTEAMS_VERSION}}\"\n EMBEDDED_IMAGE=\"${AGENTTEAMS_INSTALL_EMBEDDED_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-embedded:${AGENTTEAMS_VERSION}}\"\n # CoPaw Worker introduced in v1.0.4; Hermes Worker introduced in v1.1.0\n if [ -z \"${AGENTTEAMS_INSTALL_COPAW_WORKER_IMAGE:-}\" ] && _ver_lt \"${AGENTTEAMS_VERSION}\" \"v1.0.4\"; then\n COPAW_WORKER_IMAGE=\"\"\n fi\n if [ -z \"${AGENTTEAMS_INSTALL_HERMES_WORKER_IMAGE:-}\" ] && _ver_lt \"${AGENTTEAMS_VERSION}\" \"v1.1.0\"; then\n HERMES_WORKER_IMAGE=\"\"\n fi\n}\n\n# Resolve the embedded controller image. Embedded mode is the only supported\n# architecture since PR #616 (manager image no longer bundles Higress/Tuwunel/MinIO).\n# If the embedded image is unavailable for the requested version, fail fast with an\n# actionable error rather than silently falling back to the legacy single-container\n# path — that path is permanently broken with the slim manager image and would just\n# leave the user with a manager container looping on \"Waiting for Higress Gateway\".\n# Sets EMBEDDED_IMAGE and AGENTTEAMS_USE_EMBEDDED.\nresolve_embedded_image() {\n AGENTTEAMS_USE_EMBEDDED=1\n\n # If the user explicitly overrode the image (e.g. `make install-embedded` passes\n # a locally-built tag), respect it as-is without any registry probe.\n if [ -n \"${AGENTTEAMS_INSTALL_EMBEDDED_IMAGE:-}\" ]; then\n EMBEDDED_IMAGE=\"${AGENTTEAMS_INSTALL_EMBEDDED_IMAGE}\"\n return 0\n fi\n\n local _versioned=\"${AGENTTEAMS_REGISTRY}/agentteams/agentteams-embedded:${AGENTTEAMS_VERSION}\"\n local _latest=\"${AGENTTEAMS_REGISTRY}/agentteams/agentteams-embedded:latest\"\n\n # Skip probe when AGENTTEAMS_VERSION is \"latest\" — no point trying the same tag twice.\n if [ \"${AGENTTEAMS_VERSION}\" = \"latest\" ]; then\n EMBEDDED_IMAGE=\"${_latest}\"\n return 0\n fi\n\n if ${DOCKER_CMD} pull \"${_versioned}\" >/dev/null 2>&1; then\n EMBEDDED_IMAGE=\"${_versioned}\"\n return 0\n fi\n\n # Versions before v1.1.0 predate hiclaw-embedded entirely — their manager image\n # bundled all infrastructure. Falling back to hiclaw-embedded:latest would\n # silently swap in the v1.1.0 architecture (embedded kube-apiserver) which\n # crashes under QEMU on Apple Silicon. Auto-activate legacy mode instead.\n if _ver_lt \"${AGENTTEAMS_VERSION}\" \"v1.1.0\"; then\n log \"INFO: ${AGENTTEAMS_VERSION} predates hiclaw-embedded; switching to legacy all-in-one manager architecture.\"\n log \"WARNING: Legacy all-in-one mode requires AGENTTEAMS_VERSION <= v1.0.9 (older bundled manager image).\"\n log \"WARNING: Newer slim manager images will hang on 'Waiting for Higress Gateway'.\"\n AGENTTEAMS_USE_EMBEDDED=0\n return 0\n fi\n\n if ${DOCKER_CMD} pull \"${_latest}\" >/dev/null 2>&1; then\n log \"embedded ${AGENTTEAMS_VERSION} not found, using latest\"\n EMBEDDED_IMAGE=\"${_latest}\"\n return 0\n fi\n\n # Explicit escape hatch — still honoured for edge cases.\n if [ \"${AGENTTEAMS_FORCE_LEGACY:-0}\" = \"1\" ]; then\n log \"WARNING: AGENTTEAMS_FORCE_LEGACY=1 — using legacy all-in-one manager architecture.\"\n log \"WARNING: This requires AGENTTEAMS_VERSION <= v1.0.9 (older bundled manager image).\"\n log \"WARNING: Newer slim manager images will hang on 'Waiting for Higress Gateway'.\"\n AGENTTEAMS_USE_EMBEDDED=0\n return 0\n fi\n\n error \"Embedded controller image is not available in the registry:\"\n error \" - tried: ${_versioned}\"\n error \" - tried: ${_latest}\"\n error \"\"\n error \"Embedded mode is the only supported architecture since PR #616.\"\n error \"How to resolve:\"\n error \" 1) Pin to a AGENTTEAMS_VERSION whose embedded image has been published, or\"\n error \" wait for the release pipeline to publish it.\"\n error \" 2) For a local build, run: make install-embedded\"\n error \" (builds and uses the local embedded image without touching the registry).\"\n error \" 3) Override with a custom image: AGENTTEAMS_INSTALL_EMBEDDED_IMAGE=... ./agentteams-install.sh\"\n exit 1\n}\n\n# ============================================================\n# Known models list — used to detect custom models during install\n# ============================================================\nKNOWN_MODELS=\"gpt-5.4 gpt-5.3-codex gpt-5-mini gpt-5-nano claude-opus-4-6 claude-sonnet-4-6 claude-haiku-4-5 qwen3.6-plus qwen3.5-plus deepseek-chat deepseek-reasoner kimi-k2.5 glm-5 MiniMax-M2.7 MiniMax-M2.7-highspeed MiniMax-M2.5\"\n\nis_known_model() {\n local model=\"$1\"\n for m in ${KNOWN_MODELS}; do\n [ \"${m}\" = \"${model}\" ] && return 0\n done\n return 1\n}\n\n# Prompt user for custom model parameters when model is not in the known list.\n# Sets: AGENTTEAMS_MODEL_CONTEXT_WINDOW, AGENTTEAMS_MODEL_MAX_TOKENS, AGENTTEAMS_MODEL_REASONING, AGENTTEAMS_MODEL_VISION\nprompt_custom_model_params() {\n local model=\"$1\"\n if is_known_model \"${model}\"; then\n # Clear any stale custom params for known models\n AGENTTEAMS_MODEL_CONTEXT_WINDOW=\"\"\n AGENTTEAMS_MODEL_MAX_TOKENS=\"\"\n AGENTTEAMS_MODEL_REASONING=\"\"\n AGENTTEAMS_MODEL_VISION=\"\"\n return\n fi\n echo \"\"\n log \"$(msg llm.custom_model.detected \"${model}\")\"\n echo \"\"\n read -e -p \" $(msg llm.custom_model.context_prompt): \" AGENTTEAMS_MODEL_CONTEXT_WINDOW\n if [ \"${AGENTTEAMS_MODEL_CONTEXT_WINDOW}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n AGENTTEAMS_MODEL_CONTEXT_WINDOW=\"${AGENTTEAMS_MODEL_CONTEXT_WINDOW:-150000}\"\n read -e -p \" $(msg llm.custom_model.max_tokens_prompt): \" AGENTTEAMS_MODEL_MAX_TOKENS\n if [ \"${AGENTTEAMS_MODEL_MAX_TOKENS}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n AGENTTEAMS_MODEL_MAX_TOKENS=\"${AGENTTEAMS_MODEL_MAX_TOKENS:-128000}\"\n read -e -p \" $(msg llm.custom_model.reasoning_prompt): \" _reasoning\n if [ \"${_reasoning}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n case \"${_reasoning}\" in\n n|N|no|NO) AGENTTEAMS_MODEL_REASONING=\"false\" ;;\n *) AGENTTEAMS_MODEL_REASONING=\"true\" ;;\n esac\n read -e -p \" $(msg llm.custom_model.vision_prompt): \" _vision\n if [ \"${_vision}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n case \"${_vision}\" in\n y|Y|yes|YES) AGENTTEAMS_MODEL_VISION=\"true\" ;;\n *) AGENTTEAMS_MODEL_VISION=\"false\" ;;\n esac\n log \"$(msg llm.custom_model.summary \"${AGENTTEAMS_MODEL_CONTEXT_WINDOW}\" \"${AGENTTEAMS_MODEL_MAX_TOKENS}\" \"${AGENTTEAMS_MODEL_REASONING}\" \"${AGENTTEAMS_MODEL_VISION}\")\"\n}\n\n# ============================================================\n# Wait for Manager agent to be ready\n# Uses `openclaw gateway health` inside the container to confirm the gateway is running\n# ============================================================\n\nwait_manager_ready() {\n local timeout=\"${AGENTTEAMS_READY_TIMEOUT:-300}\"\n local elapsed=0\n local container=\"${1:-agentteams-manager}\"\n\n log \"$(msg install.wait_ready \"${timeout}\")\"\n\n # Wait for Manager agent to be healthy inside the container\n local runtime=\"${AGENTTEAMS_MANAGER_RUNTIME:-copaw}\"\n while [ \"${elapsed}\" -lt \"${timeout}\" ]; do\n case \"${runtime}\" in\n copaw)\n if ${DOCKER_CMD} exec \"${container}\" curl -sf http://127.0.0.1:18799/api/agents 2>/dev/null | grep -q '\"agents\"'; then\n log \"$(msg install.wait_ready.ok)\"\n return 0\n fi\n ;;\n *)\n if ${DOCKER_CMD} exec \"${container}\" openclaw gateway health --json 2>/dev/null | grep -q '\"ok\"' 2>/dev/null; then\n log \"$(msg install.wait_ready.ok)\"\n return 0\n fi\n ;;\n esac\n sleep 5\n elapsed=$((elapsed + 5))\n printf \"\\r\\033[36m[AgentTeams]\\033[0m $(msg install.wait_ready.waiting \"${elapsed}\" \"${timeout}\")\"\n done\n\n echo \"\"\n die \"$(msg install.wait_ready.timeout \"${timeout}\" \"${container}\")\"\n}\n\nwait_matrix_ready() {\n local timeout=\"${AGENTTEAMS_READY_TIMEOUT:-300}\"\n local elapsed=0\n local container=\"${1:-agentteams-manager}\"\n\n log \"$(msg install.wait_matrix \"${timeout}\")\"\n\n while [ \"${elapsed}\" -lt \"${timeout}\" ]; do\n if ${DOCKER_CMD} exec \"${container}\" curl -sf http://127.0.0.1:6167/_tuwunel/server_version >/dev/null 2>&1; then\n log \"$(msg install.wait_matrix.ok)\"\n return 0\n fi\n sleep 5\n elapsed=$((elapsed + 5))\n printf \"\\r\\033[36m[AgentTeams]\\033[0m $(msg install.wait_matrix.waiting \"${elapsed}\" \"${timeout}\")\"\n done\n\n echo \"\"\n die \"$(msg install.wait_matrix.timeout \"${timeout}\" \"${container}\")\"\n}\n\n# Read KEY=value from /data/agentteams-secrets.env on a Docker volume (manager container not required).\n# Requires EMBEDDED_IMAGE (resolved earlier in install_manager). Uses ${DOCKER_CMD}.\nagentteams_read_secret_from_data_volume() {\n local _vol=\"$1\" _key=\"$2\"\n if [ -z \"${_vol}\" ] || [ -z \"${_key}\" ] || [ -z \"${EMBEDDED_IMAGE:-}\" ]; then\n echo \"\"\n return 0\n fi\n ${DOCKER_CMD} run --rm --entrypoint sh \\\n -v \"${_vol}:/data:ro\" \\\n \"${EMBEDDED_IMAGE}\" -c \"grep \\\"^${_key}=\\\" /data/agentteams-secrets.env /data/hiclaw-secrets.env 2>/dev/null | cut -d= -f2- | head -1 | tr -d '\\r'\" 2>/dev/null\n}\n\n# Read KEY=value from /data/worker-creds/<worker>.env on a Docker volume.\nagentteams_read_worker_creds_value_from_volume() {\n local _vol=\"$1\" _worker=\"$2\" _key=\"$3\"\n if [ -z \"${_vol}\" ] || [ -z \"${_worker}\" ] || [ -z \"${_key}\" ] || [ -z \"${EMBEDDED_IMAGE:-}\" ]; then\n echo \"\"\n return 0\n fi\n ${DOCKER_CMD} run --rm --entrypoint sh \\\n -v \"${_vol}:/data:ro\" \\\n \"${EMBEDDED_IMAGE}\" -c \"grep \\\"^${_key}=\\\" \\\"/data/worker-creds/${_worker}.env\\\" 2>/dev/null | cut -d= -f2- | head -1 | tr -d \\\"\\\\r\\\"\" 2>/dev/null\n}\n\n# Read admin_dm_room_id from host workspace state.json (fallback when Matrix API is unavailable).\nagentteams_read_admin_dm_room_from_workspace() {\n local _ws=\"$1\"\n local _f=\"${_ws}/state.json\"\n if [ ! -f \"${_f}\" ] || ! command -v jq >/dev/null 2>&1; then\n echo \"\"\n return 0\n fi\n jq -r '.admin_dm_room_id // empty | select(. != \"null\")' \"${_f}\" 2>/dev/null\n}\n\n# Read secret input with masked echo (shows * per keystroke, supports backspace)\n# Usage: read_secret \"prompt text: \"; value=\"${_RS_RESULT}\"\nread_secret() {\n local _rs_prompt=\"$1\"\n _RS_RESULT=\"\"\n local _rs_char=\"\"\n\n printf \"%s\" \"${_rs_prompt}\"\n\n while IFS= read -r -s -n 1 _rs_char; do\n if [[ -z \"${_rs_char}\" ]]; then\n break\n elif [[ \"${_rs_char}\" == $'\\177' ]] || [[ \"${_rs_char}\" == $'\\b' ]]; then\n if [ -n \"${_RS_RESULT}\" ]; then\n _RS_RESULT=\"${_RS_RESULT%?}\"\n printf \"\\b \\b\"\n fi\n else\n _RS_RESULT=\"${_RS_RESULT}${_rs_char}\"\n printf \"*\"\n fi\n done\n echo\n}\n\n# Load current parameter values from env file for upgrade mode display\nload_current_params_from_env() {\n local env_file=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n if [ -f \"${env_file}\" ]; then\n [ -z \"${AGENTTEAMS_LLM_PROVIDER:+x}\" ] && AGENTTEAMS_LLM_PROVIDER=\"$(grep '^AGENTTEAMS_LLM_PROVIDER=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_OPENAI_BASE_URL:+x}\" ] && AGENTTEAMS_OPENAI_BASE_URL=\"$(grep '^AGENTTEAMS_OPENAI_BASE_URL=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_DEFAULT_MODEL:+x}\" ] && AGENTTEAMS_DEFAULT_MODEL=\"$(grep '^AGENTTEAMS_DEFAULT_MODEL=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_EMBEDDING_MODEL:+x}\" ] && AGENTTEAMS_EMBEDDING_MODEL=\"$(grep '^AGENTTEAMS_EMBEDDING_MODEL=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_WORKSPACE_DIR:+x}\" ] && AGENTTEAMS_WORKSPACE_DIR=\"$(grep '^AGENTTEAMS_WORKSPACE_DIR=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_HOST_SHARE_DIR:+x}\" ] && AGENTTEAMS_HOST_SHARE_DIR=\"$(grep '^AGENTTEAMS_HOST_SHARE_DIR=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_PODMAN_AUTOSTART:+x}\" ] && AGENTTEAMS_PODMAN_AUTOSTART=\"$(grep '^AGENTTEAMS_PODMAN_AUTOSTART=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n fi\n}\n\n# In non-interactive mode, uses default or errors if required and no default.\n# Usage: prompt VAR_NAME \"Prompt text\" \"default\" [true=secret]\nprompt() {\n local var_name=\"$1\"\n local prompt_text=\"$2\"\n local default_value=\"$3\"\n local is_secret=\"${4:-false}\"\n\n # If the variable is already set in the environment, use it silently\n # In upgrade mode, show current value and let user change it\n eval \"local current_value=\\\"\\${${var_name}}\\\"\"\n if [ -n \"${current_value}\" ]; then\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_NON_INTERACTIVE}\" != \"1\" ]; then\n # Show masked value for secrets, full value otherwise\n local display_value=\"${current_value}\"\n if [ \"${is_secret}\" = \"true\" ]; then\n local len=${#current_value}\n if [ \"${len}\" -le 8 ]; then\n display_value=\"****\"\n else\n display_value=\"${current_value:0:4}****${current_value: -4}\"\n fi\n fi\n log \"$(msg prompt.upgrade_keep \"${prompt_text}\" \"${display_value}\")\"\n local new_value=\"\"\n if [ \"${is_secret}\" = \"true\" ]; then\n read_secret \"${prompt_text}: \"\n new_value=\"${_RS_RESULT}\"\n else\n read -e -p \"${prompt_text}: \" new_value\n if [ \"${new_value}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n fi\n if [ -n \"${new_value}\" ]; then\n eval \"export ${var_name}='${new_value}'\"\n fi\n return\n fi\n log \"$(msg prompt.preset \"${prompt_text}\")\"\n return\n fi\n\n # Non-interactive or quickstart: use default or error\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] || [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ]; then\n if [ -n \"${default_value}\" ]; then\n eval \"export ${var_name}='${default_value}'\"\n log \"$(msg prompt.default \"${prompt_text}\" \"${default_value}\")\"\n return\n elif [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n # Only hard-error in fully non-interactive mode, not quickstart\n die \"$(msg prompt.required \"${prompt_text}\")\"\n fi\n # quickstart + no default: fall through to interactive prompt below\n fi\n\n if [ -n \"${default_value}\" ]; then\n prompt_text=\"${prompt_text} [${default_value}]\"\n fi\n\n local value=\"\"\n if [ \"${is_secret}\" = \"true\" ]; then\n read_secret \"${prompt_text}: \"\n value=\"${_RS_RESULT}\"\n else\n read -e -p \"${prompt_text}: \" value\n if [ \"${value}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n fi\n\n value=\"${value:-${default_value}}\"\n if [ -z \"${value}\" ]; then\n die \"$(msg prompt.required_empty \"${prompt_text}\")\"\n fi\n\n eval \"export ${var_name}='${value}'\"\n}\n\n# Prompt for an optional value (empty string is acceptable)\n# Skips prompt if variable is already defined in environment (even if empty)\n# In upgrade mode, shows current value and lets user change it.\n# In non-interactive mode, defaults to empty string.\nprompt_optional() {\n local var_name=\"$1\"\n local prompt_text=\"$2\"\n local is_secret=\"${3:-false}\"\n\n # Check if variable is defined (even if set to empty string)\n eval \"local _chk=\\\"\\${${var_name}+x}\\\"\"\n if [ -n \"${_chk}\" ]; then\n # In upgrade mode, show current value and let user change it\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_NON_INTERACTIVE}\" != \"1\" ]; then\n eval \"local current_value=\\\"\\${${var_name}}\\\"\"\n local display_value=\"${current_value}\"\n if [ \"${is_secret}\" = \"true\" ] && [ -n \"${current_value}\" ]; then\n local len=${#current_value}\n if [ \"${len}\" -le 8 ]; then\n display_value=\"****\"\n else\n display_value=\"${current_value:0:4}****${current_value: -4}\"\n fi\n fi\n if [ -n \"${current_value}\" ]; then\n if [ \"${is_secret}\" = \"true\" ]; then\n log \"$(msg prompt.upgrade_keep_secret \"${prompt_text}\" \"${display_value}\")\"\n else\n log \"$(msg prompt.upgrade_keep \"${prompt_text}\" \"${display_value}\")\"\n fi\n else\n log \"$(msg prompt.upgrade_empty \"${prompt_text}\")\"\n fi\n local new_value=\"\"\n if [ \"${is_secret}\" = \"true\" ]; then\n read_secret \"${prompt_text}: \"\n new_value=\"${_RS_RESULT}\"\n else\n read -e -p \"${prompt_text}: \" new_value\n if [ \"${new_value}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n fi\n if [ -n \"${new_value}\" ]; then\n eval \"export ${var_name}='${new_value}'\"\n fi\n return\n fi\n log \"$(msg prompt.preset \"${prompt_text}\")\"\n return\n fi\n\n # Non-interactive or quickstart: skip, leave unset\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] || [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ]; then\n eval \"export ${var_name}=''\"\n return\n fi\n\n local value=\"\"\n if [ \"${is_secret}\" = \"true\" ]; then\n read_secret \"${prompt_text}: \"\n value=\"${_RS_RESULT}\"\n else\n read -e -p \"${prompt_text}: \" value\n if [ \"${value}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n fi\n\n eval \"export ${var_name}='${value}'\"\n}\n\ngenerate_key() {\n openssl rand -hex 32\n}\n\n# ============================================================\n# Detect container runtime socket on the host\n#\n# Dependency: Requires DOCKER_CMD to reflect the TRUE underlying runtime.\n# (Crucial for unmasking 'podman' disguised as 'docker'.\n# Must be strictly resolved prior, e.g., via check_container_runtime)\n# ============================================================\ndetect_socket() {\n local socket_path\n\n # 1. Respect explicitly defined DOCKER_HOST environment variable\n if [ -n \"${DOCKER_HOST}\" ]; then\n socket_path=$(echo \"${DOCKER_HOST}\" | sed 's|^unix://||')\n if [ -S \"${socket_path}\" ]; then\n echo \"${socket_path}\"\n return 0\n fi\n fi\n\n # 2. Route based on the resolved container runtime\n case \"${DOCKER_CMD}\" in\n docker)\n # Docker path handling\n socket_path=$(docker context ls --format '{{if .Current}}{{.DockerEndpoint}}{{end}}' 2>/dev/null | grep . | sed 's|^unix://||')\n if [ -n \"${socket_path}\" ] && [ -S \"${socket_path}\" ]; then\n echo \"${socket_path}\"\n return 0\n fi\n\n # Rootless Docker\n if [ -n \"${XDG_RUNTIME_DIR:-}\" ] && [ -S \"${XDG_RUNTIME_DIR}/docker.sock\" ]; then\n echo \"${XDG_RUNTIME_DIR}/docker.sock\"\n return 0\n fi\n\n # Rootless Docker (macOS fallback)\n if [ -S \"${HOME}/.docker/run/docker.sock\" ]; then\n echo \"${HOME}/.docker/run/docker.sock\"\n return 0\n fi\n\n # Root Docker\n if [ -S \"/var/run/docker.sock\" ]; then\n echo \"/var/run/docker.sock\"\n return 0\n fi\n ;;\n podman)\n # Podman path handling\n if [ \"$(id -u)\" -eq 0 ]; then\n socket_path=\"/run/podman/podman.sock\"\n else\n socket_path=\"${XDG_RUNTIME_DIR}/podman/podman.sock\"\n fi\n if [ -S \"${socket_path}\" ]; then\n echo \"${socket_path}\"\n return 0\n fi\n ;;\n esac\n\n # Return empty if no socket is found\n echo \"\"\n return 0\n}\n\n# Ensure Podman API socket is active (State mutation)\nensure_podman_socket() {\n local socket_path\n\n # Only applicable if runtime is podman and systemctl is available\n if [ \"${DOCKER_CMD:-}\" != \"podman\" ] || ! command -v systemctl >/dev/null 2>&1; then\n return 0\n fi\n\n echo \"Ensuring Podman API socket is active...\" >&2\n\n # Enable and start the socket based on user privileges\n if [ \"$(id -u)\" -eq 0 ]; then\n socket_path=\"/run/podman/podman.sock\"\n systemctl enable --now podman.socket >/dev/null 2>&1 || true\n else\n socket_path=\"${XDG_RUNTIME_DIR}/podman/podman.sock\"\n systemctl --user enable --now podman.socket >/dev/null 2>&1 || true\n fi\n\n # Give systemd a brief moment to assert the socket for root or rootless user\n local _i\n for _i in 1 2 3; do\n [ -S \"${socket_path}\" ] && break\n sleep 1\n done\n}\n\n# Detect local LAN IP address (cross-platform: macOS and Linux)\ndetect_lan_ip() {\n local ip=\"\"\n\n # macOS: try common Wi-Fi / Ethernet interfaces\n if command -v ipconfig >/dev/null 2>&1; then\n for iface in en0 en1 en2 en3 en4; do\n ip=$(ipconfig getifaddr \"${iface}\" 2>/dev/null)\n if [ -n \"${ip}\" ]; then\n echo \"${ip}\"\n return 0\n fi\n done\n fi\n\n # Linux: ip route — most reliable\n if command -v ip >/dev/null 2>&1; then\n ip=$(ip route get 1.1.1.1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i==\"src\"){print $(i+1); exit}}')\n if [ -n \"${ip}\" ]; then\n echo \"${ip}\"\n return 0\n fi\n fi\n\n # Linux fallback: hostname -I (space-separated list, take first non-loopback)\n if command -v hostname >/dev/null 2>&1; then\n ip=$(hostname -I 2>/dev/null | tr ' ' '\\n' | grep -v '^127\\.' | grep -v '^::' | head -1)\n if [ -n \"${ip}\" ]; then\n echo \"${ip}\"\n return 0\n fi\n fi\n\n # Last resort: ifconfig\n if command -v ifconfig >/dev/null 2>&1; then\n ip=$(ifconfig 2>/dev/null | awk '/inet /{if($2!~/^127\\./){print $2; exit}}')\n # Strip \"addr:\" prefix that some ifconfig versions add\n ip=\"${ip#addr:}\"\n if [ -n \"${ip}\" ]; then\n echo \"${ip}\"\n return 0\n fi\n fi\n\n echo \"\"\n}\n\n\n# ============================================================\n# Step-back navigation helpers\n# ============================================================\n\n# should_skip_step: returns 0 (skip) when the step is irrelevant in current mode\nshould_skip_step() {\n local step_fn=\"$1\"\n case \"${step_fn}\" in\n step_lang|step_mode)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n ;;\n step_version)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n ;;\n step_existing)\n local _env=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n [ ! -f \"${_env}\" ] && return 0\n ;;\n # Keep-All upgrade mode: skip all config steps (step_volume/step_workspace handled separately)\n step_llm|step_admin|step_network|step_ports|step_domains|step_github|step_skills|step_runtime|step_manager_runtime|step_e2ee|step_docker_proxy|step_idle|step_hostshare)\n [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ] && return 0\n ;;\n step_volume|step_workspace)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ] && return 0\n [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ] && return 0\n ;;\n step_e2ee|step_idle)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE}\" != \"1\" ] && return 0\n ;;\n step_docker_proxy)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE}\" != \"1\" ] && return 0\n # Embedded mode handles docker access natively — skip this step\n [ \"${AGENTTEAMS_USE_EMBEDDED:-}\" = \"1\" ] && return 0\n ;;\n step_manager_runtime)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n ;;\n step_hostshare)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ] && return 0\n ;;\n step_podman_autostart)\n # Only relevant for Podman with systemd\n [ \"${DOCKER_CMD}\" != \"podman\" ] && return 0\n ! command -v systemctl >/dev/null 2>&1 && return 0\n\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE}\" != \"1\" ] && return 0\n [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ] && return 0\n ;;\n esac\n return 1\n}\n\n# clear_step_vars: unset variables set by a step so it will re-prompt on re-entry\nclear_step_vars() {\n local step_fn=\"$1\"\n case \"${step_fn}\" in\n step_mode) unset AGENTTEAMS_QUICKSTART ;;\n step_version) unset AGENTTEAMS_VERSION ;;\n step_existing) unset AGENTTEAMS_UPGRADE UPGRADE_EXISTING_WORKERS ;;\n step_llm)\n unset AGENTTEAMS_LLM_PROVIDER AGENTTEAMS_DEFAULT_MODEL AGENTTEAMS_OPENAI_BASE_URL\n unset AGENTTEAMS_LLM_API_KEY AGENTTEAMS_MODEL_CONTEXT_WINDOW AGENTTEAMS_MODEL_MAX_TOKENS\n unset AGENTTEAMS_MODEL_REASONING AGENTTEAMS_MODEL_VISION\n ;;\n step_admin) unset AGENTTEAMS_ADMIN_USER AGENTTEAMS_ADMIN_PASSWORD ;;\n step_network) unset AGENTTEAMS_LOCAL_ONLY ;;\n step_ports)\n unset AGENTTEAMS_PORT_GATEWAY AGENTTEAMS_PORT_CONSOLE\n unset AGENTTEAMS_PORT_ELEMENT_WEB AGENTTEAMS_PORT_MANAGER_CONSOLE\n ;;\n step_domains)\n unset AGENTTEAMS_MATRIX_DOMAIN AGENTTEAMS_MATRIX_CLIENT_DOMAIN\n unset AGENTTEAMS_AI_GATEWAY_DOMAIN AGENTTEAMS_FS_DOMAIN AGENTTEAMS_CONSOLE_DOMAIN\n ;;\n step_github) unset AGENTTEAMS_GITHUB_TOKEN ;;\n step_skills) unset AGENTTEAMS_SKILLS_API_URL ;;\n step_volume) unset AGENTTEAMS_DATA_DIR ;;\n step_workspace) unset AGENTTEAMS_WORKSPACE_DIR ;;\n step_runtime) unset AGENTTEAMS_DEFAULT_WORKER_RUNTIME ;;\n step_manager_runtime) unset AGENTTEAMS_MANAGER_RUNTIME ;;\n step_e2ee) unset AGENTTEAMS_MATRIX_E2EE ;;\n step_docker_proxy) unset AGENTTEAMS_DOCKER_PROXY; unset AGENTTEAMS_PROXY_ALLOWED_REGISTRIES ;;\n step_idle) unset AGENTTEAMS_WORKER_IDLE_TIMEOUT ;;\n step_hostshare) unset AGENTTEAMS_HOST_SHARE_DIR ;;\n step_podman_autostart) unset AGENTTEAMS_PODMAN_AUTOSTART ;;\n esac\n}\n\n# ============================================================\n# Individual step functions\n# ============================================================\n\nstep_lang() {\n local lang_default_choice=\"2\"\n [ \"${AGENTTEAMS_LANGUAGE}\" = \"zh\" ] && lang_default_choice=\"1\"\n log \"$(msg lang.detected)\"\n log \"$(msg lang.switch_title)\"\n echo \"$(msg lang.option_zh)\"\n echo \"$(msg lang.option_en)\"\n echo \"\"\n local LANG_CHOICE\n read -e -p \"$(msg lang.prompt) [${lang_default_choice}]: \" LANG_CHOICE\n LANG_CHOICE=\"${LANG_CHOICE:-${lang_default_choice}}\"\n if [ \"${LANG_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n case \"${LANG_CHOICE}\" in\n 1) AGENTTEAMS_LANGUAGE=\"zh\" ;;\n 2) AGENTTEAMS_LANGUAGE=\"en\" ;;\n esac\n export AGENTTEAMS_LANGUAGE\n log \"\"\n}\n\nstep_mode() {\n log \"$(msg install.mode.title)\"\n echo \"\"\n echo \"$(msg install.mode.choose)\"\n echo \"$(msg install.mode.quickstart)\"\n echo \"$(msg install.mode.manual)\"\n echo \"\"\n local ONBOARDING_CHOICE\n read -e -p \"$(msg install.mode.prompt): \" ONBOARDING_CHOICE\n ONBOARDING_CHOICE=\"${ONBOARDING_CHOICE:-1}\"\n if [ \"${ONBOARDING_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n case \"${ONBOARDING_CHOICE}\" in\n 1|quick|quickstart)\n log \"$(msg install.mode.quickstart_selected)\"\n AGENTTEAMS_QUICKSTART=1\n ;;\n 2|manual)\n log \"$(msg install.mode.manual_selected)\"\n ;;\n *)\n log \"$(msg install.mode.invalid)\"\n AGENTTEAMS_QUICKSTART=1\n ;;\n esac\n log \"\"\n}\n\nstep_version() {\n # Skip if version already provided via env var\n if [ -n \"${AGENTTEAMS_VERSION}\" ]; then\n resolve_image_tags\n return 0\n fi\n # Try to fetch the latest stable release from GitHub\n log \"$(msg install.version.fetching)\"\n local _fetched\n _fetched=$(curl -sf --max-time 5 \\\n -H \"Accept: application/vnd.github+json\" \\\n \"https://api.github.com/repos/agentscope-ai/AgentTeams/releases/latest\" \\\n 2>/dev/null | grep '\"tag_name\"' | head -1 | sed 's/.*\"tag_name\": *\"\\([^\"]*\\)\".*/\\1/')\n if [ -n \"${_fetched}\" ]; then\n AGENTTEAMS_KNOWN_STABLE_VERSION=\"${_fetched}\"\n else\n log \"$(msg install.version.fetch_failed \"${AGENTTEAMS_KNOWN_STABLE_VERSION}\")\"\n fi\n log \"$(msg install.version.title)\"\n echo \"\"\n echo \"$(msg install.version.choose)\"\n echo \"$(msg install.version.option_latest)\"\n printf \"%s\\n\" \"$(msg install.version.option_stable \"${AGENTTEAMS_KNOWN_STABLE_VERSION}\")\"\n echo \"$(msg install.version.option_custom)\"\n echo \"\"\n local VERSION_CHOICE\n read -e -p \"$(msg install.version.prompt) [1]: \" VERSION_CHOICE\n VERSION_CHOICE=\"${VERSION_CHOICE:-1}\"\n if [ \"${VERSION_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n case \"${VERSION_CHOICE}\" in\n 1|latest)\n AGENTTEAMS_VERSION=\"latest\"\n log \"$(msg install.version.selected_latest)\"\n ;;\n 2|stable)\n AGENTTEAMS_VERSION=\"${AGENTTEAMS_KNOWN_STABLE_VERSION}\"\n log \"$(msg install.version.selected_stable \"${AGENTTEAMS_VERSION}\")\"\n ;;\n 3|custom)\n local CUSTOM_VERSION\n read -e -p \"$(msg install.version.custom_prompt): \" CUSTOM_VERSION\n AGENTTEAMS_VERSION=\"${CUSTOM_VERSION:-${AGENTTEAMS_KNOWN_STABLE_VERSION}}\"\n log \"$(msg install.version.selected_custom \"${AGENTTEAMS_VERSION}\")\"\n ;;\n *)\n AGENTTEAMS_VERSION=\"${AGENTTEAMS_KNOWN_STABLE_VERSION}\"\n log \"$(msg install.version.invalid \"${AGENTTEAMS_VERSION}\")\"\n ;;\n esac\n log \"\"\n resolve_image_tags\n}\n\nstep_existing() {\n local existing_env=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n log \"$(msg install.existing.detected \"${existing_env}\")\"\n local running_manager=\"\" running_workers=\"\" existing_workers=\"\"\n if ${DOCKER_CMD} ps --format '{{.Names}}' | grep -q \"^agentteams-manager$\"; then\n running_manager=\"agentteams-manager\"\n fi\n running_workers=$(${DOCKER_CMD} ps --format '{{.Names}}' | grep \"^agentteams-worker-\" || true)\n existing_workers=$(${DOCKER_CMD} ps -a --format '{{.Names}}' | grep \"^agentteams-worker-\" || true)\n local UPGRADE_CHOICE\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n log \"$(msg install.existing.upgrade_noninteractive)\"\n UPGRADE_CHOICE=\"upgrade\"\n else\n echo \"\"\n echo \"$(msg install.existing.choose)\"\n echo \"$(msg install.existing.upgrade)\"\n echo \"$(msg install.existing.reinstall)\"\n echo \"$(msg install.existing.cancel)\"\n echo \"\"\n read -e -p \"$(msg install.existing.prompt): \" UPGRADE_CHOICE\n UPGRADE_CHOICE=\"${UPGRADE_CHOICE:-1}\"\n if [ \"${UPGRADE_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n fi\n case \"${UPGRADE_CHOICE}\" in\n 1|upgrade)\n AGENTTEAMS_UPGRADE=1\n log \"$(msg install.existing.upgrading)\"\n # Show upgrade mode sub-menu (unless already set via env var)\n if [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL:-0}\" != \"1\" ]; then\n echo \"\"\n echo \"$(msg upgrade.mode.prompt)\"\n echo \"$(msg upgrade.mode.keep_all)\"\n echo \"$(msg upgrade.mode.confirm_each)\"\n echo \"$(msg upgrade.mode.back)\"\n echo \"\"\n local UPGRADE_MODE_CHOICE\n read -e -p \"$(msg install.existing.prompt): \" UPGRADE_MODE_CHOICE\n UPGRADE_MODE_CHOICE=\"${UPGRADE_MODE_CHOICE:-1}\"\n case \"${UPGRADE_MODE_CHOICE}\" in\n 1|keep)\n AGENTTEAMS_UPGRADE_KEEP_ALL=1\n ;;\n 2|confirm)\n AGENTTEAMS_UPGRADE_KEEP_ALL=0\n ;;\n 3|b)\n STEP_RESULT=\"back\"\n return 0\n ;;\n *)\n AGENTTEAMS_UPGRADE_KEEP_ALL=0\n ;;\n esac\n fi\n # Load current parameters for both Keep-All and confirm-each modes\n load_current_params_from_env\n if [ -n \"${running_manager}\" ] || [ -n \"${running_workers}\" ]; then\n echo \"\"\n echo -e \"\\033[33m$(msg install.existing.warn_manager_stop)\\033[0m\"\n if [ -n \"${existing_workers}\" ]; then\n echo -e \"\\033[33m$(msg install.existing.warn_worker_recreate)\\033[0m\"\n fi\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" != \"1\" ]; then\n echo \"\"\n local CONFIRM_STOP\n read -e -p \"$(msg install.existing.continue_prompt): \" CONFIRM_STOP\n if [ \"${CONFIRM_STOP}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n if [ \"${CONFIRM_STOP}\" != \"y\" ] && [ \"${CONFIRM_STOP}\" != \"Y\" ]; then\n log \"$(msg install.existing.cancelled)\"\n exit 0\n fi\n fi\n fi\n UPGRADE_EXISTING_WORKERS=\"${existing_workers}\"\n ;;\n 2|reinstall)\n log \"$(msg install.reinstall.performing)\"\n local existing_workspace=\"\"\n if [ -f \"${existing_env}\" ]; then\n existing_workspace=$(grep '^AGENTTEAMS_WORKSPACE_DIR=' \"${existing_env}\" 2>/dev/null | cut -d= -f2-)\n fi\n [ -z \"${existing_workspace}\" ] && existing_workspace=\"${HOME}/agentteams-manager\"\n echo \"\"\n echo -e \"\\033[33m$(msg install.reinstall.warn_stop)\\033[0m\"\n [ -n \"${running_manager}\" ] && echo -e \"\\033[33m - ${running_manager} (manager)\\033[0m\"\n for w in ${running_workers}; do\n echo -e \"\\033[33m - ${w} (worker)\\033[0m\"\n done\n echo \"\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_delete)\\033[0m\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_volume)\\033[0m\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_env \"${existing_env}\")\\033[0m\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_workspace \"${existing_workspace}\")\\033[0m\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_workers)\\033[0m\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_proxy)\\033[0m\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_network)\\033[0m\"\n echo \"\"\n echo -e \"\\033[31m$(msg install.reinstall.confirm_type)\\033[0m\"\n echo -e \"\\033[31m ${existing_workspace}\\033[0m\"\n echo \"\"\n local CONFIRM_PATH\n read -e -p \"$(msg install.reinstall.confirm_path): \" CONFIRM_PATH\n if [ \"${CONFIRM_PATH}\" != \"${existing_workspace}\" ]; then\n die \"$(msg install.reinstall.path_mismatch \"${CONFIRM_PATH}\" \"${existing_workspace}\")\"\n fi\n log \"$(msg install.reinstall.confirmed)\"\n ${DOCKER_CMD} stop agentteams-manager 2>/dev/null || true\n ${DOCKER_CMD} rm agentteams-manager 2>/dev/null || true\n for w in $(${DOCKER_CMD} ps -a --format '{{.Names}}' | grep \"^agentteams-worker-\" || true); do\n ${DOCKER_CMD} stop \"${w}\" 2>/dev/null || true\n ${DOCKER_CMD} rm \"${w}\" 2>/dev/null || true\n log \"$(msg install.reinstall.removed_worker \"${w}\")\"\n done\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' | grep -q \"^agentteams-controller$\"; then\n log \"$(msg install.reinstall.removing_proxy)\"\n ${DOCKER_CMD} stop agentteams-controller 2>/dev/null || true\n ${DOCKER_CMD} rm agentteams-controller 2>/dev/null || true\n fi\n if ${DOCKER_CMD} network ls --format '{{.Name}}' | grep -q \"^agentteams-net$\"; then\n log \"$(msg install.reinstall.removing_network)\"\n ${DOCKER_CMD} network rm agentteams-net 2>/dev/null || true\n fi\n if ${DOCKER_CMD} volume ls -q | grep -q \"^agentteams-data$\"; then\n log \"$(msg install.reinstall.removing_volume)\"\n ${DOCKER_CMD} volume rm agentteams-data 2>/dev/null || log \"$(msg install.reinstall.warn_volume_fail)\"\n fi\n if [ -d \"${existing_workspace}\" ]; then\n log \"$(msg install.reinstall.removing_workspace \"${existing_workspace}\")\"\n rm -rf \"${existing_workspace}\" || die \"$(msg install.reinstall.failed_rm_workspace)\"\n fi\n if [ -f \"${existing_env}\" ]; then\n log \"$(msg install.reinstall.removing_env \"${existing_env}\")\"\n rm -f \"${existing_env}\"\n fi\n log \"$(msg install.reinstall.cleanup_done)\"\n unset AGENTTEAMS_WORKSPACE_DIR\n return 0\n ;;\n 3|cancel|*)\n log \"$(msg install.existing.cancelled)\"\n exit 0\n ;;\n esac\n # Load existing env file as fallback (shell env vars take priority)\n if [ -f \"${existing_env}\" ]; then\n log \"$(msg install.loading_config \"${existing_env}\")\"\n while IFS='=' read -r key value; do\n case \"${key}\" in \\#*|\"\") continue ;; esac\n value=\"${value%%#*}\"\n value=\"${value#\"${value%%[![:space:]]*}\"}\"\n value=\"${value%\"${value##*[![:space:]]}\"}\"\n eval \"_existing_val=\\\"\\${${key}+x}\\\"\"\n if [ -z \"${_existing_val}\" ]; then export \"${key}=${value}\"; fi\n done < \"${existing_env}\"\n fi\n}\n\nstep_llm() {\n log \"$(msg llm.title)\"\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n if [ \"${AGENTTEAMS_LANGUAGE}\" = \"zh\" ]; then\n AGENTTEAMS_LLM_PROVIDER=\"${AGENTTEAMS_LLM_PROVIDER:-openai-compat}\"\n AGENTTEAMS_DEFAULT_MODEL=\"${AGENTTEAMS_DEFAULT_MODEL:-qwen3.6-plus}\"\n AGENTTEAMS_OPENAI_BASE_URL=\"${AGENTTEAMS_OPENAI_BASE_URL:-https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1}\"\n log \"$(msg llm.provider.label \"${AGENTTEAMS_LLM_PROVIDER}\")\"\n log \"$(msg llm.openai.base_url_label \"${AGENTTEAMS_OPENAI_BASE_URL}\")\"\n else\n AGENTTEAMS_LLM_PROVIDER=\"${AGENTTEAMS_LLM_PROVIDER:-qwen}\"\n AGENTTEAMS_DEFAULT_MODEL=\"${AGENTTEAMS_DEFAULT_MODEL:-qwen3.6-plus}\"\n AGENTTEAMS_OPENAI_BASE_URL=\"${AGENTTEAMS_OPENAI_BASE_URL:-}\"\n log \"$(msg llm.provider.qwen_default \"${AGENTTEAMS_LLM_PROVIDER}\")\"\n fi\n log \"$(msg llm.model.default \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n prompt AGENTTEAMS_LLM_API_KEY \"$(msg llm.apikey_prompt)\" \"\" \"true\"\n AGENTTEAMS_EMBEDDING_MODEL=\"${AGENTTEAMS_EMBEDDING_MODEL-text-embedding-v4}\"\n export AGENTTEAMS_LLM_PROVIDER AGENTTEAMS_DEFAULT_MODEL AGENTTEAMS_EMBEDDING_MODEL\n [ -n \"${AGENTTEAMS_OPENAI_BASE_URL+x}\" ] && export AGENTTEAMS_OPENAI_BASE_URL\n return 0\n fi\n\n # Upgrade Keep-All mode: skip provider selection, use loaded values\n local SKIP_TO_EMBEDDING=0\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ] && [ -n \"${AGENTTEAMS_LLM_PROVIDER}\" ]; then\n log \"$(msg llm.provider.label \"${AGENTTEAMS_LLM_PROVIDER}\")\"\n log \"$(msg llm.openai.base_url_label \"${AGENTTEAMS_OPENAI_BASE_URL}\")\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n PROVIDER_CHOICE=\"${AGENTTEAMS_LLM_PROVIDER}\"\n SKIP_TO_EMBEDDING=1\n else\n echo \"\"\n echo \"$(msg llm.providers_title)\"\n echo \"$(msg llm.provider.alibaba)\"\n echo \"$(msg llm.provider.openai_compat)\"\n echo \"\"\n local PROVIDER_CHOICE\n # If upgrade mode with loaded provider, show current as default\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_LLM_PROVIDER}\" ]; then\n local _prov_display\n case \"${AGENTTEAMS_LLM_PROVIDER}\" in\n openai-compat) _prov_display=\"2\" ;;\n *) _prov_display=\"1\" ;;\n esac\n read -e -p \"$(msg llm.provider.select) [${_prov_display}]: \" PROVIDER_CHOICE\n PROVIDER_CHOICE=\"${PROVIDER_CHOICE:-${_prov_display}}\"\n elif [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ]; then\n read -e -p \"$(msg llm.provider.select) [1]: \" PROVIDER_CHOICE\n PROVIDER_CHOICE=\"${PROVIDER_CHOICE:-1}\"\n else\n read -e -p \"$(msg llm.provider.select): \" PROVIDER_CHOICE\n PROVIDER_CHOICE=\"${PROVIDER_CHOICE:-1}\"\n fi\n if [ \"${PROVIDER_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n fi\n local ALIBABA_MODEL_CHOICE=\"\"\n local ALIBABA_ACCESS=\"\"\n case \"${PROVIDER_CHOICE}\" in\n 1|alibaba-cloud|openai-compat)\n if [ \"${AGENTTEAMS_LANGUAGE}\" = \"en\" ]; then\n AGENTTEAMS_LLM_PROVIDER=\"openai-compat\"\n AGENTTEAMS_OPENAI_BASE_URL=\"https://dashscope-intl.aliyuncs.com/compatible-mode/v1\"\n ALIBABA_ACCESS=\"tokenplan\"\n echo \"\"\n echo \"$(msg llm.codingplan.models_title)\"\n echo \"$(msg llm.codingplan.model.qwen36plus)\"\n echo \"$(msg llm.codingplan.model.glm5)\"\n echo \"$(msg llm.codingplan.model.kimi)\"\n echo \"$(msg llm.codingplan.model.minimax)\"\n echo \"\"\n local CODINGPLAN_MODEL_CHOICE\n # If upgrade with loaded model, show current as default\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_DEFAULT_MODEL}\" ]; then\n local _model_default\n case \"${AGENTTEAMS_DEFAULT_MODEL}\" in\n qwen3.6-plus) _model_default=\"1\" ;;\n glm-5) _model_default=\"2\" ;;\n kimi-k2.5) _model_default=\"3\" ;;\n MiniMax-M2.5) _model_default=\"4\" ;;\n *) _model_default=\"1\" ;;\n esac\n read -e -p \"$(msg llm.codingplan.model.select) [${_model_default}]: \" CODINGPLAN_MODEL_CHOICE\n CODINGPLAN_MODEL_CHOICE=\"${CODINGPLAN_MODEL_CHOICE:-${_model_default}}\"\n elif [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ]; then\n read -e -p \"$(msg llm.codingplan.model.select) [1]: \" CODINGPLAN_MODEL_CHOICE\n CODINGPLAN_MODEL_CHOICE=\"${CODINGPLAN_MODEL_CHOICE:-1}\"\n else\n read -e -p \"$(msg llm.codingplan.model.select): \" CODINGPLAN_MODEL_CHOICE\n CODINGPLAN_MODEL_CHOICE=\"${CODINGPLAN_MODEL_CHOICE:-1}\"\n fi\n if [ \"${CODINGPLAN_MODEL_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n case \"${CODINGPLAN_MODEL_CHOICE}\" in\n 1|qwen3.6-plus) AGENTTEAMS_DEFAULT_MODEL=\"qwen3.6-plus\" ;;\n 2|glm-5) AGENTTEAMS_DEFAULT_MODEL=\"glm-5\" ;;\n 3|kimi-k2.5) AGENTTEAMS_DEFAULT_MODEL=\"kimi-k2.5\" ;;\n 4|MiniMax-M2.5) AGENTTEAMS_DEFAULT_MODEL=\"MiniMax-M2.5\" ;;\n *) AGENTTEAMS_DEFAULT_MODEL=\"qwen3.6-plus\" ;;\n esac\n log \"$(msg llm.provider.selected_codingplan)\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n else\n echo \"\"\n echo \"$(msg llm.alibaba.models_title)\"\n echo \"$(msg llm.alibaba.model.tokenplan)\"\n echo \"$(msg llm.alibaba.model.bailian)\"\n echo \"$(msg llm.alibaba.model.codingplan_legacy)\"\n echo \"\"\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_OPENAI_BASE_URL}\" ]; then\n # Determine alibaba access type from loaded URL\n local _access_default=\"1\"\n if echo \"${AGENTTEAMS_OPENAI_BASE_URL}\" | grep -q \"token-plan\"; then\n _access_default=\"1\"\n elif echo \"${AGENTTEAMS_OPENAI_BASE_URL}\" | grep -q \"dashscope\"; then\n if echo \"${AGENTTEAMS_OPENAI_BASE_URL}\" | grep -q \"intl\"; then\n _access_default=\"3\"\n else\n _access_default=\"2\"\n fi\n fi\n read -e -p \"$(msg llm.alibaba.model.select) [${_access_default}]: \" ALIBABA_MODEL_CHOICE\n ALIBABA_MODEL_CHOICE=\"${ALIBABA_MODEL_CHOICE:-${_access_default}}\"\n elif [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ]; then\n read -e -p \"$(msg llm.alibaba.model.select) [1]: \" ALIBABA_MODEL_CHOICE\n ALIBABA_MODEL_CHOICE=\"${ALIBABA_MODEL_CHOICE:-1}\"\n else\n read -e -p \"$(msg llm.alibaba.model.select): \" ALIBABA_MODEL_CHOICE\n ALIBABA_MODEL_CHOICE=\"${ALIBABA_MODEL_CHOICE:-1}\"\n fi\n if [ \"${ALIBABA_MODEL_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n case \"${ALIBABA_MODEL_CHOICE}\" in\n 1|token-plan|tokenplan)\n ALIBABA_ACCESS=\"tokenplan\"\n AGENTTEAMS_LLM_PROVIDER=\"openai-compat\"\n AGENTTEAMS_OPENAI_BASE_URL=\"https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1\"\n echo \"\"\n echo \"$(msg llm.codingplan.models_title)\"\n echo \"$(msg llm.codingplan.model.qwen36plus)\"\n echo \"$(msg llm.codingplan.model.glm5)\"\n echo \"$(msg llm.codingplan.model.kimi)\"\n echo \"$(msg llm.codingplan.model.minimax)\"\n echo \"\"\n local CODINGPLAN_MODEL_CHOICE\n # If upgrade with loaded model, show current as default\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_DEFAULT_MODEL}\" ]; then\n local _model_default\n case \"${AGENTTEAMS_DEFAULT_MODEL}\" in\n qwen3.6-plus) _model_default=\"1\" ;;\n glm-5) _model_default=\"2\" ;;\n kimi-k2.5) _model_default=\"3\" ;;\n MiniMax-M2.5) _model_default=\"4\" ;;\n *) _model_default=\"1\" ;;\n esac\n read -e -p \"$(msg llm.codingplan.model.select) [${_model_default}]: \" CODINGPLAN_MODEL_CHOICE\n CODINGPLAN_MODEL_CHOICE=\"${CODINGPLAN_MODEL_CHOICE:-${_model_default}}\"\n elif [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ]; then\n read -e -p \"$(msg llm.codingplan.model.select) [1]: \" CODINGPLAN_MODEL_CHOICE\n CODINGPLAN_MODEL_CHOICE=\"${CODINGPLAN_MODEL_CHOICE:-1}\"\n else\n read -e -p \"$(msg llm.codingplan.model.select): \" CODINGPLAN_MODEL_CHOICE\n CODINGPLAN_MODEL_CHOICE=\"${CODINGPLAN_MODEL_CHOICE:-1}\"\n fi\n if [ \"${CODINGPLAN_MODEL_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n case \"${CODINGPLAN_MODEL_CHOICE}\" in\n 1|qwen3.6-plus) AGENTTEAMS_DEFAULT_MODEL=\"qwen3.6-plus\" ;;\n 2|glm-5) AGENTTEAMS_DEFAULT_MODEL=\"glm-5\" ;;\n 3|kimi-k2.5) AGENTTEAMS_DEFAULT_MODEL=\"kimi-k2.5\" ;;\n 4|MiniMax-M2.5) AGENTTEAMS_DEFAULT_MODEL=\"MiniMax-M2.5\" ;;\n *) AGENTTEAMS_DEFAULT_MODEL=\"qwen3.6-plus\" ;;\n esac\n log \"$(msg llm.provider.selected_tokenplan)\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n ;;\n 2|qwen|bailian)\n ALIBABA_ACCESS=\"bailian\"\n AGENTTEAMS_LLM_PROVIDER=\"qwen\"\n AGENTTEAMS_OPENAI_BASE_URL=\"\"\n echo \"\"\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_DEFAULT_MODEL}\" ]; then\n read -e -p \"$(msg llm.qwen.model_prompt) [${AGENTTEAMS_DEFAULT_MODEL}]: \" AGENTTEAMS_DEFAULT_MODEL\n else\n read -e -p \"$(msg llm.qwen.model_prompt) [qwen3.6-plus]: \" AGENTTEAMS_DEFAULT_MODEL\n fi\n if [ \"${AGENTTEAMS_DEFAULT_MODEL}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_DEFAULT_MODEL=\"${AGENTTEAMS_DEFAULT_MODEL:-qwen3.6-plus}\"\n log \"$(msg llm.provider.selected_qwen)\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n prompt_custom_model_params \"${AGENTTEAMS_DEFAULT_MODEL}\" || return 0\n ;;\n 3|coding-plan|codingplan)\n ALIBABA_ACCESS=\"codingplan_legacy\"\n AGENTTEAMS_LLM_PROVIDER=\"openai-compat\"\n AGENTTEAMS_OPENAI_BASE_URL=\"https://coding.dashscope.aliyuncs.com/v1\"\n echo \"\"\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_DEFAULT_MODEL}\" ]; then\n read -e -p \"$(msg llm.qwen.model_prompt) [${AGENTTEAMS_DEFAULT_MODEL}]: \" AGENTTEAMS_DEFAULT_MODEL\n else\n read -e -p \"$(msg llm.qwen.model_prompt): \" AGENTTEAMS_DEFAULT_MODEL\n fi\n if [ \"${AGENTTEAMS_DEFAULT_MODEL}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_DEFAULT_MODEL=\"${AGENTTEAMS_DEFAULT_MODEL:-qwen3.6-plus}\"\n log \"$(msg llm.provider.selected_codingplan_legacy)\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n prompt_custom_model_params \"${AGENTTEAMS_DEFAULT_MODEL}\" || return 0\n ;;\n *)\n die \"$(msg llm.alibaba.model.invalid \"${ALIBABA_MODEL_CHOICE}\")\"\n ;;\n esac\n fi\n if [ \"${ALIBABA_ACCESS}\" = \"bailian\" ]; then\n log \"$(msg llm.apikey_hint_bailian)\"\n log \"$(msg llm.apikey_url_bailian)\"\n elif [ \"${AGENTTEAMS_LANGUAGE}\" = \"en\" ]; then\n log \"$(msg llm.apikey_hint_qwencloud)\"\n log \"$(msg llm.apikey_url_qwencloud)\"\n elif [ \"${ALIBABA_ACCESS}\" = \"codingplan_legacy\" ]; then\n log \"$(msg llm.apikey_hint_codingplan)\"\n log \"$(msg llm.apikey_url_codingplan)\"\n else\n log \"$(msg llm.apikey_hint_tokenplan)\"\n log \"$(msg llm.apikey_url_tokenplan)\"\n fi\n log \"\"\n prompt AGENTTEAMS_LLM_API_KEY \"$(msg llm.apikey_prompt)\" \"\" \"true\" || return 0\n if [ \"${ALIBABA_ACCESS}\" = \"bailian\" ]; then\n test_llm_connectivity \"https://dashscope.aliyuncs.com/compatible-mode/v1\" \"${AGENTTEAMS_LLM_API_KEY}\" \"${AGENTTEAMS_DEFAULT_MODEL}\" || return 0\n elif [ \"${ALIBABA_ACCESS}\" = \"codingplan_legacy\" ]; then\n test_llm_connectivity \"https://coding.dashscope.aliyuncs.com/v1\" \"${AGENTTEAMS_LLM_API_KEY}\" \"${AGENTTEAMS_DEFAULT_MODEL}\" \"$(msg llm.openai.test.fail.codingplan_legacy)\" || return 0\n elif [ \"${AGENTTEAMS_LANGUAGE}\" = \"en\" ]; then\n test_llm_connectivity \"${AGENTTEAMS_OPENAI_BASE_URL}\" \"${AGENTTEAMS_LLM_API_KEY}\" \"${AGENTTEAMS_DEFAULT_MODEL}\" \"$(msg llm.openai.test.fail.codingplan)\" || return 0\n else\n test_llm_connectivity \"${AGENTTEAMS_OPENAI_BASE_URL}\" \"${AGENTTEAMS_LLM_API_KEY}\" \"${AGENTTEAMS_DEFAULT_MODEL}\" \"$(msg llm.openai.test.fail.tokenplan)\" || return 0\n fi\n ;;\n 2|openai-compat)\n AGENTTEAMS_LLM_PROVIDER=\"openai-compat\"\n log \"$(msg llm.provider.selected_openai \"${AGENTTEAMS_LLM_PROVIDER}\")\"\n echo \"\"\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg llm.openai.base_url_prompt)\" \"${AGENTTEAMS_OPENAI_BASE_URL}\")\"\n else\n _current=\"${AGENTTEAMS_OPENAI_BASE_URL}\"\n if [ -n \"${_current}\" ]; then\n read -e -p \"$(msg llm.openai.base_url_prompt) [${_current}]: \" AGENTTEAMS_OPENAI_BASE_URL\n else\n read -e -p \"$(msg llm.openai.base_url_prompt) [https://api.openai.com/v1]: \" AGENTTEAMS_OPENAI_BASE_URL\n fi\n if [ \"${AGENTTEAMS_OPENAI_BASE_URL}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_OPENAI_BASE_URL=\"${AGENTTEAMS_OPENAI_BASE_URL:-${_current}}\"\n [ -z \"${AGENTTEAMS_OPENAI_BASE_URL}\" ] && AGENTTEAMS_OPENAI_BASE_URL=\"https://api.openai.com/v1\"\n fi\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg llm.openai.model_prompt)\" \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n else\n _current=\"${AGENTTEAMS_DEFAULT_MODEL}\"\n if [ -n \"${_current}\" ]; then\n read -e -p \"$(msg llm.openai.model_prompt) [${_current}]: \" AGENTTEAMS_DEFAULT_MODEL\n else\n read -e -p \"$(msg llm.openai.model_prompt) [gpt-5.4]: \" AGENTTEAMS_DEFAULT_MODEL\n fi\n if [ \"${AGENTTEAMS_DEFAULT_MODEL}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_DEFAULT_MODEL=\"${AGENTTEAMS_DEFAULT_MODEL:-${_current}}\"\n [ -z \"${AGENTTEAMS_DEFAULT_MODEL}\" ] && AGENTTEAMS_DEFAULT_MODEL=\"gpt-5.4\"\n fi\n log \"$(msg llm.openai.base_url_label \"${AGENTTEAMS_OPENAI_BASE_URL}\")\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n prompt_custom_model_params \"${AGENTTEAMS_DEFAULT_MODEL}\" || return 0\n log \"\"\n prompt AGENTTEAMS_LLM_API_KEY \"$(msg llm.apikey_prompt)\" \"\" \"true\" || return 0\n test_llm_connectivity \"${AGENTTEAMS_OPENAI_BASE_URL}\" \"${AGENTTEAMS_LLM_API_KEY}\" \"${AGENTTEAMS_DEFAULT_MODEL}\" || return 0\n ;;\n *)\n die \"$(msg llm.provider.invalid \"${PROVIDER_CHOICE}\")\"\n ;;\n esac\n\n # Skip to embedding if Keep-All mode handled LLM params\n if [ \"${SKIP_TO_EMBEDDING}\" = \"1\" ]; then\n if [ -n \"${AGENTTEAMS_EMBEDDING_MODEL}\" ]; then\n log \"$(msg llm.embedding.title)\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_EMBEDDING_MODEL}\")\"\n log \"\"\n fi\n return 0\n fi\n\n # --- Embedding model (optional, auto-tested) ---\n echo \"\"\n log \"$(msg llm.embedding.title)\"\n log \"$(msg llm.embedding.hint)\"\n echo \"\"\n echo \"$(msg llm.embedding.option.default)\"\n echo \"$(msg llm.embedding.option.custom)\"\n echo \"$(msg llm.embedding.option.disable)\"\n echo \"\"\n local EMB_CHOICE\n read -e -p \"$(msg llm.embedding.select) [1]: \" EMB_CHOICE\n EMB_CHOICE=\"${EMB_CHOICE:-1}\"\n if [ \"${EMB_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n\n case \"${EMB_CHOICE}\" in\n 1)\n AGENTTEAMS_EMBEDDING_MODEL=\"text-embedding-v4\"\n ;;\n 2)\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg llm.embedding.custom_prompt)\" \"${AGENTTEAMS_EMBEDDING_MODEL}\")\"\n else\n _current=\"${AGENTTEAMS_EMBEDDING_MODEL}\"\n if [ -n \"${_current}\" ]; then\n read -e -p \"$(msg llm.embedding.custom_prompt) [${_current}]: \" AGENTTEAMS_EMBEDDING_MODEL\n else\n read -e -p \"$(msg llm.embedding.custom_prompt): \" AGENTTEAMS_EMBEDDING_MODEL\n fi\n if [ \"${AGENTTEAMS_EMBEDDING_MODEL}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_EMBEDDING_MODEL=\"${AGENTTEAMS_EMBEDDING_MODEL:-${_current}}\"\n fi\n ;;\n 3)\n AGENTTEAMS_EMBEDDING_MODEL=\"\"\n log \"$(msg llm.embedding.disabled)\"\n ;;\n *)\n AGENTTEAMS_EMBEDDING_MODEL=\"text-embedding-v4\"\n ;;\n esac\n\n if [ -n \"${AGENTTEAMS_EMBEDDING_MODEL}\" ]; then\n # Qwen provider uses dashscope directly; others use OPENAI_BASE_URL\n local EMB_BASE_URL=\"${AGENTTEAMS_OPENAI_BASE_URL}\"\n if [ \"${AGENTTEAMS_LLM_PROVIDER}\" = \"qwen\" ]; then\n EMB_BASE_URL=\"https://dashscope.aliyuncs.com/compatible-mode/v1\"\n fi\n if ! test_embedding_connectivity \"${EMB_BASE_URL}\" \"${AGENTTEAMS_LLM_API_KEY}\" \"${AGENTTEAMS_EMBEDDING_MODEL}\"; then\n AGENTTEAMS_EMBEDDING_MODEL=\"\"\n log \"$(msg llm.embedding.auto_disabled)\"\n fi\n fi\n\n export AGENTTEAMS_LLM_PROVIDER AGENTTEAMS_DEFAULT_MODEL\n [ -n \"${AGENTTEAMS_OPENAI_BASE_URL+x}\" ] && export AGENTTEAMS_OPENAI_BASE_URL\n log \"\"\n}\n\nstep_admin() {\n log \"$(msg admin.title)\"\n prompt AGENTTEAMS_ADMIN_USER \"$(msg admin.username_prompt)\" \"admin\" || return 0\n AGENTTEAMS_ADMIN_USER=\"$(printf '%s' \"${AGENTTEAMS_ADMIN_USER}\" | tr '[:upper:]' '[:lower:]')\"\n\n # Pre-set via env var: validate; in non-interactive mode fail fast,\n # in interactive mode warn and fall through to the retry prompt.\n if [ -n \"${AGENTTEAMS_ADMIN_PASSWORD:-}\" ]; then\n log \" $(msg prompt.preset \"$(msg admin.password_prompt)\")\"\n if [ ${#AGENTTEAMS_ADMIN_PASSWORD} -ge 8 ]; then\n log \"\"\n return 0\n fi\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n die \"$(msg admin.password_too_short \"${#AGENTTEAMS_ADMIN_PASSWORD}\")\"\n fi\n error \"$(msg admin.password_too_short \"${#AGENTTEAMS_ADMIN_PASSWORD}\")\"\n unset AGENTTEAMS_ADMIN_PASSWORD\n fi\n\n while true; do\n unset AGENTTEAMS_ADMIN_PASSWORD\n prompt_optional AGENTTEAMS_ADMIN_PASSWORD \"$(msg admin.password_prompt)\" \"true\" || return 0\n if [ -z \"${AGENTTEAMS_ADMIN_PASSWORD}\" ]; then\n AGENTTEAMS_ADMIN_PASSWORD=\"admin$(openssl rand -hex 6)\"\n log \"$(msg admin.password_generated)\"\n break\n fi\n if [ ${#AGENTTEAMS_ADMIN_PASSWORD} -ge 8 ]; then\n break\n fi\n error \"$(msg admin.password_too_short \"${#AGENTTEAMS_ADMIN_PASSWORD}\")\"\n done\n log \"\"\n}\n\nstep_network() {\n log \"$(msg port.local_only.title)\"\n echo \"\"\n echo \" 1) $(msg port.local_only.hint_yes)\"\n echo \" 2) $(msg port.local_only.hint_no)\"\n echo \"\"\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_LOCAL_ONLY=\"${AGENTTEAMS_LOCAL_ONLY:-1}\"\n elif [ -z \"${AGENTTEAMS_LOCAL_ONLY+x}\" ]; then\n local _local_choice\n read -e -p \"$(msg port.local_only.choice): \" _local_choice\n if [ \"${_local_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _local_choice=\"${_local_choice:-1}\"\n case \"${_local_choice}\" in\n 2|n|N|no|NO) AGENTTEAMS_LOCAL_ONLY=\"0\" ;;\n *) AGENTTEAMS_LOCAL_ONLY=\"1\" ;;\n esac\n fi\n export AGENTTEAMS_LOCAL_ONLY\n if [ \"${AGENTTEAMS_LOCAL_ONLY}\" = \"1\" ]; then\n log \"$(msg port.local_only.selected_local)\"\n else\n log \"$(msg port.local_only.selected_external)\"\n echo \"\"\n echo -e \"\\033[33m$(msg port.local_only.https_hint)\\033[0m\"\n fi\n}\n\nstep_ports() {\n log \"$(msg port.title)\"\n prompt AGENTTEAMS_PORT_GATEWAY \"$(msg port.gateway_prompt)\" \"18080\" || return 0\n prompt AGENTTEAMS_PORT_CONSOLE \"$(msg port.console_prompt)\" \"18001\" || return 0\n prompt AGENTTEAMS_PORT_ELEMENT_WEB \"$(msg port.element_prompt)\" \"18088\" || return 0\n prompt AGENTTEAMS_PORT_MANAGER_CONSOLE \"$(msg port.manager_console_prompt)\" \"18888\" || return 0\n log \"\"\n}\n\nstep_domains() {\n log \"$(msg domain.title)\"\n log \"$(msg domain.hint)\"\n prompt AGENTTEAMS_MATRIX_DOMAIN \"$(msg domain.matrix_prompt)\" \"matrix-local.agentteams.io:${AGENTTEAMS_PORT_GATEWAY}\" || return 0\n prompt AGENTTEAMS_MATRIX_CLIENT_DOMAIN \"$(msg domain.element_prompt)\" \"matrix-client-local.agentteams.io\" || return 0\n prompt AGENTTEAMS_AI_GATEWAY_DOMAIN \"$(msg domain.gateway_prompt)\" \"aigw-local.agentteams.io\" || return 0\n prompt AGENTTEAMS_FS_DOMAIN \"$(msg domain.fs_prompt)\" \"fs-local.agentteams.io\" || return 0\n if [ \"${AGENTTEAMS_MANAGER_RUNTIME}\" != \"copaw\" ]; then\n prompt AGENTTEAMS_CONSOLE_DOMAIN \"$(msg domain.console_prompt)\" \"console-local.agentteams.io\" || return 0\n fi\n log \"\"\n}\n\nstep_github() {\n log \"$(msg github.title)\"\n prompt_optional AGENTTEAMS_GITHUB_TOKEN \"$(msg github.token_prompt)\" \"true\" || return 0\n}\n\nstep_skills() {\n log \"\"\n log \"$(msg skills.title)\"\n prompt_optional AGENTTEAMS_SKILLS_API_URL \"$(msg skills.url_prompt)\" || return 0\n log \"\"\n}\n\nstep_volume() {\n log \"$(msg data.title)\"\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_DATA_DIR=\"${AGENTTEAMS_DATA_DIR:-agentteams-data}\"\n log \" $(msg data.volume_using \"${AGENTTEAMS_DATA_DIR}\") (non-interactive, skipped)\"\n export AGENTTEAMS_DATA_DIR\n return 0\n fi\n # ─────────────────────────────────────────────────────────────────\n if [ -z \"${AGENTTEAMS_DATA_DIR+x}\" ]; then\n local _input\n read -e -p \"$(msg data.volume_prompt): \" _input\n if [ \"${_input}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_DATA_DIR=\"${_input:-agentteams-data}\"\n export AGENTTEAMS_DATA_DIR\n fi\n AGENTTEAMS_DATA_DIR=\"${AGENTTEAMS_DATA_DIR:-agentteams-data}\"\n log \"$(msg data.volume_using \"${AGENTTEAMS_DATA_DIR}\")\"\n}\n\nstep_workspace() {\n log \"$(msg workspace.title)\"\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_WORKSPACE_DIR=\"${AGENTTEAMS_WORKSPACE_DIR:-${HOME}/agentteams-manager}\"\n AGENTTEAMS_WORKSPACE_DIR=\"$(cd \"${AGENTTEAMS_WORKSPACE_DIR}\" 2>/dev/null && pwd || echo \"${AGENTTEAMS_WORKSPACE_DIR}\")\"\n mkdir -p \"${AGENTTEAMS_WORKSPACE_DIR}\"\n log \" $(msg workspace.dir_label \"${AGENTTEAMS_WORKSPACE_DIR}\") (non-interactive, skipped)\"\n export AGENTTEAMS_WORKSPACE_DIR\n return 0\n fi\n # ─────────────────────────────────────────────────────────────────\n local _input\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg workspace.dir_prompt \"${HOME}/agentteams-manager\")\" \"${AGENTTEAMS_WORKSPACE_DIR}\")\"\n _input=\"${AGENTTEAMS_WORKSPACE_DIR}\"\n else\n local _current=\"${AGENTTEAMS_WORKSPACE_DIR}\"\n if [ -n \"${_current}\" ]; then\n read -e -p \"$(msg workspace.dir_prompt \"${HOME}/agentteams-manager\") [${_current}]: \" _input\n else\n read -e -p \"$(msg workspace.dir_prompt \"${HOME}/agentteams-manager\"): \" _input\n fi\n if [ \"${_input}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _input=\"${_input:-${_current}}\"\n fi\n AGENTTEAMS_WORKSPACE_DIR=\"${_input:-${HOME}/agentteams-manager}\"\n export AGENTTEAMS_WORKSPACE_DIR\n AGENTTEAMS_WORKSPACE_DIR=\"$(cd \"${AGENTTEAMS_WORKSPACE_DIR}\" 2>/dev/null && pwd || echo \"${AGENTTEAMS_WORKSPACE_DIR}\")\"\n mkdir -p \"${AGENTTEAMS_WORKSPACE_DIR}\"\n log \"$(msg workspace.dir_label \"${AGENTTEAMS_WORKSPACE_DIR}\")\"\n}\n\nstep_runtime() {\n log \"$(msg worker_runtime.title)\"\n echo \"\"\n echo \" 1) $(msg worker_runtime.copaw)\"\n echo \" 2) $(msg worker_runtime.openclaw)\"\n if ! _ver_lt \"${AGENTTEAMS_VERSION}\" \"v1.1.0\"; then\n echo \" 3) $(msg worker_runtime.hermes)\"\n fi\n echo \"\"\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"${AGENTTEAMS_DEFAULT_WORKER_RUNTIME:-copaw}\"\n elif [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_DEFAULT_WORKER_RUNTIME}\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg worker_runtime.title_short)\" \"${AGENTTEAMS_DEFAULT_WORKER_RUNTIME}\")\"\n local _runtime_choice\n read -e -p \"$(msg worker_runtime.choice): \" _runtime_choice\n if [ \"${_runtime_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n if [ -n \"${_runtime_choice}\" ]; then\n case \"${_runtime_choice}\" in\n 2) AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"openclaw\" ;;\n 3) if ! _ver_lt \"${AGENTTEAMS_VERSION}\" \"v1.1.0\"; then\n AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"hermes\"\n else\n AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"copaw\"\n fi ;;\n *) AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"copaw\" ;;\n esac\n fi\n elif [ -z \"${AGENTTEAMS_DEFAULT_WORKER_RUNTIME+x}\" ]; then\n local _runtime_choice\n read -e -p \"$(msg worker_runtime.choice): \" _runtime_choice\n if [ \"${_runtime_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _runtime_choice=\"${_runtime_choice:-1}\"\n case \"${_runtime_choice}\" in\n 2) AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"openclaw\" ;;\n 3) if ! _ver_lt \"${AGENTTEAMS_VERSION}\" \"v1.1.0\"; then\n AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"hermes\"\n else\n AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"copaw\"\n fi ;;\n *) AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"copaw\" ;;\n esac\n fi\n export AGENTTEAMS_DEFAULT_WORKER_RUNTIME\n log \"$(msg worker_runtime.selected \"${AGENTTEAMS_DEFAULT_WORKER_RUNTIME}\")\"\n}\n\nstep_manager_runtime() {\n log \"$(msg manager_runtime.title)\"\n echo \"\"\n echo \" 1) $(msg manager_runtime.copaw)\"\n echo \" 2) $(msg manager_runtime.openclaw)\"\n echo \"\"\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_MANAGER_RUNTIME=\"${AGENTTEAMS_MANAGER_RUNTIME:-copaw}\"\n elif [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_MANAGER_RUNTIME}\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg manager_runtime.title_short)\" \"${AGENTTEAMS_MANAGER_RUNTIME}\")\"\n local _runtime_choice\n read -e -p \"$(msg manager_runtime.choice): \" _runtime_choice\n if [ \"${_runtime_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n if [ -n \"${_runtime_choice}\" ]; then\n case \"${_runtime_choice}\" in\n 2) AGENTTEAMS_MANAGER_RUNTIME=\"openclaw\" ;;\n *) AGENTTEAMS_MANAGER_RUNTIME=\"copaw\" ;;\n esac\n fi\n elif [ -z \"${AGENTTEAMS_MANAGER_RUNTIME+x}\" ]; then\n local _runtime_choice\n read -e -p \"$(msg manager_runtime.choice): \" _runtime_choice\n if [ \"${_runtime_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _runtime_choice=\"${_runtime_choice:-1}\"\n case \"${_runtime_choice}\" in\n 2) AGENTTEAMS_MANAGER_RUNTIME=\"openclaw\" ;;\n *) AGENTTEAMS_MANAGER_RUNTIME=\"copaw\" ;;\n esac\n fi\n export AGENTTEAMS_MANAGER_RUNTIME\n log \"$(msg manager_runtime.selected \"${AGENTTEAMS_MANAGER_RUNTIME}\")\"\n}\n\nstep_e2ee() {\n log \"\"\n log \"$(msg matrix_e2ee.title)\"\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_MATRIX_E2EE=\"${AGENTTEAMS_MATRIX_E2EE:-0}\"\n log \" $(msg matrix_e2ee.title_short) = ${AGENTTEAMS_MATRIX_E2EE} (non-interactive, skipped)\"\n export AGENTTEAMS_MATRIX_E2EE\n return 0\n fi\n # ─────────────────────────────────────────────────────────────────\n echo \"\"\n echo -e \" $(msg matrix_e2ee.desc)\"\n echo \"\"\n echo \" 1) $(msg matrix_e2ee.disable)\"\n echo \" 2) $(msg matrix_e2ee.enable)\"\n echo \"\"\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_MATRIX_E2EE}\" ]; then\n local _e2ee_display; if [ \"${AGENTTEAMS_MATRIX_E2EE}\" = \"1\" ]; then _e2ee_display=\"$(msg matrix_e2ee.val_enabled)\"; else _e2ee_display=\"$(msg matrix_e2ee.val_disabled)\"; fi\n log \"$(msg prompt.upgrade_keep \"$(msg matrix_e2ee.title_short)\" \"${_e2ee_display}\")\"\n local _e2ee_choice\n read -e -p \"$(msg matrix_e2ee.choice): \" _e2ee_choice\n if [ \"${_e2ee_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n if [ -n \"${_e2ee_choice}\" ]; then\n case \"${_e2ee_choice}\" in\n 2) AGENTTEAMS_MATRIX_E2EE=\"1\" ;;\n *) AGENTTEAMS_MATRIX_E2EE=\"0\" ;;\n esac\n fi\n elif [ -z \"${AGENTTEAMS_MATRIX_E2EE+x}\" ]; then\n local _e2ee_choice\n read -e -p \"$(msg matrix_e2ee.choice): \" _e2ee_choice\n if [ \"${_e2ee_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _e2ee_choice=\"${_e2ee_choice:-1}\"\n case \"${_e2ee_choice}\" in\n 2) AGENTTEAMS_MATRIX_E2EE=\"1\" ;;\n *) AGENTTEAMS_MATRIX_E2EE=\"0\" ;;\n esac\n fi\n AGENTTEAMS_MATRIX_E2EE=\"${AGENTTEAMS_MATRIX_E2EE:-0}\"\n export AGENTTEAMS_MATRIX_E2EE\n if [ \"${AGENTTEAMS_MATRIX_E2EE}\" = \"1\" ]; then\n log \"$(msg matrix_e2ee.selected_enabled)\"\n else\n log \"$(msg matrix_e2ee.selected_disabled)\"\n fi\n}\n\nstep_docker_proxy() {\n # Only relevant when socket mounting is enabled\n if [ \"${AGENTTEAMS_MOUNT_SOCKET}\" != \"1\" ]; then\n AGENTTEAMS_DOCKER_PROXY=\"0\"\n return 0\n fi\n\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_DOCKER_PROXY=\"${AGENTTEAMS_DOCKER_PROXY:-0}\"\n log \" $(msg docker_proxy.title_short) = ${AGENTTEAMS_DOCKER_PROXY} (non-interactive, skipped)\"\n export AGENTTEAMS_DOCKER_PROXY\n return 0\n fi\n # ─────────────────────────────────────────────────────────────────\n\n echo \"\"\n echo -e \" \\033[1m$(msg docker_proxy.title)\\033[0m\"\n echo \"\"\n echo -e \" $(msg docker_proxy.desc)\"\n echo \"\"\n echo \" 1) $(msg docker_proxy.enable)\"\n echo \" 2) $(msg docker_proxy.disable)\"\n echo \"\"\n\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_DOCKER_PROXY}\" ]; then\n local _proxy_display; if [ \"${AGENTTEAMS_DOCKER_PROXY}\" = \"1\" ]; then _proxy_display=\"$(msg docker_proxy.val_enabled)\"; else _proxy_display=\"$(msg docker_proxy.val_disabled)\"; fi\n log \"$(msg prompt.upgrade_keep \"$(msg docker_proxy.title_short)\" \"${_proxy_display}\")\"\n local _choice\n read -e -p \"$(msg docker_proxy.choice): \" _choice\n if [ \"${_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n if [ -n \"${_choice}\" ]; then\n case \"${_choice}\" in\n 2) AGENTTEAMS_DOCKER_PROXY=\"0\" ;;\n *) AGENTTEAMS_DOCKER_PROXY=\"1\" ;;\n esac\n fi\n elif [ -z \"${AGENTTEAMS_DOCKER_PROXY+x}\" ]; then\n local _choice\n read -e -p \"$(msg docker_proxy.choice): \" _choice\n if [ \"${_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _choice=\"${_choice:-1}\"\n case \"${_choice}\" in\n 2) AGENTTEAMS_DOCKER_PROXY=\"0\" ;;\n *) AGENTTEAMS_DOCKER_PROXY=\"1\" ;;\n esac\n fi\n AGENTTEAMS_DOCKER_PROXY=\"${AGENTTEAMS_DOCKER_PROXY:-1}\"\n export AGENTTEAMS_DOCKER_PROXY\n if [ \"${AGENTTEAMS_DOCKER_PROXY}\" = \"1\" ]; then\n log \"$(msg docker_proxy.selected_enabled)\"\n\n # Prompt for additional allowed image sources\n echo \"\"\n echo -e \" $(msg docker_proxy.registries_desc)\"\n echo \"\"\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_PROXY_ALLOWED_REGISTRIES}\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg docker_proxy.registries_label)\" \"${AGENTTEAMS_PROXY_ALLOWED_REGISTRIES}\")\"\n local _reg_input\n read -e -p \"$(msg docker_proxy.registries_prompt): \" _reg_input\n if [ \"${_reg_input}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n [ -n \"${_reg_input}\" ] && AGENTTEAMS_PROXY_ALLOWED_REGISTRIES=\"${_reg_input}\"\n elif [ -z \"${AGENTTEAMS_PROXY_ALLOWED_REGISTRIES+x}\" ]; then\n local _reg_input\n read -e -p \"$(msg docker_proxy.registries_prompt): \" _reg_input\n if [ \"${_reg_input}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_PROXY_ALLOWED_REGISTRIES=\"${_reg_input:-}\"\n fi\n export AGENTTEAMS_PROXY_ALLOWED_REGISTRIES\n else\n log \"$(msg docker_proxy.selected_disabled)\"\n fi\n}\n\nstep_idle() {\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_WORKER_IDLE_TIMEOUT=\"${AGENTTEAMS_WORKER_IDLE_TIMEOUT:-720}\"\n log \" $(msg idle_timeout.label) = ${AGENTTEAMS_WORKER_IDLE_TIMEOUT} (non-interactive, skipped)\"\n export AGENTTEAMS_WORKER_IDLE_TIMEOUT\n return 0\n fi\n # ─────────────────────────────────────────────────────────────────\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_WORKER_IDLE_TIMEOUT}\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg idle_timeout.label)\" \"${AGENTTEAMS_WORKER_IDLE_TIMEOUT}\")\"\n local _idle_timeout\n read -e -p \"$(msg idle_timeout.prompt): \" _idle_timeout\n if [ \"${_idle_timeout}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n [ -n \"${_idle_timeout}\" ] && AGENTTEAMS_WORKER_IDLE_TIMEOUT=\"${_idle_timeout}\"\n elif [ -z \"${AGENTTEAMS_WORKER_IDLE_TIMEOUT+x}\" ]; then\n local _idle_timeout\n read -e -p \"$(msg idle_timeout.prompt): \" _idle_timeout\n if [ \"${_idle_timeout}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_WORKER_IDLE_TIMEOUT=\"${_idle_timeout:-720}\"\n fi\n AGENTTEAMS_WORKER_IDLE_TIMEOUT=\"${AGENTTEAMS_WORKER_IDLE_TIMEOUT:-720}\"\n export AGENTTEAMS_WORKER_IDLE_TIMEOUT\n log \"$(msg idle_timeout.selected \"${AGENTTEAMS_WORKER_IDLE_TIMEOUT}\")\"\n}\n\nstep_hostshare() {\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_HOST_SHARE_DIR=\"${AGENTTEAMS_HOST_SHARE_DIR:-${HOME}}\"\n log \" $(msg host_share.label) = ${AGENTTEAMS_HOST_SHARE_DIR} (non-interactive, skipped)\"\n export AGENTTEAMS_HOST_SHARE_DIR\n return 0\n fi\n # ─────────────────────────────────────────────────────────────────\n local _current=\"${AGENTTEAMS_HOST_SHARE_DIR}\"\n local _share_dir\n if [ -n \"${_current}\" ]; then\n read -e -p \"$(msg host_share.prompt \"$HOME\") [${_current}]: \" _share_dir\n else\n read -e -p \"$(msg host_share.prompt \"$HOME\"): \" _share_dir\n fi\n if [ \"${_share_dir}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_HOST_SHARE_DIR=\"${_share_dir:-${_current:-$HOME}}\"\n export AGENTTEAMS_HOST_SHARE_DIR\n}\n\nstep_podman_autostart() {\n echo \"\"\n log \"$(msg podman.autostart.title)\"\n echo \"\"\n echo -e \" $(msg podman.autostart.desc)\"\n echo \"\"\n echo \" 1) $(msg podman.autostart.enable)\"\n echo \" 2) $(msg podman.autostart.disable)\"\n echo \"\"\n\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_PODMAN_AUTOSTART}\" ]; then\n local _disp; if [ \"${AGENTTEAMS_PODMAN_AUTOSTART}\" = \"1\" ]; then _disp=\"$(msg podman.autostart.val_enabled)\"; else _disp=\"$(msg podman.autostart.val_disabled)\"; fi\n log \"$(msg prompt.upgrade_keep \"$(msg podman.autostart.title_short)\" \"${_disp}\")\"\n local _choice\n read -e -p \"$(msg podman.autostart.choice): \" _choice\n if [ \"${_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n if [ -n \"${_choice}\" ]; then\n case \"${_choice}\" in\n 2) AGENTTEAMS_PODMAN_AUTOSTART=\"0\" ;;\n *) AGENTTEAMS_PODMAN_AUTOSTART=\"1\" ;;\n esac\n fi\n elif [ -z \"${AGENTTEAMS_PODMAN_AUTOSTART+x}\" ]; then\n local _choice\n read -e -p \"$(msg podman.autostart.choice) [1]: \" _choice\n if [ \"${_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _choice=\"${_choice:-1}\"\n case \"${_choice}\" in\n 2) AGENTTEAMS_PODMAN_AUTOSTART=\"0\" ;;\n *) AGENTTEAMS_PODMAN_AUTOSTART=\"1\" ;;\n esac\n fi\n AGENTTEAMS_PODMAN_AUTOSTART=\"${AGENTTEAMS_PODMAN_AUTOSTART:-1}\"\n export AGENTTEAMS_PODMAN_AUTOSTART\n if [ \"${AGENTTEAMS_PODMAN_AUTOSTART}\" = \"1\" ]; then\n log \"$(msg podman.autostart.selected_enabled)\"\n else\n log \"$(msg podman.autostart.selected_disabled)\"\n fi\n}\n\n# ============================================================\n# Setup dedicated Podman autostart service for AgentTeams\n# ============================================================\nsetup_podman_autostart() {\n # Only execute if using podman and autostart is enabled\n if [ \"${DOCKER_CMD:-docker}\" != \"podman\" ] || [ \"${AGENTTEAMS_PODMAN_AUTOSTART:-0}\" != \"1\" ]; then\n return 0\n fi\n\n log \"$(msg install.podman.autostart_title)\"\n\n local current_user\n current_user=\"$(whoami)\"\n\n # Define the dedicated AgentTeams service content\n # Strict scoping: only manage AgentTeams containers.\n local _service_content=\"[Unit]\nDescription=AgentTeams Dedicated Podman Autostart Service\nDocumentation=https://github.com/agentscope-ai/AgentTeams\nWants=network-online.target\nAfter=network-online.target\n\n[Service]\nType=oneshot\nRemainAfterExit=true\nEnvironment=LOGGING=\\\"--log-level=info\\\"\nExecStart=/usr/bin/podman \\$LOGGING start --filter name=agentteams --filter restart-policy=always --filter restart-policy=unless-stopped\nExecStop=/usr/bin/podman \\$LOGGING stop --filter name=agentteams --filter restart-policy=always --filter restart-policy=unless-stopped\n\n[Install]\nWantedBy=default.target\"\n\n local _autostart_ok=0\n\n if [ \"${current_user}\" = \"root\" ]; then\n # Rootful Mode (System-wide dedicated service)\n log \"$(msg install.podman.root_setup)\"\n\n local _sys_dir=\"/etc/systemd/system\"\n mkdir -p \"${_sys_dir}\"\n echo \"${_service_content}\" > \"${_sys_dir}/agentteams-podman-restart.service\"\n\n systemctl daemon-reload >/dev/null 2>&1 || true\n\n if systemctl enable --now agentteams-podman-restart.service >/dev/null 2>&1; then\n log \"$(msg install.podman.root_success)\"\n _autostart_ok=1\n else\n log \"$(msg install.podman.root_fail)\"\n fi\n else\n # Rootless Mode (User-level dedicated service)\n log \"$(msg install.podman.user_setup \"${current_user}\")\"\n\n local _linger_ok=0\n\n # 1. Enable Linger for background execution (Graceful escalation strategy)\n if loginctl show-user \"${current_user}\" --property=Linger 2>/dev/null | grep -q \"Linger=yes\"; then\n _linger_ok=1\n else\n log \"$(msg install.podman.linger_enable)\"\n\n # Step A: Try to enable linger natively without sudo\n if loginctl enable-linger \"${current_user}\" >/dev/null 2>&1; then\n log \"Successfully enabled systemd linger natively.\"\n _linger_ok=1\n else\n # Step B: Fallback to sudo\n log \"Native linger enablement failed. Distro security policy requires privileges.\"\n if command -v sudo >/dev/null 2>&1; then\n log \"Attempting to enable systemd linger via sudo (enter password if prompted)...\"\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n sudo -n loginctl enable-linger \"${current_user}\" 2>/dev/null && _linger_ok=1 || log \"$(msg install.podman.linger_warn)\"\n else\n sudo loginctl enable-linger \"${current_user}\" && _linger_ok=1 || log \"$(msg install.podman.linger_warn)\"\n fi\n else\n log \"$(msg install.podman.linger_warn)\"\n fi\n fi\n fi\n\n # 2. Write dedicated service file to user directory\n local _user_dir=\"${HOME}/.config/systemd/user\"\n mkdir -p \"${_user_dir}\"\n echo \"${_service_content}\" > \"${_user_dir}/agentteams-podman-restart.service\"\n\n systemctl --user daemon-reload >/dev/null 2>&1 || true\n\n if systemctl --user enable --now agentteams-podman-restart.service >/dev/null 2>&1; then\n log \"$(msg install.podman.user_success)\"\n # Crucial: Only consider autostart fully successful if systemd linger is also enabled\n if [ \"${_linger_ok}\" = \"1\" ]; then\n _autostart_ok=1\n else\n log \"⚠️ Service enabled, but since systemd linger is not active, containers will not auto-recover on system reboot.\"\n fi\n else\n log \"$(msg install.podman.user_fail)\"\n fi\n fi\n\n # Only print final success banner if the service AND linger were actually enabled\n if [ \"${_autostart_ok}\" = \"1\" ]; then\n log \"$(msg install.podman.success)\"\n fi\n}\n\n# ============================================================\n# Manager Installation (Interactive)\n# ============================================================\n\ninstall_manager() {\n log \"$(msg install.title)\"\n log \"$(msg install.registry \"${AGENTTEAMS_REGISTRY}\")\"\n log \"\"\n log \"$(msg install.dir \"$(pwd)\")\"\n log \"$(msg install.dir_hint)\"\n log \"$(msg install.dir_hint2)\"\n log \"\"\n\n # Non-interactive fallback: resolve version immediately so image tags are available\n # before the step state machine runs. Interactive mode lets step_version handle it.\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_VERSION=\"${AGENTTEAMS_VERSION:-${AGENTTEAMS_KNOWN_STABLE_VERSION}}\"\n resolve_image_tags\n fi\n\n # Migrate legacy env file location before checks\n local existing_env=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n if [ ! -f \"${existing_env}\" ] && [ -f \"./agentteams-manager.env\" ]; then\n log \"Migrating agentteams-manager.env from current directory to ${existing_env}...\"\n mv \"./agentteams-manager.env\" \"${existing_env}\"\n fi\n\n # Orphan volume detection (only when no env file — step_existing handles the env-file case)\n if [ ! -f \"${existing_env}\" ]; then\n local data_vol=\"${AGENTTEAMS_DATA_DIR:-agentteams-data}\"\n if ${DOCKER_CMD} volume ls -q | grep -q \"^${data_vol}$\"; then\n echo \"\"\n log \"$(msg install.orphan_volume.detected \"${data_vol}\")\"\n log \"$(msg install.orphan_volume.warn)\"\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n log \"$(msg install.orphan_volume.clean_noninteractive)\"\n ${DOCKER_CMD} stop agentteams-manager 2>/dev/null || true\n ${DOCKER_CMD} rm agentteams-manager 2>/dev/null || true\n for w in $(${DOCKER_CMD} ps -a --format '{{.Names}}' | grep \"^agentteams-worker-\" || true); do\n ${DOCKER_CMD} stop \"${w}\" 2>/dev/null || true\n ${DOCKER_CMD} rm \"${w}\" 2>/dev/null || true\n done\n log \"$(msg install.orphan_volume.cleaning)\"\n ${DOCKER_CMD} volume rm \"${data_vol}\" 2>/dev/null || true\n log \"$(msg install.orphan_volume.cleaned)\"\n else\n echo \"\"\n echo \"$(msg install.orphan_volume.choose)\"\n echo \"$(msg install.orphan_volume.clean)\"\n echo \"$(msg install.orphan_volume.keep)\"\n echo \"\"\n local ORPHAN_CHOICE\n read -e -p \"$(msg install.orphan_volume.prompt): \" ORPHAN_CHOICE\n ORPHAN_CHOICE=\"${ORPHAN_CHOICE:-1}\"\n case \"${ORPHAN_CHOICE}\" in\n 1|clean)\n ${DOCKER_CMD} stop agentteams-manager 2>/dev/null || true\n ${DOCKER_CMD} rm agentteams-manager 2>/dev/null || true\n for w in $(${DOCKER_CMD} ps -a --format '{{.Names}}' | grep \"^agentteams-worker-\" || true); do\n ${DOCKER_CMD} stop \"${w}\" 2>/dev/null || true\n ${DOCKER_CMD} rm \"${w}\" 2>/dev/null || true\n done\n log \"$(msg install.orphan_volume.cleaning)\"\n ${DOCKER_CMD} volume rm \"${data_vol}\" 2>/dev/null || true\n log \"$(msg install.orphan_volume.cleaned)\"\n ;;\n 2|keep)\n log \"$(msg install.orphan_volume.keeping)\"\n ;;\n esac\n fi\n fi\n fi\n\n # ── State machine ─────────────────────────────────────────────────────────\n local _STEPS=( step_lang step_mode step_version step_existing step_llm step_manager_runtime step_runtime step_admin step_network\n step_ports step_domains step_github step_skills step_volume\n step_workspace step_e2ee step_docker_proxy step_idle step_hostshare step_podman_autostart )\n local _STEP_HISTORY=()\n local _step_idx=0\n while [ \"${_step_idx}\" -lt \"${#_STEPS[@]}\" ]; do\n local _step_fn=\"${_STEPS[$_step_idx]}\"\n if should_skip_step \"${_step_fn}\"; then\n _step_idx=$((_step_idx + 1))\n continue\n fi\n if [ \"${#_STEP_HISTORY[@]}\" -gt 0 ]; then\n log \"$(msg nav.back_hint)\"\n fi\n STEP_RESULT=\"\"\n \"${_step_fn}\"\n if [ \"${STEP_RESULT}\" = \"back\" ]; then\n if [ \"${#_STEP_HISTORY[@]}\" -gt 0 ]; then\n local _last=$(( ${#_STEP_HISTORY[@]} - 1 ))\n _step_idx=\"${_STEP_HISTORY[$_last]}\"\n _STEP_HISTORY=(\"${_STEP_HISTORY[@]:0:${_last}}\")\n clear_step_vars \"${_STEPS[$_step_idx]}\"\n fi\n # else: first step, ignore 'b'\n else\n _STEP_HISTORY+=(\"${_step_idx}\")\n _step_idx=$((_step_idx + 1))\n fi\n done\n # ── End state machine ──────────────────────────────────────────────────────\n\n # Post-machine defaults for any steps that were skipped\n AGENTTEAMS_DATA_DIR=\"${AGENTTEAMS_DATA_DIR:-agentteams-data}\"\n if [ -z \"${AGENTTEAMS_WORKSPACE_DIR+x}\" ] || [ -z \"${AGENTTEAMS_WORKSPACE_DIR}\" ]; then\n AGENTTEAMS_WORKSPACE_DIR=\"${HOME}/agentteams-manager\"\n export AGENTTEAMS_WORKSPACE_DIR\n fi\n AGENTTEAMS_WORKSPACE_DIR=\"$(cd \"${AGENTTEAMS_WORKSPACE_DIR}\" 2>/dev/null && pwd || echo \"${AGENTTEAMS_WORKSPACE_DIR}\")\"\n mkdir -p \"${AGENTTEAMS_WORKSPACE_DIR}\"\n AGENTTEAMS_MANAGER_RUNTIME=\"${AGENTTEAMS_MANAGER_RUNTIME:-copaw}\"\n export AGENTTEAMS_MANAGER_RUNTIME\n AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"${AGENTTEAMS_DEFAULT_WORKER_RUNTIME:-copaw}\"\n AGENTTEAMS_MATRIX_E2EE=\"${AGENTTEAMS_MATRIX_E2EE:-0}\"\n export AGENTTEAMS_MATRIX_E2EE\n AGENTTEAMS_WORKER_IDLE_TIMEOUT=\"${AGENTTEAMS_WORKER_IDLE_TIMEOUT:-720}\"\n export AGENTTEAMS_WORKER_IDLE_TIMEOUT\n AGENTTEAMS_HOST_SHARE_DIR=\"${AGENTTEAMS_HOST_SHARE_DIR:-$HOME}\"\n export AGENTTEAMS_HOST_SHARE_DIR\n\n log \"\"\n\n # Generate secrets (only if not already set)\n log \"$(msg install.generating_secrets)\"\n AGENTTEAMS_MANAGER_PASSWORD=\"${AGENTTEAMS_MANAGER_PASSWORD:-$(generate_key)}\"\n AGENTTEAMS_REGISTRATION_TOKEN=\"${AGENTTEAMS_REGISTRATION_TOKEN:-$(generate_key)}\"\n AGENTTEAMS_MINIO_USER=\"${AGENTTEAMS_MINIO_USER:-${AGENTTEAMS_ADMIN_USER}}\"\n AGENTTEAMS_MINIO_PASSWORD=\"${AGENTTEAMS_MINIO_PASSWORD:-${AGENTTEAMS_ADMIN_PASSWORD}}\"\n AGENTTEAMS_MANAGER_GATEWAY_KEY=\"${AGENTTEAMS_MANAGER_GATEWAY_KEY:-$(generate_key)}\"\n\n # Matrix AppService tokens — generate once during install/upgrade if not provided.\n # Persisted to env file so they survive controller restarts.\n AGENTTEAMS_MATRIX_APPSERVICE_ENABLED=\"${AGENTTEAMS_MATRIX_APPSERVICE_ENABLED:-true}\"\n export AGENTTEAMS_MATRIX_APPSERVICE_ENABLED\n if [ \"${AGENTTEAMS_MATRIX_APPSERVICE_ENABLED}\" != \"false\" ] && [ \"${AGENTTEAMS_MATRIX_APPSERVICE_ENABLED}\" != \"0\" ]; then\n if [ -z \"${AGENTTEAMS_MATRIX_APPSERVICE_AS_TOKEN}\" ]; then\n AGENTTEAMS_MATRIX_APPSERVICE_AS_TOKEN=\"$(openssl rand -hex 32)\"\n log \" Auto-generated AppService as_token (saved to env file)\"\n fi\n if [ -z \"${AGENTTEAMS_MATRIX_APPSERVICE_HS_TOKEN}\" ]; then\n AGENTTEAMS_MATRIX_APPSERVICE_HS_TOKEN=\"$(openssl rand -hex 32)\"\n log \" Auto-generated AppService hs_token (saved to env file)\"\n fi\n export AGENTTEAMS_MATRIX_APPSERVICE_AS_TOKEN\n export AGENTTEAMS_MATRIX_APPSERVICE_HS_TOKEN\n fi\n\n # Detect Apple Silicon (M1/M2/M3/M4) - need JVM fix for Higress Console\n # See: https://github.com/agentscope-ai/AgentTeams/issues/249\n if [ -z \"${JVM_ARGS:-}\" ] && [ \"$(uname -m)\" = \"arm64\" ] && [ \"$(uname -s)\" = \"Darwin\" ]; then\n log \"Apple Silicon detected - setting JVM_ARGS to fix Higress Console SIGILL issue\"\n JVM_ARGS=\"-XX:+UnlockDiagnosticVMOptions -XX:-UseAESCTRIntrinsics -XX:UseSVE=0\"\n fi\n\n # Write .env file\n ENV_FILE=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n cat > \"${ENV_FILE}\" << EOF\n# AgentTeams Manager Configuration\n# Generated by agentteams-install.sh on $(date)\n\n# Language\nAGENTTEAMS_LANGUAGE=${AGENTTEAMS_LANGUAGE}\n\n# LLM\nAGENTTEAMS_LLM_PROVIDER=${AGENTTEAMS_LLM_PROVIDER}\nAGENTTEAMS_DEFAULT_MODEL=${AGENTTEAMS_DEFAULT_MODEL}\nAGENTTEAMS_LLM_API_KEY=${AGENTTEAMS_LLM_API_KEY}\nAGENTTEAMS_OPENAI_BASE_URL=${AGENTTEAMS_OPENAI_BASE_URL:-}\nAGENTTEAMS_MODEL_CONTEXT_WINDOW=${AGENTTEAMS_MODEL_CONTEXT_WINDOW:-}\nAGENTTEAMS_MODEL_MAX_TOKENS=${AGENTTEAMS_MODEL_MAX_TOKENS:-}\nAGENTTEAMS_MODEL_REASONING=${AGENTTEAMS_MODEL_REASONING:-}\nAGENTTEAMS_MODEL_VISION=${AGENTTEAMS_MODEL_VISION:-}\n\n# Embedding model (empty = disabled, default: text-embedding-v4)\nAGENTTEAMS_EMBEDDING_MODEL=${AGENTTEAMS_EMBEDDING_MODEL}\n\n# Admin\nAGENTTEAMS_ADMIN_USER=${AGENTTEAMS_ADMIN_USER}\nAGENTTEAMS_ADMIN_PASSWORD=${AGENTTEAMS_ADMIN_PASSWORD}\n\n# Ports\nAGENTTEAMS_LOCAL_ONLY=${AGENTTEAMS_LOCAL_ONLY}\nAGENTTEAMS_PORT_GATEWAY=${AGENTTEAMS_PORT_GATEWAY}\nAGENTTEAMS_PORT_CONSOLE=${AGENTTEAMS_PORT_CONSOLE}\nAGENTTEAMS_PORT_ELEMENT_WEB=${AGENTTEAMS_PORT_ELEMENT_WEB}\nAGENTTEAMS_PORT_MANAGER_CONSOLE=${AGENTTEAMS_PORT_MANAGER_CONSOLE:-18888}\n\n# Manager runtime (openclaw | copaw)\nAGENTTEAMS_MANAGER_RUNTIME=${AGENTTEAMS_MANAGER_RUNTIME:-copaw}\n\n# Matrix\nAGENTTEAMS_MATRIX_DOMAIN=${AGENTTEAMS_MATRIX_DOMAIN}\nAGENTTEAMS_MATRIX_CLIENT_DOMAIN=${AGENTTEAMS_MATRIX_CLIENT_DOMAIN}\n\n# Gateway\nAGENTTEAMS_AI_GATEWAY_DOMAIN=${AGENTTEAMS_AI_GATEWAY_DOMAIN}\nAGENTTEAMS_MANAGER_GATEWAY_KEY=${AGENTTEAMS_MANAGER_GATEWAY_KEY}\n\n# File System\nAGENTTEAMS_FS_DOMAIN=${AGENTTEAMS_FS_DOMAIN}\nAGENTTEAMS_CONSOLE_DOMAIN=${AGENTTEAMS_CONSOLE_DOMAIN}\nAGENTTEAMS_MINIO_USER=${AGENTTEAMS_MINIO_USER}\nAGENTTEAMS_MINIO_PASSWORD=${AGENTTEAMS_MINIO_PASSWORD}\n\n# Internal\nAGENTTEAMS_MANAGER_PASSWORD=${AGENTTEAMS_MANAGER_PASSWORD}\nAGENTTEAMS_REGISTRATION_TOKEN=${AGENTTEAMS_REGISTRATION_TOKEN}\n\n# GitHub (optional)\nAGENTTEAMS_GITHUB_TOKEN=${AGENTTEAMS_GITHUB_TOKEN:-}\n\n# Nacos package import defaults\nAGENTTEAMS_NACOS_REGISTRY_URI=${AGENTTEAMS_NACOS_REGISTRY_URI:-nacos://market.agentteams.io:80/public}\nAGENTTEAMS_NACOS_USERNAME=${AGENTTEAMS_NACOS_USERNAME:-}\nAGENTTEAMS_NACOS_PASSWORD=${AGENTTEAMS_NACOS_PASSWORD:-}\nAGENTTEAMS_NACOS_TOKEN=${AGENTTEAMS_NACOS_TOKEN:-}\n\n# Skills Registry (optional, default: nacos://market.agentteams.io:80/public)\nAGENTTEAMS_SKILLS_API_URL=${AGENTTEAMS_SKILLS_API_URL:-nacos://market.agentteams.io:80/public}\n\n# OpenClaw CMS plugin (optional)\nAGENTTEAMS_CMS_TRACES_ENABLED=${AGENTTEAMS_CMS_TRACES_ENABLED:-false}\nAGENTTEAMS_CMS_ENDPOINT=${AGENTTEAMS_CMS_ENDPOINT:-}\nAGENTTEAMS_CMS_LICENSE_KEY=${AGENTTEAMS_CMS_LICENSE_KEY:-}\nAGENTTEAMS_CMS_PROJECT=${AGENTTEAMS_CMS_PROJECT:-}\nAGENTTEAMS_CMS_WORKSPACE=${AGENTTEAMS_CMS_WORKSPACE:-}\nAGENTTEAMS_CMS_SERVICE_NAME=${AGENTTEAMS_CMS_SERVICE_NAME:-agentteams-manager}\nAGENTTEAMS_CMS_METRICS_ENABLED=${AGENTTEAMS_CMS_METRICS_ENABLED:-false}\n\n# Worker images (for direct container creation)\nAGENTTEAMS_WORKER_IMAGE=${WORKER_IMAGE}\nAGENTTEAMS_COPAW_WORKER_IMAGE=${COPAW_WORKER_IMAGE}\nAGENTTEAMS_HERMES_WORKER_IMAGE=${HERMES_WORKER_IMAGE}\n\n# Default Worker runtime (openclaw | copaw | hermes)\nAGENTTEAMS_DEFAULT_WORKER_RUNTIME=${AGENTTEAMS_DEFAULT_WORKER_RUNTIME:-copaw}\n\n# Matrix E2EE (0=disabled, 1=enabled; default: 0)\nAGENTTEAMS_MATRIX_E2EE=${AGENTTEAMS_MATRIX_E2EE:-0}\n\n# Matrix AppService\nAGENTTEAMS_MATRIX_APPSERVICE_ENABLED=${AGENTTEAMS_MATRIX_APPSERVICE_ENABLED:-true}\nAGENTTEAMS_MATRIX_APPSERVICE_AS_TOKEN=${AGENTTEAMS_MATRIX_APPSERVICE_AS_TOKEN:-}\nAGENTTEAMS_MATRIX_APPSERVICE_HS_TOKEN=${AGENTTEAMS_MATRIX_APPSERVICE_HS_TOKEN:-}\n\n# Docker API proxy (0=disabled, 1=enabled; default: 1)\nAGENTTEAMS_DOCKER_PROXY=${AGENTTEAMS_DOCKER_PROXY:-1}\n\n# Docker API proxy: additional allowed image sources (comma-separated)\nAGENTTEAMS_PROXY_ALLOWED_REGISTRIES=${AGENTTEAMS_PROXY_ALLOWED_REGISTRIES:-}\n\n# Worker idle timeout in minutes (default: 720 = 12 hours)\nAGENTTEAMS_WORKER_IDLE_TIMEOUT=${AGENTTEAMS_WORKER_IDLE_TIMEOUT:-720}\n\n# Podman autostart via systemd (0=disabled, 1=enabled)\nAGENTTEAMS_PODMAN_AUTOSTART=${AGENTTEAMS_PODMAN_AUTOSTART:-0}\n\n# JVM Args for Higress Console (fixes SIGILL on Apple Silicon)\nJVM_ARGS=${JVM_ARGS:-}\n\n# Higress WASM plugin image registry (auto-selected by timezone)\nHIGRESS_ADMIN_WASM_PLUGIN_IMAGE_REGISTRY=${AGENTTEAMS_REGISTRY}\n\n# Data persistence\nAGENTTEAMS_DATA_DIR=${AGENTTEAMS_DATA_DIR:-agentteams-data}\n# Manager workspace (skills, memory, state — host-editable)\nAGENTTEAMS_WORKSPACE_DIR=${AGENTTEAMS_WORKSPACE_DIR:-}\n# Host directory sharing\nAGENTTEAMS_HOST_SHARE_DIR=${AGENTTEAMS_HOST_SHARE_DIR:-}\nEOF\n\n chmod 600 \"${ENV_FILE}\"\n log \"$(msg install.config_saved \"${ENV_FILE}\")\"\n\n # Detect container runtime socket\n SOCKET_MOUNT_ARGS=\"\"\n if [ \"${AGENTTEAMS_MOUNT_SOCKET}\" = \"1\" ]; then\n # Actively ensure the API socket is enabled and active before detection.\n if [ \"${DOCKER_CMD:-}\" = \"podman\" ]; then\n ensure_podman_socket\n fi\n if [ -z \"${CONTAINER_SOCK}\" ]; then\n CONTAINER_SOCK=$(detect_socket)\n fi\n if [ -n \"${CONTAINER_SOCK}\" ]; then\n log \"$(msg install.socket_detected \"${CONTAINER_SOCK}\")\"\n SOCKET_MOUNT_ARGS=\"-v ${CONTAINER_SOCK}:/var/run/docker.sock --security-opt label=disable\"\n else\n log \"$(msg install.socket_not_found)\"\n # Interactive confirmation when socket not found\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" != \"1\" ]; then\n echo \"\"\n echo -e \"\\033[33m$(msg install.socket_confirm.title)\\033[0m\"\n echo \"\"\n echo -e \"$(msg install.socket_confirm.message)\"\n echo \"\"\n read -p \"$(msg install.socket_confirm.prompt)\" SOCKET_CONFIRM\n if [ \"${SOCKET_CONFIRM}\" != \"y\" ] && [ \"${SOCKET_CONFIRM}\" != \"Y\" ]; then\n log \"$(msg install.socket_confirm.cancelled)\"\n exit 0\n fi\n fi\n fi\n fi\n\n # Create the data volume if it doesn't already exist (reuse on reinstall)\n if ! ${DOCKER_CMD} volume ls -q | grep -q \"^${AGENTTEAMS_DATA_DIR}$\"; then\n ${DOCKER_CMD} volume create \"${AGENTTEAMS_DATA_DIR}\" > /dev/null\n fi\n\n # Data mount: Docker volume\n DATA_MOUNT_ARGS=\"-v ${AGENTTEAMS_DATA_DIR}:/data\"\n\n # Manager workspace mount (always a host directory, defaulting to ~/agentteams-manager)\n WORKSPACE_MOUNT_ARGS=\"-v ${AGENTTEAMS_WORKSPACE_DIR}:/root/manager-workspace\"\n\n # Pass host timezone to container so date/time commands reflect local time\n TZ_ARGS=\"-e TZ=${AGENTTEAMS_TIMEZONE}\"\n\n # Host directory mount\n if [ -d \"${AGENTTEAMS_HOST_SHARE_DIR}\" ]; then\n HOST_SHARE_MOUNT_ARGS=\"-v ${AGENTTEAMS_HOST_SHARE_DIR}:/host-share\"\n log \"$(msg host_share.sharing \"${AGENTTEAMS_HOST_SHARE_DIR}\")\"\n else\n log \"$(msg host_share.not_exist \"${AGENTTEAMS_HOST_SHARE_DIR}\")\"\n HOST_SHARE_MOUNT_ARGS=\"-v ${AGENTTEAMS_HOST_SHARE_DIR}:/host-share\"\n fi\n\n # YOLO mode: pass through if set in environment (enables autonomous decisions)\n YOLO_ARGS=\"\"\n if [ \"${AGENTTEAMS_YOLO:-}\" = \"1\" ]; then\n YOLO_ARGS=\"-e AGENTTEAMS_YOLO=1\"\n log \"$(msg install.yolo)\"\n fi\n\n # Matrix-plugin debug tracing: pass through if AGENTTEAMS_MATRIX_DEBUG=1.\n # The container entrypoints translate this to OPENCLAW_MATRIX_DEBUG=1\n # so the openclaw matrix plugin emits structured INFO-level lifecycle\n # traces (sync state transitions, room.invite/join, message handler\n # arrival + filter outcomes). Used to diagnose worker/manager hangs.\n MATRIX_DEBUG_ARGS=\"\"\n if [ \"${AGENTTEAMS_MATRIX_DEBUG:-}\" = \"1\" ]; then\n MATRIX_DEBUG_ARGS=\"-e AGENTTEAMS_MATRIX_DEBUG=1\"\n fi\n\n # E2EE is already in the env file; but also pass explicitly in case env file is not the source\n # (AGENTTEAMS_MATRIX_E2EE is already written to ENV_FILE above via --env-file)\n\n # Pull images (manager based on runtime config; all worker runtimes always pulled)\n _is_local_image() {\n case \"$1\" in\n hiclaw/*|agentteams/*) return 0 ;;\n *) return 1 ;;\n esac\n }\n\n _legacy_local_image_for() {\n case \"$1\" in\n agentteams/manager:*) printf '%s\\n' \"hiclaw/hiclaw-manager:${1##*:}\" ;;\n agentteams/manager-copaw:*) printf '%s\\n' \"hiclaw/hiclaw-manager-copaw:${1##*:}\" ;;\n agentteams/worker-agent:*) printf '%s\\n' \"hiclaw/worker-agent:${1##*:}\" ;;\n agentteams/copaw-worker:*) printf '%s\\n' \"hiclaw/copaw-worker:${1##*:}\" ;;\n agentteams/hermes-worker:*) printf '%s\\n' \"hiclaw/hermes-worker:${1##*:}\" ;;\n agentteams/qwenpaw-worker:*) printf '%s\\n' \"hiclaw/qwenpaw-worker:${1##*:}\" ;;\n agentteams/agentteams-embedded:*) printf '%s\\n' \"hiclaw/hiclaw-embedded:${1##*:}\" ;;\n agentteams/agentteams-controller:*) printf '%s\\n' \"hiclaw/hiclaw-controller:${1##*:}\" ;;\n esac\n }\n\n _ensure_local_image_tag() {\n local _img=\"$1\"\n [ -z \"${_img}\" ] && return 1\n _is_local_image \"${_img}\" || return 1\n if ${DOCKER_CMD} image inspect \"${_img}\" >/dev/null 2>&1; then\n return 0\n fi\n local _legacy_img\n _legacy_img=\"$(_legacy_local_image_for \"${_img}\")\"\n if [ -n \"${_legacy_img}\" ] && ${DOCKER_CMD} image inspect \"${_legacy_img}\" >/dev/null 2>&1; then\n ${DOCKER_CMD} tag \"${_legacy_img}\" \"${_img}\"\n return 0\n fi\n return 1\n }\n\n # Helper: pull or skip a single image\n # Args: $1=image $2=exists_msg_key $3=pulling_msg_key\n _pull_image() {\n local _img=\"$1\" _exists_key=\"$2\" _pull_key=\"$3\"\n [ -z \"${_img}\" ] && return 0\n if _ensure_local_image_tag \"${_img}\"; then\n log \"$(msg \"${_exists_key}\" \"${_img}\")\"\n return 0\n fi\n log \"$(msg \"${_pull_key}\" \"${_img}\")\"\n local _attempt=1\n while [ $_attempt -le 3 ]; do\n if ${DOCKER_CMD} pull \"${_img}\"; then\n return 0\n fi\n if [ $_attempt -lt 3 ]; then\n log \"Pull failed (attempt ${_attempt}/3), retrying in 5s...\"\n sleep 5\n fi\n _attempt=$((_attempt + 1))\n done\n die \"Failed to pull ${_img} after 3 attempts\"\n return 1\n }\n\n # Embedded controller image (resolve versioned tag, fallback to latest)\n resolve_embedded_image\n if [ \"${AGENTTEAMS_USE_EMBEDDED}\" = \"1\" ]; then\n _ensure_local_image_tag \"${EMBEDDED_IMAGE}\" || true\n fi\n\n # Manager image is always required (select based on runtime)\n if [ \"${AGENTTEAMS_MANAGER_RUNTIME}\" = \"copaw\" ]; then\n _pull_image \"${MANAGER_COPAW_IMAGE}\" \"install.image.exists\" \"install.image.pulling_manager\"\n else\n _pull_image \"${MANAGER_IMAGE}\" \"install.image.exists\" \"install.image.pulling_manager\"\n fi\n\n # Pull all worker runtime images (workers may use any runtime regardless of the default)\n _pull_image \"${WORKER_IMAGE}\" \"install.image.worker_exists\" \"install.image.pulling_worker\"\n _pull_image \"${COPAW_WORKER_IMAGE}\" \"install.image.worker_exists\" \"install.image.pulling_worker\"\n _pull_image \"${HERMES_WORKER_IMAGE}\" \"install.image.worker_exists\" \"install.image.pulling_worker\"\n\n # --- Pre-upgrade: extract Matrix passwords from running old containers ---\n # Only needed when upgrading FROM old architecture (v1.0.9) TO embedded.\n # For new-arch-to-new-arch upgrades, credential files already exist with\n # correct room IDs — we must NOT overwrite them.\n _creds_tmp=\"\"\n if [ \"${AGENTTEAMS_UPGRADE:-0}\" = \"1\" ] && [ \"${AGENTTEAMS_USE_EMBEDDED}\" = \"1\" ]; then\n # Detect if upgrading from old arch: old arch has no agentteams-controller container\n # (or has it as a docker-proxy only, not embedded). Check if the existing\n # agentteams-controller is an embedded image (has supervisord) or just a proxy.\n _is_old_arch=0\n if ! ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -q \"^agentteams-controller$\"; then\n _is_old_arch=1\n elif ${DOCKER_CMD} ps -a --format '{{.Names}} {{.Image}}' 2>/dev/null | grep \"^agentteams-controller \" | grep -qv \"embedded\"; then\n _is_old_arch=1\n fi\n\n if [ \"${_is_old_arch}\" = \"1\" ]; then\n _creds_tmp=$(mktemp -d)\n\n # docker exec for Matrix/minio paths only works while agentteams-manager is running.\n _mgr_creds_tempstart=0\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -q '^agentteams-manager$'; then\n if ! ${DOCKER_CMD} ps --format '{{.Names}}' 2>/dev/null | grep -q '^agentteams-manager$'; then\n log \"agentteams-manager is stopped; starting it temporarily to extract Matrix credentials for upgrade...\"\n ${DOCKER_CMD} start agentteams-manager 2>/dev/null || true\n wait_matrix_ready \"agentteams-manager\"\n _mgr_creds_tempstart=1\n fi\n fi\n\n # Manager password (container Config.Env, then secrets file inside running manager, then data volume)\n _mgr_pw=$(${DOCKER_CMD} inspect agentteams-manager --format '{{range .Config.Env}}{{println .}}{{end}}' 2>/dev/null | grep '^AGENTTEAMS_MANAGER_PASSWORD=' | cut -d= -f2-)\n if [ -z \"${_mgr_pw}\" ] && ${DOCKER_CMD} ps --format '{{.Names}}' 2>/dev/null | grep -q '^agentteams-manager$'; then\n _mgr_pw=$(${DOCKER_CMD} exec agentteams-manager bash -c 'source /data/agentteams-secrets.env 2>/dev/null && echo \"${AGENTTEAMS_MANAGER_PASSWORD}\"' 2>/dev/null)\n fi\n if [ -z \"${_mgr_pw}\" ] && ${DOCKER_CMD} volume ls -q 2>/dev/null | grep -q \"^${AGENTTEAMS_DATA_DIR}$\"; then\n _mgr_pw=$(agentteams_read_secret_from_data_volume \"${AGENTTEAMS_DATA_DIR}\" AGENTTEAMS_MANAGER_PASSWORD)\n fi\n\n # Manager admin DM room ID: login as admin, find DM room with @manager\n _mgr_room=\"\"\n if [ -n \"${_mgr_pw}\" ]; then\n _admin_pw=$(grep AGENTTEAMS_ADMIN_PASSWORD \"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\" 2>/dev/null | cut -d= -f2-)\n _admin_user=$(grep AGENTTEAMS_ADMIN_USER \"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\" 2>/dev/null | cut -d= -f2-)\n _admin_user=\"${_admin_user:-admin}\"\n _matrix_domain=$(grep AGENTTEAMS_MATRIX_DOMAIN \"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\" 2>/dev/null | cut -d= -f2-)\n if [ -n \"${_admin_pw}\" ] && ${DOCKER_CMD} ps --format '{{.Names}}' 2>/dev/null | grep -q '^agentteams-manager$'; then\n _admin_token=$(${DOCKER_CMD} exec agentteams-manager curl -sf -X POST http://127.0.0.1:6167/_matrix/client/v3/login \\\n -H \"Content-Type: application/json\" \\\n -d '{\"type\":\"m.login.password\",\"identifier\":{\"type\":\"m.id.user\",\"user\":\"'\"${_admin_user}\"'\"},\"password\":\"'\"${_admin_pw}\"'\"}' 2>/dev/null | python3 -c \"import sys,json; print(json.load(sys.stdin).get('access_token',''))\" 2>/dev/null || true)\n if [ -n \"${_admin_token}\" ]; then\n _mgr_room=$(${DOCKER_CMD} exec agentteams-manager curl -sf -X GET \\\n -H \"Authorization: Bearer ${_admin_token}\" \\\n \"http://127.0.0.1:6167/_matrix/client/v3/joined_rooms\" 2>/dev/null | python3 -c \"\nimport sys,json,subprocess\nrooms = json.load(sys.stdin).get('joined_rooms',[])\nfor room_id in rooms:\n enc = room_id.replace('!','%21')\n members = json.loads(subprocess.check_output([\n 'docker','exec','agentteams-manager','curl','-sf','-X','GET',\n '-H','Authorization: Bearer ${_admin_token}',\n 'http://127.0.0.1:6167/_matrix/client/v3/rooms/'+enc+'/members'\n ]).decode()).get('chunk',[])\n member_ids = [m['state_key'] for m in members]\n if any('manager' in m and 'admin' not in m.split(':')[0] for m in member_ids):\n if len(member_ids) <= 3:\n print(room_id)\n break\n\" 2>/dev/null || true)\n fi\n fi\n if [ -z \"${_mgr_room}\" ]; then\n _mgr_room=$(agentteams_read_admin_dm_room_from_workspace \"${AGENTTEAMS_WORKSPACE_DIR}\")\n fi\n cat > \"${_creds_tmp}/default.env\" <<CREDEOF\nWORKER_PASSWORD=\"${_mgr_pw}\"\nWORKER_MINIO_PASSWORD=\"$(openssl rand -hex 24)\"\nWORKER_GATEWAY_KEY=\"${AGENTTEAMS_MANAGER_GATEWAY_KEY}\"\nWORKER_ROOM_ID=\"${_mgr_room}\"\nCREDEOF\n log \"Extracted Manager Matrix password${_mgr_room:+ and room ID}\"\n fi\n\n # Worker passwords and room IDs from workers-registry.json\n if [ -f \"${AGENTTEAMS_WORKSPACE_DIR}/workers-registry.json\" ]; then\n _worker_names=$(python3 -c \"import json; d=json.load(open('${AGENTTEAMS_WORKSPACE_DIR}/workers-registry.json')); print(' '.join(d.get('workers',{}).keys()))\" 2>/dev/null || true)\n for _wname in ${_worker_names}; do\n _wpw=\"\"\n if ${DOCKER_CMD} ps --format '{{.Names}}' 2>/dev/null | grep -q '^agentteams-manager$'; then\n _wpw=$(${DOCKER_CMD} exec agentteams-manager cat \"/root/agentteams-fs/agents/${_wname}/credentials/matrix/password\" 2>/dev/null || true)\n fi\n if [ -z \"${_wpw}\" ] && ${DOCKER_CMD} volume ls -q 2>/dev/null | grep -q \"^${AGENTTEAMS_DATA_DIR}$\"; then\n _wpw=$(agentteams_read_worker_creds_value_from_volume \"${AGENTTEAMS_DATA_DIR}\" \"${_wname}\" WORKER_PASSWORD)\n fi\n _wroom=$(python3 -c \"import json; d=json.load(open('${AGENTTEAMS_WORKSPACE_DIR}/workers-registry.json')); print(d.get('workers',{}).get('${_wname}',{}).get('room_id',''))\" 2>/dev/null || true)\n if [ -z \"${_wroom}\" ] && ${DOCKER_CMD} volume ls -q 2>/dev/null | grep -q \"^${AGENTTEAMS_DATA_DIR}$\"; then\n _wroom=$(agentteams_read_worker_creds_value_from_volume \"${AGENTTEAMS_DATA_DIR}\" \"${_wname}\" WORKER_ROOM_ID)\n fi\n if [ -n \"${_wpw}\" ]; then\n cat > \"${_creds_tmp}/${_wname}.env\" <<CREDEOF\nWORKER_PASSWORD=\"${_wpw}\"\nWORKER_MINIO_PASSWORD=\"$(openssl rand -hex 24)\"\nWORKER_GATEWAY_KEY=\"$(openssl rand -hex 32)\"\nWORKER_ROOM_ID=\"${_wroom}\"\nCREDEOF\n log \"Extracted ${_wname} Matrix password${_wroom:+ and room ID}\"\n fi\n done\n fi\n\n if [ \"${_mgr_creds_tempstart}\" = \"1\" ]; then\n log \"Stopping agentteams-manager after credential extraction (upgrade will recreate containers)...\"\n ${DOCKER_CMD} stop agentteams-manager 2>/dev/null || true\n fi\n fi # _is_old_arch\n fi\n\n # --- Stop and remove existing containers ---\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' | grep -q \"^agentteams-controller$\"; then\n ${DOCKER_CMD} stop agentteams-controller 2>/dev/null || true\n ${DOCKER_CMD} rm agentteams-controller 2>/dev/null || true\n fi\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' | grep -q \"^agentteams-manager$\"; then\n log \"$(msg install.removing_existing)\"\n ${DOCKER_CMD} stop agentteams-manager 2>/dev/null || true\n ${DOCKER_CMD} rm agentteams-manager 2>/dev/null || true\n fi\n\n # Stop and remove worker containers (controller will recreate via CR reconciliation)\n if [ -n \"${UPGRADE_EXISTING_WORKERS:-}\" ]; then\n log \"$(msg install.existing.stopping_workers)\"\n for w in ${UPGRADE_EXISTING_WORKERS}; do\n ${DOCKER_CMD} stop \"${w}\" 2>/dev/null || true\n ${DOCKER_CMD} rm \"${w}\" 2>/dev/null || true\n log \"$(msg install.existing.removed \"${w}\")\"\n done\n fi\n\n # Clean up legacy containers (e.g. hiclaw-docker-proxy from v1.0.x)\n for _legacy in $(${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -E '^hiclaw-' | grep -vE \"^(agentteams-controller|agentteams-manager|agentteams-worker-)\" || true); do\n log \"Removing legacy container: ${_legacy}\"\n ${DOCKER_CMD} stop \"${_legacy}\" 2>/dev/null || true\n ${DOCKER_CMD} rm -f \"${_legacy}\" 2>/dev/null || true\n done\n\n # --- Upgrade: inject extracted credentials into data volume ---\n # Only needed for old-arch upgrades (credential files were extracted above).\n if [ -n \"${_creds_tmp}\" ] && [ -d \"${_creds_tmp}\" ] && [ -n \"$(ls -A \"${_creds_tmp}\" 2>/dev/null)\" ]; then\n local _cleanup_ctr=\"agentteams-upgrade-cleanup\"\n ${DOCKER_CMD} rm -f \"${_cleanup_ctr}\" 2>/dev/null || true\n ${DOCKER_CMD} run --rm --name \"${_cleanup_ctr}\" \\\n --entrypoint sh \\\n -v \"${AGENTTEAMS_DATA_DIR}:/data\" \\\n -v \"${_creds_tmp}:/creds:ro\" \\\n \"${EMBEDDED_IMAGE}\" -c '\n rm -rf /data/worker-creds\n mkdir -p /data/worker-creds\n cp /creds/*.env /data/worker-creds/ 2>/dev/null || true\n chmod 600 /data/worker-creds/*.env 2>/dev/null || true\n ' 2>/dev/null && log \"Injected credentials for upgrade\" || log \"Warning: credential injection failed, continuing\"\n rm -rf \"${_creds_tmp}\"\n fi\n\n # --- Start containers ---\n log \"$(msg install.starting_manager)\"\n\n # Build port binding args\n if [ \"${AGENTTEAMS_LOCAL_ONLY:-1}\" = \"1\" ]; then\n _port_prefix=\"127.0.0.1:\"\n else\n _port_prefix=\"\"\n fi\n\n # Ensure agentteams-net Docker network exists\n ${DOCKER_CMD} network inspect agentteams-net >/dev/null 2>&1 || ${DOCKER_CMD} network create agentteams-net\n\n if [ \"${AGENTTEAMS_USE_EMBEDDED}\" != \"1\" ] && [ \"${AGENTTEAMS_UPGRADE:-0}\" = \"1\" ]; then\n # Check if current installation is embedded — downgrade to legacy is not supported\n if ${DOCKER_CMD} ps -a --format '{{.Names}} {{.Image}}' 2>/dev/null | grep \"^agentteams-controller \" | grep -q \"embedded\"; then\n error \"Downgrade from embedded architecture to legacy version (${AGENTTEAMS_VERSION}) is not supported.\"\n error \"Please use 'make uninstall-embedded' first, then do a clean install of the target version.\"\n exit 1\n fi\n fi\n\n if [ \"${AGENTTEAMS_USE_EMBEDDED}\" = \"1\" ]; then\n # ============================================================\n # New architecture: embedded controller + auto-created manager\n # ============================================================\n\n # Internal port: 8080 (Higress gateway inside the container).\n local _internal_gw_port=8080\n local _matrix_domain=\"${AGENTTEAMS_MATRIX_DOMAIN:-matrix-local.agentteams.io:${AGENTTEAMS_PORT_GATEWAY}}\"\n local _aigw_domain=\"${AGENTTEAMS_AI_GATEWAY_DOMAIN:-aigw-local.agentteams.io}\"\n # Ensure internal gateway port is present (container-internal traffic uses 8080)\n case \"${_aigw_domain}\" in *:*) ;; *) _aigw_domain=\"${_aigw_domain}:${_internal_gw_port}\" ;; esac\n local _fs_domain=\"${AGENTTEAMS_FS_DOMAIN:-fs-local.agentteams.io}\"\n case \"${_fs_domain}\" in *:*) ;; *) _fs_domain=\"${_fs_domain}:${_internal_gw_port}\" ;; esac\n\n # Controller env args\n local _ctrl_env_args=(\n -e \"AGENTTEAMS_ADMIN_USER=${AGENTTEAMS_ADMIN_USER}\"\n -e \"AGENTTEAMS_ADMIN_PASSWORD=${AGENTTEAMS_ADMIN_PASSWORD}\"\n -e \"AGENTTEAMS_MANAGER_PASSWORD=${AGENTTEAMS_MANAGER_PASSWORD}\"\n -e \"AGENTTEAMS_REGISTRATION_TOKEN=${AGENTTEAMS_REGISTRATION_TOKEN}\"\n -e \"AGENTTEAMS_MINIO_USER=${AGENTTEAMS_MINIO_USER}\"\n -e \"AGENTTEAMS_MINIO_PASSWORD=${AGENTTEAMS_MINIO_PASSWORD}\"\n -e \"AGENTTEAMS_LLM_PROVIDER=${AGENTTEAMS_LLM_PROVIDER}\"\n -e \"AGENTTEAMS_LLM_API_KEY=${AGENTTEAMS_LLM_API_KEY}\"\n -e \"AGENTTEAMS_DEFAULT_MODEL=${AGENTTEAMS_DEFAULT_MODEL}\"\n -e \"AGENTTEAMS_MANAGER_GATEWAY_KEY=${AGENTTEAMS_MANAGER_GATEWAY_KEY}\"\n -e \"AGENTTEAMS_MANAGER_RUNTIME=${AGENTTEAMS_MANAGER_RUNTIME:-copaw}\"\n -e \"AGENTTEAMS_MANAGER_IMAGE=$([ \"${AGENTTEAMS_MANAGER_RUNTIME}\" = \"copaw\" ] && echo \"${MANAGER_COPAW_IMAGE}\" || echo \"${MANAGER_IMAGE}\")\"\n -e \"AGENTTEAMS_DEFAULT_WORKER_RUNTIME=${AGENTTEAMS_DEFAULT_WORKER_RUNTIME:-copaw}\"\n -e \"AGENTTEAMS_WORKER_IMAGE=${WORKER_IMAGE}\"\n -e \"AGENTTEAMS_COPAW_WORKER_IMAGE=${COPAW_WORKER_IMAGE}\"\n -e \"AGENTTEAMS_HERMES_WORKER_IMAGE=${HERMES_WORKER_IMAGE}\"\n -e \"AGENTTEAMS_MATRIX_DOMAIN=${_matrix_domain}\"\n -e \"AGENTTEAMS_ELEMENT_HOMESERVER_URL=http://127.0.0.1:${AGENTTEAMS_PORT_GATEWAY}\"\n -e \"AGENTTEAMS_MATRIX_URL=http://127.0.0.1:6167\"\n -e \"AGENTTEAMS_MATRIX_E2EE=${AGENTTEAMS_MATRIX_E2EE:-0}\"\n -e \"AGENTTEAMS_MATRIX_APPSERVICE_ENABLED=${AGENTTEAMS_MATRIX_APPSERVICE_ENABLED:-true}\"\n -e \"AGENTTEAMS_MATRIX_APPSERVICE_AS_TOKEN=${AGENTTEAMS_MATRIX_APPSERVICE_AS_TOKEN:-}\"\n -e \"AGENTTEAMS_MATRIX_APPSERVICE_HS_TOKEN=${AGENTTEAMS_MATRIX_APPSERVICE_HS_TOKEN:-}\"\n -e \"AGENTTEAMS_MINIO_ENDPOINT=http://127.0.0.1:9000\"\n -e \"AGENTTEAMS_MINIO_BUCKET=agentteams-storage\"\n -e \"AGENTTEAMS_STORAGE_PREFIX=agentteams/agentteams-storage\"\n -e \"AGENTTEAMS_FS_ENDPOINT=http://127.0.0.1:9000\"\n -e \"AGENTTEAMS_AI_GATEWAY_URL=http://${_aigw_domain}\"\n -e \"AGENTTEAMS_CONTROLLER_URL=http://agentteams-controller:8090\"\n -e \"AGENTTEAMS_DOCKER_NETWORK=agentteams-net\"\n -e \"AGENTTEAMS_WORKSPACE_DIR=${AGENTTEAMS_WORKSPACE_DIR}\"\n -e \"AGENTTEAMS_HOST_SHARE_DIR=${AGENTTEAMS_HOST_SHARE_DIR}\"\n -e \"AGENTTEAMS_MANAGER_ENABLED=true\"\n -e \"AGENTTEAMS_PORT_MANAGER_CONSOLE=${AGENTTEAMS_PORT_MANAGER_CONSOLE:-18888}\"\n )\n\n # Timezone\n if [ -n \"${AGENTTEAMS_TIMEZONE:-}\" ]; then\n _ctrl_env_args+=(-e \"TZ=${AGENTTEAMS_TIMEZONE}\")\n fi\n\n # Yolo mode\n if [ \"${AGENTTEAMS_YOLO:-}\" = \"1\" ]; then\n _ctrl_env_args+=(-e \"AGENTTEAMS_YOLO=1\")\n fi\n\n # Matrix-plugin debug tracing — propagated to every manager + worker\n # the controller spawns, then translated to OPENCLAW_MATRIX_DEBUG=1\n # by the container entrypoints. Use this to diagnose\n # \"worker did not join\" / \"manager replied empty\" hangs.\n if [ \"${AGENTTEAMS_MATRIX_DEBUG:-}\" = \"1\" ]; then\n _ctrl_env_args+=(-e \"AGENTTEAMS_MATRIX_DEBUG=1\")\n fi\n\n # Optional: GitHub token\n if [ -n \"${AGENTTEAMS_GITHUB_TOKEN:-}\" ]; then\n _ctrl_env_args+=(-e \"AGENTTEAMS_GITHUB_TOKEN=${AGENTTEAMS_GITHUB_TOKEN}\")\n fi\n\n # Optional: embedding model\n if [ -n \"${AGENTTEAMS_EMBEDDING_MODEL:-}\" ]; then\n _ctrl_env_args+=(-e \"AGENTTEAMS_EMBEDDING_MODEL=${AGENTTEAMS_EMBEDDING_MODEL}\")\n fi\n\n # Optional: OpenAI-compatible base URL\n if [ -n \"${AGENTTEAMS_OPENAI_BASE_URL:-}\" ]; then\n _ctrl_env_args+=(-e \"AGENTTEAMS_OPENAI_BASE_URL=${AGENTTEAMS_OPENAI_BASE_URL}\")\n fi\n # Optional: language\n if [ -n \"${AGENTTEAMS_LANGUAGE:-}\" ]; then\n _ctrl_env_args+=(-e \"AGENTTEAMS_LANGUAGE=${AGENTTEAMS_LANGUAGE}\")\n fi\n\n # Optional: CMS/ARMS observability. In embedded mode the controller\n # spawns the Manager and Workers, so it must receive these settings.\n _ctrl_env_args+=(-e \"AGENTTEAMS_CMS_TRACES_ENABLED=${AGENTTEAMS_CMS_TRACES_ENABLED:-false}\")\n _ctrl_env_args+=(-e \"AGENTTEAMS_CMS_SERVICE_NAME=${AGENTTEAMS_CMS_SERVICE_NAME:-agentteams-manager}\")\n _ctrl_env_args+=(-e \"AGENTTEAMS_CMS_METRICS_ENABLED=${AGENTTEAMS_CMS_METRICS_ENABLED:-false}\")\n if [ -n \"${AGENTTEAMS_CMS_ENDPOINT:-}\" ]; then\n _ctrl_env_args+=(-e \"AGENTTEAMS_CMS_ENDPOINT=${AGENTTEAMS_CMS_ENDPOINT}\")\n fi\n if [ -n \"${AGENTTEAMS_CMS_LICENSE_KEY:-}\" ]; then\n _ctrl_env_args+=(-e \"AGENTTEAMS_CMS_LICENSE_KEY=${AGENTTEAMS_CMS_LICENSE_KEY}\")\n fi\n if [ -n \"${AGENTTEAMS_CMS_PROJECT:-}\" ]; then\n _ctrl_env_args+=(-e \"AGENTTEAMS_CMS_PROJECT=${AGENTTEAMS_CMS_PROJECT}\")\n fi\n if [ -n \"${AGENTTEAMS_CMS_WORKSPACE:-}\" ]; then\n _ctrl_env_args+=(-e \"AGENTTEAMS_CMS_WORKSPACE=${AGENTTEAMS_CMS_WORKSPACE}\")\n fi\n\n # shellcheck disable=SC2086\n ${DOCKER_CMD} run -d \\\n --name agentteams-controller \\\n --network agentteams-net \\\n --network-alias matrix-local.agentteams.io \\\n --network-alias aigw-local.agentteams.io \\\n --network-alias fs-local.agentteams.io \\\n \"${_ctrl_env_args[@]}\" \\\n -v \"${CONTAINER_SOCK}:/var/run/docker.sock\" \\\n --security-opt label=disable \\\n -v \"${AGENTTEAMS_DATA_DIR}:/data\" \\\n -v \"${AGENTTEAMS_WORKSPACE_DIR}:/root/agentteams-fs/agents/manager\" \\\n -p \"${_port_prefix}${AGENTTEAMS_PORT_GATEWAY}:8080\" \\\n -p \"${_port_prefix}${AGENTTEAMS_PORT_CONSOLE}:8001\" \\\n -p \"${_port_prefix}${AGENTTEAMS_PORT_ELEMENT_WEB:-18088}:8088\" \\\n --restart unless-stopped \\\n \"${EMBEDDED_IMAGE}\"\n\n log \"Embedded controller started: agentteams-controller\"\n\n # Wait for infrastructure inside the controller container\n _wait_for_url() {\n local url=\"$1\" ctr=\"$2\" max_wait=\"${3:-120}\" desc=\"${4:-service}\"\n local elapsed=0\n log \"Waiting for ${desc}...\"\n while [ $elapsed -lt $max_wait ]; do\n if ${DOCKER_CMD} exec \"${ctr}\" curl -sf \"${url}\" >/dev/null 2>&1; then\n log \"${desc} is ready (${elapsed}s)\"\n return 0\n fi\n sleep 2\n elapsed=$((elapsed + 2))\n done\n log \"ERROR: ${desc} not ready after ${max_wait}s\"\n return 1\n }\n\n _wait_for_url \"http://127.0.0.1:6167/_tuwunel/server_version\" agentteams-controller 120 \"Tuwunel (Matrix)\" || exit 1\n _wait_for_url \"http://127.0.0.1:9000/minio/health/live\" agentteams-controller 60 \"MinIO\" || exit 1\n _wait_for_url \"http://127.0.0.1:8080/status\" agentteams-controller 120 \"Higress Gateway\" || exit 1\n\n # Wait for controller to create Manager Agent container\n log \"Waiting for Manager Agent container...\"\n local _mgr_wait=0\n local _mgr_max=300\n while [ $_mgr_wait -lt $_mgr_max ]; do\n if ${DOCKER_CMD} ps --format '{{.Names}}' 2>/dev/null | grep -q \"^agentteams-manager$\"; then\n log \"Manager Agent container detected (${_mgr_wait}s)\"\n break\n fi\n sleep 3\n _mgr_wait=$((_mgr_wait + 3))\n done\n if [ $_mgr_wait -ge $_mgr_max ]; then\n log \"ERROR: Manager Agent container not created after ${_mgr_max}s\"\n log \"Controller logs:\"\n ${DOCKER_CMD} exec agentteams-controller tail -30 /var/log/agentteams/agentteams-controller-error.log 2>/dev/null || true\n exit 1\n fi\n\n # Wait for Manager Agent to be running\n log \"Waiting for Manager Agent to start...\"\n local _agent_wait=0\n while [ $_agent_wait -lt 120 ]; do\n local _state\n _state=$(${DOCKER_CMD} inspect --format '{{.State.Status}}' agentteams-manager 2>/dev/null || echo \"missing\")\n if [ \"${_state}\" = \"running\" ]; then\n log \"Manager Agent is running\"\n break\n fi\n sleep 2\n _agent_wait=$((_agent_wait + 2))\n done\n\n # Enable yolo mode in agent if requested\n if [ \"${AGENTTEAMS_YOLO:-}\" = \"1\" ]; then\n ${DOCKER_CMD} exec agentteams-manager touch /root/manager-workspace/yolo-mode 2>/dev/null || true\n fi\n\n # Wait for the controller to send the first-boot welcome message.\n # The controller gates this on (a) Manager joining the DM room and\n # (b) Higress WASM key-auth propagation actually clearing /v1/chat/completions\n # for the Manager's gateway key — typically ~45-90s on a fresh install.\n # We poll Manager CR Status.WelcomeSent via the in-container AgentTeams CLI,\n # exec'd inside agentteams-controller (the source-of-truth container — its\n # bundled CLI binary is always in lockstep with whatever controller\n # binary is currently serving the HTTP API, since they're the same\n # `go build` output. The agentteams-manager container's CLI may lag the\n # controller across image upgrades and silently drop the welcomeSent\n # field, leaving this loop hung). The controller container mints a\n # long-lived admin SA token at startup and writes it to\n # AGENTTEAMS_AUTH_TOKEN_FILE=/var/run/agentteams/cli-token (set as a Dockerfile\n # ENV default), so a bare `docker exec agentteams-controller agt …`\n # auto-discovers both the endpoint and the token. There is a brief\n # window after container start before bootstrapAdminCLIToken completes\n # where the file may be empty / absent — the loop's silent retry\n # handles that the same way it handles the manager-not-yet-running\n # case below.\n if ${DOCKER_CMD} exec agentteams-controller sh -c 'command -v agt' >/dev/null 2>&1; then\n log \"$(msg install.welcome_msg.waiting)\"\n local _welcome_wait=0\n local _welcome_max=\"${AGENTTEAMS_WELCOME_TIMEOUT:-300}\"\n local _welcome_done=0\n while [ $_welcome_wait -lt $_welcome_max ]; do\n # `tr -d` strips whitespace/CR so the grep stays robust to\n # any future change in go-json field ordering or formatting.\n local _wjson\n _wjson=$(${DOCKER_CMD} exec agentteams-controller \\\n agt get managers default -o json 2>/dev/null || true)\n if [ -n \"${_wjson}\" ] && printf '%s' \"${_wjson}\" | tr -d ' \\r\\n' | grep -q '\"welcomeSent\":true'; then\n log \"$(msg install.welcome_msg.confirmed \"${_welcome_wait}\")\"\n _welcome_done=1\n break\n fi\n sleep 3\n _welcome_wait=$((_welcome_wait + 3))\n done\n if [ $_welcome_done -ne 1 ]; then\n # Non-fatal: install is still good. Keep going to the success\n # banner so the admin can use Element Web to nudge Manager into\n # onboarding manually (one DM message is enough).\n log \"$(msg install.welcome_msg.timeout \"${_welcome_max}\")\"\n log \"$(msg install.welcome_msg.timeout_hint)\"\n log \"$(msg install.welcome_msg.timeout_inspect)\"\n fi\n else\n log \"$(msg install.welcome_msg.poll_unavailable)\"\n fi\n\n else\n # ============================================================\n # Legacy architecture: all-in-one manager container\n # ============================================================\n\n NETWORK_ARGS=\"--network agentteams-net\"\n NETWORK_ALIAS_ARGS=\"--network-alias matrix-local.agentteams.io --network-alias aigw-local.agentteams.io --network-alias fs-local.agentteams.io\"\n for _domain in \"${AGENTTEAMS_MATRIX_CLIENT_DOMAIN:-}\" \"${AGENTTEAMS_CONSOLE_DOMAIN:-}\"; do\n if [ -n \"${_domain}\" ] && [[ \"${_domain}\" == *-local.agentteams.io ]]; then\n NETWORK_ALIAS_ARGS=\"${NETWORK_ALIAS_ARGS} --network-alias ${_domain}\"\n fi\n done\n\n # Start Docker API proxy if enabled (security layer between Manager and Docker daemon)\n PROXY_ARGS=\"\"\n if [ \"${AGENTTEAMS_DOCKER_PROXY:-1}\" = \"1\" ] && [ -n \"${CONTAINER_SOCK:-}\" ]; then\n local _proxy_image=\"${AGENTTEAMS_REGISTRY}/higress/hiclaw-docker-proxy:${AGENTTEAMS_VERSION}\"\n # Try versioned tag, fallback to latest\n if ! ${DOCKER_CMD} image inspect \"${_proxy_image}\" >/dev/null 2>&1; then\n ${DOCKER_CMD} pull \"${_proxy_image}\" 2>/dev/null || {\n _proxy_image=\"${AGENTTEAMS_REGISTRY}/higress/hiclaw-docker-proxy:latest\"\n ${DOCKER_CMD} pull \"${_proxy_image}\" 2>/dev/null || true\n }\n fi\n if ${DOCKER_CMD} image inspect \"${_proxy_image}\" >/dev/null 2>&1; then\n log \"Starting Docker API proxy...\"\n ${DOCKER_CMD} run -d \\\n --name hiclaw-docker-proxy \\\n --network agentteams-net \\\n -v \"${CONTAINER_SOCK}:/var/run/docker.sock\" \\\n --security-opt label=disable \\\n -e AGENTTEAMS_WORKER_IMAGE=\"${WORKER_IMAGE}\" \\\n -e AGENTTEAMS_COPAW_WORKER_IMAGE=\"${COPAW_WORKER_IMAGE}\" \\\n -e AGENTTEAMS_HERMES_WORKER_IMAGE=\"${HERMES_WORKER_IMAGE}\" \\\n ${AGENTTEAMS_PROXY_ALLOWED_REGISTRIES:+-e AGENTTEAMS_PROXY_ALLOWED_REGISTRIES=\"${AGENTTEAMS_PROXY_ALLOWED_REGISTRIES}\"} \\\n --restart unless-stopped \\\n \"${_proxy_image}\"\n PROXY_ARGS=\"-e AGENTTEAMS_CONTROLLER_URL=http://hiclaw-docker-proxy:2375 -e AGENTTEAMS_CONTAINER_API=http://hiclaw-docker-proxy:2375\"\n SOCKET_MOUNT_ARGS=\"\"\n fi\n fi\n\n # Pass host timezone to container\n TZ_ARGS=\"\"\n if [ -n \"${AGENTTEAMS_TIMEZONE:-}\" ]; then\n TZ_ARGS=\"-e TZ=${AGENTTEAMS_TIMEZONE}\"\n fi\n\n YOLO_ARGS=\"\"\n if [ \"${AGENTTEAMS_YOLO:-}\" = \"1\" ]; then\n YOLO_ARGS=\"-e AGENTTEAMS_YOLO=1\"\n fi\n\n MATRIX_DEBUG_ARGS=\"\"\n if [ \"${AGENTTEAMS_MATRIX_DEBUG:-}\" = \"1\" ]; then\n MATRIX_DEBUG_ARGS=\"-e AGENTTEAMS_MATRIX_DEBUG=1\"\n fi\n\n # shellcheck disable=SC2086\n ${DOCKER_CMD} run -d \\\n --name agentteams-manager \\\n --env-file \"${ENV_FILE}\" \\\n -e HOME=/root/manager-workspace \\\n -w /root/manager-workspace \\\n -e HOST_ORIGINAL_HOME=\"${AGENTTEAMS_HOST_SHARE_DIR}\" \\\n -e AGENTTEAMS_MANAGER_RUNTIME=\"${AGENTTEAMS_MANAGER_RUNTIME:-copaw}\" \\\n ${JVM_ARGS:+-e JVM_ARGS=\"${JVM_ARGS}\"} \\\n ${YOLO_ARGS} \\\n ${MATRIX_DEBUG_ARGS} \\\n ${TZ_ARGS} \\\n ${SOCKET_MOUNT_ARGS} \\\n ${NETWORK_ARGS} \\\n ${NETWORK_ALIAS_ARGS} \\\n ${PROXY_ARGS} \\\n -p \"${_port_prefix}${AGENTTEAMS_PORT_GATEWAY}:8080\" \\\n -p \"${_port_prefix}${AGENTTEAMS_PORT_CONSOLE}:8001\" \\\n -p \"${_port_prefix}${AGENTTEAMS_PORT_ELEMENT_WEB:-18088}:8088\" \\\n -p \"127.0.0.1:${AGENTTEAMS_PORT_MANAGER_CONSOLE:-18888}:18888\" \\\n ${DATA_MOUNT_ARGS} \\\n ${WORKSPACE_MOUNT_ARGS} \\\n ${HOST_SHARE_MOUNT_ARGS} \\\n --restart unless-stopped \\\n \"$([ \"${AGENTTEAMS_MANAGER_RUNTIME}\" = \"copaw\" ] && echo \"${MANAGER_COPAW_IMAGE}\" || echo \"${MANAGER_IMAGE}\")\"\n\n # Wait for Manager agent to be ready\n wait_manager_ready \"agentteams-manager\"\n\n # Wait for Matrix server to be ready\n wait_matrix_ready \"agentteams-manager\"\n\n # Post-install verification (non-fatal: warnings only)\n local _verify_script\n _verify_script=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)/agentteams-verify.sh\"\n if [ -f \"${_verify_script}\" ]; then\n bash \"${_verify_script}\" \"agentteams-manager\" || {\n log \"WARNING: Some post-install checks failed. Re-run: bash install/agentteams-verify.sh\"\n }\n fi\n fi\n unset _port_prefix\n\n # Apply Podman autostart if selected and environment matches\n if [ \"${DOCKER_CMD}\" = \"podman\" ] && [ \"${AGENTTEAMS_PODMAN_AUTOSTART:-0}\" = \"1\" ]; then\n setup_podman_autostart\n fi\n\n log \"\"\n log \"$(msg success.title)\"\n log \"\"\n log \"$(msg success.domains_configured)\"\n log \" ${AGENTTEAMS_MATRIX_DOMAIN%%:*} ${AGENTTEAMS_MATRIX_CLIENT_DOMAIN} ${AGENTTEAMS_AI_GATEWAY_DOMAIN} ${AGENTTEAMS_FS_DOMAIN} ${AGENTTEAMS_CONSOLE_DOMAIN}\"\n log \"\"\n local lan_ip\n lan_ip=$(detect_lan_ip)\n echo -e \"\\033[33m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\033[0m\"\n echo -e \"\\033[33m $(msg success.open_url)\\033[0m\"\n echo -e \"\\033[33m \\033[0m\"\n echo -e \"\\033[1;36m http://127.0.0.1:${AGENTTEAMS_PORT_ELEMENT_WEB:-18088}/#/login\\033[0m\"\n echo -e \"\\033[33m \\033[0m\"\n echo -e \"\\033[33m $(msg success.login_with)\\033[0m\"\n echo -e \"\\033[33m $(msg success.username \"${AGENTTEAMS_ADMIN_USER}\")\\033[0m\"\n echo -e \"\\033[33m $(msg success.password \"${AGENTTEAMS_ADMIN_PASSWORD}\")\\033[0m\"\n echo -e \"\\033[33m \\033[0m\"\n echo -e \"\\033[33m $(msg success.after_login)\\033[0m\"\n echo -e \"\\033[33m $(msg success.tell_it)\\033[0m\"\n echo -e \"\\033[33m $(msg success.manager_auto)\\033[0m\"\n echo -e \"\\033[33m \\033[0m\"\n echo -e \"\\033[33m ───────────────────────────────────────────────────────────────────────────── \\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_title)\\033[0m\"\n echo -e \"\\033[33m \\033[0m\"\n if [ -n \"${lan_ip}\" ]; then\n echo -e \"\\033[33m $(msg success.mobile_step1)\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_step2 \"http://${lan_ip}:${AGENTTEAMS_PORT_GATEWAY}\")\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_step3)\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_username \"${AGENTTEAMS_ADMIN_USER}\")\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_password \"${AGENTTEAMS_ADMIN_PASSWORD}\")\\033[0m\"\n else\n echo -e \"\\033[33m $(msg success.mobile_step1)\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_step2_noip \"${AGENTTEAMS_PORT_GATEWAY}\")\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_noip_hint)\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_step3)\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_username \"${AGENTTEAMS_ADMIN_USER}\")\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_password \"${AGENTTEAMS_ADMIN_PASSWORD}\")\\033[0m\"\n fi\n echo -e \"\\033[33m \\033[0m\"\n echo -e \"\\033[33m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\033[0m\"\n log \"\"\n log \"$(msg success.other_consoles)\"\n log \"$(msg success.higress_console \"${AGENTTEAMS_PORT_CONSOLE}\" \"${AGENTTEAMS_ADMIN_USER}\" \"${AGENTTEAMS_ADMIN_PASSWORD}\")\"\n if [ \"${AGENTTEAMS_USE_EMBEDDED}\" != \"1\" ]; then\n log \"$(msg success.manager_console \"${AGENTTEAMS_PORT_MANAGER_CONSOLE:-18888}\")\"\n log \"$(msg success.manager_console_gateway \"${AGENTTEAMS_ADMIN_USER}\" \"${AGENTTEAMS_ADMIN_PASSWORD}\")\"\n fi\n log \"\"\n log \"$(msg success.switch_llm.title)\"\n log \"$(msg success.switch_llm.hint)\"\n log \"$(msg success.switch_llm.docs)\"\n log \"$(msg success.switch_llm.url)\"\n log \"\"\n log \"$(msg success.tip)\"\n log \"\"\n if [ \"${AGENTTEAMS_LOCAL_ONLY:-1}\" != \"1\" ]; then\n echo -e \"\\033[33m$(msg port.local_only.https_hint)\\033[0m\"\n log \"\"\n fi\n log \"$(msg success.config_file \"${ENV_FILE}\")\"\n log \"$(msg success.data_volume \"${AGENTTEAMS_DATA_DIR}\")\"\n log \"$(msg success.workspace \"${AGENTTEAMS_WORKSPACE_DIR}\")\"\n}\n\n# ============================================================\n# Worker Installation (One-Click)\n# ============================================================\n\ninstall_worker() {\n local WORKER_NAME=\"\"\n local FS=\"\"\n local FS_KEY=\"\"\n local FS_SECRET=\"\"\n local RESET=false\n local SKILLS_API_URL=\"\"\n\n # Parse arguments\n while [ $# -gt 0 ]; do\n case $1 in\n --name) WORKER_NAME=\"$2\"; shift 2 ;;\n --fs) FS=\"$2\"; shift 2 ;;\n --fs-key) FS_KEY=\"$2\"; shift 2 ;;\n --fs-secret) FS_SECRET=\"$2\"; shift 2 ;;\n --skills-api-url) SKILLS_API_URL=\"$2\"; shift 2 ;;\n --reset) RESET=true; shift ;;\n *) die \"$(msg error.unknown_option \"$1\")\" ;;\n esac\n done\n\n # Validate required params\n [ -z \"${WORKER_NAME}\" ] && die \"$(msg error.name_required)\"\n [ -z \"${FS}\" ] && die \"$(msg error.fs_required)\"\n [ -z \"${FS_KEY}\" ] && die \"$(msg error.fs_key_required)\"\n [ -z \"${FS_SECRET}\" ] && die \"$(msg error.fs_secret_required)\"\n\n local CONTAINER_NAME=\"agentteams-worker-${WORKER_NAME}\"\n\n # Handle reset\n if [ \"${RESET}\" = true ]; then\n log \"$(msg worker.resetting \"${WORKER_NAME}\")\"\n ${DOCKER_CMD} stop \"${CONTAINER_NAME}\" 2>/dev/null || true\n ${DOCKER_CMD} rm \"${CONTAINER_NAME}\" 2>/dev/null || true\n fi\n\n # Check for existing container\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' | grep -q \"^${CONTAINER_NAME}$\"; then\n die \"$(msg worker.exists \"${CONTAINER_NAME}\")\"\n fi\n\n log \"$(msg worker.starting \"${WORKER_NAME}\")\"\n\n # Build docker run args\n local DOCKER_ENV=\"\"\n DOCKER_ENV=\"${DOCKER_ENV} -e HOME=/root/agentteams-fs/agents/${WORKER_NAME}\"\n DOCKER_ENV=\"${DOCKER_ENV} -w /root/agentteams-fs/agents/${WORKER_NAME}\"\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_WORKER_NAME=${WORKER_NAME}\"\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_FS_ENDPOINT=${FS}\"\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_FS_ACCESS_KEY=${FS_KEY}\"\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_FS_SECRET_KEY=${FS_SECRET}\"\n\n if [ -z \"${SKILLS_API_URL}\" ]; then\n if [ -n \"${AGENTTEAMS_SKILLS_API_URL:-}\" ]; then\n SKILLS_API_URL=\"${AGENTTEAMS_SKILLS_API_URL}\"\n else\n SKILLS_API_URL=\"nacos://market.agentteams.io:80/public\"\n fi\n fi\n\n # Add SKILLS_API_URL if specified\n DOCKER_ENV=\"${DOCKER_ENV} -e SKILLS_API_URL=${SKILLS_API_URL}\"\n log \"$(msg worker.skills_url \"${SKILLS_API_URL}\")\"\n if [ -n \"${AGENTTEAMS_NACOS_USERNAME:-}\" ]; then\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_NACOS_USERNAME=${AGENTTEAMS_NACOS_USERNAME}\"\n fi\n if [ -n \"${AGENTTEAMS_NACOS_PASSWORD:-}\" ]; then\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_NACOS_PASSWORD=${AGENTTEAMS_NACOS_PASSWORD}\"\n fi\n if [ -n \"${AGENTTEAMS_NACOS_TOKEN:-}\" ]; then\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_NACOS_TOKEN=${AGENTTEAMS_NACOS_TOKEN}\"\n fi\n\n # shellcheck disable=SC2086\n ${DOCKER_CMD} run -d \\\n --name \"${CONTAINER_NAME}\" \\\n ${DOCKER_ENV} \\\n --restart unless-stopped \\\n \"${WORKER_IMAGE}\"\n\n log \"\"\n log \"$(msg worker.started \"${WORKER_NAME}\")\"\n log \"$(msg worker.container \"${CONTAINER_NAME}\")\"\n log \"$(msg worker.view_logs \"${CONTAINER_NAME}\")\"\n}\n\n# ============================================================\n# Main\n# ============================================================\n\n# ============================================================\n# LLM API connectivity test\n# ============================================================\n\ntest_llm_connectivity() {\n local base_url=\"$1\"\n local api_key=\"$2\"\n local model=\"$3\"\n local hint=\"${4:-}\" # optional: extra hint shown on failure\n if ! command -v curl >/dev/null 2>&1; then\n echo -e \"\\033[33m$(msg llm.openai.test.no_curl)\\033[0m\"\n return\n fi\n log \"$(msg llm.openai.test.testing)\"\n local _body _http_code _tmpfile\n _tmpfile=$(mktemp)\n _http_code=$(curl -s -o \"${_tmpfile}\" -w \"%{http_code}\" \\\n -X POST \"${base_url%/}/chat/completions\" \\\n -H \"Authorization: Bearer ${api_key}\" \\\n -H \"Content-Type: application/json\" \\\n -H \"User-Agent: AgentTeams/${AGENTTEAMS_VERSION:-latest}\" \\\n --max-time 30 \\\n -d \"{\\\"model\\\":\\\"${model}\\\",\\\"messages\\\":[{\\\"role\\\":\\\"user\\\",\\\"content\\\":\\\"hi\\\"}]}\" \\\n 2>/dev/null)\n _body=$(cat \"${_tmpfile}\")\n rm -f \"${_tmpfile}\"\n if [ \"${_http_code}\" = \"200\" ] || [ \"${_http_code}\" = \"201\" ]; then\n log \"$(msg llm.openai.test.ok)\"\n else\n echo -e \"\\033[33m$(msg llm.openai.test.fail \"${_http_code}\" \"${_body}\")\\033[0m\"\n if [ -n \"${hint}\" ]; then\n echo -e \"\\033[33m${hint}\\033[0m\"\n fi\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" != \"1\" ]; then\n local _confirm\n read -e -p \"$(msg llm.openai.test.confirm)\" _confirm\n if [ \"${_confirm}\" = \"b\" ]; then\n STEP_RESULT=\"back\"\n return 1\n fi\n if [ \"${_confirm}\" != \"y\" ] && [ \"${_confirm}\" != \"Y\" ]; then\n log \"$(msg llm.openai.test.aborted)\"\n exit 1\n fi\n fi\n fi\n}\n\ntest_embedding_connectivity() {\n local base_url=\"$1\"\n local api_key=\"$2\"\n local model=\"$3\"\n if ! command -v curl >/dev/null 2>&1; then\n return 0\n fi\n log \"$(msg llm.embedding.test.testing)\"\n local _body _http_code _tmpfile\n _tmpfile=$(mktemp)\n _http_code=$(curl -s -o \"${_tmpfile}\" -w \"%{http_code}\" \\\n -X POST \"${base_url%/}/embeddings\" \\\n -H \"Authorization: Bearer ${api_key}\" \\\n -H \"Content-Type: application/json\" \\\n -H \"User-Agent: AgentTeams/${AGENTTEAMS_VERSION:-latest}\" \\\n --max-time 30 \\\n -d \"{\\\"model\\\":\\\"${model}\\\",\\\"input\\\":\\\"test\\\"}\" \\\n 2>/dev/null)\n _body=$(cat \"${_tmpfile}\")\n rm -f \"${_tmpfile}\"\n if [ \"${_http_code}\" = \"200\" ] || [ \"${_http_code}\" = \"201\" ]; then\n log \"$(msg llm.embedding.test.ok)\"\n return 0\n else\n echo -e \"\\033[33m$(msg llm.embedding.test.fail \"${_http_code}\" \"${_body}\")\\033[0m\"\n return 1\n fi\n}\n\n# ============================================================\n# Uninstall\n# ============================================================\n\nuninstall_agentteams() {\n log \"$(msg uninstall.title)\"\n\n # Safely disable and remove the dedicated AgentTeams Podman autostart service\n if [ \"${DOCKER_CMD}\" = \"podman\" ] && command -v systemctl >/dev/null 2>&1; then\n local _systemctl_cmd=\"systemctl\"\n local _service_dir=\"/etc/systemd/system\"\n\n if [ \"$(id -u)\" != \"0\" ]; then\n _systemctl_cmd=\"systemctl --user\"\n _service_dir=\"${HOME}/.config/systemd/user\"\n fi\n\n if [ -f \"${_service_dir}/agentteams-podman-restart.service\" ]; then\n log \"Disabling and removing dedicated AgentTeams Podman autostart service...\"\n ${_systemctl_cmd} disable --now agentteams-podman-restart.service 2>/dev/null || true\n rm -f \"${_service_dir}/agentteams-podman-restart.service\"\n ${_systemctl_cmd} daemon-reload 2>/dev/null || true\n fi\n if [ -f \"${_service_dir}/hiclaw-podman-restart.service\" ]; then\n ${_systemctl_cmd} disable --now hiclaw-podman-restart.service 2>/dev/null || true\n rm -f \"${_service_dir}/hiclaw-podman-restart.service\"\n ${_systemctl_cmd} daemon-reload 2>/dev/null || true\n fi\n # Note: Native podman-restart.service is strictly untouched per maintainer review.\n fi\n\n # Capture manager image before removing (needed for workspace cleanup on Linux)\n local manager_image=\"\"\n manager_image=$(${DOCKER_CMD} inspect agentteams-manager --format '{{.Config.Image}}' 2>/dev/null || true)\n\n # Stop and remove manager\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -q \"^agentteams-manager$\"; then\n log \"$(msg uninstall.stopping_manager)\"\n ${DOCKER_CMD} stop agentteams-manager >/dev/null 2>&1 || true\n ${DOCKER_CMD} rm agentteams-manager >/dev/null 2>&1 || true\n fi\n\n # Stop and remove workers\n local workers\n workers=$(${DOCKER_CMD} ps -a --filter \"name=agentteams-worker-\" --format '{{.Names}}' 2>/dev/null || true)\n if [ -n \"${workers}\" ]; then\n log \"$(msg uninstall.stopping_workers)\"\n echo \"${workers}\" | while read -r w; do\n ${DOCKER_CMD} rm -f \"${w}\" >/dev/null 2>&1 || true\n log \"$(msg uninstall.removed \"${w}\")\"\n done\n fi\n\n # Stop and remove docker-proxy (legacy ≤ v1.0.x; current arch uses\n # agentteams-controller for the same role)\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -q \"^hiclaw-docker-proxy$\"; then\n log \"$(msg uninstall.removing_proxy)\"\n ${DOCKER_CMD} stop hiclaw-docker-proxy >/dev/null 2>&1 || true\n ${DOCKER_CMD} rm hiclaw-docker-proxy >/dev/null 2>&1 || true\n fi\n\n # Stop and remove the embedded controller container. MUST happen\n # before the `docker volume rm agentteams-data` step below — in embedded\n # mode agentteams-controller mounts agentteams-data at /data (Tuwunel DB,\n # MinIO state, Higress state, all the room messages), and `volume\n # rm` against an in-use volume fails silently because of the trailing\n # `|| true`. Skipping this used to leave room/message history behind\n # across \"uninstall + reinstall\" cycles. See PR #692.\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -q \"^agentteams-controller$\"; then\n log \"$(msg uninstall.stopping_controller)\"\n ${DOCKER_CMD} stop agentteams-controller >/dev/null 2>&1 || true\n ${DOCKER_CMD} rm agentteams-controller >/dev/null 2>&1 || true\n fi\n\n # Read env file for data/workspace info before removing\n local env_file=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n [ ! -f \"${env_file}\" ] && [ -f \"./agentteams-manager.env\" ] && env_file=\"./agentteams-manager.env\"\n\n local data_dir=\"\" workspace_dir=\"\"\n if [ -f \"${env_file}\" ]; then\n data_dir=$(grep '^AGENTTEAMS_DATA_DIR=' \"${env_file}\" 2>/dev/null | cut -d= -f2- || true)\n workspace_dir=$(grep '^AGENTTEAMS_WORKSPACE_DIR=' \"${env_file}\" 2>/dev/null | cut -d= -f2- || true)\n fi\n\n # Remove Docker volume\n local vol=\"${data_dir:-agentteams-data}\"\n if ${DOCKER_CMD} volume inspect \"${vol}\" >/dev/null 2>&1; then\n log \"$(msg uninstall.removing_volume \"${vol}\")\"\n ${DOCKER_CMD} volume rm \"${vol}\" >/dev/null 2>&1 || true\n fi\n\n # Remove Docker network\n if ${DOCKER_CMD} network ls --format '{{.Name}}' 2>/dev/null | grep -q \"^agentteams-net$\"; then\n log \"$(msg uninstall.removing_network)\"\n ${DOCKER_CMD} network rm agentteams-net >/dev/null 2>&1 || true\n fi\n\n # Remove workspace directory\n if [ -n \"${workspace_dir}\" ] && [ -d \"${workspace_dir}\" ]; then\n log \"$(msg uninstall.removing_workspace \"${workspace_dir}\")\"\n rm -rf \"${workspace_dir}\" 2>/dev/null || true\n if [ -d \"${workspace_dir}\" ]; then\n log \"$(msg uninstall.removing_workspace_elevated)\"\n local _parent _base\n _parent=$(dirname \"${workspace_dir}\")\n _base=$(basename \"${workspace_dir}\")\n local _rm_image=\"${manager_image:-busybox}\"\n ${DOCKER_CMD} run --rm --entrypoint sh \\\n -v \"${_parent}:/host-parent\" \\\n \"${_rm_image}\" \\\n -c \"rm -rf /host-parent/${_base}\" 2>/dev/null || true\n fi\n fi\n\n # Remove env file\n if [ -f \"${env_file}\" ]; then\n log \"$(msg uninstall.removing_env \"${env_file}\")\"\n rm -f \"${env_file}\"\n fi\n\n # Remove install log\n if [ -f \"${HOME}/agentteams-install.log\" ]; then\n log \"$(msg uninstall.removing_log \"${HOME}/agentteams-install.log\")\"\n rm -f \"${HOME}/agentteams-install.log\"\n fi\n\n echo \"\"\n log \"$(msg uninstall.done)\"\n}\n\n# ============================================================\n# Check container runtime (docker or podman) and environment\n# ============================================================\ncheck_container_runtime() {\n # Ensure the rootless environment baseline (XDG_RUNTIME_DIR) is ready before engine detection.\n if [ \"$(id -u)\" -ne 0 ] && [ -z \"${XDG_RUNTIME_DIR:-}\" ]; then\n export XDG_RUNTIME_DIR=\"/run/user/$(id -u)\"\n fi\n\n if command -v docker >/dev/null 2>&1; then\n DOCKER_CMD=\"docker\"\n # Check for podman disguised as docker (alias or podman-docker package)\n if docker --version 2>/dev/null | grep -qi \"podman\"; then\n DOCKER_CMD=\"podman\"\n fi\n elif command -v podman >/dev/null 2>&1; then\n DOCKER_CMD=\"podman\"\n else\n echo -e \"\\033[31m[AgentTeams ERROR]\\033[0m $(msg error.docker_not_found)\" >&2\n exit 1\n fi\n\n # Unified health check (Validates runtime responsiveness)\n if ! ${DOCKER_CMD} ps >/dev/null 2>&1; then\n echo -e \"\\033[31m[AgentTeams ERROR]\\033[0m $(msg error.docker_not_running)\" >&2\n exit 1\n fi\n}\n\ncheck_container_runtime\n\ncase \"${1:-}\" in\n manager|\"\")\n install_manager\n ;;\n worker)\n shift\n install_worker \"$@\"\n ;;\n uninstall)\n uninstall_agentteams\n ;;\n *)\n echo \"Usage: $0 [manager|worker [options]|uninstall]\"\n echo \"\"\n echo \"Commands:\"\n echo \" manager Interactive Manager installation (default)\"\n echo \" Choose Quick Start (all defaults) or Manual mode\"\n echo \" worker Worker installation (requires --name and connection params)\"\n echo \" uninstall Stop and remove Manager + all Worker containers\"\n echo \"\"\n echo \"Quick Start (fastest):\"\n echo \" $0\"\n echo \" # Then select '1' for Quick Start mode\"\n echo \"\"\n echo \"Non-interactive (for automation):\"\n echo \" AGENTTEAMS_NON_INTERACTIVE=1 AGENTTEAMS_LLM_API_KEY=sk-xxx $0\"\n echo \"\"\n echo \"Worker Options:\"\n echo \" --name <name> Worker name (required)\"\n echo \" --fs <url> MinIO endpoint URL (required)\"\n echo \" --fs-key <key> MinIO access key (required)\"\n echo \" --fs-secret <secret> MinIO secret key (required)\"\n echo \" --reset Remove existing Worker container before creating\"\n exit 1\n ;;\nesac\n"}, "1145f796b3de8b9e07c298add26d0d825c48dc39": {"t": 1786067818.3234963, "n": 248781, "h": "ed80ee68e8132683", "c": "#!/bin/bash\n# agentteams-install.sh - One-click installation for AgentTeams Manager, Worker, and Dashboard\n#\n# Usage:\n# ./agentteams-install.sh # Interactive installation (choose Quick Start or Manual)\n# ./agentteams-install.sh manager # Same as above (explicit)\n# ./agentteams-install.sh worker --name <name> ... # Worker installation\n# ./agentteams-install.sh dashboard # Start/stop agentteams-dashboard (AGENTTEAMS_DASHBOARD=0 to stop)\n# ./agentteams-install.sh uninstall # Stop and remove Manager + all Workers\n#\n# NOTE: agentteams-dashboard is currently only supported via the Bash installer.\n# The PowerShell installer (agentteams-install.ps1) does not yet include dashboard support.\n#\n# Onboarding Modes:\n# Quick Start - Fast installation with all default values (recommended)\n# Manual - Customize each option step by step\n#\n# Environment variables (for automation):\n# AGENTTEAMS_NON_INTERACTIVE Skip all prompts, use defaults (default: 0)\n# AGENTTEAMS_LLM_PROVIDER LLM provider (default: openai-compat for zh non-interactive Token Plan; qwen for en)\n# AGENTTEAMS_DEFAULT_MODEL Default model (default: qwen3.6-plus for zh Token Plan and en non-interactive)\n# AGENTTEAMS_OPENAI_BASE_URL OpenAI-compatible base URL (default for zh non-interactive: Alibaba Token Plan endpoint)\n# AGENTTEAMS_LLM_API_KEY LLM API key (required)\n# AGENTTEAMS_ADMIN_USER Admin username (default: admin)\n# AGENTTEAMS_ADMIN_PASSWORD Admin password (auto-generated if not set, min 8 chars)\n# AGENTTEAMS_MATRIX_DOMAIN Matrix domain (default: matrix-local.agentteams.io:18080)\n# AGENTTEAMS_MOUNT_SOCKET Mount container runtime socket (default: 1)\n# AGENTTEAMS_DATA_DIR Docker volume name for persistent data (default: agentteams-data)\n# AGENTTEAMS_WORKSPACE_DIR Host directory for manager workspace (default: ~/agentteams-manager)\n# AGENTTEAMS_VERSION Image tag (default: latest)\n# AGENTTEAMS_REGISTRY Image registry (default: auto-detected by timezone)\n# AGENTTEAMS_INSTALL_MANAGER_IMAGE Override manager image (e.g., local build)\n# AGENTTEAMS_INSTALL_WORKER_IMAGE Override worker image (e.g., local build)\n# AGENTTEAMS_INSTALL_COPAW_WORKER_IMAGE Override copaw worker image (e.g., local build)\n# AGENTTEAMS_INSTALL_HERMES_WORKER_IMAGE Override hermes worker image (e.g., local build)\n# AGENTTEAMS_NACOS_REGISTRY_URI Default Nacos registry URI for Worker market search/import\n# (default: nacos://market.agentteams.io:80/public)\n# AGENTTEAMS_NACOS_USERNAME Default Nacos username for nacos:// package imports (optional)\n# AGENTTEAMS_NACOS_PASSWORD Default Nacos password for nacos:// package imports (optional)\n# AGENTTEAMS_CMS_TRACES_ENABLED Enable openclaw-cms-plugin traces for Manager AND all Workers (default: false)\n# AGENTTEAMS_CMS_ENDPOINT ARMS OTLP endpoint (required if traces enabled)\n# AGENTTEAMS_CMS_LICENSE_KEY CMS license key (required if traces enabled)\n# AGENTTEAMS_CMS_PROJECT CMS project name (optional)\n# AGENTTEAMS_CMS_WORKSPACE CMS workspace ID (required if traces enabled)\n# AGENTTEAMS_CMS_SERVICE_NAME Manager service name in ARMS (default: agentteams-manager)\n# Workers always report as agentteams-worker-<name> automatically\n# AGENTTEAMS_CMS_METRICS_ENABLED Enable diagnostics-otel metrics for Manager AND all Workers (default: false)\n# AGENTTEAMS_PORT_GATEWAY Host port for Higress gateway (default: 18080)\n# AGENTTEAMS_PORT_CONSOLE Host port for Higress console (default: 18001)\n# AGENTTEAMS_PORT_ELEMENT_WEB Host port for Element Web direct access (default: 18088)\n# AGENTTEAMS_PORT_MANAGER_CONSOLE Host port for Manager console (default: 18888)\n# AGENTTEAMS_WORKER_IDLE_TIMEOUT Worker idle timeout in minutes (default: 720, i.e. 12 hours)\n# AGENTTEAMS_DASHBOARD Install agentteams-dashboard management UI (default: 1)\n# AGENTTEAMS_DASHBOARD_VERSION Dashboard version (default: v1.2.0, independent of AgentTeams version)\n# AGENTTEAMS_PORT_DASHBOARD Dashboard host port (default: 13000)\n# AGENTTEAMS_DASHBOARD_IMAGE Override dashboard image (default: <registry>/agentteams/agentteams-dashboard:<DASHBOARD_VERSION>)\n# AGENTTEAMS_AI_GATEWAY_ADMIN_URL Higress Console URL for shared auth (auto-detected)\n\nset -e\n\nAGENTTEAMS_VERSION=\"${AGENTTEAMS_VERSION:-}\"\nAGENTTEAMS_KNOWN_STABLE_VERSION=\"v1.2.0\" # fallback if GitHub API is unreachable\n\n_normalize_version() {\n local version=\"$1\"\n case \"${version}\" in\n [0-9]*.[0-9]*.[0-9]*) version=\"v${version}\" ;;\n esac\n case \"${version}\" in\n v[0-9]*.[0-9]*.[0-9]*.beta.[0-9]*) version=\"${version/.beta./-beta.}\" ;;\n esac\n printf '%s' \"${version}\"\n}\n\n# Returns 0 (true) if $1 < $2 using semver order; \"latest\" is treated as greatest\n_ver_lt() {\n [ \"$1\" = \"latest\" ] && return 1\n [ \"$2\" = \"latest\" ] && return 0\n [ \"$1\" = \"$2\" ] && return 1\n [ \"$(printf '%s\\n%s' \"$1\" \"$2\" | sort -V | head -1)\" = \"$1\" ]\n}\n\n_use_legacy_image_env() {\n if [ \"$1\" = \"latest\" ]; then\n _ver_lt \"${AGENTTEAMS_KNOWN_STABLE_VERSION}\" \"v1.2.0\"\n else\n _ver_lt \"$1\" \"v1.2.0\"\n fi\n}\n\n_controller_env_prefix() {\n if _use_legacy_image_env \"$1\"; then\n printf '%s%s' 'HIC' 'LAW_'\n else\n printf '%s' 'AGENTTEAMS_'\n fi\n}\n\n_controller_storage_prefix() {\n if _use_legacy_image_env \"$1\"; then\n printf '%s%s' 'hic' 'law/agentteams-storage'\n else\n printf '%s' 'agentteams/agentteams-storage'\n fi\n}\nAGENTTEAMS_NON_INTERACTIVE=\"${AGENTTEAMS_NON_INTERACTIVE:-0}\"\nAGENTTEAMS_MOUNT_SOCKET=\"${AGENTTEAMS_MOUNT_SOCKET:-1}\"\nAGENTTEAMS_DOCKER_PROXY=\"${AGENTTEAMS_DOCKER_PROXY:-1}\"\nSTEP_RESULT=\"\" # Used by state machine to signal \"back\" navigation\n\n# ============================================================\n# Log all output to file\n# ============================================================\n\nAGENTTEAMS_LOG_FILE=\"${HOME}/agentteams-install.log\"\n\nif [ \"${1:-}\" != \"uninstall\" ]; then\n # Redirect all output (stdout and stderr) to both terminal and log file\n exec > >(tee -a \"${AGENTTEAMS_LOG_FILE}\") 2>&1\n\n echo \"\"\n echo \"========================================\"\n echo \"AgentTeams Installation Log\"\n echo \"Started: $(date)\"\n echo \"User: $(whoami)\"\n echo \"System: $(uname -a)\"\n echo \"Log file: ${AGENTTEAMS_LOG_FILE}\"\n echo \"========================================\"\n echo \"\"\nfi\n\n# ============================================================\n# Utility functions (needed early for timezone detection)\n# ============================================================\n\nlog() {\n echo -e \"\\033[36m[AgentTeams]\\033[0m $1\"\n}\n\nerror() {\n echo -e \"\\033[31m[AgentTeams ERROR]\\033[0m $1\" >&2\n}\n\ndie() {\n error \"$1\"\n exit 1\n}\n\n# ============================================================\n# Timezone detection (compatible with Linux and macOS)\n# ============================================================\n\ndetect_timezone() {\n local tz=\"\"\n\n # Try /etc/timezone (Debian/Ubuntu)\n if [ -f /etc/timezone ]; then\n tz=$(cat /etc/timezone 2>/dev/null | tr -d '[:space:]')\n fi\n\n # Try /etc/localtime symlink (macOS and some Linux)\n if [ -z \"${tz}\" ] && [ -L /etc/localtime ]; then\n tz=$(ls -l /etc/localtime 2>/dev/null | sed 's|.*/zoneinfo/||')\n fi\n\n # Try timedatectl (systemd)\n if [ -z \"${tz}\" ]; then\n tz=$(timedatectl show --value -p Timezone 2>/dev/null)\n fi\n\n # If still not detected, warn and prompt user. Diagnostics go to stderr —\n # this function returns its value via stdout to `$(detect_timezone)`, so\n # any echo here would leak into AGENTTEAMS_TIMEZONE and downstream image tags.\n if [ -z \"${tz}\" ]; then\n echo \"\" >&2\n echo -e \"\\033[33m[AgentTeams WARNING]\\033[0m Could not detect timezone automatically.\" >&2\n echo -e \"\\033[33m[AgentTeams]\\033[0m Please enter your timezone (e.g., Asia/Shanghai, America/New_York).\" >&2\n echo \"\" >&2\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n tz=\"Asia/Shanghai\"\n log \"Using default timezone: ${tz}\" >&2\n else\n read -e -p \"Timezone [Asia/Shanghai]: \" tz\n tz=\"${tz:-Asia/Shanghai}\"\n fi\n fi\n\n echo \"${tz}\"\n}\n\n# Detect timezone once at startup (used by registry selection and container TZ)\nAGENTTEAMS_TIMEZONE=\"${AGENTTEAMS_TIMEZONE:-$(detect_timezone)}\"\n\n# ============================================================\n# Language detection based on timezone\n# ============================================================\n\ndetect_language() {\n local tz=\"${AGENTTEAMS_TIMEZONE}\"\n case \"${tz}\" in\n Asia/Shanghai|Asia/Chongqing|Asia/Harbin|Asia/Urumqi|\\\n Asia/Taipei|Asia/Hong_Kong|Asia/Macau)\n echo \"zh\"\n ;;\n *)\n echo \"en\"\n ;;\n esac\n}\n\n# Language priority: env var > existing env file > timezone detection\nif [ -z \"${AGENTTEAMS_LANGUAGE}\" ]; then\n # Check existing env file for saved language preference (upgrade scenario)\n _env_file=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n # Migrate from legacy location (current directory) if needed\n if [ ! -f \"${_env_file}\" ] && [ -f \"./agentteams-manager.env\" ]; then\n mv \"./agentteams-manager.env\" \"${_env_file}\" 2>/dev/null || true\n fi\n if [ -f \"${_env_file}\" ]; then\n _saved_lang=$(grep '^AGENTTEAMS_LANGUAGE=' \"${_env_file}\" 2>/dev/null | cut -d= -f2-)\n if [ -n \"${_saved_lang}\" ]; then\n AGENTTEAMS_LANGUAGE=\"${_saved_lang}\"\n fi\n fi\n # Fall back to timezone-based detection\n if [ -z \"${AGENTTEAMS_LANGUAGE}\" ]; then\n AGENTTEAMS_LANGUAGE=\"$(detect_language)\"\n fi\n unset _env_file _saved_lang\nfi\nexport AGENTTEAMS_LANGUAGE\n\n# ============================================================\n# Centralized message dictionary and msg() function\n# Compatible with bash 3.2+ (macOS default) — uses case instead of declare -A\n# ============================================================\n\n# msg() function: look up message by key, with printf-style argument substitution\n# Falls back to English if the current language translation is missing.\nmsg() {\n local key=\"$1\"\n shift\n local lang=\"${AGENTTEAMS_LANGUAGE:-en}\"\n local text=\"\"\n case \"${key}.${lang}\" in\n # --- Timezone detection messages ---\n \"tz.warning.title.zh\") text=\"无法自动检测时区。\" ;;\n \"tz.warning.title.en\") text=\"Could not detect timezone automatically.\" ;;\n \"tz.warning.prompt.zh\") text=\"请输入您的时区(例如 Asia/Shanghai、America/New_York)。\" ;;\n \"tz.warning.prompt.en\") text=\"Please enter your timezone (e.g., Asia/Shanghai, America/New_York).\" ;;\n \"tz.default.zh\") text=\"使用默认时区: %s\" ;;\n \"tz.default.en\") text=\"Using default timezone: %s\" ;;\n \"tz.input_prompt.zh\") text=\"时区\" ;;\n \"tz.input_prompt.en\") text=\"Timezone\" ;;\n # --- Installation title and info ---\n \"install.title.zh\") text=\"=== AgentTeams Manager 安装 ===\" ;;\n \"install.title.en\") text=\"=== AgentTeams Manager Installation ===\" ;;\n \"install.registry.zh\") text=\"镜像仓库: %s\" ;;\n \"install.registry.en\") text=\"Registry: %s\" ;;\n \"install.dir.zh\") text=\"安装目录: %s\" ;;\n \"install.dir.en\") text=\"Installation directory: %s\" ;;\n \"install.dir_hint.zh\") text=\" (env 文件 'agentteams-manager.env' 将保存到 HOME 目录。)\" ;;\n \"install.dir_hint.en\") text=\" (The env file 'agentteams-manager.env' will be saved to your HOME directory.)\" ;;\n \"install.dir_hint2.zh\") text=\" (请从您希望管理此安装的目录运行此脚本。)\" ;;\n \"install.dir_hint2.en\") text=\" (Run this script from the directory where you want to manage this installation.)\" ;;\n # --- Onboarding mode ---\n \"install.mode.title.zh\") text=\"--- Onboarding 模式 ---\" ;;\n \"install.mode.title.en\") text=\"--- Onboarding Mode ---\" ;;\n \"install.mode.choose.zh\") text=\"选择安装模式:\" ;;\n \"install.mode.choose.en\") text=\"Choose your installation mode:\" ;;\n \"install.mode.quickstart.zh\") text=\" 1) 快速开始 - 使用阿里云通义 Token 套餐快速安装(推荐)\" ;;\n \"install.mode.quickstart.en\") text=\" 1) Quick Start - Fast installation with Qwen Cloud (recommended)\" ;;\n \"install.mode.manual.zh\") text=\" 2) 手动配置 - 选择 LLM 提供商并自定义选项\" ;;\n \"install.mode.manual.en\") text=\" 2) Manual - Choose LLM provider and customize options\" ;;\n \"install.mode.prompt.zh\") text=\"请选择 [1/2]\" ;;\n \"install.mode.prompt.en\") text=\"Enter choice [1/2]\" ;;\n \"install.mode.quickstart_selected.zh\") text=\"已选择快速开始模式 - 使用阿里云通义 Token 套餐\" ;;\n \"install.mode.quickstart_selected.en\") text=\"Quick Start mode selected - using Qwen Cloud\" ;;\n \"install.mode.manual_selected.zh\") text=\"已选择手动配置模式 - 您将选择 LLM 提供商并自定义选项\" ;;\n \"install.mode.manual_selected.en\") text=\"Manual mode selected - you will choose LLM provider and customize options\" ;;\n \"install.mode.invalid.zh\") text=\"无效选择,默认使用快速开始模式\" ;;\n \"install.mode.invalid.en\") text=\"Invalid choice, defaulting to Quick Start mode\" ;;\n # --- Upgrade mode sub-menu ---\n \"upgrade.mode.prompt.zh\") text=\"请选择升级方式:\" ;;\n \"upgrade.mode.prompt.en\") text=\"Select upgrade mode:\" ;;\n \"upgrade.mode.keep_all.zh\") text=\" 1) 保留所有参数,一键升级(推荐)\" ;;\n \"upgrade.mode.keep_all.en\") text=\" 1) Keep all parameters, quick upgrade (recommended)\" ;;\n \"upgrade.mode.confirm_each.zh\") text=\" 2) 逐个确认参数(可修改)\" ;;\n \"upgrade.mode.confirm_each.en\") text=\" 2) Confirm each parameter (can modify)\" ;;\n \"upgrade.mode.back.zh\") text=\" 3) 返回\" ;;\n \"upgrade.mode.back.en\") text=\" 3) Back\" ;;\n # --- Version selection ---\n \"install.version.title.zh\") text=\"--- 版本选择 ---\" ;;\n \"install.version.title.en\") text=\"--- Version Selection ---\" ;;\n \"install.version.choose.zh\") text=\"选择要安装的版本:\" ;;\n \"install.version.choose.en\") text=\"Choose the version to install:\" ;;\n \"install.version.option_latest.zh\") text=\" 1) latest - 最新版(默认)\" ;;\n \"install.version.option_latest.en\") text=\" 1) latest - Latest build (default)\" ;;\n \"install.version.option_stable.zh\") text=\" 2) %s - 最新稳定版\" ;;\n \"install.version.option_stable.en\") text=\" 2) %s - Latest stable release\" ;;\n \"install.version.fetching.zh\") text=\"正在查询最新稳定版本...\" ;;\n \"install.version.fetching.en\") text=\"Fetching latest stable release...\" ;;\n \"install.version.fetch_failed.zh\") text=\"无法查询 GitHub,使用内置版本 %s\" ;;\n \"install.version.fetch_failed.en\") text=\"Could not reach GitHub, using built-in version %s\" ;;\n \"install.version.option_custom.zh\") text=\" 3) 自定义 - 手动输入版本号(如 v1.0.5)\" ;;\n \"install.version.option_custom.en\") text=\" 3) Custom - Enter a specific version (e.g. v1.0.5)\" ;;\n \"install.version.prompt.zh\") text=\"请选择 [1/2/3]\" ;;\n \"install.version.prompt.en\") text=\"Enter choice [1/2/3]\" ;;\n \"install.version.custom_prompt.zh\") text=\"请输入版本号\" ;;\n \"install.version.custom_prompt.en\") text=\"Enter version tag\" ;;\n \"install.version.selected_latest.zh\") text=\"已选择最新版 (latest)\" ;;\n \"install.version.selected_latest.en\") text=\"Selected latest version\" ;;\n \"install.version.selected_stable.zh\") text=\"已选择最新稳定版 (%s)\" ;;\n \"install.version.selected_stable.en\") text=\"Selected latest stable version (%s)\" ;;\n \"install.version.selected_custom.zh\") text=\"已选择自定义版本 (%s)\" ;;\n \"install.version.selected_custom.en\") text=\"Selected custom version (%s)\" ;;\n \"install.version.invalid.zh\") text=\"无效选择,使用最新稳定版 (%s)\" ;;\n \"install.version.invalid.en\") text=\"Invalid choice, defaulting to latest stable version (%s)\" ;;\n # --- Existing installation detected ---\n \"install.existing.detected.zh\") text=\"检测到已有 Manager 安装(env 文件: %s)\" ;;\n \"install.existing.detected.en\") text=\"Existing Manager installation detected (env file: %s)\" ;;\n \"install.existing.choose.zh\") text=\"选择操作:\" ;;\n \"install.existing.choose.en\") text=\"Choose an action:\" ;;\n \"install.existing.upgrade.zh\") text=\" 1) 就地升级(保留数据、工作空间、env 文件)\" ;;\n \"install.existing.upgrade.en\") text=\" 1) In-place upgrade (keep data, workspace, env file)\" ;;\n \"install.existing.reinstall.zh\") text=\" 2) 全新重装(删除所有数据,重新开始)\" ;;\n \"install.existing.reinstall.en\") text=\" 2) Clean reinstall (remove all data, start fresh)\" ;;\n \"install.existing.cancel.zh\") text=\" 3) 取消\" ;;\n \"install.existing.cancel.en\") text=\" 3) Cancel\" ;;\n \"install.existing.prompt.zh\") text=\"请选择 [1/2/3]\" ;;\n \"install.existing.prompt.en\") text=\"Enter choice [1/2/3]\" ;;\n \"install.existing.upgrade_noninteractive.zh\") text=\"非交互模式: 执行就地升级...\" ;;\n \"install.existing.upgrade_noninteractive.en\") text=\"Non-interactive mode: performing in-place upgrade...\" ;;\n \"install.existing.upgrading.zh\") text=\"执行就地升级...\" ;;\n \"install.existing.upgrading.en\") text=\"Performing in-place upgrade...\" ;;\n \"install.existing.warn_manager_stop.zh\") text=\"⚠️ Manager 容器将被停止并重新创建。\" ;;\n \"install.existing.warn_manager_stop.en\") text=\"⚠️ Manager container will be stopped and recreated.\" ;;\n \"install.existing.warn_worker_recreate.zh\") text=\"⚠️ Worker 容器也将被重新创建(以更新 Manager IP)。\" ;;\n \"install.existing.warn_worker_recreate.en\") text=\"⚠️ Worker containers will also be recreated (to update Manager IP in hosts).\" ;;\n \"install.existing.continue_prompt.zh\") text=\"继续?[y/N]\" ;;\n \"install.existing.continue_prompt.en\") text=\"Continue? [y/N]\" ;;\n \"install.existing.cancelled.zh\") text=\"安装已取消。\" ;;\n \"install.existing.cancelled.en\") text=\"Installation cancelled.\" ;;\n \"install.existing.stopping_manager.zh\") text=\"停止并移除现有 manager 容器...\" ;;\n \"install.existing.stopping_manager.en\") text=\"Stopping and removing existing manager container...\" ;;\n \"install.existing.stopping_workers.zh\") text=\"停止并移除现有 worker 容器...\" ;;\n \"install.existing.stopping_workers.en\") text=\"Stopping and removing existing worker containers...\" ;;\n \"install.existing.removed.zh\") text=\" 已移除: %s\" ;;\n \"install.existing.removed.en\") text=\" Removed: %s\" ;;\n # --- Clean reinstall messages ---\n \"install.reinstall.performing.zh\") text=\"执行全新重装...\" ;;\n \"install.reinstall.performing.en\") text=\"Performing clean reinstall...\" ;;\n \"install.reinstall.warn_stop.zh\") text=\"⚠️ 以下运行中的容器将被停止:\" ;;\n \"install.reinstall.warn_stop.en\") text=\"⚠️ The following running containers will be stopped:\" ;;\n \"install.reinstall.warn_delete.zh\") text=\"⚠️ 警告: 以下内容将被删除:\" ;;\n \"install.reinstall.warn_delete.en\") text=\"⚠️ WARNING: This will DELETE the following:\" ;;\n \"install.reinstall.warn_volume.zh\") text=\" - Docker 卷: agentteams-data\" ;;\n \"install.reinstall.warn_volume.en\") text=\" - Docker volume: agentteams-data\" ;;\n \"install.reinstall.warn_env.zh\") text=\" - Env 文件: %s\" ;;\n \"install.reinstall.warn_env.en\") text=\" - Env file: %s\" ;;\n \"install.reinstall.warn_workspace.zh\") text=\" - Manager 工作空间: %s\" ;;\n \"install.reinstall.warn_workspace.en\") text=\" - Manager workspace: %s\" ;;\n \"install.reinstall.warn_workers.zh\") text=\" - 所有 worker 容器\" ;;\n \"install.reinstall.warn_workers.en\") text=\" - All worker containers\" ;;\n \"install.reinstall.warn_proxy.zh\") text=\" - Docker API 代理容器: agentteams-controller\" ;;\n \"install.reinstall.warn_proxy.en\") text=\" - Docker API proxy container: agentteams-controller\" ;;\n \"install.reinstall.warn_network.zh\") text=\" - Docker 网络: agentteams-net\" ;;\n \"install.reinstall.warn_network.en\") text=\" - Docker network: agentteams-net\" ;;\n \"install.reinstall.confirm_type.zh\") text=\"请输入工作空间路径以确认删除(或按 Ctrl+C 取消):\" ;;\n \"install.reinstall.confirm_type.en\") text=\"To confirm deletion, please type the workspace path:\" ;;\n \"install.reinstall.confirm_path.zh\") text=\"输入路径以确认(或按 Ctrl+C 取消)\" ;;\n \"install.reinstall.confirm_path.en\") text=\"Type the path to confirm (or press Ctrl+C to cancel)\" ;;\n \"install.reinstall.path_mismatch.zh\") text=\"路径不匹配。中止重装。输入: '%s',期望: '%s'\" ;;\n \"install.reinstall.path_mismatch.en\") text=\"Path mismatch. Aborting reinstall. Input: '%s', Expected: '%s'\" ;;\n \"install.reinstall.confirmed.zh\") text=\"已确认。正在清理...\" ;;\n \"install.reinstall.confirmed.en\") text=\"Confirmed. Cleaning up...\" ;;\n \"install.reinstall.removed_worker.zh\") text=\" 已移除 worker: %s\" ;;\n \"install.reinstall.removed_worker.en\") text=\" Removed worker: %s\" ;;\n \"install.reinstall.removing_volume.zh\") text=\"正在移除 Docker 卷: agentteams-data\" ;;\n \"install.reinstall.removing_volume.en\") text=\"Removing Docker volume: agentteams-data\" ;;\n \"install.reinstall.warn_volume_fail.zh\") text=\" 警告: 无法移除卷(可能有引用)\" ;;\n \"install.reinstall.warn_volume_fail.en\") text=\" Warning: Could not remove volume (may have references)\" ;;\n \"install.reinstall.removing_proxy.zh\") text=\"正在移除 Docker API 代理容器: agentteams-controller\" ;;\n \"install.reinstall.removing_proxy.en\") text=\"Removing Docker API proxy container: agentteams-controller\" ;;\n \"install.reinstall.removing_network.zh\") text=\"正在移除 Docker 网络: agentteams-net\" ;;\n \"install.reinstall.removing_network.en\") text=\"Removing Docker network: agentteams-net\" ;;\n \"install.reinstall.removing_workspace.zh\") text=\"正在移除工作空间目录: %s\" ;;\n \"install.reinstall.removing_workspace.en\") text=\"Removing workspace directory: %s\" ;;\n \"install.reinstall.removing_env.zh\") text=\"正在移除 env 文件: %s\" ;;\n \"install.reinstall.removing_env.en\") text=\"Removing env file: %s\" ;;\n \"install.reinstall.cleanup_done.zh\") text=\"清理完成。开始全新安装...\" ;;\n \"install.reinstall.cleanup_done.en\") text=\"Cleanup complete. Starting fresh installation...\" ;;\n \"install.reinstall.failed_rm_workspace.zh\") text=\"无法移除工作空间目录\" ;;\n \"install.reinstall.failed_rm_workspace.en\") text=\"Failed to remove workspace directory\" ;;\n # --- Orphan volume detection ---\n \"install.orphan_volume.detected.zh\") text=\"⚠️ 检测到残留数据卷 '%s',但未找到对应的 env 配置文件。\" ;;\n \"install.orphan_volume.detected.en\") text=\"⚠️ Found leftover data volume '%s' but no matching env config file.\" ;;\n \"install.orphan_volume.warn.zh\") text=\"这可能是之前安装的残留数据,会导致新安装出现异常(如密码冲突、服务启动失败)。\" ;;\n \"install.orphan_volume.warn.en\") text=\"This is likely leftover data from a previous installation and may cause issues (e.g., credential conflicts, service startup failures).\" ;;\n \"install.orphan_volume.choose.zh\") text=\"选择操作:\" ;;\n \"install.orphan_volume.choose.en\") text=\"Choose an action:\" ;;\n \"install.orphan_volume.clean.zh\") text=\" 1) 清理残留数据卷后继续安装(推荐)\" ;;\n \"install.orphan_volume.clean.en\") text=\" 1) Remove leftover volume and continue installation (recommended)\" ;;\n \"install.orphan_volume.keep.zh\") text=\" 2) 保留数据卷继续安装(可能出现异常)\" ;;\n \"install.orphan_volume.keep.en\") text=\" 2) Keep the volume and continue installation (may cause issues)\" ;;\n \"install.orphan_volume.prompt.zh\") text=\"请选择 [1/2]\" ;;\n \"install.orphan_volume.prompt.en\") text=\"Enter choice [1/2]\" ;;\n \"install.orphan_volume.cleaning.zh\") text=\"正在清理残留数据卷...\" ;;\n \"install.orphan_volume.cleaning.en\") text=\"Removing leftover data volume...\" ;;\n \"install.orphan_volume.cleaned.zh\") text=\"残留数据卷已清理。继续全新安装...\" ;;\n \"install.orphan_volume.cleaned.en\") text=\"Leftover volume removed. Continuing with fresh installation...\" ;;\n \"install.orphan_volume.keeping.zh\") text=\"保留数据卷,继续安装。如遇异常请选择全新重装。\" ;;\n \"install.orphan_volume.keeping.en\") text=\"Keeping existing volume. If you encounter issues, consider a clean reinstall.\" ;;\n \"install.orphan_volume.clean_noninteractive.zh\") text=\"非交互模式: 自动清理残留数据卷...\" ;;\n \"install.orphan_volume.clean_noninteractive.en\") text=\"Non-interactive mode: automatically removing leftover volume...\" ;;\n # --- Loading existing config ---\n \"install.loading_config.zh\") text=\"从 %s 加载已有配置(shell 环境变量优先)...\" ;;\n \"install.loading_config.en\") text=\"Loading existing config from %s (shell env vars take priority)...\" ;;\n # --- LLM Configuration ---\n \"llm.title.zh\") text=\"--- LLM 配置 ---\" ;;\n \"llm.title.en\") text=\"--- LLM Configuration ---\" ;;\n \"llm.provider.label.zh\") text=\" 提供商: %s\" ;;\n \"llm.provider.label.en\") text=\" Provider: %s\" ;;\n \"llm.model.label.zh\") text=\" 模型: %s\" ;;\n \"llm.model.label.en\") text=\" Model: %s\" ;;\n \"llm.provider.qwen.zh\") text=\" 提供商: qwen(阿里云百炼)\" ;;\n \"llm.provider.qwen.en\") text=\" Provider: qwen (Alibaba Cloud Bailian)\" ;;\n \"llm.provider.qwen_default.zh\") text=\" 提供商: %s(默认)\" ;;\n \"llm.provider.qwen_default.en\") text=\" Provider: %s (default)\" ;;\n \"llm.model.default.zh\") text=\" 模型: %s(默认)\" ;;\n \"llm.model.default.en\") text=\" Model: %s (default)\" ;;\n \"llm.apikey_hint_bailian.zh\") text=\" 💡 获取阿里云百炼(DashScope)API Key:\" ;;\n \"llm.apikey_hint_bailian.en\") text=\" 💡 Get your Alibaba Cloud Bailian (DashScope) API Key:\" ;;\n \"llm.apikey_url_bailian.zh\") text=\" https://www.aliyun.com/product/bailian\" ;;\n \"llm.apikey_url_bailian.en\") text=\" https://www.aliyun.com/product/bailian\" ;;\n \"llm.apikey_hint_qwencloud.zh\") text=\" 💡 从 Qwen Cloud(国际站)获取 DASHSCOPE_API_KEY:\" ;;\n \"llm.apikey_hint_qwencloud.en\") text=\" 💡 Get your DASHSCOPE_API_KEY for Qwen Cloud (international) from:\" ;;\n \"llm.apikey_url_qwencloud.zh\") text=\" https://home.qwencloud.com/api-keys (文档: https://docs.qwencloud.com/)\" ;;\n \"llm.apikey_url_qwencloud.en\") text=\" https://home.qwencloud.com/api-keys | Docs: https://docs.qwencloud.com/\" ;;\n \"llm.apikey_hint_tokenplan.zh\") text=\" 💡 获取 DashScope API Key 或开通通义 Token 套餐,请参考:\" ;;\n \"llm.apikey_hint_tokenplan.en\") text=\" 💡 Get your DashScope or Token Plan API key (Alibaba Model Studio):\" ;;\n \"llm.apikey_url_tokenplan.zh\") text=\" https://help.aliyun.com/zh/model-studio/token-plan-quickstart\" ;;\n \"llm.apikey_url_tokenplan.en\") text=\" https://common-buy.aliyun.com/token-plan/ | https://help.aliyun.com/zh/model-studio/token-plan-quickstart\" ;;\n \"llm.apikey_hint_codingplan.zh\") text=\" 💡 获取 DashScope API Key(Coding 套餐 / coding.dashscope 接口):\" ;;\n \"llm.apikey_hint_codingplan.en\") text=\" 💡 Get your DashScope API key for Coding Plan (coding.dashscope endpoint):\" ;;\n \"llm.apikey_url_codingplan.zh\") text=\" https://help.aliyun.com/zh/model-studio/get-api-key\" ;;\n \"llm.apikey_url_codingplan.en\") text=\" https://help.aliyun.com/zh/model-studio/get-api-key\" ;;\n \"llm.apikey_prompt.zh\") text=\"LLM API Key\" ;;\n \"llm.apikey_prompt.en\") text=\"LLM API Key\" ;;\n \"llm.providers_title.zh\") text=\"可用 LLM 提供商:\" ;;\n \"llm.providers_title.en\") text=\"Available LLM Providers:\" ;;\n \"llm.provider.alibaba.zh\") text=\" 1) 阿里云通义 Token 套餐 - 推荐中国用户使用\" ;;\n \"llm.provider.alibaba.en\") text=\" 1) Qwen Cloud - International (OpenAI-compatible API, recommended)\" ;;\n \"llm.provider.openai_compat.zh\") text=\" 2) OpenAI 兼容 API - 自定义 Base URL(OpenAI、DeepSeek 等)\" ;;\n \"llm.provider.openai_compat.en\") text=\" 2) OpenAI-compatible API - Custom Base URL (OpenAI, DeepSeek, etc.)\" ;;\n \"llm.provider.select.zh\") text=\"选择提供商 [1/2]\" ;;\n \"llm.provider.select.en\") text=\"Select provider [1/2]\" ;;\n \"llm.alibaba.models_title.zh\") text=\"选择阿里云模型接入方式:\" ;;\n \"llm.alibaba.models_title.en\") text=\"Select Alibaba Cloud model access:\" ;;\n \"llm.alibaba.model.tokenplan.zh\") text=\" 1) 阿里云通义 Token 套餐 - 兼容模式(推荐)\" ;;\n \"llm.alibaba.model.tokenplan.en\") text=\" 1) Alibaba Cloud Token Plan - compatible-mode (recommended)\" ;;\n \"llm.alibaba.model.bailian.zh\") text=\" 2) 阿里云百炼 - DashScope 通用兼容接口\" ;;\n \"llm.alibaba.model.bailian.en\") text=\" 2) Alibaba Cloud Bailian - DashScope compatible mode\" ;;\n \"llm.alibaba.model.codingplan_legacy.zh\") text=\" 3) 阿里云 Coding 套餐 - 旧版端点(兼容保留)\" ;;\n \"llm.alibaba.model.codingplan_legacy.en\") text=\" 3) Alibaba Cloud Coding Plan - legacy endpoint (backward compatible)\" ;;\n \"llm.alibaba.model.select.zh\") text=\"选择接入方式 [1/2/3]\" ;;\n \"llm.alibaba.model.select.en\") text=\"Select access option [1/2/3]\" ;;\n \"llm.alibaba.model.invalid.zh\") text=\"无效选择: %s(请输入 1、2 或 3)\" ;;\n \"llm.alibaba.model.invalid.en\") text=\"Invalid choice: %s (please enter 1, 2, or 3)\" ;;\n \"llm.codingplan.models_title.zh\") text=\"选择通义 Token 套餐默认模型:\" ;;\n \"llm.codingplan.models_title.en\") text=\"Select Qwen Cloud default model:\" ;;\n \"llm.codingplan.model.qwen36plus.zh\") text=\" 1) qwen3.6-plus - 千问 3.6(推荐)\" ;;\n \"llm.codingplan.model.qwen36plus.en\") text=\" 1) qwen3.6-plus - Qwen 3.6 (recommended)\" ;;\n \"llm.codingplan.model.glm5.zh\") text=\" 2) glm-5 - 智谱 GLM-5(编程推荐)\" ;;\n \"llm.codingplan.model.glm5.en\") text=\" 2) glm-5 - Zhipu GLM-5 (recommended for coding)\" ;;\n \"llm.codingplan.model.kimi.zh\") text=\" 3) kimi-k2.5 - Moonshot Kimi K2.5\" ;;\n \"llm.codingplan.model.kimi.en\") text=\" 3) kimi-k2.5 - Moonshot Kimi K2.5\" ;;\n \"llm.codingplan.model.minimax.zh\") text=\" 4) MiniMax-M2.5 - MiniMax M2.5\" ;;\n \"llm.codingplan.model.minimax.en\") text=\" 4) MiniMax-M2.5 - MiniMax M2.5\" ;;\n \"llm.codingplan.model.select.zh\") text=\"选择模型 [1/2/3/4]\" ;;\n \"llm.codingplan.model.select.en\") text=\"Select model [1/2/3/4]\" ;;\n \"llm.provider.selected_tokenplan.zh\") text=\" 提供商: 阿里云通义 Token 套餐(兼容模式)\" ;;\n \"llm.provider.selected_tokenplan.en\") text=\" Provider: Alibaba Cloud Token Plan (compatible mode)\" ;;\n \"llm.provider.selected_codingplan.zh\") text=\" 提供商: 阿里云通义 Token 套餐(alibaba-cloud)\" ;;\n \"llm.provider.selected_codingplan.en\") text=\" Provider: Qwen Cloud (international) (alibaba-cloud)\" ;;\n \"llm.provider.selected_codingplan_legacy.zh\") text=\" 提供商: 阿里云 Coding 套餐(coding.dashscope)\" ;;\n \"llm.provider.selected_codingplan_legacy.en\") text=\" Provider: Alibaba Cloud Coding Plan (coding.dashscope)\" ;;\n \"llm.provider.selected_qwen.zh\") text=\" 提供商: 阿里云百炼\" ;;\n \"llm.provider.selected_qwen.en\") text=\" Provider: Alibaba Cloud Bailian\" ;;\n \"llm.provider.selected_openai.zh\") text=\" 提供商: %s(OpenAI 兼容)\" ;;\n \"llm.provider.selected_openai.en\") text=\" Provider: %s (OpenAI-compatible)\" ;;\n \"llm.provider.invalid.zh\") text=\"无效选择: %s(请输入 1 或 2)\" ;;\n \"llm.provider.invalid.en\") text=\"Invalid choice: %s (please enter 1 or 2)\" ;;\n \"llm.qwen.model_prompt.zh\") text=\"默认模型 ID\" ;;\n \"llm.qwen.model_prompt.en\") text=\"Default Model ID\" ;;\n \"llm.openai.base_url_prompt.zh\") text=\"Base URL\" ;;\n \"llm.openai.base_url_prompt.en\") text=\"Base URL\" ;;\n \"llm.openai.model_prompt.zh\") text=\"默认模型 ID\" ;;\n \"llm.openai.model_prompt.en\") text=\"Default Model ID\" ;;\n \"llm.openai.base_url_label.zh\") text=\" Base URL: %s\" ;;\n \"llm.openai.base_url_label.en\") text=\" Base URL: %s\" ;;\n # --- Custom model parameters ---\n \"llm.custom_model.detected.zh\") text=\" ⚠️ 模型 '%s' 不在内置模型列表中,请配置模型参数:\" ;;\n \"llm.custom_model.detected.en\") text=\" ⚠️ Model '%s' is not in the built-in model list. Please configure model parameters:\" ;;\n \"llm.custom_model.context_prompt.zh\") text=\"最大上下文长度(token 数)[150000]\" ;;\n \"llm.custom_model.context_prompt.en\") text=\"Max context window (tokens) [150000]\" ;;\n \"llm.custom_model.max_tokens_prompt.zh\") text=\"最大输出长度(token 数)[128000]\" ;;\n \"llm.custom_model.max_tokens_prompt.en\") text=\"Max output tokens [128000]\" ;;\n \"llm.custom_model.reasoning_prompt.zh\") text=\"是否支持推理/思考模式?[Y/n]\" ;;\n \"llm.custom_model.reasoning_prompt.en\") text=\"Does it support reasoning/thinking mode? [Y/n]\" ;;\n \"llm.custom_model.vision_prompt.zh\") text=\"是否支持图片输入?[y/N]\" ;;\n \"llm.custom_model.vision_prompt.en\") text=\"Does it support image input? [y/N]\" ;;\n \"llm.custom_model.summary.zh\") text=\" 自定义模型参数: 上下文=%s, 最大输出=%s, 推理=%s, 图片=%s\" ;;\n \"llm.custom_model.summary.en\") text=\" Custom model params: context=%s, maxTokens=%s, reasoning=%s, vision=%s\" ;;\n # --- Admin Credentials ---\n \"admin.title.zh\") text=\"--- 管理员凭据 ---\" ;;\n \"admin.title.en\") text=\"--- Admin Credentials ---\" ;;\n \"admin.username_prompt.zh\") text=\"管理员用户名\" ;;\n \"admin.username_prompt.en\") text=\"Admin Username\" ;;\n \"admin.password_prompt.zh\") text=\"管理员密码(留空自动生成,最少 8 位)\" ;;\n \"admin.password_prompt.en\") text=\"Admin Password (leave empty to auto-generate, min 8 chars)\" ;;\n \"admin.password_generated.zh\") text=\" 已自动生成管理员密码\" ;;\n \"admin.password_generated.en\") text=\" Auto-generated admin password\" ;;\n \"admin.password_too_short.zh\") text=\"管理员密码至少需要 8 个字符(MinIO 要求)。当前长度: %s\" ;;\n \"admin.password_too_short.en\") text=\"Admin password must be at least 8 characters (MinIO requirement). Current length: %s\" ;;\n # --- Port Configuration ---\n \"port.title.zh\") text=\"--- 端口配置(按回车使用默认值)---\" ;;\n \"port.title.en\") text=\"--- Port Configuration (press Enter for defaults) ---\" ;;\n \"port.gateway_prompt.zh\") text=\"网关主机端口(容器内 8080)\" ;;\n \"port.gateway_prompt.en\") text=\"Host port for gateway (8080 inside container)\" ;;\n \"port.console_prompt.zh\") text=\"Higress 控制台主机端口(容器内 8001)\" ;;\n \"port.console_prompt.en\") text=\"Host port for Higress console (8001 inside container)\" ;;\n \"port.element_prompt.zh\") text=\"Element Web 直接访问主机端口(容器内 8088)\" ;;\n \"port.element_prompt.en\") text=\"Host port for Element Web direct access (8088 inside container)\" ;;\n \"port.manager_console_prompt.zh\") text=\"Manager 控制台主机端口(容器内 18888)\" ;;\n \"port.manager_console_prompt.en\") text=\"Host port for Manager console (18888 inside container)\" ;;\n \"port.copaw_app_prompt.zh\") text=\"CoPaw App API 主机端口(容器内 18799)\" ;;\n \"port.copaw_app_prompt.en\") text=\"Host port for CoPaw App API (18799 inside container)\" ;;\n # --- Local-only binding ---\n \"port.local_only.title.zh\") text=\"--- 网络访问模式 ---\" ;;\n \"port.local_only.title.en\") text=\"--- Network Access Mode ---\" ;;\n \"port.local_only.prompt.zh\") text=\"是否仅允许本机访问(端口绑定到 127.0.0.1)?\" ;;\n \"port.local_only.prompt.en\") text=\"Bind ports to localhost only (127.0.0.1)?\" ;;\n \"port.local_only.hint_yes.zh\") text=\" 仅本机使用,无需开放外部端口(推荐)\" ;;\n \"port.local_only.hint_yes.en\") text=\" Local use only, no external port exposure (recommended)\" ;;\n \"port.local_only.hint_no.zh\") text=\" 允许外部访问(局域网 / 公网)\" ;;\n \"port.local_only.hint_no.en\") text=\" Allow external access (LAN / public network)\" ;;\n \"port.local_only.choice.zh\") text=\"请选择 [1/2]\" ;;\n \"port.local_only.choice.en\") text=\"Enter choice [1/2]\" ;;\n \"port.local_only.selected_local.zh\") text=\"端口已绑定到 127.0.0.1(仅本机访问)\" ;;\n \"port.local_only.selected_local.en\") text=\"Ports bound to 127.0.0.1 (localhost only)\" ;;\n \"port.local_only.selected_external.zh\") text=\"端口已绑定到所有网络接口(0.0.0.0)\" ;;\n \"port.local_only.selected_external.en\") text=\"Ports bound to all interfaces (0.0.0.0)\" ;;\n \"port.local_only.https_hint.zh\") text=\"⚠️ 建议在 Higress 控制台配置 TLS 证书并启用 HTTPS,避免明文传输。\" ;;\n \"port.local_only.https_hint.en\") text=\"⚠️ It is recommended to configure TLS certificates and enable HTTPS in the Higress Console to avoid plaintext transmission.\" ;;\n \"port.local_only.https_docs.zh\") text=\"\" ;;\n \"port.local_only.https_docs.en\") text=\"\" ;;\n # --- Domain Configuration ---\n \"domain.title.zh\") text=\"--- 域名配置(按回车使用默认值)---\" ;;\n \"domain.title.en\") text=\"--- Domain Configuration (press Enter for defaults) ---\" ;;\n \"domain.hint.zh\") text=\"提示: 自定义域名前必须事先做好 DNS 解析。单机 ECS 部署时无需修改 aigw、fs 等域名;Element Web 和 Matrix Server 也可通过 IP 直接访问。\" ;;\n \"domain.hint.en\") text=\"Hint: Configure DNS resolution before customizing domains. For single ECS deployment, no need to change aigw, fs, etc.; Element Web and Matrix Server can also be accessed directly via IP.\" ;;\n \"domain.matrix_prompt.zh\") text=\"Matrix 域名\" ;;\n \"domain.matrix_prompt.en\") text=\"Matrix Domain\" ;;\n \"domain.element_prompt.zh\") text=\"Element Web 域名\" ;;\n \"domain.element_prompt.en\") text=\"Element Web Domain\" ;;\n \"domain.gateway_prompt.zh\") text=\"AI 网关域名\" ;;\n \"domain.gateway_prompt.en\") text=\"AI Gateway Domain\" ;;\n \"domain.fs_prompt.zh\") text=\"文件系统域名\" ;;\n \"domain.fs_prompt.en\") text=\"File System Domain\" ;;\n \"domain.console_prompt.zh\") text=\"Manager 控制台域名\" ;;\n \"domain.console_prompt.en\") text=\"Manager Console Domain\" ;;\n # --- GitHub Integration ---\n \"github.title.zh\") text=\"--- GitHub 集成(可选,按回车跳过)---\" ;;\n \"github.title.en\") text=\"--- GitHub Integration (optional, press Enter to skip) ---\" ;;\n \"github.token_prompt.zh\") text=\"GitHub 个人访问令牌(可选)\" ;;\n \"github.token_prompt.en\") text=\"GitHub Personal Access Token (optional)\" ;;\n # --- Skills Registry ---\n \"skills.title.zh\") text=\"--- Skills 注册中心(可选,按回车使用默认 nacos://market.agentteams.io:80/public)---\" ;;\n \"skills.title.en\") text=\"--- Skills Registry (optional, press Enter for default nacos://market.agentteams.io:80/public) ---\" ;;\n \"skills.url_prompt.zh\") text=\"Skills 注册中心 URL(留空使用默认 nacos://market.agentteams.io:80/public)\" ;;\n \"skills.url_prompt.en\") text=\"Skills Registry URL (leave empty for default nacos://market.agentteams.io:80/public)\" ;;\n # --- Data Persistence ---\n \"data.title.zh\") text=\"--- 数据持久化 ---\" ;;\n \"data.title.en\") text=\"--- Data Persistence ---\" ;;\n \"data.volume_prompt.zh\") text=\"Docker 卷名称 [agentteams-data]\" ;;\n \"data.volume_prompt.en\") text=\"Docker volume name for persistent data [agentteams-data]\" ;;\n \"data.volume_using.zh\") text=\" 使用 Docker 卷: %s\" ;;\n \"data.volume_using.en\") text=\" Using Docker volume: %s\" ;;\n # --- Manager Workspace ---\n \"workspace.title.zh\") text=\"--- Manager 工作空间 ---\" ;;\n \"workspace.title.en\") text=\"--- Manager Workspace ---\" ;;\n \"workspace.dir_prompt.zh\") text=\"Manager 工作空间目录 [%s]\" ;;\n \"workspace.dir_prompt.en\") text=\"Manager workspace directory [%s]\" ;;\n \"workspace.dir_label.zh\") text=\" Manager 工作空间: %s\" ;;\n \"workspace.dir_label.en\") text=\" Manager workspace: %s\" ;;\n # --- Host directory sharing ---\n \"host_share.prompt.zh\") text=\"与 Agent 共享的主机目录(默认: %s)\" ;;\n \"host_share.prompt.en\") text=\"Host directory to share with agents (default: %s)\" ;;\n \"host_share.sharing.zh\") text=\"共享主机目录: %s -> 容器内 /host-share\" ;;\n \"host_share.sharing.en\") text=\"Sharing host directory: %s -> /host-share in container\" ;;\n \"host_share.not_exist.zh\") text=\"警告: 主机目录 %s 不存在,跳过验证继续使用\" ;;\n \"host_share.not_exist.en\") text=\"WARNING: Host directory %s does not exist, using without validation\" ;;\n # --- Default worker runtime ---\n \"worker_runtime.title.zh\") text=\"--- 默认 Worker 运行时 ---\" ;;\n \"worker_runtime.title.en\") text=\"--- Default Worker Runtime ---\" ;;\n \"worker_runtime.openclaw.zh\") text=\"OpenClaw\" ;;\n \"worker_runtime.openclaw.en\") text=\"OpenClaw\" ;;\n \"worker_runtime.copaw.zh\") text=\"CoPaw\" ;;\n \"worker_runtime.copaw.en\") text=\"CoPaw\" ;;\n \"worker_runtime.hermes.zh\") text=\"Hermes\" ;;\n \"worker_runtime.hermes.en\") text=\"Hermes\" ;;\n \"worker_runtime.choice.zh\") text=\"请选择 [1/2/3]\" ;;\n \"worker_runtime.choice.en\") text=\"Enter choice [1/2/3]\" ;;\n \"worker_runtime.selected.zh\") text=\"默认 Worker 运行时: %s\" ;;\n \"worker_runtime.selected.en\") text=\"Default Worker runtime: %s\" ;;\n \"worker_runtime.title_short.zh\") text=\"默认 Worker 运行时\" ;;\n \"worker_runtime.title_short.en\") text=\"Default Worker Runtime\" ;;\n \"manager_runtime.title.zh\") text=\"--- Manager 运行时 ---\" ;;\n \"manager_runtime.title.en\") text=\"--- Manager Runtime ---\" ;;\n \"manager_runtime.openclaw.zh\") text=\"OpenClaw\" ;;\n \"manager_runtime.openclaw.en\") text=\"OpenClaw\" ;;\n \"manager_runtime.copaw.zh\") text=\"CoPaw\" ;;\n \"manager_runtime.copaw.en\") text=\"CoPaw\" ;;\n \"manager_runtime.choice.zh\") text=\"请选择 [1/2]\" ;;\n \"manager_runtime.choice.en\") text=\"Enter choice [1/2]\" ;;\n \"manager_runtime.selected.zh\") text=\"Manager 运行时: %s\" ;;\n \"manager_runtime.selected.en\") text=\"Manager runtime: %s\" ;;\n \"manager_runtime.title_short.zh\") text=\"Manager 运行时\" ;;\n \"manager_runtime.title_short.en\") text=\"Manager Runtime\" ;;\n # --- Secrets and config ---\n \"install.generating_secrets.zh\") text=\"正在生成密钥...\" ;;\n \"install.generating_secrets.en\") text=\"Generating secrets...\" ;;\n \"install.config_saved.zh\") text=\"配置已保存到 %s\" ;;\n \"install.config_saved.en\") text=\"Configuration saved to %s\" ;;\n # --- Container runtime socket ---\n \"install.socket_detected.zh\") text=\"容器运行时 socket: %s(已启用直接创建 Worker)\" ;;\n \"install.socket_detected.en\") text=\"Container runtime socket: %s (direct Worker creation enabled)\" ;;\n \"install.socket_not_found.zh\") text=\"未找到容器运行时 socket(Manager 无法直接创建 Worker 容器,需要你手动执行 docker 命令创建)\" ;;\n \"install.socket_not_found.en\") text=\"No container runtime socket found (Manager cannot create Worker containers directly, you will need to create them manually using docker commands)\" ;;\n \"install.socket_confirm.title.zh\") text=\"⚠️ 未检测到容器运行时 Socket\" ;;\n \"install.socket_confirm.title.en\") text=\"⚠️ Container Runtime Socket Not Detected\" ;;\n \"install.socket_confirm.message.zh\") text=\"未找到 Docker/Podman socket,Manager 将无法自动创建 Worker 容器。\\n你需要手动执行 docker run 命令来创建 Worker。\\n\\n是否继续安装?\" ;;\n \"install.socket_confirm.message.en\") text=\"Docker/Podman socket not found. Manager will not be able to create Worker containers automatically.\\nYou will need to manually run docker commands to create Workers.\\n\\nContinue installation?\" ;;\n \"install.socket_confirm.prompt.zh\") text=\"继续安装? [y/N]: \" ;;\n \"install.socket_confirm.prompt.en\") text=\"Continue? [y/N]: \" ;;\n \"install.socket_confirm.cancelled.zh\") text=\"安装已取消。如需启用 Worker 自动创建,请确保 Docker/Podman 正在运行,然后重新运行安装脚本。\" ;;\n \"install.socket_confirm.cancelled.en\") text=\"Installation cancelled. To enable automatic Worker creation, ensure Docker/Podman is running and re-run the installer.\" ;;\n # --- Container management ---\n \"install.removing_existing.zh\") text=\"正在移除现有 agentteams-manager 容器...\" ;;\n \"install.removing_existing.en\") text=\"Removing existing agentteams-manager container...\" ;;\n # --- Matrix E2EE ---\n \"matrix_e2ee.title.zh\") text=\"--- Matrix 端到端加密(E2EE)---\" ;;\n \"matrix_e2ee.title.en\") text=\"--- Matrix End-to-End Encryption (E2EE) ---\" ;;\n \"matrix_e2ee.desc.zh\") text=\"E2EE 会对 Manager 与 Worker 之间的 Matrix 消息进行端到端加密。\\n 启用后,即使 Matrix 服务器被入侵,消息内容也无法被窃取。\\n 但 E2EE 会增加首次握手耗时,且要求所有 Agent 都支持 matrix-sdk-crypto。\\n 如果不确定,建议保持禁用。\\n ⚠ 注意:禁用 E2EE 后,请勿在 Element 上创建默认启用加密的 Private 房间,\\n 否则 Agent 将无法读取该房间中的加密消息。请改用 Public 房间或关闭房间加密。\" ;;\n \"matrix_e2ee.desc.en\") text=\"E2EE encrypts Matrix messages between Manager and Workers end-to-end.\\n When enabled, message content stays private even if the Matrix server is compromised.\\n However, E2EE adds overhead to the initial handshake and requires all Agents\\n to support matrix-sdk-crypto. If unsure, keep it disabled.\\n ⚠ Note: When E2EE is disabled, do NOT create Private rooms in Element (which\\n enable encryption by default) — Agents cannot read encrypted messages without\\n E2EE support. Use Public rooms or turn off room encryption instead.\" ;;\n \"matrix_e2ee.enable.zh\") text=\"启用 E2EE\" ;;\n \"matrix_e2ee.enable.en\") text=\"Enable E2EE\" ;;\n \"matrix_e2ee.disable.zh\") text=\"禁用 E2EE(推荐)\" ;;\n \"matrix_e2ee.disable.en\") text=\"Disable E2EE (recommended)\" ;;\n \"matrix_e2ee.choice.zh\") text=\"请选择 [1/2]\" ;;\n \"matrix_e2ee.choice.en\") text=\"Enter choice [1/2]\" ;;\n \"matrix_e2ee.selected_enabled.zh\") text=\"Matrix E2EE: 已启用\" ;;\n \"matrix_e2ee.selected_enabled.en\") text=\"Matrix E2EE: enabled\" ;;\n \"matrix_e2ee.selected_disabled.zh\") text=\"Matrix E2EE: 已禁用(默认)\" ;;\n \"matrix_e2ee.selected_disabled.en\") text=\"Matrix E2EE: disabled (default)\" ;;\n \"matrix_e2ee.title_short.zh\") text=\"Matrix E2EE\" ;;\n \"matrix_e2ee.title_short.en\") text=\"Matrix E2EE\" ;;\n \"matrix_e2ee.val_enabled.zh\") text=\"已启用\" ;;\n \"matrix_e2ee.val_enabled.en\") text=\"enabled\" ;;\n \"matrix_e2ee.val_disabled.zh\") text=\"已禁用\" ;;\n \"matrix_e2ee.val_disabled.en\") text=\"disabled\" ;;\n # --- Docker API proxy ---\n \"docker_proxy.title.zh\") text=\"--- Docker API 安全代理 ---\" ;;\n \"docker_proxy.title.en\") text=\"--- Docker API Security Proxy ---\" ;;\n \"docker_proxy.desc.zh\") text=\"Docker API 代理可防止 AI Agent 通过 Docker API 越狱访问宿主机。\\n 启用后,Manager 不再直接持有 Docker socket,所有容器操作经过安全校验。\" ;;\n \"docker_proxy.desc.en\") text=\"Docker API proxy prevents AI Agents from escaping via Docker API to access the host.\\n When enabled, Manager no longer has direct Docker socket access; all container operations go through security validation.\" ;;\n \"docker_proxy.enable.zh\") text=\"启用(推荐)\" ;;\n \"docker_proxy.enable.en\") text=\"Enable (recommended)\" ;;\n \"docker_proxy.disable.zh\") text=\"禁用(直接挂载 Docker socket)\" ;;\n \"docker_proxy.disable.en\") text=\"Disable (mount Docker socket directly)\" ;;\n \"docker_proxy.choice.zh\") text=\"请选择 [1/2]\" ;;\n \"docker_proxy.choice.en\") text=\"Enter choice [1/2]\" ;;\n \"docker_proxy.selected_enabled.zh\") text=\"Docker API 代理: 已启用\" ;;\n \"docker_proxy.selected_enabled.en\") text=\"Docker API proxy: enabled\" ;;\n \"docker_proxy.selected_disabled.zh\") text=\"Docker API 代理: 已禁用\" ;;\n \"docker_proxy.selected_disabled.en\") text=\"Docker API proxy: disabled\" ;;\n \"docker_proxy.title_short.zh\") text=\"Docker API 代理\" ;;\n \"docker_proxy.title_short.en\") text=\"Docker API Proxy\" ;;\n \"docker_proxy.val_enabled.zh\") text=\"已启用\" ;;\n \"docker_proxy.val_enabled.en\") text=\"enabled\" ;;\n \"docker_proxy.val_disabled.zh\") text=\"已禁用\" ;;\n \"docker_proxy.val_disabled.en\") text=\"disabled\" ;;\n \"docker_proxy.registries_desc.zh\") text=\"默认放行的镜像来源:本地镜像、localhost、Higress 仓库(所有 region)。\\n 如需放行其他镜像仓库,请输入逗号分隔的地址前缀。\\n 示例: ghcr.io/myorg,registry.example.com/team\" ;;\n \"docker_proxy.registries_desc.en\") text=\"Default allowed image sources: local images, localhost, Higress registries (all regions).\\n To allow additional image sources, enter comma-separated address prefixes.\\n Example: ghcr.io/myorg,registry.example.com/team\" ;;\n \"docker_proxy.registries_prompt.zh\") text=\"额外放行的镜像来源(按回车跳过)\" ;;\n \"docker_proxy.registries_prompt.en\") text=\"Additional allowed image sources (press Enter to skip)\" ;;\n \"docker_proxy.registries_label.zh\") text=\"额外放行的镜像来源\" ;;\n \"docker_proxy.registries_label.en\") text=\"Additional allowed image sources\" ;;\n # --- Podman Autostart ---\n \"podman.autostart.title.zh\") text=\"--- Podman 驻留与开机自启 ---\" ;;\n \"podman.autostart.title.en\") text=\"--- Podman Linger & Autostart ---\" ;;\n \"podman.autostart.desc.zh\") text=\"配置为 systemd 服务,支持开机自动启动及后台驻留。\" ;;\n \"podman.autostart.desc.en\") text=\"Configure as a systemd service for automatic startup on boot.\" ;;\n \"podman.autostart.enable.zh\") text=\"启用(推荐)\" ;;\n \"podman.autostart.enable.en\") text=\"Enable (recommended)\" ;;\n \"podman.autostart.disable.zh\") text=\"不启用(容器随当前会话关闭而停止)\" ;;\n \"podman.autostart.disable.en\") text=\"Disable (containers stop when session ends)\" ;;\n \"podman.autostart.choice.zh\") text=\"请选择 [1/2]\" ;;\n \"podman.autostart.choice.en\") text=\"Enter choice [1/2]\" ;;\n \"podman.autostart.selected_enabled.zh\") text=\"Podman 开机自启: 已启用\" ;;\n \"podman.autostart.selected_enabled.en\") text=\"Podman Autostart: enabled\" ;;\n \"podman.autostart.selected_disabled.zh\") text=\"Podman 开机自启: 已禁用\" ;;\n \"podman.autostart.selected_disabled.en\") text=\"Podman Autostart: disabled\" ;;\n \"podman.autostart.title_short.zh\") text=\"Podman 开机自启\" ;;\n \"podman.autostart.title_short.en\") text=\"Podman Autostart\" ;;\n \"podman.autostart.val_enabled.zh\") text=\"已启用\" ;;\n \"podman.autostart.val_enabled.en\") text=\"enabled\" ;;\n \"podman.autostart.val_disabled.zh\") text=\"已禁用\" ;;\n \"podman.autostart.val_disabled.en\") text=\"disabled\" ;;\n \"install.podman.autostart_title.zh\") text=\"正在配置 Podman 容器开机自启...\" ;;\n \"install.podman.autostart_title.en\") text=\"Configuring Podman container autostart...\" ;;\n \"install.podman.linger_enable.zh\") text=\"尝试启用 systemd linger (如有提示请在此输入 sudo 密码)...\" ;;\n \"install.podman.linger_enable.en\") text=\"Attempting to enable systemd linger (enter sudo password if prompted)...\" ;;\n \"install.podman.linger_warn.zh\") text=\"警告: 无法自动启用 linger。驻留可能失败,请手动执行: sudo loginctl enable-linger \\$(whoami)\" ;;\n \"install.podman.linger_warn.en\") text=\"WARNING: Failed to auto-enable linger. Please run manually: sudo loginctl enable-linger \\$(whoami)\" ;;\n \"install.podman.root_setup.zh\") text=\"检测到 root 用户。正在配置系统级 agentteams-podman-restart 服务...\" ;;\n \"install.podman.root_setup.en\") text=\"Root user detected. Configuring system-wide agentteams-podman-restart service...\" ;;\n \"install.podman.root_success.zh\") text=\"✅ 系统级 agentteams-podman-restart 服务已成功启用并运行。\" ;;\n \"install.podman.root_success.en\") text=\"✅ System-wide agentteams-podman-restart service successfully enabled and running.\" ;;\n \"install.podman.root_fail.zh\") text=\"⚠️ 系统级 agentteams-podman-restart 服务启用失败,请稍后手动检查。\" ;;\n \"install.podman.root_fail.en\") text=\"⚠️ Failed to enable system-wide agentteams-podman-restart service. Please check manually later.\" ;;\n \"install.podman.user_setup.zh\") text=\"检测到普通用户 (%s)。正在配置用户级 agentteams-podman-restart 服务...\" ;;\n \"install.podman.user_setup.en\") text=\"Non-root user (%s) detected. Configuring user-level agentteams-podman-restart service...\" ;;\n \"install.podman.user_success.zh\") text=\"✅ 用户级 agentteams-podman-restart 服务已成功启用并运行。\" ;;\n \"install.podman.user_success.en\") text=\"✅ User-level agentteams-podman-restart service successfully enabled and running.\" ;;\n \"install.podman.user_fail.zh\") text=\"⚠️ 用户级 agentteams-podman-restart 服务启用失败。\\n提示:这通常是因为缺少 XDG_RUNTIME_DIR 或 dbus 没有运行。您可以尝试手动执行: systemctl --user enable --now agentteams-podman-restart.service\" ;;\n \"install.podman.user_fail.en\") text=\"⚠️ Failed to enable user-level agentteams-podman-restart service.\\nHint: This is usually due to missing XDG_RUNTIME_DIR or dbus not running. Try manually: systemctl --user enable --now agentteams-podman-restart.service\" ;;\n \"install.podman.success.zh\") text=\"Podman 开机自启配置完成。\" ;;\n \"install.podman.success.en\") text=\"Podman autostart successfully configured.\" ;;\n # --- Worker idle timeout ---\n \"idle_timeout.prompt.zh\") text=\"Worker 空闲自动停止超时(分钟)[720]\" ;;\n \"idle_timeout.prompt.en\") text=\"Worker idle auto-stop timeout in minutes [720]\" ;;\n \"idle_timeout.selected.zh\") text=\"Worker 空闲超时: %s 分钟\" ;;\n \"idle_timeout.selected.en\") text=\"Worker idle timeout: %s minutes\" ;;\n \"idle_timeout.label.zh\") text=\"Worker 空闲超时(分钟)\" ;;\n \"idle_timeout.label.en\") text=\"Worker idle timeout (min)\" ;;\n # --- YOLO mode ---\n \"install.yolo.zh\") text=\"YOLO 模式已启用(自主决策,无交互提示)\" ;;\n \"install.yolo.en\") text=\"YOLO mode enabled (autonomous decisions, no interactive prompts)\" ;;\n # --- Image pulling ---\n \"install.image.exists.zh\") text=\"Manager 镜像已存在: %s\" ;;\n \"install.image.exists.en\") text=\"Manager image already exists locally: %s\" ;;\n \"install.image.pulling_manager.zh\") text=\"正在拉取 Manager 镜像: %s\" ;;\n \"install.image.pulling_manager.en\") text=\"Pulling Manager image: %s\" ;;\n \"install.image.worker_exists.zh\") text=\"Worker 镜像已存在: %s\" ;;\n \"install.image.worker_exists.en\") text=\"Worker image already exists locally: %s\" ;;\n \"install.image.pulling_worker.zh\") text=\"正在拉取 Worker 镜像: %s\" ;;\n \"install.image.pulling_worker.en\") text=\"Pulling Worker image: %s\" ;;\n # --- Starting container ---\n \"install.starting_manager.zh\") text=\"正在启动 Manager 容器...\" ;;\n \"install.starting_manager.en\") text=\"Starting Manager container...\" ;;\n # --- Wait for Manager ready ---\n \"install.wait_ready.zh\") text=\"等待 Manager Agent 就绪(超时: %ss)...\" ;;\n \"install.wait_ready.en\") text=\"Waiting for Manager agent to be ready (timeout: %ss)...\" ;;\n \"install.wait_ready.ok.zh\") text=\"Manager Agent 已就绪!\" ;;\n \"install.wait_ready.ok.en\") text=\"Manager agent is ready!\" ;;\n \"install.wait_ready.waiting.zh\") text=\"等待中... (%ds/%ds)\" ;;\n \"install.wait_ready.waiting.en\") text=\"Waiting... (%ds/%ds)\" ;;\n \"install.wait_ready.timeout.zh\") text=\"Manager Agent 在 %ss 内未就绪。请检查: docker logs %s\" ;;\n \"install.wait_ready.timeout.en\") text=\"Manager agent did not become ready within %ss. Check: docker logs %s\" ;;\n # --- Wait for Matrix ready ---\n \"install.wait_matrix.zh\") text=\"等待 Matrix 服务就绪(超时: %ss)...\" ;;\n \"install.wait_matrix.en\") text=\"Waiting for Matrix server to be ready (timeout: %ss)...\" ;;\n \"install.wait_matrix.ok.zh\") text=\"Matrix 服务已就绪!\" ;;\n \"install.wait_matrix.ok.en\") text=\"Matrix server is ready!\" ;;\n \"install.wait_matrix.waiting.zh\") text=\"等待 Matrix 中... (%ds/%ds)\" ;;\n \"install.wait_matrix.waiting.en\") text=\"Waiting for Matrix... (%ds/%ds)\" ;;\n \"install.wait_matrix.timeout.zh\") text=\"Matrix 服务在 %ss 内未就绪。请检查: docker logs %s\" ;;\n \"install.wait_matrix.timeout.en\") text=\"Matrix server did not become ready within %ss. Check: docker logs %s\" ;;\n # --- OpenAI-compatible connectivity test ---\n \"llm.openai.test.testing.zh\") text=\"正在测试 API 联通性...\" ;;\n \"llm.openai.test.testing.en\") text=\"Testing API connectivity...\" ;;\n \"llm.openai.test.ok.zh\") text=\"✅ API 联通性测试通过\" ;;\n \"llm.openai.test.ok.en\") text=\"✅ API connectivity test passed\" ;;\n \"llm.openai.test.fail.zh\") text=\"⚠️ API 联通性测试失败(HTTP %s)。响应内容:\\n%s\\n请根据以上错误信息联系您的模型服务商解决。\" ;;\n \"llm.openai.test.fail.en\") text=\"⚠️ API connectivity test failed (HTTP %s). Response body:\\n%s\\nPlease contact your model provider to resolve the issue.\" ;;\n \"llm.openai.test.fail.tokenplan.zh\") text=\"⚠️ 提示: 请确认 API Key 有效且已开通通义 Token 套餐。文档: https://help.aliyun.com/zh/model-studio/token-plan-quickstart\" ;;\n \"llm.openai.test.fail.tokenplan.en\") text=\"⚠️ Hint: Verify your Token Plan API key and compatible-mode access. Docs: https://help.aliyun.com/zh/model-studio/token-plan-quickstart\" ;;\n \"llm.openai.test.fail.codingplan.zh\") text=\"⚠️ 提示: 请确认 API Key 有效且已开通通义 Token 套餐。文档: https://help.aliyun.com/zh/model-studio/token-plan-quickstart\" ;;\n \"llm.openai.test.fail.codingplan.en\") text=\"⚠️ Hint: Verify your DASHSCOPE_API_KEY for Qwen Cloud. API keys: https://home.qwencloud.com/api-keys Docs: https://docs.qwencloud.com/\" ;;\n \"llm.openai.test.fail.codingplan_legacy.zh\") text=\"⚠️ 提示: 请确认 API Key 有效且 Coding 套餐接口可用。文档: https://help.aliyun.com/zh/model-studio/get-api-key\" ;;\n \"llm.openai.test.fail.codingplan_legacy.en\") text=\"⚠️ Hint: Verify your DashScope API key and Coding Plan access. Docs: https://help.aliyun.com/zh/model-studio/get-api-key\" ;;\n \"llm.openai.test.no_curl.zh\") text=\"⚠️ 未找到 curl,跳过 API 联通性测试\" ;;\n \"llm.openai.test.no_curl.en\") text=\"⚠️ curl not found, skipping API connectivity test\" ;;\n \"llm.openai.test.confirm.zh\") text=\"是否仍要继续安装?[y/N/b] \" ;;\n \"llm.openai.test.confirm.en\") text=\"Continue with installation anyway? [y/N/b] \" ;;\n \"llm.embedding.title.zh\") text=\"📦 记忆搜索配置\" ;;\n \"llm.embedding.title.en\") text=\"📦 Memory Search Configuration\" ;;\n \"llm.embedding.hint.zh\") text=\" Embedding 模型可提升记忆搜索质量(语义匹配)。不启用也可正常使用记忆功能(关键词匹配)。\" ;;\n \"llm.embedding.hint.en\") text=\" Embedding model improves memory search quality (semantic matching). Memory still works without it (keyword matching).\" ;;\n \"llm.embedding.option.default.zh\") text=\" 1) text-embedding-v4(推荐)\" ;;\n \"llm.embedding.option.default.en\") text=\" 1) text-embedding-v4 (Recommended)\" ;;\n \"llm.embedding.option.custom.zh\") text=\" 2) 自定义 Embedding 模型\" ;;\n \"llm.embedding.option.custom.en\") text=\" 2) Custom embedding model\" ;;\n \"llm.embedding.option.disable.zh\") text=\" 3) 不启用\" ;;\n \"llm.embedding.option.disable.en\") text=\" 3) Do not enable\" ;;\n \"llm.embedding.select.zh\") text=\"选择\" ;;\n \"llm.embedding.select.en\") text=\"Select\" ;;\n \"llm.embedding.custom_prompt.zh\") text=\" Embedding 模型名称\" ;;\n \"llm.embedding.custom_prompt.en\") text=\" Embedding model name\" ;;\n \"llm.embedding.test.testing.zh\") text=\"正在测试 Embedding API 联通性...\" ;;\n \"llm.embedding.test.testing.en\") text=\"Testing Embedding API connectivity...\" ;;\n \"llm.embedding.test.ok.zh\") text=\"✅ Embedding API 联通性测试通过\" ;;\n \"llm.embedding.test.ok.en\") text=\"✅ Embedding API connectivity test passed\" ;;\n \"llm.embedding.test.fail.zh\") text=\"⚠️ Embedding API 测试失败(HTTP %s)。响应: %s\" ;;\n \"llm.embedding.test.fail.en\") text=\"⚠️ Embedding API test failed (HTTP %s). Response: %s\" ;;\n \"llm.embedding.auto_disabled.zh\") text=\"⚠️ Embedding 已自动禁用,记忆搜索将使用关键词匹配。您可以稍后在 agentteams-manager.env 中设置 AGENTTEAMS_EMBEDDING_MODEL 启用。\" ;;\n \"llm.embedding.auto_disabled.en\") text=\"⚠️ Embedding auto-disabled. Memory search will use keyword matching. You can enable it later in agentteams-manager.env by setting AGENTTEAMS_EMBEDDING_MODEL.\" ;;\n \"llm.embedding.disabled.zh\") text=\"ℹ️ Embedding 已禁用,记忆搜索将使用关键词匹配。\" ;;\n \"llm.embedding.disabled.en\") text=\"ℹ️ Embedding disabled. Memory search will use keyword matching.\" ;;\n \"llm.openai.test.aborted.zh\") text=\"安装已中止。\" ;;\n \"llm.openai.test.aborted.en\") text=\"Installation aborted.\" ;;\n \"nav.back_hint.zh\") text=\"(输入 b 返回上一步)\" ;;\n \"nav.back_hint.en\") text=\"(enter b to go back)\" ;;\n # --- OpenAI-compatible provider creation ---\n \"install.openai_compat.missing.zh\") text=\"警告: OpenAI Base URL 或 API Key 未设置,跳过提供商创建\" ;;\n \"install.openai_compat.missing.en\") text=\"WARNING: OpenAI Base URL or API Key not set, skipping provider creation\" ;;\n \"install.openai_compat.creating.zh\") text=\"正在创建 OpenAI 兼容提供商...\" ;;\n \"install.openai_compat.creating.en\") text=\"Creating OpenAI-compatible provider...\" ;;\n \"install.openai_compat.domain.zh\") text=\" 域名: %s\" ;;\n \"install.openai_compat.domain.en\") text=\" Domain: %s\" ;;\n \"install.openai_compat.port.zh\") text=\" 端口: %s\" ;;\n \"install.openai_compat.port.en\") text=\" Port: %s\" ;;\n \"install.openai_compat.protocol.zh\") text=\" 协议: %s\" ;;\n \"install.openai_compat.protocol.en\") text=\" Protocol: %s\" ;;\n \"install.openai_compat.service_fail.zh\") text=\"警告: 创建 DNS 服务源失败(可能已存在)\" ;;\n \"install.openai_compat.service_fail.en\") text=\"WARNING: Failed to create DNS service source (may already exist)\" ;;\n \"install.openai_compat.provider_fail.zh\") text=\"警告: 创建 AI 提供商失败(可能已存在)\" ;;\n \"install.openai_compat.provider_fail.en\") text=\"WARNING: Failed to create AI provider (may already exist)\" ;;\n \"install.openai_compat.success.zh\") text=\"OpenAI 兼容提供商创建成功\" ;;\n \"install.openai_compat.success.en\") text=\"OpenAI-compatible provider created successfully\" ;;\n # --- Welcome message ---\n \"install.welcome_msg.soul_configured.zh\") text=\"Soul 已配置(找到 soul-configured 标记),跳过 onboarding 消息\" ;;\n \"install.welcome_msg.soul_configured.en\") text=\"Soul already configured (soul-configured marker found), skipping onboarding message\" ;;\n \"install.welcome_msg.logging_in.zh\") text=\"正在以 %s 身份登录以发送欢迎消息...\" ;;\n \"install.welcome_msg.logging_in.en\") text=\"Logging in as %s to send welcome message...\" ;;\n \"install.welcome_msg.login_failed.zh\") text=\"警告: 以 %s 身份登录失败,跳过欢迎消息\" ;;\n \"install.welcome_msg.login_failed.en\") text=\"WARNING: Failed to login as %s, skipping welcome message\" ;;\n \"install.welcome_msg.finding_room.zh\") text=\"正在查找与 Manager 的 DM 房间...\" ;;\n \"install.welcome_msg.finding_room.en\") text=\"Finding DM room with Manager...\" ;;\n \"install.welcome_msg.creating_room.zh\") text=\"正在创建与 Manager 的 DM 房间...\" ;;\n \"install.welcome_msg.creating_room.en\") text=\"Creating DM room with Manager...\" ;;\n \"install.welcome_msg.no_room.zh\") text=\"警告: 无法找到或创建与 Manager 的 DM 房间\" ;;\n \"install.welcome_msg.no_room.en\") text=\"WARNING: Could not find or create DM room with Manager\" ;;\n \"install.welcome_msg.waiting_join.zh\") text=\"等待 Manager 加入房间...\" ;;\n \"install.welcome_msg.waiting_join.en\") text=\"Waiting for Manager to join the room...\" ;;\n \"install.welcome_msg.sending.zh\") text=\"正在向 Manager 发送欢迎消息...\" ;;\n \"install.welcome_msg.sending.en\") text=\"Sending welcome message to Manager...\" ;;\n \"install.welcome_msg.send_failed.zh\") text=\"警告: 发送欢迎消息失败\" ;;\n \"install.welcome_msg.send_failed.en\") text=\"WARNING: Failed to send welcome message\" ;;\n \"install.welcome_msg.sent.zh\") text=\"欢迎消息已发送给 Manager\" ;;\n \"install.welcome_msg.sent.en\") text=\"Welcome message sent to Manager\" ;;\n \"install.welcome_msg.waiting.zh\") text=\"等待 Manager 发送欢迎消息(Higress 路由授权 + LLM 探活,约 45-90s)...\" ;;\n \"install.welcome_msg.waiting.en\") text=\"Waiting for Manager to send the welcome message (Higress route auth + LLM probe, ~45-90s)...\" ;;\n \"install.welcome_msg.confirmed.zh\") text=\"Manager 已确认发送欢迎消息(status.welcomeSent=true,用时 %ss)\" ;;\n \"install.welcome_msg.confirmed.en\") text=\"Manager confirmed welcome message sent (status.welcomeSent=true, %ss elapsed)\" ;;\n \"install.welcome_msg.timeout.zh\") text=\"警告: 在 %ss 内未观察到 Manager 发送欢迎消息(status.welcomeSent=true)。安装仍然成功,所有服务已就绪——可继续按下方提示登录 Element Web。\" ;;\n \"install.welcome_msg.timeout.en\") text=\"WARNING: Did not observe the Manager sending its welcome message (status.welcomeSent=true) within %ss. Installation is still successful, all services are up — continue with the Element Web instructions below.\" ;;\n \"install.welcome_msg.timeout_hint.zh\") text=\"手动触发 onboarding: 登录 Element Web → 打开与 Manager 的 DM 房间 → 发送任意一句话(例如 \\\"hi\\\"),Manager 会接管对话并开始引导。\" ;;\n \"install.welcome_msg.timeout_hint.en\") text=\"Manual onboarding: log in to Element Web → open the DM with the Manager → send any message (e.g. \\\"hi\\\") and the Manager will take over and start the guided setup.\" ;;\n \"install.welcome_msg.timeout_inspect.zh\") text=\"排查命令: docker exec agentteams-controller agt get managers default\" ;;\n \"install.welcome_msg.timeout_inspect.en\") text=\"Inspect status: docker exec agentteams-controller agt get managers default\" ;;\n \"install.welcome_msg.poll_unavailable.zh\") text=\"提示: agentteams-manager 内未找到 AgentTeams CLI,跳过 welcome 等待(旧镜像?)\" ;;\n \"install.welcome_msg.poll_unavailable.en\") text=\"Note: AgentTeams CLI not found inside agentteams-manager; skipping welcome wait (old image?)\" ;;\n # --- Dashboard wizard ---\n \"dash.prompt.zh\") text=\"是否安装 agentteams-dashboard 管理面板?\" ;;\n \"dash.prompt.en\") text=\"Install agentteams-dashboard management UI?\" ;;\n \"dash.version_prompt.zh\") text=\"Dashboard 版本\" ;;\n \"dash.version_prompt.en\") text=\"Dashboard version\" ;;\n \"dash.port_prompt.zh\") text=\"Dashboard 端口号\" ;;\n \"dash.port_prompt.en\") text=\"Dashboard port\" ;;\n \"dash.image_prompt.zh\") text=\"Dashboard 镜像\" ;;\n \"dash.image_prompt.en\") text=\"Dashboard image\" ;;\n \"dash.gateway_prompt.zh\") text=\"Higress Console URL (用于共享登录)\" ;;\n \"dash.gateway_prompt.en\") text=\"Higress Console URL (for shared login)\" ;;\n \"dash.skip.zh\") text=\"跳过 Dashboard 安装\" ;;\n \"dash.skip.en\") text=\"Skipping Dashboard installation\" ;;\n \"dash.auto_url.zh\") text=\"自动检测到 Higress Console URL\" ;;\n \"dash.auto_url.en\") text=\"Auto-detected Higress Console URL\" ;;\n \"dash.auto_url_fail.zh\") text=\"无法自动检测 Higress Console URL,请手动输入\" ;;\n \"dash.auto_url_fail.en\") text=\"Cannot auto-detect Higress Console URL, please enter manually\" ;;\n \"dash.summary.zh\") text=\"Dashboard 配置: 端口=%s 镜像=%s\" ;;\n \"dash.summary.en\") text=\"Dashboard config: port=%s image=%s\" ;;\n # --- Dashboard success ---\n \"success.dashboard.zh\") text=\" agentteams-dashboard 管理面板: http://localhost:%s/\" ;;\n \"success.dashboard.en\") text=\" agentteams-dashboard: http://localhost:%s/\" ;;\n # --- Final output panel ---\n \"success.title.zh\") text=\"=== AgentTeams Manager 已启动!===\" ;;\n \"success.title.en\") text=\"=== AgentTeams Manager Started! ===\" ;;\n \"success.domains_configured.zh\") text=\"以下域名已配置解析到 127.0.0.1:\" ;;\n \"success.domains_configured.en\") text=\"The following domains are configured to resolve to 127.0.0.1:\" ;;\n \"success.open_url.zh\") text=\" ★ 在浏览器中打开以下 URL 开始使用: ★\" ;;\n \"success.open_url.en\") text=\" ★ Open the following URL in your browser to start: ★\" ;;\n \"success.login_with.zh\") text=\" 登录信息:\" ;;\n \"success.login_with.en\") text=\" Login with:\" ;;\n \"success.username.zh\") text=\" 用户名: %s\" ;;\n \"success.username.en\") text=\" Username: %s\" ;;\n \"success.password.zh\") text=\" 密码: %s\" ;;\n \"success.password.en\") text=\" Password: %s\" ;;\n \"success.after_login.zh\") text=\" 登录后,开始与 Manager 聊天!\" ;;\n \"success.after_login.en\") text=\" After login, start chatting with the Manager!\" ;;\n \"success.tell_it.zh\") text=\" 告诉它: \\\"创建一个名为 alice 的前端开发 Worker\\\"\" ;;\n \"success.tell_it.en\") text=\" Tell it: \\\"Create a Worker named alice for frontend dev\\\"\" ;;\n \"success.manager_auto.zh\") text=\" Manager 会自动处理一切。\" ;;\n \"success.manager_auto.en\") text=\" The Manager will handle everything automatically.\" ;;\n \"success.mobile_title.zh\") text=\" 📱 移动端访问(FluffyChat / Element Mobile):\" ;;\n \"success.mobile_title.en\") text=\" 📱 Mobile access (FluffyChat / Element Mobile):\" ;;\n \"success.mobile_step1.zh\") text=\" 1. 在手机上下载 FluffyChat 或 Element\" ;;\n \"success.mobile_step1.en\") text=\" 1. Download FluffyChat or Element on your phone\" ;;\n \"success.mobile_step2.zh\") text=\" 2. 设置 homeserver 为: %s\" ;;\n \"success.mobile_step2.en\") text=\" 2. Set homeserver to: %s\" ;;\n \"success.mobile_step2_noip.zh\") text=\" 2. 设置 homeserver 为: http://<本机局域网IP>:%s\" ;;\n \"success.mobile_step2_noip.en\") text=\" 2. Set homeserver to: http://<this-machine-LAN-IP>:%s\" ;;\n \"success.mobile_noip_hint.zh\") text=\" (无法自动检测局域网 IP — 请使用 ifconfig / ip addr 查看)\" ;;\n \"success.mobile_noip_hint.en\") text=\" (Could not detect LAN IP automatically — check with: ifconfig / ip addr)\" ;;\n \"success.mobile_step3.zh\") text=\" 3. 登录信息:\" ;;\n \"success.mobile_step3.en\") text=\" 3. Login with:\" ;;\n \"success.mobile_username.zh\") text=\" 用户名: %s\" ;;\n \"success.mobile_username.en\") text=\" Username: %s\" ;;\n \"success.mobile_password.zh\") text=\" 密码: %s\" ;;\n \"success.mobile_password.en\") text=\" Password: %s\" ;;\n # --- Other consoles and tips ---\n \"success.other_consoles.zh\") text=\"--- 其他控制台 ---\" ;;\n \"success.other_consoles.en\") text=\"--- Other Consoles ---\" ;;\n \"success.higress_console.zh\") text=\" Higress 控制台: http://localhost:%s(用户名: %s / 密码: %s)\" ;;\n \"success.higress_console.en\") text=\" Higress Console: http://localhost:%s (Username: %s / Password: %s)\" ;;\n \"success.manager_console.zh\") text=\" Manager 控制台(本地): http://localhost:%s(无需登录)\" ;;\n \"success.manager_console.en\") text=\" Manager Console (local): http://localhost:%s (no login required)\" ;;\n \"success.manager_console_gateway.zh\") text=\" Manager 控制台(网关): http://console-local.agentteams.io(用户名: %s / 密码: %s)\" ;;\n \"success.manager_console_gateway.en\") text=\" Manager Console (gateway): http://console-local.agentteams.io (Username: %s / Password: %s)\" ;;\n \"success.copaw_console.zh\") text=\" CoPaw App API: http://localhost:%s(无需登录)\" ;;\n \"success.copaw_console.en\") text=\" CoPaw App API: http://localhost:%s (no login required)\" ;;\n \"success.switch_llm.title.zh\") text=\"--- 切换 LLM 提供商 ---\" ;;\n \"success.switch_llm.title.en\") text=\"--- Switch LLM Providers ---\" ;;\n \"success.switch_llm.hint.zh\") text=\" 您可以通过 Higress 控制台切换到其他 LLM 提供商(OpenAI、Anthropic 等)。\" ;;\n \"success.switch_llm.hint.en\") text=\" You can switch to other LLM providers (OpenAI, Anthropic, etc.) via Higress Console.\" ;;\n \"success.switch_llm.docs.zh\") text=\" 详细说明请参阅:\" ;;\n \"success.switch_llm.docs.en\") text=\" For detailed instructions, see:\" ;;\n \"success.switch_llm.url.zh\") text=\" https://higress.ai/en/docs/ai/scene-guide/multi-proxy#console-configuration\" ;;\n \"success.switch_llm.url.en\") text=\" https://higress.ai/en/docs/ai/scene-guide/multi-proxy#console-configuration\" ;;\n \"success.tip.zh\") text=\"提示: 您也可以在聊天中让 Manager 为您配置 LLM 提供商。\" ;;\n \"success.tip.en\") text=\"Tip: You can also ask the Manager to configure LLM providers for you in the chat.\" ;;\n \"success.config_file.zh\") text=\"配置文件: %s\" ;;\n \"success.config_file.en\") text=\"Configuration file: %s\" ;;\n \"success.data_volume.zh\") text=\"数据卷: %s\" ;;\n \"success.data_volume.en\") text=\"Data volume: %s\" ;;\n \"success.workspace.zh\") text=\"Manager 工作空间: %s\" ;;\n \"success.workspace.en\") text=\"Manager workspace: %s\" ;;\n # --- Worker installation ---\n \"worker.resetting.zh\") text=\"正在重置 Worker: %s...\" ;;\n \"worker.resetting.en\") text=\"Resetting Worker: %s...\" ;;\n \"worker.exists.zh\") text=\"容器 '%s' 已存在。使用 --reset 重新创建。\" ;;\n \"worker.exists.en\") text=\"Container '%s' already exists. Use --reset to recreate.\" ;;\n \"worker.starting.zh\") text=\"正在启动 Worker: %s...\" ;;\n \"worker.starting.en\") text=\"Starting Worker: %s...\" ;;\n \"worker.skills_url.zh\") text=\" Skills API URL: %s\" ;;\n \"worker.skills_url.en\") text=\" Skills API URL: %s\" ;;\n \"worker.started.zh\") text=\"=== Worker %s 已启动!===\" ;;\n \"worker.started.en\") text=\"=== Worker %s Started! ===\" ;;\n \"worker.container.zh\") text=\"容器: %s\" ;;\n \"worker.container.en\") text=\"Container: %s\" ;;\n \"worker.view_logs.zh\") text=\"查看日志: docker logs -f %s\" ;;\n \"worker.view_logs.en\") text=\"View logs: docker logs -f %s\" ;;\n # --- Prompt function messages ---\n \"prompt.preset.zh\") text=\" %s = (已通过环境变量预设)\" ;;\n \"prompt.preset.en\") text=\" %s = (pre-set via env)\" ;;\n \"prompt.upgrade_keep.zh\") text=\" %s = %s(当前值,回车保留 / 输入新值覆盖)\" ;;\n \"prompt.upgrade_keep.en\") text=\" %s = %s (current value, press Enter to keep / type new value to change)\" ;;\n \"prompt.upgrade_keep_secret.zh\") text=\" %s = %s(当前值,回车保留 / 输入新值覆盖)\" ;;\n \"prompt.upgrade_keep_secret.en\") text=\" %s = %s (current value, press Enter to keep / type new value to change)\" ;;\n \"prompt.upgrade_empty.zh\") text=\" %s = (未设置,回车跳过 / 输入新值设置)\" ;;\n \"prompt.upgrade_empty.en\") text=\" %s = (not set, press Enter to skip / type new value to set)\" ;;\n \"prompt.default.zh\") text=\" %s = %s(默认)\" ;;\n \"prompt.default.en\") text=\" %s = %s (default)\" ;;\n \"prompt.required.zh\") text=\"%s 是必需的(在非交互模式下通过环境变量设置)\" ;;\n \"prompt.required.en\") text=\"%s is required (set via environment variable in non-interactive mode)\" ;;\n \"prompt.required_empty.zh\") text=\"%s 是必需的\" ;;\n \"prompt.required_empty.en\") text=\"%s is required\" ;;\n # --- Language switch prompt (bilingual by design) ---\n \"lang.detected.zh\") text=\"检测到语言 / Detected language: 中文\" ;;\n \"lang.detected.en\") text=\"检测到语言 / Detected language: English\" ;;\n \"lang.switch_title.zh\") text=\"切换语言 / Switch language:\" ;;\n \"lang.switch_title.en\") text=\"切换语言 / Switch language:\" ;;\n \"lang.option_zh.zh\") text=\" 1) 中文\" ;;\n \"lang.option_zh.en\") text=\" 1) 中文\" ;;\n \"lang.option_en.zh\") text=\" 2) English\" ;;\n \"lang.option_en.en\") text=\" 2) English\" ;;\n \"lang.prompt.zh\") text=\"请选择 / Enter choice\" ;;\n \"lang.prompt.en\") text=\"请选择 / Enter choice\" ;;\n # --- Uninstall messages ---\n \"uninstall.title.zh\") text=\"正在卸载 AgentTeams...\" ;;\n \"uninstall.title.en\") text=\"Uninstalling AgentTeams...\" ;;\n \"uninstall.stopping_manager.zh\") text=\"正在停止并移除 agentteams-manager...\" ;;\n \"uninstall.stopping_manager.en\") text=\"Stopping and removing agentteams-manager...\" ;;\n \"uninstall.stopping_workers.zh\") text=\"正在停止并移除 Worker 容器...\" ;;\n \"uninstall.stopping_workers.en\") text=\"Stopping and removing worker containers...\" ;;\n \"uninstall.removed.zh\") text=\" 已移除: %s\" ;;\n \"uninstall.removed.en\") text=\" Removed: %s\" ;;\n \"uninstall.removing_volume.zh\") text=\"正在移除 Docker 卷: %s\" ;;\n \"uninstall.removing_volume.en\") text=\"Removing Docker volume: %s\" ;;\n \"uninstall.removing_env.zh\") text=\"正在移除 env 文件: %s\" ;;\n \"uninstall.removing_env.en\") text=\"Removing env file: %s\" ;;\n \"uninstall.removing_proxy.zh\") text=\"正在停止并移除 Docker API 代理容器: agentteams-docker-proxy\" ;;\n \"uninstall.removing_proxy.en\") text=\"Stopping and removing Docker API proxy container: agentteams-docker-proxy\" ;;\n \"uninstall.stopping_controller.zh\") text=\"正在停止并移除 agentteams-controller (内嵌 Tuwunel/MinIO/Higress)...\" ;;\n \"uninstall.stopping_controller.en\") text=\"Stopping and removing agentteams-controller (embedded Tuwunel/MinIO/Higress)...\" ;;\n \"uninstall.removing_network.zh\") text=\"正在移除 Docker 网络: agentteams-net\" ;;\n \"uninstall.removing_network.en\") text=\"Removing Docker network: agentteams-net\" ;;\n \"uninstall.removing_workspace.zh\") text=\"正在移除工作空间目录: %s\" ;;\n \"uninstall.removing_workspace.en\") text=\"Removing workspace directory: %s\" ;;\n \"uninstall.removing_workspace_elevated.zh\") text=\" 工作空间包含 root 文件,通过容器清理...\" ;;\n \"uninstall.removing_workspace_elevated.en\") text=\" Workspace contains root-owned files, cleaning via container...\" ;;\n \"uninstall.removing_log.zh\") text=\"正在移除日志文件: %s\" ;;\n \"uninstall.removing_log.en\") text=\"Removing log file: %s\" ;;\n \"uninstall.done.zh\") text=\"AgentTeams 已卸载。\" ;;\n \"uninstall.done.en\") text=\"AgentTeams has been uninstalled.\" ;;\n # --- Error messages ---\n \"error.name_required.zh\") text=\"--name 是必需的\" ;;\n \"error.name_required.en\") text=\"--name is required\" ;;\n \"error.fs_required.zh\") text=\"--fs 是必需的\" ;;\n \"error.fs_required.en\") text=\"--fs is required\" ;;\n \"error.fs_key_required.zh\") text=\"--fs-key 是必需的\" ;;\n \"error.fs_key_required.en\") text=\"--fs-key is required\" ;;\n \"error.fs_secret_required.zh\") text=\"--fs-secret 是必需的\" ;;\n \"error.fs_secret_required.en\") text=\"--fs-secret is required\" ;;\n \"error.unknown_option.zh\") text=\"未知选项: %s\" ;;\n \"error.unknown_option.en\") text=\"Unknown option: %s\" ;;\n \"error.docker_not_found.zh\") text=\"未找到 docker 或 podman 命令。请先安装 Docker Desktop 或 Podman Desktop:\\n Docker Desktop: https://www.docker.com/products/docker-desktop/\\n Podman Desktop: https://podman-desktop.io/\" ;;\n \"error.docker_not_found.en\") text=\"docker or podman command not found. Please install Docker Desktop or Podman Desktop first:\\n Docker Desktop: https://www.docker.com/products/docker-desktop/\\n Podman Desktop: https://podman-desktop.io/\" ;;\n \"error.docker_not_running.zh\") text=\"Docker 未运行。请先启动 Docker Desktop 或 Podman Desktop。\" ;;\n \"error.docker_not_running.en\") text=\"Docker is not running. Please start Docker Desktop or Podman Desktop first.\" ;;\n # --- Fallback: try English for unknown lang ---\n *)\n case \"${key}.en\" in\n \"tz.warning.title.en\") text=\"Could not detect timezone automatically.\" ;;\n \"install.title.en\") text=\"=== AgentTeams Manager Installation ===\" ;;\n *) text=\"${key}\" ;;\n esac\n ;;\n esac\n if [ $# -gt 0 ]; then\n # shellcheck disable=SC2059\n printf \"${text}\\n\" \"$@\"\n else\n echo \"${text}\"\n fi\n}\n\n# ============================================================\n# Registry selection based on timezone\n# ============================================================\n\ndetect_registry() {\n local tz=\"${AGENTTEAMS_TIMEZONE}\"\n\n case \"${tz}\" in\n America/*)\n echo \"higress-registry.us-west-1.cr.aliyuncs.com\"\n ;;\n Asia/Singapore|Asia/Bangkok|Asia/Jakarta|Asia/Makassar|Asia/Jayapura|\\\n Asia/Kuala_Lumpur|Asia/Ho_Chi_Minh|Asia/Manila|Asia/Yangon|\\\n Asia/Vientiane|Asia/Phnom_Penh|Asia/Pontianak|Asia/Ujung_Pandang)\n echo \"higress-registry.ap-southeast-7.cr.aliyuncs.com\"\n ;;\n *)\n echo \"higress-registry.cn-hangzhou.cr.aliyuncs.com\"\n ;;\n esac\n}\n\nAGENTTEAMS_REGISTRY=\"${AGENTTEAMS_REGISTRY:-$(detect_registry)}\"\n# Backward compatibility: accept old env var names from previous versions\nAGENTTEAMS_INSTALL_CONTROLLER_IMAGE=\"${AGENTTEAMS_INSTALL_CONTROLLER_IMAGE:-${AGENTTEAMS_INSTALL_DOCKER_PROXY_IMAGE:-}}\"\n# Image variables are resolved after version selection in step_version().\n# These placeholders allow early code paths to reference them without errors.\nMANAGER_IMAGE=\"${AGENTTEAMS_INSTALL_MANAGER_IMAGE:-}\"\nMANAGER_COPAW_IMAGE=\"${AGENTTEAMS_INSTALL_MANAGER_COPAW_IMAGE:-}\"\nWORKER_IMAGE=\"${AGENTTEAMS_INSTALL_WORKER_IMAGE:-}\"\nCOPAW_WORKER_IMAGE=\"${AGENTTEAMS_INSTALL_COPAW_WORKER_IMAGE:-}\"\nQWENPAW_WORKER_IMAGE=\"${AGENTTEAMS_INSTALL_QWENPAW_WORKER_IMAGE:-}\"\nHERMES_WORKER_IMAGE=\"${AGENTTEAMS_INSTALL_HERMES_WORKER_IMAGE:-}\"\nCONTROLLER_IMAGE=\"${AGENTTEAMS_INSTALL_CONTROLLER_IMAGE:-}\"\n\nresolve_image_tags() {\n AGENTTEAMS_VERSION=\"$(_normalize_version \"${AGENTTEAMS_VERSION}\")\"\n MANAGER_IMAGE=\"${AGENTTEAMS_INSTALL_MANAGER_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-manager:${AGENTTEAMS_VERSION}}\"\n MANAGER_COPAW_IMAGE=\"${AGENTTEAMS_INSTALL_MANAGER_COPAW_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-manager-copaw:${AGENTTEAMS_VERSION}}\"\n WORKER_IMAGE=\"${AGENTTEAMS_INSTALL_WORKER_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-worker:${AGENTTEAMS_VERSION}}\"\n COPAW_WORKER_IMAGE=\"${AGENTTEAMS_INSTALL_COPAW_WORKER_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-copaw-worker:${AGENTTEAMS_VERSION}}\"\n QWENPAW_WORKER_IMAGE=\"${AGENTTEAMS_INSTALL_QWENPAW_WORKER_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-qwenpaw-worker:${AGENTTEAMS_VERSION}}\"\n HERMES_WORKER_IMAGE=\"${AGENTTEAMS_INSTALL_HERMES_WORKER_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-hermes-worker:${AGENTTEAMS_VERSION}}\"\n EMBEDDED_IMAGE=\"${AGENTTEAMS_INSTALL_EMBEDDED_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-embedded:${AGENTTEAMS_VERSION}}\"\n # CoPaw Worker introduced in v1.0.4; Hermes Worker introduced in v1.1.0\n if [ -z \"${AGENTTEAMS_INSTALL_COPAW_WORKER_IMAGE:-}\" ] && _ver_lt \"${AGENTTEAMS_VERSION}\" \"v1.0.4\"; then\n COPAW_WORKER_IMAGE=\"\"\n fi\n if [ -z \"${AGENTTEAMS_INSTALL_HERMES_WORKER_IMAGE:-}\" ] && _ver_lt \"${AGENTTEAMS_VERSION}\" \"v1.1.0\"; then\n HERMES_WORKER_IMAGE=\"\"\n fi\n}\n\n# Resolve the embedded controller image. Embedded mode is the only supported\n# architecture since PR #616 (manager image no longer bundles Higress/Tuwunel/MinIO).\n# If the embedded image is unavailable for the requested version, fail fast with an\n# actionable error rather than silently falling back to the legacy single-container\n# path — that path is permanently broken with the slim manager image and would just\n# leave the user with a manager container looping on \"Waiting for Higress Gateway\".\n# Sets EMBEDDED_IMAGE and AGENTTEAMS_USE_EMBEDDED.\nresolve_embedded_image() {\n AGENTTEAMS_USE_EMBEDDED=1\n\n # If the user explicitly overrode the image (e.g. `make install-embedded` passes\n # a locally-built tag), respect it as-is without any registry probe.\n if [ -n \"${AGENTTEAMS_INSTALL_EMBEDDED_IMAGE:-}\" ]; then\n EMBEDDED_IMAGE=\"${AGENTTEAMS_INSTALL_EMBEDDED_IMAGE}\"\n return 0\n fi\n\n local _versioned=\"${AGENTTEAMS_REGISTRY}/agentteams/agentteams-embedded:${AGENTTEAMS_VERSION}\"\n local _latest=\"${AGENTTEAMS_REGISTRY}/agentteams/agentteams-embedded:latest\"\n\n # Skip probe when AGENTTEAMS_VERSION is \"latest\" — no point trying the same tag twice.\n if [ \"${AGENTTEAMS_VERSION}\" = \"latest\" ]; then\n EMBEDDED_IMAGE=\"${_latest}\"\n return 0\n fi\n\n if ${DOCKER_CMD} pull \"${_versioned}\" >/dev/null 2>&1; then\n EMBEDDED_IMAGE=\"${_versioned}\"\n return 0\n fi\n\n # Versions before v1.1.0 predate agentteams-embedded entirely — their manager image\n # bundled all infrastructure. Falling back to agentteams-embedded:latest would\n # silently swap in the v1.1.0 architecture (embedded kube-apiserver) which\n # crashes under QEMU on Apple Silicon. Auto-activate legacy mode instead.\n if _ver_lt \"${AGENTTEAMS_VERSION}\" \"v1.1.0\"; then\n log \"INFO: ${AGENTTEAMS_VERSION} predates agentteams-embedded; switching to legacy all-in-one manager architecture.\"\n log \"WARNING: Legacy all-in-one mode requires AGENTTEAMS_VERSION <= v1.0.9 (older bundled manager image).\"\n log \"WARNING: Newer slim manager images will hang on 'Waiting for Higress Gateway'.\"\n AGENTTEAMS_USE_EMBEDDED=0\n return 0\n fi\n\n if ${DOCKER_CMD} pull \"${_latest}\" >/dev/null 2>&1; then\n log \"embedded ${AGENTTEAMS_VERSION} not found, using latest\"\n EMBEDDED_IMAGE=\"${_latest}\"\n return 0\n fi\n\n # Explicit escape hatch — still honoured for edge cases.\n if [ \"${AGENTTEAMS_FORCE_LEGACY:-0}\" = \"1\" ]; then\n log \"WARNING: AGENTTEAMS_FORCE_LEGACY=1 — using legacy all-in-one manager architecture.\"\n log \"WARNING: This requires AGENTTEAMS_VERSION <= v1.0.9 (older bundled manager image).\"\n log \"WARNING: Newer slim manager images will hang on 'Waiting for Higress Gateway'.\"\n AGENTTEAMS_USE_EMBEDDED=0\n return 0\n fi\n\n error \"Embedded controller image is not available in the registry:\"\n error \" - tried: ${_versioned}\"\n error \" - tried: ${_latest}\"\n error \"\"\n error \"Embedded mode is the only supported architecture since PR #616.\"\n error \"How to resolve:\"\n error \" 1) Pin to a AGENTTEAMS_VERSION whose embedded image has been published, or\"\n error \" wait for the release pipeline to publish it.\"\n error \" 2) For a local build, run: make install-embedded\"\n error \" (builds and uses the local embedded image without touching the registry).\"\n error \" 3) Override with a custom image: AGENTTEAMS_INSTALL_EMBEDDED_IMAGE=... ./agentteams-install.sh\"\n exit 1\n}\n\n# ============================================================\n# Known models list — used to detect custom models during install\n# ============================================================\nKNOWN_MODELS=\"gpt-5.4 gpt-5.3-codex gpt-5-mini gpt-5-nano claude-opus-4-6 claude-sonnet-4-6 claude-haiku-4-5 qwen3.6-plus qwen3.5-plus deepseek-chat deepseek-reasoner kimi-k2.5 glm-5 MiniMax-M2.7 MiniMax-M2.7-highspeed MiniMax-M2.5\"\n\nis_known_model() {\n local model=\"$1\"\n for m in ${KNOWN_MODELS}; do\n [ \"${m}\" = \"${model}\" ] && return 0\n done\n return 1\n}\n\n# Prompt user for custom model parameters when model is not in the known list.\n# Sets: AGENTTEAMS_MODEL_CONTEXT_WINDOW, AGENTTEAMS_MODEL_MAX_TOKENS, AGENTTEAMS_MODEL_REASONING, AGENTTEAMS_MODEL_VISION\nprompt_custom_model_params() {\n local model=\"$1\"\n if is_known_model \"${model}\"; then\n # Clear any stale custom params for known models\n AGENTTEAMS_MODEL_CONTEXT_WINDOW=\"\"\n AGENTTEAMS_MODEL_MAX_TOKENS=\"\"\n AGENTTEAMS_MODEL_REASONING=\"\"\n AGENTTEAMS_MODEL_VISION=\"\"\n return\n fi\n echo \"\"\n log \"$(msg llm.custom_model.detected \"${model}\")\"\n echo \"\"\n read -e -p \" $(msg llm.custom_model.context_prompt): \" AGENTTEAMS_MODEL_CONTEXT_WINDOW\n if [ \"${AGENTTEAMS_MODEL_CONTEXT_WINDOW}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n AGENTTEAMS_MODEL_CONTEXT_WINDOW=\"${AGENTTEAMS_MODEL_CONTEXT_WINDOW:-150000}\"\n read -e -p \" $(msg llm.custom_model.max_tokens_prompt): \" AGENTTEAMS_MODEL_MAX_TOKENS\n if [ \"${AGENTTEAMS_MODEL_MAX_TOKENS}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n AGENTTEAMS_MODEL_MAX_TOKENS=\"${AGENTTEAMS_MODEL_MAX_TOKENS:-128000}\"\n read -e -p \" $(msg llm.custom_model.reasoning_prompt): \" _reasoning\n if [ \"${_reasoning}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n case \"${_reasoning}\" in\n n|N|no|NO) AGENTTEAMS_MODEL_REASONING=\"false\" ;;\n *) AGENTTEAMS_MODEL_REASONING=\"true\" ;;\n esac\n read -e -p \" $(msg llm.custom_model.vision_prompt): \" _vision\n if [ \"${_vision}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n case \"${_vision}\" in\n y|Y|yes|YES) AGENTTEAMS_MODEL_VISION=\"true\" ;;\n *) AGENTTEAMS_MODEL_VISION=\"false\" ;;\n esac\n log \"$(msg llm.custom_model.summary \"${AGENTTEAMS_MODEL_CONTEXT_WINDOW}\" \"${AGENTTEAMS_MODEL_MAX_TOKENS}\" \"${AGENTTEAMS_MODEL_REASONING}\" \"${AGENTTEAMS_MODEL_VISION}\")\"\n}\n\n# ============================================================\n# Wait for Manager agent to be ready\n# Uses `openclaw gateway health` inside the container to confirm the gateway is running\n# ============================================================\n\nwait_manager_ready() {\n local timeout=\"${AGENTTEAMS_READY_TIMEOUT:-300}\"\n local elapsed=0\n local container=\"${1:-agentteams-manager}\"\n\n log \"$(msg install.wait_ready \"${timeout}\")\"\n\n # Wait for Manager agent to be healthy inside the container\n local runtime=\"${AGENTTEAMS_MANAGER_RUNTIME:-copaw}\"\n while [ \"${elapsed}\" -lt \"${timeout}\" ]; do\n case \"${runtime}\" in\n copaw)\n if ${DOCKER_CMD} exec \"${container}\" curl -sf http://127.0.0.1:18799/api/agents 2>/dev/null | grep -q '\"agents\"'; then\n log \"$(msg install.wait_ready.ok)\"\n return 0\n fi\n ;;\n *)\n if ${DOCKER_CMD} exec \"${container}\" openclaw gateway health --json 2>/dev/null | grep -q '\"ok\"' 2>/dev/null; then\n log \"$(msg install.wait_ready.ok)\"\n return 0\n fi\n ;;\n esac\n sleep 5\n elapsed=$((elapsed + 5))\n printf \"\\r\\033[36m[AgentTeams]\\033[0m $(msg install.wait_ready.waiting \"${elapsed}\" \"${timeout}\")\"\n done\n\n echo \"\"\n die \"$(msg install.wait_ready.timeout \"${timeout}\" \"${container}\")\"\n}\n\nwait_matrix_ready() {\n local timeout=\"${AGENTTEAMS_READY_TIMEOUT:-300}\"\n local elapsed=0\n local container=\"${1:-agentteams-manager}\"\n\n log \"$(msg install.wait_matrix \"${timeout}\")\"\n\n while [ \"${elapsed}\" -lt \"${timeout}\" ]; do\n if ${DOCKER_CMD} exec \"${container}\" curl -sf http://127.0.0.1:6167/_tuwunel/server_version >/dev/null 2>&1; then\n log \"$(msg install.wait_matrix.ok)\"\n return 0\n fi\n sleep 5\n elapsed=$((elapsed + 5))\n printf \"\\r\\033[36m[AgentTeams]\\033[0m $(msg install.wait_matrix.waiting \"${elapsed}\" \"${timeout}\")\"\n done\n\n echo \"\"\n die \"$(msg install.wait_matrix.timeout \"${timeout}\" \"${container}\")\"\n}\n\n# Read KEY=value from /data/agentteams-secrets.env on a Docker volume (manager container not required).\n# Requires EMBEDDED_IMAGE (resolved earlier in install_manager). Uses ${DOCKER_CMD}.\nagentteams_read_secret_from_data_volume() {\n local _vol=\"$1\" _key=\"$2\"\n if [ -z \"${_vol}\" ] || [ -z \"${_key}\" ] || [ -z \"${EMBEDDED_IMAGE:-}\" ]; then\n echo \"\"\n return 0\n fi\n ${DOCKER_CMD} run --rm --entrypoint sh \\\n -v \"${_vol}:/data:ro\" \\\n \"${EMBEDDED_IMAGE}\" -c \"grep \\\"^${_key}=\\\" /data/agentteams-secrets.env 2>/dev/null | cut -d= -f2- | head -1 | tr -d '\\r'\" 2>/dev/null\n}\n\n# Read KEY=value from /data/worker-creds/<worker>.env on a Docker volume.\nagentteams_read_worker_creds_value_from_volume() {\n local _vol=\"$1\" _worker=\"$2\" _key=\"$3\"\n if [ -z \"${_vol}\" ] || [ -z \"${_worker}\" ] || [ -z \"${_key}\" ] || [ -z \"${EMBEDDED_IMAGE:-}\" ]; then\n echo \"\"\n return 0\n fi\n ${DOCKER_CMD} run --rm --entrypoint sh \\\n -v \"${_vol}:/data:ro\" \\\n \"${EMBEDDED_IMAGE}\" -c \"grep \\\"^${_key}=\\\" \\\"/data/worker-creds/${_worker}.env\\\" 2>/dev/null | cut -d= -f2- | head -1 | tr -d \\\"\\\\r\\\"\" 2>/dev/null\n}\n\n# Read admin_dm_room_id from host workspace state.json (fallback when Matrix API is unavailable).\nagentteams_read_admin_dm_room_from_workspace() {\n local _ws=\"$1\"\n local _f=\"${_ws}/state.json\"\n if [ ! -f \"${_f}\" ] || ! command -v jq >/dev/null 2>&1; then\n echo \"\"\n return 0\n fi\n jq -r '.admin_dm_room_id // empty | select(. != \"null\")' \"${_f}\" 2>/dev/null\n}\n\n# Read secret input with masked echo (shows * per keystroke, supports backspace)\n# Usage: read_secret \"prompt text: \"; value=\"${_RS_RESULT}\"\nread_secret() {\n local _rs_prompt=\"$1\"\n _RS_RESULT=\"\"\n local _rs_char=\"\"\n\n printf \"%s\" \"${_rs_prompt}\"\n\n while IFS= read -r -s -n 1 _rs_char; do\n if [[ -z \"${_rs_char}\" ]]; then\n break\n elif [[ \"${_rs_char}\" == $'\\177' ]] || [[ \"${_rs_char}\" == $'\\b' ]]; then\n if [ -n \"${_RS_RESULT}\" ]; then\n _RS_RESULT=\"${_RS_RESULT%?}\"\n printf \"\\b \\b\"\n fi\n else\n _RS_RESULT=\"${_RS_RESULT}${_rs_char}\"\n printf \"*\"\n fi\n done\n echo\n}\n\n# Load current parameter values from env file for upgrade mode display\nload_current_params_from_env() {\n local env_file=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n if [ -f \"${env_file}\" ]; then\n [ -z \"${AGENTTEAMS_LLM_PROVIDER:+x}\" ] && AGENTTEAMS_LLM_PROVIDER=\"$(grep '^AGENTTEAMS_LLM_PROVIDER=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_OPENAI_BASE_URL:+x}\" ] && AGENTTEAMS_OPENAI_BASE_URL=\"$(grep '^AGENTTEAMS_OPENAI_BASE_URL=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_DEFAULT_MODEL:+x}\" ] && AGENTTEAMS_DEFAULT_MODEL=\"$(grep '^AGENTTEAMS_DEFAULT_MODEL=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_EMBEDDING_MODEL:+x}\" ] && AGENTTEAMS_EMBEDDING_MODEL=\"$(grep '^AGENTTEAMS_EMBEDDING_MODEL=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_WORKSPACE_DIR:+x}\" ] && AGENTTEAMS_WORKSPACE_DIR=\"$(grep '^AGENTTEAMS_WORKSPACE_DIR=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_HOST_SHARE_DIR:+x}\" ] && AGENTTEAMS_HOST_SHARE_DIR=\"$(grep '^AGENTTEAMS_HOST_SHARE_DIR=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_PODMAN_AUTOSTART:+x}\" ] && AGENTTEAMS_PODMAN_AUTOSTART=\"$(grep '^AGENTTEAMS_PODMAN_AUTOSTART=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_DASHBOARD:+x}\" ] && AGENTTEAMS_DASHBOARD=\"$(grep '^AGENTTEAMS_DASHBOARD=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_DASHBOARD_VERSION:+x}\" ] && AGENTTEAMS_DASHBOARD_VERSION=\"$(grep '^AGENTTEAMS_DASHBOARD_VERSION=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_PORT_DASHBOARD:+x}\" ] && AGENTTEAMS_PORT_DASHBOARD=\"$(grep '^AGENTTEAMS_PORT_DASHBOARD=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_DASHBOARD_IMAGE:+x}\" ] && AGENTTEAMS_DASHBOARD_IMAGE=\"$(grep '^AGENTTEAMS_DASHBOARD_IMAGE=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL:+x}\" ] && AGENTTEAMS_AI_GATEWAY_ADMIN_URL=\"$(grep '^AGENTTEAMS_AI_GATEWAY_ADMIN_URL=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n fi\n}\n\n# In non-interactive mode, uses default or errors if required and no default.\n# Usage: prompt VAR_NAME \"Prompt text\" \"default\" [true=secret]\nprompt() {\n local var_name=\"$1\"\n local prompt_text=\"$2\"\n local default_value=\"$3\"\n local is_secret=\"${4:-false}\"\n\n # If the variable is already set in the environment, use it silently\n # In upgrade mode, show current value and let user change it\n eval \"local current_value=\\\"\\${${var_name}}\\\"\"\n if [ -n \"${current_value}\" ]; then\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_NON_INTERACTIVE}\" != \"1\" ]; then\n # Show masked value for secrets, full value otherwise\n local display_value=\"${current_value}\"\n if [ \"${is_secret}\" = \"true\" ]; then\n local len=${#current_value}\n if [ \"${len}\" -le 8 ]; then\n display_value=\"****\"\n else\n display_value=\"${current_value:0:4}****${current_value: -4}\"\n fi\n fi\n log \"$(msg prompt.upgrade_keep \"${prompt_text}\" \"${display_value}\")\"\n local new_value=\"\"\n if [ \"${is_secret}\" = \"true\" ]; then\n read_secret \"${prompt_text}: \"\n new_value=\"${_RS_RESULT}\"\n else\n read -e -p \"${prompt_text}: \" new_value\n if [ \"${new_value}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n fi\n if [ -n \"${new_value}\" ]; then\n eval \"export ${var_name}='${new_value}'\"\n fi\n return\n fi\n log \"$(msg prompt.preset \"${prompt_text}\")\"\n return\n fi\n\n # Non-interactive or quickstart: use default or error\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] || [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ]; then\n if [ -n \"${default_value}\" ]; then\n eval \"export ${var_name}='${default_value}'\"\n log \"$(msg prompt.default \"${prompt_text}\" \"${default_value}\")\"\n return\n elif [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n # Only hard-error in fully non-interactive mode, not quickstart\n die \"$(msg prompt.required \"${prompt_text}\")\"\n fi\n # quickstart + no default: fall through to interactive prompt below\n fi\n\n if [ -n \"${default_value}\" ]; then\n prompt_text=\"${prompt_text} [${default_value}]\"\n fi\n\n local value=\"\"\n if [ \"${is_secret}\" = \"true\" ]; then\n read_secret \"${prompt_text}: \"\n value=\"${_RS_RESULT}\"\n else\n read -e -p \"${prompt_text}: \" value\n if [ \"${value}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n fi\n\n value=\"${value:-${default_value}}\"\n if [ -z \"${value}\" ]; then\n die \"$(msg prompt.required_empty \"${prompt_text}\")\"\n fi\n\n eval \"export ${var_name}='${value}'\"\n}\n\n# Prompt for an optional value (empty string is acceptable)\n# Skips prompt if variable is already defined in environment (even if empty)\n# In upgrade mode, shows current value and lets user change it.\n# In non-interactive mode, defaults to empty string.\nprompt_optional() {\n local var_name=\"$1\"\n local prompt_text=\"$2\"\n local is_secret=\"${3:-false}\"\n\n # Check if variable is defined (even if set to empty string)\n eval \"local _chk=\\\"\\${${var_name}+x}\\\"\"\n if [ -n \"${_chk}\" ]; then\n # In upgrade mode, show current value and let user change it\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_NON_INTERACTIVE}\" != \"1\" ]; then\n eval \"local current_value=\\\"\\${${var_name}}\\\"\"\n local display_value=\"${current_value}\"\n if [ \"${is_secret}\" = \"true\" ] && [ -n \"${current_value}\" ]; then\n local len=${#current_value}\n if [ \"${len}\" -le 8 ]; then\n display_value=\"****\"\n else\n display_value=\"${current_value:0:4}****${current_value: -4}\"\n fi\n fi\n if [ -n \"${current_value}\" ]; then\n if [ \"${is_secret}\" = \"true\" ]; then\n log \"$(msg prompt.upgrade_keep_secret \"${prompt_text}\" \"${display_value}\")\"\n else\n log \"$(msg prompt.upgrade_keep \"${prompt_text}\" \"${display_value}\")\"\n fi\n else\n log \"$(msg prompt.upgrade_empty \"${prompt_text}\")\"\n fi\n local new_value=\"\"\n if [ \"${is_secret}\" = \"true\" ]; then\n read_secret \"${prompt_text}: \"\n new_value=\"${_RS_RESULT}\"\n else\n read -e -p \"${prompt_text}: \" new_value\n if [ \"${new_value}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n fi\n if [ -n \"${new_value}\" ]; then\n eval \"export ${var_name}='${new_value}'\"\n fi\n return\n fi\n log \"$(msg prompt.preset \"${prompt_text}\")\"\n return\n fi\n\n # Non-interactive or quickstart: skip, leave unset\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] || [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ]; then\n eval \"export ${var_name}=''\"\n return\n fi\n\n local value=\"\"\n if [ \"${is_secret}\" = \"true\" ]; then\n read_secret \"${prompt_text}: \"\n value=\"${_RS_RESULT}\"\n else\n read -e -p \"${prompt_text}: \" value\n if [ \"${value}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n fi\n\n eval \"export ${var_name}='${value}'\"\n}\n\ngenerate_key() {\n openssl rand -hex 32\n}\n\n# ============================================================\n# Detect container runtime socket on the host\n#\n# Dependency: Requires DOCKER_CMD to reflect the TRUE underlying runtime.\n# (Crucial for unmasking 'podman' disguised as 'docker'.\n# Must be strictly resolved prior, e.g., via check_container_runtime)\n# ============================================================\ndetect_socket() {\n local socket_path\n\n # 1. Respect explicitly defined DOCKER_HOST environment variable\n if [ -n \"${DOCKER_HOST}\" ]; then\n socket_path=$(echo \"${DOCKER_HOST}\" | sed 's|^unix://||')\n if [ -S \"${socket_path}\" ]; then\n echo \"${socket_path}\"\n return 0\n fi\n fi\n\n # 2. Route based on the resolved container runtime\n case \"${DOCKER_CMD}\" in\n docker)\n # Docker path handling\n socket_path=$(docker context ls --format '{{if .Current}}{{.DockerEndpoint}}{{end}}' 2>/dev/null | grep . | sed 's|^unix://||')\n if [ -n \"${socket_path}\" ] && [ -S \"${socket_path}\" ]; then\n echo \"${socket_path}\"\n return 0\n fi\n\n # Rootless Docker\n if [ -n \"${XDG_RUNTIME_DIR:-}\" ] && [ -S \"${XDG_RUNTIME_DIR}/docker.sock\" ]; then\n echo \"${XDG_RUNTIME_DIR}/docker.sock\"\n return 0\n fi\n\n # Rootless Docker (macOS fallback)\n if [ -S \"${HOME}/.docker/run/docker.sock\" ]; then\n echo \"${HOME}/.docker/run/docker.sock\"\n return 0\n fi\n\n # Root Docker\n if [ -S \"/var/run/docker.sock\" ]; then\n echo \"/var/run/docker.sock\"\n return 0\n fi\n ;;\n podman)\n # Podman path handling\n if [ \"$(id -u)\" -eq 0 ]; then\n socket_path=\"/run/podman/podman.sock\"\n else\n socket_path=\"${XDG_RUNTIME_DIR}/podman/podman.sock\"\n fi\n if [ -S \"${socket_path}\" ]; then\n echo \"${socket_path}\"\n return 0\n fi\n ;;\n esac\n\n # Return empty if no socket is found\n echo \"\"\n return 0\n}\n\n# Ensure Podman API socket is active (State mutation)\nensure_podman_socket() {\n local socket_path\n\n # Only applicable if runtime is podman and systemctl is available\n if [ \"${DOCKER_CMD:-}\" != \"podman\" ] || ! command -v systemctl >/dev/null 2>&1; then\n return 0\n fi\n\n echo \"Ensuring Podman API socket is active...\" >&2\n\n # Enable and start the socket based on user privileges\n if [ \"$(id -u)\" -eq 0 ]; then\n socket_path=\"/run/podman/podman.sock\"\n systemctl enable --now podman.socket >/dev/null 2>&1 || true\n else\n socket_path=\"${XDG_RUNTIME_DIR}/podman/podman.sock\"\n systemctl --user enable --now podman.socket >/dev/null 2>&1 || true\n fi\n\n # Give systemd a brief moment to assert the socket for root or rootless user\n local _i\n for _i in 1 2 3; do\n [ -S \"${socket_path}\" ] && break\n sleep 1\n done\n}\n\n# Detect local LAN IP address (cross-platform: macOS and Linux)\ndetect_lan_ip() {\n local ip=\"\"\n\n # macOS: try common Wi-Fi / Ethernet interfaces\n if command -v ipconfig >/dev/null 2>&1; then\n for iface in en0 en1 en2 en3 en4; do\n ip=$(ipconfig getifaddr \"${iface}\" 2>/dev/null)\n if [ -n \"${ip}\" ]; then\n echo \"${ip}\"\n return 0\n fi\n done\n fi\n\n # Linux: ip route — most reliable\n if command -v ip >/dev/null 2>&1; then\n ip=$(ip route get 1.1.1.1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i==\"src\"){print $(i+1); exit}}')\n if [ -n \"${ip}\" ]; then\n echo \"${ip}\"\n return 0\n fi\n fi\n\n # Linux fallback: hostname -I (space-separated list, take first non-loopback)\n if command -v hostname >/dev/null 2>&1; then\n ip=$(hostname -I 2>/dev/null | tr ' ' '\\n' | grep -v '^127\\.' | grep -v '^::' | head -1)\n if [ -n \"${ip}\" ]; then\n echo \"${ip}\"\n return 0\n fi\n fi\n\n # Last resort: ifconfig\n if command -v ifconfig >/dev/null 2>&1; then\n ip=$(ifconfig 2>/dev/null | awk '/inet /{if($2!~/^127\\./){print $2; exit}}')\n # Strip \"addr:\" prefix that some ifconfig versions add\n ip=\"${ip#addr:}\"\n if [ -n \"${ip}\" ]; then\n echo \"${ip}\"\n return 0\n fi\n fi\n\n echo \"\"\n}\n\n\n# ============================================================\n# Step-back navigation helpers\n# ============================================================\n\n# should_skip_step: returns 0 (skip) when the step is irrelevant in current mode\nshould_skip_step() {\n local step_fn=\"$1\"\n case \"${step_fn}\" in\n step_lang|step_mode)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n ;;\n step_version)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n ;;\n step_existing)\n local _env=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n [ ! -f \"${_env}\" ] && return 0\n ;;\n # Keep-All upgrade mode: skip all config steps (step_volume/step_workspace handled separately)\n step_llm|step_admin|step_network|step_ports|step_domains|step_github|step_skills|step_runtime|step_manager_runtime|step_dashboard|step_e2ee|step_docker_proxy|step_idle|step_hostshare)\n [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ] && return 0\n ;;\n step_volume|step_workspace)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ] && return 0\n [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ] && return 0\n ;;\n step_e2ee|step_idle)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE}\" != \"1\" ] && return 0\n ;;\n step_docker_proxy)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE}\" != \"1\" ] && return 0\n # Embedded mode handles docker access natively — skip this step\n [ \"${AGENTTEAMS_USE_EMBEDDED:-}\" = \"1\" ] && return 0\n ;;\n step_manager_runtime)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n ;;\n step_hostshare)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ] && return 0\n ;;\n step_podman_autostart)\n # Only relevant for Podman with systemd\n [ \"${DOCKER_CMD}\" != \"podman\" ] && return 0\n ! command -v systemctl >/dev/null 2>&1 && return 0\n\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE}\" != \"1\" ] && return 0\n [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ] && return 0\n ;;\n esac\n return 1\n}\n\n# clear_step_vars: unset variables set by a step so it will re-prompt on re-entry\nclear_step_vars() {\n local step_fn=\"$1\"\n case \"${step_fn}\" in\n step_mode) unset AGENTTEAMS_QUICKSTART ;;\n step_version) unset AGENTTEAMS_VERSION ;;\n step_existing) unset AGENTTEAMS_UPGRADE UPGRADE_EXISTING_WORKERS ;;\n step_llm)\n unset AGENTTEAMS_LLM_PROVIDER AGENTTEAMS_DEFAULT_MODEL AGENTTEAMS_OPENAI_BASE_URL\n unset AGENTTEAMS_LLM_API_KEY AGENTTEAMS_MODEL_CONTEXT_WINDOW AGENTTEAMS_MODEL_MAX_TOKENS\n unset AGENTTEAMS_MODEL_REASONING AGENTTEAMS_MODEL_VISION\n ;;\n step_admin) unset AGENTTEAMS_ADMIN_USER AGENTTEAMS_ADMIN_PASSWORD ;;\n step_network) unset AGENTTEAMS_LOCAL_ONLY ;;\n step_ports)\n unset AGENTTEAMS_PORT_GATEWAY AGENTTEAMS_PORT_CONSOLE\n unset AGENTTEAMS_PORT_ELEMENT_WEB AGENTTEAMS_PORT_MANAGER_CONSOLE\n ;;\n step_domains)\n unset AGENTTEAMS_MATRIX_DOMAIN AGENTTEAMS_MATRIX_CLIENT_DOMAIN\n unset AGENTTEAMS_AI_GATEWAY_DOMAIN AGENTTEAMS_FS_DOMAIN AGENTTEAMS_CONSOLE_DOMAIN\n ;;\n step_github) unset AGENTTEAMS_GITHUB_TOKEN ;;\n step_skills) unset AGENTTEAMS_SKILLS_API_URL ;;\n step_volume) unset AGENTTEAMS_DATA_DIR ;;\n step_workspace) unset AGENTTEAMS_WORKSPACE_DIR ;;\n step_dashboard) unset AGENTTEAMS_DASHBOARD AGENTTEAMS_DASHBOARD_VERSION AGENTTEAMS_PORT_DASHBOARD AGENTTEAMS_DASHBOARD_IMAGE AGENTTEAMS_AI_GATEWAY_ADMIN_URL ;;\n step_runtime) unset AGENTTEAMS_DEFAULT_WORKER_RUNTIME ;;\n step_manager_runtime) unset AGENTTEAMS_MANAGER_RUNTIME ;;\n step_e2ee) unset AGENTTEAMS_MATRIX_E2EE ;;\n step_docker_proxy) unset AGENTTEAMS_DOCKER_PROXY; unset AGENTTEAMS_PROXY_ALLOWED_REGISTRIES ;;\n step_idle) unset AGENTTEAMS_WORKER_IDLE_TIMEOUT ;;\n step_hostshare) unset AGENTTEAMS_HOST_SHARE_DIR ;;\n step_podman_autostart) unset AGENTTEAMS_PODMAN_AUTOSTART ;;\n esac\n}\n\n# ============================================================\n# Individual step functions\n# ============================================================\n\nstep_lang() {\n local lang_default_choice=\"2\"\n [ \"${AGENTTEAMS_LANGUAGE}\" = \"zh\" ] && lang_default_choice=\"1\"\n log \"$(msg lang.detected)\"\n log \"$(msg lang.switch_title)\"\n echo \"$(msg lang.option_zh)\"\n echo \"$(msg lang.option_en)\"\n echo \"\"\n local LANG_CHOICE\n read -e -p \"$(msg lang.prompt) [${lang_default_choice}]: \" LANG_CHOICE\n LANG_CHOICE=\"${LANG_CHOICE:-${lang_default_choice}}\"\n if [ \"${LANG_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n case \"${LANG_CHOICE}\" in\n 1) AGENTTEAMS_LANGUAGE=\"zh\" ;;\n 2) AGENTTEAMS_LANGUAGE=\"en\" ;;\n esac\n export AGENTTEAMS_LANGUAGE\n log \"\"\n}\n\nstep_mode() {\n log \"$(msg install.mode.title)\"\n echo \"\"\n echo \"$(msg install.mode.choose)\"\n echo \"$(msg install.mode.quickstart)\"\n echo \"$(msg install.mode.manual)\"\n echo \"\"\n local ONBOARDING_CHOICE\n read -e -p \"$(msg install.mode.prompt): \" ONBOARDING_CHOICE\n ONBOARDING_CHOICE=\"${ONBOARDING_CHOICE:-1}\"\n if [ \"${ONBOARDING_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n case \"${ONBOARDING_CHOICE}\" in\n 1|quick|quickstart)\n log \"$(msg install.mode.quickstart_selected)\"\n AGENTTEAMS_QUICKSTART=1\n ;;\n 2|manual)\n log \"$(msg install.mode.manual_selected)\"\n ;;\n *)\n log \"$(msg install.mode.invalid)\"\n AGENTTEAMS_QUICKSTART=1\n ;;\n esac\n log \"\"\n}\n\nstep_version() {\n # Skip if version already provided via env var\n if [ -n \"${AGENTTEAMS_VERSION}\" ]; then\n resolve_image_tags\n return 0\n fi\n # Try to fetch the latest stable release from GitHub\n log \"$(msg install.version.fetching)\"\n local _fetched\n _fetched=$(curl -sf --max-time 5 \\\n -H \"Accept: application/vnd.github+json\" \\\n \"https://api.github.com/repos/agentscope-ai/AgentTeams/releases/latest\" \\\n 2>/dev/null | grep '\"tag_name\"' | head -1 | sed 's/.*\"tag_name\": *\"\\([^\"]*\\)\".*/\\1/')\n if [ -n \"${_fetched}\" ]; then\n AGENTTEAMS_KNOWN_STABLE_VERSION=\"${_fetched}\"\n else\n log \"$(msg install.version.fetch_failed \"${AGENTTEAMS_KNOWN_STABLE_VERSION}\")\"\n fi\n log \"$(msg install.version.title)\"\n echo \"\"\n echo \"$(msg install.version.choose)\"\n echo \"$(msg install.version.option_latest)\"\n printf \"%s\\n\" \"$(msg install.version.option_stable \"${AGENTTEAMS_KNOWN_STABLE_VERSION}\")\"\n echo \"$(msg install.version.option_custom)\"\n echo \"\"\n local VERSION_CHOICE\n read -e -p \"$(msg install.version.prompt) [1]: \" VERSION_CHOICE\n VERSION_CHOICE=\"${VERSION_CHOICE:-1}\"\n if [ \"${VERSION_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n case \"${VERSION_CHOICE}\" in\n 1|latest)\n AGENTTEAMS_VERSION=\"latest\"\n log \"$(msg install.version.selected_latest)\"\n ;;\n 2|stable)\n AGENTTEAMS_VERSION=\"${AGENTTEAMS_KNOWN_STABLE_VERSION}\"\n log \"$(msg install.version.selected_stable \"${AGENTTEAMS_VERSION}\")\"\n ;;\n 3|custom)\n local CUSTOM_VERSION\n read -e -p \"$(msg install.version.custom_prompt): \" CUSTOM_VERSION\n AGENTTEAMS_VERSION=\"${CUSTOM_VERSION:-${AGENTTEAMS_KNOWN_STABLE_VERSION}}\"\n log \"$(msg install.version.selected_custom \"${AGENTTEAMS_VERSION}\")\"\n ;;\n *)\n AGENTTEAMS_VERSION=\"${AGENTTEAMS_KNOWN_STABLE_VERSION}\"\n log \"$(msg install.version.invalid \"${AGENTTEAMS_VERSION}\")\"\n ;;\n esac\n log \"\"\n resolve_image_tags\n}\n\nstep_existing() {\n local existing_env=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n log \"$(msg install.existing.detected \"${existing_env}\")\"\n local running_manager=\"\" running_workers=\"\" existing_workers=\"\"\n if ${DOCKER_CMD} ps --format '{{.Names}}' | grep -q \"^agentteams-manager$\"; then\n running_manager=\"agentteams-manager\"\n fi\n running_workers=$(${DOCKER_CMD} ps --format '{{.Names}}' | grep \"^agentteams-worker-\" || true)\n existing_workers=$(${DOCKER_CMD} ps -a --format '{{.Names}}' | grep \"^agentteams-worker-\" || true)\n local UPGRADE_CHOICE\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n log \"$(msg install.existing.upgrade_noninteractive)\"\n UPGRADE_CHOICE=\"upgrade\"\n else\n echo \"\"\n echo \"$(msg install.existing.choose)\"\n echo \"$(msg install.existing.upgrade)\"\n echo \"$(msg install.existing.reinstall)\"\n echo \"$(msg install.existing.cancel)\"\n echo \"\"\n read -e -p \"$(msg install.existing.prompt): \" UPGRADE_CHOICE\n UPGRADE_CHOICE=\"${UPGRADE_CHOICE:-1}\"\n if [ \"${UPGRADE_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n fi\n case \"${UPGRADE_CHOICE}\" in\n 1|upgrade)\n AGENTTEAMS_UPGRADE=1\n log \"$(msg install.existing.upgrading)\"\n # Show upgrade mode sub-menu (unless already set via env var)\n if [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL:-0}\" != \"1\" ]; then\n echo \"\"\n echo \"$(msg upgrade.mode.prompt)\"\n echo \"$(msg upgrade.mode.keep_all)\"\n echo \"$(msg upgrade.mode.confirm_each)\"\n echo \"$(msg upgrade.mode.back)\"\n echo \"\"\n local UPGRADE_MODE_CHOICE\n read -e -p \"$(msg install.existing.prompt): \" UPGRADE_MODE_CHOICE\n UPGRADE_MODE_CHOICE=\"${UPGRADE_MODE_CHOICE:-1}\"\n case \"${UPGRADE_MODE_CHOICE}\" in\n 1|keep)\n AGENTTEAMS_UPGRADE_KEEP_ALL=1\n ;;\n 2|confirm)\n AGENTTEAMS_UPGRADE_KEEP_ALL=0\n ;;\n 3|b)\n STEP_RESULT=\"back\"\n return 0\n ;;\n *)\n AGENTTEAMS_UPGRADE_KEEP_ALL=0\n ;;\n esac\n fi\n # Load current parameters for both Keep-All and confirm-each modes\n load_current_params_from_env\n if [ -n \"${running_manager}\" ] || [ -n \"${running_workers}\" ]; then\n echo \"\"\n echo -e \"\\033[33m$(msg install.existing.warn_manager_stop)\\033[0m\"\n if [ -n \"${existing_workers}\" ]; then\n echo -e \"\\033[33m$(msg install.existing.warn_worker_recreate)\\033[0m\"\n fi\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" != \"1\" ]; then\n echo \"\"\n local CONFIRM_STOP\n read -e -p \"$(msg install.existing.continue_prompt): \" CONFIRM_STOP\n if [ \"${CONFIRM_STOP}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n if [ \"${CONFIRM_STOP}\" != \"y\" ] && [ \"${CONFIRM_STOP}\" != \"Y\" ]; then\n log \"$(msg install.existing.cancelled)\"\n exit 0\n fi\n fi\n fi\n UPGRADE_EXISTING_WORKERS=\"${existing_workers}\"\n ;;\n 2|reinstall)\n log \"$(msg install.reinstall.performing)\"\n local existing_workspace=\"\"\n if [ -f \"${existing_env}\" ]; then\n existing_workspace=$(grep '^AGENTTEAMS_WORKSPACE_DIR=' \"${existing_env}\" 2>/dev/null | cut -d= -f2-)\n fi\n [ -z \"${existing_workspace}\" ] && existing_workspace=\"${HOME}/agentteams-manager\"\n echo \"\"\n echo -e \"\\033[33m$(msg install.reinstall.warn_stop)\\033[0m\"\n [ -n \"${running_manager}\" ] && echo -e \"\\033[33m - ${running_manager} (manager)\\033[0m\"\n for w in ${running_workers}; do\n echo -e \"\\033[33m - ${w} (worker)\\033[0m\"\n done\n echo \"\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_delete)\\033[0m\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_volume)\\033[0m\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_env \"${existing_env}\")\\033[0m\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_workspace \"${existing_workspace}\")\\033[0m\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_workers)\\033[0m\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_proxy)\\033[0m\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_network)\\033[0m\"\n echo \"\"\n echo -e \"\\033[31m$(msg install.reinstall.confirm_type)\\033[0m\"\n echo -e \"\\033[31m ${existing_workspace}\\033[0m\"\n echo \"\"\n local CONFIRM_PATH\n read -e -p \"$(msg install.reinstall.confirm_path): \" CONFIRM_PATH\n if [ \"${CONFIRM_PATH}\" != \"${existing_workspace}\" ]; then\n die \"$(msg install.reinstall.path_mismatch \"${CONFIRM_PATH}\" \"${existing_workspace}\")\"\n fi\n log \"$(msg install.reinstall.confirmed)\"\n ${DOCKER_CMD} stop agentteams-manager 2>/dev/null || true\n ${DOCKER_CMD} rm agentteams-manager 2>/dev/null || true\n for w in $(${DOCKER_CMD} ps -a --format '{{.Names}}' | grep \"^agentteams-worker-\" || true); do\n ${DOCKER_CMD} stop \"${w}\" 2>/dev/null || true\n ${DOCKER_CMD} rm \"${w}\" 2>/dev/null || true\n log \"$(msg install.reinstall.removed_worker \"${w}\")\"\n done\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' | grep -q \"^agentteams-controller$\"; then\n log \"$(msg install.reinstall.removing_proxy)\"\n ${DOCKER_CMD} stop agentteams-controller 2>/dev/null || true\n ${DOCKER_CMD} rm agentteams-controller 2>/dev/null || true\n fi\n if ${DOCKER_CMD} network ls --format '{{.Name}}' | grep -q \"^agentteams-net$\"; then\n log \"$(msg install.reinstall.removing_network)\"\n ${DOCKER_CMD} network rm agentteams-net 2>/dev/null || true\n fi\n if ${DOCKER_CMD} volume ls -q | grep -q \"^agentteams-data$\"; then\n log \"$(msg install.reinstall.removing_volume)\"\n ${DOCKER_CMD} volume rm agentteams-data 2>/dev/null || log \"$(msg install.reinstall.warn_volume_fail)\"\n fi\n if [ -d \"${existing_workspace}\" ]; then\n log \"$(msg install.reinstall.removing_workspace \"${existing_workspace}\")\"\n rm -rf \"${existing_workspace}\" || die \"$(msg install.reinstall.failed_rm_workspace)\"\n fi\n if [ -f \"${existing_env}\" ]; then\n log \"$(msg install.reinstall.removing_env \"${existing_env}\")\"\n rm -f \"${existing_env}\"\n fi\n log \"$(msg install.reinstall.cleanup_done)\"\n unset AGENTTEAMS_WORKSPACE_DIR\n return 0\n ;;\n 3|cancel|*)\n log \"$(msg install.existing.cancelled)\"\n exit 0\n ;;\n esac\n # Load existing env file as fallback (shell env vars take priority)\n if [ -f \"${existing_env}\" ]; then\n log \"$(msg install.loading_config \"${existing_env}\")\"\n while IFS='=' read -r key value; do\n case \"${key}\" in \\#*|\"\") continue ;; esac\n value=\"${value%%#*}\"\n value=\"${value#\"${value%%[![:space:]]*}\"}\"\n value=\"${value%\"${value##*[![:space:]]}\"}\"\n eval \"_existing_val=\\\"\\${${key}+x}\\\"\"\n if [ -z \"${_existing_val}\" ]; then export \"${key}=${value}\"; fi\n done < \"${existing_env}\"\n fi\n}\n\nstep_llm() {\n log \"$(msg llm.title)\"\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n if [ \"${AGENTTEAMS_LANGUAGE}\" = \"zh\" ]; then\n AGENTTEAMS_LLM_PROVIDER=\"${AGENTTEAMS_LLM_PROVIDER:-openai-compat}\"\n AGENTTEAMS_DEFAULT_MODEL=\"${AGENTTEAMS_DEFAULT_MODEL:-qwen3.6-plus}\"\n AGENTTEAMS_OPENAI_BASE_URL=\"${AGENTTEAMS_OPENAI_BASE_URL:-https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1}\"\n log \"$(msg llm.provider.label \"${AGENTTEAMS_LLM_PROVIDER}\")\"\n log \"$(msg llm.openai.base_url_label \"${AGENTTEAMS_OPENAI_BASE_URL}\")\"\n else\n AGENTTEAMS_LLM_PROVIDER=\"${AGENTTEAMS_LLM_PROVIDER:-qwen}\"\n AGENTTEAMS_DEFAULT_MODEL=\"${AGENTTEAMS_DEFAULT_MODEL:-qwen3.6-plus}\"\n AGENTTEAMS_OPENAI_BASE_URL=\"${AGENTTEAMS_OPENAI_BASE_URL:-}\"\n log \"$(msg llm.provider.qwen_default \"${AGENTTEAMS_LLM_PROVIDER}\")\"\n fi\n log \"$(msg llm.model.default \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n prompt AGENTTEAMS_LLM_API_KEY \"$(msg llm.apikey_prompt)\" \"\" \"true\"\n AGENTTEAMS_EMBEDDING_MODEL=\"${AGENTTEAMS_EMBEDDING_MODEL-text-embedding-v4}\"\n export AGENTTEAMS_LLM_PROVIDER AGENTTEAMS_DEFAULT_MODEL AGENTTEAMS_EMBEDDING_MODEL\n [ -n \"${AGENTTEAMS_OPENAI_BASE_URL+x}\" ] && export AGENTTEAMS_OPENAI_BASE_URL\n return 0\n fi\n\n # Upgrade Keep-All mode: skip provider selection, use loaded values\n local SKIP_TO_EMBEDDING=0\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ] && [ -n \"${AGENTTEAMS_LLM_PROVIDER}\" ]; then\n log \"$(msg llm.provider.label \"${AGENTTEAMS_LLM_PROVIDER}\")\"\n log \"$(msg llm.openai.base_url_label \"${AGENTTEAMS_OPENAI_BASE_URL}\")\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n PROVIDER_CHOICE=\"${AGENTTEAMS_LLM_PROVIDER}\"\n SKIP_TO_EMBEDDING=1\n else\n echo \"\"\n echo \"$(msg llm.providers_title)\"\n echo \"$(msg llm.provider.alibaba)\"\n echo \"$(msg llm.provider.openai_compat)\"\n echo \"\"\n local PROVIDER_CHOICE\n # If upgrade mode with loaded provider, show current as default\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_LLM_PROVIDER}\" ]; then\n local _prov_display\n case \"${AGENTTEAMS_LLM_PROVIDER}\" in\n openai-compat) _prov_display=\"2\" ;;\n *) _prov_display=\"1\" ;;\n esac\n read -e -p \"$(msg llm.provider.select) [${_prov_display}]: \" PROVIDER_CHOICE\n PROVIDER_CHOICE=\"${PROVIDER_CHOICE:-${_prov_display}}\"\n elif [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ]; then\n read -e -p \"$(msg llm.provider.select) [1]: \" PROVIDER_CHOICE\n PROVIDER_CHOICE=\"${PROVIDER_CHOICE:-1}\"\n else\n read -e -p \"$(msg llm.provider.select): \" PROVIDER_CHOICE\n PROVIDER_CHOICE=\"${PROVIDER_CHOICE:-1}\"\n fi\n if [ \"${PROVIDER_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n fi\n local ALIBABA_MODEL_CHOICE=\"\"\n local ALIBABA_ACCESS=\"\"\n case \"${PROVIDER_CHOICE}\" in\n 1|alibaba-cloud|openai-compat)\n if [ \"${AGENTTEAMS_LANGUAGE}\" = \"en\" ]; then\n AGENTTEAMS_LLM_PROVIDER=\"openai-compat\"\n AGENTTEAMS_OPENAI_BASE_URL=\"https://dashscope-intl.aliyuncs.com/compatible-mode/v1\"\n ALIBABA_ACCESS=\"tokenplan\"\n echo \"\"\n echo \"$(msg llm.codingplan.models_title)\"\n echo \"$(msg llm.codingplan.model.qwen36plus)\"\n echo \"$(msg llm.codingplan.model.glm5)\"\n echo \"$(msg llm.codingplan.model.kimi)\"\n echo \"$(msg llm.codingplan.model.minimax)\"\n echo \"\"\n local CODINGPLAN_MODEL_CHOICE\n # If upgrade with loaded model, show current as default\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_DEFAULT_MODEL}\" ]; then\n local _model_default\n case \"${AGENTTEAMS_DEFAULT_MODEL}\" in\n qwen3.6-plus) _model_default=\"1\" ;;\n glm-5) _model_default=\"2\" ;;\n kimi-k2.5) _model_default=\"3\" ;;\n MiniMax-M2.5) _model_default=\"4\" ;;\n *) _model_default=\"1\" ;;\n esac\n read -e -p \"$(msg llm.codingplan.model.select) [${_model_default}]: \" CODINGPLAN_MODEL_CHOICE\n CODINGPLAN_MODEL_CHOICE=\"${CODINGPLAN_MODEL_CHOICE:-${_model_default}}\"\n elif [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ]; then\n read -e -p \"$(msg llm.codingplan.model.select) [1]: \" CODINGPLAN_MODEL_CHOICE\n CODINGPLAN_MODEL_CHOICE=\"${CODINGPLAN_MODEL_CHOICE:-1}\"\n else\n read -e -p \"$(msg llm.codingplan.model.select): \" CODINGPLAN_MODEL_CHOICE\n CODINGPLAN_MODEL_CHOICE=\"${CODINGPLAN_MODEL_CHOICE:-1}\"\n fi\n if [ \"${CODINGPLAN_MODEL_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n case \"${CODINGPLAN_MODEL_CHOICE}\" in\n 1|qwen3.6-plus) AGENTTEAMS_DEFAULT_MODEL=\"qwen3.6-plus\" ;;\n 2|glm-5) AGENTTEAMS_DEFAULT_MODEL=\"glm-5\" ;;\n 3|kimi-k2.5) AGENTTEAMS_DEFAULT_MODEL=\"kimi-k2.5\" ;;\n 4|MiniMax-M2.5) AGENTTEAMS_DEFAULT_MODEL=\"MiniMax-M2.5\" ;;\n *) AGENTTEAMS_DEFAULT_MODEL=\"qwen3.6-plus\" ;;\n esac\n log \"$(msg llm.provider.selected_codingplan)\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n else\n echo \"\"\n echo \"$(msg llm.alibaba.models_title)\"\n echo \"$(msg llm.alibaba.model.tokenplan)\"\n echo \"$(msg llm.alibaba.model.bailian)\"\n echo \"$(msg llm.alibaba.model.codingplan_legacy)\"\n echo \"\"\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_OPENAI_BASE_URL}\" ]; then\n # Determine alibaba access type from loaded URL\n local _access_default=\"1\"\n if echo \"${AGENTTEAMS_OPENAI_BASE_URL}\" | grep -q \"token-plan\"; then\n _access_default=\"1\"\n elif echo \"${AGENTTEAMS_OPENAI_BASE_URL}\" | grep -q \"dashscope\"; then\n if echo \"${AGENTTEAMS_OPENAI_BASE_URL}\" | grep -q \"intl\"; then\n _access_default=\"3\"\n else\n _access_default=\"2\"\n fi\n fi\n read -e -p \"$(msg llm.alibaba.model.select) [${_access_default}]: \" ALIBABA_MODEL_CHOICE\n ALIBABA_MODEL_CHOICE=\"${ALIBABA_MODEL_CHOICE:-${_access_default}}\"\n elif [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ]; then\n read -e -p \"$(msg llm.alibaba.model.select) [1]: \" ALIBABA_MODEL_CHOICE\n ALIBABA_MODEL_CHOICE=\"${ALIBABA_MODEL_CHOICE:-1}\"\n else\n read -e -p \"$(msg llm.alibaba.model.select): \" ALIBABA_MODEL_CHOICE\n ALIBABA_MODEL_CHOICE=\"${ALIBABA_MODEL_CHOICE:-1}\"\n fi\n if [ \"${ALIBABA_MODEL_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n case \"${ALIBABA_MODEL_CHOICE}\" in\n 1|token-plan|tokenplan)\n ALIBABA_ACCESS=\"tokenplan\"\n AGENTTEAMS_LLM_PROVIDER=\"openai-compat\"\n AGENTTEAMS_OPENAI_BASE_URL=\"https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1\"\n echo \"\"\n echo \"$(msg llm.codingplan.models_title)\"\n echo \"$(msg llm.codingplan.model.qwen36plus)\"\n echo \"$(msg llm.codingplan.model.glm5)\"\n echo \"$(msg llm.codingplan.model.kimi)\"\n echo \"$(msg llm.codingplan.model.minimax)\"\n echo \"\"\n local CODINGPLAN_MODEL_CHOICE\n # If upgrade with loaded model, show current as default\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_DEFAULT_MODEL}\" ]; then\n local _model_default\n case \"${AGENTTEAMS_DEFAULT_MODEL}\" in\n qwen3.6-plus) _model_default=\"1\" ;;\n glm-5) _model_default=\"2\" ;;\n kimi-k2.5) _model_default=\"3\" ;;\n MiniMax-M2.5) _model_default=\"4\" ;;\n *) _model_default=\"1\" ;;\n esac\n read -e -p \"$(msg llm.codingplan.model.select) [${_model_default}]: \" CODINGPLAN_MODEL_CHOICE\n CODINGPLAN_MODEL_CHOICE=\"${CODINGPLAN_MODEL_CHOICE:-${_model_default}}\"\n elif [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ]; then\n read -e -p \"$(msg llm.codingplan.model.select) [1]: \" CODINGPLAN_MODEL_CHOICE\n CODINGPLAN_MODEL_CHOICE=\"${CODINGPLAN_MODEL_CHOICE:-1}\"\n else\n read -e -p \"$(msg llm.codingplan.model.select): \" CODINGPLAN_MODEL_CHOICE\n CODINGPLAN_MODEL_CHOICE=\"${CODINGPLAN_MODEL_CHOICE:-1}\"\n fi\n if [ \"${CODINGPLAN_MODEL_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n case \"${CODINGPLAN_MODEL_CHOICE}\" in\n 1|qwen3.6-plus) AGENTTEAMS_DEFAULT_MODEL=\"qwen3.6-plus\" ;;\n 2|glm-5) AGENTTEAMS_DEFAULT_MODEL=\"glm-5\" ;;\n 3|kimi-k2.5) AGENTTEAMS_DEFAULT_MODEL=\"kimi-k2.5\" ;;\n 4|MiniMax-M2.5) AGENTTEAMS_DEFAULT_MODEL=\"MiniMax-M2.5\" ;;\n *) AGENTTEAMS_DEFAULT_MODEL=\"qwen3.6-plus\" ;;\n esac\n log \"$(msg llm.provider.selected_tokenplan)\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n ;;\n 2|qwen|bailian)\n ALIBABA_ACCESS=\"bailian\"\n AGENTTEAMS_LLM_PROVIDER=\"qwen\"\n AGENTTEAMS_OPENAI_BASE_URL=\"\"\n echo \"\"\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_DEFAULT_MODEL}\" ]; then\n read -e -p \"$(msg llm.qwen.model_prompt) [${AGENTTEAMS_DEFAULT_MODEL}]: \" AGENTTEAMS_DEFAULT_MODEL\n else\n read -e -p \"$(msg llm.qwen.model_prompt) [qwen3.6-plus]: \" AGENTTEAMS_DEFAULT_MODEL\n fi\n if [ \"${AGENTTEAMS_DEFAULT_MODEL}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_DEFAULT_MODEL=\"${AGENTTEAMS_DEFAULT_MODEL:-qwen3.6-plus}\"\n log \"$(msg llm.provider.selected_qwen)\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n prompt_custom_model_params \"${AGENTTEAMS_DEFAULT_MODEL}\" || return 0\n ;;\n 3|coding-plan|codingplan)\n ALIBABA_ACCESS=\"codingplan_legacy\"\n AGENTTEAMS_LLM_PROVIDER=\"openai-compat\"\n AGENTTEAMS_OPENAI_BASE_URL=\"https://coding.dashscope.aliyuncs.com/v1\"\n echo \"\"\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_DEFAULT_MODEL}\" ]; then\n read -e -p \"$(msg llm.qwen.model_prompt) [${AGENTTEAMS_DEFAULT_MODEL}]: \" AGENTTEAMS_DEFAULT_MODEL\n else\n read -e -p \"$(msg llm.qwen.model_prompt): \" AGENTTEAMS_DEFAULT_MODEL\n fi\n if [ \"${AGENTTEAMS_DEFAULT_MODEL}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_DEFAULT_MODEL=\"${AGENTTEAMS_DEFAULT_MODEL:-qwen3.6-plus}\"\n log \"$(msg llm.provider.selected_codingplan_legacy)\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n prompt_custom_model_params \"${AGENTTEAMS_DEFAULT_MODEL}\" || return 0\n ;;\n *)\n die \"$(msg llm.alibaba.model.invalid \"${ALIBABA_MODEL_CHOICE}\")\"\n ;;\n esac\n fi\n if [ \"${ALIBABA_ACCESS}\" = \"bailian\" ]; then\n log \"$(msg llm.apikey_hint_bailian)\"\n log \"$(msg llm.apikey_url_bailian)\"\n elif [ \"${AGENTTEAMS_LANGUAGE}\" = \"en\" ]; then\n log \"$(msg llm.apikey_hint_qwencloud)\"\n log \"$(msg llm.apikey_url_qwencloud)\"\n elif [ \"${ALIBABA_ACCESS}\" = \"codingplan_legacy\" ]; then\n log \"$(msg llm.apikey_hint_codingplan)\"\n log \"$(msg llm.apikey_url_codingplan)\"\n else\n log \"$(msg llm.apikey_hint_tokenplan)\"\n log \"$(msg llm.apikey_url_tokenplan)\"\n fi\n log \"\"\n prompt AGENTTEAMS_LLM_API_KEY \"$(msg llm.apikey_prompt)\" \"\" \"true\" || return 0\n if [ \"${ALIBABA_ACCESS}\" = \"bailian\" ]; then\n test_llm_connectivity \"https://dashscope.aliyuncs.com/compatible-mode/v1\" \"${AGENTTEAMS_LLM_API_KEY}\" \"${AGENTTEAMS_DEFAULT_MODEL}\" || return 0\n elif [ \"${ALIBABA_ACCESS}\" = \"codingplan_legacy\" ]; then\n test_llm_connectivity \"https://coding.dashscope.aliyuncs.com/v1\" \"${AGENTTEAMS_LLM_API_KEY}\" \"${AGENTTEAMS_DEFAULT_MODEL}\" \"$(msg llm.openai.test.fail.codingplan_legacy)\" || return 0\n elif [ \"${AGENTTEAMS_LANGUAGE}\" = \"en\" ]; then\n test_llm_connectivity \"${AGENTTEAMS_OPENAI_BASE_URL}\" \"${AGENTTEAMS_LLM_API_KEY}\" \"${AGENTTEAMS_DEFAULT_MODEL}\" \"$(msg llm.openai.test.fail.codingplan)\" || return 0\n else\n test_llm_connectivity \"${AGENTTEAMS_OPENAI_BASE_URL}\" \"${AGENTTEAMS_LLM_API_KEY}\" \"${AGENTTEAMS_DEFAULT_MODEL}\" \"$(msg llm.openai.test.fail.tokenplan)\" || return 0\n fi\n ;;\n 2|openai-compat)\n AGENTTEAMS_LLM_PROVIDER=\"openai-compat\"\n log \"$(msg llm.provider.selected_openai \"${AGENTTEAMS_LLM_PROVIDER}\")\"\n echo \"\"\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg llm.openai.base_url_prompt)\" \"${AGENTTEAMS_OPENAI_BASE_URL}\")\"\n else\n _current=\"${AGENTTEAMS_OPENAI_BASE_URL}\"\n if [ -n \"${_current}\" ]; then\n read -e -p \"$(msg llm.openai.base_url_prompt) [${_current}]: \" AGENTTEAMS_OPENAI_BASE_URL\n else\n read -e -p \"$(msg llm.openai.base_url_prompt) [https://api.openai.com/v1]: \" AGENTTEAMS_OPENAI_BASE_URL\n fi\n if [ \"${AGENTTEAMS_OPENAI_BASE_URL}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_OPENAI_BASE_URL=\"${AGENTTEAMS_OPENAI_BASE_URL:-${_current}}\"\n [ -z \"${AGENTTEAMS_OPENAI_BASE_URL}\" ] && AGENTTEAMS_OPENAI_BASE_URL=\"https://api.openai.com/v1\"\n fi\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg llm.openai.model_prompt)\" \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n else\n _current=\"${AGENTTEAMS_DEFAULT_MODEL}\"\n if [ -n \"${_current}\" ]; then\n read -e -p \"$(msg llm.openai.model_prompt) [${_current}]: \" AGENTTEAMS_DEFAULT_MODEL\n else\n read -e -p \"$(msg llm.openai.model_prompt) [gpt-5.4]: \" AGENTTEAMS_DEFAULT_MODEL\n fi\n if [ \"${AGENTTEAMS_DEFAULT_MODEL}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_DEFAULT_MODEL=\"${AGENTTEAMS_DEFAULT_MODEL:-${_current}}\"\n [ -z \"${AGENTTEAMS_DEFAULT_MODEL}\" ] && AGENTTEAMS_DEFAULT_MODEL=\"gpt-5.4\"\n fi\n log \"$(msg llm.openai.base_url_label \"${AGENTTEAMS_OPENAI_BASE_URL}\")\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n prompt_custom_model_params \"${AGENTTEAMS_DEFAULT_MODEL}\" || return 0\n log \"\"\n prompt AGENTTEAMS_LLM_API_KEY \"$(msg llm.apikey_prompt)\" \"\" \"true\" || return 0\n test_llm_connectivity \"${AGENTTEAMS_OPENAI_BASE_URL}\" \"${AGENTTEAMS_LLM_API_KEY}\" \"${AGENTTEAMS_DEFAULT_MODEL}\" || return 0\n ;;\n *)\n die \"$(msg llm.provider.invalid \"${PROVIDER_CHOICE}\")\"\n ;;\n esac\n\n # Skip to embedding if Keep-All mode handled LLM params\n if [ \"${SKIP_TO_EMBEDDING}\" = \"1\" ]; then\n if [ -n \"${AGENTTEAMS_EMBEDDING_MODEL}\" ]; then\n log \"$(msg llm.embedding.title)\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_EMBEDDING_MODEL}\")\"\n log \"\"\n fi\n return 0\n fi\n\n # --- Embedding model (optional, auto-tested) ---\n echo \"\"\n log \"$(msg llm.embedding.title)\"\n log \"$(msg llm.embedding.hint)\"\n echo \"\"\n echo \"$(msg llm.embedding.option.default)\"\n echo \"$(msg llm.embedding.option.custom)\"\n echo \"$(msg llm.embedding.option.disable)\"\n echo \"\"\n local EMB_CHOICE\n read -e -p \"$(msg llm.embedding.select) [1]: \" EMB_CHOICE\n EMB_CHOICE=\"${EMB_CHOICE:-1}\"\n if [ \"${EMB_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n\n case \"${EMB_CHOICE}\" in\n 1)\n AGENTTEAMS_EMBEDDING_MODEL=\"text-embedding-v4\"\n ;;\n 2)\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg llm.embedding.custom_prompt)\" \"${AGENTTEAMS_EMBEDDING_MODEL}\")\"\n else\n _current=\"${AGENTTEAMS_EMBEDDING_MODEL}\"\n if [ -n \"${_current}\" ]; then\n read -e -p \"$(msg llm.embedding.custom_prompt) [${_current}]: \" AGENTTEAMS_EMBEDDING_MODEL\n else\n read -e -p \"$(msg llm.embedding.custom_prompt): \" AGENTTEAMS_EMBEDDING_MODEL\n fi\n if [ \"${AGENTTEAMS_EMBEDDING_MODEL}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_EMBEDDING_MODEL=\"${AGENTTEAMS_EMBEDDING_MODEL:-${_current}}\"\n fi\n ;;\n 3)\n AGENTTEAMS_EMBEDDING_MODEL=\"\"\n log \"$(msg llm.embedding.disabled)\"\n ;;\n *)\n AGENTTEAMS_EMBEDDING_MODEL=\"text-embedding-v4\"\n ;;\n esac\n\n if [ -n \"${AGENTTEAMS_EMBEDDING_MODEL}\" ]; then\n # Qwen provider uses dashscope directly; others use OPENAI_BASE_URL\n local EMB_BASE_URL=\"${AGENTTEAMS_OPENAI_BASE_URL}\"\n if [ \"${AGENTTEAMS_LLM_PROVIDER}\" = \"qwen\" ]; then\n EMB_BASE_URL=\"https://dashscope.aliyuncs.com/compatible-mode/v1\"\n fi\n if ! test_embedding_connectivity \"${EMB_BASE_URL}\" \"${AGENTTEAMS_LLM_API_KEY}\" \"${AGENTTEAMS_EMBEDDING_MODEL}\"; then\n AGENTTEAMS_EMBEDDING_MODEL=\"\"\n log \"$(msg llm.embedding.auto_disabled)\"\n fi\n fi\n\n export AGENTTEAMS_LLM_PROVIDER AGENTTEAMS_DEFAULT_MODEL\n [ -n \"${AGENTTEAMS_OPENAI_BASE_URL+x}\" ] && export AGENTTEAMS_OPENAI_BASE_URL\n log \"\"\n}\n\nstep_admin() {\n log \"$(msg admin.title)\"\n prompt AGENTTEAMS_ADMIN_USER \"$(msg admin.username_prompt)\" \"admin\" || return 0\n AGENTTEAMS_ADMIN_USER=\"$(printf '%s' \"${AGENTTEAMS_ADMIN_USER}\" | tr '[:upper:]' '[:lower:]')\"\n\n # Pre-set via env var: validate; in non-interactive mode fail fast,\n # in interactive mode warn and fall through to the retry prompt.\n if [ -n \"${AGENTTEAMS_ADMIN_PASSWORD:-}\" ]; then\n log \" $(msg prompt.preset \"$(msg admin.password_prompt)\")\"\n if [ ${#AGENTTEAMS_ADMIN_PASSWORD} -ge 8 ]; then\n log \"\"\n return 0\n fi\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n die \"$(msg admin.password_too_short \"${#AGENTTEAMS_ADMIN_PASSWORD}\")\"\n fi\n error \"$(msg admin.password_too_short \"${#AGENTTEAMS_ADMIN_PASSWORD}\")\"\n unset AGENTTEAMS_ADMIN_PASSWORD\n fi\n\n while true; do\n unset AGENTTEAMS_ADMIN_PASSWORD\n prompt_optional AGENTTEAMS_ADMIN_PASSWORD \"$(msg admin.password_prompt)\" \"true\" || return 0\n if [ -z \"${AGENTTEAMS_ADMIN_PASSWORD}\" ]; then\n AGENTTEAMS_ADMIN_PASSWORD=\"admin$(openssl rand -hex 6)\"\n log \"$(msg admin.password_generated)\"\n break\n fi\n if [ ${#AGENTTEAMS_ADMIN_PASSWORD} -ge 8 ]; then\n break\n fi\n error \"$(msg admin.password_too_short \"${#AGENTTEAMS_ADMIN_PASSWORD}\")\"\n done\n log \"\"\n}\n\nstep_network() {\n log \"$(msg port.local_only.title)\"\n echo \"\"\n echo \" 1) $(msg port.local_only.hint_yes)\"\n echo \" 2) $(msg port.local_only.hint_no)\"\n echo \"\"\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_LOCAL_ONLY=\"${AGENTTEAMS_LOCAL_ONLY:-1}\"\n elif [ -z \"${AGENTTEAMS_LOCAL_ONLY+x}\" ]; then\n local _local_choice\n read -e -p \"$(msg port.local_only.choice): \" _local_choice\n if [ \"${_local_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _local_choice=\"${_local_choice:-1}\"\n case \"${_local_choice}\" in\n 2|n|N|no|NO) AGENTTEAMS_LOCAL_ONLY=\"0\" ;;\n *) AGENTTEAMS_LOCAL_ONLY=\"1\" ;;\n esac\n fi\n export AGENTTEAMS_LOCAL_ONLY\n if [ \"${AGENTTEAMS_LOCAL_ONLY}\" = \"1\" ]; then\n log \"$(msg port.local_only.selected_local)\"\n else\n log \"$(msg port.local_only.selected_external)\"\n echo \"\"\n echo -e \"\\033[33m$(msg port.local_only.https_hint)\\033[0m\"\n fi\n}\n\nstep_ports() {\n log \"$(msg port.title)\"\n prompt AGENTTEAMS_PORT_GATEWAY \"$(msg port.gateway_prompt)\" \"18080\" || return 0\n prompt AGENTTEAMS_PORT_CONSOLE \"$(msg port.console_prompt)\" \"18001\" || return 0\n prompt AGENTTEAMS_PORT_ELEMENT_WEB \"$(msg port.element_prompt)\" \"18088\" || return 0\n prompt AGENTTEAMS_PORT_MANAGER_CONSOLE \"$(msg port.manager_console_prompt)\" \"18888\" || return 0\n log \"\"\n}\n\nstep_domains() {\n log \"$(msg domain.title)\"\n log \"$(msg domain.hint)\"\n prompt AGENTTEAMS_MATRIX_DOMAIN \"$(msg domain.matrix_prompt)\" \"matrix-local.agentteams.io:${AGENTTEAMS_PORT_GATEWAY}\" || return 0\n prompt AGENTTEAMS_MATRIX_CLIENT_DOMAIN \"$(msg domain.element_prompt)\" \"matrix-client-local.agentteams.io\" || return 0\n prompt AGENTTEAMS_AI_GATEWAY_DOMAIN \"$(msg domain.gateway_prompt)\" \"aigw-local.agentteams.io\" || return 0\n prompt AGENTTEAMS_FS_DOMAIN \"$(msg domain.fs_prompt)\" \"fs-local.agentteams.io\" || return 0\n if [ \"${AGENTTEAMS_MANAGER_RUNTIME}\" != \"copaw\" ]; then\n prompt AGENTTEAMS_CONSOLE_DOMAIN \"$(msg domain.console_prompt)\" \"console-local.agentteams.io\" || return 0\n fi\n log \"\"\n}\n\nstep_github() {\n log \"$(msg github.title)\"\n prompt_optional AGENTTEAMS_GITHUB_TOKEN \"$(msg github.token_prompt)\" \"true\" || return 0\n}\n\nstep_skills() {\n log \"\"\n log \"$(msg skills.title)\"\n prompt_optional AGENTTEAMS_SKILLS_API_URL \"$(msg skills.url_prompt)\" || return 0\n log \"\"\n}\n\nstep_volume() {\n log \"$(msg data.title)\"\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_DATA_DIR=\"${AGENTTEAMS_DATA_DIR:-agentteams-data}\"\n log \" $(msg data.volume_using \"${AGENTTEAMS_DATA_DIR}\") (non-interactive, skipped)\"\n export AGENTTEAMS_DATA_DIR\n return 0\n fi\n # ─────────────────────────────────────────────────────────────────\n if [ -z \"${AGENTTEAMS_DATA_DIR+x}\" ]; then\n local _input\n read -e -p \"$(msg data.volume_prompt): \" _input\n if [ \"${_input}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_DATA_DIR=\"${_input:-agentteams-data}\"\n export AGENTTEAMS_DATA_DIR\n fi\n AGENTTEAMS_DATA_DIR=\"${AGENTTEAMS_DATA_DIR:-agentteams-data}\"\n log \"$(msg data.volume_using \"${AGENTTEAMS_DATA_DIR}\")\"\n}\n\nstep_workspace() {\n log \"$(msg workspace.title)\"\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_WORKSPACE_DIR=\"${AGENTTEAMS_WORKSPACE_DIR:-${HOME}/agentteams-manager}\"\n AGENTTEAMS_WORKSPACE_DIR=\"$(cd \"${AGENTTEAMS_WORKSPACE_DIR}\" 2>/dev/null && pwd || echo \"${AGENTTEAMS_WORKSPACE_DIR}\")\"\n mkdir -p \"${AGENTTEAMS_WORKSPACE_DIR}\"\n log \" $(msg workspace.dir_label \"${AGENTTEAMS_WORKSPACE_DIR}\") (non-interactive, skipped)\"\n export AGENTTEAMS_WORKSPACE_DIR\n return 0\n fi\n # ─────────────────────────────────────────────────────────────────\n local _input\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg workspace.dir_prompt \"${HOME}/agentteams-manager\")\" \"${AGENTTEAMS_WORKSPACE_DIR}\")\"\n _input=\"${AGENTTEAMS_WORKSPACE_DIR}\"\n else\n local _current=\"${AGENTTEAMS_WORKSPACE_DIR}\"\n if [ -n \"${_current}\" ]; then\n read -e -p \"$(msg workspace.dir_prompt \"${HOME}/agentteams-manager\") [${_current}]: \" _input\n else\n read -e -p \"$(msg workspace.dir_prompt \"${HOME}/agentteams-manager\"): \" _input\n fi\n if [ \"${_input}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _input=\"${_input:-${_current}}\"\n fi\n AGENTTEAMS_WORKSPACE_DIR=\"${_input:-${HOME}/agentteams-manager}\"\n export AGENTTEAMS_WORKSPACE_DIR\n AGENTTEAMS_WORKSPACE_DIR=\"$(cd \"${AGENTTEAMS_WORKSPACE_DIR}\" 2>/dev/null && pwd || echo \"${AGENTTEAMS_WORKSPACE_DIR}\")\"\n mkdir -p \"${AGENTTEAMS_WORKSPACE_DIR}\"\n log \"$(msg workspace.dir_label \"${AGENTTEAMS_WORKSPACE_DIR}\")\"\n}\n\nstep_dashboard() {\n AGENTTEAMS_DASHBOARD=\"${AGENTTEAMS_DASHBOARD:-1}\"\n AGENTTEAMS_DASHBOARD_VERSION=\"${AGENTTEAMS_DASHBOARD_VERSION:-v1.2.0}\"\n AGENTTEAMS_PORT_DASHBOARD=\"${AGENTTEAMS_PORT_DASHBOARD:-13000}\"\n AGENTTEAMS_AI_GATEWAY_ADMIN_URL=\"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL:-}\"\n\n # Compute the default image for the current version.\n # We track whether the current image matches the derived default so that\n # changing the version also updates the image (unless the user has\n # explicitly overridden it to a non-default value).\n _dashboard_default_image() {\n echo \"${AGENTTEAMS_REGISTRY}/agentteams/agentteams-dashboard:${1:-${AGENTTEAMS_DASHBOARD_VERSION}}\"\n }\n local _init_default\n _init_default=$(_dashboard_default_image)\n if [ -z \"${AGENTTEAMS_DASHBOARD_IMAGE:-}\" ]; then\n AGENTTEAMS_DASHBOARD_IMAGE=\"${_init_default}\"\n fi\n\n # Recompute default image when the version changes AND the current image\n # matches the default for the *previous* (env-file) version.\n # This handles non-interactive upgrades correctly:\n # - env file loads old default image + old version\n # - CLI passes new version\n # - image should follow (because it was auto-derived before)\n # But preserves truly custom images (same repo but custom tag like :canary)\n # because they won't match the old-version default exactly.\n local _old_saved_version=\"\"\n local _env_file=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n if [ -f \"${_env_file}\" ]; then\n _old_saved_version=\"$(grep '^AGENTTEAMS_DASHBOARD_VERSION=' \"${_env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n fi\n if [ -n \"${_old_saved_version}\" ] && \\\n [ \"${_old_saved_version}\" != \"${AGENTTEAMS_DASHBOARD_VERSION}\" ] && \\\n [ \"${AGENTTEAMS_DASHBOARD_IMAGE}\" = \"$(_dashboard_default_image \"${_old_saved_version}\")\" ]; then\n AGENTTEAMS_DASHBOARD_IMAGE=$(_dashboard_default_image)\n fi\n\n # Quick Start: accept all defaults without prompting. The dashboard is\n # installed by default (AGENTTEAMS_DASHBOARD=1); set AGENTTEAMS_DASHBOARD=0\n # to opt out.\n if [ \"${AGENTTEAMS_QUICKSTART:-0}\" = \"1\" ]; then\n if [ \"${AGENTTEAMS_DASHBOARD}\" = \"1\" ]; then\n log \"$(msg dash.summary \"${AGENTTEAMS_PORT_DASHBOARD}\" \"${AGENTTEAMS_DASHBOARD_IMAGE}\") (quick start)\"\n else\n log \"$(msg dash.skip) (quick start)\"\n fi\n export AGENTTEAMS_DASHBOARD AGENTTEAMS_DASHBOARD_VERSION AGENTTEAMS_PORT_DASHBOARD AGENTTEAMS_DASHBOARD_IMAGE AGENTTEAMS_AI_GATEWAY_ADMIN_URL\n return 0\n fi\n\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_DASHBOARD=\"${AGENTTEAMS_DASHBOARD:-1}\"\n if [ \"${AGENTTEAMS_DASHBOARD}\" = \"1\" ]; then\n log \"$(msg dash.summary \"${AGENTTEAMS_PORT_DASHBOARD}\" \"${AGENTTEAMS_DASHBOARD_IMAGE}\") (non-interactive)\"\n else\n log \"$(msg dash.skip) (non-interactive)\"\n fi\n export AGENTTEAMS_DASHBOARD AGENTTEAMS_DASHBOARD_VERSION AGENTTEAMS_PORT_DASHBOARD AGENTTEAMS_DASHBOARD_IMAGE AGENTTEAMS_AI_GATEWAY_ADMIN_URL\n return 0\n fi\n\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ]; then\n log \"$(msg prompt.upgrade_keep \"\" \"${AGENTTEAMS_DASHBOARD}\") dashboard=${AGENTTEAMS_DASHBOARD} version=${AGENTTEAMS_DASHBOARD_VERSION} port=${AGENTTEAMS_PORT_DASHBOARD}\"\n export AGENTTEAMS_DASHBOARD AGENTTEAMS_DASHBOARD_VERSION AGENTTEAMS_PORT_DASHBOARD AGENTTEAMS_DASHBOARD_IMAGE AGENTTEAMS_AI_GATEWAY_ADMIN_URL\n return 0\n fi\n\n local _input\n echo \"\"\n read -p \"$(msg dash.prompt) [Y/n]: \" _input\n case \"${_input:-}\" in\n [nN0]) AGENTTEAMS_DASHBOARD=0 ;;\n *) AGENTTEAMS_DASHBOARD=1 ;;\n esac\n if [ \"${_input}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n\n if [ \"${AGENTTEAMS_DASHBOARD}\" != \"1\" ]; then\n log \"$(msg dash.skip)\"\n export AGENTTEAMS_DASHBOARD AGENTTEAMS_DASHBOARD_VERSION AGENTTEAMS_PORT_DASHBOARD AGENTTEAMS_DASHBOARD_IMAGE AGENTTEAMS_AI_GATEWAY_ADMIN_URL\n return 0\n fi\n\n # Dashboard version\n local _current_version=\"${AGENTTEAMS_DASHBOARD_VERSION}\"\n local _old_version=\"${AGENTTEAMS_DASHBOARD_VERSION}\"\n local _old_default_image\n _old_default_image=$(_dashboard_default_image \"${_old_version}\")\n read -p \"$(msg dash.version_prompt) [${_current_version}]: \" _input\n AGENTTEAMS_DASHBOARD_VERSION=\"${_input:-${_current_version}}\"\n\n # Dashboard port\n local _current_port=\"${AGENTTEAMS_PORT_DASHBOARD}\"\n read -p \"$(msg dash.port_prompt) [${_current_port}]: \" _input\n AGENTTEAMS_PORT_DASHBOARD=\"${_input:-${_current_port}}\"\n\n # Dashboard image: if the current image matches the default derived from\n # the OLD version, recompute it from the NEW version. This handles both\n # fresh installs and upgrades where the previous image was the default.\n # Only a real user override (image != default for old version) is preserved.\n if [ \"${AGENTTEAMS_DASHBOARD_VERSION}\" != \"${_old_version}\" ] && \\\n [ \"${AGENTTEAMS_DASHBOARD_IMAGE}\" = \"${_old_default_image}\" ]; then\n AGENTTEAMS_DASHBOARD_IMAGE=$(_dashboard_default_image)\n fi\n local _current_image=\"${AGENTTEAMS_DASHBOARD_IMAGE}\"\n read -p \"$(msg dash.image_prompt) [${_current_image}]: \" _input\n if [ -n \"${_input}\" ]; then\n AGENTTEAMS_DASHBOARD_IMAGE=\"${_input}\"\n fi\n\n # Higress Console URL (explicit config takes priority; auto-detect as fallback)\n local _higress_url=\"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\"\n if [ -z \"${_higress_url}\" ]; then\n if ${DOCKER_CMD} ps --format '{{.Names}}' | grep -q \"^agentteams-controller$\"; then\n if ${DOCKER_CMD} exec agentteams-controller curl -sf --max-time 2 http://127.0.0.1:8001/ >/dev/null 2>&1; then\n _higress_url=\"http://agentteams-controller:8001\"\n log \"$(msg dash.auto_url): ${_higress_url}\"\n else\n log \"$(msg dash.auto_url_fail)\"\n fi\n fi\n fi\n read -p \"$(msg dash.gateway_prompt) [${_higress_url:-<auto>}]: \" _input\n AGENTTEAMS_AI_GATEWAY_ADMIN_URL=\"${_input:-${_higress_url}}\"\n\n # Normalize URL: add http:// prefix if missing\n if [ -n \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\" ]; then\n case \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\" in\n http://*|https://*) ;;\n *) AGENTTEAMS_AI_GATEWAY_ADMIN_URL=\"http://${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\" ;;\n esac\n fi\n\n # Verify the Higress Console URL is reachable (best-effort warning only)\n if [ -n \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\" ]; then\n local _gw_reachable=0\n # If the URL points to the controller container, test from inside the container\n if echo \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\" | grep -qE \"(agentteams-controller|hiclaw-controller|127\\.0\\.0\\.1|localhost)\"; then\n if ${DOCKER_CMD} ps --format '{{.Names}}' | grep -q \"^agentteams-controller$\"; then\n if ${DOCKER_CMD} exec agentteams-controller curl -sf --max-time 3 \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}/\" >/dev/null 2>&1; then\n _gw_reachable=1\n fi\n fi\n else\n # External URL — probe from inside the controller (same network as\n # the Dashboard container) when possible; fall back to the host.\n if ${DOCKER_CMD} ps --format '{{.Names}}' | grep -q \"^agentteams-controller$\"; then\n if ${DOCKER_CMD} exec agentteams-controller curl -sf --max-time 3 \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}/\" >/dev/null 2>&1; then\n _gw_reachable=1\n fi\n elif curl -sf --max-time 3 \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}/\" >/dev/null 2>&1; then\n _gw_reachable=1\n fi\n fi\n if [ \"${_gw_reachable}\" = \"0\" ]; then\n log \"WARNING: Higress Console URL may not be reachable: ${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\"\n log \" Dashboard will still work, but shared login via Higress Console may fail.\"\n log \" To fix: verify the URL is correct and the Higress Console service is running.\"\n fi\n fi\n\n log \"$(msg dash.summary \"${AGENTTEAMS_PORT_DASHBOARD}\" \"${AGENTTEAMS_DASHBOARD_IMAGE}\")\"\n export AGENTTEAMS_DASHBOARD AGENTTEAMS_DASHBOARD_VERSION AGENTTEAMS_PORT_DASHBOARD AGENTTEAMS_DASHBOARD_IMAGE AGENTTEAMS_AI_GATEWAY_ADMIN_URL\n}\n\nstep_runtime() {\n log \"$(msg worker_runtime.title)\"\n echo \"\"\n echo \" 1) $(msg worker_runtime.copaw)\"\n echo \" 2) $(msg worker_runtime.openclaw)\"\n if ! _ver_lt \"${AGENTTEAMS_VERSION}\" \"v1.1.0\"; then\n echo \" 3) $(msg worker_runtime.hermes)\"\n fi\n echo \"\"\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"${AGENTTEAMS_DEFAULT_WORKER_RUNTIME:-copaw}\"\n elif [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_DEFAULT_WORKER_RUNTIME}\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg worker_runtime.title_short)\" \"${AGENTTEAMS_DEFAULT_WORKER_RUNTIME}\")\"\n local _runtime_choice\n read -e -p \"$(msg worker_runtime.choice): \" _runtime_choice\n if [ \"${_runtime_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n if [ -n \"${_runtime_choice}\" ]; then\n case \"${_runtime_choice}\" in\n 2) AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"openclaw\" ;;\n 3) if ! _ver_lt \"${AGENTTEAMS_VERSION}\" \"v1.1.0\"; then\n AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"hermes\"\n else\n AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"copaw\"\n fi ;;\n *) AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"copaw\" ;;\n esac\n fi\n elif [ -z \"${AGENTTEAMS_DEFAULT_WORKER_RUNTIME+x}\" ]; then\n local _runtime_choice\n read -e -p \"$(msg worker_runtime.choice): \" _runtime_choice\n if [ \"${_runtime_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _runtime_choice=\"${_runtime_choice:-1}\"\n case \"${_runtime_choice}\" in\n 2) AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"openclaw\" ;;\n 3) if ! _ver_lt \"${AGENTTEAMS_VERSION}\" \"v1.1.0\"; then\n AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"hermes\"\n else\n AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"copaw\"\n fi ;;\n *) AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"copaw\" ;;\n esac\n fi\n export AGENTTEAMS_DEFAULT_WORKER_RUNTIME\n log \"$(msg worker_runtime.selected \"${AGENTTEAMS_DEFAULT_WORKER_RUNTIME}\")\"\n}\n\nstep_manager_runtime() {\n log \"$(msg manager_runtime.title)\"\n echo \"\"\n echo \" 1) $(msg manager_runtime.copaw)\"\n echo \" 2) $(msg manager_runtime.openclaw)\"\n echo \"\"\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_MANAGER_RUNTIME=\"${AGENTTEAMS_MANAGER_RUNTIME:-copaw}\"\n elif [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_MANAGER_RUNTIME}\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg manager_runtime.title_short)\" \"${AGENTTEAMS_MANAGER_RUNTIME}\")\"\n local _runtime_choice\n read -e -p \"$(msg manager_runtime.choice): \" _runtime_choice\n if [ \"${_runtime_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n if [ -n \"${_runtime_choice}\" ]; then\n case \"${_runtime_choice}\" in\n 2) AGENTTEAMS_MANAGER_RUNTIME=\"openclaw\" ;;\n *) AGENTTEAMS_MANAGER_RUNTIME=\"copaw\" ;;\n esac\n fi\n elif [ -z \"${AGENTTEAMS_MANAGER_RUNTIME+x}\" ]; then\n local _runtime_choice\n read -e -p \"$(msg manager_runtime.choice): \" _runtime_choice\n if [ \"${_runtime_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _runtime_choice=\"${_runtime_choice:-1}\"\n case \"${_runtime_choice}\" in\n 2) AGENTTEAMS_MANAGER_RUNTIME=\"openclaw\" ;;\n *) AGENTTEAMS_MANAGER_RUNTIME=\"copaw\" ;;\n esac\n fi\n export AGENTTEAMS_MANAGER_RUNTIME\n log \"$(msg manager_runtime.selected \"${AGENTTEAMS_MANAGER_RUNTIME}\")\"\n}\n\nstep_e2ee() {\n log \"\"\n log \"$(msg matrix_e2ee.title)\"\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_MATRIX_E2EE=\"${AGENTTEAMS_MATRIX_E2EE:-0}\"\n log \" $(msg matrix_e2ee.title_short) = ${AGENTTEAMS_MATRIX_E2EE} (non-interactive, skipped)\"\n export AGENTTEAMS_MATRIX_E2EE\n return 0\n fi\n # ─────────────────────────────────────────────────────────────────\n echo \"\"\n echo -e \" $(msg matrix_e2ee.desc)\"\n echo \"\"\n echo \" 1) $(msg matrix_e2ee.disable)\"\n echo \" 2) $(msg matrix_e2ee.enable)\"\n echo \"\"\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_MATRIX_E2EE}\" ]; then\n local _e2ee_display; if [ \"${AGENTTEAMS_MATRIX_E2EE}\" = \"1\" ]; then _e2ee_display=\"$(msg matrix_e2ee.val_enabled)\"; else _e2ee_display=\"$(msg matrix_e2ee.val_disabled)\"; fi\n log \"$(msg prompt.upgrade_keep \"$(msg matrix_e2ee.title_short)\" \"${_e2ee_display}\")\"\n local _e2ee_choice\n read -e -p \"$(msg matrix_e2ee.choice): \" _e2ee_choice\n if [ \"${_e2ee_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n if [ -n \"${_e2ee_choice}\" ]; then\n case \"${_e2ee_choice}\" in\n 2) AGENTTEAMS_MATRIX_E2EE=\"1\" ;;\n *) AGENTTEAMS_MATRIX_E2EE=\"0\" ;;\n esac\n fi\n elif [ -z \"${AGENTTEAMS_MATRIX_E2EE+x}\" ]; then\n local _e2ee_choice\n read -e -p \"$(msg matrix_e2ee.choice): \" _e2ee_choice\n if [ \"${_e2ee_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _e2ee_choice=\"${_e2ee_choice:-1}\"\n case \"${_e2ee_choice}\" in\n 2) AGENTTEAMS_MATRIX_E2EE=\"1\" ;;\n *) AGENTTEAMS_MATRIX_E2EE=\"0\" ;;\n esac\n fi\n AGENTTEAMS_MATRIX_E2EE=\"${AGENTTEAMS_MATRIX_E2EE:-0}\"\n export AGENTTEAMS_MATRIX_E2EE\n if [ \"${AGENTTEAMS_MATRIX_E2EE}\" = \"1\" ]; then\n log \"$(msg matrix_e2ee.selected_enabled)\"\n else\n log \"$(msg matrix_e2ee.selected_disabled)\"\n fi\n}\n\nstep_docker_proxy() {\n # Only relevant when socket mounting is enabled\n if [ \"${AGENTTEAMS_MOUNT_SOCKET}\" != \"1\" ]; then\n AGENTTEAMS_DOCKER_PROXY=\"0\"\n return 0\n fi\n\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_DOCKER_PROXY=\"${AGENTTEAMS_DOCKER_PROXY:-0}\"\n log \" $(msg docker_proxy.title_short) = ${AGENTTEAMS_DOCKER_PROXY} (non-interactive, skipped)\"\n export AGENTTEAMS_DOCKER_PROXY\n return 0\n fi\n # ─────────────────────────────────────────────────────────────────\n\n echo \"\"\n echo -e \" \\033[1m$(msg docker_proxy.title)\\033[0m\"\n echo \"\"\n echo -e \" $(msg docker_proxy.desc)\"\n echo \"\"\n echo \" 1) $(msg docker_proxy.enable)\"\n echo \" 2) $(msg docker_proxy.disable)\"\n echo \"\"\n\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_DOCKER_PROXY}\" ]; then\n local _proxy_display; if [ \"${AGENTTEAMS_DOCKER_PROXY}\" = \"1\" ]; then _proxy_display=\"$(msg docker_proxy.val_enabled)\"; else _proxy_display=\"$(msg docker_proxy.val_disabled)\"; fi\n log \"$(msg prompt.upgrade_keep \"$(msg docker_proxy.title_short)\" \"${_proxy_display}\")\"\n local _choice\n read -e -p \"$(msg docker_proxy.choice): \" _choice\n if [ \"${_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n if [ -n \"${_choice}\" ]; then\n case \"${_choice}\" in\n 2) AGENTTEAMS_DOCKER_PROXY=\"0\" ;;\n *) AGENTTEAMS_DOCKER_PROXY=\"1\" ;;\n esac\n fi\n elif [ -z \"${AGENTTEAMS_DOCKER_PROXY+x}\" ]; then\n local _choice\n read -e -p \"$(msg docker_proxy.choice): \" _choice\n if [ \"${_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _choice=\"${_choice:-1}\"\n case \"${_choice}\" in\n 2) AGENTTEAMS_DOCKER_PROXY=\"0\" ;;\n *) AGENTTEAMS_DOCKER_PROXY=\"1\" ;;\n esac\n fi\n AGENTTEAMS_DOCKER_PROXY=\"${AGENTTEAMS_DOCKER_PROXY:-1}\"\n export AGENTTEAMS_DOCKER_PROXY\n if [ \"${AGENTTEAMS_DOCKER_PROXY}\" = \"1\" ]; then\n log \"$(msg docker_proxy.selected_enabled)\"\n\n # Prompt for additional allowed image sources\n echo \"\"\n echo -e \" $(msg docker_proxy.registries_desc)\"\n echo \"\"\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_PROXY_ALLOWED_REGISTRIES}\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg docker_proxy.registries_label)\" \"${AGENTTEAMS_PROXY_ALLOWED_REGISTRIES}\")\"\n local _reg_input\n read -e -p \"$(msg docker_proxy.registries_prompt): \" _reg_input\n if [ \"${_reg_input}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n [ -n \"${_reg_input}\" ] && AGENTTEAMS_PROXY_ALLOWED_REGISTRIES=\"${_reg_input}\"\n elif [ -z \"${AGENTTEAMS_PROXY_ALLOWED_REGISTRIES+x}\" ]; then\n local _reg_input\n read -e -p \"$(msg docker_proxy.registries_prompt): \" _reg_input\n if [ \"${_reg_input}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_PROXY_ALLOWED_REGISTRIES=\"${_reg_input:-}\"\n fi\n export AGENTTEAMS_PROXY_ALLOWED_REGISTRIES\n else\n log \"$(msg docker_proxy.selected_disabled)\"\n fi\n}\n\nstep_idle() {\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_WORKER_IDLE_TIMEOUT=\"${AGENTTEAMS_WORKER_IDLE_TIMEOUT:-720}\"\n log \" $(msg idle_timeout.label) = ${AGENTTEAMS_WORKER_IDLE_TIMEOUT} (non-interactive, skipped)\"\n export AGENTTEAMS_WORKER_IDLE_TIMEOUT\n return 0\n fi\n # ─────────────────────────────────────────────────────────────────\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_WORKER_IDLE_TIMEOUT}\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg idle_timeout.label)\" \"${AGENTTEAMS_WORKER_IDLE_TIMEOUT}\")\"\n local _idle_timeout\n read -e -p \"$(msg idle_timeout.prompt): \" _idle_timeout\n if [ \"${_idle_timeout}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n [ -n \"${_idle_timeout}\" ] && AGENTTEAMS_WORKER_IDLE_TIMEOUT=\"${_idle_timeout}\"\n elif [ -z \"${AGENTTEAMS_WORKER_IDLE_TIMEOUT+x}\" ]; then\n local _idle_timeout\n read -e -p \"$(msg idle_timeout.prompt): \" _idle_timeout\n if [ \"${_idle_timeout}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_WORKER_IDLE_TIMEOUT=\"${_idle_timeout:-720}\"\n fi\n AGENTTEAMS_WORKER_IDLE_TIMEOUT=\"${AGENTTEAMS_WORKER_IDLE_TIMEOUT:-720}\"\n export AGENTTEAMS_WORKER_IDLE_TIMEOUT\n log \"$(msg idle_timeout.selected \"${AGENTTEAMS_WORKER_IDLE_TIMEOUT}\")\"\n}\n\nstep_hostshare() {\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_HOST_SHARE_DIR=\"${AGENTTEAMS_HOST_SHARE_DIR:-${HOME}}\"\n log \" $(msg host_share.label) = ${AGENTTEAMS_HOST_SHARE_DIR} (non-interactive, skipped)\"\n export AGENTTEAMS_HOST_SHARE_DIR\n return 0\n fi\n # ─────────────────────────────────────────────────────────────────\n local _current=\"${AGENTTEAMS_HOST_SHARE_DIR}\"\n local _share_dir\n if [ -n \"${_current}\" ]; then\n read -e -p \"$(msg host_share.prompt \"$HOME\") [${_current}]: \" _share_dir\n else\n read -e -p \"$(msg host_share.prompt \"$HOME\"): \" _share_dir\n fi\n if [ \"${_share_dir}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_HOST_SHARE_DIR=\"${_share_dir:-${_current:-$HOME}}\"\n export AGENTTEAMS_HOST_SHARE_DIR\n}\n\nstep_podman_autostart() {\n echo \"\"\n log \"$(msg podman.autostart.title)\"\n echo \"\"\n echo -e \" $(msg podman.autostart.desc)\"\n echo \"\"\n echo \" 1) $(msg podman.autostart.enable)\"\n echo \" 2) $(msg podman.autostart.disable)\"\n echo \"\"\n\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_PODMAN_AUTOSTART}\" ]; then\n local _disp; if [ \"${AGENTTEAMS_PODMAN_AUTOSTART}\" = \"1\" ]; then _disp=\"$(msg podman.autostart.val_enabled)\"; else _disp=\"$(msg podman.autostart.val_disabled)\"; fi\n log \"$(msg prompt.upgrade_keep \"$(msg podman.autostart.title_short)\" \"${_disp}\")\"\n local _choice\n read -e -p \"$(msg podman.autostart.choice): \" _choice\n if [ \"${_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n if [ -n \"${_choice}\" ]; then\n case \"${_choice}\" in\n 2) AGENTTEAMS_PODMAN_AUTOSTART=\"0\" ;;\n *) AGENTTEAMS_PODMAN_AUTOSTART=\"1\" ;;\n esac\n fi\n elif [ -z \"${AGENTTEAMS_PODMAN_AUTOSTART+x}\" ]; then\n local _choice\n read -e -p \"$(msg podman.autostart.choice) [1]: \" _choice\n if [ \"${_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _choice=\"${_choice:-1}\"\n case \"${_choice}\" in\n 2) AGENTTEAMS_PODMAN_AUTOSTART=\"0\" ;;\n *) AGENTTEAMS_PODMAN_AUTOSTART=\"1\" ;;\n esac\n fi\n AGENTTEAMS_PODMAN_AUTOSTART=\"${AGENTTEAMS_PODMAN_AUTOSTART:-1}\"\n export AGENTTEAMS_PODMAN_AUTOSTART\n if [ \"${AGENTTEAMS_PODMAN_AUTOSTART}\" = \"1\" ]; then\n log \"$(msg podman.autostart.selected_enabled)\"\n else\n log \"$(msg podman.autostart.selected_disabled)\"\n fi\n}\n\n# ============================================================\n# Setup dedicated Podman autostart service for AgentTeams\n# ============================================================\nsetup_podman_autostart() {\n # Only execute if using podman and autostart is enabled\n if [ \"${DOCKER_CMD:-docker}\" != \"podman\" ] || [ \"${AGENTTEAMS_PODMAN_AUTOSTART:-0}\" != \"1\" ]; then\n return 0\n fi\n\n log \"$(msg install.podman.autostart_title)\"\n\n local current_user\n current_user=\"$(whoami)\"\n\n # Define the dedicated AgentTeams service content\n # Strict scoping: Only manage containers matching '--filter name=agentteams'\n local _service_content=\"[Unit]\nDescription=AgentTeams Dedicated Podman Autostart Service\nDocumentation=https://github.com/agentscope-ai/AgentTeams\nWants=network-online.target\nAfter=network-online.target\n\n[Service]\nType=oneshot\nRemainAfterExit=true\nEnvironment=LOGGING=\\\"--log-level=info\\\"\nExecStart=/usr/bin/podman \\$LOGGING start --filter name=agentteams --filter restart-policy=always --filter restart-policy=unless-stopped\nExecStop=/usr/bin/podman \\$LOGGING stop --filter name=agentteams --filter restart-policy=always --filter restart-policy=unless-stopped\n\n[Install]\nWantedBy=default.target\"\n\n local _autostart_ok=0\n\n if [ \"${current_user}\" = \"root\" ]; then\n # Rootful Mode (System-wide dedicated service)\n log \"$(msg install.podman.root_setup)\"\n\n local _sys_dir=\"/etc/systemd/system\"\n mkdir -p \"${_sys_dir}\"\n echo \"${_service_content}\" > \"${_sys_dir}/agentteams-podman-restart.service\"\n\n systemctl daemon-reload >/dev/null 2>&1 || true\n\n if systemctl enable --now agentteams-podman-restart.service >/dev/null 2>&1; then\n log \"$(msg install.podman.root_success)\"\n _autostart_ok=1\n else\n log \"$(msg install.podman.root_fail)\"\n fi\n else\n # Rootless Mode (User-level dedicated service)\n log \"$(msg install.podman.user_setup \"${current_user}\")\"\n\n local _linger_ok=0\n\n # 1. Enable Linger for background execution (Graceful escalation strategy)\n if loginctl show-user \"${current_user}\" --property=Linger 2>/dev/null | grep -q \"Linger=yes\"; then\n _linger_ok=1\n else\n log \"$(msg install.podman.linger_enable)\"\n\n # Step A: Try to enable linger natively without sudo\n if loginctl enable-linger \"${current_user}\" >/dev/null 2>&1; then\n log \"Successfully enabled systemd linger natively.\"\n _linger_ok=1\n else\n # Step B: Fallback to sudo\n log \"Native linger enablement failed. Distro security policy requires privileges.\"\n if command -v sudo >/dev/null 2>&1; then\n log \"Attempting to enable systemd linger via sudo (enter password if prompted)...\"\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n sudo -n loginctl enable-linger \"${current_user}\" 2>/dev/null && _linger_ok=1 || log \"$(msg install.podman.linger_warn)\"\n else\n sudo loginctl enable-linger \"${current_user}\" && _linger_ok=1 || log \"$(msg install.podman.linger_warn)\"\n fi\n else\n log \"$(msg install.podman.linger_warn)\"\n fi\n fi\n fi\n\n # 2. Write dedicated service file to user directory\n local _user_dir=\"${HOME}/.config/systemd/user\"\n mkdir -p \"${_user_dir}\"\n echo \"${_service_content}\" > \"${_user_dir}/agentteams-podman-restart.service\"\n\n systemctl --user daemon-reload >/dev/null 2>&1 || true\n\n if systemctl --user enable --now agentteams-podman-restart.service >/dev/null 2>&1; then\n log \"$(msg install.podman.user_success)\"\n # Crucial: Only consider autostart fully successful if systemd linger is also enabled\n if [ \"${_linger_ok}\" = \"1\" ]; then\n _autostart_ok=1\n else\n log \"⚠️ Service enabled, but since systemd linger is not active, containers will not auto-recover on system reboot.\"\n fi\n else\n log \"$(msg install.podman.user_fail)\"\n fi\n fi\n\n # Only print final success banner if the service AND linger were actually enabled\n if [ \"${_autostart_ok}\" = \"1\" ]; then\n log \"$(msg install.podman.success)\"\n fi\n}\n\n# ============================================================\n# Dashboard startup (called after controller is ready)\n# ============================================================\n\n_start_dashboard() {\n local CTRL_CONTAINER=\"agentteams-controller\"\n local DASHBOARD_CONTAINER=\"agentteams-dashboard\"\n\n if [ \"${AGENTTEAMS_DASHBOARD:-0}\" != \"1\" ]; then\n # Stop and remove the existing dashboard container if present.\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -q \"^${DASHBOARD_CONTAINER}$\"; then\n log \"Stopping and removing ${DASHBOARD_CONTAINER}...\"\n ${DOCKER_CMD} stop \"${DASHBOARD_CONTAINER}\" >/dev/null 2>&1 || true\n ${DOCKER_CMD} rm -f \"${DASHBOARD_CONTAINER}\" >/dev/null 2>&1 || true\n log \"Dashboard stopped.\"\n fi\n return 0\n fi\n\n # Dashboard requires the embedded architecture (controller container).\n # In legacy all-in-one mode the controller API is not available.\n if [ \"${AGENTTEAMS_USE_EMBEDDED:-0}\" != \"1\" ]; then\n log \"Skipping Dashboard: requires embedded architecture (agentteams-controller), not available in legacy mode.\"\n log \"Use Quick Start (embedded) mode to enable agentteams-dashboard.\"\n return 0\n fi\n\n AGENTTEAMS_PORT_DASHBOARD=\"${AGENTTEAMS_PORT_DASHBOARD:-13000}\"\n AGENTTEAMS_DASHBOARD_VERSION=\"${AGENTTEAMS_DASHBOARD_VERSION:-v1.2.0}\"\n AGENTTEAMS_DASHBOARD_IMAGE=\"${AGENTTEAMS_DASHBOARD_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-dashboard:${AGENTTEAMS_DASHBOARD_VERSION}}\"\n\n log \"\"\n log \"Starting agentteams-dashboard...\"\n\n local BIND_ADDR=\"127.0.0.1\"\n [ \"${AGENTTEAMS_LOCAL_ONLY:-1}\" = \"0\" ] && BIND_ADDR=\"0.0.0.0\"\n\n # Remove existing container\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' | grep -qx \"${DASHBOARD_CONTAINER}\"; then\n ${DOCKER_CMD} rm -f \"${DASHBOARD_CONTAINER}\" >/dev/null 2>&1 || true\n fi\n\n # Pull or use local image\n if ! ${DOCKER_CMD} image inspect \"${AGENTTEAMS_DASHBOARD_IMAGE}\" >/dev/null 2>&1; then\n log \"Pulling dashboard image: ${AGENTTEAMS_DASHBOARD_IMAGE}\"\n ${DOCKER_CMD} pull \"${AGENTTEAMS_DASHBOARD_IMAGE}\" 2>/dev/null || {\n log \"WARNING: Could not pull dashboard image. Skipping.\"\n return 0\n }\n fi\n\n # Build env args from controller container (MinIO/LLM/auth).\n local env_args=()\n env_args+=(-e AGENTTEAMS_CONTROLLER_URL=\"http://${CTRL_CONTAINER}:8090\")\n env_args+=(-e NEXT_PUBLIC_MATRIX_API_URL=\"http://${CTRL_CONTAINER}:6167\")\n env_args+=(-e MATRIX_HOMESERVER_ALLOWLIST=\"${CTRL_CONTAINER},matrix-local.agentteams.io,matrix.org\")\n\n if ${DOCKER_CMD} ps --format '{{.Names}}' | grep -qx \"${CTRL_CONTAINER}\"; then\n local env_out\n env_out=$(${DOCKER_CMD} inspect \"${CTRL_CONTAINER}\" --format='{{range .Config.Env}}{{.}}{{\"\\n\"}}{{end}}')\n\n _env() { echo \"${env_out}\" | sed -n \"s|^${1}=||p\" | head -n1; }\n\n local fs_endpoint; fs_endpoint=$(_env AGENTTEAMS_FS_ENDPOINT)\n [ -z \"${fs_endpoint}\" ] && fs_endpoint=$(_env AGENTTEAMS_MINIO_ENDPOINT)\n if [ -n \"${fs_endpoint}\" ]; then\n fs_endpoint=$(echo \"${fs_endpoint}\" | sed -e \"s|127\\.0\\.0\\.1|${CTRL_CONTAINER}|\" -e \"s|localhost|${CTRL_CONTAINER}|\")\n env_args+=(-e AGENTTEAMS_FS_ENDPOINT=\"${fs_endpoint}\")\n else\n env_args+=(-e AGENTTEAMS_FS_ENDPOINT=\"http://${CTRL_CONTAINER}:9000\")\n fi\n\n local fs_bucket; fs_bucket=$(_env AGENTTEAMS_FS_BUCKET)\n [ -z \"${fs_bucket}\" ] && fs_bucket=$(_env AGENTTEAMS_MINIO_BUCKET)\n [ -n \"${fs_bucket}\" ] && env_args+=(-e AGENTTEAMS_FS_BUCKET=\"${fs_bucket}\")\n\n local fs_user; fs_user=$(_env AGENTTEAMS_FS_ACCESS_KEY)\n [ -z \"${fs_user}\" ] && fs_user=$(_env AGENTTEAMS_MINIO_USER)\n [ -n \"${fs_user}\" ] && env_args+=(-e AGENTTEAMS_FS_ACCESS_KEY=\"${fs_user}\")\n\n local fs_pass; fs_pass=$(_env AGENTTEAMS_FS_SECRET_KEY)\n [ -z \"${fs_pass}\" ] && fs_pass=$(_env AGENTTEAMS_MINIO_PASSWORD)\n [ -n \"${fs_pass}\" ] && env_args+=(-e AGENTTEAMS_FS_SECRET_KEY=\"${fs_pass}\")\n\n for k in AGENTTEAMS_LLM_PROVIDER AGENTTEAMS_LLM_API_KEY AGENTTEAMS_OPENAI_BASE_URL AGENTTEAMS_DEFAULT_MODEL; do\n local v; v=$(_env \"${k}\")\n [ -n \"${v}\" ] && env_args+=(-e \"${k}=${v}\")\n done\n\n # The controller writes the CLI SA token to /var/run/agentteams/cli-token.\n # The token is generated asynchronously during the first reconcile loop,\n # so it may not be available immediately after the container starts.\n # We poll for up to 30s to handle both fresh installs (token not yet minted)\n # and older images that don't support cli-token at all.\n # For backward compatibility, also try the legacy /var/run/hiclaw/cli-token\n # path used by older HiClaw images.\n local auth_token=\"${AGENTTEAMS_AUTH_TOKEN:-}\"\n if [ -z \"${auth_token}\" ]; then\n local _token_wait=0\n local _token_max_wait=30\n while [ ${_token_wait} -lt ${_token_max_wait} ]; do\n auth_token=$(${DOCKER_CMD} exec \"${CTRL_CONTAINER}\" sh -c 'cat /var/run/agentteams/cli-token 2>/dev/null || cat /var/run/hiclaw/cli-token 2>/dev/null' | tr -d '\\n' || true)\n if [ -n \"${auth_token}\" ]; then\n break\n fi\n sleep 2\n _token_wait=$((_token_wait + 2))\n done\n fi\n if [ -n \"${auth_token}\" ]; then\n env_args+=(-e AGENTTEAMS_AUTH_TOKEN=\"${auth_token}\")\n else\n log \"WARNING: could not read controller SA token (timed out after ${_token_max_wait:-30}s)\"\n log \" This can happen with older AgentTeams/HiClaw images or if the controller is still starting.\"\n log \" Dashboard will still work but some API calls may fail until you log in via Higress Console.\"\n log \" To fix: upgrade AgentTeams to v1.2.0-beta.1+ or set AGENTTEAMS_AUTH_TOKEN manually.\"\n fi\n\n local admin_user; admin_user=$(_env AGENTTEAMS_ADMIN_USER)\n local admin_pass; admin_pass=$(_env AGENTTEAMS_ADMIN_PASSWORD)\n [ -n \"${admin_user}\" ] && env_args+=(-e AGENTTEAMS_ADMIN_USER=\"${admin_user}\")\n [ -n \"${admin_pass}\" ] && env_args+=(-e AGENTTEAMS_ADMIN_PASSWORD=\"${admin_pass}\")\n\n # Higress Console URL: explicit config takes priority, auto-detect as fallback.\n local _resolved_gw_url=\"\"\n if [ -n \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\" ]; then\n # Normalize URL: add http:// prefix if missing\n _resolved_gw_url=\"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\"\n case \"${_resolved_gw_url}\" in\n http://*|https://*) ;;\n *) _resolved_gw_url=\"http://${_resolved_gw_url}\" ;;\n esac\n env_args+=(-e AGENTTEAMS_AI_GATEWAY_ADMIN_URL=\"${_resolved_gw_url}\")\n elif ${DOCKER_CMD} exec \"${CTRL_CONTAINER}\" wget -q -O- --timeout=2 http://127.0.0.1:8001/ >/dev/null 2>&1; then\n _resolved_gw_url=\"http://${CTRL_CONTAINER}:8001\"\n env_args+=(-e AGENTTEAMS_AI_GATEWAY_ADMIN_URL=\"${_resolved_gw_url}\")\n fi\n\n # Verify Higress Console URL reachability (best-effort warning)\n if [ -n \"${_resolved_gw_url}\" ]; then\n if ! ${DOCKER_CMD} exec \"${CTRL_CONTAINER}\" curl -sf --max-time 3 \"${_resolved_gw_url}/\" >/dev/null 2>&1; then\n log \"WARNING: Higress Console URL may not be reachable: ${_resolved_gw_url}\"\n log \" Dashboard will still work, but shared login via Higress Console may fail.\"\n log \" To fix: verify the URL is correct and the Higress Console service is running.\"\n fi\n fi\n else\n log \"WARNING: ${CTRL_CONTAINER} not running; launching Dashboard without Controller/MinIO/LLM env.\"\n if [ -n \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\" ]; then\n local _gw_norm=\"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\"\n case \"${_gw_norm}\" in\n http://*|https://*) ;;\n *) _gw_norm=\"http://${_gw_norm}\" ;;\n esac\n env_args+=(-e AGENTTEAMS_AI_GATEWAY_ADMIN_URL=\"${_gw_norm}\")\n fi\n fi\n\n # Create persistent data volume (for future use)\n ${DOCKER_CMD} volume create agentteams-dashboard-data >/dev/null 2>&1 || true\n\n ${DOCKER_CMD} run -d \\\n --name \"${DASHBOARD_CONTAINER}\" \\\n --restart unless-stopped \\\n --network \"${AGENTTEAMS_NETWORK:-agentteams-net}\" \\\n --network-alias dashboard.agentteams.io \\\n -p \"${BIND_ADDR}:${AGENTTEAMS_PORT_DASHBOARD}:3000\" \\\n \"${env_args[@]}\" \\\n -v agentteams-dashboard-data:/app/db \\\n \"${AGENTTEAMS_DASHBOARD_IMAGE}\"\n\n # Wait for dashboard to be ready\n local max_wait=60\n local waited=0\n while [ $waited -lt $max_wait ]; do\n if curl -sf \"http://127.0.0.1:${AGENTTEAMS_PORT_DASHBOARD}/\" >/dev/null 2>&1; then\n log \"Dashboard ready on port ${AGENTTEAMS_PORT_DASHBOARD}\"\n return 0\n fi\n sleep 2\n waited=$((waited + 2))\n done\n log \"WARNING: Dashboard startup timed out after ${max_wait}s\"\n ${DOCKER_CMD} logs \"${DASHBOARD_CONTAINER}\" 2>&1 | tail -20\n return 0\n}\n\n# ============================================================\n# Manager Installation (Interactive)\n# ============================================================\n\ninstall_manager() {\n log \"$(msg install.title)\"\n log \"$(msg install.registry \"${AGENTTEAMS_REGISTRY}\")\"\n log \"\"\n log \"$(msg install.dir \"$(pwd)\")\"\n log \"$(msg install.dir_hint)\"\n log \"$(msg install.dir_hint2)\"\n log \"\"\n\n # Non-interactive fallback: resolve version immediately so image tags are available\n # before the step state machine runs. Interactive mode lets step_version handle it.\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_VERSION=\"${AGENTTEAMS_VERSION:-${AGENTTEAMS_KNOWN_STABLE_VERSION}}\"\n resolve_image_tags\n fi\n\n # Migrate legacy env file location before checks\n local existing_env=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n if [ ! -f \"${existing_env}\" ] && [ -f \"./agentteams-manager.env\" ]; then\n log \"Migrating agentteams-manager.env from current directory to ${existing_env}...\"\n mv \"./agentteams-manager.env\" \"${existing_env}\"\n fi\n\n # Orphan volume detection (only when no env file — step_existing handles the env-file case)\n if [ ! -f \"${existing_env}\" ]; then\n local data_vol=\"${AGENTTEAMS_DATA_DIR:-agentteams-data}\"\n if ${DOCKER_CMD} volume ls -q | grep -q \"^${data_vol}$\"; then\n echo \"\"\n log \"$(msg install.orphan_volume.detected \"${data_vol}\")\"\n log \"$(msg install.orphan_volume.warn)\"\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n log \"$(msg install.orphan_volume.clean_noninteractive)\"\n ${DOCKER_CMD} stop agentteams-manager 2>/dev/null || true\n ${DOCKER_CMD} rm agentteams-manager 2>/dev/null || true\n for w in $(${DOCKER_CMD} ps -a --format '{{.Names}}' | grep \"^agentteams-worker-\" || true); do\n ${DOCKER_CMD} stop \"${w}\" 2>/dev/null || true\n ${DOCKER_CMD} rm \"${w}\" 2>/dev/null || true\n done\n log \"$(msg install.orphan_volume.cleaning)\"\n ${DOCKER_CMD} volume rm \"${data_vol}\" 2>/dev/null || true\n log \"$(msg install.orphan_volume.cleaned)\"\n else\n echo \"\"\n echo \"$(msg install.orphan_volume.choose)\"\n echo \"$(msg install.orphan_volume.clean)\"\n echo \"$(msg install.orphan_volume.keep)\"\n echo \"\"\n local ORPHAN_CHOICE\n read -e -p \"$(msg install.orphan_volume.prompt): \" ORPHAN_CHOICE\n ORPHAN_CHOICE=\"${ORPHAN_CHOICE:-1}\"\n case \"${ORPHAN_CHOICE}\" in\n 1|clean)\n ${DOCKER_CMD} stop agentteams-manager 2>/dev/null || true\n ${DOCKER_CMD} rm agentteams-manager 2>/dev/null || true\n for w in $(${DOCKER_CMD} ps -a --format '{{.Names}}' | grep \"^agentteams-worker-\" || true); do\n ${DOCKER_CMD} stop \"${w}\" 2>/dev/null || true\n ${DOCKER_CMD} rm \"${w}\" 2>/dev/null || true\n done\n log \"$(msg install.orphan_volume.cleaning)\"\n ${DOCKER_CMD} volume rm \"${data_vol}\" 2>/dev/null || true\n log \"$(msg install.orphan_volume.cleaned)\"\n ;;\n 2|keep)\n log \"$(msg install.orphan_volume.keeping)\"\n ;;\n esac\n fi\n fi\n fi\n\n # ── State machine ─────────────────────────────────────────────────────────\n local _STEPS=( step_lang step_mode step_version step_existing step_llm step_manager_runtime step_runtime step_admin step_network\n step_ports step_domains step_github step_skills step_volume\n step_workspace step_dashboard step_e2ee step_docker_proxy step_idle step_hostshare step_podman_autostart )\n local _STEP_HISTORY=()\n local _step_idx=0\n while [ \"${_step_idx}\" -lt \"${#_STEPS[@]}\" ]; do\n local _step_fn=\"${_STEPS[$_step_idx]}\"\n if should_skip_step \"${_step_fn}\"; then\n _step_idx=$((_step_idx + 1))\n continue\n fi\n if [ \"${#_STEP_HISTORY[@]}\" -gt 0 ]; then\n log \"$(msg nav.back_hint)\"\n fi\n STEP_RESULT=\"\"\n \"${_step_fn}\"\n if [ \"${STEP_RESULT}\" = \"back\" ]; then\n if [ \"${#_STEP_HISTORY[@]}\" -gt 0 ]; then\n local _last=$(( ${#_STEP_HISTORY[@]} - 1 ))\n _step_idx=\"${_STEP_HISTORY[$_last]}\"\n _STEP_HISTORY=(\"${_STEP_HISTORY[@]:0:${_last}}\")\n clear_step_vars \"${_STEPS[$_step_idx]}\"\n fi\n # else: first step, ignore 'b'\n else\n _STEP_HISTORY+=(\"${_step_idx}\")\n _step_idx=$((_step_idx + 1))\n fi\n done\n # ── End state machine ──────────────────────────────────────────────────────\n\n # Post-machine defaults for any steps that were skipped\n AGENTTEAMS_DATA_DIR=\"${AGENTTEAMS_DATA_DIR:-agentteams-data}\"\n if [ -z \"${AGENTTEAMS_WORKSPACE_DIR+x}\" ] || [ -z \"${AGENTTEAMS_WORKSPACE_DIR}\" ]; then\n AGENTTEAMS_WORKSPACE_DIR=\"${HOME}/agentteams-manager\"\n export AGENTTEAMS_WORKSPACE_DIR\n fi\n AGENTTEAMS_WORKSPACE_DIR=\"$(cd \"${AGENTTEAMS_WORKSPACE_DIR}\" 2>/dev/null && pwd || echo \"${AGENTTEAMS_WORKSPACE_DIR}\")\"\n mkdir -p \"${AGENTTEAMS_WORKSPACE_DIR}\"\n AGENTTEAMS_MANAGER_RUNTIME=\"${AGENTTEAMS_MANAGER_RUNTIME:-copaw}\"\n export AGENTTEAMS_MANAGER_RUNTIME\n AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"${AGENTTEAMS_DEFAULT_WORKER_RUNTIME:-copaw}\"\n AGENTTEAMS_MATRIX_E2EE=\"${AGENTTEAMS_MATRIX_E2EE:-0}\"\n export AGENTTEAMS_MATRIX_E2EE\n AGENTTEAMS_WORKER_IDLE_TIMEOUT=\"${AGENTTEAMS_WORKER_IDLE_TIMEOUT:-720}\"\n export AGENTTEAMS_WORKER_IDLE_TIMEOUT\n AGENTTEAMS_HOST_SHARE_DIR=\"${AGENTTEAMS_HOST_SHARE_DIR:-$HOME}\"\n export AGENTTEAMS_HOST_SHARE_DIR\n\n log \"\"\n\n # Generate secrets (only if not already set)\n log \"$(msg install.generating_secrets)\"\n AGENTTEAMS_MANAGER_PASSWORD=\"${AGENTTEAMS_MANAGER_PASSWORD:-$(generate_key)}\"\n AGENTTEAMS_REGISTRATION_TOKEN=\"${AGENTTEAMS_REGISTRATION_TOKEN:-$(generate_key)}\"\n AGENTTEAMS_MINIO_USER=\"${AGENTTEAMS_MINIO_USER:-${AGENTTEAMS_ADMIN_USER}}\"\n AGENTTEAMS_MINIO_PASSWORD=\"${AGENTTEAMS_MINIO_PASSWORD:-${AGENTTEAMS_ADMIN_PASSWORD}}\"\n AGENTTEAMS_MANAGER_GATEWAY_KEY=\"${AGENTTEAMS_MANAGER_GATEWAY_KEY:-$(generate_key)}\"\n\n # Matrix AppService tokens — generate once during install/upgrade if not provided.\n # Persisted to env file so they survive controller restarts.\n AGENTTEAMS_MATRIX_APPSERVICE_ENABLED=\"${AGENTTEAMS_MATRIX_APPSERVICE_ENABLED:-true}\"\n export AGENTTEAMS_MATRIX_APPSERVICE_ENABLED\n if [ \"${AGENTTEAMS_MATRIX_APPSERVICE_ENABLED}\" != \"false\" ] && [ \"${AGENTTEAMS_MATRIX_APPSERVICE_ENABLED}\" != \"0\" ]; then\n if [ -z \"${AGENTTEAMS_MATRIX_APPSERVICE_AS_TOKEN}\" ]; then\n AGENTTEAMS_MATRIX_APPSERVICE_AS_TOKEN=\"$(openssl rand -hex 32)\"\n log \" Auto-generated AppService as_token (saved to env file)\"\n fi\n if [ -z \"${AGENTTEAMS_MATRIX_APPSERVICE_HS_TOKEN}\" ]; then\n AGENTTEAMS_MATRIX_APPSERVICE_HS_TOKEN=\"$(openssl rand -hex 32)\"\n log \" Auto-generated AppService hs_token (saved to env file)\"\n fi\n export AGENTTEAMS_MATRIX_APPSERVICE_AS_TOKEN\n export AGENTTEAMS_MATRIX_APPSERVICE_HS_TOKEN\n fi\n\n # Detect Apple Silicon (M1/M2/M3/M4) - need JVM fix for Higress Console\n # See: https://github.com/agentscope-ai/AgentTeams/issues/249\n if [ -z \"${JVM_ARGS:-}\" ] && [ \"$(uname -m)\" = \"arm64\" ] && [ \"$(uname -s)\" = \"Darwin\" ]; then\n log \"Apple Silicon detected - setting JVM_ARGS to fix Higress Console SIGILL issue\"\n JVM_ARGS=\"-XX:+UnlockDiagnosticVMOptions -XX:-UseAESCTRIntrinsics -XX:UseSVE=0\"\n fi\n\n # Write .env file\n ENV_FILE=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n cat > \"${ENV_FILE}\" << EOF\n# AgentTeams Manager Configuration\n# Generated by agentteams-install.sh on $(date)\n\n# Language\nAGENTTEAMS_LANGUAGE=${AGENTTEAMS_LANGUAGE}\n\n# LLM\nAGENTTEAMS_LLM_PROVIDER=${AGENTTEAMS_LLM_PROVIDER}\nAGENTTEAMS_DEFAULT_MODEL=${AGENTTEAMS_DEFAULT_MODEL}\nAGENTTEAMS_LLM_API_KEY=${AGENTTEAMS_LLM_API_KEY}\nAGENTTEAMS_OPENAI_BASE_URL=${AGENTTEAMS_OPENAI_BASE_URL:-}\nAGENTTEAMS_MODEL_CONTEXT_WINDOW=${AGENTTEAMS_MODEL_CONTEXT_WINDOW:-}\nAGENTTEAMS_MODEL_MAX_TOKENS=${AGENTTEAMS_MODEL_MAX_TOKENS:-}\nAGENTTEAMS_MODEL_REASONING=${AGENTTEAMS_MODEL_REASONING:-}\nAGENTTEAMS_MODEL_VISION=${AGENTTEAMS_MODEL_VISION:-}\n\n# Embedding model (empty = disabled, default: text-embedding-v4)\nAGENTTEAMS_EMBEDDING_MODEL=${AGENTTEAMS_EMBEDDING_MODEL}\n\n# Admin\nAGENTTEAMS_ADMIN_USER=${AGENTTEAMS_ADMIN_USER}\nAGENTTEAMS_ADMIN_PASSWORD=${AGENTTEAMS_ADMIN_PASSWORD}\n\n# Ports\nAGENTTEAMS_LOCAL_ONLY=${AGENTTEAMS_LOCAL_ONLY}\nAGENTTEAMS_PORT_GATEWAY=${AGENTTEAMS_PORT_GATEWAY}\nAGENTTEAMS_PORT_CONSOLE=${AGENTTEAMS_PORT_CONSOLE}\nAGENTTEAMS_PORT_ELEMENT_WEB=${AGENTTEAMS_PORT_ELEMENT_WEB}\nAGENTTEAMS_PORT_MANAGER_CONSOLE=${AGENTTEAMS_PORT_MANAGER_CONSOLE:-18888}\n\n# Manager runtime (openclaw | copaw)\nAGENTTEAMS_MANAGER_RUNTIME=${AGENTTEAMS_MANAGER_RUNTIME:-copaw}\n\n# Matrix\nAGENTTEAMS_MATRIX_DOMAIN=${AGENTTEAMS_MATRIX_DOMAIN}\nAGENTTEAMS_MATRIX_CLIENT_DOMAIN=${AGENTTEAMS_MATRIX_CLIENT_DOMAIN}\n\n# Gateway\nAGENTTEAMS_AI_GATEWAY_DOMAIN=${AGENTTEAMS_AI_GATEWAY_DOMAIN}\nAGENTTEAMS_MANAGER_GATEWAY_KEY=${AGENTTEAMS_MANAGER_GATEWAY_KEY}\n\n# File System\nAGENTTEAMS_FS_DOMAIN=${AGENTTEAMS_FS_DOMAIN}\nAGENTTEAMS_CONSOLE_DOMAIN=${AGENTTEAMS_CONSOLE_DOMAIN}\nAGENTTEAMS_MINIO_USER=${AGENTTEAMS_MINIO_USER}\nAGENTTEAMS_MINIO_PASSWORD=${AGENTTEAMS_MINIO_PASSWORD}\n\n# Internal\nAGENTTEAMS_MANAGER_PASSWORD=${AGENTTEAMS_MANAGER_PASSWORD}\nAGENTTEAMS_REGISTRATION_TOKEN=${AGENTTEAMS_REGISTRATION_TOKEN}\n\n# GitHub (optional)\nAGENTTEAMS_GITHUB_TOKEN=${AGENTTEAMS_GITHUB_TOKEN:-}\n\n# Nacos package import defaults\nAGENTTEAMS_NACOS_REGISTRY_URI=${AGENTTEAMS_NACOS_REGISTRY_URI:-nacos://market.agentteams.io:80/public}\nAGENTTEAMS_NACOS_USERNAME=${AGENTTEAMS_NACOS_USERNAME:-}\nAGENTTEAMS_NACOS_PASSWORD=${AGENTTEAMS_NACOS_PASSWORD:-}\nAGENTTEAMS_NACOS_TOKEN=${AGENTTEAMS_NACOS_TOKEN:-}\n\n# Skills Registry (optional, default: nacos://market.agentteams.io:80/public)\nAGENTTEAMS_SKILLS_API_URL=${AGENTTEAMS_SKILLS_API_URL:-nacos://market.agentteams.io:80/public}\n\n# OpenClaw CMS plugin (optional)\nAGENTTEAMS_CMS_TRACES_ENABLED=${AGENTTEAMS_CMS_TRACES_ENABLED:-false}\nAGENTTEAMS_CMS_ENDPOINT=${AGENTTEAMS_CMS_ENDPOINT:-}\nAGENTTEAMS_CMS_LICENSE_KEY=${AGENTTEAMS_CMS_LICENSE_KEY:-}\nAGENTTEAMS_CMS_PROJECT=${AGENTTEAMS_CMS_PROJECT:-}\nAGENTTEAMS_CMS_WORKSPACE=${AGENTTEAMS_CMS_WORKSPACE:-}\nAGENTTEAMS_CMS_SERVICE_NAME=${AGENTTEAMS_CMS_SERVICE_NAME:-agentteams-manager}\nAGENTTEAMS_CMS_METRICS_ENABLED=${AGENTTEAMS_CMS_METRICS_ENABLED:-false}\n\n# Worker images (for direct container creation)\nAGENTTEAMS_WORKER_IMAGE=${WORKER_IMAGE}\nAGENTTEAMS_COPAW_WORKER_IMAGE=${COPAW_WORKER_IMAGE}\nAGENTTEAMS_QWENPAW_WORKER_IMAGE=${QWENPAW_WORKER_IMAGE}\nAGENTTEAMS_HERMES_WORKER_IMAGE=${HERMES_WORKER_IMAGE}\n\n# Default Worker runtime (openclaw | copaw | qwenpaw | hermes)\nAGENTTEAMS_DEFAULT_WORKER_RUNTIME=${AGENTTEAMS_DEFAULT_WORKER_RUNTIME:-copaw}\n\n# Matrix E2EE (0=disabled, 1=enabled; default: 0)\nAGENTTEAMS_MATRIX_E2EE=${AGENTTEAMS_MATRIX_E2EE:-0}\n\n# Matrix AppService\nAGENTTEAMS_MATRIX_APPSERVICE_ENABLED=${AGENTTEAMS_MATRIX_APPSERVICE_ENABLED:-true}\nAGENTTEAMS_MATRIX_APPSERVICE_AS_TOKEN=${AGENTTEAMS_MATRIX_APPSERVICE_AS_TOKEN:-}\nAGENTTEAMS_MATRIX_APPSERVICE_HS_TOKEN=${AGENTTEAMS_MATRIX_APPSERVICE_HS_TOKEN:-}\n\n# Docker API proxy (0=disabled, 1=enabled; default: 1)\nAGENTTEAMS_DOCKER_PROXY=${AGENTTEAMS_DOCKER_PROXY:-1}\n\n# Docker API proxy: additional allowed image sources (comma-separated)\nAGENTTEAMS_PROXY_ALLOWED_REGISTRIES=${AGENTTEAMS_PROXY_ALLOWED_REGISTRIES:-}\n\n# Worker idle timeout in minutes (default: 720 = 12 hours)\nAGENTTEAMS_WORKER_IDLE_TIMEOUT=${AGENTTEAMS_WORKER_IDLE_TIMEOUT:-720}\n\n# Podman autostart via systemd (0=disabled, 1=enabled)\nAGENTTEAMS_PODMAN_AUTOSTART=${AGENTTEAMS_PODMAN_AUTOSTART:-0}\n\n# JVM Args for Higress Console (fixes SIGILL on Apple Silicon)\nJVM_ARGS=${JVM_ARGS:-}\n\n# Higress WASM plugin image registry (auto-selected by timezone)\nHIGRESS_ADMIN_WASM_PLUGIN_IMAGE_REGISTRY=${AGENTTEAMS_REGISTRY}\n\n# Data persistence\nAGENTTEAMS_DATA_DIR=${AGENTTEAMS_DATA_DIR:-agentteams-data}\n# Manager workspace (skills, memory, state — host-editable)\nAGENTTEAMS_WORKSPACE_DIR=${AGENTTEAMS_WORKSPACE_DIR:-}\n# Host directory sharing\nAGENTTEAMS_HOST_SHARE_DIR=${AGENTTEAMS_HOST_SHARE_DIR:-}\n\n# agentteams-dashboard (management UI)\nAGENTTEAMS_DASHBOARD=${AGENTTEAMS_DASHBOARD:-1}\nAGENTTEAMS_DASHBOARD_VERSION=${AGENTTEAMS_DASHBOARD_VERSION:-v1.2.0}\nAGENTTEAMS_PORT_DASHBOARD=${AGENTTEAMS_PORT_DASHBOARD:-13000}\nAGENTTEAMS_DASHBOARD_IMAGE=${AGENTTEAMS_DASHBOARD_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-dashboard:${AGENTTEAMS_DASHBOARD_VERSION}}\nAGENTTEAMS_AI_GATEWAY_ADMIN_URL=${AGENTTEAMS_AI_GATEWAY_ADMIN_URL:-}\nEOF\n\n chmod 600 \"${ENV_FILE}\"\n log \"$(msg install.config_saved \"${ENV_FILE}\")\"\n\n # Detect container runtime socket\n SOCKET_MOUNT_ARGS=\"\"\n if [ \"${AGENTTEAMS_MOUNT_SOCKET}\" = \"1\" ]; then\n # Actively ensure the API socket is enabled and active before detection.\n if [ \"${DOCKER_CMD:-}\" = \"podman\" ]; then\n ensure_podman_socket\n fi\n CONTAINER_SOCK=$(detect_socket)\n if [ -n \"${CONTAINER_SOCK}\" ]; then\n log \"$(msg install.socket_detected \"${CONTAINER_SOCK}\")\"\n SOCKET_MOUNT_ARGS=\"-v ${CONTAINER_SOCK}:/var/run/docker.sock --security-opt label=disable\"\n else\n log \"$(msg install.socket_not_found)\"\n # Interactive confirmation when socket not found\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" != \"1\" ]; then\n echo \"\"\n echo -e \"\\033[33m$(msg install.socket_confirm.title)\\033[0m\"\n echo \"\"\n echo -e \"$(msg install.socket_confirm.message)\"\n echo \"\"\n read -p \"$(msg install.socket_confirm.prompt)\" SOCKET_CONFIRM\n if [ \"${SOCKET_CONFIRM}\" != \"y\" ] && [ \"${SOCKET_CONFIRM}\" != \"Y\" ]; then\n log \"$(msg install.socket_confirm.cancelled)\"\n exit 0\n fi\n fi\n fi\n fi\n\n # Create the data volume if it doesn't already exist (reuse on reinstall)\n if ! ${DOCKER_CMD} volume ls -q | grep -q \"^${AGENTTEAMS_DATA_DIR}$\"; then\n ${DOCKER_CMD} volume create \"${AGENTTEAMS_DATA_DIR}\" > /dev/null\n fi\n\n # Data mount: Docker volume\n DATA_MOUNT_ARGS=\"-v ${AGENTTEAMS_DATA_DIR}:/data\"\n\n # Manager workspace mount (always a host directory, defaulting to ~/agentteams-manager)\n WORKSPACE_MOUNT_ARGS=\"-v ${AGENTTEAMS_WORKSPACE_DIR}:/root/manager-workspace\"\n\n # Pass host timezone to container so date/time commands reflect local time\n TZ_ARGS=\"-e TZ=${AGENTTEAMS_TIMEZONE}\"\n\n # Host directory mount\n if [ -d \"${AGENTTEAMS_HOST_SHARE_DIR}\" ]; then\n HOST_SHARE_MOUNT_ARGS=\"-v ${AGENTTEAMS_HOST_SHARE_DIR}:/host-share\"\n log \"$(msg host_share.sharing \"${AGENTTEAMS_HOST_SHARE_DIR}\")\"\n else\n log \"$(msg host_share.not_exist \"${AGENTTEAMS_HOST_SHARE_DIR}\")\"\n HOST_SHARE_MOUNT_ARGS=\"-v ${AGENTTEAMS_HOST_SHARE_DIR}:/host-share\"\n fi\n\n # YOLO mode: pass through if set in environment (enables autonomous decisions)\n YOLO_ARGS=\"\"\n if [ \"${AGENTTEAMS_YOLO:-}\" = \"1\" ]; then\n YOLO_ARGS=\"-e AGENTTEAMS_YOLO=1\"\n log \"$(msg install.yolo)\"\n fi\n\n # Matrix-plugin debug tracing: pass through if AGENTTEAMS_MATRIX_DEBUG=1.\n # The container entrypoints translate this to OPENCLAW_MATRIX_DEBUG=1\n # so the openclaw matrix plugin emits structured INFO-level lifecycle\n # traces (sync state transitions, room.invite/join, message handler\n # arrival + filter outcomes). Used to diagnose worker/manager hangs.\n MATRIX_DEBUG_ARGS=\"\"\n if [ \"${AGENTTEAMS_MATRIX_DEBUG:-}\" = \"1\" ]; then\n MATRIX_DEBUG_ARGS=\"-e AGENTTEAMS_MATRIX_DEBUG=1\"\n fi\n\n # E2EE is already in the env file; but also pass explicitly in case env file is not the source\n # (AGENTTEAMS_MATRIX_E2EE is already written to ENV_FILE above via --env-file)\n\n # Pull images (manager based on runtime config; all worker runtimes always pulled)\n _is_local_image() {\n case \"$1\" in\n agentteams/*) return 0 ;;\n *) return 1 ;;\n esac\n }\n\n _local_image_exists() {\n local _img=\"$1\"\n [ -z \"${_img}\" ] && return 1\n _is_local_image \"${_img}\" || return 1\n ${DOCKER_CMD} image inspect \"${_img}\" >/dev/null 2>&1\n }\n\n # Helper: pull or skip a single image\n # Args: $1=image $2=exists_msg_key $3=pulling_msg_key\n _pull_image() {\n local _img=\"$1\" _exists_key=\"$2\" _pull_key=\"$3\"\n [ -z \"${_img}\" ] && return 0\n if _local_image_exists \"${_img}\"; then\n log \"$(msg \"${_exists_key}\" \"${_img}\")\"\n return 0\n fi\n log \"$(msg \"${_pull_key}\" \"${_img}\")\"\n local _attempt=1\n while [ $_attempt -le 3 ]; do\n if ${DOCKER_CMD} pull \"${_img}\"; then\n return 0\n fi\n if [ $_attempt -lt 3 ]; then\n log \"Pull failed (attempt ${_attempt}/3), retrying in 5s...\"\n sleep 5\n fi\n _attempt=$((_attempt + 1))\n done\n die \"Failed to pull ${_img} after 3 attempts\"\n return 1\n }\n\n # Embedded controller image (resolve versioned tag, fallback to latest)\n resolve_embedded_image\n if [ \"${AGENTTEAMS_USE_EMBEDDED}\" = \"1\" ]; then\n _local_image_exists \"${EMBEDDED_IMAGE}\" || true\n fi\n\n # Manager image is always required (select based on runtime)\n if [ \"${AGENTTEAMS_MANAGER_RUNTIME}\" = \"copaw\" ]; then\n _pull_image \"${MANAGER_COPAW_IMAGE}\" \"install.image.exists\" \"install.image.pulling_manager\"\n else\n _pull_image \"${MANAGER_IMAGE}\" \"install.image.exists\" \"install.image.pulling_manager\"\n fi\n\n # Pull all worker runtime images (workers may use any runtime regardless of the default)\n _pull_image \"${WORKER_IMAGE}\" \"install.image.worker_exists\" \"install.image.pulling_worker\"\n _pull_image \"${COPAW_WORKER_IMAGE}\" \"install.image.worker_exists\" \"install.image.pulling_worker\"\n # Temporarily disabled until the QwenPaw worker image is published.\n # _pull_image \"${QWENPAW_WORKER_IMAGE}\" \"install.image.worker_exists\" \"install.image.pulling_worker\"\n _pull_image \"${HERMES_WORKER_IMAGE}\" \"install.image.worker_exists\" \"install.image.pulling_worker\"\n\n # --- Pre-upgrade: extract Matrix passwords from running old containers ---\n # Only needed when upgrading FROM old architecture (v1.0.9) TO embedded.\n # For new-arch-to-new-arch upgrades, credential files already exist with\n # correct room IDs — we must NOT overwrite them.\n _creds_tmp=\"\"\n if [ \"${AGENTTEAMS_UPGRADE:-0}\" = \"1\" ] && [ \"${AGENTTEAMS_USE_EMBEDDED}\" = \"1\" ]; then\n # Detect if upgrading from old arch: old arch has no agentteams-controller container\n # (or has it as a docker-proxy only, not embedded). Check if the existing\n # agentteams-controller is an embedded image (has supervisord) or just a proxy.\n _is_old_arch=0\n if ! ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -q \"^agentteams-controller$\"; then\n _is_old_arch=1\n elif ${DOCKER_CMD} ps -a --format '{{.Names}} {{.Image}}' 2>/dev/null | grep \"^agentteams-controller \" | grep -qv \"embedded\"; then\n _is_old_arch=1\n fi\n\n if [ \"${_is_old_arch}\" = \"1\" ]; then\n _creds_tmp=$(mktemp -d)\n\n # docker exec for Matrix/minio paths only works while agentteams-manager is running.\n _mgr_creds_tempstart=0\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -q '^agentteams-manager$'; then\n if ! ${DOCKER_CMD} ps --format '{{.Names}}' 2>/dev/null | grep -q '^agentteams-manager$'; then\n log \"agentteams-manager is stopped; starting it temporarily to extract Matrix credentials for upgrade...\"\n ${DOCKER_CMD} start agentteams-manager 2>/dev/null || true\n wait_matrix_ready \"agentteams-manager\"\n _mgr_creds_tempstart=1\n fi\n fi\n\n # Manager password (container Config.Env, then secrets file inside running manager, then data volume)\n _mgr_pw=$(${DOCKER_CMD} inspect agentteams-manager --format '{{range .Config.Env}}{{println .}}{{end}}' 2>/dev/null | grep '^AGENTTEAMS_MANAGER_PASSWORD=' | cut -d= -f2-)\n if [ -z \"${_mgr_pw}\" ] && ${DOCKER_CMD} ps --format '{{.Names}}' 2>/dev/null | grep -q '^agentteams-manager$'; then\n _mgr_pw=$(${DOCKER_CMD} exec agentteams-manager bash -c 'source /data/agentteams-secrets.env 2>/dev/null && echo \"${AGENTTEAMS_MANAGER_PASSWORD}\"' 2>/dev/null)\n fi\n if [ -z \"${_mgr_pw}\" ] && ${DOCKER_CMD} volume ls -q 2>/dev/null | grep -q \"^${AGENTTEAMS_DATA_DIR}$\"; then\n _mgr_pw=$(agentteams_read_secret_from_data_volume \"${AGENTTEAMS_DATA_DIR}\" AGENTTEAMS_MANAGER_PASSWORD)\n fi\n\n # Manager admin DM room ID: login as admin, find DM room with @manager\n _mgr_room=\"\"\n if [ -n \"${_mgr_pw}\" ]; then\n _admin_pw=$(grep AGENTTEAMS_ADMIN_PASSWORD \"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\" 2>/dev/null | cut -d= -f2-)\n _admin_user=$(grep AGENTTEAMS_ADMIN_USER \"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\" 2>/dev/null | cut -d= -f2-)\n _admin_user=\"${_admin_user:-admin}\"\n _matrix_domain=$(grep AGENTTEAMS_MATRIX_DOMAIN \"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\" 2>/dev/null | cut -d= -f2-)\n if [ -n \"${_admin_pw}\" ] && ${DOCKER_CMD} ps --format '{{.Names}}' 2>/dev/null | grep -q '^agentteams-manager$'; then\n _admin_token=$(${DOCKER_CMD} exec agentteams-manager curl -sf -X POST http://127.0.0.1:6167/_matrix/client/v3/login \\\n -H \"Content-Type: application/json\" \\\n -d '{\"type\":\"m.login.password\",\"identifier\":{\"type\":\"m.id.user\",\"user\":\"'\"${_admin_user}\"'\"},\"password\":\"'\"${_admin_pw}\"'\"}' 2>/dev/null | python3 -c \"import sys,json; print(json.load(sys.stdin).get('access_token',''))\" 2>/dev/null || true)\n if [ -n \"${_admin_token}\" ]; then\n _mgr_room=$(${DOCKER_CMD} exec agentteams-manager curl -sf -X GET \\\n -H \"Authorization: Bearer ${_admin_token}\" \\\n \"http://127.0.0.1:6167/_matrix/client/v3/joined_rooms\" 2>/dev/null | python3 -c \"\nimport sys,json,subprocess\nrooms = json.load(sys.stdin).get('joined_rooms',[])\nfor room_id in rooms:\n enc = room_id.replace('!','%21')\n members = json.loads(subprocess.check_output([\n 'docker','exec','agentteams-manager','curl','-sf','-X','GET',\n '-H','Authorization: Bearer ${_admin_token}',\n 'http://127.0.0.1:6167/_matrix/client/v3/rooms/'+enc+'/members'\n ]).decode()).get('chunk',[])\n member_ids = [m['state_key'] for m in members]\n if any('manager' in m and 'admin' not in m.split(':')[0] for m in member_ids):\n if len(member_ids) <= 3:\n print(room_id)\n break\n\" 2>/dev/null || true)\n fi\n fi\n if [ -z \"${_mgr_room}\" ]; then\n _mgr_room=$(agentteams_read_admin_dm_room_from_workspace \"${AGENTTEAMS_WORKSPACE_DIR}\")\n fi\n cat > \"${_creds_tmp}/default.env\" <<CREDEOF\nWORKER_PASSWORD=\"${_mgr_pw}\"\nWORKER_MINIO_PASSWORD=\"$(openssl rand -hex 24)\"\nWORKER_GATEWAY_KEY=\"${AGENTTEAMS_MANAGER_GATEWAY_KEY}\"\nWORKER_ROOM_ID=\"${_mgr_room}\"\nCREDEOF\n log \"Extracted Manager Matrix password${_mgr_room:+ and room ID}\"\n fi\n\n # Worker passwords and room IDs from workers-registry.json\n if [ -f \"${AGENTTEAMS_WORKSPACE_DIR}/workers-registry.json\" ]; then\n _worker_names=$(python3 -c \"import json; d=json.load(open('${AGENTTEAMS_WORKSPACE_DIR}/workers-registry.json')); print(' '.join(d.get('workers',{}).keys()))\" 2>/dev/null || true)\n for _wname in ${_worker_names}; do\n _wpw=\"\"\n if ${DOCKER_CMD} ps --format '{{.Names}}' 2>/dev/null | grep -q '^agentteams-manager$'; then\n _wpw=$(${DOCKER_CMD} exec agentteams-manager cat \"/root/agentteams-fs/agents/${_wname}/credentials/matrix/password\" 2>/dev/null || true)\n fi\n if [ -z \"${_wpw}\" ] && ${DOCKER_CMD} volume ls -q 2>/dev/null | grep -q \"^${AGENTTEAMS_DATA_DIR}$\"; then\n _wpw=$(agentteams_read_worker_creds_value_from_volume \"${AGENTTEAMS_DATA_DIR}\" \"${_wname}\" WORKER_PASSWORD)\n fi\n _wroom=$(python3 -c \"import json; d=json.load(open('${AGENTTEAMS_WORKSPACE_DIR}/workers-registry.json')); print(d.get('workers',{}).get('${_wname}',{}).get('room_id',''))\" 2>/dev/null || true)\n if [ -z \"${_wroom}\" ] && ${DOCKER_CMD} volume ls -q 2>/dev/null | grep -q \"^${AGENTTEAMS_DATA_DIR}$\"; then\n _wroom=$(agentteams_read_worker_creds_value_from_volume \"${AGENTTEAMS_DATA_DIR}\" \"${_wname}\" WORKER_ROOM_ID)\n fi\n if [ -n \"${_wpw}\" ]; then\n cat > \"${_creds_tmp}/${_wname}.env\" <<CREDEOF\nWORKER_PASSWORD=\"${_wpw}\"\nWORKER_MINIO_PASSWORD=\"$(openssl rand -hex 24)\"\nWORKER_GATEWAY_KEY=\"$(openssl rand -hex 32)\"\nWORKER_ROOM_ID=\"${_wroom}\"\nCREDEOF\n log \"Extracted ${_wname} Matrix password${_wroom:+ and room ID}\"\n fi\n done\n fi\n\n if [ \"${_mgr_creds_tempstart}\" = \"1\" ]; then\n log \"Stopping agentteams-manager after credential extraction (upgrade will recreate containers)...\"\n ${DOCKER_CMD} stop agentteams-manager 2>/dev/null || true\n fi\n fi # _is_old_arch\n fi\n\n # --- Stop and remove existing containers ---\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' | grep -q \"^agentteams-controller$\"; then\n ${DOCKER_CMD} stop agentteams-controller 2>/dev/null || true\n ${DOCKER_CMD} rm agentteams-controller 2>/dev/null || true\n fi\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' | grep -q \"^agentteams-manager$\"; then\n log \"$(msg install.removing_existing)\"\n ${DOCKER_CMD} stop agentteams-manager 2>/dev/null || true\n ${DOCKER_CMD} rm agentteams-manager 2>/dev/null || true\n fi\n\n # Stop and remove worker containers (controller will recreate via CR reconciliation)\n if [ -n \"${UPGRADE_EXISTING_WORKERS:-}\" ]; then\n log \"$(msg install.existing.stopping_workers)\"\n for w in ${UPGRADE_EXISTING_WORKERS}; do\n ${DOCKER_CMD} stop \"${w}\" 2>/dev/null || true\n ${DOCKER_CMD} rm \"${w}\" 2>/dev/null || true\n log \"$(msg install.existing.removed \"${w}\")\"\n done\n fi\n\n # Clean up legacy containers from v1.0.x and earlier\n for _legacy_name in agentteams-docker-proxy; do\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -qx \"${_legacy_name}\"; then\n log \"Removing legacy container: ${_legacy_name}\"\n ${DOCKER_CMD} stop \"${_legacy_name}\" 2>/dev/null || true\n ${DOCKER_CMD} rm -f \"${_legacy_name}\" 2>/dev/null || true\n fi\n done\n\n # --- Upgrade: inject extracted credentials into data volume ---\n # Only needed for old-arch upgrades (credential files were extracted above).\n if [ -n \"${_creds_tmp}\" ] && [ -d \"${_creds_tmp}\" ] && [ -n \"$(ls -A \"${_creds_tmp}\" 2>/dev/null)\" ]; then\n local _cleanup_ctr=\"agentteams-upgrade-cleanup\"\n ${DOCKER_CMD} rm -f \"${_cleanup_ctr}\" 2>/dev/null || true\n ${DOCKER_CMD} run --rm --name \"${_cleanup_ctr}\" \\\n --entrypoint sh \\\n -v \"${AGENTTEAMS_DATA_DIR}:/data\" \\\n -v \"${_creds_tmp}:/creds:ro\" \\\n \"${EMBEDDED_IMAGE}\" -c '\n rm -rf /data/worker-creds\n mkdir -p /data/worker-creds\n cp /creds/*.env /data/worker-creds/ 2>/dev/null || true\n chmod 600 /data/worker-creds/*.env 2>/dev/null || true\n ' 2>/dev/null && log \"Injected credentials for upgrade\" || log \"Warning: credential injection failed, continuing\"\n rm -rf \"${_creds_tmp}\"\n fi\n\n # --- Start containers ---\n log \"$(msg install.starting_manager)\"\n\n # Build port binding args\n if [ \"${AGENTTEAMS_LOCAL_ONLY:-1}\" = \"1\" ]; then\n _port_prefix=\"127.0.0.1:\"\n else\n _port_prefix=\"\"\n fi\n\n # Ensure agentteams-net Docker network exists\n ${DOCKER_CMD} network inspect agentteams-net >/dev/null 2>&1 || ${DOCKER_CMD} network create agentteams-net\n\n if [ \"${AGENTTEAMS_USE_EMBEDDED}\" != \"1\" ] && [ \"${AGENTTEAMS_UPGRADE:-0}\" = \"1\" ]; then\n # Check if current installation is embedded — downgrade to legacy is not supported\n if ${DOCKER_CMD} ps -a --format '{{.Names}} {{.Image}}' 2>/dev/null | grep \"^agentteams-controller \" | grep -q \"embedded\"; then\n error \"Downgrade from embedded architecture to legacy version (${AGENTTEAMS_VERSION}) is not supported.\"\n error \"Please use 'make uninstall-embedded' first, then do a clean install of the target version.\"\n exit 1\n fi\n fi\n\n if [ \"${AGENTTEAMS_USE_EMBEDDED}\" = \"1\" ]; then\n # ============================================================\n # New architecture: embedded controller + auto-created manager\n # ============================================================\n\n # Internal port: 8080 (Higress gateway inside the container).\n local _internal_gw_port=8080\n local _matrix_domain=\"${AGENTTEAMS_MATRIX_DOMAIN:-matrix-local.agentteams.io:${AGENTTEAMS_PORT_GATEWAY}}\"\n local _aigw_domain=\"${AGENTTEAMS_AI_GATEWAY_DOMAIN:-aigw-local.agentteams.io}\"\n # Ensure internal gateway port is present (container-internal traffic uses 8080)\n case \"${_aigw_domain}\" in *:*) ;; *) _aigw_domain=\"${_aigw_domain}:${_internal_gw_port}\" ;; esac\n local _fs_domain=\"${AGENTTEAMS_FS_DOMAIN:-fs-local.agentteams.io}\"\n case \"${_fs_domain}\" in *:*) ;; *) _fs_domain=\"${_fs_domain}:${_internal_gw_port}\" ;; esac\n\n # Controller env args\n local _ctrl_env_prefix\n _ctrl_env_prefix=\"$(_controller_env_prefix \"${AGENTTEAMS_VERSION}\")\"\n local _storage_prefix\n _storage_prefix=\"$(_controller_storage_prefix \"${AGENTTEAMS_VERSION}\")\"\n local _ctrl_env_args=(\n -e \"${_ctrl_env_prefix}ADMIN_USER=${AGENTTEAMS_ADMIN_USER}\"\n -e \"${_ctrl_env_prefix}ADMIN_PASSWORD=${AGENTTEAMS_ADMIN_PASSWORD}\"\n -e \"${_ctrl_env_prefix}MANAGER_PASSWORD=${AGENTTEAMS_MANAGER_PASSWORD}\"\n -e \"${_ctrl_env_prefix}REGISTRATION_TOKEN=${AGENTTEAMS_REGISTRATION_TOKEN}\"\n -e \"${_ctrl_env_prefix}MINIO_USER=${AGENTTEAMS_MINIO_USER}\"\n -e \"${_ctrl_env_prefix}MINIO_PASSWORD=${AGENTTEAMS_MINIO_PASSWORD}\"\n -e \"${_ctrl_env_prefix}LLM_PROVIDER=${AGENTTEAMS_LLM_PROVIDER}\"\n -e \"${_ctrl_env_prefix}LLM_API_KEY=${AGENTTEAMS_LLM_API_KEY}\"\n -e \"${_ctrl_env_prefix}DEFAULT_MODEL=${AGENTTEAMS_DEFAULT_MODEL}\"\n -e \"${_ctrl_env_prefix}MANAGER_GATEWAY_KEY=${AGENTTEAMS_MANAGER_GATEWAY_KEY}\"\n -e \"${_ctrl_env_prefix}MANAGER_RUNTIME=${AGENTTEAMS_MANAGER_RUNTIME:-copaw}\"\n -e \"${_ctrl_env_prefix}MANAGER_IMAGE=$([ \"${AGENTTEAMS_MANAGER_RUNTIME}\" = \"copaw\" ] && echo \"${MANAGER_COPAW_IMAGE}\" || echo \"${MANAGER_IMAGE}\")\"\n -e \"${_ctrl_env_prefix}DEFAULT_WORKER_RUNTIME=${AGENTTEAMS_DEFAULT_WORKER_RUNTIME:-copaw}\"\n -e \"${_ctrl_env_prefix}WORKER_IMAGE=${WORKER_IMAGE}\"\n -e \"${_ctrl_env_prefix}COPAW_WORKER_IMAGE=${COPAW_WORKER_IMAGE}\"\n -e \"${_ctrl_env_prefix}QWENPAW_WORKER_IMAGE=${QWENPAW_WORKER_IMAGE}\"\n -e \"${_ctrl_env_prefix}HERMES_WORKER_IMAGE=${HERMES_WORKER_IMAGE}\"\n -e \"${_ctrl_env_prefix}MATRIX_DOMAIN=${_matrix_domain}\"\n -e \"${_ctrl_env_prefix}ELEMENT_HOMESERVER_URL=http://127.0.0.1:${AGENTTEAMS_PORT_GATEWAY}\"\n -e \"${_ctrl_env_prefix}MATRIX_URL=http://127.0.0.1:6167\"\n -e \"${_ctrl_env_prefix}MATRIX_E2EE=${AGENTTEAMS_MATRIX_E2EE:-0}\"\n -e \"${_ctrl_env_prefix}MINIO_ENDPOINT=http://127.0.0.1:9000\"\n -e \"${_ctrl_env_prefix}MINIO_BUCKET=agentteams-storage\"\n -e \"${_ctrl_env_prefix}STORAGE_PREFIX=${_storage_prefix}\"\n -e \"${_ctrl_env_prefix}FS_ENDPOINT=http://127.0.0.1:9000\"\n -e \"${_ctrl_env_prefix}AI_GATEWAY_URL=http://${_aigw_domain}\"\n -e \"${_ctrl_env_prefix}CONTROLLER_URL=http://agentteams-controller:8090\"\n -e \"${_ctrl_env_prefix}DOCKER_NETWORK=agentteams-net\"\n -e \"${_ctrl_env_prefix}WORKSPACE_DIR=${AGENTTEAMS_WORKSPACE_DIR}\"\n -e \"${_ctrl_env_prefix}HOST_SHARE_DIR=${AGENTTEAMS_HOST_SHARE_DIR}\"\n -e \"${_ctrl_env_prefix}MANAGER_ENABLED=true\"\n -e \"${_ctrl_env_prefix}PORT_MANAGER_CONSOLE=${AGENTTEAMS_PORT_MANAGER_CONSOLE:-18888}\"\n )\n if _use_legacy_image_env \"${AGENTTEAMS_VERSION}\"; then\n _ctrl_env_args+=(\n -e \"${_ctrl_env_prefix}FS_BUCKET=agentteams-storage\"\n -e \"${_ctrl_env_prefix}RESOURCE_PREFIX=agentteams-\"\n )\n else\n _ctrl_env_args+=(\n -e \"${_ctrl_env_prefix}MATRIX_APPSERVICE_ENABLED=${AGENTTEAMS_MATRIX_APPSERVICE_ENABLED:-true}\"\n -e \"${_ctrl_env_prefix}MATRIX_APPSERVICE_AS_TOKEN=${AGENTTEAMS_MATRIX_APPSERVICE_AS_TOKEN:-}\"\n -e \"${_ctrl_env_prefix}MATRIX_APPSERVICE_HS_TOKEN=${AGENTTEAMS_MATRIX_APPSERVICE_HS_TOKEN:-}\"\n )\n fi\n\n # Timezone\n if [ -n \"${AGENTTEAMS_TIMEZONE:-}\" ]; then\n _ctrl_env_args+=(-e \"TZ=${AGENTTEAMS_TIMEZONE}\")\n fi\n\n # Yolo mode\n if [ \"${AGENTTEAMS_YOLO:-}\" = \"1\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}YOLO=1\")\n fi\n\n # Matrix-plugin debug tracing — propagated to every manager + worker\n # the controller spawns, then translated to OPENCLAW_MATRIX_DEBUG=1\n # by the container entrypoints. Use this to diagnose\n # \"worker did not join\" / \"manager replied empty\" hangs.\n if [ \"${AGENTTEAMS_MATRIX_DEBUG:-}\" = \"1\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}MATRIX_DEBUG=1\")\n fi\n\n # Optional: GitHub token\n if [ -n \"${AGENTTEAMS_GITHUB_TOKEN:-}\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}GITHUB_TOKEN=${AGENTTEAMS_GITHUB_TOKEN}\")\n fi\n\n # Optional: embedding model\n if [ -n \"${AGENTTEAMS_EMBEDDING_MODEL:-}\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}EMBEDDING_MODEL=${AGENTTEAMS_EMBEDDING_MODEL}\")\n fi\n\n # Optional: OpenAI-compatible base URL\n if [ -n \"${AGENTTEAMS_OPENAI_BASE_URL:-}\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}OPENAI_BASE_URL=${AGENTTEAMS_OPENAI_BASE_URL}\")\n fi\n # Optional: language\n if [ -n \"${AGENTTEAMS_LANGUAGE:-}\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}LANGUAGE=${AGENTTEAMS_LANGUAGE}\")\n fi\n\n # Optional: CMS/ARMS observability. In embedded mode the controller\n # spawns the Manager and Workers, so it must receive these settings.\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}CMS_TRACES_ENABLED=${AGENTTEAMS_CMS_TRACES_ENABLED:-false}\")\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}CMS_SERVICE_NAME=${AGENTTEAMS_CMS_SERVICE_NAME:-agentteams-manager}\")\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}CMS_METRICS_ENABLED=${AGENTTEAMS_CMS_METRICS_ENABLED:-false}\")\n if [ -n \"${AGENTTEAMS_CMS_ENDPOINT:-}\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}CMS_ENDPOINT=${AGENTTEAMS_CMS_ENDPOINT}\")\n fi\n if [ -n \"${AGENTTEAMS_CMS_LICENSE_KEY:-}\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}CMS_LICENSE_KEY=${AGENTTEAMS_CMS_LICENSE_KEY}\")\n fi\n if [ -n \"${AGENTTEAMS_CMS_PROJECT:-}\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}CMS_PROJECT=${AGENTTEAMS_CMS_PROJECT}\")\n fi\n if [ -n \"${AGENTTEAMS_CMS_WORKSPACE:-}\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}CMS_WORKSPACE=${AGENTTEAMS_CMS_WORKSPACE}\")\n fi\n\n # shellcheck disable=SC2086\n ${DOCKER_CMD} run -d \\\n --name agentteams-controller \\\n --network agentteams-net \\\n --network-alias matrix-local.agentteams.io \\\n --network-alias aigw-local.agentteams.io \\\n --network-alias fs-local.agentteams.io \\\n \"${_ctrl_env_args[@]}\" \\\n -v \"${CONTAINER_SOCK}:/var/run/docker.sock\" \\\n --security-opt label=disable \\\n -v \"${AGENTTEAMS_DATA_DIR}:/data\" \\\n -v \"${AGENTTEAMS_WORKSPACE_DIR}:/root/agentteams-fs/agents/manager\" \\\n -p \"${_port_prefix}${AGENTTEAMS_PORT_GATEWAY}:8080\" \\\n -p \"${_port_prefix}${AGENTTEAMS_PORT_CONSOLE}:8001\" \\\n -p \"${_port_prefix}${AGENTTEAMS_PORT_ELEMENT_WEB:-18088}:8088\" \\\n --restart unless-stopped \\\n \"${EMBEDDED_IMAGE}\"\n\n log \"Embedded controller started: agentteams-controller\"\n\n # Wait for infrastructure inside the controller container\n _wait_for_url() {\n local url=\"$1\" ctr=\"$2\" max_wait=\"${3:-120}\" desc=\"${4:-service}\"\n local elapsed=0\n log \"Waiting for ${desc}...\"\n while [ $elapsed -lt $max_wait ]; do\n if ${DOCKER_CMD} exec \"${ctr}\" curl -sf \"${url}\" >/dev/null 2>&1; then\n log \"${desc} is ready (${elapsed}s)\"\n return 0\n fi\n sleep 2\n elapsed=$((elapsed + 2))\n done\n log \"ERROR: ${desc} not ready after ${max_wait}s\"\n return 1\n }\n\n _wait_for_url \"http://127.0.0.1:6167/_tuwunel/server_version\" agentteams-controller 120 \"Tuwunel (Matrix)\" || exit 1\n _wait_for_url \"http://127.0.0.1:9000/minio/health/live\" agentteams-controller 60 \"MinIO\" || exit 1\n _wait_for_url \"http://127.0.0.1:8080/status\" agentteams-controller 120 \"Higress Gateway\" || exit 1\n\n # Wait for controller to create Manager Agent container\n log \"Waiting for Manager Agent container...\"\n local _mgr_wait=0\n local _mgr_max=300\n while [ $_mgr_wait -lt $_mgr_max ]; do\n if ${DOCKER_CMD} ps --format '{{.Names}}' 2>/dev/null | grep -q \"^agentteams-manager$\"; then\n log \"Manager Agent container detected (${_mgr_wait}s)\"\n break\n fi\n sleep 3\n _mgr_wait=$((_mgr_wait + 3))\n done\n if [ $_mgr_wait -ge $_mgr_max ]; then\n log \"ERROR: Manager Agent container not created after ${_mgr_max}s\"\n log \"Controller logs:\"\n ${DOCKER_CMD} exec agentteams-controller tail -30 /var/log/agentteams/agentteams-controller-error.log 2>/dev/null || true\n exit 1\n fi\n\n # Wait for Manager Agent to be running\n log \"Waiting for Manager Agent to start...\"\n local _agent_wait=0\n while [ $_agent_wait -lt 120 ]; do\n local _state\n _state=$(${DOCKER_CMD} inspect --format '{{.State.Status}}' agentteams-manager 2>/dev/null || echo \"missing\")\n if [ \"${_state}\" = \"running\" ]; then\n log \"Manager Agent is running\"\n break\n fi\n sleep 2\n _agent_wait=$((_agent_wait + 2))\n done\n\n # Enable yolo mode in agent if requested\n if [ \"${AGENTTEAMS_YOLO:-}\" = \"1\" ]; then\n ${DOCKER_CMD} exec agentteams-manager touch /root/manager-workspace/yolo-mode 2>/dev/null || true\n fi\n\n # Wait for the controller to send the first-boot welcome message.\n # The controller gates this on (a) Manager joining the DM room and\n # (b) Higress WASM key-auth propagation actually clearing /v1/chat/completions\n # for the Manager's gateway key — typically ~45-90s on a fresh install.\n # We poll Manager CR Status.WelcomeSent via the in-container agt CLI,\n # exec'd inside agentteams-controller (the source-of-truth container — its\n # bundled CLI binary is always in lockstep with whatever controller\n # binary is currently serving the HTTP API, since they're the same\n # `go build` output. The agentteams-manager container's CLI may lag the\n # controller across image upgrades and silently drop the welcomeSent\n # field, leaving this loop hung). The controller container mints a\n # long-lived admin SA token at startup and writes it to\n # AGENTTEAMS_AUTH_TOKEN_FILE=/var/run/agentteams/cli-token (set as a Dockerfile\n # ENV default), so a bare `docker exec agentteams-controller agt …`\n # auto-discovers both the endpoint and the token. There is a brief\n # window after container start before bootstrapAdminCLIToken completes\n # where the file may be empty / absent — the loop's silent retry\n # handles that the same way it handles the manager-not-yet-running\n # case below.\n if ${DOCKER_CMD} exec agentteams-controller sh -c 'command -v agt' >/dev/null 2>&1; then\n log \"$(msg install.welcome_msg.waiting)\"\n local _welcome_wait=0\n local _welcome_max=\"${AGENTTEAMS_WELCOME_TIMEOUT:-300}\"\n local _welcome_done=0\n while [ $_welcome_wait -lt $_welcome_max ]; do\n # `tr -d` strips whitespace/CR so the grep stays robust to\n # any future change in go-json field ordering or formatting.\n local _wjson\n _wjson=$(${DOCKER_CMD} exec agentteams-controller \\\n agt get managers default -o json 2>/dev/null || true)\n if [ -n \"${_wjson}\" ] && printf '%s' \"${_wjson}\" | tr -d ' \\r\\n' | grep -q '\"welcomeSent\":true'; then\n log \"$(msg install.welcome_msg.confirmed \"${_welcome_wait}\")\"\n _welcome_done=1\n break\n fi\n sleep 3\n _welcome_wait=$((_welcome_wait + 3))\n done\n if [ $_welcome_done -ne 1 ]; then\n # Non-fatal: install is still good. Keep going to the success\n # banner so the admin can use Element Web to nudge Manager into\n # onboarding manually (one DM message is enough).\n log \"$(msg install.welcome_msg.timeout \"${_welcome_max}\")\"\n log \"$(msg install.welcome_msg.timeout_hint)\"\n log \"$(msg install.welcome_msg.timeout_inspect)\"\n fi\n else\n log \"$(msg install.welcome_msg.poll_unavailable)\"\n fi\n\n else\n # ============================================================\n # Legacy architecture: all-in-one manager container\n # ============================================================\n\n NETWORK_ARGS=\"--network agentteams-net\"\n NETWORK_ALIAS_ARGS=\"--network-alias matrix-local.agentteams.io --network-alias aigw-local.agentteams.io --network-alias fs-local.agentteams.io\"\n for _domain in \"${AGENTTEAMS_MATRIX_CLIENT_DOMAIN:-}\" \"${AGENTTEAMS_CONSOLE_DOMAIN:-}\"; do\n if [ -n \"${_domain}\" ] && [[ \"${_domain}\" == *-local.agentteams.io ]]; then\n NETWORK_ALIAS_ARGS=\"${NETWORK_ALIAS_ARGS} --network-alias ${_domain}\"\n fi\n done\n\n # Start Docker API proxy if enabled (security layer between Manager and Docker daemon)\n PROXY_ARGS=\"\"\n if [ \"${AGENTTEAMS_DOCKER_PROXY:-1}\" = \"1\" ] && [ -n \"${CONTAINER_SOCK:-}\" ]; then\n local _proxy_image=\"${AGENTTEAMS_REGISTRY}/higress/agentteams-docker-proxy:${AGENTTEAMS_VERSION}\"\n # Try versioned tag, fallback to latest\n if ! ${DOCKER_CMD} image inspect \"${_proxy_image}\" >/dev/null 2>&1; then\n ${DOCKER_CMD} pull \"${_proxy_image}\" 2>/dev/null || {\n _proxy_image=\"${AGENTTEAMS_REGISTRY}/higress/agentteams-docker-proxy:latest\"\n ${DOCKER_CMD} pull \"${_proxy_image}\" 2>/dev/null || true\n }\n fi\n if ${DOCKER_CMD} image inspect \"${_proxy_image}\" >/dev/null 2>&1; then\n log \"Starting Docker API proxy...\"\n ${DOCKER_CMD} run -d \\\n --name agentteams-docker-proxy \\\n --network agentteams-net \\\n -v \"${CONTAINER_SOCK}:/var/run/docker.sock\" \\\n --security-opt label=disable \\\n -e AGENTTEAMS_WORKER_IMAGE=\"${WORKER_IMAGE}\" \\\n -e AGENTTEAMS_COPAW_WORKER_IMAGE=\"${COPAW_WORKER_IMAGE}\" \\\n -e AGENTTEAMS_QWENPAW_WORKER_IMAGE=\"${QWENPAW_WORKER_IMAGE}\" \\\n -e AGENTTEAMS_HERMES_WORKER_IMAGE=\"${HERMES_WORKER_IMAGE}\" \\\n ${AGENTTEAMS_PROXY_ALLOWED_REGISTRIES:+-e AGENTTEAMS_PROXY_ALLOWED_REGISTRIES=\"${AGENTTEAMS_PROXY_ALLOWED_REGISTRIES}\"} \\\n --restart unless-stopped \\\n \"${_proxy_image}\"\n PROXY_ARGS=\"-e AGENTTEAMS_CONTROLLER_URL=http://agentteams-docker-proxy:2375 -e AGENTTEAMS_CONTAINER_API=http://agentteams-docker-proxy:2375\"\n SOCKET_MOUNT_ARGS=\"\"\n fi\n fi\n\n # Pass host timezone to container\n TZ_ARGS=\"\"\n if [ -n \"${AGENTTEAMS_TIMEZONE:-}\" ]; then\n TZ_ARGS=\"-e TZ=${AGENTTEAMS_TIMEZONE}\"\n fi\n\n YOLO_ARGS=\"\"\n if [ \"${AGENTTEAMS_YOLO:-}\" = \"1\" ]; then\n YOLO_ARGS=\"-e AGENTTEAMS_YOLO=1\"\n fi\n\n MATRIX_DEBUG_ARGS=\"\"\n if [ \"${AGENTTEAMS_MATRIX_DEBUG:-}\" = \"1\" ]; then\n MATRIX_DEBUG_ARGS=\"-e AGENTTEAMS_MATRIX_DEBUG=1\"\n fi\n\n # shellcheck disable=SC2086\n ${DOCKER_CMD} run -d \\\n --name agentteams-manager \\\n --env-file \"${ENV_FILE}\" \\\n -e HOME=/root/manager-workspace \\\n -w /root/manager-workspace \\\n -e HOST_ORIGINAL_HOME=\"${AGENTTEAMS_HOST_SHARE_DIR}\" \\\n -e AGENTTEAMS_MANAGER_RUNTIME=\"${AGENTTEAMS_MANAGER_RUNTIME:-copaw}\" \\\n ${JVM_ARGS:+-e JVM_ARGS=\"${JVM_ARGS}\"} \\\n ${YOLO_ARGS} \\\n ${MATRIX_DEBUG_ARGS} \\\n ${TZ_ARGS} \\\n ${SOCKET_MOUNT_ARGS} \\\n ${NETWORK_ARGS} \\\n ${NETWORK_ALIAS_ARGS} \\\n ${PROXY_ARGS} \\\n -p \"${_port_prefix}${AGENTTEAMS_PORT_GATEWAY}:8080\" \\\n -p \"${_port_prefix}${AGENTTEAMS_PORT_CONSOLE}:8001\" \\\n -p \"${_port_prefix}${AGENTTEAMS_PORT_ELEMENT_WEB:-18088}:8088\" \\\n -p \"127.0.0.1:${AGENTTEAMS_PORT_MANAGER_CONSOLE:-18888}:18888\" \\\n ${DATA_MOUNT_ARGS} \\\n ${WORKSPACE_MOUNT_ARGS} \\\n ${HOST_SHARE_MOUNT_ARGS} \\\n --restart unless-stopped \\\n \"$([ \"${AGENTTEAMS_MANAGER_RUNTIME}\" = \"copaw\" ] && echo \"${MANAGER_COPAW_IMAGE}\" || echo \"${MANAGER_IMAGE}\")\"\n\n # Wait for Manager agent to be ready\n wait_manager_ready \"agentteams-manager\"\n\n # Wait for Matrix server to be ready\n wait_matrix_ready \"agentteams-manager\"\n\n # Post-install verification (non-fatal: warnings only)\n local _verify_script\n _verify_script=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)/agentteams-verify.sh\"\n if [ -f \"${_verify_script}\" ]; then\n bash \"${_verify_script}\" \"agentteams-manager\" || {\n log \"WARNING: Some post-install checks failed. Re-run: bash install/agentteams-verify.sh\"\n }\n fi\n fi\n unset _port_prefix\n\n # ── Start Dashboard (optional) ────────────────────────────────────\n _start_dashboard\n\n # Apply Podman autostart if selected and environment matches\n if [ \"${DOCKER_CMD}\" = \"podman\" ] && [ \"${AGENTTEAMS_PODMAN_AUTOSTART:-0}\" = \"1\" ]; then\n setup_podman_autostart\n fi\n\n log \"\"\n log \"$(msg success.title)\"\n log \"\"\n log \"$(msg success.domains_configured)\"\n log \" ${AGENTTEAMS_MATRIX_DOMAIN%%:*} ${AGENTTEAMS_MATRIX_CLIENT_DOMAIN} ${AGENTTEAMS_AI_GATEWAY_DOMAIN} ${AGENTTEAMS_FS_DOMAIN} ${AGENTTEAMS_CONSOLE_DOMAIN}\"\n log \"\"\n local lan_ip\n lan_ip=$(detect_lan_ip)\n echo -e \"\\033[33m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\033[0m\"\n echo -e \"\\033[33m $(msg success.open_url)\\033[0m\"\n echo -e \"\\033[33m \\033[0m\"\n echo -e \"\\033[1;36m http://127.0.0.1:${AGENTTEAMS_PORT_ELEMENT_WEB:-18088}/#/login\\033[0m\"\n echo -e \"\\033[33m \\033[0m\"\n echo -e \"\\033[33m $(msg success.login_with)\\033[0m\"\n echo -e \"\\033[33m $(msg success.username \"${AGENTTEAMS_ADMIN_USER}\")\\033[0m\"\n echo -e \"\\033[33m $(msg success.password \"${AGENTTEAMS_ADMIN_PASSWORD}\")\\033[0m\"\n echo -e \"\\033[33m \\033[0m\"\n echo -e \"\\033[33m $(msg success.after_login)\\033[0m\"\n echo -e \"\\033[33m $(msg success.tell_it)\\033[0m\"\n echo -e \"\\033[33m $(msg success.manager_auto)\\033[0m\"\n echo -e \"\\033[33m \\033[0m\"\n echo -e \"\\033[33m ───────────────────────────────────────────────────────────────────────────── \\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_title)\\033[0m\"\n echo -e \"\\033[33m \\033[0m\"\n if [ -n \"${lan_ip}\" ]; then\n echo -e \"\\033[33m $(msg success.mobile_step1)\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_step2 \"http://${lan_ip}:${AGENTTEAMS_PORT_GATEWAY}\")\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_step3)\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_username \"${AGENTTEAMS_ADMIN_USER}\")\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_password \"${AGENTTEAMS_ADMIN_PASSWORD}\")\\033[0m\"\n else\n echo -e \"\\033[33m $(msg success.mobile_step1)\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_step2_noip \"${AGENTTEAMS_PORT_GATEWAY}\")\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_noip_hint)\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_step3)\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_username \"${AGENTTEAMS_ADMIN_USER}\")\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_password \"${AGENTTEAMS_ADMIN_PASSWORD}\")\\033[0m\"\n fi\n echo -e \"\\033[33m \\033[0m\"\n echo -e \"\\033[33m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\033[0m\"\n log \"\"\n log \"$(msg success.other_consoles)\"\n log \"$(msg success.higress_console \"${AGENTTEAMS_PORT_CONSOLE}\" \"${AGENTTEAMS_ADMIN_USER}\" \"${AGENTTEAMS_ADMIN_PASSWORD}\")\"\n if [ \"${AGENTTEAMS_DASHBOARD:-0}\" = \"1\" ]; then\n log \"$(msg success.dashboard \"${AGENTTEAMS_PORT_DASHBOARD:-13000}\")\"\n fi\n if [ \"${AGENTTEAMS_USE_EMBEDDED}\" != \"1\" ]; then\n log \"$(msg success.manager_console \"${AGENTTEAMS_PORT_MANAGER_CONSOLE:-18888}\")\"\n log \"$(msg success.manager_console_gateway \"${AGENTTEAMS_ADMIN_USER}\" \"${AGENTTEAMS_ADMIN_PASSWORD}\")\"\n fi\n log \"\"\n log \"$(msg success.switch_llm.title)\"\n log \"$(msg success.switch_llm.hint)\"\n log \"$(msg success.switch_llm.docs)\"\n log \"$(msg success.switch_llm.url)\"\n log \"\"\n log \"$(msg success.tip)\"\n log \"\"\n if [ \"${AGENTTEAMS_LOCAL_ONLY:-1}\" != \"1\" ]; then\n echo -e \"\\033[33m$(msg port.local_only.https_hint)\\033[0m\"\n log \"\"\n fi\n log \"$(msg success.config_file \"${ENV_FILE}\")\"\n log \"$(msg success.data_volume \"${AGENTTEAMS_DATA_DIR}\")\"\n log \"$(msg success.workspace \"${AGENTTEAMS_WORKSPACE_DIR}\")\"\n}\n\n# ============================================================\n# Worker Installation (One-Click)\n# ============================================================\n\ninstall_worker() {\n local WORKER_NAME=\"\"\n local FS=\"\"\n local FS_KEY=\"\"\n local FS_SECRET=\"\"\n local RESET=false\n local SKILLS_API_URL=\"\"\n\n # Parse arguments\n while [ $# -gt 0 ]; do\n case $1 in\n --name) WORKER_NAME=\"$2\"; shift 2 ;;\n --fs) FS=\"$2\"; shift 2 ;;\n --fs-key) FS_KEY=\"$2\"; shift 2 ;;\n --fs-secret) FS_SECRET=\"$2\"; shift 2 ;;\n --skills-api-url) SKILLS_API_URL=\"$2\"; shift 2 ;;\n --reset) RESET=true; shift ;;\n *) die \"$(msg error.unknown_option \"$1\")\" ;;\n esac\n done\n\n # Validate required params\n [ -z \"${WORKER_NAME}\" ] && die \"$(msg error.name_required)\"\n [ -z \"${FS}\" ] && die \"$(msg error.fs_required)\"\n [ -z \"${FS_KEY}\" ] && die \"$(msg error.fs_key_required)\"\n [ -z \"${FS_SECRET}\" ] && die \"$(msg error.fs_secret_required)\"\n\n local CONTAINER_NAME=\"agentteams-worker-${WORKER_NAME}\"\n\n # Handle reset\n if [ \"${RESET}\" = true ]; then\n log \"$(msg worker.resetting \"${WORKER_NAME}\")\"\n ${DOCKER_CMD} stop \"${CONTAINER_NAME}\" 2>/dev/null || true\n ${DOCKER_CMD} rm \"${CONTAINER_NAME}\" 2>/dev/null || true\n fi\n\n # Check for existing container\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' | grep -q \"^${CONTAINER_NAME}$\"; then\n die \"$(msg worker.exists \"${CONTAINER_NAME}\")\"\n fi\n\n log \"$(msg worker.starting \"${WORKER_NAME}\")\"\n\n # Build docker run args\n local DOCKER_ENV=\"\"\n DOCKER_ENV=\"${DOCKER_ENV} -e HOME=/root/agentteams-fs/agents/${WORKER_NAME}\"\n DOCKER_ENV=\"${DOCKER_ENV} -w /root/agentteams-fs/agents/${WORKER_NAME}\"\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_WORKER_NAME=${WORKER_NAME}\"\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_FS_ENDPOINT=${FS}\"\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_FS_ACCESS_KEY=${FS_KEY}\"\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_FS_SECRET_KEY=${FS_SECRET}\"\n\n if [ -z \"${SKILLS_API_URL}\" ]; then\n if [ -n \"${AGENTTEAMS_SKILLS_API_URL:-}\" ]; then\n SKILLS_API_URL=\"${AGENTTEAMS_SKILLS_API_URL}\"\n else\n SKILLS_API_URL=\"nacos://market.agentteams.io:80/public\"\n fi\n fi\n\n # Add SKILLS_API_URL if specified\n DOCKER_ENV=\"${DOCKER_ENV} -e SKILLS_API_URL=${SKILLS_API_URL}\"\n log \"$(msg worker.skills_url \"${SKILLS_API_URL}\")\"\n if [ -n \"${AGENTTEAMS_NACOS_USERNAME:-}\" ]; then\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_NACOS_USERNAME=${AGENTTEAMS_NACOS_USERNAME}\"\n fi\n if [ -n \"${AGENTTEAMS_NACOS_PASSWORD:-}\" ]; then\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_NACOS_PASSWORD=${AGENTTEAMS_NACOS_PASSWORD}\"\n fi\n if [ -n \"${AGENTTEAMS_NACOS_TOKEN:-}\" ]; then\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_NACOS_TOKEN=${AGENTTEAMS_NACOS_TOKEN}\"\n fi\n\n # shellcheck disable=SC2086\n ${DOCKER_CMD} run -d \\\n --name \"${CONTAINER_NAME}\" \\\n ${DOCKER_ENV} \\\n --restart unless-stopped \\\n \"${WORKER_IMAGE}\"\n\n log \"\"\n log \"$(msg worker.started \"${WORKER_NAME}\")\"\n log \"$(msg worker.container \"${CONTAINER_NAME}\")\"\n log \"$(msg worker.view_logs \"${CONTAINER_NAME}\")\"\n}\n\n# ============================================================\n# Main\n# ============================================================\n\n# ============================================================\n# LLM API connectivity test\n# ============================================================\n\ntest_llm_connectivity() {\n local base_url=\"$1\"\n local api_key=\"$2\"\n local model=\"$3\"\n local hint=\"${4:-}\" # optional: extra hint shown on failure\n if ! command -v curl >/dev/null 2>&1; then\n echo -e \"\\033[33m$(msg llm.openai.test.no_curl)\\033[0m\"\n return\n fi\n log \"$(msg llm.openai.test.testing)\"\n local _body _http_code _tmpfile\n _tmpfile=$(mktemp)\n _http_code=$(curl -s -o \"${_tmpfile}\" -w \"%{http_code}\" \\\n -X POST \"${base_url%/}/chat/completions\" \\\n -H \"Authorization: Bearer ${api_key}\" \\\n -H \"Content-Type: application/json\" \\\n -H \"User-Agent: AgentTeams/${AGENTTEAMS_VERSION:-latest}\" \\\n --max-time 30 \\\n -d \"{\\\"model\\\":\\\"${model}\\\",\\\"messages\\\":[{\\\"role\\\":\\\"user\\\",\\\"content\\\":\\\"hi\\\"}]}\" \\\n 2>/dev/null)\n _body=$(cat \"${_tmpfile}\")\n rm -f \"${_tmpfile}\"\n if [ \"${_http_code}\" = \"200\" ] || [ \"${_http_code}\" = \"201\" ]; then\n log \"$(msg llm.openai.test.ok)\"\n else\n echo -e \"\\033[33m$(msg llm.openai.test.fail \"${_http_code}\" \"${_body}\")\\033[0m\"\n if [ -n \"${hint}\" ]; then\n echo -e \"\\033[33m${hint}\\033[0m\"\n fi\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" != \"1\" ]; then\n local _confirm\n read -e -p \"$(msg llm.openai.test.confirm)\" _confirm\n if [ \"${_confirm}\" = \"b\" ]; then\n STEP_RESULT=\"back\"\n return 1\n fi\n if [ \"${_confirm}\" != \"y\" ] && [ \"${_confirm}\" != \"Y\" ]; then\n log \"$(msg llm.openai.test.aborted)\"\n exit 1\n fi\n fi\n fi\n}\n\ntest_embedding_connectivity() {\n local base_url=\"$1\"\n local api_key=\"$2\"\n local model=\"$3\"\n if ! command -v curl >/dev/null 2>&1; then\n return 0\n fi\n log \"$(msg llm.embedding.test.testing)\"\n local _body _http_code _tmpfile\n _tmpfile=$(mktemp)\n _http_code=$(curl -s -o \"${_tmpfile}\" -w \"%{http_code}\" \\\n -X POST \"${base_url%/}/embeddings\" \\\n -H \"Authorization: Bearer ${api_key}\" \\\n -H \"Content-Type: application/json\" \\\n -H \"User-Agent: AgentTeams/${AGENTTEAMS_VERSION:-latest}\" \\\n --max-time 30 \\\n -d \"{\\\"model\\\":\\\"${model}\\\",\\\"input\\\":\\\"test\\\"}\" \\\n 2>/dev/null)\n _body=$(cat \"${_tmpfile}\")\n rm -f \"${_tmpfile}\"\n if [ \"${_http_code}\" = \"200\" ] || [ \"${_http_code}\" = \"201\" ]; then\n log \"$(msg llm.embedding.test.ok)\"\n return 0\n else\n echo -e \"\\033[33m$(msg llm.embedding.test.fail \"${_http_code}\" \"${_body}\")\\033[0m\"\n return 1\n fi\n}\n\n# ============================================================\n# Uninstall\n# ============================================================\n\nuninstall_agentteams() {\n log \"$(msg uninstall.title)\"\n\n # Safely disable and remove the dedicated AgentTeams Podman autostart service\n if [ \"${DOCKER_CMD}\" = \"podman\" ] && command -v systemctl >/dev/null 2>&1; then\n local _systemctl_cmd=\"systemctl\"\n local _service_dir=\"/etc/systemd/system\"\n\n if [ \"$(id -u)\" != \"0\" ]; then\n _systemctl_cmd=\"systemctl --user\"\n _service_dir=\"${HOME}/.config/systemd/user\"\n fi\n\n if [ -f \"${_service_dir}/agentteams-podman-restart.service\" ]; then\n log \"Disabling and removing dedicated AgentTeams Podman autostart service...\"\n ${_systemctl_cmd} disable --now agentteams-podman-restart.service 2>/dev/null || true\n rm -f \"${_service_dir}/agentteams-podman-restart.service\"\n ${_systemctl_cmd} daemon-reload 2>/dev/null || true\n fi\n # Note: Native podman-restart.service is strictly untouched per maintainer review.\n fi\n\n # Capture manager image before removing (needed for workspace cleanup on Linux)\n local manager_image=\"\"\n manager_image=$(${DOCKER_CMD} inspect agentteams-manager --format '{{.Config.Image}}' 2>/dev/null || true)\n\n # Stop and remove manager\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -q \"^agentteams-manager$\"; then\n log \"$(msg uninstall.stopping_manager)\"\n ${DOCKER_CMD} stop agentteams-manager >/dev/null 2>&1 || true\n ${DOCKER_CMD} rm agentteams-manager >/dev/null 2>&1 || true\n fi\n\n # Stop and remove workers\n local workers\n workers=$(${DOCKER_CMD} ps -a --filter \"name=agentteams-worker-\" --format '{{.Names}}' 2>/dev/null || true)\n if [ -n \"${workers}\" ]; then\n log \"$(msg uninstall.stopping_workers)\"\n echo \"${workers}\" | while read -r w; do\n ${DOCKER_CMD} rm -f \"${w}\" >/dev/null 2>&1 || true\n log \"$(msg uninstall.removed \"${w}\")\"\n done\n fi\n\n # Stop and remove the dashboard container (installed via step_dashboard)\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -q \"^agentteams-dashboard$\"; then\n log \"Stopping and removing agentteams-dashboard...\"\n ${DOCKER_CMD} stop agentteams-dashboard >/dev/null 2>&1 || true\n ${DOCKER_CMD} rm agentteams-dashboard >/dev/null 2>&1 || true\n fi\n if ${DOCKER_CMD} volume inspect agentteams-dashboard-data >/dev/null 2>&1; then\n log \"Removing dashboard data volume: agentteams-dashboard-data\"\n ${DOCKER_CMD} volume rm agentteams-dashboard-data >/dev/null 2>&1 || true\n fi\n\n # Stop and remove docker-proxy (legacy ≤ v1.0.x; current arch uses\n # agentteams-controller for the same role)\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -q \"^agentteams-docker-proxy$\"; then\n log \"$(msg uninstall.removing_proxy)\"\n ${DOCKER_CMD} stop agentteams-docker-proxy >/dev/null 2>&1 || true\n ${DOCKER_CMD} rm agentteams-docker-proxy >/dev/null 2>&1 || true\n fi\n\n # Stop and remove the embedded controller container. MUST happen\n # before the `docker volume rm agentteams-data` step below — in embedded\n # mode agentteams-controller mounts agentteams-data at /data (Tuwunel DB,\n # MinIO state, Higress state, all the room messages), and `volume\n # rm` against an in-use volume fails silently because of the trailing\n # `|| true`. Skipping this used to leave room/message history behind\n # across \"uninstall + reinstall\" cycles. See PR #692.\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -q \"^agentteams-controller$\"; then\n log \"$(msg uninstall.stopping_controller)\"\n ${DOCKER_CMD} stop agentteams-controller >/dev/null 2>&1 || true\n ${DOCKER_CMD} rm agentteams-controller >/dev/null 2>&1 || true\n fi\n\n # Read env file for data/workspace info before removing\n local env_file=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n [ ! -f \"${env_file}\" ] && [ -f \"./agentteams-manager.env\" ] && env_file=\"./agentteams-manager.env\"\n\n local data_dir=\"\" workspace_dir=\"\"\n if [ -f \"${env_file}\" ]; then\n data_dir=$(grep '^AGENTTEAMS_DATA_DIR=' \"${env_file}\" 2>/dev/null | cut -d= -f2- || true)\n workspace_dir=$(grep '^AGENTTEAMS_WORKSPACE_DIR=' \"${env_file}\" 2>/dev/null | cut -d= -f2- || true)\n fi\n\n # Remove Docker volume\n local vol=\"${data_dir:-agentteams-data}\"\n if ${DOCKER_CMD} volume inspect \"${vol}\" >/dev/null 2>&1; then\n log \"$(msg uninstall.removing_volume \"${vol}\")\"\n ${DOCKER_CMD} volume rm \"${vol}\" >/dev/null 2>&1 || true\n fi\n\n # Remove Docker network\n if ${DOCKER_CMD} network ls --format '{{.Name}}' 2>/dev/null | grep -q \"^agentteams-net$\"; then\n log \"$(msg uninstall.removing_network)\"\n ${DOCKER_CMD} network rm agentteams-net >/dev/null 2>&1 || true\n fi\n\n # Remove workspace directory\n if [ -n \"${workspace_dir}\" ] && [ -d \"${workspace_dir}\" ]; then\n log \"$(msg uninstall.removing_workspace \"${workspace_dir}\")\"\n rm -rf \"${workspace_dir}\" 2>/dev/null || true\n if [ -d \"${workspace_dir}\" ]; then\n log \"$(msg uninstall.removing_workspace_elevated)\"\n local _parent _base\n _parent=$(dirname \"${workspace_dir}\")\n _base=$(basename \"${workspace_dir}\")\n local _rm_image=\"${manager_image:-busybox}\"\n ${DOCKER_CMD} run --rm --entrypoint sh \\\n -v \"${_parent}:/host-parent\" \\\n \"${_rm_image}\" \\\n -c \"rm -rf /host-parent/${_base}\" 2>/dev/null || true\n fi\n fi\n\n # Remove env file\n if [ -f \"${env_file}\" ]; then\n log \"$(msg uninstall.removing_env \"${env_file}\")\"\n rm -f \"${env_file}\"\n fi\n\n # Remove install log\n if [ -f \"${HOME}/agentteams-install.log\" ]; then\n log \"$(msg uninstall.removing_log \"${HOME}/agentteams-install.log\")\"\n rm -f \"${HOME}/agentteams-install.log\"\n fi\n\n echo \"\"\n log \"$(msg uninstall.done)\"\n}\n\n# ============================================================\n# Check container runtime (docker or podman) and environment\n# ============================================================\ncheck_container_runtime() {\n # Ensure the rootless environment baseline (XDG_RUNTIME_DIR) is ready before engine detection.\n if [ \"$(id -u)\" -ne 0 ] && [ -z \"${XDG_RUNTIME_DIR:-}\" ]; then\n export XDG_RUNTIME_DIR=\"/run/user/$(id -u)\"\n fi\n\n if command -v docker >/dev/null 2>&1; then\n DOCKER_CMD=\"docker\"\n # Check for podman disguised as docker (alias or podman-docker package)\n if docker --version 2>/dev/null | grep -qi \"podman\"; then\n DOCKER_CMD=\"podman\"\n fi\n elif command -v podman >/dev/null 2>&1; then\n DOCKER_CMD=\"podman\"\n else\n echo -e \"\\033[31m[AgentTeams ERROR]\\033[0m $(msg error.docker_not_found)\" >&2\n exit 1\n fi\n\n # Unified health check (Validates runtime responsiveness)\n if ! ${DOCKER_CMD} ps >/dev/null 2>&1; then\n echo -e \"\\033[31m[AgentTeams ERROR]\\033[0m $(msg error.docker_not_running)\" >&2\n exit 1\n fi\n}\n\ncheck_container_runtime\n\ncase \"${1:-}\" in\n manager|\"\")\n install_manager\n ;;\n worker)\n shift\n install_worker \"$@\"\n ;;\n dashboard)\n check_container_runtime\n load_current_params_from_env\n AGENTTEAMS_DASHBOARD=\"${AGENTTEAMS_DASHBOARD:-1}\"\n AGENTTEAMS_DASHBOARD_VERSION=\"${AGENTTEAMS_DASHBOARD_VERSION:-v1.2.0}\"\n AGENTTEAMS_PORT_DASHBOARD=\"${AGENTTEAMS_PORT_DASHBOARD:-13000}\"\n AGENTTEAMS_DASHBOARD_IMAGE=\"${AGENTTEAMS_DASHBOARD_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-dashboard:${AGENTTEAMS_DASHBOARD_VERSION}}\"\n AGENTTEAMS_USE_EMBEDDED=1\n _start_dashboard\n ;;\n uninstall)\n uninstall_agentteams\n ;;\n *)\n echo \"Usage: $0 [manager|worker [options]|dashboard|uninstall]\"\n echo \"\"\n echo \"Commands:\"\n echo \" manager Interactive Manager installation (default)\"\n echo \" Choose Quick Start (all defaults) or Manual mode\"\n echo \" worker Worker installation (requires --name and connection params)\"\n echo \" dashboard Start/stop agentteams-dashboard (use with AGENTTEAMS_DASHBOARD=0 to stop)\"\n echo \" uninstall Stop and remove Manager + all Worker containers\"\n echo \"\"\n echo \"Quick Start (fastest):\"\n echo \" $0\"\n echo \" # Then select '1' for Quick Start mode\"\n echo \"\"\n echo \"Non-interactive (for automation):\"\n echo \" AGENTTEAMS_NON_INTERACTIVE=1 AGENTTEAMS_LLM_API_KEY=sk-xxx $0\"\n echo \"\"\n echo \"Worker Options:\"\n echo \" --name <name> Worker name (required)\"\n echo \" --fs <url> MinIO endpoint URL (required)\"\n echo \" --fs-key <key> MinIO access key (required)\"\n echo \" --fs-secret <secret> MinIO secret key (required)\"\n echo \" --reset Remove existing Worker container before creating\"\n exit 1\n ;;\nesac\n"}, "cf9bc5ea7e37e5d96dddbaf5cd7f2c6b4ea40207": {"t": 1786068472.5710459, "n": 249717, "h": "e7fd127314ffa7d8", "c": "#!/bin/bash\n# agentteams-install.sh - One-click installation for AgentTeams Manager, Worker, and Dashboard\n#\n# Usage:\n# ./agentteams-install.sh # Interactive installation (choose Quick Start or Manual)\n# ./agentteams-install.sh manager # Same as above (explicit)\n# ./agentteams-install.sh worker --name <name> ... # Worker installation\n# ./agentteams-install.sh dashboard # Start/stop agentteams-dashboard (AGENTTEAMS_DASHBOARD=0 to stop)\n# ./agentteams-install.sh uninstall # Stop and remove Manager + all Workers\n#\n# NOTE: agentteams-dashboard is currently only supported via the Bash installer.\n# The PowerShell installer (agentteams-install.ps1) does not yet include dashboard support.\n#\n# Onboarding Modes:\n# Quick Start - Fast installation with all default values (recommended)\n# Manual - Customize each option step by step\n#\n# Environment variables (for automation):\n# AGENTTEAMS_NON_INTERACTIVE Skip all prompts, use defaults (default: 0)\n# AGENTTEAMS_LLM_PROVIDER LLM provider (default: openai-compat for zh non-interactive Token Plan; qwen for en)\n# AGENTTEAMS_DEFAULT_MODEL Default model (default: qwen3.6-plus for zh Token Plan and en non-interactive)\n# AGENTTEAMS_OPENAI_BASE_URL OpenAI-compatible base URL (default for zh non-interactive: Alibaba Token Plan endpoint)\n# AGENTTEAMS_LLM_API_KEY LLM API key (required)\n# AGENTTEAMS_ADMIN_USER Admin username (default: admin)\n# AGENTTEAMS_ADMIN_PASSWORD Admin password (auto-generated if not set, min 8 chars)\n# AGENTTEAMS_MATRIX_DOMAIN Matrix domain (default: matrix-local.agentteams.io:18080)\n# AGENTTEAMS_MOUNT_SOCKET Mount container runtime socket (default: 1)\n# AGENTTEAMS_DATA_DIR Docker volume name for persistent data (default: agentteams-data)\n# AGENTTEAMS_WORKSPACE_DIR Host directory for manager workspace (default: ~/agentteams-manager)\n# AGENTTEAMS_VERSION Image tag (default: latest)\n# AGENTTEAMS_REGISTRY Image registry (default: auto-detected by timezone)\n# AGENTTEAMS_INSTALL_MANAGER_IMAGE Override manager image (e.g., local build)\n# AGENTTEAMS_INSTALL_WORKER_IMAGE Override worker image (e.g., local build)\n# AGENTTEAMS_INSTALL_COPAW_WORKER_IMAGE Override copaw worker image (e.g., local build)\n# AGENTTEAMS_INSTALL_HERMES_WORKER_IMAGE Override hermes worker image (e.g., local build)\n# AGENTTEAMS_NACOS_REGISTRY_URI Default Nacos registry URI for Worker market search/import\n# (default: nacos://market.agentteams.io:80/public)\n# AGENTTEAMS_NACOS_USERNAME Default Nacos username for nacos:// package imports (optional)\n# AGENTTEAMS_NACOS_PASSWORD Default Nacos password for nacos:// package imports (optional)\n# AGENTTEAMS_CMS_TRACES_ENABLED Enable openclaw-cms-plugin traces for Manager AND all Workers (default: false)\n# AGENTTEAMS_CMS_ENDPOINT ARMS OTLP endpoint (required if traces enabled)\n# AGENTTEAMS_CMS_LICENSE_KEY CMS license key (required if traces enabled)\n# AGENTTEAMS_CMS_PROJECT CMS project name (optional)\n# AGENTTEAMS_CMS_WORKSPACE CMS workspace ID (required if traces enabled)\n# AGENTTEAMS_CMS_SERVICE_NAME Manager service name in ARMS (default: agentteams-manager)\n# Workers always report as agentteams-worker-<name> automatically\n# AGENTTEAMS_CMS_METRICS_ENABLED Enable diagnostics-otel metrics for Manager AND all Workers (default: false)\n# AGENTTEAMS_PORT_GATEWAY Host port for Higress gateway (default: 18080)\n# AGENTTEAMS_PORT_CONSOLE Host port for Higress console (default: 18001)\n# AGENTTEAMS_PORT_ELEMENT_WEB Host port for Element Web direct access (default: 18088)\n# AGENTTEAMS_PORT_MANAGER_CONSOLE Host port for Manager console (default: 18888)\n# AGENTTEAMS_WORKER_IDLE_TIMEOUT Worker idle timeout in minutes (default: 720, i.e. 12 hours)\n# AGENTTEAMS_DASHBOARD Install agentteams-dashboard management UI (default: 1)\n# AGENTTEAMS_DASHBOARD_VERSION Dashboard version (default: v1.2.0, independent of AgentTeams version)\n# AGENTTEAMS_PORT_DASHBOARD Dashboard host port (default: 13000)\n# AGENTTEAMS_DASHBOARD_IMAGE Override dashboard image (default: <registry>/agentteams/agentteams-dashboard:<DASHBOARD_VERSION>)\n# AGENTTEAMS_AI_GATEWAY_ADMIN_URL Higress Console URL for shared auth (auto-detected)\n\nset -e\n\nAGENTTEAMS_VERSION=\"${AGENTTEAMS_VERSION:-}\"\nAGENTTEAMS_KNOWN_STABLE_VERSION=\"v1.2.1\" # fallback if GitHub API is unreachable\n\n_normalize_version() {\n local version=\"$1\"\n case \"${version}\" in\n [0-9]*.[0-9]*.[0-9]*) version=\"v${version}\" ;;\n esac\n case \"${version}\" in\n v[0-9]*.[0-9]*.[0-9]*.beta.[0-9]*) version=\"${version/.beta./-beta.}\" ;;\n esac\n printf '%s' \"${version}\"\n}\n\n# Returns 0 (true) if $1 < $2 using semver order; \"latest\" is treated as greatest\n_ver_lt() {\n [ \"$1\" = \"latest\" ] && return 1\n [ \"$2\" = \"latest\" ] && return 0\n [ \"$1\" = \"$2\" ] && return 1\n [ \"$(printf '%s\\n%s' \"$1\" \"$2\" | sort -V | head -1)\" = \"$1\" ]\n}\n\n_use_legacy_image_env() {\n if [ \"$1\" = \"latest\" ]; then\n _ver_lt \"${AGENTTEAMS_KNOWN_STABLE_VERSION}\" \"v1.2.0\"\n else\n _ver_lt \"$1\" \"v1.2.0\"\n fi\n}\n\n_controller_env_prefix() {\n if _use_legacy_image_env \"$1\"; then\n printf '%s%s' 'HIC' 'LAW_'\n else\n printf '%s' 'AGENTTEAMS_'\n fi\n}\n\n_controller_storage_prefix() {\n if _use_legacy_image_env \"$1\"; then\n printf '%s%s' 'hic' 'law/agentteams-storage'\n else\n printf '%s' 'agentteams/agentteams-storage'\n fi\n}\nAGENTTEAMS_NON_INTERACTIVE=\"${AGENTTEAMS_NON_INTERACTIVE:-0}\"\nAGENTTEAMS_MOUNT_SOCKET=\"${AGENTTEAMS_MOUNT_SOCKET:-1}\"\nAGENTTEAMS_DOCKER_PROXY=\"${AGENTTEAMS_DOCKER_PROXY:-1}\"\nSTEP_RESULT=\"\" # Used by state machine to signal \"back\" navigation\n\n# ============================================================\n# Log all output to file\n# ============================================================\n\nAGENTTEAMS_LOG_FILE=\"${HOME}/agentteams-install.log\"\n\nif [ \"${1:-}\" != \"uninstall\" ]; then\n # Redirect all output (stdout and stderr) to both terminal and log file\n exec > >(tee -a \"${AGENTTEAMS_LOG_FILE}\") 2>&1\n\n echo \"\"\n echo \"========================================\"\n echo \"AgentTeams Installation Log\"\n echo \"Started: $(date)\"\n echo \"User: $(whoami)\"\n echo \"System: $(uname -a)\"\n echo \"Log file: ${AGENTTEAMS_LOG_FILE}\"\n echo \"========================================\"\n echo \"\"\nfi\n\n# ============================================================\n# Utility functions (needed early for timezone detection)\n# ============================================================\n\nlog() {\n echo -e \"\\033[36m[AgentTeams]\\033[0m $1\"\n}\n\nerror() {\n echo -e \"\\033[31m[AgentTeams ERROR]\\033[0m $1\" >&2\n}\n\ndie() {\n error \"$1\"\n exit 1\n}\n\n# ============================================================\n# Timezone detection (compatible with Linux and macOS)\n# ============================================================\n\ndetect_timezone() {\n local tz=\"\"\n\n # Try /etc/timezone (Debian/Ubuntu)\n if [ -f /etc/timezone ]; then\n tz=$(cat /etc/timezone 2>/dev/null | tr -d '[:space:]')\n fi\n\n # Try /etc/localtime symlink (macOS and some Linux)\n if [ -z \"${tz}\" ] && [ -L /etc/localtime ]; then\n tz=$(ls -l /etc/localtime 2>/dev/null | sed 's|.*/zoneinfo/||')\n fi\n\n # Try timedatectl (systemd)\n if [ -z \"${tz}\" ]; then\n tz=$(timedatectl show --value -p Timezone 2>/dev/null)\n fi\n\n # If still not detected, warn and prompt user. Diagnostics go to stderr —\n # this function returns its value via stdout to `$(detect_timezone)`, so\n # any echo here would leak into AGENTTEAMS_TIMEZONE and downstream image tags.\n if [ -z \"${tz}\" ]; then\n echo \"\" >&2\n echo -e \"\\033[33m[AgentTeams WARNING]\\033[0m Could not detect timezone automatically.\" >&2\n echo -e \"\\033[33m[AgentTeams]\\033[0m Please enter your timezone (e.g., Asia/Shanghai, America/New_York).\" >&2\n echo \"\" >&2\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n tz=\"Asia/Shanghai\"\n log \"Using default timezone: ${tz}\" >&2\n else\n read -e -p \"Timezone [Asia/Shanghai]: \" tz\n tz=\"${tz:-Asia/Shanghai}\"\n fi\n fi\n\n echo \"${tz}\"\n}\n\n# Detect timezone once at startup (used by registry selection and container TZ)\nAGENTTEAMS_TIMEZONE=\"${AGENTTEAMS_TIMEZONE:-$(detect_timezone)}\"\n\n# ============================================================\n# Language detection based on timezone\n# ============================================================\n\ndetect_language() {\n local tz=\"${AGENTTEAMS_TIMEZONE}\"\n case \"${tz}\" in\n Asia/Shanghai|Asia/Chongqing|Asia/Harbin|Asia/Urumqi|\\\n Asia/Taipei|Asia/Hong_Kong|Asia/Macau)\n echo \"zh\"\n ;;\n *)\n echo \"en\"\n ;;\n esac\n}\n\n# Language priority: env var > existing env file > timezone detection\nif [ -z \"${AGENTTEAMS_LANGUAGE}\" ]; then\n # Check existing env file for saved language preference (upgrade scenario)\n _env_file=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n # Migrate from legacy location (current directory) if needed\n if [ ! -f \"${_env_file}\" ] && [ -f \"./agentteams-manager.env\" ]; then\n mv \"./agentteams-manager.env\" \"${_env_file}\" 2>/dev/null || true\n fi\n if [ -f \"${_env_file}\" ]; then\n _saved_lang=$(grep '^AGENTTEAMS_LANGUAGE=' \"${_env_file}\" 2>/dev/null | cut -d= -f2-)\n if [ -n \"${_saved_lang}\" ]; then\n AGENTTEAMS_LANGUAGE=\"${_saved_lang}\"\n fi\n fi\n # Fall back to timezone-based detection\n if [ -z \"${AGENTTEAMS_LANGUAGE}\" ]; then\n AGENTTEAMS_LANGUAGE=\"$(detect_language)\"\n fi\n unset _env_file _saved_lang\nfi\nexport AGENTTEAMS_LANGUAGE\n\n# ============================================================\n# Centralized message dictionary and msg() function\n# Compatible with bash 3.2+ (macOS default) — uses case instead of declare -A\n# ============================================================\n\n# msg() function: look up message by key, with printf-style argument substitution\n# Falls back to English if the current language translation is missing.\nmsg() {\n local key=\"$1\"\n shift\n local lang=\"${AGENTTEAMS_LANGUAGE:-en}\"\n local text=\"\"\n case \"${key}.${lang}\" in\n # --- Timezone detection messages ---\n \"tz.warning.title.zh\") text=\"无法自动检测时区。\" ;;\n \"tz.warning.title.en\") text=\"Could not detect timezone automatically.\" ;;\n \"tz.warning.prompt.zh\") text=\"请输入您的时区(例如 Asia/Shanghai、America/New_York)。\" ;;\n \"tz.warning.prompt.en\") text=\"Please enter your timezone (e.g., Asia/Shanghai, America/New_York).\" ;;\n \"tz.default.zh\") text=\"使用默认时区: %s\" ;;\n \"tz.default.en\") text=\"Using default timezone: %s\" ;;\n \"tz.input_prompt.zh\") text=\"时区\" ;;\n \"tz.input_prompt.en\") text=\"Timezone\" ;;\n # --- Installation title and info ---\n \"install.title.zh\") text=\"=== AgentTeams Manager 安装 ===\" ;;\n \"install.title.en\") text=\"=== AgentTeams Manager Installation ===\" ;;\n \"install.registry.zh\") text=\"镜像仓库: %s\" ;;\n \"install.registry.en\") text=\"Registry: %s\" ;;\n \"install.dir.zh\") text=\"安装目录: %s\" ;;\n \"install.dir.en\") text=\"Installation directory: %s\" ;;\n \"install.dir_hint.zh\") text=\" (env 文件 'agentteams-manager.env' 将保存到 HOME 目录。)\" ;;\n \"install.dir_hint.en\") text=\" (The env file 'agentteams-manager.env' will be saved to your HOME directory.)\" ;;\n \"install.dir_hint2.zh\") text=\" (请从您希望管理此安装的目录运行此脚本。)\" ;;\n \"install.dir_hint2.en\") text=\" (Run this script from the directory where you want to manage this installation.)\" ;;\n # --- Onboarding mode ---\n \"install.mode.title.zh\") text=\"--- Onboarding 模式 ---\" ;;\n \"install.mode.title.en\") text=\"--- Onboarding Mode ---\" ;;\n \"install.mode.choose.zh\") text=\"选择安装模式:\" ;;\n \"install.mode.choose.en\") text=\"Choose your installation mode:\" ;;\n \"install.mode.quickstart.zh\") text=\" 1) 快速开始 - 使用阿里云通义 Token 套餐快速安装(推荐)\" ;;\n \"install.mode.quickstart.en\") text=\" 1) Quick Start - Fast installation with Qwen Cloud (recommended)\" ;;\n \"install.mode.manual.zh\") text=\" 2) 手动配置 - 选择 LLM 提供商并自定义选项\" ;;\n \"install.mode.manual.en\") text=\" 2) Manual - Choose LLM provider and customize options\" ;;\n \"install.mode.prompt.zh\") text=\"请选择 [1/2]\" ;;\n \"install.mode.prompt.en\") text=\"Enter choice [1/2]\" ;;\n \"install.mode.quickstart_selected.zh\") text=\"已选择快速开始模式 - 使用阿里云通义 Token 套餐\" ;;\n \"install.mode.quickstart_selected.en\") text=\"Quick Start mode selected - using Qwen Cloud\" ;;\n \"install.mode.manual_selected.zh\") text=\"已选择手动配置模式 - 您将选择 LLM 提供商并自定义选项\" ;;\n \"install.mode.manual_selected.en\") text=\"Manual mode selected - you will choose LLM provider and customize options\" ;;\n \"install.mode.invalid.zh\") text=\"无效选择,默认使用快速开始模式\" ;;\n \"install.mode.invalid.en\") text=\"Invalid choice, defaulting to Quick Start mode\" ;;\n # --- Upgrade mode sub-menu ---\n \"upgrade.mode.prompt.zh\") text=\"请选择升级方式:\" ;;\n \"upgrade.mode.prompt.en\") text=\"Select upgrade mode:\" ;;\n \"upgrade.mode.keep_all.zh\") text=\" 1) 保留所有参数,一键升级(推荐)\" ;;\n \"upgrade.mode.keep_all.en\") text=\" 1) Keep all parameters, quick upgrade (recommended)\" ;;\n \"upgrade.mode.confirm_each.zh\") text=\" 2) 逐个确认参数(可修改)\" ;;\n \"upgrade.mode.confirm_each.en\") text=\" 2) Confirm each parameter (can modify)\" ;;\n \"upgrade.mode.back.zh\") text=\" 3) 返回\" ;;\n \"upgrade.mode.back.en\") text=\" 3) Back\" ;;\n # --- Version selection ---\n \"install.version.title.zh\") text=\"--- 版本选择 ---\" ;;\n \"install.version.title.en\") text=\"--- Version Selection ---\" ;;\n \"install.version.choose.zh\") text=\"选择要安装的版本:\" ;;\n \"install.version.choose.en\") text=\"Choose the version to install:\" ;;\n \"install.version.option_latest.zh\") text=\" 1) latest - 最新版(默认)\" ;;\n \"install.version.option_latest.en\") text=\" 1) latest - Latest build (default)\" ;;\n \"install.version.option_stable.zh\") text=\" 2) %s - 最新稳定版\" ;;\n \"install.version.option_stable.en\") text=\" 2) %s - Latest stable release\" ;;\n \"install.version.fetching.zh\") text=\"正在查询最新稳定版本...\" ;;\n \"install.version.fetching.en\") text=\"Fetching latest stable release...\" ;;\n \"install.version.fetch_failed.zh\") text=\"无法查询 GitHub,使用内置版本 %s\" ;;\n \"install.version.fetch_failed.en\") text=\"Could not reach GitHub, using built-in version %s\" ;;\n \"install.version.option_custom.zh\") text=\" 3) 自定义 - 手动输入版本号(如 v1.0.5)\" ;;\n \"install.version.option_custom.en\") text=\" 3) Custom - Enter a specific version (e.g. v1.0.5)\" ;;\n \"install.version.prompt.zh\") text=\"请选择 [1/2/3]\" ;;\n \"install.version.prompt.en\") text=\"Enter choice [1/2/3]\" ;;\n \"install.version.custom_prompt.zh\") text=\"请输入版本号\" ;;\n \"install.version.custom_prompt.en\") text=\"Enter version tag\" ;;\n \"install.version.selected_latest.zh\") text=\"已选择最新版 (latest)\" ;;\n \"install.version.selected_latest.en\") text=\"Selected latest version\" ;;\n \"install.version.selected_stable.zh\") text=\"已选择最新稳定版 (%s)\" ;;\n \"install.version.selected_stable.en\") text=\"Selected latest stable version (%s)\" ;;\n \"install.version.selected_custom.zh\") text=\"已选择自定义版本 (%s)\" ;;\n \"install.version.selected_custom.en\") text=\"Selected custom version (%s)\" ;;\n \"install.version.invalid.zh\") text=\"无效选择,使用最新稳定版 (%s)\" ;;\n \"install.version.invalid.en\") text=\"Invalid choice, defaulting to latest stable version (%s)\" ;;\n # --- Existing installation detected ---\n \"install.existing.detected.zh\") text=\"检测到已有 Manager 安装(env 文件: %s)\" ;;\n \"install.existing.detected.en\") text=\"Existing Manager installation detected (env file: %s)\" ;;\n \"install.existing.choose.zh\") text=\"选择操作:\" ;;\n \"install.existing.choose.en\") text=\"Choose an action:\" ;;\n \"install.existing.upgrade.zh\") text=\" 1) 就地升级(保留数据、工作空间、env 文件)\" ;;\n \"install.existing.upgrade.en\") text=\" 1) In-place upgrade (keep data, workspace, env file)\" ;;\n \"install.existing.reinstall.zh\") text=\" 2) 全新重装(删除所有数据,重新开始)\" ;;\n \"install.existing.reinstall.en\") text=\" 2) Clean reinstall (remove all data, start fresh)\" ;;\n \"install.existing.cancel.zh\") text=\" 3) 取消\" ;;\n \"install.existing.cancel.en\") text=\" 3) Cancel\" ;;\n \"install.existing.prompt.zh\") text=\"请选择 [1/2/3]\" ;;\n \"install.existing.prompt.en\") text=\"Enter choice [1/2/3]\" ;;\n \"install.existing.upgrade_noninteractive.zh\") text=\"非交互模式: 执行就地升级...\" ;;\n \"install.existing.upgrade_noninteractive.en\") text=\"Non-interactive mode: performing in-place upgrade...\" ;;\n \"install.existing.upgrading.zh\") text=\"执行就地升级...\" ;;\n \"install.existing.upgrading.en\") text=\"Performing in-place upgrade...\" ;;\n \"install.existing.warn_manager_stop.zh\") text=\"⚠️ Manager 容器将被停止并重新创建。\" ;;\n \"install.existing.warn_manager_stop.en\") text=\"⚠️ Manager container will be stopped and recreated.\" ;;\n \"install.existing.warn_worker_recreate.zh\") text=\"⚠️ Worker 容器也将被重新创建(以更新 Manager IP)。\" ;;\n \"install.existing.warn_worker_recreate.en\") text=\"⚠️ Worker containers will also be recreated (to update Manager IP in hosts).\" ;;\n \"install.existing.continue_prompt.zh\") text=\"继续?[y/N]\" ;;\n \"install.existing.continue_prompt.en\") text=\"Continue? [y/N]\" ;;\n \"install.existing.cancelled.zh\") text=\"安装已取消。\" ;;\n \"install.existing.cancelled.en\") text=\"Installation cancelled.\" ;;\n \"install.existing.stopping_manager.zh\") text=\"停止并移除现有 manager 容器...\" ;;\n \"install.existing.stopping_manager.en\") text=\"Stopping and removing existing manager container...\" ;;\n \"install.existing.stopping_workers.zh\") text=\"停止并移除现有 worker 容器...\" ;;\n \"install.existing.stopping_workers.en\") text=\"Stopping and removing existing worker containers...\" ;;\n \"install.existing.removed.zh\") text=\" 已移除: %s\" ;;\n \"install.existing.removed.en\") text=\" Removed: %s\" ;;\n # --- Clean reinstall messages ---\n \"install.reinstall.performing.zh\") text=\"执行全新重装...\" ;;\n \"install.reinstall.performing.en\") text=\"Performing clean reinstall...\" ;;\n \"install.reinstall.warn_stop.zh\") text=\"⚠️ 以下运行中的容器将被停止:\" ;;\n \"install.reinstall.warn_stop.en\") text=\"⚠️ The following running containers will be stopped:\" ;;\n \"install.reinstall.warn_delete.zh\") text=\"⚠️ 警告: 以下内容将被删除:\" ;;\n \"install.reinstall.warn_delete.en\") text=\"⚠️ WARNING: This will DELETE the following:\" ;;\n \"install.reinstall.warn_volume.zh\") text=\" - Docker 卷: agentteams-data\" ;;\n \"install.reinstall.warn_volume.en\") text=\" - Docker volume: agentteams-data\" ;;\n \"install.reinstall.warn_env.zh\") text=\" - Env 文件: %s\" ;;\n \"install.reinstall.warn_env.en\") text=\" - Env file: %s\" ;;\n \"install.reinstall.warn_workspace.zh\") text=\" - Manager 工作空间: %s\" ;;\n \"install.reinstall.warn_workspace.en\") text=\" - Manager workspace: %s\" ;;\n \"install.reinstall.warn_workers.zh\") text=\" - 所有 worker 容器\" ;;\n \"install.reinstall.warn_workers.en\") text=\" - All worker containers\" ;;\n \"install.reinstall.warn_proxy.zh\") text=\" - Docker API 代理容器: agentteams-controller\" ;;\n \"install.reinstall.warn_proxy.en\") text=\" - Docker API proxy container: agentteams-controller\" ;;\n \"install.reinstall.warn_network.zh\") text=\" - Docker 网络: agentteams-net\" ;;\n \"install.reinstall.warn_network.en\") text=\" - Docker network: agentteams-net\" ;;\n \"install.reinstall.confirm_type.zh\") text=\"请输入工作空间路径以确认删除(或按 Ctrl+C 取消):\" ;;\n \"install.reinstall.confirm_type.en\") text=\"To confirm deletion, please type the workspace path:\" ;;\n \"install.reinstall.confirm_path.zh\") text=\"输入路径以确认(或按 Ctrl+C 取消)\" ;;\n \"install.reinstall.confirm_path.en\") text=\"Type the path to confirm (or press Ctrl+C to cancel)\" ;;\n \"install.reinstall.path_mismatch.zh\") text=\"路径不匹配。中止重装。输入: '%s',期望: '%s'\" ;;\n \"install.reinstall.path_mismatch.en\") text=\"Path mismatch. Aborting reinstall. Input: '%s', Expected: '%s'\" ;;\n \"install.reinstall.confirmed.zh\") text=\"已确认。正在清理...\" ;;\n \"install.reinstall.confirmed.en\") text=\"Confirmed. Cleaning up...\" ;;\n \"install.reinstall.removed_worker.zh\") text=\" 已移除 worker: %s\" ;;\n \"install.reinstall.removed_worker.en\") text=\" Removed worker: %s\" ;;\n \"install.reinstall.removing_volume.zh\") text=\"正在移除 Docker 卷: agentteams-data\" ;;\n \"install.reinstall.removing_volume.en\") text=\"Removing Docker volume: agentteams-data\" ;;\n \"install.reinstall.warn_volume_fail.zh\") text=\" 警告: 无法移除卷(可能有引用)\" ;;\n \"install.reinstall.warn_volume_fail.en\") text=\" Warning: Could not remove volume (may have references)\" ;;\n \"install.reinstall.removing_proxy.zh\") text=\"正在移除 Docker API 代理容器: agentteams-controller\" ;;\n \"install.reinstall.removing_proxy.en\") text=\"Removing Docker API proxy container: agentteams-controller\" ;;\n \"install.reinstall.removing_network.zh\") text=\"正在移除 Docker 网络: agentteams-net\" ;;\n \"install.reinstall.removing_network.en\") text=\"Removing Docker network: agentteams-net\" ;;\n \"install.reinstall.removing_workspace.zh\") text=\"正在移除工作空间目录: %s\" ;;\n \"install.reinstall.removing_workspace.en\") text=\"Removing workspace directory: %s\" ;;\n \"install.reinstall.removing_env.zh\") text=\"正在移除 env 文件: %s\" ;;\n \"install.reinstall.removing_env.en\") text=\"Removing env file: %s\" ;;\n \"install.reinstall.cleanup_done.zh\") text=\"清理完成。开始全新安装...\" ;;\n \"install.reinstall.cleanup_done.en\") text=\"Cleanup complete. Starting fresh installation...\" ;;\n \"install.reinstall.failed_rm_workspace.zh\") text=\"无法移除工作空间目录\" ;;\n \"install.reinstall.failed_rm_workspace.en\") text=\"Failed to remove workspace directory\" ;;\n # --- Orphan volume detection ---\n \"install.orphan_volume.detected.zh\") text=\"⚠️ 检测到残留数据卷 '%s',但未找到对应的 env 配置文件。\" ;;\n \"install.orphan_volume.detected.en\") text=\"⚠️ Found leftover data volume '%s' but no matching env config file.\" ;;\n \"install.orphan_volume.warn.zh\") text=\"这可能是之前安装的残留数据,会导致新安装出现异常(如密码冲突、服务启动失败)。\" ;;\n \"install.orphan_volume.warn.en\") text=\"This is likely leftover data from a previous installation and may cause issues (e.g., credential conflicts, service startup failures).\" ;;\n \"install.orphan_volume.choose.zh\") text=\"选择操作:\" ;;\n \"install.orphan_volume.choose.en\") text=\"Choose an action:\" ;;\n \"install.orphan_volume.clean.zh\") text=\" 1) 清理残留数据卷后继续安装(推荐)\" ;;\n \"install.orphan_volume.clean.en\") text=\" 1) Remove leftover volume and continue installation (recommended)\" ;;\n \"install.orphan_volume.keep.zh\") text=\" 2) 保留数据卷继续安装(可能出现异常)\" ;;\n \"install.orphan_volume.keep.en\") text=\" 2) Keep the volume and continue installation (may cause issues)\" ;;\n \"install.orphan_volume.prompt.zh\") text=\"请选择 [1/2]\" ;;\n \"install.orphan_volume.prompt.en\") text=\"Enter choice [1/2]\" ;;\n \"install.orphan_volume.cleaning.zh\") text=\"正在清理残留数据卷...\" ;;\n \"install.orphan_volume.cleaning.en\") text=\"Removing leftover data volume...\" ;;\n \"install.orphan_volume.cleaned.zh\") text=\"残留数据卷已清理。继续全新安装...\" ;;\n \"install.orphan_volume.cleaned.en\") text=\"Leftover volume removed. Continuing with fresh installation...\" ;;\n \"install.orphan_volume.keeping.zh\") text=\"保留数据卷,继续安装。如遇异常请选择全新重装。\" ;;\n \"install.orphan_volume.keeping.en\") text=\"Keeping existing volume. If you encounter issues, consider a clean reinstall.\" ;;\n \"install.orphan_volume.clean_noninteractive.zh\") text=\"非交互模式: 自动清理残留数据卷...\" ;;\n \"install.orphan_volume.clean_noninteractive.en\") text=\"Non-interactive mode: automatically removing leftover volume...\" ;;\n # --- Loading existing config ---\n \"install.loading_config.zh\") text=\"从 %s 加载已有配置(shell 环境变量优先)...\" ;;\n \"install.loading_config.en\") text=\"Loading existing config from %s (shell env vars take priority)...\" ;;\n # --- LLM Configuration ---\n \"llm.title.zh\") text=\"--- LLM 配置 ---\" ;;\n \"llm.title.en\") text=\"--- LLM Configuration ---\" ;;\n \"llm.provider.label.zh\") text=\" 提供商: %s\" ;;\n \"llm.provider.label.en\") text=\" Provider: %s\" ;;\n \"llm.model.label.zh\") text=\" 模型: %s\" ;;\n \"llm.model.label.en\") text=\" Model: %s\" ;;\n \"llm.provider.qwen.zh\") text=\" 提供商: qwen(阿里云百炼)\" ;;\n \"llm.provider.qwen.en\") text=\" Provider: qwen (Alibaba Cloud Bailian)\" ;;\n \"llm.provider.qwen_default.zh\") text=\" 提供商: %s(默认)\" ;;\n \"llm.provider.qwen_default.en\") text=\" Provider: %s (default)\" ;;\n \"llm.model.default.zh\") text=\" 模型: %s(默认)\" ;;\n \"llm.model.default.en\") text=\" Model: %s (default)\" ;;\n \"llm.apikey_hint_bailian.zh\") text=\" 💡 获取阿里云百炼(DashScope)API Key:\" ;;\n \"llm.apikey_hint_bailian.en\") text=\" 💡 Get your Alibaba Cloud Bailian (DashScope) API Key:\" ;;\n \"llm.apikey_url_bailian.zh\") text=\" https://www.aliyun.com/product/bailian\" ;;\n \"llm.apikey_url_bailian.en\") text=\" https://www.aliyun.com/product/bailian\" ;;\n \"llm.apikey_hint_qwencloud.zh\") text=\" 💡 从 Qwen Cloud(国际站)获取 DASHSCOPE_API_KEY:\" ;;\n \"llm.apikey_hint_qwencloud.en\") text=\" 💡 Get your DASHSCOPE_API_KEY for Qwen Cloud (international) from:\" ;;\n \"llm.apikey_url_qwencloud.zh\") text=\" https://home.qwencloud.com/api-keys (文档: https://docs.qwencloud.com/)\" ;;\n \"llm.apikey_url_qwencloud.en\") text=\" https://home.qwencloud.com/api-keys | Docs: https://docs.qwencloud.com/\" ;;\n \"llm.apikey_hint_tokenplan.zh\") text=\" 💡 获取 DashScope API Key 或开通通义 Token 套餐,请参考:\" ;;\n \"llm.apikey_hint_tokenplan.en\") text=\" 💡 Get your DashScope or Token Plan API key (Alibaba Model Studio):\" ;;\n \"llm.apikey_url_tokenplan.zh\") text=\" https://help.aliyun.com/zh/model-studio/token-plan-quickstart\" ;;\n \"llm.apikey_url_tokenplan.en\") text=\" https://common-buy.aliyun.com/token-plan/ | https://help.aliyun.com/zh/model-studio/token-plan-quickstart\" ;;\n \"llm.apikey_hint_codingplan.zh\") text=\" 💡 获取 DashScope API Key(Coding 套餐 / coding.dashscope 接口):\" ;;\n \"llm.apikey_hint_codingplan.en\") text=\" 💡 Get your DashScope API key for Coding Plan (coding.dashscope endpoint):\" ;;\n \"llm.apikey_url_codingplan.zh\") text=\" https://help.aliyun.com/zh/model-studio/get-api-key\" ;;\n \"llm.apikey_url_codingplan.en\") text=\" https://help.aliyun.com/zh/model-studio/get-api-key\" ;;\n \"llm.apikey_prompt.zh\") text=\"LLM API Key\" ;;\n \"llm.apikey_prompt.en\") text=\"LLM API Key\" ;;\n \"llm.providers_title.zh\") text=\"可用 LLM 提供商:\" ;;\n \"llm.providers_title.en\") text=\"Available LLM Providers:\" ;;\n \"llm.provider.alibaba.zh\") text=\" 1) 阿里云通义 Token 套餐 - 推荐中国用户使用\" ;;\n \"llm.provider.alibaba.en\") text=\" 1) Qwen Cloud - International (OpenAI-compatible API, recommended)\" ;;\n \"llm.provider.openai_compat.zh\") text=\" 2) OpenAI 兼容 API - 自定义 Base URL(OpenAI、DeepSeek 等)\" ;;\n \"llm.provider.openai_compat.en\") text=\" 2) OpenAI-compatible API - Custom Base URL (OpenAI, DeepSeek, etc.)\" ;;\n \"llm.provider.select.zh\") text=\"选择提供商 [1/2]\" ;;\n \"llm.provider.select.en\") text=\"Select provider [1/2]\" ;;\n \"llm.alibaba.models_title.zh\") text=\"选择阿里云模型接入方式:\" ;;\n \"llm.alibaba.models_title.en\") text=\"Select Alibaba Cloud model access:\" ;;\n \"llm.alibaba.model.tokenplan.zh\") text=\" 1) 阿里云通义 Token 套餐 - 兼容模式(推荐)\" ;;\n \"llm.alibaba.model.tokenplan.en\") text=\" 1) Alibaba Cloud Token Plan - compatible-mode (recommended)\" ;;\n \"llm.alibaba.model.bailian.zh\") text=\" 2) 阿里云百炼 - DashScope 通用兼容接口\" ;;\n \"llm.alibaba.model.bailian.en\") text=\" 2) Alibaba Cloud Bailian - DashScope compatible mode\" ;;\n \"llm.alibaba.model.codingplan_legacy.zh\") text=\" 3) 阿里云 Coding 套餐 - 旧版端点(兼容保留)\" ;;\n \"llm.alibaba.model.codingplan_legacy.en\") text=\" 3) Alibaba Cloud Coding Plan - legacy endpoint (backward compatible)\" ;;\n \"llm.alibaba.model.select.zh\") text=\"选择接入方式 [1/2/3]\" ;;\n \"llm.alibaba.model.select.en\") text=\"Select access option [1/2/3]\" ;;\n \"llm.alibaba.model.invalid.zh\") text=\"无效选择: %s(请输入 1、2 或 3)\" ;;\n \"llm.alibaba.model.invalid.en\") text=\"Invalid choice: %s (please enter 1, 2, or 3)\" ;;\n \"llm.codingplan.models_title.zh\") text=\"选择通义 Token 套餐默认模型:\" ;;\n \"llm.codingplan.models_title.en\") text=\"Select Qwen Cloud default model:\" ;;\n \"llm.codingplan.model.qwen36plus.zh\") text=\" 1) qwen3.6-plus - 千问 3.6(推荐)\" ;;\n \"llm.codingplan.model.qwen36plus.en\") text=\" 1) qwen3.6-plus - Qwen 3.6 (recommended)\" ;;\n \"llm.codingplan.model.glm5.zh\") text=\" 2) glm-5 - 智谱 GLM-5(编程推荐)\" ;;\n \"llm.codingplan.model.glm5.en\") text=\" 2) glm-5 - Zhipu GLM-5 (recommended for coding)\" ;;\n \"llm.codingplan.model.kimi.zh\") text=\" 3) kimi-k2.5 - Moonshot Kimi K2.5\" ;;\n \"llm.codingplan.model.kimi.en\") text=\" 3) kimi-k2.5 - Moonshot Kimi K2.5\" ;;\n \"llm.codingplan.model.minimax.zh\") text=\" 4) MiniMax-M2.5 - MiniMax M2.5\" ;;\n \"llm.codingplan.model.minimax.en\") text=\" 4) MiniMax-M2.5 - MiniMax M2.5\" ;;\n \"llm.codingplan.model.select.zh\") text=\"选择模型 [1/2/3/4]\" ;;\n \"llm.codingplan.model.select.en\") text=\"Select model [1/2/3/4]\" ;;\n \"llm.provider.selected_tokenplan.zh\") text=\" 提供商: 阿里云通义 Token 套餐(兼容模式)\" ;;\n \"llm.provider.selected_tokenplan.en\") text=\" Provider: Alibaba Cloud Token Plan (compatible mode)\" ;;\n \"llm.provider.selected_codingplan.zh\") text=\" 提供商: 阿里云通义 Token 套餐(alibaba-cloud)\" ;;\n \"llm.provider.selected_codingplan.en\") text=\" Provider: Qwen Cloud (international) (alibaba-cloud)\" ;;\n \"llm.provider.selected_codingplan_legacy.zh\") text=\" 提供商: 阿里云 Coding 套餐(coding.dashscope)\" ;;\n \"llm.provider.selected_codingplan_legacy.en\") text=\" Provider: Alibaba Cloud Coding Plan (coding.dashscope)\" ;;\n \"llm.provider.selected_qwen.zh\") text=\" 提供商: 阿里云百炼\" ;;\n \"llm.provider.selected_qwen.en\") text=\" Provider: Alibaba Cloud Bailian\" ;;\n \"llm.provider.selected_openai.zh\") text=\" 提供商: %s(OpenAI 兼容)\" ;;\n \"llm.provider.selected_openai.en\") text=\" Provider: %s (OpenAI-compatible)\" ;;\n \"llm.provider.invalid.zh\") text=\"无效选择: %s(请输入 1 或 2)\" ;;\n \"llm.provider.invalid.en\") text=\"Invalid choice: %s (please enter 1 or 2)\" ;;\n \"llm.qwen.model_prompt.zh\") text=\"默认模型 ID\" ;;\n \"llm.qwen.model_prompt.en\") text=\"Default Model ID\" ;;\n \"llm.openai.base_url_prompt.zh\") text=\"Base URL\" ;;\n \"llm.openai.base_url_prompt.en\") text=\"Base URL\" ;;\n \"llm.openai.model_prompt.zh\") text=\"默认模型 ID\" ;;\n \"llm.openai.model_prompt.en\") text=\"Default Model ID\" ;;\n \"llm.openai.base_url_label.zh\") text=\" Base URL: %s\" ;;\n \"llm.openai.base_url_label.en\") text=\" Base URL: %s\" ;;\n # --- Custom model parameters ---\n \"llm.custom_model.detected.zh\") text=\" ⚠️ 模型 '%s' 不在内置模型列表中,请配置模型参数:\" ;;\n \"llm.custom_model.detected.en\") text=\" ⚠️ Model '%s' is not in the built-in model list. Please configure model parameters:\" ;;\n \"llm.custom_model.context_prompt.zh\") text=\"最大上下文长度(token 数)[150000]\" ;;\n \"llm.custom_model.context_prompt.en\") text=\"Max context window (tokens) [150000]\" ;;\n \"llm.custom_model.max_tokens_prompt.zh\") text=\"最大输出长度(token 数)[128000]\" ;;\n \"llm.custom_model.max_tokens_prompt.en\") text=\"Max output tokens [128000]\" ;;\n \"llm.custom_model.reasoning_prompt.zh\") text=\"是否支持推理/思考模式?[Y/n]\" ;;\n \"llm.custom_model.reasoning_prompt.en\") text=\"Does it support reasoning/thinking mode? [Y/n]\" ;;\n \"llm.custom_model.vision_prompt.zh\") text=\"是否支持图片输入?[y/N]\" ;;\n \"llm.custom_model.vision_prompt.en\") text=\"Does it support image input? [y/N]\" ;;\n \"llm.custom_model.summary.zh\") text=\" 自定义模型参数: 上下文=%s, 最大输出=%s, 推理=%s, 图片=%s\" ;;\n \"llm.custom_model.summary.en\") text=\" Custom model params: context=%s, maxTokens=%s, reasoning=%s, vision=%s\" ;;\n # --- Admin Credentials ---\n \"admin.title.zh\") text=\"--- 管理员凭据 ---\" ;;\n \"admin.title.en\") text=\"--- Admin Credentials ---\" ;;\n \"admin.username_prompt.zh\") text=\"管理员用户名\" ;;\n \"admin.username_prompt.en\") text=\"Admin Username\" ;;\n \"admin.password_prompt.zh\") text=\"管理员密码(留空自动生成,最少 8 位)\" ;;\n \"admin.password_prompt.en\") text=\"Admin Password (leave empty to auto-generate, min 8 chars)\" ;;\n \"admin.password_generated.zh\") text=\" 已自动生成管理员密码\" ;;\n \"admin.password_generated.en\") text=\" Auto-generated admin password\" ;;\n \"admin.password_too_short.zh\") text=\"管理员密码至少需要 8 个字符(MinIO 要求)。当前长度: %s\" ;;\n \"admin.password_too_short.en\") text=\"Admin password must be at least 8 characters (MinIO requirement). Current length: %s\" ;;\n # --- Port Configuration ---\n \"port.title.zh\") text=\"--- 端口配置(按回车使用默认值)---\" ;;\n \"port.title.en\") text=\"--- Port Configuration (press Enter for defaults) ---\" ;;\n \"port.gateway_prompt.zh\") text=\"网关主机端口(容器内 8080)\" ;;\n \"port.gateway_prompt.en\") text=\"Host port for gateway (8080 inside container)\" ;;\n \"port.console_prompt.zh\") text=\"Higress 控制台主机端口(容器内 8001)\" ;;\n \"port.console_prompt.en\") text=\"Host port for Higress console (8001 inside container)\" ;;\n \"port.element_prompt.zh\") text=\"Element Web 直接访问主机端口(容器内 8088)\" ;;\n \"port.element_prompt.en\") text=\"Host port for Element Web direct access (8088 inside container)\" ;;\n \"port.manager_console_prompt.zh\") text=\"Manager 控制台主机端口(容器内 18888)\" ;;\n \"port.manager_console_prompt.en\") text=\"Host port for Manager console (18888 inside container)\" ;;\n \"port.copaw_app_prompt.zh\") text=\"CoPaw App API 主机端口(容器内 18799)\" ;;\n \"port.copaw_app_prompt.en\") text=\"Host port for CoPaw App API (18799 inside container)\" ;;\n # --- Local-only binding ---\n \"port.local_only.title.zh\") text=\"--- 网络访问模式 ---\" ;;\n \"port.local_only.title.en\") text=\"--- Network Access Mode ---\" ;;\n \"port.local_only.prompt.zh\") text=\"是否仅允许本机访问(端口绑定到 127.0.0.1)?\" ;;\n \"port.local_only.prompt.en\") text=\"Bind ports to localhost only (127.0.0.1)?\" ;;\n \"port.local_only.hint_yes.zh\") text=\" 仅本机使用,无需开放外部端口(推荐)\" ;;\n \"port.local_only.hint_yes.en\") text=\" Local use only, no external port exposure (recommended)\" ;;\n \"port.local_only.hint_no.zh\") text=\" 允许外部访问(局域网 / 公网)\" ;;\n \"port.local_only.hint_no.en\") text=\" Allow external access (LAN / public network)\" ;;\n \"port.local_only.choice.zh\") text=\"请选择 [1/2]\" ;;\n \"port.local_only.choice.en\") text=\"Enter choice [1/2]\" ;;\n \"port.local_only.selected_local.zh\") text=\"端口已绑定到 127.0.0.1(仅本机访问)\" ;;\n \"port.local_only.selected_local.en\") text=\"Ports bound to 127.0.0.1 (localhost only)\" ;;\n \"port.local_only.selected_external.zh\") text=\"端口已绑定到所有网络接口(0.0.0.0)\" ;;\n \"port.local_only.selected_external.en\") text=\"Ports bound to all interfaces (0.0.0.0)\" ;;\n \"port.local_only.https_hint.zh\") text=\"⚠️ 建议在 Higress 控制台配置 TLS 证书并启用 HTTPS,避免明文传输。\" ;;\n \"port.local_only.https_hint.en\") text=\"⚠️ It is recommended to configure TLS certificates and enable HTTPS in the Higress Console to avoid plaintext transmission.\" ;;\n \"port.local_only.https_docs.zh\") text=\"\" ;;\n \"port.local_only.https_docs.en\") text=\"\" ;;\n # --- Domain Configuration ---\n \"domain.title.zh\") text=\"--- 域名配置(按回车使用默认值)---\" ;;\n \"domain.title.en\") text=\"--- Domain Configuration (press Enter for defaults) ---\" ;;\n \"domain.hint.zh\") text=\"提示: 自定义域名前必须事先做好 DNS 解析。单机 ECS 部署时无需修改 aigw、fs 等域名;Element Web 和 Matrix Server 也可通过 IP 直接访问。\" ;;\n \"domain.hint.en\") text=\"Hint: Configure DNS resolution before customizing domains. For single ECS deployment, no need to change aigw, fs, etc.; Element Web and Matrix Server can also be accessed directly via IP.\" ;;\n \"domain.matrix_prompt.zh\") text=\"Matrix 域名\" ;;\n \"domain.matrix_prompt.en\") text=\"Matrix Domain\" ;;\n \"domain.element_prompt.zh\") text=\"Element Web 域名\" ;;\n \"domain.element_prompt.en\") text=\"Element Web Domain\" ;;\n \"domain.gateway_prompt.zh\") text=\"AI 网关域名\" ;;\n \"domain.gateway_prompt.en\") text=\"AI Gateway Domain\" ;;\n \"domain.fs_prompt.zh\") text=\"文件系统域名\" ;;\n \"domain.fs_prompt.en\") text=\"File System Domain\" ;;\n \"domain.console_prompt.zh\") text=\"Manager 控制台域名\" ;;\n \"domain.console_prompt.en\") text=\"Manager Console Domain\" ;;\n # --- GitHub Integration ---\n \"github.title.zh\") text=\"--- GitHub 集成(可选,按回车跳过)---\" ;;\n \"github.title.en\") text=\"--- GitHub Integration (optional, press Enter to skip) ---\" ;;\n \"github.token_prompt.zh\") text=\"GitHub 个人访问令牌(可选)\" ;;\n \"github.token_prompt.en\") text=\"GitHub Personal Access Token (optional)\" ;;\n # --- Skills Registry ---\n \"skills.title.zh\") text=\"--- Skills 注册中心(可选,按回车使用默认 nacos://market.agentteams.io:80/public)---\" ;;\n \"skills.title.en\") text=\"--- Skills Registry (optional, press Enter for default nacos://market.agentteams.io:80/public) ---\" ;;\n \"skills.url_prompt.zh\") text=\"Skills 注册中心 URL(留空使用默认 nacos://market.agentteams.io:80/public)\" ;;\n \"skills.url_prompt.en\") text=\"Skills Registry URL (leave empty for default nacos://market.agentteams.io:80/public)\" ;;\n # --- Data Persistence ---\n \"data.title.zh\") text=\"--- 数据持久化 ---\" ;;\n \"data.title.en\") text=\"--- Data Persistence ---\" ;;\n \"data.volume_prompt.zh\") text=\"Docker 卷名称 [agentteams-data]\" ;;\n \"data.volume_prompt.en\") text=\"Docker volume name for persistent data [agentteams-data]\" ;;\n \"data.volume_using.zh\") text=\" 使用 Docker 卷: %s\" ;;\n \"data.volume_using.en\") text=\" Using Docker volume: %s\" ;;\n # --- Manager Workspace ---\n \"workspace.title.zh\") text=\"--- Manager 工作空间 ---\" ;;\n \"workspace.title.en\") text=\"--- Manager Workspace ---\" ;;\n \"workspace.dir_prompt.zh\") text=\"Manager 工作空间目录 [%s]\" ;;\n \"workspace.dir_prompt.en\") text=\"Manager workspace directory [%s]\" ;;\n \"workspace.dir_label.zh\") text=\" Manager 工作空间: %s\" ;;\n \"workspace.dir_label.en\") text=\" Manager workspace: %s\" ;;\n # --- Host directory sharing ---\n \"host_share.prompt.zh\") text=\"与 Agent 共享的主机目录(默认: %s)\" ;;\n \"host_share.prompt.en\") text=\"Host directory to share with agents (default: %s)\" ;;\n \"host_share.sharing.zh\") text=\"共享主机目录: %s -> 容器内 /host-share\" ;;\n \"host_share.sharing.en\") text=\"Sharing host directory: %s -> /host-share in container\" ;;\n \"host_share.not_exist.zh\") text=\"警告: 主机目录 %s 不存在,跳过验证继续使用\" ;;\n \"host_share.not_exist.en\") text=\"WARNING: Host directory %s does not exist, using without validation\" ;;\n # --- Default worker runtime ---\n \"worker_runtime.title.zh\") text=\"--- 默认 Worker 运行时 ---\" ;;\n \"worker_runtime.title.en\") text=\"--- Default Worker Runtime ---\" ;;\n \"worker_runtime.openclaw.zh\") text=\"OpenClaw\" ;;\n \"worker_runtime.openclaw.en\") text=\"OpenClaw\" ;;\n \"worker_runtime.copaw.zh\") text=\"CoPaw\" ;;\n \"worker_runtime.copaw.en\") text=\"CoPaw\" ;;\n \"worker_runtime.hermes.zh\") text=\"Hermes\" ;;\n \"worker_runtime.hermes.en\") text=\"Hermes\" ;;\n \"worker_runtime.choice.zh\") text=\"请选择 [1/2/3]\" ;;\n \"worker_runtime.choice.en\") text=\"Enter choice [1/2/3]\" ;;\n \"worker_runtime.selected.zh\") text=\"默认 Worker 运行时: %s\" ;;\n \"worker_runtime.selected.en\") text=\"Default Worker runtime: %s\" ;;\n \"worker_runtime.title_short.zh\") text=\"默认 Worker 运行时\" ;;\n \"worker_runtime.title_short.en\") text=\"Default Worker Runtime\" ;;\n \"manager_runtime.title.zh\") text=\"--- Manager 运行时 ---\" ;;\n \"manager_runtime.title.en\") text=\"--- Manager Runtime ---\" ;;\n \"manager_runtime.openclaw.zh\") text=\"OpenClaw\" ;;\n \"manager_runtime.openclaw.en\") text=\"OpenClaw\" ;;\n \"manager_runtime.copaw.zh\") text=\"CoPaw\" ;;\n \"manager_runtime.copaw.en\") text=\"CoPaw\" ;;\n \"manager_runtime.choice.zh\") text=\"请选择 [1/2]\" ;;\n \"manager_runtime.choice.en\") text=\"Enter choice [1/2]\" ;;\n \"manager_runtime.selected.zh\") text=\"Manager 运行时: %s\" ;;\n \"manager_runtime.selected.en\") text=\"Manager runtime: %s\" ;;\n \"manager_runtime.title_short.zh\") text=\"Manager 运行时\" ;;\n \"manager_runtime.title_short.en\") text=\"Manager Runtime\" ;;\n # --- Secrets and config ---\n \"install.generating_secrets.zh\") text=\"正在生成密钥...\" ;;\n \"install.generating_secrets.en\") text=\"Generating secrets...\" ;;\n \"install.config_saved.zh\") text=\"配置已保存到 %s\" ;;\n \"install.config_saved.en\") text=\"Configuration saved to %s\" ;;\n # --- Container runtime socket ---\n \"install.socket_detected.zh\") text=\"容器运行时 socket: %s(已启用直接创建 Worker)\" ;;\n \"install.socket_detected.en\") text=\"Container runtime socket: %s (direct Worker creation enabled)\" ;;\n \"install.socket_not_found.zh\") text=\"未找到容器运行时 socket(Manager 无法直接创建 Worker 容器,需要你手动执行 docker 命令创建)\" ;;\n \"install.socket_not_found.en\") text=\"No container runtime socket found (Manager cannot create Worker containers directly, you will need to create them manually using docker commands)\" ;;\n \"install.socket_confirm.title.zh\") text=\"⚠️ 未检测到容器运行时 Socket\" ;;\n \"install.socket_confirm.title.en\") text=\"⚠️ Container Runtime Socket Not Detected\" ;;\n \"install.socket_confirm.message.zh\") text=\"未找到 Docker/Podman socket,Manager 将无法自动创建 Worker 容器。\\n你需要手动执行 docker run 命令来创建 Worker。\\n\\n是否继续安装?\" ;;\n \"install.socket_confirm.message.en\") text=\"Docker/Podman socket not found. Manager will not be able to create Worker containers automatically.\\nYou will need to manually run docker commands to create Workers.\\n\\nContinue installation?\" ;;\n \"install.socket_confirm.prompt.zh\") text=\"继续安装? [y/N]: \" ;;\n \"install.socket_confirm.prompt.en\") text=\"Continue? [y/N]: \" ;;\n \"install.socket_confirm.cancelled.zh\") text=\"安装已取消。如需启用 Worker 自动创建,请确保 Docker/Podman 正在运行,然后重新运行安装脚本。\" ;;\n \"install.socket_confirm.cancelled.en\") text=\"Installation cancelled. To enable automatic Worker creation, ensure Docker/Podman is running and re-run the installer.\" ;;\n # --- Container management ---\n \"install.removing_existing.zh\") text=\"正在移除现有 agentteams-manager 容器...\" ;;\n \"install.removing_existing.en\") text=\"Removing existing agentteams-manager container...\" ;;\n # --- Matrix E2EE ---\n \"matrix_e2ee.title.zh\") text=\"--- Matrix 端到端加密(E2EE)---\" ;;\n \"matrix_e2ee.title.en\") text=\"--- Matrix End-to-End Encryption (E2EE) ---\" ;;\n \"matrix_e2ee.desc.zh\") text=\"E2EE 会对 Manager 与 Worker 之间的 Matrix 消息进行端到端加密。\\n 启用后,即使 Matrix 服务器被入侵,消息内容也无法被窃取。\\n 但 E2EE 会增加首次握手耗时,且要求所有 Agent 都支持 matrix-sdk-crypto。\\n 如果不确定,建议保持禁用。\\n ⚠ 注意:禁用 E2EE 后,请勿在 Element 上创建默认启用加密的 Private 房间,\\n 否则 Agent 将无法读取该房间中的加密消息。请改用 Public 房间或关闭房间加密。\" ;;\n \"matrix_e2ee.desc.en\") text=\"E2EE encrypts Matrix messages between Manager and Workers end-to-end.\\n When enabled, message content stays private even if the Matrix server is compromised.\\n However, E2EE adds overhead to the initial handshake and requires all Agents\\n to support matrix-sdk-crypto. If unsure, keep it disabled.\\n ⚠ Note: When E2EE is disabled, do NOT create Private rooms in Element (which\\n enable encryption by default) — Agents cannot read encrypted messages without\\n E2EE support. Use Public rooms or turn off room encryption instead.\" ;;\n \"matrix_e2ee.enable.zh\") text=\"启用 E2EE\" ;;\n \"matrix_e2ee.enable.en\") text=\"Enable E2EE\" ;;\n \"matrix_e2ee.disable.zh\") text=\"禁用 E2EE(推荐)\" ;;\n \"matrix_e2ee.disable.en\") text=\"Disable E2EE (recommended)\" ;;\n \"matrix_e2ee.choice.zh\") text=\"请选择 [1/2]\" ;;\n \"matrix_e2ee.choice.en\") text=\"Enter choice [1/2]\" ;;\n \"matrix_e2ee.selected_enabled.zh\") text=\"Matrix E2EE: 已启用\" ;;\n \"matrix_e2ee.selected_enabled.en\") text=\"Matrix E2EE: enabled\" ;;\n \"matrix_e2ee.selected_disabled.zh\") text=\"Matrix E2EE: 已禁用(默认)\" ;;\n \"matrix_e2ee.selected_disabled.en\") text=\"Matrix E2EE: disabled (default)\" ;;\n \"matrix_e2ee.title_short.zh\") text=\"Matrix E2EE\" ;;\n \"matrix_e2ee.title_short.en\") text=\"Matrix E2EE\" ;;\n \"matrix_e2ee.val_enabled.zh\") text=\"已启用\" ;;\n \"matrix_e2ee.val_enabled.en\") text=\"enabled\" ;;\n \"matrix_e2ee.val_disabled.zh\") text=\"已禁用\" ;;\n \"matrix_e2ee.val_disabled.en\") text=\"disabled\" ;;\n # --- Docker API proxy ---\n \"docker_proxy.title.zh\") text=\"--- Docker API 安全代理 ---\" ;;\n \"docker_proxy.title.en\") text=\"--- Docker API Security Proxy ---\" ;;\n \"docker_proxy.desc.zh\") text=\"Docker API 代理可防止 AI Agent 通过 Docker API 越狱访问宿主机。\\n 启用后,Manager 不再直接持有 Docker socket,所有容器操作经过安全校验。\" ;;\n \"docker_proxy.desc.en\") text=\"Docker API proxy prevents AI Agents from escaping via Docker API to access the host.\\n When enabled, Manager no longer has direct Docker socket access; all container operations go through security validation.\" ;;\n \"docker_proxy.enable.zh\") text=\"启用(推荐)\" ;;\n \"docker_proxy.enable.en\") text=\"Enable (recommended)\" ;;\n \"docker_proxy.disable.zh\") text=\"禁用(直接挂载 Docker socket)\" ;;\n \"docker_proxy.disable.en\") text=\"Disable (mount Docker socket directly)\" ;;\n \"docker_proxy.choice.zh\") text=\"请选择 [1/2]\" ;;\n \"docker_proxy.choice.en\") text=\"Enter choice [1/2]\" ;;\n \"docker_proxy.selected_enabled.zh\") text=\"Docker API 代理: 已启用\" ;;\n \"docker_proxy.selected_enabled.en\") text=\"Docker API proxy: enabled\" ;;\n \"docker_proxy.selected_disabled.zh\") text=\"Docker API 代理: 已禁用\" ;;\n \"docker_proxy.selected_disabled.en\") text=\"Docker API proxy: disabled\" ;;\n \"docker_proxy.title_short.zh\") text=\"Docker API 代理\" ;;\n \"docker_proxy.title_short.en\") text=\"Docker API Proxy\" ;;\n \"docker_proxy.val_enabled.zh\") text=\"已启用\" ;;\n \"docker_proxy.val_enabled.en\") text=\"enabled\" ;;\n \"docker_proxy.val_disabled.zh\") text=\"已禁用\" ;;\n \"docker_proxy.val_disabled.en\") text=\"disabled\" ;;\n \"docker_proxy.registries_desc.zh\") text=\"默认放行的镜像来源:本地镜像、localhost、Higress 仓库(所有 region)。\\n 如需放行其他镜像仓库,请输入逗号分隔的地址前缀。\\n 示例: ghcr.io/myorg,registry.example.com/team\" ;;\n \"docker_proxy.registries_desc.en\") text=\"Default allowed image sources: local images, localhost, Higress registries (all regions).\\n To allow additional image sources, enter comma-separated address prefixes.\\n Example: ghcr.io/myorg,registry.example.com/team\" ;;\n \"docker_proxy.registries_prompt.zh\") text=\"额外放行的镜像来源(按回车跳过)\" ;;\n \"docker_proxy.registries_prompt.en\") text=\"Additional allowed image sources (press Enter to skip)\" ;;\n \"docker_proxy.registries_label.zh\") text=\"额外放行的镜像来源\" ;;\n \"docker_proxy.registries_label.en\") text=\"Additional allowed image sources\" ;;\n # --- Podman Autostart ---\n \"podman.autostart.title.zh\") text=\"--- Podman 驻留与开机自启 ---\" ;;\n \"podman.autostart.title.en\") text=\"--- Podman Linger & Autostart ---\" ;;\n \"podman.autostart.desc.zh\") text=\"配置为 systemd 服务,支持开机自动启动及后台驻留。\" ;;\n \"podman.autostart.desc.en\") text=\"Configure as a systemd service for automatic startup on boot.\" ;;\n \"podman.autostart.enable.zh\") text=\"启用(推荐)\" ;;\n \"podman.autostart.enable.en\") text=\"Enable (recommended)\" ;;\n \"podman.autostart.disable.zh\") text=\"不启用(容器随当前会话关闭而停止)\" ;;\n \"podman.autostart.disable.en\") text=\"Disable (containers stop when session ends)\" ;;\n \"podman.autostart.choice.zh\") text=\"请选择 [1/2]\" ;;\n \"podman.autostart.choice.en\") text=\"Enter choice [1/2]\" ;;\n \"podman.autostart.selected_enabled.zh\") text=\"Podman 开机自启: 已启用\" ;;\n \"podman.autostart.selected_enabled.en\") text=\"Podman Autostart: enabled\" ;;\n \"podman.autostart.selected_disabled.zh\") text=\"Podman 开机自启: 已禁用\" ;;\n \"podman.autostart.selected_disabled.en\") text=\"Podman Autostart: disabled\" ;;\n \"podman.autostart.title_short.zh\") text=\"Podman 开机自启\" ;;\n \"podman.autostart.title_short.en\") text=\"Podman Autostart\" ;;\n \"podman.autostart.val_enabled.zh\") text=\"已启用\" ;;\n \"podman.autostart.val_enabled.en\") text=\"enabled\" ;;\n \"podman.autostart.val_disabled.zh\") text=\"已禁用\" ;;\n \"podman.autostart.val_disabled.en\") text=\"disabled\" ;;\n \"install.podman.autostart_title.zh\") text=\"正在配置 Podman 容器开机自启...\" ;;\n \"install.podman.autostart_title.en\") text=\"Configuring Podman container autostart...\" ;;\n \"install.podman.linger_enable.zh\") text=\"尝试启用 systemd linger (如有提示请在此输入 sudo 密码)...\" ;;\n \"install.podman.linger_enable.en\") text=\"Attempting to enable systemd linger (enter sudo password if prompted)...\" ;;\n \"install.podman.linger_warn.zh\") text=\"警告: 无法自动启用 linger。驻留可能失败,请手动执行: sudo loginctl enable-linger \\$(whoami)\" ;;\n \"install.podman.linger_warn.en\") text=\"WARNING: Failed to auto-enable linger. Please run manually: sudo loginctl enable-linger \\$(whoami)\" ;;\n \"install.podman.root_setup.zh\") text=\"检测到 root 用户。正在配置系统级 agentteams-podman-restart 服务...\" ;;\n \"install.podman.root_setup.en\") text=\"Root user detected. Configuring system-wide agentteams-podman-restart service...\" ;;\n \"install.podman.root_success.zh\") text=\"✅ 系统级 agentteams-podman-restart 服务已成功启用并运行。\" ;;\n \"install.podman.root_success.en\") text=\"✅ System-wide agentteams-podman-restart service successfully enabled and running.\" ;;\n \"install.podman.root_fail.zh\") text=\"⚠️ 系统级 agentteams-podman-restart 服务启用失败,请稍后手动检查。\" ;;\n \"install.podman.root_fail.en\") text=\"⚠️ Failed to enable system-wide agentteams-podman-restart service. Please check manually later.\" ;;\n \"install.podman.user_setup.zh\") text=\"检测到普通用户 (%s)。正在配置用户级 agentteams-podman-restart 服务...\" ;;\n \"install.podman.user_setup.en\") text=\"Non-root user (%s) detected. Configuring user-level agentteams-podman-restart service...\" ;;\n \"install.podman.user_success.zh\") text=\"✅ 用户级 agentteams-podman-restart 服务已成功启用并运行。\" ;;\n \"install.podman.user_success.en\") text=\"✅ User-level agentteams-podman-restart service successfully enabled and running.\" ;;\n \"install.podman.user_fail.zh\") text=\"⚠️ 用户级 agentteams-podman-restart 服务启用失败。\\n提示:这通常是因为缺少 XDG_RUNTIME_DIR 或 dbus 没有运行。您可以尝试手动执行: systemctl --user enable --now agentteams-podman-restart.service\" ;;\n \"install.podman.user_fail.en\") text=\"⚠️ Failed to enable user-level agentteams-podman-restart service.\\nHint: This is usually due to missing XDG_RUNTIME_DIR or dbus not running. Try manually: systemctl --user enable --now agentteams-podman-restart.service\" ;;\n \"install.podman.success.zh\") text=\"Podman 开机自启配置完成。\" ;;\n \"install.podman.success.en\") text=\"Podman autostart successfully configured.\" ;;\n # --- Worker idle timeout ---\n \"idle_timeout.prompt.zh\") text=\"Worker 空闲自动停止超时(分钟)[720]\" ;;\n \"idle_timeout.prompt.en\") text=\"Worker idle auto-stop timeout in minutes [720]\" ;;\n \"idle_timeout.selected.zh\") text=\"Worker 空闲超时: %s 分钟\" ;;\n \"idle_timeout.selected.en\") text=\"Worker idle timeout: %s minutes\" ;;\n \"idle_timeout.label.zh\") text=\"Worker 空闲超时(分钟)\" ;;\n \"idle_timeout.label.en\") text=\"Worker idle timeout (min)\" ;;\n # --- YOLO mode ---\n \"install.yolo.zh\") text=\"YOLO 模式已启用(自主决策,无交互提示)\" ;;\n \"install.yolo.en\") text=\"YOLO mode enabled (autonomous decisions, no interactive prompts)\" ;;\n # --- Image pulling ---\n \"install.image.exists.zh\") text=\"Manager 镜像已存在: %s\" ;;\n \"install.image.exists.en\") text=\"Manager image already exists locally: %s\" ;;\n \"install.image.pulling_manager.zh\") text=\"正在拉取 Manager 镜像: %s\" ;;\n \"install.image.pulling_manager.en\") text=\"Pulling Manager image: %s\" ;;\n \"install.image.worker_exists.zh\") text=\"Worker 镜像已存在: %s\" ;;\n \"install.image.worker_exists.en\") text=\"Worker image already exists locally: %s\" ;;\n \"install.image.pulling_worker.zh\") text=\"正在拉取 Worker 镜像: %s\" ;;\n \"install.image.pulling_worker.en\") text=\"Pulling Worker image: %s\" ;;\n # --- Starting container ---\n \"install.starting_manager.zh\") text=\"正在启动 Manager 容器...\" ;;\n \"install.starting_manager.en\") text=\"Starting Manager container...\" ;;\n # --- Wait for Manager ready ---\n \"install.wait_ready.zh\") text=\"等待 Manager Agent 就绪(超时: %ss)...\" ;;\n \"install.wait_ready.en\") text=\"Waiting for Manager agent to be ready (timeout: %ss)...\" ;;\n \"install.wait_ready.ok.zh\") text=\"Manager Agent 已就绪!\" ;;\n \"install.wait_ready.ok.en\") text=\"Manager agent is ready!\" ;;\n \"install.wait_ready.waiting.zh\") text=\"等待中... (%ds/%ds)\" ;;\n \"install.wait_ready.waiting.en\") text=\"Waiting... (%ds/%ds)\" ;;\n \"install.wait_ready.timeout.zh\") text=\"Manager Agent 在 %ss 内未就绪。请检查: docker logs %s\" ;;\n \"install.wait_ready.timeout.en\") text=\"Manager agent did not become ready within %ss. Check: docker logs %s\" ;;\n # --- Wait for Matrix ready ---\n \"install.wait_matrix.zh\") text=\"等待 Matrix 服务就绪(超时: %ss)...\" ;;\n \"install.wait_matrix.en\") text=\"Waiting for Matrix server to be ready (timeout: %ss)...\" ;;\n \"install.wait_matrix.ok.zh\") text=\"Matrix 服务已就绪!\" ;;\n \"install.wait_matrix.ok.en\") text=\"Matrix server is ready!\" ;;\n \"install.wait_matrix.waiting.zh\") text=\"等待 Matrix 中... (%ds/%ds)\" ;;\n \"install.wait_matrix.waiting.en\") text=\"Waiting for Matrix... (%ds/%ds)\" ;;\n \"install.wait_matrix.timeout.zh\") text=\"Matrix 服务在 %ss 内未就绪。请检查: docker logs %s\" ;;\n \"install.wait_matrix.timeout.en\") text=\"Matrix server did not become ready within %ss. Check: docker logs %s\" ;;\n # --- OpenAI-compatible connectivity test ---\n \"llm.openai.test.testing.zh\") text=\"正在测试 API 联通性...\" ;;\n \"llm.openai.test.testing.en\") text=\"Testing API connectivity...\" ;;\n \"llm.openai.test.ok.zh\") text=\"✅ API 联通性测试通过\" ;;\n \"llm.openai.test.ok.en\") text=\"✅ API connectivity test passed\" ;;\n \"llm.openai.test.fail.zh\") text=\"⚠️ API 联通性测试失败(HTTP %s)。响应内容:\\n%s\\n请根据以上错误信息联系您的模型服务商解决。\" ;;\n \"llm.openai.test.fail.en\") text=\"⚠️ API connectivity test failed (HTTP %s). Response body:\\n%s\\nPlease contact your model provider to resolve the issue.\" ;;\n \"llm.openai.test.fail.tokenplan.zh\") text=\"⚠️ 提示: 请确认 API Key 有效且已开通通义 Token 套餐。文档: https://help.aliyun.com/zh/model-studio/token-plan-quickstart\" ;;\n \"llm.openai.test.fail.tokenplan.en\") text=\"⚠️ Hint: Verify your Token Plan API key and compatible-mode access. Docs: https://help.aliyun.com/zh/model-studio/token-plan-quickstart\" ;;\n \"llm.openai.test.fail.codingplan.zh\") text=\"⚠️ 提示: 请确认 API Key 有效且已开通通义 Token 套餐。文档: https://help.aliyun.com/zh/model-studio/token-plan-quickstart\" ;;\n \"llm.openai.test.fail.codingplan.en\") text=\"⚠️ Hint: Verify your DASHSCOPE_API_KEY for Qwen Cloud. API keys: https://home.qwencloud.com/api-keys Docs: https://docs.qwencloud.com/\" ;;\n \"llm.openai.test.fail.codingplan_legacy.zh\") text=\"⚠️ 提示: 请确认 API Key 有效且 Coding 套餐接口可用。文档: https://help.aliyun.com/zh/model-studio/get-api-key\" ;;\n \"llm.openai.test.fail.codingplan_legacy.en\") text=\"⚠️ Hint: Verify your DashScope API key and Coding Plan access. Docs: https://help.aliyun.com/zh/model-studio/get-api-key\" ;;\n \"llm.openai.test.no_curl.zh\") text=\"⚠️ 未找到 curl,跳过 API 联通性测试\" ;;\n \"llm.openai.test.no_curl.en\") text=\"⚠️ curl not found, skipping API connectivity test\" ;;\n \"llm.openai.test.confirm.zh\") text=\"是否仍要继续安装?[y/N/b] \" ;;\n \"llm.openai.test.confirm.en\") text=\"Continue with installation anyway? [y/N/b] \" ;;\n \"llm.embedding.title.zh\") text=\"📦 记忆搜索配置\" ;;\n \"llm.embedding.title.en\") text=\"📦 Memory Search Configuration\" ;;\n \"llm.embedding.hint.zh\") text=\" Embedding 模型可提升记忆搜索质量(语义匹配)。不启用也可正常使用记忆功能(关键词匹配)。\" ;;\n \"llm.embedding.hint.en\") text=\" Embedding model improves memory search quality (semantic matching). Memory still works without it (keyword matching).\" ;;\n \"llm.embedding.option.default.zh\") text=\" 1) text-embedding-v4(推荐)\" ;;\n \"llm.embedding.option.default.en\") text=\" 1) text-embedding-v4 (Recommended)\" ;;\n \"llm.embedding.option.custom.zh\") text=\" 2) 自定义 Embedding 模型\" ;;\n \"llm.embedding.option.custom.en\") text=\" 2) Custom embedding model\" ;;\n \"llm.embedding.option.disable.zh\") text=\" 3) 不启用\" ;;\n \"llm.embedding.option.disable.en\") text=\" 3) Do not enable\" ;;\n \"llm.embedding.select.zh\") text=\"选择\" ;;\n \"llm.embedding.select.en\") text=\"Select\" ;;\n \"llm.embedding.custom_prompt.zh\") text=\" Embedding 模型名称\" ;;\n \"llm.embedding.custom_prompt.en\") text=\" Embedding model name\" ;;\n \"llm.embedding.test.testing.zh\") text=\"正在测试 Embedding API 联通性...\" ;;\n \"llm.embedding.test.testing.en\") text=\"Testing Embedding API connectivity...\" ;;\n \"llm.embedding.test.ok.zh\") text=\"✅ Embedding API 联通性测试通过\" ;;\n \"llm.embedding.test.ok.en\") text=\"✅ Embedding API connectivity test passed\" ;;\n \"llm.embedding.test.fail.zh\") text=\"⚠️ Embedding API 测试失败(HTTP %s)。响应: %s\" ;;\n \"llm.embedding.test.fail.en\") text=\"⚠️ Embedding API test failed (HTTP %s). Response: %s\" ;;\n \"llm.embedding.auto_disabled.zh\") text=\"⚠️ Embedding 已自动禁用,记忆搜索将使用关键词匹配。您可以稍后在 agentteams-manager.env 中设置 AGENTTEAMS_EMBEDDING_MODEL 启用。\" ;;\n \"llm.embedding.auto_disabled.en\") text=\"⚠️ Embedding auto-disabled. Memory search will use keyword matching. You can enable it later in agentteams-manager.env by setting AGENTTEAMS_EMBEDDING_MODEL.\" ;;\n \"llm.embedding.disabled.zh\") text=\"ℹ️ Embedding 已禁用,记忆搜索将使用关键词匹配。\" ;;\n \"llm.embedding.disabled.en\") text=\"ℹ️ Embedding disabled. Memory search will use keyword matching.\" ;;\n \"llm.openai.test.aborted.zh\") text=\"安装已中止。\" ;;\n \"llm.openai.test.aborted.en\") text=\"Installation aborted.\" ;;\n \"nav.back_hint.zh\") text=\"(输入 b 返回上一步)\" ;;\n \"nav.back_hint.en\") text=\"(enter b to go back)\" ;;\n # --- OpenAI-compatible provider creation ---\n \"install.openai_compat.missing.zh\") text=\"警告: OpenAI Base URL 或 API Key 未设置,跳过提供商创建\" ;;\n \"install.openai_compat.missing.en\") text=\"WARNING: OpenAI Base URL or API Key not set, skipping provider creation\" ;;\n \"install.openai_compat.creating.zh\") text=\"正在创建 OpenAI 兼容提供商...\" ;;\n \"install.openai_compat.creating.en\") text=\"Creating OpenAI-compatible provider...\" ;;\n \"install.openai_compat.domain.zh\") text=\" 域名: %s\" ;;\n \"install.openai_compat.domain.en\") text=\" Domain: %s\" ;;\n \"install.openai_compat.port.zh\") text=\" 端口: %s\" ;;\n \"install.openai_compat.port.en\") text=\" Port: %s\" ;;\n \"install.openai_compat.protocol.zh\") text=\" 协议: %s\" ;;\n \"install.openai_compat.protocol.en\") text=\" Protocol: %s\" ;;\n \"install.openai_compat.service_fail.zh\") text=\"警告: 创建 DNS 服务源失败(可能已存在)\" ;;\n \"install.openai_compat.service_fail.en\") text=\"WARNING: Failed to create DNS service source (may already exist)\" ;;\n \"install.openai_compat.provider_fail.zh\") text=\"警告: 创建 AI 提供商失败(可能已存在)\" ;;\n \"install.openai_compat.provider_fail.en\") text=\"WARNING: Failed to create AI provider (may already exist)\" ;;\n \"install.openai_compat.success.zh\") text=\"OpenAI 兼容提供商创建成功\" ;;\n \"install.openai_compat.success.en\") text=\"OpenAI-compatible provider created successfully\" ;;\n # --- Welcome message ---\n \"install.welcome_msg.soul_configured.zh\") text=\"Soul 已配置(找到 soul-configured 标记),跳过 onboarding 消息\" ;;\n \"install.welcome_msg.soul_configured.en\") text=\"Soul already configured (soul-configured marker found), skipping onboarding message\" ;;\n \"install.welcome_msg.logging_in.zh\") text=\"正在以 %s 身份登录以发送欢迎消息...\" ;;\n \"install.welcome_msg.logging_in.en\") text=\"Logging in as %s to send welcome message...\" ;;\n \"install.welcome_msg.login_failed.zh\") text=\"警告: 以 %s 身份登录失败,跳过欢迎消息\" ;;\n \"install.welcome_msg.login_failed.en\") text=\"WARNING: Failed to login as %s, skipping welcome message\" ;;\n \"install.welcome_msg.finding_room.zh\") text=\"正在查找与 Manager 的 DM 房间...\" ;;\n \"install.welcome_msg.finding_room.en\") text=\"Finding DM room with Manager...\" ;;\n \"install.welcome_msg.creating_room.zh\") text=\"正在创建与 Manager 的 DM 房间...\" ;;\n \"install.welcome_msg.creating_room.en\") text=\"Creating DM room with Manager...\" ;;\n \"install.welcome_msg.no_room.zh\") text=\"警告: 无法找到或创建与 Manager 的 DM 房间\" ;;\n \"install.welcome_msg.no_room.en\") text=\"WARNING: Could not find or create DM room with Manager\" ;;\n \"install.welcome_msg.waiting_join.zh\") text=\"等待 Manager 加入房间...\" ;;\n \"install.welcome_msg.waiting_join.en\") text=\"Waiting for Manager to join the room...\" ;;\n \"install.welcome_msg.sending.zh\") text=\"正在向 Manager 发送欢迎消息...\" ;;\n \"install.welcome_msg.sending.en\") text=\"Sending welcome message to Manager...\" ;;\n \"install.welcome_msg.send_failed.zh\") text=\"警告: 发送欢迎消息失败\" ;;\n \"install.welcome_msg.send_failed.en\") text=\"WARNING: Failed to send welcome message\" ;;\n \"install.welcome_msg.sent.zh\") text=\"欢迎消息已发送给 Manager\" ;;\n \"install.welcome_msg.sent.en\") text=\"Welcome message sent to Manager\" ;;\n \"install.welcome_msg.waiting.zh\") text=\"等待 Manager 发送欢迎消息(Higress 路由授权 + LLM 探活,约 45-90s)...\" ;;\n \"install.welcome_msg.waiting.en\") text=\"Waiting for Manager to send the welcome message (Higress route auth + LLM probe, ~45-90s)...\" ;;\n \"install.welcome_msg.confirmed.zh\") text=\"Manager 已确认发送欢迎消息(status.welcomeSent=true,用时 %ss)\" ;;\n \"install.welcome_msg.confirmed.en\") text=\"Manager confirmed welcome message sent (status.welcomeSent=true, %ss elapsed)\" ;;\n \"install.welcome_msg.timeout.zh\") text=\"警告: 在 %ss 内未观察到 Manager 发送欢迎消息(status.welcomeSent=true)。安装仍然成功,所有服务已就绪——可继续按下方提示登录 Element Web。\" ;;\n \"install.welcome_msg.timeout.en\") text=\"WARNING: Did not observe the Manager sending its welcome message (status.welcomeSent=true) within %ss. Installation is still successful, all services are up — continue with the Element Web instructions below.\" ;;\n \"install.welcome_msg.timeout_hint.zh\") text=\"手动触发 onboarding: 登录 Element Web → 打开与 Manager 的 DM 房间 → 发送任意一句话(例如 \\\"hi\\\"),Manager 会接管对话并开始引导。\" ;;\n \"install.welcome_msg.timeout_hint.en\") text=\"Manual onboarding: log in to Element Web → open the DM with the Manager → send any message (e.g. \\\"hi\\\") and the Manager will take over and start the guided setup.\" ;;\n \"install.welcome_msg.timeout_inspect.zh\") text=\"排查命令: docker exec agentteams-controller agt get managers default\" ;;\n \"install.welcome_msg.timeout_inspect.en\") text=\"Inspect status: docker exec agentteams-controller agt get managers default\" ;;\n \"install.welcome_msg.poll_unavailable.zh\") text=\"提示: agentteams-manager 内未找到 AgentTeams CLI,跳过 welcome 等待(旧镜像?)\" ;;\n \"install.welcome_msg.poll_unavailable.en\") text=\"Note: AgentTeams CLI not found inside agentteams-manager; skipping welcome wait (old image?)\" ;;\n # --- Dashboard wizard ---\n \"dash.prompt.zh\") text=\"是否安装 agentteams-dashboard 管理面板?\" ;;\n \"dash.prompt.en\") text=\"Install agentteams-dashboard management UI?\" ;;\n \"dash.version_prompt.zh\") text=\"Dashboard 版本\" ;;\n \"dash.version_prompt.en\") text=\"Dashboard version\" ;;\n \"dash.port_prompt.zh\") text=\"Dashboard 端口号\" ;;\n \"dash.port_prompt.en\") text=\"Dashboard port\" ;;\n \"dash.image_prompt.zh\") text=\"Dashboard 镜像\" ;;\n \"dash.image_prompt.en\") text=\"Dashboard image\" ;;\n \"dash.gateway_prompt.zh\") text=\"Higress Console URL (用于共享登录)\" ;;\n \"dash.gateway_prompt.en\") text=\"Higress Console URL (for shared login)\" ;;\n \"dash.skip.zh\") text=\"跳过 Dashboard 安装\" ;;\n \"dash.skip.en\") text=\"Skipping Dashboard installation\" ;;\n \"dash.auto_url.zh\") text=\"自动检测到 Higress Console URL\" ;;\n \"dash.auto_url.en\") text=\"Auto-detected Higress Console URL\" ;;\n \"dash.auto_url_fail.zh\") text=\"无法自动检测 Higress Console URL,请手动输入\" ;;\n \"dash.auto_url_fail.en\") text=\"Cannot auto-detect Higress Console URL, please enter manually\" ;;\n \"dash.summary.zh\") text=\"Dashboard 配置: 端口=%s 镜像=%s\" ;;\n \"dash.summary.en\") text=\"Dashboard config: port=%s image=%s\" ;;\n # --- Dashboard success ---\n \"success.dashboard.zh\") text=\" agentteams-dashboard 管理面板: http://localhost:%s/\" ;;\n \"success.dashboard.en\") text=\" agentteams-dashboard: http://localhost:%s/\" ;;\n # --- Final output panel ---\n \"success.title.zh\") text=\"=== AgentTeams Manager 已启动!===\" ;;\n \"success.title.en\") text=\"=== AgentTeams Manager Started! ===\" ;;\n \"success.domains_configured.zh\") text=\"以下域名已配置解析到 127.0.0.1:\" ;;\n \"success.domains_configured.en\") text=\"The following domains are configured to resolve to 127.0.0.1:\" ;;\n \"success.open_url.zh\") text=\" ★ 在浏览器中打开以下 URL 开始使用: ★\" ;;\n \"success.open_url.en\") text=\" ★ Open the following URL in your browser to start: ★\" ;;\n \"success.login_with.zh\") text=\" 登录信息:\" ;;\n \"success.login_with.en\") text=\" Login with:\" ;;\n \"success.username.zh\") text=\" 用户名: %s\" ;;\n \"success.username.en\") text=\" Username: %s\" ;;\n \"success.password.zh\") text=\" 密码: %s\" ;;\n \"success.password.en\") text=\" Password: %s\" ;;\n \"success.after_login.zh\") text=\" 登录后,开始与 Manager 聊天!\" ;;\n \"success.after_login.en\") text=\" After login, start chatting with the Manager!\" ;;\n \"success.tell_it.zh\") text=\" 告诉它: \\\"创建一个名为 alice 的前端开发 Worker\\\"\" ;;\n \"success.tell_it.en\") text=\" Tell it: \\\"Create a Worker named alice for frontend dev\\\"\" ;;\n \"success.manager_auto.zh\") text=\" Manager 会自动处理一切。\" ;;\n \"success.manager_auto.en\") text=\" The Manager will handle everything automatically.\" ;;\n \"success.mobile_title.zh\") text=\" 📱 移动端访问(FluffyChat / Element Mobile):\" ;;\n \"success.mobile_title.en\") text=\" 📱 Mobile access (FluffyChat / Element Mobile):\" ;;\n \"success.mobile_step1.zh\") text=\" 1. 在手机上下载 FluffyChat 或 Element\" ;;\n \"success.mobile_step1.en\") text=\" 1. Download FluffyChat or Element on your phone\" ;;\n \"success.mobile_step2.zh\") text=\" 2. 设置 homeserver 为: %s\" ;;\n \"success.mobile_step2.en\") text=\" 2. Set homeserver to: %s\" ;;\n \"success.mobile_step2_noip.zh\") text=\" 2. 设置 homeserver 为: http://<本机局域网IP>:%s\" ;;\n \"success.mobile_step2_noip.en\") text=\" 2. Set homeserver to: http://<this-machine-LAN-IP>:%s\" ;;\n \"success.mobile_noip_hint.zh\") text=\" (无法自动检测局域网 IP — 请使用 ifconfig / ip addr 查看)\" ;;\n \"success.mobile_noip_hint.en\") text=\" (Could not detect LAN IP automatically — check with: ifconfig / ip addr)\" ;;\n \"success.mobile_step3.zh\") text=\" 3. 登录信息:\" ;;\n \"success.mobile_step3.en\") text=\" 3. Login with:\" ;;\n \"success.mobile_username.zh\") text=\" 用户名: %s\" ;;\n \"success.mobile_username.en\") text=\" Username: %s\" ;;\n \"success.mobile_password.zh\") text=\" 密码: %s\" ;;\n \"success.mobile_password.en\") text=\" Password: %s\" ;;\n # --- Other consoles and tips ---\n \"success.other_consoles.zh\") text=\"--- 其他控制台 ---\" ;;\n \"success.other_consoles.en\") text=\"--- Other Consoles ---\" ;;\n \"success.higress_console.zh\") text=\" Higress 控制台: http://localhost:%s(用户名: %s / 密码: %s)\" ;;\n \"success.higress_console.en\") text=\" Higress Console: http://localhost:%s (Username: %s / Password: %s)\" ;;\n \"success.manager_console.zh\") text=\" Manager 控制台(本地): http://localhost:%s(无需登录)\" ;;\n \"success.manager_console.en\") text=\" Manager Console (local): http://localhost:%s (no login required)\" ;;\n \"success.manager_console_gateway.zh\") text=\" Manager 控制台(网关): http://console-local.agentteams.io(用户名: %s / 密码: %s)\" ;;\n \"success.manager_console_gateway.en\") text=\" Manager Console (gateway): http://console-local.agentteams.io (Username: %s / Password: %s)\" ;;\n \"success.copaw_console.zh\") text=\" CoPaw App API: http://localhost:%s(无需登录)\" ;;\n \"success.copaw_console.en\") text=\" CoPaw App API: http://localhost:%s (no login required)\" ;;\n \"success.switch_llm.title.zh\") text=\"--- 切换 LLM 提供商 ---\" ;;\n \"success.switch_llm.title.en\") text=\"--- Switch LLM Providers ---\" ;;\n \"success.switch_llm.hint.zh\") text=\" 您可以通过 Higress 控制台切换到其他 LLM 提供商(OpenAI、Anthropic 等)。\" ;;\n \"success.switch_llm.hint.en\") text=\" You can switch to other LLM providers (OpenAI, Anthropic, etc.) via Higress Console.\" ;;\n \"success.switch_llm.docs.zh\") text=\" 详细说明请参阅:\" ;;\n \"success.switch_llm.docs.en\") text=\" For detailed instructions, see:\" ;;\n \"success.switch_llm.url.zh\") text=\" https://higress.ai/en/docs/ai/scene-guide/multi-proxy#console-configuration\" ;;\n \"success.switch_llm.url.en\") text=\" https://higress.ai/en/docs/ai/scene-guide/multi-proxy#console-configuration\" ;;\n \"success.tip.zh\") text=\"提示: 您也可以在聊天中让 Manager 为您配置 LLM 提供商。\" ;;\n \"success.tip.en\") text=\"Tip: You can also ask the Manager to configure LLM providers for you in the chat.\" ;;\n \"success.config_file.zh\") text=\"配置文件: %s\" ;;\n \"success.config_file.en\") text=\"Configuration file: %s\" ;;\n \"success.data_volume.zh\") text=\"数据卷: %s\" ;;\n \"success.data_volume.en\") text=\"Data volume: %s\" ;;\n \"success.workspace.zh\") text=\"Manager 工作空间: %s\" ;;\n \"success.workspace.en\") text=\"Manager workspace: %s\" ;;\n # --- Worker installation ---\n \"worker.resetting.zh\") text=\"正在重置 Worker: %s...\" ;;\n \"worker.resetting.en\") text=\"Resetting Worker: %s...\" ;;\n \"worker.exists.zh\") text=\"容器 '%s' 已存在。使用 --reset 重新创建。\" ;;\n \"worker.exists.en\") text=\"Container '%s' already exists. Use --reset to recreate.\" ;;\n \"worker.starting.zh\") text=\"正在启动 Worker: %s...\" ;;\n \"worker.starting.en\") text=\"Starting Worker: %s...\" ;;\n \"worker.skills_url.zh\") text=\" Skills API URL: %s\" ;;\n \"worker.skills_url.en\") text=\" Skills API URL: %s\" ;;\n \"worker.started.zh\") text=\"=== Worker %s 已启动!===\" ;;\n \"worker.started.en\") text=\"=== Worker %s Started! ===\" ;;\n \"worker.container.zh\") text=\"容器: %s\" ;;\n \"worker.container.en\") text=\"Container: %s\" ;;\n \"worker.view_logs.zh\") text=\"查看日志: docker logs -f %s\" ;;\n \"worker.view_logs.en\") text=\"View logs: docker logs -f %s\" ;;\n # --- Prompt function messages ---\n \"prompt.preset.zh\") text=\" %s = (已通过环境变量预设)\" ;;\n \"prompt.preset.en\") text=\" %s = (pre-set via env)\" ;;\n \"prompt.upgrade_keep.zh\") text=\" %s = %s(当前值,回车保留 / 输入新值覆盖)\" ;;\n \"prompt.upgrade_keep.en\") text=\" %s = %s (current value, press Enter to keep / type new value to change)\" ;;\n \"prompt.upgrade_keep_secret.zh\") text=\" %s = %s(当前值,回车保留 / 输入新值覆盖)\" ;;\n \"prompt.upgrade_keep_secret.en\") text=\" %s = %s (current value, press Enter to keep / type new value to change)\" ;;\n \"prompt.upgrade_empty.zh\") text=\" %s = (未设置,回车跳过 / 输入新值设置)\" ;;\n \"prompt.upgrade_empty.en\") text=\" %s = (not set, press Enter to skip / type new value to set)\" ;;\n \"prompt.default.zh\") text=\" %s = %s(默认)\" ;;\n \"prompt.default.en\") text=\" %s = %s (default)\" ;;\n \"prompt.required.zh\") text=\"%s 是必需的(在非交互模式下通过环境变量设置)\" ;;\n \"prompt.required.en\") text=\"%s is required (set via environment variable in non-interactive mode)\" ;;\n \"prompt.required_empty.zh\") text=\"%s 是必需的\" ;;\n \"prompt.required_empty.en\") text=\"%s is required\" ;;\n # --- Language switch prompt (bilingual by design) ---\n \"lang.detected.zh\") text=\"检测到语言 / Detected language: 中文\" ;;\n \"lang.detected.en\") text=\"检测到语言 / Detected language: English\" ;;\n \"lang.switch_title.zh\") text=\"切换语言 / Switch language:\" ;;\n \"lang.switch_title.en\") text=\"切换语言 / Switch language:\" ;;\n \"lang.option_zh.zh\") text=\" 1) 中文\" ;;\n \"lang.option_zh.en\") text=\" 1) 中文\" ;;\n \"lang.option_en.zh\") text=\" 2) English\" ;;\n \"lang.option_en.en\") text=\" 2) English\" ;;\n \"lang.prompt.zh\") text=\"请选择 / Enter choice\" ;;\n \"lang.prompt.en\") text=\"请选择 / Enter choice\" ;;\n # --- Uninstall messages ---\n \"uninstall.title.zh\") text=\"正在卸载 AgentTeams...\" ;;\n \"uninstall.title.en\") text=\"Uninstalling AgentTeams...\" ;;\n \"uninstall.stopping_manager.zh\") text=\"正在停止并移除 agentteams-manager...\" ;;\n \"uninstall.stopping_manager.en\") text=\"Stopping and removing agentteams-manager...\" ;;\n \"uninstall.stopping_workers.zh\") text=\"正在停止并移除 Worker 容器...\" ;;\n \"uninstall.stopping_workers.en\") text=\"Stopping and removing worker containers...\" ;;\n \"uninstall.removed.zh\") text=\" 已移除: %s\" ;;\n \"uninstall.removed.en\") text=\" Removed: %s\" ;;\n \"uninstall.removing_volume.zh\") text=\"正在移除 Docker 卷: %s\" ;;\n \"uninstall.removing_volume.en\") text=\"Removing Docker volume: %s\" ;;\n \"uninstall.removing_env.zh\") text=\"正在移除 env 文件: %s\" ;;\n \"uninstall.removing_env.en\") text=\"Removing env file: %s\" ;;\n \"uninstall.removing_proxy.zh\") text=\"正在停止并移除 Docker API 代理容器: agentteams-docker-proxy\" ;;\n \"uninstall.removing_proxy.en\") text=\"Stopping and removing Docker API proxy container: agentteams-docker-proxy\" ;;\n \"uninstall.stopping_controller.zh\") text=\"正在停止并移除 agentteams-controller (内嵌 Tuwunel/MinIO/Higress)...\" ;;\n \"uninstall.stopping_controller.en\") text=\"Stopping and removing agentteams-controller (embedded Tuwunel/MinIO/Higress)...\" ;;\n \"uninstall.removing_network.zh\") text=\"正在移除 Docker 网络: agentteams-net\" ;;\n \"uninstall.removing_network.en\") text=\"Removing Docker network: agentteams-net\" ;;\n \"uninstall.removing_workspace.zh\") text=\"正在移除工作空间目录: %s\" ;;\n \"uninstall.removing_workspace.en\") text=\"Removing workspace directory: %s\" ;;\n \"uninstall.removing_workspace_elevated.zh\") text=\" 工作空间包含 root 文件,通过容器清理...\" ;;\n \"uninstall.removing_workspace_elevated.en\") text=\" Workspace contains root-owned files, cleaning via container...\" ;;\n \"uninstall.removing_log.zh\") text=\"正在移除日志文件: %s\" ;;\n \"uninstall.removing_log.en\") text=\"Removing log file: %s\" ;;\n \"uninstall.done.zh\") text=\"AgentTeams 已卸载。\" ;;\n \"uninstall.done.en\") text=\"AgentTeams has been uninstalled.\" ;;\n # --- Error messages ---\n \"error.name_required.zh\") text=\"--name 是必需的\" ;;\n \"error.name_required.en\") text=\"--name is required\" ;;\n \"error.fs_required.zh\") text=\"--fs 是必需的\" ;;\n \"error.fs_required.en\") text=\"--fs is required\" ;;\n \"error.fs_key_required.zh\") text=\"--fs-key 是必需的\" ;;\n \"error.fs_key_required.en\") text=\"--fs-key is required\" ;;\n \"error.fs_secret_required.zh\") text=\"--fs-secret 是必需的\" ;;\n \"error.fs_secret_required.en\") text=\"--fs-secret is required\" ;;\n \"error.unknown_option.zh\") text=\"未知选项: %s\" ;;\n \"error.unknown_option.en\") text=\"Unknown option: %s\" ;;\n \"error.docker_not_found.zh\") text=\"未找到 docker 或 podman 命令。请先安装 Docker Desktop 或 Podman Desktop:\\n Docker Desktop: https://www.docker.com/products/docker-desktop/\\n Podman Desktop: https://podman-desktop.io/\" ;;\n \"error.docker_not_found.en\") text=\"docker or podman command not found. Please install Docker Desktop or Podman Desktop first:\\n Docker Desktop: https://www.docker.com/products/docker-desktop/\\n Podman Desktop: https://podman-desktop.io/\" ;;\n \"error.docker_not_running.zh\") text=\"Docker 未运行。请先启动 Docker Desktop 或 Podman Desktop。\" ;;\n \"error.docker_not_running.en\") text=\"Docker is not running. Please start Docker Desktop or Podman Desktop first.\" ;;\n # --- Fallback: try English for unknown lang ---\n *)\n case \"${key}.en\" in\n \"tz.warning.title.en\") text=\"Could not detect timezone automatically.\" ;;\n \"install.title.en\") text=\"=== AgentTeams Manager Installation ===\" ;;\n *) text=\"${key}\" ;;\n esac\n ;;\n esac\n if [ $# -gt 0 ]; then\n # shellcheck disable=SC2059\n printf \"${text}\\n\" \"$@\"\n else\n echo \"${text}\"\n fi\n}\n\n# ============================================================\n# Registry selection based on timezone\n# ============================================================\n\ndetect_registry() {\n local tz=\"${AGENTTEAMS_TIMEZONE}\"\n\n case \"${tz}\" in\n America/*)\n echo \"higress-registry.us-west-1.cr.aliyuncs.com\"\n ;;\n Asia/Singapore|Asia/Bangkok|Asia/Jakarta|Asia/Makassar|Asia/Jayapura|\\\n Asia/Kuala_Lumpur|Asia/Ho_Chi_Minh|Asia/Manila|Asia/Yangon|\\\n Asia/Vientiane|Asia/Phnom_Penh|Asia/Pontianak|Asia/Ujung_Pandang)\n echo \"higress-registry.ap-southeast-7.cr.aliyuncs.com\"\n ;;\n *)\n echo \"higress-registry.cn-hangzhou.cr.aliyuncs.com\"\n ;;\n esac\n}\n\nAGENTTEAMS_REGISTRY=\"${AGENTTEAMS_REGISTRY:-$(detect_registry)}\"\n# Backward compatibility: accept old env var names from previous versions\nAGENTTEAMS_INSTALL_CONTROLLER_IMAGE=\"${AGENTTEAMS_INSTALL_CONTROLLER_IMAGE:-${AGENTTEAMS_INSTALL_DOCKER_PROXY_IMAGE:-}}\"\n# Image variables are resolved after version selection in step_version().\n# These placeholders allow early code paths to reference them without errors.\nMANAGER_IMAGE=\"${AGENTTEAMS_INSTALL_MANAGER_IMAGE:-}\"\nMANAGER_COPAW_IMAGE=\"${AGENTTEAMS_INSTALL_MANAGER_COPAW_IMAGE:-}\"\nWORKER_IMAGE=\"${AGENTTEAMS_INSTALL_WORKER_IMAGE:-}\"\nCOPAW_WORKER_IMAGE=\"${AGENTTEAMS_INSTALL_COPAW_WORKER_IMAGE:-}\"\nQWENPAW_WORKER_IMAGE=\"${AGENTTEAMS_INSTALL_QWENPAW_WORKER_IMAGE:-}\"\nHERMES_WORKER_IMAGE=\"${AGENTTEAMS_INSTALL_HERMES_WORKER_IMAGE:-}\"\nCONTROLLER_IMAGE=\"${AGENTTEAMS_INSTALL_CONTROLLER_IMAGE:-}\"\n\nresolve_image_tags() {\n AGENTTEAMS_VERSION=\"$(_normalize_version \"${AGENTTEAMS_VERSION}\")\"\n MANAGER_IMAGE=\"${AGENTTEAMS_INSTALL_MANAGER_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-manager:${AGENTTEAMS_VERSION}}\"\n MANAGER_COPAW_IMAGE=\"${AGENTTEAMS_INSTALL_MANAGER_COPAW_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-manager-copaw:${AGENTTEAMS_VERSION}}\"\n WORKER_IMAGE=\"${AGENTTEAMS_INSTALL_WORKER_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-worker:${AGENTTEAMS_VERSION}}\"\n COPAW_WORKER_IMAGE=\"${AGENTTEAMS_INSTALL_COPAW_WORKER_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-copaw-worker:${AGENTTEAMS_VERSION}}\"\n QWENPAW_WORKER_IMAGE=\"${AGENTTEAMS_INSTALL_QWENPAW_WORKER_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-qwenpaw-worker:${AGENTTEAMS_VERSION}}\"\n HERMES_WORKER_IMAGE=\"${AGENTTEAMS_INSTALL_HERMES_WORKER_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-hermes-worker:${AGENTTEAMS_VERSION}}\"\n EMBEDDED_IMAGE=\"${AGENTTEAMS_INSTALL_EMBEDDED_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-embedded:${AGENTTEAMS_VERSION}}\"\n # CoPaw Worker introduced in v1.0.4; Hermes Worker introduced in v1.1.0\n if [ -z \"${AGENTTEAMS_INSTALL_COPAW_WORKER_IMAGE:-}\" ] && _ver_lt \"${AGENTTEAMS_VERSION}\" \"v1.0.4\"; then\n COPAW_WORKER_IMAGE=\"\"\n fi\n if [ -z \"${AGENTTEAMS_INSTALL_HERMES_WORKER_IMAGE:-}\" ] && _ver_lt \"${AGENTTEAMS_VERSION}\" \"v1.1.0\"; then\n HERMES_WORKER_IMAGE=\"\"\n fi\n}\n\n# Resolve the embedded controller image. Embedded mode is the only supported\n# architecture since PR #616 (manager image no longer bundles Higress/Tuwunel/MinIO).\n# If the embedded image is unavailable for the requested version, fail fast with an\n# actionable error rather than silently falling back to the legacy single-container\n# path — that path is permanently broken with the slim manager image and would just\n# leave the user with a manager container looping on \"Waiting for Higress Gateway\".\n# Sets EMBEDDED_IMAGE and AGENTTEAMS_USE_EMBEDDED.\nresolve_embedded_image() {\n AGENTTEAMS_USE_EMBEDDED=1\n\n # If the user explicitly overrode the image (e.g. `make install-embedded` passes\n # a locally-built tag), respect it as-is without any registry probe.\n if [ -n \"${AGENTTEAMS_INSTALL_EMBEDDED_IMAGE:-}\" ]; then\n EMBEDDED_IMAGE=\"${AGENTTEAMS_INSTALL_EMBEDDED_IMAGE}\"\n return 0\n fi\n\n local _versioned=\"${AGENTTEAMS_REGISTRY}/agentteams/agentteams-embedded:${AGENTTEAMS_VERSION}\"\n local _latest=\"${AGENTTEAMS_REGISTRY}/agentteams/agentteams-embedded:latest\"\n\n # Skip probe when AGENTTEAMS_VERSION is \"latest\" — no point trying the same tag twice.\n if [ \"${AGENTTEAMS_VERSION}\" = \"latest\" ]; then\n EMBEDDED_IMAGE=\"${_latest}\"\n return 0\n fi\n\n if ${DOCKER_CMD} pull \"${_versioned}\" >/dev/null 2>&1; then\n EMBEDDED_IMAGE=\"${_versioned}\"\n return 0\n fi\n\n # Versions before v1.1.0 predate agentteams-embedded entirely — their manager image\n # bundled all infrastructure. Falling back to agentteams-embedded:latest would\n # silently swap in the v1.1.0 architecture (embedded kube-apiserver) which\n # crashes under QEMU on Apple Silicon. Auto-activate legacy mode instead.\n if _ver_lt \"${AGENTTEAMS_VERSION}\" \"v1.1.0\"; then\n log \"INFO: ${AGENTTEAMS_VERSION} predates agentteams-embedded; switching to legacy all-in-one manager architecture.\"\n log \"WARNING: Legacy all-in-one mode requires AGENTTEAMS_VERSION <= v1.0.9 (older bundled manager image).\"\n log \"WARNING: Newer slim manager images will hang on 'Waiting for Higress Gateway'.\"\n AGENTTEAMS_USE_EMBEDDED=0\n return 0\n fi\n\n if ${DOCKER_CMD} pull \"${_latest}\" >/dev/null 2>&1; then\n log \"embedded ${AGENTTEAMS_VERSION} not found, using latest\"\n EMBEDDED_IMAGE=\"${_latest}\"\n return 0\n fi\n\n # Explicit escape hatch — still honoured for edge cases.\n if [ \"${AGENTTEAMS_FORCE_LEGACY:-0}\" = \"1\" ]; then\n log \"WARNING: AGENTTEAMS_FORCE_LEGACY=1 — using legacy all-in-one manager architecture.\"\n log \"WARNING: This requires AGENTTEAMS_VERSION <= v1.0.9 (older bundled manager image).\"\n log \"WARNING: Newer slim manager images will hang on 'Waiting for Higress Gateway'.\"\n AGENTTEAMS_USE_EMBEDDED=0\n return 0\n fi\n\n error \"Embedded controller image is not available in the registry:\"\n error \" - tried: ${_versioned}\"\n error \" - tried: ${_latest}\"\n error \"\"\n error \"Embedded mode is the only supported architecture since PR #616.\"\n error \"How to resolve:\"\n error \" 1) Pin to a AGENTTEAMS_VERSION whose embedded image has been published, or\"\n error \" wait for the release pipeline to publish it.\"\n error \" 2) For a local build, run: make install-embedded\"\n error \" (builds and uses the local embedded image without touching the registry).\"\n error \" 3) Override with a custom image: AGENTTEAMS_INSTALL_EMBEDDED_IMAGE=... ./agentteams-install.sh\"\n exit 1\n}\n\n# ============================================================\n# Known models list — used to detect custom models during install\n# ============================================================\nKNOWN_MODELS=\"gpt-5.4 gpt-5.3-codex gpt-5-mini gpt-5-nano claude-opus-4-6 claude-sonnet-4-6 claude-haiku-4-5 qwen3.6-plus qwen3.5-plus deepseek-chat deepseek-reasoner kimi-k2.5 glm-5 MiniMax-M2.7 MiniMax-M2.7-highspeed MiniMax-M2.5\"\n\nis_known_model() {\n local model=\"$1\"\n for m in ${KNOWN_MODELS}; do\n [ \"${m}\" = \"${model}\" ] && return 0\n done\n return 1\n}\n\n# Prompt user for custom model parameters when model is not in the known list.\n# Sets: AGENTTEAMS_MODEL_CONTEXT_WINDOW, AGENTTEAMS_MODEL_MAX_TOKENS, AGENTTEAMS_MODEL_REASONING, AGENTTEAMS_MODEL_VISION\nprompt_custom_model_params() {\n local model=\"$1\"\n if is_known_model \"${model}\"; then\n # Clear any stale custom params for known models\n AGENTTEAMS_MODEL_CONTEXT_WINDOW=\"\"\n AGENTTEAMS_MODEL_MAX_TOKENS=\"\"\n AGENTTEAMS_MODEL_REASONING=\"\"\n AGENTTEAMS_MODEL_VISION=\"\"\n return\n fi\n echo \"\"\n log \"$(msg llm.custom_model.detected \"${model}\")\"\n echo \"\"\n read -e -p \" $(msg llm.custom_model.context_prompt): \" AGENTTEAMS_MODEL_CONTEXT_WINDOW\n if [ \"${AGENTTEAMS_MODEL_CONTEXT_WINDOW}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n AGENTTEAMS_MODEL_CONTEXT_WINDOW=\"${AGENTTEAMS_MODEL_CONTEXT_WINDOW:-150000}\"\n read -e -p \" $(msg llm.custom_model.max_tokens_prompt): \" AGENTTEAMS_MODEL_MAX_TOKENS\n if [ \"${AGENTTEAMS_MODEL_MAX_TOKENS}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n AGENTTEAMS_MODEL_MAX_TOKENS=\"${AGENTTEAMS_MODEL_MAX_TOKENS:-128000}\"\n read -e -p \" $(msg llm.custom_model.reasoning_prompt): \" _reasoning\n if [ \"${_reasoning}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n case \"${_reasoning}\" in\n n|N|no|NO) AGENTTEAMS_MODEL_REASONING=\"false\" ;;\n *) AGENTTEAMS_MODEL_REASONING=\"true\" ;;\n esac\n read -e -p \" $(msg llm.custom_model.vision_prompt): \" _vision\n if [ \"${_vision}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n case \"${_vision}\" in\n y|Y|yes|YES) AGENTTEAMS_MODEL_VISION=\"true\" ;;\n *) AGENTTEAMS_MODEL_VISION=\"false\" ;;\n esac\n log \"$(msg llm.custom_model.summary \"${AGENTTEAMS_MODEL_CONTEXT_WINDOW}\" \"${AGENTTEAMS_MODEL_MAX_TOKENS}\" \"${AGENTTEAMS_MODEL_REASONING}\" \"${AGENTTEAMS_MODEL_VISION}\")\"\n}\n\n# ============================================================\n# Wait for Manager agent to be ready\n# Uses `openclaw gateway health` inside the container to confirm the gateway is running\n# ============================================================\n\nwait_manager_ready() {\n local timeout=\"${AGENTTEAMS_READY_TIMEOUT:-300}\"\n local elapsed=0\n local container=\"${1:-agentteams-manager}\"\n\n log \"$(msg install.wait_ready \"${timeout}\")\"\n\n # Wait for Manager agent to be healthy inside the container\n local runtime=\"${AGENTTEAMS_MANAGER_RUNTIME:-copaw}\"\n while [ \"${elapsed}\" -lt \"${timeout}\" ]; do\n case \"${runtime}\" in\n copaw)\n if ${DOCKER_CMD} exec \"${container}\" curl -sf http://127.0.0.1:18799/api/agents 2>/dev/null | grep -q '\"agents\"'; then\n log \"$(msg install.wait_ready.ok)\"\n return 0\n fi\n ;;\n *)\n if ${DOCKER_CMD} exec \"${container}\" openclaw gateway health --json 2>/dev/null | grep -q '\"ok\"' 2>/dev/null; then\n log \"$(msg install.wait_ready.ok)\"\n return 0\n fi\n ;;\n esac\n sleep 5\n elapsed=$((elapsed + 5))\n printf \"\\r\\033[36m[AgentTeams]\\033[0m $(msg install.wait_ready.waiting \"${elapsed}\" \"${timeout}\")\"\n done\n\n echo \"\"\n die \"$(msg install.wait_ready.timeout \"${timeout}\" \"${container}\")\"\n}\n\nwait_matrix_ready() {\n local timeout=\"${AGENTTEAMS_READY_TIMEOUT:-300}\"\n local elapsed=0\n local container=\"${1:-agentteams-manager}\"\n\n log \"$(msg install.wait_matrix \"${timeout}\")\"\n\n while [ \"${elapsed}\" -lt \"${timeout}\" ]; do\n if ${DOCKER_CMD} exec \"${container}\" curl -sf http://127.0.0.1:6167/_tuwunel/server_version >/dev/null 2>&1; then\n log \"$(msg install.wait_matrix.ok)\"\n return 0\n fi\n sleep 5\n elapsed=$((elapsed + 5))\n printf \"\\r\\033[36m[AgentTeams]\\033[0m $(msg install.wait_matrix.waiting \"${elapsed}\" \"${timeout}\")\"\n done\n\n echo \"\"\n die \"$(msg install.wait_matrix.timeout \"${timeout}\" \"${container}\")\"\n}\n\n# Read KEY=value from /data/agentteams-secrets.env on a Docker volume (manager container not required).\n# Requires EMBEDDED_IMAGE (resolved earlier in install_manager). Uses ${DOCKER_CMD}.\nagentteams_read_secret_from_data_volume() {\n local _vol=\"$1\" _key=\"$2\"\n if [ -z \"${_vol}\" ] || [ -z \"${_key}\" ] || [ -z \"${EMBEDDED_IMAGE:-}\" ]; then\n echo \"\"\n return 0\n fi\n ${DOCKER_CMD} run --rm --entrypoint sh \\\n -v \"${_vol}:/data:ro\" \\\n \"${EMBEDDED_IMAGE}\" -c \"grep \\\"^${_key}=\\\" /data/agentteams-secrets.env 2>/dev/null | cut -d= -f2- | head -1 | tr -d '\\r'\" 2>/dev/null\n}\n\n# Read KEY=value from /data/worker-creds/<worker>.env on a Docker volume.\nagentteams_read_worker_creds_value_from_volume() {\n local _vol=\"$1\" _worker=\"$2\" _key=\"$3\"\n if [ -z \"${_vol}\" ] || [ -z \"${_worker}\" ] || [ -z \"${_key}\" ] || [ -z \"${EMBEDDED_IMAGE:-}\" ]; then\n echo \"\"\n return 0\n fi\n ${DOCKER_CMD} run --rm --entrypoint sh \\\n -v \"${_vol}:/data:ro\" \\\n \"${EMBEDDED_IMAGE}\" -c \"grep \\\"^${_key}=\\\" \\\"/data/worker-creds/${_worker}.env\\\" 2>/dev/null | cut -d= -f2- | head -1 | tr -d \\\"\\\\r\\\"\" 2>/dev/null\n}\n\n# Read admin_dm_room_id from host workspace state.json (fallback when Matrix API is unavailable).\nagentteams_read_admin_dm_room_from_workspace() {\n local _ws=\"$1\"\n local _f=\"${_ws}/state.json\"\n if [ ! -f \"${_f}\" ] || ! command -v jq >/dev/null 2>&1; then\n echo \"\"\n return 0\n fi\n jq -r '.admin_dm_room_id // empty | select(. != \"null\")' \"${_f}\" 2>/dev/null\n}\n\n# Read secret input with masked echo (shows * per keystroke, supports backspace)\n# Usage: read_secret \"prompt text: \"; value=\"${_RS_RESULT}\"\nread_secret() {\n local _rs_prompt=\"$1\"\n _RS_RESULT=\"\"\n local _rs_char=\"\"\n\n printf \"%s\" \"${_rs_prompt}\"\n\n while IFS= read -r -s -n 1 _rs_char; do\n if [[ -z \"${_rs_char}\" ]]; then\n break\n elif [[ \"${_rs_char}\" == $'\\177' ]] || [[ \"${_rs_char}\" == $'\\b' ]]; then\n if [ -n \"${_RS_RESULT}\" ]; then\n _RS_RESULT=\"${_RS_RESULT%?}\"\n printf \"\\b \\b\"\n fi\n else\n _RS_RESULT=\"${_RS_RESULT}${_rs_char}\"\n printf \"*\"\n fi\n done\n echo\n}\n\n# Load current parameter values from env file for upgrade mode display\nload_current_params_from_env() {\n local env_file=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n if [ -f \"${env_file}\" ]; then\n [ -z \"${AGENTTEAMS_LLM_PROVIDER:+x}\" ] && AGENTTEAMS_LLM_PROVIDER=\"$(grep '^AGENTTEAMS_LLM_PROVIDER=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_OPENAI_BASE_URL:+x}\" ] && AGENTTEAMS_OPENAI_BASE_URL=\"$(grep '^AGENTTEAMS_OPENAI_BASE_URL=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_DEFAULT_MODEL:+x}\" ] && AGENTTEAMS_DEFAULT_MODEL=\"$(grep '^AGENTTEAMS_DEFAULT_MODEL=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_EMBEDDING_MODEL:+x}\" ] && AGENTTEAMS_EMBEDDING_MODEL=\"$(grep '^AGENTTEAMS_EMBEDDING_MODEL=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_WORKSPACE_DIR:+x}\" ] && AGENTTEAMS_WORKSPACE_DIR=\"$(grep '^AGENTTEAMS_WORKSPACE_DIR=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_HOST_SHARE_DIR:+x}\" ] && AGENTTEAMS_HOST_SHARE_DIR=\"$(grep '^AGENTTEAMS_HOST_SHARE_DIR=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_PODMAN_AUTOSTART:+x}\" ] && AGENTTEAMS_PODMAN_AUTOSTART=\"$(grep '^AGENTTEAMS_PODMAN_AUTOSTART=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_DASHBOARD:+x}\" ] && AGENTTEAMS_DASHBOARD=\"$(grep '^AGENTTEAMS_DASHBOARD=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_DASHBOARD_VERSION:+x}\" ] && AGENTTEAMS_DASHBOARD_VERSION=\"$(grep '^AGENTTEAMS_DASHBOARD_VERSION=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_PORT_DASHBOARD:+x}\" ] && AGENTTEAMS_PORT_DASHBOARD=\"$(grep '^AGENTTEAMS_PORT_DASHBOARD=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_DASHBOARD_IMAGE:+x}\" ] && AGENTTEAMS_DASHBOARD_IMAGE=\"$(grep '^AGENTTEAMS_DASHBOARD_IMAGE=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n [ -z \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL:+x}\" ] && AGENTTEAMS_AI_GATEWAY_ADMIN_URL=\"$(grep '^AGENTTEAMS_AI_GATEWAY_ADMIN_URL=' \"${env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n fi\n}\n\n# In non-interactive mode, uses default or errors if required and no default.\n# Usage: prompt VAR_NAME \"Prompt text\" \"default\" [true=secret]\nprompt() {\n local var_name=\"$1\"\n local prompt_text=\"$2\"\n local default_value=\"$3\"\n local is_secret=\"${4:-false}\"\n\n # If the variable is already set in the environment, use it silently\n # In upgrade mode, show current value and let user change it\n eval \"local current_value=\\\"\\${${var_name}}\\\"\"\n if [ -n \"${current_value}\" ]; then\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_NON_INTERACTIVE}\" != \"1\" ]; then\n # Show masked value for secrets, full value otherwise\n local display_value=\"${current_value}\"\n if [ \"${is_secret}\" = \"true\" ]; then\n local len=${#current_value}\n if [ \"${len}\" -le 8 ]; then\n display_value=\"****\"\n else\n display_value=\"${current_value:0:4}****${current_value: -4}\"\n fi\n fi\n log \"$(msg prompt.upgrade_keep \"${prompt_text}\" \"${display_value}\")\"\n local new_value=\"\"\n if [ \"${is_secret}\" = \"true\" ]; then\n read_secret \"${prompt_text}: \"\n new_value=\"${_RS_RESULT}\"\n else\n read -e -p \"${prompt_text}: \" new_value\n if [ \"${new_value}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n fi\n if [ -n \"${new_value}\" ]; then\n eval \"export ${var_name}='${new_value}'\"\n fi\n return\n fi\n log \"$(msg prompt.preset \"${prompt_text}\")\"\n return\n fi\n\n # Non-interactive or quickstart: use default or error\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] || [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ]; then\n if [ -n \"${default_value}\" ]; then\n eval \"export ${var_name}='${default_value}'\"\n log \"$(msg prompt.default \"${prompt_text}\" \"${default_value}\")\"\n return\n elif [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n # Only hard-error in fully non-interactive mode, not quickstart\n die \"$(msg prompt.required \"${prompt_text}\")\"\n fi\n # quickstart + no default: fall through to interactive prompt below\n fi\n\n if [ -n \"${default_value}\" ]; then\n prompt_text=\"${prompt_text} [${default_value}]\"\n fi\n\n local value=\"\"\n if [ \"${is_secret}\" = \"true\" ]; then\n read_secret \"${prompt_text}: \"\n value=\"${_RS_RESULT}\"\n else\n read -e -p \"${prompt_text}: \" value\n if [ \"${value}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n fi\n\n value=\"${value:-${default_value}}\"\n if [ -z \"${value}\" ]; then\n die \"$(msg prompt.required_empty \"${prompt_text}\")\"\n fi\n\n eval \"export ${var_name}='${value}'\"\n}\n\n# Prompt for an optional value (empty string is acceptable)\n# Skips prompt if variable is already defined in environment (even if empty)\n# In upgrade mode, shows current value and lets user change it.\n# In non-interactive mode, defaults to empty string.\nprompt_optional() {\n local var_name=\"$1\"\n local prompt_text=\"$2\"\n local is_secret=\"${3:-false}\"\n\n # Check if variable is defined (even if set to empty string)\n eval \"local _chk=\\\"\\${${var_name}+x}\\\"\"\n if [ -n \"${_chk}\" ]; then\n # In upgrade mode, show current value and let user change it\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_NON_INTERACTIVE}\" != \"1\" ]; then\n eval \"local current_value=\\\"\\${${var_name}}\\\"\"\n local display_value=\"${current_value}\"\n if [ \"${is_secret}\" = \"true\" ] && [ -n \"${current_value}\" ]; then\n local len=${#current_value}\n if [ \"${len}\" -le 8 ]; then\n display_value=\"****\"\n else\n display_value=\"${current_value:0:4}****${current_value: -4}\"\n fi\n fi\n if [ -n \"${current_value}\" ]; then\n if [ \"${is_secret}\" = \"true\" ]; then\n log \"$(msg prompt.upgrade_keep_secret \"${prompt_text}\" \"${display_value}\")\"\n else\n log \"$(msg prompt.upgrade_keep \"${prompt_text}\" \"${display_value}\")\"\n fi\n else\n log \"$(msg prompt.upgrade_empty \"${prompt_text}\")\"\n fi\n local new_value=\"\"\n if [ \"${is_secret}\" = \"true\" ]; then\n read_secret \"${prompt_text}: \"\n new_value=\"${_RS_RESULT}\"\n else\n read -e -p \"${prompt_text}: \" new_value\n if [ \"${new_value}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n fi\n if [ -n \"${new_value}\" ]; then\n eval \"export ${var_name}='${new_value}'\"\n fi\n return\n fi\n log \"$(msg prompt.preset \"${prompt_text}\")\"\n return\n fi\n\n # Non-interactive or quickstart: skip, leave unset\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] || [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ]; then\n eval \"export ${var_name}=''\"\n return\n fi\n\n local value=\"\"\n if [ \"${is_secret}\" = \"true\" ]; then\n read_secret \"${prompt_text}: \"\n value=\"${_RS_RESULT}\"\n else\n read -e -p \"${prompt_text}: \" value\n if [ \"${value}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 1; fi\n fi\n\n eval \"export ${var_name}='${value}'\"\n}\n\ngenerate_key() {\n openssl rand -hex 32\n}\n\n# ============================================================\n# Detect container runtime socket on the host\n#\n# Dependency: Requires DOCKER_CMD to reflect the TRUE underlying runtime.\n# (Crucial for unmasking 'podman' disguised as 'docker'.\n# Must be strictly resolved prior, e.g., via check_container_runtime)\n# ============================================================\ndetect_socket() {\n local socket_path\n\n # 1. Respect explicitly defined DOCKER_HOST environment variable\n if [ -n \"${DOCKER_HOST}\" ]; then\n socket_path=$(echo \"${DOCKER_HOST}\" | sed 's|^unix://||')\n if [ -S \"${socket_path}\" ]; then\n echo \"${socket_path}\"\n return 0\n fi\n fi\n\n # 2. Route based on the resolved container runtime\n case \"${DOCKER_CMD}\" in\n docker)\n # Docker path handling\n socket_path=$(docker context ls --format '{{if .Current}}{{.DockerEndpoint}}{{end}}' 2>/dev/null | grep . | sed 's|^unix://||')\n if [ -n \"${socket_path}\" ] && [ -S \"${socket_path}\" ]; then\n echo \"${socket_path}\"\n return 0\n fi\n\n # Rootless Docker\n if [ -n \"${XDG_RUNTIME_DIR:-}\" ] && [ -S \"${XDG_RUNTIME_DIR}/docker.sock\" ]; then\n echo \"${XDG_RUNTIME_DIR}/docker.sock\"\n return 0\n fi\n\n # Rootless Docker (macOS fallback)\n if [ -S \"${HOME}/.docker/run/docker.sock\" ]; then\n echo \"${HOME}/.docker/run/docker.sock\"\n return 0\n fi\n\n # Root Docker\n if [ -S \"/var/run/docker.sock\" ]; then\n echo \"/var/run/docker.sock\"\n return 0\n fi\n ;;\n podman)\n # Podman path handling\n if [ \"$(id -u)\" -eq 0 ]; then\n socket_path=\"/run/podman/podman.sock\"\n else\n socket_path=\"${XDG_RUNTIME_DIR}/podman/podman.sock\"\n fi\n if [ -S \"${socket_path}\" ]; then\n echo \"${socket_path}\"\n return 0\n fi\n ;;\n esac\n\n # Return empty if no socket is found\n echo \"\"\n return 0\n}\n\n# Ensure Podman API socket is active (State mutation)\nensure_podman_socket() {\n local socket_path\n\n # Only applicable if runtime is podman and systemctl is available\n if [ \"${DOCKER_CMD:-}\" != \"podman\" ] || ! command -v systemctl >/dev/null 2>&1; then\n return 0\n fi\n\n echo \"Ensuring Podman API socket is active...\" >&2\n\n # Enable and start the socket based on user privileges\n if [ \"$(id -u)\" -eq 0 ]; then\n socket_path=\"/run/podman/podman.sock\"\n systemctl enable --now podman.socket >/dev/null 2>&1 || true\n else\n socket_path=\"${XDG_RUNTIME_DIR}/podman/podman.sock\"\n systemctl --user enable --now podman.socket >/dev/null 2>&1 || true\n fi\n\n # Give systemd a brief moment to assert the socket for root or rootless user\n local _i\n for _i in 1 2 3; do\n [ -S \"${socket_path}\" ] && break\n sleep 1\n done\n}\n\n# Detect local LAN IP address (cross-platform: macOS and Linux)\ndetect_lan_ip() {\n local ip=\"\"\n\n # macOS: try common Wi-Fi / Ethernet interfaces\n if command -v ipconfig >/dev/null 2>&1; then\n for iface in en0 en1 en2 en3 en4; do\n ip=$(ipconfig getifaddr \"${iface}\" 2>/dev/null)\n if [ -n \"${ip}\" ]; then\n echo \"${ip}\"\n return 0\n fi\n done\n fi\n\n # Linux: ip route — most reliable\n if command -v ip >/dev/null 2>&1; then\n ip=$(ip route get 1.1.1.1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i==\"src\"){print $(i+1); exit}}')\n if [ -n \"${ip}\" ]; then\n echo \"${ip}\"\n return 0\n fi\n fi\n\n # Linux fallback: hostname -I (space-separated list, take first non-loopback)\n if command -v hostname >/dev/null 2>&1; then\n ip=$(hostname -I 2>/dev/null | tr ' ' '\\n' | grep -v '^127\\.' | grep -v '^::' | head -1)\n if [ -n \"${ip}\" ]; then\n echo \"${ip}\"\n return 0\n fi\n fi\n\n # Last resort: ifconfig\n if command -v ifconfig >/dev/null 2>&1; then\n ip=$(ifconfig 2>/dev/null | awk '/inet /{if($2!~/^127\\./){print $2; exit}}')\n # Strip \"addr:\" prefix that some ifconfig versions add\n ip=\"${ip#addr:}\"\n if [ -n \"${ip}\" ]; then\n echo \"${ip}\"\n return 0\n fi\n fi\n\n echo \"\"\n}\n\n\n# ============================================================\n# Step-back navigation helpers\n# ============================================================\n\n# should_skip_step: returns 0 (skip) when the step is irrelevant in current mode\nshould_skip_step() {\n local step_fn=\"$1\"\n case \"${step_fn}\" in\n step_lang|step_mode)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n ;;\n step_version)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n ;;\n step_existing)\n local _env=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n [ ! -f \"${_env}\" ] && return 0\n ;;\n # Keep-All upgrade mode: skip all config steps (step_volume/step_workspace handled separately)\n step_llm|step_admin|step_network|step_ports|step_domains|step_github|step_skills|step_runtime|step_manager_runtime|step_dashboard|step_e2ee|step_docker_proxy|step_idle|step_hostshare)\n [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ] && return 0\n ;;\n step_volume|step_workspace)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ] && return 0\n [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ] && return 0\n ;;\n step_e2ee|step_idle)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE}\" != \"1\" ] && return 0\n ;;\n step_docker_proxy)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE}\" != \"1\" ] && return 0\n # Embedded mode handles docker access natively — skip this step\n [ \"${AGENTTEAMS_USE_EMBEDDED:-}\" = \"1\" ] && return 0\n ;;\n step_manager_runtime)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n ;;\n step_hostshare)\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ] && return 0\n ;;\n step_podman_autostart)\n # Only relevant for Podman with systemd\n [ \"${DOCKER_CMD}\" != \"podman\" ] && return 0\n ! command -v systemctl >/dev/null 2>&1 && return 0\n\n [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ] && return 0\n [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE}\" != \"1\" ] && return 0\n [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ] && return 0\n ;;\n esac\n return 1\n}\n\n# clear_step_vars: unset variables set by a step so it will re-prompt on re-entry\nclear_step_vars() {\n local step_fn=\"$1\"\n case \"${step_fn}\" in\n step_mode) unset AGENTTEAMS_QUICKSTART ;;\n step_version) unset AGENTTEAMS_VERSION ;;\n step_existing) unset AGENTTEAMS_UPGRADE UPGRADE_EXISTING_WORKERS ;;\n step_llm)\n unset AGENTTEAMS_LLM_PROVIDER AGENTTEAMS_DEFAULT_MODEL AGENTTEAMS_OPENAI_BASE_URL\n unset AGENTTEAMS_LLM_API_KEY AGENTTEAMS_MODEL_CONTEXT_WINDOW AGENTTEAMS_MODEL_MAX_TOKENS\n unset AGENTTEAMS_MODEL_REASONING AGENTTEAMS_MODEL_VISION\n ;;\n step_admin) unset AGENTTEAMS_ADMIN_USER AGENTTEAMS_ADMIN_PASSWORD ;;\n step_network) unset AGENTTEAMS_LOCAL_ONLY ;;\n step_ports)\n unset AGENTTEAMS_PORT_GATEWAY AGENTTEAMS_PORT_CONSOLE\n unset AGENTTEAMS_PORT_ELEMENT_WEB AGENTTEAMS_PORT_MANAGER_CONSOLE\n ;;\n step_domains)\n unset AGENTTEAMS_MATRIX_DOMAIN AGENTTEAMS_MATRIX_CLIENT_DOMAIN\n unset AGENTTEAMS_AI_GATEWAY_DOMAIN AGENTTEAMS_FS_DOMAIN AGENTTEAMS_CONSOLE_DOMAIN\n ;;\n step_github) unset AGENTTEAMS_GITHUB_TOKEN ;;\n step_skills) unset AGENTTEAMS_SKILLS_API_URL ;;\n step_volume) unset AGENTTEAMS_DATA_DIR ;;\n step_workspace) unset AGENTTEAMS_WORKSPACE_DIR ;;\n step_dashboard) unset AGENTTEAMS_DASHBOARD AGENTTEAMS_DASHBOARD_VERSION AGENTTEAMS_PORT_DASHBOARD AGENTTEAMS_DASHBOARD_IMAGE AGENTTEAMS_AI_GATEWAY_ADMIN_URL ;;\n step_runtime) unset AGENTTEAMS_DEFAULT_WORKER_RUNTIME ;;\n step_manager_runtime) unset AGENTTEAMS_MANAGER_RUNTIME ;;\n step_e2ee) unset AGENTTEAMS_MATRIX_E2EE ;;\n step_docker_proxy) unset AGENTTEAMS_DOCKER_PROXY; unset AGENTTEAMS_PROXY_ALLOWED_REGISTRIES ;;\n step_idle) unset AGENTTEAMS_WORKER_IDLE_TIMEOUT ;;\n step_hostshare) unset AGENTTEAMS_HOST_SHARE_DIR ;;\n step_podman_autostart) unset AGENTTEAMS_PODMAN_AUTOSTART ;;\n esac\n}\n\n# ============================================================\n# Individual step functions\n# ============================================================\n\nstep_lang() {\n local lang_default_choice=\"2\"\n [ \"${AGENTTEAMS_LANGUAGE}\" = \"zh\" ] && lang_default_choice=\"1\"\n log \"$(msg lang.detected)\"\n log \"$(msg lang.switch_title)\"\n echo \"$(msg lang.option_zh)\"\n echo \"$(msg lang.option_en)\"\n echo \"\"\n local LANG_CHOICE\n read -e -p \"$(msg lang.prompt) [${lang_default_choice}]: \" LANG_CHOICE\n LANG_CHOICE=\"${LANG_CHOICE:-${lang_default_choice}}\"\n if [ \"${LANG_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n case \"${LANG_CHOICE}\" in\n 1) AGENTTEAMS_LANGUAGE=\"zh\" ;;\n 2) AGENTTEAMS_LANGUAGE=\"en\" ;;\n esac\n export AGENTTEAMS_LANGUAGE\n log \"\"\n}\n\nstep_mode() {\n log \"$(msg install.mode.title)\"\n echo \"\"\n echo \"$(msg install.mode.choose)\"\n echo \"$(msg install.mode.quickstart)\"\n echo \"$(msg install.mode.manual)\"\n echo \"\"\n local ONBOARDING_CHOICE\n read -e -p \"$(msg install.mode.prompt): \" ONBOARDING_CHOICE\n ONBOARDING_CHOICE=\"${ONBOARDING_CHOICE:-1}\"\n if [ \"${ONBOARDING_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n case \"${ONBOARDING_CHOICE}\" in\n 1|quick|quickstart)\n log \"$(msg install.mode.quickstart_selected)\"\n AGENTTEAMS_QUICKSTART=1\n ;;\n 2|manual)\n log \"$(msg install.mode.manual_selected)\"\n ;;\n *)\n log \"$(msg install.mode.invalid)\"\n AGENTTEAMS_QUICKSTART=1\n ;;\n esac\n log \"\"\n}\n\nstep_version() {\n # Skip if version already provided via env var\n if [ -n \"${AGENTTEAMS_VERSION}\" ]; then\n resolve_image_tags\n return 0\n fi\n # Try to fetch the latest stable release from GitHub\n log \"$(msg install.version.fetching)\"\n local _fetched\n _fetched=$(curl -sf --max-time 5 \\\n -H \"Accept: application/vnd.github+json\" \\\n \"https://api.github.com/repos/agentscope-ai/AgentTeams/releases/latest\" \\\n 2>/dev/null | grep '\"tag_name\"' | head -1 | sed 's/.*\"tag_name\": *\"\\([^\"]*\\)\".*/\\1/')\n if [ -n \"${_fetched}\" ]; then\n AGENTTEAMS_KNOWN_STABLE_VERSION=\"${_fetched}\"\n else\n log \"$(msg install.version.fetch_failed \"${AGENTTEAMS_KNOWN_STABLE_VERSION}\")\"\n fi\n log \"$(msg install.version.title)\"\n echo \"\"\n echo \"$(msg install.version.choose)\"\n echo \"$(msg install.version.option_latest)\"\n printf \"%s\\n\" \"$(msg install.version.option_stable \"${AGENTTEAMS_KNOWN_STABLE_VERSION}\")\"\n echo \"$(msg install.version.option_custom)\"\n echo \"\"\n local VERSION_CHOICE\n read -e -p \"$(msg install.version.prompt) [1]: \" VERSION_CHOICE\n VERSION_CHOICE=\"${VERSION_CHOICE:-1}\"\n if [ \"${VERSION_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n case \"${VERSION_CHOICE}\" in\n 1|latest)\n AGENTTEAMS_VERSION=\"latest\"\n log \"$(msg install.version.selected_latest)\"\n ;;\n 2|stable)\n AGENTTEAMS_VERSION=\"${AGENTTEAMS_KNOWN_STABLE_VERSION}\"\n log \"$(msg install.version.selected_stable \"${AGENTTEAMS_VERSION}\")\"\n ;;\n 3|custom)\n local CUSTOM_VERSION\n read -e -p \"$(msg install.version.custom_prompt): \" CUSTOM_VERSION\n AGENTTEAMS_VERSION=\"${CUSTOM_VERSION:-${AGENTTEAMS_KNOWN_STABLE_VERSION}}\"\n log \"$(msg install.version.selected_custom \"${AGENTTEAMS_VERSION}\")\"\n ;;\n *)\n AGENTTEAMS_VERSION=\"${AGENTTEAMS_KNOWN_STABLE_VERSION}\"\n log \"$(msg install.version.invalid \"${AGENTTEAMS_VERSION}\")\"\n ;;\n esac\n log \"\"\n resolve_image_tags\n}\n\nstep_existing() {\n local existing_env=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n log \"$(msg install.existing.detected \"${existing_env}\")\"\n local running_manager=\"\" running_workers=\"\" existing_workers=\"\"\n if ${DOCKER_CMD} ps --format '{{.Names}}' | grep -q \"^agentteams-manager$\"; then\n running_manager=\"agentteams-manager\"\n fi\n running_workers=$(${DOCKER_CMD} ps --format '{{.Names}}' | grep \"^agentteams-worker-\" || true)\n existing_workers=$(${DOCKER_CMD} ps -a --format '{{.Names}}' | grep \"^agentteams-worker-\" || true)\n local UPGRADE_CHOICE\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n log \"$(msg install.existing.upgrade_noninteractive)\"\n UPGRADE_CHOICE=\"upgrade\"\n else\n echo \"\"\n echo \"$(msg install.existing.choose)\"\n echo \"$(msg install.existing.upgrade)\"\n echo \"$(msg install.existing.reinstall)\"\n echo \"$(msg install.existing.cancel)\"\n echo \"\"\n read -e -p \"$(msg install.existing.prompt): \" UPGRADE_CHOICE\n UPGRADE_CHOICE=\"${UPGRADE_CHOICE:-1}\"\n if [ \"${UPGRADE_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n fi\n case \"${UPGRADE_CHOICE}\" in\n 1|upgrade)\n AGENTTEAMS_UPGRADE=1\n log \"$(msg install.existing.upgrading)\"\n # Show upgrade mode sub-menu (unless already set via env var)\n if [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL:-0}\" != \"1\" ]; then\n echo \"\"\n echo \"$(msg upgrade.mode.prompt)\"\n echo \"$(msg upgrade.mode.keep_all)\"\n echo \"$(msg upgrade.mode.confirm_each)\"\n echo \"$(msg upgrade.mode.back)\"\n echo \"\"\n local UPGRADE_MODE_CHOICE\n read -e -p \"$(msg install.existing.prompt): \" UPGRADE_MODE_CHOICE\n UPGRADE_MODE_CHOICE=\"${UPGRADE_MODE_CHOICE:-1}\"\n case \"${UPGRADE_MODE_CHOICE}\" in\n 1|keep)\n AGENTTEAMS_UPGRADE_KEEP_ALL=1\n ;;\n 2|confirm)\n AGENTTEAMS_UPGRADE_KEEP_ALL=0\n ;;\n 3|b)\n STEP_RESULT=\"back\"\n return 0\n ;;\n *)\n AGENTTEAMS_UPGRADE_KEEP_ALL=0\n ;;\n esac\n fi\n # Load current parameters for both Keep-All and confirm-each modes\n load_current_params_from_env\n if [ -n \"${running_manager}\" ] || [ -n \"${running_workers}\" ]; then\n echo \"\"\n echo -e \"\\033[33m$(msg install.existing.warn_manager_stop)\\033[0m\"\n if [ -n \"${existing_workers}\" ]; then\n echo -e \"\\033[33m$(msg install.existing.warn_worker_recreate)\\033[0m\"\n fi\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" != \"1\" ]; then\n echo \"\"\n local CONFIRM_STOP\n read -e -p \"$(msg install.existing.continue_prompt): \" CONFIRM_STOP\n if [ \"${CONFIRM_STOP}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n if [ \"${CONFIRM_STOP}\" != \"y\" ] && [ \"${CONFIRM_STOP}\" != \"Y\" ]; then\n log \"$(msg install.existing.cancelled)\"\n exit 0\n fi\n fi\n fi\n UPGRADE_EXISTING_WORKERS=\"${existing_workers}\"\n ;;\n 2|reinstall)\n log \"$(msg install.reinstall.performing)\"\n local existing_workspace=\"\"\n if [ -f \"${existing_env}\" ]; then\n existing_workspace=$(grep '^AGENTTEAMS_WORKSPACE_DIR=' \"${existing_env}\" 2>/dev/null | cut -d= -f2-)\n fi\n [ -z \"${existing_workspace}\" ] && existing_workspace=\"${HOME}/agentteams-manager\"\n echo \"\"\n echo -e \"\\033[33m$(msg install.reinstall.warn_stop)\\033[0m\"\n [ -n \"${running_manager}\" ] && echo -e \"\\033[33m - ${running_manager} (manager)\\033[0m\"\n for w in ${running_workers}; do\n echo -e \"\\033[33m - ${w} (worker)\\033[0m\"\n done\n echo \"\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_delete)\\033[0m\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_volume)\\033[0m\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_env \"${existing_env}\")\\033[0m\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_workspace \"${existing_workspace}\")\\033[0m\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_workers)\\033[0m\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_proxy)\\033[0m\"\n echo -e \"\\033[31m$(msg install.reinstall.warn_network)\\033[0m\"\n echo \"\"\n echo -e \"\\033[31m$(msg install.reinstall.confirm_type)\\033[0m\"\n echo -e \"\\033[31m ${existing_workspace}\\033[0m\"\n echo \"\"\n local CONFIRM_PATH\n read -e -p \"$(msg install.reinstall.confirm_path): \" CONFIRM_PATH\n if [ \"${CONFIRM_PATH}\" != \"${existing_workspace}\" ]; then\n die \"$(msg install.reinstall.path_mismatch \"${CONFIRM_PATH}\" \"${existing_workspace}\")\"\n fi\n log \"$(msg install.reinstall.confirmed)\"\n ${DOCKER_CMD} stop agentteams-manager 2>/dev/null || true\n ${DOCKER_CMD} rm agentteams-manager 2>/dev/null || true\n for w in $(${DOCKER_CMD} ps -a --format '{{.Names}}' | grep \"^agentteams-worker-\" || true); do\n ${DOCKER_CMD} stop \"${w}\" 2>/dev/null || true\n ${DOCKER_CMD} rm \"${w}\" 2>/dev/null || true\n log \"$(msg install.reinstall.removed_worker \"${w}\")\"\n done\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' | grep -q \"^agentteams-controller$\"; then\n log \"$(msg install.reinstall.removing_proxy)\"\n ${DOCKER_CMD} stop agentteams-controller 2>/dev/null || true\n ${DOCKER_CMD} rm agentteams-controller 2>/dev/null || true\n fi\n if ${DOCKER_CMD} network ls --format '{{.Name}}' | grep -q \"^agentteams-net$\"; then\n log \"$(msg install.reinstall.removing_network)\"\n ${DOCKER_CMD} network rm agentteams-net 2>/dev/null || true\n fi\n if ${DOCKER_CMD} volume ls -q | grep -q \"^agentteams-data$\"; then\n log \"$(msg install.reinstall.removing_volume)\"\n ${DOCKER_CMD} volume rm agentteams-data 2>/dev/null || log \"$(msg install.reinstall.warn_volume_fail)\"\n fi\n if [ -d \"${existing_workspace}\" ]; then\n log \"$(msg install.reinstall.removing_workspace \"${existing_workspace}\")\"\n rm -rf \"${existing_workspace}\" || die \"$(msg install.reinstall.failed_rm_workspace)\"\n fi\n if [ -f \"${existing_env}\" ]; then\n log \"$(msg install.reinstall.removing_env \"${existing_env}\")\"\n rm -f \"${existing_env}\"\n fi\n log \"$(msg install.reinstall.cleanup_done)\"\n unset AGENTTEAMS_WORKSPACE_DIR\n return 0\n ;;\n 3|cancel|*)\n log \"$(msg install.existing.cancelled)\"\n exit 0\n ;;\n esac\n # Load existing env file as fallback (shell env vars take priority)\n if [ -f \"${existing_env}\" ]; then\n log \"$(msg install.loading_config \"${existing_env}\")\"\n while IFS='=' read -r key value; do\n case \"${key}\" in \\#*|\"\") continue ;; esac\n value=\"${value%%#*}\"\n value=\"${value#\"${value%%[![:space:]]*}\"}\"\n value=\"${value%\"${value##*[![:space:]]}\"}\"\n eval \"_existing_val=\\\"\\${${key}+x}\\\"\"\n if [ -z \"${_existing_val}\" ]; then export \"${key}=${value}\"; fi\n done < \"${existing_env}\"\n fi\n}\n\nstep_llm() {\n log \"$(msg llm.title)\"\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n if [ \"${AGENTTEAMS_LANGUAGE}\" = \"zh\" ]; then\n AGENTTEAMS_LLM_PROVIDER=\"${AGENTTEAMS_LLM_PROVIDER:-openai-compat}\"\n AGENTTEAMS_DEFAULT_MODEL=\"${AGENTTEAMS_DEFAULT_MODEL:-qwen3.6-plus}\"\n AGENTTEAMS_OPENAI_BASE_URL=\"${AGENTTEAMS_OPENAI_BASE_URL:-https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1}\"\n log \"$(msg llm.provider.label \"${AGENTTEAMS_LLM_PROVIDER}\")\"\n log \"$(msg llm.openai.base_url_label \"${AGENTTEAMS_OPENAI_BASE_URL}\")\"\n else\n AGENTTEAMS_LLM_PROVIDER=\"${AGENTTEAMS_LLM_PROVIDER:-qwen}\"\n AGENTTEAMS_DEFAULT_MODEL=\"${AGENTTEAMS_DEFAULT_MODEL:-qwen3.6-plus}\"\n AGENTTEAMS_OPENAI_BASE_URL=\"${AGENTTEAMS_OPENAI_BASE_URL:-}\"\n log \"$(msg llm.provider.qwen_default \"${AGENTTEAMS_LLM_PROVIDER}\")\"\n fi\n log \"$(msg llm.model.default \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n prompt AGENTTEAMS_LLM_API_KEY \"$(msg llm.apikey_prompt)\" \"\" \"true\"\n AGENTTEAMS_EMBEDDING_MODEL=\"${AGENTTEAMS_EMBEDDING_MODEL-text-embedding-v4}\"\n export AGENTTEAMS_LLM_PROVIDER AGENTTEAMS_DEFAULT_MODEL AGENTTEAMS_EMBEDDING_MODEL\n [ -n \"${AGENTTEAMS_OPENAI_BASE_URL+x}\" ] && export AGENTTEAMS_OPENAI_BASE_URL\n return 0\n fi\n\n # Upgrade Keep-All mode: skip provider selection, use loaded values\n local SKIP_TO_EMBEDDING=0\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ] && [ -n \"${AGENTTEAMS_LLM_PROVIDER}\" ]; then\n log \"$(msg llm.provider.label \"${AGENTTEAMS_LLM_PROVIDER}\")\"\n log \"$(msg llm.openai.base_url_label \"${AGENTTEAMS_OPENAI_BASE_URL}\")\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n PROVIDER_CHOICE=\"${AGENTTEAMS_LLM_PROVIDER}\"\n SKIP_TO_EMBEDDING=1\n else\n echo \"\"\n echo \"$(msg llm.providers_title)\"\n echo \"$(msg llm.provider.alibaba)\"\n echo \"$(msg llm.provider.openai_compat)\"\n echo \"\"\n local PROVIDER_CHOICE\n # If upgrade mode with loaded provider, show current as default\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_LLM_PROVIDER}\" ]; then\n local _prov_display\n case \"${AGENTTEAMS_LLM_PROVIDER}\" in\n openai-compat) _prov_display=\"2\" ;;\n *) _prov_display=\"1\" ;;\n esac\n read -e -p \"$(msg llm.provider.select) [${_prov_display}]: \" PROVIDER_CHOICE\n PROVIDER_CHOICE=\"${PROVIDER_CHOICE:-${_prov_display}}\"\n elif [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ]; then\n read -e -p \"$(msg llm.provider.select) [1]: \" PROVIDER_CHOICE\n PROVIDER_CHOICE=\"${PROVIDER_CHOICE:-1}\"\n else\n read -e -p \"$(msg llm.provider.select): \" PROVIDER_CHOICE\n PROVIDER_CHOICE=\"${PROVIDER_CHOICE:-1}\"\n fi\n if [ \"${PROVIDER_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n fi\n local ALIBABA_MODEL_CHOICE=\"\"\n local ALIBABA_ACCESS=\"\"\n case \"${PROVIDER_CHOICE}\" in\n 1|alibaba-cloud|openai-compat)\n if [ \"${AGENTTEAMS_LANGUAGE}\" = \"en\" ]; then\n AGENTTEAMS_LLM_PROVIDER=\"openai-compat\"\n AGENTTEAMS_OPENAI_BASE_URL=\"https://dashscope-intl.aliyuncs.com/compatible-mode/v1\"\n ALIBABA_ACCESS=\"tokenplan\"\n echo \"\"\n echo \"$(msg llm.codingplan.models_title)\"\n echo \"$(msg llm.codingplan.model.qwen36plus)\"\n echo \"$(msg llm.codingplan.model.glm5)\"\n echo \"$(msg llm.codingplan.model.kimi)\"\n echo \"$(msg llm.codingplan.model.minimax)\"\n echo \"\"\n local CODINGPLAN_MODEL_CHOICE\n # If upgrade with loaded model, show current as default\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_DEFAULT_MODEL}\" ]; then\n local _model_default\n case \"${AGENTTEAMS_DEFAULT_MODEL}\" in\n qwen3.6-plus) _model_default=\"1\" ;;\n glm-5) _model_default=\"2\" ;;\n kimi-k2.5) _model_default=\"3\" ;;\n MiniMax-M2.5) _model_default=\"4\" ;;\n *) _model_default=\"1\" ;;\n esac\n read -e -p \"$(msg llm.codingplan.model.select) [${_model_default}]: \" CODINGPLAN_MODEL_CHOICE\n CODINGPLAN_MODEL_CHOICE=\"${CODINGPLAN_MODEL_CHOICE:-${_model_default}}\"\n elif [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ]; then\n read -e -p \"$(msg llm.codingplan.model.select) [1]: \" CODINGPLAN_MODEL_CHOICE\n CODINGPLAN_MODEL_CHOICE=\"${CODINGPLAN_MODEL_CHOICE:-1}\"\n else\n read -e -p \"$(msg llm.codingplan.model.select): \" CODINGPLAN_MODEL_CHOICE\n CODINGPLAN_MODEL_CHOICE=\"${CODINGPLAN_MODEL_CHOICE:-1}\"\n fi\n if [ \"${CODINGPLAN_MODEL_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n case \"${CODINGPLAN_MODEL_CHOICE}\" in\n 1|qwen3.6-plus) AGENTTEAMS_DEFAULT_MODEL=\"qwen3.6-plus\" ;;\n 2|glm-5) AGENTTEAMS_DEFAULT_MODEL=\"glm-5\" ;;\n 3|kimi-k2.5) AGENTTEAMS_DEFAULT_MODEL=\"kimi-k2.5\" ;;\n 4|MiniMax-M2.5) AGENTTEAMS_DEFAULT_MODEL=\"MiniMax-M2.5\" ;;\n *) AGENTTEAMS_DEFAULT_MODEL=\"qwen3.6-plus\" ;;\n esac\n log \"$(msg llm.provider.selected_codingplan)\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n else\n echo \"\"\n echo \"$(msg llm.alibaba.models_title)\"\n echo \"$(msg llm.alibaba.model.tokenplan)\"\n echo \"$(msg llm.alibaba.model.bailian)\"\n echo \"$(msg llm.alibaba.model.codingplan_legacy)\"\n echo \"\"\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_OPENAI_BASE_URL}\" ]; then\n # Determine alibaba access type from loaded URL\n local _access_default=\"1\"\n if echo \"${AGENTTEAMS_OPENAI_BASE_URL}\" | grep -q \"token-plan\"; then\n _access_default=\"1\"\n elif echo \"${AGENTTEAMS_OPENAI_BASE_URL}\" | grep -q \"dashscope\"; then\n if echo \"${AGENTTEAMS_OPENAI_BASE_URL}\" | grep -q \"intl\"; then\n _access_default=\"3\"\n else\n _access_default=\"2\"\n fi\n fi\n read -e -p \"$(msg llm.alibaba.model.select) [${_access_default}]: \" ALIBABA_MODEL_CHOICE\n ALIBABA_MODEL_CHOICE=\"${ALIBABA_MODEL_CHOICE:-${_access_default}}\"\n elif [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ]; then\n read -e -p \"$(msg llm.alibaba.model.select) [1]: \" ALIBABA_MODEL_CHOICE\n ALIBABA_MODEL_CHOICE=\"${ALIBABA_MODEL_CHOICE:-1}\"\n else\n read -e -p \"$(msg llm.alibaba.model.select): \" ALIBABA_MODEL_CHOICE\n ALIBABA_MODEL_CHOICE=\"${ALIBABA_MODEL_CHOICE:-1}\"\n fi\n if [ \"${ALIBABA_MODEL_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n case \"${ALIBABA_MODEL_CHOICE}\" in\n 1|token-plan|tokenplan)\n ALIBABA_ACCESS=\"tokenplan\"\n AGENTTEAMS_LLM_PROVIDER=\"openai-compat\"\n AGENTTEAMS_OPENAI_BASE_URL=\"https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1\"\n echo \"\"\n echo \"$(msg llm.codingplan.models_title)\"\n echo \"$(msg llm.codingplan.model.qwen36plus)\"\n echo \"$(msg llm.codingplan.model.glm5)\"\n echo \"$(msg llm.codingplan.model.kimi)\"\n echo \"$(msg llm.codingplan.model.minimax)\"\n echo \"\"\n local CODINGPLAN_MODEL_CHOICE\n # If upgrade with loaded model, show current as default\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_DEFAULT_MODEL}\" ]; then\n local _model_default\n case \"${AGENTTEAMS_DEFAULT_MODEL}\" in\n qwen3.6-plus) _model_default=\"1\" ;;\n glm-5) _model_default=\"2\" ;;\n kimi-k2.5) _model_default=\"3\" ;;\n MiniMax-M2.5) _model_default=\"4\" ;;\n *) _model_default=\"1\" ;;\n esac\n read -e -p \"$(msg llm.codingplan.model.select) [${_model_default}]: \" CODINGPLAN_MODEL_CHOICE\n CODINGPLAN_MODEL_CHOICE=\"${CODINGPLAN_MODEL_CHOICE:-${_model_default}}\"\n elif [ \"${AGENTTEAMS_QUICKSTART}\" = \"1\" ]; then\n read -e -p \"$(msg llm.codingplan.model.select) [1]: \" CODINGPLAN_MODEL_CHOICE\n CODINGPLAN_MODEL_CHOICE=\"${CODINGPLAN_MODEL_CHOICE:-1}\"\n else\n read -e -p \"$(msg llm.codingplan.model.select): \" CODINGPLAN_MODEL_CHOICE\n CODINGPLAN_MODEL_CHOICE=\"${CODINGPLAN_MODEL_CHOICE:-1}\"\n fi\n if [ \"${CODINGPLAN_MODEL_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n case \"${CODINGPLAN_MODEL_CHOICE}\" in\n 1|qwen3.6-plus) AGENTTEAMS_DEFAULT_MODEL=\"qwen3.6-plus\" ;;\n 2|glm-5) AGENTTEAMS_DEFAULT_MODEL=\"glm-5\" ;;\n 3|kimi-k2.5) AGENTTEAMS_DEFAULT_MODEL=\"kimi-k2.5\" ;;\n 4|MiniMax-M2.5) AGENTTEAMS_DEFAULT_MODEL=\"MiniMax-M2.5\" ;;\n *) AGENTTEAMS_DEFAULT_MODEL=\"qwen3.6-plus\" ;;\n esac\n log \"$(msg llm.provider.selected_tokenplan)\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n ;;\n 2|qwen|bailian)\n ALIBABA_ACCESS=\"bailian\"\n AGENTTEAMS_LLM_PROVIDER=\"qwen\"\n AGENTTEAMS_OPENAI_BASE_URL=\"\"\n echo \"\"\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_DEFAULT_MODEL}\" ]; then\n read -e -p \"$(msg llm.qwen.model_prompt) [${AGENTTEAMS_DEFAULT_MODEL}]: \" AGENTTEAMS_DEFAULT_MODEL\n else\n read -e -p \"$(msg llm.qwen.model_prompt) [qwen3.6-plus]: \" AGENTTEAMS_DEFAULT_MODEL\n fi\n if [ \"${AGENTTEAMS_DEFAULT_MODEL}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_DEFAULT_MODEL=\"${AGENTTEAMS_DEFAULT_MODEL:-qwen3.6-plus}\"\n log \"$(msg llm.provider.selected_qwen)\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n prompt_custom_model_params \"${AGENTTEAMS_DEFAULT_MODEL}\" || return 0\n ;;\n 3|coding-plan|codingplan)\n ALIBABA_ACCESS=\"codingplan_legacy\"\n AGENTTEAMS_LLM_PROVIDER=\"openai-compat\"\n AGENTTEAMS_OPENAI_BASE_URL=\"https://coding.dashscope.aliyuncs.com/v1\"\n echo \"\"\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_DEFAULT_MODEL}\" ]; then\n read -e -p \"$(msg llm.qwen.model_prompt) [${AGENTTEAMS_DEFAULT_MODEL}]: \" AGENTTEAMS_DEFAULT_MODEL\n else\n read -e -p \"$(msg llm.qwen.model_prompt): \" AGENTTEAMS_DEFAULT_MODEL\n fi\n if [ \"${AGENTTEAMS_DEFAULT_MODEL}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_DEFAULT_MODEL=\"${AGENTTEAMS_DEFAULT_MODEL:-qwen3.6-plus}\"\n log \"$(msg llm.provider.selected_codingplan_legacy)\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n prompt_custom_model_params \"${AGENTTEAMS_DEFAULT_MODEL}\" || return 0\n ;;\n *)\n die \"$(msg llm.alibaba.model.invalid \"${ALIBABA_MODEL_CHOICE}\")\"\n ;;\n esac\n fi\n if [ \"${ALIBABA_ACCESS}\" = \"bailian\" ]; then\n log \"$(msg llm.apikey_hint_bailian)\"\n log \"$(msg llm.apikey_url_bailian)\"\n elif [ \"${AGENTTEAMS_LANGUAGE}\" = \"en\" ]; then\n log \"$(msg llm.apikey_hint_qwencloud)\"\n log \"$(msg llm.apikey_url_qwencloud)\"\n elif [ \"${ALIBABA_ACCESS}\" = \"codingplan_legacy\" ]; then\n log \"$(msg llm.apikey_hint_codingplan)\"\n log \"$(msg llm.apikey_url_codingplan)\"\n else\n log \"$(msg llm.apikey_hint_tokenplan)\"\n log \"$(msg llm.apikey_url_tokenplan)\"\n fi\n log \"\"\n prompt AGENTTEAMS_LLM_API_KEY \"$(msg llm.apikey_prompt)\" \"\" \"true\" || return 0\n if [ \"${ALIBABA_ACCESS}\" = \"bailian\" ]; then\n test_llm_connectivity \"https://dashscope.aliyuncs.com/compatible-mode/v1\" \"${AGENTTEAMS_LLM_API_KEY}\" \"${AGENTTEAMS_DEFAULT_MODEL}\" || return 0\n elif [ \"${ALIBABA_ACCESS}\" = \"codingplan_legacy\" ]; then\n test_llm_connectivity \"https://coding.dashscope.aliyuncs.com/v1\" \"${AGENTTEAMS_LLM_API_KEY}\" \"${AGENTTEAMS_DEFAULT_MODEL}\" \"$(msg llm.openai.test.fail.codingplan_legacy)\" || return 0\n elif [ \"${AGENTTEAMS_LANGUAGE}\" = \"en\" ]; then\n test_llm_connectivity \"${AGENTTEAMS_OPENAI_BASE_URL}\" \"${AGENTTEAMS_LLM_API_KEY}\" \"${AGENTTEAMS_DEFAULT_MODEL}\" \"$(msg llm.openai.test.fail.codingplan)\" || return 0\n else\n test_llm_connectivity \"${AGENTTEAMS_OPENAI_BASE_URL}\" \"${AGENTTEAMS_LLM_API_KEY}\" \"${AGENTTEAMS_DEFAULT_MODEL}\" \"$(msg llm.openai.test.fail.tokenplan)\" || return 0\n fi\n ;;\n 2|openai-compat)\n AGENTTEAMS_LLM_PROVIDER=\"openai-compat\"\n log \"$(msg llm.provider.selected_openai \"${AGENTTEAMS_LLM_PROVIDER}\")\"\n echo \"\"\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg llm.openai.base_url_prompt)\" \"${AGENTTEAMS_OPENAI_BASE_URL}\")\"\n else\n _current=\"${AGENTTEAMS_OPENAI_BASE_URL}\"\n if [ -n \"${_current}\" ]; then\n read -e -p \"$(msg llm.openai.base_url_prompt) [${_current}]: \" AGENTTEAMS_OPENAI_BASE_URL\n else\n read -e -p \"$(msg llm.openai.base_url_prompt) [https://api.openai.com/v1]: \" AGENTTEAMS_OPENAI_BASE_URL\n fi\n if [ \"${AGENTTEAMS_OPENAI_BASE_URL}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_OPENAI_BASE_URL=\"${AGENTTEAMS_OPENAI_BASE_URL:-${_current}}\"\n [ -z \"${AGENTTEAMS_OPENAI_BASE_URL}\" ] && AGENTTEAMS_OPENAI_BASE_URL=\"https://api.openai.com/v1\"\n fi\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg llm.openai.model_prompt)\" \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n else\n _current=\"${AGENTTEAMS_DEFAULT_MODEL}\"\n if [ -n \"${_current}\" ]; then\n read -e -p \"$(msg llm.openai.model_prompt) [${_current}]: \" AGENTTEAMS_DEFAULT_MODEL\n else\n read -e -p \"$(msg llm.openai.model_prompt) [gpt-5.4]: \" AGENTTEAMS_DEFAULT_MODEL\n fi\n if [ \"${AGENTTEAMS_DEFAULT_MODEL}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_DEFAULT_MODEL=\"${AGENTTEAMS_DEFAULT_MODEL:-${_current}}\"\n [ -z \"${AGENTTEAMS_DEFAULT_MODEL}\" ] && AGENTTEAMS_DEFAULT_MODEL=\"gpt-5.4\"\n fi\n log \"$(msg llm.openai.base_url_label \"${AGENTTEAMS_OPENAI_BASE_URL}\")\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_DEFAULT_MODEL}\")\"\n prompt_custom_model_params \"${AGENTTEAMS_DEFAULT_MODEL}\" || return 0\n log \"\"\n prompt AGENTTEAMS_LLM_API_KEY \"$(msg llm.apikey_prompt)\" \"\" \"true\" || return 0\n test_llm_connectivity \"${AGENTTEAMS_OPENAI_BASE_URL}\" \"${AGENTTEAMS_LLM_API_KEY}\" \"${AGENTTEAMS_DEFAULT_MODEL}\" || return 0\n ;;\n *)\n die \"$(msg llm.provider.invalid \"${PROVIDER_CHOICE}\")\"\n ;;\n esac\n\n # Skip to embedding if Keep-All mode handled LLM params\n if [ \"${SKIP_TO_EMBEDDING}\" = \"1\" ]; then\n if [ -n \"${AGENTTEAMS_EMBEDDING_MODEL}\" ]; then\n log \"$(msg llm.embedding.title)\"\n log \"$(msg llm.model.label \"${AGENTTEAMS_EMBEDDING_MODEL}\")\"\n log \"\"\n fi\n return 0\n fi\n\n # --- Embedding model (optional, auto-tested) ---\n echo \"\"\n log \"$(msg llm.embedding.title)\"\n log \"$(msg llm.embedding.hint)\"\n echo \"\"\n echo \"$(msg llm.embedding.option.default)\"\n echo \"$(msg llm.embedding.option.custom)\"\n echo \"$(msg llm.embedding.option.disable)\"\n echo \"\"\n local EMB_CHOICE\n read -e -p \"$(msg llm.embedding.select) [1]: \" EMB_CHOICE\n EMB_CHOICE=\"${EMB_CHOICE:-1}\"\n if [ \"${EMB_CHOICE}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n\n case \"${EMB_CHOICE}\" in\n 1)\n AGENTTEAMS_EMBEDDING_MODEL=\"text-embedding-v4\"\n ;;\n 2)\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg llm.embedding.custom_prompt)\" \"${AGENTTEAMS_EMBEDDING_MODEL}\")\"\n else\n _current=\"${AGENTTEAMS_EMBEDDING_MODEL}\"\n if [ -n \"${_current}\" ]; then\n read -e -p \"$(msg llm.embedding.custom_prompt) [${_current}]: \" AGENTTEAMS_EMBEDDING_MODEL\n else\n read -e -p \"$(msg llm.embedding.custom_prompt): \" AGENTTEAMS_EMBEDDING_MODEL\n fi\n if [ \"${AGENTTEAMS_EMBEDDING_MODEL}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_EMBEDDING_MODEL=\"${AGENTTEAMS_EMBEDDING_MODEL:-${_current}}\"\n fi\n ;;\n 3)\n AGENTTEAMS_EMBEDDING_MODEL=\"\"\n log \"$(msg llm.embedding.disabled)\"\n ;;\n *)\n AGENTTEAMS_EMBEDDING_MODEL=\"text-embedding-v4\"\n ;;\n esac\n\n if [ -n \"${AGENTTEAMS_EMBEDDING_MODEL}\" ]; then\n # Qwen provider uses dashscope directly; others use OPENAI_BASE_URL\n local EMB_BASE_URL=\"${AGENTTEAMS_OPENAI_BASE_URL}\"\n if [ \"${AGENTTEAMS_LLM_PROVIDER}\" = \"qwen\" ]; then\n EMB_BASE_URL=\"https://dashscope.aliyuncs.com/compatible-mode/v1\"\n fi\n if ! test_embedding_connectivity \"${EMB_BASE_URL}\" \"${AGENTTEAMS_LLM_API_KEY}\" \"${AGENTTEAMS_EMBEDDING_MODEL}\"; then\n AGENTTEAMS_EMBEDDING_MODEL=\"\"\n log \"$(msg llm.embedding.auto_disabled)\"\n fi\n fi\n\n export AGENTTEAMS_LLM_PROVIDER AGENTTEAMS_DEFAULT_MODEL\n [ -n \"${AGENTTEAMS_OPENAI_BASE_URL+x}\" ] && export AGENTTEAMS_OPENAI_BASE_URL\n log \"\"\n}\n\nstep_admin() {\n log \"$(msg admin.title)\"\n prompt AGENTTEAMS_ADMIN_USER \"$(msg admin.username_prompt)\" \"admin\" || return 0\n AGENTTEAMS_ADMIN_USER=\"$(printf '%s' \"${AGENTTEAMS_ADMIN_USER}\" | tr '[:upper:]' '[:lower:]')\"\n\n # Pre-set via env var: validate; in non-interactive mode fail fast,\n # in interactive mode warn and fall through to the retry prompt.\n if [ -n \"${AGENTTEAMS_ADMIN_PASSWORD:-}\" ]; then\n log \" $(msg prompt.preset \"$(msg admin.password_prompt)\")\"\n if [ ${#AGENTTEAMS_ADMIN_PASSWORD} -ge 8 ]; then\n log \"\"\n return 0\n fi\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n die \"$(msg admin.password_too_short \"${#AGENTTEAMS_ADMIN_PASSWORD}\")\"\n fi\n error \"$(msg admin.password_too_short \"${#AGENTTEAMS_ADMIN_PASSWORD}\")\"\n unset AGENTTEAMS_ADMIN_PASSWORD\n fi\n\n while true; do\n unset AGENTTEAMS_ADMIN_PASSWORD\n prompt_optional AGENTTEAMS_ADMIN_PASSWORD \"$(msg admin.password_prompt)\" \"true\" || return 0\n if [ -z \"${AGENTTEAMS_ADMIN_PASSWORD}\" ]; then\n AGENTTEAMS_ADMIN_PASSWORD=\"admin$(openssl rand -hex 6)\"\n log \"$(msg admin.password_generated)\"\n break\n fi\n if [ ${#AGENTTEAMS_ADMIN_PASSWORD} -ge 8 ]; then\n break\n fi\n error \"$(msg admin.password_too_short \"${#AGENTTEAMS_ADMIN_PASSWORD}\")\"\n done\n log \"\"\n}\n\nstep_network() {\n log \"$(msg port.local_only.title)\"\n echo \"\"\n echo \" 1) $(msg port.local_only.hint_yes)\"\n echo \" 2) $(msg port.local_only.hint_no)\"\n echo \"\"\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_LOCAL_ONLY=\"${AGENTTEAMS_LOCAL_ONLY:-1}\"\n elif [ -z \"${AGENTTEAMS_LOCAL_ONLY+x}\" ]; then\n local _local_choice\n read -e -p \"$(msg port.local_only.choice): \" _local_choice\n if [ \"${_local_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _local_choice=\"${_local_choice:-1}\"\n case \"${_local_choice}\" in\n 2|n|N|no|NO) AGENTTEAMS_LOCAL_ONLY=\"0\" ;;\n *) AGENTTEAMS_LOCAL_ONLY=\"1\" ;;\n esac\n fi\n export AGENTTEAMS_LOCAL_ONLY\n if [ \"${AGENTTEAMS_LOCAL_ONLY}\" = \"1\" ]; then\n log \"$(msg port.local_only.selected_local)\"\n else\n log \"$(msg port.local_only.selected_external)\"\n echo \"\"\n echo -e \"\\033[33m$(msg port.local_only.https_hint)\\033[0m\"\n fi\n}\n\nstep_ports() {\n log \"$(msg port.title)\"\n prompt AGENTTEAMS_PORT_GATEWAY \"$(msg port.gateway_prompt)\" \"18080\" || return 0\n prompt AGENTTEAMS_PORT_CONSOLE \"$(msg port.console_prompt)\" \"18001\" || return 0\n prompt AGENTTEAMS_PORT_ELEMENT_WEB \"$(msg port.element_prompt)\" \"18088\" || return 0\n prompt AGENTTEAMS_PORT_MANAGER_CONSOLE \"$(msg port.manager_console_prompt)\" \"18888\" || return 0\n log \"\"\n}\n\nstep_domains() {\n log \"$(msg domain.title)\"\n log \"$(msg domain.hint)\"\n prompt AGENTTEAMS_MATRIX_DOMAIN \"$(msg domain.matrix_prompt)\" \"matrix-local.agentteams.io:${AGENTTEAMS_PORT_GATEWAY}\" || return 0\n prompt AGENTTEAMS_MATRIX_CLIENT_DOMAIN \"$(msg domain.element_prompt)\" \"matrix-client-local.agentteams.io\" || return 0\n prompt AGENTTEAMS_AI_GATEWAY_DOMAIN \"$(msg domain.gateway_prompt)\" \"aigw-local.agentteams.io\" || return 0\n prompt AGENTTEAMS_FS_DOMAIN \"$(msg domain.fs_prompt)\" \"fs-local.agentteams.io\" || return 0\n if [ \"${AGENTTEAMS_MANAGER_RUNTIME}\" != \"copaw\" ]; then\n prompt AGENTTEAMS_CONSOLE_DOMAIN \"$(msg domain.console_prompt)\" \"console-local.agentteams.io\" || return 0\n fi\n log \"\"\n}\n\nstep_github() {\n log \"$(msg github.title)\"\n prompt_optional AGENTTEAMS_GITHUB_TOKEN \"$(msg github.token_prompt)\" \"true\" || return 0\n}\n\nstep_skills() {\n log \"\"\n log \"$(msg skills.title)\"\n prompt_optional AGENTTEAMS_SKILLS_API_URL \"$(msg skills.url_prompt)\" || return 0\n log \"\"\n}\n\nstep_volume() {\n log \"$(msg data.title)\"\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_DATA_DIR=\"${AGENTTEAMS_DATA_DIR:-agentteams-data}\"\n log \" $(msg data.volume_using \"${AGENTTEAMS_DATA_DIR}\") (non-interactive, skipped)\"\n export AGENTTEAMS_DATA_DIR\n return 0\n fi\n # ─────────────────────────────────────────────────────────────────\n if [ -z \"${AGENTTEAMS_DATA_DIR+x}\" ]; then\n local _input\n read -e -p \"$(msg data.volume_prompt): \" _input\n if [ \"${_input}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_DATA_DIR=\"${_input:-agentteams-data}\"\n export AGENTTEAMS_DATA_DIR\n fi\n AGENTTEAMS_DATA_DIR=\"${AGENTTEAMS_DATA_DIR:-agentteams-data}\"\n log \"$(msg data.volume_using \"${AGENTTEAMS_DATA_DIR}\")\"\n}\n\nstep_workspace() {\n log \"$(msg workspace.title)\"\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_WORKSPACE_DIR=\"${AGENTTEAMS_WORKSPACE_DIR:-${HOME}/agentteams-manager}\"\n AGENTTEAMS_WORKSPACE_DIR=\"$(cd \"${AGENTTEAMS_WORKSPACE_DIR}\" 2>/dev/null && pwd || echo \"${AGENTTEAMS_WORKSPACE_DIR}\")\"\n mkdir -p \"${AGENTTEAMS_WORKSPACE_DIR}\"\n log \" $(msg workspace.dir_label \"${AGENTTEAMS_WORKSPACE_DIR}\") (non-interactive, skipped)\"\n export AGENTTEAMS_WORKSPACE_DIR\n return 0\n fi\n # ─────────────────────────────────────────────────────────────────\n local _input\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg workspace.dir_prompt \"${HOME}/agentteams-manager\")\" \"${AGENTTEAMS_WORKSPACE_DIR}\")\"\n _input=\"${AGENTTEAMS_WORKSPACE_DIR}\"\n else\n local _current=\"${AGENTTEAMS_WORKSPACE_DIR}\"\n if [ -n \"${_current}\" ]; then\n read -e -p \"$(msg workspace.dir_prompt \"${HOME}/agentteams-manager\") [${_current}]: \" _input\n else\n read -e -p \"$(msg workspace.dir_prompt \"${HOME}/agentteams-manager\"): \" _input\n fi\n if [ \"${_input}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _input=\"${_input:-${_current}}\"\n fi\n AGENTTEAMS_WORKSPACE_DIR=\"${_input:-${HOME}/agentteams-manager}\"\n export AGENTTEAMS_WORKSPACE_DIR\n AGENTTEAMS_WORKSPACE_DIR=\"$(cd \"${AGENTTEAMS_WORKSPACE_DIR}\" 2>/dev/null && pwd || echo \"${AGENTTEAMS_WORKSPACE_DIR}\")\"\n mkdir -p \"${AGENTTEAMS_WORKSPACE_DIR}\"\n log \"$(msg workspace.dir_label \"${AGENTTEAMS_WORKSPACE_DIR}\")\"\n}\n\nstep_dashboard() {\n AGENTTEAMS_DASHBOARD=\"${AGENTTEAMS_DASHBOARD:-1}\"\n AGENTTEAMS_DASHBOARD_VERSION=\"${AGENTTEAMS_DASHBOARD_VERSION:-v1.2.0}\"\n AGENTTEAMS_PORT_DASHBOARD=\"${AGENTTEAMS_PORT_DASHBOARD:-13000}\"\n AGENTTEAMS_AI_GATEWAY_ADMIN_URL=\"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL:-}\"\n\n # Compute the default image for the current version.\n # We track whether the current image matches the derived default so that\n # changing the version also updates the image (unless the user has\n # explicitly overridden it to a non-default value).\n _dashboard_default_image() {\n echo \"${AGENTTEAMS_REGISTRY}/agentteams/agentteams-dashboard:${1:-${AGENTTEAMS_DASHBOARD_VERSION}}\"\n }\n local _init_default\n _init_default=$(_dashboard_default_image)\n if [ -z \"${AGENTTEAMS_DASHBOARD_IMAGE:-}\" ]; then\n AGENTTEAMS_DASHBOARD_IMAGE=\"${_init_default}\"\n fi\n\n # Recompute default image when the version changes AND the current image\n # matches the default for the *previous* (env-file) version.\n # This handles non-interactive upgrades correctly:\n # - env file loads old default image + old version\n # - CLI passes new version\n # - image should follow (because it was auto-derived before)\n # But preserves truly custom images (same repo but custom tag like :canary)\n # because they won't match the old-version default exactly.\n local _old_saved_version=\"\"\n local _env_file=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n if [ -f \"${_env_file}\" ]; then\n _old_saved_version=\"$(grep '^AGENTTEAMS_DASHBOARD_VERSION=' \"${_env_file}\" 2>/dev/null | cut -d= -f2- | tr -d '\\r')\"\n fi\n if [ -n \"${_old_saved_version}\" ] && \\\n [ \"${_old_saved_version}\" != \"${AGENTTEAMS_DASHBOARD_VERSION}\" ] && \\\n [ \"${AGENTTEAMS_DASHBOARD_IMAGE}\" = \"$(_dashboard_default_image \"${_old_saved_version}\")\" ]; then\n AGENTTEAMS_DASHBOARD_IMAGE=$(_dashboard_default_image)\n fi\n\n # Quick Start: accept all defaults without prompting. The dashboard is\n # installed by default (AGENTTEAMS_DASHBOARD=1); set AGENTTEAMS_DASHBOARD=0\n # to opt out.\n if [ \"${AGENTTEAMS_QUICKSTART:-0}\" = \"1\" ]; then\n if [ \"${AGENTTEAMS_DASHBOARD}\" = \"1\" ]; then\n log \"$(msg dash.summary \"${AGENTTEAMS_PORT_DASHBOARD}\" \"${AGENTTEAMS_DASHBOARD_IMAGE}\") (quick start)\"\n else\n log \"$(msg dash.skip) (quick start)\"\n fi\n export AGENTTEAMS_DASHBOARD AGENTTEAMS_DASHBOARD_VERSION AGENTTEAMS_PORT_DASHBOARD AGENTTEAMS_DASHBOARD_IMAGE AGENTTEAMS_AI_GATEWAY_ADMIN_URL\n return 0\n fi\n\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_DASHBOARD=\"${AGENTTEAMS_DASHBOARD:-1}\"\n if [ \"${AGENTTEAMS_DASHBOARD}\" = \"1\" ]; then\n log \"$(msg dash.summary \"${AGENTTEAMS_PORT_DASHBOARD}\" \"${AGENTTEAMS_DASHBOARD_IMAGE}\") (non-interactive)\"\n else\n log \"$(msg dash.skip) (non-interactive)\"\n fi\n export AGENTTEAMS_DASHBOARD AGENTTEAMS_DASHBOARD_VERSION AGENTTEAMS_PORT_DASHBOARD AGENTTEAMS_DASHBOARD_IMAGE AGENTTEAMS_AI_GATEWAY_ADMIN_URL\n return 0\n fi\n\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ \"${AGENTTEAMS_UPGRADE_KEEP_ALL}\" = \"1\" ]; then\n log \"$(msg prompt.upgrade_keep \"\" \"${AGENTTEAMS_DASHBOARD}\") dashboard=${AGENTTEAMS_DASHBOARD} version=${AGENTTEAMS_DASHBOARD_VERSION} port=${AGENTTEAMS_PORT_DASHBOARD}\"\n export AGENTTEAMS_DASHBOARD AGENTTEAMS_DASHBOARD_VERSION AGENTTEAMS_PORT_DASHBOARD AGENTTEAMS_DASHBOARD_IMAGE AGENTTEAMS_AI_GATEWAY_ADMIN_URL\n return 0\n fi\n\n local _input\n echo \"\"\n read -p \"$(msg dash.prompt) [Y/n]: \" _input\n case \"${_input:-}\" in\n [nN0]) AGENTTEAMS_DASHBOARD=0 ;;\n *) AGENTTEAMS_DASHBOARD=1 ;;\n esac\n if [ \"${_input}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n\n if [ \"${AGENTTEAMS_DASHBOARD}\" != \"1\" ]; then\n log \"$(msg dash.skip)\"\n export AGENTTEAMS_DASHBOARD AGENTTEAMS_DASHBOARD_VERSION AGENTTEAMS_PORT_DASHBOARD AGENTTEAMS_DASHBOARD_IMAGE AGENTTEAMS_AI_GATEWAY_ADMIN_URL\n return 0\n fi\n\n # Dashboard version\n local _current_version=\"${AGENTTEAMS_DASHBOARD_VERSION}\"\n local _old_version=\"${AGENTTEAMS_DASHBOARD_VERSION}\"\n local _old_default_image\n _old_default_image=$(_dashboard_default_image \"${_old_version}\")\n read -p \"$(msg dash.version_prompt) [${_current_version}]: \" _input\n AGENTTEAMS_DASHBOARD_VERSION=\"${_input:-${_current_version}}\"\n\n # Dashboard port\n local _current_port=\"${AGENTTEAMS_PORT_DASHBOARD}\"\n read -p \"$(msg dash.port_prompt) [${_current_port}]: \" _input\n AGENTTEAMS_PORT_DASHBOARD=\"${_input:-${_current_port}}\"\n\n # Dashboard image: if the current image matches the default derived from\n # the OLD version, recompute it from the NEW version. This handles both\n # fresh installs and upgrades where the previous image was the default.\n # Only a real user override (image != default for old version) is preserved.\n if [ \"${AGENTTEAMS_DASHBOARD_VERSION}\" != \"${_old_version}\" ] && \\\n [ \"${AGENTTEAMS_DASHBOARD_IMAGE}\" = \"${_old_default_image}\" ]; then\n AGENTTEAMS_DASHBOARD_IMAGE=$(_dashboard_default_image)\n fi\n local _current_image=\"${AGENTTEAMS_DASHBOARD_IMAGE}\"\n read -p \"$(msg dash.image_prompt) [${_current_image}]: \" _input\n if [ -n \"${_input}\" ]; then\n AGENTTEAMS_DASHBOARD_IMAGE=\"${_input}\"\n fi\n\n # Higress Console URL (explicit config takes priority; auto-detect as fallback)\n local _higress_url=\"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\"\n if [ -z \"${_higress_url}\" ]; then\n if ${DOCKER_CMD} ps --format '{{.Names}}' | grep -q \"^agentteams-controller$\"; then\n if ${DOCKER_CMD} exec agentteams-controller curl -sf --max-time 2 http://127.0.0.1:8001/ >/dev/null 2>&1; then\n _higress_url=\"http://agentteams-controller:8001\"\n log \"$(msg dash.auto_url): ${_higress_url}\"\n else\n log \"$(msg dash.auto_url_fail)\"\n fi\n fi\n fi\n read -p \"$(msg dash.gateway_prompt) [${_higress_url:-<auto>}]: \" _input\n AGENTTEAMS_AI_GATEWAY_ADMIN_URL=\"${_input:-${_higress_url}}\"\n\n # Normalize URL: add http:// prefix if missing\n if [ -n \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\" ]; then\n case \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\" in\n http://*|https://*) ;;\n *) AGENTTEAMS_AI_GATEWAY_ADMIN_URL=\"http://${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\" ;;\n esac\n fi\n\n # Verify the Higress Console URL is reachable (best-effort warning only)\n if [ -n \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\" ]; then\n local _gw_reachable=0\n # If the URL points to the controller container, test from inside the container\n if echo \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\" | grep -qE \"(agentteams-controller|hiclaw-controller|127\\.0\\.0\\.1|localhost)\"; then\n if ${DOCKER_CMD} ps --format '{{.Names}}' | grep -q \"^agentteams-controller$\"; then\n if ${DOCKER_CMD} exec agentteams-controller curl -sf --max-time 3 \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}/\" >/dev/null 2>&1; then\n _gw_reachable=1\n fi\n fi\n else\n # External URL — probe from inside the controller (same network as\n # the Dashboard container) when possible; fall back to the host.\n if ${DOCKER_CMD} ps --format '{{.Names}}' | grep -q \"^agentteams-controller$\"; then\n if ${DOCKER_CMD} exec agentteams-controller curl -sf --max-time 3 \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}/\" >/dev/null 2>&1; then\n _gw_reachable=1\n fi\n elif curl -sf --max-time 3 \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}/\" >/dev/null 2>&1; then\n _gw_reachable=1\n fi\n fi\n if [ \"${_gw_reachable}\" = \"0\" ]; then\n log \"WARNING: Higress Console URL may not be reachable: ${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\"\n log \" Dashboard will still work, but shared login via Higress Console may fail.\"\n log \" To fix: verify the URL is correct and the Higress Console service is running.\"\n fi\n fi\n\n log \"$(msg dash.summary \"${AGENTTEAMS_PORT_DASHBOARD}\" \"${AGENTTEAMS_DASHBOARD_IMAGE}\")\"\n export AGENTTEAMS_DASHBOARD AGENTTEAMS_DASHBOARD_VERSION AGENTTEAMS_PORT_DASHBOARD AGENTTEAMS_DASHBOARD_IMAGE AGENTTEAMS_AI_GATEWAY_ADMIN_URL\n}\n\nstep_runtime() {\n log \"$(msg worker_runtime.title)\"\n echo \"\"\n echo \" 1) $(msg worker_runtime.copaw)\"\n echo \" 2) $(msg worker_runtime.openclaw)\"\n if ! _ver_lt \"${AGENTTEAMS_VERSION}\" \"v1.1.0\"; then\n echo \" 3) $(msg worker_runtime.hermes)\"\n fi\n echo \"\"\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"${AGENTTEAMS_DEFAULT_WORKER_RUNTIME:-copaw}\"\n elif [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_DEFAULT_WORKER_RUNTIME}\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg worker_runtime.title_short)\" \"${AGENTTEAMS_DEFAULT_WORKER_RUNTIME}\")\"\n local _runtime_choice\n read -e -p \"$(msg worker_runtime.choice): \" _runtime_choice\n if [ \"${_runtime_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n if [ -n \"${_runtime_choice}\" ]; then\n case \"${_runtime_choice}\" in\n 2) AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"openclaw\" ;;\n 3) if ! _ver_lt \"${AGENTTEAMS_VERSION}\" \"v1.1.0\"; then\n AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"hermes\"\n else\n AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"copaw\"\n fi ;;\n *) AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"copaw\" ;;\n esac\n fi\n elif [ -z \"${AGENTTEAMS_DEFAULT_WORKER_RUNTIME+x}\" ]; then\n local _runtime_choice\n read -e -p \"$(msg worker_runtime.choice): \" _runtime_choice\n if [ \"${_runtime_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _runtime_choice=\"${_runtime_choice:-1}\"\n case \"${_runtime_choice}\" in\n 2) AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"openclaw\" ;;\n 3) if ! _ver_lt \"${AGENTTEAMS_VERSION}\" \"v1.1.0\"; then\n AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"hermes\"\n else\n AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"copaw\"\n fi ;;\n *) AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"copaw\" ;;\n esac\n fi\n export AGENTTEAMS_DEFAULT_WORKER_RUNTIME\n log \"$(msg worker_runtime.selected \"${AGENTTEAMS_DEFAULT_WORKER_RUNTIME}\")\"\n}\n\nstep_manager_runtime() {\n log \"$(msg manager_runtime.title)\"\n echo \"\"\n echo \" 1) $(msg manager_runtime.copaw)\"\n echo \" 2) $(msg manager_runtime.openclaw)\"\n echo \"\"\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_MANAGER_RUNTIME=\"${AGENTTEAMS_MANAGER_RUNTIME:-copaw}\"\n elif [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_MANAGER_RUNTIME}\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg manager_runtime.title_short)\" \"${AGENTTEAMS_MANAGER_RUNTIME}\")\"\n local _runtime_choice\n read -e -p \"$(msg manager_runtime.choice): \" _runtime_choice\n if [ \"${_runtime_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n if [ -n \"${_runtime_choice}\" ]; then\n case \"${_runtime_choice}\" in\n 2) AGENTTEAMS_MANAGER_RUNTIME=\"openclaw\" ;;\n *) AGENTTEAMS_MANAGER_RUNTIME=\"copaw\" ;;\n esac\n fi\n elif [ -z \"${AGENTTEAMS_MANAGER_RUNTIME+x}\" ]; then\n local _runtime_choice\n read -e -p \"$(msg manager_runtime.choice): \" _runtime_choice\n if [ \"${_runtime_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _runtime_choice=\"${_runtime_choice:-1}\"\n case \"${_runtime_choice}\" in\n 2) AGENTTEAMS_MANAGER_RUNTIME=\"openclaw\" ;;\n *) AGENTTEAMS_MANAGER_RUNTIME=\"copaw\" ;;\n esac\n fi\n export AGENTTEAMS_MANAGER_RUNTIME\n log \"$(msg manager_runtime.selected \"${AGENTTEAMS_MANAGER_RUNTIME}\")\"\n}\n\nstep_e2ee() {\n log \"\"\n log \"$(msg matrix_e2ee.title)\"\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_MATRIX_E2EE=\"${AGENTTEAMS_MATRIX_E2EE:-0}\"\n log \" $(msg matrix_e2ee.title_short) = ${AGENTTEAMS_MATRIX_E2EE} (non-interactive, skipped)\"\n export AGENTTEAMS_MATRIX_E2EE\n return 0\n fi\n # ─────────────────────────────────────────────────────────────────\n echo \"\"\n echo -e \" $(msg matrix_e2ee.desc)\"\n echo \"\"\n echo \" 1) $(msg matrix_e2ee.disable)\"\n echo \" 2) $(msg matrix_e2ee.enable)\"\n echo \"\"\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_MATRIX_E2EE}\" ]; then\n local _e2ee_display; if [ \"${AGENTTEAMS_MATRIX_E2EE}\" = \"1\" ]; then _e2ee_display=\"$(msg matrix_e2ee.val_enabled)\"; else _e2ee_display=\"$(msg matrix_e2ee.val_disabled)\"; fi\n log \"$(msg prompt.upgrade_keep \"$(msg matrix_e2ee.title_short)\" \"${_e2ee_display}\")\"\n local _e2ee_choice\n read -e -p \"$(msg matrix_e2ee.choice): \" _e2ee_choice\n if [ \"${_e2ee_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n if [ -n \"${_e2ee_choice}\" ]; then\n case \"${_e2ee_choice}\" in\n 2) AGENTTEAMS_MATRIX_E2EE=\"1\" ;;\n *) AGENTTEAMS_MATRIX_E2EE=\"0\" ;;\n esac\n fi\n elif [ -z \"${AGENTTEAMS_MATRIX_E2EE+x}\" ]; then\n local _e2ee_choice\n read -e -p \"$(msg matrix_e2ee.choice): \" _e2ee_choice\n if [ \"${_e2ee_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _e2ee_choice=\"${_e2ee_choice:-1}\"\n case \"${_e2ee_choice}\" in\n 2) AGENTTEAMS_MATRIX_E2EE=\"1\" ;;\n *) AGENTTEAMS_MATRIX_E2EE=\"0\" ;;\n esac\n fi\n AGENTTEAMS_MATRIX_E2EE=\"${AGENTTEAMS_MATRIX_E2EE:-0}\"\n export AGENTTEAMS_MATRIX_E2EE\n if [ \"${AGENTTEAMS_MATRIX_E2EE}\" = \"1\" ]; then\n log \"$(msg matrix_e2ee.selected_enabled)\"\n else\n log \"$(msg matrix_e2ee.selected_disabled)\"\n fi\n}\n\nstep_docker_proxy() {\n # Only relevant when socket mounting is enabled\n if [ \"${AGENTTEAMS_MOUNT_SOCKET}\" != \"1\" ]; then\n AGENTTEAMS_DOCKER_PROXY=\"0\"\n return 0\n fi\n\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_DOCKER_PROXY=\"${AGENTTEAMS_DOCKER_PROXY:-0}\"\n log \" $(msg docker_proxy.title_short) = ${AGENTTEAMS_DOCKER_PROXY} (non-interactive, skipped)\"\n export AGENTTEAMS_DOCKER_PROXY\n return 0\n fi\n # ─────────────────────────────────────────────────────────────────\n\n echo \"\"\n echo -e \" \\033[1m$(msg docker_proxy.title)\\033[0m\"\n echo \"\"\n echo -e \" $(msg docker_proxy.desc)\"\n echo \"\"\n echo \" 1) $(msg docker_proxy.enable)\"\n echo \" 2) $(msg docker_proxy.disable)\"\n echo \"\"\n\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_DOCKER_PROXY}\" ]; then\n local _proxy_display; if [ \"${AGENTTEAMS_DOCKER_PROXY}\" = \"1\" ]; then _proxy_display=\"$(msg docker_proxy.val_enabled)\"; else _proxy_display=\"$(msg docker_proxy.val_disabled)\"; fi\n log \"$(msg prompt.upgrade_keep \"$(msg docker_proxy.title_short)\" \"${_proxy_display}\")\"\n local _choice\n read -e -p \"$(msg docker_proxy.choice): \" _choice\n if [ \"${_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n if [ -n \"${_choice}\" ]; then\n case \"${_choice}\" in\n 2) AGENTTEAMS_DOCKER_PROXY=\"0\" ;;\n *) AGENTTEAMS_DOCKER_PROXY=\"1\" ;;\n esac\n fi\n elif [ -z \"${AGENTTEAMS_DOCKER_PROXY+x}\" ]; then\n local _choice\n read -e -p \"$(msg docker_proxy.choice): \" _choice\n if [ \"${_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _choice=\"${_choice:-1}\"\n case \"${_choice}\" in\n 2) AGENTTEAMS_DOCKER_PROXY=\"0\" ;;\n *) AGENTTEAMS_DOCKER_PROXY=\"1\" ;;\n esac\n fi\n AGENTTEAMS_DOCKER_PROXY=\"${AGENTTEAMS_DOCKER_PROXY:-1}\"\n export AGENTTEAMS_DOCKER_PROXY\n if [ \"${AGENTTEAMS_DOCKER_PROXY}\" = \"1\" ]; then\n log \"$(msg docker_proxy.selected_enabled)\"\n\n # Prompt for additional allowed image sources\n echo \"\"\n echo -e \" $(msg docker_proxy.registries_desc)\"\n echo \"\"\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_PROXY_ALLOWED_REGISTRIES}\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg docker_proxy.registries_label)\" \"${AGENTTEAMS_PROXY_ALLOWED_REGISTRIES}\")\"\n local _reg_input\n read -e -p \"$(msg docker_proxy.registries_prompt): \" _reg_input\n if [ \"${_reg_input}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n [ -n \"${_reg_input}\" ] && AGENTTEAMS_PROXY_ALLOWED_REGISTRIES=\"${_reg_input}\"\n elif [ -z \"${AGENTTEAMS_PROXY_ALLOWED_REGISTRIES+x}\" ]; then\n local _reg_input\n read -e -p \"$(msg docker_proxy.registries_prompt): \" _reg_input\n if [ \"${_reg_input}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_PROXY_ALLOWED_REGISTRIES=\"${_reg_input:-}\"\n fi\n export AGENTTEAMS_PROXY_ALLOWED_REGISTRIES\n else\n log \"$(msg docker_proxy.selected_disabled)\"\n fi\n}\n\nstep_idle() {\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_WORKER_IDLE_TIMEOUT=\"${AGENTTEAMS_WORKER_IDLE_TIMEOUT:-720}\"\n log \" $(msg idle_timeout.label) = ${AGENTTEAMS_WORKER_IDLE_TIMEOUT} (non-interactive, skipped)\"\n export AGENTTEAMS_WORKER_IDLE_TIMEOUT\n return 0\n fi\n # ─────────────────────────────────────────────────────────────────\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_WORKER_IDLE_TIMEOUT}\" ]; then\n log \"$(msg prompt.upgrade_keep \"$(msg idle_timeout.label)\" \"${AGENTTEAMS_WORKER_IDLE_TIMEOUT}\")\"\n local _idle_timeout\n read -e -p \"$(msg idle_timeout.prompt): \" _idle_timeout\n if [ \"${_idle_timeout}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n [ -n \"${_idle_timeout}\" ] && AGENTTEAMS_WORKER_IDLE_TIMEOUT=\"${_idle_timeout}\"\n elif [ -z \"${AGENTTEAMS_WORKER_IDLE_TIMEOUT+x}\" ]; then\n local _idle_timeout\n read -e -p \"$(msg idle_timeout.prompt): \" _idle_timeout\n if [ \"${_idle_timeout}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_WORKER_IDLE_TIMEOUT=\"${_idle_timeout:-720}\"\n fi\n AGENTTEAMS_WORKER_IDLE_TIMEOUT=\"${AGENTTEAMS_WORKER_IDLE_TIMEOUT:-720}\"\n export AGENTTEAMS_WORKER_IDLE_TIMEOUT\n log \"$(msg idle_timeout.selected \"${AGENTTEAMS_WORKER_IDLE_TIMEOUT}\")\"\n}\n\nstep_hostshare() {\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_HOST_SHARE_DIR=\"${AGENTTEAMS_HOST_SHARE_DIR:-${HOME}}\"\n log \" $(msg host_share.label) = ${AGENTTEAMS_HOST_SHARE_DIR} (non-interactive, skipped)\"\n export AGENTTEAMS_HOST_SHARE_DIR\n return 0\n fi\n # ─────────────────────────────────────────────────────────────────\n local _current=\"${AGENTTEAMS_HOST_SHARE_DIR}\"\n local _share_dir\n if [ -n \"${_current}\" ]; then\n read -e -p \"$(msg host_share.prompt \"$HOME\") [${_current}]: \" _share_dir\n else\n read -e -p \"$(msg host_share.prompt \"$HOME\"): \" _share_dir\n fi\n if [ \"${_share_dir}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n AGENTTEAMS_HOST_SHARE_DIR=\"${_share_dir:-${_current:-$HOME}}\"\n export AGENTTEAMS_HOST_SHARE_DIR\n}\n\nstep_podman_autostart() {\n echo \"\"\n log \"$(msg podman.autostart.title)\"\n echo \"\"\n echo -e \" $(msg podman.autostart.desc)\"\n echo \"\"\n echo \" 1) $(msg podman.autostart.enable)\"\n echo \" 2) $(msg podman.autostart.disable)\"\n echo \"\"\n\n if [ \"${AGENTTEAMS_UPGRADE}\" = \"1\" ] && [ -n \"${AGENTTEAMS_PODMAN_AUTOSTART}\" ]; then\n local _disp; if [ \"${AGENTTEAMS_PODMAN_AUTOSTART}\" = \"1\" ]; then _disp=\"$(msg podman.autostart.val_enabled)\"; else _disp=\"$(msg podman.autostart.val_disabled)\"; fi\n log \"$(msg prompt.upgrade_keep \"$(msg podman.autostart.title_short)\" \"${_disp}\")\"\n local _choice\n read -e -p \"$(msg podman.autostart.choice): \" _choice\n if [ \"${_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n if [ -n \"${_choice}\" ]; then\n case \"${_choice}\" in\n 2) AGENTTEAMS_PODMAN_AUTOSTART=\"0\" ;;\n *) AGENTTEAMS_PODMAN_AUTOSTART=\"1\" ;;\n esac\n fi\n elif [ -z \"${AGENTTEAMS_PODMAN_AUTOSTART+x}\" ]; then\n local _choice\n read -e -p \"$(msg podman.autostart.choice) [1]: \" _choice\n if [ \"${_choice}\" = \"b\" ]; then STEP_RESULT=\"back\"; return 0; fi\n _choice=\"${_choice:-1}\"\n case \"${_choice}\" in\n 2) AGENTTEAMS_PODMAN_AUTOSTART=\"0\" ;;\n *) AGENTTEAMS_PODMAN_AUTOSTART=\"1\" ;;\n esac\n fi\n AGENTTEAMS_PODMAN_AUTOSTART=\"${AGENTTEAMS_PODMAN_AUTOSTART:-1}\"\n export AGENTTEAMS_PODMAN_AUTOSTART\n if [ \"${AGENTTEAMS_PODMAN_AUTOSTART}\" = \"1\" ]; then\n log \"$(msg podman.autostart.selected_enabled)\"\n else\n log \"$(msg podman.autostart.selected_disabled)\"\n fi\n}\n\n# ============================================================\n# Setup dedicated Podman autostart service for AgentTeams\n# ============================================================\nsetup_podman_autostart() {\n # Only execute if using podman and autostart is enabled\n if [ \"${DOCKER_CMD:-docker}\" != \"podman\" ] || [ \"${AGENTTEAMS_PODMAN_AUTOSTART:-0}\" != \"1\" ]; then\n return 0\n fi\n\n log \"$(msg install.podman.autostart_title)\"\n\n local current_user\n current_user=\"$(whoami)\"\n\n # Define the dedicated AgentTeams service content\n # Strict scoping: Only manage containers matching '--filter name=agentteams'\n local _service_content=\"[Unit]\nDescription=AgentTeams Dedicated Podman Autostart Service\nDocumentation=https://github.com/agentscope-ai/AgentTeams\nWants=network-online.target\nAfter=network-online.target\n\n[Service]\nType=oneshot\nRemainAfterExit=true\nEnvironment=LOGGING=\\\"--log-level=info\\\"\nExecStart=/usr/bin/podman \\$LOGGING start --filter name=agentteams --filter restart-policy=always --filter restart-policy=unless-stopped\nExecStop=/usr/bin/podman \\$LOGGING stop --filter name=agentteams --filter restart-policy=always --filter restart-policy=unless-stopped\n\n[Install]\nWantedBy=default.target\"\n\n local _autostart_ok=0\n\n if [ \"${current_user}\" = \"root\" ]; then\n # Rootful Mode (System-wide dedicated service)\n log \"$(msg install.podman.root_setup)\"\n\n local _sys_dir=\"/etc/systemd/system\"\n mkdir -p \"${_sys_dir}\"\n echo \"${_service_content}\" > \"${_sys_dir}/agentteams-podman-restart.service\"\n\n systemctl daemon-reload >/dev/null 2>&1 || true\n\n if systemctl enable --now agentteams-podman-restart.service >/dev/null 2>&1; then\n log \"$(msg install.podman.root_success)\"\n _autostart_ok=1\n else\n log \"$(msg install.podman.root_fail)\"\n fi\n else\n # Rootless Mode (User-level dedicated service)\n log \"$(msg install.podman.user_setup \"${current_user}\")\"\n\n local _linger_ok=0\n\n # 1. Enable Linger for background execution (Graceful escalation strategy)\n if loginctl show-user \"${current_user}\" --property=Linger 2>/dev/null | grep -q \"Linger=yes\"; then\n _linger_ok=1\n else\n log \"$(msg install.podman.linger_enable)\"\n\n # Step A: Try to enable linger natively without sudo\n if loginctl enable-linger \"${current_user}\" >/dev/null 2>&1; then\n log \"Successfully enabled systemd linger natively.\"\n _linger_ok=1\n else\n # Step B: Fallback to sudo\n log \"Native linger enablement failed. Distro security policy requires privileges.\"\n if command -v sudo >/dev/null 2>&1; then\n log \"Attempting to enable systemd linger via sudo (enter password if prompted)...\"\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n sudo -n loginctl enable-linger \"${current_user}\" 2>/dev/null && _linger_ok=1 || log \"$(msg install.podman.linger_warn)\"\n else\n sudo loginctl enable-linger \"${current_user}\" && _linger_ok=1 || log \"$(msg install.podman.linger_warn)\"\n fi\n else\n log \"$(msg install.podman.linger_warn)\"\n fi\n fi\n fi\n\n # 2. Write dedicated service file to user directory\n local _user_dir=\"${HOME}/.config/systemd/user\"\n mkdir -p \"${_user_dir}\"\n echo \"${_service_content}\" > \"${_user_dir}/agentteams-podman-restart.service\"\n\n systemctl --user daemon-reload >/dev/null 2>&1 || true\n\n if systemctl --user enable --now agentteams-podman-restart.service >/dev/null 2>&1; then\n log \"$(msg install.podman.user_success)\"\n # Crucial: Only consider autostart fully successful if systemd linger is also enabled\n if [ \"${_linger_ok}\" = \"1\" ]; then\n _autostart_ok=1\n else\n log \"⚠️ Service enabled, but since systemd linger is not active, containers will not auto-recover on system reboot.\"\n fi\n else\n log \"$(msg install.podman.user_fail)\"\n fi\n fi\n\n # Only print final success banner if the service AND linger were actually enabled\n if [ \"${_autostart_ok}\" = \"1\" ]; then\n log \"$(msg install.podman.success)\"\n fi\n}\n\n# ============================================================\n# Dashboard startup (called after controller is ready)\n# ============================================================\n\n_start_dashboard() {\n local CTRL_CONTAINER=\"agentteams-controller\"\n local DASHBOARD_CONTAINER=\"agentteams-dashboard\"\n\n if [ \"${AGENTTEAMS_DASHBOARD:-0}\" != \"1\" ]; then\n # Stop and remove the existing dashboard container if present.\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -q \"^${DASHBOARD_CONTAINER}$\"; then\n log \"Stopping and removing ${DASHBOARD_CONTAINER}...\"\n ${DOCKER_CMD} stop \"${DASHBOARD_CONTAINER}\" >/dev/null 2>&1 || true\n ${DOCKER_CMD} rm -f \"${DASHBOARD_CONTAINER}\" >/dev/null 2>&1 || true\n log \"Dashboard stopped.\"\n fi\n return 0\n fi\n\n # Dashboard requires the embedded architecture (controller container).\n # In legacy all-in-one mode the controller API is not available.\n if [ \"${AGENTTEAMS_USE_EMBEDDED:-0}\" != \"1\" ]; then\n log \"Skipping Dashboard: requires embedded architecture (agentteams-controller), not available in legacy mode.\"\n log \"Use Quick Start (embedded) mode to enable agentteams-dashboard.\"\n return 0\n fi\n\n AGENTTEAMS_PORT_DASHBOARD=\"${AGENTTEAMS_PORT_DASHBOARD:-13000}\"\n AGENTTEAMS_DASHBOARD_VERSION=\"${AGENTTEAMS_DASHBOARD_VERSION:-v1.2.0}\"\n AGENTTEAMS_DASHBOARD_IMAGE=\"${AGENTTEAMS_DASHBOARD_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-dashboard:${AGENTTEAMS_DASHBOARD_VERSION}}\"\n\n log \"\"\n log \"Starting agentteams-dashboard...\"\n\n local BIND_ADDR=\"127.0.0.1\"\n [ \"${AGENTTEAMS_LOCAL_ONLY:-1}\" = \"0\" ] && BIND_ADDR=\"0.0.0.0\"\n\n # Remove existing container\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' | grep -qx \"${DASHBOARD_CONTAINER}\"; then\n ${DOCKER_CMD} rm -f \"${DASHBOARD_CONTAINER}\" >/dev/null 2>&1 || true\n fi\n\n # Pull or use local image\n if ! ${DOCKER_CMD} image inspect \"${AGENTTEAMS_DASHBOARD_IMAGE}\" >/dev/null 2>&1; then\n log \"Pulling dashboard image: ${AGENTTEAMS_DASHBOARD_IMAGE}\"\n ${DOCKER_CMD} pull \"${AGENTTEAMS_DASHBOARD_IMAGE}\" 2>/dev/null || {\n log \"WARNING: Could not pull dashboard image. Skipping.\"\n return 0\n }\n fi\n\n # Build env args from controller container (MinIO/LLM/auth).\n local env_args=()\n env_args+=(-e AGENTTEAMS_CONTROLLER_URL=\"http://${CTRL_CONTAINER}:8090\")\n env_args+=(-e NEXT_PUBLIC_MATRIX_API_URL=\"http://${CTRL_CONTAINER}:6167\")\n env_args+=(-e MATRIX_HOMESERVER_ALLOWLIST=\"${CTRL_CONTAINER},matrix-local.agentteams.io,matrix.org\")\n\n if ${DOCKER_CMD} ps --format '{{.Names}}' | grep -qx \"${CTRL_CONTAINER}\"; then\n local env_out\n env_out=$(${DOCKER_CMD} inspect \"${CTRL_CONTAINER}\" --format='{{range .Config.Env}}{{.}}{{\"\\n\"}}{{end}}')\n\n _env() { echo \"${env_out}\" | sed -n \"s|^${1}=||p\" | head -n1; }\n\n local fs_endpoint; fs_endpoint=$(_env AGENTTEAMS_FS_ENDPOINT)\n [ -z \"${fs_endpoint}\" ] && fs_endpoint=$(_env AGENTTEAMS_MINIO_ENDPOINT)\n if [ -n \"${fs_endpoint}\" ]; then\n fs_endpoint=$(echo \"${fs_endpoint}\" | sed -e \"s|127\\.0\\.0\\.1|${CTRL_CONTAINER}|\" -e \"s|localhost|${CTRL_CONTAINER}|\")\n env_args+=(-e AGENTTEAMS_FS_ENDPOINT=\"${fs_endpoint}\")\n else\n env_args+=(-e AGENTTEAMS_FS_ENDPOINT=\"http://${CTRL_CONTAINER}:9000\")\n fi\n\n local fs_bucket; fs_bucket=$(_env AGENTTEAMS_FS_BUCKET)\n [ -z \"${fs_bucket}\" ] && fs_bucket=$(_env AGENTTEAMS_MINIO_BUCKET)\n [ -n \"${fs_bucket}\" ] && env_args+=(-e AGENTTEAMS_FS_BUCKET=\"${fs_bucket}\")\n\n local fs_user; fs_user=$(_env AGENTTEAMS_FS_ACCESS_KEY)\n [ -z \"${fs_user}\" ] && fs_user=$(_env AGENTTEAMS_MINIO_USER)\n [ -n \"${fs_user}\" ] && env_args+=(-e AGENTTEAMS_FS_ACCESS_KEY=\"${fs_user}\")\n\n local fs_pass; fs_pass=$(_env AGENTTEAMS_FS_SECRET_KEY)\n [ -z \"${fs_pass}\" ] && fs_pass=$(_env AGENTTEAMS_MINIO_PASSWORD)\n [ -n \"${fs_pass}\" ] && env_args+=(-e AGENTTEAMS_FS_SECRET_KEY=\"${fs_pass}\")\n\n for k in AGENTTEAMS_LLM_PROVIDER AGENTTEAMS_LLM_API_KEY AGENTTEAMS_OPENAI_BASE_URL AGENTTEAMS_DEFAULT_MODEL; do\n local v; v=$(_env \"${k}\")\n [ -n \"${v}\" ] && env_args+=(-e \"${k}=${v}\")\n done\n\n # The controller writes the CLI SA token to /var/run/agentteams/cli-token.\n # The token is generated asynchronously during the first reconcile loop,\n # so it may not be available immediately after the container starts.\n # We poll for up to 30s to handle both fresh installs (token not yet minted)\n # and older images that don't support cli-token at all.\n # For backward compatibility, also try the legacy /var/run/hiclaw/cli-token\n # path used by older HiClaw images.\n local auth_token=\"${AGENTTEAMS_AUTH_TOKEN:-}\"\n if [ -z \"${auth_token}\" ]; then\n local _token_wait=0\n local _token_max_wait=30\n while [ ${_token_wait} -lt ${_token_max_wait} ]; do\n auth_token=$(${DOCKER_CMD} exec \"${CTRL_CONTAINER}\" sh -c 'cat /var/run/agentteams/cli-token 2>/dev/null || cat /var/run/hiclaw/cli-token 2>/dev/null' | tr -d '\\n' || true)\n if [ -n \"${auth_token}\" ]; then\n break\n fi\n sleep 2\n _token_wait=$((_token_wait + 2))\n done\n fi\n if [ -n \"${auth_token}\" ]; then\n env_args+=(-e AGENTTEAMS_AUTH_TOKEN=\"${auth_token}\")\n else\n log \"WARNING: could not read controller SA token (timed out after ${_token_max_wait:-30}s)\"\n log \" This can happen with older AgentTeams/HiClaw images or if the controller is still starting.\"\n log \" Dashboard will still work but some API calls may fail until you log in via Higress Console.\"\n log \" To fix: upgrade AgentTeams to v1.2.0-beta.1+ or set AGENTTEAMS_AUTH_TOKEN manually.\"\n fi\n\n local admin_user; admin_user=$(_env AGENTTEAMS_ADMIN_USER)\n local admin_pass; admin_pass=$(_env AGENTTEAMS_ADMIN_PASSWORD)\n [ -n \"${admin_user}\" ] && env_args+=(-e AGENTTEAMS_ADMIN_USER=\"${admin_user}\")\n [ -n \"${admin_pass}\" ] && env_args+=(-e AGENTTEAMS_ADMIN_PASSWORD=\"${admin_pass}\")\n\n # Higress Console URL: explicit config takes priority, auto-detect as fallback.\n local _resolved_gw_url=\"\"\n if [ -n \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\" ]; then\n # Normalize URL: add http:// prefix if missing\n _resolved_gw_url=\"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\"\n case \"${_resolved_gw_url}\" in\n http://*|https://*) ;;\n *) _resolved_gw_url=\"http://${_resolved_gw_url}\" ;;\n esac\n env_args+=(-e AGENTTEAMS_AI_GATEWAY_ADMIN_URL=\"${_resolved_gw_url}\")\n elif ${DOCKER_CMD} exec \"${CTRL_CONTAINER}\" wget -q -O- --timeout=2 http://127.0.0.1:8001/ >/dev/null 2>&1; then\n _resolved_gw_url=\"http://${CTRL_CONTAINER}:8001\"\n env_args+=(-e AGENTTEAMS_AI_GATEWAY_ADMIN_URL=\"${_resolved_gw_url}\")\n fi\n\n # Verify Higress Console URL reachability (best-effort warning)\n if [ -n \"${_resolved_gw_url}\" ]; then\n if ! ${DOCKER_CMD} exec \"${CTRL_CONTAINER}\" curl -sf --max-time 3 \"${_resolved_gw_url}/\" >/dev/null 2>&1; then\n log \"WARNING: Higress Console URL may not be reachable: ${_resolved_gw_url}\"\n log \" Dashboard will still work, but shared login via Higress Console may fail.\"\n log \" To fix: verify the URL is correct and the Higress Console service is running.\"\n fi\n fi\n else\n log \"WARNING: ${CTRL_CONTAINER} not running; launching Dashboard without Controller/MinIO/LLM env.\"\n if [ -n \"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\" ]; then\n local _gw_norm=\"${AGENTTEAMS_AI_GATEWAY_ADMIN_URL}\"\n case \"${_gw_norm}\" in\n http://*|https://*) ;;\n *) _gw_norm=\"http://${_gw_norm}\" ;;\n esac\n env_args+=(-e AGENTTEAMS_AI_GATEWAY_ADMIN_URL=\"${_gw_norm}\")\n fi\n fi\n\n # Create persistent data volume (for future use)\n ${DOCKER_CMD} volume create agentteams-dashboard-data >/dev/null 2>&1 || true\n\n ${DOCKER_CMD} run -d \\\n --name \"${DASHBOARD_CONTAINER}\" \\\n --restart unless-stopped \\\n --network \"${AGENTTEAMS_NETWORK:-agentteams-net}\" \\\n --network-alias dashboard.agentteams.io \\\n -p \"${BIND_ADDR}:${AGENTTEAMS_PORT_DASHBOARD}:3000\" \\\n \"${env_args[@]}\" \\\n -v agentteams-dashboard-data:/app/db \\\n \"${AGENTTEAMS_DASHBOARD_IMAGE}\"\n\n # Wait for dashboard to be ready\n local max_wait=60\n local waited=0\n while [ $waited -lt $max_wait ]; do\n if curl -sf \"http://127.0.0.1:${AGENTTEAMS_PORT_DASHBOARD}/\" >/dev/null 2>&1; then\n log \"Dashboard ready on port ${AGENTTEAMS_PORT_DASHBOARD}\"\n return 0\n fi\n sleep 2\n waited=$((waited + 2))\n done\n log \"WARNING: Dashboard startup timed out after ${max_wait}s\"\n ${DOCKER_CMD} logs \"${DASHBOARD_CONTAINER}\" 2>&1 | tail -20\n return 0\n}\n\n# ============================================================\n# Manager Installation (Interactive)\n# ============================================================\n\ninstall_manager() {\n log \"$(msg install.title)\"\n log \"$(msg install.registry \"${AGENTTEAMS_REGISTRY}\")\"\n log \"\"\n log \"$(msg install.dir \"$(pwd)\")\"\n log \"$(msg install.dir_hint)\"\n log \"$(msg install.dir_hint2)\"\n log \"\"\n\n # Non-interactive fallback: resolve version immediately so image tags are available\n # before the step state machine runs. Interactive mode lets step_version handle it.\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n AGENTTEAMS_VERSION=\"${AGENTTEAMS_VERSION:-${AGENTTEAMS_KNOWN_STABLE_VERSION}}\"\n resolve_image_tags\n fi\n\n # Migrate legacy env file location before checks\n local existing_env=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n if [ ! -f \"${existing_env}\" ] && [ -f \"./agentteams-manager.env\" ]; then\n log \"Migrating agentteams-manager.env from current directory to ${existing_env}...\"\n mv \"./agentteams-manager.env\" \"${existing_env}\"\n fi\n\n # Orphan volume detection (only when no env file — step_existing handles the env-file case)\n if [ ! -f \"${existing_env}\" ]; then\n local data_vol=\"${AGENTTEAMS_DATA_DIR:-agentteams-data}\"\n if ${DOCKER_CMD} volume ls -q | grep -q \"^${data_vol}$\"; then\n echo \"\"\n log \"$(msg install.orphan_volume.detected \"${data_vol}\")\"\n log \"$(msg install.orphan_volume.warn)\"\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" = \"1\" ]; then\n log \"$(msg install.orphan_volume.clean_noninteractive)\"\n ${DOCKER_CMD} stop agentteams-manager 2>/dev/null || true\n ${DOCKER_CMD} rm agentteams-manager 2>/dev/null || true\n for w in $(${DOCKER_CMD} ps -a --format '{{.Names}}' | grep \"^agentteams-worker-\" || true); do\n ${DOCKER_CMD} stop \"${w}\" 2>/dev/null || true\n ${DOCKER_CMD} rm \"${w}\" 2>/dev/null || true\n done\n log \"$(msg install.orphan_volume.cleaning)\"\n ${DOCKER_CMD} volume rm \"${data_vol}\" 2>/dev/null || true\n log \"$(msg install.orphan_volume.cleaned)\"\n else\n echo \"\"\n echo \"$(msg install.orphan_volume.choose)\"\n echo \"$(msg install.orphan_volume.clean)\"\n echo \"$(msg install.orphan_volume.keep)\"\n echo \"\"\n local ORPHAN_CHOICE\n read -e -p \"$(msg install.orphan_volume.prompt): \" ORPHAN_CHOICE\n ORPHAN_CHOICE=\"${ORPHAN_CHOICE:-1}\"\n case \"${ORPHAN_CHOICE}\" in\n 1|clean)\n ${DOCKER_CMD} stop agentteams-manager 2>/dev/null || true\n ${DOCKER_CMD} rm agentteams-manager 2>/dev/null || true\n for w in $(${DOCKER_CMD} ps -a --format '{{.Names}}' | grep \"^agentteams-worker-\" || true); do\n ${DOCKER_CMD} stop \"${w}\" 2>/dev/null || true\n ${DOCKER_CMD} rm \"${w}\" 2>/dev/null || true\n done\n log \"$(msg install.orphan_volume.cleaning)\"\n ${DOCKER_CMD} volume rm \"${data_vol}\" 2>/dev/null || true\n log \"$(msg install.orphan_volume.cleaned)\"\n ;;\n 2|keep)\n log \"$(msg install.orphan_volume.keeping)\"\n ;;\n esac\n fi\n fi\n fi\n\n # ── State machine ─────────────────────────────────────────────────────────\n local _STEPS=( step_lang step_mode step_version step_existing step_llm step_manager_runtime step_runtime step_admin step_network\n step_ports step_domains step_github step_skills step_volume\n step_workspace step_dashboard step_e2ee step_docker_proxy step_idle step_hostshare step_podman_autostart )\n local _STEP_HISTORY=()\n local _step_idx=0\n while [ \"${_step_idx}\" -lt \"${#_STEPS[@]}\" ]; do\n local _step_fn=\"${_STEPS[$_step_idx]}\"\n if should_skip_step \"${_step_fn}\"; then\n _step_idx=$((_step_idx + 1))\n continue\n fi\n if [ \"${#_STEP_HISTORY[@]}\" -gt 0 ]; then\n log \"$(msg nav.back_hint)\"\n fi\n STEP_RESULT=\"\"\n \"${_step_fn}\"\n if [ \"${STEP_RESULT}\" = \"back\" ]; then\n if [ \"${#_STEP_HISTORY[@]}\" -gt 0 ]; then\n local _last=$(( ${#_STEP_HISTORY[@]} - 1 ))\n _step_idx=\"${_STEP_HISTORY[$_last]}\"\n _STEP_HISTORY=(\"${_STEP_HISTORY[@]:0:${_last}}\")\n clear_step_vars \"${_STEPS[$_step_idx]}\"\n fi\n # else: first step, ignore 'b'\n else\n _STEP_HISTORY+=(\"${_step_idx}\")\n _step_idx=$((_step_idx + 1))\n fi\n done\n # ── End state machine ──────────────────────────────────────────────────────\n\n # Post-machine defaults for any steps that were skipped\n AGENTTEAMS_DATA_DIR=\"${AGENTTEAMS_DATA_DIR:-agentteams-data}\"\n if [ -z \"${AGENTTEAMS_WORKSPACE_DIR+x}\" ] || [ -z \"${AGENTTEAMS_WORKSPACE_DIR}\" ]; then\n AGENTTEAMS_WORKSPACE_DIR=\"${HOME}/agentteams-manager\"\n export AGENTTEAMS_WORKSPACE_DIR\n fi\n AGENTTEAMS_WORKSPACE_DIR=\"$(cd \"${AGENTTEAMS_WORKSPACE_DIR}\" 2>/dev/null && pwd || echo \"${AGENTTEAMS_WORKSPACE_DIR}\")\"\n mkdir -p \"${AGENTTEAMS_WORKSPACE_DIR}\"\n AGENTTEAMS_MANAGER_RUNTIME=\"${AGENTTEAMS_MANAGER_RUNTIME:-copaw}\"\n export AGENTTEAMS_MANAGER_RUNTIME\n AGENTTEAMS_DEFAULT_WORKER_RUNTIME=\"${AGENTTEAMS_DEFAULT_WORKER_RUNTIME:-copaw}\"\n AGENTTEAMS_MATRIX_E2EE=\"${AGENTTEAMS_MATRIX_E2EE:-0}\"\n export AGENTTEAMS_MATRIX_E2EE\n AGENTTEAMS_WORKER_IDLE_TIMEOUT=\"${AGENTTEAMS_WORKER_IDLE_TIMEOUT:-720}\"\n export AGENTTEAMS_WORKER_IDLE_TIMEOUT\n AGENTTEAMS_HOST_SHARE_DIR=\"${AGENTTEAMS_HOST_SHARE_DIR:-$HOME}\"\n export AGENTTEAMS_HOST_SHARE_DIR\n\n log \"\"\n\n # Generate secrets (only if not already set)\n log \"$(msg install.generating_secrets)\"\n AGENTTEAMS_MANAGER_PASSWORD=\"${AGENTTEAMS_MANAGER_PASSWORD:-$(generate_key)}\"\n AGENTTEAMS_REGISTRATION_TOKEN=\"${AGENTTEAMS_REGISTRATION_TOKEN:-$(generate_key)}\"\n AGENTTEAMS_MINIO_USER=\"${AGENTTEAMS_MINIO_USER:-${AGENTTEAMS_ADMIN_USER}}\"\n AGENTTEAMS_MINIO_PASSWORD=\"${AGENTTEAMS_MINIO_PASSWORD:-${AGENTTEAMS_ADMIN_PASSWORD}}\"\n AGENTTEAMS_MANAGER_GATEWAY_KEY=\"${AGENTTEAMS_MANAGER_GATEWAY_KEY:-$(generate_key)}\"\n\n # Matrix AppService tokens — generate once during install/upgrade if not provided.\n # Persisted to env file so they survive controller restarts.\n AGENTTEAMS_MATRIX_APPSERVICE_ENABLED=\"${AGENTTEAMS_MATRIX_APPSERVICE_ENABLED:-true}\"\n export AGENTTEAMS_MATRIX_APPSERVICE_ENABLED\n if [ \"${AGENTTEAMS_MATRIX_APPSERVICE_ENABLED}\" != \"false\" ] && [ \"${AGENTTEAMS_MATRIX_APPSERVICE_ENABLED}\" != \"0\" ]; then\n if [ -z \"${AGENTTEAMS_MATRIX_APPSERVICE_AS_TOKEN}\" ]; then\n AGENTTEAMS_MATRIX_APPSERVICE_AS_TOKEN=\"$(openssl rand -hex 32)\"\n log \" Auto-generated AppService as_token (saved to env file)\"\n fi\n if [ -z \"${AGENTTEAMS_MATRIX_APPSERVICE_HS_TOKEN}\" ]; then\n AGENTTEAMS_MATRIX_APPSERVICE_HS_TOKEN=\"$(openssl rand -hex 32)\"\n log \" Auto-generated AppService hs_token (saved to env file)\"\n fi\n export AGENTTEAMS_MATRIX_APPSERVICE_AS_TOKEN\n export AGENTTEAMS_MATRIX_APPSERVICE_HS_TOKEN\n fi\n\n # Detect Apple Silicon (M1/M2/M3/M4) - need JVM fix for Higress Console\n # See: https://github.com/agentscope-ai/AgentTeams/issues/249\n if [ -z \"${JVM_ARGS:-}\" ] && [ \"$(uname -m)\" = \"arm64\" ] && [ \"$(uname -s)\" = \"Darwin\" ]; then\n log \"Apple Silicon detected - setting JVM_ARGS to fix Higress Console SIGILL issue\"\n JVM_ARGS=\"-XX:+UnlockDiagnosticVMOptions -XX:-UseAESCTRIntrinsics -XX:UseSVE=0\"\n fi\n\n # Write .env file\n ENV_FILE=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n cat > \"${ENV_FILE}\" << EOF\n# AgentTeams Manager Configuration\n# Generated by agentteams-install.sh on $(date)\n\n# Language\nAGENTTEAMS_LANGUAGE=${AGENTTEAMS_LANGUAGE}\n\n# LLM\nAGENTTEAMS_LLM_PROVIDER=${AGENTTEAMS_LLM_PROVIDER}\nAGENTTEAMS_DEFAULT_MODEL=${AGENTTEAMS_DEFAULT_MODEL}\nAGENTTEAMS_LLM_API_KEY=${AGENTTEAMS_LLM_API_KEY}\nAGENTTEAMS_OPENAI_BASE_URL=${AGENTTEAMS_OPENAI_BASE_URL:-}\nAGENTTEAMS_MODEL_CONTEXT_WINDOW=${AGENTTEAMS_MODEL_CONTEXT_WINDOW:-}\nAGENTTEAMS_MODEL_MAX_TOKENS=${AGENTTEAMS_MODEL_MAX_TOKENS:-}\nAGENTTEAMS_MODEL_REASONING=${AGENTTEAMS_MODEL_REASONING:-}\nAGENTTEAMS_MODEL_VISION=${AGENTTEAMS_MODEL_VISION:-}\n\n# Embedding model (empty = disabled, default: text-embedding-v4)\nAGENTTEAMS_EMBEDDING_MODEL=${AGENTTEAMS_EMBEDDING_MODEL}\n\n# Admin\nAGENTTEAMS_ADMIN_USER=${AGENTTEAMS_ADMIN_USER}\nAGENTTEAMS_ADMIN_PASSWORD=${AGENTTEAMS_ADMIN_PASSWORD}\n\n# Ports\nAGENTTEAMS_LOCAL_ONLY=${AGENTTEAMS_LOCAL_ONLY}\nAGENTTEAMS_PORT_GATEWAY=${AGENTTEAMS_PORT_GATEWAY}\nAGENTTEAMS_PORT_CONSOLE=${AGENTTEAMS_PORT_CONSOLE}\nAGENTTEAMS_PORT_ELEMENT_WEB=${AGENTTEAMS_PORT_ELEMENT_WEB}\nAGENTTEAMS_PORT_MANAGER_CONSOLE=${AGENTTEAMS_PORT_MANAGER_CONSOLE:-18888}\n\n# Manager runtime (openclaw | copaw)\nAGENTTEAMS_MANAGER_RUNTIME=${AGENTTEAMS_MANAGER_RUNTIME:-copaw}\n\n# Matrix\nAGENTTEAMS_MATRIX_DOMAIN=${AGENTTEAMS_MATRIX_DOMAIN}\nAGENTTEAMS_MATRIX_CLIENT_DOMAIN=${AGENTTEAMS_MATRIX_CLIENT_DOMAIN}\n\n# Gateway\nAGENTTEAMS_AI_GATEWAY_DOMAIN=${AGENTTEAMS_AI_GATEWAY_DOMAIN}\nAGENTTEAMS_MANAGER_GATEWAY_KEY=${AGENTTEAMS_MANAGER_GATEWAY_KEY}\n\n# File System\nAGENTTEAMS_FS_DOMAIN=${AGENTTEAMS_FS_DOMAIN}\nAGENTTEAMS_CONSOLE_DOMAIN=${AGENTTEAMS_CONSOLE_DOMAIN}\nAGENTTEAMS_MINIO_USER=${AGENTTEAMS_MINIO_USER}\nAGENTTEAMS_MINIO_PASSWORD=${AGENTTEAMS_MINIO_PASSWORD}\n\n# Internal\nAGENTTEAMS_MANAGER_PASSWORD=${AGENTTEAMS_MANAGER_PASSWORD}\nAGENTTEAMS_REGISTRATION_TOKEN=${AGENTTEAMS_REGISTRATION_TOKEN}\n\n# GitHub (optional)\nAGENTTEAMS_GITHUB_TOKEN=${AGENTTEAMS_GITHUB_TOKEN:-}\n\n# Nacos package import defaults\nAGENTTEAMS_NACOS_REGISTRY_URI=${AGENTTEAMS_NACOS_REGISTRY_URI:-nacos://market.agentteams.io:80/public}\nAGENTTEAMS_NACOS_USERNAME=${AGENTTEAMS_NACOS_USERNAME:-}\nAGENTTEAMS_NACOS_PASSWORD=${AGENTTEAMS_NACOS_PASSWORD:-}\nAGENTTEAMS_NACOS_TOKEN=${AGENTTEAMS_NACOS_TOKEN:-}\n\n# Skills Registry (optional, default: nacos://market.agentteams.io:80/public)\nAGENTTEAMS_SKILLS_API_URL=${AGENTTEAMS_SKILLS_API_URL:-nacos://market.agentteams.io:80/public}\n\n# OpenClaw CMS plugin (optional)\nAGENTTEAMS_CMS_TRACES_ENABLED=${AGENTTEAMS_CMS_TRACES_ENABLED:-false}\nAGENTTEAMS_CMS_ENDPOINT=${AGENTTEAMS_CMS_ENDPOINT:-}\nAGENTTEAMS_CMS_LICENSE_KEY=${AGENTTEAMS_CMS_LICENSE_KEY:-}\nAGENTTEAMS_CMS_PROJECT=${AGENTTEAMS_CMS_PROJECT:-}\nAGENTTEAMS_CMS_WORKSPACE=${AGENTTEAMS_CMS_WORKSPACE:-}\nAGENTTEAMS_CMS_SERVICE_NAME=${AGENTTEAMS_CMS_SERVICE_NAME:-agentteams-manager}\nAGENTTEAMS_CMS_METRICS_ENABLED=${AGENTTEAMS_CMS_METRICS_ENABLED:-false}\n\n# Worker images (for direct container creation)\nAGENTTEAMS_WORKER_IMAGE=${WORKER_IMAGE}\nAGENTTEAMS_COPAW_WORKER_IMAGE=${COPAW_WORKER_IMAGE}\nAGENTTEAMS_QWENPAW_WORKER_IMAGE=${QWENPAW_WORKER_IMAGE}\nAGENTTEAMS_HERMES_WORKER_IMAGE=${HERMES_WORKER_IMAGE}\n\n# Default Worker runtime (openclaw | copaw | qwenpaw | hermes)\nAGENTTEAMS_DEFAULT_WORKER_RUNTIME=${AGENTTEAMS_DEFAULT_WORKER_RUNTIME:-copaw}\n\n# Matrix E2EE (0=disabled, 1=enabled; default: 0)\nAGENTTEAMS_MATRIX_E2EE=${AGENTTEAMS_MATRIX_E2EE:-0}\n\n# Matrix AppService\nAGENTTEAMS_MATRIX_APPSERVICE_ENABLED=${AGENTTEAMS_MATRIX_APPSERVICE_ENABLED:-true}\nAGENTTEAMS_MATRIX_APPSERVICE_AS_TOKEN=${AGENTTEAMS_MATRIX_APPSERVICE_AS_TOKEN:-}\nAGENTTEAMS_MATRIX_APPSERVICE_HS_TOKEN=${AGENTTEAMS_MATRIX_APPSERVICE_HS_TOKEN:-}\n\n# Docker API proxy (0=disabled, 1=enabled; default: 1)\nAGENTTEAMS_DOCKER_PROXY=${AGENTTEAMS_DOCKER_PROXY:-1}\n\n# Docker API proxy: additional allowed image sources (comma-separated)\nAGENTTEAMS_PROXY_ALLOWED_REGISTRIES=${AGENTTEAMS_PROXY_ALLOWED_REGISTRIES:-}\n\n# Worker idle timeout in minutes (default: 720 = 12 hours)\nAGENTTEAMS_WORKER_IDLE_TIMEOUT=${AGENTTEAMS_WORKER_IDLE_TIMEOUT:-720}\n\n# Podman autostart via systemd (0=disabled, 1=enabled)\nAGENTTEAMS_PODMAN_AUTOSTART=${AGENTTEAMS_PODMAN_AUTOSTART:-0}\n\n# JVM Args for Higress Console (fixes SIGILL on Apple Silicon)\nJVM_ARGS=${JVM_ARGS:-}\n\n# Higress WASM plugin image registry (auto-selected by timezone)\nHIGRESS_ADMIN_WASM_PLUGIN_IMAGE_REGISTRY=${AGENTTEAMS_REGISTRY}\n\n# Data persistence\nAGENTTEAMS_DATA_DIR=${AGENTTEAMS_DATA_DIR:-agentteams-data}\n# Manager workspace (skills, memory, state — host-editable)\nAGENTTEAMS_WORKSPACE_DIR=${AGENTTEAMS_WORKSPACE_DIR:-}\n# Host directory sharing\nAGENTTEAMS_HOST_SHARE_DIR=${AGENTTEAMS_HOST_SHARE_DIR:-}\n\n# agentteams-dashboard (management UI)\nAGENTTEAMS_DASHBOARD=${AGENTTEAMS_DASHBOARD:-1}\nAGENTTEAMS_DASHBOARD_VERSION=${AGENTTEAMS_DASHBOARD_VERSION:-v1.2.0}\nAGENTTEAMS_PORT_DASHBOARD=${AGENTTEAMS_PORT_DASHBOARD:-13000}\nAGENTTEAMS_DASHBOARD_IMAGE=${AGENTTEAMS_DASHBOARD_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-dashboard:${AGENTTEAMS_DASHBOARD_VERSION}}\nAGENTTEAMS_AI_GATEWAY_ADMIN_URL=${AGENTTEAMS_AI_GATEWAY_ADMIN_URL:-}\nEOF\n\n chmod 600 \"${ENV_FILE}\"\n log \"$(msg install.config_saved \"${ENV_FILE}\")\"\n\n # Detect container runtime socket\n SOCKET_MOUNT_ARGS=\"\"\n if [ \"${AGENTTEAMS_MOUNT_SOCKET}\" = \"1\" ]; then\n # Actively ensure the API socket is enabled and active before detection.\n if [ \"${DOCKER_CMD:-}\" = \"podman\" ]; then\n ensure_podman_socket\n fi\n CONTAINER_SOCK=$(detect_socket)\n if [ -n \"${CONTAINER_SOCK}\" ]; then\n log \"$(msg install.socket_detected \"${CONTAINER_SOCK}\")\"\n SOCKET_MOUNT_ARGS=\"-v ${CONTAINER_SOCK}:/var/run/docker.sock --security-opt label=disable\"\n else\n log \"$(msg install.socket_not_found)\"\n # Interactive confirmation when socket not found\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" != \"1\" ]; then\n echo \"\"\n echo -e \"\\033[33m$(msg install.socket_confirm.title)\\033[0m\"\n echo \"\"\n echo -e \"$(msg install.socket_confirm.message)\"\n echo \"\"\n read -p \"$(msg install.socket_confirm.prompt)\" SOCKET_CONFIRM\n if [ \"${SOCKET_CONFIRM}\" != \"y\" ] && [ \"${SOCKET_CONFIRM}\" != \"Y\" ]; then\n log \"$(msg install.socket_confirm.cancelled)\"\n exit 0\n fi\n fi\n fi\n fi\n\n # Create the data volume if it doesn't already exist (reuse on reinstall)\n if ! ${DOCKER_CMD} volume ls -q | grep -q \"^${AGENTTEAMS_DATA_DIR}$\"; then\n ${DOCKER_CMD} volume create \"${AGENTTEAMS_DATA_DIR}\" > /dev/null\n fi\n\n # Data mount: Docker volume\n DATA_MOUNT_ARGS=\"-v ${AGENTTEAMS_DATA_DIR}:/data\"\n\n # Manager workspace mount (always a host directory, defaulting to ~/agentteams-manager)\n WORKSPACE_MOUNT_ARGS=\"-v ${AGENTTEAMS_WORKSPACE_DIR}:/root/manager-workspace\"\n\n # Pass host timezone to container so date/time commands reflect local time\n TZ_ARGS=\"-e TZ=${AGENTTEAMS_TIMEZONE}\"\n\n # Host directory mount\n if [ -d \"${AGENTTEAMS_HOST_SHARE_DIR}\" ]; then\n HOST_SHARE_MOUNT_ARGS=\"-v ${AGENTTEAMS_HOST_SHARE_DIR}:/host-share\"\n log \"$(msg host_share.sharing \"${AGENTTEAMS_HOST_SHARE_DIR}\")\"\n else\n log \"$(msg host_share.not_exist \"${AGENTTEAMS_HOST_SHARE_DIR}\")\"\n HOST_SHARE_MOUNT_ARGS=\"-v ${AGENTTEAMS_HOST_SHARE_DIR}:/host-share\"\n fi\n\n # YOLO mode: pass through if set in environment (enables autonomous decisions)\n YOLO_ARGS=\"\"\n if [ \"${AGENTTEAMS_YOLO:-}\" = \"1\" ]; then\n YOLO_ARGS=\"-e AGENTTEAMS_YOLO=1\"\n log \"$(msg install.yolo)\"\n fi\n\n # Matrix-plugin debug tracing: pass through if AGENTTEAMS_MATRIX_DEBUG=1.\n # The container entrypoints translate this to OPENCLAW_MATRIX_DEBUG=1\n # so the openclaw matrix plugin emits structured INFO-level lifecycle\n # traces (sync state transitions, room.invite/join, message handler\n # arrival + filter outcomes). Used to diagnose worker/manager hangs.\n MATRIX_DEBUG_ARGS=\"\"\n if [ \"${AGENTTEAMS_MATRIX_DEBUG:-}\" = \"1\" ]; then\n MATRIX_DEBUG_ARGS=\"-e AGENTTEAMS_MATRIX_DEBUG=1\"\n fi\n\n # E2EE is already in the env file; but also pass explicitly in case env file is not the source\n # (AGENTTEAMS_MATRIX_E2EE is already written to ENV_FILE above via --env-file)\n\n # Pull images (manager based on runtime config; all worker runtimes always pulled)\n _is_local_image() {\n case \"$1\" in\n agentteams/*) return 0 ;;\n *) return 1 ;;\n esac\n }\n\n _local_image_exists() {\n local _img=\"$1\"\n [ -z \"${_img}\" ] && return 1\n _is_local_image \"${_img}\" || return 1\n ${DOCKER_CMD} image inspect \"${_img}\" >/dev/null 2>&1\n }\n\n # Helper: pull or skip a single image\n # Args: $1=image $2=exists_msg_key $3=pulling_msg_key\n _pull_image() {\n local _img=\"$1\" _exists_key=\"$2\" _pull_key=\"$3\"\n [ -z \"${_img}\" ] && return 0\n if _local_image_exists \"${_img}\"; then\n log \"$(msg \"${_exists_key}\" \"${_img}\")\"\n return 0\n fi\n log \"$(msg \"${_pull_key}\" \"${_img}\")\"\n local _attempt=1\n while [ $_attempt -le 3 ]; do\n if ${DOCKER_CMD} pull \"${_img}\"; then\n return 0\n fi\n if [ $_attempt -lt 3 ]; then\n log \"Pull failed (attempt ${_attempt}/3), retrying in 5s...\"\n sleep 5\n fi\n _attempt=$((_attempt + 1))\n done\n die \"Failed to pull ${_img} after 3 attempts\"\n return 1\n }\n\n # Embedded controller image (resolve versioned tag, fallback to latest)\n resolve_embedded_image\n if [ \"${AGENTTEAMS_USE_EMBEDDED}\" = \"1\" ]; then\n _local_image_exists \"${EMBEDDED_IMAGE}\" || true\n fi\n\n # Manager image is always required (select based on runtime)\n if [ \"${AGENTTEAMS_MANAGER_RUNTIME}\" = \"copaw\" ]; then\n _pull_image \"${MANAGER_COPAW_IMAGE}\" \"install.image.exists\" \"install.image.pulling_manager\"\n else\n _pull_image \"${MANAGER_IMAGE}\" \"install.image.exists\" \"install.image.pulling_manager\"\n fi\n\n # Pull all worker runtime images (workers may use any runtime regardless of the default)\n _pull_image \"${WORKER_IMAGE}\" \"install.image.worker_exists\" \"install.image.pulling_worker\"\n _pull_image \"${COPAW_WORKER_IMAGE}\" \"install.image.worker_exists\" \"install.image.pulling_worker\"\n # Temporarily disabled until the QwenPaw worker image is published.\n # _pull_image \"${QWENPAW_WORKER_IMAGE}\" \"install.image.worker_exists\" \"install.image.pulling_worker\"\n _pull_image \"${HERMES_WORKER_IMAGE}\" \"install.image.worker_exists\" \"install.image.pulling_worker\"\n\n # --- Pre-upgrade: extract Matrix passwords from running old containers ---\n # Only needed when upgrading FROM old architecture (v1.0.9) TO embedded.\n # For new-arch-to-new-arch upgrades, credential files already exist with\n # correct room IDs — we must NOT overwrite them.\n _creds_tmp=\"\"\n if [ \"${AGENTTEAMS_UPGRADE:-0}\" = \"1\" ] && [ \"${AGENTTEAMS_USE_EMBEDDED}\" = \"1\" ]; then\n # Detect if upgrading from old arch: old arch has no agentteams-controller container\n # (or has it as a docker-proxy only, not embedded). Check if the existing\n # agentteams-controller is an embedded image (has supervisord) or just a proxy.\n _is_old_arch=0\n if ! ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -q \"^agentteams-controller$\"; then\n _is_old_arch=1\n elif ${DOCKER_CMD} ps -a --format '{{.Names}} {{.Image}}' 2>/dev/null | grep \"^agentteams-controller \" | grep -qv \"embedded\"; then\n _is_old_arch=1\n fi\n\n if [ \"${_is_old_arch}\" = \"1\" ]; then\n _creds_tmp=$(mktemp -d)\n\n # docker exec for Matrix/minio paths only works while agentteams-manager is running.\n _mgr_creds_tempstart=0\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -q '^agentteams-manager$'; then\n if ! ${DOCKER_CMD} ps --format '{{.Names}}' 2>/dev/null | grep -q '^agentteams-manager$'; then\n log \"agentteams-manager is stopped; starting it temporarily to extract Matrix credentials for upgrade...\"\n ${DOCKER_CMD} start agentteams-manager 2>/dev/null || true\n wait_matrix_ready \"agentteams-manager\"\n _mgr_creds_tempstart=1\n fi\n fi\n\n # Manager password (container Config.Env, then secrets file inside running manager, then data volume)\n _mgr_pw=$(${DOCKER_CMD} inspect agentteams-manager --format '{{range .Config.Env}}{{println .}}{{end}}' 2>/dev/null | grep '^AGENTTEAMS_MANAGER_PASSWORD=' | cut -d= -f2-)\n if [ -z \"${_mgr_pw}\" ] && ${DOCKER_CMD} ps --format '{{.Names}}' 2>/dev/null | grep -q '^agentteams-manager$'; then\n _mgr_pw=$(${DOCKER_CMD} exec agentteams-manager bash -c 'source /data/agentteams-secrets.env 2>/dev/null && echo \"${AGENTTEAMS_MANAGER_PASSWORD}\"' 2>/dev/null)\n fi\n if [ -z \"${_mgr_pw}\" ] && ${DOCKER_CMD} volume ls -q 2>/dev/null | grep -q \"^${AGENTTEAMS_DATA_DIR}$\"; then\n _mgr_pw=$(agentteams_read_secret_from_data_volume \"${AGENTTEAMS_DATA_DIR}\" AGENTTEAMS_MANAGER_PASSWORD)\n fi\n\n # Manager admin DM room ID: login as admin, find DM room with @manager\n _mgr_room=\"\"\n if [ -n \"${_mgr_pw}\" ]; then\n _admin_pw=$(grep AGENTTEAMS_ADMIN_PASSWORD \"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\" 2>/dev/null | cut -d= -f2-)\n _admin_user=$(grep AGENTTEAMS_ADMIN_USER \"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\" 2>/dev/null | cut -d= -f2-)\n _admin_user=\"${_admin_user:-admin}\"\n _matrix_domain=$(grep AGENTTEAMS_MATRIX_DOMAIN \"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\" 2>/dev/null | cut -d= -f2-)\n if [ -n \"${_admin_pw}\" ] && ${DOCKER_CMD} ps --format '{{.Names}}' 2>/dev/null | grep -q '^agentteams-manager$'; then\n _admin_token=$(${DOCKER_CMD} exec agentteams-manager curl -sf -X POST http://127.0.0.1:6167/_matrix/client/v3/login \\\n -H \"Content-Type: application/json\" \\\n -d '{\"type\":\"m.login.password\",\"identifier\":{\"type\":\"m.id.user\",\"user\":\"'\"${_admin_user}\"'\"},\"password\":\"'\"${_admin_pw}\"'\"}' 2>/dev/null | python3 -c \"import sys,json; print(json.load(sys.stdin).get('access_token',''))\" 2>/dev/null || true)\n if [ -n \"${_admin_token}\" ]; then\n _mgr_room=$(${DOCKER_CMD} exec agentteams-manager curl -sf -X GET \\\n -H \"Authorization: Bearer ${_admin_token}\" \\\n \"http://127.0.0.1:6167/_matrix/client/v3/joined_rooms\" 2>/dev/null | python3 -c \"\nimport sys,json,subprocess\nrooms = json.load(sys.stdin).get('joined_rooms',[])\nfor room_id in rooms:\n enc = room_id.replace('!','%21')\n members = json.loads(subprocess.check_output([\n 'docker','exec','agentteams-manager','curl','-sf','-X','GET',\n '-H','Authorization: Bearer ${_admin_token}',\n 'http://127.0.0.1:6167/_matrix/client/v3/rooms/'+enc+'/members'\n ]).decode()).get('chunk',[])\n member_ids = [m['state_key'] for m in members]\n if any('manager' in m and 'admin' not in m.split(':')[0] for m in member_ids):\n if len(member_ids) <= 3:\n print(room_id)\n break\n\" 2>/dev/null || true)\n fi\n fi\n if [ -z \"${_mgr_room}\" ]; then\n _mgr_room=$(agentteams_read_admin_dm_room_from_workspace \"${AGENTTEAMS_WORKSPACE_DIR}\")\n fi\n cat > \"${_creds_tmp}/default.env\" <<CREDEOF\nWORKER_PASSWORD=\"${_mgr_pw}\"\nWORKER_MINIO_PASSWORD=\"$(openssl rand -hex 24)\"\nWORKER_GATEWAY_KEY=\"${AGENTTEAMS_MANAGER_GATEWAY_KEY}\"\nWORKER_ROOM_ID=\"${_mgr_room}\"\nCREDEOF\n log \"Extracted Manager Matrix password${_mgr_room:+ and room ID}\"\n fi\n\n # Worker passwords and room IDs from workers-registry.json\n if [ -f \"${AGENTTEAMS_WORKSPACE_DIR}/workers-registry.json\" ]; then\n _worker_names=$(python3 -c \"import json; d=json.load(open('${AGENTTEAMS_WORKSPACE_DIR}/workers-registry.json')); print(' '.join(d.get('workers',{}).keys()))\" 2>/dev/null || true)\n for _wname in ${_worker_names}; do\n _wpw=\"\"\n if ${DOCKER_CMD} ps --format '{{.Names}}' 2>/dev/null | grep -q '^agentteams-manager$'; then\n _wpw=$(${DOCKER_CMD} exec agentteams-manager cat \"/root/agentteams-fs/agents/${_wname}/credentials/matrix/password\" 2>/dev/null || true)\n fi\n if [ -z \"${_wpw}\" ] && ${DOCKER_CMD} volume ls -q 2>/dev/null | grep -q \"^${AGENTTEAMS_DATA_DIR}$\"; then\n _wpw=$(agentteams_read_worker_creds_value_from_volume \"${AGENTTEAMS_DATA_DIR}\" \"${_wname}\" WORKER_PASSWORD)\n fi\n _wroom=$(python3 -c \"import json; d=json.load(open('${AGENTTEAMS_WORKSPACE_DIR}/workers-registry.json')); print(d.get('workers',{}).get('${_wname}',{}).get('room_id',''))\" 2>/dev/null || true)\n if [ -z \"${_wroom}\" ] && ${DOCKER_CMD} volume ls -q 2>/dev/null | grep -q \"^${AGENTTEAMS_DATA_DIR}$\"; then\n _wroom=$(agentteams_read_worker_creds_value_from_volume \"${AGENTTEAMS_DATA_DIR}\" \"${_wname}\" WORKER_ROOM_ID)\n fi\n if [ -n \"${_wpw}\" ]; then\n cat > \"${_creds_tmp}/${_wname}.env\" <<CREDEOF\nWORKER_PASSWORD=\"${_wpw}\"\nWORKER_MINIO_PASSWORD=\"$(openssl rand -hex 24)\"\nWORKER_GATEWAY_KEY=\"$(openssl rand -hex 32)\"\nWORKER_ROOM_ID=\"${_wroom}\"\nCREDEOF\n log \"Extracted ${_wname} Matrix password${_wroom:+ and room ID}\"\n fi\n done\n fi\n\n if [ \"${_mgr_creds_tempstart}\" = \"1\" ]; then\n log \"Stopping agentteams-manager after credential extraction (upgrade will recreate containers)...\"\n ${DOCKER_CMD} stop agentteams-manager 2>/dev/null || true\n fi\n fi # _is_old_arch\n fi\n\n # --- Stop and remove existing containers ---\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' | grep -q \"^agentteams-controller$\"; then\n ${DOCKER_CMD} stop agentteams-controller 2>/dev/null || true\n ${DOCKER_CMD} rm agentteams-controller 2>/dev/null || true\n fi\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' | grep -q \"^agentteams-manager$\"; then\n log \"$(msg install.removing_existing)\"\n ${DOCKER_CMD} stop agentteams-manager 2>/dev/null || true\n ${DOCKER_CMD} rm agentteams-manager 2>/dev/null || true\n fi\n\n # Stop and remove worker containers (controller will recreate via CR reconciliation)\n if [ -n \"${UPGRADE_EXISTING_WORKERS:-}\" ]; then\n log \"$(msg install.existing.stopping_workers)\"\n for w in ${UPGRADE_EXISTING_WORKERS}; do\n ${DOCKER_CMD} stop \"${w}\" 2>/dev/null || true\n ${DOCKER_CMD} rm \"${w}\" 2>/dev/null || true\n log \"$(msg install.existing.removed \"${w}\")\"\n done\n fi\n\n # Clean up legacy containers from v1.0.x and earlier\n for _legacy_name in agentteams-docker-proxy; do\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -qx \"${_legacy_name}\"; then\n log \"Removing legacy container: ${_legacy_name}\"\n ${DOCKER_CMD} stop \"${_legacy_name}\" 2>/dev/null || true\n ${DOCKER_CMD} rm -f \"${_legacy_name}\" 2>/dev/null || true\n fi\n done\n\n # --- Upgrade: inject extracted credentials into data volume ---\n # Only needed for old-arch upgrades (credential files were extracted above).\n if [ -n \"${_creds_tmp}\" ] && [ -d \"${_creds_tmp}\" ] && [ -n \"$(ls -A \"${_creds_tmp}\" 2>/dev/null)\" ]; then\n local _cleanup_ctr=\"agentteams-upgrade-cleanup\"\n ${DOCKER_CMD} rm -f \"${_cleanup_ctr}\" 2>/dev/null || true\n ${DOCKER_CMD} run --rm --name \"${_cleanup_ctr}\" \\\n --entrypoint sh \\\n -v \"${AGENTTEAMS_DATA_DIR}:/data\" \\\n -v \"${_creds_tmp}:/creds:ro\" \\\n \"${EMBEDDED_IMAGE}\" -c '\n rm -rf /data/worker-creds\n mkdir -p /data/worker-creds\n cp /creds/*.env /data/worker-creds/ 2>/dev/null || true\n chmod 600 /data/worker-creds/*.env 2>/dev/null || true\n ' 2>/dev/null && log \"Injected credentials for upgrade\" || log \"Warning: credential injection failed, continuing\"\n rm -rf \"${_creds_tmp}\"\n fi\n\n # --- Start containers ---\n log \"$(msg install.starting_manager)\"\n\n # Build port binding args\n if [ \"${AGENTTEAMS_LOCAL_ONLY:-1}\" = \"1\" ]; then\n _port_prefix=\"127.0.0.1:\"\n else\n _port_prefix=\"\"\n fi\n\n # Ensure agentteams-net Docker network exists\n ${DOCKER_CMD} network inspect agentteams-net >/dev/null 2>&1 || ${DOCKER_CMD} network create agentteams-net\n\n if [ \"${AGENTTEAMS_USE_EMBEDDED}\" != \"1\" ] && [ \"${AGENTTEAMS_UPGRADE:-0}\" = \"1\" ]; then\n # Check if current installation is embedded — downgrade to legacy is not supported\n if ${DOCKER_CMD} ps -a --format '{{.Names}} {{.Image}}' 2>/dev/null | grep \"^agentteams-controller \" | grep -q \"embedded\"; then\n error \"Downgrade from embedded architecture to legacy version (${AGENTTEAMS_VERSION}) is not supported.\"\n error \"Please use 'make uninstall-embedded' first, then do a clean install of the target version.\"\n exit 1\n fi\n fi\n\n if [ \"${AGENTTEAMS_USE_EMBEDDED}\" = \"1\" ]; then\n # ============================================================\n # New architecture: embedded controller + auto-created manager\n # ============================================================\n\n # Internal port: 8080 (Higress gateway inside the container).\n local _internal_gw_port=8080\n local _matrix_domain=\"${AGENTTEAMS_MATRIX_DOMAIN:-matrix-local.agentteams.io:${AGENTTEAMS_PORT_GATEWAY}}\"\n local _aigw_domain=\"${AGENTTEAMS_AI_GATEWAY_DOMAIN:-aigw-local.agentteams.io}\"\n # Ensure internal gateway port is present (container-internal traffic uses 8080)\n case \"${_aigw_domain}\" in *:*) ;; *) _aigw_domain=\"${_aigw_domain}:${_internal_gw_port}\" ;; esac\n local _fs_domain=\"${AGENTTEAMS_FS_DOMAIN:-fs-local.agentteams.io}\"\n case \"${_fs_domain}\" in *:*) ;; *) _fs_domain=\"${_fs_domain}:${_internal_gw_port}\" ;; esac\n\n # Controller env args\n local _ctrl_env_prefix\n _ctrl_env_prefix=\"$(_controller_env_prefix \"${AGENTTEAMS_VERSION}\")\"\n local _storage_prefix\n _storage_prefix=\"$(_controller_storage_prefix \"${AGENTTEAMS_VERSION}\")\"\n local _ctrl_env_args=(\n -e \"${_ctrl_env_prefix}ADMIN_USER=${AGENTTEAMS_ADMIN_USER}\"\n -e \"${_ctrl_env_prefix}ADMIN_PASSWORD=${AGENTTEAMS_ADMIN_PASSWORD}\"\n -e \"${_ctrl_env_prefix}MANAGER_PASSWORD=${AGENTTEAMS_MANAGER_PASSWORD}\"\n -e \"${_ctrl_env_prefix}REGISTRATION_TOKEN=${AGENTTEAMS_REGISTRATION_TOKEN}\"\n -e \"${_ctrl_env_prefix}MINIO_USER=${AGENTTEAMS_MINIO_USER}\"\n -e \"${_ctrl_env_prefix}MINIO_PASSWORD=${AGENTTEAMS_MINIO_PASSWORD}\"\n -e \"${_ctrl_env_prefix}LLM_PROVIDER=${AGENTTEAMS_LLM_PROVIDER}\"\n -e \"${_ctrl_env_prefix}LLM_API_KEY=${AGENTTEAMS_LLM_API_KEY}\"\n -e \"${_ctrl_env_prefix}DEFAULT_MODEL=${AGENTTEAMS_DEFAULT_MODEL}\"\n -e \"${_ctrl_env_prefix}MANAGER_GATEWAY_KEY=${AGENTTEAMS_MANAGER_GATEWAY_KEY}\"\n -e \"${_ctrl_env_prefix}MANAGER_RUNTIME=${AGENTTEAMS_MANAGER_RUNTIME:-copaw}\"\n -e \"${_ctrl_env_prefix}MANAGER_IMAGE=$([ \"${AGENTTEAMS_MANAGER_RUNTIME}\" = \"copaw\" ] && echo \"${MANAGER_COPAW_IMAGE}\" || echo \"${MANAGER_IMAGE}\")\"\n -e \"${_ctrl_env_prefix}DEFAULT_WORKER_RUNTIME=${AGENTTEAMS_DEFAULT_WORKER_RUNTIME:-copaw}\"\n -e \"${_ctrl_env_prefix}WORKER_IMAGE=${WORKER_IMAGE}\"\n -e \"${_ctrl_env_prefix}COPAW_WORKER_IMAGE=${COPAW_WORKER_IMAGE}\"\n -e \"${_ctrl_env_prefix}QWENPAW_WORKER_IMAGE=${QWENPAW_WORKER_IMAGE}\"\n -e \"${_ctrl_env_prefix}HERMES_WORKER_IMAGE=${HERMES_WORKER_IMAGE}\"\n -e \"${_ctrl_env_prefix}MATRIX_DOMAIN=${_matrix_domain}\"\n -e \"${_ctrl_env_prefix}ELEMENT_HOMESERVER_URL=http://127.0.0.1:${AGENTTEAMS_PORT_GATEWAY}\"\n -e \"${_ctrl_env_prefix}MATRIX_URL=http://127.0.0.1:6167\"\n -e \"${_ctrl_env_prefix}MATRIX_E2EE=${AGENTTEAMS_MATRIX_E2EE:-0}\"\n -e \"${_ctrl_env_prefix}MINIO_ENDPOINT=http://127.0.0.1:9000\"\n -e \"${_ctrl_env_prefix}MINIO_BUCKET=agentteams-storage\"\n -e \"${_ctrl_env_prefix}STORAGE_PREFIX=${_storage_prefix}\"\n -e \"${_ctrl_env_prefix}FS_ENDPOINT=http://127.0.0.1:9000\"\n -e \"${_ctrl_env_prefix}AI_GATEWAY_URL=http://${_aigw_domain}\"\n -e \"${_ctrl_env_prefix}CONTROLLER_URL=http://agentteams-controller:8090\"\n -e \"${_ctrl_env_prefix}DOCKER_NETWORK=agentteams-net\"\n -e \"${_ctrl_env_prefix}WORKSPACE_DIR=${AGENTTEAMS_WORKSPACE_DIR}\"\n -e \"${_ctrl_env_prefix}HOST_SHARE_DIR=${AGENTTEAMS_HOST_SHARE_DIR}\"\n -e \"${_ctrl_env_prefix}MANAGER_ENABLED=true\"\n -e \"${_ctrl_env_prefix}PORT_MANAGER_CONSOLE=${AGENTTEAMS_PORT_MANAGER_CONSOLE:-18888}\"\n )\n if _use_legacy_image_env \"${AGENTTEAMS_VERSION}\"; then\n _ctrl_env_args+=(\n -e \"${_ctrl_env_prefix}FS_BUCKET=agentteams-storage\"\n -e \"${_ctrl_env_prefix}RESOURCE_PREFIX=agentteams-\"\n )\n else\n _ctrl_env_args+=(\n -e \"${_ctrl_env_prefix}MATRIX_APPSERVICE_ENABLED=${AGENTTEAMS_MATRIX_APPSERVICE_ENABLED:-true}\"\n -e \"${_ctrl_env_prefix}MATRIX_APPSERVICE_AS_TOKEN=${AGENTTEAMS_MATRIX_APPSERVICE_AS_TOKEN:-}\"\n -e \"${_ctrl_env_prefix}MATRIX_APPSERVICE_HS_TOKEN=${AGENTTEAMS_MATRIX_APPSERVICE_HS_TOKEN:-}\"\n )\n fi\n\n # Timezone\n if [ -n \"${AGENTTEAMS_TIMEZONE:-}\" ]; then\n _ctrl_env_args+=(-e \"TZ=${AGENTTEAMS_TIMEZONE}\")\n fi\n\n # Yolo mode\n if [ \"${AGENTTEAMS_YOLO:-}\" = \"1\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}YOLO=1\")\n fi\n\n # Matrix-plugin debug tracing — propagated to every manager + worker\n # the controller spawns, then translated to OPENCLAW_MATRIX_DEBUG=1\n # by the container entrypoints. Use this to diagnose\n # \"worker did not join\" / \"manager replied empty\" hangs.\n if [ \"${AGENTTEAMS_MATRIX_DEBUG:-}\" = \"1\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}MATRIX_DEBUG=1\")\n fi\n\n # Optional: GitHub token\n if [ -n \"${AGENTTEAMS_GITHUB_TOKEN:-}\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}GITHUB_TOKEN=${AGENTTEAMS_GITHUB_TOKEN}\")\n fi\n\n # Optional: embedding model\n if [ -n \"${AGENTTEAMS_EMBEDDING_MODEL:-}\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}EMBEDDING_MODEL=${AGENTTEAMS_EMBEDDING_MODEL}\")\n fi\n\n # Optional: custom model capability overrides (context window, max\n # tokens, vision, reasoning). These are read by the Controller's\n # config.go LoadConfig to annotate custom models not in the built-in\n # presets table.\n if [ -n \"${AGENTTEAMS_MODEL_CONTEXT_WINDOW:-}\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}MODEL_CONTEXT_WINDOW=${AGENTTEAMS_MODEL_CONTEXT_WINDOW}\")\n fi\n if [ -n \"${AGENTTEAMS_MODEL_MAX_TOKENS:-}\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}MODEL_MAX_TOKENS=${AGENTTEAMS_MODEL_MAX_TOKENS}\")\n fi\n if [ -n \"${AGENTTEAMS_MODEL_VISION:-}\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}MODEL_VISION=${AGENTTEAMS_MODEL_VISION}\")\n fi\n if [ -n \"${AGENTTEAMS_MODEL_REASONING:-}\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}MODEL_REASONING=${AGENTTEAMS_MODEL_REASONING}\")\n fi\n\n # Optional: OpenAI-compatible base URL\n if [ -n \"${AGENTTEAMS_OPENAI_BASE_URL:-}\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}OPENAI_BASE_URL=${AGENTTEAMS_OPENAI_BASE_URL}\")\n fi\n # Optional: language\n if [ -n \"${AGENTTEAMS_LANGUAGE:-}\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}LANGUAGE=${AGENTTEAMS_LANGUAGE}\")\n fi\n\n # Optional: CMS/ARMS observability. In embedded mode the controller\n # spawns the Manager and Workers, so it must receive these settings.\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}CMS_TRACES_ENABLED=${AGENTTEAMS_CMS_TRACES_ENABLED:-false}\")\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}CMS_SERVICE_NAME=${AGENTTEAMS_CMS_SERVICE_NAME:-agentteams-manager}\")\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}CMS_METRICS_ENABLED=${AGENTTEAMS_CMS_METRICS_ENABLED:-false}\")\n if [ -n \"${AGENTTEAMS_CMS_ENDPOINT:-}\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}CMS_ENDPOINT=${AGENTTEAMS_CMS_ENDPOINT}\")\n fi\n if [ -n \"${AGENTTEAMS_CMS_LICENSE_KEY:-}\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}CMS_LICENSE_KEY=${AGENTTEAMS_CMS_LICENSE_KEY}\")\n fi\n if [ -n \"${AGENTTEAMS_CMS_PROJECT:-}\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}CMS_PROJECT=${AGENTTEAMS_CMS_PROJECT}\")\n fi\n if [ -n \"${AGENTTEAMS_CMS_WORKSPACE:-}\" ]; then\n _ctrl_env_args+=(-e \"${_ctrl_env_prefix}CMS_WORKSPACE=${AGENTTEAMS_CMS_WORKSPACE}\")\n fi\n\n # shellcheck disable=SC2086\n ${DOCKER_CMD} run -d \\\n --name agentteams-controller \\\n --network agentteams-net \\\n --network-alias matrix-local.agentteams.io \\\n --network-alias aigw-local.agentteams.io \\\n --network-alias fs-local.agentteams.io \\\n \"${_ctrl_env_args[@]}\" \\\n -v \"${CONTAINER_SOCK}:/var/run/docker.sock\" \\\n --security-opt label=disable \\\n -v \"${AGENTTEAMS_DATA_DIR}:/data\" \\\n -v \"${AGENTTEAMS_WORKSPACE_DIR}:/root/agentteams-fs/agents/manager\" \\\n -p \"${_port_prefix}${AGENTTEAMS_PORT_GATEWAY}:8080\" \\\n -p \"${_port_prefix}${AGENTTEAMS_PORT_CONSOLE}:8001\" \\\n -p \"${_port_prefix}${AGENTTEAMS_PORT_ELEMENT_WEB:-18088}:8088\" \\\n --restart unless-stopped \\\n \"${EMBEDDED_IMAGE}\"\n\n log \"Embedded controller started: agentteams-controller\"\n\n # Wait for infrastructure inside the controller container\n _wait_for_url() {\n local url=\"$1\" ctr=\"$2\" max_wait=\"${3:-120}\" desc=\"${4:-service}\"\n local elapsed=0\n log \"Waiting for ${desc}...\"\n while [ $elapsed -lt $max_wait ]; do\n if ${DOCKER_CMD} exec \"${ctr}\" curl -sf \"${url}\" >/dev/null 2>&1; then\n log \"${desc} is ready (${elapsed}s)\"\n return 0\n fi\n sleep 2\n elapsed=$((elapsed + 2))\n done\n log \"ERROR: ${desc} not ready after ${max_wait}s\"\n return 1\n }\n\n _wait_for_url \"http://127.0.0.1:6167/_tuwunel/server_version\" agentteams-controller 120 \"Tuwunel (Matrix)\" || exit 1\n _wait_for_url \"http://127.0.0.1:9000/minio/health/live\" agentteams-controller 60 \"MinIO\" || exit 1\n _wait_for_url \"http://127.0.0.1:8080/status\" agentteams-controller 120 \"Higress Gateway\" || exit 1\n\n # Wait for controller to create Manager Agent container\n log \"Waiting for Manager Agent container...\"\n local _mgr_wait=0\n local _mgr_max=300\n while [ $_mgr_wait -lt $_mgr_max ]; do\n if ${DOCKER_CMD} ps --format '{{.Names}}' 2>/dev/null | grep -q \"^agentteams-manager$\"; then\n log \"Manager Agent container detected (${_mgr_wait}s)\"\n break\n fi\n sleep 3\n _mgr_wait=$((_mgr_wait + 3))\n done\n if [ $_mgr_wait -ge $_mgr_max ]; then\n log \"ERROR: Manager Agent container not created after ${_mgr_max}s\"\n log \"Controller logs:\"\n ${DOCKER_CMD} exec agentteams-controller tail -30 /var/log/agentteams/agentteams-controller-error.log 2>/dev/null || true\n exit 1\n fi\n\n # Wait for Manager Agent to be running\n log \"Waiting for Manager Agent to start...\"\n local _agent_wait=0\n while [ $_agent_wait -lt 120 ]; do\n local _state\n _state=$(${DOCKER_CMD} inspect --format '{{.State.Status}}' agentteams-manager 2>/dev/null || echo \"missing\")\n if [ \"${_state}\" = \"running\" ]; then\n log \"Manager Agent is running\"\n break\n fi\n sleep 2\n _agent_wait=$((_agent_wait + 2))\n done\n\n # Enable yolo mode in agent if requested\n if [ \"${AGENTTEAMS_YOLO:-}\" = \"1\" ]; then\n ${DOCKER_CMD} exec agentteams-manager touch /root/manager-workspace/yolo-mode 2>/dev/null || true\n fi\n\n # Wait for the controller to send the first-boot welcome message.\n # The controller gates this on (a) Manager joining the DM room and\n # (b) Higress WASM key-auth propagation actually clearing /v1/chat/completions\n # for the Manager's gateway key — typically ~45-90s on a fresh install.\n # We poll Manager CR Status.WelcomeSent via the in-container agt CLI,\n # exec'd inside agentteams-controller (the source-of-truth container — its\n # bundled CLI binary is always in lockstep with whatever controller\n # binary is currently serving the HTTP API, since they're the same\n # `go build` output. The agentteams-manager container's CLI may lag the\n # controller across image upgrades and silently drop the welcomeSent\n # field, leaving this loop hung). The controller container mints a\n # long-lived admin SA token at startup and writes it to\n # AGENTTEAMS_AUTH_TOKEN_FILE=/var/run/agentteams/cli-token (set as a Dockerfile\n # ENV default), so a bare `docker exec agentteams-controller agt …`\n # auto-discovers both the endpoint and the token. There is a brief\n # window after container start before bootstrapAdminCLIToken completes\n # where the file may be empty / absent — the loop's silent retry\n # handles that the same way it handles the manager-not-yet-running\n # case below.\n if ${DOCKER_CMD} exec agentteams-controller sh -c 'command -v agt' >/dev/null 2>&1; then\n log \"$(msg install.welcome_msg.waiting)\"\n local _welcome_wait=0\n local _welcome_max=\"${AGENTTEAMS_WELCOME_TIMEOUT:-300}\"\n local _welcome_done=0\n while [ $_welcome_wait -lt $_welcome_max ]; do\n # `tr -d` strips whitespace/CR so the grep stays robust to\n # any future change in go-json field ordering or formatting.\n local _wjson\n _wjson=$(${DOCKER_CMD} exec agentteams-controller \\\n agt get managers default -o json 2>/dev/null || true)\n if [ -n \"${_wjson}\" ] && printf '%s' \"${_wjson}\" | tr -d ' \\r\\n' | grep -q '\"welcomeSent\":true'; then\n log \"$(msg install.welcome_msg.confirmed \"${_welcome_wait}\")\"\n _welcome_done=1\n break\n fi\n sleep 3\n _welcome_wait=$((_welcome_wait + 3))\n done\n if [ $_welcome_done -ne 1 ]; then\n # Non-fatal: install is still good. Keep going to the success\n # banner so the admin can use Element Web to nudge Manager into\n # onboarding manually (one DM message is enough).\n log \"$(msg install.welcome_msg.timeout \"${_welcome_max}\")\"\n log \"$(msg install.welcome_msg.timeout_hint)\"\n log \"$(msg install.welcome_msg.timeout_inspect)\"\n fi\n else\n log \"$(msg install.welcome_msg.poll_unavailable)\"\n fi\n\n else\n # ============================================================\n # Legacy architecture: all-in-one manager container\n # ============================================================\n\n NETWORK_ARGS=\"--network agentteams-net\"\n NETWORK_ALIAS_ARGS=\"--network-alias matrix-local.agentteams.io --network-alias aigw-local.agentteams.io --network-alias fs-local.agentteams.io\"\n for _domain in \"${AGENTTEAMS_MATRIX_CLIENT_DOMAIN:-}\" \"${AGENTTEAMS_CONSOLE_DOMAIN:-}\"; do\n if [ -n \"${_domain}\" ] && [[ \"${_domain}\" == *-local.agentteams.io ]]; then\n NETWORK_ALIAS_ARGS=\"${NETWORK_ALIAS_ARGS} --network-alias ${_domain}\"\n fi\n done\n\n # Start Docker API proxy if enabled (security layer between Manager and Docker daemon)\n PROXY_ARGS=\"\"\n if [ \"${AGENTTEAMS_DOCKER_PROXY:-1}\" = \"1\" ] && [ -n \"${CONTAINER_SOCK:-}\" ]; then\n local _proxy_image=\"${AGENTTEAMS_REGISTRY}/higress/agentteams-docker-proxy:${AGENTTEAMS_VERSION}\"\n # Try versioned tag, fallback to latest\n if ! ${DOCKER_CMD} image inspect \"${_proxy_image}\" >/dev/null 2>&1; then\n ${DOCKER_CMD} pull \"${_proxy_image}\" 2>/dev/null || {\n _proxy_image=\"${AGENTTEAMS_REGISTRY}/higress/agentteams-docker-proxy:latest\"\n ${DOCKER_CMD} pull \"${_proxy_image}\" 2>/dev/null || true\n }\n fi\n if ${DOCKER_CMD} image inspect \"${_proxy_image}\" >/dev/null 2>&1; then\n log \"Starting Docker API proxy...\"\n ${DOCKER_CMD} run -d \\\n --name agentteams-docker-proxy \\\n --network agentteams-net \\\n -v \"${CONTAINER_SOCK}:/var/run/docker.sock\" \\\n --security-opt label=disable \\\n -e AGENTTEAMS_WORKER_IMAGE=\"${WORKER_IMAGE}\" \\\n -e AGENTTEAMS_COPAW_WORKER_IMAGE=\"${COPAW_WORKER_IMAGE}\" \\\n -e AGENTTEAMS_QWENPAW_WORKER_IMAGE=\"${QWENPAW_WORKER_IMAGE}\" \\\n -e AGENTTEAMS_HERMES_WORKER_IMAGE=\"${HERMES_WORKER_IMAGE}\" \\\n ${AGENTTEAMS_PROXY_ALLOWED_REGISTRIES:+-e AGENTTEAMS_PROXY_ALLOWED_REGISTRIES=\"${AGENTTEAMS_PROXY_ALLOWED_REGISTRIES}\"} \\\n --restart unless-stopped \\\n \"${_proxy_image}\"\n PROXY_ARGS=\"-e AGENTTEAMS_CONTROLLER_URL=http://agentteams-docker-proxy:2375 -e AGENTTEAMS_CONTAINER_API=http://agentteams-docker-proxy:2375\"\n SOCKET_MOUNT_ARGS=\"\"\n fi\n fi\n\n # Pass host timezone to container\n TZ_ARGS=\"\"\n if [ -n \"${AGENTTEAMS_TIMEZONE:-}\" ]; then\n TZ_ARGS=\"-e TZ=${AGENTTEAMS_TIMEZONE}\"\n fi\n\n YOLO_ARGS=\"\"\n if [ \"${AGENTTEAMS_YOLO:-}\" = \"1\" ]; then\n YOLO_ARGS=\"-e AGENTTEAMS_YOLO=1\"\n fi\n\n MATRIX_DEBUG_ARGS=\"\"\n if [ \"${AGENTTEAMS_MATRIX_DEBUG:-}\" = \"1\" ]; then\n MATRIX_DEBUG_ARGS=\"-e AGENTTEAMS_MATRIX_DEBUG=1\"\n fi\n\n # shellcheck disable=SC2086\n ${DOCKER_CMD} run -d \\\n --name agentteams-manager \\\n --env-file \"${ENV_FILE}\" \\\n -e HOME=/root/manager-workspace \\\n -w /root/manager-workspace \\\n -e HOST_ORIGINAL_HOME=\"${AGENTTEAMS_HOST_SHARE_DIR}\" \\\n -e AGENTTEAMS_MANAGER_RUNTIME=\"${AGENTTEAMS_MANAGER_RUNTIME:-copaw}\" \\\n ${JVM_ARGS:+-e JVM_ARGS=\"${JVM_ARGS}\"} \\\n ${YOLO_ARGS} \\\n ${MATRIX_DEBUG_ARGS} \\\n ${TZ_ARGS} \\\n ${SOCKET_MOUNT_ARGS} \\\n ${NETWORK_ARGS} \\\n ${NETWORK_ALIAS_ARGS} \\\n ${PROXY_ARGS} \\\n -p \"${_port_prefix}${AGENTTEAMS_PORT_GATEWAY}:8080\" \\\n -p \"${_port_prefix}${AGENTTEAMS_PORT_CONSOLE}:8001\" \\\n -p \"${_port_prefix}${AGENTTEAMS_PORT_ELEMENT_WEB:-18088}:8088\" \\\n -p \"127.0.0.1:${AGENTTEAMS_PORT_MANAGER_CONSOLE:-18888}:18888\" \\\n ${DATA_MOUNT_ARGS} \\\n ${WORKSPACE_MOUNT_ARGS} \\\n ${HOST_SHARE_MOUNT_ARGS} \\\n --restart unless-stopped \\\n \"$([ \"${AGENTTEAMS_MANAGER_RUNTIME}\" = \"copaw\" ] && echo \"${MANAGER_COPAW_IMAGE}\" || echo \"${MANAGER_IMAGE}\")\"\n\n # Wait for Manager agent to be ready\n wait_manager_ready \"agentteams-manager\"\n\n # Wait for Matrix server to be ready\n wait_matrix_ready \"agentteams-manager\"\n\n # Post-install verification (non-fatal: warnings only)\n local _verify_script\n _verify_script=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)/agentteams-verify.sh\"\n if [ -f \"${_verify_script}\" ]; then\n bash \"${_verify_script}\" \"agentteams-manager\" || {\n log \"WARNING: Some post-install checks failed. Re-run: bash install/agentteams-verify.sh\"\n }\n fi\n fi\n unset _port_prefix\n\n # ── Start Dashboard (optional) ────────────────────────────────────\n _start_dashboard\n\n # Apply Podman autostart if selected and environment matches\n if [ \"${DOCKER_CMD}\" = \"podman\" ] && [ \"${AGENTTEAMS_PODMAN_AUTOSTART:-0}\" = \"1\" ]; then\n setup_podman_autostart\n fi\n\n log \"\"\n log \"$(msg success.title)\"\n log \"\"\n log \"$(msg success.domains_configured)\"\n log \" ${AGENTTEAMS_MATRIX_DOMAIN%%:*} ${AGENTTEAMS_MATRIX_CLIENT_DOMAIN} ${AGENTTEAMS_AI_GATEWAY_DOMAIN} ${AGENTTEAMS_FS_DOMAIN} ${AGENTTEAMS_CONSOLE_DOMAIN}\"\n log \"\"\n local lan_ip\n lan_ip=$(detect_lan_ip)\n echo -e \"\\033[33m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\033[0m\"\n echo -e \"\\033[33m $(msg success.open_url)\\033[0m\"\n echo -e \"\\033[33m \\033[0m\"\n echo -e \"\\033[1;36m http://127.0.0.1:${AGENTTEAMS_PORT_ELEMENT_WEB:-18088}/#/login\\033[0m\"\n echo -e \"\\033[33m \\033[0m\"\n echo -e \"\\033[33m $(msg success.login_with)\\033[0m\"\n echo -e \"\\033[33m $(msg success.username \"${AGENTTEAMS_ADMIN_USER}\")\\033[0m\"\n echo -e \"\\033[33m $(msg success.password \"${AGENTTEAMS_ADMIN_PASSWORD}\")\\033[0m\"\n echo -e \"\\033[33m \\033[0m\"\n echo -e \"\\033[33m $(msg success.after_login)\\033[0m\"\n echo -e \"\\033[33m $(msg success.tell_it)\\033[0m\"\n echo -e \"\\033[33m $(msg success.manager_auto)\\033[0m\"\n echo -e \"\\033[33m \\033[0m\"\n echo -e \"\\033[33m ───────────────────────────────────────────────────────────────────────────── \\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_title)\\033[0m\"\n echo -e \"\\033[33m \\033[0m\"\n if [ -n \"${lan_ip}\" ]; then\n echo -e \"\\033[33m $(msg success.mobile_step1)\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_step2 \"http://${lan_ip}:${AGENTTEAMS_PORT_GATEWAY}\")\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_step3)\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_username \"${AGENTTEAMS_ADMIN_USER}\")\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_password \"${AGENTTEAMS_ADMIN_PASSWORD}\")\\033[0m\"\n else\n echo -e \"\\033[33m $(msg success.mobile_step1)\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_step2_noip \"${AGENTTEAMS_PORT_GATEWAY}\")\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_noip_hint)\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_step3)\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_username \"${AGENTTEAMS_ADMIN_USER}\")\\033[0m\"\n echo -e \"\\033[33m $(msg success.mobile_password \"${AGENTTEAMS_ADMIN_PASSWORD}\")\\033[0m\"\n fi\n echo -e \"\\033[33m \\033[0m\"\n echo -e \"\\033[33m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\033[0m\"\n log \"\"\n log \"$(msg success.other_consoles)\"\n log \"$(msg success.higress_console \"${AGENTTEAMS_PORT_CONSOLE}\" \"${AGENTTEAMS_ADMIN_USER}\" \"${AGENTTEAMS_ADMIN_PASSWORD}\")\"\n if [ \"${AGENTTEAMS_DASHBOARD:-0}\" = \"1\" ]; then\n log \"$(msg success.dashboard \"${AGENTTEAMS_PORT_DASHBOARD:-13000}\")\"\n fi\n if [ \"${AGENTTEAMS_USE_EMBEDDED}\" != \"1\" ]; then\n log \"$(msg success.manager_console \"${AGENTTEAMS_PORT_MANAGER_CONSOLE:-18888}\")\"\n log \"$(msg success.manager_console_gateway \"${AGENTTEAMS_ADMIN_USER}\" \"${AGENTTEAMS_ADMIN_PASSWORD}\")\"\n fi\n log \"\"\n log \"$(msg success.switch_llm.title)\"\n log \"$(msg success.switch_llm.hint)\"\n log \"$(msg success.switch_llm.docs)\"\n log \"$(msg success.switch_llm.url)\"\n log \"\"\n log \"$(msg success.tip)\"\n log \"\"\n if [ \"${AGENTTEAMS_LOCAL_ONLY:-1}\" != \"1\" ]; then\n echo -e \"\\033[33m$(msg port.local_only.https_hint)\\033[0m\"\n log \"\"\n fi\n log \"$(msg success.config_file \"${ENV_FILE}\")\"\n log \"$(msg success.data_volume \"${AGENTTEAMS_DATA_DIR}\")\"\n log \"$(msg success.workspace \"${AGENTTEAMS_WORKSPACE_DIR}\")\"\n}\n\n# ============================================================\n# Worker Installation (One-Click)\n# ============================================================\n\ninstall_worker() {\n local WORKER_NAME=\"\"\n local FS=\"\"\n local FS_KEY=\"\"\n local FS_SECRET=\"\"\n local RESET=false\n local SKILLS_API_URL=\"\"\n\n # Parse arguments\n while [ $# -gt 0 ]; do\n case $1 in\n --name) WORKER_NAME=\"$2\"; shift 2 ;;\n --fs) FS=\"$2\"; shift 2 ;;\n --fs-key) FS_KEY=\"$2\"; shift 2 ;;\n --fs-secret) FS_SECRET=\"$2\"; shift 2 ;;\n --skills-api-url) SKILLS_API_URL=\"$2\"; shift 2 ;;\n --reset) RESET=true; shift ;;\n *) die \"$(msg error.unknown_option \"$1\")\" ;;\n esac\n done\n\n # Validate required params\n [ -z \"${WORKER_NAME}\" ] && die \"$(msg error.name_required)\"\n [ -z \"${FS}\" ] && die \"$(msg error.fs_required)\"\n [ -z \"${FS_KEY}\" ] && die \"$(msg error.fs_key_required)\"\n [ -z \"${FS_SECRET}\" ] && die \"$(msg error.fs_secret_required)\"\n\n local CONTAINER_NAME=\"agentteams-worker-${WORKER_NAME}\"\n\n # Handle reset\n if [ \"${RESET}\" = true ]; then\n log \"$(msg worker.resetting \"${WORKER_NAME}\")\"\n ${DOCKER_CMD} stop \"${CONTAINER_NAME}\" 2>/dev/null || true\n ${DOCKER_CMD} rm \"${CONTAINER_NAME}\" 2>/dev/null || true\n fi\n\n # Check for existing container\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' | grep -q \"^${CONTAINER_NAME}$\"; then\n die \"$(msg worker.exists \"${CONTAINER_NAME}\")\"\n fi\n\n log \"$(msg worker.starting \"${WORKER_NAME}\")\"\n\n # Build docker run args\n local DOCKER_ENV=\"\"\n DOCKER_ENV=\"${DOCKER_ENV} -e HOME=/root/agentteams-fs/agents/${WORKER_NAME}\"\n DOCKER_ENV=\"${DOCKER_ENV} -w /root/agentteams-fs/agents/${WORKER_NAME}\"\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_WORKER_NAME=${WORKER_NAME}\"\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_FS_ENDPOINT=${FS}\"\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_FS_ACCESS_KEY=${FS_KEY}\"\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_FS_SECRET_KEY=${FS_SECRET}\"\n\n if [ -z \"${SKILLS_API_URL}\" ]; then\n if [ -n \"${AGENTTEAMS_SKILLS_API_URL:-}\" ]; then\n SKILLS_API_URL=\"${AGENTTEAMS_SKILLS_API_URL}\"\n else\n SKILLS_API_URL=\"nacos://market.agentteams.io:80/public\"\n fi\n fi\n\n # Add SKILLS_API_URL if specified\n DOCKER_ENV=\"${DOCKER_ENV} -e SKILLS_API_URL=${SKILLS_API_URL}\"\n log \"$(msg worker.skills_url \"${SKILLS_API_URL}\")\"\n if [ -n \"${AGENTTEAMS_NACOS_USERNAME:-}\" ]; then\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_NACOS_USERNAME=${AGENTTEAMS_NACOS_USERNAME}\"\n fi\n if [ -n \"${AGENTTEAMS_NACOS_PASSWORD:-}\" ]; then\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_NACOS_PASSWORD=${AGENTTEAMS_NACOS_PASSWORD}\"\n fi\n if [ -n \"${AGENTTEAMS_NACOS_TOKEN:-}\" ]; then\n DOCKER_ENV=\"${DOCKER_ENV} -e AGENTTEAMS_NACOS_TOKEN=${AGENTTEAMS_NACOS_TOKEN}\"\n fi\n\n # shellcheck disable=SC2086\n ${DOCKER_CMD} run -d \\\n --name \"${CONTAINER_NAME}\" \\\n ${DOCKER_ENV} \\\n --restart unless-stopped \\\n \"${WORKER_IMAGE}\"\n\n log \"\"\n log \"$(msg worker.started \"${WORKER_NAME}\")\"\n log \"$(msg worker.container \"${CONTAINER_NAME}\")\"\n log \"$(msg worker.view_logs \"${CONTAINER_NAME}\")\"\n}\n\n# ============================================================\n# Main\n# ============================================================\n\n# ============================================================\n# LLM API connectivity test\n# ============================================================\n\ntest_llm_connectivity() {\n local base_url=\"$1\"\n local api_key=\"$2\"\n local model=\"$3\"\n local hint=\"${4:-}\" # optional: extra hint shown on failure\n if ! command -v curl >/dev/null 2>&1; then\n echo -e \"\\033[33m$(msg llm.openai.test.no_curl)\\033[0m\"\n return\n fi\n log \"$(msg llm.openai.test.testing)\"\n local _body _http_code _tmpfile\n _tmpfile=$(mktemp)\n _http_code=$(curl -s -o \"${_tmpfile}\" -w \"%{http_code}\" \\\n -X POST \"${base_url%/}/chat/completions\" \\\n -H \"Authorization: Bearer ${api_key}\" \\\n -H \"Content-Type: application/json\" \\\n -H \"User-Agent: AgentTeams/${AGENTTEAMS_VERSION:-latest}\" \\\n --max-time 30 \\\n -d \"{\\\"model\\\":\\\"${model}\\\",\\\"messages\\\":[{\\\"role\\\":\\\"user\\\",\\\"content\\\":\\\"hi\\\"}]}\" \\\n 2>/dev/null)\n _body=$(cat \"${_tmpfile}\")\n rm -f \"${_tmpfile}\"\n if [ \"${_http_code}\" = \"200\" ] || [ \"${_http_code}\" = \"201\" ]; then\n log \"$(msg llm.openai.test.ok)\"\n else\n echo -e \"\\033[33m$(msg llm.openai.test.fail \"${_http_code}\" \"${_body}\")\\033[0m\"\n if [ -n \"${hint}\" ]; then\n echo -e \"\\033[33m${hint}\\033[0m\"\n fi\n if [ \"${AGENTTEAMS_NON_INTERACTIVE}\" != \"1\" ]; then\n local _confirm\n read -e -p \"$(msg llm.openai.test.confirm)\" _confirm\n if [ \"${_confirm}\" = \"b\" ]; then\n STEP_RESULT=\"back\"\n return 1\n fi\n if [ \"${_confirm}\" != \"y\" ] && [ \"${_confirm}\" != \"Y\" ]; then\n log \"$(msg llm.openai.test.aborted)\"\n exit 1\n fi\n fi\n fi\n}\n\ntest_embedding_connectivity() {\n local base_url=\"$1\"\n local api_key=\"$2\"\n local model=\"$3\"\n if ! command -v curl >/dev/null 2>&1; then\n return 0\n fi\n log \"$(msg llm.embedding.test.testing)\"\n local _body _http_code _tmpfile\n _tmpfile=$(mktemp)\n _http_code=$(curl -s -o \"${_tmpfile}\" -w \"%{http_code}\" \\\n -X POST \"${base_url%/}/embeddings\" \\\n -H \"Authorization: Bearer ${api_key}\" \\\n -H \"Content-Type: application/json\" \\\n -H \"User-Agent: AgentTeams/${AGENTTEAMS_VERSION:-latest}\" \\\n --max-time 30 \\\n -d \"{\\\"model\\\":\\\"${model}\\\",\\\"input\\\":\\\"test\\\"}\" \\\n 2>/dev/null)\n _body=$(cat \"${_tmpfile}\")\n rm -f \"${_tmpfile}\"\n if [ \"${_http_code}\" = \"200\" ] || [ \"${_http_code}\" = \"201\" ]; then\n log \"$(msg llm.embedding.test.ok)\"\n return 0\n else\n echo -e \"\\033[33m$(msg llm.embedding.test.fail \"${_http_code}\" \"${_body}\")\\033[0m\"\n return 1\n fi\n}\n\n# ============================================================\n# Uninstall\n# ============================================================\n\nuninstall_agentteams() {\n log \"$(msg uninstall.title)\"\n\n # Safely disable and remove the dedicated AgentTeams Podman autostart service\n if [ \"${DOCKER_CMD}\" = \"podman\" ] && command -v systemctl >/dev/null 2>&1; then\n local _systemctl_cmd=\"systemctl\"\n local _service_dir=\"/etc/systemd/system\"\n\n if [ \"$(id -u)\" != \"0\" ]; then\n _systemctl_cmd=\"systemctl --user\"\n _service_dir=\"${HOME}/.config/systemd/user\"\n fi\n\n if [ -f \"${_service_dir}/agentteams-podman-restart.service\" ]; then\n log \"Disabling and removing dedicated AgentTeams Podman autostart service...\"\n ${_systemctl_cmd} disable --now agentteams-podman-restart.service 2>/dev/null || true\n rm -f \"${_service_dir}/agentteams-podman-restart.service\"\n ${_systemctl_cmd} daemon-reload 2>/dev/null || true\n fi\n # Note: Native podman-restart.service is strictly untouched per maintainer review.\n fi\n\n # Capture manager image before removing (needed for workspace cleanup on Linux)\n local manager_image=\"\"\n manager_image=$(${DOCKER_CMD} inspect agentteams-manager --format '{{.Config.Image}}' 2>/dev/null || true)\n\n # Stop and remove manager\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -q \"^agentteams-manager$\"; then\n log \"$(msg uninstall.stopping_manager)\"\n ${DOCKER_CMD} stop agentteams-manager >/dev/null 2>&1 || true\n ${DOCKER_CMD} rm agentteams-manager >/dev/null 2>&1 || true\n fi\n\n # Stop and remove workers\n local workers\n workers=$(${DOCKER_CMD} ps -a --filter \"name=agentteams-worker-\" --format '{{.Names}}' 2>/dev/null || true)\n if [ -n \"${workers}\" ]; then\n log \"$(msg uninstall.stopping_workers)\"\n echo \"${workers}\" | while read -r w; do\n ${DOCKER_CMD} rm -f \"${w}\" >/dev/null 2>&1 || true\n log \"$(msg uninstall.removed \"${w}\")\"\n done\n fi\n\n # Stop and remove the dashboard container (installed via step_dashboard)\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -q \"^agentteams-dashboard$\"; then\n log \"Stopping and removing agentteams-dashboard...\"\n ${DOCKER_CMD} stop agentteams-dashboard >/dev/null 2>&1 || true\n ${DOCKER_CMD} rm agentteams-dashboard >/dev/null 2>&1 || true\n fi\n if ${DOCKER_CMD} volume inspect agentteams-dashboard-data >/dev/null 2>&1; then\n log \"Removing dashboard data volume: agentteams-dashboard-data\"\n ${DOCKER_CMD} volume rm agentteams-dashboard-data >/dev/null 2>&1 || true\n fi\n\n # Stop and remove docker-proxy (legacy ≤ v1.0.x; current arch uses\n # agentteams-controller for the same role)\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -q \"^agentteams-docker-proxy$\"; then\n log \"$(msg uninstall.removing_proxy)\"\n ${DOCKER_CMD} stop agentteams-docker-proxy >/dev/null 2>&1 || true\n ${DOCKER_CMD} rm agentteams-docker-proxy >/dev/null 2>&1 || true\n fi\n\n # Stop and remove the embedded controller container. MUST happen\n # before the `docker volume rm agentteams-data` step below — in embedded\n # mode agentteams-controller mounts agentteams-data at /data (Tuwunel DB,\n # MinIO state, Higress state, all the room messages), and `volume\n # rm` against an in-use volume fails silently because of the trailing\n # `|| true`. Skipping this used to leave room/message history behind\n # across \"uninstall + reinstall\" cycles. See PR #692.\n if ${DOCKER_CMD} ps -a --format '{{.Names}}' 2>/dev/null | grep -q \"^agentteams-controller$\"; then\n log \"$(msg uninstall.stopping_controller)\"\n ${DOCKER_CMD} stop agentteams-controller >/dev/null 2>&1 || true\n ${DOCKER_CMD} rm agentteams-controller >/dev/null 2>&1 || true\n fi\n\n # Read env file for data/workspace info before removing\n local env_file=\"${AGENTTEAMS_ENV_FILE:-${HOME}/agentteams-manager.env}\"\n [ ! -f \"${env_file}\" ] && [ -f \"./agentteams-manager.env\" ] && env_file=\"./agentteams-manager.env\"\n\n local data_dir=\"\" workspace_dir=\"\"\n if [ -f \"${env_file}\" ]; then\n data_dir=$(grep '^AGENTTEAMS_DATA_DIR=' \"${env_file}\" 2>/dev/null | cut -d= -f2- || true)\n workspace_dir=$(grep '^AGENTTEAMS_WORKSPACE_DIR=' \"${env_file}\" 2>/dev/null | cut -d= -f2- || true)\n fi\n\n # Remove Docker volume\n local vol=\"${data_dir:-agentteams-data}\"\n if ${DOCKER_CMD} volume inspect \"${vol}\" >/dev/null 2>&1; then\n log \"$(msg uninstall.removing_volume \"${vol}\")\"\n ${DOCKER_CMD} volume rm \"${vol}\" >/dev/null 2>&1 || true\n fi\n\n # Remove Docker network\n if ${DOCKER_CMD} network ls --format '{{.Name}}' 2>/dev/null | grep -q \"^agentteams-net$\"; then\n log \"$(msg uninstall.removing_network)\"\n ${DOCKER_CMD} network rm agentteams-net >/dev/null 2>&1 || true\n fi\n\n # Remove workspace directory\n if [ -n \"${workspace_dir}\" ] && [ -d \"${workspace_dir}\" ]; then\n log \"$(msg uninstall.removing_workspace \"${workspace_dir}\")\"\n rm -rf \"${workspace_dir}\" 2>/dev/null || true\n if [ -d \"${workspace_dir}\" ]; then\n log \"$(msg uninstall.removing_workspace_elevated)\"\n local _parent _base\n _parent=$(dirname \"${workspace_dir}\")\n _base=$(basename \"${workspace_dir}\")\n local _rm_image=\"${manager_image:-busybox}\"\n ${DOCKER_CMD} run --rm --entrypoint sh \\\n -v \"${_parent}:/host-parent\" \\\n \"${_rm_image}\" \\\n -c \"rm -rf /host-parent/${_base}\" 2>/dev/null || true\n fi\n fi\n\n # Remove env file\n if [ -f \"${env_file}\" ]; then\n log \"$(msg uninstall.removing_env \"${env_file}\")\"\n rm -f \"${env_file}\"\n fi\n\n # Remove install log\n if [ -f \"${HOME}/agentteams-install.log\" ]; then\n log \"$(msg uninstall.removing_log \"${HOME}/agentteams-install.log\")\"\n rm -f \"${HOME}/agentteams-install.log\"\n fi\n\n echo \"\"\n log \"$(msg uninstall.done)\"\n}\n\n# ============================================================\n# Check container runtime (docker or podman) and environment\n# ============================================================\ncheck_container_runtime() {\n # Ensure the rootless environment baseline (XDG_RUNTIME_DIR) is ready before engine detection.\n if [ \"$(id -u)\" -ne 0 ] && [ -z \"${XDG_RUNTIME_DIR:-}\" ]; then\n export XDG_RUNTIME_DIR=\"/run/user/$(id -u)\"\n fi\n\n if command -v docker >/dev/null 2>&1; then\n DOCKER_CMD=\"docker\"\n # Check for podman disguised as docker (alias or podman-docker package)\n if docker --version 2>/dev/null | grep -qi \"podman\"; then\n DOCKER_CMD=\"podman\"\n fi\n elif command -v podman >/dev/null 2>&1; then\n DOCKER_CMD=\"podman\"\n else\n echo -e \"\\033[31m[AgentTeams ERROR]\\033[0m $(msg error.docker_not_found)\" >&2\n exit 1\n fi\n\n # Unified health check (Validates runtime responsiveness)\n if ! ${DOCKER_CMD} ps >/dev/null 2>&1; then\n echo -e \"\\033[31m[AgentTeams ERROR]\\033[0m $(msg error.docker_not_running)\" >&2\n exit 1\n fi\n}\n\ncheck_container_runtime\n\ncase \"${1:-}\" in\n manager|\"\")\n install_manager\n ;;\n worker)\n shift\n install_worker \"$@\"\n ;;\n dashboard)\n check_container_runtime\n load_current_params_from_env\n AGENTTEAMS_DASHBOARD=\"${AGENTTEAMS_DASHBOARD:-1}\"\n AGENTTEAMS_DASHBOARD_VERSION=\"${AGENTTEAMS_DASHBOARD_VERSION:-v1.2.0}\"\n AGENTTEAMS_PORT_DASHBOARD=\"${AGENTTEAMS_PORT_DASHBOARD:-13000}\"\n AGENTTEAMS_DASHBOARD_IMAGE=\"${AGENTTEAMS_DASHBOARD_IMAGE:-${AGENTTEAMS_REGISTRY}/agentteams/agentteams-dashboard:${AGENTTEAMS_DASHBOARD_VERSION}}\"\n AGENTTEAMS_USE_EMBEDDED=1\n _start_dashboard\n ;;\n uninstall)\n uninstall_agentteams\n ;;\n *)\n echo \"Usage: $0 [manager|worker [options]|dashboard|uninstall]\"\n echo \"\"\n echo \"Commands:\"\n echo \" manager Interactive Manager installation (default)\"\n echo \" Choose Quick Start (all defaults) or Manual mode\"\n echo \" worker Worker installation (requires --name and connection params)\"\n echo \" dashboard Start/stop agentteams-dashboard (use with AGENTTEAMS_DASHBOARD=0 to stop)\"\n echo \" uninstall Stop and remove Manager + all Worker containers\"\n echo \"\"\n echo \"Quick Start (fastest):\"\n echo \" $0\"\n echo \" # Then select '1' for Quick Start mode\"\n echo \"\"\n echo \"Non-interactive (for automation):\"\n echo \" AGENTTEAMS_NON_INTERACTIVE=1 AGENTTEAMS_LLM_API_KEY=sk-xxx $0\"\n echo \"\"\n echo \"Worker Options:\"\n echo \" --name <name> Worker name (required)\"\n echo \" --fs <url> MinIO endpoint URL (required)\"\n echo \" --fs-key <key> MinIO access key (required)\"\n echo \" --fs-secret <secret> MinIO secret key (required)\"\n echo \" --reset Remove existing Worker container before creating\"\n exit 1\n ;;\nesac\n"}}, "scripts/export-debug-log.py": {"2ea027403398dfa06f3fc86445042d59f4684d71": {"t": 1785674217.2245843, "n": 33260, "h": "611a3c0d9c21b255", "c": "#!/usr/bin/env python3\nfrom __future__ import annotations\n\"\"\"\nExport AgentTeams debug logs: Matrix messages + agent session logs.\n\nUsage:\n # Export last 1 hour (default)\n python scripts/export-debug-log.py --range 1h\n\n # Export last 1 day\n python scripts/export-debug-log.py --range 1d\n\n # Filter by container or room\n python scripts/export-debug-log.py --range 1h --container agentteams-manager --room Worker\n\n # Disable PII redaction\n python scripts/export-debug-log.py --range 1h --no-redact\n\nOutput structure:\n debug-log/20260319-153000/\n ├── summary.txt\n ├── matrix-messages/\n │ └── RoomName_!roomid.jsonl\n ├── agent-sessions/\n ├── agentteams-manager/\n │ └── {session-id}.jsonl\n └── agentteams-worker-xxx/\n └── {session-key}.jsonl\n └── container-logs/\n ├── agentteams-worker-xxx.log\n └── agentteams-worker-xxx.state.json\n\"\"\"\n\nimport argparse\nimport json\nimport os\nimport re\nimport subprocess\nimport sys\nimport time\nimport urllib.parse\nimport urllib.request\nimport urllib.error\nfrom datetime import datetime, timezone\nfrom pathlib import Path\n\n\n# ---------------------------------------------------------------------------\n# PII redaction\n# ---------------------------------------------------------------------------\n\n_PII_PATTERNS: list[tuple[str, re.Pattern]] = [\n (\"ID_CARD\", re.compile(r'\\b[1-9]\\d{5}(?:19|20)\\d{2}(?:0[1-9]|1[0-2])(?:0[1-9]|[12]\\d|3[01])\\d{3}[\\dXx]\\b')),\n (\"PHONE\", re.compile(r'(?<!\\d)(?:\\+?86[-\\s]?)?1[3-9]\\d{9}(?!\\d)')),\n (\"EMAIL\", re.compile(r'\\b[A-Za-z0-9._%+\\-]+@[A-Za-z0-9.\\-]+\\.[A-Za-z]{2,}\\b')),\n (\"BANK_CARD\", re.compile(r'\\b(?:6[0-9]{15,18}|4[0-9]{15}|5[1-5][0-9]{14}|3[47][0-9]{13}|62[0-9]{14,17})\\b')),\n (\"IP\", re.compile(r'\\b(?:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)\\.){3}(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)\\b')),\n (\"ALIYUN_AK\", re.compile(r'\\bLTAI[A-Za-z0-9]{12,30}\\b')),\n (\"ALIYUN_SK\", re.compile(r'(?i)(?:access_?key_?secret|secret_?access_?key)\\s*[:=]\\s*(\\S{20,})')),\n (\"AWS_AK\", re.compile(r'\\b(?:AKIA|ASIA)[A-Z0-9]{16}\\b')),\n (\"OPENAI_KEY\", re.compile(r'\\bsk-[A-Za-z0-9]{20,}\\b')),\n (\"ANTHROPIC_KEY\", re.compile(r'\\bsk-ant-[A-Za-z0-9\\-]{20,}\\b')),\n (\"DASHSCOPE_KEY\", re.compile(r'\\bsk-sp-[A-Za-z0-9]{20,}\\b')),\n (\"DEEPSEEK_KEY\", re.compile(r'\\bsk-[a-f0-9]{32,}\\b')),\n (\"BEARER\", re.compile(r'(?i)(Bearer\\s+)([A-Za-z0-9\\-_.]{20,})')),\n (\"SECRET_KV\", re.compile(\n r'(?i)((?:password|passwd|pwd|secret|token|api_?key|access_?key|secret_?key'\n r'|private_?key|credential|appkey|app_?secret|auth_?token|signing_?key'\n r'|client_?secret|master_?key)\\s*[:=]\\s*)'\n r'(\\S+)'\n )),\n (\"MATRIX_TOKEN\", re.compile(r'\\bsyt_[A-Za-z0-9_\\-]{10,}\\b')),\n (\"HEX_SECRET\", re.compile(r'\\b[A-Fa-f0-9]{32,}\\b')),\n (\"PASSPORT\", re.compile(r'\\b[EeGg]\\d{8}\\b')),\n (\"SSN\", re.compile(r'\\b\\d{3}-\\d{2}-\\d{4}\\b')),\n]\n\n_SECRET_FIELD_PATTERN = re.compile(\n r'(?i)^(?:password|passwd|pwd|secret|token|api[_-]?key'\n r'|access[_-]?key(?:[_-]?secret)?|secret[_-]?access[_-]?key'\n r'|secret[_-]?key|private[_-]?key|credential|app[_-]?key'\n r'|app[_-]?secret|auth[_-]?token|signing[_-]?key'\n r'|client[_-]?secret|master[_-]?key)$'\n)\n\n\ndef redact_pii(text: str) -> str:\n if not text:\n return text\n for name, pattern in _PII_PATTERNS:\n if name in (\"SECRET_KV\", \"ALIYUN_SK\", \"BEARER\"):\n text = pattern.sub(r'\\1****', text)\n else:\n text = pattern.sub('****', text)\n return text\n\n\ndef redact_json_strings(obj):\n if isinstance(obj, str):\n return redact_pii(obj)\n if isinstance(obj, list):\n return [redact_json_strings(v) for v in obj]\n if isinstance(obj, dict):\n redacted = {}\n for key, value in obj.items():\n if isinstance(key, str) and _SECRET_FIELD_PATTERN.fullmatch(key):\n redacted[key] = \"****\"\n else:\n redacted[key] = redact_json_strings(value)\n return redacted\n return obj\n\n\n# ---------------------------------------------------------------------------\n# Common helpers\n# ---------------------------------------------------------------------------\n\ndef parse_range(range_str: str) -> int:\n m = re.fullmatch(r\"(\\d+)\\s*(m|min|h|hr|hour|d|day)s?\", range_str.strip(), re.IGNORECASE)\n if not m:\n raise ValueError(f\"Invalid range format: '{range_str}'. Use e.g. 10m, 1h, 1d\")\n value, unit = int(m.group(1)), m.group(2)[0].lower()\n return value * {\"m\": 60, \"h\": 3600, \"d\": 86400}[unit]\n\n\ndef parse_ts(ts_str: str) -> float:\n if not ts_str:\n return 0\n ts_str = ts_str.replace(\"Z\", \"+00:00\")\n try:\n dt = datetime.fromisoformat(ts_str)\n if dt.tzinfo is None:\n dt = dt.replace(tzinfo=timezone.utc)\n return dt.timestamp()\n except Exception:\n return 0\n\n\ndef sanitize_filename(name: str) -> str:\n return re.sub(r'[^\\w\\-. ]', '_', name).strip()[:80]\n\n\ndef docker_exec(container: str, cmd: str) -> str:\n result = subprocess.run(\n [\"docker\", \"exec\", container, \"sh\", \"-c\", cmd],\n capture_output=True, text=True, timeout=30,\n )\n return result.stdout\n\n\ndef list_agentteams_containers() -> list[str]:\n names: list[str] = []\n for prefix in (\"agentteams-\",):\n result = subprocess.run(\n [\"docker\", \"ps\", \"--format\", \"{{.Names}}\", \"--filter\", f\"name={prefix}\"],\n capture_output=True, text=True, timeout=10,\n )\n names.extend(n.strip() for n in result.stdout.splitlines() if n.strip())\n return list(dict.fromkeys(names))\n\n\n# ---------------------------------------------------------------------------\n# Container diagnostics\n# ---------------------------------------------------------------------------\n\ndef _docker_run(*args: str) -> subprocess.CompletedProcess[str]:\n try:\n return subprocess.run(\n [\"docker\", *args],\n capture_output=True,\n text=True,\n timeout=30,\n )\n except (OSError, subprocess.SubprocessError) as exc:\n return subprocess.CompletedProcess(\n [\"docker\", *args],\n 1,\n \"\",\n f\"{type(exc).__name__}: {exc}\",\n )\n\n\ndef export_container_logs(out_dir: Path, since_epoch: float, redact: bool,\n container_filter: str | None) -> int:\n \"\"\"Export Docker state and logs for all AgentTeams containers.\"\"\"\n result = _docker_run(\"ps\", \"-a\", \"--format\", \"{{.Names}}\", \"--filter\", \"name=agentteams-\")\n containers = [name.strip() for name in result.stdout.splitlines() if name.strip()]\n if container_filter:\n containers = [name for name in containers if container_filter in name]\n if not containers:\n print(\" [containers] No matching AgentTeams containers found\")\n return 0\n\n out_dir.mkdir(parents=True, exist_ok=True)\n since = datetime.fromtimestamp(since_epoch, tz=timezone.utc).isoformat()\n\n for container in containers:\n state_result = _docker_run(\"inspect\", \"--format={{json .State}}\", container)\n image_result = _docker_run(\"inspect\", \"--format={{json .Config.Image}}\", container)\n restart_result = _docker_run(\"inspect\", \"--format={{.RestartCount}}\", container)\n try:\n state = json.loads(state_result.stdout)\n except (json.JSONDecodeError, TypeError):\n state = {\"inspect_error\": state_result.stderr.strip() or state_result.stdout.strip()}\n try:\n image = json.loads(image_result.stdout)\n except (json.JSONDecodeError, TypeError):\n image = image_result.stdout.strip()\n try:\n restart_count = int(restart_result.stdout.strip())\n except ValueError:\n restart_count = None\n\n diagnostic = {\n \"container\": container,\n \"image\": image,\n \"restart_count\": restart_count,\n \"state\": state,\n }\n if redact:\n diagnostic = redact_json_strings(diagnostic)\n filename = sanitize_filename(container)\n (out_dir / f\"{filename}.state.json\").write_text(\n json.dumps(diagnostic, ensure_ascii=False, indent=2) + \"\\n\",\n encoding=\"utf-8\",\n )\n\n logs_result = _docker_run(\"logs\", \"--timestamps\", \"--since\", since, container)\n logs = logs_result.stdout + logs_result.stderr\n if redact:\n logs = redact_pii(logs)\n (out_dir / f\"{filename}.log\").write_text(logs, encoding=\"utf-8\")\n print(f\" {container}: state + {len(logs.splitlines())} log lines\")\n\n return len(containers)\n\n\n# ---------------------------------------------------------------------------\n# Matrix messages export\n# ---------------------------------------------------------------------------\n\ndef load_env_file(path: str) -> dict[str, str]:\n env = {}\n try:\n with open(path) as f:\n for line in f:\n line = line.strip()\n if not line or line.startswith(\"#\"):\n continue\n if \"=\" in line:\n key, _, value = line.partition(\"=\")\n env[key.strip()] = value.strip()\n except FileNotFoundError:\n pass\n return env\n\n\ndef matrix_login(homeserver: str, user: str, password: str) -> str:\n url = f\"{homeserver.rstrip('/')}/_matrix/client/v3/login\"\n payload = json.dumps({\n \"type\": \"m.login.password\",\n \"identifier\": {\"type\": \"m.id.user\", \"user\": user},\n \"password\": password,\n }).encode()\n req = urllib.request.Request(url, data=payload, headers={\"Content-Type\": \"application/json\"})\n with urllib.request.urlopen(req, timeout=15) as resp:\n data = json.loads(resp.read())\n return data[\"access_token\"]\n\n\ndef matrix_api(homeserver: str, token: str, endpoint: str, params: dict | None = None) -> dict:\n url = f\"{homeserver.rstrip('/')}/_matrix/client/v3/{endpoint}\"\n if params:\n url += \"?\" + urllib.parse.urlencode(params)\n req = urllib.request.Request(url, headers={\"Authorization\": f\"Bearer {token}\"})\n try:\n with urllib.request.urlopen(req, timeout=30) as resp:\n return json.loads(resp.read())\n except urllib.error.HTTPError as e:\n body = e.read().decode(errors=\"replace\")\n print(f\" API error {e.code} on {endpoint}: {body}\", file=sys.stderr)\n raise\n\n\ndef fetch_room_messages(homeserver: str, token: str, room_id: str, since_ts: int) -> list[dict]:\n encoded = urllib.parse.quote(room_id)\n messages = []\n from_token = \"\"\n while True:\n params = {\"dir\": \"b\", \"limit\": \"100\"}\n if from_token:\n params[\"from\"] = from_token\n data = matrix_api(homeserver, token, f\"rooms/{encoded}/messages\", params)\n chunk = data.get(\"chunk\", [])\n if not chunk:\n break\n hit_boundary = False\n for event in chunk:\n if event.get(\"origin_server_ts\", 0) < since_ts:\n hit_boundary = True\n break\n messages.append(event)\n if hit_boundary:\n break\n next_token = data.get(\"end\")\n if not next_token or next_token == from_token:\n break\n from_token = next_token\n messages.reverse()\n return messages\n\n\ndef format_event(event: dict, redact: bool) -> dict:\n content = event.get(\"content\", {})\n ts = event.get(\"origin_server_ts\", 0)\n record = {\n \"event_id\": event.get(\"event_id\"),\n \"type\": event.get(\"type\"),\n \"sender\": event.get(\"sender\"),\n \"timestamp\": ts,\n \"time\": datetime.fromtimestamp(ts / 1000, tz=timezone.utc).isoformat(),\n }\n if event.get(\"type\") == \"m.room.message\":\n record[\"msgtype\"] = content.get(\"msgtype\")\n record[\"body\"] = content.get(\"body\")\n if content.get(\"format\"):\n record[\"format\"] = content[\"format\"]\n if content.get(\"url\"):\n record[\"url\"] = content[\"url\"]\n if content.get(\"m.relates_to\"):\n record[\"relates_to\"] = content[\"m.relates_to\"]\n else:\n record[\"content\"] = content\n return redact_json_strings(record) if redact else record\n\n\ndef export_matrix_messages(out_dir: Path, since_epoch: float, redact: bool,\n room_filter: str | None, env_file: str | None,\n homeserver: str, token: str,\n messages_only: bool) -> tuple[int, int]:\n \"\"\"Export Matrix messages. Returns (rooms_exported, message_count).\"\"\"\n env_path = env_file or os.path.expanduser(\"~/agentteams-manager.env\")\n agentteams_env = load_env_file(env_path)\n\n if not homeserver:\n if not agentteams_env:\n print(f\" [matrix] Cannot find {env_path}, skipping Matrix export\")\n return 0, 0\n port = agentteams_env.get(\"AGENTTEAMS_PORT_GATEWAY\", \"18080\")\n homeserver = f\"http://127.0.0.1:{port}\"\n\n if not token:\n # Use Manager token — Manager is in every room (DM, Worker, Project)\n manager_password = agentteams_env.get(\"AGENTTEAMS_MANAGER_PASSWORD\", \"\")\n if manager_password:\n try:\n token = matrix_login(homeserver, \"manager\", manager_password)\n except Exception:\n pass\n\n # Fallback to admin token if Manager login failed\n if not token:\n admin_user = agentteams_env.get(\"AGENTTEAMS_ADMIN_USER\", \"admin\")\n admin_password = agentteams_env.get(\"AGENTTEAMS_ADMIN_PASSWORD\", \"\")\n if not admin_password:\n print(f\" [matrix] No usable credentials found, skipping Matrix export\")\n return 0, 0\n try:\n token = matrix_login(homeserver, admin_user, admin_password)\n except Exception as e:\n print(f\" [matrix] Login failed: {e}, skipping Matrix export\")\n return 0, 0\n\n since_ts = int(since_epoch * 1000)\n out_dir.mkdir(parents=True, exist_ok=True)\n\n try:\n rooms = matrix_api(homeserver, token, \"joined_rooms\").get(\"joined_rooms\", [])\n except Exception as e:\n print(f\" [matrix] Failed to list rooms: {e}\")\n return 0, 0\n\n total_messages = 0\n total_rooms = 0\n\n for room_id in rooms:\n encoded = urllib.parse.quote(room_id)\n try:\n room_name = matrix_api(homeserver, token, f\"rooms/{encoded}/state/m.room.name\").get(\"name\", \"\")\n except Exception:\n room_name = \"\"\n\n if room_filter:\n if room_filter not in room_id and room_filter not in (room_name or \"\"):\n continue\n\n display = f\"{room_name} ({room_id})\" if room_name else room_id\n print(f\" {display} ... \", end=\"\", flush=True)\n\n messages = fetch_room_messages(homeserver, token, room_id, since_ts)\n if messages_only:\n messages = [e for e in messages if e.get(\"type\") == \"m.room.message\"]\n\n if not messages:\n print(\"0 messages, skipped\")\n continue\n\n name_part = sanitize_filename(room_name) if room_name else \"\"\n id_part = sanitize_filename(room_id)\n filename = f\"{name_part}_{id_part}.jsonl\" if name_part else f\"{id_part}.jsonl\"\n\n with open(out_dir / filename, \"w\", encoding=\"utf-8\") as f:\n for event in messages:\n record = format_event(event, redact=redact)\n f.write(json.dumps(record, ensure_ascii=False) + \"\\n\")\n\n print(f\"{len(messages)} messages -> {filename}\")\n total_messages += len(messages)\n total_rooms += 1\n\n return total_rooms, total_messages\n\n\n# ---------------------------------------------------------------------------\n# Agent sessions export (OpenClaw + CoPaw + Hermes)\n# ---------------------------------------------------------------------------\n\ndef detect_runtime(container: str) -> tuple[str, str]:\n \"\"\"Probe known absolute session-dir locations across runtimes & container types.\n\n Returns (runtime_name, absolute_sessions_dir). Empty tuple if none found.\n\n Layout reference (verified against worker/scripts/worker-entrypoint.sh,\n copaw/scripts/copaw-worker-entrypoint.sh, copaw/AGENTS.md and\n tests/lib/agent-metrics.sh):\n\n Manager (any runtime, HOME=/root/manager-workspace):\n openclaw -> /root/manager-workspace/.openclaw/agents/main/sessions\n hermes -> /root/manager-workspace/.hermes/sessions\n copaw -> /root/manager-workspace/.copaw/workspaces/default/sessions\n\n OpenClaw / Hermes Worker (HOME=/root/agentteams-fs/agents/<name>):\n openclaw -> /root/agentteams-fs/agents/<name>/.openclaw/agents/main/sessions\n hermes -> /root/agentteams-fs/agents/<name>/.hermes/sessions\n\n CoPaw Worker (HOME=/root/.agentteams-worker/<name>, also reachable via\n /root/agentteams-fs symlink that points to that same dir):\n copaw -> /root/.agentteams-worker/<name>/.copaw/workspaces/default/sessions\n copaw -> /root/agentteams-fs/.copaw/workspaces/default/sessions (alt)\n \"\"\"\n candidates: list[tuple[str, str]] = [\n (\"openclaw\", \"/root/manager-workspace/.openclaw/agents/main/sessions\"),\n (\"hermes\", \"/root/manager-workspace/.hermes/sessions\"),\n (\"copaw\", \"/root/manager-workspace/.copaw/workspaces/default/sessions\"),\n ]\n\n worker_name = docker_exec(container, \"echo $AGENTTEAMS_WORKER_NAME\").strip()\n if worker_name:\n candidates.extend([\n (\"openclaw\", f\"/root/agentteams-fs/agents/{worker_name}/.openclaw/agents/main/sessions\"),\n (\"hermes\", f\"/root/agentteams-fs/agents/{worker_name}/.hermes/sessions\"),\n (\"copaw\", f\"/root/.agentteams-worker/{worker_name}/.copaw/workspaces/default/sessions\"),\n (\"copaw\", \"/root/agentteams-fs/.copaw/workspaces/default/sessions\"),\n ])\n\n for runtime, path in candidates:\n if docker_exec(container, f\"test -d '{path}' && echo yes || echo no\").strip() == \"yes\":\n return runtime, path\n\n # Last-resort: scan from / for any known session dir layout.\n found = docker_exec(\n container,\n \"find / -maxdepth 7 \\\\( \"\n \"-path '*/.openclaw/agents/main/sessions' \"\n \"-o -path '*/.hermes/sessions' \"\n \"-o -path '*/.copaw/workspaces/default/sessions' \"\n \"\\\\) -type d 2>/dev/null | head -1\",\n ).strip()\n if found:\n if \"/.openclaw/\" in found:\n return \"openclaw\", found\n if \"/.hermes/\" in found:\n return \"hermes\", found\n return \"copaw\", found\n\n return \"\", \"\"\n\n\ndef export_openclaw_sessions(container: str, sessions_dir: str, since_epoch: float,\n out_dir: Path, redact: bool) -> tuple[int, int]:\n ls_output = docker_exec(container, f\"ls '{sessions_dir}'/*.jsonl 2>/dev/null\").strip()\n if not ls_output:\n return 0, 0\n\n total_sessions = 0\n total_events = 0\n\n for session_path in [f.strip() for f in ls_output.splitlines() if f.strip()]:\n filename = os.path.basename(session_path)\n\n first_line = docker_exec(container, f\"head -1 '{session_path}'\").strip()\n if not first_line:\n continue\n try:\n header = json.loads(first_line)\n except json.JSONDecodeError:\n continue\n\n last_line = docker_exec(container, f\"tail -1 '{session_path}'\").strip()\n try:\n last_ts = parse_ts(json.loads(last_line).get(\"timestamp\", \"\"))\n except Exception:\n last_ts = parse_ts(header.get(\"timestamp\", \"\"))\n\n if last_ts < since_epoch and last_ts > 0:\n continue\n\n raw = docker_exec(container, f\"cat '{session_path}'\")\n if not raw.strip():\n continue\n\n output_lines = []\n for line in raw.splitlines():\n line = line.strip()\n if not line:\n continue\n try:\n event = json.loads(line)\n except json.JSONDecodeError:\n continue\n event_ts = parse_ts(event.get(\"timestamp\", \"\"))\n if event.get(\"type\") != \"session\" and event_ts < since_epoch and event_ts > 0:\n continue\n if redact:\n event = redact_json_strings(event)\n output_lines.append(json.dumps(event, ensure_ascii=False))\n\n if len(output_lines) <= 1:\n continue\n\n with open(out_dir / filename, \"w\", encoding=\"utf-8\") as f:\n f.write(\"\\n\".join(output_lines) + \"\\n\")\n\n event_count = len(output_lines) - 1\n print(f\" {container}/{filename} (openclaw): {event_count} events\")\n total_sessions += 1\n total_events += event_count\n\n # sessions.json index\n try:\n raw = docker_exec(container, f\"cat '{sessions_dir}/sessions.json'\")\n if raw.strip():\n index = json.loads(raw)\n if redact:\n index = redact_json_strings(index)\n with open(out_dir / \"sessions.json\", \"w\", encoding=\"utf-8\") as f:\n json.dump(index, f, ensure_ascii=False, indent=2)\n except Exception:\n pass\n\n return total_sessions, total_events\n\n\ndef export_copaw_sessions(container: str, sessions_dir: str, since_epoch: float,\n out_dir: Path, redact: bool) -> tuple[int, int]:\n ls_output = docker_exec(container, f\"find '{sessions_dir}' -name '*.json' -type f 2>/dev/null\").strip()\n if not ls_output:\n return 0, 0\n\n total_sessions = 0\n total_events = 0\n\n for session_path in [f.strip() for f in ls_output.splitlines() if f.strip()]:\n raw = docker_exec(container, f\"cat '{session_path}'\")\n if not raw.strip():\n continue\n try:\n data = json.loads(raw)\n except json.JSONDecodeError:\n continue\n\n agent = data.get(\"agent\", {})\n memory = agent.get(\"memory\", {})\n content = memory.get(\"content\", [])\n if not content:\n continue\n\n basename = os.path.basename(session_path).replace(\".json\", \"\")\n header = {\n \"type\": \"session\",\n \"runtime\": \"copaw\",\n \"agent_name\": agent.get(\"name\", \"\"),\n \"session_key\": basename,\n \"compressed_summary\": memory.get(\"_compressed_summary\", \"\"),\n }\n\n messages_in_range = []\n for turn_idx, turn in enumerate(content):\n if not isinstance(turn, list):\n continue\n for msg in turn:\n if not isinstance(msg, dict):\n continue\n msg_ts = parse_ts(msg.get(\"timestamp\", \"\"))\n if msg_ts >= since_epoch or msg_ts == 0:\n event = {\n \"type\": \"message\",\n \"turn\": turn_idx,\n \"id\": msg.get(\"id\", \"\"),\n \"role\": msg.get(\"role\", \"\"),\n \"name\": msg.get(\"name\", \"\"),\n \"timestamp\": msg.get(\"timestamp\", \"\"),\n \"content\": msg.get(\"content\", []),\n }\n if msg.get(\"metadata\"):\n event[\"metadata\"] = msg[\"metadata\"]\n messages_in_range.append(event)\n\n if not messages_in_range:\n continue\n\n output_lines = []\n if redact:\n header = redact_json_strings(header)\n output_lines.append(json.dumps(header, ensure_ascii=False))\n for event in messages_in_range:\n if redact:\n event = redact_json_strings(event)\n output_lines.append(json.dumps(event, ensure_ascii=False))\n\n out_filename = basename + \".jsonl\"\n with open(out_dir / out_filename, \"w\", encoding=\"utf-8\") as f:\n f.write(\"\\n\".join(output_lines) + \"\\n\")\n\n event_count = len(output_lines) - 1\n print(f\" {container}/{out_filename} (copaw): {event_count} events\")\n total_sessions += 1\n total_events += event_count\n\n return total_sessions, total_events\n\n\ndef export_hermes_sessions(container: str, sessions_dir: str, since_epoch: float,\n out_dir: Path, redact: bool) -> tuple[int, int]:\n ls_output = docker_exec(container, f\"ls '{sessions_dir}'/*.jsonl 2>/dev/null\").strip()\n if not ls_output:\n return 0, 0\n\n total_sessions = 0\n total_events = 0\n\n for session_path in [f.strip() for f in ls_output.splitlines() if f.strip()]:\n raw = docker_exec(container, f\"cat '{session_path}'\")\n if not raw.strip():\n continue\n\n output_lines = []\n last_ts = 0.0\n for line in raw.splitlines():\n line = line.strip()\n if not line:\n continue\n try:\n event = json.loads(line)\n except json.JSONDecodeError:\n continue\n\n event_ts = parse_ts(event.get(\"timestamp\", \"\"))\n if event_ts:\n last_ts = max(last_ts, event_ts)\n\n role = event.get(\"role\", \"\")\n if role != \"session_meta\" and event_ts < since_epoch and event_ts > 0:\n continue\n\n if redact:\n event = redact_json_strings(event)\n output_lines.append(json.dumps(event, ensure_ascii=False))\n\n if not output_lines:\n continue\n\n if last_ts < since_epoch and last_ts > 0:\n continue\n\n filename = os.path.basename(session_path)\n with open(out_dir / filename, \"w\", encoding=\"utf-8\") as f:\n f.write(\"\\n\".join(output_lines) + \"\\n\")\n\n event_count = len(output_lines) - 1 if output_lines and '\"role\": \"session_meta\"' in output_lines[0] else len(output_lines)\n print(f\" {container}/{filename} (hermes): {event_count} events\")\n total_sessions += 1\n total_events += max(event_count, 0)\n\n hermes_home = str(Path(sessions_dir).parent)\n state_db = f\"{hermes_home}/state.db\"\n has_state_db = docker_exec(container, f\"test -f '{state_db}' && echo yes || echo no\").strip() == \"yes\"\n if has_state_db:\n raw = docker_exec(container, f\"python3 - <<'PY'\\nimport json, sqlite3\\nconn = sqlite3.connect('{state_db}')\\nconn.row_factory = sqlite3.Row\\nrows = conn.execute('SELECT * FROM sessions ORDER BY started_at DESC LIMIT 200').fetchall()\\nprint(json.dumps([dict(r) for r in rows], ensure_ascii=False, indent=2))\\nPY\")\n if raw.strip():\n try:\n data = json.loads(raw)\n if redact:\n data = redact_json_strings(data)\n with open(out_dir / \"sessions-db.json\", \"w\", encoding=\"utf-8\") as f:\n json.dump(data, f, ensure_ascii=False, indent=2)\n except json.JSONDecodeError:\n pass\n\n logs_dir = f\"{hermes_home}/logs\"\n has_logs_dir = docker_exec(container, f\"test -d '{logs_dir}' && echo yes || echo no\").strip() == \"yes\"\n if has_logs_dir:\n for log_name in (\"agent.log\", \"errors.log\", \"gateway.log\"):\n raw = docker_exec(container, f\"test -f '{logs_dir}/{log_name}' && cat '{logs_dir}/{log_name}' || true\")\n if raw.strip():\n content = redact_pii(raw) if redact else raw\n (out_dir / log_name).write_text(content, encoding=\"utf-8\")\n\n return total_sessions, total_events\n\n\ndef export_agent_sessions(out_dir: Path, since_epoch: float, redact: bool,\n container_filter: str | None) -> tuple[int, int]:\n \"\"\"Export agent sessions from all containers. Returns (session_count, event_count).\"\"\"\n containers = list_agentteams_containers()\n if container_filter:\n containers = [c for c in containers if container_filter in c]\n\n if not containers:\n print(\" [sessions] No matching AgentTeams containers found\")\n return 0, 0\n\n total_sessions = 0\n total_events = 0\n\n for container in containers:\n runtime, sessions_dir = detect_runtime(container)\n if not runtime:\n print(f\" {container}: no sessions directory, skipped\")\n continue\n\n container_dir = out_dir / container\n container_dir.mkdir(parents=True, exist_ok=True)\n\n if runtime == \"openclaw\":\n s, e = export_openclaw_sessions(container, sessions_dir, since_epoch,\n container_dir, redact)\n elif runtime == \"hermes\":\n s, e = export_hermes_sessions(container, sessions_dir, since_epoch,\n container_dir, redact)\n else:\n s, e = export_copaw_sessions(container, sessions_dir, since_epoch,\n container_dir, redact)\n\n if s == 0:\n if not any(container_dir.iterdir()):\n container_dir.rmdir()\n print(f\" {container} ({runtime}): no sessions in range, skipped\")\n else:\n print(f\" {container} ({runtime}): exported auxiliary debug artifacts without session events\")\n else:\n total_sessions += s\n total_events += e\n\n return total_sessions, total_events\n\n\n# ---------------------------------------------------------------------------\n# Main\n# ---------------------------------------------------------------------------\n\ndef main():\n parser = argparse.ArgumentParser(\n description=\"Export AgentTeams debug logs (Matrix messages + agent sessions)\"\n )\n parser.add_argument(\"--range\", \"-r\", required=True, dest=\"time_range\",\n help=\"Time range to export, e.g. 10m, 1h, 1d\")\n parser.add_argument(\"--container\", \"-c\", default=None,\n help=\"Only export sessions from containers matching this substring\")\n parser.add_argument(\"--room\", default=None,\n help=\"Only export Matrix rooms matching this substring\")\n parser.add_argument(\"--homeserver\", \"-s\", default=\"\",\n help=\"Matrix homeserver URL (auto-detected from env file)\")\n parser.add_argument(\"--token\", \"-t\", default=\"\",\n help=\"Matrix access token (auto-detected from env file)\")\n parser.add_argument(\"--env-file\", default=None,\n help=\"Path to agentteams-manager.env (default: ~/agentteams-manager.env)\")\n parser.add_argument(\"--messages-only\", action=\"store_true\",\n help=\"Only export m.room.message events (skip state events)\")\n parser.add_argument(\"--no-redact\", action=\"store_true\",\n help=\"Disable PII redaction\")\n args = parser.parse_args()\n\n range_seconds = parse_range(args.time_range)\n since_epoch = time.time() - range_seconds\n since_human = datetime.fromtimestamp(since_epoch, tz=timezone.utc).isoformat()\n now_str = datetime.now().strftime(\"%Y%m%d-%H%M%S\")\n\n run_dir = Path(\"debug-log\") / now_str\n run_dir.mkdir(parents=True, exist_ok=True)\n redact = not args.no_redact\n\n print(f\"AgentTeams Debug Log Export\")\n print(f\" Range: last {args.time_range} (since {since_human})\")\n print(f\" Output: {run_dir.resolve()}\")\n print(f\" PII redaction: {'on' if redact else 'off'}\")\n print()\n\n # --- Matrix messages ---\n print(\"=== Matrix Messages ===\")\n matrix_dir = run_dir / \"matrix-messages\"\n rooms, messages = export_matrix_messages(\n matrix_dir, since_epoch, redact,\n room_filter=args.room, env_file=args.env_file,\n homeserver=args.homeserver, token=args.token,\n messages_only=args.messages_only,\n )\n if rooms == 0 and matrix_dir.exists() and not any(matrix_dir.iterdir()):\n matrix_dir.rmdir()\n print()\n\n # --- Agent sessions ---\n print(\"=== Agent Sessions ===\")\n sessions_dir = run_dir / \"agent-sessions\"\n sessions, events = export_agent_sessions(\n sessions_dir, since_epoch, redact,\n container_filter=args.container,\n )\n if sessions == 0 and sessions_dir.exists() and not any(sessions_dir.iterdir()):\n sessions_dir.rmdir()\n print()\n\n # --- Container diagnostics ---\n print(\"=== Container Diagnostics ===\")\n container_logs_dir = run_dir / \"container-logs\"\n containers = export_container_logs(\n container_logs_dir, since_epoch, redact,\n container_filter=args.container,\n )\n if containers == 0 and container_logs_dir.exists() and not any(container_logs_dir.iterdir()):\n container_logs_dir.rmdir()\n print()\n\n # --- Summary ---\n summary = (\n f\"AgentTeams Debug Log\\n\"\n f\"Exported at: {now_str}\\n\"\n f\"Range: last {args.time_range} (since {since_human})\\n\"\n f\"PII redaction: {'on' if redact else 'off'}\\n\"\n f\"\\n\"\n f\"Matrix messages: {messages} messages from {rooms} rooms\\n\"\n f\"Agent sessions: {events} events from {sessions} sessions\\n\"\n f\"Container diagnostics: {containers} containers\\n\"\n )\n (run_dir / \"summary.txt\").write_text(summary)\n\n print(\n f\"Done. {messages} messages from {rooms} rooms, \"\n f\"{events} events from {sessions} sessions, \"\n f\"{containers} container diagnostics\"\n )\n print(f\"Output: {run_dir.resolve()}\")\n\n\nif __name__ == \"__main__\":\n main()\n"}, "124f06d13fd6bb4c2054128dcff26bcd9f9c8dbf": {"t": 1785747462.1187901, "n": 33260, "h": "611a3c0d9c21b255", "c": "#!/usr/bin/env python3\nfrom __future__ import annotations\n\"\"\"\nExport AgentTeams debug logs: Matrix messages + agent session logs.\n\nUsage:\n # Export last 1 hour (default)\n python scripts/export-debug-log.py --range 1h\n\n # Export last 1 day\n python scripts/export-debug-log.py --range 1d\n\n # Filter by container or room\n python scripts/export-debug-log.py --range 1h --container agentteams-manager --room Worker\n\n # Disable PII redaction\n python scripts/export-debug-log.py --range 1h --no-redact\n\nOutput structure:\n debug-log/20260319-153000/\n ├── summary.txt\n ├── matrix-messages/\n │ └── RoomName_!roomid.jsonl\n ├── agent-sessions/\n ├── agentteams-manager/\n │ └── {session-id}.jsonl\n └── agentteams-worker-xxx/\n └── {session-key}.jsonl\n └── container-logs/\n ├── agentteams-worker-xxx.log\n └── agentteams-worker-xxx.state.json\n\"\"\"\n\nimport argparse\nimport json\nimport os\nimport re\nimport subprocess\nimport sys\nimport time\nimport urllib.parse\nimport urllib.request\nimport urllib.error\nfrom datetime import datetime, timezone\nfrom pathlib import Path\n\n\n# ---------------------------------------------------------------------------\n# PII redaction\n# ---------------------------------------------------------------------------\n\n_PII_PATTERNS: list[tuple[str, re.Pattern]] = [\n (\"ID_CARD\", re.compile(r'\\b[1-9]\\d{5}(?:19|20)\\d{2}(?:0[1-9]|1[0-2])(?:0[1-9]|[12]\\d|3[01])\\d{3}[\\dXx]\\b')),\n (\"PHONE\", re.compile(r'(?<!\\d)(?:\\+?86[-\\s]?)?1[3-9]\\d{9}(?!\\d)')),\n (\"EMAIL\", re.compile(r'\\b[A-Za-z0-9._%+\\-]+@[A-Za-z0-9.\\-]+\\.[A-Za-z]{2,}\\b')),\n (\"BANK_CARD\", re.compile(r'\\b(?:6[0-9]{15,18}|4[0-9]{15}|5[1-5][0-9]{14}|3[47][0-9]{13}|62[0-9]{14,17})\\b')),\n (\"IP\", re.compile(r'\\b(?:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)\\.){3}(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)\\b')),\n (\"ALIYUN_AK\", re.compile(r'\\bLTAI[A-Za-z0-9]{12,30}\\b')),\n (\"ALIYUN_SK\", re.compile(r'(?i)(?:access_?key_?secret|secret_?access_?key)\\s*[:=]\\s*(\\S{20,})')),\n (\"AWS_AK\", re.compile(r'\\b(?:AKIA|ASIA)[A-Z0-9]{16}\\b')),\n (\"OPENAI_KEY\", re.compile(r'\\bsk-[A-Za-z0-9]{20,}\\b')),\n (\"ANTHROPIC_KEY\", re.compile(r'\\bsk-ant-[A-Za-z0-9\\-]{20,}\\b')),\n (\"DASHSCOPE_KEY\", re.compile(r'\\bsk-sp-[A-Za-z0-9]{20,}\\b')),\n (\"DEEPSEEK_KEY\", re.compile(r'\\bsk-[a-f0-9]{32,}\\b')),\n (\"BEARER\", re.compile(r'(?i)(Bearer\\s+)([A-Za-z0-9\\-_.]{20,})')),\n (\"SECRET_KV\", re.compile(\n r'(?i)((?:password|passwd|pwd|secret|token|api_?key|access_?key|secret_?key'\n r'|private_?key|credential|appkey|app_?secret|auth_?token|signing_?key'\n r'|client_?secret|master_?key)\\s*[:=]\\s*)'\n r'(\\S+)'\n )),\n (\"MATRIX_TOKEN\", re.compile(r'\\bsyt_[A-Za-z0-9_\\-]{10,}\\b')),\n (\"HEX_SECRET\", re.compile(r'\\b[A-Fa-f0-9]{32,}\\b')),\n (\"PASSPORT\", re.compile(r'\\b[EeGg]\\d{8}\\b')),\n (\"SSN\", re.compile(r'\\b\\d{3}-\\d{2}-\\d{4}\\b')),\n]\n\n_SECRET_FIELD_PATTERN = re.compile(\n r'(?i)^(?:password|passwd|pwd|secret|token|api[_-]?key'\n r'|access[_-]?key(?:[_-]?secret)?|secret[_-]?access[_-]?key'\n r'|secret[_-]?key|private[_-]?key|credential|app[_-]?key'\n r'|app[_-]?secret|auth[_-]?token|signing[_-]?key'\n r'|client[_-]?secret|master[_-]?key)$'\n)\n\n\ndef redact_pii(text: str) -> str:\n if not text:\n return text\n for name, pattern in _PII_PATTERNS:\n if name in (\"SECRET_KV\", \"ALIYUN_SK\", \"BEARER\"):\n text = pattern.sub(r'\\1****', text)\n else:\n text = pattern.sub('****', text)\n return text\n\n\ndef redact_json_strings(obj):\n if isinstance(obj, str):\n return redact_pii(obj)\n if isinstance(obj, list):\n return [redact_json_strings(v) for v in obj]\n if isinstance(obj, dict):\n redacted = {}\n for key, value in obj.items():\n if isinstance(key, str) and _SECRET_FIELD_PATTERN.fullmatch(key):\n redacted[key] = \"****\"\n else:\n redacted[key] = redact_json_strings(value)\n return redacted\n return obj\n\n\n# ---------------------------------------------------------------------------\n# Common helpers\n# ---------------------------------------------------------------------------\n\ndef parse_range(range_str: str) -> int:\n m = re.fullmatch(r\"(\\d+)\\s*(m|min|h|hr|hour|d|day)s?\", range_str.strip(), re.IGNORECASE)\n if not m:\n raise ValueError(f\"Invalid range format: '{range_str}'. Use e.g. 10m, 1h, 1d\")\n value, unit = int(m.group(1)), m.group(2)[0].lower()\n return value * {\"m\": 60, \"h\": 3600, \"d\": 86400}[unit]\n\n\ndef parse_ts(ts_str: str) -> float:\n if not ts_str:\n return 0\n ts_str = ts_str.replace(\"Z\", \"+00:00\")\n try:\n dt = datetime.fromisoformat(ts_str)\n if dt.tzinfo is None:\n dt = dt.replace(tzinfo=timezone.utc)\n return dt.timestamp()\n except Exception:\n return 0\n\n\ndef sanitize_filename(name: str) -> str:\n return re.sub(r'[^\\w\\-. ]', '_', name).strip()[:80]\n\n\ndef docker_exec(container: str, cmd: str) -> str:\n result = subprocess.run(\n [\"docker\", \"exec\", container, \"sh\", \"-c\", cmd],\n capture_output=True, text=True, timeout=30,\n )\n return result.stdout\n\n\ndef list_agentteams_containers() -> list[str]:\n names: list[str] = []\n for prefix in (\"agentteams-\",):\n result = subprocess.run(\n [\"docker\", \"ps\", \"--format\", \"{{.Names}}\", \"--filter\", f\"name={prefix}\"],\n capture_output=True, text=True, timeout=10,\n )\n names.extend(n.strip() for n in result.stdout.splitlines() if n.strip())\n return list(dict.fromkeys(names))\n\n\n# ---------------------------------------------------------------------------\n# Container diagnostics\n# ---------------------------------------------------------------------------\n\ndef _docker_run(*args: str) -> subprocess.CompletedProcess[str]:\n try:\n return subprocess.run(\n [\"docker\", *args],\n capture_output=True,\n text=True,\n timeout=30,\n )\n except (OSError, subprocess.SubprocessError) as exc:\n return subprocess.CompletedProcess(\n [\"docker\", *args],\n 1,\n \"\",\n f\"{type(exc).__name__}: {exc}\",\n )\n\n\ndef export_container_logs(out_dir: Path, since_epoch: float, redact: bool,\n container_filter: str | None) -> int:\n \"\"\"Export Docker state and logs for all AgentTeams containers.\"\"\"\n result = _docker_run(\"ps\", \"-a\", \"--format\", \"{{.Names}}\", \"--filter\", \"name=agentteams-\")\n containers = [name.strip() for name in result.stdout.splitlines() if name.strip()]\n if container_filter:\n containers = [name for name in containers if container_filter in name]\n if not containers:\n print(\" [containers] No matching AgentTeams containers found\")\n return 0\n\n out_dir.mkdir(parents=True, exist_ok=True)\n since = datetime.fromtimestamp(since_epoch, tz=timezone.utc).isoformat()\n\n for container in containers:\n state_result = _docker_run(\"inspect\", \"--format={{json .State}}\", container)\n image_result = _docker_run(\"inspect\", \"--format={{json .Config.Image}}\", container)\n restart_result = _docker_run(\"inspect\", \"--format={{.RestartCount}}\", container)\n try:\n state = json.loads(state_result.stdout)\n except (json.JSONDecodeError, TypeError):\n state = {\"inspect_error\": state_result.stderr.strip() or state_result.stdout.strip()}\n try:\n image = json.loads(image_result.stdout)\n except (json.JSONDecodeError, TypeError):\n image = image_result.stdout.strip()\n try:\n restart_count = int(restart_result.stdout.strip())\n except ValueError:\n restart_count = None\n\n diagnostic = {\n \"container\": container,\n \"image\": image,\n \"restart_count\": restart_count,\n \"state\": state,\n }\n if redact:\n diagnostic = redact_json_strings(diagnostic)\n filename = sanitize_filename(container)\n (out_dir / f\"{filename}.state.json\").write_text(\n json.dumps(diagnostic, ensure_ascii=False, indent=2) + \"\\n\",\n encoding=\"utf-8\",\n )\n\n logs_result = _docker_run(\"logs\", \"--timestamps\", \"--since\", since, container)\n logs = logs_result.stdout + logs_result.stderr\n if redact:\n logs = redact_pii(logs)\n (out_dir / f\"{filename}.log\").write_text(logs, encoding=\"utf-8\")\n print(f\" {container}: state + {len(logs.splitlines())} log lines\")\n\n return len(containers)\n\n\n# ---------------------------------------------------------------------------\n# Matrix messages export\n# ---------------------------------------------------------------------------\n\ndef load_env_file(path: str) -> dict[str, str]:\n env = {}\n try:\n with open(path) as f:\n for line in f:\n line = line.strip()\n if not line or line.startswith(\"#\"):\n continue\n if \"=\" in line:\n key, _, value = line.partition(\"=\")\n env[key.strip()] = value.strip()\n except FileNotFoundError:\n pass\n return env\n\n\ndef matrix_login(homeserver: str, user: str, password: str) -> str:\n url = f\"{homeserver.rstrip('/')}/_matrix/client/v3/login\"\n payload = json.dumps({\n \"type\": \"m.login.password\",\n \"identifier\": {\"type\": \"m.id.user\", \"user\": user},\n \"password\": password,\n }).encode()\n req = urllib.request.Request(url, data=payload, headers={\"Content-Type\": \"application/json\"})\n with urllib.request.urlopen(req, timeout=15) as resp:\n data = json.loads(resp.read())\n return data[\"access_token\"]\n\n\ndef matrix_api(homeserver: str, token: str, endpoint: str, params: dict | None = None) -> dict:\n url = f\"{homeserver.rstrip('/')}/_matrix/client/v3/{endpoint}\"\n if params:\n url += \"?\" + urllib.parse.urlencode(params)\n req = urllib.request.Request(url, headers={\"Authorization\": f\"Bearer {token}\"})\n try:\n with urllib.request.urlopen(req, timeout=30) as resp:\n return json.loads(resp.read())\n except urllib.error.HTTPError as e:\n body = e.read().decode(errors=\"replace\")\n print(f\" API error {e.code} on {endpoint}: {body}\", file=sys.stderr)\n raise\n\n\ndef fetch_room_messages(homeserver: str, token: str, room_id: str, since_ts: int) -> list[dict]:\n encoded = urllib.parse.quote(room_id)\n messages = []\n from_token = \"\"\n while True:\n params = {\"dir\": \"b\", \"limit\": \"100\"}\n if from_token:\n params[\"from\"] = from_token\n data = matrix_api(homeserver, token, f\"rooms/{encoded}/messages\", params)\n chunk = data.get(\"chunk\", [])\n if not chunk:\n break\n hit_boundary = False\n for event in chunk:\n if event.get(\"origin_server_ts\", 0) < since_ts:\n hit_boundary = True\n break\n messages.append(event)\n if hit_boundary:\n break\n next_token = data.get(\"end\")\n if not next_token or next_token == from_token:\n break\n from_token = next_token\n messages.reverse()\n return messages\n\n\ndef format_event(event: dict, redact: bool) -> dict:\n content = event.get(\"content\", {})\n ts = event.get(\"origin_server_ts\", 0)\n record = {\n \"event_id\": event.get(\"event_id\"),\n \"type\": event.get(\"type\"),\n \"sender\": event.get(\"sender\"),\n \"timestamp\": ts,\n \"time\": datetime.fromtimestamp(ts / 1000, tz=timezone.utc).isoformat(),\n }\n if event.get(\"type\") == \"m.room.message\":\n record[\"msgtype\"] = content.get(\"msgtype\")\n record[\"body\"] = content.get(\"body\")\n if content.get(\"format\"):\n record[\"format\"] = content[\"format\"]\n if content.get(\"url\"):\n record[\"url\"] = content[\"url\"]\n if content.get(\"m.relates_to\"):\n record[\"relates_to\"] = content[\"m.relates_to\"]\n else:\n record[\"content\"] = content\n return redact_json_strings(record) if redact else record\n\n\ndef export_matrix_messages(out_dir: Path, since_epoch: float, redact: bool,\n room_filter: str | None, env_file: str | None,\n homeserver: str, token: str,\n messages_only: bool) -> tuple[int, int]:\n \"\"\"Export Matrix messages. Returns (rooms_exported, message_count).\"\"\"\n env_path = env_file or os.path.expanduser(\"~/agentteams-manager.env\")\n agentteams_env = load_env_file(env_path)\n\n if not homeserver:\n if not agentteams_env:\n print(f\" [matrix] Cannot find {env_path}, skipping Matrix export\")\n return 0, 0\n port = agentteams_env.get(\"AGENTTEAMS_PORT_GATEWAY\", \"18080\")\n homeserver = f\"http://127.0.0.1:{port}\"\n\n if not token:\n # Use Manager token — Manager is in every room (DM, Worker, Project)\n manager_password = agentteams_env.get(\"AGENTTEAMS_MANAGER_PASSWORD\", \"\")\n if manager_password:\n try:\n token = matrix_login(homeserver, \"manager\", manager_password)\n except Exception:\n pass\n\n # Fallback to admin token if Manager login failed\n if not token:\n admin_user = agentteams_env.get(\"AGENTTEAMS_ADMIN_USER\", \"admin\")\n admin_password = agentteams_env.get(\"AGENTTEAMS_ADMIN_PASSWORD\", \"\")\n if not admin_password:\n print(f\" [matrix] No usable credentials found, skipping Matrix export\")\n return 0, 0\n try:\n token = matrix_login(homeserver, admin_user, admin_password)\n except Exception as e:\n print(f\" [matrix] Login failed: {e}, skipping Matrix export\")\n return 0, 0\n\n since_ts = int(since_epoch * 1000)\n out_dir.mkdir(parents=True, exist_ok=True)\n\n try:\n rooms = matrix_api(homeserver, token, \"joined_rooms\").get(\"joined_rooms\", [])\n except Exception as e:\n print(f\" [matrix] Failed to list rooms: {e}\")\n return 0, 0\n\n total_messages = 0\n total_rooms = 0\n\n for room_id in rooms:\n encoded = urllib.parse.quote(room_id)\n try:\n room_name = matrix_api(homeserver, token, f\"rooms/{encoded}/state/m.room.name\").get(\"name\", \"\")\n except Exception:\n room_name = \"\"\n\n if room_filter:\n if room_filter not in room_id and room_filter not in (room_name or \"\"):\n continue\n\n display = f\"{room_name} ({room_id})\" if room_name else room_id\n print(f\" {display} ... \", end=\"\", flush=True)\n\n messages = fetch_room_messages(homeserver, token, room_id, since_ts)\n if messages_only:\n messages = [e for e in messages if e.get(\"type\") == \"m.room.message\"]\n\n if not messages:\n print(\"0 messages, skipped\")\n continue\n\n name_part = sanitize_filename(room_name) if room_name else \"\"\n id_part = sanitize_filename(room_id)\n filename = f\"{name_part}_{id_part}.jsonl\" if name_part else f\"{id_part}.jsonl\"\n\n with open(out_dir / filename, \"w\", encoding=\"utf-8\") as f:\n for event in messages:\n record = format_event(event, redact=redact)\n f.write(json.dumps(record, ensure_ascii=False) + \"\\n\")\n\n print(f\"{len(messages)} messages -> {filename}\")\n total_messages += len(messages)\n total_rooms += 1\n\n return total_rooms, total_messages\n\n\n# ---------------------------------------------------------------------------\n# Agent sessions export (OpenClaw + CoPaw + Hermes)\n# ---------------------------------------------------------------------------\n\ndef detect_runtime(container: str) -> tuple[str, str]:\n \"\"\"Probe known absolute session-dir locations across runtimes & container types.\n\n Returns (runtime_name, absolute_sessions_dir). Empty tuple if none found.\n\n Layout reference (verified against worker/scripts/worker-entrypoint.sh,\n copaw/scripts/copaw-worker-entrypoint.sh, copaw/AGENTS.md and\n tests/lib/agent-metrics.sh):\n\n Manager (any runtime, HOME=/root/manager-workspace):\n openclaw -> /root/manager-workspace/.openclaw/agents/main/sessions\n hermes -> /root/manager-workspace/.hermes/sessions\n copaw -> /root/manager-workspace/.copaw/workspaces/default/sessions\n\n OpenClaw / Hermes Worker (HOME=/root/agentteams-fs/agents/<name>):\n openclaw -> /root/agentteams-fs/agents/<name>/.openclaw/agents/main/sessions\n hermes -> /root/agentteams-fs/agents/<name>/.hermes/sessions\n\n CoPaw Worker (HOME=/root/.agentteams-worker/<name>, also reachable via\n /root/agentteams-fs symlink that points to that same dir):\n copaw -> /root/.agentteams-worker/<name>/.copaw/workspaces/default/sessions\n copaw -> /root/agentteams-fs/.copaw/workspaces/default/sessions (alt)\n \"\"\"\n candidates: list[tuple[str, str]] = [\n (\"openclaw\", \"/root/manager-workspace/.openclaw/agents/main/sessions\"),\n (\"hermes\", \"/root/manager-workspace/.hermes/sessions\"),\n (\"copaw\", \"/root/manager-workspace/.copaw/workspaces/default/sessions\"),\n ]\n\n worker_name = docker_exec(container, \"echo $AGENTTEAMS_WORKER_NAME\").strip()\n if worker_name:\n candidates.extend([\n (\"openclaw\", f\"/root/agentteams-fs/agents/{worker_name}/.openclaw/agents/main/sessions\"),\n (\"hermes\", f\"/root/agentteams-fs/agents/{worker_name}/.hermes/sessions\"),\n (\"copaw\", f\"/root/.agentteams-worker/{worker_name}/.copaw/workspaces/default/sessions\"),\n (\"copaw\", \"/root/agentteams-fs/.copaw/workspaces/default/sessions\"),\n ])\n\n for runtime, path in candidates:\n if docker_exec(container, f\"test -d '{path}' && echo yes || echo no\").strip() == \"yes\":\n return runtime, path\n\n # Last-resort: scan from / for any known session dir layout.\n found = docker_exec(\n container,\n \"find / -maxdepth 7 \\\\( \"\n \"-path '*/.openclaw/agents/main/sessions' \"\n \"-o -path '*/.hermes/sessions' \"\n \"-o -path '*/.copaw/workspaces/default/sessions' \"\n \"\\\\) -type d 2>/dev/null | head -1\",\n ).strip()\n if found:\n if \"/.openclaw/\" in found:\n return \"openclaw\", found\n if \"/.hermes/\" in found:\n return \"hermes\", found\n return \"copaw\", found\n\n return \"\", \"\"\n\n\ndef export_openclaw_sessions(container: str, sessions_dir: str, since_epoch: float,\n out_dir: Path, redact: bool) -> tuple[int, int]:\n ls_output = docker_exec(container, f\"ls '{sessions_dir}'/*.jsonl 2>/dev/null\").strip()\n if not ls_output:\n return 0, 0\n\n total_sessions = 0\n total_events = 0\n\n for session_path in [f.strip() for f in ls_output.splitlines() if f.strip()]:\n filename = os.path.basename(session_path)\n\n first_line = docker_exec(container, f\"head -1 '{session_path}'\").strip()\n if not first_line:\n continue\n try:\n header = json.loads(first_line)\n except json.JSONDecodeError:\n continue\n\n last_line = docker_exec(container, f\"tail -1 '{session_path}'\").strip()\n try:\n last_ts = parse_ts(json.loads(last_line).get(\"timestamp\", \"\"))\n except Exception:\n last_ts = parse_ts(header.get(\"timestamp\", \"\"))\n\n if last_ts < since_epoch and last_ts > 0:\n continue\n\n raw = docker_exec(container, f\"cat '{session_path}'\")\n if not raw.strip():\n continue\n\n output_lines = []\n for line in raw.splitlines():\n line = line.strip()\n if not line:\n continue\n try:\n event = json.loads(line)\n except json.JSONDecodeError:\n continue\n event_ts = parse_ts(event.get(\"timestamp\", \"\"))\n if event.get(\"type\") != \"session\" and event_ts < since_epoch and event_ts > 0:\n continue\n if redact:\n event = redact_json_strings(event)\n output_lines.append(json.dumps(event, ensure_ascii=False))\n\n if len(output_lines) <= 1:\n continue\n\n with open(out_dir / filename, \"w\", encoding=\"utf-8\") as f:\n f.write(\"\\n\".join(output_lines) + \"\\n\")\n\n event_count = len(output_lines) - 1\n print(f\" {container}/{filename} (openclaw): {event_count} events\")\n total_sessions += 1\n total_events += event_count\n\n # sessions.json index\n try:\n raw = docker_exec(container, f\"cat '{sessions_dir}/sessions.json'\")\n if raw.strip():\n index = json.loads(raw)\n if redact:\n index = redact_json_strings(index)\n with open(out_dir / \"sessions.json\", \"w\", encoding=\"utf-8\") as f:\n json.dump(index, f, ensure_ascii=False, indent=2)\n except Exception:\n pass\n\n return total_sessions, total_events\n\n\ndef export_copaw_sessions(container: str, sessions_dir: str, since_epoch: float,\n out_dir: Path, redact: bool) -> tuple[int, int]:\n ls_output = docker_exec(container, f\"find '{sessions_dir}' -name '*.json' -type f 2>/dev/null\").strip()\n if not ls_output:\n return 0, 0\n\n total_sessions = 0\n total_events = 0\n\n for session_path in [f.strip() for f in ls_output.splitlines() if f.strip()]:\n raw = docker_exec(container, f\"cat '{session_path}'\")\n if not raw.strip():\n continue\n try:\n data = json.loads(raw)\n except json.JSONDecodeError:\n continue\n\n agent = data.get(\"agent\", {})\n memory = agent.get(\"memory\", {})\n content = memory.get(\"content\", [])\n if not content:\n continue\n\n basename = os.path.basename(session_path).replace(\".json\", \"\")\n header = {\n \"type\": \"session\",\n \"runtime\": \"copaw\",\n \"agent_name\": agent.get(\"name\", \"\"),\n \"session_key\": basename,\n \"compressed_summary\": memory.get(\"_compressed_summary\", \"\"),\n }\n\n messages_in_range = []\n for turn_idx, turn in enumerate(content):\n if not isinstance(turn, list):\n continue\n for msg in turn:\n if not isinstance(msg, dict):\n continue\n msg_ts = parse_ts(msg.get(\"timestamp\", \"\"))\n if msg_ts >= since_epoch or msg_ts == 0:\n event = {\n \"type\": \"message\",\n \"turn\": turn_idx,\n \"id\": msg.get(\"id\", \"\"),\n \"role\": msg.get(\"role\", \"\"),\n \"name\": msg.get(\"name\", \"\"),\n \"timestamp\": msg.get(\"timestamp\", \"\"),\n \"content\": msg.get(\"content\", []),\n }\n if msg.get(\"metadata\"):\n event[\"metadata\"] = msg[\"metadata\"]\n messages_in_range.append(event)\n\n if not messages_in_range:\n continue\n\n output_lines = []\n if redact:\n header = redact_json_strings(header)\n output_lines.append(json.dumps(header, ensure_ascii=False))\n for event in messages_in_range:\n if redact:\n event = redact_json_strings(event)\n output_lines.append(json.dumps(event, ensure_ascii=False))\n\n out_filename = basename + \".jsonl\"\n with open(out_dir / out_filename, \"w\", encoding=\"utf-8\") as f:\n f.write(\"\\n\".join(output_lines) + \"\\n\")\n\n event_count = len(output_lines) - 1\n print(f\" {container}/{out_filename} (copaw): {event_count} events\")\n total_sessions += 1\n total_events += event_count\n\n return total_sessions, total_events\n\n\ndef export_hermes_sessions(container: str, sessions_dir: str, since_epoch: float,\n out_dir: Path, redact: bool) -> tuple[int, int]:\n ls_output = docker_exec(container, f\"ls '{sessions_dir}'/*.jsonl 2>/dev/null\").strip()\n if not ls_output:\n return 0, 0\n\n total_sessions = 0\n total_events = 0\n\n for session_path in [f.strip() for f in ls_output.splitlines() if f.strip()]:\n raw = docker_exec(container, f\"cat '{session_path}'\")\n if not raw.strip():\n continue\n\n output_lines = []\n last_ts = 0.0\n for line in raw.splitlines():\n line = line.strip()\n if not line:\n continue\n try:\n event = json.loads(line)\n except json.JSONDecodeError:\n continue\n\n event_ts = parse_ts(event.get(\"timestamp\", \"\"))\n if event_ts:\n last_ts = max(last_ts, event_ts)\n\n role = event.get(\"role\", \"\")\n if role != \"session_meta\" and event_ts < since_epoch and event_ts > 0:\n continue\n\n if redact:\n event = redact_json_strings(event)\n output_lines.append(json.dumps(event, ensure_ascii=False))\n\n if not output_lines:\n continue\n\n if last_ts < since_epoch and last_ts > 0:\n continue\n\n filename = os.path.basename(session_path)\n with open(out_dir / filename, \"w\", encoding=\"utf-8\") as f:\n f.write(\"\\n\".join(output_lines) + \"\\n\")\n\n event_count = len(output_lines) - 1 if output_lines and '\"role\": \"session_meta\"' in output_lines[0] else len(output_lines)\n print(f\" {container}/{filename} (hermes): {event_count} events\")\n total_sessions += 1\n total_events += max(event_count, 0)\n\n hermes_home = str(Path(sessions_dir).parent)\n state_db = f\"{hermes_home}/state.db\"\n has_state_db = docker_exec(container, f\"test -f '{state_db}' && echo yes || echo no\").strip() == \"yes\"\n if has_state_db:\n raw = docker_exec(container, f\"python3 - <<'PY'\\nimport json, sqlite3\\nconn = sqlite3.connect('{state_db}')\\nconn.row_factory = sqlite3.Row\\nrows = conn.execute('SELECT * FROM sessions ORDER BY started_at DESC LIMIT 200').fetchall()\\nprint(json.dumps([dict(r) for r in rows], ensure_ascii=False, indent=2))\\nPY\")\n if raw.strip():\n try:\n data = json.loads(raw)\n if redact:\n data = redact_json_strings(data)\n with open(out_dir / \"sessions-db.json\", \"w\", encoding=\"utf-8\") as f:\n json.dump(data, f, ensure_ascii=False, indent=2)\n except json.JSONDecodeError:\n pass\n\n logs_dir = f\"{hermes_home}/logs\"\n has_logs_dir = docker_exec(container, f\"test -d '{logs_dir}' && echo yes || echo no\").strip() == \"yes\"\n if has_logs_dir:\n for log_name in (\"agent.log\", \"errors.log\", \"gateway.log\"):\n raw = docker_exec(container, f\"test -f '{logs_dir}/{log_name}' && cat '{logs_dir}/{log_name}' || true\")\n if raw.strip():\n content = redact_pii(raw) if redact else raw\n (out_dir / log_name).write_text(content, encoding=\"utf-8\")\n\n return total_sessions, total_events\n\n\ndef export_agent_sessions(out_dir: Path, since_epoch: float, redact: bool,\n container_filter: str | None) -> tuple[int, int]:\n \"\"\"Export agent sessions from all containers. Returns (session_count, event_count).\"\"\"\n containers = list_agentteams_containers()\n if container_filter:\n containers = [c for c in containers if container_filter in c]\n\n if not containers:\n print(\" [sessions] No matching AgentTeams containers found\")\n return 0, 0\n\n total_sessions = 0\n total_events = 0\n\n for container in containers:\n runtime, sessions_dir = detect_runtime(container)\n if not runtime:\n print(f\" {container}: no sessions directory, skipped\")\n continue\n\n container_dir = out_dir / container\n container_dir.mkdir(parents=True, exist_ok=True)\n\n if runtime == \"openclaw\":\n s, e = export_openclaw_sessions(container, sessions_dir, since_epoch,\n container_dir, redact)\n elif runtime == \"hermes\":\n s, e = export_hermes_sessions(container, sessions_dir, since_epoch,\n container_dir, redact)\n else:\n s, e = export_copaw_sessions(container, sessions_dir, since_epoch,\n container_dir, redact)\n\n if s == 0:\n if not any(container_dir.iterdir()):\n container_dir.rmdir()\n print(f\" {container} ({runtime}): no sessions in range, skipped\")\n else:\n print(f\" {container} ({runtime}): exported auxiliary debug artifacts without session events\")\n else:\n total_sessions += s\n total_events += e\n\n return total_sessions, total_events\n\n\n# ---------------------------------------------------------------------------\n# Main\n# ---------------------------------------------------------------------------\n\ndef main():\n parser = argparse.ArgumentParser(\n description=\"Export AgentTeams debug logs (Matrix messages + agent sessions)\"\n )\n parser.add_argument(\"--range\", \"-r\", required=True, dest=\"time_range\",\n help=\"Time range to export, e.g. 10m, 1h, 1d\")\n parser.add_argument(\"--container\", \"-c\", default=None,\n help=\"Only export sessions from containers matching this substring\")\n parser.add_argument(\"--room\", default=None,\n help=\"Only export Matrix rooms matching this substring\")\n parser.add_argument(\"--homeserver\", \"-s\", default=\"\",\n help=\"Matrix homeserver URL (auto-detected from env file)\")\n parser.add_argument(\"--token\", \"-t\", default=\"\",\n help=\"Matrix access token (auto-detected from env file)\")\n parser.add_argument(\"--env-file\", default=None,\n help=\"Path to agentteams-manager.env (default: ~/agentteams-manager.env)\")\n parser.add_argument(\"--messages-only\", action=\"store_true\",\n help=\"Only export m.room.message events (skip state events)\")\n parser.add_argument(\"--no-redact\", action=\"store_true\",\n help=\"Disable PII redaction\")\n args = parser.parse_args()\n\n range_seconds = parse_range(args.time_range)\n since_epoch = time.time() - range_seconds\n since_human = datetime.fromtimestamp(since_epoch, tz=timezone.utc).isoformat()\n now_str = datetime.now().strftime(\"%Y%m%d-%H%M%S\")\n\n run_dir = Path(\"debug-log\") / now_str\n run_dir.mkdir(parents=True, exist_ok=True)\n redact = not args.no_redact\n\n print(f\"AgentTeams Debug Log Export\")\n print(f\" Range: last {args.time_range} (since {since_human})\")\n print(f\" Output: {run_dir.resolve()}\")\n print(f\" PII redaction: {'on' if redact else 'off'}\")\n print()\n\n # --- Matrix messages ---\n print(\"=== Matrix Messages ===\")\n matrix_dir = run_dir / \"matrix-messages\"\n rooms, messages = export_matrix_messages(\n matrix_dir, since_epoch, redact,\n room_filter=args.room, env_file=args.env_file,\n homeserver=args.homeserver, token=args.token,\n messages_only=args.messages_only,\n )\n if rooms == 0 and matrix_dir.exists() and not any(matrix_dir.iterdir()):\n matrix_dir.rmdir()\n print()\n\n # --- Agent sessions ---\n print(\"=== Agent Sessions ===\")\n sessions_dir = run_dir / \"agent-sessions\"\n sessions, events = export_agent_sessions(\n sessions_dir, since_epoch, redact,\n container_filter=args.container,\n )\n if sessions == 0 and sessions_dir.exists() and not any(sessions_dir.iterdir()):\n sessions_dir.rmdir()\n print()\n\n # --- Container diagnostics ---\n print(\"=== Container Diagnostics ===\")\n container_logs_dir = run_dir / \"container-logs\"\n containers = export_container_logs(\n container_logs_dir, since_epoch, redact,\n container_filter=args.container,\n )\n if containers == 0 and container_logs_dir.exists() and not any(container_logs_dir.iterdir()):\n container_logs_dir.rmdir()\n print()\n\n # --- Summary ---\n summary = (\n f\"AgentTeams Debug Log\\n\"\n f\"Exported at: {now_str}\\n\"\n f\"Range: last {args.time_range} (since {since_human})\\n\"\n f\"PII redaction: {'on' if redact else 'off'}\\n\"\n f\"\\n\"\n f\"Matrix messages: {messages} messages from {rooms} rooms\\n\"\n f\"Agent sessions: {events} events from {sessions} sessions\\n\"\n f\"Container diagnostics: {containers} containers\\n\"\n )\n (run_dir / \"summary.txt\").write_text(summary)\n\n print(\n f\"Done. {messages} messages from {rooms} rooms, \"\n f\"{events} events from {sessions} sessions, \"\n f\"{containers} container diagnostics\"\n )\n print(f\"Output: {run_dir.resolve()}\")\n\n\nif __name__ == \"__main__\":\n main()\n"}, "fb3a40be1f005bd584f45544fc73bd4601d5c52a": {"t": 1785764734.1177483, "n": 33260, "h": "611a3c0d9c21b255", "c": "#!/usr/bin/env python3\nfrom __future__ import annotations\n\"\"\"\nExport AgentTeams debug logs: Matrix messages + agent session logs.\n\nUsage:\n # Export last 1 hour (default)\n python scripts/export-debug-log.py --range 1h\n\n # Export last 1 day\n python scripts/export-debug-log.py --range 1d\n\n # Filter by container or room\n python scripts/export-debug-log.py --range 1h --container agentteams-manager --room Worker\n\n # Disable PII redaction\n python scripts/export-debug-log.py --range 1h --no-redact\n\nOutput structure:\n debug-log/20260319-153000/\n ├── summary.txt\n ├── matrix-messages/\n │ └── RoomName_!roomid.jsonl\n ├── agent-sessions/\n ├── agentteams-manager/\n │ └── {session-id}.jsonl\n └── agentteams-worker-xxx/\n └── {session-key}.jsonl\n └── container-logs/\n ├── agentteams-worker-xxx.log\n └── agentteams-worker-xxx.state.json\n\"\"\"\n\nimport argparse\nimport json\nimport os\nimport re\nimport subprocess\nimport sys\nimport time\nimport urllib.parse\nimport urllib.request\nimport urllib.error\nfrom datetime import datetime, timezone\nfrom pathlib import Path\n\n\n# ---------------------------------------------------------------------------\n# PII redaction\n# ---------------------------------------------------------------------------\n\n_PII_PATTERNS: list[tuple[str, re.Pattern]] = [\n (\"ID_CARD\", re.compile(r'\\b[1-9]\\d{5}(?:19|20)\\d{2}(?:0[1-9]|1[0-2])(?:0[1-9]|[12]\\d|3[01])\\d{3}[\\dXx]\\b')),\n (\"PHONE\", re.compile(r'(?<!\\d)(?:\\+?86[-\\s]?)?1[3-9]\\d{9}(?!\\d)')),\n (\"EMAIL\", re.compile(r'\\b[A-Za-z0-9._%+\\-]+@[A-Za-z0-9.\\-]+\\.[A-Za-z]{2,}\\b')),\n (\"BANK_CARD\", re.compile(r'\\b(?:6[0-9]{15,18}|4[0-9]{15}|5[1-5][0-9]{14}|3[47][0-9]{13}|62[0-9]{14,17})\\b')),\n (\"IP\", re.compile(r'\\b(?:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)\\.){3}(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)\\b')),\n (\"ALIYUN_AK\", re.compile(r'\\bLTAI[A-Za-z0-9]{12,30}\\b')),\n (\"ALIYUN_SK\", re.compile(r'(?i)(?:access_?key_?secret|secret_?access_?key)\\s*[:=]\\s*(\\S{20,})')),\n (\"AWS_AK\", re.compile(r'\\b(?:AKIA|ASIA)[A-Z0-9]{16}\\b')),\n (\"OPENAI_KEY\", re.compile(r'\\bsk-[A-Za-z0-9]{20,}\\b')),\n (\"ANTHROPIC_KEY\", re.compile(r'\\bsk-ant-[A-Za-z0-9\\-]{20,}\\b')),\n (\"DASHSCOPE_KEY\", re.compile(r'\\bsk-sp-[A-Za-z0-9]{20,}\\b')),\n (\"DEEPSEEK_KEY\", re.compile(r'\\bsk-[a-f0-9]{32,}\\b')),\n (\"BEARER\", re.compile(r'(?i)(Bearer\\s+)([A-Za-z0-9\\-_.]{20,})')),\n (\"SECRET_KV\", re.compile(\n r'(?i)((?:password|passwd|pwd|secret|token|api_?key|access_?key|secret_?key'\n r'|private_?key|credential|appkey|app_?secret|auth_?token|signing_?key'\n r'|client_?secret|master_?key)\\s*[:=]\\s*)'\n r'(\\S+)'\n )),\n (\"MATRIX_TOKEN\", re.compile(r'\\bsyt_[A-Za-z0-9_\\-]{10,}\\b')),\n (\"HEX_SECRET\", re.compile(r'\\b[A-Fa-f0-9]{32,}\\b')),\n (\"PASSPORT\", re.compile(r'\\b[EeGg]\\d{8}\\b')),\n (\"SSN\", re.compile(r'\\b\\d{3}-\\d{2}-\\d{4}\\b')),\n]\n\n_SECRET_FIELD_PATTERN = re.compile(\n r'(?i)^(?:password|passwd|pwd|secret|token|api[_-]?key'\n r'|access[_-]?key(?:[_-]?secret)?|secret[_-]?access[_-]?key'\n r'|secret[_-]?key|private[_-]?key|credential|app[_-]?key'\n r'|app[_-]?secret|auth[_-]?token|signing[_-]?key'\n r'|client[_-]?secret|master[_-]?key)$'\n)\n\n\ndef redact_pii(text: str) -> str:\n if not text:\n return text\n for name, pattern in _PII_PATTERNS:\n if name in (\"SECRET_KV\", \"ALIYUN_SK\", \"BEARER\"):\n text = pattern.sub(r'\\1****', text)\n else:\n text = pattern.sub('****', text)\n return text\n\n\ndef redact_json_strings(obj):\n if isinstance(obj, str):\n return redact_pii(obj)\n if isinstance(obj, list):\n return [redact_json_strings(v) for v in obj]\n if isinstance(obj, dict):\n redacted = {}\n for key, value in obj.items():\n if isinstance(key, str) and _SECRET_FIELD_PATTERN.fullmatch(key):\n redacted[key] = \"****\"\n else:\n redacted[key] = redact_json_strings(value)\n return redacted\n return obj\n\n\n# ---------------------------------------------------------------------------\n# Common helpers\n# ---------------------------------------------------------------------------\n\ndef parse_range(range_str: str) -> int:\n m = re.fullmatch(r\"(\\d+)\\s*(m|min|h|hr|hour|d|day)s?\", range_str.strip(), re.IGNORECASE)\n if not m:\n raise ValueError(f\"Invalid range format: '{range_str}'. Use e.g. 10m, 1h, 1d\")\n value, unit = int(m.group(1)), m.group(2)[0].lower()\n return value * {\"m\": 60, \"h\": 3600, \"d\": 86400}[unit]\n\n\ndef parse_ts(ts_str: str) -> float:\n if not ts_str:\n return 0\n ts_str = ts_str.replace(\"Z\", \"+00:00\")\n try:\n dt = datetime.fromisoformat(ts_str)\n if dt.tzinfo is None:\n dt = dt.replace(tzinfo=timezone.utc)\n return dt.timestamp()\n except Exception:\n return 0\n\n\ndef sanitize_filename(name: str) -> str:\n return re.sub(r'[^\\w\\-. ]', '_', name).strip()[:80]\n\n\ndef docker_exec(container: str, cmd: str) -> str:\n result = subprocess.run(\n [\"docker\", \"exec\", container, \"sh\", \"-c\", cmd],\n capture_output=True, text=True, timeout=30,\n )\n return result.stdout\n\n\ndef list_agentteams_containers() -> list[str]:\n names: list[str] = []\n for prefix in (\"agentteams-\",):\n result = subprocess.run(\n [\"docker\", \"ps\", \"--format\", \"{{.Names}}\", \"--filter\", f\"name={prefix}\"],\n capture_output=True, text=True, timeout=10,\n )\n names.extend(n.strip() for n in result.stdout.splitlines() if n.strip())\n return list(dict.fromkeys(names))\n\n\n# ---------------------------------------------------------------------------\n# Container diagnostics\n# ---------------------------------------------------------------------------\n\ndef _docker_run(*args: str) -> subprocess.CompletedProcess[str]:\n try:\n return subprocess.run(\n [\"docker\", *args],\n capture_output=True,\n text=True,\n timeout=30,\n )\n except (OSError, subprocess.SubprocessError) as exc:\n return subprocess.CompletedProcess(\n [\"docker\", *args],\n 1,\n \"\",\n f\"{type(exc).__name__}: {exc}\",\n )\n\n\ndef export_container_logs(out_dir: Path, since_epoch: float, redact: bool,\n container_filter: str | None) -> int:\n \"\"\"Export Docker state and logs for all AgentTeams containers.\"\"\"\n result = _docker_run(\"ps\", \"-a\", \"--format\", \"{{.Names}}\", \"--filter\", \"name=agentteams-\")\n containers = [name.strip() for name in result.stdout.splitlines() if name.strip()]\n if container_filter:\n containers = [name for name in containers if container_filter in name]\n if not containers:\n print(\" [containers] No matching AgentTeams containers found\")\n return 0\n\n out_dir.mkdir(parents=True, exist_ok=True)\n since = datetime.fromtimestamp(since_epoch, tz=timezone.utc).isoformat()\n\n for container in containers:\n state_result = _docker_run(\"inspect\", \"--format={{json .State}}\", container)\n image_result = _docker_run(\"inspect\", \"--format={{json .Config.Image}}\", container)\n restart_result = _docker_run(\"inspect\", \"--format={{.RestartCount}}\", container)\n try:\n state = json.loads(state_result.stdout)\n except (json.JSONDecodeError, TypeError):\n state = {\"inspect_error\": state_result.stderr.strip() or state_result.stdout.strip()}\n try:\n image = json.loads(image_result.stdout)\n except (json.JSONDecodeError, TypeError):\n image = image_result.stdout.strip()\n try:\n restart_count = int(restart_result.stdout.strip())\n except ValueError:\n restart_count = None\n\n diagnostic = {\n \"container\": container,\n \"image\": image,\n \"restart_count\": restart_count,\n \"state\": state,\n }\n if redact:\n diagnostic = redact_json_strings(diagnostic)\n filename = sanitize_filename(container)\n (out_dir / f\"{filename}.state.json\").write_text(\n json.dumps(diagnostic, ensure_ascii=False, indent=2) + \"\\n\",\n encoding=\"utf-8\",\n )\n\n logs_result = _docker_run(\"logs\", \"--timestamps\", \"--since\", since, container)\n logs = logs_result.stdout + logs_result.stderr\n if redact:\n logs = redact_pii(logs)\n (out_dir / f\"{filename}.log\").write_text(logs, encoding=\"utf-8\")\n print(f\" {container}: state + {len(logs.splitlines())} log lines\")\n\n return len(containers)\n\n\n# ---------------------------------------------------------------------------\n# Matrix messages export\n# ---------------------------------------------------------------------------\n\ndef load_env_file(path: str) -> dict[str, str]:\n env = {}\n try:\n with open(path) as f:\n for line in f:\n line = line.strip()\n if not line or line.startswith(\"#\"):\n continue\n if \"=\" in line:\n key, _, value = line.partition(\"=\")\n env[key.strip()] = value.strip()\n except FileNotFoundError:\n pass\n return env\n\n\ndef matrix_login(homeserver: str, user: str, password: str) -> str:\n url = f\"{homeserver.rstrip('/')}/_matrix/client/v3/login\"\n payload = json.dumps({\n \"type\": \"m.login.password\",\n \"identifier\": {\"type\": \"m.id.user\", \"user\": user},\n \"password\": password,\n }).encode()\n req = urllib.request.Request(url, data=payload, headers={\"Content-Type\": \"application/json\"})\n with urllib.request.urlopen(req, timeout=15) as resp:\n data = json.loads(resp.read())\n return data[\"access_token\"]\n\n\ndef matrix_api(homeserver: str, token: str, endpoint: str, params: dict | None = None) -> dict:\n url = f\"{homeserver.rstrip('/')}/_matrix/client/v3/{endpoint}\"\n if params:\n url += \"?\" + urllib.parse.urlencode(params)\n req = urllib.request.Request(url, headers={\"Authorization\": f\"Bearer {token}\"})\n try:\n with urllib.request.urlopen(req, timeout=30) as resp:\n return json.loads(resp.read())\n except urllib.error.HTTPError as e:\n body = e.read().decode(errors=\"replace\")\n print(f\" API error {e.code} on {endpoint}: {body}\", file=sys.stderr)\n raise\n\n\ndef fetch_room_messages(homeserver: str, token: str, room_id: str, since_ts: int) -> list[dict]:\n encoded = urllib.parse.quote(room_id)\n messages = []\n from_token = \"\"\n while True:\n params = {\"dir\": \"b\", \"limit\": \"100\"}\n if from_token:\n params[\"from\"] = from_token\n data = matrix_api(homeserver, token, f\"rooms/{encoded}/messages\", params)\n chunk = data.get(\"chunk\", [])\n if not chunk:\n break\n hit_boundary = False\n for event in chunk:\n if event.get(\"origin_server_ts\", 0) < since_ts:\n hit_boundary = True\n break\n messages.append(event)\n if hit_boundary:\n break\n next_token = data.get(\"end\")\n if not next_token or next_token == from_token:\n break\n from_token = next_token\n messages.reverse()\n return messages\n\n\ndef format_event(event: dict, redact: bool) -> dict:\n content = event.get(\"content\", {})\n ts = event.get(\"origin_server_ts\", 0)\n record = {\n \"event_id\": event.get(\"event_id\"),\n \"type\": event.get(\"type\"),\n \"sender\": event.get(\"sender\"),\n \"timestamp\": ts,\n \"time\": datetime.fromtimestamp(ts / 1000, tz=timezone.utc).isoformat(),\n }\n if event.get(\"type\") == \"m.room.message\":\n record[\"msgtype\"] = content.get(\"msgtype\")\n record[\"body\"] = content.get(\"body\")\n if content.get(\"format\"):\n record[\"format\"] = content[\"format\"]\n if content.get(\"url\"):\n record[\"url\"] = content[\"url\"]\n if content.get(\"m.relates_to\"):\n record[\"relates_to\"] = content[\"m.relates_to\"]\n else:\n record[\"content\"] = content\n return redact_json_strings(record) if redact else record\n\n\ndef export_matrix_messages(out_dir: Path, since_epoch: float, redact: bool,\n room_filter: str | None, env_file: str | None,\n homeserver: str, token: str,\n messages_only: bool) -> tuple[int, int]:\n \"\"\"Export Matrix messages. Returns (rooms_exported, message_count).\"\"\"\n env_path = env_file or os.path.expanduser(\"~/agentteams-manager.env\")\n agentteams_env = load_env_file(env_path)\n\n if not homeserver:\n if not agentteams_env:\n print(f\" [matrix] Cannot find {env_path}, skipping Matrix export\")\n return 0, 0\n port = agentteams_env.get(\"AGENTTEAMS_PORT_GATEWAY\", \"18080\")\n homeserver = f\"http://127.0.0.1:{port}\"\n\n if not token:\n # Use Manager token — Manager is in every room (DM, Worker, Project)\n manager_password = agentteams_env.get(\"AGENTTEAMS_MANAGER_PASSWORD\", \"\")\n if manager_password:\n try:\n token = matrix_login(homeserver, \"manager\", manager_password)\n except Exception:\n pass\n\n # Fallback to admin token if Manager login failed\n if not token:\n admin_user = agentteams_env.get(\"AGENTTEAMS_ADMIN_USER\", \"admin\")\n admin_password = agentteams_env.get(\"AGENTTEAMS_ADMIN_PASSWORD\", \"\")\n if not admin_password:\n print(f\" [matrix] No usable credentials found, skipping Matrix export\")\n return 0, 0\n try:\n token = matrix_login(homeserver, admin_user, admin_password)\n except Exception as e:\n print(f\" [matrix] Login failed: {e}, skipping Matrix export\")\n return 0, 0\n\n since_ts = int(since_epoch * 1000)\n out_dir.mkdir(parents=True, exist_ok=True)\n\n try:\n rooms = matrix_api(homeserver, token, \"joined_rooms\").get(\"joined_rooms\", [])\n except Exception as e:\n print(f\" [matrix] Failed to list rooms: {e}\")\n return 0, 0\n\n total_messages = 0\n total_rooms = 0\n\n for room_id in rooms:\n encoded = urllib.parse.quote(room_id)\n try:\n room_name = matrix_api(homeserver, token, f\"rooms/{encoded}/state/m.room.name\").get(\"name\", \"\")\n except Exception:\n room_name = \"\"\n\n if room_filter:\n if room_filter not in room_id and room_filter not in (room_name or \"\"):\n continue\n\n display = f\"{room_name} ({room_id})\" if room_name else room_id\n print(f\" {display} ... \", end=\"\", flush=True)\n\n messages = fetch_room_messages(homeserver, token, room_id, since_ts)\n if messages_only:\n messages = [e for e in messages if e.get(\"type\") == \"m.room.message\"]\n\n if not messages:\n print(\"0 messages, skipped\")\n continue\n\n name_part = sanitize_filename(room_name) if room_name else \"\"\n id_part = sanitize_filename(room_id)\n filename = f\"{name_part}_{id_part}.jsonl\" if name_part else f\"{id_part}.jsonl\"\n\n with open(out_dir / filename, \"w\", encoding=\"utf-8\") as f:\n for event in messages:\n record = format_event(event, redact=redact)\n f.write(json.dumps(record, ensure_ascii=False) + \"\\n\")\n\n print(f\"{len(messages)} messages -> {filename}\")\n total_messages += len(messages)\n total_rooms += 1\n\n return total_rooms, total_messages\n\n\n# ---------------------------------------------------------------------------\n# Agent sessions export (OpenClaw + CoPaw + Hermes)\n# ---------------------------------------------------------------------------\n\ndef detect_runtime(container: str) -> tuple[str, str]:\n \"\"\"Probe known absolute session-dir locations across runtimes & container types.\n\n Returns (runtime_name, absolute_sessions_dir). Empty tuple if none found.\n\n Layout reference (verified against worker/scripts/worker-entrypoint.sh,\n copaw/scripts/copaw-worker-entrypoint.sh, copaw/AGENTS.md and\n tests/lib/agent-metrics.sh):\n\n Manager (any runtime, HOME=/root/manager-workspace):\n openclaw -> /root/manager-workspace/.openclaw/agents/main/sessions\n hermes -> /root/manager-workspace/.hermes/sessions\n copaw -> /root/manager-workspace/.copaw/workspaces/default/sessions\n\n OpenClaw / Hermes Worker (HOME=/root/agentteams-fs/agents/<name>):\n openclaw -> /root/agentteams-fs/agents/<name>/.openclaw/agents/main/sessions\n hermes -> /root/agentteams-fs/agents/<name>/.hermes/sessions\n\n CoPaw Worker (HOME=/root/.agentteams-worker/<name>, also reachable via\n /root/agentteams-fs symlink that points to that same dir):\n copaw -> /root/.agentteams-worker/<name>/.copaw/workspaces/default/sessions\n copaw -> /root/agentteams-fs/.copaw/workspaces/default/sessions (alt)\n \"\"\"\n candidates: list[tuple[str, str]] = [\n (\"openclaw\", \"/root/manager-workspace/.openclaw/agents/main/sessions\"),\n (\"hermes\", \"/root/manager-workspace/.hermes/sessions\"),\n (\"copaw\", \"/root/manager-workspace/.copaw/workspaces/default/sessions\"),\n ]\n\n worker_name = docker_exec(container, \"echo $AGENTTEAMS_WORKER_NAME\").strip()\n if worker_name:\n candidates.extend([\n (\"openclaw\", f\"/root/agentteams-fs/agents/{worker_name}/.openclaw/agents/main/sessions\"),\n (\"hermes\", f\"/root/agentteams-fs/agents/{worker_name}/.hermes/sessions\"),\n (\"copaw\", f\"/root/.agentteams-worker/{worker_name}/.copaw/workspaces/default/sessions\"),\n (\"copaw\", \"/root/agentteams-fs/.copaw/workspaces/default/sessions\"),\n ])\n\n for runtime, path in candidates:\n if docker_exec(container, f\"test -d '{path}' && echo yes || echo no\").strip() == \"yes\":\n return runtime, path\n\n # Last-resort: scan from / for any known session dir layout.\n found = docker_exec(\n container,\n \"find / -maxdepth 7 \\\\( \"\n \"-path '*/.openclaw/agents/main/sessions' \"\n \"-o -path '*/.hermes/sessions' \"\n \"-o -path '*/.copaw/workspaces/default/sessions' \"\n \"\\\\) -type d 2>/dev/null | head -1\",\n ).strip()\n if found:\n if \"/.openclaw/\" in found:\n return \"openclaw\", found\n if \"/.hermes/\" in found:\n return \"hermes\", found\n return \"copaw\", found\n\n return \"\", \"\"\n\n\ndef export_openclaw_sessions(container: str, sessions_dir: str, since_epoch: float,\n out_dir: Path, redact: bool) -> tuple[int, int]:\n ls_output = docker_exec(container, f\"ls '{sessions_dir}'/*.jsonl 2>/dev/null\").strip()\n if not ls_output:\n return 0, 0\n\n total_sessions = 0\n total_events = 0\n\n for session_path in [f.strip() for f in ls_output.splitlines() if f.strip()]:\n filename = os.path.basename(session_path)\n\n first_line = docker_exec(container, f\"head -1 '{session_path}'\").strip()\n if not first_line:\n continue\n try:\n header = json.loads(first_line)\n except json.JSONDecodeError:\n continue\n\n last_line = docker_exec(container, f\"tail -1 '{session_path}'\").strip()\n try:\n last_ts = parse_ts(json.loads(last_line).get(\"timestamp\", \"\"))\n except Exception:\n last_ts = parse_ts(header.get(\"timestamp\", \"\"))\n\n if last_ts < since_epoch and last_ts > 0:\n continue\n\n raw = docker_exec(container, f\"cat '{session_path}'\")\n if not raw.strip():\n continue\n\n output_lines = []\n for line in raw.splitlines():\n line = line.strip()\n if not line:\n continue\n try:\n event = json.loads(line)\n except json.JSONDecodeError:\n continue\n event_ts = parse_ts(event.get(\"timestamp\", \"\"))\n if event.get(\"type\") != \"session\" and event_ts < since_epoch and event_ts > 0:\n continue\n if redact:\n event = redact_json_strings(event)\n output_lines.append(json.dumps(event, ensure_ascii=False))\n\n if len(output_lines) <= 1:\n continue\n\n with open(out_dir / filename, \"w\", encoding=\"utf-8\") as f:\n f.write(\"\\n\".join(output_lines) + \"\\n\")\n\n event_count = len(output_lines) - 1\n print(f\" {container}/{filename} (openclaw): {event_count} events\")\n total_sessions += 1\n total_events += event_count\n\n # sessions.json index\n try:\n raw = docker_exec(container, f\"cat '{sessions_dir}/sessions.json'\")\n if raw.strip():\n index = json.loads(raw)\n if redact:\n index = redact_json_strings(index)\n with open(out_dir / \"sessions.json\", \"w\", encoding=\"utf-8\") as f:\n json.dump(index, f, ensure_ascii=False, indent=2)\n except Exception:\n pass\n\n return total_sessions, total_events\n\n\ndef export_copaw_sessions(container: str, sessions_dir: str, since_epoch: float,\n out_dir: Path, redact: bool) -> tuple[int, int]:\n ls_output = docker_exec(container, f\"find '{sessions_dir}' -name '*.json' -type f 2>/dev/null\").strip()\n if not ls_output:\n return 0, 0\n\n total_sessions = 0\n total_events = 0\n\n for session_path in [f.strip() for f in ls_output.splitlines() if f.strip()]:\n raw = docker_exec(container, f\"cat '{session_path}'\")\n if not raw.strip():\n continue\n try:\n data = json.loads(raw)\n except json.JSONDecodeError:\n continue\n\n agent = data.get(\"agent\", {})\n memory = agent.get(\"memory\", {})\n content = memory.get(\"content\", [])\n if not content:\n continue\n\n basename = os.path.basename(session_path).replace(\".json\", \"\")\n header = {\n \"type\": \"session\",\n \"runtime\": \"copaw\",\n \"agent_name\": agent.get(\"name\", \"\"),\n \"session_key\": basename,\n \"compressed_summary\": memory.get(\"_compressed_summary\", \"\"),\n }\n\n messages_in_range = []\n for turn_idx, turn in enumerate(content):\n if not isinstance(turn, list):\n continue\n for msg in turn:\n if not isinstance(msg, dict):\n continue\n msg_ts = parse_ts(msg.get(\"timestamp\", \"\"))\n if msg_ts >= since_epoch or msg_ts == 0:\n event = {\n \"type\": \"message\",\n \"turn\": turn_idx,\n \"id\": msg.get(\"id\", \"\"),\n \"role\": msg.get(\"role\", \"\"),\n \"name\": msg.get(\"name\", \"\"),\n \"timestamp\": msg.get(\"timestamp\", \"\"),\n \"content\": msg.get(\"content\", []),\n }\n if msg.get(\"metadata\"):\n event[\"metadata\"] = msg[\"metadata\"]\n messages_in_range.append(event)\n\n if not messages_in_range:\n continue\n\n output_lines = []\n if redact:\n header = redact_json_strings(header)\n output_lines.append(json.dumps(header, ensure_ascii=False))\n for event in messages_in_range:\n if redact:\n event = redact_json_strings(event)\n output_lines.append(json.dumps(event, ensure_ascii=False))\n\n out_filename = basename + \".jsonl\"\n with open(out_dir / out_filename, \"w\", encoding=\"utf-8\") as f:\n f.write(\"\\n\".join(output_lines) + \"\\n\")\n\n event_count = len(output_lines) - 1\n print(f\" {container}/{out_filename} (copaw): {event_count} events\")\n total_sessions += 1\n total_events += event_count\n\n return total_sessions, total_events\n\n\ndef export_hermes_sessions(container: str, sessions_dir: str, since_epoch: float,\n out_dir: Path, redact: bool) -> tuple[int, int]:\n ls_output = docker_exec(container, f\"ls '{sessions_dir}'/*.jsonl 2>/dev/null\").strip()\n if not ls_output:\n return 0, 0\n\n total_sessions = 0\n total_events = 0\n\n for session_path in [f.strip() for f in ls_output.splitlines() if f.strip()]:\n raw = docker_exec(container, f\"cat '{session_path}'\")\n if not raw.strip():\n continue\n\n output_lines = []\n last_ts = 0.0\n for line in raw.splitlines():\n line = line.strip()\n if not line:\n continue\n try:\n event = json.loads(line)\n except json.JSONDecodeError:\n continue\n\n event_ts = parse_ts(event.get(\"timestamp\", \"\"))\n if event_ts:\n last_ts = max(last_ts, event_ts)\n\n role = event.get(\"role\", \"\")\n if role != \"session_meta\" and event_ts < since_epoch and event_ts > 0:\n continue\n\n if redact:\n event = redact_json_strings(event)\n output_lines.append(json.dumps(event, ensure_ascii=False))\n\n if not output_lines:\n continue\n\n if last_ts < since_epoch and last_ts > 0:\n continue\n\n filename = os.path.basename(session_path)\n with open(out_dir / filename, \"w\", encoding=\"utf-8\") as f:\n f.write(\"\\n\".join(output_lines) + \"\\n\")\n\n event_count = len(output_lines) - 1 if output_lines and '\"role\": \"session_meta\"' in output_lines[0] else len(output_lines)\n print(f\" {container}/{filename} (hermes): {event_count} events\")\n total_sessions += 1\n total_events += max(event_count, 0)\n\n hermes_home = str(Path(sessions_dir).parent)\n state_db = f\"{hermes_home}/state.db\"\n has_state_db = docker_exec(container, f\"test -f '{state_db}' && echo yes || echo no\").strip() == \"yes\"\n if has_state_db:\n raw = docker_exec(container, f\"python3 - <<'PY'\\nimport json, sqlite3\\nconn = sqlite3.connect('{state_db}')\\nconn.row_factory = sqlite3.Row\\nrows = conn.execute('SELECT * FROM sessions ORDER BY started_at DESC LIMIT 200').fetchall()\\nprint(json.dumps([dict(r) for r in rows], ensure_ascii=False, indent=2))\\nPY\")\n if raw.strip():\n try:\n data = json.loads(raw)\n if redact:\n data = redact_json_strings(data)\n with open(out_dir / \"sessions-db.json\", \"w\", encoding=\"utf-8\") as f:\n json.dump(data, f, ensure_ascii=False, indent=2)\n except json.JSONDecodeError:\n pass\n\n logs_dir = f\"{hermes_home}/logs\"\n has_logs_dir = docker_exec(container, f\"test -d '{logs_dir}' && echo yes || echo no\").strip() == \"yes\"\n if has_logs_dir:\n for log_name in (\"agent.log\", \"errors.log\", \"gateway.log\"):\n raw = docker_exec(container, f\"test -f '{logs_dir}/{log_name}' && cat '{logs_dir}/{log_name}' || true\")\n if raw.strip():\n content = redact_pii(raw) if redact else raw\n (out_dir / log_name).write_text(content, encoding=\"utf-8\")\n\n return total_sessions, total_events\n\n\ndef export_agent_sessions(out_dir: Path, since_epoch: float, redact: bool,\n container_filter: str | None) -> tuple[int, int]:\n \"\"\"Export agent sessions from all containers. Returns (session_count, event_count).\"\"\"\n containers = list_agentteams_containers()\n if container_filter:\n containers = [c for c in containers if container_filter in c]\n\n if not containers:\n print(\" [sessions] No matching AgentTeams containers found\")\n return 0, 0\n\n total_sessions = 0\n total_events = 0\n\n for container in containers:\n runtime, sessions_dir = detect_runtime(container)\n if not runtime:\n print(f\" {container}: no sessions directory, skipped\")\n continue\n\n container_dir = out_dir / container\n container_dir.mkdir(parents=True, exist_ok=True)\n\n if runtime == \"openclaw\":\n s, e = export_openclaw_sessions(container, sessions_dir, since_epoch,\n container_dir, redact)\n elif runtime == \"hermes\":\n s, e = export_hermes_sessions(container, sessions_dir, since_epoch,\n container_dir, redact)\n else:\n s, e = export_copaw_sessions(container, sessions_dir, since_epoch,\n container_dir, redact)\n\n if s == 0:\n if not any(container_dir.iterdir()):\n container_dir.rmdir()\n print(f\" {container} ({runtime}): no sessions in range, skipped\")\n else:\n print(f\" {container} ({runtime}): exported auxiliary debug artifacts without session events\")\n else:\n total_sessions += s\n total_events += e\n\n return total_sessions, total_events\n\n\n# ---------------------------------------------------------------------------\n# Main\n# ---------------------------------------------------------------------------\n\ndef main():\n parser = argparse.ArgumentParser(\n description=\"Export AgentTeams debug logs (Matrix messages + agent sessions)\"\n )\n parser.add_argument(\"--range\", \"-r\", required=True, dest=\"time_range\",\n help=\"Time range to export, e.g. 10m, 1h, 1d\")\n parser.add_argument(\"--container\", \"-c\", default=None,\n help=\"Only export sessions from containers matching this substring\")\n parser.add_argument(\"--room\", default=None,\n help=\"Only export Matrix rooms matching this substring\")\n parser.add_argument(\"--homeserver\", \"-s\", default=\"\",\n help=\"Matrix homeserver URL (auto-detected from env file)\")\n parser.add_argument(\"--token\", \"-t\", default=\"\",\n help=\"Matrix access token (auto-detected from env file)\")\n parser.add_argument(\"--env-file\", default=None,\n help=\"Path to agentteams-manager.env (default: ~/agentteams-manager.env)\")\n parser.add_argument(\"--messages-only\", action=\"store_true\",\n help=\"Only export m.room.message events (skip state events)\")\n parser.add_argument(\"--no-redact\", action=\"store_true\",\n help=\"Disable PII redaction\")\n args = parser.parse_args()\n\n range_seconds = parse_range(args.time_range)\n since_epoch = time.time() - range_seconds\n since_human = datetime.fromtimestamp(since_epoch, tz=timezone.utc).isoformat()\n now_str = datetime.now().strftime(\"%Y%m%d-%H%M%S\")\n\n run_dir = Path(\"debug-log\") / now_str\n run_dir.mkdir(parents=True, exist_ok=True)\n redact = not args.no_redact\n\n print(f\"AgentTeams Debug Log Export\")\n print(f\" Range: last {args.time_range} (since {since_human})\")\n print(f\" Output: {run_dir.resolve()}\")\n print(f\" PII redaction: {'on' if redact else 'off'}\")\n print()\n\n # --- Matrix messages ---\n print(\"=== Matrix Messages ===\")\n matrix_dir = run_dir / \"matrix-messages\"\n rooms, messages = export_matrix_messages(\n matrix_dir, since_epoch, redact,\n room_filter=args.room, env_file=args.env_file,\n homeserver=args.homeserver, token=args.token,\n messages_only=args.messages_only,\n )\n if rooms == 0 and matrix_dir.exists() and not any(matrix_dir.iterdir()):\n matrix_dir.rmdir()\n print()\n\n # --- Agent sessions ---\n print(\"=== Agent Sessions ===\")\n sessions_dir = run_dir / \"agent-sessions\"\n sessions, events = export_agent_sessions(\n sessions_dir, since_epoch, redact,\n container_filter=args.container,\n )\n if sessions == 0 and sessions_dir.exists() and not any(sessions_dir.iterdir()):\n sessions_dir.rmdir()\n print()\n\n # --- Container diagnostics ---\n print(\"=== Container Diagnostics ===\")\n container_logs_dir = run_dir / \"container-logs\"\n containers = export_container_logs(\n container_logs_dir, since_epoch, redact,\n container_filter=args.container,\n )\n if containers == 0 and container_logs_dir.exists() and not any(container_logs_dir.iterdir()):\n container_logs_dir.rmdir()\n print()\n\n # --- Summary ---\n summary = (\n f\"AgentTeams Debug Log\\n\"\n f\"Exported at: {now_str}\\n\"\n f\"Range: last {args.time_range} (since {since_human})\\n\"\n f\"PII redaction: {'on' if redact else 'off'}\\n\"\n f\"\\n\"\n f\"Matrix messages: {messages} messages from {rooms} rooms\\n\"\n f\"Agent sessions: {events} events from {sessions} sessions\\n\"\n f\"Container diagnostics: {containers} containers\\n\"\n )\n (run_dir / \"summary.txt\").write_text(summary)\n\n print(\n f\"Done. {messages} messages from {rooms} rooms, \"\n f\"{events} events from {sessions} sessions, \"\n f\"{containers} container diagnostics\"\n )\n print(f\"Output: {run_dir.resolve()}\")\n\n\nif __name__ == \"__main__\":\n main()\n"}, "687b6d94ba04e894f5982150293c188158189d3e": {"t": 1785764735.5527108, "n": 29595, "h": "45e8371fd8e1fd99", "c": "#!/usr/bin/env python3\nfrom __future__ import annotations\n\"\"\"\nExport AgentTeams debug logs: Matrix messages + agent session logs.\n\nUsage:\n # Export last 1 hour (default)\n python scripts/export-debug-log.py --range 1h\n\n # Export last 1 day\n python scripts/export-debug-log.py --range 1d\n\n # Filter by container or room\n python scripts/export-debug-log.py --range 1h --container agentteams-manager --room Worker\n\n # Disable PII redaction\n python scripts/export-debug-log.py --range 1h --no-redact\n\nOutput structure:\n debug-log/20260319-153000/\n ├── summary.txt\n ├── matrix-messages/\n │ └── RoomName_!roomid.jsonl\n └── agent-sessions/\n ├── agentteams-manager/\n │ └── {session-id}.jsonl\n └── agentteams-worker-xxx/\n └── {session-key}.jsonl\n\"\"\"\n\nimport argparse\nimport json\nimport os\nimport re\nimport subprocess\nimport sys\nimport time\nimport urllib.parse\nimport urllib.request\nimport urllib.error\nfrom datetime import datetime, timezone\nfrom pathlib import Path\n\n\n# ---------------------------------------------------------------------------\n# PII redaction\n# ---------------------------------------------------------------------------\n\n_PII_PATTERNS: list[tuple[str, re.Pattern]] = [\n (\"ID_CARD\", re.compile(r'\\b[1-9]\\d{5}(?:19|20)\\d{2}(?:0[1-9]|1[0-2])(?:0[1-9]|[12]\\d|3[01])\\d{3}[\\dXx]\\b')),\n (\"PHONE\", re.compile(r'(?<!\\d)(?:\\+?86[-\\s]?)?1[3-9]\\d{9}(?!\\d)')),\n (\"EMAIL\", re.compile(r'\\b[A-Za-z0-9._%+\\-]+@[A-Za-z0-9.\\-]+\\.[A-Za-z]{2,}\\b')),\n (\"BANK_CARD\", re.compile(r'\\b(?:6[0-9]{15,18}|4[0-9]{15}|5[1-5][0-9]{14}|3[47][0-9]{13}|62[0-9]{14,17})\\b')),\n (\"IP\", re.compile(r'\\b(?:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)\\.){3}(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)\\b')),\n (\"ALIYUN_AK\", re.compile(r'\\bLTAI[A-Za-z0-9]{12,30}\\b')),\n (\"ALIYUN_SK\", re.compile(r'(?i)(?:access_?key_?secret|secret_?access_?key)\\s*[:=]\\s*(\\S{20,})')),\n (\"AWS_AK\", re.compile(r'\\b(?:AKIA|ASIA)[A-Z0-9]{16}\\b')),\n (\"OPENAI_KEY\", re.compile(r'\\bsk-[A-Za-z0-9]{20,}\\b')),\n (\"ANTHROPIC_KEY\", re.compile(r'\\bsk-ant-[A-Za-z0-9\\-]{20,}\\b')),\n (\"DASHSCOPE_KEY\", re.compile(r'\\bsk-sp-[A-Za-z0-9]{20,}\\b')),\n (\"DEEPSEEK_KEY\", re.compile(r'\\bsk-[a-f0-9]{32,}\\b')),\n (\"BEARER\", re.compile(r'(?i)(Bearer\\s+)([A-Za-z0-9\\-_.]{20,})')),\n (\"SECRET_KV\", re.compile(\n r'(?i)((?:password|passwd|pwd|secret|token|api_?key|access_?key|secret_?key'\n r'|private_?key|credential|appkey|app_?secret|auth_?token|signing_?key'\n r'|client_?secret|master_?key)\\s*[:=]\\s*)'\n r'(\\S+)'\n )),\n (\"MATRIX_TOKEN\", re.compile(r'\\bsyt_[A-Za-z0-9_\\-]{10,}\\b')),\n (\"HEX_SECRET\", re.compile(r'\\b[A-Fa-f0-9]{32,}\\b')),\n (\"PASSPORT\", re.compile(r'\\b[EeGg]\\d{8}\\b')),\n (\"SSN\", re.compile(r'\\b\\d{3}-\\d{2}-\\d{4}\\b')),\n]\n\n_SECRET_FIELD_PATTERN = re.compile(\n r'(?i)^(?:password|passwd|pwd|secret|token|api[_-]?key'\n r'|access[_-]?key(?:[_-]?secret)?|secret[_-]?access[_-]?key'\n r'|secret[_-]?key|private[_-]?key|credential|app[_-]?key'\n r'|app[_-]?secret|auth[_-]?token|signing[_-]?key'\n r'|client[_-]?secret|master[_-]?key)$'\n)\n\n\ndef redact_pii(text: str) -> str:\n if not text:\n return text\n for name, pattern in _PII_PATTERNS:\n if name in (\"SECRET_KV\", \"ALIYUN_SK\", \"BEARER\"):\n text = pattern.sub(r'\\1****', text)\n else:\n text = pattern.sub('****', text)\n return text\n\n\ndef redact_json_strings(obj):\n if isinstance(obj, str):\n return redact_pii(obj)\n if isinstance(obj, list):\n return [redact_json_strings(v) for v in obj]\n if isinstance(obj, dict):\n redacted = {}\n for key, value in obj.items():\n if isinstance(key, str) and _SECRET_FIELD_PATTERN.fullmatch(key):\n redacted[key] = \"****\"\n else:\n redacted[key] = redact_json_strings(value)\n return redacted\n return obj\n\n\n# ---------------------------------------------------------------------------\n# Common helpers\n# ---------------------------------------------------------------------------\n\ndef parse_range(range_str: str) -> int:\n m = re.fullmatch(r\"(\\d+)\\s*(m|min|h|hr|hour|d|day)s?\", range_str.strip(), re.IGNORECASE)\n if not m:\n raise ValueError(f\"Invalid range format: '{range_str}'. Use e.g. 10m, 1h, 1d\")\n value, unit = int(m.group(1)), m.group(2)[0].lower()\n return value * {\"m\": 60, \"h\": 3600, \"d\": 86400}[unit]\n\n\ndef parse_ts(ts_str: str) -> float:\n if not ts_str:\n return 0\n ts_str = ts_str.replace(\"Z\", \"+00:00\")\n try:\n dt = datetime.fromisoformat(ts_str)\n if dt.tzinfo is None:\n dt = dt.replace(tzinfo=timezone.utc)\n return dt.timestamp()\n except Exception:\n return 0\n\n\ndef sanitize_filename(name: str) -> str:\n return re.sub(r'[^\\w\\-. ]', '_', name).strip()[:80]\n\n\ndef docker_exec(container: str, cmd: str) -> str:\n result = subprocess.run(\n [\"docker\", \"exec\", container, \"sh\", \"-c\", cmd],\n capture_output=True, text=True, timeout=30,\n )\n return result.stdout\n\n\ndef list_agentteams_containers() -> list[str]:\n names: list[str] = []\n for prefix in (\"agentteams-\",):\n result = subprocess.run(\n [\"docker\", \"ps\", \"--format\", \"{{.Names}}\", \"--filter\", f\"name={prefix}\"],\n capture_output=True, text=True, timeout=10,\n )\n names.extend(n.strip() for n in result.stdout.splitlines() if n.strip())\n return list(dict.fromkeys(names))\n\n\n# ---------------------------------------------------------------------------\n# Matrix messages export\n# ---------------------------------------------------------------------------\n\ndef load_env_file(path: str) -> dict[str, str]:\n env = {}\n try:\n with open(path) as f:\n for line in f:\n line = line.strip()\n if not line or line.startswith(\"#\"):\n continue\n if \"=\" in line:\n key, _, value = line.partition(\"=\")\n env[key.strip()] = value.strip()\n except FileNotFoundError:\n pass\n return env\n\n\ndef matrix_login(homeserver: str, user: str, password: str) -> str:\n url = f\"{homeserver.rstrip('/')}/_matrix/client/v3/login\"\n payload = json.dumps({\n \"type\": \"m.login.password\",\n \"identifier\": {\"type\": \"m.id.user\", \"user\": user},\n \"password\": password,\n }).encode()\n req = urllib.request.Request(url, data=payload, headers={\"Content-Type\": \"application/json\"})\n with urllib.request.urlopen(req, timeout=15) as resp:\n data = json.loads(resp.read())\n return data[\"access_token\"]\n\n\ndef matrix_api(homeserver: str, token: str, endpoint: str, params: dict | None = None) -> dict:\n url = f\"{homeserver.rstrip('/')}/_matrix/client/v3/{endpoint}\"\n if params:\n url += \"?\" + urllib.parse.urlencode(params)\n req = urllib.request.Request(url, headers={\"Authorization\": f\"Bearer {token}\"})\n try:\n with urllib.request.urlopen(req, timeout=30) as resp:\n return json.loads(resp.read())\n except urllib.error.HTTPError as e:\n body = e.read().decode(errors=\"replace\")\n print(f\" API error {e.code} on {endpoint}: {body}\", file=sys.stderr)\n raise\n\n\ndef fetch_room_messages(homeserver: str, token: str, room_id: str, since_ts: int) -> list[dict]:\n encoded = urllib.parse.quote(room_id)\n messages = []\n from_token = \"\"\n while True:\n params = {\"dir\": \"b\", \"limit\": \"100\"}\n if from_token:\n params[\"from\"] = from_token\n data = matrix_api(homeserver, token, f\"rooms/{encoded}/messages\", params)\n chunk = data.get(\"chunk\", [])\n if not chunk:\n break\n hit_boundary = False\n for event in chunk:\n if event.get(\"origin_server_ts\", 0) < since_ts:\n hit_boundary = True\n break\n messages.append(event)\n if hit_boundary:\n break\n next_token = data.get(\"end\")\n if not next_token or next_token == from_token:\n break\n from_token = next_token\n messages.reverse()\n return messages\n\n\ndef format_event(event: dict, redact: bool) -> dict:\n content = event.get(\"content\", {})\n ts = event.get(\"origin_server_ts\", 0)\n record = {\n \"event_id\": event.get(\"event_id\"),\n \"type\": event.get(\"type\"),\n \"sender\": event.get(\"sender\"),\n \"timestamp\": ts,\n \"time\": datetime.fromtimestamp(ts / 1000, tz=timezone.utc).isoformat(),\n }\n if event.get(\"type\") == \"m.room.message\":\n record[\"msgtype\"] = content.get(\"msgtype\")\n record[\"body\"] = content.get(\"body\")\n if content.get(\"format\"):\n record[\"format\"] = content[\"format\"]\n if content.get(\"url\"):\n record[\"url\"] = content[\"url\"]\n if content.get(\"m.relates_to\"):\n record[\"relates_to\"] = content[\"m.relates_to\"]\n else:\n record[\"content\"] = content\n return redact_json_strings(record) if redact else record\n\n\ndef export_matrix_messages(out_dir: Path, since_epoch: float, redact: bool,\n room_filter: str | None, env_file: str | None,\n homeserver: str, token: str,\n messages_only: bool) -> tuple[int, int]:\n \"\"\"Export Matrix messages. Returns (rooms_exported, message_count).\"\"\"\n env_path = env_file or os.path.expanduser(\"~/agentteams-manager.env\")\n agentteams_env = load_env_file(env_path)\n\n if not homeserver:\n if not agentteams_env:\n print(f\" [matrix] Cannot find {env_path}, skipping Matrix export\")\n return 0, 0\n port = agentteams_env.get(\"AGENTTEAMS_PORT_GATEWAY\", \"18080\")\n homeserver = f\"http://127.0.0.1:{port}\"\n\n if not token:\n # Use Manager token — Manager is in every room (DM, Worker, Project)\n manager_password = agentteams_env.get(\"AGENTTEAMS_MANAGER_PASSWORD\", \"\")\n if manager_password:\n try:\n token = matrix_login(homeserver, \"manager\", manager_password)\n except Exception:\n pass\n\n # Fallback to admin token if Manager login failed\n if not token:\n admin_user = agentteams_env.get(\"AGENTTEAMS_ADMIN_USER\", \"admin\")\n admin_password = agentteams_env.get(\"AGENTTEAMS_ADMIN_PASSWORD\", \"\")\n if not admin_password:\n print(f\" [matrix] No usable credentials found, skipping Matrix export\")\n return 0, 0\n try:\n token = matrix_login(homeserver, admin_user, admin_password)\n except Exception as e:\n print(f\" [matrix] Login failed: {e}, skipping Matrix export\")\n return 0, 0\n\n since_ts = int(since_epoch * 1000)\n out_dir.mkdir(parents=True, exist_ok=True)\n\n try:\n rooms = matrix_api(homeserver, token, \"joined_rooms\").get(\"joined_rooms\", [])\n except Exception as e:\n print(f\" [matrix] Failed to list rooms: {e}\")\n return 0, 0\n\n total_messages = 0\n total_rooms = 0\n\n for room_id in rooms:\n encoded = urllib.parse.quote(room_id)\n try:\n room_name = matrix_api(homeserver, token, f\"rooms/{encoded}/state/m.room.name\").get(\"name\", \"\")\n except Exception:\n room_name = \"\"\n\n if room_filter:\n if room_filter not in room_id and room_filter not in (room_name or \"\"):\n continue\n\n display = f\"{room_name} ({room_id})\" if room_name else room_id\n print(f\" {display} ... \", end=\"\", flush=True)\n\n messages = fetch_room_messages(homeserver, token, room_id, since_ts)\n if messages_only:\n messages = [e for e in messages if e.get(\"type\") == \"m.room.message\"]\n\n if not messages:\n print(\"0 messages, skipped\")\n continue\n\n name_part = sanitize_filename(room_name) if room_name else \"\"\n id_part = sanitize_filename(room_id)\n filename = f\"{name_part}_{id_part}.jsonl\" if name_part else f\"{id_part}.jsonl\"\n\n with open(out_dir / filename, \"w\", encoding=\"utf-8\") as f:\n for event in messages:\n record = format_event(event, redact=redact)\n f.write(json.dumps(record, ensure_ascii=False) + \"\\n\")\n\n print(f\"{len(messages)} messages -> {filename}\")\n total_messages += len(messages)\n total_rooms += 1\n\n return total_rooms, total_messages\n\n\n# ---------------------------------------------------------------------------\n# Agent sessions export (OpenClaw + CoPaw + Hermes)\n# ---------------------------------------------------------------------------\n\ndef detect_runtime(container: str) -> tuple[str, str]:\n \"\"\"Probe known absolute session-dir locations across runtimes & container types.\n\n Returns (runtime_name, absolute_sessions_dir). Empty tuple if none found.\n\n Layout reference (verified against worker/scripts/worker-entrypoint.sh,\n copaw/scripts/copaw-worker-entrypoint.sh, copaw/AGENTS.md and\n tests/lib/agent-metrics.sh):\n\n Manager (any runtime, HOME=/root/manager-workspace):\n openclaw -> /root/manager-workspace/.openclaw/agents/main/sessions\n hermes -> /root/manager-workspace/.hermes/sessions\n copaw -> /root/manager-workspace/.copaw/workspaces/default/sessions\n\n OpenClaw / Hermes Worker (HOME=/root/agentteams-fs/agents/<name>):\n openclaw -> /root/agentteams-fs/agents/<name>/.openclaw/agents/main/sessions\n hermes -> /root/agentteams-fs/agents/<name>/.hermes/sessions\n\n CoPaw Worker (HOME=/root/.agentteams-worker/<name>, also reachable via\n /root/agentteams-fs symlink that points to that same dir):\n copaw -> /root/.agentteams-worker/<name>/.copaw/workspaces/default/sessions\n copaw -> /root/agentteams-fs/.copaw/workspaces/default/sessions (alt)\n \"\"\"\n candidates: list[tuple[str, str]] = [\n (\"openclaw\", \"/root/manager-workspace/.openclaw/agents/main/sessions\"),\n (\"hermes\", \"/root/manager-workspace/.hermes/sessions\"),\n (\"copaw\", \"/root/manager-workspace/.copaw/workspaces/default/sessions\"),\n ]\n\n worker_name = docker_exec(container, \"echo $AGENTTEAMS_WORKER_NAME\").strip()\n if worker_name:\n candidates.extend([\n (\"openclaw\", f\"/root/agentteams-fs/agents/{worker_name}/.openclaw/agents/main/sessions\"),\n (\"hermes\", f\"/root/agentteams-fs/agents/{worker_name}/.hermes/sessions\"),\n (\"copaw\", f\"/root/.agentteams-worker/{worker_name}/.copaw/workspaces/default/sessions\"),\n (\"copaw\", \"/root/agentteams-fs/.copaw/workspaces/default/sessions\"),\n ])\n\n for runtime, path in candidates:\n if docker_exec(container, f\"test -d '{path}' && echo yes || echo no\").strip() == \"yes\":\n return runtime, path\n\n # Last-resort: scan from / for any known session dir layout.\n found = docker_exec(\n container,\n \"find / -maxdepth 7 \\\\( \"\n \"-path '*/.openclaw/agents/main/sessions' \"\n \"-o -path '*/.hermes/sessions' \"\n \"-o -path '*/.copaw/workspaces/default/sessions' \"\n \"\\\\) -type d 2>/dev/null | head -1\",\n ).strip()\n if found:\n if \"/.openclaw/\" in found:\n return \"openclaw\", found\n if \"/.hermes/\" in found:\n return \"hermes\", found\n return \"copaw\", found\n\n return \"\", \"\"\n\n\ndef export_openclaw_sessions(container: str, sessions_dir: str, since_epoch: float,\n out_dir: Path, redact: bool) -> tuple[int, int]:\n ls_output = docker_exec(container, f\"ls '{sessions_dir}'/*.jsonl 2>/dev/null\").strip()\n if not ls_output:\n return 0, 0\n\n total_sessions = 0\n total_events = 0\n\n for session_path in [f.strip() for f in ls_output.splitlines() if f.strip()]:\n filename = os.path.basename(session_path)\n\n first_line = docker_exec(container, f\"head -1 '{session_path}'\").strip()\n if not first_line:\n continue\n try:\n header = json.loads(first_line)\n except json.JSONDecodeError:\n continue\n\n last_line = docker_exec(container, f\"tail -1 '{session_path}'\").strip()\n try:\n last_ts = parse_ts(json.loads(last_line).get(\"timestamp\", \"\"))\n except Exception:\n last_ts = parse_ts(header.get(\"timestamp\", \"\"))\n\n if last_ts < since_epoch and last_ts > 0:\n continue\n\n raw = docker_exec(container, f\"cat '{session_path}'\")\n if not raw.strip():\n continue\n\n output_lines = []\n for line in raw.splitlines():\n line = line.strip()\n if not line:\n continue\n try:\n event = json.loads(line)\n except json.JSONDecodeError:\n continue\n event_ts = parse_ts(event.get(\"timestamp\", \"\"))\n if event.get(\"type\") != \"session\" and event_ts < since_epoch and event_ts > 0:\n continue\n if redact:\n event = redact_json_strings(event)\n output_lines.append(json.dumps(event, ensure_ascii=False))\n\n if len(output_lines) <= 1:\n continue\n\n with open(out_dir / filename, \"w\", encoding=\"utf-8\") as f:\n f.write(\"\\n\".join(output_lines) + \"\\n\")\n\n event_count = len(output_lines) - 1\n print(f\" {container}/{filename} (openclaw): {event_count} events\")\n total_sessions += 1\n total_events += event_count\n\n # sessions.json index\n try:\n raw = docker_exec(container, f\"cat '{sessions_dir}/sessions.json'\")\n if raw.strip():\n index = json.loads(raw)\n if redact:\n index = redact_json_strings(index)\n with open(out_dir / \"sessions.json\", \"w\", encoding=\"utf-8\") as f:\n json.dump(index, f, ensure_ascii=False, indent=2)\n except Exception:\n pass\n\n return total_sessions, total_events\n\n\ndef export_copaw_sessions(container: str, sessions_dir: str, since_epoch: float,\n out_dir: Path, redact: bool) -> tuple[int, int]:\n ls_output = docker_exec(container, f\"find '{sessions_dir}' -name '*.json' -type f 2>/dev/null\").strip()\n if not ls_output:\n return 0, 0\n\n total_sessions = 0\n total_events = 0\n\n for session_path in [f.strip() for f in ls_output.splitlines() if f.strip()]:\n raw = docker_exec(container, f\"cat '{session_path}'\")\n if not raw.strip():\n continue\n try:\n data = json.loads(raw)\n except json.JSONDecodeError:\n continue\n\n agent = data.get(\"agent\", {})\n memory = agent.get(\"memory\", {})\n content = memory.get(\"content\", [])\n if not content:\n continue\n\n basename = os.path.basename(session_path).replace(\".json\", \"\")\n header = {\n \"type\": \"session\",\n \"runtime\": \"copaw\",\n \"agent_name\": agent.get(\"name\", \"\"),\n \"session_key\": basename,\n \"compressed_summary\": memory.get(\"_compressed_summary\", \"\"),\n }\n\n messages_in_range = []\n for turn_idx, turn in enumerate(content):\n if not isinstance(turn, list):\n continue\n for msg in turn:\n if not isinstance(msg, dict):\n continue\n msg_ts = parse_ts(msg.get(\"timestamp\", \"\"))\n if msg_ts >= since_epoch or msg_ts == 0:\n event = {\n \"type\": \"message\",\n \"turn\": turn_idx,\n \"id\": msg.get(\"id\", \"\"),\n \"role\": msg.get(\"role\", \"\"),\n \"name\": msg.get(\"name\", \"\"),\n \"timestamp\": msg.get(\"timestamp\", \"\"),\n \"content\": msg.get(\"content\", []),\n }\n if msg.get(\"metadata\"):\n event[\"metadata\"] = msg[\"metadata\"]\n messages_in_range.append(event)\n\n if not messages_in_range:\n continue\n\n output_lines = []\n if redact:\n header = redact_json_strings(header)\n output_lines.append(json.dumps(header, ensure_ascii=False))\n for event in messages_in_range:\n if redact:\n event = redact_json_strings(event)\n output_lines.append(json.dumps(event, ensure_ascii=False))\n\n out_filename = basename + \".jsonl\"\n with open(out_dir / out_filename, \"w\", encoding=\"utf-8\") as f:\n f.write(\"\\n\".join(output_lines) + \"\\n\")\n\n event_count = len(output_lines) - 1\n print(f\" {container}/{out_filename} (copaw): {event_count} events\")\n total_sessions += 1\n total_events += event_count\n\n return total_sessions, total_events\n\n\ndef export_hermes_sessions(container: str, sessions_dir: str, since_epoch: float,\n out_dir: Path, redact: bool) -> tuple[int, int]:\n ls_output = docker_exec(container, f\"ls '{sessions_dir}'/*.jsonl 2>/dev/null\").strip()\n if not ls_output:\n return 0, 0\n\n total_sessions = 0\n total_events = 0\n\n for session_path in [f.strip() for f in ls_output.splitlines() if f.strip()]:\n raw = docker_exec(container, f\"cat '{session_path}'\")\n if not raw.strip():\n continue\n\n output_lines = []\n last_ts = 0.0\n for line in raw.splitlines():\n line = line.strip()\n if not line:\n continue\n try:\n event = json.loads(line)\n except json.JSONDecodeError:\n continue\n\n event_ts = parse_ts(event.get(\"timestamp\", \"\"))\n if event_ts:\n last_ts = max(last_ts, event_ts)\n\n role = event.get(\"role\", \"\")\n if role != \"session_meta\" and event_ts < since_epoch and event_ts > 0:\n continue\n\n if redact:\n event = redact_json_strings(event)\n output_lines.append(json.dumps(event, ensure_ascii=False))\n\n if not output_lines:\n continue\n\n if last_ts < since_epoch and last_ts > 0:\n continue\n\n filename = os.path.basename(session_path)\n with open(out_dir / filename, \"w\", encoding=\"utf-8\") as f:\n f.write(\"\\n\".join(output_lines) + \"\\n\")\n\n event_count = len(output_lines) - 1 if output_lines and '\"role\": \"session_meta\"' in output_lines[0] else len(output_lines)\n print(f\" {container}/{filename} (hermes): {event_count} events\")\n total_sessions += 1\n total_events += max(event_count, 0)\n\n hermes_home = str(Path(sessions_dir).parent)\n state_db = f\"{hermes_home}/state.db\"\n has_state_db = docker_exec(container, f\"test -f '{state_db}' && echo yes || echo no\").strip() == \"yes\"\n if has_state_db:\n raw = docker_exec(container, f\"python3 - <<'PY'\\nimport json, sqlite3\\nconn = sqlite3.connect('{state_db}')\\nconn.row_factory = sqlite3.Row\\nrows = conn.execute('SELECT * FROM sessions ORDER BY started_at DESC LIMIT 200').fetchall()\\nprint(json.dumps([dict(r) for r in rows], ensure_ascii=False, indent=2))\\nPY\")\n if raw.strip():\n try:\n data = json.loads(raw)\n if redact:\n data = redact_json_strings(data)\n with open(out_dir / \"sessions-db.json\", \"w\", encoding=\"utf-8\") as f:\n json.dump(data, f, ensure_ascii=False, indent=2)\n except json.JSONDecodeError:\n pass\n\n logs_dir = f\"{hermes_home}/logs\"\n has_logs_dir = docker_exec(container, f\"test -d '{logs_dir}' && echo yes || echo no\").strip() == \"yes\"\n if has_logs_dir:\n for log_name in (\"agent.log\", \"errors.log\", \"gateway.log\"):\n raw = docker_exec(container, f\"test -f '{logs_dir}/{log_name}' && cat '{logs_dir}/{log_name}' || true\")\n if raw.strip():\n content = redact_pii(raw) if redact else raw\n (out_dir / log_name).write_text(content, encoding=\"utf-8\")\n\n return total_sessions, total_events\n\n\ndef export_agent_sessions(out_dir: Path, since_epoch: float, redact: bool,\n container_filter: str | None) -> tuple[int, int]:\n \"\"\"Export agent sessions from all containers. Returns (session_count, event_count).\"\"\"\n containers = list_agentteams_containers()\n if container_filter:\n containers = [c for c in containers if container_filter in c]\n\n if not containers:\n print(\" [sessions] No matching AgentTeams containers found\")\n return 0, 0\n\n total_sessions = 0\n total_events = 0\n\n for container in containers:\n runtime, sessions_dir = detect_runtime(container)\n if not runtime:\n print(f\" {container}: no sessions directory, skipped\")\n continue\n\n container_dir = out_dir / container\n container_dir.mkdir(parents=True, exist_ok=True)\n\n if runtime == \"openclaw\":\n s, e = export_openclaw_sessions(container, sessions_dir, since_epoch,\n container_dir, redact)\n elif runtime == \"hermes\":\n s, e = export_hermes_sessions(container, sessions_dir, since_epoch,\n container_dir, redact)\n else:\n s, e = export_copaw_sessions(container, sessions_dir, since_epoch,\n container_dir, redact)\n\n if s == 0:\n if not any(container_dir.iterdir()):\n container_dir.rmdir()\n print(f\" {container} ({runtime}): no sessions in range, skipped\")\n else:\n print(f\" {container} ({runtime}): exported auxiliary debug artifacts without session events\")\n else:\n total_sessions += s\n total_events += e\n\n return total_sessions, total_events\n\n\n# ---------------------------------------------------------------------------\n# Main\n# ---------------------------------------------------------------------------\n\ndef main():\n parser = argparse.ArgumentParser(\n description=\"Export AgentTeams debug logs (Matrix messages + agent sessions)\"\n )\n parser.add_argument(\"--range\", \"-r\", required=True, dest=\"time_range\",\n help=\"Time range to export, e.g. 10m, 1h, 1d\")\n parser.add_argument(\"--container\", \"-c\", default=None,\n help=\"Only export sessions from containers matching this substring\")\n parser.add_argument(\"--room\", default=None,\n help=\"Only export Matrix rooms matching this substring\")\n parser.add_argument(\"--homeserver\", \"-s\", default=\"\",\n help=\"Matrix homeserver URL (auto-detected from env file)\")\n parser.add_argument(\"--token\", \"-t\", default=\"\",\n help=\"Matrix access token (auto-detected from env file)\")\n parser.add_argument(\"--env-file\", default=None,\n help=\"Path to agentteams-manager.env (default: ~/agentteams-manager.env)\")\n parser.add_argument(\"--messages-only\", action=\"store_true\",\n help=\"Only export m.room.message events (skip state events)\")\n parser.add_argument(\"--no-redact\", action=\"store_true\",\n help=\"Disable PII redaction\")\n args = parser.parse_args()\n\n range_seconds = parse_range(args.time_range)\n since_epoch = time.time() - range_seconds\n since_human = datetime.fromtimestamp(since_epoch, tz=timezone.utc).isoformat()\n now_str = datetime.now().strftime(\"%Y%m%d-%H%M%S\")\n\n run_dir = Path(\"debug-log\") / now_str\n run_dir.mkdir(parents=True, exist_ok=True)\n redact = not args.no_redact\n\n print(f\"AgentTeams Debug Log Export\")\n print(f\" Range: last {args.time_range} (since {since_human})\")\n print(f\" Output: {run_dir.resolve()}\")\n print(f\" PII redaction: {'on' if redact else 'off'}\")\n print()\n\n # --- Matrix messages ---\n print(\"=== Matrix Messages ===\")\n matrix_dir = run_dir / \"matrix-messages\"\n rooms, messages = export_matrix_messages(\n matrix_dir, since_epoch, redact,\n room_filter=args.room, env_file=args.env_file,\n homeserver=args.homeserver, token=args.token,\n messages_only=args.messages_only,\n )\n if rooms == 0 and matrix_dir.exists() and not any(matrix_dir.iterdir()):\n matrix_dir.rmdir()\n print()\n\n # --- Agent sessions ---\n print(\"=== Agent Sessions ===\")\n sessions_dir = run_dir / \"agent-sessions\"\n sessions, events = export_agent_sessions(\n sessions_dir, since_epoch, redact,\n container_filter=args.container,\n )\n if sessions == 0 and sessions_dir.exists() and not any(sessions_dir.iterdir()):\n sessions_dir.rmdir()\n print()\n\n # --- Summary ---\n summary = (\n f\"AgentTeams Debug Log\\n\"\n f\"Exported at: {now_str}\\n\"\n f\"Range: last {args.time_range} (since {since_human})\\n\"\n f\"PII redaction: {'on' if redact else 'off'}\\n\"\n f\"\\n\"\n f\"Matrix messages: {messages} messages from {rooms} rooms\\n\"\n f\"Agent sessions: {events} events from {sessions} sessions\\n\"\n )\n (run_dir / \"summary.txt\").write_text(summary)\n\n print(f\"Done. {messages} messages from {rooms} rooms, {events} events from {sessions} sessions\")\n print(f\"Output: {run_dir.resolve()}\")\n\n\nif __name__ == \"__main__\":\n main()\n"}, "e6fa64c0cb7b14600c6abbf9b4a25f47bf544c1f": {"t": 1785764737.1127431, "n": 29573, "h": "f8af038cec4b4aef", "c": "#!/usr/bin/env python3\nfrom __future__ import annotations\n\"\"\"\nExport AgentTeams debug logs: Matrix messages + agent session logs.\n\nUsage:\n # Export last 1 hour (default)\n python scripts/export-debug-log.py --range 1h\n\n # Export last 1 day\n python scripts/export-debug-log.py --range 1d\n\n # Filter by container or room\n python scripts/export-debug-log.py --range 1h --container agentteams-manager --room Worker\n\n # Disable PII redaction\n python scripts/export-debug-log.py --range 1h --no-redact\n\nOutput structure:\n debug-log/20260319-153000/\n ├── summary.txt\n ├── matrix-messages/\n │ └── RoomName_!roomid.jsonl\n └── agent-sessions/\n ├── agentteams-manager/\n │ └── {session-id}.jsonl\n └── agentteams-worker-xxx/\n └── {session-key}.jsonl\n\"\"\"\n\nimport argparse\nimport json\nimport os\nimport re\nimport subprocess\nimport sys\nimport time\nimport urllib.parse\nimport urllib.request\nimport urllib.error\nfrom datetime import datetime, timezone\nfrom pathlib import Path\n\n\n# ---------------------------------------------------------------------------\n# PII redaction\n# ---------------------------------------------------------------------------\n\n_PII_PATTERNS: list[tuple[str, re.Pattern]] = [\n (\"ID_CARD\", re.compile(r'\\b[1-9]\\d{5}(?:19|20)\\d{2}(?:0[1-9]|1[0-2])(?:0[1-9]|[12]\\d|3[01])\\d{3}[\\dXx]\\b')),\n (\"PHONE\", re.compile(r'(?<!\\d)(?:\\+?86[-\\s]?)?1[3-9]\\d{9}(?!\\d)')),\n (\"EMAIL\", re.compile(r'\\b[A-Za-z0-9._%+\\-]+@[A-Za-z0-9.\\-]+\\.[A-Za-z]{2,}\\b')),\n (\"BANK_CARD\", re.compile(r'\\b(?:6[0-9]{15,18}|4[0-9]{15}|5[1-5][0-9]{14}|3[47][0-9]{13}|62[0-9]{14,17})\\b')),\n (\"IP\", re.compile(r'\\b(?:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)\\.){3}(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)\\b')),\n (\"ALIYUN_AK\", re.compile(r'\\bLTAI[A-Za-z0-9]{12,30}\\b')),\n (\"ALIYUN_SK\", re.compile(r'(?i)(?:access_?key_?secret|secret_?access_?key)\\s*[:=]\\s*(\\S{20,})')),\n (\"AWS_AK\", re.compile(r'\\b(?:AKIA|ASIA)[A-Z0-9]{16}\\b')),\n (\"OPENAI_KEY\", re.compile(r'\\bsk-[A-Za-z0-9]{20,}\\b')),\n (\"ANTHROPIC_KEY\", re.compile(r'\\bsk-ant-[A-Za-z0-9\\-]{20,}\\b')),\n (\"DASHSCOPE_KEY\", re.compile(r'\\bsk-sp-[A-Za-z0-9]{20,}\\b')),\n (\"DEEPSEEK_KEY\", re.compile(r'\\bsk-[a-f0-9]{32,}\\b')),\n (\"BEARER\", re.compile(r'(?i)(Bearer\\s+)([A-Za-z0-9\\-_.]{20,})')),\n (\"SECRET_KV\", re.compile(\n r'(?i)((?:password|passwd|pwd|secret|token|api_?key|access_?key|secret_?key'\n r'|private_?key|credential|appkey|app_?secret|auth_?token|signing_?key'\n r'|client_?secret|master_?key)\\s*[:=]\\s*)'\n r'(\\S+)'\n )),\n (\"MATRIX_TOKEN\", re.compile(r'\\bsyt_[A-Za-z0-9_\\-]{10,}\\b')),\n (\"HEX_SECRET\", re.compile(r'\\b[A-Fa-f0-9]{32,}\\b')),\n (\"PASSPORT\", re.compile(r'\\b[EeGg]\\d{8}\\b')),\n (\"SSN\", re.compile(r'\\b\\d{3}-\\d{2}-\\d{4}\\b')),\n]\n\n_SECRET_FIELD_PATTERN = re.compile(\n r'(?i)^(?:password|passwd|pwd|secret|token|api[_-]?key'\n r'|access[_-]?key(?:[_-]?secret)?|secret[_-]?access[_-]?key'\n r'|secret[_-]?key|private[_-]?key|credential|app[_-]?key'\n r'|app[_-]?secret|auth[_-]?token|signing[_-]?key'\n r'|client[_-]?secret|master[_-]?key)$'\n)\n\n\ndef redact_pii(text: str) -> str:\n if not text:\n return text\n for name, pattern in _PII_PATTERNS:\n if name in (\"SECRET_KV\", \"ALIYUN_SK\", \"BEARER\"):\n text = pattern.sub(r'\\1****', text)\n else:\n text = pattern.sub('****', text)\n return text\n\n\ndef redact_json_strings(obj):\n if isinstance(obj, str):\n return redact_pii(obj)\n if isinstance(obj, list):\n return [redact_json_strings(v) for v in obj]\n if isinstance(obj, dict):\n redacted = {}\n for key, value in obj.items():\n if isinstance(key, str) and _SECRET_FIELD_PATTERN.fullmatch(key):\n redacted[key] = \"****\"\n else:\n redacted[key] = redact_json_strings(value)\n return redacted\n return obj\n\n\n# ---------------------------------------------------------------------------\n# Common helpers\n# ---------------------------------------------------------------------------\n\ndef parse_range(range_str: str) -> int:\n m = re.fullmatch(r\"(\\d+)\\s*(m|min|h|hr|hour|d|day)s?\", range_str.strip(), re.IGNORECASE)\n if not m:\n raise ValueError(f\"Invalid range format: '{range_str}'. Use e.g. 10m, 1h, 1d\")\n value, unit = int(m.group(1)), m.group(2)[0].lower()\n return value * {\"m\": 60, \"h\": 3600, \"d\": 86400}[unit]\n\n\ndef parse_ts(ts_str: str) -> float:\n if not ts_str:\n return 0\n ts_str = ts_str.replace(\"Z\", \"+00:00\")\n try:\n dt = datetime.fromisoformat(ts_str)\n if dt.tzinfo is None:\n dt = dt.replace(tzinfo=timezone.utc)\n return dt.timestamp()\n except Exception:\n return 0\n\n\ndef sanitize_filename(name: str) -> str:\n return re.sub(r'[^\\w\\-. ]', '_', name).strip()[:80]\n\n\ndef docker_exec(container: str, cmd: str) -> str:\n result = subprocess.run(\n [\"docker\", \"exec\", container, \"sh\", \"-c\", cmd],\n capture_output=True, text=True, timeout=30,\n )\n return result.stdout\n\n\ndef list_hiclaw_containers() -> list[str]:\n names: list[str] = []\n for prefix in (\"agentteams-\", \"hiclaw-\"):\n result = subprocess.run(\n [\"docker\", \"ps\", \"--format\", \"{{.Names}}\", \"--filter\", f\"name={prefix}\"],\n capture_output=True, text=True, timeout=10,\n )\n names.extend(n.strip() for n in result.stdout.splitlines() if n.strip())\n return list(dict.fromkeys(names))\n\n\n# ---------------------------------------------------------------------------\n# Matrix messages export\n# ---------------------------------------------------------------------------\n\ndef load_env_file(path: str) -> dict[str, str]:\n env = {}\n try:\n with open(path) as f:\n for line in f:\n line = line.strip()\n if not line or line.startswith(\"#\"):\n continue\n if \"=\" in line:\n key, _, value = line.partition(\"=\")\n env[key.strip()] = value.strip()\n except FileNotFoundError:\n pass\n return env\n\n\ndef matrix_login(homeserver: str, user: str, password: str) -> str:\n url = f\"{homeserver.rstrip('/')}/_matrix/client/v3/login\"\n payload = json.dumps({\n \"type\": \"m.login.password\",\n \"identifier\": {\"type\": \"m.id.user\", \"user\": user},\n \"password\": password,\n }).encode()\n req = urllib.request.Request(url, data=payload, headers={\"Content-Type\": \"application/json\"})\n with urllib.request.urlopen(req, timeout=15) as resp:\n data = json.loads(resp.read())\n return data[\"access_token\"]\n\n\ndef matrix_api(homeserver: str, token: str, endpoint: str, params: dict | None = None) -> dict:\n url = f\"{homeserver.rstrip('/')}/_matrix/client/v3/{endpoint}\"\n if params:\n url += \"?\" + urllib.parse.urlencode(params)\n req = urllib.request.Request(url, headers={\"Authorization\": f\"Bearer {token}\"})\n try:\n with urllib.request.urlopen(req, timeout=30) as resp:\n return json.loads(resp.read())\n except urllib.error.HTTPError as e:\n body = e.read().decode(errors=\"replace\")\n print(f\" API error {e.code} on {endpoint}: {body}\", file=sys.stderr)\n raise\n\n\ndef fetch_room_messages(homeserver: str, token: str, room_id: str, since_ts: int) -> list[dict]:\n encoded = urllib.parse.quote(room_id)\n messages = []\n from_token = \"\"\n while True:\n params = {\"dir\": \"b\", \"limit\": \"100\"}\n if from_token:\n params[\"from\"] = from_token\n data = matrix_api(homeserver, token, f\"rooms/{encoded}/messages\", params)\n chunk = data.get(\"chunk\", [])\n if not chunk:\n break\n hit_boundary = False\n for event in chunk:\n if event.get(\"origin_server_ts\", 0) < since_ts:\n hit_boundary = True\n break\n messages.append(event)\n if hit_boundary:\n break\n next_token = data.get(\"end\")\n if not next_token or next_token == from_token:\n break\n from_token = next_token\n messages.reverse()\n return messages\n\n\ndef format_event(event: dict, redact: bool) -> dict:\n content = event.get(\"content\", {})\n ts = event.get(\"origin_server_ts\", 0)\n record = {\n \"event_id\": event.get(\"event_id\"),\n \"type\": event.get(\"type\"),\n \"sender\": event.get(\"sender\"),\n \"timestamp\": ts,\n \"time\": datetime.fromtimestamp(ts / 1000, tz=timezone.utc).isoformat(),\n }\n if event.get(\"type\") == \"m.room.message\":\n record[\"msgtype\"] = content.get(\"msgtype\")\n record[\"body\"] = content.get(\"body\")\n if content.get(\"format\"):\n record[\"format\"] = content[\"format\"]\n if content.get(\"url\"):\n record[\"url\"] = content[\"url\"]\n if content.get(\"m.relates_to\"):\n record[\"relates_to\"] = content[\"m.relates_to\"]\n else:\n record[\"content\"] = content\n return redact_json_strings(record) if redact else record\n\n\ndef export_matrix_messages(out_dir: Path, since_epoch: float, redact: bool,\n room_filter: str | None, env_file: str | None,\n homeserver: str, token: str,\n messages_only: bool) -> tuple[int, int]:\n \"\"\"Export Matrix messages. Returns (rooms_exported, message_count).\"\"\"\n env_path = env_file or os.path.expanduser(\"~/agentteams-manager.env\")\n hiclaw_env = load_env_file(env_path)\n\n if not homeserver:\n if not hiclaw_env:\n print(f\" [matrix] Cannot find {env_path}, skipping Matrix export\")\n return 0, 0\n port = hiclaw_env.get(\"AGENTTEAMS_PORT_GATEWAY\", \"18080\")\n homeserver = f\"http://127.0.0.1:{port}\"\n\n if not token:\n # Use Manager token — Manager is in every room (DM, Worker, Project)\n manager_password = hiclaw_env.get(\"AGENTTEAMS_MANAGER_PASSWORD\", \"\")\n if manager_password:\n try:\n token = matrix_login(homeserver, \"manager\", manager_password)\n except Exception:\n pass\n\n # Fallback to admin token if Manager login failed\n if not token:\n admin_user = hiclaw_env.get(\"AGENTTEAMS_ADMIN_USER\", \"admin\")\n admin_password = hiclaw_env.get(\"AGENTTEAMS_ADMIN_PASSWORD\", \"\")\n if not admin_password:\n print(f\" [matrix] No usable credentials found, skipping Matrix export\")\n return 0, 0\n try:\n token = matrix_login(homeserver, admin_user, admin_password)\n except Exception as e:\n print(f\" [matrix] Login failed: {e}, skipping Matrix export\")\n return 0, 0\n\n since_ts = int(since_epoch * 1000)\n out_dir.mkdir(parents=True, exist_ok=True)\n\n try:\n rooms = matrix_api(homeserver, token, \"joined_rooms\").get(\"joined_rooms\", [])\n except Exception as e:\n print(f\" [matrix] Failed to list rooms: {e}\")\n return 0, 0\n\n total_messages = 0\n total_rooms = 0\n\n for room_id in rooms:\n encoded = urllib.parse.quote(room_id)\n try:\n room_name = matrix_api(homeserver, token, f\"rooms/{encoded}/state/m.room.name\").get(\"name\", \"\")\n except Exception:\n room_name = \"\"\n\n if room_filter:\n if room_filter not in room_id and room_filter not in (room_name or \"\"):\n continue\n\n display = f\"{room_name} ({room_id})\" if room_name else room_id\n print(f\" {display} ... \", end=\"\", flush=True)\n\n messages = fetch_room_messages(homeserver, token, room_id, since_ts)\n if messages_only:\n messages = [e for e in messages if e.get(\"type\") == \"m.room.message\"]\n\n if not messages:\n print(\"0 messages, skipped\")\n continue\n\n name_part = sanitize_filename(room_name) if room_name else \"\"\n id_part = sanitize_filename(room_id)\n filename = f\"{name_part}_{id_part}.jsonl\" if name_part else f\"{id_part}.jsonl\"\n\n with open(out_dir / filename, \"w\", encoding=\"utf-8\") as f:\n for event in messages:\n record = format_event(event, redact=redact)\n f.write(json.dumps(record, ensure_ascii=False) + \"\\n\")\n\n print(f\"{len(messages)} messages -> {filename}\")\n total_messages += len(messages)\n total_rooms += 1\n\n return total_rooms, total_messages\n\n\n# ---------------------------------------------------------------------------\n# Agent sessions export (OpenClaw + CoPaw + Hermes)\n# ---------------------------------------------------------------------------\n\ndef detect_runtime(container: str) -> tuple[str, str]:\n \"\"\"Probe known absolute session-dir locations across runtimes & container types.\n\n Returns (runtime_name, absolute_sessions_dir). Empty tuple if none found.\n\n Layout reference (verified against worker/scripts/worker-entrypoint.sh,\n copaw/scripts/copaw-worker-entrypoint.sh, copaw/AGENTS.md and\n tests/lib/agent-metrics.sh):\n\n Manager (any runtime, HOME=/root/manager-workspace):\n openclaw -> /root/manager-workspace/.openclaw/agents/main/sessions\n hermes -> /root/manager-workspace/.hermes/sessions\n copaw -> /root/manager-workspace/.copaw/workspaces/default/sessions\n\n OpenClaw / Hermes Worker (HOME=/root/agentteams-fs/agents/<name>):\n openclaw -> /root/agentteams-fs/agents/<name>/.openclaw/agents/main/sessions\n hermes -> /root/agentteams-fs/agents/<name>/.hermes/sessions\n\n CoPaw Worker (HOME=/root/.agentteams-worker/<name>, also reachable via\n /root/agentteams-fs symlink that points to that same dir):\n copaw -> /root/.agentteams-worker/<name>/.copaw/workspaces/default/sessions\n copaw -> /root/agentteams-fs/.copaw/workspaces/default/sessions (alt)\n \"\"\"\n candidates: list[tuple[str, str]] = [\n (\"openclaw\", \"/root/manager-workspace/.openclaw/agents/main/sessions\"),\n (\"hermes\", \"/root/manager-workspace/.hermes/sessions\"),\n (\"copaw\", \"/root/manager-workspace/.copaw/workspaces/default/sessions\"),\n ]\n\n worker_name = docker_exec(container, \"echo $AGENTTEAMS_WORKER_NAME\").strip()\n if worker_name:\n candidates.extend([\n (\"openclaw\", f\"/root/agentteams-fs/agents/{worker_name}/.openclaw/agents/main/sessions\"),\n (\"hermes\", f\"/root/agentteams-fs/agents/{worker_name}/.hermes/sessions\"),\n (\"copaw\", f\"/root/.agentteams-worker/{worker_name}/.copaw/workspaces/default/sessions\"),\n (\"copaw\", \"/root/agentteams-fs/.copaw/workspaces/default/sessions\"),\n ])\n\n for runtime, path in candidates:\n if docker_exec(container, f\"test -d '{path}' && echo yes || echo no\").strip() == \"yes\":\n return runtime, path\n\n # Last-resort: scan from / for any known session dir layout.\n found = docker_exec(\n container,\n \"find / -maxdepth 7 \\\\( \"\n \"-path '*/.openclaw/agents/main/sessions' \"\n \"-o -path '*/.hermes/sessions' \"\n \"-o -path '*/.copaw/workspaces/default/sessions' \"\n \"\\\\) -type d 2>/dev/null | head -1\",\n ).strip()\n if found:\n if \"/.openclaw/\" in found:\n return \"openclaw\", found\n if \"/.hermes/\" in found:\n return \"hermes\", found\n return \"copaw\", found\n\n return \"\", \"\"\n\n\ndef export_openclaw_sessions(container: str, sessions_dir: str, since_epoch: float,\n out_dir: Path, redact: bool) -> tuple[int, int]:\n ls_output = docker_exec(container, f\"ls '{sessions_dir}'/*.jsonl 2>/dev/null\").strip()\n if not ls_output:\n return 0, 0\n\n total_sessions = 0\n total_events = 0\n\n for session_path in [f.strip() for f in ls_output.splitlines() if f.strip()]:\n filename = os.path.basename(session_path)\n\n first_line = docker_exec(container, f\"head -1 '{session_path}'\").strip()\n if not first_line:\n continue\n try:\n header = json.loads(first_line)\n except json.JSONDecodeError:\n continue\n\n last_line = docker_exec(container, f\"tail -1 '{session_path}'\").strip()\n try:\n last_ts = parse_ts(json.loads(last_line).get(\"timestamp\", \"\"))\n except Exception:\n last_ts = parse_ts(header.get(\"timestamp\", \"\"))\n\n if last_ts < since_epoch and last_ts > 0:\n continue\n\n raw = docker_exec(container, f\"cat '{session_path}'\")\n if not raw.strip():\n continue\n\n output_lines = []\n for line in raw.splitlines():\n line = line.strip()\n if not line:\n continue\n try:\n event = json.loads(line)\n except json.JSONDecodeError:\n continue\n event_ts = parse_ts(event.get(\"timestamp\", \"\"))\n if event.get(\"type\") != \"session\" and event_ts < since_epoch and event_ts > 0:\n continue\n if redact:\n event = redact_json_strings(event)\n output_lines.append(json.dumps(event, ensure_ascii=False))\n\n if len(output_lines) <= 1:\n continue\n\n with open(out_dir / filename, \"w\", encoding=\"utf-8\") as f:\n f.write(\"\\n\".join(output_lines) + \"\\n\")\n\n event_count = len(output_lines) - 1\n print(f\" {container}/{filename} (openclaw): {event_count} events\")\n total_sessions += 1\n total_events += event_count\n\n # sessions.json index\n try:\n raw = docker_exec(container, f\"cat '{sessions_dir}/sessions.json'\")\n if raw.strip():\n index = json.loads(raw)\n if redact:\n index = redact_json_strings(index)\n with open(out_dir / \"sessions.json\", \"w\", encoding=\"utf-8\") as f:\n json.dump(index, f, ensure_ascii=False, indent=2)\n except Exception:\n pass\n\n return total_sessions, total_events\n\n\ndef export_copaw_sessions(container: str, sessions_dir: str, since_epoch: float,\n out_dir: Path, redact: bool) -> tuple[int, int]:\n ls_output = docker_exec(container, f\"find '{sessions_dir}' -name '*.json' -type f 2>/dev/null\").strip()\n if not ls_output:\n return 0, 0\n\n total_sessions = 0\n total_events = 0\n\n for session_path in [f.strip() for f in ls_output.splitlines() if f.strip()]:\n raw = docker_exec(container, f\"cat '{session_path}'\")\n if not raw.strip():\n continue\n try:\n data = json.loads(raw)\n except json.JSONDecodeError:\n continue\n\n agent = data.get(\"agent\", {})\n memory = agent.get(\"memory\", {})\n content = memory.get(\"content\", [])\n if not content:\n continue\n\n basename = os.path.basename(session_path).replace(\".json\", \"\")\n header = {\n \"type\": \"session\",\n \"runtime\": \"copaw\",\n \"agent_name\": agent.get(\"name\", \"\"),\n \"session_key\": basename,\n \"compressed_summary\": memory.get(\"_compressed_summary\", \"\"),\n }\n\n messages_in_range = []\n for turn_idx, turn in enumerate(content):\n if not isinstance(turn, list):\n continue\n for msg in turn:\n if not isinstance(msg, dict):\n continue\n msg_ts = parse_ts(msg.get(\"timestamp\", \"\"))\n if msg_ts >= since_epoch or msg_ts == 0:\n event = {\n \"type\": \"message\",\n \"turn\": turn_idx,\n \"id\": msg.get(\"id\", \"\"),\n \"role\": msg.get(\"role\", \"\"),\n \"name\": msg.get(\"name\", \"\"),\n \"timestamp\": msg.get(\"timestamp\", \"\"),\n \"content\": msg.get(\"content\", []),\n }\n if msg.get(\"metadata\"):\n event[\"metadata\"] = msg[\"metadata\"]\n messages_in_range.append(event)\n\n if not messages_in_range:\n continue\n\n output_lines = []\n if redact:\n header = redact_json_strings(header)\n output_lines.append(json.dumps(header, ensure_ascii=False))\n for event in messages_in_range:\n if redact:\n event = redact_json_strings(event)\n output_lines.append(json.dumps(event, ensure_ascii=False))\n\n out_filename = basename + \".jsonl\"\n with open(out_dir / out_filename, \"w\", encoding=\"utf-8\") as f:\n f.write(\"\\n\".join(output_lines) + \"\\n\")\n\n event_count = len(output_lines) - 1\n print(f\" {container}/{out_filename} (copaw): {event_count} events\")\n total_sessions += 1\n total_events += event_count\n\n return total_sessions, total_events\n\n\ndef export_hermes_sessions(container: str, sessions_dir: str, since_epoch: float,\n out_dir: Path, redact: bool) -> tuple[int, int]:\n ls_output = docker_exec(container, f\"ls '{sessions_dir}'/*.jsonl 2>/dev/null\").strip()\n if not ls_output:\n return 0, 0\n\n total_sessions = 0\n total_events = 0\n\n for session_path in [f.strip() for f in ls_output.splitlines() if f.strip()]:\n raw = docker_exec(container, f\"cat '{session_path}'\")\n if not raw.strip():\n continue\n\n output_lines = []\n last_ts = 0.0\n for line in raw.splitlines():\n line = line.strip()\n if not line:\n continue\n try:\n event = json.loads(line)\n except json.JSONDecodeError:\n continue\n\n event_ts = parse_ts(event.get(\"timestamp\", \"\"))\n if event_ts:\n last_ts = max(last_ts, event_ts)\n\n role = event.get(\"role\", \"\")\n if role != \"session_meta\" and event_ts < since_epoch and event_ts > 0:\n continue\n\n if redact:\n event = redact_json_strings(event)\n output_lines.append(json.dumps(event, ensure_ascii=False))\n\n if not output_lines:\n continue\n\n if last_ts < since_epoch and last_ts > 0:\n continue\n\n filename = os.path.basename(session_path)\n with open(out_dir / filename, \"w\", encoding=\"utf-8\") as f:\n f.write(\"\\n\".join(output_lines) + \"\\n\")\n\n event_count = len(output_lines) - 1 if output_lines and '\"role\": \"session_meta\"' in output_lines[0] else len(output_lines)\n print(f\" {container}/{filename} (hermes): {event_count} events\")\n total_sessions += 1\n total_events += max(event_count, 0)\n\n hermes_home = str(Path(sessions_dir).parent)\n state_db = f\"{hermes_home}/state.db\"\n has_state_db = docker_exec(container, f\"test -f '{state_db}' && echo yes || echo no\").strip() == \"yes\"\n if has_state_db:\n raw = docker_exec(container, f\"python3 - <<'PY'\\nimport json, sqlite3\\nconn = sqlite3.connect('{state_db}')\\nconn.row_factory = sqlite3.Row\\nrows = conn.execute('SELECT * FROM sessions ORDER BY started_at DESC LIMIT 200').fetchall()\\nprint(json.dumps([dict(r) for r in rows], ensure_ascii=False, indent=2))\\nPY\")\n if raw.strip():\n try:\n data = json.loads(raw)\n if redact:\n data = redact_json_strings(data)\n with open(out_dir / \"sessions-db.json\", \"w\", encoding=\"utf-8\") as f:\n json.dump(data, f, ensure_ascii=False, indent=2)\n except json.JSONDecodeError:\n pass\n\n logs_dir = f\"{hermes_home}/logs\"\n has_logs_dir = docker_exec(container, f\"test -d '{logs_dir}' && echo yes || echo no\").strip() == \"yes\"\n if has_logs_dir:\n for log_name in (\"agent.log\", \"errors.log\", \"gateway.log\"):\n raw = docker_exec(container, f\"test -f '{logs_dir}/{log_name}' && cat '{logs_dir}/{log_name}' || true\")\n if raw.strip():\n content = redact_pii(raw) if redact else raw\n (out_dir / log_name).write_text(content, encoding=\"utf-8\")\n\n return total_sessions, total_events\n\n\ndef export_agent_sessions(out_dir: Path, since_epoch: float, redact: bool,\n container_filter: str | None) -> tuple[int, int]:\n \"\"\"Export agent sessions from all containers. Returns (session_count, event_count).\"\"\"\n containers = list_hiclaw_containers()\n if container_filter:\n containers = [c for c in containers if container_filter in c]\n\n if not containers:\n print(\" [sessions] No matching AgentTeams containers found\")\n return 0, 0\n\n total_sessions = 0\n total_events = 0\n\n for container in containers:\n runtime, sessions_dir = detect_runtime(container)\n if not runtime:\n print(f\" {container}: no sessions directory, skipped\")\n continue\n\n container_dir = out_dir / container\n container_dir.mkdir(parents=True, exist_ok=True)\n\n if runtime == \"openclaw\":\n s, e = export_openclaw_sessions(container, sessions_dir, since_epoch,\n container_dir, redact)\n elif runtime == \"hermes\":\n s, e = export_hermes_sessions(container, sessions_dir, since_epoch,\n container_dir, redact)\n else:\n s, e = export_copaw_sessions(container, sessions_dir, since_epoch,\n container_dir, redact)\n\n if s == 0:\n if not any(container_dir.iterdir()):\n container_dir.rmdir()\n print(f\" {container} ({runtime}): no sessions in range, skipped\")\n else:\n print(f\" {container} ({runtime}): exported auxiliary debug artifacts without session events\")\n else:\n total_sessions += s\n total_events += e\n\n return total_sessions, total_events\n\n\n# ---------------------------------------------------------------------------\n# Main\n# ---------------------------------------------------------------------------\n\ndef main():\n parser = argparse.ArgumentParser(\n description=\"Export AgentTeams debug logs (Matrix messages + agent sessions)\"\n )\n parser.add_argument(\"--range\", \"-r\", required=True, dest=\"time_range\",\n help=\"Time range to export, e.g. 10m, 1h, 1d\")\n parser.add_argument(\"--container\", \"-c\", default=None,\n help=\"Only export sessions from containers matching this substring\")\n parser.add_argument(\"--room\", default=None,\n help=\"Only export Matrix rooms matching this substring\")\n parser.add_argument(\"--homeserver\", \"-s\", default=\"\",\n help=\"Matrix homeserver URL (auto-detected from env file)\")\n parser.add_argument(\"--token\", \"-t\", default=\"\",\n help=\"Matrix access token (auto-detected from env file)\")\n parser.add_argument(\"--env-file\", default=None,\n help=\"Path to agentteams-manager.env (default: ~/agentteams-manager.env)\")\n parser.add_argument(\"--messages-only\", action=\"store_true\",\n help=\"Only export m.room.message events (skip state events)\")\n parser.add_argument(\"--no-redact\", action=\"store_true\",\n help=\"Disable PII redaction\")\n args = parser.parse_args()\n\n range_seconds = parse_range(args.time_range)\n since_epoch = time.time() - range_seconds\n since_human = datetime.fromtimestamp(since_epoch, tz=timezone.utc).isoformat()\n now_str = datetime.now().strftime(\"%Y%m%d-%H%M%S\")\n\n run_dir = Path(\"debug-log\") / now_str\n run_dir.mkdir(parents=True, exist_ok=True)\n redact = not args.no_redact\n\n print(f\"AgentTeams Debug Log Export\")\n print(f\" Range: last {args.time_range} (since {since_human})\")\n print(f\" Output: {run_dir.resolve()}\")\n print(f\" PII redaction: {'on' if redact else 'off'}\")\n print()\n\n # --- Matrix messages ---\n print(\"=== Matrix Messages ===\")\n matrix_dir = run_dir / \"matrix-messages\"\n rooms, messages = export_matrix_messages(\n matrix_dir, since_epoch, redact,\n room_filter=args.room, env_file=args.env_file,\n homeserver=args.homeserver, token=args.token,\n messages_only=args.messages_only,\n )\n if rooms == 0 and matrix_dir.exists() and not any(matrix_dir.iterdir()):\n matrix_dir.rmdir()\n print()\n\n # --- Agent sessions ---\n print(\"=== Agent Sessions ===\")\n sessions_dir = run_dir / \"agent-sessions\"\n sessions, events = export_agent_sessions(\n sessions_dir, since_epoch, redact,\n container_filter=args.container,\n )\n if sessions == 0 and sessions_dir.exists() and not any(sessions_dir.iterdir()):\n sessions_dir.rmdir()\n print()\n\n # --- Summary ---\n summary = (\n f\"AgentTeams Debug Log\\n\"\n f\"Exported at: {now_str}\\n\"\n f\"Range: last {args.time_range} (since {since_human})\\n\"\n f\"PII redaction: {'on' if redact else 'off'}\\n\"\n f\"\\n\"\n f\"Matrix messages: {messages} messages from {rooms} rooms\\n\"\n f\"Agent sessions: {events} events from {sessions} sessions\\n\"\n )\n (run_dir / \"summary.txt\").write_text(summary)\n\n print(f\"Done. {messages} messages from {rooms} rooms, {events} events from {sessions} sessions\")\n print(f\"Output: {run_dir.resolve()}\")\n\n\nif __name__ == \"__main__\":\n main()\n"}, "dc4aafb44bbb63526a3e168406eadba5379ec0e5": {"t": 1785764738.5203216, "n": 29529, "h": "b3adb595507dcfdf", "c": "#!/usr/bin/env python3\nfrom __future__ import annotations\n\"\"\"\nExport AgentTeams debug logs: Matrix messages + agent session logs.\n\nUsage:\n # Export last 1 hour (default)\n python scripts/export-debug-log.py --range 1h\n\n # Export last 1 day\n python scripts/export-debug-log.py --range 1d\n\n # Filter by container or room\n python scripts/export-debug-log.py --range 1h --container agentteams-manager --room Worker\n\n # Disable PII redaction\n python scripts/export-debug-log.py --range 1h --no-redact\n\nOutput structure:\n debug-log/20260319-153000/\n ├── summary.txt\n ├── matrix-messages/\n │ └── RoomName_!roomid.jsonl\n └── agent-sessions/\n ├── agentteams-manager/\n │ └── {session-id}.jsonl\n └── agentteams-worker-xxx/\n └── {session-key}.jsonl\n\"\"\"\n\nimport argparse\nimport json\nimport os\nimport re\nimport subprocess\nimport sys\nimport time\nimport urllib.parse\nimport urllib.request\nimport urllib.error\nfrom datetime import datetime, timezone\nfrom pathlib import Path\n\n\n# ---------------------------------------------------------------------------\n# PII redaction\n# ---------------------------------------------------------------------------\n\n_PII_PATTERNS: list[tuple[str, re.Pattern]] = [\n (\"ID_CARD\", re.compile(r'\\b[1-9]\\d{5}(?:19|20)\\d{2}(?:0[1-9]|1[0-2])(?:0[1-9]|[12]\\d|3[01])\\d{3}[\\dXx]\\b')),\n (\"PHONE\", re.compile(r'(?<!\\d)(?:\\+?86[-\\s]?)?1[3-9]\\d{9}(?!\\d)')),\n (\"EMAIL\", re.compile(r'\\b[A-Za-z0-9._%+\\-]+@[A-Za-z0-9.\\-]+\\.[A-Za-z]{2,}\\b')),\n (\"BANK_CARD\", re.compile(r'\\b(?:6[0-9]{15,18}|4[0-9]{15}|5[1-5][0-9]{14}|3[47][0-9]{13}|62[0-9]{14,17})\\b')),\n (\"IP\", re.compile(r'\\b(?:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)\\.){3}(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)\\b')),\n (\"ALIYUN_AK\", re.compile(r'\\bLTAI[A-Za-z0-9]{12,30}\\b')),\n (\"ALIYUN_SK\", re.compile(r'(?i)(?:access_?key_?secret|secret_?access_?key)\\s*[:=]\\s*(\\S{20,})')),\n (\"AWS_AK\", re.compile(r'\\b(?:AKIA|ASIA)[A-Z0-9]{16}\\b')),\n (\"OPENAI_KEY\", re.compile(r'\\bsk-[A-Za-z0-9]{20,}\\b')),\n (\"ANTHROPIC_KEY\", re.compile(r'\\bsk-ant-[A-Za-z0-9\\-]{20,}\\b')),\n (\"DASHSCOPE_KEY\", re.compile(r'\\bsk-sp-[A-Za-z0-9]{20,}\\b')),\n (\"DEEPSEEK_KEY\", re.compile(r'\\bsk-[a-f0-9]{32,}\\b')),\n (\"BEARER\", re.compile(r'(?i)(Bearer\\s+)([A-Za-z0-9\\-_.]{20,})')),\n (\"SECRET_KV\", re.compile(\n r'(?i)((?:password|passwd|pwd|secret|token|api_?key|access_?key|secret_?key'\n r'|private_?key|credential|appkey|app_?secret|auth_?token|signing_?key'\n r'|client_?secret|master_?key)\\s*[:=]\\s*)'\n r'(\\S+)'\n )),\n (\"MATRIX_TOKEN\", re.compile(r'\\bsyt_[A-Za-z0-9_\\-]{10,}\\b')),\n (\"HEX_SECRET\", re.compile(r'\\b[A-Fa-f0-9]{32,}\\b')),\n (\"PASSPORT\", re.compile(r'\\b[EeGg]\\d{8}\\b')),\n (\"SSN\", re.compile(r'\\b\\d{3}-\\d{2}-\\d{4}\\b')),\n]\n\n_SECRET_FIELD_PATTERN = re.compile(\n r'(?i)^(?:password|passwd|pwd|secret|token|api[_-]?key'\n r'|access[_-]?key(?:[_-]?secret)?|secret[_-]?access[_-]?key'\n r'|secret[_-]?key|private[_-]?key|credential|app[_-]?key'\n r'|app[_-]?secret|auth[_-]?token|signing[_-]?key'\n r'|client[_-]?secret|master[_-]?key)$'\n)\n\n\ndef redact_pii(text: str) -> str:\n if not text:\n return text\n for name, pattern in _PII_PATTERNS:\n if name in (\"SECRET_KV\", \"ALIYUN_SK\", \"BEARER\"):\n text = pattern.sub(r'\\1****', text)\n else:\n text = pattern.sub('****', text)\n return text\n\n\ndef redact_json_strings(obj):\n if isinstance(obj, str):\n return redact_pii(obj)\n if isinstance(obj, list):\n return [redact_json_strings(v) for v in obj]\n if isinstance(obj, dict):\n redacted = {}\n for key, value in obj.items():\n if isinstance(key, str) and _SECRET_FIELD_PATTERN.fullmatch(key):\n redacted[key] = \"****\"\n else:\n redacted[key] = redact_json_strings(value)\n return redacted\n return obj\n\n\n# ---------------------------------------------------------------------------\n# Common helpers\n# ---------------------------------------------------------------------------\n\ndef parse_range(range_str: str) -> int:\n m = re.fullmatch(r\"(\\d+)\\s*(m|min|h|hr|hour|d|day)s?\", range_str.strip(), re.IGNORECASE)\n if not m:\n raise ValueError(f\"Invalid range format: '{range_str}'. Use e.g. 10m, 1h, 1d\")\n value, unit = int(m.group(1)), m.group(2)[0].lower()\n return value * {\"m\": 60, \"h\": 3600, \"d\": 86400}[unit]\n\n\ndef parse_ts(ts_str: str) -> float:\n if not ts_str:\n return 0\n ts_str = ts_str.replace(\"Z\", \"+00:00\")\n try:\n dt = datetime.fromisoformat(ts_str)\n if dt.tzinfo is None:\n dt = dt.replace(tzinfo=timezone.utc)\n return dt.timestamp()\n except Exception:\n return 0\n\n\ndef sanitize_filename(name: str) -> str:\n return re.sub(r'[^\\w\\-. ]', '_', name).strip()[:80]\n\n\ndef docker_exec(container: str, cmd: str) -> str:\n result = subprocess.run(\n [\"docker\", \"exec\", container, \"sh\", \"-c\", cmd],\n capture_output=True, text=True, timeout=30,\n )\n return result.stdout\n\n\ndef list_hiclaw_containers() -> list[str]:\n names: list[str] = []\n for prefix in (\"agentteams-\", \"hiclaw-\"):\n result = subprocess.run(\n [\"docker\", \"ps\", \"--format\", \"{{.Names}}\", \"--filter\", f\"name={prefix}\"],\n capture_output=True, text=True, timeout=10,\n )\n names.extend(n.strip() for n in result.stdout.splitlines() if n.strip())\n return list(dict.fromkeys(names))\n\n\n# ---------------------------------------------------------------------------\n# Matrix messages export\n# ---------------------------------------------------------------------------\n\ndef load_env_file(path: str) -> dict[str, str]:\n env = {}\n try:\n with open(path) as f:\n for line in f:\n line = line.strip()\n if not line or line.startswith(\"#\"):\n continue\n if \"=\" in line:\n key, _, value = line.partition(\"=\")\n env[key.strip()] = value.strip()\n except FileNotFoundError:\n pass\n return env\n\n\ndef matrix_login(homeserver: str, user: str, password: str) -> str:\n url = f\"{homeserver.rstrip('/')}/_matrix/client/v3/login\"\n payload = json.dumps({\n \"type\": \"m.login.password\",\n \"identifier\": {\"type\": \"m.id.user\", \"user\": user},\n \"password\": password,\n }).encode()\n req = urllib.request.Request(url, data=payload, headers={\"Content-Type\": \"application/json\"})\n with urllib.request.urlopen(req, timeout=15) as resp:\n data = json.loads(resp.read())\n return data[\"access_token\"]\n\n\ndef matrix_api(homeserver: str, token: str, endpoint: str, params: dict | None = None) -> dict:\n url = f\"{homeserver.rstrip('/')}/_matrix/client/v3/{endpoint}\"\n if params:\n url += \"?\" + urllib.parse.urlencode(params)\n req = urllib.request.Request(url, headers={\"Authorization\": f\"Bearer {token}\"})\n try:\n with urllib.request.urlopen(req, timeout=30) as resp:\n return json.loads(resp.read())\n except urllib.error.HTTPError as e:\n body = e.read().decode(errors=\"replace\")\n print(f\" API error {e.code} on {endpoint}: {body}\", file=sys.stderr)\n raise\n\n\ndef fetch_room_messages(homeserver: str, token: str, room_id: str, since_ts: int) -> list[dict]:\n encoded = urllib.parse.quote(room_id)\n messages = []\n from_token = \"\"\n while True:\n params = {\"dir\": \"b\", \"limit\": \"100\"}\n if from_token:\n params[\"from\"] = from_token\n data = matrix_api(homeserver, token, f\"rooms/{encoded}/messages\", params)\n chunk = data.get(\"chunk\", [])\n if not chunk:\n break\n hit_boundary = False\n for event in chunk:\n if event.get(\"origin_server_ts\", 0) < since_ts:\n hit_boundary = True\n break\n messages.append(event)\n if hit_boundary:\n break\n next_token = data.get(\"end\")\n if not next_token or next_token == from_token:\n break\n from_token = next_token\n messages.reverse()\n return messages\n\n\ndef format_event(event: dict, redact: bool) -> dict:\n content = event.get(\"content\", {})\n ts = event.get(\"origin_server_ts\", 0)\n record = {\n \"event_id\": event.get(\"event_id\"),\n \"type\": event.get(\"type\"),\n \"sender\": event.get(\"sender\"),\n \"timestamp\": ts,\n \"time\": datetime.fromtimestamp(ts / 1000, tz=timezone.utc).isoformat(),\n }\n if event.get(\"type\") == \"m.room.message\":\n record[\"msgtype\"] = content.get(\"msgtype\")\n record[\"body\"] = content.get(\"body\")\n if content.get(\"format\"):\n record[\"format\"] = content[\"format\"]\n if content.get(\"url\"):\n record[\"url\"] = content[\"url\"]\n if content.get(\"m.relates_to\"):\n record[\"relates_to\"] = content[\"m.relates_to\"]\n else:\n record[\"content\"] = content\n return redact_json_strings(record) if redact else record\n\n\ndef export_matrix_messages(out_dir: Path, since_epoch: float, redact: bool,\n room_filter: str | None, env_file: str | None,\n homeserver: str, token: str,\n messages_only: bool) -> tuple[int, int]:\n \"\"\"Export Matrix messages. Returns (rooms_exported, message_count).\"\"\"\n env_path = env_file or os.path.expanduser(\"~/agentteams-manager.env\")\n hiclaw_env = load_env_file(env_path)\n\n if not homeserver:\n if not hiclaw_env:\n print(f\" [matrix] Cannot find {env_path}, skipping Matrix export\")\n return 0, 0\n port = hiclaw_env.get(\"AGENTTEAMS_PORT_GATEWAY\", \"18080\")\n homeserver = f\"http://127.0.0.1:{port}\"\n\n if not token:\n # Use Manager token — Manager is in every room (DM, Worker, Project)\n manager_password = hiclaw_env.get(\"AGENTTEAMS_MANAGER_PASSWORD\", \"\")\n if manager_password:\n try:\n token = matrix_login(homeserver, \"manager\", manager_password)\n except Exception:\n pass\n\n # Fallback to admin token if Manager login failed\n if not token:\n admin_user = hiclaw_env.get(\"AGENTTEAMS_ADMIN_USER\", \"admin\")\n admin_password = hiclaw_env.get(\"AGENTTEAMS_ADMIN_PASSWORD\", \"\")\n if not admin_password:\n print(f\" [matrix] No usable credentials found, skipping Matrix export\")\n return 0, 0\n try:\n token = matrix_login(homeserver, admin_user, admin_password)\n except Exception as e:\n print(f\" [matrix] Login failed: {e}, skipping Matrix export\")\n return 0, 0\n\n since_ts = int(since_epoch * 1000)\n out_dir.mkdir(parents=True, exist_ok=True)\n\n try:\n rooms = matrix_api(homeserver, token, \"joined_rooms\").get(\"joined_rooms\", [])\n except Exception as e:\n print(f\" [matrix] Failed to list rooms: {e}\")\n return 0, 0\n\n total_messages = 0\n total_rooms = 0\n\n for room_id in rooms:\n encoded = urllib.parse.quote(room_id)\n try:\n room_name = matrix_api(homeserver, token, f\"rooms/{encoded}/state/m.room.name\").get(\"name\", \"\")\n except Exception:\n room_name = \"\"\n\n if room_filter:\n if room_filter not in room_id and room_filter not in (room_name or \"\"):\n continue\n\n display = f\"{room_name} ({room_id})\" if room_name else room_id\n print(f\" {display} ... \", end=\"\", flush=True)\n\n messages = fetch_room_messages(homeserver, token, room_id, since_ts)\n if messages_only:\n messages = [e for e in messages if e.get(\"type\") == \"m.room.message\"]\n\n if not messages:\n print(\"0 messages, skipped\")\n continue\n\n name_part = sanitize_filename(room_name) if room_name else \"\"\n id_part = sanitize_filename(room_id)\n filename = f\"{name_part}_{id_part}.jsonl\" if name_part else f\"{id_part}.jsonl\"\n\n with open(out_dir / filename, \"w\", encoding=\"utf-8\") as f:\n for event in messages:\n record = format_event(event, redact=redact)\n f.write(json.dumps(record, ensure_ascii=False) + \"\\n\")\n\n print(f\"{len(messages)} messages -> {filename}\")\n total_messages += len(messages)\n total_rooms += 1\n\n return total_rooms, total_messages\n\n\n# ---------------------------------------------------------------------------\n# Agent sessions export (OpenClaw + CoPaw + Hermes)\n# ---------------------------------------------------------------------------\n\ndef detect_runtime(container: str) -> tuple[str, str]:\n \"\"\"Probe known absolute session-dir locations across runtimes & container types.\n\n Returns (runtime_name, absolute_sessions_dir). Empty tuple if none found.\n\n Layout reference (verified against worker/scripts/worker-entrypoint.sh,\n copaw/scripts/copaw-worker-entrypoint.sh, copaw/AGENTS.md and\n tests/lib/agent-metrics.sh):\n\n Manager (any runtime, HOME=/root/manager-workspace):\n openclaw -> /root/manager-workspace/.openclaw/agents/main/sessions\n hermes -> /root/manager-workspace/.hermes/sessions\n copaw -> /root/manager-workspace/.copaw/workspaces/default/sessions\n\n OpenClaw / Hermes Worker (HOME=/root/hiclaw-fs/agents/<name>):\n openclaw -> /root/hiclaw-fs/agents/<name>/.openclaw/agents/main/sessions\n hermes -> /root/hiclaw-fs/agents/<name>/.hermes/sessions\n\n CoPaw Worker (HOME=/root/.hiclaw-worker/<name>, also reachable via\n /root/hiclaw-fs symlink that points to that same dir):\n copaw -> /root/.hiclaw-worker/<name>/.copaw/workspaces/default/sessions\n copaw -> /root/hiclaw-fs/.copaw/workspaces/default/sessions (alt)\n \"\"\"\n candidates: list[tuple[str, str]] = [\n (\"openclaw\", \"/root/manager-workspace/.openclaw/agents/main/sessions\"),\n (\"hermes\", \"/root/manager-workspace/.hermes/sessions\"),\n (\"copaw\", \"/root/manager-workspace/.copaw/workspaces/default/sessions\"),\n ]\n\n worker_name = docker_exec(container, \"echo $AGENTTEAMS_WORKER_NAME\").strip()\n if worker_name:\n candidates.extend([\n (\"openclaw\", f\"/root/hiclaw-fs/agents/{worker_name}/.openclaw/agents/main/sessions\"),\n (\"hermes\", f\"/root/hiclaw-fs/agents/{worker_name}/.hermes/sessions\"),\n (\"copaw\", f\"/root/.hiclaw-worker/{worker_name}/.copaw/workspaces/default/sessions\"),\n (\"copaw\", \"/root/hiclaw-fs/.copaw/workspaces/default/sessions\"),\n ])\n\n for runtime, path in candidates:\n if docker_exec(container, f\"test -d '{path}' && echo yes || echo no\").strip() == \"yes\":\n return runtime, path\n\n # Last-resort: scan from / for any known session dir layout.\n found = docker_exec(\n container,\n \"find / -maxdepth 7 \\\\( \"\n \"-path '*/.openclaw/agents/main/sessions' \"\n \"-o -path '*/.hermes/sessions' \"\n \"-o -path '*/.copaw/workspaces/default/sessions' \"\n \"\\\\) -type d 2>/dev/null | head -1\",\n ).strip()\n if found:\n if \"/.openclaw/\" in found:\n return \"openclaw\", found\n if \"/.hermes/\" in found:\n return \"hermes\", found\n return \"copaw\", found\n\n return \"\", \"\"\n\n\ndef export_openclaw_sessions(container: str, sessions_dir: str, since_epoch: float,\n out_dir: Path, redact: bool) -> tuple[int, int]:\n ls_output = docker_exec(container, f\"ls '{sessions_dir}'/*.jsonl 2>/dev/null\").strip()\n if not ls_output:\n return 0, 0\n\n total_sessions = 0\n total_events = 0\n\n for session_path in [f.strip() for f in ls_output.splitlines() if f.strip()]:\n filename = os.path.basename(session_path)\n\n first_line = docker_exec(container, f\"head -1 '{session_path}'\").strip()\n if not first_line:\n continue\n try:\n header = json.loads(first_line)\n except json.JSONDecodeError:\n continue\n\n last_line = docker_exec(container, f\"tail -1 '{session_path}'\").strip()\n try:\n last_ts = parse_ts(json.loads(last_line).get(\"timestamp\", \"\"))\n except Exception:\n last_ts = parse_ts(header.get(\"timestamp\", \"\"))\n\n if last_ts < since_epoch and last_ts > 0:\n continue\n\n raw = docker_exec(container, f\"cat '{session_path}'\")\n if not raw.strip():\n continue\n\n output_lines = []\n for line in raw.splitlines():\n line = line.strip()\n if not line:\n continue\n try:\n event = json.loads(line)\n except json.JSONDecodeError:\n continue\n event_ts = parse_ts(event.get(\"timestamp\", \"\"))\n if event.get(\"type\") != \"session\" and event_ts < since_epoch and event_ts > 0:\n continue\n if redact:\n event = redact_json_strings(event)\n output_lines.append(json.dumps(event, ensure_ascii=False))\n\n if len(output_lines) <= 1:\n continue\n\n with open(out_dir / filename, \"w\", encoding=\"utf-8\") as f:\n f.write(\"\\n\".join(output_lines) + \"\\n\")\n\n event_count = len(output_lines) - 1\n print(f\" {container}/{filename} (openclaw): {event_count} events\")\n total_sessions += 1\n total_events += event_count\n\n # sessions.json index\n try:\n raw = docker_exec(container, f\"cat '{sessions_dir}/sessions.json'\")\n if raw.strip():\n index = json.loads(raw)\n if redact:\n index = redact_json_strings(index)\n with open(out_dir / \"sessions.json\", \"w\", encoding=\"utf-8\") as f:\n json.dump(index, f, ensure_ascii=False, indent=2)\n except Exception:\n pass\n\n return total_sessions, total_events\n\n\ndef export_copaw_sessions(container: str, sessions_dir: str, since_epoch: float,\n out_dir: Path, redact: bool) -> tuple[int, int]:\n ls_output = docker_exec(container, f\"find '{sessions_dir}' -name '*.json' -type f 2>/dev/null\").strip()\n if not ls_output:\n return 0, 0\n\n total_sessions = 0\n total_events = 0\n\n for session_path in [f.strip() for f in ls_output.splitlines() if f.strip()]:\n raw = docker_exec(container, f\"cat '{session_path}'\")\n if not raw.strip():\n continue\n try:\n data = json.loads(raw)\n except json.JSONDecodeError:\n continue\n\n agent = data.get(\"agent\", {})\n memory = agent.get(\"memory\", {})\n content = memory.get(\"content\", [])\n if not content:\n continue\n\n basename = os.path.basename(session_path).replace(\".json\", \"\")\n header = {\n \"type\": \"session\",\n \"runtime\": \"copaw\",\n \"agent_name\": agent.get(\"name\", \"\"),\n \"session_key\": basename,\n \"compressed_summary\": memory.get(\"_compressed_summary\", \"\"),\n }\n\n messages_in_range = []\n for turn_idx, turn in enumerate(content):\n if not isinstance(turn, list):\n continue\n for msg in turn:\n if not isinstance(msg, dict):\n continue\n msg_ts = parse_ts(msg.get(\"timestamp\", \"\"))\n if msg_ts >= since_epoch or msg_ts == 0:\n event = {\n \"type\": \"message\",\n \"turn\": turn_idx,\n \"id\": msg.get(\"id\", \"\"),\n \"role\": msg.get(\"role\", \"\"),\n \"name\": msg.get(\"name\", \"\"),\n \"timestamp\": msg.get(\"timestamp\", \"\"),\n \"content\": msg.get(\"content\", []),\n }\n if msg.get(\"metadata\"):\n event[\"metadata\"] = msg[\"metadata\"]\n messages_in_range.append(event)\n\n if not messages_in_range:\n continue\n\n output_lines = []\n if redact:\n header = redact_json_strings(header)\n output_lines.append(json.dumps(header, ensure_ascii=False))\n for event in messages_in_range:\n if redact:\n event = redact_json_strings(event)\n output_lines.append(json.dumps(event, ensure_ascii=False))\n\n out_filename = basename + \".jsonl\"\n with open(out_dir / out_filename, \"w\", encoding=\"utf-8\") as f:\n f.write(\"\\n\".join(output_lines) + \"\\n\")\n\n event_count = len(output_lines) - 1\n print(f\" {container}/{out_filename} (copaw): {event_count} events\")\n total_sessions += 1\n total_events += event_count\n\n return total_sessions, total_events\n\n\ndef export_hermes_sessions(container: str, sessions_dir: str, since_epoch: float,\n out_dir: Path, redact: bool) -> tuple[int, int]:\n ls_output = docker_exec(container, f\"ls '{sessions_dir}'/*.jsonl 2>/dev/null\").strip()\n if not ls_output:\n return 0, 0\n\n total_sessions = 0\n total_events = 0\n\n for session_path in [f.strip() for f in ls_output.splitlines() if f.strip()]:\n raw = docker_exec(container, f\"cat '{session_path}'\")\n if not raw.strip():\n continue\n\n output_lines = []\n last_ts = 0.0\n for line in raw.splitlines():\n line = line.strip()\n if not line:\n continue\n try:\n event = json.loads(line)\n except json.JSONDecodeError:\n continue\n\n event_ts = parse_ts(event.get(\"timestamp\", \"\"))\n if event_ts:\n last_ts = max(last_ts, event_ts)\n\n role = event.get(\"role\", \"\")\n if role != \"session_meta\" and event_ts < since_epoch and event_ts > 0:\n continue\n\n if redact:\n event = redact_json_strings(event)\n output_lines.append(json.dumps(event, ensure_ascii=False))\n\n if not output_lines:\n continue\n\n if last_ts < since_epoch and last_ts > 0:\n continue\n\n filename = os.path.basename(session_path)\n with open(out_dir / filename, \"w\", encoding=\"utf-8\") as f:\n f.write(\"\\n\".join(output_lines) + \"\\n\")\n\n event_count = len(output_lines) - 1 if output_lines and '\"role\": \"session_meta\"' in output_lines[0] else len(output_lines)\n print(f\" {container}/{filename} (hermes): {event_count} events\")\n total_sessions += 1\n total_events += max(event_count, 0)\n\n hermes_home = str(Path(sessions_dir).parent)\n state_db = f\"{hermes_home}/state.db\"\n has_state_db = docker_exec(container, f\"test -f '{state_db}' && echo yes || echo no\").strip() == \"yes\"\n if has_state_db:\n raw = docker_exec(container, f\"python3 - <<'PY'\\nimport json, sqlite3\\nconn = sqlite3.connect('{state_db}')\\nconn.row_factory = sqlite3.Row\\nrows = conn.execute('SELECT * FROM sessions ORDER BY started_at DESC LIMIT 200').fetchall()\\nprint(json.dumps([dict(r) for r in rows], ensure_ascii=False, indent=2))\\nPY\")\n if raw.strip():\n try:\n data = json.loads(raw)\n if redact:\n data = redact_json_strings(data)\n with open(out_dir / \"sessions-db.json\", \"w\", encoding=\"utf-8\") as f:\n json.dump(data, f, ensure_ascii=False, indent=2)\n except json.JSONDecodeError:\n pass\n\n logs_dir = f\"{hermes_home}/logs\"\n has_logs_dir = docker_exec(container, f\"test -d '{logs_dir}' && echo yes || echo no\").strip() == \"yes\"\n if has_logs_dir:\n for log_name in (\"agent.log\", \"errors.log\", \"gateway.log\"):\n raw = docker_exec(container, f\"test -f '{logs_dir}/{log_name}' && cat '{logs_dir}/{log_name}' || true\")\n if raw.strip():\n content = redact_pii(raw) if redact else raw\n (out_dir / log_name).write_text(content, encoding=\"utf-8\")\n\n return total_sessions, total_events\n\n\ndef export_agent_sessions(out_dir: Path, since_epoch: float, redact: bool,\n container_filter: str | None) -> tuple[int, int]:\n \"\"\"Export agent sessions from all containers. Returns (session_count, event_count).\"\"\"\n containers = list_hiclaw_containers()\n if container_filter:\n containers = [c for c in containers if container_filter in c]\n\n if not containers:\n print(\" [sessions] No matching AgentTeams containers found\")\n return 0, 0\n\n total_sessions = 0\n total_events = 0\n\n for container in containers:\n runtime, sessions_dir = detect_runtime(container)\n if not runtime:\n print(f\" {container}: no sessions directory, skipped\")\n continue\n\n container_dir = out_dir / container\n container_dir.mkdir(parents=True, exist_ok=True)\n\n if runtime == \"openclaw\":\n s, e = export_openclaw_sessions(container, sessions_dir, since_epoch,\n container_dir, redact)\n elif runtime == \"hermes\":\n s, e = export_hermes_sessions(container, sessions_dir, since_epoch,\n container_dir, redact)\n else:\n s, e = export_copaw_sessions(container, sessions_dir, since_epoch,\n container_dir, redact)\n\n if s == 0:\n if not any(container_dir.iterdir()):\n container_dir.rmdir()\n print(f\" {container} ({runtime}): no sessions in range, skipped\")\n else:\n print(f\" {container} ({runtime}): exported auxiliary debug artifacts without session events\")\n else:\n total_sessions += s\n total_events += e\n\n return total_sessions, total_events\n\n\n# ---------------------------------------------------------------------------\n# Main\n# ---------------------------------------------------------------------------\n\ndef main():\n parser = argparse.ArgumentParser(\n description=\"Export AgentTeams debug logs (Matrix messages + agent sessions)\"\n )\n parser.add_argument(\"--range\", \"-r\", required=True, dest=\"time_range\",\n help=\"Time range to export, e.g. 10m, 1h, 1d\")\n parser.add_argument(\"--container\", \"-c\", default=None,\n help=\"Only export sessions from containers matching this substring\")\n parser.add_argument(\"--room\", default=None,\n help=\"Only export Matrix rooms matching this substring\")\n parser.add_argument(\"--homeserver\", \"-s\", default=\"\",\n help=\"Matrix homeserver URL (auto-detected from env file)\")\n parser.add_argument(\"--token\", \"-t\", default=\"\",\n help=\"Matrix access token (auto-detected from env file)\")\n parser.add_argument(\"--env-file\", default=None,\n help=\"Path to agentteams-manager.env (default: ~/agentteams-manager.env)\")\n parser.add_argument(\"--messages-only\", action=\"store_true\",\n help=\"Only export m.room.message events (skip state events)\")\n parser.add_argument(\"--no-redact\", action=\"store_true\",\n help=\"Disable PII redaction\")\n args = parser.parse_args()\n\n range_seconds = parse_range(args.time_range)\n since_epoch = time.time() - range_seconds\n since_human = datetime.fromtimestamp(since_epoch, tz=timezone.utc).isoformat()\n now_str = datetime.now().strftime(\"%Y%m%d-%H%M%S\")\n\n run_dir = Path(\"debug-log\") / now_str\n run_dir.mkdir(parents=True, exist_ok=True)\n redact = not args.no_redact\n\n print(f\"AgentTeams Debug Log Export\")\n print(f\" Range: last {args.time_range} (since {since_human})\")\n print(f\" Output: {run_dir.resolve()}\")\n print(f\" PII redaction: {'on' if redact else 'off'}\")\n print()\n\n # --- Matrix messages ---\n print(\"=== Matrix Messages ===\")\n matrix_dir = run_dir / \"matrix-messages\"\n rooms, messages = export_matrix_messages(\n matrix_dir, since_epoch, redact,\n room_filter=args.room, env_file=args.env_file,\n homeserver=args.homeserver, token=args.token,\n messages_only=args.messages_only,\n )\n if rooms == 0 and matrix_dir.exists() and not any(matrix_dir.iterdir()):\n matrix_dir.rmdir()\n print()\n\n # --- Agent sessions ---\n print(\"=== Agent Sessions ===\")\n sessions_dir = run_dir / \"agent-sessions\"\n sessions, events = export_agent_sessions(\n sessions_dir, since_epoch, redact,\n container_filter=args.container,\n )\n if sessions == 0 and sessions_dir.exists() and not any(sessions_dir.iterdir()):\n sessions_dir.rmdir()\n print()\n\n # --- Summary ---\n summary = (\n f\"AgentTeams Debug Log\\n\"\n f\"Exported at: {now_str}\\n\"\n f\"Range: last {args.time_range} (since {since_human})\\n\"\n f\"PII redaction: {'on' if redact else 'off'}\\n\"\n f\"\\n\"\n f\"Matrix messages: {messages} messages from {rooms} rooms\\n\"\n f\"Agent sessions: {events} events from {sessions} sessions\\n\"\n )\n (run_dir / \"summary.txt\").write_text(summary)\n\n print(f\"Done. {messages} messages from {rooms} rooms, {events} events from {sessions} sessions\")\n print(f\"Output: {run_dir.resolve()}\")\n\n\nif __name__ == \"__main__\":\n main()\n"}, "a7b707efcbb28cf09f68af8d77387e94c34cfc37": {"t": 1785764740.10975, "n": 29003, "h": "56e07411b14b8c8b", "c": "#!/usr/bin/env python3\nfrom __future__ import annotations\n\"\"\"\nExport AgentTeams debug logs: Matrix messages + agent session logs.\n\nUsage:\n # Export last 1 hour (default)\n python scripts/export-debug-log.py --range 1h\n\n # Export last 1 day\n python scripts/export-debug-log.py --range 1d\n\n # Filter by container or room\n python scripts/export-debug-log.py --range 1h --container agentteams-manager --room Worker\n\n # Disable PII redaction\n python scripts/export-debug-log.py --range 1h --no-redact\n\nOutput structure:\n debug-log/20260319-153000/\n ├── summary.txt\n ├── matrix-messages/\n │ └── RoomName_!roomid.jsonl\n └── agent-sessions/\n ├── agentteams-manager/\n │ └── {session-id}.jsonl\n └── agentteams-worker-xxx/\n └── {session-key}.jsonl\n\"\"\"\n\nimport argparse\nimport json\nimport os\nimport re\nimport subprocess\nimport sys\nimport time\nimport urllib.parse\nimport urllib.request\nimport urllib.error\nfrom datetime import datetime, timezone\nfrom pathlib import Path\n\n\n# ---------------------------------------------------------------------------\n# PII redaction\n# ---------------------------------------------------------------------------\n\n_PII_PATTERNS: list[tuple[str, re.Pattern]] = [\n (\"ID_CARD\", re.compile(r'\\b[1-9]\\d{5}(?:19|20)\\d{2}(?:0[1-9]|1[0-2])(?:0[1-9]|[12]\\d|3[01])\\d{3}[\\dXx]\\b')),\n (\"PHONE\", re.compile(r'(?<!\\d)(?:\\+?86[-\\s]?)?1[3-9]\\d{9}(?!\\d)')),\n (\"EMAIL\", re.compile(r'\\b[A-Za-z0-9._%+\\-]+@[A-Za-z0-9.\\-]+\\.[A-Za-z]{2,}\\b')),\n (\"BANK_CARD\", re.compile(r'\\b(?:6[0-9]{15,18}|4[0-9]{15}|5[1-5][0-9]{14}|3[47][0-9]{13}|62[0-9]{14,17})\\b')),\n (\"IP\", re.compile(r'\\b(?:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)\\.){3}(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)\\b')),\n (\"ALIYUN_AK\", re.compile(r'\\bLTAI[A-Za-z0-9]{12,30}\\b')),\n (\"ALIYUN_SK\", re.compile(r'(?i)(?:access_?key_?secret|secret_?access_?key)\\s*[:=]\\s*(\\S{20,})')),\n (\"AWS_AK\", re.compile(r'\\b(?:AKIA|ASIA)[A-Z0-9]{16}\\b')),\n (\"OPENAI_KEY\", re.compile(r'\\bsk-[A-Za-z0-9]{20,}\\b')),\n (\"ANTHROPIC_KEY\", re.compile(r'\\bsk-ant-[A-Za-z0-9\\-]{20,}\\b')),\n (\"DASHSCOPE_KEY\", re.compile(r'\\bsk-sp-[A-Za-z0-9]{20,}\\b')),\n (\"DEEPSEEK_KEY\", re.compile(r'\\bsk-[a-f0-9]{32,}\\b')),\n (\"BEARER\", re.compile(r'(?i)(Bearer\\s+)([A-Za-z0-9\\-_.]{20,})')),\n (\"SECRET_KV\", re.compile(\n r'(?i)((?:password|passwd|pwd|secret|token|api_?key|access_?key|secret_?key'\n r'|private_?key|credential|appkey|app_?secret|auth_?token|signing_?key'\n r'|client_?secret|master_?key)\\s*[:=]\\s*)'\n r'(\\S+)'\n )),\n (\"MATRIX_TOKEN\", re.compile(r'\\bsyt_[A-Za-z0-9_\\-]{10,}\\b')),\n (\"HEX_SECRET\", re.compile(r'\\b[A-Fa-f0-9]{32,}\\b')),\n (\"PASSPORT\", re.compile(r'\\b[EeGg]\\d{8}\\b')),\n (\"SSN\", re.compile(r'\\b\\d{3}-\\d{2}-\\d{4}\\b')),\n]\n\n\ndef redact_pii(text: str) -> str:\n if not text:\n return text\n for name, pattern in _PII_PATTERNS:\n if name in (\"SECRET_KV\", \"ALIYUN_SK\", \"BEARER\"):\n text = pattern.sub(r'\\1****', text)\n else:\n text = pattern.sub('****', text)\n return text\n\n\ndef redact_json_strings(obj):\n if isinstance(obj, str):\n return redact_pii(obj)\n if isinstance(obj, list):\n return [redact_json_strings(v) for v in obj]\n if isinstance(obj, dict):\n return {k: redact_json_strings(v) for k, v in obj.items()}\n return obj\n\n\n# ---------------------------------------------------------------------------\n# Common helpers\n# ---------------------------------------------------------------------------\n\ndef parse_range(range_str: str) -> int:\n m = re.fullmatch(r\"(\\d+)\\s*(m|min|h|hr|hour|d|day)s?\", range_str.strip(), re.IGNORECASE)\n if not m:\n raise ValueError(f\"Invalid range format: '{range_str}'. Use e.g. 10m, 1h, 1d\")\n value, unit = int(m.group(1)), m.group(2)[0].lower()\n return value * {\"m\": 60, \"h\": 3600, \"d\": 86400}[unit]\n\n\ndef parse_ts(ts_str: str) -> float:\n if not ts_str:\n return 0\n ts_str = ts_str.replace(\"Z\", \"+00:00\")\n try:\n dt = datetime.fromisoformat(ts_str)\n if dt.tzinfo is None:\n dt = dt.replace(tzinfo=timezone.utc)\n return dt.timestamp()\n except Exception:\n return 0\n\n\ndef sanitize_filename(name: str) -> str:\n return re.sub(r'[^\\w\\-. ]', '_', name).strip()[:80]\n\n\ndef docker_exec(container: str, cmd: str) -> str:\n result = subprocess.run(\n [\"docker\", \"exec\", container, \"sh\", \"-c\", cmd],\n capture_output=True, text=True, timeout=30,\n )\n return result.stdout\n\n\ndef list_hiclaw_containers() -> list[str]:\n names: list[str] = []\n for prefix in (\"agentteams-\", \"hiclaw-\"):\n result = subprocess.run(\n [\"docker\", \"ps\", \"--format\", \"{{.Names}}\", \"--filter\", f\"name={prefix}\"],\n capture_output=True, text=True, timeout=10,\n )\n names.extend(n.strip() for n in result.stdout.splitlines() if n.strip())\n return list(dict.fromkeys(names))\n\n\n# ---------------------------------------------------------------------------\n# Matrix messages export\n# ---------------------------------------------------------------------------\n\ndef load_env_file(path: str) -> dict[str, str]:\n env = {}\n try:\n with open(path) as f:\n for line in f:\n line = line.strip()\n if not line or line.startswith(\"#\"):\n continue\n if \"=\" in line:\n key, _, value = line.partition(\"=\")\n env[key.strip()] = value.strip()\n except FileNotFoundError:\n pass\n return env\n\n\ndef matrix_login(homeserver: str, user: str, password: str) -> str:\n url = f\"{homeserver.rstrip('/')}/_matrix/client/v3/login\"\n payload = json.dumps({\n \"type\": \"m.login.password\",\n \"identifier\": {\"type\": \"m.id.user\", \"user\": user},\n \"password\": password,\n }).encode()\n req = urllib.request.Request(url, data=payload, headers={\"Content-Type\": \"application/json\"})\n with urllib.request.urlopen(req, timeout=15) as resp:\n data = json.loads(resp.read())\n return data[\"access_token\"]\n\n\ndef matrix_api(homeserver: str, token: str, endpoint: str, params: dict | None = None) -> dict:\n url = f\"{homeserver.rstrip('/')}/_matrix/client/v3/{endpoint}\"\n if params:\n url += \"?\" + urllib.parse.urlencode(params)\n req = urllib.request.Request(url, headers={\"Authorization\": f\"Bearer {token}\"})\n try:\n with urllib.request.urlopen(req, timeout=30) as resp:\n return json.loads(resp.read())\n except urllib.error.HTTPError as e:\n body = e.read().decode(errors=\"replace\")\n print(f\" API error {e.code} on {endpoint}: {body}\", file=sys.stderr)\n raise\n\n\ndef fetch_room_messages(homeserver: str, token: str, room_id: str, since_ts: int) -> list[dict]:\n encoded = urllib.parse.quote(room_id)\n messages = []\n from_token = \"\"\n while True:\n params = {\"dir\": \"b\", \"limit\": \"100\"}\n if from_token:\n params[\"from\"] = from_token\n data = matrix_api(homeserver, token, f\"rooms/{encoded}/messages\", params)\n chunk = data.get(\"chunk\", [])\n if not chunk:\n break\n hit_boundary = False\n for event in chunk:\n if event.get(\"origin_server_ts\", 0) < since_ts:\n hit_boundary = True\n break\n messages.append(event)\n if hit_boundary:\n break\n next_token = data.get(\"end\")\n if not next_token or next_token == from_token:\n break\n from_token = next_token\n messages.reverse()\n return messages\n\n\ndef format_event(event: dict, redact: bool) -> dict:\n content = event.get(\"content\", {})\n ts = event.get(\"origin_server_ts\", 0)\n record = {\n \"event_id\": event.get(\"event_id\"),\n \"type\": event.get(\"type\"),\n \"sender\": event.get(\"sender\"),\n \"timestamp\": ts,\n \"time\": datetime.fromtimestamp(ts / 1000, tz=timezone.utc).isoformat(),\n }\n if event.get(\"type\") == \"m.room.message\":\n record[\"msgtype\"] = content.get(\"msgtype\")\n body = content.get(\"body\")\n record[\"body\"] = redact_pii(body) if redact else body\n if content.get(\"format\"):\n record[\"format\"] = content[\"format\"]\n if content.get(\"url\"):\n record[\"url\"] = content[\"url\"]\n if content.get(\"m.relates_to\"):\n record[\"relates_to\"] = content[\"m.relates_to\"]\n else:\n record[\"content\"] = content\n return record\n\n\ndef export_matrix_messages(out_dir: Path, since_epoch: float, redact: bool,\n room_filter: str | None, env_file: str | None,\n homeserver: str, token: str,\n messages_only: bool) -> tuple[int, int]:\n \"\"\"Export Matrix messages. Returns (rooms_exported, message_count).\"\"\"\n env_path = env_file or os.path.expanduser(\"~/agentteams-manager.env\")\n hiclaw_env = load_env_file(env_path)\n\n if not homeserver:\n if not hiclaw_env:\n print(f\" [matrix] Cannot find {env_path}, skipping Matrix export\")\n return 0, 0\n port = hiclaw_env.get(\"AGENTTEAMS_PORT_GATEWAY\", \"18080\")\n homeserver = f\"http://127.0.0.1:{port}\"\n\n if not token:\n # Use Manager token — Manager is in every room (DM, Worker, Project)\n manager_password = hiclaw_env.get(\"AGENTTEAMS_MANAGER_PASSWORD\", \"\")\n if manager_password:\n try:\n token = matrix_login(homeserver, \"manager\", manager_password)\n except Exception:\n pass\n\n # Fallback to admin token if Manager login failed\n if not token:\n admin_user = hiclaw_env.get(\"AGENTTEAMS_ADMIN_USER\", \"admin\")\n admin_password = hiclaw_env.get(\"AGENTTEAMS_ADMIN_PASSWORD\", \"\")\n if not admin_password:\n print(f\" [matrix] No usable credentials found, skipping Matrix export\")\n return 0, 0\n try:\n token = matrix_login(homeserver, admin_user, admin_password)\n except Exception as e:\n print(f\" [matrix] Login failed: {e}, skipping Matrix export\")\n return 0, 0\n\n since_ts = int(since_epoch * 1000)\n out_dir.mkdir(parents=True, exist_ok=True)\n\n try:\n rooms = matrix_api(homeserver, token, \"joined_rooms\").get(\"joined_rooms\", [])\n except Exception as e:\n print(f\" [matrix] Failed to list rooms: {e}\")\n return 0, 0\n\n total_messages = 0\n total_rooms = 0\n\n for room_id in rooms:\n encoded = urllib.parse.quote(room_id)\n try:\n room_name = matrix_api(homeserver, token, f\"rooms/{encoded}/state/m.room.name\").get(\"name\", \"\")\n except Exception:\n room_name = \"\"\n\n if room_filter:\n if room_filter not in room_id and room_filter not in (room_name or \"\"):\n continue\n\n display = f\"{room_name} ({room_id})\" if room_name else room_id\n print(f\" {display} ... \", end=\"\", flush=True)\n\n messages = fetch_room_messages(homeserver, token, room_id, since_ts)\n if messages_only:\n messages = [e for e in messages if e.get(\"type\") == \"m.room.message\"]\n\n if not messages:\n print(\"0 messages, skipped\")\n continue\n\n name_part = sanitize_filename(room_name) if room_name else \"\"\n id_part = sanitize_filename(room_id)\n filename = f\"{name_part}_{id_part}.jsonl\" if name_part else f\"{id_part}.jsonl\"\n\n with open(out_dir / filename, \"w\", encoding=\"utf-8\") as f:\n for event in messages:\n record = format_event(event, redact=redact)\n f.write(json.dumps(record, ensure_ascii=False) + \"\\n\")\n\n print(f\"{len(messages)} messages -> {filename}\")\n total_messages += len(messages)\n total_rooms += 1\n\n return total_rooms, total_messages\n\n\n# ---------------------------------------------------------------------------\n# Agent sessions export (OpenClaw + CoPaw + Hermes)\n# ---------------------------------------------------------------------------\n\ndef detect_runtime(container: str) -> tuple[str, str]:\n \"\"\"Probe known absolute session-dir locations across runtimes & container types.\n\n Returns (runtime_name, absolute_sessions_dir). Empty tuple if none found.\n\n Layout reference (verified against worker/scripts/worker-entrypoint.sh,\n copaw/scripts/copaw-worker-entrypoint.sh, copaw/AGENTS.md and\n tests/lib/agent-metrics.sh):\n\n Manager (any runtime, HOME=/root/manager-workspace):\n openclaw -> /root/manager-workspace/.openclaw/agents/main/sessions\n hermes -> /root/manager-workspace/.hermes/sessions\n copaw -> /root/manager-workspace/.copaw/workspaces/default/sessions\n\n OpenClaw / Hermes Worker (HOME=/root/hiclaw-fs/agents/<name>):\n openclaw -> /root/hiclaw-fs/agents/<name>/.openclaw/agents/main/sessions\n hermes -> /root/hiclaw-fs/agents/<name>/.hermes/sessions\n\n CoPaw Worker (HOME=/root/.hiclaw-worker/<name>, also reachable via\n /root/hiclaw-fs symlink that points to that same dir):\n copaw -> /root/.hiclaw-worker/<name>/.copaw/workspaces/default/sessions\n copaw -> /root/hiclaw-fs/.copaw/workspaces/default/sessions (alt)\n \"\"\"\n candidates: list[tuple[str, str]] = [\n (\"openclaw\", \"/root/manager-workspace/.openclaw/agents/main/sessions\"),\n (\"hermes\", \"/root/manager-workspace/.hermes/sessions\"),\n (\"copaw\", \"/root/manager-workspace/.copaw/workspaces/default/sessions\"),\n ]\n\n worker_name = docker_exec(container, \"echo $AGENTTEAMS_WORKER_NAME\").strip()\n if worker_name:\n candidates.extend([\n (\"openclaw\", f\"/root/hiclaw-fs/agents/{worker_name}/.openclaw/agents/main/sessions\"),\n (\"hermes\", f\"/root/hiclaw-fs/agents/{worker_name}/.hermes/sessions\"),\n (\"copaw\", f\"/root/.hiclaw-worker/{worker_name}/.copaw/workspaces/default/sessions\"),\n (\"copaw\", \"/root/hiclaw-fs/.copaw/workspaces/default/sessions\"),\n ])\n\n for runtime, path in candidates:\n if docker_exec(container, f\"test -d '{path}' && echo yes || echo no\").strip() == \"yes\":\n return runtime, path\n\n # Last-resort: scan from / for any known session dir layout.\n found = docker_exec(\n container,\n \"find / -maxdepth 7 \\\\( \"\n \"-path '*/.openclaw/agents/main/sessions' \"\n \"-o -path '*/.hermes/sessions' \"\n \"-o -path '*/.copaw/workspaces/default/sessions' \"\n \"\\\\) -type d 2>/dev/null | head -1\",\n ).strip()\n if found:\n if \"/.openclaw/\" in found:\n return \"openclaw\", found\n if \"/.hermes/\" in found:\n return \"hermes\", found\n return \"copaw\", found\n\n return \"\", \"\"\n\n\ndef export_openclaw_sessions(container: str, sessions_dir: str, since_epoch: float,\n out_dir: Path, redact: bool) -> tuple[int, int]:\n ls_output = docker_exec(container, f\"ls '{sessions_dir}'/*.jsonl 2>/dev/null\").strip()\n if not ls_output:\n return 0, 0\n\n total_sessions = 0\n total_events = 0\n\n for session_path in [f.strip() for f in ls_output.splitlines() if f.strip()]:\n filename = os.path.basename(session_path)\n\n first_line = docker_exec(container, f\"head -1 '{session_path}'\").strip()\n if not first_line:\n continue\n try:\n header = json.loads(first_line)\n except json.JSONDecodeError:\n continue\n\n last_line = docker_exec(container, f\"tail -1 '{session_path}'\").strip()\n try:\n last_ts = parse_ts(json.loads(last_line).get(\"timestamp\", \"\"))\n except Exception:\n last_ts = parse_ts(header.get(\"timestamp\", \"\"))\n\n if last_ts < since_epoch and last_ts > 0:\n continue\n\n raw = docker_exec(container, f\"cat '{session_path}'\")\n if not raw.strip():\n continue\n\n output_lines = []\n for line in raw.splitlines():\n line = line.strip()\n if not line:\n continue\n try:\n event = json.loads(line)\n except json.JSONDecodeError:\n continue\n event_ts = parse_ts(event.get(\"timestamp\", \"\"))\n if event.get(\"type\") != \"session\" and event_ts < since_epoch and event_ts > 0:\n continue\n if redact:\n event = redact_json_strings(event)\n output_lines.append(json.dumps(event, ensure_ascii=False))\n\n if len(output_lines) <= 1:\n continue\n\n with open(out_dir / filename, \"w\", encoding=\"utf-8\") as f:\n f.write(\"\\n\".join(output_lines) + \"\\n\")\n\n event_count = len(output_lines) - 1\n print(f\" {container}/{filename} (openclaw): {event_count} events\")\n total_sessions += 1\n total_events += event_count\n\n # sessions.json index\n try:\n raw = docker_exec(container, f\"cat '{sessions_dir}/sessions.json'\")\n if raw.strip():\n index = json.loads(raw)\n if redact:\n index = redact_json_strings(index)\n with open(out_dir / \"sessions.json\", \"w\", encoding=\"utf-8\") as f:\n json.dump(index, f, ensure_ascii=False, indent=2)\n except Exception:\n pass\n\n return total_sessions, total_events\n\n\ndef export_copaw_sessions(container: str, sessions_dir: str, since_epoch: float,\n out_dir: Path, redact: bool) -> tuple[int, int]:\n ls_output = docker_exec(container, f\"find '{sessions_dir}' -name '*.json' -type f 2>/dev/null\").strip()\n if not ls_output:\n return 0, 0\n\n total_sessions = 0\n total_events = 0\n\n for session_path in [f.strip() for f in ls_output.splitlines() if f.strip()]:\n raw = docker_exec(container, f\"cat '{session_path}'\")\n if not raw.strip():\n continue\n try:\n data = json.loads(raw)\n except json.JSONDecodeError:\n continue\n\n agent = data.get(\"agent\", {})\n memory = agent.get(\"memory\", {})\n content = memory.get(\"content\", [])\n if not content:\n continue\n\n basename = os.path.basename(session_path).replace(\".json\", \"\")\n header = {\n \"type\": \"session\",\n \"runtime\": \"copaw\",\n \"agent_name\": agent.get(\"name\", \"\"),\n \"session_key\": basename,\n \"compressed_summary\": memory.get(\"_compressed_summary\", \"\"),\n }\n\n messages_in_range = []\n for turn_idx, turn in enumerate(content):\n if not isinstance(turn, list):\n continue\n for msg in turn:\n if not isinstance(msg, dict):\n continue\n msg_ts = parse_ts(msg.get(\"timestamp\", \"\"))\n if msg_ts >= since_epoch or msg_ts == 0:\n event = {\n \"type\": \"message\",\n \"turn\": turn_idx,\n \"id\": msg.get(\"id\", \"\"),\n \"role\": msg.get(\"role\", \"\"),\n \"name\": msg.get(\"name\", \"\"),\n \"timestamp\": msg.get(\"timestamp\", \"\"),\n \"content\": msg.get(\"content\", []),\n }\n if msg.get(\"metadata\"):\n event[\"metadata\"] = msg[\"metadata\"]\n messages_in_range.append(event)\n\n if not messages_in_range:\n continue\n\n output_lines = []\n if redact:\n header = redact_json_strings(header)\n output_lines.append(json.dumps(header, ensure_ascii=False))\n for event in messages_in_range:\n if redact:\n event = redact_json_strings(event)\n output_lines.append(json.dumps(event, ensure_ascii=False))\n\n out_filename = basename + \".jsonl\"\n with open(out_dir / out_filename, \"w\", encoding=\"utf-8\") as f:\n f.write(\"\\n\".join(output_lines) + \"\\n\")\n\n event_count = len(output_lines) - 1\n print(f\" {container}/{out_filename} (copaw): {event_count} events\")\n total_sessions += 1\n total_events += event_count\n\n return total_sessions, total_events\n\n\ndef export_hermes_sessions(container: str, sessions_dir: str, since_epoch: float,\n out_dir: Path, redact: bool) -> tuple[int, int]:\n ls_output = docker_exec(container, f\"ls '{sessions_dir}'/*.jsonl 2>/dev/null\").strip()\n if not ls_output:\n return 0, 0\n\n total_sessions = 0\n total_events = 0\n\n for session_path in [f.strip() for f in ls_output.splitlines() if f.strip()]:\n raw = docker_exec(container, f\"cat '{session_path}'\")\n if not raw.strip():\n continue\n\n output_lines = []\n last_ts = 0.0\n for line in raw.splitlines():\n line = line.strip()\n if not line:\n continue\n try:\n event = json.loads(line)\n except json.JSONDecodeError:\n continue\n\n event_ts = parse_ts(event.get(\"timestamp\", \"\"))\n if event_ts:\n last_ts = max(last_ts, event_ts)\n\n role = event.get(\"role\", \"\")\n if role != \"session_meta\" and event_ts < since_epoch and event_ts > 0:\n continue\n\n if redact:\n event = redact_json_strings(event)\n output_lines.append(json.dumps(event, ensure_ascii=False))\n\n if not output_lines:\n continue\n\n if last_ts < since_epoch and last_ts > 0:\n continue\n\n filename = os.path.basename(session_path)\n with open(out_dir / filename, \"w\", encoding=\"utf-8\") as f:\n f.write(\"\\n\".join(output_lines) + \"\\n\")\n\n event_count = len(output_lines) - 1 if output_lines and '\"role\": \"session_meta\"' in output_lines[0] else len(output_lines)\n print(f\" {container}/{filename} (hermes): {event_count} events\")\n total_sessions += 1\n total_events += max(event_count, 0)\n\n hermes_home = str(Path(sessions_dir).parent)\n state_db = f\"{hermes_home}/state.db\"\n has_state_db = docker_exec(container, f\"test -f '{state_db}' && echo yes || echo no\").strip() == \"yes\"\n if has_state_db:\n raw = docker_exec(container, f\"python3 - <<'PY'\\nimport json, sqlite3\\nconn = sqlite3.connect('{state_db}')\\nconn.row_factory = sqlite3.Row\\nrows = conn.execute('SELECT * FROM sessions ORDER BY started_at DESC LIMIT 200').fetchall()\\nprint(json.dumps([dict(r) for r in rows], ensure_ascii=False, indent=2))\\nPY\")\n if raw.strip():\n try:\n data = json.loads(raw)\n if redact:\n data = redact_json_strings(data)\n with open(out_dir / \"sessions-db.json\", \"w\", encoding=\"utf-8\") as f:\n json.dump(data, f, ensure_ascii=False, indent=2)\n except json.JSONDecodeError:\n pass\n\n logs_dir = f\"{hermes_home}/logs\"\n has_logs_dir = docker_exec(container, f\"test -d '{logs_dir}' && echo yes || echo no\").strip() == \"yes\"\n if has_logs_dir:\n for log_name in (\"agent.log\", \"errors.log\", \"gateway.log\"):\n raw = docker_exec(container, f\"test -f '{logs_dir}/{log_name}' && cat '{logs_dir}/{log_name}' || true\")\n if raw.strip():\n content = redact_pii(raw) if redact else raw\n (out_dir / log_name).write_text(content, encoding=\"utf-8\")\n\n return total_sessions, total_events\n\n\ndef export_agent_sessions(out_dir: Path, since_epoch: float, redact: bool,\n container_filter: str | None) -> tuple[int, int]:\n \"\"\"Export agent sessions from all containers. Returns (session_count, event_count).\"\"\"\n containers = list_hiclaw_containers()\n if container_filter:\n containers = [c for c in containers if container_filter in c]\n\n if not containers:\n print(\" [sessions] No matching AgentTeams containers found\")\n return 0, 0\n\n total_sessions = 0\n total_events = 0\n\n for container in containers:\n runtime, sessions_dir = detect_runtime(container)\n if not runtime:\n print(f\" {container}: no sessions directory, skipped\")\n continue\n\n container_dir = out_dir / container\n container_dir.mkdir(parents=True, exist_ok=True)\n\n if runtime == \"openclaw\":\n s, e = export_openclaw_sessions(container, sessions_dir, since_epoch,\n container_dir, redact)\n elif runtime == \"hermes\":\n s, e = export_hermes_sessions(container, sessions_dir, since_epoch,\n container_dir, redact)\n else:\n s, e = export_copaw_sessions(container, sessions_dir, since_epoch,\n container_dir, redact)\n\n if s == 0:\n if not any(container_dir.iterdir()):\n container_dir.rmdir()\n print(f\" {container} ({runtime}): no sessions in range, skipped\")\n else:\n print(f\" {container} ({runtime}): exported auxiliary debug artifacts without session events\")\n else:\n total_sessions += s\n total_events += e\n\n return total_sessions, total_events\n\n\n# ---------------------------------------------------------------------------\n# Main\n# ---------------------------------------------------------------------------\n\ndef main():\n parser = argparse.ArgumentParser(\n description=\"Export AgentTeams debug logs (Matrix messages + agent sessions)\"\n )\n parser.add_argument(\"--range\", \"-r\", required=True, dest=\"time_range\",\n help=\"Time range to export, e.g. 10m, 1h, 1d\")\n parser.add_argument(\"--container\", \"-c\", default=None,\n help=\"Only export sessions from containers matching this substring\")\n parser.add_argument(\"--room\", default=None,\n help=\"Only export Matrix rooms matching this substring\")\n parser.add_argument(\"--homeserver\", \"-s\", default=\"\",\n help=\"Matrix homeserver URL (auto-detected from env file)\")\n parser.add_argument(\"--token\", \"-t\", default=\"\",\n help=\"Matrix access token (auto-detected from env file)\")\n parser.add_argument(\"--env-file\", default=None,\n help=\"Path to agentteams-manager.env (default: ~/agentteams-manager.env)\")\n parser.add_argument(\"--messages-only\", action=\"store_true\",\n help=\"Only export m.room.message events (skip state events)\")\n parser.add_argument(\"--no-redact\", action=\"store_true\",\n help=\"Disable PII redaction\")\n args = parser.parse_args()\n\n range_seconds = parse_range(args.time_range)\n since_epoch = time.time() - range_seconds\n since_human = datetime.fromtimestamp(since_epoch, tz=timezone.utc).isoformat()\n now_str = datetime.now().strftime(\"%Y%m%d-%H%M%S\")\n\n run_dir = Path(\"debug-log\") / now_str\n run_dir.mkdir(parents=True, exist_ok=True)\n redact = not args.no_redact\n\n print(f\"AgentTeams Debug Log Export\")\n print(f\" Range: last {args.time_range} (since {since_human})\")\n print(f\" Output: {run_dir.resolve()}\")\n print(f\" PII redaction: {'on' if redact else 'off'}\")\n print()\n\n # --- Matrix messages ---\n print(\"=== Matrix Messages ===\")\n matrix_dir = run_dir / \"matrix-messages\"\n rooms, messages = export_matrix_messages(\n matrix_dir, since_epoch, redact,\n room_filter=args.room, env_file=args.env_file,\n homeserver=args.homeserver, token=args.token,\n messages_only=args.messages_only,\n )\n if rooms == 0 and matrix_dir.exists() and not any(matrix_dir.iterdir()):\n matrix_dir.rmdir()\n print()\n\n # --- Agent sessions ---\n print(\"=== Agent Sessions ===\")\n sessions_dir = run_dir / \"agent-sessions\"\n sessions, events = export_agent_sessions(\n sessions_dir, since_epoch, redact,\n container_filter=args.container,\n )\n if sessions == 0 and sessions_dir.exists() and not any(sessions_dir.iterdir()):\n sessions_dir.rmdir()\n print()\n\n # --- Summary ---\n summary = (\n f\"AgentTeams Debug Log\\n\"\n f\"Exported at: {now_str}\\n\"\n f\"Range: last {args.time_range} (since {since_human})\\n\"\n f\"PII redaction: {'on' if redact else 'off'}\\n\"\n f\"\\n\"\n f\"Matrix messages: {messages} messages from {rooms} rooms\\n\"\n f\"Agent sessions: {events} events from {sessions} sessions\\n\"\n )\n (run_dir / \"summary.txt\").write_text(summary)\n\n print(f\"Done. {messages} messages from {rooms} rooms, {events} events from {sessions} sessions\")\n print(f\"Output: {run_dir.resolve()}\")\n\n\nif __name__ == \"__main__\":\n main()\n"}, "24d3113962335db56cad7bff2e758373968791d6": {"t": 1785764741.7238102, "n": 28802, "h": "2d52e541d8045264", "c": "#!/usr/bin/env python3\nfrom __future__ import annotations\n\"\"\"\nExport HiClaw debug logs: Matrix messages + agent session logs.\n\nUsage:\n # Export last 1 hour (default)\n python scripts/export-debug-log.py --range 1h\n\n # Export last 1 day\n python scripts/export-debug-log.py --range 1d\n\n # Filter by container or room\n python scripts/export-debug-log.py --range 1h --container hiclaw-manager --room Worker\n\n # Disable PII redaction\n python scripts/export-debug-log.py --range 1h --no-redact\n\nOutput structure:\n debug-log/20260319-153000/\n ├── summary.txt\n ├── matrix-messages/\n │ └── RoomName_!roomid.jsonl\n └── agent-sessions/\n ├── hiclaw-manager/\n │ └── {session-id}.jsonl\n └── hiclaw-worker-xxx/\n └── {session-key}.jsonl\n\"\"\"\n\nimport argparse\nimport json\nimport os\nimport re\nimport subprocess\nimport sys\nimport time\nimport urllib.parse\nimport urllib.request\nimport urllib.error\nfrom datetime import datetime, timezone\nfrom pathlib import Path\n\n\n# ---------------------------------------------------------------------------\n# PII redaction\n# ---------------------------------------------------------------------------\n\n_PII_PATTERNS: list[tuple[str, re.Pattern]] = [\n (\"ID_CARD\", re.compile(r'\\b[1-9]\\d{5}(?:19|20)\\d{2}(?:0[1-9]|1[0-2])(?:0[1-9]|[12]\\d|3[01])\\d{3}[\\dXx]\\b')),\n (\"PHONE\", re.compile(r'(?<!\\d)(?:\\+?86[-\\s]?)?1[3-9]\\d{9}(?!\\d)')),\n (\"EMAIL\", re.compile(r'\\b[A-Za-z0-9._%+\\-]+@[A-Za-z0-9.\\-]+\\.[A-Za-z]{2,}\\b')),\n (\"BANK_CARD\", re.compile(r'\\b(?:6[0-9]{15,18}|4[0-9]{15}|5[1-5][0-9]{14}|3[47][0-9]{13}|62[0-9]{14,17})\\b')),\n (\"IP\", re.compile(r'\\b(?:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)\\.){3}(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)\\b')),\n (\"ALIYUN_AK\", re.compile(r'\\bLTAI[A-Za-z0-9]{12,30}\\b')),\n (\"ALIYUN_SK\", re.compile(r'(?i)(?:access_?key_?secret|secret_?access_?key)\\s*[:=]\\s*(\\S{20,})')),\n (\"AWS_AK\", re.compile(r'\\b(?:AKIA|ASIA)[A-Z0-9]{16}\\b')),\n (\"OPENAI_KEY\", re.compile(r'\\bsk-[A-Za-z0-9]{20,}\\b')),\n (\"ANTHROPIC_KEY\", re.compile(r'\\bsk-ant-[A-Za-z0-9\\-]{20,}\\b')),\n (\"DASHSCOPE_KEY\", re.compile(r'\\bsk-sp-[A-Za-z0-9]{20,}\\b')),\n (\"DEEPSEEK_KEY\", re.compile(r'\\bsk-[a-f0-9]{32,}\\b')),\n (\"BEARER\", re.compile(r'(?i)(Bearer\\s+)([A-Za-z0-9\\-_.]{20,})')),\n (\"SECRET_KV\", re.compile(\n r'(?i)((?:password|passwd|pwd|secret|token|api_?key|access_?key|secret_?key'\n r'|private_?key|credential|appkey|app_?secret|auth_?token|signing_?key'\n r'|client_?secret|master_?key)\\s*[:=]\\s*)'\n r'(\\S+)'\n )),\n (\"MATRIX_TOKEN\", re.compile(r'\\bsyt_[A-Za-z0-9_\\-]{10,}\\b')),\n (\"HEX_SECRET\", re.compile(r'\\b[A-Fa-f0-9]{32,}\\b')),\n (\"PASSPORT\", re.compile(r'\\b[EeGg]\\d{8}\\b')),\n (\"SSN\", re.compile(r'\\b\\d{3}-\\d{2}-\\d{4}\\b')),\n]\n\n\ndef redact_pii(text: str) -> str:\n if not text:\n return text\n for name, pattern in _PII_PATTERNS:\n if name in (\"SECRET_KV\", \"ALIYUN_SK\", \"BEARER\"):\n text = pattern.sub(r'\\1****', text)\n else:\n text = pattern.sub('****', text)\n return text\n\n\ndef redact_json_strings(obj):\n if isinstance(obj, str):\n return redact_pii(obj)\n if isinstance(obj, list):\n return [redact_json_strings(v) for v in obj]\n if isinstance(obj, dict):\n return {k: redact_json_strings(v) for k, v in obj.items()}\n return obj\n\n\n# ---------------------------------------------------------------------------\n# Common helpers\n# ---------------------------------------------------------------------------\n\ndef parse_range(range_str: str) -> int:\n m = re.fullmatch(r\"(\\d+)\\s*(m|min|h|hr|hour|d|day)s?\", range_str.strip(), re.IGNORECASE)\n if not m:\n raise ValueError(f\"Invalid range format: '{range_str}'. Use e.g. 10m, 1h, 1d\")\n value, unit = int(m.group(1)), m.group(2)[0].lower()\n return value * {\"m\": 60, \"h\": 3600, \"d\": 86400}[unit]\n\n\ndef parse_ts(ts_str: str) -> float:\n if not ts_str:\n return 0\n ts_str = ts_str.replace(\"Z\", \"+00:00\")\n try:\n dt = datetime.fromisoformat(ts_str)\n if dt.tzinfo is None:\n dt = dt.replace(tzinfo=timezone.utc)\n return dt.timestamp()\n except Exception:\n return 0\n\n\ndef sanitize_filename(name: str) -> str:\n return re.sub(r'[^\\w\\-. ]', '_', name).strip()[:80]\n\n\ndef docker_exec(container: str, cmd: str) -> str:\n result = subprocess.run(\n [\"docker\", \"exec\", container, \"sh\", \"-c\", cmd],\n capture_output=True, text=True, timeout=30,\n )\n return result.stdout\n\n\ndef list_hiclaw_containers() -> list[str]:\n result = subprocess.run(\n [\"docker\", \"ps\", \"--format\", \"{{.Names}}\", \"--filter\", \"name=hiclaw-\"],\n capture_output=True, text=True, timeout=10,\n )\n return [n.strip() for n in result.stdout.splitlines() if n.strip()]\n\n\n# ---------------------------------------------------------------------------\n# Matrix messages export\n# ---------------------------------------------------------------------------\n\ndef load_env_file(path: str) -> dict[str, str]:\n env = {}\n try:\n with open(path) as f:\n for line in f:\n line = line.strip()\n if not line or line.startswith(\"#\"):\n continue\n if \"=\" in line:\n key, _, value = line.partition(\"=\")\n env[key.strip()] = value.strip()\n except FileNotFoundError:\n pass\n return env\n\n\ndef matrix_login(homeserver: str, user: str, password: str) -> str:\n url = f\"{homeserver.rstrip('/')}/_matrix/client/v3/login\"\n payload = json.dumps({\n \"type\": \"m.login.password\",\n \"identifier\": {\"type\": \"m.id.user\", \"user\": user},\n \"password\": password,\n }).encode()\n req = urllib.request.Request(url, data=payload, headers={\"Content-Type\": \"application/json\"})\n with urllib.request.urlopen(req, timeout=15) as resp:\n data = json.loads(resp.read())\n return data[\"access_token\"]\n\n\ndef matrix_api(homeserver: str, token: str, endpoint: str, params: dict | None = None) -> dict:\n url = f\"{homeserver.rstrip('/')}/_matrix/client/v3/{endpoint}\"\n if params:\n url += \"?\" + urllib.parse.urlencode(params)\n req = urllib.request.Request(url, headers={\"Authorization\": f\"Bearer {token}\"})\n try:\n with urllib.request.urlopen(req, timeout=30) as resp:\n return json.loads(resp.read())\n except urllib.error.HTTPError as e:\n body = e.read().decode(errors=\"replace\")\n print(f\" API error {e.code} on {endpoint}: {body}\", file=sys.stderr)\n raise\n\n\ndef fetch_room_messages(homeserver: str, token: str, room_id: str, since_ts: int) -> list[dict]:\n encoded = urllib.parse.quote(room_id)\n messages = []\n from_token = \"\"\n while True:\n params = {\"dir\": \"b\", \"limit\": \"100\"}\n if from_token:\n params[\"from\"] = from_token\n data = matrix_api(homeserver, token, f\"rooms/{encoded}/messages\", params)\n chunk = data.get(\"chunk\", [])\n if not chunk:\n break\n hit_boundary = False\n for event in chunk:\n if event.get(\"origin_server_ts\", 0) < since_ts:\n hit_boundary = True\n break\n messages.append(event)\n if hit_boundary:\n break\n next_token = data.get(\"end\")\n if not next_token or next_token == from_token:\n break\n from_token = next_token\n messages.reverse()\n return messages\n\n\ndef format_event(event: dict, redact: bool) -> dict:\n content = event.get(\"content\", {})\n ts = event.get(\"origin_server_ts\", 0)\n record = {\n \"event_id\": event.get(\"event_id\"),\n \"type\": event.get(\"type\"),\n \"sender\": event.get(\"sender\"),\n \"timestamp\": ts,\n \"time\": datetime.fromtimestamp(ts / 1000, tz=timezone.utc).isoformat(),\n }\n if event.get(\"type\") == \"m.room.message\":\n record[\"msgtype\"] = content.get(\"msgtype\")\n body = content.get(\"body\")\n record[\"body\"] = redact_pii(body) if redact else body\n if content.get(\"format\"):\n record[\"format\"] = content[\"format\"]\n if content.get(\"url\"):\n record[\"url\"] = content[\"url\"]\n if content.get(\"m.relates_to\"):\n record[\"relates_to\"] = content[\"m.relates_to\"]\n else:\n record[\"content\"] = content\n return record\n\n\ndef export_matrix_messages(out_dir: Path, since_epoch: float, redact: bool,\n room_filter: str | None, env_file: str | None,\n homeserver: str, token: str,\n messages_only: bool) -> tuple[int, int]:\n \"\"\"Export Matrix messages. Returns (rooms_exported, message_count).\"\"\"\n env_path = env_file or os.path.expanduser(\"~/hiclaw-manager.env\")\n hiclaw_env = load_env_file(env_path)\n\n if not homeserver:\n if not hiclaw_env:\n print(f\" [matrix] Cannot find {env_path}, skipping Matrix export\")\n return 0, 0\n port = hiclaw_env.get(\"HICLAW_PORT_GATEWAY\", \"18080\")\n homeserver = f\"http://127.0.0.1:{port}\"\n\n if not token:\n # Use Manager token — Manager is in every room (DM, Worker, Project)\n manager_password = hiclaw_env.get(\"HICLAW_MANAGER_PASSWORD\", \"\")\n if manager_password:\n try:\n token = matrix_login(homeserver, \"manager\", manager_password)\n except Exception:\n pass\n\n # Fallback to admin token if Manager login failed\n if not token:\n admin_user = hiclaw_env.get(\"HICLAW_ADMIN_USER\", \"admin\")\n admin_password = hiclaw_env.get(\"HICLAW_ADMIN_PASSWORD\", \"\")\n if not admin_password:\n print(f\" [matrix] No usable credentials found, skipping Matrix export\")\n return 0, 0\n try:\n token = matrix_login(homeserver, admin_user, admin_password)\n except Exception as e:\n print(f\" [matrix] Login failed: {e}, skipping Matrix export\")\n return 0, 0\n\n since_ts = int(since_epoch * 1000)\n out_dir.mkdir(parents=True, exist_ok=True)\n\n try:\n rooms = matrix_api(homeserver, token, \"joined_rooms\").get(\"joined_rooms\", [])\n except Exception as e:\n print(f\" [matrix] Failed to list rooms: {e}\")\n return 0, 0\n\n total_messages = 0\n total_rooms = 0\n\n for room_id in rooms:\n encoded = urllib.parse.quote(room_id)\n try:\n room_name = matrix_api(homeserver, token, f\"rooms/{encoded}/state/m.room.name\").get(\"name\", \"\")\n except Exception:\n room_name = \"\"\n\n if room_filter:\n if room_filter not in room_id and room_filter not in (room_name or \"\"):\n continue\n\n display = f\"{room_name} ({room_id})\" if room_name else room_id\n print(f\" {display} ... \", end=\"\", flush=True)\n\n messages = fetch_room_messages(homeserver, token, room_id, since_ts)\n if messages_only:\n messages = [e for e in messages if e.get(\"type\") == \"m.room.message\"]\n\n if not messages:\n print(\"0 messages, skipped\")\n continue\n\n name_part = sanitize_filename(room_name) if room_name else \"\"\n id_part = sanitize_filename(room_id)\n filename = f\"{name_part}_{id_part}.jsonl\" if name_part else f\"{id_part}.jsonl\"\n\n with open(out_dir / filename, \"w\", encoding=\"utf-8\") as f:\n for event in messages:\n record = format_event(event, redact=redact)\n f.write(json.dumps(record, ensure_ascii=False) + \"\\n\")\n\n print(f\"{len(messages)} messages -> {filename}\")\n total_messages += len(messages)\n total_rooms += 1\n\n return total_rooms, total_messages\n\n\n# ---------------------------------------------------------------------------\n# Agent sessions export (OpenClaw + CoPaw + Hermes)\n# ---------------------------------------------------------------------------\n\ndef detect_runtime(container: str) -> tuple[str, str]:\n \"\"\"Probe known absolute session-dir locations across runtimes & container types.\n\n Returns (runtime_name, absolute_sessions_dir). Empty tuple if none found.\n\n Layout reference (verified against worker/scripts/worker-entrypoint.sh,\n copaw/scripts/copaw-worker-entrypoint.sh, copaw/AGENTS.md and\n tests/lib/agent-metrics.sh):\n\n Manager (any runtime, HOME=/root/manager-workspace):\n openclaw -> /root/manager-workspace/.openclaw/agents/main/sessions\n hermes -> /root/manager-workspace/.hermes/sessions\n copaw -> /root/manager-workspace/.copaw/workspaces/default/sessions\n\n OpenClaw / Hermes Worker (HOME=/root/hiclaw-fs/agents/<name>):\n openclaw -> /root/hiclaw-fs/agents/<name>/.openclaw/agents/main/sessions\n hermes -> /root/hiclaw-fs/agents/<name>/.hermes/sessions\n\n CoPaw Worker (HOME=/root/.hiclaw-worker/<name>, also reachable via\n /root/hiclaw-fs symlink that points to that same dir):\n copaw -> /root/.hiclaw-worker/<name>/.copaw/workspaces/default/sessions\n copaw -> /root/hiclaw-fs/.copaw/workspaces/default/sessions (alt)\n \"\"\"\n candidates: list[tuple[str, str]] = [\n (\"openclaw\", \"/root/manager-workspace/.openclaw/agents/main/sessions\"),\n (\"hermes\", \"/root/manager-workspace/.hermes/sessions\"),\n (\"copaw\", \"/root/manager-workspace/.copaw/workspaces/default/sessions\"),\n ]\n\n worker_name = docker_exec(container, \"echo $HICLAW_WORKER_NAME\").strip()\n if worker_name:\n candidates.extend([\n (\"openclaw\", f\"/root/hiclaw-fs/agents/{worker_name}/.openclaw/agents/main/sessions\"),\n (\"hermes\", f\"/root/hiclaw-fs/agents/{worker_name}/.hermes/sessions\"),\n (\"copaw\", f\"/root/.hiclaw-worker/{worker_name}/.copaw/workspaces/default/sessions\"),\n (\"copaw\", \"/root/hiclaw-fs/.copaw/workspaces/default/sessions\"),\n ])\n\n for runtime, path in candidates:\n if docker_exec(container, f\"test -d '{path}' && echo yes || echo no\").strip() == \"yes\":\n return runtime, path\n\n # Last-resort: scan from / for any known session dir layout.\n found = docker_exec(\n container,\n \"find / -maxdepth 7 \\\\( \"\n \"-path '*/.openclaw/agents/main/sessions' \"\n \"-o -path '*/.hermes/sessions' \"\n \"-o -path '*/.copaw/workspaces/default/sessions' \"\n \"\\\\) -type d 2>/dev/null | head -1\",\n ).strip()\n if found:\n if \"/.openclaw/\" in found:\n return \"openclaw\", found\n if \"/.hermes/\" in found:\n return \"hermes\", found\n return \"copaw\", found\n\n return \"\", \"\"\n\n\ndef export_openclaw_sessions(container: str, sessions_dir: str, since_epoch: float,\n out_dir: Path, redact: bool) -> tuple[int, int]:\n ls_output = docker_exec(container, f\"ls '{sessions_dir}'/*.jsonl 2>/dev/null\").strip()\n if not ls_output:\n return 0, 0\n\n total_sessions = 0\n total_events = 0\n\n for session_path in [f.strip() for f in ls_output.splitlines() if f.strip()]:\n filename = os.path.basename(session_path)\n\n first_line = docker_exec(container, f\"head -1 '{session_path}'\").strip()\n if not first_line:\n continue\n try:\n header = json.loads(first_line)\n except json.JSONDecodeError:\n continue\n\n last_line = docker_exec(container, f\"tail -1 '{session_path}'\").strip()\n try:\n last_ts = parse_ts(json.loads(last_line).get(\"timestamp\", \"\"))\n except Exception:\n last_ts = parse_ts(header.get(\"timestamp\", \"\"))\n\n if last_ts < since_epoch and last_ts > 0:\n continue\n\n raw = docker_exec(container, f\"cat '{session_path}'\")\n if not raw.strip():\n continue\n\n output_lines = []\n for line in raw.splitlines():\n line = line.strip()\n if not line:\n continue\n try:\n event = json.loads(line)\n except json.JSONDecodeError:\n continue\n event_ts = parse_ts(event.get(\"timestamp\", \"\"))\n if event.get(\"type\") != \"session\" and event_ts < since_epoch and event_ts > 0:\n continue\n if redact:\n event = redact_json_strings(event)\n output_lines.append(json.dumps(event, ensure_ascii=False))\n\n if len(output_lines) <= 1:\n continue\n\n with open(out_dir / filename, \"w\", encoding=\"utf-8\") as f:\n f.write(\"\\n\".join(output_lines) + \"\\n\")\n\n event_count = len(output_lines) - 1\n print(f\" {container}/{filename} (openclaw): {event_count} events\")\n total_sessions += 1\n total_events += event_count\n\n # sessions.json index\n try:\n raw = docker_exec(container, f\"cat '{sessions_dir}/sessions.json'\")\n if raw.strip():\n index = json.loads(raw)\n if redact:\n index = redact_json_strings(index)\n with open(out_dir / \"sessions.json\", \"w\", encoding=\"utf-8\") as f:\n json.dump(index, f, ensure_ascii=False, indent=2)\n except Exception:\n pass\n\n return total_sessions, total_events\n\n\ndef export_copaw_sessions(container: str, sessions_dir: str, since_epoch: float,\n out_dir: Path, redact: bool) -> tuple[int, int]:\n ls_output = docker_exec(container, f\"find '{sessions_dir}' -name '*.json' -type f 2>/dev/null\").strip()\n if not ls_output:\n return 0, 0\n\n total_sessions = 0\n total_events = 0\n\n for session_path in [f.strip() for f in ls_output.splitlines() if f.strip()]:\n raw = docker_exec(container, f\"cat '{session_path}'\")\n if not raw.strip():\n continue\n try:\n data = json.loads(raw)\n except json.JSONDecodeError:\n continue\n\n agent = data.get(\"agent\", {})\n memory = agent.get(\"memory\", {})\n content = memory.get(\"content\", [])\n if not content:\n continue\n\n basename = os.path.basename(session_path).replace(\".json\", \"\")\n header = {\n \"type\": \"session\",\n \"runtime\": \"copaw\",\n \"agent_name\": agent.get(\"name\", \"\"),\n \"session_key\": basename,\n \"compressed_summary\": memory.get(\"_compressed_summary\", \"\"),\n }\n\n messages_in_range = []\n for turn_idx, turn in enumerate(content):\n if not isinstance(turn, list):\n continue\n for msg in turn:\n if not isinstance(msg, dict):\n continue\n msg_ts = parse_ts(msg.get(\"timestamp\", \"\"))\n if msg_ts >= since_epoch or msg_ts == 0:\n event = {\n \"type\": \"message\",\n \"turn\": turn_idx,\n \"id\": msg.get(\"id\", \"\"),\n \"role\": msg.get(\"role\", \"\"),\n \"name\": msg.get(\"name\", \"\"),\n \"timestamp\": msg.get(\"timestamp\", \"\"),\n \"content\": msg.get(\"content\", []),\n }\n if msg.get(\"metadata\"):\n event[\"metadata\"] = msg[\"metadata\"]\n messages_in_range.append(event)\n\n if not messages_in_range:\n continue\n\n output_lines = []\n if redact:\n header = redact_json_strings(header)\n output_lines.append(json.dumps(header, ensure_ascii=False))\n for event in messages_in_range:\n if redact:\n event = redact_json_strings(event)\n output_lines.append(json.dumps(event, ensure_ascii=False))\n\n out_filename = basename + \".jsonl\"\n with open(out_dir / out_filename, \"w\", encoding=\"utf-8\") as f:\n f.write(\"\\n\".join(output_lines) + \"\\n\")\n\n event_count = len(output_lines) - 1\n print(f\" {container}/{out_filename} (copaw): {event_count} events\")\n total_sessions += 1\n total_events += event_count\n\n return total_sessions, total_events\n\n\ndef export_hermes_sessions(container: str, sessions_dir: str, since_epoch: float,\n out_dir: Path, redact: bool) -> tuple[int, int]:\n ls_output = docker_exec(container, f\"ls '{sessions_dir}'/*.jsonl 2>/dev/null\").strip()\n if not ls_output:\n return 0, 0\n\n total_sessions = 0\n total_events = 0\n\n for session_path in [f.strip() for f in ls_output.splitlines() if f.strip()]:\n raw = docker_exec(container, f\"cat '{session_path}'\")\n if not raw.strip():\n continue\n\n output_lines = []\n last_ts = 0.0\n for line in raw.splitlines():\n line = line.strip()\n if not line:\n continue\n try:\n event = json.loads(line)\n except json.JSONDecodeError:\n continue\n\n event_ts = parse_ts(event.get(\"timestamp\", \"\"))\n if event_ts:\n last_ts = max(last_ts, event_ts)\n\n role = event.get(\"role\", \"\")\n if role != \"session_meta\" and event_ts < since_epoch and event_ts > 0:\n continue\n\n if redact:\n event = redact_json_strings(event)\n output_lines.append(json.dumps(event, ensure_ascii=False))\n\n if not output_lines:\n continue\n\n if last_ts < since_epoch and last_ts > 0:\n continue\n\n filename = os.path.basename(session_path)\n with open(out_dir / filename, \"w\", encoding=\"utf-8\") as f:\n f.write(\"\\n\".join(output_lines) + \"\\n\")\n\n event_count = len(output_lines) - 1 if output_lines and '\"role\": \"session_meta\"' in output_lines[0] else len(output_lines)\n print(f\" {container}/{filename} (hermes): {event_count} events\")\n total_sessions += 1\n total_events += max(event_count, 0)\n\n hermes_home = str(Path(sessions_dir).parent)\n state_db = f\"{hermes_home}/state.db\"\n has_state_db = docker_exec(container, f\"test -f '{state_db}' && echo yes || echo no\").strip() == \"yes\"\n if has_state_db:\n raw = docker_exec(container, f\"python3 - <<'PY'\\nimport json, sqlite3\\nconn = sqlite3.connect('{state_db}')\\nconn.row_factory = sqlite3.Row\\nrows = conn.execute('SELECT * FROM sessions ORDER BY started_at DESC LIMIT 200').fetchall()\\nprint(json.dumps([dict(r) for r in rows], ensure_ascii=False, indent=2))\\nPY\")\n if raw.strip():\n try:\n data = json.loads(raw)\n if redact:\n data = redact_json_strings(data)\n with open(out_dir / \"sessions-db.json\", \"w\", encoding=\"utf-8\") as f:\n json.dump(data, f, ensure_ascii=False, indent=2)\n except json.JSONDecodeError:\n pass\n\n logs_dir = f\"{hermes_home}/logs\"\n has_logs_dir = docker_exec(container, f\"test -d '{logs_dir}' && echo yes || echo no\").strip() == \"yes\"\n if has_logs_dir:\n for log_name in (\"agent.log\", \"errors.log\", \"gateway.log\"):\n raw = docker_exec(container, f\"test -f '{logs_dir}/{log_name}' && cat '{logs_dir}/{log_name}' || true\")\n if raw.strip():\n content = redact_pii(raw) if redact else raw\n (out_dir / log_name).write_text(content, encoding=\"utf-8\")\n\n return total_sessions, total_events\n\n\ndef export_agent_sessions(out_dir: Path, since_epoch: float, redact: bool,\n container_filter: str | None) -> tuple[int, int]:\n \"\"\"Export agent sessions from all containers. Returns (session_count, event_count).\"\"\"\n containers = list_hiclaw_containers()\n if container_filter:\n containers = [c for c in containers if container_filter in c]\n\n if not containers:\n print(\" [sessions] No matching hiclaw containers found\")\n return 0, 0\n\n total_sessions = 0\n total_events = 0\n\n for container in containers:\n runtime, sessions_dir = detect_runtime(container)\n if not runtime:\n print(f\" {container}: no sessions directory, skipped\")\n continue\n\n container_dir = out_dir / container\n container_dir.mkdir(parents=True, exist_ok=True)\n\n if runtime == \"openclaw\":\n s, e = export_openclaw_sessions(container, sessions_dir, since_epoch,\n container_dir, redact)\n elif runtime == \"hermes\":\n s, e = export_hermes_sessions(container, sessions_dir, since_epoch,\n container_dir, redact)\n else:\n s, e = export_copaw_sessions(container, sessions_dir, since_epoch,\n container_dir, redact)\n\n if s == 0:\n if not any(container_dir.iterdir()):\n container_dir.rmdir()\n print(f\" {container} ({runtime}): no sessions in range, skipped\")\n else:\n print(f\" {container} ({runtime}): exported auxiliary debug artifacts without session events\")\n else:\n total_sessions += s\n total_events += e\n\n return total_sessions, total_events\n\n\n# ---------------------------------------------------------------------------\n# Main\n# ---------------------------------------------------------------------------\n\ndef main():\n parser = argparse.ArgumentParser(\n description=\"Export HiClaw debug logs (Matrix messages + agent sessions)\"\n )\n parser.add_argument(\"--range\", \"-r\", required=True, dest=\"time_range\",\n help=\"Time range to export, e.g. 10m, 1h, 1d\")\n parser.add_argument(\"--container\", \"-c\", default=None,\n help=\"Only export sessions from containers matching this substring\")\n parser.add_argument(\"--room\", default=None,\n help=\"Only export Matrix rooms matching this substring\")\n parser.add_argument(\"--homeserver\", \"-s\", default=\"\",\n help=\"Matrix homeserver URL (auto-detected from env file)\")\n parser.add_argument(\"--token\", \"-t\", default=\"\",\n help=\"Matrix access token (auto-detected from env file)\")\n parser.add_argument(\"--env-file\", default=None,\n help=\"Path to hiclaw-manager.env (default: ~/hiclaw-manager.env)\")\n parser.add_argument(\"--messages-only\", action=\"store_true\",\n help=\"Only export m.room.message events (skip state events)\")\n parser.add_argument(\"--no-redact\", action=\"store_true\",\n help=\"Disable PII redaction\")\n args = parser.parse_args()\n\n range_seconds = parse_range(args.time_range)\n since_epoch = time.time() - range_seconds\n since_human = datetime.fromtimestamp(since_epoch, tz=timezone.utc).isoformat()\n now_str = datetime.now().strftime(\"%Y%m%d-%H%M%S\")\n\n run_dir = Path(\"debug-log\") / now_str\n run_dir.mkdir(parents=True, exist_ok=True)\n redact = not args.no_redact\n\n print(f\"HiClaw Debug Log Export\")\n print(f\" Range: last {args.time_range} (since {since_human})\")\n print(f\" Output: {run_dir.resolve()}\")\n print(f\" PII redaction: {'on' if redact else 'off'}\")\n print()\n\n # --- Matrix messages ---\n print(\"=== Matrix Messages ===\")\n matrix_dir = run_dir / \"matrix-messages\"\n rooms, messages = export_matrix_messages(\n matrix_dir, since_epoch, redact,\n room_filter=args.room, env_file=args.env_file,\n homeserver=args.homeserver, token=args.token,\n messages_only=args.messages_only,\n )\n if rooms == 0 and matrix_dir.exists() and not any(matrix_dir.iterdir()):\n matrix_dir.rmdir()\n print()\n\n # --- Agent sessions ---\n print(\"=== Agent Sessions ===\")\n sessions_dir = run_dir / \"agent-sessions\"\n sessions, events = export_agent_sessions(\n sessions_dir, since_epoch, redact,\n container_filter=args.container,\n )\n if sessions == 0 and sessions_dir.exists() and not any(sessions_dir.iterdir()):\n sessions_dir.rmdir()\n print()\n\n # --- Summary ---\n summary = (\n f\"HiClaw Debug Log\\n\"\n f\"Exported at: {now_str}\\n\"\n f\"Range: last {args.time_range} (since {since_human})\\n\"\n f\"PII redaction: {'on' if redact else 'off'}\\n\"\n f\"\\n\"\n f\"Matrix messages: {messages} messages from {rooms} rooms\\n\"\n f\"Agent sessions: {events} events from {sessions} sessions\\n\"\n )\n (run_dir / \"summary.txt\").write_text(summary)\n\n print(f\"Done. {messages} messages from {rooms} rooms, {events} events from {sessions} sessions\")\n print(f\"Output: {run_dir.resolve()}\")\n\n\nif __name__ == \"__main__\":\n main()\n"}, "e8573c5a5f6d56e168cfb297c7ccf6b0b6666d36": {"t": 1785764743.3521657, "n": 27093, "h": "bebbad10e49daba1", "c": "#!/usr/bin/env python3\nfrom __future__ import annotations\n\"\"\"\nExport HiClaw debug logs: Matrix messages + agent session logs.\n\nUsage:\n # Export last 1 hour (default)\n python scripts/export-debug-log.py --range 1h\n\n # Export last 1 day\n python scripts/export-debug-log.py --range 1d\n\n # Filter by container or room\n python scripts/export-debug-log.py --range 1h --container hiclaw-manager --room Worker\n\n # Disable PII redaction\n python scripts/export-debug-log.py --range 1h --no-redact\n\nOutput structure:\n debug-log/20260319-153000/\n ├── summary.txt\n ├── matrix-messages/\n │ └── RoomName_!roomid.jsonl\n └── agent-sessions/\n ├── hiclaw-manager/\n │ └── {session-id}.jsonl\n └── hiclaw-worker-xxx/\n └── {session-key}.jsonl\n\"\"\"\n\nimport argparse\nimport json\nimport os\nimport re\nimport subprocess\nimport sys\nimport time\nimport urllib.parse\nimport urllib.request\nimport urllib.error\nfrom datetime import datetime, timezone\nfrom pathlib import Path\n\n\n# ---------------------------------------------------------------------------\n# PII redaction\n# ---------------------------------------------------------------------------\n\n_PII_PATTERNS: list[tuple[str, re.Pattern]] = [\n (\"ID_CARD\", re.compile(r'\\b[1-9]\\d{5}(?:19|20)\\d{2}(?:0[1-9]|1[0-2])(?:0[1-9]|[12]\\d|3[01])\\d{3}[\\dXx]\\b')),\n (\"PHONE\", re.compile(r'(?<!\\d)(?:\\+?86[-\\s]?)?1[3-9]\\d{9}(?!\\d)')),\n (\"EMAIL\", re.compile(r'\\b[A-Za-z0-9._%+\\-]+@[A-Za-z0-9.\\-]+\\.[A-Za-z]{2,}\\b')),\n (\"BANK_CARD\", re.compile(r'\\b(?:6[0-9]{15,18}|4[0-9]{15}|5[1-5][0-9]{14}|3[47][0-9]{13}|62[0-9]{14,17})\\b')),\n (\"IP\", re.compile(r'\\b(?:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)\\.){3}(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)\\b')),\n (\"ALIYUN_AK\", re.compile(r'\\bLTAI[A-Za-z0-9]{12,30}\\b')),\n (\"ALIYUN_SK\", re.compile(r'(?i)(?:access_?key_?secret|secret_?access_?key)\\s*[:=]\\s*(\\S{20,})')),\n (\"AWS_AK\", re.compile(r'\\b(?:AKIA|ASIA)[A-Z0-9]{16}\\b')),\n (\"OPENAI_KEY\", re.compile(r'\\bsk-[A-Za-z0-9]{20,}\\b')),\n (\"ANTHROPIC_KEY\", re.compile(r'\\bsk-ant-[A-Za-z0-9\\-]{20,}\\b')),\n (\"DASHSCOPE_KEY\", re.compile(r'\\bsk-sp-[A-Za-z0-9]{20,}\\b')),\n (\"DEEPSEEK_KEY\", re.compile(r'\\bsk-[a-f0-9]{32,}\\b')),\n (\"BEARER\", re.compile(r'(?i)(Bearer\\s+)([A-Za-z0-9\\-_.]{20,})')),\n (\"SECRET_KV\", re.compile(\n r'(?i)((?:password|passwd|pwd|secret|token|api_?key|access_?key|secret_?key'\n r'|private_?key|credential|appkey|app_?secret|auth_?token|signing_?key'\n r'|client_?secret|master_?key)\\s*[:=]\\s*)'\n r'(\\S+)'\n )),\n (\"MATRIX_TOKEN\", re.compile(r'\\bsyt_[A-Za-z0-9_\\-]{10,}\\b')),\n (\"HEX_SECRET\", re.compile(r'\\b[A-Fa-f0-9]{32,}\\b')),\n (\"PASSPORT\", re.compile(r'\\b[EeGg]\\d{8}\\b')),\n (\"SSN\", re.compile(r'\\b\\d{3}-\\d{2}-\\d{4}\\b')),\n]\n\n\ndef redact_pii(text: str) -> str:\n if not text:\n return text\n for name, pattern in _PII_PATTERNS:\n if name in (\"SECRET_KV\", \"ALIYUN_SK\", \"BEARER\"):\n text = pattern.sub(r'\\1****', text)\n else:\n text = pattern.sub('****', text)\n return text\n\n\ndef redact_json_strings(obj):\n if isinstance(obj, str):\n return redact_pii(obj)\n if isinstance(obj, list):\n return [redact_json_strings(v) for v in obj]\n if isinstance(obj, dict):\n return {k: redact_json_strings(v) for k, v in obj.items()}\n return obj\n\n\n# ---------------------------------------------------------------------------\n# Common helpers\n# ---------------------------------------------------------------------------\n\ndef parse_range(range_str: str) -> int:\n m = re.fullmatch(r\"(\\d+)\\s*(m|min|h|hr|hour|d|day)s?\", range_str.strip(), re.IGNORECASE)\n if not m:\n raise ValueError(f\"Invalid range format: '{range_str}'. Use e.g. 10m, 1h, 1d\")\n value, unit = int(m.group(1)), m.group(2)[0].lower()\n return value * {\"m\": 60, \"h\": 3600, \"d\": 86400}[unit]\n\n\ndef parse_ts(ts_str: str) -> float:\n if not ts_str:\n return 0\n ts_str = ts_str.replace(\"Z\", \"+00:00\")\n try:\n dt = datetime.fromisoformat(ts_str)\n if dt.tzinfo is None:\n dt = dt.replace(tzinfo=timezone.utc)\n return dt.timestamp()\n except Exception:\n return 0\n\n\ndef sanitize_filename(name: str) -> str:\n return re.sub(r'[^\\w\\-. ]', '_', name).strip()[:80]\n\n\ndef docker_exec(container: str, cmd: str) -> str:\n result = subprocess.run(\n [\"docker\", \"exec\", container, \"sh\", \"-c\", cmd],\n capture_output=True, text=True, timeout=30,\n )\n return result.stdout\n\n\ndef list_hiclaw_containers() -> list[str]:\n result = subprocess.run(\n [\"docker\", \"ps\", \"--format\", \"{{.Names}}\", \"--filter\", \"name=hiclaw-\"],\n capture_output=True, text=True, timeout=10,\n )\n return [n.strip() for n in result.stdout.splitlines() if n.strip()]\n\n\n# ---------------------------------------------------------------------------\n# Matrix messages export\n# ---------------------------------------------------------------------------\n\ndef load_env_file(path: str) -> dict[str, str]:\n env = {}\n try:\n with open(path) as f:\n for line in f:\n line = line.strip()\n if not line or line.startswith(\"#\"):\n continue\n if \"=\" in line:\n key, _, value = line.partition(\"=\")\n env[key.strip()] = value.strip()\n except FileNotFoundError:\n pass\n return env\n\n\ndef matrix_login(homeserver: str, user: str, password: str) -> str:\n url = f\"{homeserver.rstrip('/')}/_matrix/client/v3/login\"\n payload = json.dumps({\n \"type\": \"m.login.password\",\n \"identifier\": {\"type\": \"m.id.user\", \"user\": user},\n \"password\": password,\n }).encode()\n req = urllib.request.Request(url, data=payload, headers={\"Content-Type\": \"application/json\"})\n with urllib.request.urlopen(req, timeout=15) as resp:\n data = json.loads(resp.read())\n return data[\"access_token\"]\n\n\ndef matrix_api(homeserver: str, token: str, endpoint: str, params: dict | None = None) -> dict:\n url = f\"{homeserver.rstrip('/')}/_matrix/client/v3/{endpoint}\"\n if params:\n url += \"?\" + urllib.parse.urlencode(params)\n req = urllib.request.Request(url, headers={\"Authorization\": f\"Bearer {token}\"})\n try:\n with urllib.request.urlopen(req, timeout=30) as resp:\n return json.loads(resp.read())\n except urllib.error.HTTPError as e:\n body = e.read().decode(errors=\"replace\")\n print(f\" API error {e.code} on {endpoint}: {body}\", file=sys.stderr)\n raise\n\n\ndef fetch_room_messages(homeserver: str, token: str, room_id: str, since_ts: int) -> list[dict]:\n encoded = urllib.parse.quote(room_id)\n messages = []\n from_token = \"\"\n while True:\n params = {\"dir\": \"b\", \"limit\": \"100\"}\n if from_token:\n params[\"from\"] = from_token\n data = matrix_api(homeserver, token, f\"rooms/{encoded}/messages\", params)\n chunk = data.get(\"chunk\", [])\n if not chunk:\n break\n hit_boundary = False\n for event in chunk:\n if event.get(\"origin_server_ts\", 0) < since_ts:\n hit_boundary = True\n break\n messages.append(event)\n if hit_boundary:\n break\n next_token = data.get(\"end\")\n if not next_token or next_token == from_token:\n break\n from_token = next_token\n messages.reverse()\n return messages\n\n\ndef format_event(event: dict, redact: bool) -> dict:\n content = event.get(\"content\", {})\n ts = event.get(\"origin_server_ts\", 0)\n record = {\n \"event_id\": event.get(\"event_id\"),\n \"type\": event.get(\"type\"),\n \"sender\": event.get(\"sender\"),\n \"timestamp\": ts,\n \"time\": datetime.fromtimestamp(ts / 1000, tz=timezone.utc).isoformat(),\n }\n if event.get(\"type\") == \"m.room.message\":\n record[\"msgtype\"] = content.get(\"msgtype\")\n body = content.get(\"body\")\n record[\"body\"] = redact_pii(body) if redact else body\n if content.get(\"format\"):\n record[\"format\"] = content[\"format\"]\n if content.get(\"url\"):\n record[\"url\"] = content[\"url\"]\n if content.get(\"m.relates_to\"):\n record[\"relates_to\"] = content[\"m.relates_to\"]\n else:\n record[\"content\"] = content\n return record\n\n\ndef export_matrix_messages(out_dir: Path, since_epoch: float, redact: bool,\n room_filter: str | None, env_file: str | None,\n homeserver: str, token: str,\n messages_only: bool) -> tuple[int, int]:\n \"\"\"Export Matrix messages. Returns (rooms_exported, message_count).\"\"\"\n env_path = env_file or os.path.expanduser(\"~/hiclaw-manager.env\")\n hiclaw_env = load_env_file(env_path)\n\n if not homeserver:\n if not hiclaw_env:\n print(f\" [matrix] Cannot find {env_path}, skipping Matrix export\")\n return 0, 0\n port = hiclaw_env.get(\"HICLAW_PORT_GATEWAY\", \"18080\")\n homeserver = f\"http://127.0.0.1:{port}\"\n\n if not token:\n # Use Manager token — Manager is in every room (DM, Worker, Project)\n manager_password = hiclaw_env.get(\"HICLAW_MANAGER_PASSWORD\", \"\")\n if manager_password:\n try:\n token = matrix_login(homeserver, \"manager\", manager_password)\n except Exception:\n pass\n\n # Fallback to admin token if Manager login failed\n if not token:\n admin_user = hiclaw_env.get(\"HICLAW_ADMIN_USER\", \"admin\")\n admin_password = hiclaw_env.get(\"HICLAW_ADMIN_PASSWORD\", \"\")\n if not admin_password:\n print(f\" [matrix] No usable credentials found, skipping Matrix export\")\n return 0, 0\n try:\n token = matrix_login(homeserver, admin_user, admin_password)\n except Exception as e:\n print(f\" [matrix] Login failed: {e}, skipping Matrix export\")\n return 0, 0\n\n since_ts = int(since_epoch * 1000)\n out_dir.mkdir(parents=True, exist_ok=True)\n\n try:\n rooms = matrix_api(homeserver, token, \"joined_rooms\").get(\"joined_rooms\", [])\n except Exception as e:\n print(f\" [matrix] Failed to list rooms: {e}\")\n return 0, 0\n\n total_messages = 0\n total_rooms = 0\n\n for room_id in rooms:\n encoded = urllib.parse.quote(room_id)\n try:\n room_name = matrix_api(homeserver, token, f\"rooms/{encoded}/state/m.room.name\").get(\"name\", \"\")\n except Exception:\n room_name = \"\"\n\n if room_filter:\n if room_filter not in room_id and room_filter not in (room_name or \"\"):\n continue\n\n display = f\"{room_name} ({room_id})\" if room_name else room_id\n print(f\" {display} ... \", end=\"\", flush=True)\n\n messages = fetch_room_messages(homeserver, token, room_id, since_ts)\n if messages_only:\n messages = [e for e in messages if e.get(\"type\") == \"m.room.message\"]\n\n if not messages:\n print(\"0 messages, skipped\")\n continue\n\n name_part = sanitize_filename(room_name) if room_name else \"\"\n id_part = sanitize_filename(room_id)\n filename = f\"{name_part}_{id_part}.jsonl\" if name_part else f\"{id_part}.jsonl\"\n\n with open(out_dir / filename, \"w\", encoding=\"utf-8\") as f:\n for event in messages:\n record = format_event(event, redact=redact)\n f.write(json.dumps(record, ensure_ascii=False) + \"\\n\")\n\n print(f\"{len(messages)} messages -> {filename}\")\n total_messages += len(messages)\n total_rooms += 1\n\n return total_rooms, total_messages\n\n\n# ---------------------------------------------------------------------------\n# Agent sessions export (OpenClaw + CoPaw + Hermes)\n# ---------------------------------------------------------------------------\n\ndef detect_runtime(container: str) -> tuple[str, str]:\n std = \".openclaw/agents/main/sessions\"\n if docker_exec(container, f\"test -d {std} && echo yes || echo no\").strip() == \"yes\":\n return \"openclaw\", std\n\n hermes_dir = \".hermes/sessions\"\n if docker_exec(container, f\"test -d {hermes_dir} && echo yes || echo no\").strip() == \"yes\":\n return \"hermes\", hermes_dir\n\n worker_name = docker_exec(container, \"echo $HICLAW_WORKER_NAME\").strip()\n if worker_name:\n copaw_dir = f\"{worker_name}/.copaw/sessions\"\n if docker_exec(container, f\"test -d '{copaw_dir}' && echo yes || echo no\").strip() == \"yes\":\n return \"copaw\", copaw_dir\n\n found = docker_exec(container, \"find . -maxdepth 3 \\\\( -path '*/.hermes/sessions' -o -path '*/.copaw/sessions' \\\\) -type d 2>/dev/null | head -1\").strip()\n if found:\n runtime = \"hermes\" if \"/.hermes/sessions\" in found else \"copaw\"\n return runtime, found.lstrip(\"./\")\n\n return \"\", \"\"\n\n\ndef export_openclaw_sessions(container: str, sessions_dir: str, since_epoch: float,\n out_dir: Path, redact: bool) -> tuple[int, int]:\n ls_output = docker_exec(container, f\"ls '{sessions_dir}'/*.jsonl 2>/dev/null\").strip()\n if not ls_output:\n return 0, 0\n\n total_sessions = 0\n total_events = 0\n\n for session_path in [f.strip() for f in ls_output.splitlines() if f.strip()]:\n filename = os.path.basename(session_path)\n\n first_line = docker_exec(container, f\"head -1 '{session_path}'\").strip()\n if not first_line:\n continue\n try:\n header = json.loads(first_line)\n except json.JSONDecodeError:\n continue\n\n last_line = docker_exec(container, f\"tail -1 '{session_path}'\").strip()\n try:\n last_ts = parse_ts(json.loads(last_line).get(\"timestamp\", \"\"))\n except Exception:\n last_ts = parse_ts(header.get(\"timestamp\", \"\"))\n\n if last_ts < since_epoch and last_ts > 0:\n continue\n\n raw = docker_exec(container, f\"cat '{session_path}'\")\n if not raw.strip():\n continue\n\n output_lines = []\n for line in raw.splitlines():\n line = line.strip()\n if not line:\n continue\n try:\n event = json.loads(line)\n except json.JSONDecodeError:\n continue\n event_ts = parse_ts(event.get(\"timestamp\", \"\"))\n if event.get(\"type\") != \"session\" and event_ts < since_epoch and event_ts > 0:\n continue\n if redact:\n event = redact_json_strings(event)\n output_lines.append(json.dumps(event, ensure_ascii=False))\n\n if len(output_lines) <= 1:\n continue\n\n with open(out_dir / filename, \"w\", encoding=\"utf-8\") as f:\n f.write(\"\\n\".join(output_lines) + \"\\n\")\n\n event_count = len(output_lines) - 1\n print(f\" {container}/{filename} (openclaw): {event_count} events\")\n total_sessions += 1\n total_events += event_count\n\n # sessions.json index\n try:\n raw = docker_exec(container, f\"cat '{sessions_dir}/sessions.json'\")\n if raw.strip():\n index = json.loads(raw)\n if redact:\n index = redact_json_strings(index)\n with open(out_dir / \"sessions.json\", \"w\", encoding=\"utf-8\") as f:\n json.dump(index, f, ensure_ascii=False, indent=2)\n except Exception:\n pass\n\n return total_sessions, total_events\n\n\ndef export_copaw_sessions(container: str, sessions_dir: str, since_epoch: float,\n out_dir: Path, redact: bool) -> tuple[int, int]:\n ls_output = docker_exec(container, f\"find '{sessions_dir}' -name '*.json' -type f 2>/dev/null\").strip()\n if not ls_output:\n return 0, 0\n\n total_sessions = 0\n total_events = 0\n\n for session_path in [f.strip() for f in ls_output.splitlines() if f.strip()]:\n raw = docker_exec(container, f\"cat '{session_path}'\")\n if not raw.strip():\n continue\n try:\n data = json.loads(raw)\n except json.JSONDecodeError:\n continue\n\n agent = data.get(\"agent\", {})\n memory = agent.get(\"memory\", {})\n content = memory.get(\"content\", [])\n if not content:\n continue\n\n basename = os.path.basename(session_path).replace(\".json\", \"\")\n header = {\n \"type\": \"session\",\n \"runtime\": \"copaw\",\n \"agent_name\": agent.get(\"name\", \"\"),\n \"session_key\": basename,\n \"compressed_summary\": memory.get(\"_compressed_summary\", \"\"),\n }\n\n messages_in_range = []\n for turn_idx, turn in enumerate(content):\n if not isinstance(turn, list):\n continue\n for msg in turn:\n if not isinstance(msg, dict):\n continue\n msg_ts = parse_ts(msg.get(\"timestamp\", \"\"))\n if msg_ts >= since_epoch or msg_ts == 0:\n event = {\n \"type\": \"message\",\n \"turn\": turn_idx,\n \"id\": msg.get(\"id\", \"\"),\n \"role\": msg.get(\"role\", \"\"),\n \"name\": msg.get(\"name\", \"\"),\n \"timestamp\": msg.get(\"timestamp\", \"\"),\n \"content\": msg.get(\"content\", []),\n }\n if msg.get(\"metadata\"):\n event[\"metadata\"] = msg[\"metadata\"]\n messages_in_range.append(event)\n\n if not messages_in_range:\n continue\n\n output_lines = []\n if redact:\n header = redact_json_strings(header)\n output_lines.append(json.dumps(header, ensure_ascii=False))\n for event in messages_in_range:\n if redact:\n event = redact_json_strings(event)\n output_lines.append(json.dumps(event, ensure_ascii=False))\n\n out_filename = basename + \".jsonl\"\n with open(out_dir / out_filename, \"w\", encoding=\"utf-8\") as f:\n f.write(\"\\n\".join(output_lines) + \"\\n\")\n\n event_count = len(output_lines) - 1\n print(f\" {container}/{out_filename} (copaw): {event_count} events\")\n total_sessions += 1\n total_events += event_count\n\n return total_sessions, total_events\n\n\ndef export_hermes_sessions(container: str, sessions_dir: str, since_epoch: float,\n out_dir: Path, redact: bool) -> tuple[int, int]:\n ls_output = docker_exec(container, f\"ls '{sessions_dir}'/*.jsonl 2>/dev/null\").strip()\n if not ls_output:\n return 0, 0\n\n total_sessions = 0\n total_events = 0\n\n for session_path in [f.strip() for f in ls_output.splitlines() if f.strip()]:\n raw = docker_exec(container, f\"cat '{session_path}'\")\n if not raw.strip():\n continue\n\n output_lines = []\n last_ts = 0.0\n for line in raw.splitlines():\n line = line.strip()\n if not line:\n continue\n try:\n event = json.loads(line)\n except json.JSONDecodeError:\n continue\n\n event_ts = parse_ts(event.get(\"timestamp\", \"\"))\n if event_ts:\n last_ts = max(last_ts, event_ts)\n\n role = event.get(\"role\", \"\")\n if role != \"session_meta\" and event_ts < since_epoch and event_ts > 0:\n continue\n\n if redact:\n event = redact_json_strings(event)\n output_lines.append(json.dumps(event, ensure_ascii=False))\n\n if not output_lines:\n continue\n\n if last_ts < since_epoch and last_ts > 0:\n continue\n\n filename = os.path.basename(session_path)\n with open(out_dir / filename, \"w\", encoding=\"utf-8\") as f:\n f.write(\"\\n\".join(output_lines) + \"\\n\")\n\n event_count = len(output_lines) - 1 if output_lines and '\"role\": \"session_meta\"' in output_lines[0] else len(output_lines)\n print(f\" {container}/{filename} (hermes): {event_count} events\")\n total_sessions += 1\n total_events += max(event_count, 0)\n\n hermes_home = str(Path(sessions_dir).parent)\n state_db = f\"{hermes_home}/state.db\"\n has_state_db = docker_exec(container, f\"test -f '{state_db}' && echo yes || echo no\").strip() == \"yes\"\n if has_state_db:\n raw = docker_exec(container, f\"python3 - <<'PY'\\nimport json, sqlite3\\nconn = sqlite3.connect('{state_db}')\\nconn.row_factory = sqlite3.Row\\nrows = conn.execute('SELECT * FROM sessions ORDER BY started_at DESC LIMIT 200').fetchall()\\nprint(json.dumps([dict(r) for r in rows], ensure_ascii=False, indent=2))\\nPY\")\n if raw.strip():\n try:\n data = json.loads(raw)\n if redact:\n data = redact_json_strings(data)\n with open(out_dir / \"sessions-db.json\", \"w\", encoding=\"utf-8\") as f:\n json.dump(data, f, ensure_ascii=False, indent=2)\n except json.JSONDecodeError:\n pass\n\n logs_dir = f\"{hermes_home}/logs\"\n has_logs_dir = docker_exec(container, f\"test -d '{logs_dir}' && echo yes || echo no\").strip() == \"yes\"\n if has_logs_dir:\n for log_name in (\"agent.log\", \"errors.log\", \"gateway.log\"):\n raw = docker_exec(container, f\"test -f '{logs_dir}/{log_name}' && cat '{logs_dir}/{log_name}' || true\")\n if raw.strip():\n content = redact_pii(raw) if redact else raw\n (out_dir / log_name).write_text(content, encoding=\"utf-8\")\n\n return total_sessions, total_events\n\n\ndef export_agent_sessions(out_dir: Path, since_epoch: float, redact: bool,\n container_filter: str | None) -> tuple[int, int]:\n \"\"\"Export agent sessions from all containers. Returns (session_count, event_count).\"\"\"\n containers = list_hiclaw_containers()\n if container_filter:\n containers = [c for c in containers if container_filter in c]\n\n if not containers:\n print(\" [sessions] No matching hiclaw containers found\")\n return 0, 0\n\n total_sessions = 0\n total_events = 0\n\n for container in containers:\n runtime, sessions_dir = detect_runtime(container)\n if not runtime:\n print(f\" {container}: no sessions directory, skipped\")\n continue\n\n container_dir = out_dir / container\n container_dir.mkdir(parents=True, exist_ok=True)\n\n if runtime == \"openclaw\":\n s, e = export_openclaw_sessions(container, sessions_dir, since_epoch,\n container_dir, redact)\n elif runtime == \"hermes\":\n s, e = export_hermes_sessions(container, sessions_dir, since_epoch,\n container_dir, redact)\n else:\n s, e = export_copaw_sessions(container, sessions_dir, since_epoch,\n container_dir, redact)\n\n if s == 0:\n if not any(container_dir.iterdir()):\n container_dir.rmdir()\n print(f\" {container} ({runtime}): no sessions in range, skipped\")\n else:\n print(f\" {container} ({runtime}): exported auxiliary debug artifacts without session events\")\n else:\n total_sessions += s\n total_events += e\n\n return total_sessions, total_events\n\n\n# ---------------------------------------------------------------------------\n# Main\n# ---------------------------------------------------------------------------\n\ndef main():\n parser = argparse.ArgumentParser(\n description=\"Export HiClaw debug logs (Matrix messages + agent sessions)\"\n )\n parser.add_argument(\"--range\", \"-r\", required=True, dest=\"time_range\",\n help=\"Time range to export, e.g. 10m, 1h, 1d\")\n parser.add_argument(\"--container\", \"-c\", default=None,\n help=\"Only export sessions from containers matching this substring\")\n parser.add_argument(\"--room\", default=None,\n help=\"Only export Matrix rooms matching this substring\")\n parser.add_argument(\"--homeserver\", \"-s\", default=\"\",\n help=\"Matrix homeserver URL (auto-detected from env file)\")\n parser.add_argument(\"--token\", \"-t\", default=\"\",\n help=\"Matrix access token (auto-detected from env file)\")\n parser.add_argument(\"--env-file\", default=None,\n help=\"Path to hiclaw-manager.env (default: ~/hiclaw-manager.env)\")\n parser.add_argument(\"--messages-only\", action=\"store_true\",\n help=\"Only export m.room.message events (skip state events)\")\n parser.add_argument(\"--no-redact\", action=\"store_true\",\n help=\"Disable PII redaction\")\n args = parser.parse_args()\n\n range_seconds = parse_range(args.time_range)\n since_epoch = time.time() - range_seconds\n since_human = datetime.fromtimestamp(since_epoch, tz=timezone.utc).isoformat()\n now_str = datetime.now().strftime(\"%Y%m%d-%H%M%S\")\n\n run_dir = Path(\"debug-log\") / now_str\n run_dir.mkdir(parents=True, exist_ok=True)\n redact = not args.no_redact\n\n print(f\"HiClaw Debug Log Export\")\n print(f\" Range: last {args.time_range} (since {since_human})\")\n print(f\" Output: {run_dir.resolve()}\")\n print(f\" PII redaction: {'on' if redact else 'off'}\")\n print()\n\n # --- Matrix messages ---\n print(\"=== Matrix Messages ===\")\n matrix_dir = run_dir / \"matrix-messages\"\n rooms, messages = export_matrix_messages(\n matrix_dir, since_epoch, redact,\n room_filter=args.room, env_file=args.env_file,\n homeserver=args.homeserver, token=args.token,\n messages_only=args.messages_only,\n )\n if rooms == 0 and matrix_dir.exists() and not any(matrix_dir.iterdir()):\n matrix_dir.rmdir()\n print()\n\n # --- Agent sessions ---\n print(\"=== Agent Sessions ===\")\n sessions_dir = run_dir / \"agent-sessions\"\n sessions, events = export_agent_sessions(\n sessions_dir, since_epoch, redact,\n container_filter=args.container,\n )\n if sessions == 0 and sessions_dir.exists() and not any(sessions_dir.iterdir()):\n sessions_dir.rmdir()\n print()\n\n # --- Summary ---\n summary = (\n f\"HiClaw Debug Log\\n\"\n f\"Exported at: {now_str}\\n\"\n f\"Range: last {args.time_range} (since {since_human})\\n\"\n f\"PII redaction: {'on' if redact else 'off'}\\n\"\n f\"\\n\"\n f\"Matrix messages: {messages} messages from {rooms} rooms\\n\"\n f\"Agent sessions: {events} events from {sessions} sessions\\n\"\n )\n (run_dir / \"summary.txt\").write_text(summary)\n\n print(f\"Done. {messages} messages from {rooms} rooms, {events} events from {sessions} sessions\")\n print(f\"Output: {run_dir.resolve()}\")\n\n\nif __name__ == \"__main__\":\n main()\n"}, "8f953f1c642792035377b9bc49830c33f4d178c4": {"t": 1785764744.9695537, "n": 23142, "h": "261ea826c1cbb7ae", "c": "#!/usr/bin/env python3\nfrom __future__ import annotations\n\"\"\"\nExport HiClaw debug logs: Matrix messages + agent session logs.\n\nUsage:\n # Export last 1 hour (default)\n python scripts/export-debug-log.py --range 1h\n\n # Export last 1 day\n python scripts/export-debug-log.py --range 1d\n\n # Filter by container or room\n python scripts/export-debug-log.py --range 1h --container hiclaw-manager --room Worker\n\n # Disable PII redaction\n python scripts/export-debug-log.py --range 1h --no-redact\n\nOutput structure:\n debug-log/20260319-153000/\n ├── summary.txt\n ├── matrix-messages/\n │ └── RoomName_!roomid.jsonl\n └── agent-sessions/\n ├── hiclaw-manager/\n │ └── {session-id}.jsonl\n └── hiclaw-worker-xxx/\n └── {session-key}.jsonl\n\"\"\"\n\nimport argparse\nimport json\nimport os\nimport re\nimport subprocess\nimport sys\nimport time\nimport urllib.parse\nimport urllib.request\nimport urllib.error\nfrom datetime import datetime, timezone\nfrom pathlib import Path\n\n\n# ---------------------------------------------------------------------------\n# PII redaction\n# ---------------------------------------------------------------------------\n\n_PII_PATTERNS: list[tuple[str, re.Pattern]] = [\n (\"ID_CARD\", re.compile(r'\\b[1-9]\\d{5}(?:19|20)\\d{2}(?:0[1-9]|1[0-2])(?:0[1-9]|[12]\\d|3[01])\\d{3}[\\dXx]\\b')),\n (\"PHONE\", re.compile(r'(?<!\\d)(?:\\+?86[-\\s]?)?1[3-9]\\d{9}(?!\\d)')),\n (\"EMAIL\", re.compile(r'\\b[A-Za-z0-9._%+\\-]+@[A-Za-z0-9.\\-]+\\.[A-Za-z]{2,}\\b')),\n (\"BANK_CARD\", re.compile(r'\\b(?:6[0-9]{15,18}|4[0-9]{15}|5[1-5][0-9]{14}|3[47][0-9]{13}|62[0-9]{14,17})\\b')),\n (\"IP\", re.compile(r'\\b(?:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)\\.){3}(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)\\b')),\n (\"ALIYUN_AK\", re.compile(r'\\bLTAI[A-Za-z0-9]{12,30}\\b')),\n (\"ALIYUN_SK\", re.compile(r'(?i)(?:access_?key_?secret|secret_?access_?key)\\s*[:=]\\s*(\\S{20,})')),\n (\"AWS_AK\", re.compile(r'\\b(?:AKIA|ASIA)[A-Z0-9]{16}\\b')),\n (\"OPENAI_KEY\", re.compile(r'\\bsk-[A-Za-z0-9]{20,}\\b')),\n (\"ANTHROPIC_KEY\", re.compile(r'\\bsk-ant-[A-Za-z0-9\\-]{20,}\\b')),\n (\"DASHSCOPE_KEY\", re.compile(r'\\bsk-sp-[A-Za-z0-9]{20,}\\b')),\n (\"DEEPSEEK_KEY\", re.compile(r'\\bsk-[a-f0-9]{32,}\\b')),\n (\"BEARER\", re.compile(r'(?i)(Bearer\\s+)([A-Za-z0-9\\-_.]{20,})')),\n (\"SECRET_KV\", re.compile(\n r'(?i)((?:password|passwd|pwd|secret|token|api_?key|access_?key|secret_?key'\n r'|private_?key|credential|appkey|app_?secret|auth_?token|signing_?key'\n r'|client_?secret|master_?key)\\s*[:=]\\s*)'\n r'(\\S+)'\n )),\n (\"MATRIX_TOKEN\", re.compile(r'\\bsyt_[A-Za-z0-9_\\-]{10,}\\b')),\n (\"HEX_SECRET\", re.compile(r'\\b[A-Fa-f0-9]{32,}\\b')),\n (\"PASSPORT\", re.compile(r'\\b[EeGg]\\d{8}\\b')),\n (\"SSN\", re.compile(r'\\b\\d{3}-\\d{2}-\\d{4}\\b')),\n]\n\n\ndef redact_pii(text: str) -> str:\n if not text:\n return text\n for name, pattern in _PII_PATTERNS:\n if name in (\"SECRET_KV\", \"ALIYUN_SK\", \"BEARER\"):\n text = pattern.sub(r'\\1****', text)\n else:\n text = pattern.sub('****', text)\n return text\n\n\ndef redact_json_strings(obj):\n if isinstance(obj, str):\n return redact_pii(obj)\n if isinstance(obj, list):\n return [redact_json_strings(v) for v in obj]\n if isinstance(obj, dict):\n return {k: redact_json_strings(v) for k, v in obj.items()}\n return obj\n\n\n# ---------------------------------------------------------------------------\n# Common helpers\n# ---------------------------------------------------------------------------\n\ndef parse_range(range_str: str) -> int:\n m = re.fullmatch(r\"(\\d+)\\s*(m|min|h|hr|hour|d|day)s?\", range_str.strip(), re.IGNORECASE)\n if not m:\n raise ValueError(f\"Invalid range format: '{range_str}'. Use e.g. 10m, 1h, 1d\")\n value, unit = int(m.group(1)), m.group(2)[0].lower()\n return value * {\"m\": 60, \"h\": 3600, \"d\": 86400}[unit]\n\n\ndef parse_ts(ts_str: str) -> float:\n if not ts_str:\n return 0\n ts_str = ts_str.replace(\"Z\", \"+00:00\")\n try:\n dt = datetime.fromisoformat(ts_str)\n if dt.tzinfo is None:\n dt = dt.replace(tzinfo=timezone.utc)\n return dt.timestamp()\n except Exception:\n return 0\n\n\ndef sanitize_filename(name: str) -> str:\n return re.sub(r'[^\\w\\-. ]', '_', name).strip()[:80]\n\n\ndef docker_exec(container: str, cmd: str) -> str:\n result = subprocess.run(\n [\"docker\", \"exec\", container, \"sh\", \"-c\", cmd],\n capture_output=True, text=True, timeout=30,\n )\n return result.stdout\n\n\ndef list_hiclaw_containers() -> list[str]:\n result = subprocess.run(\n [\"docker\", \"ps\", \"--format\", \"{{.Names}}\", \"--filter\", \"name=hiclaw-\"],\n capture_output=True, text=True, timeout=10,\n )\n return [n.strip() for n in result.stdout.splitlines() if n.strip()]\n\n\n# ---------------------------------------------------------------------------\n# Matrix messages export\n# ---------------------------------------------------------------------------\n\ndef load_env_file(path: str) -> dict[str, str]:\n env = {}\n try:\n with open(path) as f:\n for line in f:\n line = line.strip()\n if not line or line.startswith(\"#\"):\n continue\n if \"=\" in line:\n key, _, value = line.partition(\"=\")\n env[key.strip()] = value.strip()\n except FileNotFoundError:\n pass\n return env\n\n\ndef matrix_login(homeserver: str, user: str, password: str) -> str:\n url = f\"{homeserver.rstrip('/')}/_matrix/client/v3/login\"\n payload = json.dumps({\n \"type\": \"m.login.password\",\n \"identifier\": {\"type\": \"m.id.user\", \"user\": user},\n \"password\": password,\n }).encode()\n req = urllib.request.Request(url, data=payload, headers={\"Content-Type\": \"application/json\"})\n with urllib.request.urlopen(req, timeout=15) as resp:\n data = json.loads(resp.read())\n return data[\"access_token\"]\n\n\ndef matrix_api(homeserver: str, token: str, endpoint: str, params: dict | None = None) -> dict:\n url = f\"{homeserver.rstrip('/')}/_matrix/client/v3/{endpoint}\"\n if params:\n url += \"?\" + urllib.parse.urlencode(params)\n req = urllib.request.Request(url, headers={\"Authorization\": f\"Bearer {token}\"})\n try:\n with urllib.request.urlopen(req, timeout=30) as resp:\n return json.loads(resp.read())\n except urllib.error.HTTPError as e:\n body = e.read().decode(errors=\"replace\")\n print(f\" API error {e.code} on {endpoint}: {body}\", file=sys.stderr)\n raise\n\n\ndef fetch_room_messages(homeserver: str, token: str, room_id: str, since_ts: int) -> list[dict]:\n encoded = urllib.parse.quote(room_id)\n messages = []\n from_token = \"\"\n while True:\n params = {\"dir\": \"b\", \"limit\": \"100\"}\n if from_token:\n params[\"from\"] = from_token\n data = matrix_api(homeserver, token, f\"rooms/{encoded}/messages\", params)\n chunk = data.get(\"chunk\", [])\n if not chunk:\n break\n hit_boundary = False\n for event in chunk:\n if event.get(\"origin_server_ts\", 0) < since_ts:\n hit_boundary = True\n break\n messages.append(event)\n if hit_boundary:\n break\n next_token = data.get(\"end\")\n if not next_token or next_token == from_token:\n break\n from_token = next_token\n messages.reverse()\n return messages\n\n\ndef format_event(event: dict, redact: bool) -> dict:\n content = event.get(\"content\", {})\n ts = event.get(\"origin_server_ts\", 0)\n record = {\n \"event_id\": event.get(\"event_id\"),\n \"type\": event.get(\"type\"),\n \"sender\": event.get(\"sender\"),\n \"timestamp\": ts,\n \"time\": datetime.fromtimestamp(ts / 1000, tz=timezone.utc).isoformat(),\n }\n if event.get(\"type\") == \"m.room.message\":\n record[\"msgtype\"] = content.get(\"msgtype\")\n body = content.get(\"body\")\n record[\"body\"] = redact_pii(body) if redact else body\n if content.get(\"format\"):\n record[\"format\"] = content[\"format\"]\n if content.get(\"url\"):\n record[\"url\"] = content[\"url\"]\n if content.get(\"m.relates_to\"):\n record[\"relates_to\"] = content[\"m.relates_to\"]\n else:\n record[\"content\"] = content\n return record\n\n\ndef export_matrix_messages(out_dir: Path, since_epoch: float, redact: bool,\n room_filter: str | None, env_file: str | None,\n homeserver: str, token: str,\n messages_only: bool) -> tuple[int, int]:\n \"\"\"Export Matrix messages. Returns (rooms_exported, message_count).\"\"\"\n env_path = env_file or os.path.expanduser(\"~/hiclaw-manager.env\")\n hiclaw_env = load_env_file(env_path)\n\n if not homeserver:\n if not hiclaw_env:\n print(f\" [matrix] Cannot find {env_path}, skipping Matrix export\")\n return 0, 0\n port = hiclaw_env.get(\"HICLAW_PORT_GATEWAY\", \"18080\")\n homeserver = f\"http://127.0.0.1:{port}\"\n\n if not token:\n # Use Manager token — Manager is in every room (DM, Worker, Project)\n manager_password = hiclaw_env.get(\"HICLAW_MANAGER_PASSWORD\", \"\")\n if manager_password:\n try:\n token = matrix_login(homeserver, \"manager\", manager_password)\n except Exception:\n pass\n\n # Fallback to admin token if Manager login failed\n if not token:\n admin_user = hiclaw_env.get(\"HICLAW_ADMIN_USER\", \"admin\")\n admin_password = hiclaw_env.get(\"HICLAW_ADMIN_PASSWORD\", \"\")\n if not admin_password:\n print(f\" [matrix] No usable credentials found, skipping Matrix export\")\n return 0, 0\n try:\n token = matrix_login(homeserver, admin_user, admin_password)\n except Exception as e:\n print(f\" [matrix] Login failed: {e}, skipping Matrix export\")\n return 0, 0\n\n since_ts = int(since_epoch * 1000)\n out_dir.mkdir(parents=True, exist_ok=True)\n\n try:\n rooms = matrix_api(homeserver, token, \"joined_rooms\").get(\"joined_rooms\", [])\n except Exception as e:\n print(f\" [matrix] Failed to list rooms: {e}\")\n return 0, 0\n\n total_messages = 0\n total_rooms = 0\n\n for room_id in rooms:\n encoded = urllib.parse.quote(room_id)\n try:\n room_name = matrix_api(homeserver, token, f\"rooms/{encoded}/state/m.room.name\").get(\"name\", \"\")\n except Exception:\n room_name = \"\"\n\n if room_filter:\n if room_filter not in room_id and room_filter not in (room_name or \"\"):\n continue\n\n display = f\"{room_name} ({room_id})\" if room_name else room_id\n print(f\" {display} ... \", end=\"\", flush=True)\n\n messages = fetch_room_messages(homeserver, token, room_id, since_ts)\n if messages_only:\n messages = [e for e in messages if e.get(\"type\") == \"m.room.message\"]\n\n if not messages:\n print(\"0 messages, skipped\")\n continue\n\n name_part = sanitize_filename(room_name) if room_name else \"\"\n id_part = sanitize_filename(room_id)\n filename = f\"{name_part}_{id_part}.jsonl\" if name_part else f\"{id_part}.jsonl\"\n\n with open(out_dir / filename, \"w\", encoding=\"utf-8\") as f:\n for event in messages:\n record = format_event(event, redact=redact)\n f.write(json.dumps(record, ensure_ascii=False) + \"\\n\")\n\n print(f\"{len(messages)} messages -> {filename}\")\n total_messages += len(messages)\n total_rooms += 1\n\n return total_rooms, total_messages\n\n\n# ---------------------------------------------------------------------------\n# Agent sessions export (OpenClaw + CoPaw)\n# ---------------------------------------------------------------------------\n\ndef detect_runtime(container: str) -> tuple[str, str]:\n std = \".openclaw/agents/main/sessions\"\n if docker_exec(container, f\"test -d {std} && echo yes || echo no\").strip() == \"yes\":\n return \"openclaw\", std\n\n worker_name = docker_exec(container, \"echo $HICLAW_WORKER_NAME\").strip()\n if worker_name:\n copaw_dir = f\"{worker_name}/.copaw/sessions\"\n if docker_exec(container, f\"test -d '{copaw_dir}' && echo yes || echo no\").strip() == \"yes\":\n return \"copaw\", copaw_dir\n\n found = docker_exec(container, \"find . -maxdepth 3 -path '*/.copaw/sessions' -type d 2>/dev/null | head -1\").strip()\n if found:\n return \"copaw\", found.lstrip(\"./\")\n\n return \"\", \"\"\n\n\ndef export_openclaw_sessions(container: str, sessions_dir: str, since_epoch: float,\n out_dir: Path, redact: bool) -> tuple[int, int]:\n ls_output = docker_exec(container, f\"ls '{sessions_dir}'/*.jsonl 2>/dev/null\").strip()\n if not ls_output:\n return 0, 0\n\n total_sessions = 0\n total_events = 0\n\n for session_path in [f.strip() for f in ls_output.splitlines() if f.strip()]:\n filename = os.path.basename(session_path)\n\n first_line = docker_exec(container, f\"head -1 '{session_path}'\").strip()\n if not first_line:\n continue\n try:\n header = json.loads(first_line)\n except json.JSONDecodeError:\n continue\n\n last_line = docker_exec(container, f\"tail -1 '{session_path}'\").strip()\n try:\n last_ts = parse_ts(json.loads(last_line).get(\"timestamp\", \"\"))\n except Exception:\n last_ts = parse_ts(header.get(\"timestamp\", \"\"))\n\n if last_ts < since_epoch and last_ts > 0:\n continue\n\n raw = docker_exec(container, f\"cat '{session_path}'\")\n if not raw.strip():\n continue\n\n output_lines = []\n for line in raw.splitlines():\n line = line.strip()\n if not line:\n continue\n try:\n event = json.loads(line)\n except json.JSONDecodeError:\n continue\n event_ts = parse_ts(event.get(\"timestamp\", \"\"))\n if event.get(\"type\") != \"session\" and event_ts < since_epoch and event_ts > 0:\n continue\n if redact:\n event = redact_json_strings(event)\n output_lines.append(json.dumps(event, ensure_ascii=False))\n\n if len(output_lines) <= 1:\n continue\n\n with open(out_dir / filename, \"w\", encoding=\"utf-8\") as f:\n f.write(\"\\n\".join(output_lines) + \"\\n\")\n\n event_count = len(output_lines) - 1\n print(f\" {container}/{filename} (openclaw): {event_count} events\")\n total_sessions += 1\n total_events += event_count\n\n # sessions.json index\n try:\n raw = docker_exec(container, f\"cat '{sessions_dir}/sessions.json'\")\n if raw.strip():\n index = json.loads(raw)\n if redact:\n index = redact_json_strings(index)\n with open(out_dir / \"sessions.json\", \"w\", encoding=\"utf-8\") as f:\n json.dump(index, f, ensure_ascii=False, indent=2)\n except Exception:\n pass\n\n return total_sessions, total_events\n\n\ndef export_copaw_sessions(container: str, sessions_dir: str, since_epoch: float,\n out_dir: Path, redact: bool) -> tuple[int, int]:\n ls_output = docker_exec(container, f\"find '{sessions_dir}' -name '*.json' -type f 2>/dev/null\").strip()\n if not ls_output:\n return 0, 0\n\n total_sessions = 0\n total_events = 0\n\n for session_path in [f.strip() for f in ls_output.splitlines() if f.strip()]:\n raw = docker_exec(container, f\"cat '{session_path}'\")\n if not raw.strip():\n continue\n try:\n data = json.loads(raw)\n except json.JSONDecodeError:\n continue\n\n agent = data.get(\"agent\", {})\n memory = agent.get(\"memory\", {})\n content = memory.get(\"content\", [])\n if not content:\n continue\n\n basename = os.path.basename(session_path).replace(\".json\", \"\")\n header = {\n \"type\": \"session\",\n \"runtime\": \"copaw\",\n \"agent_name\": agent.get(\"name\", \"\"),\n \"session_key\": basename,\n \"compressed_summary\": memory.get(\"_compressed_summary\", \"\"),\n }\n\n messages_in_range = []\n for turn_idx, turn in enumerate(content):\n if not isinstance(turn, list):\n continue\n for msg in turn:\n if not isinstance(msg, dict):\n continue\n msg_ts = parse_ts(msg.get(\"timestamp\", \"\"))\n if msg_ts >= since_epoch or msg_ts == 0:\n event = {\n \"type\": \"message\",\n \"turn\": turn_idx,\n \"id\": msg.get(\"id\", \"\"),\n \"role\": msg.get(\"role\", \"\"),\n \"name\": msg.get(\"name\", \"\"),\n \"timestamp\": msg.get(\"timestamp\", \"\"),\n \"content\": msg.get(\"content\", []),\n }\n if msg.get(\"metadata\"):\n event[\"metadata\"] = msg[\"metadata\"]\n messages_in_range.append(event)\n\n if not messages_in_range:\n continue\n\n output_lines = []\n if redact:\n header = redact_json_strings(header)\n output_lines.append(json.dumps(header, ensure_ascii=False))\n for event in messages_in_range:\n if redact:\n event = redact_json_strings(event)\n output_lines.append(json.dumps(event, ensure_ascii=False))\n\n out_filename = basename + \".jsonl\"\n with open(out_dir / out_filename, \"w\", encoding=\"utf-8\") as f:\n f.write(\"\\n\".join(output_lines) + \"\\n\")\n\n event_count = len(output_lines) - 1\n print(f\" {container}/{out_filename} (copaw): {event_count} events\")\n total_sessions += 1\n total_events += event_count\n\n return total_sessions, total_events\n\n\ndef export_agent_sessions(out_dir: Path, since_epoch: float, redact: bool,\n container_filter: str | None) -> tuple[int, int]:\n \"\"\"Export agent sessions from all containers. Returns (session_count, event_count).\"\"\"\n containers = list_hiclaw_containers()\n if container_filter:\n containers = [c for c in containers if container_filter in c]\n\n if not containers:\n print(\" [sessions] No matching hiclaw containers found\")\n return 0, 0\n\n total_sessions = 0\n total_events = 0\n\n for container in containers:\n runtime, sessions_dir = detect_runtime(container)\n if not runtime:\n print(f\" {container}: no sessions directory, skipped\")\n continue\n\n container_dir = out_dir / container\n container_dir.mkdir(parents=True, exist_ok=True)\n\n if runtime == \"openclaw\":\n s, e = export_openclaw_sessions(container, sessions_dir, since_epoch,\n container_dir, redact)\n else:\n s, e = export_copaw_sessions(container, sessions_dir, since_epoch,\n container_dir, redact)\n\n if s == 0:\n container_dir.rmdir()\n print(f\" {container} ({runtime}): no sessions in range, skipped\")\n else:\n total_sessions += s\n total_events += e\n\n return total_sessions, total_events\n\n\n# ---------------------------------------------------------------------------\n# Main\n# ---------------------------------------------------------------------------\n\ndef main():\n parser = argparse.ArgumentParser(\n description=\"Export HiClaw debug logs (Matrix messages + agent sessions)\"\n )\n parser.add_argument(\"--range\", \"-r\", required=True, dest=\"time_range\",\n help=\"Time range to export, e.g. 10m, 1h, 1d\")\n parser.add_argument(\"--container\", \"-c\", default=None,\n help=\"Only export sessions from containers matching this substring\")\n parser.add_argument(\"--room\", default=None,\n help=\"Only export Matrix rooms matching this substring\")\n parser.add_argument(\"--homeserver\", \"-s\", default=\"\",\n help=\"Matrix homeserver URL (auto-detected from env file)\")\n parser.add_argument(\"--token\", \"-t\", default=\"\",\n help=\"Matrix access token (auto-detected from env file)\")\n parser.add_argument(\"--env-file\", default=None,\n help=\"Path to hiclaw-manager.env (default: ~/hiclaw-manager.env)\")\n parser.add_argument(\"--messages-only\", action=\"store_true\",\n help=\"Only export m.room.message events (skip state events)\")\n parser.add_argument(\"--no-redact\", action=\"store_true\",\n help=\"Disable PII redaction\")\n args = parser.parse_args()\n\n range_seconds = parse_range(args.time_range)\n since_epoch = time.time() - range_seconds\n since_human = datetime.fromtimestamp(since_epoch, tz=timezone.utc).isoformat()\n now_str = datetime.now().strftime(\"%Y%m%d-%H%M%S\")\n\n run_dir = Path(\"debug-log\") / now_str\n run_dir.mkdir(parents=True, exist_ok=True)\n redact = not args.no_redact\n\n print(f\"HiClaw Debug Log Export\")\n print(f\" Range: last {args.time_range} (since {since_human})\")\n print(f\" Output: {run_dir.resolve()}\")\n print(f\" PII redaction: {'on' if redact else 'off'}\")\n print()\n\n # --- Matrix messages ---\n print(\"=== Matrix Messages ===\")\n matrix_dir = run_dir / \"matrix-messages\"\n rooms, messages = export_matrix_messages(\n matrix_dir, since_epoch, redact,\n room_filter=args.room, env_file=args.env_file,\n homeserver=args.homeserver, token=args.token,\n messages_only=args.messages_only,\n )\n if rooms == 0 and matrix_dir.exists() and not any(matrix_dir.iterdir()):\n matrix_dir.rmdir()\n print()\n\n # --- Agent sessions ---\n print(\"=== Agent Sessions ===\")\n sessions_dir = run_dir / \"agent-sessions\"\n sessions, events = export_agent_sessions(\n sessions_dir, since_epoch, redact,\n container_filter=args.container,\n )\n if sessions == 0 and sessions_dir.exists() and not any(sessions_dir.iterdir()):\n sessions_dir.rmdir()\n print()\n\n # --- Summary ---\n summary = (\n f\"HiClaw Debug Log\\n\"\n f\"Exported at: {now_str}\\n\"\n f\"Range: last {args.time_range} (since {since_human})\\n\"\n f\"PII redaction: {'on' if redact else 'off'}\\n\"\n f\"\\n\"\n f\"Matrix messages: {messages} messages from {rooms} rooms\\n\"\n f\"Agent sessions: {events} events from {sessions} sessions\\n\"\n )\n (run_dir / \"summary.txt\").write_text(summary)\n\n print(f\"Done. {messages} messages from {rooms} rooms, {events} events from {sessions} sessions\")\n print(f\"Output: {run_dir.resolve()}\")\n\n\nif __name__ == \"__main__\":\n main()\n"}, "440713ebf84a537dc28edd802b1daf36bfa5b08c": {"t": 1785764746.4843967, "n": 22800, "h": "4b616e2797ee0a18", "c": "#!/usr/bin/env python3\nfrom __future__ import annotations\n\"\"\"\nExport HiClaw debug logs: Matrix messages + agent session logs.\n\nUsage:\n # Export last 1 hour (default)\n python scripts/export-debug-log.py --range 1h\n\n # Export last 1 day\n python scripts/export-debug-log.py --range 1d\n\n # Filter by container or room\n python scripts/export-debug-log.py --range 1h --container hiclaw-manager --room Worker\n\n # Disable PII redaction\n python scripts/export-debug-log.py --range 1h --no-redact\n\nOutput structure:\n debug-log/20260319-153000/\n ├── summary.txt\n ├── matrix-messages/\n │ └── RoomName_!roomid.jsonl\n └── agent-sessions/\n ├── hiclaw-manager/\n │ └── {session-id}.jsonl\n └── hiclaw-worker-xxx/\n └── {session-key}.jsonl\n\"\"\"\n\nimport argparse\nimport json\nimport os\nimport re\nimport subprocess\nimport sys\nimport time\nimport urllib.parse\nimport urllib.request\nimport urllib.error\nfrom datetime import datetime, timezone\nfrom pathlib import Path\n\n\n# ---------------------------------------------------------------------------\n# PII redaction\n# ---------------------------------------------------------------------------\n\n_PII_PATTERNS: list[tuple[str, re.Pattern]] = [\n (\"ID_CARD\", re.compile(r'\\b[1-9]\\d{5}(?:19|20)\\d{2}(?:0[1-9]|1[0-2])(?:0[1-9]|[12]\\d|3[01])\\d{3}[\\dXx]\\b')),\n (\"PHONE\", re.compile(r'(?<!\\d)(?:\\+?86[-\\s]?)?1[3-9]\\d{9}(?!\\d)')),\n (\"EMAIL\", re.compile(r'\\b[A-Za-z0-9._%+\\-]+@[A-Za-z0-9.\\-]+\\.[A-Za-z]{2,}\\b')),\n (\"BANK_CARD\", re.compile(r'\\b(?:6[0-9]{15,18}|4[0-9]{15}|5[1-5][0-9]{14}|3[47][0-9]{13}|62[0-9]{14,17})\\b')),\n (\"IP\", re.compile(r'\\b(?:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)\\.){3}(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)\\b')),\n (\"ALIYUN_AK\", re.compile(r'\\bLTAI[A-Za-z0-9]{12,30}\\b')),\n (\"ALIYUN_SK\", re.compile(r'(?i)(?:access_?key_?secret|secret_?access_?key)\\s*[:=]\\s*(\\S{20,})')),\n (\"AWS_AK\", re.compile(r'\\b(?:AKIA|ASIA)[A-Z0-9]{16}\\b')),\n (\"OPENAI_KEY\", re.compile(r'\\bsk-[A-Za-z0-9]{20,}\\b')),\n (\"ANTHROPIC_KEY\", re.compile(r'\\bsk-ant-[A-Za-z0-9\\-]{20,}\\b')),\n (\"DASHSCOPE_KEY\", re.compile(r'\\bsk-sp-[A-Za-z0-9]{20,}\\b')),\n (\"DEEPSEEK_KEY\", re.compile(r'\\bsk-[a-f0-9]{32,}\\b')),\n (\"BEARER\", re.compile(r'(?i)(Bearer\\s+)([A-Za-z0-9\\-_.]{20,})')),\n (\"SECRET_KV\", re.compile(\n r'(?i)((?:password|passwd|pwd|secret|token|api_?key|access_?key|secret_?key'\n r'|private_?key|credential|appkey|app_?secret|auth_?token|signing_?key'\n r'|client_?secret|master_?key)\\s*[:=]\\s*)'\n r'(\\S+)'\n )),\n (\"MATRIX_TOKEN\", re.compile(r'\\bsyt_[A-Za-z0-9_\\-]{10,}\\b')),\n (\"HEX_SECRET\", re.compile(r'\\b[A-Fa-f0-9]{32,}\\b')),\n (\"PASSPORT\", re.compile(r'\\b[EeGg]\\d{8}\\b')),\n (\"SSN\", re.compile(r'\\b\\d{3}-\\d{2}-\\d{4}\\b')),\n]\n\n\ndef redact_pii(text: str) -> str:\n if not text:\n return text\n for name, pattern in _PII_PATTERNS:\n if name in (\"SECRET_KV\", \"ALIYUN_SK\", \"BEARER\"):\n text = pattern.sub(r'\\1****', text)\n else:\n text = pattern.sub('****', text)\n return text\n\n\ndef redact_json_strings(obj):\n if isinstance(obj, str):\n return redact_pii(obj)\n if isinstance(obj, list):\n return [redact_json_strings(v) for v in obj]\n if isinstance(obj, dict):\n return {k: redact_json_strings(v) for k, v in obj.items()}\n return obj\n\n\n# ---------------------------------------------------------------------------\n# Common helpers\n# ---------------------------------------------------------------------------\n\ndef parse_range(range_str: str) -> int:\n m = re.fullmatch(r\"(\\d+)\\s*(m|min|h|hr|hour|d|day)s?\", range_str.strip(), re.IGNORECASE)\n if not m:\n raise ValueError(f\"Invalid range format: '{range_str}'. Use e.g. 10m, 1h, 1d\")\n value, unit = int(m.group(1)), m.group(2)[0].lower()\n return value * {\"m\": 60, \"h\": 3600, \"d\": 86400}[unit]\n\n\ndef parse_ts(ts_str: str) -> float:\n if not ts_str:\n return 0\n ts_str = ts_str.replace(\"Z\", \"+00:00\")\n try:\n dt = datetime.fromisoformat(ts_str)\n if dt.tzinfo is None:\n dt = dt.replace(tzinfo=timezone.utc)\n return dt.timestamp()\n except Exception:\n return 0\n\n\ndef sanitize_filename(name: str) -> str:\n return re.sub(r'[^\\w\\-. ]', '_', name).strip()[:80]\n\n\ndef docker_exec(container: str, cmd: str) -> str:\n result = subprocess.run(\n [\"docker\", \"exec\", container, \"sh\", \"-c\", cmd],\n capture_output=True, text=True, timeout=30,\n )\n return result.stdout\n\n\ndef list_hiclaw_containers() -> list[str]:\n result = subprocess.run(\n [\"docker\", \"ps\", \"--format\", \"{{.Names}}\", \"--filter\", \"name=hiclaw-\"],\n capture_output=True, text=True, timeout=10,\n )\n return [n.strip() for n in result.stdout.splitlines() if n.strip()]\n\n\n# ---------------------------------------------------------------------------\n# Matrix messages export\n# ---------------------------------------------------------------------------\n\ndef load_env_file(path: str) -> dict[str, str]:\n env = {}\n try:\n with open(path) as f:\n for line in f:\n line = line.strip()\n if not line or line.startswith(\"#\"):\n continue\n if \"=\" in line:\n key, _, value = line.partition(\"=\")\n env[key.strip()] = value.strip()\n except FileNotFoundError:\n pass\n return env\n\n\ndef matrix_login(homeserver: str, user: str, password: str) -> str:\n url = f\"{homeserver.rstrip('/')}/_matrix/client/v3/login\"\n payload = json.dumps({\n \"type\": \"m.login.password\",\n \"identifier\": {\"type\": \"m.id.user\", \"user\": user},\n \"password\": password,\n }).encode()\n req = urllib.request.Request(url, data=payload, headers={\"Content-Type\": \"application/json\"})\n with urllib.request.urlopen(req, timeout=15) as resp:\n data = json.loads(resp.read())\n return data[\"access_token\"]\n\n\ndef matrix_api(homeserver: str, token: str, endpoint: str, params: dict | None = None) -> dict:\n url = f\"{homeserver.rstrip('/')}/_matrix/client/v3/{endpoint}\"\n if params:\n url += \"?\" + urllib.parse.urlencode(params)\n req = urllib.request.Request(url, headers={\"Authorization\": f\"Bearer {token}\"})\n try:\n with urllib.request.urlopen(req, timeout=30) as resp:\n return json.loads(resp.read())\n except urllib.error.HTTPError as e:\n body = e.read().decode(errors=\"replace\")\n print(f\" API error {e.code} on {endpoint}: {body}\", file=sys.stderr)\n raise\n\n\ndef fetch_room_messages(homeserver: str, token: str, room_id: str, since_ts: int) -> list[dict]:\n encoded = urllib.parse.quote(room_id)\n messages = []\n from_token = \"\"\n while True:\n params = {\"dir\": \"b\", \"limit\": \"100\"}\n if from_token:\n params[\"from\"] = from_token\n data = matrix_api(homeserver, token, f\"rooms/{encoded}/messages\", params)\n chunk = data.get(\"chunk\", [])\n if not chunk:\n break\n hit_boundary = False\n for event in chunk:\n if event.get(\"origin_server_ts\", 0) < since_ts:\n hit_boundary = True\n break\n messages.append(event)\n if hit_boundary:\n break\n next_token = data.get(\"end\")\n if not next_token or next_token == from_token:\n break\n from_token = next_token\n messages.reverse()\n return messages\n\n\ndef format_event(event: dict, redact: bool) -> dict:\n content = event.get(\"content\", {})\n ts = event.get(\"origin_server_ts\", 0)\n record = {\n \"event_id\": event.get(\"event_id\"),\n \"type\": event.get(\"type\"),\n \"sender\": event.get(\"sender\"),\n \"timestamp\": ts,\n \"time\": datetime.fromtimestamp(ts / 1000, tz=timezone.utc).isoformat(),\n }\n if event.get(\"type\") == \"m.room.message\":\n record[\"msgtype\"] = content.get(\"msgtype\")\n body = content.get(\"body\")\n record[\"body\"] = redact_pii(body) if redact else body\n if content.get(\"format\"):\n record[\"format\"] = content[\"format\"]\n if content.get(\"url\"):\n record[\"url\"] = content[\"url\"]\n if content.get(\"m.relates_to\"):\n record[\"relates_to\"] = content[\"m.relates_to\"]\n else:\n record[\"content\"] = content\n return record\n\n\ndef export_matrix_messages(out_dir: Path, since_epoch: float, redact: bool,\n room_filter: str | None, env_file: str | None,\n homeserver: str, token: str,\n messages_only: bool) -> tuple[int, int]:\n \"\"\"Export Matrix messages. Returns (rooms_exported, message_count).\"\"\"\n if not homeserver or not token:\n env_path = env_file or os.path.expanduser(\"~/hiclaw-manager.env\")\n hiclaw_env = load_env_file(env_path)\n if not hiclaw_env:\n print(f\" [matrix] Cannot find {env_path}, skipping Matrix export\")\n return 0, 0\n\n if not homeserver:\n port = hiclaw_env.get(\"HICLAW_PORT_GATEWAY\", \"18080\")\n homeserver = f\"http://127.0.0.1:{port}\"\n\n if not token:\n admin_user = hiclaw_env.get(\"HICLAW_ADMIN_USER\", \"admin\")\n admin_password = hiclaw_env.get(\"HICLAW_ADMIN_PASSWORD\", \"\")\n if not admin_password:\n print(f\" [matrix] HICLAW_ADMIN_PASSWORD not found, skipping Matrix export\")\n return 0, 0\n try:\n token = matrix_login(homeserver, admin_user, admin_password)\n except Exception as e:\n print(f\" [matrix] Login failed: {e}, skipping Matrix export\")\n return 0, 0\n\n since_ts = int(since_epoch * 1000)\n out_dir.mkdir(parents=True, exist_ok=True)\n\n try:\n rooms = matrix_api(homeserver, token, \"joined_rooms\").get(\"joined_rooms\", [])\n except Exception as e:\n print(f\" [matrix] Failed to list rooms: {e}\")\n return 0, 0\n\n total_messages = 0\n total_rooms = 0\n\n for room_id in rooms:\n encoded = urllib.parse.quote(room_id)\n try:\n room_name = matrix_api(homeserver, token, f\"rooms/{encoded}/state/m.room.name\").get(\"name\", \"\")\n except Exception:\n room_name = \"\"\n\n if room_filter:\n if room_filter not in room_id and room_filter not in (room_name or \"\"):\n continue\n\n display = f\"{room_name} ({room_id})\" if room_name else room_id\n print(f\" {display} ... \", end=\"\", flush=True)\n\n messages = fetch_room_messages(homeserver, token, room_id, since_ts)\n if messages_only:\n messages = [e for e in messages if e.get(\"type\") == \"m.room.message\"]\n\n if not messages:\n print(\"0 messages, skipped\")\n continue\n\n name_part = sanitize_filename(room_name) if room_name else \"\"\n id_part = sanitize_filename(room_id)\n filename = f\"{name_part}_{id_part}.jsonl\" if name_part else f\"{id_part}.jsonl\"\n\n with open(out_dir / filename, \"w\", encoding=\"utf-8\") as f:\n for event in messages:\n record = format_event(event, redact=redact)\n f.write(json.dumps(record, ensure_ascii=False) + \"\\n\")\n\n print(f\"{len(messages)} messages -> {filename}\")\n total_messages += len(messages)\n total_rooms += 1\n\n return total_rooms, total_messages\n\n\n# ---------------------------------------------------------------------------\n# Agent sessions export (OpenClaw + CoPaw)\n# ---------------------------------------------------------------------------\n\ndef detect_runtime(container: str) -> tuple[str, str]:\n std = \".openclaw/agents/main/sessions\"\n if docker_exec(container, f\"test -d {std} && echo yes || echo no\").strip() == \"yes\":\n return \"openclaw\", std\n\n worker_name = docker_exec(container, \"echo $HICLAW_WORKER_NAME\").strip()\n if worker_name:\n copaw_dir = f\"{worker_name}/.copaw/sessions\"\n if docker_exec(container, f\"test -d '{copaw_dir}' && echo yes || echo no\").strip() == \"yes\":\n return \"copaw\", copaw_dir\n\n found = docker_exec(container, \"find . -maxdepth 3 -path '*/.copaw/sessions' -type d 2>/dev/null | head -1\").strip()\n if found:\n return \"copaw\", found.lstrip(\"./\")\n\n return \"\", \"\"\n\n\ndef export_openclaw_sessions(container: str, sessions_dir: str, since_epoch: float,\n out_dir: Path, redact: bool) -> tuple[int, int]:\n ls_output = docker_exec(container, f\"ls '{sessions_dir}'/*.jsonl 2>/dev/null\").strip()\n if not ls_output:\n return 0, 0\n\n total_sessions = 0\n total_events = 0\n\n for session_path in [f.strip() for f in ls_output.splitlines() if f.strip()]:\n filename = os.path.basename(session_path)\n\n first_line = docker_exec(container, f\"head -1 '{session_path}'\").strip()\n if not first_line:\n continue\n try:\n header = json.loads(first_line)\n except json.JSONDecodeError:\n continue\n\n last_line = docker_exec(container, f\"tail -1 '{session_path}'\").strip()\n try:\n last_ts = parse_ts(json.loads(last_line).get(\"timestamp\", \"\"))\n except Exception:\n last_ts = parse_ts(header.get(\"timestamp\", \"\"))\n\n if last_ts < since_epoch and last_ts > 0:\n continue\n\n raw = docker_exec(container, f\"cat '{session_path}'\")\n if not raw.strip():\n continue\n\n output_lines = []\n for line in raw.splitlines():\n line = line.strip()\n if not line:\n continue\n try:\n event = json.loads(line)\n except json.JSONDecodeError:\n continue\n event_ts = parse_ts(event.get(\"timestamp\", \"\"))\n if event.get(\"type\") != \"session\" and event_ts < since_epoch and event_ts > 0:\n continue\n if redact:\n event = redact_json_strings(event)\n output_lines.append(json.dumps(event, ensure_ascii=False))\n\n if len(output_lines) <= 1:\n continue\n\n with open(out_dir / filename, \"w\", encoding=\"utf-8\") as f:\n f.write(\"\\n\".join(output_lines) + \"\\n\")\n\n event_count = len(output_lines) - 1\n print(f\" {container}/{filename} (openclaw): {event_count} events\")\n total_sessions += 1\n total_events += event_count\n\n # sessions.json index\n try:\n raw = docker_exec(container, f\"cat '{sessions_dir}/sessions.json'\")\n if raw.strip():\n index = json.loads(raw)\n if redact:\n index = redact_json_strings(index)\n with open(out_dir / \"sessions.json\", \"w\", encoding=\"utf-8\") as f:\n json.dump(index, f, ensure_ascii=False, indent=2)\n except Exception:\n pass\n\n return total_sessions, total_events\n\n\ndef export_copaw_sessions(container: str, sessions_dir: str, since_epoch: float,\n out_dir: Path, redact: bool) -> tuple[int, int]:\n ls_output = docker_exec(container, f\"find '{sessions_dir}' -name '*.json' -type f 2>/dev/null\").strip()\n if not ls_output:\n return 0, 0\n\n total_sessions = 0\n total_events = 0\n\n for session_path in [f.strip() for f in ls_output.splitlines() if f.strip()]:\n raw = docker_exec(container, f\"cat '{session_path}'\")\n if not raw.strip():\n continue\n try:\n data = json.loads(raw)\n except json.JSONDecodeError:\n continue\n\n agent = data.get(\"agent\", {})\n memory = agent.get(\"memory\", {})\n content = memory.get(\"content\", [])\n if not content:\n continue\n\n basename = os.path.basename(session_path).replace(\".json\", \"\")\n header = {\n \"type\": \"session\",\n \"runtime\": \"copaw\",\n \"agent_name\": agent.get(\"name\", \"\"),\n \"session_key\": basename,\n \"compressed_summary\": memory.get(\"_compressed_summary\", \"\"),\n }\n\n messages_in_range = []\n for turn_idx, turn in enumerate(content):\n if not isinstance(turn, list):\n continue\n for msg in turn:\n if not isinstance(msg, dict):\n continue\n msg_ts = parse_ts(msg.get(\"timestamp\", \"\"))\n if msg_ts >= since_epoch or msg_ts == 0:\n event = {\n \"type\": \"message\",\n \"turn\": turn_idx,\n \"id\": msg.get(\"id\", \"\"),\n \"role\": msg.get(\"role\", \"\"),\n \"name\": msg.get(\"name\", \"\"),\n \"timestamp\": msg.get(\"timestamp\", \"\"),\n \"content\": msg.get(\"content\", []),\n }\n if msg.get(\"metadata\"):\n event[\"metadata\"] = msg[\"metadata\"]\n messages_in_range.append(event)\n\n if not messages_in_range:\n continue\n\n output_lines = []\n if redact:\n header = redact_json_strings(header)\n output_lines.append(json.dumps(header, ensure_ascii=False))\n for event in messages_in_range:\n if redact:\n event = redact_json_strings(event)\n output_lines.append(json.dumps(event, ensure_ascii=False))\n\n out_filename = basename + \".jsonl\"\n with open(out_dir / out_filename, \"w\", encoding=\"utf-8\") as f:\n f.write(\"\\n\".join(output_lines) + \"\\n\")\n\n event_count = len(output_lines) - 1\n print(f\" {container}/{out_filename} (copaw): {event_count} events\")\n total_sessions += 1\n total_events += event_count\n\n return total_sessions, total_events\n\n\ndef export_agent_sessions(out_dir: Path, since_epoch: float, redact: bool,\n container_filter: str | None) -> tuple[int, int]:\n \"\"\"Export agent sessions from all containers. Returns (session_count, event_count).\"\"\"\n containers = list_hiclaw_containers()\n if container_filter:\n containers = [c for c in containers if container_filter in c]\n\n if not containers:\n print(\" [sessions] No matching hiclaw containers found\")\n return 0, 0\n\n total_sessions = 0\n total_events = 0\n\n for container in containers:\n runtime, sessions_dir = detect_runtime(container)\n if not runtime:\n print(f\" {container}: no sessions directory, skipped\")\n continue\n\n container_dir = out_dir / container\n container_dir.mkdir(parents=True, exist_ok=True)\n\n if runtime == \"openclaw\":\n s, e = export_openclaw_sessions(container, sessions_dir, since_epoch,\n container_dir, redact)\n else:\n s, e = export_copaw_sessions(container, sessions_dir, since_epoch,\n container_dir, redact)\n\n if s == 0:\n container_dir.rmdir()\n print(f\" {container} ({runtime}): no sessions in range, skipped\")\n else:\n total_sessions += s\n total_events += e\n\n return total_sessions, total_events\n\n\n# ---------------------------------------------------------------------------\n# Main\n# ---------------------------------------------------------------------------\n\ndef main():\n parser = argparse.ArgumentParser(\n description=\"Export HiClaw debug logs (Matrix messages + agent sessions)\"\n )\n parser.add_argument(\"--range\", \"-r\", required=True, dest=\"time_range\",\n help=\"Time range to export, e.g. 10m, 1h, 1d\")\n parser.add_argument(\"--container\", \"-c\", default=None,\n help=\"Only export sessions from containers matching this substring\")\n parser.add_argument(\"--room\", default=None,\n help=\"Only export Matrix rooms matching this substring\")\n parser.add_argument(\"--homeserver\", \"-s\", default=\"\",\n help=\"Matrix homeserver URL (auto-detected from env file)\")\n parser.add_argument(\"--token\", \"-t\", default=\"\",\n help=\"Matrix access token (auto-detected from env file)\")\n parser.add_argument(\"--env-file\", default=None,\n help=\"Path to hiclaw-manager.env (default: ~/hiclaw-manager.env)\")\n parser.add_argument(\"--messages-only\", action=\"store_true\",\n help=\"Only export m.room.message events (skip state events)\")\n parser.add_argument(\"--no-redact\", action=\"store_true\",\n help=\"Disable PII redaction\")\n args = parser.parse_args()\n\n range_seconds = parse_range(args.time_range)\n since_epoch = time.time() - range_seconds\n since_human = datetime.fromtimestamp(since_epoch, tz=timezone.utc).isoformat()\n now_str = datetime.now().strftime(\"%Y%m%d-%H%M%S\")\n\n run_dir = Path(\"debug-log\") / now_str\n run_dir.mkdir(parents=True, exist_ok=True)\n redact = not args.no_redact\n\n print(f\"HiClaw Debug Log Export\")\n print(f\" Range: last {args.time_range} (since {since_human})\")\n print(f\" Output: {run_dir.resolve()}\")\n print(f\" PII redaction: {'on' if redact else 'off'}\")\n print()\n\n # --- Matrix messages ---\n print(\"=== Matrix Messages ===\")\n matrix_dir = run_dir / \"matrix-messages\"\n rooms, messages = export_matrix_messages(\n matrix_dir, since_epoch, redact,\n room_filter=args.room, env_file=args.env_file,\n homeserver=args.homeserver, token=args.token,\n messages_only=args.messages_only,\n )\n if rooms == 0 and matrix_dir.exists() and not any(matrix_dir.iterdir()):\n matrix_dir.rmdir()\n print()\n\n # --- Agent sessions ---\n print(\"=== Agent Sessions ===\")\n sessions_dir = run_dir / \"agent-sessions\"\n sessions, events = export_agent_sessions(\n sessions_dir, since_epoch, redact,\n container_filter=args.container,\n )\n if sessions == 0 and sessions_dir.exists() and not any(sessions_dir.iterdir()):\n sessions_dir.rmdir()\n print()\n\n # --- Summary ---\n summary = (\n f\"HiClaw Debug Log\\n\"\n f\"Exported at: {now_str}\\n\"\n f\"Range: last {args.time_range} (since {since_human})\\n\"\n f\"PII redaction: {'on' if redact else 'off'}\\n\"\n f\"\\n\"\n f\"Matrix messages: {messages} messages from {rooms} rooms\\n\"\n f\"Agent sessions: {events} events from {sessions} sessions\\n\"\n )\n (run_dir / \"summary.txt\").write_text(summary)\n\n print(f\"Done. {messages} messages from {rooms} rooms, {events} events from {sessions} sessions\")\n print(f\"Output: {run_dir.resolve()}\")\n\n\nif __name__ == \"__main__\":\n main()\n"}, "4abd8ccff3c1b206cb423246a84668e181b65e1f": {"t": 1785764748.0615156, "n": 22765, "h": "f72d24eb69ae9487", "c": "#!/usr/bin/env python3\n\"\"\"\nExport HiClaw debug logs: Matrix messages + agent session logs.\n\nUsage:\n # Export last 1 hour (default)\n python scripts/export-debug-log.py --range 1h\n\n # Export last 1 day\n python scripts/export-debug-log.py --range 1d\n\n # Filter by container or room\n python scripts/export-debug-log.py --range 1h --container hiclaw-manager --room Worker\n\n # Disable PII redaction\n python scripts/export-debug-log.py --range 1h --no-redact\n\nOutput structure:\n debug-log/20260319-153000/\n ├── summary.txt\n ├── matrix-messages/\n │ └── RoomName_!roomid.jsonl\n └── agent-sessions/\n ├── hiclaw-manager/\n │ └── {session-id}.jsonl\n └── hiclaw-worker-xxx/\n └── {session-key}.jsonl\n\"\"\"\n\nimport argparse\nimport json\nimport os\nimport re\nimport subprocess\nimport sys\nimport time\nimport urllib.parse\nimport urllib.request\nimport urllib.error\nfrom datetime import datetime, timezone\nfrom pathlib import Path\n\n\n# ---------------------------------------------------------------------------\n# PII redaction\n# ---------------------------------------------------------------------------\n\n_PII_PATTERNS: list[tuple[str, re.Pattern]] = [\n (\"ID_CARD\", re.compile(r'\\b[1-9]\\d{5}(?:19|20)\\d{2}(?:0[1-9]|1[0-2])(?:0[1-9]|[12]\\d|3[01])\\d{3}[\\dXx]\\b')),\n (\"PHONE\", re.compile(r'(?<!\\d)(?:\\+?86[-\\s]?)?1[3-9]\\d{9}(?!\\d)')),\n (\"EMAIL\", re.compile(r'\\b[A-Za-z0-9._%+\\-]+@[A-Za-z0-9.\\-]+\\.[A-Za-z]{2,}\\b')),\n (\"BANK_CARD\", re.compile(r'\\b(?:6[0-9]{15,18}|4[0-9]{15}|5[1-5][0-9]{14}|3[47][0-9]{13}|62[0-9]{14,17})\\b')),\n (\"IP\", re.compile(r'\\b(?:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)\\.){3}(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)\\b')),\n (\"ALIYUN_AK\", re.compile(r'\\bLTAI[A-Za-z0-9]{12,30}\\b')),\n (\"ALIYUN_SK\", re.compile(r'(?i)(?:access_?key_?secret|secret_?access_?key)\\s*[:=]\\s*(\\S{20,})')),\n (\"AWS_AK\", re.compile(r'\\b(?:AKIA|ASIA)[A-Z0-9]{16}\\b')),\n (\"OPENAI_KEY\", re.compile(r'\\bsk-[A-Za-z0-9]{20,}\\b')),\n (\"ANTHROPIC_KEY\", re.compile(r'\\bsk-ant-[A-Za-z0-9\\-]{20,}\\b')),\n (\"DASHSCOPE_KEY\", re.compile(r'\\bsk-sp-[A-Za-z0-9]{20,}\\b')),\n (\"DEEPSEEK_KEY\", re.compile(r'\\bsk-[a-f0-9]{32,}\\b')),\n (\"BEARER\", re.compile(r'(?i)(Bearer\\s+)([A-Za-z0-9\\-_.]{20,})')),\n (\"SECRET_KV\", re.compile(\n r'(?i)((?:password|passwd|pwd|secret|token|api_?key|access_?key|secret_?key'\n r'|private_?key|credential|appkey|app_?secret|auth_?token|signing_?key'\n r'|client_?secret|master_?key)\\s*[:=]\\s*)'\n r'(\\S+)'\n )),\n (\"MATRIX_TOKEN\", re.compile(r'\\bsyt_[A-Za-z0-9_\\-]{10,}\\b')),\n (\"HEX_SECRET\", re.compile(r'\\b[A-Fa-f0-9]{32,}\\b')),\n (\"PASSPORT\", re.compile(r'\\b[EeGg]\\d{8}\\b')),\n (\"SSN\", re.compile(r'\\b\\d{3}-\\d{2}-\\d{4}\\b')),\n]\n\n\ndef redact_pii(text: str) -> str:\n if not text:\n return text\n for name, pattern in _PII_PATTERNS:\n if name in (\"SECRET_KV\", \"ALIYUN_SK\", \"BEARER\"):\n text = pattern.sub(r'\\1****', text)\n else:\n text = pattern.sub('****', text)\n return text\n\n\ndef redact_json_strings(obj):\n if isinstance(obj, str):\n return redact_pii(obj)\n if isinstance(obj, list):\n return [redact_json_strings(v) for v in obj]\n if isinstance(obj, dict):\n return {k: redact_json_strings(v) for k, v in obj.items()}\n return obj\n\n\n# ---------------------------------------------------------------------------\n# Common helpers\n# ---------------------------------------------------------------------------\n\ndef parse_range(range_str: str) -> int:\n m = re.fullmatch(r\"(\\d+)\\s*(m|min|h|hr|hour|d|day)s?\", range_str.strip(), re.IGNORECASE)\n if not m:\n raise ValueError(f\"Invalid range format: '{range_str}'. Use e.g. 10m, 1h, 1d\")\n value, unit = int(m.group(1)), m.group(2)[0].lower()\n return value * {\"m\": 60, \"h\": 3600, \"d\": 86400}[unit]\n\n\ndef parse_ts(ts_str: str) -> float:\n if not ts_str:\n return 0\n ts_str = ts_str.replace(\"Z\", \"+00:00\")\n try:\n dt = datetime.fromisoformat(ts_str)\n if dt.tzinfo is None:\n dt = dt.replace(tzinfo=timezone.utc)\n return dt.timestamp()\n except Exception:\n return 0\n\n\ndef sanitize_filename(name: str) -> str:\n return re.sub(r'[^\\w\\-. ]', '_', name).strip()[:80]\n\n\ndef docker_exec(container: str, cmd: str) -> str:\n result = subprocess.run(\n [\"docker\", \"exec\", container, \"sh\", \"-c\", cmd],\n capture_output=True, text=True, timeout=30,\n )\n return result.stdout\n\n\ndef list_hiclaw_containers() -> list[str]:\n result = subprocess.run(\n [\"docker\", \"ps\", \"--format\", \"{{.Names}}\", \"--filter\", \"name=hiclaw-\"],\n capture_output=True, text=True, timeout=10,\n )\n return [n.strip() for n in result.stdout.splitlines() if n.strip()]\n\n\n# ---------------------------------------------------------------------------\n# Matrix messages export\n# ---------------------------------------------------------------------------\n\ndef load_env_file(path: str) -> dict[str, str]:\n env = {}\n try:\n with open(path) as f:\n for line in f:\n line = line.strip()\n if not line or line.startswith(\"#\"):\n continue\n if \"=\" in line:\n key, _, value = line.partition(\"=\")\n env[key.strip()] = value.strip()\n except FileNotFoundError:\n pass\n return env\n\n\ndef matrix_login(homeserver: str, user: str, password: str) -> str:\n url = f\"{homeserver.rstrip('/')}/_matrix/client/v3/login\"\n payload = json.dumps({\n \"type\": \"m.login.password\",\n \"identifier\": {\"type\": \"m.id.user\", \"user\": user},\n \"password\": password,\n }).encode()\n req = urllib.request.Request(url, data=payload, headers={\"Content-Type\": \"application/json\"})\n with urllib.request.urlopen(req, timeout=15) as resp:\n data = json.loads(resp.read())\n return data[\"access_token\"]\n\n\ndef matrix_api(homeserver: str, token: str, endpoint: str, params: dict | None = None) -> dict:\n url = f\"{homeserver.rstrip('/')}/_matrix/client/v3/{endpoint}\"\n if params:\n url += \"?\" + urllib.parse.urlencode(params)\n req = urllib.request.Request(url, headers={\"Authorization\": f\"Bearer {token}\"})\n try:\n with urllib.request.urlopen(req, timeout=30) as resp:\n return json.loads(resp.read())\n except urllib.error.HTTPError as e:\n body = e.read().decode(errors=\"replace\")\n print(f\" API error {e.code} on {endpoint}: {body}\", file=sys.stderr)\n raise\n\n\ndef fetch_room_messages(homeserver: str, token: str, room_id: str, since_ts: int) -> list[dict]:\n encoded = urllib.parse.quote(room_id)\n messages = []\n from_token = \"\"\n while True:\n params = {\"dir\": \"b\", \"limit\": \"100\"}\n if from_token:\n params[\"from\"] = from_token\n data = matrix_api(homeserver, token, f\"rooms/{encoded}/messages\", params)\n chunk = data.get(\"chunk\", [])\n if not chunk:\n break\n hit_boundary = False\n for event in chunk:\n if event.get(\"origin_server_ts\", 0) < since_ts:\n hit_boundary = True\n break\n messages.append(event)\n if hit_boundary:\n break\n next_token = data.get(\"end\")\n if not next_token or next_token == from_token:\n break\n from_token = next_token\n messages.reverse()\n return messages\n\n\ndef format_event(event: dict, redact: bool) -> dict:\n content = event.get(\"content\", {})\n ts = event.get(\"origin_server_ts\", 0)\n record = {\n \"event_id\": event.get(\"event_id\"),\n \"type\": event.get(\"type\"),\n \"sender\": event.get(\"sender\"),\n \"timestamp\": ts,\n \"time\": datetime.fromtimestamp(ts / 1000, tz=timezone.utc).isoformat(),\n }\n if event.get(\"type\") == \"m.room.message\":\n record[\"msgtype\"] = content.get(\"msgtype\")\n body = content.get(\"body\")\n record[\"body\"] = redact_pii(body) if redact else body\n if content.get(\"format\"):\n record[\"format\"] = content[\"format\"]\n if content.get(\"url\"):\n record[\"url\"] = content[\"url\"]\n if content.get(\"m.relates_to\"):\n record[\"relates_to\"] = content[\"m.relates_to\"]\n else:\n record[\"content\"] = content\n return record\n\n\ndef export_matrix_messages(out_dir: Path, since_epoch: float, redact: bool,\n room_filter: str | None, env_file: str | None,\n homeserver: str, token: str,\n messages_only: bool) -> tuple[int, int]:\n \"\"\"Export Matrix messages. Returns (rooms_exported, message_count).\"\"\"\n if not homeserver or not token:\n env_path = env_file or os.path.expanduser(\"~/hiclaw-manager.env\")\n hiclaw_env = load_env_file(env_path)\n if not hiclaw_env:\n print(f\" [matrix] Cannot find {env_path}, skipping Matrix export\")\n return 0, 0\n\n if not homeserver:\n port = hiclaw_env.get(\"HICLAW_PORT_GATEWAY\", \"18080\")\n homeserver = f\"http://127.0.0.1:{port}\"\n\n if not token:\n admin_user = hiclaw_env.get(\"HICLAW_ADMIN_USER\", \"admin\")\n admin_password = hiclaw_env.get(\"HICLAW_ADMIN_PASSWORD\", \"\")\n if not admin_password:\n print(f\" [matrix] HICLAW_ADMIN_PASSWORD not found, skipping Matrix export\")\n return 0, 0\n try:\n token = matrix_login(homeserver, admin_user, admin_password)\n except Exception as e:\n print(f\" [matrix] Login failed: {e}, skipping Matrix export\")\n return 0, 0\n\n since_ts = int(since_epoch * 1000)\n out_dir.mkdir(parents=True, exist_ok=True)\n\n try:\n rooms = matrix_api(homeserver, token, \"joined_rooms\").get(\"joined_rooms\", [])\n except Exception as e:\n print(f\" [matrix] Failed to list rooms: {e}\")\n return 0, 0\n\n total_messages = 0\n total_rooms = 0\n\n for room_id in rooms:\n encoded = urllib.parse.quote(room_id)\n try:\n room_name = matrix_api(homeserver, token, f\"rooms/{encoded}/state/m.room.name\").get(\"name\", \"\")\n except Exception:\n room_name = \"\"\n\n if room_filter:\n if room_filter not in room_id and room_filter not in (room_name or \"\"):\n continue\n\n display = f\"{room_name} ({room_id})\" if room_name else room_id\n print(f\" {display} ... \", end=\"\", flush=True)\n\n messages = fetch_room_messages(homeserver, token, room_id, since_ts)\n if messages_only:\n messages = [e for e in messages if e.get(\"type\") == \"m.room.message\"]\n\n if not messages:\n print(\"0 messages, skipped\")\n continue\n\n name_part = sanitize_filename(room_name) if room_name else \"\"\n id_part = sanitize_filename(room_id)\n filename = f\"{name_part}_{id_part}.jsonl\" if name_part else f\"{id_part}.jsonl\"\n\n with open(out_dir / filename, \"w\", encoding=\"utf-8\") as f:\n for event in messages:\n record = format_event(event, redact=redact)\n f.write(json.dumps(record, ensure_ascii=False) + \"\\n\")\n\n print(f\"{len(messages)} messages -> {filename}\")\n total_messages += len(messages)\n total_rooms += 1\n\n return total_rooms, total_messages\n\n\n# ---------------------------------------------------------------------------\n# Agent sessions export (OpenClaw + CoPaw)\n# ---------------------------------------------------------------------------\n\ndef detect_runtime(container: str) -> tuple[str, str]:\n std = \".openclaw/agents/main/sessions\"\n if docker_exec(container, f\"test -d {std} && echo yes || echo no\").strip() == \"yes\":\n return \"openclaw\", std\n\n worker_name = docker_exec(container, \"echo $HICLAW_WORKER_NAME\").strip()\n if worker_name:\n copaw_dir = f\"{worker_name}/.copaw/sessions\"\n if docker_exec(container, f\"test -d '{copaw_dir}' && echo yes || echo no\").strip() == \"yes\":\n return \"copaw\", copaw_dir\n\n found = docker_exec(container, \"find . -maxdepth 3 -path '*/.copaw/sessions' -type d 2>/dev/null | head -1\").strip()\n if found:\n return \"copaw\", found.lstrip(\"./\")\n\n return \"\", \"\"\n\n\ndef export_openclaw_sessions(container: str, sessions_dir: str, since_epoch: float,\n out_dir: Path, redact: bool) -> tuple[int, int]:\n ls_output = docker_exec(container, f\"ls '{sessions_dir}'/*.jsonl 2>/dev/null\").strip()\n if not ls_output:\n return 0, 0\n\n total_sessions = 0\n total_events = 0\n\n for session_path in [f.strip() for f in ls_output.splitlines() if f.strip()]:\n filename = os.path.basename(session_path)\n\n first_line = docker_exec(container, f\"head -1 '{session_path}'\").strip()\n if not first_line:\n continue\n try:\n header = json.loads(first_line)\n except json.JSONDecodeError:\n continue\n\n last_line = docker_exec(container, f\"tail -1 '{session_path}'\").strip()\n try:\n last_ts = parse_ts(json.loads(last_line).get(\"timestamp\", \"\"))\n except Exception:\n last_ts = parse_ts(header.get(\"timestamp\", \"\"))\n\n if last_ts < since_epoch and last_ts > 0:\n continue\n\n raw = docker_exec(container, f\"cat '{session_path}'\")\n if not raw.strip():\n continue\n\n output_lines = []\n for line in raw.splitlines():\n line = line.strip()\n if not line:\n continue\n try:\n event = json.loads(line)\n except json.JSONDecodeError:\n continue\n event_ts = parse_ts(event.get(\"timestamp\", \"\"))\n if event.get(\"type\") != \"session\" and event_ts < since_epoch and event_ts > 0:\n continue\n if redact:\n event = redact_json_strings(event)\n output_lines.append(json.dumps(event, ensure_ascii=False))\n\n if len(output_lines) <= 1:\n continue\n\n with open(out_dir / filename, \"w\", encoding=\"utf-8\") as f:\n f.write(\"\\n\".join(output_lines) + \"\\n\")\n\n event_count = len(output_lines) - 1\n print(f\" {container}/{filename} (openclaw): {event_count} events\")\n total_sessions += 1\n total_events += event_count\n\n # sessions.json index\n try:\n raw = docker_exec(container, f\"cat '{sessions_dir}/sessions.json'\")\n if raw.strip():\n index = json.loads(raw)\n if redact:\n index = redact_json_strings(index)\n with open(out_dir / \"sessions.json\", \"w\", encoding=\"utf-8\") as f:\n json.dump(index, f, ensure_ascii=False, indent=2)\n except Exception:\n pass\n\n return total_sessions, total_events\n\n\ndef export_copaw_sessions(container: str, sessions_dir: str, since_epoch: float,\n out_dir: Path, redact: bool) -> tuple[int, int]:\n ls_output = docker_exec(container, f\"find '{sessions_dir}' -name '*.json' -type f 2>/dev/null\").strip()\n if not ls_output:\n return 0, 0\n\n total_sessions = 0\n total_events = 0\n\n for session_path in [f.strip() for f in ls_output.splitlines() if f.strip()]:\n raw = docker_exec(container, f\"cat '{session_path}'\")\n if not raw.strip():\n continue\n try:\n data = json.loads(raw)\n except json.JSONDecodeError:\n continue\n\n agent = data.get(\"agent\", {})\n memory = agent.get(\"memory\", {})\n content = memory.get(\"content\", [])\n if not content:\n continue\n\n basename = os.path.basename(session_path).replace(\".json\", \"\")\n header = {\n \"type\": \"session\",\n \"runtime\": \"copaw\",\n \"agent_name\": agent.get(\"name\", \"\"),\n \"session_key\": basename,\n \"compressed_summary\": memory.get(\"_compressed_summary\", \"\"),\n }\n\n messages_in_range = []\n for turn_idx, turn in enumerate(content):\n if not isinstance(turn, list):\n continue\n for msg in turn:\n if not isinstance(msg, dict):\n continue\n msg_ts = parse_ts(msg.get(\"timestamp\", \"\"))\n if msg_ts >= since_epoch or msg_ts == 0:\n event = {\n \"type\": \"message\",\n \"turn\": turn_idx,\n \"id\": msg.get(\"id\", \"\"),\n \"role\": msg.get(\"role\", \"\"),\n \"name\": msg.get(\"name\", \"\"),\n \"timestamp\": msg.get(\"timestamp\", \"\"),\n \"content\": msg.get(\"content\", []),\n }\n if msg.get(\"metadata\"):\n event[\"metadata\"] = msg[\"metadata\"]\n messages_in_range.append(event)\n\n if not messages_in_range:\n continue\n\n output_lines = []\n if redact:\n header = redact_json_strings(header)\n output_lines.append(json.dumps(header, ensure_ascii=False))\n for event in messages_in_range:\n if redact:\n event = redact_json_strings(event)\n output_lines.append(json.dumps(event, ensure_ascii=False))\n\n out_filename = basename + \".jsonl\"\n with open(out_dir / out_filename, \"w\", encoding=\"utf-8\") as f:\n f.write(\"\\n\".join(output_lines) + \"\\n\")\n\n event_count = len(output_lines) - 1\n print(f\" {container}/{out_filename} (copaw): {event_count} events\")\n total_sessions += 1\n total_events += event_count\n\n return total_sessions, total_events\n\n\ndef export_agent_sessions(out_dir: Path, since_epoch: float, redact: bool,\n container_filter: str | None) -> tuple[int, int]:\n \"\"\"Export agent sessions from all containers. Returns (session_count, event_count).\"\"\"\n containers = list_hiclaw_containers()\n if container_filter:\n containers = [c for c in containers if container_filter in c]\n\n if not containers:\n print(\" [sessions] No matching hiclaw containers found\")\n return 0, 0\n\n total_sessions = 0\n total_events = 0\n\n for container in containers:\n runtime, sessions_dir = detect_runtime(container)\n if not runtime:\n print(f\" {container}: no sessions directory, skipped\")\n continue\n\n container_dir = out_dir / container\n container_dir.mkdir(parents=True, exist_ok=True)\n\n if runtime == \"openclaw\":\n s, e = export_openclaw_sessions(container, sessions_dir, since_epoch,\n container_dir, redact)\n else:\n s, e = export_copaw_sessions(container, sessions_dir, since_epoch,\n container_dir, redact)\n\n if s == 0:\n container_dir.rmdir()\n print(f\" {container} ({runtime}): no sessions in range, skipped\")\n else:\n total_sessions += s\n total_events += e\n\n return total_sessions, total_events\n\n\n# ---------------------------------------------------------------------------\n# Main\n# ---------------------------------------------------------------------------\n\ndef main():\n parser = argparse.ArgumentParser(\n description=\"Export HiClaw debug logs (Matrix messages + agent sessions)\"\n )\n parser.add_argument(\"--range\", \"-r\", required=True, dest=\"time_range\",\n help=\"Time range to export, e.g. 10m, 1h, 1d\")\n parser.add_argument(\"--container\", \"-c\", default=None,\n help=\"Only export sessions from containers matching this substring\")\n parser.add_argument(\"--room\", default=None,\n help=\"Only export Matrix rooms matching this substring\")\n parser.add_argument(\"--homeserver\", \"-s\", default=\"\",\n help=\"Matrix homeserver URL (auto-detected from env file)\")\n parser.add_argument(\"--token\", \"-t\", default=\"\",\n help=\"Matrix access token (auto-detected from env file)\")\n parser.add_argument(\"--env-file\", default=None,\n help=\"Path to hiclaw-manager.env (default: ~/hiclaw-manager.env)\")\n parser.add_argument(\"--messages-only\", action=\"store_true\",\n help=\"Only export m.room.message events (skip state events)\")\n parser.add_argument(\"--no-redact\", action=\"store_true\",\n help=\"Disable PII redaction\")\n args = parser.parse_args()\n\n range_seconds = parse_range(args.time_range)\n since_epoch = time.time() - range_seconds\n since_human = datetime.fromtimestamp(since_epoch, tz=timezone.utc).isoformat()\n now_str = datetime.now().strftime(\"%Y%m%d-%H%M%S\")\n\n run_dir = Path(\"debug-log\") / now_str\n run_dir.mkdir(parents=True, exist_ok=True)\n redact = not args.no_redact\n\n print(f\"HiClaw Debug Log Export\")\n print(f\" Range: last {args.time_range} (since {since_human})\")\n print(f\" Output: {run_dir.resolve()}\")\n print(f\" PII redaction: {'on' if redact else 'off'}\")\n print()\n\n # --- Matrix messages ---\n print(\"=== Matrix Messages ===\")\n matrix_dir = run_dir / \"matrix-messages\"\n rooms, messages = export_matrix_messages(\n matrix_dir, since_epoch, redact,\n room_filter=args.room, env_file=args.env_file,\n homeserver=args.homeserver, token=args.token,\n messages_only=args.messages_only,\n )\n if rooms == 0 and matrix_dir.exists() and not any(matrix_dir.iterdir()):\n matrix_dir.rmdir()\n print()\n\n # --- Agent sessions ---\n print(\"=== Agent Sessions ===\")\n sessions_dir = run_dir / \"agent-sessions\"\n sessions, events = export_agent_sessions(\n sessions_dir, since_epoch, redact,\n container_filter=args.container,\n )\n if sessions == 0 and sessions_dir.exists() and not any(sessions_dir.iterdir()):\n sessions_dir.rmdir()\n print()\n\n # --- Summary ---\n summary = (\n f\"HiClaw Debug Log\\n\"\n f\"Exported at: {now_str}\\n\"\n f\"Range: last {args.time_range} (since {since_human})\\n\"\n f\"PII redaction: {'on' if redact else 'off'}\\n\"\n f\"\\n\"\n f\"Matrix messages: {messages} messages from {rooms} rooms\\n\"\n f\"Agent sessions: {events} events from {sessions} sessions\\n\"\n )\n (run_dir / \"summary.txt\").write_text(summary)\n\n print(f\"Done. {messages} messages from {rooms} rooms, {events} events from {sessions} sessions\")\n print(f\"Output: {run_dir.resolve()}\")\n\n\nif __name__ == \"__main__\":\n main()\n"}, "1145f796b3de8b9e07c298add26d0d825c48dc39": {"t": 1786068176.1916487, "n": 33260, "h": "611a3c0d9c21b255", "c": "#!/usr/bin/env python3\nfrom __future__ import annotations\n\"\"\"\nExport AgentTeams debug logs: Matrix messages + agent session logs.\n\nUsage:\n # Export last 1 hour (default)\n python scripts/export-debug-log.py --range 1h\n\n # Export last 1 day\n python scripts/export-debug-log.py --range 1d\n\n # Filter by container or room\n python scripts/export-debug-log.py --range 1h --container agentteams-manager --room Worker\n\n # Disable PII redaction\n python scripts/export-debug-log.py --range 1h --no-redact\n\nOutput structure:\n debug-log/20260319-153000/\n ├── summary.txt\n ├── matrix-messages/\n │ └── RoomName_!roomid.jsonl\n ├── agent-sessions/\n ├── agentteams-manager/\n │ └── {session-id}.jsonl\n └── agentteams-worker-xxx/\n └── {session-key}.jsonl\n └── container-logs/\n ├── agentteams-worker-xxx.log\n └── agentteams-worker-xxx.state.json\n\"\"\"\n\nimport argparse\nimport json\nimport os\nimport re\nimport subprocess\nimport sys\nimport time\nimport urllib.parse\nimport urllib.request\nimport urllib.error\nfrom datetime import datetime, timezone\nfrom pathlib import Path\n\n\n# ---------------------------------------------------------------------------\n# PII redaction\n# ---------------------------------------------------------------------------\n\n_PII_PATTERNS: list[tuple[str, re.Pattern]] = [\n (\"ID_CARD\", re.compile(r'\\b[1-9]\\d{5}(?:19|20)\\d{2}(?:0[1-9]|1[0-2])(?:0[1-9]|[12]\\d|3[01])\\d{3}[\\dXx]\\b')),\n (\"PHONE\", re.compile(r'(?<!\\d)(?:\\+?86[-\\s]?)?1[3-9]\\d{9}(?!\\d)')),\n (\"EMAIL\", re.compile(r'\\b[A-Za-z0-9._%+\\-]+@[A-Za-z0-9.\\-]+\\.[A-Za-z]{2,}\\b')),\n (\"BANK_CARD\", re.compile(r'\\b(?:6[0-9]{15,18}|4[0-9]{15}|5[1-5][0-9]{14}|3[47][0-9]{13}|62[0-9]{14,17})\\b')),\n (\"IP\", re.compile(r'\\b(?:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)\\.){3}(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)\\b')),\n (\"ALIYUN_AK\", re.compile(r'\\bLTAI[A-Za-z0-9]{12,30}\\b')),\n (\"ALIYUN_SK\", re.compile(r'(?i)(?:access_?key_?secret|secret_?access_?key)\\s*[:=]\\s*(\\S{20,})')),\n (\"AWS_AK\", re.compile(r'\\b(?:AKIA|ASIA)[A-Z0-9]{16}\\b')),\n (\"OPENAI_KEY\", re.compile(r'\\bsk-[A-Za-z0-9]{20,}\\b')),\n (\"ANTHROPIC_KEY\", re.compile(r'\\bsk-ant-[A-Za-z0-9\\-]{20,}\\b')),\n (\"DASHSCOPE_KEY\", re.compile(r'\\bsk-sp-[A-Za-z0-9]{20,}\\b')),\n (\"DEEPSEEK_KEY\", re.compile(r'\\bsk-[a-f0-9]{32,}\\b')),\n (\"BEARER\", re.compile(r'(?i)(Bearer\\s+)([A-Za-z0-9\\-_.]{20,})')),\n (\"SECRET_KV\", re.compile(\n r'(?i)((?:password|passwd|pwd|secret|token|api_?key|access_?key|secret_?key'\n r'|private_?key|credential|appkey|app_?secret|auth_?token|signing_?key'\n r'|client_?secret|master_?key)\\s*[:=]\\s*)'\n r'(\\S+)'\n )),\n (\"MATRIX_TOKEN\", re.compile(r'\\bsyt_[A-Za-z0-9_\\-]{10,}\\b')),\n (\"HEX_SECRET\", re.compile(r'\\b[A-Fa-f0-9]{32,}\\b')),\n (\"PASSPORT\", re.compile(r'\\b[EeGg]\\d{8}\\b')),\n (\"SSN\", re.compile(r'\\b\\d{3}-\\d{2}-\\d{4}\\b')),\n]\n\n_SECRET_FIELD_PATTERN = re.compile(\n r'(?i)^(?:password|passwd|pwd|secret|token|api[_-]?key'\n r'|access[_-]?key(?:[_-]?secret)?|secret[_-]?access[_-]?key'\n r'|secret[_-]?key|private[_-]?key|credential|app[_-]?key'\n r'|app[_-]?secret|auth[_-]?token|signing[_-]?key'\n r'|client[_-]?secret|master[_-]?key)$'\n)\n\n\ndef redact_pii(text: str) -> str:\n if not text:\n return text\n for name, pattern in _PII_PATTERNS:\n if name in (\"SECRET_KV\", \"ALIYUN_SK\", \"BEARER\"):\n text = pattern.sub(r'\\1****', text)\n else:\n text = pattern.sub('****', text)\n return text\n\n\ndef redact_json_strings(obj):\n if isinstance(obj, str):\n return redact_pii(obj)\n if isinstance(obj, list):\n return [redact_json_strings(v) for v in obj]\n if isinstance(obj, dict):\n redacted = {}\n for key, value in obj.items():\n if isinstance(key, str) and _SECRET_FIELD_PATTERN.fullmatch(key):\n redacted[key] = \"****\"\n else:\n redacted[key] = redact_json_strings(value)\n return redacted\n return obj\n\n\n# ---------------------------------------------------------------------------\n# Common helpers\n# ---------------------------------------------------------------------------\n\ndef parse_range(range_str: str) -> int:\n m = re.fullmatch(r\"(\\d+)\\s*(m|min|h|hr|hour|d|day)s?\", range_str.strip(), re.IGNORECASE)\n if not m:\n raise ValueError(f\"Invalid range format: '{range_str}'. Use e.g. 10m, 1h, 1d\")\n value, unit = int(m.group(1)), m.group(2)[0].lower()\n return value * {\"m\": 60, \"h\": 3600, \"d\": 86400}[unit]\n\n\ndef parse_ts(ts_str: str) -> float:\n if not ts_str:\n return 0\n ts_str = ts_str.replace(\"Z\", \"+00:00\")\n try:\n dt = datetime.fromisoformat(ts_str)\n if dt.tzinfo is None:\n dt = dt.replace(tzinfo=timezone.utc)\n return dt.timestamp()\n except Exception:\n return 0\n\n\ndef sanitize_filename(name: str) -> str:\n return re.sub(r'[^\\w\\-. ]', '_', name).strip()[:80]\n\n\ndef docker_exec(container: str, cmd: str) -> str:\n result = subprocess.run(\n [\"docker\", \"exec\", container, \"sh\", \"-c\", cmd],\n capture_output=True, text=True, timeout=30,\n )\n return result.stdout\n\n\ndef list_agentteams_containers() -> list[str]:\n names: list[str] = []\n for prefix in (\"agentteams-\",):\n result = subprocess.run(\n [\"docker\", \"ps\", \"--format\", \"{{.Names}}\", \"--filter\", f\"name={prefix}\"],\n capture_output=True, text=True, timeout=10,\n )\n names.extend(n.strip() for n in result.stdout.splitlines() if n.strip())\n return list(dict.fromkeys(names))\n\n\n# ---------------------------------------------------------------------------\n# Container diagnostics\n# ---------------------------------------------------------------------------\n\ndef _docker_run(*args: str) -> subprocess.CompletedProcess[str]:\n try:\n return subprocess.run(\n [\"docker\", *args],\n capture_output=True,\n text=True,\n timeout=30,\n )\n except (OSError, subprocess.SubprocessError) as exc:\n return subprocess.CompletedProcess(\n [\"docker\", *args],\n 1,\n \"\",\n f\"{type(exc).__name__}: {exc}\",\n )\n\n\ndef export_container_logs(out_dir: Path, since_epoch: float, redact: bool,\n container_filter: str | None) -> int:\n \"\"\"Export Docker state and logs for all AgentTeams containers.\"\"\"\n result = _docker_run(\"ps\", \"-a\", \"--format\", \"{{.Names}}\", \"--filter\", \"name=agentteams-\")\n containers = [name.strip() for name in result.stdout.splitlines() if name.strip()]\n if container_filter:\n containers = [name for name in containers if container_filter in name]\n if not containers:\n print(\" [containers] No matching AgentTeams containers found\")\n return 0\n\n out_dir.mkdir(parents=True, exist_ok=True)\n since = datetime.fromtimestamp(since_epoch, tz=timezone.utc).isoformat()\n\n for container in containers:\n state_result = _docker_run(\"inspect\", \"--format={{json .State}}\", container)\n image_result = _docker_run(\"inspect\", \"--format={{json .Config.Image}}\", container)\n restart_result = _docker_run(\"inspect\", \"--format={{.RestartCount}}\", container)\n try:\n state = json.loads(state_result.stdout)\n except (json.JSONDecodeError, TypeError):\n state = {\"inspect_error\": state_result.stderr.strip() or state_result.stdout.strip()}\n try:\n image = json.loads(image_result.stdout)\n except (json.JSONDecodeError, TypeError):\n image = image_result.stdout.strip()\n try:\n restart_count = int(restart_result.stdout.strip())\n except ValueError:\n restart_count = None\n\n diagnostic = {\n \"container\": container,\n \"image\": image,\n \"restart_count\": restart_count,\n \"state\": state,\n }\n if redact:\n diagnostic = redact_json_strings(diagnostic)\n filename = sanitize_filename(container)\n (out_dir / f\"{filename}.state.json\").write_text(\n json.dumps(diagnostic, ensure_ascii=False, indent=2) + \"\\n\",\n encoding=\"utf-8\",\n )\n\n logs_result = _docker_run(\"logs\", \"--timestamps\", \"--since\", since, container)\n logs = logs_result.stdout + logs_result.stderr\n if redact:\n logs = redact_pii(logs)\n (out_dir / f\"{filename}.log\").write_text(logs, encoding=\"utf-8\")\n print(f\" {container}: state + {len(logs.splitlines())} log lines\")\n\n return len(containers)\n\n\n# ---------------------------------------------------------------------------\n# Matrix messages export\n# ---------------------------------------------------------------------------\n\ndef load_env_file(path: str) -> dict[str, str]:\n env = {}\n try:\n with open(path) as f:\n for line in f:\n line = line.strip()\n if not line or line.startswith(\"#\"):\n continue\n if \"=\" in line:\n key, _, value = line.partition(\"=\")\n env[key.strip()] = value.strip()\n except FileNotFoundError:\n pass\n return env\n\n\ndef matrix_login(homeserver: str, user: str, password: str) -> str:\n url = f\"{homeserver.rstrip('/')}/_matrix/client/v3/login\"\n payload = json.dumps({\n \"type\": \"m.login.password\",\n \"identifier\": {\"type\": \"m.id.user\", \"user\": user},\n \"password\": password,\n }).encode()\n req = urllib.request.Request(url, data=payload, headers={\"Content-Type\": \"application/json\"})\n with urllib.request.urlopen(req, timeout=15) as resp:\n data = json.loads(resp.read())\n return data[\"access_token\"]\n\n\ndef matrix_api(homeserver: str, token: str, endpoint: str, params: dict | None = None) -> dict:\n url = f\"{homeserver.rstrip('/')}/_matrix/client/v3/{endpoint}\"\n if params:\n url += \"?\" + urllib.parse.urlencode(params)\n req = urllib.request.Request(url, headers={\"Authorization\": f\"Bearer {token}\"})\n try:\n with urllib.request.urlopen(req, timeout=30) as resp:\n return json.loads(resp.read())\n except urllib.error.HTTPError as e:\n body = e.read().decode(errors=\"replace\")\n print(f\" API error {e.code} on {endpoint}: {body}\", file=sys.stderr)\n raise\n\n\ndef fetch_room_messages(homeserver: str, token: str, room_id: str, since_ts: int) -> list[dict]:\n encoded = urllib.parse.quote(room_id)\n messages = []\n from_token = \"\"\n while True:\n params = {\"dir\": \"b\", \"limit\": \"100\"}\n if from_token:\n params[\"from\"] = from_token\n data = matrix_api(homeserver, token, f\"rooms/{encoded}/messages\", params)\n chunk = data.get(\"chunk\", [])\n if not chunk:\n break\n hit_boundary = False\n for event in chunk:\n if event.get(\"origin_server_ts\", 0) < since_ts:\n hit_boundary = True\n break\n messages.append(event)\n if hit_boundary:\n break\n next_token = data.get(\"end\")\n if not next_token or next_token == from_token:\n break\n from_token = next_token\n messages.reverse()\n return messages\n\n\ndef format_event(event: dict, redact: bool) -> dict:\n content = event.get(\"content\", {})\n ts = event.get(\"origin_server_ts\", 0)\n record = {\n \"event_id\": event.get(\"event_id\"),\n \"type\": event.get(\"type\"),\n \"sender\": event.get(\"sender\"),\n \"timestamp\": ts,\n \"time\": datetime.fromtimestamp(ts / 1000, tz=timezone.utc).isoformat(),\n }\n if event.get(\"type\") == \"m.room.message\":\n record[\"msgtype\"] = content.get(\"msgtype\")\n record[\"body\"] = content.get(\"body\")\n if content.get(\"format\"):\n record[\"format\"] = content[\"format\"]\n if content.get(\"url\"):\n record[\"url\"] = content[\"url\"]\n if content.get(\"m.relates_to\"):\n record[\"relates_to\"] = content[\"m.relates_to\"]\n else:\n record[\"content\"] = content\n return redact_json_strings(record) if redact else record\n\n\ndef export_matrix_messages(out_dir: Path, since_epoch: float, redact: bool,\n room_filter: str | None, env_file: str | None,\n homeserver: str, token: str,\n messages_only: bool) -> tuple[int, int]:\n \"\"\"Export Matrix messages. Returns (rooms_exported, message_count).\"\"\"\n env_path = env_file or os.path.expanduser(\"~/agentteams-manager.env\")\n agentteams_env = load_env_file(env_path)\n\n if not homeserver:\n if not agentteams_env:\n print(f\" [matrix] Cannot find {env_path}, skipping Matrix export\")\n return 0, 0\n port = agentteams_env.get(\"AGENTTEAMS_PORT_GATEWAY\", \"18080\")\n homeserver = f\"http://127.0.0.1:{port}\"\n\n if not token:\n # Use Manager token — Manager is in every room (DM, Worker, Project)\n manager_password = agentteams_env.get(\"AGENTTEAMS_MANAGER_PASSWORD\", \"\")\n if manager_password:\n try:\n token = matrix_login(homeserver, \"manager\", manager_password)\n except Exception:\n pass\n\n # Fallback to admin token if Manager login failed\n if not token:\n admin_user = agentteams_env.get(\"AGENTTEAMS_ADMIN_USER\", \"admin\")\n admin_password = agentteams_env.get(\"AGENTTEAMS_ADMIN_PASSWORD\", \"\")\n if not admin_password:\n print(f\" [matrix] No usable credentials found, skipping Matrix export\")\n return 0, 0\n try:\n token = matrix_login(homeserver, admin_user, admin_password)\n except Exception as e:\n print(f\" [matrix] Login failed: {e}, skipping Matrix export\")\n return 0, 0\n\n since_ts = int(since_epoch * 1000)\n out_dir.mkdir(parents=True, exist_ok=True)\n\n try:\n rooms = matrix_api(homeserver, token, \"joined_rooms\").get(\"joined_rooms\", [])\n except Exception as e:\n print(f\" [matrix] Failed to list rooms: {e}\")\n return 0, 0\n\n total_messages = 0\n total_rooms = 0\n\n for room_id in rooms:\n encoded = urllib.parse.quote(room_id)\n try:\n room_name = matrix_api(homeserver, token, f\"rooms/{encoded}/state/m.room.name\").get(\"name\", \"\")\n except Exception:\n room_name = \"\"\n\n if room_filter:\n if room_filter not in room_id and room_filter not in (room_name or \"\"):\n continue\n\n display = f\"{room_name} ({room_id})\" if room_name else room_id\n print(f\" {display} ... \", end=\"\", flush=True)\n\n messages = fetch_room_messages(homeserver, token, room_id, since_ts)\n if messages_only:\n messages = [e for e in messages if e.get(\"type\") == \"m.room.message\"]\n\n if not messages:\n print(\"0 messages, skipped\")\n continue\n\n name_part = sanitize_filename(room_name) if room_name else \"\"\n id_part = sanitize_filename(room_id)\n filename = f\"{name_part}_{id_part}.jsonl\" if name_part else f\"{id_part}.jsonl\"\n\n with open(out_dir / filename, \"w\", encoding=\"utf-8\") as f:\n for event in messages:\n record = format_event(event, redact=redact)\n f.write(json.dumps(record, ensure_ascii=False) + \"\\n\")\n\n print(f\"{len(messages)} messages -> {filename}\")\n total_messages += len(messages)\n total_rooms += 1\n\n return total_rooms, total_messages\n\n\n# ---------------------------------------------------------------------------\n# Agent sessions export (OpenClaw + CoPaw + Hermes)\n# ---------------------------------------------------------------------------\n\ndef detect_runtime(container: str) -> tuple[str, str]:\n \"\"\"Probe known absolute session-dir locations across runtimes & container types.\n\n Returns (runtime_name, absolute_sessions_dir). Empty tuple if none found.\n\n Layout reference (verified against worker/scripts/worker-entrypoint.sh,\n copaw/scripts/copaw-worker-entrypoint.sh, copaw/AGENTS.md and\n tests/lib/agent-metrics.sh):\n\n Manager (any runtime, HOME=/root/manager-workspace):\n openclaw -> /root/manager-workspace/.openclaw/agents/main/sessions\n hermes -> /root/manager-workspace/.hermes/sessions\n copaw -> /root/manager-workspace/.copaw/workspaces/default/sessions\n\n OpenClaw / Hermes Worker (HOME=/root/agentteams-fs/agents/<name>):\n openclaw -> /root/agentteams-fs/agents/<name>/.openclaw/agents/main/sessions\n hermes -> /root/agentteams-fs/agents/<name>/.hermes/sessions\n\n CoPaw Worker (HOME=/root/.agentteams-worker/<name>, also reachable via\n /root/agentteams-fs symlink that points to that same dir):\n copaw -> /root/.agentteams-worker/<name>/.copaw/workspaces/default/sessions\n copaw -> /root/agentteams-fs/.copaw/workspaces/default/sessions (alt)\n \"\"\"\n candidates: list[tuple[str, str]] = [\n (\"openclaw\", \"/root/manager-workspace/.openclaw/agents/main/sessions\"),\n (\"hermes\", \"/root/manager-workspace/.hermes/sessions\"),\n (\"copaw\", \"/root/manager-workspace/.copaw/workspaces/default/sessions\"),\n ]\n\n worker_name = docker_exec(container, \"echo $AGENTTEAMS_WORKER_NAME\").strip()\n if worker_name:\n candidates.extend([\n (\"openclaw\", f\"/root/agentteams-fs/agents/{worker_name}/.openclaw/agents/main/sessions\"),\n (\"hermes\", f\"/root/agentteams-fs/agents/{worker_name}/.hermes/sessions\"),\n (\"copaw\", f\"/root/.agentteams-worker/{worker_name}/.copaw/workspaces/default/sessions\"),\n (\"copaw\", \"/root/agentteams-fs/.copaw/workspaces/default/sessions\"),\n ])\n\n for runtime, path in candidates:\n if docker_exec(container, f\"test -d '{path}' && echo yes || echo no\").strip() == \"yes\":\n return runtime, path\n\n # Last-resort: scan from / for any known session dir layout.\n found = docker_exec(\n container,\n \"find / -maxdepth 7 \\\\( \"\n \"-path '*/.openclaw/agents/main/sessions' \"\n \"-o -path '*/.hermes/sessions' \"\n \"-o -path '*/.copaw/workspaces/default/sessions' \"\n \"\\\\) -type d 2>/dev/null | head -1\",\n ).strip()\n if found:\n if \"/.openclaw/\" in found:\n return \"openclaw\", found\n if \"/.hermes/\" in found:\n return \"hermes\", found\n return \"copaw\", found\n\n return \"\", \"\"\n\n\ndef export_openclaw_sessions(container: str, sessions_dir: str, since_epoch: float,\n out_dir: Path, redact: bool) -> tuple[int, int]:\n ls_output = docker_exec(container, f\"ls '{sessions_dir}'/*.jsonl 2>/dev/null\").strip()\n if not ls_output:\n return 0, 0\n\n total_sessions = 0\n total_events = 0\n\n for session_path in [f.strip() for f in ls_output.splitlines() if f.strip()]:\n filename = os.path.basename(session_path)\n\n first_line = docker_exec(container, f\"head -1 '{session_path}'\").strip()\n if not first_line:\n continue\n try:\n header = json.loads(first_line)\n except json.JSONDecodeError:\n continue\n\n last_line = docker_exec(container, f\"tail -1 '{session_path}'\").strip()\n try:\n last_ts = parse_ts(json.loads(last_line).get(\"timestamp\", \"\"))\n except Exception:\n last_ts = parse_ts(header.get(\"timestamp\", \"\"))\n\n if last_ts < since_epoch and last_ts > 0:\n continue\n\n raw = docker_exec(container, f\"cat '{session_path}'\")\n if not raw.strip():\n continue\n\n output_lines = []\n for line in raw.splitlines():\n line = line.strip()\n if not line:\n continue\n try:\n event = json.loads(line)\n except json.JSONDecodeError:\n continue\n event_ts = parse_ts(event.get(\"timestamp\", \"\"))\n if event.get(\"type\") != \"session\" and event_ts < since_epoch and event_ts > 0:\n continue\n if redact:\n event = redact_json_strings(event)\n output_lines.append(json.dumps(event, ensure_ascii=False))\n\n if len(output_lines) <= 1:\n continue\n\n with open(out_dir / filename, \"w\", encoding=\"utf-8\") as f:\n f.write(\"\\n\".join(output_lines) + \"\\n\")\n\n event_count = len(output_lines) - 1\n print(f\" {container}/{filename} (openclaw): {event_count} events\")\n total_sessions += 1\n total_events += event_count\n\n # sessions.json index\n try:\n raw = docker_exec(container, f\"cat '{sessions_dir}/sessions.json'\")\n if raw.strip():\n index = json.loads(raw)\n if redact:\n index = redact_json_strings(index)\n with open(out_dir / \"sessions.json\", \"w\", encoding=\"utf-8\") as f:\n json.dump(index, f, ensure_ascii=False, indent=2)\n except Exception:\n pass\n\n return total_sessions, total_events\n\n\ndef export_copaw_sessions(container: str, sessions_dir: str, since_epoch: float,\n out_dir: Path, redact: bool) -> tuple[int, int]:\n ls_output = docker_exec(container, f\"find '{sessions_dir}' -name '*.json' -type f 2>/dev/null\").strip()\n if not ls_output:\n return 0, 0\n\n total_sessions = 0\n total_events = 0\n\n for session_path in [f.strip() for f in ls_output.splitlines() if f.strip()]:\n raw = docker_exec(container, f\"cat '{session_path}'\")\n if not raw.strip():\n continue\n try:\n data = json.loads(raw)\n except json.JSONDecodeError:\n continue\n\n agent = data.get(\"agent\", {})\n memory = agent.get(\"memory\", {})\n content = memory.get(\"content\", [])\n if not content:\n continue\n\n basename = os.path.basename(session_path).replace(\".json\", \"\")\n header = {\n \"type\": \"session\",\n \"runtime\": \"copaw\",\n \"agent_name\": agent.get(\"name\", \"\"),\n \"session_key\": basename,\n \"compressed_summary\": memory.get(\"_compressed_summary\", \"\"),\n }\n\n messages_in_range = []\n for turn_idx, turn in enumerate(content):\n if not isinstance(turn, list):\n continue\n for msg in turn:\n if not isinstance(msg, dict):\n continue\n msg_ts = parse_ts(msg.get(\"timestamp\", \"\"))\n if msg_ts >= since_epoch or msg_ts == 0:\n event = {\n \"type\": \"message\",\n \"turn\": turn_idx,\n \"id\": msg.get(\"id\", \"\"),\n \"role\": msg.get(\"role\", \"\"),\n \"name\": msg.get(\"name\", \"\"),\n \"timestamp\": msg.get(\"timestamp\", \"\"),\n \"content\": msg.get(\"content\", []),\n }\n if msg.get(\"metadata\"):\n event[\"metadata\"] = msg[\"metadata\"]\n messages_in_range.append(event)\n\n if not messages_in_range:\n continue\n\n output_lines = []\n if redact:\n header = redact_json_strings(header)\n output_lines.append(json.dumps(header, ensure_ascii=False))\n for event in messages_in_range:\n if redact:\n event = redact_json_strings(event)\n output_lines.append(json.dumps(event, ensure_ascii=False))\n\n out_filename = basename + \".jsonl\"\n with open(out_dir / out_filename, \"w\", encoding=\"utf-8\") as f:\n f.write(\"\\n\".join(output_lines) + \"\\n\")\n\n event_count = len(output_lines) - 1\n print(f\" {container}/{out_filename} (copaw): {event_count} events\")\n total_sessions += 1\n total_events += event_count\n\n return total_sessions, total_events\n\n\ndef export_hermes_sessions(container: str, sessions_dir: str, since_epoch: float,\n out_dir: Path, redact: bool) -> tuple[int, int]:\n ls_output = docker_exec(container, f\"ls '{sessions_dir}'/*.jsonl 2>/dev/null\").strip()\n if not ls_output:\n return 0, 0\n\n total_sessions = 0\n total_events = 0\n\n for session_path in [f.strip() for f in ls_output.splitlines() if f.strip()]:\n raw = docker_exec(container, f\"cat '{session_path}'\")\n if not raw.strip():\n continue\n\n output_lines = []\n last_ts = 0.0\n for line in raw.splitlines():\n line = line.strip()\n if not line:\n continue\n try:\n event = json.loads(line)\n except json.JSONDecodeError:\n continue\n\n event_ts = parse_ts(event.get(\"timestamp\", \"\"))\n if event_ts:\n last_ts = max(last_ts, event_ts)\n\n role = event.get(\"role\", \"\")\n if role != \"session_meta\" and event_ts < since_epoch and event_ts > 0:\n continue\n\n if redact:\n event = redact_json_strings(event)\n output_lines.append(json.dumps(event, ensure_ascii=False))\n\n if not output_lines:\n continue\n\n if last_ts < since_epoch and last_ts > 0:\n continue\n\n filename = os.path.basename(session_path)\n with open(out_dir / filename, \"w\", encoding=\"utf-8\") as f:\n f.write(\"\\n\".join(output_lines) + \"\\n\")\n\n event_count = len(output_lines) - 1 if output_lines and '\"role\": \"session_meta\"' in output_lines[0] else len(output_lines)\n print(f\" {container}/{filename} (hermes): {event_count} events\")\n total_sessions += 1\n total_events += max(event_count, 0)\n\n hermes_home = str(Path(sessions_dir).parent)\n state_db = f\"{hermes_home}/state.db\"\n has_state_db = docker_exec(container, f\"test -f '{state_db}' && echo yes || echo no\").strip() == \"yes\"\n if has_state_db:\n raw = docker_exec(container, f\"python3 - <<'PY'\\nimport json, sqlite3\\nconn = sqlite3.connect('{state_db}')\\nconn.row_factory = sqlite3.Row\\nrows = conn.execute('SELECT * FROM sessions ORDER BY started_at DESC LIMIT 200').fetchall()\\nprint(json.dumps([dict(r) for r in rows], ensure_ascii=False, indent=2))\\nPY\")\n if raw.strip():\n try:\n data = json.loads(raw)\n if redact:\n data = redact_json_strings(data)\n with open(out_dir / \"sessions-db.json\", \"w\", encoding=\"utf-8\") as f:\n json.dump(data, f, ensure_ascii=False, indent=2)\n except json.JSONDecodeError:\n pass\n\n logs_dir = f\"{hermes_home}/logs\"\n has_logs_dir = docker_exec(container, f\"test -d '{logs_dir}' && echo yes || echo no\").strip() == \"yes\"\n if has_logs_dir:\n for log_name in (\"agent.log\", \"errors.log\", \"gateway.log\"):\n raw = docker_exec(container, f\"test -f '{logs_dir}/{log_name}' && cat '{logs_dir}/{log_name}' || true\")\n if raw.strip():\n content = redact_pii(raw) if redact else raw\n (out_dir / log_name).write_text(content, encoding=\"utf-8\")\n\n return total_sessions, total_events\n\n\ndef export_agent_sessions(out_dir: Path, since_epoch: float, redact: bool,\n container_filter: str | None) -> tuple[int, int]:\n \"\"\"Export agent sessions from all containers. Returns (session_count, event_count).\"\"\"\n containers = list_agentteams_containers()\n if container_filter:\n containers = [c for c in containers if container_filter in c]\n\n if not containers:\n print(\" [sessions] No matching AgentTeams containers found\")\n return 0, 0\n\n total_sessions = 0\n total_events = 0\n\n for container in containers:\n runtime, sessions_dir = detect_runtime(container)\n if not runtime:\n print(f\" {container}: no sessions directory, skipped\")\n continue\n\n container_dir = out_dir / container\n container_dir.mkdir(parents=True, exist_ok=True)\n\n if runtime == \"openclaw\":\n s, e = export_openclaw_sessions(container, sessions_dir, since_epoch,\n container_dir, redact)\n elif runtime == \"hermes\":\n s, e = export_hermes_sessions(container, sessions_dir, since_epoch,\n container_dir, redact)\n else:\n s, e = export_copaw_sessions(container, sessions_dir, since_epoch,\n container_dir, redact)\n\n if s == 0:\n if not any(container_dir.iterdir()):\n container_dir.rmdir()\n print(f\" {container} ({runtime}): no sessions in range, skipped\")\n else:\n print(f\" {container} ({runtime}): exported auxiliary debug artifacts without session events\")\n else:\n total_sessions += s\n total_events += e\n\n return total_sessions, total_events\n\n\n# ---------------------------------------------------------------------------\n# Main\n# ---------------------------------------------------------------------------\n\ndef main():\n parser = argparse.ArgumentParser(\n description=\"Export AgentTeams debug logs (Matrix messages + agent sessions)\"\n )\n parser.add_argument(\"--range\", \"-r\", required=True, dest=\"time_range\",\n help=\"Time range to export, e.g. 10m, 1h, 1d\")\n parser.add_argument(\"--container\", \"-c\", default=None,\n help=\"Only export sessions from containers matching this substring\")\n parser.add_argument(\"--room\", default=None,\n help=\"Only export Matrix rooms matching this substring\")\n parser.add_argument(\"--homeserver\", \"-s\", default=\"\",\n help=\"Matrix homeserver URL (auto-detected from env file)\")\n parser.add_argument(\"--token\", \"-t\", default=\"\",\n help=\"Matrix access token (auto-detected from env file)\")\n parser.add_argument(\"--env-file\", default=None,\n help=\"Path to agentteams-manager.env (default: ~/agentteams-manager.env)\")\n parser.add_argument(\"--messages-only\", action=\"store_true\",\n help=\"Only export m.room.message events (skip state events)\")\n parser.add_argument(\"--no-redact\", action=\"store_true\",\n help=\"Disable PII redaction\")\n args = parser.parse_args()\n\n range_seconds = parse_range(args.time_range)\n since_epoch = time.time() - range_seconds\n since_human = datetime.fromtimestamp(since_epoch, tz=timezone.utc).isoformat()\n now_str = datetime.now().strftime(\"%Y%m%d-%H%M%S\")\n\n run_dir = Path(\"debug-log\") / now_str\n run_dir.mkdir(parents=True, exist_ok=True)\n redact = not args.no_redact\n\n print(f\"AgentTeams Debug Log Export\")\n print(f\" Range: last {args.time_range} (since {since_human})\")\n print(f\" Output: {run_dir.resolve()}\")\n print(f\" PII redaction: {'on' if redact else 'off'}\")\n print()\n\n # --- Matrix messages ---\n print(\"=== Matrix Messages ===\")\n matrix_dir = run_dir / \"matrix-messages\"\n rooms, messages = export_matrix_messages(\n matrix_dir, since_epoch, redact,\n room_filter=args.room, env_file=args.env_file,\n homeserver=args.homeserver, token=args.token,\n messages_only=args.messages_only,\n )\n if rooms == 0 and matrix_dir.exists() and not any(matrix_dir.iterdir()):\n matrix_dir.rmdir()\n print()\n\n # --- Agent sessions ---\n print(\"=== Agent Sessions ===\")\n sessions_dir = run_dir / \"agent-sessions\"\n sessions, events = export_agent_sessions(\n sessions_dir, since_epoch, redact,\n container_filter=args.container,\n )\n if sessions == 0 and sessions_dir.exists() and not any(sessions_dir.iterdir()):\n sessions_dir.rmdir()\n print()\n\n # --- Container diagnostics ---\n print(\"=== Container Diagnostics ===\")\n container_logs_dir = run_dir / \"container-logs\"\n containers = export_container_logs(\n container_logs_dir, since_epoch, redact,\n container_filter=args.container,\n )\n if containers == 0 and container_logs_dir.exists() and not any(container_logs_dir.iterdir()):\n container_logs_dir.rmdir()\n print()\n\n # --- Summary ---\n summary = (\n f\"AgentTeams Debug Log\\n\"\n f\"Exported at: {now_str}\\n\"\n f\"Range: last {args.time_range} (since {since_human})\\n\"\n f\"PII redaction: {'on' if redact else 'off'}\\n\"\n f\"\\n\"\n f\"Matrix messages: {messages} messages from {rooms} rooms\\n\"\n f\"Agent sessions: {events} events from {sessions} sessions\\n\"\n f\"Container diagnostics: {containers} containers\\n\"\n )\n (run_dir / \"summary.txt\").write_text(summary)\n\n print(\n f\"Done. {messages} messages from {rooms} rooms, \"\n f\"{events} events from {sessions} sessions, \"\n f\"{containers} container diagnostics\"\n )\n print(f\"Output: {run_dir.resolve()}\")\n\n\nif __name__ == \"__main__\":\n main()\n"}, "cf9bc5ea7e37e5d96dddbaf5cd7f2c6b4ea40207": {"t": 1786068621.531959, "n": 33260, "h": "611a3c0d9c21b255", "c": "#!/usr/bin/env python3\nfrom __future__ import annotations\n\"\"\"\nExport AgentTeams debug logs: Matrix messages + agent session logs.\n\nUsage:\n # Export last 1 hour (default)\n python scripts/export-debug-log.py --range 1h\n\n # Export last 1 day\n python scripts/export-debug-log.py --range 1d\n\n # Filter by container or room\n python scripts/export-debug-log.py --range 1h --container agentteams-manager --room Worker\n\n # Disable PII redaction\n python scripts/export-debug-log.py --range 1h --no-redact\n\nOutput structure:\n debug-log/20260319-153000/\n ├── summary.txt\n ├── matrix-messages/\n │ └── RoomName_!roomid.jsonl\n ├── agent-sessions/\n ├── agentteams-manager/\n │ └── {session-id}.jsonl\n └── agentteams-worker-xxx/\n └── {session-key}.jsonl\n └── container-logs/\n ├── agentteams-worker-xxx.log\n └── agentteams-worker-xxx.state.json\n\"\"\"\n\nimport argparse\nimport json\nimport os\nimport re\nimport subprocess\nimport sys\nimport time\nimport urllib.parse\nimport urllib.request\nimport urllib.error\nfrom datetime import datetime, timezone\nfrom pathlib import Path\n\n\n# ---------------------------------------------------------------------------\n# PII redaction\n# ---------------------------------------------------------------------------\n\n_PII_PATTERNS: list[tuple[str, re.Pattern]] = [\n (\"ID_CARD\", re.compile(r'\\b[1-9]\\d{5}(?:19|20)\\d{2}(?:0[1-9]|1[0-2])(?:0[1-9]|[12]\\d|3[01])\\d{3}[\\dXx]\\b')),\n (\"PHONE\", re.compile(r'(?<!\\d)(?:\\+?86[-\\s]?)?1[3-9]\\d{9}(?!\\d)')),\n (\"EMAIL\", re.compile(r'\\b[A-Za-z0-9._%+\\-]+@[A-Za-z0-9.\\-]+\\.[A-Za-z]{2,}\\b')),\n (\"BANK_CARD\", re.compile(r'\\b(?:6[0-9]{15,18}|4[0-9]{15}|5[1-5][0-9]{14}|3[47][0-9]{13}|62[0-9]{14,17})\\b')),\n (\"IP\", re.compile(r'\\b(?:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)\\.){3}(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)\\b')),\n (\"ALIYUN_AK\", re.compile(r'\\bLTAI[A-Za-z0-9]{12,30}\\b')),\n (\"ALIYUN_SK\", re.compile(r'(?i)(?:access_?key_?secret|secret_?access_?key)\\s*[:=]\\s*(\\S{20,})')),\n (\"AWS_AK\", re.compile(r'\\b(?:AKIA|ASIA)[A-Z0-9]{16}\\b')),\n (\"OPENAI_KEY\", re.compile(r'\\bsk-[A-Za-z0-9]{20,}\\b')),\n (\"ANTHROPIC_KEY\", re.compile(r'\\bsk-ant-[A-Za-z0-9\\-]{20,}\\b')),\n (\"DASHSCOPE_KEY\", re.compile(r'\\bsk-sp-[A-Za-z0-9]{20,}\\b')),\n (\"DEEPSEEK_KEY\", re.compile(r'\\bsk-[a-f0-9]{32,}\\b')),\n (\"BEARER\", re.compile(r'(?i)(Bearer\\s+)([A-Za-z0-9\\-_.]{20,})')),\n (\"SECRET_KV\", re.compile(\n r'(?i)((?:password|passwd|pwd|secret|token|api_?key|access_?key|secret_?key'\n r'|private_?key|credential|appkey|app_?secret|auth_?token|signing_?key'\n r'|client_?secret|master_?key)\\s*[:=]\\s*)'\n r'(\\S+)'\n )),\n (\"MATRIX_TOKEN\", re.compile(r'\\bsyt_[A-Za-z0-9_\\-]{10,}\\b')),\n (\"HEX_SECRET\", re.compile(r'\\b[A-Fa-f0-9]{32,}\\b')),\n (\"PASSPORT\", re.compile(r'\\b[EeGg]\\d{8}\\b')),\n (\"SSN\", re.compile(r'\\b\\d{3}-\\d{2}-\\d{4}\\b')),\n]\n\n_SECRET_FIELD_PATTERN = re.compile(\n r'(?i)^(?:password|passwd|pwd|secret|token|api[_-]?key'\n r'|access[_-]?key(?:[_-]?secret)?|secret[_-]?access[_-]?key'\n r'|secret[_-]?key|private[_-]?key|credential|app[_-]?key'\n r'|app[_-]?secret|auth[_-]?token|signing[_-]?key'\n r'|client[_-]?secret|master[_-]?key)$'\n)\n\n\ndef redact_pii(text: str) -> str:\n if not text:\n return text\n for name, pattern in _PII_PATTERNS:\n if name in (\"SECRET_KV\", \"ALIYUN_SK\", \"BEARER\"):\n text = pattern.sub(r'\\1****', text)\n else:\n text = pattern.sub('****', text)\n return text\n\n\ndef redact_json_strings(obj):\n if isinstance(obj, str):\n return redact_pii(obj)\n if isinstance(obj, list):\n return [redact_json_strings(v) for v in obj]\n if isinstance(obj, dict):\n redacted = {}\n for key, value in obj.items():\n if isinstance(key, str) and _SECRET_FIELD_PATTERN.fullmatch(key):\n redacted[key] = \"****\"\n else:\n redacted[key] = redact_json_strings(value)\n return redacted\n return obj\n\n\n# ---------------------------------------------------------------------------\n# Common helpers\n# ---------------------------------------------------------------------------\n\ndef parse_range(range_str: str) -> int:\n m = re.fullmatch(r\"(\\d+)\\s*(m|min|h|hr|hour|d|day)s?\", range_str.strip(), re.IGNORECASE)\n if not m:\n raise ValueError(f\"Invalid range format: '{range_str}'. Use e.g. 10m, 1h, 1d\")\n value, unit = int(m.group(1)), m.group(2)[0].lower()\n return value * {\"m\": 60, \"h\": 3600, \"d\": 86400}[unit]\n\n\ndef parse_ts(ts_str: str) -> float:\n if not ts_str:\n return 0\n ts_str = ts_str.replace(\"Z\", \"+00:00\")\n try:\n dt = datetime.fromisoformat(ts_str)\n if dt.tzinfo is None:\n dt = dt.replace(tzinfo=timezone.utc)\n return dt.timestamp()\n except Exception:\n return 0\n\n\ndef sanitize_filename(name: str) -> str:\n return re.sub(r'[^\\w\\-. ]', '_', name).strip()[:80]\n\n\ndef docker_exec(container: str, cmd: str) -> str:\n result = subprocess.run(\n [\"docker\", \"exec\", container, \"sh\", \"-c\", cmd],\n capture_output=True, text=True, timeout=30,\n )\n return result.stdout\n\n\ndef list_agentteams_containers() -> list[str]:\n names: list[str] = []\n for prefix in (\"agentteams-\",):\n result = subprocess.run(\n [\"docker\", \"ps\", \"--format\", \"{{.Names}}\", \"--filter\", f\"name={prefix}\"],\n capture_output=True, text=True, timeout=10,\n )\n names.extend(n.strip() for n in result.stdout.splitlines() if n.strip())\n return list(dict.fromkeys(names))\n\n\n# ---------------------------------------------------------------------------\n# Container diagnostics\n# ---------------------------------------------------------------------------\n\ndef _docker_run(*args: str) -> subprocess.CompletedProcess[str]:\n try:\n return subprocess.run(\n [\"docker\", *args],\n capture_output=True,\n text=True,\n timeout=30,\n )\n except (OSError, subprocess.SubprocessError) as exc:\n return subprocess.CompletedProcess(\n [\"docker\", *args],\n 1,\n \"\",\n f\"{type(exc).__name__}: {exc}\",\n )\n\n\ndef export_container_logs(out_dir: Path, since_epoch: float, redact: bool,\n container_filter: str | None) -> int:\n \"\"\"Export Docker state and logs for all AgentTeams containers.\"\"\"\n result = _docker_run(\"ps\", \"-a\", \"--format\", \"{{.Names}}\", \"--filter\", \"name=agentteams-\")\n containers = [name.strip() for name in result.stdout.splitlines() if name.strip()]\n if container_filter:\n containers = [name for name in containers if container_filter in name]\n if not containers:\n print(\" [containers] No matching AgentTeams containers found\")\n return 0\n\n out_dir.mkdir(parents=True, exist_ok=True)\n since = datetime.fromtimestamp(since_epoch, tz=timezone.utc).isoformat()\n\n for container in containers:\n state_result = _docker_run(\"inspect\", \"--format={{json .State}}\", container)\n image_result = _docker_run(\"inspect\", \"--format={{json .Config.Image}}\", container)\n restart_result = _docker_run(\"inspect\", \"--format={{.RestartCount}}\", container)\n try:\n state = json.loads(state_result.stdout)\n except (json.JSONDecodeError, TypeError):\n state = {\"inspect_error\": state_result.stderr.strip() or state_result.stdout.strip()}\n try:\n image = json.loads(image_result.stdout)\n except (json.JSONDecodeError, TypeError):\n image = image_result.stdout.strip()\n try:\n restart_count = int(restart_result.stdout.strip())\n except ValueError:\n restart_count = None\n\n diagnostic = {\n \"container\": container,\n \"image\": image,\n \"restart_count\": restart_count,\n \"state\": state,\n }\n if redact:\n diagnostic = redact_json_strings(diagnostic)\n filename = sanitize_filename(container)\n (out_dir / f\"{filename}.state.json\").write_text(\n json.dumps(diagnostic, ensure_ascii=False, indent=2) + \"\\n\",\n encoding=\"utf-8\",\n )\n\n logs_result = _docker_run(\"logs\", \"--timestamps\", \"--since\", since, container)\n logs = logs_result.stdout + logs_result.stderr\n if redact:\n logs = redact_pii(logs)\n (out_dir / f\"{filename}.log\").write_text(logs, encoding=\"utf-8\")\n print(f\" {container}: state + {len(logs.splitlines())} log lines\")\n\n return len(containers)\n\n\n# ---------------------------------------------------------------------------\n# Matrix messages export\n# ---------------------------------------------------------------------------\n\ndef load_env_file(path: str) -> dict[str, str]:\n env = {}\n try:\n with open(path) as f:\n for line in f:\n line = line.strip()\n if not line or line.startswith(\"#\"):\n continue\n if \"=\" in line:\n key, _, value = line.partition(\"=\")\n env[key.strip()] = value.strip()\n except FileNotFoundError:\n pass\n return env\n\n\ndef matrix_login(homeserver: str, user: str, password: str) -> str:\n url = f\"{homeserver.rstrip('/')}/_matrix/client/v3/login\"\n payload = json.dumps({\n \"type\": \"m.login.password\",\n \"identifier\": {\"type\": \"m.id.user\", \"user\": user},\n \"password\": password,\n }).encode()\n req = urllib.request.Request(url, data=payload, headers={\"Content-Type\": \"application/json\"})\n with urllib.request.urlopen(req, timeout=15) as resp:\n data = json.loads(resp.read())\n return data[\"access_token\"]\n\n\ndef matrix_api(homeserver: str, token: str, endpoint: str, params: dict | None = None) -> dict:\n url = f\"{homeserver.rstrip('/')}/_matrix/client/v3/{endpoint}\"\n if params:\n url += \"?\" + urllib.parse.urlencode(params)\n req = urllib.request.Request(url, headers={\"Authorization\": f\"Bearer {token}\"})\n try:\n with urllib.request.urlopen(req, timeout=30) as resp:\n return json.loads(resp.read())\n except urllib.error.HTTPError as e:\n body = e.read().decode(errors=\"replace\")\n print(f\" API error {e.code} on {endpoint}: {body}\", file=sys.stderr)\n raise\n\n\ndef fetch_room_messages(homeserver: str, token: str, room_id: str, since_ts: int) -> list[dict]:\n encoded = urllib.parse.quote(room_id)\n messages = []\n from_token = \"\"\n while True:\n params = {\"dir\": \"b\", \"limit\": \"100\"}\n if from_token:\n params[\"from\"] = from_token\n data = matrix_api(homeserver, token, f\"rooms/{encoded}/messages\", params)\n chunk = data.get(\"chunk\", [])\n if not chunk:\n break\n hit_boundary = False\n for event in chunk:\n if event.get(\"origin_server_ts\", 0) < since_ts:\n hit_boundary = True\n break\n messages.append(event)\n if hit_boundary:\n break\n next_token = data.get(\"end\")\n if not next_token or next_token == from_token:\n break\n from_token = next_token\n messages.reverse()\n return messages\n\n\ndef format_event(event: dict, redact: bool) -> dict:\n content = event.get(\"content\", {})\n ts = event.get(\"origin_server_ts\", 0)\n record = {\n \"event_id\": event.get(\"event_id\"),\n \"type\": event.get(\"type\"),\n \"sender\": event.get(\"sender\"),\n \"timestamp\": ts,\n \"time\": datetime.fromtimestamp(ts / 1000, tz=timezone.utc).isoformat(),\n }\n if event.get(\"type\") == \"m.room.message\":\n record[\"msgtype\"] = content.get(\"msgtype\")\n record[\"body\"] = content.get(\"body\")\n if content.get(\"format\"):\n record[\"format\"] = content[\"format\"]\n if content.get(\"url\"):\n record[\"url\"] = content[\"url\"]\n if content.get(\"m.relates_to\"):\n record[\"relates_to\"] = content[\"m.relates_to\"]\n else:\n record[\"content\"] = content\n return redact_json_strings(record) if redact else record\n\n\ndef export_matrix_messages(out_dir: Path, since_epoch: float, redact: bool,\n room_filter: str | None, env_file: str | None,\n homeserver: str, token: str,\n messages_only: bool) -> tuple[int, int]:\n \"\"\"Export Matrix messages. Returns (rooms_exported, message_count).\"\"\"\n env_path = env_file or os.path.expanduser(\"~/agentteams-manager.env\")\n agentteams_env = load_env_file(env_path)\n\n if not homeserver:\n if not agentteams_env:\n print(f\" [matrix] Cannot find {env_path}, skipping Matrix export\")\n return 0, 0\n port = agentteams_env.get(\"AGENTTEAMS_PORT_GATEWAY\", \"18080\")\n homeserver = f\"http://127.0.0.1:{port}\"\n\n if not token:\n # Use Manager token — Manager is in every room (DM, Worker, Project)\n manager_password = agentteams_env.get(\"AGENTTEAMS_MANAGER_PASSWORD\", \"\")\n if manager_password:\n try:\n token = matrix_login(homeserver, \"manager\", manager_password)\n except Exception:\n pass\n\n # Fallback to admin token if Manager login failed\n if not token:\n admin_user = agentteams_env.get(\"AGENTTEAMS_ADMIN_USER\", \"admin\")\n admin_password = agentteams_env.get(\"AGENTTEAMS_ADMIN_PASSWORD\", \"\")\n if not admin_password:\n print(f\" [matrix] No usable credentials found, skipping Matrix export\")\n return 0, 0\n try:\n token = matrix_login(homeserver, admin_user, admin_password)\n except Exception as e:\n print(f\" [matrix] Login failed: {e}, skipping Matrix export\")\n return 0, 0\n\n since_ts = int(since_epoch * 1000)\n out_dir.mkdir(parents=True, exist_ok=True)\n\n try:\n rooms = matrix_api(homeserver, token, \"joined_rooms\").get(\"joined_rooms\", [])\n except Exception as e:\n print(f\" [matrix] Failed to list rooms: {e}\")\n return 0, 0\n\n total_messages = 0\n total_rooms = 0\n\n for room_id in rooms:\n encoded = urllib.parse.quote(room_id)\n try:\n room_name = matrix_api(homeserver, token, f\"rooms/{encoded}/state/m.room.name\").get(\"name\", \"\")\n except Exception:\n room_name = \"\"\n\n if room_filter:\n if room_filter not in room_id and room_filter not in (room_name or \"\"):\n continue\n\n display = f\"{room_name} ({room_id})\" if room_name else room_id\n print(f\" {display} ... \", end=\"\", flush=True)\n\n messages = fetch_room_messages(homeserver, token, room_id, since_ts)\n if messages_only:\n messages = [e for e in messages if e.get(\"type\") == \"m.room.message\"]\n\n if not messages:\n print(\"0 messages, skipped\")\n continue\n\n name_part = sanitize_filename(room_name) if room_name else \"\"\n id_part = sanitize_filename(room_id)\n filename = f\"{name_part}_{id_part}.jsonl\" if name_part else f\"{id_part}.jsonl\"\n\n with open(out_dir / filename, \"w\", encoding=\"utf-8\") as f:\n for event in messages:\n record = format_event(event, redact=redact)\n f.write(json.dumps(record, ensure_ascii=False) + \"\\n\")\n\n print(f\"{len(messages)} messages -> {filename}\")\n total_messages += len(messages)\n total_rooms += 1\n\n return total_rooms, total_messages\n\n\n# ---------------------------------------------------------------------------\n# Agent sessions export (OpenClaw + CoPaw + Hermes)\n# ---------------------------------------------------------------------------\n\ndef detect_runtime(container: str) -> tuple[str, str]:\n \"\"\"Probe known absolute session-dir locations across runtimes & container types.\n\n Returns (runtime_name, absolute_sessions_dir). Empty tuple if none found.\n\n Layout reference (verified against worker/scripts/worker-entrypoint.sh,\n copaw/scripts/copaw-worker-entrypoint.sh, copaw/AGENTS.md and\n tests/lib/agent-metrics.sh):\n\n Manager (any runtime, HOME=/root/manager-workspace):\n openclaw -> /root/manager-workspace/.openclaw/agents/main/sessions\n hermes -> /root/manager-workspace/.hermes/sessions\n copaw -> /root/manager-workspace/.copaw/workspaces/default/sessions\n\n OpenClaw / Hermes Worker (HOME=/root/agentteams-fs/agents/<name>):\n openclaw -> /root/agentteams-fs/agents/<name>/.openclaw/agents/main/sessions\n hermes -> /root/agentteams-fs/agents/<name>/.hermes/sessions\n\n CoPaw Worker (HOME=/root/.agentteams-worker/<name>, also reachable via\n /root/agentteams-fs symlink that points to that same dir):\n copaw -> /root/.agentteams-worker/<name>/.copaw/workspaces/default/sessions\n copaw -> /root/agentteams-fs/.copaw/workspaces/default/sessions (alt)\n \"\"\"\n candidates: list[tuple[str, str]] = [\n (\"openclaw\", \"/root/manager-workspace/.openclaw/agents/main/sessions\"),\n (\"hermes\", \"/root/manager-workspace/.hermes/sessions\"),\n (\"copaw\", \"/root/manager-workspace/.copaw/workspaces/default/sessions\"),\n ]\n\n worker_name = docker_exec(container, \"echo $AGENTTEAMS_WORKER_NAME\").strip()\n if worker_name:\n candidates.extend([\n (\"openclaw\", f\"/root/agentteams-fs/agents/{worker_name}/.openclaw/agents/main/sessions\"),\n (\"hermes\", f\"/root/agentteams-fs/agents/{worker_name}/.hermes/sessions\"),\n (\"copaw\", f\"/root/.agentteams-worker/{worker_name}/.copaw/workspaces/default/sessions\"),\n (\"copaw\", \"/root/agentteams-fs/.copaw/workspaces/default/sessions\"),\n ])\n\n for runtime, path in candidates:\n if docker_exec(container, f\"test -d '{path}' && echo yes || echo no\").strip() == \"yes\":\n return runtime, path\n\n # Last-resort: scan from / for any known session dir layout.\n found = docker_exec(\n container,\n \"find / -maxdepth 7 \\\\( \"\n \"-path '*/.openclaw/agents/main/sessions' \"\n \"-o -path '*/.hermes/sessions' \"\n \"-o -path '*/.copaw/workspaces/default/sessions' \"\n \"\\\\) -type d 2>/dev/null | head -1\",\n ).strip()\n if found:\n if \"/.openclaw/\" in found:\n return \"openclaw\", found\n if \"/.hermes/\" in found:\n return \"hermes\", found\n return \"copaw\", found\n\n return \"\", \"\"\n\n\ndef export_openclaw_sessions(container: str, sessions_dir: str, since_epoch: float,\n out_dir: Path, redact: bool) -> tuple[int, int]:\n ls_output = docker_exec(container, f\"ls '{sessions_dir}'/*.jsonl 2>/dev/null\").strip()\n if not ls_output:\n return 0, 0\n\n total_sessions = 0\n total_events = 0\n\n for session_path in [f.strip() for f in ls_output.splitlines() if f.strip()]:\n filename = os.path.basename(session_path)\n\n first_line = docker_exec(container, f\"head -1 '{session_path}'\").strip()\n if not first_line:\n continue\n try:\n header = json.loads(first_line)\n except json.JSONDecodeError:\n continue\n\n last_line = docker_exec(container, f\"tail -1 '{session_path}'\").strip()\n try:\n last_ts = parse_ts(json.loads(last_line).get(\"timestamp\", \"\"))\n except Exception:\n last_ts = parse_ts(header.get(\"timestamp\", \"\"))\n\n if last_ts < since_epoch and last_ts > 0:\n continue\n\n raw = docker_exec(container, f\"cat '{session_path}'\")\n if not raw.strip():\n continue\n\n output_lines = []\n for line in raw.splitlines():\n line = line.strip()\n if not line:\n continue\n try:\n event = json.loads(line)\n except json.JSONDecodeError:\n continue\n event_ts = parse_ts(event.get(\"timestamp\", \"\"))\n if event.get(\"type\") != \"session\" and event_ts < since_epoch and event_ts > 0:\n continue\n if redact:\n event = redact_json_strings(event)\n output_lines.append(json.dumps(event, ensure_ascii=False))\n\n if len(output_lines) <= 1:\n continue\n\n with open(out_dir / filename, \"w\", encoding=\"utf-8\") as f:\n f.write(\"\\n\".join(output_lines) + \"\\n\")\n\n event_count = len(output_lines) - 1\n print(f\" {container}/{filename} (openclaw): {event_count} events\")\n total_sessions += 1\n total_events += event_count\n\n # sessions.json index\n try:\n raw = docker_exec(container, f\"cat '{sessions_dir}/sessions.json'\")\n if raw.strip():\n index = json.loads(raw)\n if redact:\n index = redact_json_strings(index)\n with open(out_dir / \"sessions.json\", \"w\", encoding=\"utf-8\") as f:\n json.dump(index, f, ensure_ascii=False, indent=2)\n except Exception:\n pass\n\n return total_sessions, total_events\n\n\ndef export_copaw_sessions(container: str, sessions_dir: str, since_epoch: float,\n out_dir: Path, redact: bool) -> tuple[int, int]:\n ls_output = docker_exec(container, f\"find '{sessions_dir}' -name '*.json' -type f 2>/dev/null\").strip()\n if not ls_output:\n return 0, 0\n\n total_sessions = 0\n total_events = 0\n\n for session_path in [f.strip() for f in ls_output.splitlines() if f.strip()]:\n raw = docker_exec(container, f\"cat '{session_path}'\")\n if not raw.strip():\n continue\n try:\n data = json.loads(raw)\n except json.JSONDecodeError:\n continue\n\n agent = data.get(\"agent\", {})\n memory = agent.get(\"memory\", {})\n content = memory.get(\"content\", [])\n if not content:\n continue\n\n basename = os.path.basename(session_path).replace(\".json\", \"\")\n header = {\n \"type\": \"session\",\n \"runtime\": \"copaw\",\n \"agent_name\": agent.get(\"name\", \"\"),\n \"session_key\": basename,\n \"compressed_summary\": memory.get(\"_compressed_summary\", \"\"),\n }\n\n messages_in_range = []\n for turn_idx, turn in enumerate(content):\n if not isinstance(turn, list):\n continue\n for msg in turn:\n if not isinstance(msg, dict):\n continue\n msg_ts = parse_ts(msg.get(\"timestamp\", \"\"))\n if msg_ts >= since_epoch or msg_ts == 0:\n event = {\n \"type\": \"message\",\n \"turn\": turn_idx,\n \"id\": msg.get(\"id\", \"\"),\n \"role\": msg.get(\"role\", \"\"),\n \"name\": msg.get(\"name\", \"\"),\n \"timestamp\": msg.get(\"timestamp\", \"\"),\n \"content\": msg.get(\"content\", []),\n }\n if msg.get(\"metadata\"):\n event[\"metadata\"] = msg[\"metadata\"]\n messages_in_range.append(event)\n\n if not messages_in_range:\n continue\n\n output_lines = []\n if redact:\n header = redact_json_strings(header)\n output_lines.append(json.dumps(header, ensure_ascii=False))\n for event in messages_in_range:\n if redact:\n event = redact_json_strings(event)\n output_lines.append(json.dumps(event, ensure_ascii=False))\n\n out_filename = basename + \".jsonl\"\n with open(out_dir / out_filename, \"w\", encoding=\"utf-8\") as f:\n f.write(\"\\n\".join(output_lines) + \"\\n\")\n\n event_count = len(output_lines) - 1\n print(f\" {container}/{out_filename} (copaw): {event_count} events\")\n total_sessions += 1\n total_events += event_count\n\n return total_sessions, total_events\n\n\ndef export_hermes_sessions(container: str, sessions_dir: str, since_epoch: float,\n out_dir: Path, redact: bool) -> tuple[int, int]:\n ls_output = docker_exec(container, f\"ls '{sessions_dir}'/*.jsonl 2>/dev/null\").strip()\n if not ls_output:\n return 0, 0\n\n total_sessions = 0\n total_events = 0\n\n for session_path in [f.strip() for f in ls_output.splitlines() if f.strip()]:\n raw = docker_exec(container, f\"cat '{session_path}'\")\n if not raw.strip():\n continue\n\n output_lines = []\n last_ts = 0.0\n for line in raw.splitlines():\n line = line.strip()\n if not line:\n continue\n try:\n event = json.loads(line)\n except json.JSONDecodeError:\n continue\n\n event_ts = parse_ts(event.get(\"timestamp\", \"\"))\n if event_ts:\n last_ts = max(last_ts, event_ts)\n\n role = event.get(\"role\", \"\")\n if role != \"session_meta\" and event_ts < since_epoch and event_ts > 0:\n continue\n\n if redact:\n event = redact_json_strings(event)\n output_lines.append(json.dumps(event, ensure_ascii=False))\n\n if not output_lines:\n continue\n\n if last_ts < since_epoch and last_ts > 0:\n continue\n\n filename = os.path.basename(session_path)\n with open(out_dir / filename, \"w\", encoding=\"utf-8\") as f:\n f.write(\"\\n\".join(output_lines) + \"\\n\")\n\n event_count = len(output_lines) - 1 if output_lines and '\"role\": \"session_meta\"' in output_lines[0] else len(output_lines)\n print(f\" {container}/{filename} (hermes): {event_count} events\")\n total_sessions += 1\n total_events += max(event_count, 0)\n\n hermes_home = str(Path(sessions_dir).parent)\n state_db = f\"{hermes_home}/state.db\"\n has_state_db = docker_exec(container, f\"test -f '{state_db}' && echo yes || echo no\").strip() == \"yes\"\n if has_state_db:\n raw = docker_exec(container, f\"python3 - <<'PY'\\nimport json, sqlite3\\nconn = sqlite3.connect('{state_db}')\\nconn.row_factory = sqlite3.Row\\nrows = conn.execute('SELECT * FROM sessions ORDER BY started_at DESC LIMIT 200').fetchall()\\nprint(json.dumps([dict(r) for r in rows], ensure_ascii=False, indent=2))\\nPY\")\n if raw.strip():\n try:\n data = json.loads(raw)\n if redact:\n data = redact_json_strings(data)\n with open(out_dir / \"sessions-db.json\", \"w\", encoding=\"utf-8\") as f:\n json.dump(data, f, ensure_ascii=False, indent=2)\n except json.JSONDecodeError:\n pass\n\n logs_dir = f\"{hermes_home}/logs\"\n has_logs_dir = docker_exec(container, f\"test -d '{logs_dir}' && echo yes || echo no\").strip() == \"yes\"\n if has_logs_dir:\n for log_name in (\"agent.log\", \"errors.log\", \"gateway.log\"):\n raw = docker_exec(container, f\"test -f '{logs_dir}/{log_name}' && cat '{logs_dir}/{log_name}' || true\")\n if raw.strip():\n content = redact_pii(raw) if redact else raw\n (out_dir / log_name).write_text(content, encoding=\"utf-8\")\n\n return total_sessions, total_events\n\n\ndef export_agent_sessions(out_dir: Path, since_epoch: float, redact: bool,\n container_filter: str | None) -> tuple[int, int]:\n \"\"\"Export agent sessions from all containers. Returns (session_count, event_count).\"\"\"\n containers = list_agentteams_containers()\n if container_filter:\n containers = [c for c in containers if container_filter in c]\n\n if not containers:\n print(\" [sessions] No matching AgentTeams containers found\")\n return 0, 0\n\n total_sessions = 0\n total_events = 0\n\n for container in containers:\n runtime, sessions_dir = detect_runtime(container)\n if not runtime:\n print(f\" {container}: no sessions directory, skipped\")\n continue\n\n container_dir = out_dir / container\n container_dir.mkdir(parents=True, exist_ok=True)\n\n if runtime == \"openclaw\":\n s, e = export_openclaw_sessions(container, sessions_dir, since_epoch,\n container_dir, redact)\n elif runtime == \"hermes\":\n s, e = export_hermes_sessions(container, sessions_dir, since_epoch,\n container_dir, redact)\n else:\n s, e = export_copaw_sessions(container, sessions_dir, since_epoch,\n container_dir, redact)\n\n if s == 0:\n if not any(container_dir.iterdir()):\n container_dir.rmdir()\n print(f\" {container} ({runtime}): no sessions in range, skipped\")\n else:\n print(f\" {container} ({runtime}): exported auxiliary debug artifacts without session events\")\n else:\n total_sessions += s\n total_events += e\n\n return total_sessions, total_events\n\n\n# ---------------------------------------------------------------------------\n# Main\n# ---------------------------------------------------------------------------\n\ndef main():\n parser = argparse.ArgumentParser(\n description=\"Export AgentTeams debug logs (Matrix messages + agent sessions)\"\n )\n parser.add_argument(\"--range\", \"-r\", required=True, dest=\"time_range\",\n help=\"Time range to export, e.g. 10m, 1h, 1d\")\n parser.add_argument(\"--container\", \"-c\", default=None,\n help=\"Only export sessions from containers matching this substring\")\n parser.add_argument(\"--room\", default=None,\n help=\"Only export Matrix rooms matching this substring\")\n parser.add_argument(\"--homeserver\", \"-s\", default=\"\",\n help=\"Matrix homeserver URL (auto-detected from env file)\")\n parser.add_argument(\"--token\", \"-t\", default=\"\",\n help=\"Matrix access token (auto-detected from env file)\")\n parser.add_argument(\"--env-file\", default=None,\n help=\"Path to agentteams-manager.env (default: ~/agentteams-manager.env)\")\n parser.add_argument(\"--messages-only\", action=\"store_true\",\n help=\"Only export m.room.message events (skip state events)\")\n parser.add_argument(\"--no-redact\", action=\"store_true\",\n help=\"Disable PII redaction\")\n args = parser.parse_args()\n\n range_seconds = parse_range(args.time_range)\n since_epoch = time.time() - range_seconds\n since_human = datetime.fromtimestamp(since_epoch, tz=timezone.utc).isoformat()\n now_str = datetime.now().strftime(\"%Y%m%d-%H%M%S\")\n\n run_dir = Path(\"debug-log\") / now_str\n run_dir.mkdir(parents=True, exist_ok=True)\n redact = not args.no_redact\n\n print(f\"AgentTeams Debug Log Export\")\n print(f\" Range: last {args.time_range} (since {since_human})\")\n print(f\" Output: {run_dir.resolve()}\")\n print(f\" PII redaction: {'on' if redact else 'off'}\")\n print()\n\n # --- Matrix messages ---\n print(\"=== Matrix Messages ===\")\n matrix_dir = run_dir / \"matrix-messages\"\n rooms, messages = export_matrix_messages(\n matrix_dir, since_epoch, redact,\n room_filter=args.room, env_file=args.env_file,\n homeserver=args.homeserver, token=args.token,\n messages_only=args.messages_only,\n )\n if rooms == 0 and matrix_dir.exists() and not any(matrix_dir.iterdir()):\n matrix_dir.rmdir()\n print()\n\n # --- Agent sessions ---\n print(\"=== Agent Sessions ===\")\n sessions_dir = run_dir / \"agent-sessions\"\n sessions, events = export_agent_sessions(\n sessions_dir, since_epoch, redact,\n container_filter=args.container,\n )\n if sessions == 0 and sessions_dir.exists() and not any(sessions_dir.iterdir()):\n sessions_dir.rmdir()\n print()\n\n # --- Container diagnostics ---\n print(\"=== Container Diagnostics ===\")\n container_logs_dir = run_dir / \"container-logs\"\n containers = export_container_logs(\n container_logs_dir, since_epoch, redact,\n container_filter=args.container,\n )\n if containers == 0 and container_logs_dir.exists() and not any(container_logs_dir.iterdir()):\n container_logs_dir.rmdir()\n print()\n\n # --- Summary ---\n summary = (\n f\"AgentTeams Debug Log\\n\"\n f\"Exported at: {now_str}\\n\"\n f\"Range: last {args.time_range} (since {since_human})\\n\"\n f\"PII redaction: {'on' if redact else 'off'}\\n\"\n f\"\\n\"\n f\"Matrix messages: {messages} messages from {rooms} rooms\\n\"\n f\"Agent sessions: {events} events from {sessions} sessions\\n\"\n f\"Container diagnostics: {containers} containers\\n\"\n )\n (run_dir / \"summary.txt\").write_text(summary)\n\n print(\n f\"Done. {messages} messages from {rooms} rooms, \"\n f\"{events} events from {sessions} sessions, \"\n f\"{containers} container diagnostics\"\n )\n print(f\"Output: {run_dir.resolve()}\")\n\n\nif __name__ == \"__main__\":\n main()\n"}}, "install/agentteams-install.ps1": {"124f06d13fd6bb4c2054128dcff26bcd9f9c8dbf": {"t": 1785746477.197231, "n": 193564, "h": "6fc90f14bd01f519", "c": "#!/usr/bin/env powershell\n# agentteams-install.ps1 - One-click installation for AgentTeams Manager and Worker on Windows\n# Requires PowerShell 7.0+ (recommended)\n#\n# Usage:\n# .\\agentteams-install.ps1 # Interactive installation (choose Quick Start or Manual)\n# .\\agentteams-install.ps1 manager # Same as above (explicit)\n# .\\agentteams-install.ps1 worker --name <name> ... # Worker installation\n#\n# Onboarding Modes:\n# Quick Start - Fast installation with all default values (recommended)\n# Manual - Customize each option step by step\n#\n# Environment variables (for automation):\n# AGENTTEAMS_NON_INTERACTIVE Skip all prompts, use defaults (default: 0)\n# AGENTTEAMS_LLM_PROVIDER LLM provider (default: openai-compat for zh non-interactive Token Plan; qwen for en)\n# AGENTTEAMS_DEFAULT_MODEL Default model (default: qwen3.6-plus for zh Token Plan and en non-interactive)\n# AGENTTEAMS_OPENAI_BASE_URL OpenAI-compatible base URL (default for zh non-interactive: Alibaba Token Plan endpoint)\n# AGENTTEAMS_LLM_API_KEY LLM API key (required)\n# AGENTTEAMS_ADMIN_USER Admin username (default: admin)\n# AGENTTEAMS_ADMIN_PASSWORD Admin password (auto-generated if not set, min 8 chars)\n# AGENTTEAMS_MATRIX_DOMAIN Matrix domain (default: matrix-local.agentteams.io:18080)\n# AGENTTEAMS_MOUNT_SOCKET Mount container runtime socket (default: 1)\n# AGENTTEAMS_MATRIX_E2EE Matrix E2EE (default: 0, disabled)\n# AGENTTEAMS_DATA_DIR Docker volume name for persistent data (default: agentteams-data)\n# AGENTTEAMS_WORKSPACE_DIR Host directory for manager workspace (default: ~/agentteams-manager)\n# AGENTTEAMS_VERSION Image tag (default: latest)\n# AGENTTEAMS_REGISTRY Image registry (default: auto-detected by timezone)\n# AGENTTEAMS_INSTALL_MANAGER_IMAGE Override manager image (e.g., local build)\n# AGENTTEAMS_INSTALL_WORKER_IMAGE Override worker image (e.g., local build)\n# AGENTTEAMS_INSTALL_COPAW_WORKER_IMAGE Override copaw worker image (e.g., local build)\n# AGENTTEAMS_INSTALL_QWENPAW_WORKER_IMAGE Override qwenpaw worker image (e.g., local build)\n# AGENTTEAMS_INSTALL_HERMES_WORKER_IMAGE Override hermes worker image (e.g., local build)\n# AGENTTEAMS_PORT_GATEWAY Host port for Higress gateway (default: 18080)\n# AGENTTEAMS_PORT_CONSOLE Host port for Higress console (default: 18001)\n# AGENTTEAMS_PORT_ELEMENT_WEB Host port for Element Web direct access (default: 18088)\n\n#Requires -Version 5.1\n\n[CmdletBinding()]\nparam(\n [Parameter(Position = 0)]\n [ValidateSet(\"manager\", \"worker\", \"uninstall\")]\n [string]$Command = \"manager\",\n\n # Worker options\n [string]$Name,\n [string]$Fs,\n [string]$FsKey,\n [string]$FsSecret,\n [switch]$Reset,\n [switch]$FindSkills,\n [string]$SkillsApiUrl,\n\n # General options\n [switch]$NonInteractive,\n [string]$EnvFile\n)\n\n# ============================================================\n# Configuration\n# ============================================================\n\n$script:AGENTTEAMS_VERSION = if ($env:AGENTTEAMS_VERSION) { $env:AGENTTEAMS_VERSION } else { \"latest\" }\n$script:AGENTTEAMS_NON_INTERACTIVE = if ($env:AGENTTEAMS_NON_INTERACTIVE -eq \"1\" -or $NonInteractive) { $true } else { $false }\n$script:AGENTTEAMS_MOUNT_SOCKET = if ($env:AGENTTEAMS_MOUNT_SOCKET -eq \"0\") { $false } else { $true }\n$script:AGENTTEAMS_ENV_FILE = if ($EnvFile) { $EnvFile } elseif ($env:AGENTTEAMS_ENV_FILE) { $env:AGENTTEAMS_ENV_FILE } else { \"$env:USERPROFILE\\agentteams-manager.env\" }\n$script:StepResult = \"\" # Used by state machine to signal \"back\" navigation\n$script:config = @{} # Shared config hashtable for step functions\n\n# ANSI escape character for terminal colors\n$script:ESC = [char]0x1B\n\n# ============================================================\n# Log all output to file\n# ============================================================\n\n$script:AGENTTEAMS_LOG_FILE = \"$env:USERPROFILE\\agentteams-install.log\"\n\n# Start transcript for logging (PowerShell's built-in logging mechanism)\ntry {\n Start-Transcript -Path $script:AGENTTEAMS_LOG_FILE -Append -ErrorAction SilentlyContinue | Out-Null\n} catch {\n # If transcript fails, continue without logging\n}\n\nWrite-Host \"\"\nWrite-Host \"========================================\"\nWrite-Host \"AgentTeams Installation Log\"\nWrite-Host \"Started: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')\"\nWrite-Host \"User: $env:USERNAME\"\nWrite-Host \"System: $(hostname)\"\nWrite-Host \"Log file: $($script:AGENTTEAMS_LOG_FILE)\"\nWrite-Host \"========================================\"\nWrite-Host \"\"\n\n# ============================================================\n# Utility Functions\n# ============================================================\n\nfunction Write-Log {\n param([string]$Message)\n Write-Host \"$($script:ESC)[36m[AgentTeams]$($script:ESC)[0m $Message\"\n}\n\nfunction Write-Error {\n param([string]$Message)\n Write-Host \"$($script:ESC)[31m[AgentTeams ERROR]$($script:ESC)[0m $Message\" -ForegroundColor Red\n throw $Message\n}\n\nfunction Write-Warning {\n param([string]$Message)\n Write-Host \"$($script:ESC)[33m[AgentTeams WARNING]$($script:ESC)[0m $Message\"\n}\n\n# Pause before exit on error so user can read the message when running via double-click\nfunction Exit-Script {\n param([int]$ExitCode = 0)\n if ($ExitCode -ne 0 -and -not $script:AGENTTEAMS_NON_INTERACTIVE) {\n Write-Host \"\"\n Write-Host \"Press Enter to exit.\" -NoNewline\n $Host.UI.ReadLine()\n }\n exit $ExitCode\n}\n\nfunction Test-DockerRunning {\n try {\n $null = docker info 2>&1\n if ($LASTEXITCODE -ne 0) {\n return $false\n }\n return $true\n }\n catch {\n return $false\n }\n}\n\nfunction Get-AgentTeamsTimeZone {\n try {\n if ($env:AGENTTEAMS_TIMEZONE) {\n return $env:AGENTTEAMS_TIMEZONE\n }\n\n $tz = (Get-TimeZone).Id\n # Convert Windows timezone to IANA format\n $tzMap = @{\n \"China Standard Time\" = \"Asia/Shanghai\"\n \"Pacific Standard Time\" = \"America/Los_Angeles\"\n \"Mountain Standard Time\" = \"America/Denver\"\n \"Central Standard Time\" = \"America/Chicago\"\n \"Eastern Standard Time\" = \"America/New_York\"\n \"GMT Standard Time\" = \"Europe/London\"\n \"Central European Standard Time\" = \"Europe/Berlin\"\n \"Tokyo Standard Time\" = \"Asia/Tokyo\"\n \"Singapore Standard Time\" = \"Asia/Singapore\"\n \"Korea Standard Time\" = \"Asia/Seoul\"\n \"India Standard Time\" = \"Asia/Kolkata\"\n }\n\n if ($tzMap.ContainsKey($tz)) {\n return $tzMap[$tz]\n }\n return $tz\n }\n catch {\n return \"Asia/Shanghai\"\n }\n}\n\nfunction Get-Registry {\n param([string]$Timezone)\n\n if ($env:AGENTTEAMS_REGISTRY) {\n return $env:AGENTTEAMS_REGISTRY\n }\n\n # Americas\n if ($Timezone -match \"^America/\") {\n return \"higress-registry.us-west-1.cr.aliyuncs.com\"\n }\n\n # Southeast Asia\n if ($Timezone -match \"^(Asia/Singapore|Asia/Bangkok|Asia/Jakarta|Asia/Makassar|Asia/Jayapura|Asia/Kuala_Lumpur|Asia/Ho_Chi_Minh|Asia/Manila|Asia/Yangon|Asia/Vientiane|Asia/Phnom_Penh|Asia/Pontianak|Asia/Ujung_Pandang)\") {\n return \"higress-registry.ap-southeast-7.cr.aliyuncs.com\"\n }\n\n # Default: China\n return \"higress-registry.cn-hangzhou.cr.aliyuncs.com\"\n}\n\nfunction Get-AgentTeamsLanguage {\n param([string]$Timezone)\n $chineseZones = @(\n \"Asia/Shanghai\", \"Asia/Chongqing\", \"Asia/Harbin\", \"Asia/Urumqi\",\n \"Asia/Taipei\", \"Asia/Hong_Kong\", \"Asia/Macau\"\n )\n if ($chineseZones -contains $Timezone) { return \"zh\" }\n return \"en\"\n}\n\n# ============================================================\n# Centralized message dictionary and Get-Msg function\n# ============================================================\n\n$script:Messages = @{\n # --- Timezone detection messages ---\n \"tz.warning.title\" = @{ zh = \"无法自动检测时区。\"; en = \"Could not detect timezone automatically.\" }\n \"tz.warning.prompt\" = @{ zh = \"请输入您的时区(例如 Asia/Shanghai、America/New_York)。\"; en = \"Please enter your timezone (e.g., Asia/Shanghai, America/New_York).\" }\n \"tz.default\" = @{ zh = \"使用默认时区: {0}\"; en = \"Using default timezone: {0}\" }\n \"tz.input_prompt\" = @{ zh = \"时区\"; en = \"Timezone\" }\n\n # --- Installation title and info ---\n \"install.title\" = @{ zh = \"=== AgentTeams Manager 安装 ===\"; en = \"=== AgentTeams Manager Installation ===\" }\n \"install.registry\" = @{ zh = \"镜像仓库: {0}\"; en = \"Registry: {0}\" }\n \"install.dir\" = @{ zh = \"安装目录: {0}\"; en = \"Installation directory: {0}\" }\n \"install.dir_hint\" = @{ zh = \" (env 文件 'agentteams-manager.env' 将保存到 HOME 目录。)\"; en = \" (The env file 'agentteams-manager.env' will be saved to your HOME directory.)\" }\n \"install.dir_hint2\" = @{ zh = \" (请从您希望管理此安装的目录运行此脚本。)\"; en = \" (Run this script from the directory where you want to manage this installation.)\" }\n\n # --- Onboarding mode ---\n \"install.mode.title\" = @{ zh = \"--- Onboarding 模式 ---\"; en = \"--- Onboarding Mode ---\" }\n \"install.mode.choose\" = @{ zh = \"选择安装模式:\"; en = \"Choose your installation mode:\" }\n \"install.mode.quickstart\" = @{ zh = \" 1) 快速开始 - 使用阿里云通义 Token 套餐快速安装(推荐)\"; en = \" 1) Quick Start - Fast installation with Qwen Cloud (recommended)\" }\n \"install.mode.manual\" = @{ zh = \" 2) 手动配置 - 选择 LLM 提供商并自定义选项\"; en = \" 2) Manual - Choose LLM provider and customize options\" }\n \"install.mode.prompt\" = @{ zh = \"请选择 [1/2]\"; en = \"Enter choice [1/2]\" }\n \"install.mode.quickstart_selected\" = @{ zh = \"已选择快速开始模式 - 使用阿里云通义 Token 套餐\"; en = \"Quick Start mode selected - using Qwen Cloud\" }\n \"install.mode.manual_selected\" = @{ zh = \"已选择手动配置模式 - 您将选择 LLM 提供商并自定义选项\"; en = \"Manual mode selected - you will choose LLM provider and customize options\" }\n \"install.mode.invalid\" = @{ zh = \"无效选择,默认使用快速开始模式\"; en = \"Invalid choice, defaulting to Quick Start mode\" }\n\n # --- Existing installation detected ---\n \"install.existing.detected\" = @{ zh = \"检测到已有 Manager 安装(env 文件: {0})\"; en = \"Existing Manager installation detected (env file: {0})\" }\n \"install.existing.choose\" = @{ zh = \"选择操作:\"; en = \"Choose an action:\" }\n \"install.existing.upgrade\" = @{ zh = \" 1) 就地升级(保留数据、工作空间、env 文件)\"; en = \" 1) In-place upgrade (keep data, workspace, env file)\" }\n \"install.existing.reinstall\" = @{ zh = \" 2) 全新重装(删除所有数据,重新开始)\"; en = \" 2) Clean reinstall (remove all data, start fresh)\" }\n \"install.existing.cancel\" = @{ zh = \" 3) 取消\"; en = \" 3) Cancel\" }\n \"install.existing.prompt\" = @{ zh = \"请选择 [1/2/3]\"; en = \"Enter choice [1/2/3]\" }\n \"install.existing.upgrade_noninteractive\" = @{ zh = \"非交互模式: 执行就地升级...\"; en = \"Non-interactive mode: performing in-place upgrade...\" }\n \"install.existing.upgrading\" = @{ zh = \"执行就地升级...\"; en = \"Performing in-place upgrade...\" }\n \"install.existing.warn_manager_stop\" = @{ zh = \"警告: Manager 容器将被停止并重新创建。\"; en = \"WARNING: Manager container will be stopped and recreated.\" }\n \"install.existing.warn_worker_recreate\" = @{ zh = \"警告: Worker 容器也将被重新创建(以更新 Manager IP)。\"; en = \"WARNING: Worker containers will also be recreated (to update Manager IP in hosts).\" }\n \"install.existing.continue_prompt\" = @{ zh = \"继续?[y/N]\"; en = \"Continue? [y/N]\" }\n \"install.existing.cancelled\" = @{ zh = \"安装已取消。\"; en = \"Installation cancelled.\" }\n \"install.existing.stopping_manager\" = @{ zh = \"停止并移除现有 manager 容器...\"; en = \"Stopping and removing existing manager container...\" }\n \"install.existing.stopping_workers\" = @{ zh = \"停止并移除现有 worker 容器...\"; en = \"Stopping and removing existing worker containers...\" }\n \"install.existing.removed\" = @{ zh = \" 已移除: {0}\"; en = \" Removed: {0}\" }\n\n # --- Upgrade mode sub-menu ---\n \"upgrade.mode.prompt\" = @{ zh = \"请选择升级方式:\"; en = \"Select upgrade mode:\" }\n \"upgrade.mode.keep_all\" = @{ zh = \" 1) 保留所有参数,一键升级(推荐)\"; en = \" 1) Keep all parameters, quick upgrade (recommended)\" }\n \"upgrade.mode.confirm_each\" = @{ zh = \" 2) 逐个确认参数(可修改)\"; en = \" 2) Confirm each parameter (can modify)\" }\n \"upgrade.mode.back\" = @{ zh = \" 3) 返回\"; en = \" 3) Back\" }\n\n # --- Clean reinstall messages ---\n \"install.reinstall.performing\" = @{ zh = \"执行全新重装...\"; en = \"Performing clean reinstall...\" }\n \"install.reinstall.warn_stop\" = @{ zh = \"警告: 以下运行中的容器将被停止:\"; en = \"WARNING: The following running containers will be stopped:\" }\n \"install.reinstall.warn_delete\" = @{ zh = \"警告: 以下内容将被删除:\"; en = \"WARNING: This will DELETE the following:\" }\n \"install.reinstall.warn_volume\" = @{ zh = \" - Docker 卷: agentteams-data\"; en = \" - Docker volume: agentteams-data\" }\n \"install.reinstall.warn_env\" = @{ zh = \" - Env 文件: {0}\"; en = \" - Env file: {0}\" }\n \"install.reinstall.warn_workspace\" = @{ zh = \" - Manager 工作空间: {0}\"; en = \" - Manager workspace: {0}\" }\n \"install.reinstall.warn_workers\" = @{ zh = \" - 所有 worker 容器\"; en = \" - All worker containers\" }\n \"install.reinstall.warn_proxy\" = @{ zh = \" - Docker API 代理容器: agentteams-controller\"; en = \" - Docker API proxy container: agentteams-controller\" }\n \"install.reinstall.removing_proxy\" = @{ zh = \"正在移除 Docker API 代理容器: agentteams-controller\"; en = \"Removing Docker API proxy container: agentteams-controller\" }\n \"install.reinstall.warn_network\" = @{ zh = \" - Docker 网络: agentteams-net\"; en = \" - Docker network: agentteams-net\" }\n \"install.reinstall.removing_network\" = @{ zh = \"正在移除 Docker 网络: agentteams-net\"; en = \"Removing Docker network: agentteams-net\" }\n \"install.reinstall.confirm_type\" = @{ zh = \"请输入工作空间路径以确认删除(或按 Ctrl+C 取消):\"; en = \"To confirm deletion, please type the workspace path:\" }\n \"install.reinstall.confirm_path\" = @{ zh = \"输入路径以确认(或按 Ctrl+C 取消)\"; en = \"Type the path to confirm (or press Ctrl+C to cancel)\" }\n \"install.reinstall.path_mismatch\" = @{ zh = \"路径不匹配。中止重装。输入: '{0}',期望: '{1}'\"; en = \"Path mismatch. Aborting reinstall. Input: '{0}', Expected: '{1}'\" }\n \"install.reinstall.confirmed\" = @{ zh = \"已确认。正在清理...\"; en = \"Confirmed. Cleaning up...\" }\n \"install.reinstall.removed_worker\" = @{ zh = \" 已移除 worker: {0}\"; en = \" Removed worker: {0}\" }\n \"install.reinstall.removing_volume\" = @{ zh = \"正在移除 Docker 卷: agentteams-data\"; en = \"Removing Docker volume: agentteams-data\" }\n \"install.reinstall.warn_volume_fail\" = @{ zh = \" 警告: 无法移除卷(可能有引用)\"; en = \" Warning: Could not remove volume (may have references)\" }\n \"install.reinstall.removing_workspace\" = @{ zh = \"正在移除工作空间目录: {0}\"; en = \"Removing workspace directory: {0}\" }\n \"install.reinstall.removing_env\" = @{ zh = \"正在移除 env 文件: {0}\"; en = \"Removing env file: {0}\" }\n \"install.reinstall.cleanup_done\" = @{ zh = \"清理完成。开始全新安装...\"; en = \"Cleanup complete. Starting fresh installation...\" }\n \"install.reinstall.failed_rm_workspace\" = @{ zh = \"无法移除工作空间目录\"; en = \"Failed to remove workspace directory\" }\n\n # --- Orphan volume detection ---\n \"install.orphan_volume.detected\" = @{ zh = \"⚠️ 检测到残留数据卷 '{0}',但未找到对应的 env 配置文件。\"; en = \"⚠️ Found leftover data volume '{0}' but no matching env config file.\" }\n \"install.orphan_volume.warn\" = @{ zh = \"这可能是之前安装的残留数据,会导致新安装出现异常(如密码冲突、服务启动失败)。\"; en = \"This is likely leftover data from a previous installation and may cause issues (e.g., credential conflicts, service startup failures).\" }\n \"install.orphan_volume.choose\" = @{ zh = \"选择操作:\"; en = \"Choose an action:\" }\n \"install.orphan_volume.clean\" = @{ zh = \" 1) 清理残留数据卷后继续安装(推荐)\"; en = \" 1) Remove leftover volume and continue installation (recommended)\" }\n \"install.orphan_volume.keep\" = @{ zh = \" 2) 保留数据卷继续安装(可能出现异常)\"; en = \" 2) Keep the volume and continue installation (may cause issues)\" }\n \"install.orphan_volume.prompt\" = @{ zh = \"请选择 [1/2]\"; en = \"Enter choice [1/2]\" }\n \"install.orphan_volume.cleaning\" = @{ zh = \"正在清理残留数据卷...\"; en = \"Removing leftover data volume...\" }\n \"install.orphan_volume.cleaned\" = @{ zh = \"残留数据卷已清理。继续全新安装...\"; en = \"Leftover volume removed. Continuing with fresh installation...\" }\n \"install.orphan_volume.keeping\" = @{ zh = \"保留数据卷,继续安装。如遇异常请选择全新重装。\"; en = \"Keeping existing volume. If you encounter issues, consider a clean reinstall.\" }\n \"install.orphan_volume.clean_noninteractive\" = @{ zh = \"非交互模式: 自动清理残留数据卷...\"; en = \"Non-interactive mode: automatically removing leftover volume...\" }\n\n # --- Loading existing config ---\n \"install.loading_config\" = @{ zh = \"从 {0} 加载已有配置(shell 环境变量优先)...\"; en = \"Loading existing config from {0} (shell env vars take priority)...\" }\n\n # --- LLM Configuration ---\n \"llm.title\" = @{ zh = \"--- LLM 配置 ---\"; en = \"--- LLM Configuration ---\" }\n \"llm.provider.label\" = @{ zh = \" 提供商: {0}\"; en = \" Provider: {0}\" }\n \"llm.model.label\" = @{ zh = \" 模型: {0}\"; en = \" Model: {0}\" }\n \"llm.provider.qwen\" = @{ zh = \" 提供商: qwen(阿里云百炼)\"; en = \" Provider: qwen (Alibaba Cloud Bailian)\" }\n \"llm.provider.qwen_default\" = @{ zh = \" 提供商: {0}(默认)\"; en = \" Provider: {0} (default)\" }\n \"llm.model.default\" = @{ zh = \" 模型: {0}(默认)\"; en = \" Model: {0} (default)\" }\n \"llm.apikey_hint_bailian\" = @{ zh = \" 提示: 获取阿里云百炼(DashScope)API Key:\"; en = \" Hint: Get your Alibaba Cloud Bailian (DashScope) API Key:\" }\n \"llm.apikey_url_bailian\" = @{ zh = \" https://www.aliyun.com/product/bailian\"; en = \" https://www.aliyun.com/product/bailian\" }\n \"llm.apikey_hint_qwencloud\" = @{ zh = \" 提示: 从 Qwen Cloud(国际站)获取 DASHSCOPE_API_KEY:\"; en = \" Hint: Get your DASHSCOPE_API_KEY for Qwen Cloud (international) from:\" }\n \"llm.apikey_url_qwencloud\" = @{ zh = \" https://home.qwencloud.com/api-keys (文档: https://docs.qwencloud.com/)\"; en = \" https://home.qwencloud.com/api-keys | Docs: https://docs.qwencloud.com/\" }\n \"llm.apikey_hint_tokenplan\" = @{ zh = \" 提示: 获取 DashScope API Key 或开通通义 Token 套餐,请参考:\"; en = \" Hint: Get your DashScope or Token Plan API key (Alibaba Model Studio):\" }\n \"llm.apikey_url_tokenplan\" = @{ zh = \" https://help.aliyun.com/zh/model-studio/token-plan-quickstart\"; en = \" https://common-buy.aliyun.com/token-plan/ | https://help.aliyun.com/zh/model-studio/token-plan-quickstart\" }\n \"llm.apikey_hint_codingplan\" = @{ zh = \" 提示: 获取 DashScope API Key(Coding 套餐 / coding.dashscope 接口):\"; en = \" Hint: Get your DashScope API key for Coding Plan (coding.dashscope endpoint):\" }\n \"llm.apikey_url_codingplan\" = @{ zh = \" https://help.aliyun.com/zh/model-studio/get-api-key\"; en = \" https://help.aliyun.com/zh/model-studio/get-api-key\" }\n \"llm.apikey_prompt\" = @{ zh = \"LLM API Key\"; en = \"LLM API Key\" }\n \"llm.providers_title\" = @{ zh = \"可用 LLM 提供商:\"; en = \"Available LLM Providers:\" }\n \"llm.provider.alibaba\" = @{ zh = \" 1) 阿里云通义 Token 套餐 - 推荐中国用户使用\"; en = \" 1) Qwen Cloud - International (OpenAI-compatible API, recommended)\" }\n \"llm.provider.openai_compat\" = @{ zh = \" 2) OpenAI 兼容 API - 自定义 Base URL(OpenAI、DeepSeek 等)\"; en = \" 2) OpenAI-compatible API - Custom Base URL (OpenAI, DeepSeek, etc.)\" }\n \"llm.provider.select\" = @{ zh = \"选择提供商 [1/2]\"; en = \"Select provider [1/2]\" }\n \"llm.alibaba.models_title\" = @{ zh = \"选择阿里云模型接入方式:\"; en = \"Select Alibaba Cloud model access:\" }\n \"llm.alibaba.model.tokenplan\" = @{ zh = \" 1) 阿里云通义 Token 套餐 - 兼容模式(推荐)\"; en = \" 1) Alibaba Cloud Token Plan - compatible-mode (recommended)\" }\n \"llm.alibaba.model.bailian\" = @{ zh = \" 2) 阿里云百炼 - DashScope 通用兼容接口\"; en = \" 2) Alibaba Cloud Bailian - DashScope compatible mode\" }\n \"llm.alibaba.model.codingplan_legacy\" = @{ zh = \" 3) 阿里云 Coding 套餐 - 旧版端点(兼容保留)\"; en = \" 3) Alibaba Cloud Coding Plan - legacy endpoint (backward compatible)\" }\n \"llm.alibaba.model.select\" = @{ zh = \"选择接入方式 [1/2/3]\"; en = \"Select access option [1/2/3]\" }\n \"llm.alibaba.model.invalid\" = @{ zh = \"无效选择: {0}(请输入 1、2 或 3)\"; en = \"Invalid choice: {0} (please enter 1, 2, or 3)\" }\n \"llm.codingplan.models_title\" = @{ zh = \"选择通义 Token 套餐默认模型:\"; en = \"Select Qwen Cloud default model:\" }\n \"llm.codingplan.model.qwen36plus\" = @{ zh = \" 1) qwen3.6-plus - 千问 3.6(推荐)\"; en = \" 1) qwen3.6-plus - Qwen 3.6 (recommended)\" }\n \"llm.codingplan.model.glm5\" = @{ zh = \" 2) glm-5 - 智谱 GLM-5(编程推荐)\"; en = \" 2) glm-5 - Zhipu GLM-5 (recommended for coding)\" }\n \"llm.codingplan.model.kimi\" = @{ zh = \" 3) kimi-k2.5 - Moonshot Kimi K2.5\"; en = \" 3) kimi-k2.5 - Moonshot Kimi K2.5\" }\n \"llm.codingplan.model.minimax\" = @{ zh = \" 4) MiniMax-M2.5 - MiniMax M2.5\"; en = \" 4) MiniMax-M2.5 - MiniMax M2.5\" }\n \"llm.codingplan.model.select\" = @{ zh = \"选择模型 [1/2/3/4]\"; en = \"Select model [1/2/3/4]\" }\n \"llm.provider.selected_tokenplan\" = @{ zh = \" 提供商: 阿里云通义 Token 套餐(兼容模式)\"; en = \" Provider: Alibaba Cloud Token Plan (compatible mode)\" }\n \"llm.provider.selected_codingplan\" = @{ zh = \" 提供商: 阿里云通义 Token 套餐(alibaba-cloud)\"; en = \" Provider: Qwen Cloud (international) (alibaba-cloud)\" }\n \"llm.provider.selected_codingplan_legacy\" = @{ zh = \" 提供商: 阿里云 Coding 套餐(coding.dashscope)\"; en = \" Provider: Alibaba Cloud Coding Plan (coding.dashscope)\" }\n \"llm.provider.selected_qwen\" = @{ zh = \" 提供商: 阿里云百炼\"; en = \" Provider: Alibaba Cloud Bailian\" }\n \"llm.provider.selected_openai\" = @{ zh = \" 提供商: {0}(OpenAI 兼容)\"; en = \" Provider: {0} (OpenAI-compatible)\" }\n \"llm.provider.invalid\" = @{ zh = \"无效选择: {0}(请输入 1 或 2)\"; en = \"Invalid choice: {0} (please enter 1 or 2)\" }\n \"llm.qwen.model_prompt\" = @{ zh = \"默认模型 ID\"; en = \"Default Model ID\" }\n \"llm.openai.base_url_prompt\" = @{ zh = \"Base URL\"; en = \"Base URL\" }\n \"llm.openai.model_prompt\" = @{ zh = \"默认模型 ID\"; en = \"Default Model ID\" }\n \"llm.openai.base_url_label\" = @{ zh = \" Base URL: {0}\"; en = \" Base URL: {0}\" }\n\n # --- Custom model parameters ---\n \"llm.custom_model.detected\" = @{ zh = \" ⚠️ 模型 '{0}' 不在内置模型列表中,请配置模型参数:\"; en = \" ⚠️ Model '{0}' is not in the built-in model list. Please configure model parameters:\" }\n \"llm.custom_model.context_prompt\" = @{ zh = \"最大上下文长度(token 数)[150000]\"; en = \"Max context window (tokens) [150000]\" }\n \"llm.custom_model.max_tokens_prompt\" = @{ zh = \"最大输出长度(token 数)[128000]\"; en = \"Max output tokens [128000]\" }\n \"llm.custom_model.reasoning_prompt\" = @{ zh = \"是否支持推理/思考模式?[Y/n]\"; en = \"Does it support reasoning/thinking mode? [Y/n]\" }\n \"llm.custom_model.vision_prompt\" = @{ zh = \"是否支持图片输入?[y/N]\"; en = \"Does it support image input? [y/N]\" }\n \"llm.custom_model.summary\" = @{ zh = \" 自定义模型参数: 上下文={0}, 最大输出={1}, 推理={2}, 图片={3}\"; en = \" Custom model params: context={0}, maxTokens={1}, reasoning={2}, vision={3}\" }\n\n # --- Admin Credentials ---\n \"admin.title\" = @{ zh = \"--- 管理员凭据 ---\"; en = \"--- Admin Credentials ---\" }\n \"admin.username_prompt\" = @{ zh = \"管理员用户名\"; en = \"Admin Username\" }\n \"admin.password_prompt\" = @{ zh = \"管理员密码(留空自动生成,最少 8 位)\"; en = \"Admin Password (leave empty to auto-generate, min 8 chars)\" }\n \"admin.password_generated\" = @{ zh = \" 已自动生成管理员密码\"; en = \" Auto-generated admin password\" }\n \"admin.password_too_short\" = @{ zh = \"管理员密码至少需要 8 个字符(MinIO 要求)。当前长度: {0}\"; en = \"Admin password must be at least 8 characters (MinIO requirement). Current length: {0}\" }\n\n # --- Port Configuration ---\n \"port.title\" = @{ zh = \"--- 端口配置(按回车使用默认值)---\"; en = \"--- Port Configuration (press Enter for defaults) ---\" }\n \"port.gateway_prompt\" = @{ zh = \"网关主机端口(容器内 8080)\"; en = \"Host port for gateway (8080 inside container)\" }\n \"port.console_prompt\" = @{ zh = \"Higress 控制台主机端口(容器内 8001)\"; en = \"Host port for Higress console (8001 inside container)\" }\n \"port.element_prompt\" = @{ zh = \"Element Web 直接访问主机端口(容器内 8088)\"; en = \"Host port for Element Web direct access (8088 inside container)\" }\n \"port.manager_console_prompt\" = @{ zh = \"Manager 控制台主机端口(容器内 18888)\"; en = \"Host port for Manager console (18888 inside container)\" }\n \"port.local_only.title\" = @{ zh = \"--- 网络访问模式 ---\"; en = \"--- Network Access Mode ---\" }\n \"port.local_only.hint_yes\" = @{ zh = \" 仅本机使用,无需开放外部端口(推荐)\"; en = \" Local use only, no external port exposure (recommended)\" }\n \"port.local_only.hint_no\" = @{ zh = \" 允许外部访问(局域网 / 公网)\"; en = \" Allow external access (LAN / public network)\" }\n \"port.local_only.choice\" = @{ zh = \"请选择 [1/2]\"; en = \"Enter choice [1/2]\" }\n \"port.local_only.selected_local\" = @{ zh = \"端口已绑定到 127.0.0.1(仅本机访问)\"; en = \"Ports bound to 127.0.0.1 (localhost only)\" }\n \"port.local_only.selected_external\" = @{ zh = \"端口已绑定到所有网络接口(0.0.0.0)\"; en = \"Ports bound to all interfaces (0.0.0.0)\" }\n \"port.local_only.https_hint\" = @{ zh = \"警告: 建议在 Higress 控制台配置 TLS 证书并启用 HTTPS,避免明文传输。\"; en = \"WARNING: It is recommended to configure TLS certificates and enable HTTPS in the Higress Console to avoid plaintext transmission.\" }\n \"port.local_only.https_docs\" = @{ zh = \"\"; en = \"\" }\n\n # --- Domain Configuration ---\n \"domain.title\" = @{ zh = \"--- 域名配置(按回车使用默认值)---\"; en = \"--- Domain Configuration (press Enter for defaults) ---\" }\n \"domain.hint\" = @{ zh = \"提示: 自定义域名前必须事先做好 DNS 解析。单机 ECS 部署时无需修改 aigw、fs 等域名;Element Web 和 Matrix Server 也可通过 IP 直接访问。\"; en = \"Hint: Configure DNS resolution before customizing domains. For single ECS deployment, no need to change aigw, fs, etc.; Element Web and Matrix Server can also be accessed directly via IP.\" }\n \"domain.matrix_prompt\" = @{ zh = \"Matrix 域名\"; en = \"Matrix Domain\" }\n \"domain.element_prompt\" = @{ zh = \"Element Web 域名\"; en = \"Element Web Domain\" }\n \"domain.gateway_prompt\" = @{ zh = \"AI 网关域名\"; en = \"AI Gateway Domain\" }\n \"domain.fs_prompt\" = @{ zh = \"文件系统域名\"; en = \"File System Domain\" }\n \"domain.console_prompt\" = @{ zh = \"Manager 控制台域名\"; en = \"Manager Console Domain\" }\n\n # --- GitHub Integration ---\n \"github.title\" = @{ zh = \"--- GitHub 集成(可选,按回车跳过)---\"; en = \"--- GitHub Integration (optional, press Enter to skip) ---\" }\n \"github.token_prompt\" = @{ zh = \"GitHub 个人访问令牌(可选)\"; en = \"GitHub Personal Access Token (optional)\" }\n\n # --- Skills Registry ---\n \"skills.title\" = @{ zh = \"--- Skills 注册中心(可选,按回车使用默认 nacos://market.agentteams.io:80/public)---\"; en = \"--- Skills Registry (optional, press Enter for default nacos://market.agentteams.io:80/public) ---\" }\n \"skills.url_prompt\" = @{ zh = \"Skills 注册中心 URL(留空使用默认 nacos://market.agentteams.io:80/public)\"; en = \"Skills Registry URL (leave empty for default nacos://market.agentteams.io:80/public)\" }\n\n # --- Data Persistence ---\n \"data.title\" = @{ zh = \"--- 数据持久化 ---\"; en = \"--- Data Persistence ---\" }\n \"data.volume_prompt\" = @{ zh = \"Docker 卷名称 [agentteams-data]\"; en = \"Docker volume name for persistent data [agentteams-data]\" }\n \"data.volume_using\" = @{ zh = \" 使用 Docker 卷: {0}\"; en = \" Using Docker volume: {0}\" }\n\n # --- Manager Workspace ---\n \"workspace.title\" = @{ zh = \"--- Manager 工作空间 ---\"; en = \"--- Manager Workspace ---\" }\n \"workspace.dir_prompt\" = @{ zh = \"Manager 工作空间目录 [{0}]\"; en = \"Manager workspace directory [{0}]\" }\n \"workspace.dir_label\" = @{ zh = \" Manager 工作空间: {0}\"; en = \" Manager workspace: {0}\" }\n\n # --- Host directory sharing ---\n \"host_share.prompt\" = @{ zh = \"与 Agent 共享的主机目录(默认: {0})\"; en = \"Host directory to share with agents (default: {0})\" }\n \"host_share.sharing\" = @{ zh = \"共享主机目录: {0} -> 容器内 /host-share\"; en = \"Sharing host directory: {0} -> /host-share in container\" }\n \"host_share.not_exist\" = @{ zh = \"警告: 主机目录 {0} 不存在,跳过验证继续使用\"; en = \"WARNING: Host directory {0} does not exist, using without validation\" }\n\n # --- Default worker runtime ---\n \"worker_runtime.title\" = @{ zh = \"--- 默认 Worker 运行时 ---\"; en = \"--- Default Worker Runtime ---\" }\n \"worker_runtime.openclaw\" = @{ zh = \"OpenClaw\"; en = \"OpenClaw\" }\n \"worker_runtime.copaw\" = @{ zh = \"CoPaw\"; en = \"CoPaw\" }\n \"worker_runtime.hermes\" = @{ zh = \"Hermes\"; en = \"Hermes\" }\n \"worker_runtime.choice\" = @{ zh = \"请选择 [1/2/3]\"; en = \"Enter choice [1/2/3]\" }\n \"worker_runtime.selected\" = @{ zh = \"默认 Worker 运行时: {0}\"; en = \"Default Worker runtime: {0}\" }\n \"worker_runtime.title_short\" = @{ zh = \"默认 Worker 运行时\"; en = \"Default Worker Runtime\" }\n\n # --- Manager runtime ---\n \"manager_runtime.title\" = @{ zh = \"--- Manager 运行时 ---\"; en = \"--- Manager Runtime ---\" }\n \"manager_runtime.openclaw\" = @{ zh = \"OpenClaw\"; en = \"OpenClaw\" }\n \"manager_runtime.copaw\" = @{ zh = \"CoPaw\"; en = \"CoPaw\" }\n \"manager_runtime.choice\" = @{ zh = \"请选择 [1/2]\"; en = \"Enter choice [1/2]\" }\n \"manager_runtime.selected\" = @{ zh = \"Manager 运行时: {0}\"; en = \"Manager runtime: {0}\" }\n \"manager_runtime.title_short\" = @{ zh = \"Manager 运行时\"; en = \"Manager Runtime\" }\n\n # --- Matrix E2EE ---\n \"matrix_e2ee.title\" = @{ zh = \"--- Matrix 端到端加密(E2EE)---\"; en = \"--- Matrix End-to-End Encryption (E2EE) ---\" }\n \"matrix_e2ee.desc\" = @{\n zh = \"E2EE 会对 Manager 与 Worker 之间的 Matrix 消息进行端到端加密。`n 启用后,即使 Matrix 服务器被入侵,消息内容也无法被窃取。`n 但 E2EE 会增加首次握手耗时,且要求所有 Agent 都支持 matrix-sdk-crypto。`n 如果不确定,建议保持禁用。`n ⚠ 注意:禁用 E2EE 后,请勿在 Element 上创建默认启用加密的 Private 房间,`n 否则 Agent 将无法读取该房间中的加密消息。请改用 Public 房间或关闭房间加密。\"\n en = \"E2EE encrypts Matrix messages between Manager and Workers end-to-end.`n When enabled, message content stays private even if the Matrix server is compromised.`n However, E2EE adds overhead to the initial handshake and requires all Agents`n to support matrix-sdk-crypto. If unsure, keep it disabled.`n ⚠ Note: When E2EE is disabled, do NOT create Private rooms in Element (which`n enable encryption by default) — Agents cannot read encrypted messages without`n E2EE support. Use Public rooms or turn off room encryption instead.\"\n }\n \"matrix_e2ee.enable\" = @{ zh = \"启用 E2EE\"; en = \"Enable E2EE\" }\n \"matrix_e2ee.disable\" = @{ zh = \"禁用 E2EE(推荐)\"; en = \"Disable E2EE (recommended)\" }\n \"matrix_e2ee.choice\" = @{ zh = \"请选择 [1/2]\"; en = \"Enter choice [1/2]\" }\n \"matrix_e2ee.selected_enabled\" = @{ zh = \"Matrix E2EE: 已启用\"; en = \"Matrix E2EE: enabled\" }\n \"matrix_e2ee.selected_disabled\" = @{ zh = \"Matrix E2EE: 已禁用(默认)\"; en = \"Matrix E2EE: disabled (default)\" }\n \"matrix_e2ee.title_short\" = @{ zh = \"Matrix E2EE\"; en = \"Matrix E2EE\" }\n \"matrix_e2ee.val_enabled\" = @{ zh = \"已启用\"; en = \"enabled\" }\n \"matrix_e2ee.val_disabled\" = @{ zh = \"已禁用\"; en = \"disabled\" }\n\n # --- Docker API proxy ---\n \"docker_proxy.title\" = @{ zh = \"--- Docker API 安全代理 ---\"; en = \"--- Docker API Security Proxy ---\" }\n \"docker_proxy.desc\" = @{ zh = \"Docker API 代理可防止 AI Agent 通过 Docker API 越狱访问宿主机。`n 启用后,Manager 不再直接持有 Docker socket,所有容器操作经过安全校验。\"; en = \"Docker API proxy prevents AI Agents from escaping via Docker API to access the host.`n When enabled, Manager no longer has direct Docker socket access; all container operations go through security validation.\" }\n \"docker_proxy.enable\" = @{ zh = \"启用(推荐)\"; en = \"Enable (recommended)\" }\n \"docker_proxy.disable\" = @{ zh = \"禁用(直接挂载 Docker socket)\"; en = \"Disable (mount Docker socket directly)\" }\n \"docker_proxy.choice\" = @{ zh = \"请选择 [1/2]\"; en = \"Enter choice [1/2]\" }\n \"docker_proxy.selected_enabled\" = @{ zh = \"Docker API 代理: 已启用\"; en = \"Docker API proxy: enabled\" }\n \"docker_proxy.selected_disabled\" = @{ zh = \"Docker API 代理: 已禁用\"; en = \"Docker API proxy: disabled\" }\n \"docker_proxy.title_short\" = @{ zh = \"Docker API 代理\"; en = \"Docker API Proxy\" }\n \"docker_proxy.val_enabled\" = @{ zh = \"已启用\"; en = \"enabled\" }\n \"docker_proxy.val_disabled\" = @{ zh = \"已禁用\"; en = \"disabled\" }\n \"docker_proxy.registries_desc\" = @{ zh = \"默认放行的镜像来源:本地镜像、localhost、Higress 仓库(所有 region)。`n 如需放行其他镜像仓库,请输入逗号分隔的地址前缀。`n 示例: ghcr.io/myorg,registry.example.com/team\"; en = \"Default allowed image sources: local images, localhost, Higress registries (all regions).`n To allow additional image sources, enter comma-separated address prefixes.`n Example: ghcr.io/myorg,registry.example.com/team\" }\n \"docker_proxy.registries_prompt\" = @{ zh = \"额外放行的镜像来源(按回车跳过)\"; en = \"Additional allowed image sources (press Enter to skip)\" }\n \"docker_proxy.registries_label\" = @{ zh = \"额外放行的镜像来源\"; en = \"Additional allowed image sources\" }\n\n # --- Worker idle timeout ---\n \"idle_timeout.prompt\" = @{ zh = \"Worker 空闲自动停止超时(分钟)[720]\"; en = \"Worker idle auto-stop timeout in minutes [720]\" }\n \"idle_timeout.selected\" = @{ zh = \"Worker 空闲超时: {0} 分钟\"; en = \"Worker idle timeout: {0} minutes\" }\n \"idle_timeout.label\" = @{ zh = \"Worker 空闲超时(分钟)\"; en = \"Worker idle timeout (min)\" }\n\n # --- Secrets and config ---\n \"install.generating_secrets\" = @{ zh = \"正在生成密钥...\"; en = \"Generating secrets...\" }\n \"install.config_saved\" = @{ zh = \"配置已保存到 {0}\"; en = \"Configuration saved to {0}\" }\n\n # --- Container runtime socket ---\n \"install.socket_detected\" = @{ zh = \"容器运行时 socket: {0}(已启用直接创建 Worker)\"; en = \"Container runtime socket: {0} (direct Worker creation enabled)\" }\n \"install.socket_not_found\" = @{ zh = \"未找到容器运行时 socket(Manager 无法直接创建 Worker 容器,需要你手动执行 docker 命令创建)\"; en = \"No container runtime socket found (Manager cannot create Worker containers directly, you will need to create them manually using docker commands)\" }\n \"install.socket_confirm.title\" = @{ zh = \"警告: 未检测到容器运行时 Socket\"; en = \"WARNING: Container Runtime Socket Not Detected\" }\n \"install.socket_confirm.message\" = @{ zh = \"未找到 Docker/Podman socket,Manager 将无法自动创建 Worker 容器。`n你需要手动执行 docker run 命令来创建 Worker。`n`n是否继续安装?\"; en = \"Docker/Podman socket not found. Manager will not be able to create Worker containers automatically.`nYou will need to manually run docker commands to create Workers.`n`nContinue installation?\" }\n \"install.socket_confirm.prompt\" = @{ zh = \"继续安装? [y/N]: \"; en = \"Continue? [y/N]: \" }\n \"install.socket_confirm.cancelled\" = @{ zh = \"安装已取消。如需启用 Worker 自动创建,请确保 Docker/Podman 正在运行,然后重新运行安装脚本。\"; en = \"Installation cancelled. To enable automatic Worker creation, ensure Docker/Podman is running and re-run the installer.\" }\n\n # --- Container management ---\n \"install.removing_existing\" = @{ zh = \"正在移除现有 agentteams-manager 容器...\"; en = \"Removing existing agentteams-manager container...\" }\n\n # --- YOLO mode ---\n \"install.yolo\" = @{ zh = \"YOLO 模式已启用(自主决策,无交互提示)\"; en = \"YOLO mode enabled (autonomous decisions, no interactive prompts)\" }\n\n # --- Image pulling ---\n \"install.image.exists\" = @{ zh = \"Manager 镜像已存在: {0}\"; en = \"Manager image already exists locally: {0}\" }\n \"install.image.pulling_manager\" = @{ zh = \"正在拉取 Manager 镜像: {0}\"; en = \"Pulling Manager image: {0}\" }\n \"install.image.worker_exists\" = @{ zh = \"Worker 镜像已存在: {0}\"; en = \"Worker image already exists locally: {0}\" }\n \"install.image.pulling_worker\" = @{ zh = \"正在拉取 Worker 镜像: {0}\"; en = \"Pulling Worker image: {0}\" }\n\n # --- Starting container ---\n \"install.starting_manager\" = @{ zh = \"正在启动 Manager 容器...\"; en = \"Starting Manager container...\" }\n\n # --- Wait for Manager ready ---\n \"install.wait_ready\" = @{ zh = \"等待 Manager Agent 就绪(超时: {0}s)...\"; en = \"Waiting for Manager agent to be ready (timeout: {0}s)...\" }\n \"install.wait_ready.ok\" = @{ zh = \"Manager Agent 已就绪!\"; en = \"Manager agent is ready!\" }\n \"install.wait_ready.waiting\" = @{ zh = \"等待中... ({0}s/{1}s)\"; en = \"Waiting... ({0}s/{1}s)\" }\n \"install.wait_ready.timeout\" = @{ zh = \"Manager Agent 在 {0}s 内未就绪。请检查: docker logs {1}\"; en = \"Manager agent did not become ready within {0}s. Check: docker logs {1}\" }\n\n # --- Wait for Matrix ready ---\n \"install.wait_matrix\" = @{ zh = \"等待 Matrix 服务就绪(超时: {0}s)...\"; en = \"Waiting for Matrix server to be ready (timeout: {0}s)...\" }\n \"install.wait_matrix.ok\" = @{ zh = \"Matrix 服务已就绪!\"; en = \"Matrix server is ready!\" }\n \"install.wait_matrix.waiting\" = @{ zh = \"等待 Matrix 中... ({0}s/{1}s)\"; en = \"Waiting for Matrix... ({0}s/{1}s)\" }\n \"install.wait_matrix.timeout\" = @{ zh = \"Matrix 服务在 {0}s 内未就绪。请检查: docker logs {1}\"; en = \"Matrix server did not become ready within {0}s. Check: docker logs {1}\" }\n\n # --- OpenAI-compatible connectivity test ---\n \"llm.openai.test.testing\" = @{ zh = \"正在测试 API 联通性...\"; en = \"Testing API connectivity...\" }\n \"llm.openai.test.ok\" = @{ zh = \"API 联通性测试通过\"; en = \"API connectivity test passed\" }\n \"llm.openai.test.fail\" = @{ zh = \"API 联通性测试失败(HTTP {0})。响应内容:`n{1}`n请根据以上错误信息联系您的模型服务商解决。\"; en = \"API connectivity test failed (HTTP {0}). Response body:`n{1}`nPlease contact your model provider to resolve the issue.\" }\n \"llm.openai.test.fail.tokenplan\" = @{ zh = \"提示: 请确认 API Key 有效且已开通通义 Token 套餐。文档: https://help.aliyun.com/zh/model-studio/token-plan-quickstart\"; en = \"Hint: Verify your Token Plan API key and compatible-mode access. Docs: https://help.aliyun.com/zh/model-studio/token-plan-quickstart\" }\n \"llm.openai.test.fail.codingplan\" = @{ zh = \"提示: 请确认 API Key 有效且已开通通义 Token 套餐。文档: https://help.aliyun.com/zh/model-studio/token-plan-quickstart\"; en = \"Hint: Verify your DASHSCOPE_API_KEY for Qwen Cloud. API keys: https://home.qwencloud.com/api-keys Docs: https://docs.qwencloud.com/\" }\n \"llm.openai.test.fail.codingplan_legacy\" = @{ zh = \"提示: 请确认 API Key 有效且 Coding 套餐接口可用。文档: https://help.aliyun.com/zh/model-studio/get-api-key\"; en = \"Hint: Verify your DashScope API key and Coding Plan access. Docs: https://help.aliyun.com/zh/model-studio/get-api-key\" }\n \"llm.openai.test.confirm\" = @{ zh = \"是否仍要继续安装?[y/N/b]\"; en = \"Continue with installation anyway? [y/N/b]\" }\n \"llm.openai.test.aborted\" = @{ zh = \"安装已中止。\"; en = \"Installation aborted.\" }\n \"llm.embedding.title\" = @{ zh = \"📦 记忆搜索配置\"; en = \"📦 Memory Search Configuration\" }\n \"llm.embedding.hint\" = @{ zh = \" Embedding 模型可提升记忆搜索质量(语义匹配)。不启用也可正常使用记忆功能(关键词匹配)。\"; en = \" Embedding model improves memory search quality (semantic matching). Memory still works without it (keyword matching).\" }\n \"llm.embedding.option.default\" = @{ zh = \" 1) text-embedding-v4(推荐)\"; en = \" 1) text-embedding-v4 (Recommended)\" }\n \"llm.embedding.option.custom\" = @{ zh = \" 2) 自定义 Embedding 模型\"; en = \" 2) Custom embedding model\" }\n \"llm.embedding.option.disable\" = @{ zh = \" 3) 不启用\"; en = \" 3) Do not enable\" }\n \"llm.embedding.select\" = @{ zh = \"选择\"; en = \"Select\" }\n \"llm.embedding.custom_prompt\" = @{ zh = \" Embedding 模型名称\"; en = \" Embedding model name\" }\n \"llm.embedding.test.testing\" = @{ zh = \"正在测试 Embedding API 联通性...\"; en = \"Testing Embedding API connectivity...\" }\n \"llm.embedding.test.ok\" = @{ zh = \"✅ Embedding API 联通性测试通过\"; en = \"✅ Embedding API connectivity test passed\" }\n \"llm.embedding.test.fail\" = @{ zh = \"⚠️ Embedding API 测试失败(HTTP {0})。响应: {1}\"; en = \"⚠️ Embedding API test failed (HTTP {0}). Response: {1}\" }\n \"llm.embedding.auto_disabled\" = @{ zh = \"⚠️ Embedding 已自动禁用,记忆搜索将使用关键词匹配。您可以稍后在 agentteams-manager.env 中设置 AGENTTEAMS_EMBEDDING_MODEL 启用。\"; en = \"⚠️ Embedding auto-disabled. Memory search will use keyword matching. You can enable it later in agentteams-manager.env by setting AGENTTEAMS_EMBEDDING_MODEL.\" }\n \"llm.embedding.disabled\" = @{ zh = \"ℹ️ Embedding 已禁用,记忆搜索将使用关键词匹配。\"; en = \"ℹ️ Embedding disabled. Memory search will use keyword matching.\" }\n \"nav.back_hint\" = @{ zh = \"(输入 b 返回上一步)\"; en = \"(enter b to go back)\" }\n # --- OpenAI-compatible provider creation ---\n \"install.openai_compat.missing\" = @{ zh = \"警告: OpenAI Base URL 或 API Key 未设置,跳过提供商创建\"; en = \"WARNING: OpenAI Base URL or API Key not set, skipping provider creation\" }\n \"install.openai_compat.creating\" = @{ zh = \"正在创建 OpenAI 兼容提供商...\"; en = \"Creating OpenAI-compatible provider...\" }\n \"install.openai_compat.domain\" = @{ zh = \" 域名: {0}\"; en = \" Domain: {0}\" }\n \"install.openai_compat.port\" = @{ zh = \" 端口: {0}\"; en = \" Port: {0}\" }\n \"install.openai_compat.protocol\" = @{ zh = \" 协议: {0}\"; en = \" Protocol: {0}\" }\n \"install.openai_compat.service_fail\" = @{ zh = \"警告: 创建 DNS 服务源失败(可能已存在)\"; en = \"WARNING: Failed to create DNS service source (may already exist)\" }\n \"install.openai_compat.provider_fail\" = @{ zh = \"警告: 创建 AI 提供商失败(可能已存在)\"; en = \"WARNING: Failed to create AI provider (may already exist)\" }\n \"install.openai_compat.success\" = @{ zh = \"OpenAI 兼容提供商创建成功\"; en = \"OpenAI-compatible provider created successfully\" }\n\n # --- Welcome message ---\n \"install.welcome_msg.soul_configured\" = @{ zh = \"Soul 已配置(找到 soul-configured 标记),跳过 onboarding 消息\"; en = \"Soul already configured (soul-configured marker found), skipping onboarding message\" }\n \"install.welcome_msg.logging_in\" = @{ zh = \"正在以 {0} 身份登录以发送欢迎消息...\"; en = \"Logging in as {0} to send welcome message...\" }\n \"install.welcome_msg.login_failed\" = @{ zh = \"警告: 以 {0} 身份登录失败,跳过欢迎消息\"; en = \"WARNING: Failed to login as {0}, skipping welcome message\" }\n \"install.welcome_msg.finding_room\" = @{ zh = \"正在查找与 Manager 的 DM 房间...\"; en = \"Finding DM room with Manager...\" }\n \"install.welcome_msg.creating_room\" = @{ zh = \"正在创建与 Manager 的 DM 房间...\"; en = \"Creating DM room with Manager...\" }\n \"install.welcome_msg.no_room\" = @{ zh = \"警告: 无法找到或创建与 Manager 的 DM 房间\"; en = \"WARNING: Could not find or create DM room with Manager\" }\n \"install.welcome_msg.waiting_join\" = @{ zh = \"等待 Manager 加入房间...\"; en = \"Waiting for Manager to join the room...\" }\n \"install.welcome_msg.sending\" = @{ zh = \"正在向 Manager 发送欢迎消息...\"; en = \"Sending welcome message to Manager...\" }\n \"install.welcome_msg.send_failed\" = @{ zh = \"警告: 发送欢迎消息失败\"; en = \"WARNING: Failed to send welcome message\" }\n \"install.welcome_msg.sent\" = @{ zh = \"欢迎消息已发送给 Manager\"; en = \"Welcome message sent to Manager\" }\n \"install.welcome_msg.waiting\" = @{ zh = \"等待 Manager 发送欢迎消息(Higress 路由授权 + LLM 探活,约 45-90s)...\"; en = \"Waiting for Manager to send the welcome message (Higress route auth + LLM probe, ~45-90s)...\" }\n \"install.welcome_msg.confirmed\" = @{ zh = \"Manager 已确认发送欢迎消息(status.welcomeSent=true,用时 {0}s)\"; en = \"Manager confirmed welcome message sent (status.welcomeSent=true, {0}s elapsed)\" }\n \"install.welcome_msg.timeout\" = @{ zh = \"警告: 在 {0}s 内未观察到 Manager 发送欢迎消息(status.welcomeSent=true)。安装仍然成功,所有服务已就绪——可继续按下方提示登录 Element Web。\"; en = \"WARNING: Did not observe the Manager sending its welcome message (status.welcomeSent=true) within {0}s. Installation is still successful, all services are up — continue with the Element Web instructions below.\" }\n \"install.welcome_msg.timeout_hint\" = @{ zh = \"手动触发 onboarding: 登录 Element Web → 打开与 Manager 的 DM 房间 → 发送任意一句话(例如 `\"hi`\"),Manager 会接管对话并开始引导。\"; en = \"Manual onboarding: log in to Element Web -> open the DM with the Manager -> send any message (e.g. `\"hi`\") and the Manager will take over and start the guided setup.\" }\n \"install.welcome_msg.timeout_inspect\" = @{ zh = \"排查命令: docker exec agentteams-controller agt get managers default\"; en = \"Inspect status: docker exec agentteams-controller agt get managers default\" }\n \"install.welcome_msg.poll_unavailable\" = @{ zh = \"提示: agentteams-manager 内未找到 AgentTeams CLI,跳过 welcome 等待(旧镜像?)\"; en = \"Note: AgentTeams CLI not found inside agentteams-manager; skipping welcome wait (old image?)\" }\n\n # --- Final output panel ---\n \"success.title\" = @{ zh = \"=== AgentTeams Manager 已启动!===\"; en = \"=== AgentTeams Manager Started! ===\" }\n \"success.domains_configured\" = @{ zh = \"以下域名已配置解析到 127.0.0.1:\"; en = \"The following domains are configured to resolve to 127.0.0.1:\" }\n \"success.open_url\" = @{ zh = \" 在浏览器中打开以下 URL 开始使用: \"; en = \" Open the following URL in your browser to start: \" }\n \"success.login_with\" = @{ zh = \" 登录信息:\"; en = \" Login with:\" }\n \"success.username\" = @{ zh = \" 用户名: {0}\"; en = \" Username: {0}\" }\n \"success.password\" = @{ zh = \" 密码: {0}\"; en = \" Password: {0}\" }\n \"success.after_login\" = @{ zh = \" 登录后,开始与 Manager 聊天!\"; en = \" After login, start chatting with the Manager!\" }\n \"success.tell_it\" = @{ zh = \" 告诉它: `\"创建一个名为 alice 的前端开发 Worker`\"\"; en = \" Tell it: `\"Create a Worker named alice for frontend dev`\"\" }\n \"success.manager_auto\" = @{ zh = \" Manager 会自动处理一切。\"; en = \" The Manager will handle everything automatically.\" }\n \"success.mobile_title\" = @{ zh = \" 移动端访问(FluffyChat / Element Mobile):\"; en = \" Mobile access (FluffyChat / Element Mobile):\" }\n \"success.mobile_step1\" = @{ zh = \" 1. 在手机上下载 FluffyChat 或 Element\"; en = \" 1. Download FluffyChat or Element on your phone\" }\n \"success.mobile_step2\" = @{ zh = \" 2. 设置 homeserver 为: {0}\"; en = \" 2. Set homeserver to: {0}\" }\n \"success.mobile_step2_noip\" = @{ zh = \" 2. 设置 homeserver 为: http://<本机局域网IP>:{0}\"; en = \" 2. Set homeserver to: http://<this-machine-LAN-IP>:{0}\" }\n \"success.mobile_noip_hint\" = @{ zh = \" (无法自动检测局域网 IP — 请使用 ifconfig / ip addr 查看)\"; en = \" (Could not detect LAN IP automatically — check with: ifconfig / ip addr)\" }\n \"success.mobile_step3\" = @{ zh = \" 3. 登录信息:\"; en = \" 3. Login with:\" }\n \"success.mobile_username\" = @{ zh = \" 用户名: {0}\"; en = \" Username: {0}\" }\n \"success.mobile_password\" = @{ zh = \" 密码: {0}\"; en = \" Password: {0}\" }\n\n # --- Other consoles and tips ---\n \"success.other_consoles\" = @{ zh = \"--- 其他控制台 ---\"; en = \"--- Other Consoles ---\" }\n \"success.higress_console\" = @{ zh = \" Higress 控制台: http://localhost:{0}(用户名: {1} / 密码: {2})\"; en = \" Higress Console: http://localhost:{0} (Username: {1} / Password: {2})\" }\n \"success.manager_console\" = @{ zh = \" Manager 控制台(本地): http://localhost:{0}(无需登录)\"; en = \" Manager Console (local): http://localhost:{0} (no login required)\" }\n \"success.manager_console_gateway\" = @{ zh = \" Manager 控制台(网关): http://console-local.agentteams.io(用户名: {0} / 密码: {1})\"; en = \" Manager Console (gateway): http://console-local.agentteams.io (Username: {0} / Password: {1})\" }\n \"success.copaw_console\" = @{ zh = \" CoPaw 控制台(本地): http://localhost:{0}(无需登录)\"; en = \" CoPaw Console (local): http://localhost:{0} (no login required)\" }\n \"success.switch_llm.title\" = @{ zh = \"--- 切换 LLM 提供商 ---\"; en = \"--- Switch LLM Providers ---\" }\n \"success.switch_llm.hint\" = @{ zh = \" 您可以通过 Higress 控制台切换到其他 LLM 提供商(OpenAI、Anthropic 等)。\"; en = \" You can switch to other LLM providers (OpenAI, Anthropic, etc.) via Higress Console.\" }\n \"success.switch_llm.docs\" = @{ zh = \" 详细说明请参阅:\"; en = \" For detailed instructions, see:\" }\n \"success.switch_llm.url\" = @{ zh = \" https://higress.ai/en/docs/ai/scene-guide/multi-proxy#console-configuration\"; en = \" https://higress.ai/en/docs/ai/scene-guide/multi-proxy#console-configuration\" }\n \"success.tip\" = @{ zh = \"提示: 您也可以在聊天中让 Manager 为您配置 LLM 提供商。\"; en = \"Tip: You can also ask the Manager to configure LLM providers for you in the chat.\" }\n \"success.config_file\" = @{ zh = \"配置文件: {0}\"; en = \"Configuration file: {0}\" }\n \"success.data_volume\" = @{ zh = \"数据卷: {0}\"; en = \"Data volume: {0}\" }\n \"success.workspace\" = @{ zh = \"Manager 工作空间: {0}\"; en = \"Manager workspace: {0}\" }\n\n # --- Worker installation ---\n \"worker.resetting\" = @{ zh = \"正在重置 Worker: {0}...\"; en = \"Resetting Worker: {0}...\" }\n \"worker.exists\" = @{ zh = \"容器 '{0}' 已存在。使用 --reset 重新创建。\"; en = \"Container '{0}' already exists. Use --reset to recreate.\" }\n \"worker.starting\" = @{ zh = \"正在启动 Worker: {0}...\"; en = \"Starting Worker: {0}...\" }\n \"worker.skills_url\" = @{ zh = \" Skills API URL: {0}\"; en = \" Skills API URL: {0}\" }\n \"worker.started\" = @{ zh = \"=== Worker {0} 已启动!===\"; en = \"=== Worker {0} Started! ===\" }\n \"worker.container\" = @{ zh = \"容器: {0}\"; en = \"Container: {0}\" }\n \"worker.view_logs\" = @{ zh = \"查看日志: docker logs -f {0}\"; en = \"View logs: docker logs -f {0}\" }\n\n # --- Prompt function messages ---\n \"prompt.preset\" = @{ zh = \" {0} = (已通过环境变量预设)\"; en = \" {0} = (pre-set via env)\" }\n \"prompt.upgrade_keep\" = @{ zh = \" {0} = {1}(当前值,回车保留 / 输入新值覆盖)\"; en = \" {0} = {1} (current value, press Enter to keep / type new value to change)\" }\n \"prompt.upgrade_empty\" = @{ zh = \" {0} = (未设置,回车跳过 / 输入新值设置)\"; en = \" {0} = (not set, press Enter to skip / type new value to set)\" }\n \"prompt.default\" = @{ zh = \" {0} = {1}(默认)\"; en = \" {0} = {1} (default)\" }\n \"prompt.required\" = @{ zh = \"{0} 是必需的(在非交互模式下通过环境变量设置)\"; en = \"{0} is required (set via environment variable in non-interactive mode)\" }\n \"prompt.required_empty\" = @{ zh = \"{0} 是必需的\"; en = \"{0} is required\" }\n\n # --- Language switch prompt (bilingual by design) ---\n \"lang.detected.zh\" = @{ zh = \"检测到语言 / Detected language: 中文\"; en = \"检测到语言 / Detected language: 中文\" }\n \"lang.detected.en\" = @{ zh = \"检测到语言 / Detected language: English\"; en = \"检测到语言 / Detected language: English\" }\n \"lang.switch_title\" = @{ zh = \"切换语言 / Switch language:\"; en = \"切换语言 / Switch language:\" }\n \"lang.option_zh\" = @{ zh = \" 1) 中文\"; en = \" 1) 中文\" }\n \"lang.option_en\" = @{ zh = \" 2) English\"; en = \" 2) English\" }\n \"lang.prompt\" = @{ zh = \"请选择 / Enter choice\"; en = \"请选择 / Enter choice\" }\n\n # --- Error messages ---\n \"error.name_required\" = @{ zh = \"--name 是必需的\"; en = \"--name is required\" }\n \"error.fs_required\" = @{ zh = \"--fs 是必需的\"; en = \"--fs is required\" }\n \"error.fs_key_required\" = @{ zh = \"--fs-key 是必需的\"; en = \"--fs-key is required\" }\n \"error.fs_secret_required\" = @{ zh = \"--fs-secret 是必需的\"; en = \"--fs-secret is required\" }\n \"error.unknown_option\" = @{ zh = \"未知选项: {0}\"; en = \"Unknown option: {0}\" }\n \"error.docker_not_running\" = @{ zh = \"Docker 未运行。请先启动 Docker Desktop 或 Podman Desktop。\"; en = \"Docker is not running. Please start Docker Desktop or Podman Desktop first.\" }\n \"error.docker_not_found\" = @{ zh = \"未找到 docker 或 podman 命令。请先安装 Docker Desktop 或 Podman Desktop:`n Docker Desktop: https://www.docker.com/products/docker-desktop/`n Podman Desktop: https://podman-desktop.io/\"; en = \"docker or podman command not found. Please install Docker Desktop or Podman Desktop first:`n Docker Desktop: https://www.docker.com/products/docker-desktop/`n Podman Desktop: https://podman-desktop.io/\" }\n\n # --- Uninstall messages ---\n \"uninstall.title\" = @{ zh = \"正在卸载 AgentTeams...\"; en = \"Uninstalling AgentTeams...\" }\n \"uninstall.stopping_manager\" = @{ zh = \"正在停止并移除 agentteams-manager...\"; en = \"Stopping and removing agentteams-manager...\" }\n \"uninstall.stopping_workers\" = @{ zh = \"正在停止并移除 worker 容器...\"; en = \"Stopping and removing worker containers...\" }\n \"uninstall.removed\" = @{ zh = \" 已移除: {0}\"; en = \" Removed: {0}\" }\n \"uninstall.removing_volume\" = @{ zh = \"正在移除 Docker 卷: agentteams-data\"; en = \"Removing Docker volume: agentteams-data\" }\n \"uninstall.removing_env\" = @{ zh = \"正在移除 env 文件: {0}\"; en = \"Removing env file: {0}\" }\n \"uninstall.removing_proxy\" = @{ zh = \"正在停止并移除 Docker API 代理容器: agentteams-docker-proxy\"; en = \"Stopping and removing Docker API proxy container: agentteams-docker-proxy\" }\n \"uninstall.stopping_controller\" = @{ zh = \"正在停止并移除 agentteams-controller (内嵌 Tuwunel/MinIO/Higress)...\"; en = \"Stopping and removing agentteams-controller (embedded Tuwunel/MinIO/Higress)...\" }\n \"uninstall.removing_network\" = @{ zh = \"正在移除 Docker 网络: agentteams-net\"; en = \"Removing Docker network: agentteams-net\" }\n \"uninstall.removing_workspace\" = @{ zh = \"正在移除工作空间目录: {0}\"; en = \"Removing workspace directory: {0}\" }\n \"uninstall.removing_log\" = @{ zh = \"正在移除日志文件: {0}\"; en = \"Removing log file: {0}\" }\n \"uninstall.done\" = @{ zh = \"AgentTeams 已卸载。\"; en = \"AgentTeams has been uninstalled.\" }\n}\n\n# Get-Msg: look up message by key, with -f style argument substitution.\n# Falls back to English if the current language translation is missing.\nfunction Get-Msg {\n param(\n [Parameter(Mandatory)][string]$Key,\n [object[]]$f\n )\n $lang = $script:AGENTTEAMS_LANGUAGE\n if (-not $lang) { $lang = \"en\" }\n $entry = $script:Messages[$Key]\n if (-not $entry) { return $Key }\n $text = $entry[$lang]\n if (-not $text) { $text = $entry[\"en\"] }\n if (-not $text) { return $Key }\n if ($f) { return ($text -f $f) }\n return $text\n}\n\nfunction Get-LanIP {\n # Detect local LAN IP address on Windows\n try {\n # Get network adapters with IPv4 addresses, prefer connected/active interfaces\n $adapters = Get-NetIPAddress -AddressFamily IPv4 -ErrorAction SilentlyContinue |\n Where-Object {\n $_.IPAddress -notlike \"127.*\" -and\n $_.PrefixOrigin -ne \"WellKnown\" -and\n $_.InterfaceAlias -notlike \"*Loopback*\"\n } |\n Sort-Object {\n # PowerShell 5.1 compatible: use if-else instead of ternary operator\n if ($_.InterfaceAlias -like \"*Wi-Fi*\" -or $_.InterfaceAlias -like \"*Ethernet*\") { 0 } else { 1 }\n }\n\n if ($adapters) {\n return $adapters[0].IPAddress\n }\n\n # Fallback: use ipconfig\n $ipconfig = ipconfig 2>$null\n $ip = ($ipconfig | Select-String \"IPv4 Address.*?: (\\d+\\.\\d+\\.\\d+\\.\\d+)\" | Select-Object -First 1)\n if ($ip -match \"(\\d+\\.\\d+\\.\\d+\\.\\d+)\") {\n return $Matches[1]\n }\n }\n catch {\n # Ignore errors\n }\n\n return \"\"\n}\n\n# Known models list — used to detect custom models during install\n$script:KnownModels = @(\n \"gpt-5.4\", \"gpt-5.3-codex\", \"gpt-5-mini\", \"gpt-5-nano\",\n \"claude-opus-4-6\", \"claude-sonnet-4-6\", \"claude-haiku-4-5\",\n \"qwen3.6-plus\", \"qwen3.5-plus\", \"deepseek-chat\", \"deepseek-reasoner\",\n \"kimi-k2.5\", \"glm-5\", \"MiniMax-M2.7\", \"MiniMax-M2.7-highspeed\", \"MiniMax-M2.5\"\n)\n\nfunction Test-KnownModel {\n param([string]$Model)\n return $script:KnownModels -contains $Model\n}\n\nfunction Request-CustomModelParams {\n param([string]$Model)\n if (Test-KnownModel $Model) {\n $script:config.MODEL_CONTEXT_WINDOW = \"\"\n $script:config.MODEL_MAX_TOKENS = \"\"\n $script:config.MODEL_REASONING = \"\"\n $script:config.MODEL_VISION = \"\"\n return\n }\n Write-Host \"\"\n Write-Log (Get-Msg \"llm.custom_model.detected\" -f $Model)\n Write-Host \"\"\n $ctxInput = Read-Host \" $(Get-Msg 'llm.custom_model.context_prompt')\"\n if ($ctxInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $script:config.MODEL_CONTEXT_WINDOW = if ($ctxInput) { $ctxInput } else { \"150000\" }\n $maxInput = Read-Host \" $(Get-Msg 'llm.custom_model.max_tokens_prompt')\"\n if ($maxInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $script:config.MODEL_MAX_TOKENS = if ($maxInput) { $maxInput } else { \"128000\" }\n $reasoningInput = Read-Host \" $(Get-Msg 'llm.custom_model.reasoning_prompt')\"\n if ($reasoningInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $script:config.MODEL_REASONING = if ($reasoningInput -match \"^[nN]\") { \"false\" } else { \"true\" }\n $visionInput = Read-Host \" $(Get-Msg 'llm.custom_model.vision_prompt')\"\n if ($visionInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $script:config.MODEL_VISION = if ($visionInput -match \"^[yY]\") { \"true\" } else { \"false\" }\n Write-Log (Get-Msg \"llm.custom_model.summary\" -f $script:config.MODEL_CONTEXT_WINDOW, $script:config.MODEL_MAX_TOKENS, $script:config.MODEL_REASONING, $script:config.MODEL_VISION)\n}\n\nfunction New-RandomKey {\n # Generate 64 character hex string (32 bytes)\n $bytes = New-Object byte[] 32\n $rng = [System.Security.Cryptography.RandomNumberGenerator]::Create()\n $rng.GetBytes($bytes)\n return [BitConverter]::ToString($bytes).Replace(\"-\", \"\").ToLower()\n}\n\nfunction ConvertTo-DockerPath {\n param([string]$Path)\n\n # Convert Windows path to Docker mount format\n $fullPath = (Resolve-Path $Path -ErrorAction SilentlyContinue).Path\n if (-not $fullPath) {\n $fullPath = $Path\n }\n\n # Convert C:\\path to /c/path format for Docker\n if ($fullPath -match \"^([A-Za-z]):\") {\n $drive = $Matches[1].ToLower()\n $rest = $fullPath.Substring(2).Replace(\"\\\", \"/\")\n return \"/$drive$rest\"\n }\n return $fullPath.Replace(\"\\\", \"/\")\n}\n\n# Resolve the embedded controller image. Embedded mode is the only supported\n# architecture since PR #616 (manager image no longer bundles Higress/Tuwunel/MinIO).\n# Mirrors install/agentteams-install.sh::resolve_embedded_image — fail fast when the\n# embedded image is unavailable rather than silently falling back to the broken\n# legacy single-container path.\n# Sets $script:EMBEDDED_IMAGE and $script:AGENTTEAMS_USE_EMBEDDED.\nfunction Resolve-EmbeddedImage {\n $script:AGENTTEAMS_USE_EMBEDDED = \"1\"\n\n # Explicit override always wins (used by `make install-embedded` for local builds).\n if ($env:AGENTTEAMS_INSTALL_EMBEDDED_IMAGE) {\n $script:EMBEDDED_IMAGE = $env:AGENTTEAMS_INSTALL_EMBEDDED_IMAGE\n return\n }\n\n $versioned = \"$($script:AGENTTEAMS_REGISTRY)/agentteams/agentteams-embedded:$($script:AGENTTEAMS_VERSION)\"\n $latestTag = \"$($script:AGENTTEAMS_REGISTRY)/agentteams/agentteams-embedded:latest\"\n\n if ($script:AGENTTEAMS_VERSION -eq \"latest\") {\n $script:EMBEDDED_IMAGE = $latestTag\n return\n }\n\n docker pull $versioned *>$null\n if ($LASTEXITCODE -eq 0) {\n $script:EMBEDDED_IMAGE = $versioned\n return\n }\n docker pull $latestTag *>$null\n if ($LASTEXITCODE -eq 0) {\n Write-Log \"embedded $($script:AGENTTEAMS_VERSION) not found, using latest\"\n $script:EMBEDDED_IMAGE = $latestTag\n return\n }\n\n # Escape hatch for older versions (AGENTTEAMS_VERSION <= v1.0.9) whose manager image\n # still bundled the infrastructure — opt-in only, never silent.\n if ($env:AGENTTEAMS_FORCE_LEGACY -eq \"1\") {\n Write-Log \"WARNING: AGENTTEAMS_FORCE_LEGACY=1 - using legacy all-in-one manager architecture.\"\n Write-Log \"WARNING: This requires AGENTTEAMS_VERSION <= v1.0.9 (older bundled manager image).\"\n Write-Log \"WARNING: Newer slim manager images will hang on 'Waiting for Higress Gateway'.\"\n $script:AGENTTEAMS_USE_EMBEDDED = \"0\"\n return\n }\n\n Write-Host \"$($script:ESC)[31m[AgentTeams ERROR]$($script:ESC)[0m Embedded controller image is not available in the registry:\" -ForegroundColor Red\n Write-Host \" - tried: $versioned\" -ForegroundColor Red\n Write-Host \" - tried: $latestTag\" -ForegroundColor Red\n Write-Host \"\" -ForegroundColor Red\n Write-Host \"Embedded mode is the only supported architecture since PR #616.\" -ForegroundColor Red\n Write-Host \"How to resolve:\" -ForegroundColor Red\n Write-Host \" 1) Pin to a AGENTTEAMS_VERSION whose embedded image has been published, or\" -ForegroundColor Red\n Write-Host \" wait for the release pipeline to publish it.\" -ForegroundColor Red\n Write-Host \" 2) For a local build, run: make install-embedded\" -ForegroundColor Red\n Write-Host \" (builds and uses the local embedded image without touching the registry).\" -ForegroundColor Red\n Write-Host \" 3) Override with a custom image:\" -ForegroundColor Red\n Write-Host \" `$env:AGENTTEAMS_INSTALL_EMBEDDED_IMAGE=...; .\\agentteams-install.ps1\" -ForegroundColor Red\n Exit-Script 1\n}\n\nfunction Wait-ManagerReady {\n param(\n [string]$Container = \"agentteams-manager\",\n [int]$Timeout = $(if ($env:AGENTTEAMS_READY_TIMEOUT) { [int]$env:AGENTTEAMS_READY_TIMEOUT } else { 300 })\n )\n\n $elapsed = 0\n Write-Log (Get-Msg \"install.wait_ready\" -f $Timeout)\n\n $runtime = if ($script:config.MANAGER_RUNTIME) { $script:config.MANAGER_RUNTIME } else { \"copaw\" }\n\n while ($elapsed -lt $Timeout) {\n try {\n switch ($runtime) {\n \"copaw\" {\n $result = docker exec $Container curl -sf http://127.0.0.1:18799/api/agents 2>$null\n if ($result -match '\"agents\"') {\n Write-Log (Get-Msg \"install.wait_ready.ok\")\n return $true\n }\n }\n default {\n $result = docker exec $Container openclaw gateway health --json 2>$null\n if ($result -match '\"ok\"') {\n Write-Log (Get-Msg \"install.wait_ready.ok\")\n return $true\n }\n }\n }\n } catch {\n # Ignore errors during polling\n }\n\n Start-Sleep -Seconds 5\n $elapsed += 5\n Write-Host \"`r$($script:ESC)[36m[AgentTeams]$($script:ESC)[0m $(Get-Msg 'install.wait_ready.waiting' -f $elapsed, $Timeout)\" -NoNewline\n }\n\n Write-Host \"\"\n Write-Error (Get-Msg \"install.wait_ready.timeout\" -f $Timeout, $Container)\n}\n\nfunction Wait-MatrixReady {\n param(\n [string]$Container = \"agentteams-manager\",\n [int]$Timeout = $(if ($env:AGENTTEAMS_READY_TIMEOUT) { [int]$env:AGENTTEAMS_READY_TIMEOUT } else { 300 })\n )\n\n $elapsed = 0\n Write-Log (Get-Msg \"install.wait_matrix\" -f $Timeout)\n\n while ($elapsed -lt $Timeout) {\n try {\n $result = docker exec $Container curl -sf http://127.0.0.1:6167/_tuwunel/server_version 2>$null\n if ($result) {\n Write-Log (Get-Msg \"install.wait_matrix.ok\")\n return $true\n }\n } catch {\n # Ignore errors during polling\n }\n\n Start-Sleep -Seconds 5\n $elapsed += 5\n Write-Host \"`r$($script:ESC)[36m[AgentTeams]$($script:ESC)[0m $(Get-Msg 'install.wait_matrix.waiting' -f $elapsed, $Timeout)\" -NoNewline\n }\n\n Write-Host \"\"\n Write-Error (Get-Msg \"install.wait_matrix.timeout\" -f $Timeout, $Container)\n}\n\n# Read KEY=value from /data/agentteams-secrets.env on a Docker volume (manager container not required).\n# Requires $script:EMBEDDED_IMAGE (set by Resolve-EmbeddedImage before Install-Manager uses this).\nfunction Read-AgentTeamsSecretFromDataVolume {\n param(\n [string]$VolumeName,\n [string]$Key\n )\n if ([string]::IsNullOrEmpty($VolumeName) -or [string]::IsNullOrEmpty($Key) -or [string]::IsNullOrEmpty($script:EMBEDDED_IMAGE)) {\n return \"\"\n }\n $grepKey = [regex]::Escape($Key)\n $shCmd = \"grep \"\"^${grepKey}=\"\" /data/agentteams-secrets.env 2>/dev/null | cut -d= -f2- | head -1 | tr -d '\\r'\"\n $out = docker run --rm --entrypoint sh -v \"${VolumeName}:/data:ro\" $script:EMBEDDED_IMAGE -c $shCmd 2>$null\n if ($null -eq $out) { return \"\" }\n return ($out | Out-String).Trim()\n}\n\n# Read KEY=value from /data/worker-creds/<worker>.env on a Docker volume.\nfunction Read-AgentTeamsWorkerCredsFromVolume {\n param(\n [string]$VolumeName,\n [string]$WorkerName,\n [string]$Key\n )\n if ([string]::IsNullOrEmpty($VolumeName) -or [string]::IsNullOrEmpty($WorkerName) -or [string]::IsNullOrEmpty($Key) -or [string]::IsNullOrEmpty($script:EMBEDDED_IMAGE)) {\n return \"\"\n }\n $grepKey = [regex]::Escape($Key)\n $path = \"/data/worker-creds/${WorkerName}.env\"\n $shCmd = \"grep \"\"^${grepKey}=\"\" \"\"${path}\"\" 2>/dev/null | cut -d= -f2- | head -1 | tr -d '\\r'\"\n $out = docker run --rm --entrypoint sh -v \"${VolumeName}:/data:ro\" $script:EMBEDDED_IMAGE -c $shCmd 2>$null\n if ($null -eq $out) { return \"\" }\n return ($out | Out-String).Trim()\n}\n\n# Read admin_dm_room_id from host workspace state.json (fallback when Matrix API is unavailable).\nfunction Read-AgentTeamsAdminDmRoomFromWorkspace {\n param([string]$WorkspaceDir)\n if ([string]::IsNullOrEmpty($WorkspaceDir)) { return \"\" }\n $statePath = Join-Path $WorkspaceDir \"state.json\"\n if (-not (Test-Path $statePath)) { return \"\" }\n try {\n $j = Get-Content $statePath -Raw -ErrorAction Stop | ConvertFrom-Json\n $rid = $j.admin_dm_room_id\n if ($null -eq $rid) { return \"\" }\n $s = [string]$rid\n if ($s -eq \"\" -or $s -eq \"null\") { return \"\" }\n return $s.Trim()\n } catch {\n return \"\"\n }\n}\n\nfunction Get-AgentTeamsRandomHex {\n param([int]$ByteCount)\n $bytes = New-Object byte[] $ByteCount\n $rng = [System.Security.Cryptography.RandomNumberGenerator]::Create()\n $rng.GetBytes($bytes)\n return ([BitConverter]::ToString($bytes)).Replace(\"-\", \"\").ToLower()\n}\n\n# Resolve admin DM room with @manager (small room) via Matrix Client API inside agentteams-manager.\nfunction Get-AgentTeamsAdminDmRoomViaMatrix {\n param(\n [string]$AdminUser,\n [string]$AdminPassword\n )\n $running = docker ps --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-manager$\"\n if (-not $running) { return \"\" }\n if ([string]::IsNullOrEmpty($AdminPassword)) { return \"\" }\n\n $loginObj = @{\n type = \"m.login.password\"\n identifier = @{ type = \"m.id.user\"; user = $AdminUser }\n password = $AdminPassword\n }\n $loginJson = $loginObj | ConvertTo-Json -Compress -Depth 5\n try {\n $loginRaw = $loginJson | docker exec -i agentteams-manager sh -c \"curl -sf -X POST http://127.0.0.1:6167/_matrix/client/v3/login -H 'Content-Type: application/json' -d @-\" 2>$null\n if (-not $loginRaw) { return \"\" }\n $loginResp = $loginRaw | ConvertFrom-Json\n $token = [string]$loginResp.access_token\n if ([string]::IsNullOrEmpty($token)) { return \"\" }\n\n $roomsRaw = docker exec agentteams-manager curl -sf -X GET -H \"Authorization: Bearer $token\" `\n \"http://127.0.0.1:6167/_matrix/client/v3/joined_rooms\" 2>$null\n if (-not $roomsRaw) { return \"\" }\n $roomsObj = $roomsRaw | ConvertFrom-Json\n $roomList = @($roomsObj.joined_rooms)\n foreach ($roomId in $roomList) {\n if ([string]::IsNullOrEmpty($roomId)) { continue }\n $enc = $roomId.Replace(\"!\", \"%21\")\n $memRaw = docker exec agentteams-manager curl -sf -X GET -H \"Authorization: Bearer $token\" `\n \"http://127.0.0.1:6167/_matrix/client/v3/rooms/${enc}/members\" 2>$null\n if (-not $memRaw) { continue }\n $memObj = $memRaw | ConvertFrom-Json\n $chunk = @($memObj.chunk)\n $memberIds = @($chunk | ForEach-Object { $_.state_key })\n $match = $false\n foreach ($m in $memberIds) {\n $beforeColon = ($m -split \":\")[0]\n if ($m -like \"*manager*\" -and $beforeColon -notlike \"*admin*\") {\n $match = $true\n break\n }\n }\n if ($match -and $memberIds.Count -le 3) {\n return $roomId\n }\n }\n } catch {\n return \"\"\n }\n return \"\"\n}\n\nfunction New-EnvFile {\n param([hashtable]$Config, [string]$Path)\n\n $content = @\"\n# AgentTeams Manager Configuration\n# Generated by agentteams-install.ps1 on $(Get-Date -Format \"yyyy-MM-dd HH:mm:ss\")\n\n# Language\nAGENTTEAMS_LANGUAGE=$($Config.LANGUAGE)\n\n# LLM\nAGENTTEAMS_LLM_PROVIDER=$($Config.LLM_PROVIDER)\nAGENTTEAMS_DEFAULT_MODEL=$($Config.DEFAULT_MODEL)\nAGENTTEAMS_LLM_API_KEY=$($Config.LLM_API_KEY)\nAGENTTEAMS_OPENAI_BASE_URL=$($Config.OPENAI_BASE_URL)\nAGENTTEAMS_MODEL_CONTEXT_WINDOW=$($Config.MODEL_CONTEXT_WINDOW)\nAGENTTEAMS_MODEL_MAX_TOKENS=$($Config.MODEL_MAX_TOKENS)\nAGENTTEAMS_MODEL_REASONING=$($Config.MODEL_REASONING)\nAGENTTEAMS_MODEL_VISION=$($Config.MODEL_VISION)\n\n# Embedding model (empty = disabled, default: text-embedding-v4)\nAGENTTEAMS_EMBEDDING_MODEL=$($Config.EMBEDDING_MODEL)\n\n# Admin\nAGENTTEAMS_ADMIN_USER=$($Config.ADMIN_USER)\nAGENTTEAMS_ADMIN_PASSWORD=$($Config.ADMIN_PASSWORD)\n\n# Ports\nAGENTTEAMS_LOCAL_ONLY=$($Config.LOCAL_ONLY)\nAGENTTEAMS_PORT_GATEWAY=$($Config.PORT_GATEWAY)\nAGENTTEAMS_PORT_CONSOLE=$($Config.PORT_CONSOLE)\nAGENTTEAMS_PORT_ELEMENT_WEB=$($Config.PORT_ELEMENT_WEB)\nAGENTTEAMS_PORT_MANAGER_CONSOLE=$($Config.PORT_MANAGER_CONSOLE)\n\n# Matrix\nAGENTTEAMS_MATRIX_DOMAIN=$($Config.MATRIX_DOMAIN)\nAGENTTEAMS_MATRIX_CLIENT_DOMAIN=$($Config.MATRIX_CLIENT_DOMAIN)\n\n# Gateway\nAGENTTEAMS_AI_GATEWAY_DOMAIN=$($Config.AI_GATEWAY_DOMAIN)\nAGENTTEAMS_MANAGER_GATEWAY_KEY=$($Config.MANAGER_GATEWAY_KEY)\n\n# File System\nAGENTTEAMS_FS_DOMAIN=$($Config.FS_DOMAIN)\nAGENTTEAMS_CONSOLE_DOMAIN=$($Config.CONSOLE_DOMAIN)\nAGENTTEAMS_MINIO_USER=$($Config.MINIO_USER)\nAGENTTEAMS_MINIO_PASSWORD=$($Config.MINIO_PASSWORD)\n\n# Internal\nAGENTTEAMS_MANAGER_PASSWORD=$($Config.MANAGER_PASSWORD)\nAGENTTEAMS_REGISTRATION_TOKEN=$($Config.REGISTRATION_TOKEN)\n\n# GitHub (optional)\nAGENTTEAMS_GITHUB_TOKEN=$($Config.GITHUB_TOKEN)\n\n# Nacos package import defaults\nAGENTTEAMS_NACOS_REGISTRY_URI=$(if ($env:AGENTTEAMS_NACOS_REGISTRY_URI) { $env:AGENTTEAMS_NACOS_REGISTRY_URI } else { \"nacos://market.agentteams.io:80/public\" })\nAGENTTEAMS_NACOS_USERNAME=$($env:AGENTTEAMS_NACOS_USERNAME)\nAGENTTEAMS_NACOS_PASSWORD=$($env:AGENTTEAMS_NACOS_PASSWORD)\nAGENTTEAMS_NACOS_TOKEN=$($env:AGENTTEAMS_NACOS_TOKEN)\n\n# Skills Registry (optional, default: nacos://market.agentteams.io:80/public)\nAGENTTEAMS_SKILLS_API_URL=$(if ($Config.SKILLS_API_URL) { $Config.SKILLS_API_URL } else { \"nacos://market.agentteams.io:80/public\" })\n\n# OpenClaw CMS plugin (optional)\nAGENTTEAMS_CMS_TRACES_ENABLED=$(if ($env:AGENTTEAMS_CMS_TRACES_ENABLED) { $env:AGENTTEAMS_CMS_TRACES_ENABLED } else { \"false\" })\nAGENTTEAMS_CMS_ENDPOINT=$($env:AGENTTEAMS_CMS_ENDPOINT)\nAGENTTEAMS_CMS_LICENSE_KEY=$($env:AGENTTEAMS_CMS_LICENSE_KEY)\nAGENTTEAMS_CMS_PROJECT=$($env:AGENTTEAMS_CMS_PROJECT)\nAGENTTEAMS_CMS_WORKSPACE=$($env:AGENTTEAMS_CMS_WORKSPACE)\nAGENTTEAMS_CMS_SERVICE_NAME=$(if ($env:AGENTTEAMS_CMS_SERVICE_NAME) { $env:AGENTTEAMS_CMS_SERVICE_NAME } else { \"agentteams-manager\" })\nAGENTTEAMS_CMS_METRICS_ENABLED=$(if ($env:AGENTTEAMS_CMS_METRICS_ENABLED) { $env:AGENTTEAMS_CMS_METRICS_ENABLED } else { \"false\" })\n\n# Worker images (for direct container creation)\nAGENTTEAMS_WORKER_IMAGE=$($Config.WORKER_IMAGE)\nAGENTTEAMS_COPAW_WORKER_IMAGE=$($Config.COPAW_WORKER_IMAGE)\nAGENTTEAMS_QWENPAW_WORKER_IMAGE=$($Config.QWENPAW_WORKER_IMAGE)\nAGENTTEAMS_HERMES_WORKER_IMAGE=$($Config.HERMES_WORKER_IMAGE)\n\n# Manager runtime (openclaw | copaw)\nAGENTTEAMS_MANAGER_RUNTIME=$($Config.MANAGER_RUNTIME)\n\n# Default Worker runtime (openclaw | copaw | hermes)\nAGENTTEAMS_DEFAULT_WORKER_RUNTIME=$($Config.DEFAULT_WORKER_RUNTIME)\n\n# Matrix E2EE (0=disabled, 1=enabled; default: 0)\nAGENTTEAMS_MATRIX_E2EE=$($Config.MATRIX_E2EE)\n\n# Docker API proxy (0=disabled, 1=enabled; default: 1)\nAGENTTEAMS_DOCKER_PROXY=$($Config.DOCKER_PROXY)\n\n# Docker API proxy: additional allowed image sources (comma-separated)\nAGENTTEAMS_PROXY_ALLOWED_REGISTRIES=$($Config.PROXY_ALLOWED_REGISTRIES)\n\n# Worker idle timeout in minutes (default: 720 = 12 hours)\nAGENTTEAMS_WORKER_IDLE_TIMEOUT=$($Config.WORKER_IDLE_TIMEOUT)\n\n# JVM Args for Higress Console\nJVM_ARGS=$($env:JVM_ARGS)\n\n# Higress WASM plugin image registry (auto-selected by timezone)\nHIGRESS_ADMIN_WASM_PLUGIN_IMAGE_REGISTRY=$($Config.REGISTRY)\n\n# Data persistence\nAGENTTEAMS_DATA_DIR=$($Config.DATA_DIR)\n# Manager workspace (skills, memory, state - host-editable)\nAGENTTEAMS_WORKSPACE_DIR=$($Config.WORKSPACE_DIR)\n# Host directory sharing\nAGENTTEAMS_HOST_SHARE_DIR=$($Config.HOST_SHARE_DIR)\n\"@\n\n Set-Content -Path $Path -Value $content -Encoding UTF8\n Write-Log (Get-Msg \"install.config_saved\" -f $Path)\n}\n\n# ============================================================\n# Prompt Functions\n# ============================================================\n\nfunction Read-Prompt {\n param(\n [string]$VarName,\n [string]$PromptText,\n [string]$Default = \"\",\n [switch]$Secret,\n [switch]$Optional\n )\n\n # Check if already set in environment\n $envValue = [Environment]::GetEnvironmentVariable($VarName)\n if ($envValue) {\n if ($script:AGENTTEAMS_UPGRADE -and -not $script:AGENTTEAMS_NON_INTERACTIVE) {\n # Show current value (masked for secrets) and let user change it\n $displayValue = $envValue\n if ($Secret) {\n if ($envValue.Length -le 8) {\n $displayValue = \"****\"\n } else {\n $displayValue = $envValue.Substring(0, 4) + \"****\" + $envValue.Substring($envValue.Length - 4)\n }\n }\n Write-Log (Get-Msg \"prompt.upgrade_keep\" -f $PromptText, $displayValue)\n $prompt = $PromptText\n if ($Secret) {\n $newValue = Read-Host -Prompt $prompt -AsSecureString\n $newValue = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto(\n [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($newValue)\n )\n } else {\n $newValue = Read-Host -Prompt $prompt\n if ($newValue -eq \"b\") { $script:StepResult = \"back\"; return $null }\n }\n if ($newValue) {\n return $newValue\n }\n return $envValue\n }\n Write-Log (Get-Msg \"prompt.preset\" -f $PromptText)\n return $envValue\n }\n # Upgrade mode: optional fields with empty value — let user set a new value\n elseif ($Optional -and $script:AGENTTEAMS_UPGRADE -and -not $script:AGENTTEAMS_NON_INTERACTIVE) {\n Write-Log (Get-Msg \"prompt.upgrade_empty\" -f $PromptText)\n $prompt = $PromptText\n if ($Secret) {\n $newValue = Read-Host -Prompt $prompt -AsSecureString\n $newValue = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto(\n [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($newValue)\n )\n } else {\n $newValue = Read-Host -Prompt $prompt\n if ($newValue -eq \"b\") { $script:StepResult = \"back\"; return $null }\n }\n if ($newValue) {\n return $newValue\n }\n return \"\"\n }\n\n # Non-interactive or quickstart mode\n if ($script:AGENTTEAMS_NON_INTERACTIVE -or $script:AGENTTEAMS_QUICKSTART) {\n if ($Default) {\n Write-Log (Get-Msg \"prompt.default\" -f $PromptText, $Default)\n return $Default\n }\n elseif ($Optional) {\n return \"\"\n }\n elseif ($script:AGENTTEAMS_NON_INTERACTIVE) {\n # Only hard-error in fully non-interactive mode, not quickstart\n Write-Error (Get-Msg \"prompt.required\" -f $PromptText)\n }\n # quickstart + no default + not optional: fall through to interactive prompt\n }\n\n # Interactive prompt\n $prompt = if ($Default) { \"$PromptText [$Default]\" } else { $PromptText }\n\n if ($Secret) {\n $value = Read-Host -Prompt $prompt -AsSecureString\n $value = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto(\n [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($value)\n )\n }\n else {\n $value = Read-Host -Prompt $prompt\n if ($value -eq \"b\") { $script:StepResult = \"back\"; return $null }\n }\n\n if (-not $value -and $Default) {\n $value = $Default\n }\n\n if (-not $value -and -not $Optional) {\n Write-Error (Get-Msg \"prompt.required_empty\" -f $PromptText)\n }\n\n return $value\n}\n\n# Load current parameter values from env file for upgrade mode display\nfunction Load-CurrentParamsFromEnv {\n $envFile = $script:AGENTTEAMS_ENV_FILE\n if (Test-Path $envFile) {\n $content = Get-Content $envFile\n $content | ForEach-Object {\n if ($_ -match \"^AGENTTEAMS_LLM_PROVIDER=(.*)$\") {\n $script:config.LLM_PROVIDER = $Matches[1].Trim()\n }\n if ($_ -match \"^AGENTTEAMS_OPENAI_BASE_URL=(.*)$\") {\n $script:config.OPENAI_BASE_URL = $Matches[1].Trim()\n }\n if ($_ -match \"^AGENTTEAMS_DEFAULT_MODEL=(.*)$\") {\n $script:config.DEFAULT_MODEL = $Matches[1].Trim()\n }\n if ($_ -match \"^AGENTTEAMS_EMBEDDING_MODEL=(.*)$\") {\n $script:config.EMBEDDING_MODEL = $Matches[1].Trim()\n }\n if ($_ -match \"^AGENTTEAMS_WORKSPACE_DIR=(.*)$\") {\n $script:config.WORKSPACE_DIR = $Matches[1].Trim()\n }\n if ($_ -match \"^AGENTTEAMS_HOST_SHARE_DIR=(.*)$\") {\n $script:config.HOST_SHARE_DIR = $Matches[1].Trim()\n }\n }\n }\n}\n\n# ============================================================\n# OpenAI-Compatible Provider\n# ============================================================\n\nfunction Test-LlmConnectivity {\n param(\n [string]$BaseUrl,\n [string]$ApiKey,\n [string]$Model,\n [string]$Hint = \"\"\n )\n Write-Log (Get-Msg \"llm.openai.test.testing\")\n $uri = ($BaseUrl.TrimEnd('/')) + \"/chat/completions\"\n $bodyHash = @{\n model = $Model\n messages = @(@{ role = \"user\"; content = \"hi\" })\n }\n $body = $bodyHash | ConvertTo-Json -Compress\n try {\n $response = Invoke-WebRequest -Uri $uri -Method POST `\n -Headers @{ \"Authorization\" = \"Bearer $ApiKey\"; \"Content-Type\" = \"application/json\"; \"User-Agent\" = \"AgentTeams/$($script:AGENTTEAMS_VERSION)\" } `\n -Body $body -TimeoutSec 30 -ErrorAction Stop -UseBasicParsing\n Write-Log (Get-Msg \"llm.openai.test.ok\")\n } catch {\n $statusCode = if ($_.Exception.Response) { [int]$_.Exception.Response.StatusCode } else { 0 }\n $responseBody = \"\"\n if ($_.Exception.Response) {\n try {\n $stream = $_.Exception.Response.GetResponseStream()\n $reader = New-Object System.IO.StreamReader($stream)\n $responseBody = $reader.ReadToEnd()\n } catch {}\n }\n Write-Host (Get-Msg \"llm.openai.test.fail\" -f $statusCode, $responseBody) -ForegroundColor Yellow\n if ($Hint) {\n Write-Host $Hint -ForegroundColor Yellow\n }\n if (-not $script:AGENTTEAMS_NON_INTERACTIVE) {\n $confirm = Read-Host (Get-Msg \"llm.openai.test.confirm\")\n if ($confirm -eq \"b\" -or $confirm -eq \"B\") {\n $script:StepResult = \"back\"\n return\n }\n if ($confirm -ne \"y\" -and $confirm -ne \"Y\") {\n Write-Log (Get-Msg \"llm.openai.test.aborted\")\n Exit-Script 1\n }\n }\n }\n}\n\nfunction Test-EmbeddingConnectivity {\n param(\n [string]$BaseUrl,\n [string]$ApiKey,\n [string]$Model\n )\n Write-Log (Get-Msg \"llm.embedding.test.testing\")\n $uri = ($BaseUrl.TrimEnd('/')) + \"/embeddings\"\n $bodyHash = @{\n model = $Model\n input = \"test\"\n }\n $body = $bodyHash | ConvertTo-Json -Compress\n try {\n $response = Invoke-WebRequest -Uri $uri -Method POST `\n -Headers @{ \"Authorization\" = \"Bearer $ApiKey\"; \"Content-Type\" = \"application/json\"; \"User-Agent\" = \"AgentTeams/$($script:AGENTTEAMS_VERSION)\" } `\n -Body $body -TimeoutSec 30 -ErrorAction Stop -UseBasicParsing\n Write-Log (Get-Msg \"llm.embedding.test.ok\")\n return $true\n } catch {\n $statusCode = if ($_.Exception.Response) { [int]$_.Exception.Response.StatusCode } else { 0 }\n $responseBody = \"\"\n if ($_.Exception.Response) {\n try {\n $stream = $_.Exception.Response.GetResponseStream()\n $reader = New-Object System.IO.StreamReader($stream)\n $responseBody = $reader.ReadToEnd()\n } catch {}\n }\n Write-Host (Get-Msg \"llm.embedding.test.fail\" -f $statusCode, $responseBody) -ForegroundColor Yellow\n return $false\n }\n}\n\nfunction New-OpenAICompatProvider {\n param(\n [string]$BaseUrl,\n [string]$ApiKey,\n [int]$ConsolePort = 18001\n )\n\n if (-not $BaseUrl -or -not $ApiKey) {\n Write-Log (Get-Msg \"install.openai_compat.missing\")\n return $false\n }\n\n $consoleUrl = \"http://localhost:$ConsolePort\"\n\n # Parse base URL\n $protocol = \"https\"\n $port = 443\n $urlWithoutProto = $BaseUrl -replace \"^https?://\", \"\"\n\n if ($BaseUrl -match \"^http://\") {\n $protocol = \"http\"\n $port = 80\n }\n\n $domain = $urlWithoutProto.Split(\"/\")[0]\n\n if ($domain -match \":(\\d+)$\") {\n $port = [int]$Matches[1]\n $domain = $domain -replace \":\\d+$\", \"\"\n }\n\n Write-Log (Get-Msg \"install.openai_compat.creating\")\n Write-Log (Get-Msg \"install.openai_compat.domain\" -f $domain)\n Write-Log (Get-Msg \"install.openai_compat.port\" -f $port)\n Write-Log (Get-Msg \"install.openai_compat.protocol\" -f $protocol)\n\n $serviceName = \"openai-compat\"\n\n # Create DNS service source\n $serviceBody = @{\n type = \"dns\"\n name = $serviceName\n port = $port.ToString()\n protocol = $protocol\n proxyName = \"\"\n domain = $domain\n } | ConvertTo-Json -Compress\n\n try {\n Invoke-RestMethod -Uri \"$consoleUrl/v1/service-sources\" -Method POST -ContentType \"application/json\" -Body $serviceBody -ErrorAction SilentlyContinue | Out-Null\n }\n catch {\n Write-Log (Get-Msg \"install.openai_compat.service_fail\")\n }\n\n Start-Sleep -Seconds 2\n\n # Create AI provider\n $providerBody = @{\n type = \"openai\"\n name = \"openai-compat\"\n tokens = @($ApiKey)\n version = 0\n protocol = \"openai/v1\"\n tokenFailoverConfig = @{ enabled = $false }\n rawConfigs = @{\n openaiCustomUrl = $BaseUrl\n openaiCustomServiceName = \"$serviceName.dns\"\n openaiCustomServicePort = $port\n }\n } | ConvertTo-Json -Compress -Depth 3\n\n try {\n Invoke-RestMethod -Uri \"$consoleUrl/v1/ai/providers\" -Method POST -ContentType \"application/json\" -Body $providerBody -ErrorAction SilentlyContinue | Out-Null\n Write-Log (Get-Msg \"install.openai_compat.success\")\n return $true\n }\n catch {\n Write-Log (Get-Msg \"install.openai_compat.provider_fail\")\n return $false\n }\n}\n\n# ============================================================\n# State Machine Helpers\n# ============================================================\n\nfunction Test-ShouldSkipStep {\n param([string]$StepFn)\n switch ($StepFn) {\n { $_ -in @(\"Step-Lang\", \"Step-Mode\") } {\n return $script:AGENTTEAMS_NON_INTERACTIVE\n }\n \"Step-Existing\" {\n return (-not (Test-Path $script:AGENTTEAMS_ENV_FILE))\n }\n # Keep-All upgrade mode: skip all config steps, values are already loaded\n { $_ -in @(\"Step-Llm\", \"Step-Admin\", \"Step-Network\", \"Step-Ports\", \"Step-Domains\",\n \"Step-Github\", \"Step-Skills\",\n \"Step-Runtime\", \"Step-ManagerRuntime\", \"Step-E2ee\", \"Step-DockerProxy\",\n \"Step-Idle\", \"Step-Hostshare\") } {\n if ($script:AGENTTEAMS_UPGRADE -and $env:AGENTTEAMS_UPGRADE_KEEP_ALL -eq \"1\") { return $true }\n return $false\n }\n { $_ -in @(\"Step-Volume\", \"Step-Workspace\") } {\n if ($script:AGENTTEAMS_UPGRADE -and $env:AGENTTEAMS_UPGRADE_KEEP_ALL -eq \"1\") { return $true }\n if ($script:AGENTTEAMS_NON_INTERACTIVE) { return $true }\n if ($script:AGENTTEAMS_QUICKSTART) { return $true }\n return $false\n }\n { $_ -in @(\"Step-E2ee\", \"Step-Idle\") } {\n if ($script:AGENTTEAMS_NON_INTERACTIVE) { return $true }\n if ($script:AGENTTEAMS_QUICKSTART -and -not $script:AGENTTEAMS_UPGRADE) { return $true }\n return $false\n }\n \"Step-DockerProxy\" {\n # Embedded controller IS the proxy/orchestrator — no separate proxy step needed.\n if ($script:AGENTTEAMS_USE_EMBEDDED -eq \"1\") { return $true }\n if ($script:AGENTTEAMS_NON_INTERACTIVE) { return $true }\n if ($script:AGENTTEAMS_QUICKSTART -and -not $script:AGENTTEAMS_UPGRADE) { return $true }\n return $false\n }\n \"Step-ManagerRuntime\" {\n if ($script:AGENTTEAMS_NON_INTERACTIVE) { return $true }\n return $false\n }\n \"Step-Hostshare\" {\n if ($script:AGENTTEAMS_NON_INTERACTIVE) { return $true }\n if ($script:AGENTTEAMS_QUICKSTART) { return $true }\n return $false\n }\n default { return $false }\n }\n}\n\nfunction Clear-StepVars {\n param([string]$StepFn)\n switch ($StepFn) {\n \"Step-Mode\" { $script:AGENTTEAMS_QUICKSTART = $false }\n \"Step-Existing\" {\n $script:AGENTTEAMS_UPGRADE = $false\n $script:UPGRADE_EXISTING_WORKERS = $null\n }\n \"Step-Llm\" {\n foreach ($k in @(\"AGENTTEAMS_LLM_PROVIDER\",\"AGENTTEAMS_DEFAULT_MODEL\",\"AGENTTEAMS_OPENAI_BASE_URL\",\n \"AGENTTEAMS_LLM_API_KEY\",\"AGENTTEAMS_MODEL_CONTEXT_WINDOW\",\"AGENTTEAMS_MODEL_MAX_TOKENS\",\n \"AGENTTEAMS_MODEL_REASONING\",\"AGENTTEAMS_MODEL_VISION\")) {\n [Environment]::SetEnvironmentVariable($k, $null, \"Process\")\n }\n }\n \"Step-Admin\" {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_ADMIN_USER\", $null, \"Process\")\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_ADMIN_PASSWORD\", $null, \"Process\")\n }\n \"Step-Network\" {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_LOCAL_ONLY\", $null, \"Process\")\n }\n \"Step-Ports\" {\n foreach ($k in @(\"AGENTTEAMS_PORT_GATEWAY\",\"AGENTTEAMS_PORT_CONSOLE\",\"AGENTTEAMS_PORT_ELEMENT_WEB\",\"AGENTTEAMS_PORT_MANAGER_CONSOLE\")) {\n [Environment]::SetEnvironmentVariable($k, $null, \"Process\")\n }\n }\n \"Step-Domains\" {\n foreach ($k in @(\"AGENTTEAMS_MATRIX_DOMAIN\",\"AGENTTEAMS_MATRIX_CLIENT_DOMAIN\",\"AGENTTEAMS_AI_GATEWAY_DOMAIN\",\"AGENTTEAMS_FS_DOMAIN\",\"AGENTTEAMS_CONSOLE_DOMAIN\")) {\n [Environment]::SetEnvironmentVariable($k, $null, \"Process\")\n }\n }\n \"Step-Github\" {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_GITHUB_TOKEN\", $null, \"Process\")\n }\n \"Step-Skills\" {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_SKILLS_API_URL\", $null, \"Process\")\n }\n \"Step-Volume\" {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_DATA_DIR\", $null, \"Process\")\n }\n \"Step-Workspace\" {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_WORKSPACE_DIR\", $null, \"Process\")\n }\n \"Step-Runtime\" {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_DEFAULT_WORKER_RUNTIME\", $null, \"Process\")\n }\n \"Step-ManagerRuntime\" {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_MANAGER_RUNTIME\", $null, \"Process\")\n }\n \"Step-E2ee\" {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_MATRIX_E2EE\", $null, \"Process\")\n }\n \"Step-DockerProxy\" {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_DOCKER_PROXY\", $null, \"Process\")\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_PROXY_ALLOWED_REGISTRIES\", $null, \"Process\")\n }\n \"Step-Idle\" {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_WORKER_IDLE_TIMEOUT\", $null, \"Process\")\n }\n \"Step-Hostshare\" {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_HOST_SHARE_DIR\", $null, \"Process\")\n }\n }\n}\n\n# ============================================================\n# Individual step functions\n# ============================================================\n\nfunction Step-Lang {\n $langDefaultChoice = if ($script:AGENTTEAMS_LANGUAGE -eq \"zh\") { \"1\" } else { \"2\" }\n $langDetectedKey = \"lang.detected.$($script:AGENTTEAMS_LANGUAGE)\"\n Write-Log (Get-Msg $langDetectedKey)\n Write-Log (Get-Msg \"lang.switch_title\")\n Write-Host (Get-Msg \"lang.option_zh\")\n Write-Host (Get-Msg \"lang.option_en\")\n Write-Host \"\"\n $langChoice = Read-Host \"$(Get-Msg 'lang.prompt') [$langDefaultChoice]\"\n if (-not $langChoice) { $langChoice = $langDefaultChoice }\n if ($langChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n switch ($langChoice) {\n \"1\" { $script:AGENTTEAMS_LANGUAGE = \"zh\" }\n \"2\" { $script:AGENTTEAMS_LANGUAGE = \"en\" }\n }\n $env:AGENTTEAMS_LANGUAGE = $script:AGENTTEAMS_LANGUAGE\n Write-Log \"\"\n}\n\nfunction Step-Mode {\n Write-Log (Get-Msg \"install.mode.title\")\n Write-Host \"\"\n Write-Host (Get-Msg \"install.mode.choose\")\n Write-Host (Get-Msg \"install.mode.quickstart\")\n Write-Host (Get-Msg \"install.mode.manual\")\n Write-Host \"\"\n $choice = Read-Host (Get-Msg \"install.mode.prompt\")\n $choice = if ($choice) { $choice } else { \"1\" }\n if ($choice -eq \"b\") { $script:StepResult = \"back\"; return }\n switch -Regex ($choice) {\n \"^(1|quick|quickstart)$\" {\n Write-Log (Get-Msg \"install.mode.quickstart_selected\")\n $script:AGENTTEAMS_QUICKSTART = $true\n }\n \"^(2|manual)$\" {\n Write-Log (Get-Msg \"install.mode.manual_selected\")\n $script:AGENTTEAMS_QUICKSTART = $false\n }\n default {\n Write-Log (Get-Msg \"install.mode.invalid\")\n $script:AGENTTEAMS_QUICKSTART = $true\n }\n }\n Write-Log \"\"\n}\n\nfunction Step-Existing {\n # This step is skipped when env file doesn't exist\n Write-Log (Get-Msg \"install.existing.detected\" -f $script:AGENTTEAMS_ENV_FILE)\n\n $runningManager = docker ps --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-manager$\"\n $runningWorkers = docker ps --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-worker-\"\n $existingWorkers = docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-worker-\"\n\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n Write-Log (Get-Msg \"install.existing.upgrade_noninteractive\")\n $upgradeChoice = \"1\"\n } else {\n Write-Host \"\"\n Write-Host (Get-Msg \"install.existing.choose\")\n Write-Host (Get-Msg \"install.existing.upgrade\")\n Write-Host (Get-Msg \"install.existing.reinstall\")\n Write-Host (Get-Msg \"install.existing.cancel\")\n Write-Host \"\"\n $upgradeChoice = Read-Host (Get-Msg \"install.existing.prompt\")\n $upgradeChoice = if ($upgradeChoice) { $upgradeChoice } else { \"1\" }\n if ($upgradeChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n }\n\n switch -Regex ($upgradeChoice) {\n \"^(1|upgrade)$\" {\n $script:AGENTTEAMS_UPGRADE = $true\n Write-Log (Get-Msg \"install.existing.upgrading\")\n\n # Show upgrade mode sub-menu (unless already set via env var)\n if ($env:AGENTTEAMS_UPGRADE_KEEP_ALL -ne \"1\") {\n Write-Host \"\"\n Write-Host (Get-Msg \"upgrade.mode.prompt\")\n Write-Host (Get-Msg \"upgrade.mode.keep_all\")\n Write-Host (Get-Msg \"upgrade.mode.confirm_each\")\n Write-Host (Get-Msg \"upgrade.mode.back\")\n Write-Host \"\"\n $upgradeModeChoice = Read-Host (Get-Msg \"install.existing.prompt\")\n $upgradeModeChoice = if ($upgradeModeChoice) { $upgradeModeChoice } else { \"1\" }\n switch -Regex ($upgradeModeChoice) {\n \"^(1|keep)$\" {\n $env:AGENTTEAMS_UPGRADE_KEEP_ALL = \"1\"\n }\n \"^(2|confirm)$\" {\n $env:AGENTTEAMS_UPGRADE_KEEP_ALL = \"0\"\n }\n \"^(3|b)$\" {\n $script:StepResult = \"back\"\n return\n }\n default {\n $env:AGENTTEAMS_UPGRADE_KEEP_ALL = \"0\"\n }\n }\n }\n # Load current parameters for both Keep-All and confirm-each modes\n Load-CurrentParamsFromEnv\n\n if ($runningManager -or $runningWorkers) {\n Write-Host \"\"\n Write-Host \"$($script:ESC)[33m$(Get-Msg 'install.existing.warn_manager_stop')$($script:ESC)[0m\"\n if ($existingWorkers) {\n Write-Host \"$($script:ESC)[33m$(Get-Msg 'install.existing.warn_worker_recreate')$($script:ESC)[0m\"\n }\n if (-not $script:AGENTTEAMS_NON_INTERACTIVE) {\n $confirm = Read-Host (Get-Msg \"install.existing.continue_prompt\")\n if ($confirm -eq \"b\" -or $confirm -eq \"B\") { $script:StepResult = \"back\"; return }\n if ($confirm -ne \"y\" -and $confirm -ne \"Y\") {\n Write-Log (Get-Msg \"install.existing.cancelled\")\n exit 0\n }\n }\n }\n $script:UPGRADE_EXISTING_WORKERS = $existingWorkers\n }\n \"^(2|reinstall)$\" {\n Write-Log (Get-Msg \"install.reinstall.performing\")\n $existingWorkspace = \"$env:USERPROFILE\\agentteams-manager\"\n if (Test-Path $script:AGENTTEAMS_ENV_FILE) {\n $envContent = Get-Content $script:AGENTTEAMS_ENV_FILE\n $wsLine = $envContent | Select-String \"^AGENTTEAMS_WORKSPACE_DIR=\"\n if ($wsLine) {\n $existingWorkspace = $wsLine.Line.Substring(21)\n }\n }\n Write-Host \"\"\n Write-Host \"$($script:ESC)[33m$(Get-Msg 'install.reinstall.warn_stop')$($script:ESC)[0m\"\n if ($runningManager) { Write-Host \"$($script:ESC)[33m - agentteams-manager (manager)$($script:ESC)[0m\" }\n $runningWorkers | ForEach-Object { Write-Host \"$($script:ESC)[33m - $_ (worker)$($script:ESC)[0m\" }\n Write-Host \"\"\n Write-Host \"$($script:ESC)[31m$(Get-Msg 'install.reinstall.warn_delete')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[31m$(Get-Msg 'install.reinstall.warn_volume')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[31m$(Get-Msg 'install.reinstall.warn_env' -f $script:AGENTTEAMS_ENV_FILE)$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[31m$(Get-Msg 'install.reinstall.warn_workspace' -f $existingWorkspace)$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[31m$(Get-Msg 'install.reinstall.warn_workers')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[31m$(Get-Msg 'install.reinstall.warn_proxy')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[31m$(Get-Msg 'install.reinstall.warn_network')$($script:ESC)[0m\"\n Write-Host \"\"\n Write-Host \"$($script:ESC)[31m$(Get-Msg 'install.reinstall.confirm_type')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[31m $existingWorkspace$($script:ESC)[0m\"\n Write-Host \"\"\n $confirmPath = Read-Host (Get-Msg \"install.reinstall.confirm_path\")\n if ($confirmPath -ne $existingWorkspace) {\n Write-Error (Get-Msg \"install.reinstall.path_mismatch\" -f $confirmPath, $existingWorkspace)\n }\n Write-Log (Get-Msg \"install.reinstall.confirmed\")\n docker stop agentteams-manager *>$null\n docker rm agentteams-manager *>$null\n docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-worker-\" | ForEach-Object {\n docker stop $_ *>$null\n docker rm $_ *>$null\n Write-Log (Get-Msg \"install.reinstall.removed_worker\" -f $_)\n }\n $existingController = docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-controller$\"\n if ($existingController) {\n Write-Log (Get-Msg \"install.reinstall.removing_proxy\")\n docker stop agentteams-controller *>$null\n docker rm agentteams-controller *>$null\n }\n if (docker volume ls -q 2>$null | Select-String \"^agentteams-data$\") {\n Write-Log (Get-Msg \"install.reinstall.removing_volume\")\n docker volume rm agentteams-data *>$null\n }\n if (Test-Path $existingWorkspace) {\n Write-Log (Get-Msg \"install.reinstall.removing_workspace\" -f $existingWorkspace)\n Remove-Item -Recurse -Force $existingWorkspace\n }\n if (Test-Path $script:AGENTTEAMS_ENV_FILE) {\n Write-Log (Get-Msg \"install.reinstall.removing_env\" -f $script:AGENTTEAMS_ENV_FILE)\n Remove-Item -Force $script:AGENTTEAMS_ENV_FILE\n }\n $existingNetwork = docker network ls --format \"{{.Name}}\" 2>$null | Select-String \"^agentteams-net$\"\n if ($existingNetwork) {\n Write-Log (Get-Msg \"install.reinstall.removing_network\")\n docker network rm agentteams-net *>$null\n }\n Write-Log (Get-Msg \"install.reinstall.cleanup_done\")\n }\n default {\n Write-Log (Get-Msg \"install.existing.cancelled\")\n exit 0\n }\n }\n\n # Load existing env file (upgrade path)\n if (Test-Path $script:AGENTTEAMS_ENV_FILE) {\n Write-Log (Get-Msg \"install.loading_config\" -f $script:AGENTTEAMS_ENV_FILE)\n Get-Content $script:AGENTTEAMS_ENV_FILE | ForEach-Object {\n if ($_ -match \"^([^#=][^=]*)=(.*)$\") {\n $key = $Matches[1].Trim()\n $value = $Matches[2].Split(\"#\")[0].Trim()\n if (-not [Environment]::GetEnvironmentVariable($key)) {\n [Environment]::SetEnvironmentVariable($key, $value, \"Process\")\n }\n }\n }\n }\n}\n\nfunction Step-Llm {\n Write-Log (Get-Msg \"llm.title\")\n\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n if ($script:AGENTTEAMS_LANGUAGE -eq \"zh\") {\n $script:config.LLM_PROVIDER = if ($env:AGENTTEAMS_LLM_PROVIDER) { $env:AGENTTEAMS_LLM_PROVIDER } else { \"openai-compat\" }\n $script:config.DEFAULT_MODEL = if ($env:AGENTTEAMS_DEFAULT_MODEL) { $env:AGENTTEAMS_DEFAULT_MODEL } else { \"qwen3.6-plus\" }\n $script:config.OPENAI_BASE_URL = if ($env:AGENTTEAMS_OPENAI_BASE_URL) { $env:AGENTTEAMS_OPENAI_BASE_URL } else { \"https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1\" }\n Write-Log (Get-Msg \"llm.provider.label\" -f $script:config.LLM_PROVIDER)\n Write-Log (Get-Msg \"llm.openai.base_url_label\" -f $script:config.OPENAI_BASE_URL)\n } else {\n $script:config.LLM_PROVIDER = if ($env:AGENTTEAMS_LLM_PROVIDER) { $env:AGENTTEAMS_LLM_PROVIDER } else { \"qwen\" }\n $script:config.DEFAULT_MODEL = if ($env:AGENTTEAMS_DEFAULT_MODEL) { $env:AGENTTEAMS_DEFAULT_MODEL } else { \"qwen3.6-plus\" }\n $script:config.OPENAI_BASE_URL = if ($env:AGENTTEAMS_OPENAI_BASE_URL) { $env:AGENTTEAMS_OPENAI_BASE_URL } else { \"\" }\n Write-Log (Get-Msg \"llm.provider.qwen_default\" -f $script:config.LLM_PROVIDER)\n }\n Write-Log (Get-Msg \"llm.model.label\" -f $script:config.DEFAULT_MODEL)\n Write-Log \"\"\n $script:config.LLM_API_KEY = Read-Prompt -VarName \"AGENTTEAMS_LLM_API_KEY\" -PromptText (Get-Msg \"llm.apikey_prompt\") -Secret\n $script:config.EMBEDDING_MODEL = if ($null -ne $env:AGENTTEAMS_EMBEDDING_MODEL) { $env:AGENTTEAMS_EMBEDDING_MODEL } else { \"text-embedding-v4\" }\n return\n }\n\n Write-Host \"\"\n Write-Host (Get-Msg \"llm.providers_title\")\n Write-Host (Get-Msg \"llm.provider.alibaba\")\n Write-Host (Get-Msg \"llm.provider.openai_compat\")\n Write-Host \"\"\n\n # If upgrade mode with loaded provider, show current as default\n if ($script:AGENTTEAMS_UPGRADE -and $script:config.LLM_PROVIDER) {\n $defaultProvider = if ($script:config.LLM_PROVIDER -eq \"openai-compat\") { \"2\" } else { \"1\" }\n $providerChoice = Read-Host \"$(Get-Msg 'llm.provider.select') [${defaultProvider}]\"\n $providerChoice = if ($providerChoice) { $providerChoice } else { $defaultProvider }\n } elseif ($script:AGENTTEAMS_QUICKSTART) {\n $providerChoice = Read-Host \"$(Get-Msg 'llm.provider.select') [1]\"\n } else {\n $providerChoice = Read-Host (Get-Msg \"llm.provider.select\")\n $providerChoice = if ($providerChoice) { $providerChoice } else { \"1\" }\n }\n if ($providerChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n\n switch -Regex ($providerChoice) {\n \"^(1|alibaba-cloud)$\" {\n $alibabaAccess = $null\n if ($script:AGENTTEAMS_LANGUAGE -eq \"en\") {\n $script:config.LLM_PROVIDER = \"openai-compat\"\n $script:config.OPENAI_BASE_URL = \"https://dashscope-intl.aliyuncs.com/compatible-mode/v1\"\n $alibabaAccess = \"tokenplan\"\n Write-Host \"\"\n Write-Host (Get-Msg \"llm.codingplan.models_title\")\n Write-Host (Get-Msg \"llm.codingplan.model.qwen36plus\")\n Write-Host (Get-Msg \"llm.codingplan.model.glm5\")\n Write-Host (Get-Msg \"llm.codingplan.model.kimi\")\n Write-Host (Get-Msg \"llm.codingplan.model.minimax\")\n Write-Host \"\"\n # If upgrade with loaded model, show current as default\n if ($script:AGENTTEAMS_UPGRADE -and $script:config.DEFAULT_MODEL) {\n $defaultModel = switch ($script:config.DEFAULT_MODEL) {\n \"qwen3.6-plus\" { \"1\" }\n \"glm-5\" { \"2\" }\n \"kimi-k2.5\" { \"3\" }\n \"MiniMax-M2.5\" { \"4\" }\n default { \"1\" }\n }\n $codingPlanModelChoice = Read-Host \"$(Get-Msg 'llm.codingplan.model.select') [${defaultModel}]\"\n $codingPlanModelChoice = if ($codingPlanModelChoice) { $codingPlanModelChoice } else { $defaultModel }\n } elseif ($script:AGENTTEAMS_QUICKSTART) {\n $codingPlanModelChoice = Read-Host \"$(Get-Msg 'llm.codingplan.model.select') [1]\"\n } else {\n $codingPlanModelChoice = Read-Host (Get-Msg \"llm.codingplan.model.select\")\n }\n $codingPlanModelChoice = if ($codingPlanModelChoice) { $codingPlanModelChoice } else { \"1\" }\n if ($codingPlanModelChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n switch -Regex ($codingPlanModelChoice) {\n \"^(1|qwen3\\.6-plus)$\" { $script:config.DEFAULT_MODEL = \"qwen3.6-plus\" }\n \"^(2|glm-5)$\" { $script:config.DEFAULT_MODEL = \"glm-5\" }\n \"^(3|kimi-k2\\.5)$\" { $script:config.DEFAULT_MODEL = \"kimi-k2.5\" }\n \"^(4|MiniMax-M2\\.5)$\" { $script:config.DEFAULT_MODEL = \"MiniMax-M2.5\" }\n default { $script:config.DEFAULT_MODEL = \"qwen3.6-plus\" }\n }\n Write-Log (Get-Msg \"llm.provider.selected_codingplan\")\n } else {\n Write-Host \"\"\n Write-Host (Get-Msg \"llm.alibaba.models_title\")\n Write-Host (Get-Msg \"llm.alibaba.model.tokenplan\")\n Write-Host (Get-Msg \"llm.alibaba.model.bailian\")\n Write-Host (Get-Msg \"llm.alibaba.model.codingplan_legacy\")\n Write-Host \"\"\n $modelChoice = if ($script:AGENTTEAMS_QUICKSTART) {\n Read-Host \"$(Get-Msg 'llm.alibaba.model.select') [1]\"\n } else {\n Read-Host (Get-Msg \"llm.alibaba.model.select\")\n }\n $modelChoice = if ($modelChoice) { $modelChoice } else { \"1\" }\n if ($modelChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n\n if ($modelChoice -match \"^(1|token-plan|tokenplan)$\") {\n $alibabaAccess = \"tokenplan\"\n $script:config.LLM_PROVIDER = \"openai-compat\"\n $script:config.OPENAI_BASE_URL = \"https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1\"\n Write-Host \"\"\n Write-Host (Get-Msg \"llm.codingplan.models_title\")\n Write-Host (Get-Msg \"llm.codingplan.model.qwen36plus\")\n Write-Host (Get-Msg \"llm.codingplan.model.glm5\")\n Write-Host (Get-Msg \"llm.codingplan.model.kimi\")\n Write-Host (Get-Msg \"llm.codingplan.model.minimax\")\n Write-Host \"\"\n # If upgrade with loaded model, show current as default\n if ($script:AGENTTEAMS_UPGRADE -and $script:config.DEFAULT_MODEL) {\n $defaultModel = switch ($script:config.DEFAULT_MODEL) {\n \"qwen3.6-plus\" { \"1\" }\n \"glm-5\" { \"2\" }\n \"kimi-k2.5\" { \"3\" }\n \"MiniMax-M2.5\" { \"4\" }\n default { \"1\" }\n }\n $codingPlanModelChoice = Read-Host \"$(Get-Msg 'llm.codingplan.model.select') [${defaultModel}]\"\n $codingPlanModelChoice = if ($codingPlanModelChoice) { $codingPlanModelChoice } else { $defaultModel }\n } elseif ($script:AGENTTEAMS_QUICKSTART) {\n $codingPlanModelChoice = Read-Host \"$(Get-Msg 'llm.codingplan.model.select') [1]\"\n } else {\n $codingPlanModelChoice = Read-Host (Get-Msg \"llm.codingplan.model.select\")\n }\n $codingPlanModelChoice = if ($codingPlanModelChoice) { $codingPlanModelChoice } else { \"1\" }\n if ($codingPlanModelChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n switch -Regex ($codingPlanModelChoice) {\n \"^(1|qwen3\\.6-plus)$\" { $script:config.DEFAULT_MODEL = \"qwen3.6-plus\" }\n \"^(2|glm-5)$\" { $script:config.DEFAULT_MODEL = \"glm-5\" }\n \"^(3|kimi-k2\\.5)$\" { $script:config.DEFAULT_MODEL = \"kimi-k2.5\" }\n \"^(4|MiniMax-M2\\.5)$\" { $script:config.DEFAULT_MODEL = \"MiniMax-M2.5\" }\n default { $script:config.DEFAULT_MODEL = \"qwen3.6-plus\" }\n }\n Write-Log (Get-Msg \"llm.provider.selected_tokenplan\")\n } elseif ($modelChoice -match \"^(2|qwen|bailian)$\") {\n $alibabaAccess = \"bailian\"\n $script:config.LLM_PROVIDER = \"qwen\"\n $script:config.OPENAI_BASE_URL = \"\"\n Write-Host \"\"\n if ($script:AGENTTEAMS_UPGRADE -and $script:config.DEFAULT_MODEL) {\n $qwenModelInput = Read-Host \"$(Get-Msg 'llm.qwen.model_prompt') [${script:config.DEFAULT_MODEL}]\"\n } else {\n $qwenModelInput = Read-Host (Get-Msg \"llm.qwen.model_prompt\")\n }\n if ($qwenModelInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $script:config.DEFAULT_MODEL = if ($qwenModelInput) { $qwenModelInput } elseif ($script:config.DEFAULT_MODEL) { $script:config.DEFAULT_MODEL } else { \"qwen3.6-plus\" }\n Write-Log (Get-Msg \"llm.provider.selected_qwen\")\n Request-CustomModelParams $script:config.DEFAULT_MODEL\n if ($script:StepResult -eq \"back\") { return }\n } elseif ($modelChoice -match \"^(3|coding-plan|codingplan)$\") {\n $alibabaAccess = \"codingplan_legacy\"\n $script:config.LLM_PROVIDER = \"openai-compat\"\n $script:config.OPENAI_BASE_URL = \"https://coding.dashscope.aliyuncs.com/v1\"\n Write-Host \"\"\n if ($script:AGENTTEAMS_UPGRADE -and $script:config.DEFAULT_MODEL) {\n $codingModelInput = Read-Host \"$(Get-Msg 'llm.qwen.model_prompt') [${script:config.DEFAULT_MODEL}]\"\n } else {\n $codingModelInput = Read-Host (Get-Msg \"llm.qwen.model_prompt\")\n }\n if ($codingModelInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $script:config.DEFAULT_MODEL = if ($codingModelInput) { $codingModelInput } elseif ($script:config.DEFAULT_MODEL) { $script:config.DEFAULT_MODEL } else { \"qwen3.6-plus\" }\n Write-Log (Get-Msg \"llm.provider.selected_codingplan_legacy\")\n Request-CustomModelParams $script:config.DEFAULT_MODEL\n if ($script:StepResult -eq \"back\") { return }\n } else {\n Write-Error (Get-Msg \"llm.alibaba.model.invalid\" -f $modelChoice)\n }\n }\n\n Write-Log (Get-Msg \"llm.model.label\" -f $script:config.DEFAULT_MODEL)\n Write-Log \"\"\n if ($alibabaAccess -eq \"bailian\") {\n Write-Log (Get-Msg \"llm.apikey_hint_bailian\")\n Write-Log (Get-Msg \"llm.apikey_url_bailian\")\n } elseif ($script:AGENTTEAMS_LANGUAGE -eq \"en\") {\n Write-Log (Get-Msg \"llm.apikey_hint_qwencloud\")\n Write-Log (Get-Msg \"llm.apikey_url_qwencloud\")\n } elseif ($alibabaAccess -eq \"codingplan_legacy\") {\n Write-Log (Get-Msg \"llm.apikey_hint_codingplan\")\n Write-Log (Get-Msg \"llm.apikey_url_codingplan\")\n } else {\n Write-Log (Get-Msg \"llm.apikey_hint_tokenplan\")\n Write-Log (Get-Msg \"llm.apikey_url_tokenplan\")\n }\n Write-Log \"\"\n $script:config.LLM_API_KEY = Read-Prompt -VarName \"AGENTTEAMS_LLM_API_KEY\" -PromptText (Get-Msg \"llm.apikey_prompt\") -Secret\n if ($script:StepResult -eq \"back\") { return }\n if ($alibabaAccess -eq \"bailian\") {\n Test-LlmConnectivity -BaseUrl \"https://dashscope.aliyuncs.com/compatible-mode/v1\" -ApiKey $script:config.LLM_API_KEY -Model $script:config.DEFAULT_MODEL\n } elseif ($alibabaAccess -eq \"codingplan_legacy\") {\n Test-LlmConnectivity -BaseUrl \"https://coding.dashscope.aliyuncs.com/v1\" -ApiKey $script:config.LLM_API_KEY -Model $script:config.DEFAULT_MODEL -Hint (Get-Msg \"llm.openai.test.fail.codingplan_legacy\")\n } elseif ($script:AGENTTEAMS_LANGUAGE -eq \"en\") {\n Test-LlmConnectivity -BaseUrl $script:config.OPENAI_BASE_URL -ApiKey $script:config.LLM_API_KEY -Model $script:config.DEFAULT_MODEL -Hint (Get-Msg \"llm.openai.test.fail.codingplan\")\n } else {\n Test-LlmConnectivity -BaseUrl $script:config.OPENAI_BASE_URL -ApiKey $script:config.LLM_API_KEY -Model $script:config.DEFAULT_MODEL -Hint (Get-Msg \"llm.openai.test.fail.tokenplan\")\n }\n if ($script:StepResult -eq \"back\") { return }\n }\n \"^(2|openai-compat)$\" {\n $script:config.LLM_PROVIDER = \"openai-compat\"\n Write-Log (Get-Msg \"llm.provider.selected_openai\" -f $script:config.LLM_PROVIDER)\n Write-Host \"\"\n if ($script:AGENTTEAMS_UPGRADE -and $env:AGENTTEAMS_UPGRADE_KEEP_ALL -eq \"1\") {\n Write-Log (Get-Msg \"prompt.upgrade_keep\" -f (Get-Msg \"llm.openai.base_url_prompt\"), $script:config.OPENAI_BASE_URL)\n } else {\n $currentUrl = $script:config.OPENAI_BASE_URL\n # Use base prompt without embedded default when showing current value\n if ($currentUrl) {\n $urlInput = Read-Host \"$(Get-Msg 'llm.openai.base_url_prompt') [$currentUrl]\"\n } else {\n $urlInput = Read-Host \"$(Get-Msg 'llm.openai.base_url_prompt') [https://api.openai.com/v1]\"\n }\n if ($urlInput -eq \"b\") { $script:StepResult = \"back\"; return }\n if ($urlInput) {\n $script:config.OPENAI_BASE_URL = $urlInput\n } elseif ($currentUrl) {\n $script:config.OPENAI_BASE_URL = $currentUrl\n } else {\n $script:config.OPENAI_BASE_URL = \"https://api.openai.com/v1\"\n }\n }\n if ($script:AGENTTEAMS_UPGRADE -and $env:AGENTTEAMS_UPGRADE_KEEP_ALL -eq \"1\") {\n Write-Log (Get-Msg \"prompt.upgrade_keep\" -f (Get-Msg \"llm.openai.model_prompt\"), $script:config.DEFAULT_MODEL)\n } else {\n $currentModel = $script:config.DEFAULT_MODEL\n if ($currentModel) {\n $modelInput = Read-Host \"$(Get-Msg 'llm.openai.model_prompt') [${currentModel}]\"\n } else {\n $modelInput = Read-Host (Get-Msg 'llm.openai.model_prompt')\n }\n if ($modelInput -eq \"b\") { $script:StepResult = \"back\"; return }\n if ($modelInput) {\n $script:config.DEFAULT_MODEL = $modelInput\n } elseif ($currentModel) {\n $script:config.DEFAULT_MODEL = $currentModel\n } else {\n $script:config.DEFAULT_MODEL = \"gpt-5.4\"\n }\n }\n Write-Log (Get-Msg \"llm.openai.base_url_label\" -f $script:config.OPENAI_BASE_URL)\n Write-Log (Get-Msg \"llm.model.label\" -f $script:config.DEFAULT_MODEL)\n Request-CustomModelParams $script:config.DEFAULT_MODEL\n if ($script:StepResult -eq \"back\") { return }\n Write-Log \"\"\n $script:config.LLM_API_KEY = Read-Prompt -VarName \"AGENTTEAMS_LLM_API_KEY\" -PromptText (Get-Msg \"llm.apikey_prompt\") -Secret\n if ($script:StepResult -eq \"back\") { return }\n Test-LlmConnectivity -BaseUrl $script:config.OPENAI_BASE_URL -ApiKey $script:config.LLM_API_KEY -Model $script:config.DEFAULT_MODEL\n if ($script:StepResult -eq \"back\") { return }\n }\n default {\n Write-Error (Get-Msg \"llm.provider.invalid\" -f $providerChoice)\n }\n }\n\n # Skip to embedding if Keep-All mode handled LLM params\n if ($skipToEmbedding) {\n # Skip embedding selection, use loaded value (already in $script:config.EMBEDDING_MODEL)\n if ($script:config.EMBEDDING_MODEL) {\n Write-Log (Get-Msg \"llm.embedding.title\")\n Write-Log (Get-Msg \"llm.model.label\" -f $script:config.EMBEDDING_MODEL)\n Write-Log \"\"\n }\n return\n }\n\n # --- Embedding model (optional, auto-tested) ---\n Write-Host \"\"\n Write-Log (Get-Msg \"llm.embedding.title\")\n Write-Log (Get-Msg \"llm.embedding.hint\")\n Write-Host \"\"\n Write-Host (Get-Msg \"llm.embedding.option.default\")\n Write-Host (Get-Msg \"llm.embedding.option.custom\")\n Write-Host (Get-Msg \"llm.embedding.option.disable\")\n Write-Host \"\"\n $embChoice = Read-Host \"$(Get-Msg 'llm.embedding.select') [1]\"\n $embChoice = if ($embChoice) { $embChoice } else { \"1\" }\n if ($embChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n\n switch ($embChoice) {\n \"1\" {\n $script:config.EMBEDDING_MODEL = \"text-embedding-v4\"\n }\n \"2\" {\n if ($script:AGENTTEAMS_UPGRADE -and $env:AGENTTEAMS_UPGRADE_KEEP_ALL -eq \"1\") {\n Write-Log (Get-Msg \"prompt.upgrade_keep\" -f (Get-Msg \"llm.embedding.custom_prompt\"), $script:config.EMBEDDING_MODEL)\n } else {\n $currentEmb = $script:config.EMBEDDING_MODEL\n $embCustom = Read-Host (Get-Msg \"llm.embedding.custom_prompt\")\n if ($embCustom -eq \"b\") { $script:StepResult = \"back\"; return }\n if ($embCustom) {\n $script:config.EMBEDDING_MODEL = $embCustom\n } else {\n $script:config.EMBEDDING_MODEL = $currentEmb\n if (-not $script:config.EMBEDDING_MODEL) {\n Write-Log (Get-Msg \"llm.embedding.disabled\")\n }\n }\n }\n }\n \"3\" {\n $script:config.EMBEDDING_MODEL = \"\"\n Write-Log (Get-Msg \"llm.embedding.disabled\")\n }\n default {\n $script:config.EMBEDDING_MODEL = \"text-embedding-v4\"\n }\n }\n\n if ($script:config.EMBEDDING_MODEL) {\n # Qwen provider uses dashscope directly; others use OPENAI_BASE_URL\n $embBaseUrl = $script:config.OPENAI_BASE_URL\n if ($script:config.LLM_PROVIDER -eq \"qwen\") {\n $embBaseUrl = \"https://dashscope.aliyuncs.com/compatible-mode/v1\"\n }\n $embResult = Test-EmbeddingConnectivity -BaseUrl $embBaseUrl -ApiKey $script:config.LLM_API_KEY -Model $script:config.EMBEDDING_MODEL\n if (-not $embResult) {\n $script:config.EMBEDDING_MODEL = \"\"\n Write-Log (Get-Msg \"llm.embedding.auto_disabled\")\n }\n }\n\n Write-Log \"\"\n}\n\nfunction Step-Admin {\n Write-Log (Get-Msg \"admin.title\")\n $script:config.ADMIN_USER = Read-Prompt -VarName \"AGENTTEAMS_ADMIN_USER\" -PromptText (Get-Msg \"admin.username_prompt\") -Default \"admin\"\n if ($script:StepResult -eq \"back\") { return }\n $script:config.ADMIN_USER = $script:config.ADMIN_USER.ToLowerInvariant()\n\n # Pre-set via env var: validate; non-interactive fails fast,\n # interactive warns and falls through to the retry prompt.\n if ($env:AGENTTEAMS_ADMIN_PASSWORD) {\n $script:config.ADMIN_PASSWORD = $env:AGENTTEAMS_ADMIN_PASSWORD\n Write-Log (Get-Msg \"prompt.preset\" -f (Get-Msg \"admin.password_prompt\"))\n if ($script:config.ADMIN_PASSWORD.Length -ge 8) {\n Write-Log \"\"\n return\n }\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n Write-Error (Get-Msg \"admin.password_too_short\" -f $script:config.ADMIN_PASSWORD.Length)\n }\n Write-Host \"$($script:ESC)[31m[AgentTeams ERROR]$($script:ESC)[0m $(Get-Msg \"admin.password_too_short\" -f $script:config.ADMIN_PASSWORD.Length)\"\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_ADMIN_PASSWORD\", $null, \"Process\")\n }\n\n while ($true) {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_ADMIN_PASSWORD\", $null, \"Process\")\n $script:config.ADMIN_PASSWORD = Read-Prompt -VarName \"AGENTTEAMS_ADMIN_PASSWORD\" -PromptText (Get-Msg \"admin.password_prompt\") -Secret -Optional\n if ($script:StepResult -eq \"back\") { return }\n if (-not $script:config.ADMIN_PASSWORD) {\n $randomSuffix = (New-RandomKey).Substring(0, 12)\n $script:config.ADMIN_PASSWORD = \"admin$randomSuffix\"\n Write-Log (Get-Msg \"admin.password_generated\")\n break\n }\n if ($script:config.ADMIN_PASSWORD.Length -ge 8) {\n break\n }\n Write-Host \"$($script:ESC)[31m[AgentTeams ERROR]$($script:ESC)[0m $(Get-Msg \"admin.password_too_short\" -f $script:config.ADMIN_PASSWORD.Length)\"\n }\n\n Write-Log \"\"\n}\n\nfunction Step-Network {\n Write-Log (Get-Msg \"port.local_only.title\")\n Write-Host \"\"\n Write-Host \" 1) $(Get-Msg 'port.local_only.hint_yes')\"\n Write-Host \" 2) $(Get-Msg 'port.local_only.hint_no')\"\n Write-Host \"\"\n\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n $localOnly = if ($env:AGENTTEAMS_LOCAL_ONLY) { $env:AGENTTEAMS_LOCAL_ONLY } else { \"1\" }\n } elseif ($null -ne $env:AGENTTEAMS_LOCAL_ONLY) {\n $localOnly = $env:AGENTTEAMS_LOCAL_ONLY\n } else {\n $localChoice = Read-Host \"$(Get-Msg 'port.local_only.choice')\"\n if ($localChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n if (-not $localChoice) { $localChoice = \"1\" }\n $localOnly = if ($localChoice -match '^(2|n|N|no|NO)$') { \"0\" } else { \"1\" }\n }\n $script:config.LOCAL_ONLY = $localOnly\n\n if ($localOnly -eq \"1\") {\n Write-Log (Get-Msg \"port.local_only.selected_local\")\n } else {\n Write-Log (Get-Msg \"port.local_only.selected_external\")\n Write-Host \"\"\n Write-Host (Get-Msg \"port.local_only.https_hint\") -ForegroundColor Yellow\n }\n Write-Log \"\"\n}\n\nfunction Step-Ports {\n Write-Log (Get-Msg \"port.title\")\n $script:config.PORT_GATEWAY = Read-Prompt -VarName \"AGENTTEAMS_PORT_GATEWAY\" -PromptText (Get-Msg \"port.gateway_prompt\") -Default \"18080\"\n if ($script:StepResult -eq \"back\") { return }\n $script:config.PORT_CONSOLE = Read-Prompt -VarName \"AGENTTEAMS_PORT_CONSOLE\" -PromptText (Get-Msg \"port.console_prompt\") -Default \"18001\"\n if ($script:StepResult -eq \"back\") { return }\n $script:config.PORT_ELEMENT_WEB = Read-Prompt -VarName \"AGENTTEAMS_PORT_ELEMENT_WEB\" -PromptText (Get-Msg \"port.element_prompt\") -Default \"18088\"\n if ($script:StepResult -eq \"back\") { return }\n $script:config.PORT_MANAGER_CONSOLE = Read-Prompt -VarName \"AGENTTEAMS_PORT_MANAGER_CONSOLE\" -PromptText (Get-Msg \"port.manager_console_prompt\") -Default \"18888\"\n if ($script:StepResult -eq \"back\") { return }\n Write-Log \"\"\n}\n\nfunction Step-Domains {\n Write-Log (Get-Msg \"domain.title\")\n Write-Log (Get-Msg \"domain.hint\")\n $script:config.MATRIX_DOMAIN = Read-Prompt -VarName \"AGENTTEAMS_MATRIX_DOMAIN\" -PromptText (Get-Msg \"domain.matrix_prompt\") -Default \"matrix-local.agentteams.io:$($script:config.PORT_GATEWAY)\"\n if ($script:StepResult -eq \"back\") { return }\n $script:config.MATRIX_CLIENT_DOMAIN = Read-Prompt -VarName \"AGENTTEAMS_MATRIX_CLIENT_DOMAIN\" -PromptText (Get-Msg \"domain.element_prompt\") -Default \"matrix-client-local.agentteams.io\"\n if ($script:StepResult -eq \"back\") { return }\n $script:config.AI_GATEWAY_DOMAIN = Read-Prompt -VarName \"AGENTTEAMS_AI_GATEWAY_DOMAIN\" -PromptText (Get-Msg \"domain.gateway_prompt\") -Default \"aigw-local.agentteams.io\"\n if ($script:StepResult -eq \"back\") { return }\n $script:config.FS_DOMAIN = Read-Prompt -VarName \"AGENTTEAMS_FS_DOMAIN\" -PromptText (Get-Msg \"domain.fs_prompt\") -Default \"fs-local.agentteams.io\"\n if ($script:StepResult -eq \"back\") { return }\n $script:config.CONSOLE_DOMAIN = Read-Prompt -VarName \"AGENTTEAMS_CONSOLE_DOMAIN\" -PromptText (Get-Msg \"domain.console_prompt\") -Default \"console-local.agentteams.io\"\n if ($script:StepResult -eq \"back\") { return }\n Write-Log \"\"\n}\n\nfunction Step-Github {\n Write-Log (Get-Msg \"github.title\")\n $script:config.GITHUB_TOKEN = Read-Prompt -VarName \"AGENTTEAMS_GITHUB_TOKEN\" -PromptText (Get-Msg \"github.token_prompt\") -Secret -Optional\n}\n\nfunction Step-Skills {\n Write-Log \"\"\n Write-Log (Get-Msg \"skills.title\")\n $script:config.SKILLS_API_URL = Read-Prompt -VarName \"AGENTTEAMS_SKILLS_API_URL\" -PromptText (Get-Msg \"skills.url_prompt\") -Optional\n if ($script:StepResult -eq \"back\") { return }\n Write-Log \"\"\n}\n\nfunction Step-Volume {\n Write-Log (Get-Msg \"data.title\")\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n $script:config.DATA_DIR = if ($env:AGENTTEAMS_DATA_DIR) { $env:AGENTTEAMS_DATA_DIR } else { \"agentteams-data\" }\n Write-Log \" $(Get-Msg 'data.volume_using' -f $script:config.DATA_DIR) (non-interactive, skipped)\"\n return\n }\n # ─────────────────────────────────────────────────────────────────\n $dataDirInput = Read-Host (Get-Msg \"data.volume_prompt\")\n if ($dataDirInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $script:config.DATA_DIR = if ($dataDirInput) { $dataDirInput } else { \"agentteams-data\" }\n Write-Log (Get-Msg \"data.volume_using\" -f $script:config.DATA_DIR)\n}\n\nfunction Step-Workspace {\n Write-Log (Get-Msg \"workspace.title\")\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n $script:config.WORKSPACE_DIR = if ($env:AGENTTEAMS_WORKSPACE_DIR) { $env:AGENTTEAMS_WORKSPACE_DIR } else { \"$env:USERPROFILE\\agentteams-manager\" }\n if (-not (Test-Path $script:config.WORKSPACE_DIR)) {\n New-Item -ItemType Directory -Path $script:config.WORKSPACE_DIR -Force | Out-Null\n }\n Write-Log \" $(Get-Msg 'workspace.dir_label' -f $script:config.WORKSPACE_DIR) (non-interactive, skipped)\"\n return\n }\n # ─────────────────────────────────────────────────────────────────\n $defaultWorkspace = \"$env:USERPROFILE\\agentteams-manager\"\n if ($script:AGENTTEAMS_UPGRADE -and $env:AGENTTEAMS_UPGRADE_KEEP_ALL -eq \"1\") {\n Write-Log (Get-Msg \"prompt.upgrade_keep\" -f (Get-Msg \"workspace.dir_prompt\" -f $defaultWorkspace), $script:config.WORKSPACE_DIR)\n $wsInput = if ($script:config.WORKSPACE_DIR) { $script:config.WORKSPACE_DIR } else { $defaultWorkspace }\n } else {\n $currentWs = $script:config.WORKSPACE_DIR\n $wsInput = Read-Host (Get-Msg \"workspace.dir_prompt\" -f $defaultWorkspace)\n if ($wsInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $wsInput = if ($wsInput) { $wsInput } else { if ($currentWs) { $currentWs } else { $defaultWorkspace } }\n }\n $script:config.WORKSPACE_DIR = $wsInput\n if (-not (Test-Path $script:config.WORKSPACE_DIR)) {\n New-Item -ItemType Directory -Path $script:config.WORKSPACE_DIR -Force | Out-Null\n }\n Write-Log (Get-Msg \"workspace.dir_label\" -f $script:config.WORKSPACE_DIR)\n}\n\nfunction Step-Runtime {\n Write-Log (Get-Msg \"worker_runtime.title\")\n Write-Host \"\"\n Write-Host \" 1) $(Get-Msg 'worker_runtime.copaw')\"\n Write-Host \" 2) $(Get-Msg 'worker_runtime.openclaw')\"\n Write-Host \" 3) $(Get-Msg 'worker_runtime.hermes')\"\n Write-Host \"\"\n\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n $script:config.DEFAULT_WORKER_RUNTIME = if ($env:AGENTTEAMS_DEFAULT_WORKER_RUNTIME) { $env:AGENTTEAMS_DEFAULT_WORKER_RUNTIME } else { \"copaw\" }\n } elseif ($script:AGENTTEAMS_UPGRADE -and $env:AGENTTEAMS_DEFAULT_WORKER_RUNTIME) {\n Write-Log (Get-Msg \"prompt.upgrade_keep\" -f (Get-Msg \"worker_runtime.title_short\"), $env:AGENTTEAMS_DEFAULT_WORKER_RUNTIME)\n $rtChoice = Read-Host (Get-Msg \"worker_runtime.choice\")\n if ($rtChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n if ($rtChoice) {\n $script:config.DEFAULT_WORKER_RUNTIME = switch ($rtChoice) {\n \"2\" { \"openclaw\" }\n \"3\" { \"hermes\" }\n default { \"copaw\" }\n }\n } else {\n $script:config.DEFAULT_WORKER_RUNTIME = $env:AGENTTEAMS_DEFAULT_WORKER_RUNTIME\n }\n } elseif ($env:AGENTTEAMS_DEFAULT_WORKER_RUNTIME) {\n $script:config.DEFAULT_WORKER_RUNTIME = $env:AGENTTEAMS_DEFAULT_WORKER_RUNTIME\n } else {\n $rtChoice = Read-Host (Get-Msg \"worker_runtime.choice\")\n if ($rtChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n $rtChoice = if ($rtChoice) { $rtChoice } else { \"1\" }\n $script:config.DEFAULT_WORKER_RUNTIME = switch ($rtChoice) {\n \"2\" { \"openclaw\" }\n \"3\" { \"hermes\" }\n default { \"copaw\" }\n }\n }\n Write-Log (Get-Msg \"worker_runtime.selected\" -f $script:config.DEFAULT_WORKER_RUNTIME)\n}\n\nfunction Step-ManagerRuntime {\n Write-Log (Get-Msg \"manager_runtime.title\")\n Write-Host \"\"\n Write-Host \" 1) $(Get-Msg 'manager_runtime.copaw')\"\n Write-Host \" 2) $(Get-Msg 'manager_runtime.openclaw')\"\n Write-Host \"\"\n\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n $script:config.MANAGER_RUNTIME = if ($env:AGENTTEAMS_MANAGER_RUNTIME) { $env:AGENTTEAMS_MANAGER_RUNTIME } else { \"copaw\" }\n } elseif ($script:AGENTTEAMS_UPGRADE -and $env:AGENTTEAMS_MANAGER_RUNTIME) {\n Write-Log (Get-Msg \"prompt.upgrade_keep\" -f (Get-Msg \"manager_runtime.title_short\"), $env:AGENTTEAMS_MANAGER_RUNTIME)\n $mrChoice = Read-Host (Get-Msg \"manager_runtime.choice\")\n if ($mrChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n if ($mrChoice) {\n $script:config.MANAGER_RUNTIME = if ($mrChoice -eq \"2\") { \"openclaw\" } else { \"copaw\" }\n } else {\n $script:config.MANAGER_RUNTIME = $env:AGENTTEAMS_MANAGER_RUNTIME\n }\n } elseif ($env:AGENTTEAMS_MANAGER_RUNTIME) {\n $script:config.MANAGER_RUNTIME = $env:AGENTTEAMS_MANAGER_RUNTIME\n } else {\n $mrChoice = Read-Host (Get-Msg \"manager_runtime.choice\")\n if ($mrChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n $mrChoice = if ($mrChoice) { $mrChoice } else { \"1\" }\n $script:config.MANAGER_RUNTIME = if ($mrChoice -eq \"2\") { \"openclaw\" } else { \"copaw\" }\n }\n Write-Log (Get-Msg \"manager_runtime.selected\" -f $script:config.MANAGER_RUNTIME)\n}\n\nfunction Step-E2ee {\n Write-Host \"\"\n Write-Log (Get-Msg \"matrix_e2ee.title\")\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n $script:config.MATRIX_E2EE = if ($env:AGENTTEAMS_MATRIX_E2EE) { $env:AGENTTEAMS_MATRIX_E2EE } else { \"0\" }\n Write-Log \" $(Get-Msg 'matrix_e2ee.title_short') = $($script:config.MATRIX_E2EE) (non-interactive, skipped)\"\n return\n }\n # ─────────────────────────────────────────────────────────────────\n Write-Host \"\"\n Write-Host \" $(Get-Msg 'matrix_e2ee.desc')\"\n Write-Host \"\"\n Write-Host \" 1) $(Get-Msg 'matrix_e2ee.disable')\"\n Write-Host \" 2) $(Get-Msg 'matrix_e2ee.enable')\"\n Write-Host \"\"\n\n if ($script:AGENTTEAMS_UPGRADE -and $env:AGENTTEAMS_MATRIX_E2EE) {\n $e2eeDisplay = if ($env:AGENTTEAMS_MATRIX_E2EE -eq \"1\") { Get-Msg \"matrix_e2ee.val_enabled\" } else { Get-Msg \"matrix_e2ee.val_disabled\" }\n Write-Log (Get-Msg \"prompt.upgrade_keep\" -f (Get-Msg \"matrix_e2ee.title_short\"), $e2eeDisplay)\n $e2eeChoice = Read-Host (Get-Msg \"matrix_e2ee.choice\")\n if ($e2eeChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n if ($e2eeChoice) {\n $script:config.MATRIX_E2EE = if ($e2eeChoice -eq \"2\") { \"1\" } else { \"0\" }\n } else {\n $script:config.MATRIX_E2EE = $env:AGENTTEAMS_MATRIX_E2EE\n }\n } elseif (-not $env:AGENTTEAMS_MATRIX_E2EE) {\n $e2eeChoice = Read-Host (Get-Msg \"matrix_e2ee.choice\")\n if ($e2eeChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n $e2eeChoice = if ($e2eeChoice) { $e2eeChoice } else { \"1\" }\n $script:config.MATRIX_E2EE = if ($e2eeChoice -eq \"2\") { \"1\" } else { \"0\" }\n } else {\n $script:config.MATRIX_E2EE = $env:AGENTTEAMS_MATRIX_E2EE\n }\n\n if ($script:config.MATRIX_E2EE -eq \"1\") {\n Write-Log (Get-Msg \"matrix_e2ee.selected_enabled\")\n } else {\n Write-Log (Get-Msg \"matrix_e2ee.selected_disabled\")\n }\n}\n\nfunction Step-DockerProxy {\n if (-not $script:AGENTTEAMS_MOUNT_SOCKET) {\n $script:config.DOCKER_PROXY = \"0\"\n return\n }\n\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n $script:config.DOCKER_PROXY = if ($env:AGENTTEAMS_DOCKER_PROXY) { $env:AGENTTEAMS_DOCKER_PROXY } else { \"0\" }\n Write-Log \" $(Get-Msg 'docker_proxy.title_short') = $($script:config.DOCKER_PROXY) (non-interactive, skipped)\"\n return\n }\n # ─────────────────────────────────────────────────────────────────\n\n Write-Host \"\"\n Write-Log (Get-Msg \"docker_proxy.title\")\n Write-Host \"\"\n Write-Host \" $(Get-Msg 'docker_proxy.desc')\"\n Write-Host \"\"\n Write-Host \" 1) $(Get-Msg 'docker_proxy.enable')\"\n Write-Host \" 2) $(Get-Msg 'docker_proxy.disable')\"\n Write-Host \"\"\n\n if ($script:AGENTTEAMS_UPGRADE -and $env:AGENTTEAMS_DOCKER_PROXY) {\n $proxyDisplay = if ($env:AGENTTEAMS_DOCKER_PROXY -eq \"1\") { Get-Msg \"docker_proxy.val_enabled\" } else { Get-Msg \"docker_proxy.val_disabled\" }\n Write-Log (Get-Msg \"prompt.upgrade_keep\" -f (Get-Msg \"docker_proxy.title_short\"), $proxyDisplay)\n $proxyChoice = Read-Host (Get-Msg \"docker_proxy.choice\")\n if ($proxyChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n if ($proxyChoice) {\n $script:config.DOCKER_PROXY = if ($proxyChoice -eq \"2\") { \"0\" } else { \"1\" }\n } else {\n $script:config.DOCKER_PROXY = $env:AGENTTEAMS_DOCKER_PROXY\n }\n } elseif (-not $env:AGENTTEAMS_DOCKER_PROXY) {\n $proxyChoice = Read-Host (Get-Msg \"docker_proxy.choice\")\n if ($proxyChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n $proxyChoice = if ($proxyChoice) { $proxyChoice } else { \"1\" }\n $script:config.DOCKER_PROXY = if ($proxyChoice -eq \"2\") { \"0\" } else { \"1\" }\n } else {\n $script:config.DOCKER_PROXY = $env:AGENTTEAMS_DOCKER_PROXY\n }\n\n if ($script:config.DOCKER_PROXY -eq \"1\") {\n Write-Log (Get-Msg \"docker_proxy.selected_enabled\")\n\n # Prompt for additional allowed image sources\n Write-Host \"\"\n Write-Host \" $(Get-Msg 'docker_proxy.registries_desc')\"\n Write-Host \"\"\n if ($script:AGENTTEAMS_UPGRADE -and $env:AGENTTEAMS_PROXY_ALLOWED_REGISTRIES) {\n Write-Log (Get-Msg \"prompt.upgrade_keep\" -f (Get-Msg \"docker_proxy.registries_label\"), $env:AGENTTEAMS_PROXY_ALLOWED_REGISTRIES)\n $regInput = Read-Host (Get-Msg \"docker_proxy.registries_prompt\")\n if ($regInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $script:config.PROXY_ALLOWED_REGISTRIES = if ($regInput) { $regInput } else { $env:AGENTTEAMS_PROXY_ALLOWED_REGISTRIES }\n } elseif (-not $env:AGENTTEAMS_PROXY_ALLOWED_REGISTRIES) {\n $regInput = Read-Host (Get-Msg \"docker_proxy.registries_prompt\")\n if ($regInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $script:config.PROXY_ALLOWED_REGISTRIES = if ($regInput) { $regInput } else { \"\" }\n } else {\n $script:config.PROXY_ALLOWED_REGISTRIES = $env:AGENTTEAMS_PROXY_ALLOWED_REGISTRIES\n }\n } else {\n Write-Log (Get-Msg \"docker_proxy.selected_disabled\")\n }\n}\n\nfunction Step-Idle {\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n $script:config.WORKER_IDLE_TIMEOUT = if ($env:AGENTTEAMS_WORKER_IDLE_TIMEOUT) { $env:AGENTTEAMS_WORKER_IDLE_TIMEOUT } else { \"720\" }\n Write-Log \" $(Get-Msg 'idle_timeout.label') = $($script:config.WORKER_IDLE_TIMEOUT) (non-interactive, skipped)\"\n return\n }\n # ─────────────────────────────────────────────────────────────────\n if ($script:AGENTTEAMS_UPGRADE -and $env:AGENTTEAMS_WORKER_IDLE_TIMEOUT) {\n Write-Log (Get-Msg \"prompt.upgrade_keep\" -f (Get-Msg \"idle_timeout.label\"), $env:AGENTTEAMS_WORKER_IDLE_TIMEOUT)\n $idleInput = Read-Host (Get-Msg \"idle_timeout.prompt\")\n if ($idleInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $script:config.WORKER_IDLE_TIMEOUT = if ($idleInput) { $idleInput } else { $env:AGENTTEAMS_WORKER_IDLE_TIMEOUT }\n } elseif (-not $env:AGENTTEAMS_WORKER_IDLE_TIMEOUT) {\n $idleInput = Read-Host (Get-Msg \"idle_timeout.prompt\")\n if ($idleInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $script:config.WORKER_IDLE_TIMEOUT = if ($idleInput) { $idleInput } else { \"720\" }\n } else {\n $script:config.WORKER_IDLE_TIMEOUT = $env:AGENTTEAMS_WORKER_IDLE_TIMEOUT\n }\n Write-Log (Get-Msg \"idle_timeout.selected\" -f $script:config.WORKER_IDLE_TIMEOUT)\n Write-Log \"\"\n}\n\nfunction Step-Hostshare {\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n $script:config.HOST_SHARE_DIR = if ($env:AGENTTEAMS_HOST_SHARE_DIR) { $env:AGENTTEAMS_HOST_SHARE_DIR } else { $env:USERPROFILE }\n Write-Log \" $(Get-Msg 'host_share.label') = $($script:config.HOST_SHARE_DIR) (non-interactive, skipped)\"\n return\n }\n # ─────────────────────────────────────────────────────────────────\n $currentShare = $script:config.HOST_SHARE_DIR\n if ($currentShare) {\n $shareInput = Read-Host \"$(Get-Msg 'host_share.prompt' -f $env:USERPROFILE) [${currentShare}]\"\n } else {\n $shareInput = Read-Host (Get-Msg 'host_share.prompt' -f $env:USERPROFILE)\n }\n if ($shareInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $script:config.HOST_SHARE_DIR = if ($shareInput) { $shareInput } else { if ($currentShare) { $currentShare } else { $env:USERPROFILE } }\n}\n\n# ============================================================\n# Manager Installation\n# ============================================================\n\nfunction Install-Manager {\n Write-Log (Get-Msg \"install.title\")\n\n # Detect timezone\n $script:AGENTTEAMS_TIMEZONE = Get-AgentTeamsTimeZone\n\n # Language priority: env var > existing env file > timezone detection\n if ($env:AGENTTEAMS_LANGUAGE) {\n $script:AGENTTEAMS_LANGUAGE = $env:AGENTTEAMS_LANGUAGE\n } else {\n # Check existing env file for saved language preference (upgrade scenario)\n $_envFile = $script:AGENTTEAMS_ENV_FILE\n # Migrate from legacy location (current directory) if needed\n if (-not (Test-Path $_envFile) -and (Test-Path \".\\agentteams-manager.env\")) {\n Write-Log \"Migrating agentteams-manager.env to $_envFile...\"\n Move-Item \".\\agentteams-manager.env\" $_envFile -ErrorAction SilentlyContinue\n }\n if (Test-Path $_envFile) {\n $_savedLang = (Get-Content $_envFile | Select-String \"^AGENTTEAMS_LANGUAGE=\" | ForEach-Object {\n $_.Line -replace '^AGENTTEAMS_LANGUAGE=', ''\n } | Select-Object -First 1)\n if ($_savedLang) {\n $script:AGENTTEAMS_LANGUAGE = $_savedLang\n }\n }\n # Fall back to timezone-based detection\n if (-not $script:AGENTTEAMS_LANGUAGE) {\n $script:AGENTTEAMS_LANGUAGE = Get-AgentTeamsLanguage -Timezone $script:AGENTTEAMS_TIMEZONE\n }\n }\n $env:AGENTTEAMS_LANGUAGE = $script:AGENTTEAMS_LANGUAGE\n\n # Detect registry\n $script:AGENTTEAMS_REGISTRY = Get-Registry -Timezone $script:AGENTTEAMS_TIMEZONE\n\n # Set image names\n $script:MANAGER_IMAGE = if ($env:AGENTTEAMS_INSTALL_MANAGER_IMAGE) {\n $env:AGENTTEAMS_INSTALL_MANAGER_IMAGE\n } else {\n \"$($script:AGENTTEAMS_REGISTRY)/agentteams/agentteams-manager:$($script:AGENTTEAMS_VERSION)\"\n }\n\n $script:WORKER_IMAGE = if ($env:AGENTTEAMS_INSTALL_WORKER_IMAGE) {\n $env:AGENTTEAMS_INSTALL_WORKER_IMAGE\n } else {\n \"$($script:AGENTTEAMS_REGISTRY)/agentteams/agentteams-worker:$($script:AGENTTEAMS_VERSION)\"\n }\n\n $script:COPAW_WORKER_IMAGE = if ($env:AGENTTEAMS_INSTALL_COPAW_WORKER_IMAGE) {\n $env:AGENTTEAMS_INSTALL_COPAW_WORKER_IMAGE\n } else {\n \"$($script:AGENTTEAMS_REGISTRY)/agentteams/agentteams-copaw-worker:$($script:AGENTTEAMS_VERSION)\"\n }\n\n $script:QWENPAW_WORKER_IMAGE = if ($env:AGENTTEAMS_INSTALL_QWENPAW_WORKER_IMAGE) {\n $env:AGENTTEAMS_INSTALL_QWENPAW_WORKER_IMAGE\n } else {\n \"$($script:AGENTTEAMS_REGISTRY)/agentteams/agentteams-qwenpaw-worker:$($script:AGENTTEAMS_VERSION)\"\n }\n\n $script:HERMES_WORKER_IMAGE = if ($env:AGENTTEAMS_INSTALL_HERMES_WORKER_IMAGE) {\n $env:AGENTTEAMS_INSTALL_HERMES_WORKER_IMAGE\n } else {\n \"$($script:AGENTTEAMS_REGISTRY)/agentteams/agentteams-hermes-worker:$($script:AGENTTEAMS_VERSION)\"\n }\n\n $script:MANAGER_COPAW_IMAGE = if ($env:AGENTTEAMS_INSTALL_MANAGER_COPAW_IMAGE) {\n $env:AGENTTEAMS_INSTALL_MANAGER_COPAW_IMAGE\n } else {\n \"$($script:AGENTTEAMS_REGISTRY)/agentteams/agentteams-manager-copaw:$($script:AGENTTEAMS_VERSION)\"\n }\n\n $script:CONTROLLER_IMAGE = if ($env:AGENTTEAMS_INSTALL_CONTROLLER_IMAGE) {\n $env:AGENTTEAMS_INSTALL_CONTROLLER_IMAGE\n } else {\n \"$($script:AGENTTEAMS_REGISTRY)/agentteams/agentteams-controller:$($script:AGENTTEAMS_VERSION)\"\n }\n\n # Resolve embedded controller image (sets $script:EMBEDDED_IMAGE and\n # $script:AGENTTEAMS_USE_EMBEDDED). Errors out fast if no embedded image is available\n # for the requested version (mirrors the bash installer behavior).\n Resolve-EmbeddedImage\n\n Write-Log (Get-Msg \"install.registry\" -f $script:AGENTTEAMS_REGISTRY)\n Write-Log \"\"\n Write-Log (Get-Msg \"install.dir\" -f (Get-Location))\n Write-Log (Get-Msg \"install.dir_hint\")\n Write-Log (Get-Msg \"install.dir_hint2\")\n Write-Log \"\"\n\n # Check container runtime (docker or podman)\n if (-not (Get-Command \"docker\" -ErrorAction SilentlyContinue) -and\n -not (Get-Command \"podman\" -ErrorAction SilentlyContinue)) {\n Write-Host \"$($script:ESC)[31m[AgentTeams ERROR]$($script:ESC)[0m $(Get-Msg 'error.docker_not_found')\" -ForegroundColor Red\n Exit-Script 1\n }\n\n if (-not (Test-DockerRunning)) {\n Write-Host \"$($script:ESC)[31m[AgentTeams ERROR]$($script:ESC)[0m $(Get-Msg 'error.docker_not_running')\" -ForegroundColor Red\n Exit-Script 1\n }\n\n # Initialize shared config hashtable\n $script:config = @{}\n $config = $script:config # local alias so post-machine code can use $config\n\n # Orphan volume detection (env file gone but volume remains)\n if (-not (Test-Path $script:AGENTTEAMS_ENV_FILE)) {\n $dataVol = if ($env:AGENTTEAMS_DATA_DIR) { $env:AGENTTEAMS_DATA_DIR } else { \"agentteams-data\" }\n $volumeExists = docker volume ls -q 2>$null | Select-String \"^${dataVol}$\"\n if ($volumeExists) {\n Write-Host \"\"\n Write-Log (Get-Msg \"install.orphan_volume.detected\" -f $dataVol)\n Write-Log (Get-Msg \"install.orphan_volume.warn\")\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n Write-Log (Get-Msg \"install.orphan_volume.clean_noninteractive\")\n docker stop agentteams-manager *>$null\n docker rm agentteams-manager *>$null\n docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-worker-\" | ForEach-Object {\n docker stop $_.ToString().Trim() *>$null\n docker rm $_.ToString().Trim() *>$null\n }\n Write-Log (Get-Msg \"install.orphan_volume.cleaning\")\n docker volume rm $dataVol *>$null\n Write-Log (Get-Msg \"install.orphan_volume.cleaned\")\n } else {\n Write-Host \"\"\n Write-Host (Get-Msg \"install.orphan_volume.choose\")\n Write-Host (Get-Msg \"install.orphan_volume.clean\")\n Write-Host (Get-Msg \"install.orphan_volume.keep\")\n Write-Host \"\"\n $orphanChoice = Read-Host (Get-Msg \"install.orphan_volume.prompt\")\n if (-not $orphanChoice) { $orphanChoice = \"1\" }\n switch -Regex ($orphanChoice) {\n \"^(1|clean)$\" {\n docker stop agentteams-manager *>$null\n docker rm agentteams-manager *>$null\n docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-worker-\" | ForEach-Object {\n docker stop $_.ToString().Trim() *>$null\n docker rm $_.ToString().Trim() *>$null\n }\n Write-Log (Get-Msg \"install.orphan_volume.cleaning\")\n docker volume rm $dataVol *>$null\n Write-Log (Get-Msg \"install.orphan_volume.cleaned\")\n }\n \"^(2|keep)$\" {\n Write-Log (Get-Msg \"install.orphan_volume.keeping\")\n }\n }\n }\n }\n }\n\n # ── State machine ─────────────────────────────────────────────────────────\n $_steps = @(\"Step-Lang\", \"Step-Mode\", \"Step-Existing\", \"Step-Llm\", \"Step-ManagerRuntime\", \"Step-Runtime\", \"Step-Admin\",\n \"Step-Network\", \"Step-Ports\", \"Step-Domains\", \"Step-Github\", \"Step-Skills\",\n \"Step-Volume\", \"Step-Workspace\", \"Step-E2ee\", \"Step-DockerProxy\", \"Step-Idle\",\n \"Step-Hostshare\")\n $_stepHistory = [System.Collections.Generic.List[int]]::new()\n $_stepIdx = 0\n while ($_stepIdx -lt $_steps.Count) {\n $_stepFn = $_steps[$_stepIdx]\n if (Test-ShouldSkipStep $_stepFn) {\n $_stepIdx++\n continue\n }\n if ($_stepHistory.Count -gt 0) {\n Write-Log (Get-Msg \"nav.back_hint\")\n }\n $script:StepResult = \"\"\n & $_stepFn\n if ($script:StepResult -eq \"back\") {\n if ($_stepHistory.Count -gt 0) {\n $_stepIdx = $_stepHistory[$_stepHistory.Count - 1]\n $_stepHistory.RemoveAt($_stepHistory.Count - 1)\n Clear-StepVars $_steps[$_stepIdx]\n }\n # else: first step, ignore 'b'\n } else {\n $_stepHistory.Add($_stepIdx)\n $_stepIdx++\n }\n }\n # ── End state machine ──────────────────────────────────────────────────────\n\n # Post-machine defaults for any steps that were skipped\n if (-not $script:config.DATA_DIR) {\n $script:config.DATA_DIR = if ($env:AGENTTEAMS_DATA_DIR) { $env:AGENTTEAMS_DATA_DIR } else { \"agentteams-data\" }\n }\n Write-Log (Get-Msg \"data.volume_using\" -f $script:config.DATA_DIR)\n if (-not $script:config.WORKSPACE_DIR) {\n $_defaultWs = \"$env:USERPROFILE\\agentteams-manager\"\n $script:config.WORKSPACE_DIR = if ($env:AGENTTEAMS_WORKSPACE_DIR) { $env:AGENTTEAMS_WORKSPACE_DIR } else { $_defaultWs }\n if (-not (Test-Path $script:config.WORKSPACE_DIR)) {\n New-Item -ItemType Directory -Path $script:config.WORKSPACE_DIR -Force | Out-Null\n }\n Write-Log (Get-Msg \"workspace.dir_label\" -f $script:config.WORKSPACE_DIR)\n }\n if (-not $script:config.DEFAULT_WORKER_RUNTIME) {\n $script:config.DEFAULT_WORKER_RUNTIME = if ($env:AGENTTEAMS_DEFAULT_WORKER_RUNTIME) { $env:AGENTTEAMS_DEFAULT_WORKER_RUNTIME } else { \"copaw\" }\n Write-Log (Get-Msg \"worker_runtime.selected\" -f $script:config.DEFAULT_WORKER_RUNTIME)\n }\n if (-not $script:config.MATRIX_E2EE) {\n $script:config.MATRIX_E2EE = if ($env:AGENTTEAMS_MATRIX_E2EE) { $env:AGENTTEAMS_MATRIX_E2EE } else { \"0\" }\n }\n if (-not $script:config.WORKER_IDLE_TIMEOUT) {\n $script:config.WORKER_IDLE_TIMEOUT = if ($env:AGENTTEAMS_WORKER_IDLE_TIMEOUT) { $env:AGENTTEAMS_WORKER_IDLE_TIMEOUT } else { \"720\" }\n }\n if (-not $script:config.HOST_SHARE_DIR) {\n $script:config.HOST_SHARE_DIR = if ($env:AGENTTEAMS_HOST_SHARE_DIR) { $env:AGENTTEAMS_HOST_SHARE_DIR } else { $env:USERPROFILE }\n }\n if (-not $script:config.LOCAL_ONLY) {\n $script:config.LOCAL_ONLY = if ($env:AGENTTEAMS_LOCAL_ONLY) { $env:AGENTTEAMS_LOCAL_ONLY } else { \"1\" }\n }\n if (-not $script:config.MANAGER_RUNTIME) {\n $script:config.MANAGER_RUNTIME = if ($env:AGENTTEAMS_MANAGER_RUNTIME) { $env:AGENTTEAMS_MANAGER_RUNTIME } else { \"copaw\" }\n }\n if (-not $script:config.PORT_GATEWAY) {\n $script:config.PORT_GATEWAY = if ($env:AGENTTEAMS_PORT_GATEWAY) { $env:AGENTTEAMS_PORT_GATEWAY } else { \"18080\" }\n $script:config.PORT_CONSOLE = if ($env:AGENTTEAMS_PORT_CONSOLE) { $env:AGENTTEAMS_PORT_CONSOLE } else { \"18001\" }\n $script:config.PORT_ELEMENT_WEB = if ($env:AGENTTEAMS_PORT_ELEMENT_WEB) { $env:AGENTTEAMS_PORT_ELEMENT_WEB } else { \"18088\" }\n $script:config.PORT_MANAGER_CONSOLE = if ($env:AGENTTEAMS_PORT_MANAGER_CONSOLE) { $env:AGENTTEAMS_PORT_MANAGER_CONSOLE } else { \"18888\" }\n }\n if (-not $script:config.MATRIX_DOMAIN) {\n $script:config.MATRIX_DOMAIN = if ($env:AGENTTEAMS_MATRIX_DOMAIN) { $env:AGENTTEAMS_MATRIX_DOMAIN } else { \"matrix-local.agentteams.io:$($script:config.PORT_GATEWAY)\" }\n $script:config.MATRIX_CLIENT_DOMAIN = if ($env:AGENTTEAMS_MATRIX_CLIENT_DOMAIN) { $env:AGENTTEAMS_MATRIX_CLIENT_DOMAIN } else { \"matrix-client-local.agentteams.io\" }\n $script:config.AI_GATEWAY_DOMAIN = if ($env:AGENTTEAMS_AI_GATEWAY_DOMAIN) { $env:AGENTTEAMS_AI_GATEWAY_DOMAIN } else { \"aigw-local.agentteams.io\" }\n $script:config.FS_DOMAIN = if ($env:AGENTTEAMS_FS_DOMAIN) { $env:AGENTTEAMS_FS_DOMAIN } else { \"fs-local.agentteams.io\" }\n $script:config.CONSOLE_DOMAIN = if ($env:AGENTTEAMS_CONSOLE_DOMAIN) { $env:AGENTTEAMS_CONSOLE_DOMAIN } else { \"console-local.agentteams.io\" }\n }\n\n Write-Log \"\"\n\n # Generate secrets\n Write-Log (Get-Msg \"install.generating_secrets\")\n $config.MANAGER_PASSWORD = if ($env:AGENTTEAMS_MANAGER_PASSWORD) { $env:AGENTTEAMS_MANAGER_PASSWORD } else { New-RandomKey }\n $config.REGISTRATION_TOKEN = if ($env:AGENTTEAMS_REGISTRATION_TOKEN) { $env:AGENTTEAMS_REGISTRATION_TOKEN } else { New-RandomKey }\n $config.MINIO_USER = if ($env:AGENTTEAMS_MINIO_USER) { $env:AGENTTEAMS_MINIO_USER } else { $config.ADMIN_USER }\n $config.MINIO_PASSWORD = if ($env:AGENTTEAMS_MINIO_PASSWORD) { $env:AGENTTEAMS_MINIO_PASSWORD } else { $config.ADMIN_PASSWORD }\n $config.MANAGER_GATEWAY_KEY = if ($env:AGENTTEAMS_MANAGER_GATEWAY_KEY) { $env:AGENTTEAMS_MANAGER_GATEWAY_KEY } else { New-RandomKey }\n\n # Store additional config\n $config.LANGUAGE = $script:AGENTTEAMS_LANGUAGE\n $config.REGISTRY = $script:AGENTTEAMS_REGISTRY\n $config.WORKER_IMAGE = $script:WORKER_IMAGE\n $config.COPAW_WORKER_IMAGE = $script:COPAW_WORKER_IMAGE\n $config.QWENPAW_WORKER_IMAGE = $script:QWENPAW_WORKER_IMAGE\n $config.HERMES_WORKER_IMAGE = $script:HERMES_WORKER_IMAGE\n $config.MANAGER_COPAW_IMAGE = $script:MANAGER_COPAW_IMAGE\n\n # Write env file\n New-EnvFile -Config $config -Path $script:AGENTTEAMS_ENV_FILE\n\n # Manager image selection (used by both embedded — passed to controller via env —\n # and legacy — used directly as `docker run` target).\n $managerImage = if ($config.MANAGER_RUNTIME -eq \"copaw\") { $script:MANAGER_COPAW_IMAGE } else { $script:MANAGER_IMAGE }\n $portPrefix = if ($config.LOCAL_ONLY -eq \"1\") { \"127.0.0.1:\" } else { \"\" }\n\n # Ensure agentteams-net Docker network exists. Used in both modes — the embedded\n # controller and the spawned manager/worker containers all join it and rely on\n # network aliases for *-local.agentteams.io DNS resolution.\n if ($script:AGENTTEAMS_MOUNT_SOCKET) {\n $socketAvailable = Test-DockerRunning\n if ($socketAvailable) {\n docker network inspect agentteams-net *>$null\n if ($LASTEXITCODE -ne 0) { docker network create agentteams-net *>$null }\n } else {\n Write-Log (Get-Msg \"install.socket_not_found\")\n if (-not $script:AGENTTEAMS_NON_INTERACTIVE) {\n Write-Host \"\"\n Write-Host \"$($script:ESC)[33m$(Get-Msg 'install.socket_confirm.title')$($script:ESC)[0m\"\n Write-Host \"\"\n Write-Host (Get-Msg 'install.socket_confirm.message')\n Write-Host \"\"\n $confirm = Read-Host (Get-Msg 'install.socket_confirm.prompt')\n if ($confirm -ne 'y' -and $confirm -ne 'Y') {\n Write-Log (Get-Msg 'install.socket_confirm.cancelled')\n exit 0\n }\n }\n }\n }\n\n if ($script:AGENTTEAMS_USE_EMBEDDED -ne \"1\") {\n # ============================================================\n # Legacy architecture: all-in-one manager container\n # (only entered when AGENTTEAMS_FORCE_LEGACY=1 — broken with the slim manager\n # image shipped since PR #616; kept solely for AGENTTEAMS_VERSION <= v1.0.9)\n # ============================================================\n\n $dockerArgs = @(\n \"run\", \"-d\",\n \"--name\", \"agentteams-manager\",\n \"--env-file\", $script:AGENTTEAMS_ENV_FILE,\n \"-e\", \"HOME=/root/manager-workspace\",\n \"-w\", \"/root/manager-workspace\",\n \"-e\", \"HOST_ORIGINAL_HOME=$($config.HOST_SHARE_DIR)\",\n \"-e\", \"AGENTTEAMS_MANAGER_RUNTIME=$($config.MANAGER_RUNTIME)\"\n )\n\n $dockerArgs += @(\"-e\", \"TZ=$($script:AGENTTEAMS_TIMEZONE)\")\n\n if ($script:AGENTTEAMS_MOUNT_SOCKET -and (Test-DockerRunning)) {\n $dockerArgs += @(\"--network\", \"agentteams-net\")\n $dockerArgs += @(\"--network-alias\", \"matrix-local.agentteams.io\")\n $dockerArgs += @(\"--network-alias\", \"aigw-local.agentteams.io\")\n $dockerArgs += @(\"--network-alias\", \"fs-local.agentteams.io\")\n foreach ($domain in @($config.MATRIX_CLIENT_DOMAIN, $config.CONSOLE_DOMAIN)) {\n if ($domain -match '-local\\.agentteams\\.io$') {\n $dockerArgs += @(\"--network-alias\", $domain)\n }\n }\n\n if ($config.DOCKER_PROXY -eq \"1\") {\n $proxyImage = $script:CONTROLLER_IMAGE\n Write-Log \"Starting Docker API proxy...\"\n docker rm -f agentteams-controller *>$null\n docker run -d --name agentteams-controller `\n --network agentteams-net `\n -v \"//var/run/docker.sock:/var/run/docker.sock\" `\n --security-opt label=disable `\n -e \"AGENTTEAMS_WORKER_IMAGE=$($script:WORKER_IMAGE)\" `\n -e \"AGENTTEAMS_COPAW_WORKER_IMAGE=$($script:COPAW_WORKER_IMAGE)\" `\n -e \"AGENTTEAMS_QWENPAW_WORKER_IMAGE=$($script:QWENPAW_WORKER_IMAGE)\" `\n -e \"AGENTTEAMS_HERMES_WORKER_IMAGE=$($script:HERMES_WORKER_IMAGE)\" `\n -e \"AGENTTEAMS_DEFAULT_WORKER_RUNTIME=$($script:config.DEFAULT_WORKER_RUNTIME)\" `\n $(if ($config.PROXY_ALLOWED_REGISTRIES) { @(\"-e\", \"AGENTTEAMS_PROXY_ALLOWED_REGISTRIES=$($config.PROXY_ALLOWED_REGISTRIES)\") }) `\n --restart unless-stopped `\n $proxyImage\n $dockerArgs += @(\"-e\", \"AGENTTEAMS_CONTROLLER_URL=http://agentteams-controller:8090\")\n $dockerArgs += @(\"-e\", \"AGENTTEAMS_CONTAINER_API=http://agentteams-controller:8090\")\n Write-Log (Get-Msg \"docker_proxy.selected_enabled\")\n } else {\n $dockerArgs += @(\"-v\", \"//var/run/docker.sock:/var/run/docker.sock\")\n $dockerArgs += @(\"--security-opt\", \"label=disable\")\n Write-Log (Get-Msg \"install.socket_detected\" -f \"//var/run/docker.sock\")\n }\n }\n\n $dockerArgs += @(\"-p\", \"${portPrefix}$($config.PORT_GATEWAY):8080\")\n $dockerArgs += @(\"-p\", \"${portPrefix}$($config.PORT_CONSOLE):8001\")\n $dockerArgs += @(\"-p\", \"${portPrefix}$($config.PORT_ELEMENT_WEB):8088\")\n $dockerArgs += @(\"-p\", \"127.0.0.1:$($config.PORT_MANAGER_CONSOLE):18888\")\n\n $dockerArgs += @(\"-v\", \"$($config.DATA_DIR):/data\")\n\n $wsDockerPath = ConvertTo-DockerPath -Path $config.WORKSPACE_DIR\n $dockerArgs += @(\"-v\", \"${wsDockerPath}:/root/manager-workspace\")\n\n $shareDockerPath = ConvertTo-DockerPath -Path $config.HOST_SHARE_DIR\n $dockerArgs += @(\"-v\", \"${shareDockerPath}:/host-share\")\n Write-Log (Get-Msg \"host_share.sharing\" -f $config.HOST_SHARE_DIR)\n\n if ($env:AGENTTEAMS_YOLO -eq \"1\") {\n $dockerArgs += @(\"-e\", \"AGENTTEAMS_YOLO=1\")\n Write-Log (Get-Msg \"install.yolo\")\n }\n\n if ($env:AGENTTEAMS_MATRIX_DEBUG -eq \"1\") {\n $dockerArgs += @(\"-e\", \"AGENTTEAMS_MATRIX_DEBUG=1\")\n }\n\n $dockerArgs += @(\"--restart\", \"unless-stopped\")\n $dockerArgs += $managerImage\n }\n\n # Check if the Docker volume exists; create if not (reuse on reinstall) — both modes.\n $volumeExists = docker volume ls -q 2>$null | Select-String \"^$($config.DATA_DIR)$\"\n if (-not $volumeExists) {\n docker volume create $config.DATA_DIR | Out-Null\n }\n\n # Pull images (skip if already exists locally for local build tags).\n $LocalImagePattern = \"^agentteams/\"\n function Test-LocalImage {\n param([string]$Image)\n $imgExists = docker image inspect $Image 2>$null\n return $LASTEXITCODE -eq 0\n }\n if ($script:AGENTTEAMS_USE_EMBEDDED -eq \"1\") {\n # Embedded image was already pulled by Resolve-EmbeddedImage unless overridden;\n # for an explicit override we still need to ensure it is present locally.\n if ($env:AGENTTEAMS_INSTALL_EMBEDDED_IMAGE) {\n if ($script:EMBEDDED_IMAGE -match $LocalImagePattern) {\n if (-not (Test-LocalImage $script:EMBEDDED_IMAGE)) {\n Write-Log \"Pulling embedded image: $($script:EMBEDDED_IMAGE)\"\n & docker pull $script:EMBEDDED_IMAGE\n }\n } else {\n Write-Log \"Pulling embedded image: $($script:EMBEDDED_IMAGE)\"\n & docker pull $script:EMBEDDED_IMAGE\n }\n }\n # Manager image — controller will spawn it inside; pull here so the spawn doesn't\n # have to wait on the network.\n if ($managerImage -match $LocalImagePattern) {\n if (Test-LocalImage $managerImage) {\n Write-Log (Get-Msg \"install.image.exists\" -f $managerImage)\n } else {\n Write-Log (Get-Msg \"install.image.pulling_manager\" -f $managerImage)\n & docker pull $managerImage\n }\n } else {\n Write-Log (Get-Msg \"install.image.pulling_manager\" -f $managerImage)\n & docker pull $managerImage\n }\n } else {\n if ($managerImage -match $LocalImagePattern) {\n if (Test-LocalImage $managerImage) {\n Write-Log (Get-Msg \"install.image.exists\" -f $managerImage)\n } else {\n Write-Log (Get-Msg \"install.image.pulling_manager\" -f $managerImage)\n & docker pull $managerImage\n }\n } else {\n Write-Log (Get-Msg \"install.image.pulling_manager\" -f $managerImage)\n & docker pull $managerImage\n }\n }\n\n # Pull all worker runtime images (workers may use any runtime regardless of the default)\n $workerImages = @(\n $script:WORKER_IMAGE\n $script:COPAW_WORKER_IMAGE\n # Temporarily disabled until the QwenPaw worker image is published.\n # $script:QWENPAW_WORKER_IMAGE\n $script:HERMES_WORKER_IMAGE\n )\n foreach ($workerImg in $workerImages) {\n if ($workerImg -match $LocalImagePattern) {\n if (Test-LocalImage $workerImg) {\n Write-Log (Get-Msg \"install.image.worker_exists\" -f $workerImg)\n } else {\n Write-Log (Get-Msg \"install.image.pulling_worker\" -f $workerImg)\n & docker pull $workerImg\n }\n } else {\n Write-Log (Get-Msg \"install.image.pulling_worker\" -f $workerImg)\n & docker pull $workerImg\n }\n }\n\n # --- Pre-upgrade: extract Matrix passwords from old containers (old-arch -> embedded) ---\n $credsTmp = $null\n if ($script:AGENTTEAMS_UPGRADE -and $script:AGENTTEAMS_USE_EMBEDDED -eq \"1\") {\n $controllerNameHit = docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-controller$\"\n $isOldArch = $false\n if (-not $controllerNameHit) {\n $isOldArch = $true\n } else {\n $ctrlImgLine = docker ps -a --format \"{{.Names}} {{.Image}}\" 2>$null | Where-Object { $_ -match '^agentteams-controller ' } | Select-Object -First 1\n if ($ctrlImgLine -and ($ctrlImgLine -notmatch 'embedded')) {\n $isOldArch = $true\n }\n }\n\n if ($isOldArch) {\n $credsTmp = Join-Path ([System.IO.Path]::GetTempPath()) (\"agentteams-upgrade-creds-\" + [Guid]::NewGuid().ToString(\"n\"))\n New-Item -ItemType Directory -Path $credsTmp -Force | Out-Null\n $utf8NoBom = New-Object System.Text.UTF8Encoding $false\n\n $mgrCredsTempStart = $false\n $mgrExists = docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-manager$\"\n $mgrRunning = docker ps --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-manager$\"\n if ($mgrExists -and -not $mgrRunning) {\n Write-Log \"agentteams-manager is stopped; starting it temporarily to extract Matrix credentials for upgrade...\"\n docker start agentteams-manager 2>$null | Out-Null\n Wait-MatrixReady -Container \"agentteams-manager\"\n $mgrCredsTempStart = $true\n }\n\n $inspectLines = docker inspect agentteams-manager --format \"{{range .Config.Env}}{{println .}}{{end}}\" 2>$null\n $mgrPw = \"\"\n if ($inspectLines) {\n $envLine = $inspectLines -split \"`n\" | Where-Object { $_ -match '^AGENTTEAMS_MANAGER_PASSWORD=' } | Select-Object -First 1\n if ($envLine) {\n $mgrPw = ($envLine -replace '^AGENTTEAMS_MANAGER_PASSWORD=', \"\").Trim()\n }\n }\n $mgrRunningNow = docker ps --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-manager$\"\n if ([string]::IsNullOrEmpty($mgrPw) -and $mgrRunningNow) {\n $mgrPw = docker exec agentteams-manager bash -c 'source /data/agentteams-secrets.env 2>/dev/null && echo \"${AGENTTEAMS_MANAGER_PASSWORD}\"' 2>$null\n if ($mgrPw) { $mgrPw = $mgrPw.Trim() }\n }\n $dataVolPresent = docker volume ls -q 2>$null | Where-Object { $_ -eq $config.DATA_DIR }\n if ([string]::IsNullOrEmpty($mgrPw) -and $dataVolPresent) {\n $mgrPw = Read-AgentTeamsSecretFromDataVolume -VolumeName $config.DATA_DIR -Key \"AGENTTEAMS_MANAGER_PASSWORD\"\n }\n\n $envFilePath = $script:AGENTTEAMS_ENV_FILE\n $mgrRoom = \"\"\n if (-not [string]::IsNullOrEmpty($mgrPw)) {\n $adminPw = \"\"\n $adminUser = \"admin\"\n if (Test-Path $envFilePath) {\n $adminLine = Get-Content $envFilePath | Where-Object { $_ -match '^AGENTTEAMS_ADMIN_PASSWORD=' } | Select-Object -First 1\n if ($adminLine) { $adminPw = ($adminLine -replace '^AGENTTEAMS_ADMIN_PASSWORD=', \"\").Trim() }\n $userLine = Get-Content $envFilePath | Where-Object { $_ -match '^AGENTTEAMS_ADMIN_USER=' } | Select-Object -First 1\n if ($userLine) {\n $adminUser = ($userLine -replace '^AGENTTEAMS_ADMIN_USER=', \"\").Trim()\n if ([string]::IsNullOrEmpty($adminUser)) { $adminUser = \"admin\" }\n }\n }\n\n if (-not [string]::IsNullOrEmpty($adminPw) -and $mgrRunningNow) {\n $mgrRoom = Get-AgentTeamsAdminDmRoomViaMatrix -AdminUser $adminUser -AdminPassword $adminPw\n }\n if ([string]::IsNullOrEmpty($mgrRoom)) {\n $mgrRoom = Read-AgentTeamsAdminDmRoomFromWorkspace -WorkspaceDir $config.WORKSPACE_DIR\n }\n\n $gwKeyLine = if (Test-Path $envFilePath) {\n Get-Content $envFilePath | Where-Object { $_ -match '^AGENTTEAMS_MANAGER_GATEWAY_KEY=' } | Select-Object -First 1\n } else { $null }\n $gwKeyForDefault = if ($gwKeyLine) { ($gwKeyLine -replace '^AGENTTEAMS_MANAGER_GATEWAY_KEY=', \"\").Trim() } else { $config.MANAGER_GATEWAY_KEY }\n\n $defaultEnvPath = Join-Path $credsTmp \"default.env\"\n [System.IO.File]::WriteAllLines($defaultEnvPath, @(\n \"WORKER_PASSWORD=$mgrPw\"\n \"WORKER_MINIO_PASSWORD=$(Get-AgentTeamsRandomHex 24)\"\n \"WORKER_GATEWAY_KEY=$gwKeyForDefault\"\n \"WORKER_ROOM_ID=$mgrRoom\"\n ), $utf8NoBom)\n if ($mgrRoom) {\n Write-Log \"Extracted Manager Matrix password and room ID\"\n } else {\n Write-Log \"Extracted Manager Matrix password\"\n }\n }\n\n if ($mgrCredsTempStart) {\n Write-Log \"Stopping agentteams-manager after credential extraction (upgrade will recreate containers)...\"\n docker stop agentteams-manager 2>$null | Out-Null\n }\n }\n }\n\n # Stop and remove existing containers (deferred until after all\n # configuration is collected and images are pulled successfully)\n $existingProxy = docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-controller$\"\n if ($existingProxy) {\n docker stop agentteams-controller *>$null\n docker rm agentteams-controller *>$null\n }\n $existingContainer = docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-manager$\"\n if ($existingContainer) {\n Write-Log (Get-Msg \"install.removing_existing\")\n docker stop agentteams-manager *>$null\n docker rm agentteams-manager *>$null\n }\n\n # Stop and remove worker containers (controller will recreate via CR reconciliation)\n if ($script:UPGRADE_EXISTING_WORKERS) {\n Write-Log (Get-Msg \"install.existing.stopping_workers\")\n $script:UPGRADE_EXISTING_WORKERS | ForEach-Object {\n docker stop $_ *>$null\n docker rm $_ *>$null\n Write-Log (Get-Msg \"install.existing.removed\" -f $_)\n }\n }\n\n # --- Upgrade: inject extracted credentials into data volume (old-arch -> embedded) ---\n if ($credsTmp -and (Test-Path $credsTmp)) {\n $credsFiles = Get-ChildItem -Path $credsTmp -Filter \"*.env\" -ErrorAction SilentlyContinue\n if ($credsFiles -and $credsFiles.Count -gt 0) {\n $cleanupCtr = \"agentteams-upgrade-cleanup\"\n docker rm -f $cleanupCtr 2>$null | Out-Null\n $credsDockerPath = ConvertTo-DockerPath -Path $credsTmp\n $injectShell = \"rm -rf /data/worker-creds && mkdir -p /data/worker-creds && cp /creds/*.env /data/worker-creds/ 2>/dev/null || true && chmod 600 /data/worker-creds/*.env 2>/dev/null || true\"\n docker run --rm --name $cleanupCtr --entrypoint sh `\n -v \"$($config.DATA_DIR):/data\" `\n -v \"${credsDockerPath}:/creds:ro\" `\n $script:EMBEDDED_IMAGE `\n -c $injectShell 2>$null | Out-Null\n if ($LASTEXITCODE -eq 0) {\n Write-Log \"Injected credentials for upgrade\"\n } else {\n Write-Log \"Warning: credential injection failed, continuing\"\n }\n }\n Remove-Item -Path $credsTmp -Recurse -Force -ErrorAction SilentlyContinue\n }\n\n if ($script:AGENTTEAMS_USE_EMBEDDED -eq \"1\") {\n # ============================================================\n # New architecture: embedded controller + auto-created manager\n # (controller container hosts Higress / Tuwunel / MinIO / Element Web /\n # controller binary, then spawns the lightweight manager container)\n # ============================================================\n\n # Internal port: 8080 (Higress gateway inside the controller container).\n $internalGwPort = 8080\n\n $matrixDomain = if ($config.MATRIX_DOMAIN) {\n $config.MATRIX_DOMAIN\n } else {\n \"matrix-local.agentteams.io:$($config.PORT_GATEWAY)\"\n }\n $aigwDomain = if ($config.AI_GATEWAY_DOMAIN) { $config.AI_GATEWAY_DOMAIN } else { \"aigw-local.agentteams.io\" }\n if ($aigwDomain -notmatch \":\") { $aigwDomain = \"${aigwDomain}:${internalGwPort}\" }\n $fsDomain = if ($config.FS_DOMAIN) { $config.FS_DOMAIN } else { \"fs-local.agentteams.io\" }\n if ($fsDomain -notmatch \":\") { $fsDomain = \"${fsDomain}:${internalGwPort}\" }\n\n $ctrlArgs = @(\n \"run\", \"-d\",\n \"--name\", \"agentteams-controller\",\n \"--network\", \"agentteams-net\",\n \"--network-alias\", \"matrix-local.agentteams.io\",\n \"--network-alias\", \"aigw-local.agentteams.io\",\n \"--network-alias\", \"fs-local.agentteams.io\",\n \"-e\", \"AGENTTEAMS_ADMIN_USER=$($config.ADMIN_USER)\",\n \"-e\", \"AGENTTEAMS_ADMIN_PASSWORD=$($config.ADMIN_PASSWORD)\",\n \"-e\", \"AGENTTEAMS_MANAGER_PASSWORD=$($config.MANAGER_PASSWORD)\",\n \"-e\", \"AGENTTEAMS_REGISTRATION_TOKEN=$($config.REGISTRATION_TOKEN)\",\n \"-e\", \"AGENTTEAMS_MINIO_USER=$($config.MINIO_USER)\",\n \"-e\", \"AGENTTEAMS_MINIO_PASSWORD=$($config.MINIO_PASSWORD)\",\n \"-e\", \"AGENTTEAMS_LLM_PROVIDER=$($config.LLM_PROVIDER)\",\n \"-e\", \"AGENTTEAMS_LLM_API_KEY=$($config.LLM_API_KEY)\",\n \"-e\", \"AGENTTEAMS_DEFAULT_MODEL=$($config.DEFAULT_MODEL)\",\n \"-e\", \"AGENTTEAMS_MANAGER_GATEWAY_KEY=$($config.MANAGER_GATEWAY_KEY)\",\n \"-e\", \"AGENTTEAMS_MANAGER_RUNTIME=$($config.MANAGER_RUNTIME)\",\n \"-e\", \"AGENTTEAMS_MANAGER_IMAGE=$managerImage\",\n \"-e\", \"AGENTTEAMS_DEFAULT_WORKER_RUNTIME=$($config.DEFAULT_WORKER_RUNTIME)\",\n \"-e\", \"AGENTTEAMS_WORKER_IMAGE=$($script:WORKER_IMAGE)\",\n \"-e\", \"AGENTTEAMS_COPAW_WORKER_IMAGE=$($script:COPAW_WORKER_IMAGE)\",\n \"-e\", \"AGENTTEAMS_QWENPAW_WORKER_IMAGE=$($script:QWENPAW_WORKER_IMAGE)\",\n \"-e\", \"AGENTTEAMS_HERMES_WORKER_IMAGE=$($script:HERMES_WORKER_IMAGE)\",\n \"-e\", \"AGENTTEAMS_MATRIX_DOMAIN=$matrixDomain\",\n \"-e\", \"AGENTTEAMS_ELEMENT_HOMESERVER_URL=http://127.0.0.1:$($config.PORT_GATEWAY)\",\n \"-e\", \"AGENTTEAMS_MATRIX_URL=http://127.0.0.1:6167\",\n \"-e\", \"AGENTTEAMS_MATRIX_E2EE=$($config.MATRIX_E2EE)\",\n \"-e\", \"AGENTTEAMS_MINIO_ENDPOINT=http://127.0.0.1:9000\",\n \"-e\", \"AGENTTEAMS_MINIO_BUCKET=agentteams-storage\",\n \"-e\", \"AGENTTEAMS_STORAGE_PREFIX=agentteams/agentteams-storage\",\n \"-e\", \"AGENTTEAMS_FS_ENDPOINT=http://127.0.0.1:9000\",\n \"-e\", \"AGENTTEAMS_AI_GATEWAY_URL=http://$aigwDomain\",\n \"-e\", \"AGENTTEAMS_CONTROLLER_URL=http://agentteams-controller:8090\",\n \"-e\", \"AGENTTEAMS_DOCKER_NETWORK=agentteams-net\",\n \"-e\", \"AGENTTEAMS_WORKSPACE_DIR=$($config.WORKSPACE_DIR)\",\n \"-e\", \"AGENTTEAMS_HOST_SHARE_DIR=$($config.HOST_SHARE_DIR)\",\n \"-e\", \"AGENTTEAMS_MANAGER_ENABLED=true\",\n \"-e\", \"AGENTTEAMS_PORT_MANAGER_CONSOLE=$($config.PORT_MANAGER_CONSOLE)\"\n )\n\n if ($script:AGENTTEAMS_TIMEZONE) {\n $ctrlArgs += @(\"-e\", \"TZ=$($script:AGENTTEAMS_TIMEZONE)\")\n }\n if ($env:AGENTTEAMS_YOLO -eq \"1\") {\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_YOLO=1\")\n }\n if ($env:AGENTTEAMS_MATRIX_DEBUG -eq \"1\") {\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_MATRIX_DEBUG=1\")\n }\n if ($config.GITHUB_TOKEN) {\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_GITHUB_TOKEN=$($config.GITHUB_TOKEN)\")\n }\n if ($config.EMBEDDING_MODEL) {\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_EMBEDDING_MODEL=$($config.EMBEDDING_MODEL)\")\n }\n\n # Optional: custom model capability overrides (context window, max\n # tokens, vision, reasoning). These are read by the Controller's\n # config.go LoadConfig to annotate custom models not in the built-in\n # presets table.\n if ($config.MODEL_CONTEXT_WINDOW) {\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_MODEL_CONTEXT_WINDOW=$($config.MODEL_CONTEXT_WINDOW)\")\n }\n if ($config.MODEL_MAX_TOKENS) {\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_MODEL_MAX_TOKENS=$($config.MODEL_MAX_TOKENS)\")\n }\n if ($config.MODEL_VISION) {\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_MODEL_VISION=$($config.MODEL_VISION)\")\n }\n if ($config.MODEL_REASONING) {\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_MODEL_REASONING=$($config.MODEL_REASONING)\")\n }\n\n if ($config.OPENAI_BASE_URL) {\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_OPENAI_BASE_URL=$($config.OPENAI_BASE_URL)\")\n }\n if ($env:AGENTTEAMS_AI_STREAM_IDLE_TIMEOUT_SECONDS) {\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_AI_STREAM_IDLE_TIMEOUT_SECONDS=$($env:AGENTTEAMS_AI_STREAM_IDLE_TIMEOUT_SECONDS)\")\n }\n if ($script:AGENTTEAMS_LANGUAGE) {\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_LANGUAGE=$($script:AGENTTEAMS_LANGUAGE)\")\n }\n\n # Optional: CMS/ARMS observability. In embedded mode the controller\n # spawns the Manager and Workers, so it must receive these settings.\n $cmsTracesEnabled = if ($env:AGENTTEAMS_CMS_TRACES_ENABLED) { $env:AGENTTEAMS_CMS_TRACES_ENABLED } else { \"false\" }\n $cmsServiceName = if ($env:AGENTTEAMS_CMS_SERVICE_NAME) { $env:AGENTTEAMS_CMS_SERVICE_NAME } else { \"agentteams-manager\" }\n $cmsMetricsEnabled = if ($env:AGENTTEAMS_CMS_METRICS_ENABLED) { $env:AGENTTEAMS_CMS_METRICS_ENABLED } else { \"false\" }\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_CMS_TRACES_ENABLED=$cmsTracesEnabled\")\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_CMS_SERVICE_NAME=$cmsServiceName\")\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_CMS_METRICS_ENABLED=$cmsMetricsEnabled\")\n if ($env:AGENTTEAMS_CMS_ENDPOINT) {\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_CMS_ENDPOINT=$($env:AGENTTEAMS_CMS_ENDPOINT)\")\n }\n if ($env:AGENTTEAMS_CMS_LICENSE_KEY) {\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_CMS_LICENSE_KEY=$($env:AGENTTEAMS_CMS_LICENSE_KEY)\")\n }\n if ($env:AGENTTEAMS_CMS_PROJECT) {\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_CMS_PROJECT=$($env:AGENTTEAMS_CMS_PROJECT)\")\n }\n if ($env:AGENTTEAMS_CMS_WORKSPACE) {\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_CMS_WORKSPACE=$($env:AGENTTEAMS_CMS_WORKSPACE)\")\n }\n\n # Mount the docker socket so the controller can spawn manager + workers.\n $ctrlArgs += @(\"-v\", \"//var/run/docker.sock:/var/run/docker.sock\")\n $ctrlArgs += @(\"--security-opt\", \"label=disable\")\n\n # Persistent data + workspace mounts (manager workspace is bind-mounted under\n # /root/agentteams-fs/agents/manager so the controller can hand it to the spawned\n # manager container).\n $ctrlArgs += @(\"-v\", \"$($config.DATA_DIR):/data\")\n $wsDockerPath = ConvertTo-DockerPath -Path $config.WORKSPACE_DIR\n $ctrlArgs += @(\"-v\", \"${wsDockerPath}:/root/agentteams-fs/agents/manager\")\n Write-Log (Get-Msg \"host_share.sharing\" -f $config.HOST_SHARE_DIR)\n\n # Externally exposed ports — only the gateway / Higress console / Element Web,\n # since the manager console is now spawned inside its own container by the\n # controller (port mapping is handled there).\n $ctrlArgs += @(\"-p\", \"${portPrefix}$($config.PORT_GATEWAY):8080\")\n $ctrlArgs += @(\"-p\", \"${portPrefix}$($config.PORT_CONSOLE):8001\")\n $ctrlArgs += @(\"-p\", \"${portPrefix}$($config.PORT_ELEMENT_WEB):8088\")\n\n $ctrlArgs += @(\"--restart\", \"unless-stopped\")\n $ctrlArgs += $script:EMBEDDED_IMAGE\n\n Write-Log (Get-Msg \"install.starting_manager\")\n & docker $ctrlArgs\n Write-Log \"Embedded controller started: agentteams-controller\"\n\n # Wait for infra inside the controller container.\n function Wait-EmbeddedUrl {\n param([string]$Url, [string]$Container, [int]$MaxWait, [string]$Description)\n $elapsed = 0\n Write-Log \"Waiting for $Description...\"\n while ($elapsed -lt $MaxWait) {\n docker exec $Container curl -sf $Url *>$null\n if ($LASTEXITCODE -eq 0) {\n Write-Log \"$Description is ready (${elapsed}s)\"\n return $true\n }\n Start-Sleep -Seconds 2\n $elapsed += 2\n }\n Write-Host \"$($script:ESC)[31m[AgentTeams ERROR]$($script:ESC)[0m $Description not ready after ${MaxWait}s\" -ForegroundColor Red\n return $false\n }\n\n if (-not (Wait-EmbeddedUrl \"http://127.0.0.1:6167/_tuwunel/server_version\" \"agentteams-controller\" 120 \"Tuwunel (Matrix)\")) { Exit-Script 1 }\n if (-not (Wait-EmbeddedUrl \"http://127.0.0.1:9000/minio/health/live\" \"agentteams-controller\" 60 \"MinIO\")) { Exit-Script 1 }\n if (-not (Wait-EmbeddedUrl \"http://127.0.0.1:8080/status\" \"agentteams-controller\" 120 \"Higress Gateway\")) { Exit-Script 1 }\n\n # Wait for the controller to spawn the Manager Agent container.\n Write-Log \"Waiting for Manager Agent container...\"\n $mgrWait = 0\n $mgrMax = 300\n while ($mgrWait -lt $mgrMax) {\n $found = docker ps --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-manager$\"\n if ($found) {\n Write-Log \"Manager Agent container detected (${mgrWait}s)\"\n break\n }\n Start-Sleep -Seconds 3\n $mgrWait += 3\n }\n if ($mgrWait -ge $mgrMax) {\n Write-Host \"$($script:ESC)[31m[AgentTeams ERROR]$($script:ESC)[0m Manager Agent container not created after ${mgrMax}s\" -ForegroundColor Red\n Write-Log \"Controller logs:\"\n docker exec agentteams-controller tail -30 /var/log/agentteams/agentteams-controller-error.log 2>$null\n Exit-Script 1\n }\n\n # Wait for Manager Agent container to reach `running` state.\n Write-Log \"Waiting for Manager Agent to start...\"\n $agentWait = 0\n while ($agentWait -lt 120) {\n $state = docker inspect --format \"{{.State.Status}}\" agentteams-manager 2>$null\n if ($state -eq \"running\") {\n Write-Log \"Manager Agent is running\"\n break\n }\n Start-Sleep -Seconds 2\n $agentWait += 2\n }\n\n if ($env:AGENTTEAMS_YOLO -eq \"1\") {\n docker exec agentteams-manager touch /root/manager-workspace/yolo-mode 2>$null\n }\n\n # Wait for the Manager Agent's runtime + Matrix to be reachable. In embedded\n # mode Tuwunel lives in `agentteams-controller`, so the Matrix probe must target\n # the controller (the manager only exposes the agent runtime).\n Wait-ManagerReady -Container \"agentteams-manager\"\n Wait-MatrixReady -Container \"agentteams-controller\"\n\n # Wait for the controller to actually deliver the first-boot welcome\n # message. Gated by the controller on (a) Manager joining the DM room\n # and (b) Higress WASM key-auth propagating to /v1/chat/completions for\n # the Manager's gateway key — typically ~45-90s on a fresh install.\n # We poll Manager CR status.welcomeSent via the in-container AgentTeams\n # CLI, exec'd inside agentteams-controller (the source-of-truth container\n # — its bundled CLI binary is always in lockstep with the controller\n # binary serving the HTTP API, since they're the same `go build`\n # output. The agentteams-manager container's CLI may lag the controller\n # across image upgrades and silently drop the welcomeSent field,\n # leaving this loop hung). The controller mints a long-lived admin\n # SA token at startup and writes it to\n # AGENTTEAMS_AUTH_TOKEN_FILE=/var/run/agentteams/cli-token (set as a Dockerfile\n # ENV default), so a bare `docker exec agentteams-controller agt …`\n # auto-discovers both the endpoint and the token. The brief window\n # after container start before bootstrapAdminCLIToken completes is\n # absorbed by the loop's silent retry.\n $hasAgentTeamsCli = $false\n try {\n docker exec agentteams-controller sh -c 'command -v agt' *> $null\n if ($LASTEXITCODE -eq 0) { $hasAgentTeamsCli = $true }\n } catch {}\n\n if ($hasAgentTeamsCli) {\n Write-Log (Get-Msg \"install.welcome_msg.waiting\")\n $welcomeMax = if ($env:AGENTTEAMS_WELCOME_TIMEOUT) { [int]$env:AGENTTEAMS_WELCOME_TIMEOUT } else { 300 }\n $welcomeWait = 0\n $welcomeDone = $false\n while ($welcomeWait -lt $welcomeMax) {\n $wjson = \"\"\n try {\n $wjson = docker exec agentteams-controller `\n agt get managers default -o json 2>$null\n } catch {}\n if ($wjson -and ($wjson -replace '\\s', '') -match '\"welcomeSent\":true') {\n Write-Log (Get-Msg \"install.welcome_msg.confirmed\" $welcomeWait)\n $welcomeDone = $true\n break\n }\n Start-Sleep -Seconds 3\n $welcomeWait += 3\n }\n if (-not $welcomeDone) {\n # Non-fatal: install is still good. Keep going to the success\n # banner so the admin can use Element Web to nudge Manager into\n # onboarding manually (one DM message is enough).\n Write-Log (Get-Msg \"install.welcome_msg.timeout\" $welcomeMax)\n Write-Log (Get-Msg \"install.welcome_msg.timeout_hint\")\n Write-Log (Get-Msg \"install.welcome_msg.timeout_inspect\")\n }\n } else {\n Write-Log (Get-Msg \"install.welcome_msg.poll_unavailable\")\n }\n } else {\n # Run container (legacy path)\n Write-Log (Get-Msg \"install.starting_manager\")\n & docker $dockerArgs\n\n Wait-ManagerReady -Container \"agentteams-manager\"\n Wait-MatrixReady -Container \"agentteams-manager\"\n }\n\n # Create OpenAI-compatible provider if needed\n if ($config.LLM_PROVIDER -eq \"openai-compat\") {\n New-OpenAICompatProvider -BaseUrl $config.OPENAI_BASE_URL -ApiKey $config.LLM_API_KEY -ConsolePort ([int]$config.PORT_CONSOLE)\n }\n\n # Print success message\n Write-Log \"\"\n Write-Log (Get-Msg \"success.title\")\n Write-Log \"\"\n Write-Log (Get-Msg \"success.domains_configured\")\n Write-Log \" $($config.MATRIX_DOMAIN.Split(':')[0]) $($config.MATRIX_CLIENT_DOMAIN) $($config.AI_GATEWAY_DOMAIN) $($config.FS_DOMAIN) $($config.CONSOLE_DOMAIN)\"\n Write-Log \"\"\n\n $lanIP = Get-LanIP\n\n Write-Host \"$($script:ESC)[33m===============================================================$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.open_url')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[1;36m http://127.0.0.1:$($config.PORT_ELEMENT_WEB)/#/login$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.login_with')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.username' -f $config.ADMIN_USER)$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.password' -f $config.ADMIN_PASSWORD)$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.after_login')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.tell_it')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.manager_auto')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m --------------------------------------------------------------- $($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.mobile_title')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $($script:ESC)[0m\"\n if ($lanIP) {\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.mobile_step1')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.mobile_step2' -f \"http://${lanIP}:$($config.PORT_GATEWAY)\")$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.mobile_step3')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.mobile_username' -f $config.ADMIN_USER)$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.mobile_password' -f $config.ADMIN_PASSWORD)$($script:ESC)[0m\"\n } else {\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.mobile_step1')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.mobile_step2_noip' -f $config.PORT_GATEWAY)$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.mobile_noip_hint')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.mobile_step3')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.mobile_username' -f $config.ADMIN_USER)$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.mobile_password' -f $config.ADMIN_PASSWORD)$($script:ESC)[0m\"\n }\n Write-Host \"$($script:ESC)[33m $($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m===============================================================$($script:ESC)[0m\"\n\n Write-Log \"\"\n Write-Log (Get-Msg \"success.other_consoles\")\n Write-Log (Get-Msg \"success.higress_console\" -f $config.PORT_CONSOLE, $config.ADMIN_USER, $config.ADMIN_PASSWORD)\n if ($script:AGENTTEAMS_USE_EMBEDDED -ne \"1\") {\n # In embedded mode the manager runs in its own auto-spawned container with\n # its own console-port mapping handled by the controller, so don't print a\n # host-side URL/credentials hint here.\n Write-Log (Get-Msg \"success.manager_console\" -f $config.PORT_MANAGER_CONSOLE)\n Write-Log (Get-Msg \"success.manager_console_gateway\" -f $config.ADMIN_USER, $config.ADMIN_PASSWORD)\n }\n Write-Log \"\"\n Write-Log (Get-Msg \"success.switch_llm.title\")\n Write-Log (Get-Msg \"success.switch_llm.hint\")\n Write-Log (Get-Msg \"success.switch_llm.docs\")\n Write-Log (Get-Msg \"success.switch_llm.url\")\n Write-Log \"\"\n Write-Log (Get-Msg \"success.tip\")\n Write-Log \"\"\n if ($config.LOCAL_ONLY -ne \"1\") {\n Write-Host (Get-Msg \"port.local_only.https_hint\") -ForegroundColor Yellow\n Write-Log \"\"\n }\n Write-Log (Get-Msg \"success.config_file\" -f $script:AGENTTEAMS_ENV_FILE)\n\n Write-Log (Get-Msg \"success.data_volume\" -f $config.DATA_DIR)\n\n Write-Log (Get-Msg \"success.workspace\" -f $config.WORKSPACE_DIR)\n}\n\n# ============================================================\n# Worker Installation\n# ============================================================\n\nfunction Install-Worker {\n param(\n [string]$Name,\n [string]$Fs,\n [string]$FsKey,\n [string]$FsSecret,\n [switch]$Reset,\n [switch]$FindSkills,\n [string]$SkillsApiUrl\n )\n\n # Validate required parameters\n if (-not $Name) {\n Write-Error (Get-Msg \"error.name_required\")\n }\n if (-not $Fs) {\n Write-Error (Get-Msg \"error.fs_required\")\n }\n if (-not $FsKey) {\n Write-Error (Get-Msg \"error.fs_key_required\")\n }\n if (-not $FsSecret) {\n Write-Error (Get-Msg \"error.fs_secret_required\")\n }\n\n $containerName = \"agentteams-worker-$Name\"\n\n # Handle reset\n if ($Reset) {\n Write-Log (Get-Msg \"worker.resetting\" -f $Name)\n docker stop $containerName *>$null\n docker rm $containerName *>$null\n }\n\n # Check for existing container\n $existing = docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^$containerName$\"\n if ($existing) {\n Write-Error (Get-Msg \"worker.exists\" -f $containerName)\n }\n\n # Detect timezone and registry\n $timezone = Get-AgentTeamsTimeZone\n $registry = Get-Registry -Timezone $timezone\n $workerImage = if ($env:AGENTTEAMS_INSTALL_WORKER_IMAGE) {\n $env:AGENTTEAMS_INSTALL_WORKER_IMAGE\n } else {\n \"$registry/agentteams/agentteams-worker:$($script:AGENTTEAMS_VERSION)\"\n }\n\n Write-Log (Get-Msg \"worker.starting\" -f $Name)\n\n $dockerArgs = @(\n \"run\", \"-d\",\n \"--name\", $containerName,\n \"-e\", \"HOME=/root/agentteams-fs/agents/$Name\",\n \"-w\", \"/root/agentteams-fs/agents/$Name\",\n \"-e\", \"AGENTTEAMS_WORKER_NAME=$Name\",\n \"-e\", \"AGENTTEAMS_FS_ENDPOINT=$Fs\",\n \"-e\", \"AGENTTEAMS_FS_ACCESS_KEY=$FsKey\",\n \"-e\", \"AGENTTEAMS_FS_SECRET_KEY=$FsSecret\"\n )\n\n if (-not $SkillsApiUrl) {\n if ($env:AGENTTEAMS_SKILLS_API_URL) {\n $SkillsApiUrl = $env:AGENTTEAMS_SKILLS_API_URL\n } else {\n $SkillsApiUrl = \"nacos://market.agentteams.io:80/public\"\n }\n }\n\n if ($FindSkills -and $SkillsApiUrl) {\n $dockerArgs += @(\"-e\", \"SKILLS_API_URL=$SkillsApiUrl\")\n Write-Log (Get-Msg \"worker.skills_url\" -f $SkillsApiUrl)\n }\n if ($env:AGENTTEAMS_NACOS_USERNAME) {\n $dockerArgs += @(\"-e\", \"AGENTTEAMS_NACOS_USERNAME=$($env:AGENTTEAMS_NACOS_USERNAME)\")\n }\n if ($env:AGENTTEAMS_NACOS_PASSWORD) {\n $dockerArgs += @(\"-e\", \"AGENTTEAMS_NACOS_PASSWORD=$($env:AGENTTEAMS_NACOS_PASSWORD)\")\n }\n if ($env:AGENTTEAMS_NACOS_TOKEN) {\n $dockerArgs += @(\"-e\", \"AGENTTEAMS_NACOS_TOKEN=$($env:AGENTTEAMS_NACOS_TOKEN)\")\n }\n\n $dockerArgs += @(\"--restart\", \"unless-stopped\", $workerImage)\n\n & docker $dockerArgs\n\n Write-Log \"\"\n Write-Log (Get-Msg \"worker.started\" -f $Name)\n Write-Log (Get-Msg \"worker.container\" -f $containerName)\n Write-Log (Get-Msg \"worker.view_logs\" -f $containerName)\n}\n\n# ============================================================\n# Uninstall\n# ============================================================\n\nfunction Uninstall-AgentTeams {\n Write-Log (Get-Msg \"uninstall.title\")\n\n # Stop and remove manager\n $manager = docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-manager$\"\n if ($manager) {\n Write-Log (Get-Msg \"uninstall.stopping_manager\")\n docker stop agentteams-manager *>$null\n docker rm agentteams-manager *>$null\n }\n\n # Stop and remove workers\n $workers = docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-worker-\"\n if ($workers) {\n Write-Log (Get-Msg \"uninstall.stopping_workers\")\n $workers | ForEach-Object {\n docker stop $_ *>$null\n docker rm $_ *>$null\n Write-Log (Get-Msg \"uninstall.removed\" -f $_)\n }\n }\n\n # Stop and remove docker-proxy (legacy <= v1.0.x; current arch uses\n # agentteams-controller for the same role)\n $proxy = docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-docker-proxy$\"\n if ($proxy) {\n Write-Log (Get-Msg \"uninstall.removing_proxy\")\n docker stop agentteams-docker-proxy *>$null\n docker rm agentteams-docker-proxy *>$null\n }\n\n # Stop and remove the embedded controller container. MUST happen\n # before the `docker volume rm agentteams-data` step below -- in embedded\n # mode agentteams-controller mounts agentteams-data at /data (Tuwunel DB,\n # MinIO state, Higress state, all the room messages), and `volume rm`\n # against an in-use volume fails silently. Skipping this used to\n # leave room/message history behind across \"uninstall + reinstall\"\n # cycles. See PR #692.\n $controller = docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-controller$\"\n if ($controller) {\n Write-Log (Get-Msg \"uninstall.stopping_controller\")\n docker stop agentteams-controller *>$null\n docker rm agentteams-controller *>$null\n }\n\n # Remove Docker volume (read custom name from env file if available)\n $dataVolume = \"agentteams-data\"\n if (Test-Path $script:AGENTTEAMS_ENV_FILE) {\n $envContent = Get-Content $script:AGENTTEAMS_ENV_FILE -ErrorAction SilentlyContinue\n $dataLine = $envContent | Select-String \"^AGENTTEAMS_DATA_DIR=\"\n if ($dataLine) {\n $parsed = ($dataLine -split \"=\", 2)[1]\n if ($parsed) { $dataVolume = $parsed }\n }\n }\n $volume = docker volume ls -q 2>$null | Select-String \"^${dataVolume}$\"\n if ($volume) {\n Write-Log (Get-Msg \"uninstall.removing_volume\")\n docker volume rm $dataVolume *>$null\n }\n\n # Remove Docker network\n $network = docker network ls --format \"{{.Name}}\" 2>$null | Select-String \"^agentteams-net$\"\n if ($network) {\n Write-Log (Get-Msg \"uninstall.removing_network\")\n docker network rm agentteams-net *>$null\n }\n\n # Remove workspace directory\n $workspaceDir = \"$env:USERPROFILE\\agentteams-manager\"\n if (Test-Path $script:AGENTTEAMS_ENV_FILE) {\n $envContent = Get-Content $script:AGENTTEAMS_ENV_FILE -ErrorAction SilentlyContinue\n $wsLine = $envContent | Select-String \"^AGENTTEAMS_WORKSPACE_DIR=\"\n if ($wsLine) {\n $workspaceDir = ($wsLine -split \"=\", 2)[1]\n }\n }\n if ($workspaceDir -and (Test-Path $workspaceDir)) {\n Write-Log (Get-Msg \"uninstall.removing_workspace\" -f $workspaceDir)\n Remove-Item -Recurse -Force $workspaceDir -ErrorAction SilentlyContinue\n }\n\n # Remove env file\n if (Test-Path $script:AGENTTEAMS_ENV_FILE) {\n Write-Log (Get-Msg \"uninstall.removing_env\" -f $script:AGENTTEAMS_ENV_FILE)\n Remove-Item -Force $script:AGENTTEAMS_ENV_FILE\n }\n\n # Remove install log (stop transcript first to release the file)\n if (Test-Path $script:AGENTTEAMS_LOG_FILE) {\n Write-Log (Get-Msg \"uninstall.removing_log\" -f $script:AGENTTEAMS_LOG_FILE)\n try { Stop-Transcript *>$null } catch {}\n Remove-Item -Force $script:AGENTTEAMS_LOG_FILE -ErrorAction SilentlyContinue\n }\n\n Write-Log \"\"\n Write-Log (Get-Msg \"uninstall.done\")\n}\n\n# ============================================================\n# Main Entry Point\n# ============================================================\n\ntry {\n switch ($Command) {\n \"manager\" {\n Install-Manager\n }\n \"worker\" {\n Install-Worker -Name $Name -Fs $Fs -FsKey $FsKey -FsSecret $FsSecret -Reset:$Reset -FindSkills:$FindSkills -SkillsApiUrl $SkillsApiUrl\n }\n \"uninstall\" {\n Uninstall-AgentTeams\n }\n }\n} catch {\n Exit-Script 1\n}\n\n# Stop transcript logging\ntry {\n Stop-Transcript -ErrorAction SilentlyContinue | Out-Null\n} catch {\n # Ignore errors when stopping transcript\n}\n"}, "2ea027403398dfa06f3fc86445042d59f4684d71": {"t": 1785764734.0489852, "n": 193564, "h": "6fc90f14bd01f519", "c": "#!/usr/bin/env powershell\n# agentteams-install.ps1 - One-click installation for AgentTeams Manager and Worker on Windows\n# Requires PowerShell 7.0+ (recommended)\n#\n# Usage:\n# .\\agentteams-install.ps1 # Interactive installation (choose Quick Start or Manual)\n# .\\agentteams-install.ps1 manager # Same as above (explicit)\n# .\\agentteams-install.ps1 worker --name <name> ... # Worker installation\n#\n# Onboarding Modes:\n# Quick Start - Fast installation with all default values (recommended)\n# Manual - Customize each option step by step\n#\n# Environment variables (for automation):\n# AGENTTEAMS_NON_INTERACTIVE Skip all prompts, use defaults (default: 0)\n# AGENTTEAMS_LLM_PROVIDER LLM provider (default: openai-compat for zh non-interactive Token Plan; qwen for en)\n# AGENTTEAMS_DEFAULT_MODEL Default model (default: qwen3.6-plus for zh Token Plan and en non-interactive)\n# AGENTTEAMS_OPENAI_BASE_URL OpenAI-compatible base URL (default for zh non-interactive: Alibaba Token Plan endpoint)\n# AGENTTEAMS_LLM_API_KEY LLM API key (required)\n# AGENTTEAMS_ADMIN_USER Admin username (default: admin)\n# AGENTTEAMS_ADMIN_PASSWORD Admin password (auto-generated if not set, min 8 chars)\n# AGENTTEAMS_MATRIX_DOMAIN Matrix domain (default: matrix-local.agentteams.io:18080)\n# AGENTTEAMS_MOUNT_SOCKET Mount container runtime socket (default: 1)\n# AGENTTEAMS_MATRIX_E2EE Matrix E2EE (default: 0, disabled)\n# AGENTTEAMS_DATA_DIR Docker volume name for persistent data (default: agentteams-data)\n# AGENTTEAMS_WORKSPACE_DIR Host directory for manager workspace (default: ~/agentteams-manager)\n# AGENTTEAMS_VERSION Image tag (default: latest)\n# AGENTTEAMS_REGISTRY Image registry (default: auto-detected by timezone)\n# AGENTTEAMS_INSTALL_MANAGER_IMAGE Override manager image (e.g., local build)\n# AGENTTEAMS_INSTALL_WORKER_IMAGE Override worker image (e.g., local build)\n# AGENTTEAMS_INSTALL_COPAW_WORKER_IMAGE Override copaw worker image (e.g., local build)\n# AGENTTEAMS_INSTALL_QWENPAW_WORKER_IMAGE Override qwenpaw worker image (e.g., local build)\n# AGENTTEAMS_INSTALL_HERMES_WORKER_IMAGE Override hermes worker image (e.g., local build)\n# AGENTTEAMS_PORT_GATEWAY Host port for Higress gateway (default: 18080)\n# AGENTTEAMS_PORT_CONSOLE Host port for Higress console (default: 18001)\n# AGENTTEAMS_PORT_ELEMENT_WEB Host port for Element Web direct access (default: 18088)\n\n#Requires -Version 5.1\n\n[CmdletBinding()]\nparam(\n [Parameter(Position = 0)]\n [ValidateSet(\"manager\", \"worker\", \"uninstall\")]\n [string]$Command = \"manager\",\n\n # Worker options\n [string]$Name,\n [string]$Fs,\n [string]$FsKey,\n [string]$FsSecret,\n [switch]$Reset,\n [switch]$FindSkills,\n [string]$SkillsApiUrl,\n\n # General options\n [switch]$NonInteractive,\n [string]$EnvFile\n)\n\n# ============================================================\n# Configuration\n# ============================================================\n\n$script:AGENTTEAMS_VERSION = if ($env:AGENTTEAMS_VERSION) { $env:AGENTTEAMS_VERSION } else { \"latest\" }\n$script:AGENTTEAMS_NON_INTERACTIVE = if ($env:AGENTTEAMS_NON_INTERACTIVE -eq \"1\" -or $NonInteractive) { $true } else { $false }\n$script:AGENTTEAMS_MOUNT_SOCKET = if ($env:AGENTTEAMS_MOUNT_SOCKET -eq \"0\") { $false } else { $true }\n$script:AGENTTEAMS_ENV_FILE = if ($EnvFile) { $EnvFile } elseif ($env:AGENTTEAMS_ENV_FILE) { $env:AGENTTEAMS_ENV_FILE } else { \"$env:USERPROFILE\\agentteams-manager.env\" }\n$script:StepResult = \"\" # Used by state machine to signal \"back\" navigation\n$script:config = @{} # Shared config hashtable for step functions\n\n# ANSI escape character for terminal colors\n$script:ESC = [char]0x1B\n\n# ============================================================\n# Log all output to file\n# ============================================================\n\n$script:AGENTTEAMS_LOG_FILE = \"$env:USERPROFILE\\agentteams-install.log\"\n\n# Start transcript for logging (PowerShell's built-in logging mechanism)\ntry {\n Start-Transcript -Path $script:AGENTTEAMS_LOG_FILE -Append -ErrorAction SilentlyContinue | Out-Null\n} catch {\n # If transcript fails, continue without logging\n}\n\nWrite-Host \"\"\nWrite-Host \"========================================\"\nWrite-Host \"AgentTeams Installation Log\"\nWrite-Host \"Started: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')\"\nWrite-Host \"User: $env:USERNAME\"\nWrite-Host \"System: $(hostname)\"\nWrite-Host \"Log file: $($script:AGENTTEAMS_LOG_FILE)\"\nWrite-Host \"========================================\"\nWrite-Host \"\"\n\n# ============================================================\n# Utility Functions\n# ============================================================\n\nfunction Write-Log {\n param([string]$Message)\n Write-Host \"$($script:ESC)[36m[AgentTeams]$($script:ESC)[0m $Message\"\n}\n\nfunction Write-Error {\n param([string]$Message)\n Write-Host \"$($script:ESC)[31m[AgentTeams ERROR]$($script:ESC)[0m $Message\" -ForegroundColor Red\n throw $Message\n}\n\nfunction Write-Warning {\n param([string]$Message)\n Write-Host \"$($script:ESC)[33m[AgentTeams WARNING]$($script:ESC)[0m $Message\"\n}\n\n# Pause before exit on error so user can read the message when running via double-click\nfunction Exit-Script {\n param([int]$ExitCode = 0)\n if ($ExitCode -ne 0 -and -not $script:AGENTTEAMS_NON_INTERACTIVE) {\n Write-Host \"\"\n Write-Host \"Press Enter to exit.\" -NoNewline\n $Host.UI.ReadLine()\n }\n exit $ExitCode\n}\n\nfunction Test-DockerRunning {\n try {\n $null = docker info 2>&1\n if ($LASTEXITCODE -ne 0) {\n return $false\n }\n return $true\n }\n catch {\n return $false\n }\n}\n\nfunction Get-AgentTeamsTimeZone {\n try {\n if ($env:AGENTTEAMS_TIMEZONE) {\n return $env:AGENTTEAMS_TIMEZONE\n }\n\n $tz = (Get-TimeZone).Id\n # Convert Windows timezone to IANA format\n $tzMap = @{\n \"China Standard Time\" = \"Asia/Shanghai\"\n \"Pacific Standard Time\" = \"America/Los_Angeles\"\n \"Mountain Standard Time\" = \"America/Denver\"\n \"Central Standard Time\" = \"America/Chicago\"\n \"Eastern Standard Time\" = \"America/New_York\"\n \"GMT Standard Time\" = \"Europe/London\"\n \"Central European Standard Time\" = \"Europe/Berlin\"\n \"Tokyo Standard Time\" = \"Asia/Tokyo\"\n \"Singapore Standard Time\" = \"Asia/Singapore\"\n \"Korea Standard Time\" = \"Asia/Seoul\"\n \"India Standard Time\" = \"Asia/Kolkata\"\n }\n\n if ($tzMap.ContainsKey($tz)) {\n return $tzMap[$tz]\n }\n return $tz\n }\n catch {\n return \"Asia/Shanghai\"\n }\n}\n\nfunction Get-Registry {\n param([string]$Timezone)\n\n if ($env:AGENTTEAMS_REGISTRY) {\n return $env:AGENTTEAMS_REGISTRY\n }\n\n # Americas\n if ($Timezone -match \"^America/\") {\n return \"higress-registry.us-west-1.cr.aliyuncs.com\"\n }\n\n # Southeast Asia\n if ($Timezone -match \"^(Asia/Singapore|Asia/Bangkok|Asia/Jakarta|Asia/Makassar|Asia/Jayapura|Asia/Kuala_Lumpur|Asia/Ho_Chi_Minh|Asia/Manila|Asia/Yangon|Asia/Vientiane|Asia/Phnom_Penh|Asia/Pontianak|Asia/Ujung_Pandang)\") {\n return \"higress-registry.ap-southeast-7.cr.aliyuncs.com\"\n }\n\n # Default: China\n return \"higress-registry.cn-hangzhou.cr.aliyuncs.com\"\n}\n\nfunction Get-AgentTeamsLanguage {\n param([string]$Timezone)\n $chineseZones = @(\n \"Asia/Shanghai\", \"Asia/Chongqing\", \"Asia/Harbin\", \"Asia/Urumqi\",\n \"Asia/Taipei\", \"Asia/Hong_Kong\", \"Asia/Macau\"\n )\n if ($chineseZones -contains $Timezone) { return \"zh\" }\n return \"en\"\n}\n\n# ============================================================\n# Centralized message dictionary and Get-Msg function\n# ============================================================\n\n$script:Messages = @{\n # --- Timezone detection messages ---\n \"tz.warning.title\" = @{ zh = \"无法自动检测时区。\"; en = \"Could not detect timezone automatically.\" }\n \"tz.warning.prompt\" = @{ zh = \"请输入您的时区(例如 Asia/Shanghai、America/New_York)。\"; en = \"Please enter your timezone (e.g., Asia/Shanghai, America/New_York).\" }\n \"tz.default\" = @{ zh = \"使用默认时区: {0}\"; en = \"Using default timezone: {0}\" }\n \"tz.input_prompt\" = @{ zh = \"时区\"; en = \"Timezone\" }\n\n # --- Installation title and info ---\n \"install.title\" = @{ zh = \"=== AgentTeams Manager 安装 ===\"; en = \"=== AgentTeams Manager Installation ===\" }\n \"install.registry\" = @{ zh = \"镜像仓库: {0}\"; en = \"Registry: {0}\" }\n \"install.dir\" = @{ zh = \"安装目录: {0}\"; en = \"Installation directory: {0}\" }\n \"install.dir_hint\" = @{ zh = \" (env 文件 'agentteams-manager.env' 将保存到 HOME 目录。)\"; en = \" (The env file 'agentteams-manager.env' will be saved to your HOME directory.)\" }\n \"install.dir_hint2\" = @{ zh = \" (请从您希望管理此安装的目录运行此脚本。)\"; en = \" (Run this script from the directory where you want to manage this installation.)\" }\n\n # --- Onboarding mode ---\n \"install.mode.title\" = @{ zh = \"--- Onboarding 模式 ---\"; en = \"--- Onboarding Mode ---\" }\n \"install.mode.choose\" = @{ zh = \"选择安装模式:\"; en = \"Choose your installation mode:\" }\n \"install.mode.quickstart\" = @{ zh = \" 1) 快速开始 - 使用阿里云通义 Token 套餐快速安装(推荐)\"; en = \" 1) Quick Start - Fast installation with Qwen Cloud (recommended)\" }\n \"install.mode.manual\" = @{ zh = \" 2) 手动配置 - 选择 LLM 提供商并自定义选项\"; en = \" 2) Manual - Choose LLM provider and customize options\" }\n \"install.mode.prompt\" = @{ zh = \"请选择 [1/2]\"; en = \"Enter choice [1/2]\" }\n \"install.mode.quickstart_selected\" = @{ zh = \"已选择快速开始模式 - 使用阿里云通义 Token 套餐\"; en = \"Quick Start mode selected - using Qwen Cloud\" }\n \"install.mode.manual_selected\" = @{ zh = \"已选择手动配置模式 - 您将选择 LLM 提供商并自定义选项\"; en = \"Manual mode selected - you will choose LLM provider and customize options\" }\n \"install.mode.invalid\" = @{ zh = \"无效选择,默认使用快速开始模式\"; en = \"Invalid choice, defaulting to Quick Start mode\" }\n\n # --- Existing installation detected ---\n \"install.existing.detected\" = @{ zh = \"检测到已有 Manager 安装(env 文件: {0})\"; en = \"Existing Manager installation detected (env file: {0})\" }\n \"install.existing.choose\" = @{ zh = \"选择操作:\"; en = \"Choose an action:\" }\n \"install.existing.upgrade\" = @{ zh = \" 1) 就地升级(保留数据、工作空间、env 文件)\"; en = \" 1) In-place upgrade (keep data, workspace, env file)\" }\n \"install.existing.reinstall\" = @{ zh = \" 2) 全新重装(删除所有数据,重新开始)\"; en = \" 2) Clean reinstall (remove all data, start fresh)\" }\n \"install.existing.cancel\" = @{ zh = \" 3) 取消\"; en = \" 3) Cancel\" }\n \"install.existing.prompt\" = @{ zh = \"请选择 [1/2/3]\"; en = \"Enter choice [1/2/3]\" }\n \"install.existing.upgrade_noninteractive\" = @{ zh = \"非交互模式: 执行就地升级...\"; en = \"Non-interactive mode: performing in-place upgrade...\" }\n \"install.existing.upgrading\" = @{ zh = \"执行就地升级...\"; en = \"Performing in-place upgrade...\" }\n \"install.existing.warn_manager_stop\" = @{ zh = \"警告: Manager 容器将被停止并重新创建。\"; en = \"WARNING: Manager container will be stopped and recreated.\" }\n \"install.existing.warn_worker_recreate\" = @{ zh = \"警告: Worker 容器也将被重新创建(以更新 Manager IP)。\"; en = \"WARNING: Worker containers will also be recreated (to update Manager IP in hosts).\" }\n \"install.existing.continue_prompt\" = @{ zh = \"继续?[y/N]\"; en = \"Continue? [y/N]\" }\n \"install.existing.cancelled\" = @{ zh = \"安装已取消。\"; en = \"Installation cancelled.\" }\n \"install.existing.stopping_manager\" = @{ zh = \"停止并移除现有 manager 容器...\"; en = \"Stopping and removing existing manager container...\" }\n \"install.existing.stopping_workers\" = @{ zh = \"停止并移除现有 worker 容器...\"; en = \"Stopping and removing existing worker containers...\" }\n \"install.existing.removed\" = @{ zh = \" 已移除: {0}\"; en = \" Removed: {0}\" }\n\n # --- Upgrade mode sub-menu ---\n \"upgrade.mode.prompt\" = @{ zh = \"请选择升级方式:\"; en = \"Select upgrade mode:\" }\n \"upgrade.mode.keep_all\" = @{ zh = \" 1) 保留所有参数,一键升级(推荐)\"; en = \" 1) Keep all parameters, quick upgrade (recommended)\" }\n \"upgrade.mode.confirm_each\" = @{ zh = \" 2) 逐个确认参数(可修改)\"; en = \" 2) Confirm each parameter (can modify)\" }\n \"upgrade.mode.back\" = @{ zh = \" 3) 返回\"; en = \" 3) Back\" }\n\n # --- Clean reinstall messages ---\n \"install.reinstall.performing\" = @{ zh = \"执行全新重装...\"; en = \"Performing clean reinstall...\" }\n \"install.reinstall.warn_stop\" = @{ zh = \"警告: 以下运行中的容器将被停止:\"; en = \"WARNING: The following running containers will be stopped:\" }\n \"install.reinstall.warn_delete\" = @{ zh = \"警告: 以下内容将被删除:\"; en = \"WARNING: This will DELETE the following:\" }\n \"install.reinstall.warn_volume\" = @{ zh = \" - Docker 卷: agentteams-data\"; en = \" - Docker volume: agentteams-data\" }\n \"install.reinstall.warn_env\" = @{ zh = \" - Env 文件: {0}\"; en = \" - Env file: {0}\" }\n \"install.reinstall.warn_workspace\" = @{ zh = \" - Manager 工作空间: {0}\"; en = \" - Manager workspace: {0}\" }\n \"install.reinstall.warn_workers\" = @{ zh = \" - 所有 worker 容器\"; en = \" - All worker containers\" }\n \"install.reinstall.warn_proxy\" = @{ zh = \" - Docker API 代理容器: agentteams-controller\"; en = \" - Docker API proxy container: agentteams-controller\" }\n \"install.reinstall.removing_proxy\" = @{ zh = \"正在移除 Docker API 代理容器: agentteams-controller\"; en = \"Removing Docker API proxy container: agentteams-controller\" }\n \"install.reinstall.warn_network\" = @{ zh = \" - Docker 网络: agentteams-net\"; en = \" - Docker network: agentteams-net\" }\n \"install.reinstall.removing_network\" = @{ zh = \"正在移除 Docker 网络: agentteams-net\"; en = \"Removing Docker network: agentteams-net\" }\n \"install.reinstall.confirm_type\" = @{ zh = \"请输入工作空间路径以确认删除(或按 Ctrl+C 取消):\"; en = \"To confirm deletion, please type the workspace path:\" }\n \"install.reinstall.confirm_path\" = @{ zh = \"输入路径以确认(或按 Ctrl+C 取消)\"; en = \"Type the path to confirm (or press Ctrl+C to cancel)\" }\n \"install.reinstall.path_mismatch\" = @{ zh = \"路径不匹配。中止重装。输入: '{0}',期望: '{1}'\"; en = \"Path mismatch. Aborting reinstall. Input: '{0}', Expected: '{1}'\" }\n \"install.reinstall.confirmed\" = @{ zh = \"已确认。正在清理...\"; en = \"Confirmed. Cleaning up...\" }\n \"install.reinstall.removed_worker\" = @{ zh = \" 已移除 worker: {0}\"; en = \" Removed worker: {0}\" }\n \"install.reinstall.removing_volume\" = @{ zh = \"正在移除 Docker 卷: agentteams-data\"; en = \"Removing Docker volume: agentteams-data\" }\n \"install.reinstall.warn_volume_fail\" = @{ zh = \" 警告: 无法移除卷(可能有引用)\"; en = \" Warning: Could not remove volume (may have references)\" }\n \"install.reinstall.removing_workspace\" = @{ zh = \"正在移除工作空间目录: {0}\"; en = \"Removing workspace directory: {0}\" }\n \"install.reinstall.removing_env\" = @{ zh = \"正在移除 env 文件: {0}\"; en = \"Removing env file: {0}\" }\n \"install.reinstall.cleanup_done\" = @{ zh = \"清理完成。开始全新安装...\"; en = \"Cleanup complete. Starting fresh installation...\" }\n \"install.reinstall.failed_rm_workspace\" = @{ zh = \"无法移除工作空间目录\"; en = \"Failed to remove workspace directory\" }\n\n # --- Orphan volume detection ---\n \"install.orphan_volume.detected\" = @{ zh = \"⚠️ 检测到残留数据卷 '{0}',但未找到对应的 env 配置文件。\"; en = \"⚠️ Found leftover data volume '{0}' but no matching env config file.\" }\n \"install.orphan_volume.warn\" = @{ zh = \"这可能是之前安装的残留数据,会导致新安装出现异常(如密码冲突、服务启动失败)。\"; en = \"This is likely leftover data from a previous installation and may cause issues (e.g., credential conflicts, service startup failures).\" }\n \"install.orphan_volume.choose\" = @{ zh = \"选择操作:\"; en = \"Choose an action:\" }\n \"install.orphan_volume.clean\" = @{ zh = \" 1) 清理残留数据卷后继续安装(推荐)\"; en = \" 1) Remove leftover volume and continue installation (recommended)\" }\n \"install.orphan_volume.keep\" = @{ zh = \" 2) 保留数据卷继续安装(可能出现异常)\"; en = \" 2) Keep the volume and continue installation (may cause issues)\" }\n \"install.orphan_volume.prompt\" = @{ zh = \"请选择 [1/2]\"; en = \"Enter choice [1/2]\" }\n \"install.orphan_volume.cleaning\" = @{ zh = \"正在清理残留数据卷...\"; en = \"Removing leftover data volume...\" }\n \"install.orphan_volume.cleaned\" = @{ zh = \"残留数据卷已清理。继续全新安装...\"; en = \"Leftover volume removed. Continuing with fresh installation...\" }\n \"install.orphan_volume.keeping\" = @{ zh = \"保留数据卷,继续安装。如遇异常请选择全新重装。\"; en = \"Keeping existing volume. If you encounter issues, consider a clean reinstall.\" }\n \"install.orphan_volume.clean_noninteractive\" = @{ zh = \"非交互模式: 自动清理残留数据卷...\"; en = \"Non-interactive mode: automatically removing leftover volume...\" }\n\n # --- Loading existing config ---\n \"install.loading_config\" = @{ zh = \"从 {0} 加载已有配置(shell 环境变量优先)...\"; en = \"Loading existing config from {0} (shell env vars take priority)...\" }\n\n # --- LLM Configuration ---\n \"llm.title\" = @{ zh = \"--- LLM 配置 ---\"; en = \"--- LLM Configuration ---\" }\n \"llm.provider.label\" = @{ zh = \" 提供商: {0}\"; en = \" Provider: {0}\" }\n \"llm.model.label\" = @{ zh = \" 模型: {0}\"; en = \" Model: {0}\" }\n \"llm.provider.qwen\" = @{ zh = \" 提供商: qwen(阿里云百炼)\"; en = \" Provider: qwen (Alibaba Cloud Bailian)\" }\n \"llm.provider.qwen_default\" = @{ zh = \" 提供商: {0}(默认)\"; en = \" Provider: {0} (default)\" }\n \"llm.model.default\" = @{ zh = \" 模型: {0}(默认)\"; en = \" Model: {0} (default)\" }\n \"llm.apikey_hint_bailian\" = @{ zh = \" 提示: 获取阿里云百炼(DashScope)API Key:\"; en = \" Hint: Get your Alibaba Cloud Bailian (DashScope) API Key:\" }\n \"llm.apikey_url_bailian\" = @{ zh = \" https://www.aliyun.com/product/bailian\"; en = \" https://www.aliyun.com/product/bailian\" }\n \"llm.apikey_hint_qwencloud\" = @{ zh = \" 提示: 从 Qwen Cloud(国际站)获取 DASHSCOPE_API_KEY:\"; en = \" Hint: Get your DASHSCOPE_API_KEY for Qwen Cloud (international) from:\" }\n \"llm.apikey_url_qwencloud\" = @{ zh = \" https://home.qwencloud.com/api-keys (文档: https://docs.qwencloud.com/)\"; en = \" https://home.qwencloud.com/api-keys | Docs: https://docs.qwencloud.com/\" }\n \"llm.apikey_hint_tokenplan\" = @{ zh = \" 提示: 获取 DashScope API Key 或开通通义 Token 套餐,请参考:\"; en = \" Hint: Get your DashScope or Token Plan API key (Alibaba Model Studio):\" }\n \"llm.apikey_url_tokenplan\" = @{ zh = \" https://help.aliyun.com/zh/model-studio/token-plan-quickstart\"; en = \" https://common-buy.aliyun.com/token-plan/ | https://help.aliyun.com/zh/model-studio/token-plan-quickstart\" }\n \"llm.apikey_hint_codingplan\" = @{ zh = \" 提示: 获取 DashScope API Key(Coding 套餐 / coding.dashscope 接口):\"; en = \" Hint: Get your DashScope API key for Coding Plan (coding.dashscope endpoint):\" }\n \"llm.apikey_url_codingplan\" = @{ zh = \" https://help.aliyun.com/zh/model-studio/get-api-key\"; en = \" https://help.aliyun.com/zh/model-studio/get-api-key\" }\n \"llm.apikey_prompt\" = @{ zh = \"LLM API Key\"; en = \"LLM API Key\" }\n \"llm.providers_title\" = @{ zh = \"可用 LLM 提供商:\"; en = \"Available LLM Providers:\" }\n \"llm.provider.alibaba\" = @{ zh = \" 1) 阿里云通义 Token 套餐 - 推荐中国用户使用\"; en = \" 1) Qwen Cloud - International (OpenAI-compatible API, recommended)\" }\n \"llm.provider.openai_compat\" = @{ zh = \" 2) OpenAI 兼容 API - 自定义 Base URL(OpenAI、DeepSeek 等)\"; en = \" 2) OpenAI-compatible API - Custom Base URL (OpenAI, DeepSeek, etc.)\" }\n \"llm.provider.select\" = @{ zh = \"选择提供商 [1/2]\"; en = \"Select provider [1/2]\" }\n \"llm.alibaba.models_title\" = @{ zh = \"选择阿里云模型接入方式:\"; en = \"Select Alibaba Cloud model access:\" }\n \"llm.alibaba.model.tokenplan\" = @{ zh = \" 1) 阿里云通义 Token 套餐 - 兼容模式(推荐)\"; en = \" 1) Alibaba Cloud Token Plan - compatible-mode (recommended)\" }\n \"llm.alibaba.model.bailian\" = @{ zh = \" 2) 阿里云百炼 - DashScope 通用兼容接口\"; en = \" 2) Alibaba Cloud Bailian - DashScope compatible mode\" }\n \"llm.alibaba.model.codingplan_legacy\" = @{ zh = \" 3) 阿里云 Coding 套餐 - 旧版端点(兼容保留)\"; en = \" 3) Alibaba Cloud Coding Plan - legacy endpoint (backward compatible)\" }\n \"llm.alibaba.model.select\" = @{ zh = \"选择接入方式 [1/2/3]\"; en = \"Select access option [1/2/3]\" }\n \"llm.alibaba.model.invalid\" = @{ zh = \"无效选择: {0}(请输入 1、2 或 3)\"; en = \"Invalid choice: {0} (please enter 1, 2, or 3)\" }\n \"llm.codingplan.models_title\" = @{ zh = \"选择通义 Token 套餐默认模型:\"; en = \"Select Qwen Cloud default model:\" }\n \"llm.codingplan.model.qwen36plus\" = @{ zh = \" 1) qwen3.6-plus - 千问 3.6(推荐)\"; en = \" 1) qwen3.6-plus - Qwen 3.6 (recommended)\" }\n \"llm.codingplan.model.glm5\" = @{ zh = \" 2) glm-5 - 智谱 GLM-5(编程推荐)\"; en = \" 2) glm-5 - Zhipu GLM-5 (recommended for coding)\" }\n \"llm.codingplan.model.kimi\" = @{ zh = \" 3) kimi-k2.5 - Moonshot Kimi K2.5\"; en = \" 3) kimi-k2.5 - Moonshot Kimi K2.5\" }\n \"llm.codingplan.model.minimax\" = @{ zh = \" 4) MiniMax-M2.5 - MiniMax M2.5\"; en = \" 4) MiniMax-M2.5 - MiniMax M2.5\" }\n \"llm.codingplan.model.select\" = @{ zh = \"选择模型 [1/2/3/4]\"; en = \"Select model [1/2/3/4]\" }\n \"llm.provider.selected_tokenplan\" = @{ zh = \" 提供商: 阿里云通义 Token 套餐(兼容模式)\"; en = \" Provider: Alibaba Cloud Token Plan (compatible mode)\" }\n \"llm.provider.selected_codingplan\" = @{ zh = \" 提供商: 阿里云通义 Token 套餐(alibaba-cloud)\"; en = \" Provider: Qwen Cloud (international) (alibaba-cloud)\" }\n \"llm.provider.selected_codingplan_legacy\" = @{ zh = \" 提供商: 阿里云 Coding 套餐(coding.dashscope)\"; en = \" Provider: Alibaba Cloud Coding Plan (coding.dashscope)\" }\n \"llm.provider.selected_qwen\" = @{ zh = \" 提供商: 阿里云百炼\"; en = \" Provider: Alibaba Cloud Bailian\" }\n \"llm.provider.selected_openai\" = @{ zh = \" 提供商: {0}(OpenAI 兼容)\"; en = \" Provider: {0} (OpenAI-compatible)\" }\n \"llm.provider.invalid\" = @{ zh = \"无效选择: {0}(请输入 1 或 2)\"; en = \"Invalid choice: {0} (please enter 1 or 2)\" }\n \"llm.qwen.model_prompt\" = @{ zh = \"默认模型 ID\"; en = \"Default Model ID\" }\n \"llm.openai.base_url_prompt\" = @{ zh = \"Base URL\"; en = \"Base URL\" }\n \"llm.openai.model_prompt\" = @{ zh = \"默认模型 ID\"; en = \"Default Model ID\" }\n \"llm.openai.base_url_label\" = @{ zh = \" Base URL: {0}\"; en = \" Base URL: {0}\" }\n\n # --- Custom model parameters ---\n \"llm.custom_model.detected\" = @{ zh = \" ⚠️ 模型 '{0}' 不在内置模型列表中,请配置模型参数:\"; en = \" ⚠️ Model '{0}' is not in the built-in model list. Please configure model parameters:\" }\n \"llm.custom_model.context_prompt\" = @{ zh = \"最大上下文长度(token 数)[150000]\"; en = \"Max context window (tokens) [150000]\" }\n \"llm.custom_model.max_tokens_prompt\" = @{ zh = \"最大输出长度(token 数)[128000]\"; en = \"Max output tokens [128000]\" }\n \"llm.custom_model.reasoning_prompt\" = @{ zh = \"是否支持推理/思考模式?[Y/n]\"; en = \"Does it support reasoning/thinking mode? [Y/n]\" }\n \"llm.custom_model.vision_prompt\" = @{ zh = \"是否支持图片输入?[y/N]\"; en = \"Does it support image input? [y/N]\" }\n \"llm.custom_model.summary\" = @{ zh = \" 自定义模型参数: 上下文={0}, 最大输出={1}, 推理={2}, 图片={3}\"; en = \" Custom model params: context={0}, maxTokens={1}, reasoning={2}, vision={3}\" }\n\n # --- Admin Credentials ---\n \"admin.title\" = @{ zh = \"--- 管理员凭据 ---\"; en = \"--- Admin Credentials ---\" }\n \"admin.username_prompt\" = @{ zh = \"管理员用户名\"; en = \"Admin Username\" }\n \"admin.password_prompt\" = @{ zh = \"管理员密码(留空自动生成,最少 8 位)\"; en = \"Admin Password (leave empty to auto-generate, min 8 chars)\" }\n \"admin.password_generated\" = @{ zh = \" 已自动生成管理员密码\"; en = \" Auto-generated admin password\" }\n \"admin.password_too_short\" = @{ zh = \"管理员密码至少需要 8 个字符(MinIO 要求)。当前长度: {0}\"; en = \"Admin password must be at least 8 characters (MinIO requirement). Current length: {0}\" }\n\n # --- Port Configuration ---\n \"port.title\" = @{ zh = \"--- 端口配置(按回车使用默认值)---\"; en = \"--- Port Configuration (press Enter for defaults) ---\" }\n \"port.gateway_prompt\" = @{ zh = \"网关主机端口(容器内 8080)\"; en = \"Host port for gateway (8080 inside container)\" }\n \"port.console_prompt\" = @{ zh = \"Higress 控制台主机端口(容器内 8001)\"; en = \"Host port for Higress console (8001 inside container)\" }\n \"port.element_prompt\" = @{ zh = \"Element Web 直接访问主机端口(容器内 8088)\"; en = \"Host port for Element Web direct access (8088 inside container)\" }\n \"port.manager_console_prompt\" = @{ zh = \"Manager 控制台主机端口(容器内 18888)\"; en = \"Host port for Manager console (18888 inside container)\" }\n \"port.local_only.title\" = @{ zh = \"--- 网络访问模式 ---\"; en = \"--- Network Access Mode ---\" }\n \"port.local_only.hint_yes\" = @{ zh = \" 仅本机使用,无需开放外部端口(推荐)\"; en = \" Local use only, no external port exposure (recommended)\" }\n \"port.local_only.hint_no\" = @{ zh = \" 允许外部访问(局域网 / 公网)\"; en = \" Allow external access (LAN / public network)\" }\n \"port.local_only.choice\" = @{ zh = \"请选择 [1/2]\"; en = \"Enter choice [1/2]\" }\n \"port.local_only.selected_local\" = @{ zh = \"端口已绑定到 127.0.0.1(仅本机访问)\"; en = \"Ports bound to 127.0.0.1 (localhost only)\" }\n \"port.local_only.selected_external\" = @{ zh = \"端口已绑定到所有网络接口(0.0.0.0)\"; en = \"Ports bound to all interfaces (0.0.0.0)\" }\n \"port.local_only.https_hint\" = @{ zh = \"警告: 建议在 Higress 控制台配置 TLS 证书并启用 HTTPS,避免明文传输。\"; en = \"WARNING: It is recommended to configure TLS certificates and enable HTTPS in the Higress Console to avoid plaintext transmission.\" }\n \"port.local_only.https_docs\" = @{ zh = \"\"; en = \"\" }\n\n # --- Domain Configuration ---\n \"domain.title\" = @{ zh = \"--- 域名配置(按回车使用默认值)---\"; en = \"--- Domain Configuration (press Enter for defaults) ---\" }\n \"domain.hint\" = @{ zh = \"提示: 自定义域名前必须事先做好 DNS 解析。单机 ECS 部署时无需修改 aigw、fs 等域名;Element Web 和 Matrix Server 也可通过 IP 直接访问。\"; en = \"Hint: Configure DNS resolution before customizing domains. For single ECS deployment, no need to change aigw, fs, etc.; Element Web and Matrix Server can also be accessed directly via IP.\" }\n \"domain.matrix_prompt\" = @{ zh = \"Matrix 域名\"; en = \"Matrix Domain\" }\n \"domain.element_prompt\" = @{ zh = \"Element Web 域名\"; en = \"Element Web Domain\" }\n \"domain.gateway_prompt\" = @{ zh = \"AI 网关域名\"; en = \"AI Gateway Domain\" }\n \"domain.fs_prompt\" = @{ zh = \"文件系统域名\"; en = \"File System Domain\" }\n \"domain.console_prompt\" = @{ zh = \"Manager 控制台域名\"; en = \"Manager Console Domain\" }\n\n # --- GitHub Integration ---\n \"github.title\" = @{ zh = \"--- GitHub 集成(可选,按回车跳过)---\"; en = \"--- GitHub Integration (optional, press Enter to skip) ---\" }\n \"github.token_prompt\" = @{ zh = \"GitHub 个人访问令牌(可选)\"; en = \"GitHub Personal Access Token (optional)\" }\n\n # --- Skills Registry ---\n \"skills.title\" = @{ zh = \"--- Skills 注册中心(可选,按回车使用默认 nacos://market.agentteams.io:80/public)---\"; en = \"--- Skills Registry (optional, press Enter for default nacos://market.agentteams.io:80/public) ---\" }\n \"skills.url_prompt\" = @{ zh = \"Skills 注册中心 URL(留空使用默认 nacos://market.agentteams.io:80/public)\"; en = \"Skills Registry URL (leave empty for default nacos://market.agentteams.io:80/public)\" }\n\n # --- Data Persistence ---\n \"data.title\" = @{ zh = \"--- 数据持久化 ---\"; en = \"--- Data Persistence ---\" }\n \"data.volume_prompt\" = @{ zh = \"Docker 卷名称 [agentteams-data]\"; en = \"Docker volume name for persistent data [agentteams-data]\" }\n \"data.volume_using\" = @{ zh = \" 使用 Docker 卷: {0}\"; en = \" Using Docker volume: {0}\" }\n\n # --- Manager Workspace ---\n \"workspace.title\" = @{ zh = \"--- Manager 工作空间 ---\"; en = \"--- Manager Workspace ---\" }\n \"workspace.dir_prompt\" = @{ zh = \"Manager 工作空间目录 [{0}]\"; en = \"Manager workspace directory [{0}]\" }\n \"workspace.dir_label\" = @{ zh = \" Manager 工作空间: {0}\"; en = \" Manager workspace: {0}\" }\n\n # --- Host directory sharing ---\n \"host_share.prompt\" = @{ zh = \"与 Agent 共享的主机目录(默认: {0})\"; en = \"Host directory to share with agents (default: {0})\" }\n \"host_share.sharing\" = @{ zh = \"共享主机目录: {0} -> 容器内 /host-share\"; en = \"Sharing host directory: {0} -> /host-share in container\" }\n \"host_share.not_exist\" = @{ zh = \"警告: 主机目录 {0} 不存在,跳过验证继续使用\"; en = \"WARNING: Host directory {0} does not exist, using without validation\" }\n\n # --- Default worker runtime ---\n \"worker_runtime.title\" = @{ zh = \"--- 默认 Worker 运行时 ---\"; en = \"--- Default Worker Runtime ---\" }\n \"worker_runtime.openclaw\" = @{ zh = \"OpenClaw\"; en = \"OpenClaw\" }\n \"worker_runtime.copaw\" = @{ zh = \"CoPaw\"; en = \"CoPaw\" }\n \"worker_runtime.hermes\" = @{ zh = \"Hermes\"; en = \"Hermes\" }\n \"worker_runtime.choice\" = @{ zh = \"请选择 [1/2/3]\"; en = \"Enter choice [1/2/3]\" }\n \"worker_runtime.selected\" = @{ zh = \"默认 Worker 运行时: {0}\"; en = \"Default Worker runtime: {0}\" }\n \"worker_runtime.title_short\" = @{ zh = \"默认 Worker 运行时\"; en = \"Default Worker Runtime\" }\n\n # --- Manager runtime ---\n \"manager_runtime.title\" = @{ zh = \"--- Manager 运行时 ---\"; en = \"--- Manager Runtime ---\" }\n \"manager_runtime.openclaw\" = @{ zh = \"OpenClaw\"; en = \"OpenClaw\" }\n \"manager_runtime.copaw\" = @{ zh = \"CoPaw\"; en = \"CoPaw\" }\n \"manager_runtime.choice\" = @{ zh = \"请选择 [1/2]\"; en = \"Enter choice [1/2]\" }\n \"manager_runtime.selected\" = @{ zh = \"Manager 运行时: {0}\"; en = \"Manager runtime: {0}\" }\n \"manager_runtime.title_short\" = @{ zh = \"Manager 运行时\"; en = \"Manager Runtime\" }\n\n # --- Matrix E2EE ---\n \"matrix_e2ee.title\" = @{ zh = \"--- Matrix 端到端加密(E2EE)---\"; en = \"--- Matrix End-to-End Encryption (E2EE) ---\" }\n \"matrix_e2ee.desc\" = @{\n zh = \"E2EE 会对 Manager 与 Worker 之间的 Matrix 消息进行端到端加密。`n 启用后,即使 Matrix 服务器被入侵,消息内容也无法被窃取。`n 但 E2EE 会增加首次握手耗时,且要求所有 Agent 都支持 matrix-sdk-crypto。`n 如果不确定,建议保持禁用。`n ⚠ 注意:禁用 E2EE 后,请勿在 Element 上创建默认启用加密的 Private 房间,`n 否则 Agent 将无法读取该房间中的加密消息。请改用 Public 房间或关闭房间加密。\"\n en = \"E2EE encrypts Matrix messages between Manager and Workers end-to-end.`n When enabled, message content stays private even if the Matrix server is compromised.`n However, E2EE adds overhead to the initial handshake and requires all Agents`n to support matrix-sdk-crypto. If unsure, keep it disabled.`n ⚠ Note: When E2EE is disabled, do NOT create Private rooms in Element (which`n enable encryption by default) — Agents cannot read encrypted messages without`n E2EE support. Use Public rooms or turn off room encryption instead.\"\n }\n \"matrix_e2ee.enable\" = @{ zh = \"启用 E2EE\"; en = \"Enable E2EE\" }\n \"matrix_e2ee.disable\" = @{ zh = \"禁用 E2EE(推荐)\"; en = \"Disable E2EE (recommended)\" }\n \"matrix_e2ee.choice\" = @{ zh = \"请选择 [1/2]\"; en = \"Enter choice [1/2]\" }\n \"matrix_e2ee.selected_enabled\" = @{ zh = \"Matrix E2EE: 已启用\"; en = \"Matrix E2EE: enabled\" }\n \"matrix_e2ee.selected_disabled\" = @{ zh = \"Matrix E2EE: 已禁用(默认)\"; en = \"Matrix E2EE: disabled (default)\" }\n \"matrix_e2ee.title_short\" = @{ zh = \"Matrix E2EE\"; en = \"Matrix E2EE\" }\n \"matrix_e2ee.val_enabled\" = @{ zh = \"已启用\"; en = \"enabled\" }\n \"matrix_e2ee.val_disabled\" = @{ zh = \"已禁用\"; en = \"disabled\" }\n\n # --- Docker API proxy ---\n \"docker_proxy.title\" = @{ zh = \"--- Docker API 安全代理 ---\"; en = \"--- Docker API Security Proxy ---\" }\n \"docker_proxy.desc\" = @{ zh = \"Docker API 代理可防止 AI Agent 通过 Docker API 越狱访问宿主机。`n 启用后,Manager 不再直接持有 Docker socket,所有容器操作经过安全校验。\"; en = \"Docker API proxy prevents AI Agents from escaping via Docker API to access the host.`n When enabled, Manager no longer has direct Docker socket access; all container operations go through security validation.\" }\n \"docker_proxy.enable\" = @{ zh = \"启用(推荐)\"; en = \"Enable (recommended)\" }\n \"docker_proxy.disable\" = @{ zh = \"禁用(直接挂载 Docker socket)\"; en = \"Disable (mount Docker socket directly)\" }\n \"docker_proxy.choice\" = @{ zh = \"请选择 [1/2]\"; en = \"Enter choice [1/2]\" }\n \"docker_proxy.selected_enabled\" = @{ zh = \"Docker API 代理: 已启用\"; en = \"Docker API proxy: enabled\" }\n \"docker_proxy.selected_disabled\" = @{ zh = \"Docker API 代理: 已禁用\"; en = \"Docker API proxy: disabled\" }\n \"docker_proxy.title_short\" = @{ zh = \"Docker API 代理\"; en = \"Docker API Proxy\" }\n \"docker_proxy.val_enabled\" = @{ zh = \"已启用\"; en = \"enabled\" }\n \"docker_proxy.val_disabled\" = @{ zh = \"已禁用\"; en = \"disabled\" }\n \"docker_proxy.registries_desc\" = @{ zh = \"默认放行的镜像来源:本地镜像、localhost、Higress 仓库(所有 region)。`n 如需放行其他镜像仓库,请输入逗号分隔的地址前缀。`n 示例: ghcr.io/myorg,registry.example.com/team\"; en = \"Default allowed image sources: local images, localhost, Higress registries (all regions).`n To allow additional image sources, enter comma-separated address prefixes.`n Example: ghcr.io/myorg,registry.example.com/team\" }\n \"docker_proxy.registries_prompt\" = @{ zh = \"额外放行的镜像来源(按回车跳过)\"; en = \"Additional allowed image sources (press Enter to skip)\" }\n \"docker_proxy.registries_label\" = @{ zh = \"额外放行的镜像来源\"; en = \"Additional allowed image sources\" }\n\n # --- Worker idle timeout ---\n \"idle_timeout.prompt\" = @{ zh = \"Worker 空闲自动停止超时(分钟)[720]\"; en = \"Worker idle auto-stop timeout in minutes [720]\" }\n \"idle_timeout.selected\" = @{ zh = \"Worker 空闲超时: {0} 分钟\"; en = \"Worker idle timeout: {0} minutes\" }\n \"idle_timeout.label\" = @{ zh = \"Worker 空闲超时(分钟)\"; en = \"Worker idle timeout (min)\" }\n\n # --- Secrets and config ---\n \"install.generating_secrets\" = @{ zh = \"正在生成密钥...\"; en = \"Generating secrets...\" }\n \"install.config_saved\" = @{ zh = \"配置已保存到 {0}\"; en = \"Configuration saved to {0}\" }\n\n # --- Container runtime socket ---\n \"install.socket_detected\" = @{ zh = \"容器运行时 socket: {0}(已启用直接创建 Worker)\"; en = \"Container runtime socket: {0} (direct Worker creation enabled)\" }\n \"install.socket_not_found\" = @{ zh = \"未找到容器运行时 socket(Manager 无法直接创建 Worker 容器,需要你手动执行 docker 命令创建)\"; en = \"No container runtime socket found (Manager cannot create Worker containers directly, you will need to create them manually using docker commands)\" }\n \"install.socket_confirm.title\" = @{ zh = \"警告: 未检测到容器运行时 Socket\"; en = \"WARNING: Container Runtime Socket Not Detected\" }\n \"install.socket_confirm.message\" = @{ zh = \"未找到 Docker/Podman socket,Manager 将无法自动创建 Worker 容器。`n你需要手动执行 docker run 命令来创建 Worker。`n`n是否继续安装?\"; en = \"Docker/Podman socket not found. Manager will not be able to create Worker containers automatically.`nYou will need to manually run docker commands to create Workers.`n`nContinue installation?\" }\n \"install.socket_confirm.prompt\" = @{ zh = \"继续安装? [y/N]: \"; en = \"Continue? [y/N]: \" }\n \"install.socket_confirm.cancelled\" = @{ zh = \"安装已取消。如需启用 Worker 自动创建,请确保 Docker/Podman 正在运行,然后重新运行安装脚本。\"; en = \"Installation cancelled. To enable automatic Worker creation, ensure Docker/Podman is running and re-run the installer.\" }\n\n # --- Container management ---\n \"install.removing_existing\" = @{ zh = \"正在移除现有 agentteams-manager 容器...\"; en = \"Removing existing agentteams-manager container...\" }\n\n # --- YOLO mode ---\n \"install.yolo\" = @{ zh = \"YOLO 模式已启用(自主决策,无交互提示)\"; en = \"YOLO mode enabled (autonomous decisions, no interactive prompts)\" }\n\n # --- Image pulling ---\n \"install.image.exists\" = @{ zh = \"Manager 镜像已存在: {0}\"; en = \"Manager image already exists locally: {0}\" }\n \"install.image.pulling_manager\" = @{ zh = \"正在拉取 Manager 镜像: {0}\"; en = \"Pulling Manager image: {0}\" }\n \"install.image.worker_exists\" = @{ zh = \"Worker 镜像已存在: {0}\"; en = \"Worker image already exists locally: {0}\" }\n \"install.image.pulling_worker\" = @{ zh = \"正在拉取 Worker 镜像: {0}\"; en = \"Pulling Worker image: {0}\" }\n\n # --- Starting container ---\n \"install.starting_manager\" = @{ zh = \"正在启动 Manager 容器...\"; en = \"Starting Manager container...\" }\n\n # --- Wait for Manager ready ---\n \"install.wait_ready\" = @{ zh = \"等待 Manager Agent 就绪(超时: {0}s)...\"; en = \"Waiting for Manager agent to be ready (timeout: {0}s)...\" }\n \"install.wait_ready.ok\" = @{ zh = \"Manager Agent 已就绪!\"; en = \"Manager agent is ready!\" }\n \"install.wait_ready.waiting\" = @{ zh = \"等待中... ({0}s/{1}s)\"; en = \"Waiting... ({0}s/{1}s)\" }\n \"install.wait_ready.timeout\" = @{ zh = \"Manager Agent 在 {0}s 内未就绪。请检查: docker logs {1}\"; en = \"Manager agent did not become ready within {0}s. Check: docker logs {1}\" }\n\n # --- Wait for Matrix ready ---\n \"install.wait_matrix\" = @{ zh = \"等待 Matrix 服务就绪(超时: {0}s)...\"; en = \"Waiting for Matrix server to be ready (timeout: {0}s)...\" }\n \"install.wait_matrix.ok\" = @{ zh = \"Matrix 服务已就绪!\"; en = \"Matrix server is ready!\" }\n \"install.wait_matrix.waiting\" = @{ zh = \"等待 Matrix 中... ({0}s/{1}s)\"; en = \"Waiting for Matrix... ({0}s/{1}s)\" }\n \"install.wait_matrix.timeout\" = @{ zh = \"Matrix 服务在 {0}s 内未就绪。请检查: docker logs {1}\"; en = \"Matrix server did not become ready within {0}s. Check: docker logs {1}\" }\n\n # --- OpenAI-compatible connectivity test ---\n \"llm.openai.test.testing\" = @{ zh = \"正在测试 API 联通性...\"; en = \"Testing API connectivity...\" }\n \"llm.openai.test.ok\" = @{ zh = \"API 联通性测试通过\"; en = \"API connectivity test passed\" }\n \"llm.openai.test.fail\" = @{ zh = \"API 联通性测试失败(HTTP {0})。响应内容:`n{1}`n请根据以上错误信息联系您的模型服务商解决。\"; en = \"API connectivity test failed (HTTP {0}). Response body:`n{1}`nPlease contact your model provider to resolve the issue.\" }\n \"llm.openai.test.fail.tokenplan\" = @{ zh = \"提示: 请确认 API Key 有效且已开通通义 Token 套餐。文档: https://help.aliyun.com/zh/model-studio/token-plan-quickstart\"; en = \"Hint: Verify your Token Plan API key and compatible-mode access. Docs: https://help.aliyun.com/zh/model-studio/token-plan-quickstart\" }\n \"llm.openai.test.fail.codingplan\" = @{ zh = \"提示: 请确认 API Key 有效且已开通通义 Token 套餐。文档: https://help.aliyun.com/zh/model-studio/token-plan-quickstart\"; en = \"Hint: Verify your DASHSCOPE_API_KEY for Qwen Cloud. API keys: https://home.qwencloud.com/api-keys Docs: https://docs.qwencloud.com/\" }\n \"llm.openai.test.fail.codingplan_legacy\" = @{ zh = \"提示: 请确认 API Key 有效且 Coding 套餐接口可用。文档: https://help.aliyun.com/zh/model-studio/get-api-key\"; en = \"Hint: Verify your DashScope API key and Coding Plan access. Docs: https://help.aliyun.com/zh/model-studio/get-api-key\" }\n \"llm.openai.test.confirm\" = @{ zh = \"是否仍要继续安装?[y/N/b]\"; en = \"Continue with installation anyway? [y/N/b]\" }\n \"llm.openai.test.aborted\" = @{ zh = \"安装已中止。\"; en = \"Installation aborted.\" }\n \"llm.embedding.title\" = @{ zh = \"📦 记忆搜索配置\"; en = \"📦 Memory Search Configuration\" }\n \"llm.embedding.hint\" = @{ zh = \" Embedding 模型可提升记忆搜索质量(语义匹配)。不启用也可正常使用记忆功能(关键词匹配)。\"; en = \" Embedding model improves memory search quality (semantic matching). Memory still works without it (keyword matching).\" }\n \"llm.embedding.option.default\" = @{ zh = \" 1) text-embedding-v4(推荐)\"; en = \" 1) text-embedding-v4 (Recommended)\" }\n \"llm.embedding.option.custom\" = @{ zh = \" 2) 自定义 Embedding 模型\"; en = \" 2) Custom embedding model\" }\n \"llm.embedding.option.disable\" = @{ zh = \" 3) 不启用\"; en = \" 3) Do not enable\" }\n \"llm.embedding.select\" = @{ zh = \"选择\"; en = \"Select\" }\n \"llm.embedding.custom_prompt\" = @{ zh = \" Embedding 模型名称\"; en = \" Embedding model name\" }\n \"llm.embedding.test.testing\" = @{ zh = \"正在测试 Embedding API 联通性...\"; en = \"Testing Embedding API connectivity...\" }\n \"llm.embedding.test.ok\" = @{ zh = \"✅ Embedding API 联通性测试通过\"; en = \"✅ Embedding API connectivity test passed\" }\n \"llm.embedding.test.fail\" = @{ zh = \"⚠️ Embedding API 测试失败(HTTP {0})。响应: {1}\"; en = \"⚠️ Embedding API test failed (HTTP {0}). Response: {1}\" }\n \"llm.embedding.auto_disabled\" = @{ zh = \"⚠️ Embedding 已自动禁用,记忆搜索将使用关键词匹配。您可以稍后在 agentteams-manager.env 中设置 AGENTTEAMS_EMBEDDING_MODEL 启用。\"; en = \"⚠️ Embedding auto-disabled. Memory search will use keyword matching. You can enable it later in agentteams-manager.env by setting AGENTTEAMS_EMBEDDING_MODEL.\" }\n \"llm.embedding.disabled\" = @{ zh = \"ℹ️ Embedding 已禁用,记忆搜索将使用关键词匹配。\"; en = \"ℹ️ Embedding disabled. Memory search will use keyword matching.\" }\n \"nav.back_hint\" = @{ zh = \"(输入 b 返回上一步)\"; en = \"(enter b to go back)\" }\n # --- OpenAI-compatible provider creation ---\n \"install.openai_compat.missing\" = @{ zh = \"警告: OpenAI Base URL 或 API Key 未设置,跳过提供商创建\"; en = \"WARNING: OpenAI Base URL or API Key not set, skipping provider creation\" }\n \"install.openai_compat.creating\" = @{ zh = \"正在创建 OpenAI 兼容提供商...\"; en = \"Creating OpenAI-compatible provider...\" }\n \"install.openai_compat.domain\" = @{ zh = \" 域名: {0}\"; en = \" Domain: {0}\" }\n \"install.openai_compat.port\" = @{ zh = \" 端口: {0}\"; en = \" Port: {0}\" }\n \"install.openai_compat.protocol\" = @{ zh = \" 协议: {0}\"; en = \" Protocol: {0}\" }\n \"install.openai_compat.service_fail\" = @{ zh = \"警告: 创建 DNS 服务源失败(可能已存在)\"; en = \"WARNING: Failed to create DNS service source (may already exist)\" }\n \"install.openai_compat.provider_fail\" = @{ zh = \"警告: 创建 AI 提供商失败(可能已存在)\"; en = \"WARNING: Failed to create AI provider (may already exist)\" }\n \"install.openai_compat.success\" = @{ zh = \"OpenAI 兼容提供商创建成功\"; en = \"OpenAI-compatible provider created successfully\" }\n\n # --- Welcome message ---\n \"install.welcome_msg.soul_configured\" = @{ zh = \"Soul 已配置(找到 soul-configured 标记),跳过 onboarding 消息\"; en = \"Soul already configured (soul-configured marker found), skipping onboarding message\" }\n \"install.welcome_msg.logging_in\" = @{ zh = \"正在以 {0} 身份登录以发送欢迎消息...\"; en = \"Logging in as {0} to send welcome message...\" }\n \"install.welcome_msg.login_failed\" = @{ zh = \"警告: 以 {0} 身份登录失败,跳过欢迎消息\"; en = \"WARNING: Failed to login as {0}, skipping welcome message\" }\n \"install.welcome_msg.finding_room\" = @{ zh = \"正在查找与 Manager 的 DM 房间...\"; en = \"Finding DM room with Manager...\" }\n \"install.welcome_msg.creating_room\" = @{ zh = \"正在创建与 Manager 的 DM 房间...\"; en = \"Creating DM room with Manager...\" }\n \"install.welcome_msg.no_room\" = @{ zh = \"警告: 无法找到或创建与 Manager 的 DM 房间\"; en = \"WARNING: Could not find or create DM room with Manager\" }\n \"install.welcome_msg.waiting_join\" = @{ zh = \"等待 Manager 加入房间...\"; en = \"Waiting for Manager to join the room...\" }\n \"install.welcome_msg.sending\" = @{ zh = \"正在向 Manager 发送欢迎消息...\"; en = \"Sending welcome message to Manager...\" }\n \"install.welcome_msg.send_failed\" = @{ zh = \"警告: 发送欢迎消息失败\"; en = \"WARNING: Failed to send welcome message\" }\n \"install.welcome_msg.sent\" = @{ zh = \"欢迎消息已发送给 Manager\"; en = \"Welcome message sent to Manager\" }\n \"install.welcome_msg.waiting\" = @{ zh = \"等待 Manager 发送欢迎消息(Higress 路由授权 + LLM 探活,约 45-90s)...\"; en = \"Waiting for Manager to send the welcome message (Higress route auth + LLM probe, ~45-90s)...\" }\n \"install.welcome_msg.confirmed\" = @{ zh = \"Manager 已确认发送欢迎消息(status.welcomeSent=true,用时 {0}s)\"; en = \"Manager confirmed welcome message sent (status.welcomeSent=true, {0}s elapsed)\" }\n \"install.welcome_msg.timeout\" = @{ zh = \"警告: 在 {0}s 内未观察到 Manager 发送欢迎消息(status.welcomeSent=true)。安装仍然成功,所有服务已就绪——可继续按下方提示登录 Element Web。\"; en = \"WARNING: Did not observe the Manager sending its welcome message (status.welcomeSent=true) within {0}s. Installation is still successful, all services are up — continue with the Element Web instructions below.\" }\n \"install.welcome_msg.timeout_hint\" = @{ zh = \"手动触发 onboarding: 登录 Element Web → 打开与 Manager 的 DM 房间 → 发送任意一句话(例如 `\"hi`\"),Manager 会接管对话并开始引导。\"; en = \"Manual onboarding: log in to Element Web -> open the DM with the Manager -> send any message (e.g. `\"hi`\") and the Manager will take over and start the guided setup.\" }\n \"install.welcome_msg.timeout_inspect\" = @{ zh = \"排查命令: docker exec agentteams-controller agt get managers default\"; en = \"Inspect status: docker exec agentteams-controller agt get managers default\" }\n \"install.welcome_msg.poll_unavailable\" = @{ zh = \"提示: agentteams-manager 内未找到 AgentTeams CLI,跳过 welcome 等待(旧镜像?)\"; en = \"Note: AgentTeams CLI not found inside agentteams-manager; skipping welcome wait (old image?)\" }\n\n # --- Final output panel ---\n \"success.title\" = @{ zh = \"=== AgentTeams Manager 已启动!===\"; en = \"=== AgentTeams Manager Started! ===\" }\n \"success.domains_configured\" = @{ zh = \"以下域名已配置解析到 127.0.0.1:\"; en = \"The following domains are configured to resolve to 127.0.0.1:\" }\n \"success.open_url\" = @{ zh = \" 在浏览器中打开以下 URL 开始使用: \"; en = \" Open the following URL in your browser to start: \" }\n \"success.login_with\" = @{ zh = \" 登录信息:\"; en = \" Login with:\" }\n \"success.username\" = @{ zh = \" 用户名: {0}\"; en = \" Username: {0}\" }\n \"success.password\" = @{ zh = \" 密码: {0}\"; en = \" Password: {0}\" }\n \"success.after_login\" = @{ zh = \" 登录后,开始与 Manager 聊天!\"; en = \" After login, start chatting with the Manager!\" }\n \"success.tell_it\" = @{ zh = \" 告诉它: `\"创建一个名为 alice 的前端开发 Worker`\"\"; en = \" Tell it: `\"Create a Worker named alice for frontend dev`\"\" }\n \"success.manager_auto\" = @{ zh = \" Manager 会自动处理一切。\"; en = \" The Manager will handle everything automatically.\" }\n \"success.mobile_title\" = @{ zh = \" 移动端访问(FluffyChat / Element Mobile):\"; en = \" Mobile access (FluffyChat / Element Mobile):\" }\n \"success.mobile_step1\" = @{ zh = \" 1. 在手机上下载 FluffyChat 或 Element\"; en = \" 1. Download FluffyChat or Element on your phone\" }\n \"success.mobile_step2\" = @{ zh = \" 2. 设置 homeserver 为: {0}\"; en = \" 2. Set homeserver to: {0}\" }\n \"success.mobile_step2_noip\" = @{ zh = \" 2. 设置 homeserver 为: http://<本机局域网IP>:{0}\"; en = \" 2. Set homeserver to: http://<this-machine-LAN-IP>:{0}\" }\n \"success.mobile_noip_hint\" = @{ zh = \" (无法自动检测局域网 IP — 请使用 ifconfig / ip addr 查看)\"; en = \" (Could not detect LAN IP automatically — check with: ifconfig / ip addr)\" }\n \"success.mobile_step3\" = @{ zh = \" 3. 登录信息:\"; en = \" 3. Login with:\" }\n \"success.mobile_username\" = @{ zh = \" 用户名: {0}\"; en = \" Username: {0}\" }\n \"success.mobile_password\" = @{ zh = \" 密码: {0}\"; en = \" Password: {0}\" }\n\n # --- Other consoles and tips ---\n \"success.other_consoles\" = @{ zh = \"--- 其他控制台 ---\"; en = \"--- Other Consoles ---\" }\n \"success.higress_console\" = @{ zh = \" Higress 控制台: http://localhost:{0}(用户名: {1} / 密码: {2})\"; en = \" Higress Console: http://localhost:{0} (Username: {1} / Password: {2})\" }\n \"success.manager_console\" = @{ zh = \" Manager 控制台(本地): http://localhost:{0}(无需登录)\"; en = \" Manager Console (local): http://localhost:{0} (no login required)\" }\n \"success.manager_console_gateway\" = @{ zh = \" Manager 控制台(网关): http://console-local.agentteams.io(用户名: {0} / 密码: {1})\"; en = \" Manager Console (gateway): http://console-local.agentteams.io (Username: {0} / Password: {1})\" }\n \"success.copaw_console\" = @{ zh = \" CoPaw 控制台(本地): http://localhost:{0}(无需登录)\"; en = \" CoPaw Console (local): http://localhost:{0} (no login required)\" }\n \"success.switch_llm.title\" = @{ zh = \"--- 切换 LLM 提供商 ---\"; en = \"--- Switch LLM Providers ---\" }\n \"success.switch_llm.hint\" = @{ zh = \" 您可以通过 Higress 控制台切换到其他 LLM 提供商(OpenAI、Anthropic 等)。\"; en = \" You can switch to other LLM providers (OpenAI, Anthropic, etc.) via Higress Console.\" }\n \"success.switch_llm.docs\" = @{ zh = \" 详细说明请参阅:\"; en = \" For detailed instructions, see:\" }\n \"success.switch_llm.url\" = @{ zh = \" https://higress.ai/en/docs/ai/scene-guide/multi-proxy#console-configuration\"; en = \" https://higress.ai/en/docs/ai/scene-guide/multi-proxy#console-configuration\" }\n \"success.tip\" = @{ zh = \"提示: 您也可以在聊天中让 Manager 为您配置 LLM 提供商。\"; en = \"Tip: You can also ask the Manager to configure LLM providers for you in the chat.\" }\n \"success.config_file\" = @{ zh = \"配置文件: {0}\"; en = \"Configuration file: {0}\" }\n \"success.data_volume\" = @{ zh = \"数据卷: {0}\"; en = \"Data volume: {0}\" }\n \"success.workspace\" = @{ zh = \"Manager 工作空间: {0}\"; en = \"Manager workspace: {0}\" }\n\n # --- Worker installation ---\n \"worker.resetting\" = @{ zh = \"正在重置 Worker: {0}...\"; en = \"Resetting Worker: {0}...\" }\n \"worker.exists\" = @{ zh = \"容器 '{0}' 已存在。使用 --reset 重新创建。\"; en = \"Container '{0}' already exists. Use --reset to recreate.\" }\n \"worker.starting\" = @{ zh = \"正在启动 Worker: {0}...\"; en = \"Starting Worker: {0}...\" }\n \"worker.skills_url\" = @{ zh = \" Skills API URL: {0}\"; en = \" Skills API URL: {0}\" }\n \"worker.started\" = @{ zh = \"=== Worker {0} 已启动!===\"; en = \"=== Worker {0} Started! ===\" }\n \"worker.container\" = @{ zh = \"容器: {0}\"; en = \"Container: {0}\" }\n \"worker.view_logs\" = @{ zh = \"查看日志: docker logs -f {0}\"; en = \"View logs: docker logs -f {0}\" }\n\n # --- Prompt function messages ---\n \"prompt.preset\" = @{ zh = \" {0} = (已通过环境变量预设)\"; en = \" {0} = (pre-set via env)\" }\n \"prompt.upgrade_keep\" = @{ zh = \" {0} = {1}(当前值,回车保留 / 输入新值覆盖)\"; en = \" {0} = {1} (current value, press Enter to keep / type new value to change)\" }\n \"prompt.upgrade_empty\" = @{ zh = \" {0} = (未设置,回车跳过 / 输入新值设置)\"; en = \" {0} = (not set, press Enter to skip / type new value to set)\" }\n \"prompt.default\" = @{ zh = \" {0} = {1}(默认)\"; en = \" {0} = {1} (default)\" }\n \"prompt.required\" = @{ zh = \"{0} 是必需的(在非交互模式下通过环境变量设置)\"; en = \"{0} is required (set via environment variable in non-interactive mode)\" }\n \"prompt.required_empty\" = @{ zh = \"{0} 是必需的\"; en = \"{0} is required\" }\n\n # --- Language switch prompt (bilingual by design) ---\n \"lang.detected.zh\" = @{ zh = \"检测到语言 / Detected language: 中文\"; en = \"检测到语言 / Detected language: 中文\" }\n \"lang.detected.en\" = @{ zh = \"检测到语言 / Detected language: English\"; en = \"检测到语言 / Detected language: English\" }\n \"lang.switch_title\" = @{ zh = \"切换语言 / Switch language:\"; en = \"切换语言 / Switch language:\" }\n \"lang.option_zh\" = @{ zh = \" 1) 中文\"; en = \" 1) 中文\" }\n \"lang.option_en\" = @{ zh = \" 2) English\"; en = \" 2) English\" }\n \"lang.prompt\" = @{ zh = \"请选择 / Enter choice\"; en = \"请选择 / Enter choice\" }\n\n # --- Error messages ---\n \"error.name_required\" = @{ zh = \"--name 是必需的\"; en = \"--name is required\" }\n \"error.fs_required\" = @{ zh = \"--fs 是必需的\"; en = \"--fs is required\" }\n \"error.fs_key_required\" = @{ zh = \"--fs-key 是必需的\"; en = \"--fs-key is required\" }\n \"error.fs_secret_required\" = @{ zh = \"--fs-secret 是必需的\"; en = \"--fs-secret is required\" }\n \"error.unknown_option\" = @{ zh = \"未知选项: {0}\"; en = \"Unknown option: {0}\" }\n \"error.docker_not_running\" = @{ zh = \"Docker 未运行。请先启动 Docker Desktop 或 Podman Desktop。\"; en = \"Docker is not running. Please start Docker Desktop or Podman Desktop first.\" }\n \"error.docker_not_found\" = @{ zh = \"未找到 docker 或 podman 命令。请先安装 Docker Desktop 或 Podman Desktop:`n Docker Desktop: https://www.docker.com/products/docker-desktop/`n Podman Desktop: https://podman-desktop.io/\"; en = \"docker or podman command not found. Please install Docker Desktop or Podman Desktop first:`n Docker Desktop: https://www.docker.com/products/docker-desktop/`n Podman Desktop: https://podman-desktop.io/\" }\n\n # --- Uninstall messages ---\n \"uninstall.title\" = @{ zh = \"正在卸载 AgentTeams...\"; en = \"Uninstalling AgentTeams...\" }\n \"uninstall.stopping_manager\" = @{ zh = \"正在停止并移除 agentteams-manager...\"; en = \"Stopping and removing agentteams-manager...\" }\n \"uninstall.stopping_workers\" = @{ zh = \"正在停止并移除 worker 容器...\"; en = \"Stopping and removing worker containers...\" }\n \"uninstall.removed\" = @{ zh = \" 已移除: {0}\"; en = \" Removed: {0}\" }\n \"uninstall.removing_volume\" = @{ zh = \"正在移除 Docker 卷: agentteams-data\"; en = \"Removing Docker volume: agentteams-data\" }\n \"uninstall.removing_env\" = @{ zh = \"正在移除 env 文件: {0}\"; en = \"Removing env file: {0}\" }\n \"uninstall.removing_proxy\" = @{ zh = \"正在停止并移除 Docker API 代理容器: agentteams-docker-proxy\"; en = \"Stopping and removing Docker API proxy container: agentteams-docker-proxy\" }\n \"uninstall.stopping_controller\" = @{ zh = \"正在停止并移除 agentteams-controller (内嵌 Tuwunel/MinIO/Higress)...\"; en = \"Stopping and removing agentteams-controller (embedded Tuwunel/MinIO/Higress)...\" }\n \"uninstall.removing_network\" = @{ zh = \"正在移除 Docker 网络: agentteams-net\"; en = \"Removing Docker network: agentteams-net\" }\n \"uninstall.removing_workspace\" = @{ zh = \"正在移除工作空间目录: {0}\"; en = \"Removing workspace directory: {0}\" }\n \"uninstall.removing_log\" = @{ zh = \"正在移除日志文件: {0}\"; en = \"Removing log file: {0}\" }\n \"uninstall.done\" = @{ zh = \"AgentTeams 已卸载。\"; en = \"AgentTeams has been uninstalled.\" }\n}\n\n# Get-Msg: look up message by key, with -f style argument substitution.\n# Falls back to English if the current language translation is missing.\nfunction Get-Msg {\n param(\n [Parameter(Mandatory)][string]$Key,\n [object[]]$f\n )\n $lang = $script:AGENTTEAMS_LANGUAGE\n if (-not $lang) { $lang = \"en\" }\n $entry = $script:Messages[$Key]\n if (-not $entry) { return $Key }\n $text = $entry[$lang]\n if (-not $text) { $text = $entry[\"en\"] }\n if (-not $text) { return $Key }\n if ($f) { return ($text -f $f) }\n return $text\n}\n\nfunction Get-LanIP {\n # Detect local LAN IP address on Windows\n try {\n # Get network adapters with IPv4 addresses, prefer connected/active interfaces\n $adapters = Get-NetIPAddress -AddressFamily IPv4 -ErrorAction SilentlyContinue |\n Where-Object {\n $_.IPAddress -notlike \"127.*\" -and\n $_.PrefixOrigin -ne \"WellKnown\" -and\n $_.InterfaceAlias -notlike \"*Loopback*\"\n } |\n Sort-Object {\n # PowerShell 5.1 compatible: use if-else instead of ternary operator\n if ($_.InterfaceAlias -like \"*Wi-Fi*\" -or $_.InterfaceAlias -like \"*Ethernet*\") { 0 } else { 1 }\n }\n\n if ($adapters) {\n return $adapters[0].IPAddress\n }\n\n # Fallback: use ipconfig\n $ipconfig = ipconfig 2>$null\n $ip = ($ipconfig | Select-String \"IPv4 Address.*?: (\\d+\\.\\d+\\.\\d+\\.\\d+)\" | Select-Object -First 1)\n if ($ip -match \"(\\d+\\.\\d+\\.\\d+\\.\\d+)\") {\n return $Matches[1]\n }\n }\n catch {\n # Ignore errors\n }\n\n return \"\"\n}\n\n# Known models list — used to detect custom models during install\n$script:KnownModels = @(\n \"gpt-5.4\", \"gpt-5.3-codex\", \"gpt-5-mini\", \"gpt-5-nano\",\n \"claude-opus-4-6\", \"claude-sonnet-4-6\", \"claude-haiku-4-5\",\n \"qwen3.6-plus\", \"qwen3.5-plus\", \"deepseek-chat\", \"deepseek-reasoner\",\n \"kimi-k2.5\", \"glm-5\", \"MiniMax-M2.7\", \"MiniMax-M2.7-highspeed\", \"MiniMax-M2.5\"\n)\n\nfunction Test-KnownModel {\n param([string]$Model)\n return $script:KnownModels -contains $Model\n}\n\nfunction Request-CustomModelParams {\n param([string]$Model)\n if (Test-KnownModel $Model) {\n $script:config.MODEL_CONTEXT_WINDOW = \"\"\n $script:config.MODEL_MAX_TOKENS = \"\"\n $script:config.MODEL_REASONING = \"\"\n $script:config.MODEL_VISION = \"\"\n return\n }\n Write-Host \"\"\n Write-Log (Get-Msg \"llm.custom_model.detected\" -f $Model)\n Write-Host \"\"\n $ctxInput = Read-Host \" $(Get-Msg 'llm.custom_model.context_prompt')\"\n if ($ctxInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $script:config.MODEL_CONTEXT_WINDOW = if ($ctxInput) { $ctxInput } else { \"150000\" }\n $maxInput = Read-Host \" $(Get-Msg 'llm.custom_model.max_tokens_prompt')\"\n if ($maxInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $script:config.MODEL_MAX_TOKENS = if ($maxInput) { $maxInput } else { \"128000\" }\n $reasoningInput = Read-Host \" $(Get-Msg 'llm.custom_model.reasoning_prompt')\"\n if ($reasoningInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $script:config.MODEL_REASONING = if ($reasoningInput -match \"^[nN]\") { \"false\" } else { \"true\" }\n $visionInput = Read-Host \" $(Get-Msg 'llm.custom_model.vision_prompt')\"\n if ($visionInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $script:config.MODEL_VISION = if ($visionInput -match \"^[yY]\") { \"true\" } else { \"false\" }\n Write-Log (Get-Msg \"llm.custom_model.summary\" -f $script:config.MODEL_CONTEXT_WINDOW, $script:config.MODEL_MAX_TOKENS, $script:config.MODEL_REASONING, $script:config.MODEL_VISION)\n}\n\nfunction New-RandomKey {\n # Generate 64 character hex string (32 bytes)\n $bytes = New-Object byte[] 32\n $rng = [System.Security.Cryptography.RandomNumberGenerator]::Create()\n $rng.GetBytes($bytes)\n return [BitConverter]::ToString($bytes).Replace(\"-\", \"\").ToLower()\n}\n\nfunction ConvertTo-DockerPath {\n param([string]$Path)\n\n # Convert Windows path to Docker mount format\n $fullPath = (Resolve-Path $Path -ErrorAction SilentlyContinue).Path\n if (-not $fullPath) {\n $fullPath = $Path\n }\n\n # Convert C:\\path to /c/path format for Docker\n if ($fullPath -match \"^([A-Za-z]):\") {\n $drive = $Matches[1].ToLower()\n $rest = $fullPath.Substring(2).Replace(\"\\\", \"/\")\n return \"/$drive$rest\"\n }\n return $fullPath.Replace(\"\\\", \"/\")\n}\n\n# Resolve the embedded controller image. Embedded mode is the only supported\n# architecture since PR #616 (manager image no longer bundles Higress/Tuwunel/MinIO).\n# Mirrors install/agentteams-install.sh::resolve_embedded_image — fail fast when the\n# embedded image is unavailable rather than silently falling back to the broken\n# legacy single-container path.\n# Sets $script:EMBEDDED_IMAGE and $script:AGENTTEAMS_USE_EMBEDDED.\nfunction Resolve-EmbeddedImage {\n $script:AGENTTEAMS_USE_EMBEDDED = \"1\"\n\n # Explicit override always wins (used by `make install-embedded` for local builds).\n if ($env:AGENTTEAMS_INSTALL_EMBEDDED_IMAGE) {\n $script:EMBEDDED_IMAGE = $env:AGENTTEAMS_INSTALL_EMBEDDED_IMAGE\n return\n }\n\n $versioned = \"$($script:AGENTTEAMS_REGISTRY)/agentteams/agentteams-embedded:$($script:AGENTTEAMS_VERSION)\"\n $latestTag = \"$($script:AGENTTEAMS_REGISTRY)/agentteams/agentteams-embedded:latest\"\n\n if ($script:AGENTTEAMS_VERSION -eq \"latest\") {\n $script:EMBEDDED_IMAGE = $latestTag\n return\n }\n\n docker pull $versioned *>$null\n if ($LASTEXITCODE -eq 0) {\n $script:EMBEDDED_IMAGE = $versioned\n return\n }\n docker pull $latestTag *>$null\n if ($LASTEXITCODE -eq 0) {\n Write-Log \"embedded $($script:AGENTTEAMS_VERSION) not found, using latest\"\n $script:EMBEDDED_IMAGE = $latestTag\n return\n }\n\n # Escape hatch for older versions (AGENTTEAMS_VERSION <= v1.0.9) whose manager image\n # still bundled the infrastructure — opt-in only, never silent.\n if ($env:AGENTTEAMS_FORCE_LEGACY -eq \"1\") {\n Write-Log \"WARNING: AGENTTEAMS_FORCE_LEGACY=1 - using legacy all-in-one manager architecture.\"\n Write-Log \"WARNING: This requires AGENTTEAMS_VERSION <= v1.0.9 (older bundled manager image).\"\n Write-Log \"WARNING: Newer slim manager images will hang on 'Waiting for Higress Gateway'.\"\n $script:AGENTTEAMS_USE_EMBEDDED = \"0\"\n return\n }\n\n Write-Host \"$($script:ESC)[31m[AgentTeams ERROR]$($script:ESC)[0m Embedded controller image is not available in the registry:\" -ForegroundColor Red\n Write-Host \" - tried: $versioned\" -ForegroundColor Red\n Write-Host \" - tried: $latestTag\" -ForegroundColor Red\n Write-Host \"\" -ForegroundColor Red\n Write-Host \"Embedded mode is the only supported architecture since PR #616.\" -ForegroundColor Red\n Write-Host \"How to resolve:\" -ForegroundColor Red\n Write-Host \" 1) Pin to a AGENTTEAMS_VERSION whose embedded image has been published, or\" -ForegroundColor Red\n Write-Host \" wait for the release pipeline to publish it.\" -ForegroundColor Red\n Write-Host \" 2) For a local build, run: make install-embedded\" -ForegroundColor Red\n Write-Host \" (builds and uses the local embedded image without touching the registry).\" -ForegroundColor Red\n Write-Host \" 3) Override with a custom image:\" -ForegroundColor Red\n Write-Host \" `$env:AGENTTEAMS_INSTALL_EMBEDDED_IMAGE=...; .\\agentteams-install.ps1\" -ForegroundColor Red\n Exit-Script 1\n}\n\nfunction Wait-ManagerReady {\n param(\n [string]$Container = \"agentteams-manager\",\n [int]$Timeout = $(if ($env:AGENTTEAMS_READY_TIMEOUT) { [int]$env:AGENTTEAMS_READY_TIMEOUT } else { 300 })\n )\n\n $elapsed = 0\n Write-Log (Get-Msg \"install.wait_ready\" -f $Timeout)\n\n $runtime = if ($script:config.MANAGER_RUNTIME) { $script:config.MANAGER_RUNTIME } else { \"copaw\" }\n\n while ($elapsed -lt $Timeout) {\n try {\n switch ($runtime) {\n \"copaw\" {\n $result = docker exec $Container curl -sf http://127.0.0.1:18799/api/agents 2>$null\n if ($result -match '\"agents\"') {\n Write-Log (Get-Msg \"install.wait_ready.ok\")\n return $true\n }\n }\n default {\n $result = docker exec $Container openclaw gateway health --json 2>$null\n if ($result -match '\"ok\"') {\n Write-Log (Get-Msg \"install.wait_ready.ok\")\n return $true\n }\n }\n }\n } catch {\n # Ignore errors during polling\n }\n\n Start-Sleep -Seconds 5\n $elapsed += 5\n Write-Host \"`r$($script:ESC)[36m[AgentTeams]$($script:ESC)[0m $(Get-Msg 'install.wait_ready.waiting' -f $elapsed, $Timeout)\" -NoNewline\n }\n\n Write-Host \"\"\n Write-Error (Get-Msg \"install.wait_ready.timeout\" -f $Timeout, $Container)\n}\n\nfunction Wait-MatrixReady {\n param(\n [string]$Container = \"agentteams-manager\",\n [int]$Timeout = $(if ($env:AGENTTEAMS_READY_TIMEOUT) { [int]$env:AGENTTEAMS_READY_TIMEOUT } else { 300 })\n )\n\n $elapsed = 0\n Write-Log (Get-Msg \"install.wait_matrix\" -f $Timeout)\n\n while ($elapsed -lt $Timeout) {\n try {\n $result = docker exec $Container curl -sf http://127.0.0.1:6167/_tuwunel/server_version 2>$null\n if ($result) {\n Write-Log (Get-Msg \"install.wait_matrix.ok\")\n return $true\n }\n } catch {\n # Ignore errors during polling\n }\n\n Start-Sleep -Seconds 5\n $elapsed += 5\n Write-Host \"`r$($script:ESC)[36m[AgentTeams]$($script:ESC)[0m $(Get-Msg 'install.wait_matrix.waiting' -f $elapsed, $Timeout)\" -NoNewline\n }\n\n Write-Host \"\"\n Write-Error (Get-Msg \"install.wait_matrix.timeout\" -f $Timeout, $Container)\n}\n\n# Read KEY=value from /data/agentteams-secrets.env on a Docker volume (manager container not required).\n# Requires $script:EMBEDDED_IMAGE (set by Resolve-EmbeddedImage before Install-Manager uses this).\nfunction Read-AgentTeamsSecretFromDataVolume {\n param(\n [string]$VolumeName,\n [string]$Key\n )\n if ([string]::IsNullOrEmpty($VolumeName) -or [string]::IsNullOrEmpty($Key) -or [string]::IsNullOrEmpty($script:EMBEDDED_IMAGE)) {\n return \"\"\n }\n $grepKey = [regex]::Escape($Key)\n $shCmd = \"grep \"\"^${grepKey}=\"\" /data/agentteams-secrets.env 2>/dev/null | cut -d= -f2- | head -1 | tr -d '\\r'\"\n $out = docker run --rm --entrypoint sh -v \"${VolumeName}:/data:ro\" $script:EMBEDDED_IMAGE -c $shCmd 2>$null\n if ($null -eq $out) { return \"\" }\n return ($out | Out-String).Trim()\n}\n\n# Read KEY=value from /data/worker-creds/<worker>.env on a Docker volume.\nfunction Read-AgentTeamsWorkerCredsFromVolume {\n param(\n [string]$VolumeName,\n [string]$WorkerName,\n [string]$Key\n )\n if ([string]::IsNullOrEmpty($VolumeName) -or [string]::IsNullOrEmpty($WorkerName) -or [string]::IsNullOrEmpty($Key) -or [string]::IsNullOrEmpty($script:EMBEDDED_IMAGE)) {\n return \"\"\n }\n $grepKey = [regex]::Escape($Key)\n $path = \"/data/worker-creds/${WorkerName}.env\"\n $shCmd = \"grep \"\"^${grepKey}=\"\" \"\"${path}\"\" 2>/dev/null | cut -d= -f2- | head -1 | tr -d '\\r'\"\n $out = docker run --rm --entrypoint sh -v \"${VolumeName}:/data:ro\" $script:EMBEDDED_IMAGE -c $shCmd 2>$null\n if ($null -eq $out) { return \"\" }\n return ($out | Out-String).Trim()\n}\n\n# Read admin_dm_room_id from host workspace state.json (fallback when Matrix API is unavailable).\nfunction Read-AgentTeamsAdminDmRoomFromWorkspace {\n param([string]$WorkspaceDir)\n if ([string]::IsNullOrEmpty($WorkspaceDir)) { return \"\" }\n $statePath = Join-Path $WorkspaceDir \"state.json\"\n if (-not (Test-Path $statePath)) { return \"\" }\n try {\n $j = Get-Content $statePath -Raw -ErrorAction Stop | ConvertFrom-Json\n $rid = $j.admin_dm_room_id\n if ($null -eq $rid) { return \"\" }\n $s = [string]$rid\n if ($s -eq \"\" -or $s -eq \"null\") { return \"\" }\n return $s.Trim()\n } catch {\n return \"\"\n }\n}\n\nfunction Get-AgentTeamsRandomHex {\n param([int]$ByteCount)\n $bytes = New-Object byte[] $ByteCount\n $rng = [System.Security.Cryptography.RandomNumberGenerator]::Create()\n $rng.GetBytes($bytes)\n return ([BitConverter]::ToString($bytes)).Replace(\"-\", \"\").ToLower()\n}\n\n# Resolve admin DM room with @manager (small room) via Matrix Client API inside agentteams-manager.\nfunction Get-AgentTeamsAdminDmRoomViaMatrix {\n param(\n [string]$AdminUser,\n [string]$AdminPassword\n )\n $running = docker ps --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-manager$\"\n if (-not $running) { return \"\" }\n if ([string]::IsNullOrEmpty($AdminPassword)) { return \"\" }\n\n $loginObj = @{\n type = \"m.login.password\"\n identifier = @{ type = \"m.id.user\"; user = $AdminUser }\n password = $AdminPassword\n }\n $loginJson = $loginObj | ConvertTo-Json -Compress -Depth 5\n try {\n $loginRaw = $loginJson | docker exec -i agentteams-manager sh -c \"curl -sf -X POST http://127.0.0.1:6167/_matrix/client/v3/login -H 'Content-Type: application/json' -d @-\" 2>$null\n if (-not $loginRaw) { return \"\" }\n $loginResp = $loginRaw | ConvertFrom-Json\n $token = [string]$loginResp.access_token\n if ([string]::IsNullOrEmpty($token)) { return \"\" }\n\n $roomsRaw = docker exec agentteams-manager curl -sf -X GET -H \"Authorization: Bearer $token\" `\n \"http://127.0.0.1:6167/_matrix/client/v3/joined_rooms\" 2>$null\n if (-not $roomsRaw) { return \"\" }\n $roomsObj = $roomsRaw | ConvertFrom-Json\n $roomList = @($roomsObj.joined_rooms)\n foreach ($roomId in $roomList) {\n if ([string]::IsNullOrEmpty($roomId)) { continue }\n $enc = $roomId.Replace(\"!\", \"%21\")\n $memRaw = docker exec agentteams-manager curl -sf -X GET -H \"Authorization: Bearer $token\" `\n \"http://127.0.0.1:6167/_matrix/client/v3/rooms/${enc}/members\" 2>$null\n if (-not $memRaw) { continue }\n $memObj = $memRaw | ConvertFrom-Json\n $chunk = @($memObj.chunk)\n $memberIds = @($chunk | ForEach-Object { $_.state_key })\n $match = $false\n foreach ($m in $memberIds) {\n $beforeColon = ($m -split \":\")[0]\n if ($m -like \"*manager*\" -and $beforeColon -notlike \"*admin*\") {\n $match = $true\n break\n }\n }\n if ($match -and $memberIds.Count -le 3) {\n return $roomId\n }\n }\n } catch {\n return \"\"\n }\n return \"\"\n}\n\nfunction New-EnvFile {\n param([hashtable]$Config, [string]$Path)\n\n $content = @\"\n# AgentTeams Manager Configuration\n# Generated by agentteams-install.ps1 on $(Get-Date -Format \"yyyy-MM-dd HH:mm:ss\")\n\n# Language\nAGENTTEAMS_LANGUAGE=$($Config.LANGUAGE)\n\n# LLM\nAGENTTEAMS_LLM_PROVIDER=$($Config.LLM_PROVIDER)\nAGENTTEAMS_DEFAULT_MODEL=$($Config.DEFAULT_MODEL)\nAGENTTEAMS_LLM_API_KEY=$($Config.LLM_API_KEY)\nAGENTTEAMS_OPENAI_BASE_URL=$($Config.OPENAI_BASE_URL)\nAGENTTEAMS_MODEL_CONTEXT_WINDOW=$($Config.MODEL_CONTEXT_WINDOW)\nAGENTTEAMS_MODEL_MAX_TOKENS=$($Config.MODEL_MAX_TOKENS)\nAGENTTEAMS_MODEL_REASONING=$($Config.MODEL_REASONING)\nAGENTTEAMS_MODEL_VISION=$($Config.MODEL_VISION)\n\n# Embedding model (empty = disabled, default: text-embedding-v4)\nAGENTTEAMS_EMBEDDING_MODEL=$($Config.EMBEDDING_MODEL)\n\n# Admin\nAGENTTEAMS_ADMIN_USER=$($Config.ADMIN_USER)\nAGENTTEAMS_ADMIN_PASSWORD=$($Config.ADMIN_PASSWORD)\n\n# Ports\nAGENTTEAMS_LOCAL_ONLY=$($Config.LOCAL_ONLY)\nAGENTTEAMS_PORT_GATEWAY=$($Config.PORT_GATEWAY)\nAGENTTEAMS_PORT_CONSOLE=$($Config.PORT_CONSOLE)\nAGENTTEAMS_PORT_ELEMENT_WEB=$($Config.PORT_ELEMENT_WEB)\nAGENTTEAMS_PORT_MANAGER_CONSOLE=$($Config.PORT_MANAGER_CONSOLE)\n\n# Matrix\nAGENTTEAMS_MATRIX_DOMAIN=$($Config.MATRIX_DOMAIN)\nAGENTTEAMS_MATRIX_CLIENT_DOMAIN=$($Config.MATRIX_CLIENT_DOMAIN)\n\n# Gateway\nAGENTTEAMS_AI_GATEWAY_DOMAIN=$($Config.AI_GATEWAY_DOMAIN)\nAGENTTEAMS_MANAGER_GATEWAY_KEY=$($Config.MANAGER_GATEWAY_KEY)\n\n# File System\nAGENTTEAMS_FS_DOMAIN=$($Config.FS_DOMAIN)\nAGENTTEAMS_CONSOLE_DOMAIN=$($Config.CONSOLE_DOMAIN)\nAGENTTEAMS_MINIO_USER=$($Config.MINIO_USER)\nAGENTTEAMS_MINIO_PASSWORD=$($Config.MINIO_PASSWORD)\n\n# Internal\nAGENTTEAMS_MANAGER_PASSWORD=$($Config.MANAGER_PASSWORD)\nAGENTTEAMS_REGISTRATION_TOKEN=$($Config.REGISTRATION_TOKEN)\n\n# GitHub (optional)\nAGENTTEAMS_GITHUB_TOKEN=$($Config.GITHUB_TOKEN)\n\n# Nacos package import defaults\nAGENTTEAMS_NACOS_REGISTRY_URI=$(if ($env:AGENTTEAMS_NACOS_REGISTRY_URI) { $env:AGENTTEAMS_NACOS_REGISTRY_URI } else { \"nacos://market.agentteams.io:80/public\" })\nAGENTTEAMS_NACOS_USERNAME=$($env:AGENTTEAMS_NACOS_USERNAME)\nAGENTTEAMS_NACOS_PASSWORD=$($env:AGENTTEAMS_NACOS_PASSWORD)\nAGENTTEAMS_NACOS_TOKEN=$($env:AGENTTEAMS_NACOS_TOKEN)\n\n# Skills Registry (optional, default: nacos://market.agentteams.io:80/public)\nAGENTTEAMS_SKILLS_API_URL=$(if ($Config.SKILLS_API_URL) { $Config.SKILLS_API_URL } else { \"nacos://market.agentteams.io:80/public\" })\n\n# OpenClaw CMS plugin (optional)\nAGENTTEAMS_CMS_TRACES_ENABLED=$(if ($env:AGENTTEAMS_CMS_TRACES_ENABLED) { $env:AGENTTEAMS_CMS_TRACES_ENABLED } else { \"false\" })\nAGENTTEAMS_CMS_ENDPOINT=$($env:AGENTTEAMS_CMS_ENDPOINT)\nAGENTTEAMS_CMS_LICENSE_KEY=$($env:AGENTTEAMS_CMS_LICENSE_KEY)\nAGENTTEAMS_CMS_PROJECT=$($env:AGENTTEAMS_CMS_PROJECT)\nAGENTTEAMS_CMS_WORKSPACE=$($env:AGENTTEAMS_CMS_WORKSPACE)\nAGENTTEAMS_CMS_SERVICE_NAME=$(if ($env:AGENTTEAMS_CMS_SERVICE_NAME) { $env:AGENTTEAMS_CMS_SERVICE_NAME } else { \"agentteams-manager\" })\nAGENTTEAMS_CMS_METRICS_ENABLED=$(if ($env:AGENTTEAMS_CMS_METRICS_ENABLED) { $env:AGENTTEAMS_CMS_METRICS_ENABLED } else { \"false\" })\n\n# Worker images (for direct container creation)\nAGENTTEAMS_WORKER_IMAGE=$($Config.WORKER_IMAGE)\nAGENTTEAMS_COPAW_WORKER_IMAGE=$($Config.COPAW_WORKER_IMAGE)\nAGENTTEAMS_QWENPAW_WORKER_IMAGE=$($Config.QWENPAW_WORKER_IMAGE)\nAGENTTEAMS_HERMES_WORKER_IMAGE=$($Config.HERMES_WORKER_IMAGE)\n\n# Manager runtime (openclaw | copaw)\nAGENTTEAMS_MANAGER_RUNTIME=$($Config.MANAGER_RUNTIME)\n\n# Default Worker runtime (openclaw | copaw | hermes)\nAGENTTEAMS_DEFAULT_WORKER_RUNTIME=$($Config.DEFAULT_WORKER_RUNTIME)\n\n# Matrix E2EE (0=disabled, 1=enabled; default: 0)\nAGENTTEAMS_MATRIX_E2EE=$($Config.MATRIX_E2EE)\n\n# Docker API proxy (0=disabled, 1=enabled; default: 1)\nAGENTTEAMS_DOCKER_PROXY=$($Config.DOCKER_PROXY)\n\n# Docker API proxy: additional allowed image sources (comma-separated)\nAGENTTEAMS_PROXY_ALLOWED_REGISTRIES=$($Config.PROXY_ALLOWED_REGISTRIES)\n\n# Worker idle timeout in minutes (default: 720 = 12 hours)\nAGENTTEAMS_WORKER_IDLE_TIMEOUT=$($Config.WORKER_IDLE_TIMEOUT)\n\n# JVM Args for Higress Console\nJVM_ARGS=$($env:JVM_ARGS)\n\n# Higress WASM plugin image registry (auto-selected by timezone)\nHIGRESS_ADMIN_WASM_PLUGIN_IMAGE_REGISTRY=$($Config.REGISTRY)\n\n# Data persistence\nAGENTTEAMS_DATA_DIR=$($Config.DATA_DIR)\n# Manager workspace (skills, memory, state - host-editable)\nAGENTTEAMS_WORKSPACE_DIR=$($Config.WORKSPACE_DIR)\n# Host directory sharing\nAGENTTEAMS_HOST_SHARE_DIR=$($Config.HOST_SHARE_DIR)\n\"@\n\n Set-Content -Path $Path -Value $content -Encoding UTF8\n Write-Log (Get-Msg \"install.config_saved\" -f $Path)\n}\n\n# ============================================================\n# Prompt Functions\n# ============================================================\n\nfunction Read-Prompt {\n param(\n [string]$VarName,\n [string]$PromptText,\n [string]$Default = \"\",\n [switch]$Secret,\n [switch]$Optional\n )\n\n # Check if already set in environment\n $envValue = [Environment]::GetEnvironmentVariable($VarName)\n if ($envValue) {\n if ($script:AGENTTEAMS_UPGRADE -and -not $script:AGENTTEAMS_NON_INTERACTIVE) {\n # Show current value (masked for secrets) and let user change it\n $displayValue = $envValue\n if ($Secret) {\n if ($envValue.Length -le 8) {\n $displayValue = \"****\"\n } else {\n $displayValue = $envValue.Substring(0, 4) + \"****\" + $envValue.Substring($envValue.Length - 4)\n }\n }\n Write-Log (Get-Msg \"prompt.upgrade_keep\" -f $PromptText, $displayValue)\n $prompt = $PromptText\n if ($Secret) {\n $newValue = Read-Host -Prompt $prompt -AsSecureString\n $newValue = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto(\n [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($newValue)\n )\n } else {\n $newValue = Read-Host -Prompt $prompt\n if ($newValue -eq \"b\") { $script:StepResult = \"back\"; return $null }\n }\n if ($newValue) {\n return $newValue\n }\n return $envValue\n }\n Write-Log (Get-Msg \"prompt.preset\" -f $PromptText)\n return $envValue\n }\n # Upgrade mode: optional fields with empty value — let user set a new value\n elseif ($Optional -and $script:AGENTTEAMS_UPGRADE -and -not $script:AGENTTEAMS_NON_INTERACTIVE) {\n Write-Log (Get-Msg \"prompt.upgrade_empty\" -f $PromptText)\n $prompt = $PromptText\n if ($Secret) {\n $newValue = Read-Host -Prompt $prompt -AsSecureString\n $newValue = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto(\n [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($newValue)\n )\n } else {\n $newValue = Read-Host -Prompt $prompt\n if ($newValue -eq \"b\") { $script:StepResult = \"back\"; return $null }\n }\n if ($newValue) {\n return $newValue\n }\n return \"\"\n }\n\n # Non-interactive or quickstart mode\n if ($script:AGENTTEAMS_NON_INTERACTIVE -or $script:AGENTTEAMS_QUICKSTART) {\n if ($Default) {\n Write-Log (Get-Msg \"prompt.default\" -f $PromptText, $Default)\n return $Default\n }\n elseif ($Optional) {\n return \"\"\n }\n elseif ($script:AGENTTEAMS_NON_INTERACTIVE) {\n # Only hard-error in fully non-interactive mode, not quickstart\n Write-Error (Get-Msg \"prompt.required\" -f $PromptText)\n }\n # quickstart + no default + not optional: fall through to interactive prompt\n }\n\n # Interactive prompt\n $prompt = if ($Default) { \"$PromptText [$Default]\" } else { $PromptText }\n\n if ($Secret) {\n $value = Read-Host -Prompt $prompt -AsSecureString\n $value = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto(\n [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($value)\n )\n }\n else {\n $value = Read-Host -Prompt $prompt\n if ($value -eq \"b\") { $script:StepResult = \"back\"; return $null }\n }\n\n if (-not $value -and $Default) {\n $value = $Default\n }\n\n if (-not $value -and -not $Optional) {\n Write-Error (Get-Msg \"prompt.required_empty\" -f $PromptText)\n }\n\n return $value\n}\n\n# Load current parameter values from env file for upgrade mode display\nfunction Load-CurrentParamsFromEnv {\n $envFile = $script:AGENTTEAMS_ENV_FILE\n if (Test-Path $envFile) {\n $content = Get-Content $envFile\n $content | ForEach-Object {\n if ($_ -match \"^AGENTTEAMS_LLM_PROVIDER=(.*)$\") {\n $script:config.LLM_PROVIDER = $Matches[1].Trim()\n }\n if ($_ -match \"^AGENTTEAMS_OPENAI_BASE_URL=(.*)$\") {\n $script:config.OPENAI_BASE_URL = $Matches[1].Trim()\n }\n if ($_ -match \"^AGENTTEAMS_DEFAULT_MODEL=(.*)$\") {\n $script:config.DEFAULT_MODEL = $Matches[1].Trim()\n }\n if ($_ -match \"^AGENTTEAMS_EMBEDDING_MODEL=(.*)$\") {\n $script:config.EMBEDDING_MODEL = $Matches[1].Trim()\n }\n if ($_ -match \"^AGENTTEAMS_WORKSPACE_DIR=(.*)$\") {\n $script:config.WORKSPACE_DIR = $Matches[1].Trim()\n }\n if ($_ -match \"^AGENTTEAMS_HOST_SHARE_DIR=(.*)$\") {\n $script:config.HOST_SHARE_DIR = $Matches[1].Trim()\n }\n }\n }\n}\n\n# ============================================================\n# OpenAI-Compatible Provider\n# ============================================================\n\nfunction Test-LlmConnectivity {\n param(\n [string]$BaseUrl,\n [string]$ApiKey,\n [string]$Model,\n [string]$Hint = \"\"\n )\n Write-Log (Get-Msg \"llm.openai.test.testing\")\n $uri = ($BaseUrl.TrimEnd('/')) + \"/chat/completions\"\n $bodyHash = @{\n model = $Model\n messages = @(@{ role = \"user\"; content = \"hi\" })\n }\n $body = $bodyHash | ConvertTo-Json -Compress\n try {\n $response = Invoke-WebRequest -Uri $uri -Method POST `\n -Headers @{ \"Authorization\" = \"Bearer $ApiKey\"; \"Content-Type\" = \"application/json\"; \"User-Agent\" = \"AgentTeams/$($script:AGENTTEAMS_VERSION)\" } `\n -Body $body -TimeoutSec 30 -ErrorAction Stop -UseBasicParsing\n Write-Log (Get-Msg \"llm.openai.test.ok\")\n } catch {\n $statusCode = if ($_.Exception.Response) { [int]$_.Exception.Response.StatusCode } else { 0 }\n $responseBody = \"\"\n if ($_.Exception.Response) {\n try {\n $stream = $_.Exception.Response.GetResponseStream()\n $reader = New-Object System.IO.StreamReader($stream)\n $responseBody = $reader.ReadToEnd()\n } catch {}\n }\n Write-Host (Get-Msg \"llm.openai.test.fail\" -f $statusCode, $responseBody) -ForegroundColor Yellow\n if ($Hint) {\n Write-Host $Hint -ForegroundColor Yellow\n }\n if (-not $script:AGENTTEAMS_NON_INTERACTIVE) {\n $confirm = Read-Host (Get-Msg \"llm.openai.test.confirm\")\n if ($confirm -eq \"b\" -or $confirm -eq \"B\") {\n $script:StepResult = \"back\"\n return\n }\n if ($confirm -ne \"y\" -and $confirm -ne \"Y\") {\n Write-Log (Get-Msg \"llm.openai.test.aborted\")\n Exit-Script 1\n }\n }\n }\n}\n\nfunction Test-EmbeddingConnectivity {\n param(\n [string]$BaseUrl,\n [string]$ApiKey,\n [string]$Model\n )\n Write-Log (Get-Msg \"llm.embedding.test.testing\")\n $uri = ($BaseUrl.TrimEnd('/')) + \"/embeddings\"\n $bodyHash = @{\n model = $Model\n input = \"test\"\n }\n $body = $bodyHash | ConvertTo-Json -Compress\n try {\n $response = Invoke-WebRequest -Uri $uri -Method POST `\n -Headers @{ \"Authorization\" = \"Bearer $ApiKey\"; \"Content-Type\" = \"application/json\"; \"User-Agent\" = \"AgentTeams/$($script:AGENTTEAMS_VERSION)\" } `\n -Body $body -TimeoutSec 30 -ErrorAction Stop -UseBasicParsing\n Write-Log (Get-Msg \"llm.embedding.test.ok\")\n return $true\n } catch {\n $statusCode = if ($_.Exception.Response) { [int]$_.Exception.Response.StatusCode } else { 0 }\n $responseBody = \"\"\n if ($_.Exception.Response) {\n try {\n $stream = $_.Exception.Response.GetResponseStream()\n $reader = New-Object System.IO.StreamReader($stream)\n $responseBody = $reader.ReadToEnd()\n } catch {}\n }\n Write-Host (Get-Msg \"llm.embedding.test.fail\" -f $statusCode, $responseBody) -ForegroundColor Yellow\n return $false\n }\n}\n\nfunction New-OpenAICompatProvider {\n param(\n [string]$BaseUrl,\n [string]$ApiKey,\n [int]$ConsolePort = 18001\n )\n\n if (-not $BaseUrl -or -not $ApiKey) {\n Write-Log (Get-Msg \"install.openai_compat.missing\")\n return $false\n }\n\n $consoleUrl = \"http://localhost:$ConsolePort\"\n\n # Parse base URL\n $protocol = \"https\"\n $port = 443\n $urlWithoutProto = $BaseUrl -replace \"^https?://\", \"\"\n\n if ($BaseUrl -match \"^http://\") {\n $protocol = \"http\"\n $port = 80\n }\n\n $domain = $urlWithoutProto.Split(\"/\")[0]\n\n if ($domain -match \":(\\d+)$\") {\n $port = [int]$Matches[1]\n $domain = $domain -replace \":\\d+$\", \"\"\n }\n\n Write-Log (Get-Msg \"install.openai_compat.creating\")\n Write-Log (Get-Msg \"install.openai_compat.domain\" -f $domain)\n Write-Log (Get-Msg \"install.openai_compat.port\" -f $port)\n Write-Log (Get-Msg \"install.openai_compat.protocol\" -f $protocol)\n\n $serviceName = \"openai-compat\"\n\n # Create DNS service source\n $serviceBody = @{\n type = \"dns\"\n name = $serviceName\n port = $port.ToString()\n protocol = $protocol\n proxyName = \"\"\n domain = $domain\n } | ConvertTo-Json -Compress\n\n try {\n Invoke-RestMethod -Uri \"$consoleUrl/v1/service-sources\" -Method POST -ContentType \"application/json\" -Body $serviceBody -ErrorAction SilentlyContinue | Out-Null\n }\n catch {\n Write-Log (Get-Msg \"install.openai_compat.service_fail\")\n }\n\n Start-Sleep -Seconds 2\n\n # Create AI provider\n $providerBody = @{\n type = \"openai\"\n name = \"openai-compat\"\n tokens = @($ApiKey)\n version = 0\n protocol = \"openai/v1\"\n tokenFailoverConfig = @{ enabled = $false }\n rawConfigs = @{\n openaiCustomUrl = $BaseUrl\n openaiCustomServiceName = \"$serviceName.dns\"\n openaiCustomServicePort = $port\n }\n } | ConvertTo-Json -Compress -Depth 3\n\n try {\n Invoke-RestMethod -Uri \"$consoleUrl/v1/ai/providers\" -Method POST -ContentType \"application/json\" -Body $providerBody -ErrorAction SilentlyContinue | Out-Null\n Write-Log (Get-Msg \"install.openai_compat.success\")\n return $true\n }\n catch {\n Write-Log (Get-Msg \"install.openai_compat.provider_fail\")\n return $false\n }\n}\n\n# ============================================================\n# State Machine Helpers\n# ============================================================\n\nfunction Test-ShouldSkipStep {\n param([string]$StepFn)\n switch ($StepFn) {\n { $_ -in @(\"Step-Lang\", \"Step-Mode\") } {\n return $script:AGENTTEAMS_NON_INTERACTIVE\n }\n \"Step-Existing\" {\n return (-not (Test-Path $script:AGENTTEAMS_ENV_FILE))\n }\n # Keep-All upgrade mode: skip all config steps, values are already loaded\n { $_ -in @(\"Step-Llm\", \"Step-Admin\", \"Step-Network\", \"Step-Ports\", \"Step-Domains\",\n \"Step-Github\", \"Step-Skills\",\n \"Step-Runtime\", \"Step-ManagerRuntime\", \"Step-E2ee\", \"Step-DockerProxy\",\n \"Step-Idle\", \"Step-Hostshare\") } {\n if ($script:AGENTTEAMS_UPGRADE -and $env:AGENTTEAMS_UPGRADE_KEEP_ALL -eq \"1\") { return $true }\n return $false\n }\n { $_ -in @(\"Step-Volume\", \"Step-Workspace\") } {\n if ($script:AGENTTEAMS_UPGRADE -and $env:AGENTTEAMS_UPGRADE_KEEP_ALL -eq \"1\") { return $true }\n if ($script:AGENTTEAMS_NON_INTERACTIVE) { return $true }\n if ($script:AGENTTEAMS_QUICKSTART) { return $true }\n return $false\n }\n { $_ -in @(\"Step-E2ee\", \"Step-Idle\") } {\n if ($script:AGENTTEAMS_NON_INTERACTIVE) { return $true }\n if ($script:AGENTTEAMS_QUICKSTART -and -not $script:AGENTTEAMS_UPGRADE) { return $true }\n return $false\n }\n \"Step-DockerProxy\" {\n # Embedded controller IS the proxy/orchestrator — no separate proxy step needed.\n if ($script:AGENTTEAMS_USE_EMBEDDED -eq \"1\") { return $true }\n if ($script:AGENTTEAMS_NON_INTERACTIVE) { return $true }\n if ($script:AGENTTEAMS_QUICKSTART -and -not $script:AGENTTEAMS_UPGRADE) { return $true }\n return $false\n }\n \"Step-ManagerRuntime\" {\n if ($script:AGENTTEAMS_NON_INTERACTIVE) { return $true }\n return $false\n }\n \"Step-Hostshare\" {\n if ($script:AGENTTEAMS_NON_INTERACTIVE) { return $true }\n if ($script:AGENTTEAMS_QUICKSTART) { return $true }\n return $false\n }\n default { return $false }\n }\n}\n\nfunction Clear-StepVars {\n param([string]$StepFn)\n switch ($StepFn) {\n \"Step-Mode\" { $script:AGENTTEAMS_QUICKSTART = $false }\n \"Step-Existing\" {\n $script:AGENTTEAMS_UPGRADE = $false\n $script:UPGRADE_EXISTING_WORKERS = $null\n }\n \"Step-Llm\" {\n foreach ($k in @(\"AGENTTEAMS_LLM_PROVIDER\",\"AGENTTEAMS_DEFAULT_MODEL\",\"AGENTTEAMS_OPENAI_BASE_URL\",\n \"AGENTTEAMS_LLM_API_KEY\",\"AGENTTEAMS_MODEL_CONTEXT_WINDOW\",\"AGENTTEAMS_MODEL_MAX_TOKENS\",\n \"AGENTTEAMS_MODEL_REASONING\",\"AGENTTEAMS_MODEL_VISION\")) {\n [Environment]::SetEnvironmentVariable($k, $null, \"Process\")\n }\n }\n \"Step-Admin\" {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_ADMIN_USER\", $null, \"Process\")\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_ADMIN_PASSWORD\", $null, \"Process\")\n }\n \"Step-Network\" {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_LOCAL_ONLY\", $null, \"Process\")\n }\n \"Step-Ports\" {\n foreach ($k in @(\"AGENTTEAMS_PORT_GATEWAY\",\"AGENTTEAMS_PORT_CONSOLE\",\"AGENTTEAMS_PORT_ELEMENT_WEB\",\"AGENTTEAMS_PORT_MANAGER_CONSOLE\")) {\n [Environment]::SetEnvironmentVariable($k, $null, \"Process\")\n }\n }\n \"Step-Domains\" {\n foreach ($k in @(\"AGENTTEAMS_MATRIX_DOMAIN\",\"AGENTTEAMS_MATRIX_CLIENT_DOMAIN\",\"AGENTTEAMS_AI_GATEWAY_DOMAIN\",\"AGENTTEAMS_FS_DOMAIN\",\"AGENTTEAMS_CONSOLE_DOMAIN\")) {\n [Environment]::SetEnvironmentVariable($k, $null, \"Process\")\n }\n }\n \"Step-Github\" {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_GITHUB_TOKEN\", $null, \"Process\")\n }\n \"Step-Skills\" {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_SKILLS_API_URL\", $null, \"Process\")\n }\n \"Step-Volume\" {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_DATA_DIR\", $null, \"Process\")\n }\n \"Step-Workspace\" {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_WORKSPACE_DIR\", $null, \"Process\")\n }\n \"Step-Runtime\" {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_DEFAULT_WORKER_RUNTIME\", $null, \"Process\")\n }\n \"Step-ManagerRuntime\" {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_MANAGER_RUNTIME\", $null, \"Process\")\n }\n \"Step-E2ee\" {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_MATRIX_E2EE\", $null, \"Process\")\n }\n \"Step-DockerProxy\" {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_DOCKER_PROXY\", $null, \"Process\")\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_PROXY_ALLOWED_REGISTRIES\", $null, \"Process\")\n }\n \"Step-Idle\" {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_WORKER_IDLE_TIMEOUT\", $null, \"Process\")\n }\n \"Step-Hostshare\" {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_HOST_SHARE_DIR\", $null, \"Process\")\n }\n }\n}\n\n# ============================================================\n# Individual step functions\n# ============================================================\n\nfunction Step-Lang {\n $langDefaultChoice = if ($script:AGENTTEAMS_LANGUAGE -eq \"zh\") { \"1\" } else { \"2\" }\n $langDetectedKey = \"lang.detected.$($script:AGENTTEAMS_LANGUAGE)\"\n Write-Log (Get-Msg $langDetectedKey)\n Write-Log (Get-Msg \"lang.switch_title\")\n Write-Host (Get-Msg \"lang.option_zh\")\n Write-Host (Get-Msg \"lang.option_en\")\n Write-Host \"\"\n $langChoice = Read-Host \"$(Get-Msg 'lang.prompt') [$langDefaultChoice]\"\n if (-not $langChoice) { $langChoice = $langDefaultChoice }\n if ($langChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n switch ($langChoice) {\n \"1\" { $script:AGENTTEAMS_LANGUAGE = \"zh\" }\n \"2\" { $script:AGENTTEAMS_LANGUAGE = \"en\" }\n }\n $env:AGENTTEAMS_LANGUAGE = $script:AGENTTEAMS_LANGUAGE\n Write-Log \"\"\n}\n\nfunction Step-Mode {\n Write-Log (Get-Msg \"install.mode.title\")\n Write-Host \"\"\n Write-Host (Get-Msg \"install.mode.choose\")\n Write-Host (Get-Msg \"install.mode.quickstart\")\n Write-Host (Get-Msg \"install.mode.manual\")\n Write-Host \"\"\n $choice = Read-Host (Get-Msg \"install.mode.prompt\")\n $choice = if ($choice) { $choice } else { \"1\" }\n if ($choice -eq \"b\") { $script:StepResult = \"back\"; return }\n switch -Regex ($choice) {\n \"^(1|quick|quickstart)$\" {\n Write-Log (Get-Msg \"install.mode.quickstart_selected\")\n $script:AGENTTEAMS_QUICKSTART = $true\n }\n \"^(2|manual)$\" {\n Write-Log (Get-Msg \"install.mode.manual_selected\")\n $script:AGENTTEAMS_QUICKSTART = $false\n }\n default {\n Write-Log (Get-Msg \"install.mode.invalid\")\n $script:AGENTTEAMS_QUICKSTART = $true\n }\n }\n Write-Log \"\"\n}\n\nfunction Step-Existing {\n # This step is skipped when env file doesn't exist\n Write-Log (Get-Msg \"install.existing.detected\" -f $script:AGENTTEAMS_ENV_FILE)\n\n $runningManager = docker ps --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-manager$\"\n $runningWorkers = docker ps --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-worker-\"\n $existingWorkers = docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-worker-\"\n\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n Write-Log (Get-Msg \"install.existing.upgrade_noninteractive\")\n $upgradeChoice = \"1\"\n } else {\n Write-Host \"\"\n Write-Host (Get-Msg \"install.existing.choose\")\n Write-Host (Get-Msg \"install.existing.upgrade\")\n Write-Host (Get-Msg \"install.existing.reinstall\")\n Write-Host (Get-Msg \"install.existing.cancel\")\n Write-Host \"\"\n $upgradeChoice = Read-Host (Get-Msg \"install.existing.prompt\")\n $upgradeChoice = if ($upgradeChoice) { $upgradeChoice } else { \"1\" }\n if ($upgradeChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n }\n\n switch -Regex ($upgradeChoice) {\n \"^(1|upgrade)$\" {\n $script:AGENTTEAMS_UPGRADE = $true\n Write-Log (Get-Msg \"install.existing.upgrading\")\n\n # Show upgrade mode sub-menu (unless already set via env var)\n if ($env:AGENTTEAMS_UPGRADE_KEEP_ALL -ne \"1\") {\n Write-Host \"\"\n Write-Host (Get-Msg \"upgrade.mode.prompt\")\n Write-Host (Get-Msg \"upgrade.mode.keep_all\")\n Write-Host (Get-Msg \"upgrade.mode.confirm_each\")\n Write-Host (Get-Msg \"upgrade.mode.back\")\n Write-Host \"\"\n $upgradeModeChoice = Read-Host (Get-Msg \"install.existing.prompt\")\n $upgradeModeChoice = if ($upgradeModeChoice) { $upgradeModeChoice } else { \"1\" }\n switch -Regex ($upgradeModeChoice) {\n \"^(1|keep)$\" {\n $env:AGENTTEAMS_UPGRADE_KEEP_ALL = \"1\"\n }\n \"^(2|confirm)$\" {\n $env:AGENTTEAMS_UPGRADE_KEEP_ALL = \"0\"\n }\n \"^(3|b)$\" {\n $script:StepResult = \"back\"\n return\n }\n default {\n $env:AGENTTEAMS_UPGRADE_KEEP_ALL = \"0\"\n }\n }\n }\n # Load current parameters for both Keep-All and confirm-each modes\n Load-CurrentParamsFromEnv\n\n if ($runningManager -or $runningWorkers) {\n Write-Host \"\"\n Write-Host \"$($script:ESC)[33m$(Get-Msg 'install.existing.warn_manager_stop')$($script:ESC)[0m\"\n if ($existingWorkers) {\n Write-Host \"$($script:ESC)[33m$(Get-Msg 'install.existing.warn_worker_recreate')$($script:ESC)[0m\"\n }\n if (-not $script:AGENTTEAMS_NON_INTERACTIVE) {\n $confirm = Read-Host (Get-Msg \"install.existing.continue_prompt\")\n if ($confirm -eq \"b\" -or $confirm -eq \"B\") { $script:StepResult = \"back\"; return }\n if ($confirm -ne \"y\" -and $confirm -ne \"Y\") {\n Write-Log (Get-Msg \"install.existing.cancelled\")\n exit 0\n }\n }\n }\n $script:UPGRADE_EXISTING_WORKERS = $existingWorkers\n }\n \"^(2|reinstall)$\" {\n Write-Log (Get-Msg \"install.reinstall.performing\")\n $existingWorkspace = \"$env:USERPROFILE\\agentteams-manager\"\n if (Test-Path $script:AGENTTEAMS_ENV_FILE) {\n $envContent = Get-Content $script:AGENTTEAMS_ENV_FILE\n $wsLine = $envContent | Select-String \"^AGENTTEAMS_WORKSPACE_DIR=\"\n if ($wsLine) {\n $existingWorkspace = $wsLine.Line.Substring(21)\n }\n }\n Write-Host \"\"\n Write-Host \"$($script:ESC)[33m$(Get-Msg 'install.reinstall.warn_stop')$($script:ESC)[0m\"\n if ($runningManager) { Write-Host \"$($script:ESC)[33m - agentteams-manager (manager)$($script:ESC)[0m\" }\n $runningWorkers | ForEach-Object { Write-Host \"$($script:ESC)[33m - $_ (worker)$($script:ESC)[0m\" }\n Write-Host \"\"\n Write-Host \"$($script:ESC)[31m$(Get-Msg 'install.reinstall.warn_delete')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[31m$(Get-Msg 'install.reinstall.warn_volume')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[31m$(Get-Msg 'install.reinstall.warn_env' -f $script:AGENTTEAMS_ENV_FILE)$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[31m$(Get-Msg 'install.reinstall.warn_workspace' -f $existingWorkspace)$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[31m$(Get-Msg 'install.reinstall.warn_workers')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[31m$(Get-Msg 'install.reinstall.warn_proxy')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[31m$(Get-Msg 'install.reinstall.warn_network')$($script:ESC)[0m\"\n Write-Host \"\"\n Write-Host \"$($script:ESC)[31m$(Get-Msg 'install.reinstall.confirm_type')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[31m $existingWorkspace$($script:ESC)[0m\"\n Write-Host \"\"\n $confirmPath = Read-Host (Get-Msg \"install.reinstall.confirm_path\")\n if ($confirmPath -ne $existingWorkspace) {\n Write-Error (Get-Msg \"install.reinstall.path_mismatch\" -f $confirmPath, $existingWorkspace)\n }\n Write-Log (Get-Msg \"install.reinstall.confirmed\")\n docker stop agentteams-manager *>$null\n docker rm agentteams-manager *>$null\n docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-worker-\" | ForEach-Object {\n docker stop $_ *>$null\n docker rm $_ *>$null\n Write-Log (Get-Msg \"install.reinstall.removed_worker\" -f $_)\n }\n $existingController = docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-controller$\"\n if ($existingController) {\n Write-Log (Get-Msg \"install.reinstall.removing_proxy\")\n docker stop agentteams-controller *>$null\n docker rm agentteams-controller *>$null\n }\n if (docker volume ls -q 2>$null | Select-String \"^agentteams-data$\") {\n Write-Log (Get-Msg \"install.reinstall.removing_volume\")\n docker volume rm agentteams-data *>$null\n }\n if (Test-Path $existingWorkspace) {\n Write-Log (Get-Msg \"install.reinstall.removing_workspace\" -f $existingWorkspace)\n Remove-Item -Recurse -Force $existingWorkspace\n }\n if (Test-Path $script:AGENTTEAMS_ENV_FILE) {\n Write-Log (Get-Msg \"install.reinstall.removing_env\" -f $script:AGENTTEAMS_ENV_FILE)\n Remove-Item -Force $script:AGENTTEAMS_ENV_FILE\n }\n $existingNetwork = docker network ls --format \"{{.Name}}\" 2>$null | Select-String \"^agentteams-net$\"\n if ($existingNetwork) {\n Write-Log (Get-Msg \"install.reinstall.removing_network\")\n docker network rm agentteams-net *>$null\n }\n Write-Log (Get-Msg \"install.reinstall.cleanup_done\")\n }\n default {\n Write-Log (Get-Msg \"install.existing.cancelled\")\n exit 0\n }\n }\n\n # Load existing env file (upgrade path)\n if (Test-Path $script:AGENTTEAMS_ENV_FILE) {\n Write-Log (Get-Msg \"install.loading_config\" -f $script:AGENTTEAMS_ENV_FILE)\n Get-Content $script:AGENTTEAMS_ENV_FILE | ForEach-Object {\n if ($_ -match \"^([^#=][^=]*)=(.*)$\") {\n $key = $Matches[1].Trim()\n $value = $Matches[2].Split(\"#\")[0].Trim()\n if (-not [Environment]::GetEnvironmentVariable($key)) {\n [Environment]::SetEnvironmentVariable($key, $value, \"Process\")\n }\n }\n }\n }\n}\n\nfunction Step-Llm {\n Write-Log (Get-Msg \"llm.title\")\n\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n if ($script:AGENTTEAMS_LANGUAGE -eq \"zh\") {\n $script:config.LLM_PROVIDER = if ($env:AGENTTEAMS_LLM_PROVIDER) { $env:AGENTTEAMS_LLM_PROVIDER } else { \"openai-compat\" }\n $script:config.DEFAULT_MODEL = if ($env:AGENTTEAMS_DEFAULT_MODEL) { $env:AGENTTEAMS_DEFAULT_MODEL } else { \"qwen3.6-plus\" }\n $script:config.OPENAI_BASE_URL = if ($env:AGENTTEAMS_OPENAI_BASE_URL) { $env:AGENTTEAMS_OPENAI_BASE_URL } else { \"https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1\" }\n Write-Log (Get-Msg \"llm.provider.label\" -f $script:config.LLM_PROVIDER)\n Write-Log (Get-Msg \"llm.openai.base_url_label\" -f $script:config.OPENAI_BASE_URL)\n } else {\n $script:config.LLM_PROVIDER = if ($env:AGENTTEAMS_LLM_PROVIDER) { $env:AGENTTEAMS_LLM_PROVIDER } else { \"qwen\" }\n $script:config.DEFAULT_MODEL = if ($env:AGENTTEAMS_DEFAULT_MODEL) { $env:AGENTTEAMS_DEFAULT_MODEL } else { \"qwen3.6-plus\" }\n $script:config.OPENAI_BASE_URL = if ($env:AGENTTEAMS_OPENAI_BASE_URL) { $env:AGENTTEAMS_OPENAI_BASE_URL } else { \"\" }\n Write-Log (Get-Msg \"llm.provider.qwen_default\" -f $script:config.LLM_PROVIDER)\n }\n Write-Log (Get-Msg \"llm.model.label\" -f $script:config.DEFAULT_MODEL)\n Write-Log \"\"\n $script:config.LLM_API_KEY = Read-Prompt -VarName \"AGENTTEAMS_LLM_API_KEY\" -PromptText (Get-Msg \"llm.apikey_prompt\") -Secret\n $script:config.EMBEDDING_MODEL = if ($null -ne $env:AGENTTEAMS_EMBEDDING_MODEL) { $env:AGENTTEAMS_EMBEDDING_MODEL } else { \"text-embedding-v4\" }\n return\n }\n\n Write-Host \"\"\n Write-Host (Get-Msg \"llm.providers_title\")\n Write-Host (Get-Msg \"llm.provider.alibaba\")\n Write-Host (Get-Msg \"llm.provider.openai_compat\")\n Write-Host \"\"\n\n # If upgrade mode with loaded provider, show current as default\n if ($script:AGENTTEAMS_UPGRADE -and $script:config.LLM_PROVIDER) {\n $defaultProvider = if ($script:config.LLM_PROVIDER -eq \"openai-compat\") { \"2\" } else { \"1\" }\n $providerChoice = Read-Host \"$(Get-Msg 'llm.provider.select') [${defaultProvider}]\"\n $providerChoice = if ($providerChoice) { $providerChoice } else { $defaultProvider }\n } elseif ($script:AGENTTEAMS_QUICKSTART) {\n $providerChoice = Read-Host \"$(Get-Msg 'llm.provider.select') [1]\"\n } else {\n $providerChoice = Read-Host (Get-Msg \"llm.provider.select\")\n $providerChoice = if ($providerChoice) { $providerChoice } else { \"1\" }\n }\n if ($providerChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n\n switch -Regex ($providerChoice) {\n \"^(1|alibaba-cloud)$\" {\n $alibabaAccess = $null\n if ($script:AGENTTEAMS_LANGUAGE -eq \"en\") {\n $script:config.LLM_PROVIDER = \"openai-compat\"\n $script:config.OPENAI_BASE_URL = \"https://dashscope-intl.aliyuncs.com/compatible-mode/v1\"\n $alibabaAccess = \"tokenplan\"\n Write-Host \"\"\n Write-Host (Get-Msg \"llm.codingplan.models_title\")\n Write-Host (Get-Msg \"llm.codingplan.model.qwen36plus\")\n Write-Host (Get-Msg \"llm.codingplan.model.glm5\")\n Write-Host (Get-Msg \"llm.codingplan.model.kimi\")\n Write-Host (Get-Msg \"llm.codingplan.model.minimax\")\n Write-Host \"\"\n # If upgrade with loaded model, show current as default\n if ($script:AGENTTEAMS_UPGRADE -and $script:config.DEFAULT_MODEL) {\n $defaultModel = switch ($script:config.DEFAULT_MODEL) {\n \"qwen3.6-plus\" { \"1\" }\n \"glm-5\" { \"2\" }\n \"kimi-k2.5\" { \"3\" }\n \"MiniMax-M2.5\" { \"4\" }\n default { \"1\" }\n }\n $codingPlanModelChoice = Read-Host \"$(Get-Msg 'llm.codingplan.model.select') [${defaultModel}]\"\n $codingPlanModelChoice = if ($codingPlanModelChoice) { $codingPlanModelChoice } else { $defaultModel }\n } elseif ($script:AGENTTEAMS_QUICKSTART) {\n $codingPlanModelChoice = Read-Host \"$(Get-Msg 'llm.codingplan.model.select') [1]\"\n } else {\n $codingPlanModelChoice = Read-Host (Get-Msg \"llm.codingplan.model.select\")\n }\n $codingPlanModelChoice = if ($codingPlanModelChoice) { $codingPlanModelChoice } else { \"1\" }\n if ($codingPlanModelChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n switch -Regex ($codingPlanModelChoice) {\n \"^(1|qwen3\\.6-plus)$\" { $script:config.DEFAULT_MODEL = \"qwen3.6-plus\" }\n \"^(2|glm-5)$\" { $script:config.DEFAULT_MODEL = \"glm-5\" }\n \"^(3|kimi-k2\\.5)$\" { $script:config.DEFAULT_MODEL = \"kimi-k2.5\" }\n \"^(4|MiniMax-M2\\.5)$\" { $script:config.DEFAULT_MODEL = \"MiniMax-M2.5\" }\n default { $script:config.DEFAULT_MODEL = \"qwen3.6-plus\" }\n }\n Write-Log (Get-Msg \"llm.provider.selected_codingplan\")\n } else {\n Write-Host \"\"\n Write-Host (Get-Msg \"llm.alibaba.models_title\")\n Write-Host (Get-Msg \"llm.alibaba.model.tokenplan\")\n Write-Host (Get-Msg \"llm.alibaba.model.bailian\")\n Write-Host (Get-Msg \"llm.alibaba.model.codingplan_legacy\")\n Write-Host \"\"\n $modelChoice = if ($script:AGENTTEAMS_QUICKSTART) {\n Read-Host \"$(Get-Msg 'llm.alibaba.model.select') [1]\"\n } else {\n Read-Host (Get-Msg \"llm.alibaba.model.select\")\n }\n $modelChoice = if ($modelChoice) { $modelChoice } else { \"1\" }\n if ($modelChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n\n if ($modelChoice -match \"^(1|token-plan|tokenplan)$\") {\n $alibabaAccess = \"tokenplan\"\n $script:config.LLM_PROVIDER = \"openai-compat\"\n $script:config.OPENAI_BASE_URL = \"https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1\"\n Write-Host \"\"\n Write-Host (Get-Msg \"llm.codingplan.models_title\")\n Write-Host (Get-Msg \"llm.codingplan.model.qwen36plus\")\n Write-Host (Get-Msg \"llm.codingplan.model.glm5\")\n Write-Host (Get-Msg \"llm.codingplan.model.kimi\")\n Write-Host (Get-Msg \"llm.codingplan.model.minimax\")\n Write-Host \"\"\n # If upgrade with loaded model, show current as default\n if ($script:AGENTTEAMS_UPGRADE -and $script:config.DEFAULT_MODEL) {\n $defaultModel = switch ($script:config.DEFAULT_MODEL) {\n \"qwen3.6-plus\" { \"1\" }\n \"glm-5\" { \"2\" }\n \"kimi-k2.5\" { \"3\" }\n \"MiniMax-M2.5\" { \"4\" }\n default { \"1\" }\n }\n $codingPlanModelChoice = Read-Host \"$(Get-Msg 'llm.codingplan.model.select') [${defaultModel}]\"\n $codingPlanModelChoice = if ($codingPlanModelChoice) { $codingPlanModelChoice } else { $defaultModel }\n } elseif ($script:AGENTTEAMS_QUICKSTART) {\n $codingPlanModelChoice = Read-Host \"$(Get-Msg 'llm.codingplan.model.select') [1]\"\n } else {\n $codingPlanModelChoice = Read-Host (Get-Msg \"llm.codingplan.model.select\")\n }\n $codingPlanModelChoice = if ($codingPlanModelChoice) { $codingPlanModelChoice } else { \"1\" }\n if ($codingPlanModelChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n switch -Regex ($codingPlanModelChoice) {\n \"^(1|qwen3\\.6-plus)$\" { $script:config.DEFAULT_MODEL = \"qwen3.6-plus\" }\n \"^(2|glm-5)$\" { $script:config.DEFAULT_MODEL = \"glm-5\" }\n \"^(3|kimi-k2\\.5)$\" { $script:config.DEFAULT_MODEL = \"kimi-k2.5\" }\n \"^(4|MiniMax-M2\\.5)$\" { $script:config.DEFAULT_MODEL = \"MiniMax-M2.5\" }\n default { $script:config.DEFAULT_MODEL = \"qwen3.6-plus\" }\n }\n Write-Log (Get-Msg \"llm.provider.selected_tokenplan\")\n } elseif ($modelChoice -match \"^(2|qwen|bailian)$\") {\n $alibabaAccess = \"bailian\"\n $script:config.LLM_PROVIDER = \"qwen\"\n $script:config.OPENAI_BASE_URL = \"\"\n Write-Host \"\"\n if ($script:AGENTTEAMS_UPGRADE -and $script:config.DEFAULT_MODEL) {\n $qwenModelInput = Read-Host \"$(Get-Msg 'llm.qwen.model_prompt') [${script:config.DEFAULT_MODEL}]\"\n } else {\n $qwenModelInput = Read-Host (Get-Msg \"llm.qwen.model_prompt\")\n }\n if ($qwenModelInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $script:config.DEFAULT_MODEL = if ($qwenModelInput) { $qwenModelInput } elseif ($script:config.DEFAULT_MODEL) { $script:config.DEFAULT_MODEL } else { \"qwen3.6-plus\" }\n Write-Log (Get-Msg \"llm.provider.selected_qwen\")\n Request-CustomModelParams $script:config.DEFAULT_MODEL\n if ($script:StepResult -eq \"back\") { return }\n } elseif ($modelChoice -match \"^(3|coding-plan|codingplan)$\") {\n $alibabaAccess = \"codingplan_legacy\"\n $script:config.LLM_PROVIDER = \"openai-compat\"\n $script:config.OPENAI_BASE_URL = \"https://coding.dashscope.aliyuncs.com/v1\"\n Write-Host \"\"\n if ($script:AGENTTEAMS_UPGRADE -and $script:config.DEFAULT_MODEL) {\n $codingModelInput = Read-Host \"$(Get-Msg 'llm.qwen.model_prompt') [${script:config.DEFAULT_MODEL}]\"\n } else {\n $codingModelInput = Read-Host (Get-Msg \"llm.qwen.model_prompt\")\n }\n if ($codingModelInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $script:config.DEFAULT_MODEL = if ($codingModelInput) { $codingModelInput } elseif ($script:config.DEFAULT_MODEL) { $script:config.DEFAULT_MODEL } else { \"qwen3.6-plus\" }\n Write-Log (Get-Msg \"llm.provider.selected_codingplan_legacy\")\n Request-CustomModelParams $script:config.DEFAULT_MODEL\n if ($script:StepResult -eq \"back\") { return }\n } else {\n Write-Error (Get-Msg \"llm.alibaba.model.invalid\" -f $modelChoice)\n }\n }\n\n Write-Log (Get-Msg \"llm.model.label\" -f $script:config.DEFAULT_MODEL)\n Write-Log \"\"\n if ($alibabaAccess -eq \"bailian\") {\n Write-Log (Get-Msg \"llm.apikey_hint_bailian\")\n Write-Log (Get-Msg \"llm.apikey_url_bailian\")\n } elseif ($script:AGENTTEAMS_LANGUAGE -eq \"en\") {\n Write-Log (Get-Msg \"llm.apikey_hint_qwencloud\")\n Write-Log (Get-Msg \"llm.apikey_url_qwencloud\")\n } elseif ($alibabaAccess -eq \"codingplan_legacy\") {\n Write-Log (Get-Msg \"llm.apikey_hint_codingplan\")\n Write-Log (Get-Msg \"llm.apikey_url_codingplan\")\n } else {\n Write-Log (Get-Msg \"llm.apikey_hint_tokenplan\")\n Write-Log (Get-Msg \"llm.apikey_url_tokenplan\")\n }\n Write-Log \"\"\n $script:config.LLM_API_KEY = Read-Prompt -VarName \"AGENTTEAMS_LLM_API_KEY\" -PromptText (Get-Msg \"llm.apikey_prompt\") -Secret\n if ($script:StepResult -eq \"back\") { return }\n if ($alibabaAccess -eq \"bailian\") {\n Test-LlmConnectivity -BaseUrl \"https://dashscope.aliyuncs.com/compatible-mode/v1\" -ApiKey $script:config.LLM_API_KEY -Model $script:config.DEFAULT_MODEL\n } elseif ($alibabaAccess -eq \"codingplan_legacy\") {\n Test-LlmConnectivity -BaseUrl \"https://coding.dashscope.aliyuncs.com/v1\" -ApiKey $script:config.LLM_API_KEY -Model $script:config.DEFAULT_MODEL -Hint (Get-Msg \"llm.openai.test.fail.codingplan_legacy\")\n } elseif ($script:AGENTTEAMS_LANGUAGE -eq \"en\") {\n Test-LlmConnectivity -BaseUrl $script:config.OPENAI_BASE_URL -ApiKey $script:config.LLM_API_KEY -Model $script:config.DEFAULT_MODEL -Hint (Get-Msg \"llm.openai.test.fail.codingplan\")\n } else {\n Test-LlmConnectivity -BaseUrl $script:config.OPENAI_BASE_URL -ApiKey $script:config.LLM_API_KEY -Model $script:config.DEFAULT_MODEL -Hint (Get-Msg \"llm.openai.test.fail.tokenplan\")\n }\n if ($script:StepResult -eq \"back\") { return }\n }\n \"^(2|openai-compat)$\" {\n $script:config.LLM_PROVIDER = \"openai-compat\"\n Write-Log (Get-Msg \"llm.provider.selected_openai\" -f $script:config.LLM_PROVIDER)\n Write-Host \"\"\n if ($script:AGENTTEAMS_UPGRADE -and $env:AGENTTEAMS_UPGRADE_KEEP_ALL -eq \"1\") {\n Write-Log (Get-Msg \"prompt.upgrade_keep\" -f (Get-Msg \"llm.openai.base_url_prompt\"), $script:config.OPENAI_BASE_URL)\n } else {\n $currentUrl = $script:config.OPENAI_BASE_URL\n # Use base prompt without embedded default when showing current value\n if ($currentUrl) {\n $urlInput = Read-Host \"$(Get-Msg 'llm.openai.base_url_prompt') [$currentUrl]\"\n } else {\n $urlInput = Read-Host \"$(Get-Msg 'llm.openai.base_url_prompt') [https://api.openai.com/v1]\"\n }\n if ($urlInput -eq \"b\") { $script:StepResult = \"back\"; return }\n if ($urlInput) {\n $script:config.OPENAI_BASE_URL = $urlInput\n } elseif ($currentUrl) {\n $script:config.OPENAI_BASE_URL = $currentUrl\n } else {\n $script:config.OPENAI_BASE_URL = \"https://api.openai.com/v1\"\n }\n }\n if ($script:AGENTTEAMS_UPGRADE -and $env:AGENTTEAMS_UPGRADE_KEEP_ALL -eq \"1\") {\n Write-Log (Get-Msg \"prompt.upgrade_keep\" -f (Get-Msg \"llm.openai.model_prompt\"), $script:config.DEFAULT_MODEL)\n } else {\n $currentModel = $script:config.DEFAULT_MODEL\n if ($currentModel) {\n $modelInput = Read-Host \"$(Get-Msg 'llm.openai.model_prompt') [${currentModel}]\"\n } else {\n $modelInput = Read-Host (Get-Msg 'llm.openai.model_prompt')\n }\n if ($modelInput -eq \"b\") { $script:StepResult = \"back\"; return }\n if ($modelInput) {\n $script:config.DEFAULT_MODEL = $modelInput\n } elseif ($currentModel) {\n $script:config.DEFAULT_MODEL = $currentModel\n } else {\n $script:config.DEFAULT_MODEL = \"gpt-5.4\"\n }\n }\n Write-Log (Get-Msg \"llm.openai.base_url_label\" -f $script:config.OPENAI_BASE_URL)\n Write-Log (Get-Msg \"llm.model.label\" -f $script:config.DEFAULT_MODEL)\n Request-CustomModelParams $script:config.DEFAULT_MODEL\n if ($script:StepResult -eq \"back\") { return }\n Write-Log \"\"\n $script:config.LLM_API_KEY = Read-Prompt -VarName \"AGENTTEAMS_LLM_API_KEY\" -PromptText (Get-Msg \"llm.apikey_prompt\") -Secret\n if ($script:StepResult -eq \"back\") { return }\n Test-LlmConnectivity -BaseUrl $script:config.OPENAI_BASE_URL -ApiKey $script:config.LLM_API_KEY -Model $script:config.DEFAULT_MODEL\n if ($script:StepResult -eq \"back\") { return }\n }\n default {\n Write-Error (Get-Msg \"llm.provider.invalid\" -f $providerChoice)\n }\n }\n\n # Skip to embedding if Keep-All mode handled LLM params\n if ($skipToEmbedding) {\n # Skip embedding selection, use loaded value (already in $script:config.EMBEDDING_MODEL)\n if ($script:config.EMBEDDING_MODEL) {\n Write-Log (Get-Msg \"llm.embedding.title\")\n Write-Log (Get-Msg \"llm.model.label\" -f $script:config.EMBEDDING_MODEL)\n Write-Log \"\"\n }\n return\n }\n\n # --- Embedding model (optional, auto-tested) ---\n Write-Host \"\"\n Write-Log (Get-Msg \"llm.embedding.title\")\n Write-Log (Get-Msg \"llm.embedding.hint\")\n Write-Host \"\"\n Write-Host (Get-Msg \"llm.embedding.option.default\")\n Write-Host (Get-Msg \"llm.embedding.option.custom\")\n Write-Host (Get-Msg \"llm.embedding.option.disable\")\n Write-Host \"\"\n $embChoice = Read-Host \"$(Get-Msg 'llm.embedding.select') [1]\"\n $embChoice = if ($embChoice) { $embChoice } else { \"1\" }\n if ($embChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n\n switch ($embChoice) {\n \"1\" {\n $script:config.EMBEDDING_MODEL = \"text-embedding-v4\"\n }\n \"2\" {\n if ($script:AGENTTEAMS_UPGRADE -and $env:AGENTTEAMS_UPGRADE_KEEP_ALL -eq \"1\") {\n Write-Log (Get-Msg \"prompt.upgrade_keep\" -f (Get-Msg \"llm.embedding.custom_prompt\"), $script:config.EMBEDDING_MODEL)\n } else {\n $currentEmb = $script:config.EMBEDDING_MODEL\n $embCustom = Read-Host (Get-Msg \"llm.embedding.custom_prompt\")\n if ($embCustom -eq \"b\") { $script:StepResult = \"back\"; return }\n if ($embCustom) {\n $script:config.EMBEDDING_MODEL = $embCustom\n } else {\n $script:config.EMBEDDING_MODEL = $currentEmb\n if (-not $script:config.EMBEDDING_MODEL) {\n Write-Log (Get-Msg \"llm.embedding.disabled\")\n }\n }\n }\n }\n \"3\" {\n $script:config.EMBEDDING_MODEL = \"\"\n Write-Log (Get-Msg \"llm.embedding.disabled\")\n }\n default {\n $script:config.EMBEDDING_MODEL = \"text-embedding-v4\"\n }\n }\n\n if ($script:config.EMBEDDING_MODEL) {\n # Qwen provider uses dashscope directly; others use OPENAI_BASE_URL\n $embBaseUrl = $script:config.OPENAI_BASE_URL\n if ($script:config.LLM_PROVIDER -eq \"qwen\") {\n $embBaseUrl = \"https://dashscope.aliyuncs.com/compatible-mode/v1\"\n }\n $embResult = Test-EmbeddingConnectivity -BaseUrl $embBaseUrl -ApiKey $script:config.LLM_API_KEY -Model $script:config.EMBEDDING_MODEL\n if (-not $embResult) {\n $script:config.EMBEDDING_MODEL = \"\"\n Write-Log (Get-Msg \"llm.embedding.auto_disabled\")\n }\n }\n\n Write-Log \"\"\n}\n\nfunction Step-Admin {\n Write-Log (Get-Msg \"admin.title\")\n $script:config.ADMIN_USER = Read-Prompt -VarName \"AGENTTEAMS_ADMIN_USER\" -PromptText (Get-Msg \"admin.username_prompt\") -Default \"admin\"\n if ($script:StepResult -eq \"back\") { return }\n $script:config.ADMIN_USER = $script:config.ADMIN_USER.ToLowerInvariant()\n\n # Pre-set via env var: validate; non-interactive fails fast,\n # interactive warns and falls through to the retry prompt.\n if ($env:AGENTTEAMS_ADMIN_PASSWORD) {\n $script:config.ADMIN_PASSWORD = $env:AGENTTEAMS_ADMIN_PASSWORD\n Write-Log (Get-Msg \"prompt.preset\" -f (Get-Msg \"admin.password_prompt\"))\n if ($script:config.ADMIN_PASSWORD.Length -ge 8) {\n Write-Log \"\"\n return\n }\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n Write-Error (Get-Msg \"admin.password_too_short\" -f $script:config.ADMIN_PASSWORD.Length)\n }\n Write-Host \"$($script:ESC)[31m[AgentTeams ERROR]$($script:ESC)[0m $(Get-Msg \"admin.password_too_short\" -f $script:config.ADMIN_PASSWORD.Length)\"\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_ADMIN_PASSWORD\", $null, \"Process\")\n }\n\n while ($true) {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_ADMIN_PASSWORD\", $null, \"Process\")\n $script:config.ADMIN_PASSWORD = Read-Prompt -VarName \"AGENTTEAMS_ADMIN_PASSWORD\" -PromptText (Get-Msg \"admin.password_prompt\") -Secret -Optional\n if ($script:StepResult -eq \"back\") { return }\n if (-not $script:config.ADMIN_PASSWORD) {\n $randomSuffix = (New-RandomKey).Substring(0, 12)\n $script:config.ADMIN_PASSWORD = \"admin$randomSuffix\"\n Write-Log (Get-Msg \"admin.password_generated\")\n break\n }\n if ($script:config.ADMIN_PASSWORD.Length -ge 8) {\n break\n }\n Write-Host \"$($script:ESC)[31m[AgentTeams ERROR]$($script:ESC)[0m $(Get-Msg \"admin.password_too_short\" -f $script:config.ADMIN_PASSWORD.Length)\"\n }\n\n Write-Log \"\"\n}\n\nfunction Step-Network {\n Write-Log (Get-Msg \"port.local_only.title\")\n Write-Host \"\"\n Write-Host \" 1) $(Get-Msg 'port.local_only.hint_yes')\"\n Write-Host \" 2) $(Get-Msg 'port.local_only.hint_no')\"\n Write-Host \"\"\n\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n $localOnly = if ($env:AGENTTEAMS_LOCAL_ONLY) { $env:AGENTTEAMS_LOCAL_ONLY } else { \"1\" }\n } elseif ($null -ne $env:AGENTTEAMS_LOCAL_ONLY) {\n $localOnly = $env:AGENTTEAMS_LOCAL_ONLY\n } else {\n $localChoice = Read-Host \"$(Get-Msg 'port.local_only.choice')\"\n if ($localChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n if (-not $localChoice) { $localChoice = \"1\" }\n $localOnly = if ($localChoice -match '^(2|n|N|no|NO)$') { \"0\" } else { \"1\" }\n }\n $script:config.LOCAL_ONLY = $localOnly\n\n if ($localOnly -eq \"1\") {\n Write-Log (Get-Msg \"port.local_only.selected_local\")\n } else {\n Write-Log (Get-Msg \"port.local_only.selected_external\")\n Write-Host \"\"\n Write-Host (Get-Msg \"port.local_only.https_hint\") -ForegroundColor Yellow\n }\n Write-Log \"\"\n}\n\nfunction Step-Ports {\n Write-Log (Get-Msg \"port.title\")\n $script:config.PORT_GATEWAY = Read-Prompt -VarName \"AGENTTEAMS_PORT_GATEWAY\" -PromptText (Get-Msg \"port.gateway_prompt\") -Default \"18080\"\n if ($script:StepResult -eq \"back\") { return }\n $script:config.PORT_CONSOLE = Read-Prompt -VarName \"AGENTTEAMS_PORT_CONSOLE\" -PromptText (Get-Msg \"port.console_prompt\") -Default \"18001\"\n if ($script:StepResult -eq \"back\") { return }\n $script:config.PORT_ELEMENT_WEB = Read-Prompt -VarName \"AGENTTEAMS_PORT_ELEMENT_WEB\" -PromptText (Get-Msg \"port.element_prompt\") -Default \"18088\"\n if ($script:StepResult -eq \"back\") { return }\n $script:config.PORT_MANAGER_CONSOLE = Read-Prompt -VarName \"AGENTTEAMS_PORT_MANAGER_CONSOLE\" -PromptText (Get-Msg \"port.manager_console_prompt\") -Default \"18888\"\n if ($script:StepResult -eq \"back\") { return }\n Write-Log \"\"\n}\n\nfunction Step-Domains {\n Write-Log (Get-Msg \"domain.title\")\n Write-Log (Get-Msg \"domain.hint\")\n $script:config.MATRIX_DOMAIN = Read-Prompt -VarName \"AGENTTEAMS_MATRIX_DOMAIN\" -PromptText (Get-Msg \"domain.matrix_prompt\") -Default \"matrix-local.agentteams.io:$($script:config.PORT_GATEWAY)\"\n if ($script:StepResult -eq \"back\") { return }\n $script:config.MATRIX_CLIENT_DOMAIN = Read-Prompt -VarName \"AGENTTEAMS_MATRIX_CLIENT_DOMAIN\" -PromptText (Get-Msg \"domain.element_prompt\") -Default \"matrix-client-local.agentteams.io\"\n if ($script:StepResult -eq \"back\") { return }\n $script:config.AI_GATEWAY_DOMAIN = Read-Prompt -VarName \"AGENTTEAMS_AI_GATEWAY_DOMAIN\" -PromptText (Get-Msg \"domain.gateway_prompt\") -Default \"aigw-local.agentteams.io\"\n if ($script:StepResult -eq \"back\") { return }\n $script:config.FS_DOMAIN = Read-Prompt -VarName \"AGENTTEAMS_FS_DOMAIN\" -PromptText (Get-Msg \"domain.fs_prompt\") -Default \"fs-local.agentteams.io\"\n if ($script:StepResult -eq \"back\") { return }\n $script:config.CONSOLE_DOMAIN = Read-Prompt -VarName \"AGENTTEAMS_CONSOLE_DOMAIN\" -PromptText (Get-Msg \"domain.console_prompt\") -Default \"console-local.agentteams.io\"\n if ($script:StepResult -eq \"back\") { return }\n Write-Log \"\"\n}\n\nfunction Step-Github {\n Write-Log (Get-Msg \"github.title\")\n $script:config.GITHUB_TOKEN = Read-Prompt -VarName \"AGENTTEAMS_GITHUB_TOKEN\" -PromptText (Get-Msg \"github.token_prompt\") -Secret -Optional\n}\n\nfunction Step-Skills {\n Write-Log \"\"\n Write-Log (Get-Msg \"skills.title\")\n $script:config.SKILLS_API_URL = Read-Prompt -VarName \"AGENTTEAMS_SKILLS_API_URL\" -PromptText (Get-Msg \"skills.url_prompt\") -Optional\n if ($script:StepResult -eq \"back\") { return }\n Write-Log \"\"\n}\n\nfunction Step-Volume {\n Write-Log (Get-Msg \"data.title\")\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n $script:config.DATA_DIR = if ($env:AGENTTEAMS_DATA_DIR) { $env:AGENTTEAMS_DATA_DIR } else { \"agentteams-data\" }\n Write-Log \" $(Get-Msg 'data.volume_using' -f $script:config.DATA_DIR) (non-interactive, skipped)\"\n return\n }\n # ─────────────────────────────────────────────────────────────────\n $dataDirInput = Read-Host (Get-Msg \"data.volume_prompt\")\n if ($dataDirInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $script:config.DATA_DIR = if ($dataDirInput) { $dataDirInput } else { \"agentteams-data\" }\n Write-Log (Get-Msg \"data.volume_using\" -f $script:config.DATA_DIR)\n}\n\nfunction Step-Workspace {\n Write-Log (Get-Msg \"workspace.title\")\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n $script:config.WORKSPACE_DIR = if ($env:AGENTTEAMS_WORKSPACE_DIR) { $env:AGENTTEAMS_WORKSPACE_DIR } else { \"$env:USERPROFILE\\agentteams-manager\" }\n if (-not (Test-Path $script:config.WORKSPACE_DIR)) {\n New-Item -ItemType Directory -Path $script:config.WORKSPACE_DIR -Force | Out-Null\n }\n Write-Log \" $(Get-Msg 'workspace.dir_label' -f $script:config.WORKSPACE_DIR) (non-interactive, skipped)\"\n return\n }\n # ─────────────────────────────────────────────────────────────────\n $defaultWorkspace = \"$env:USERPROFILE\\agentteams-manager\"\n if ($script:AGENTTEAMS_UPGRADE -and $env:AGENTTEAMS_UPGRADE_KEEP_ALL -eq \"1\") {\n Write-Log (Get-Msg \"prompt.upgrade_keep\" -f (Get-Msg \"workspace.dir_prompt\" -f $defaultWorkspace), $script:config.WORKSPACE_DIR)\n $wsInput = if ($script:config.WORKSPACE_DIR) { $script:config.WORKSPACE_DIR } else { $defaultWorkspace }\n } else {\n $currentWs = $script:config.WORKSPACE_DIR\n $wsInput = Read-Host (Get-Msg \"workspace.dir_prompt\" -f $defaultWorkspace)\n if ($wsInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $wsInput = if ($wsInput) { $wsInput } else { if ($currentWs) { $currentWs } else { $defaultWorkspace } }\n }\n $script:config.WORKSPACE_DIR = $wsInput\n if (-not (Test-Path $script:config.WORKSPACE_DIR)) {\n New-Item -ItemType Directory -Path $script:config.WORKSPACE_DIR -Force | Out-Null\n }\n Write-Log (Get-Msg \"workspace.dir_label\" -f $script:config.WORKSPACE_DIR)\n}\n\nfunction Step-Runtime {\n Write-Log (Get-Msg \"worker_runtime.title\")\n Write-Host \"\"\n Write-Host \" 1) $(Get-Msg 'worker_runtime.copaw')\"\n Write-Host \" 2) $(Get-Msg 'worker_runtime.openclaw')\"\n Write-Host \" 3) $(Get-Msg 'worker_runtime.hermes')\"\n Write-Host \"\"\n\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n $script:config.DEFAULT_WORKER_RUNTIME = if ($env:AGENTTEAMS_DEFAULT_WORKER_RUNTIME) { $env:AGENTTEAMS_DEFAULT_WORKER_RUNTIME } else { \"copaw\" }\n } elseif ($script:AGENTTEAMS_UPGRADE -and $env:AGENTTEAMS_DEFAULT_WORKER_RUNTIME) {\n Write-Log (Get-Msg \"prompt.upgrade_keep\" -f (Get-Msg \"worker_runtime.title_short\"), $env:AGENTTEAMS_DEFAULT_WORKER_RUNTIME)\n $rtChoice = Read-Host (Get-Msg \"worker_runtime.choice\")\n if ($rtChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n if ($rtChoice) {\n $script:config.DEFAULT_WORKER_RUNTIME = switch ($rtChoice) {\n \"2\" { \"openclaw\" }\n \"3\" { \"hermes\" }\n default { \"copaw\" }\n }\n } else {\n $script:config.DEFAULT_WORKER_RUNTIME = $env:AGENTTEAMS_DEFAULT_WORKER_RUNTIME\n }\n } elseif ($env:AGENTTEAMS_DEFAULT_WORKER_RUNTIME) {\n $script:config.DEFAULT_WORKER_RUNTIME = $env:AGENTTEAMS_DEFAULT_WORKER_RUNTIME\n } else {\n $rtChoice = Read-Host (Get-Msg \"worker_runtime.choice\")\n if ($rtChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n $rtChoice = if ($rtChoice) { $rtChoice } else { \"1\" }\n $script:config.DEFAULT_WORKER_RUNTIME = switch ($rtChoice) {\n \"2\" { \"openclaw\" }\n \"3\" { \"hermes\" }\n default { \"copaw\" }\n }\n }\n Write-Log (Get-Msg \"worker_runtime.selected\" -f $script:config.DEFAULT_WORKER_RUNTIME)\n}\n\nfunction Step-ManagerRuntime {\n Write-Log (Get-Msg \"manager_runtime.title\")\n Write-Host \"\"\n Write-Host \" 1) $(Get-Msg 'manager_runtime.copaw')\"\n Write-Host \" 2) $(Get-Msg 'manager_runtime.openclaw')\"\n Write-Host \"\"\n\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n $script:config.MANAGER_RUNTIME = if ($env:AGENTTEAMS_MANAGER_RUNTIME) { $env:AGENTTEAMS_MANAGER_RUNTIME } else { \"copaw\" }\n } elseif ($script:AGENTTEAMS_UPGRADE -and $env:AGENTTEAMS_MANAGER_RUNTIME) {\n Write-Log (Get-Msg \"prompt.upgrade_keep\" -f (Get-Msg \"manager_runtime.title_short\"), $env:AGENTTEAMS_MANAGER_RUNTIME)\n $mrChoice = Read-Host (Get-Msg \"manager_runtime.choice\")\n if ($mrChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n if ($mrChoice) {\n $script:config.MANAGER_RUNTIME = if ($mrChoice -eq \"2\") { \"openclaw\" } else { \"copaw\" }\n } else {\n $script:config.MANAGER_RUNTIME = $env:AGENTTEAMS_MANAGER_RUNTIME\n }\n } elseif ($env:AGENTTEAMS_MANAGER_RUNTIME) {\n $script:config.MANAGER_RUNTIME = $env:AGENTTEAMS_MANAGER_RUNTIME\n } else {\n $mrChoice = Read-Host (Get-Msg \"manager_runtime.choice\")\n if ($mrChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n $mrChoice = if ($mrChoice) { $mrChoice } else { \"1\" }\n $script:config.MANAGER_RUNTIME = if ($mrChoice -eq \"2\") { \"openclaw\" } else { \"copaw\" }\n }\n Write-Log (Get-Msg \"manager_runtime.selected\" -f $script:config.MANAGER_RUNTIME)\n}\n\nfunction Step-E2ee {\n Write-Host \"\"\n Write-Log (Get-Msg \"matrix_e2ee.title\")\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n $script:config.MATRIX_E2EE = if ($env:AGENTTEAMS_MATRIX_E2EE) { $env:AGENTTEAMS_MATRIX_E2EE } else { \"0\" }\n Write-Log \" $(Get-Msg 'matrix_e2ee.title_short') = $($script:config.MATRIX_E2EE) (non-interactive, skipped)\"\n return\n }\n # ─────────────────────────────────────────────────────────────────\n Write-Host \"\"\n Write-Host \" $(Get-Msg 'matrix_e2ee.desc')\"\n Write-Host \"\"\n Write-Host \" 1) $(Get-Msg 'matrix_e2ee.disable')\"\n Write-Host \" 2) $(Get-Msg 'matrix_e2ee.enable')\"\n Write-Host \"\"\n\n if ($script:AGENTTEAMS_UPGRADE -and $env:AGENTTEAMS_MATRIX_E2EE) {\n $e2eeDisplay = if ($env:AGENTTEAMS_MATRIX_E2EE -eq \"1\") { Get-Msg \"matrix_e2ee.val_enabled\" } else { Get-Msg \"matrix_e2ee.val_disabled\" }\n Write-Log (Get-Msg \"prompt.upgrade_keep\" -f (Get-Msg \"matrix_e2ee.title_short\"), $e2eeDisplay)\n $e2eeChoice = Read-Host (Get-Msg \"matrix_e2ee.choice\")\n if ($e2eeChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n if ($e2eeChoice) {\n $script:config.MATRIX_E2EE = if ($e2eeChoice -eq \"2\") { \"1\" } else { \"0\" }\n } else {\n $script:config.MATRIX_E2EE = $env:AGENTTEAMS_MATRIX_E2EE\n }\n } elseif (-not $env:AGENTTEAMS_MATRIX_E2EE) {\n $e2eeChoice = Read-Host (Get-Msg \"matrix_e2ee.choice\")\n if ($e2eeChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n $e2eeChoice = if ($e2eeChoice) { $e2eeChoice } else { \"1\" }\n $script:config.MATRIX_E2EE = if ($e2eeChoice -eq \"2\") { \"1\" } else { \"0\" }\n } else {\n $script:config.MATRIX_E2EE = $env:AGENTTEAMS_MATRIX_E2EE\n }\n\n if ($script:config.MATRIX_E2EE -eq \"1\") {\n Write-Log (Get-Msg \"matrix_e2ee.selected_enabled\")\n } else {\n Write-Log (Get-Msg \"matrix_e2ee.selected_disabled\")\n }\n}\n\nfunction Step-DockerProxy {\n if (-not $script:AGENTTEAMS_MOUNT_SOCKET) {\n $script:config.DOCKER_PROXY = \"0\"\n return\n }\n\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n $script:config.DOCKER_PROXY = if ($env:AGENTTEAMS_DOCKER_PROXY) { $env:AGENTTEAMS_DOCKER_PROXY } else { \"0\" }\n Write-Log \" $(Get-Msg 'docker_proxy.title_short') = $($script:config.DOCKER_PROXY) (non-interactive, skipped)\"\n return\n }\n # ─────────────────────────────────────────────────────────────────\n\n Write-Host \"\"\n Write-Log (Get-Msg \"docker_proxy.title\")\n Write-Host \"\"\n Write-Host \" $(Get-Msg 'docker_proxy.desc')\"\n Write-Host \"\"\n Write-Host \" 1) $(Get-Msg 'docker_proxy.enable')\"\n Write-Host \" 2) $(Get-Msg 'docker_proxy.disable')\"\n Write-Host \"\"\n\n if ($script:AGENTTEAMS_UPGRADE -and $env:AGENTTEAMS_DOCKER_PROXY) {\n $proxyDisplay = if ($env:AGENTTEAMS_DOCKER_PROXY -eq \"1\") { Get-Msg \"docker_proxy.val_enabled\" } else { Get-Msg \"docker_proxy.val_disabled\" }\n Write-Log (Get-Msg \"prompt.upgrade_keep\" -f (Get-Msg \"docker_proxy.title_short\"), $proxyDisplay)\n $proxyChoice = Read-Host (Get-Msg \"docker_proxy.choice\")\n if ($proxyChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n if ($proxyChoice) {\n $script:config.DOCKER_PROXY = if ($proxyChoice -eq \"2\") { \"0\" } else { \"1\" }\n } else {\n $script:config.DOCKER_PROXY = $env:AGENTTEAMS_DOCKER_PROXY\n }\n } elseif (-not $env:AGENTTEAMS_DOCKER_PROXY) {\n $proxyChoice = Read-Host (Get-Msg \"docker_proxy.choice\")\n if ($proxyChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n $proxyChoice = if ($proxyChoice) { $proxyChoice } else { \"1\" }\n $script:config.DOCKER_PROXY = if ($proxyChoice -eq \"2\") { \"0\" } else { \"1\" }\n } else {\n $script:config.DOCKER_PROXY = $env:AGENTTEAMS_DOCKER_PROXY\n }\n\n if ($script:config.DOCKER_PROXY -eq \"1\") {\n Write-Log (Get-Msg \"docker_proxy.selected_enabled\")\n\n # Prompt for additional allowed image sources\n Write-Host \"\"\n Write-Host \" $(Get-Msg 'docker_proxy.registries_desc')\"\n Write-Host \"\"\n if ($script:AGENTTEAMS_UPGRADE -and $env:AGENTTEAMS_PROXY_ALLOWED_REGISTRIES) {\n Write-Log (Get-Msg \"prompt.upgrade_keep\" -f (Get-Msg \"docker_proxy.registries_label\"), $env:AGENTTEAMS_PROXY_ALLOWED_REGISTRIES)\n $regInput = Read-Host (Get-Msg \"docker_proxy.registries_prompt\")\n if ($regInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $script:config.PROXY_ALLOWED_REGISTRIES = if ($regInput) { $regInput } else { $env:AGENTTEAMS_PROXY_ALLOWED_REGISTRIES }\n } elseif (-not $env:AGENTTEAMS_PROXY_ALLOWED_REGISTRIES) {\n $regInput = Read-Host (Get-Msg \"docker_proxy.registries_prompt\")\n if ($regInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $script:config.PROXY_ALLOWED_REGISTRIES = if ($regInput) { $regInput } else { \"\" }\n } else {\n $script:config.PROXY_ALLOWED_REGISTRIES = $env:AGENTTEAMS_PROXY_ALLOWED_REGISTRIES\n }\n } else {\n Write-Log (Get-Msg \"docker_proxy.selected_disabled\")\n }\n}\n\nfunction Step-Idle {\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n $script:config.WORKER_IDLE_TIMEOUT = if ($env:AGENTTEAMS_WORKER_IDLE_TIMEOUT) { $env:AGENTTEAMS_WORKER_IDLE_TIMEOUT } else { \"720\" }\n Write-Log \" $(Get-Msg 'idle_timeout.label') = $($script:config.WORKER_IDLE_TIMEOUT) (non-interactive, skipped)\"\n return\n }\n # ─────────────────────────────────────────────────────────────────\n if ($script:AGENTTEAMS_UPGRADE -and $env:AGENTTEAMS_WORKER_IDLE_TIMEOUT) {\n Write-Log (Get-Msg \"prompt.upgrade_keep\" -f (Get-Msg \"idle_timeout.label\"), $env:AGENTTEAMS_WORKER_IDLE_TIMEOUT)\n $idleInput = Read-Host (Get-Msg \"idle_timeout.prompt\")\n if ($idleInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $script:config.WORKER_IDLE_TIMEOUT = if ($idleInput) { $idleInput } else { $env:AGENTTEAMS_WORKER_IDLE_TIMEOUT }\n } elseif (-not $env:AGENTTEAMS_WORKER_IDLE_TIMEOUT) {\n $idleInput = Read-Host (Get-Msg \"idle_timeout.prompt\")\n if ($idleInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $script:config.WORKER_IDLE_TIMEOUT = if ($idleInput) { $idleInput } else { \"720\" }\n } else {\n $script:config.WORKER_IDLE_TIMEOUT = $env:AGENTTEAMS_WORKER_IDLE_TIMEOUT\n }\n Write-Log (Get-Msg \"idle_timeout.selected\" -f $script:config.WORKER_IDLE_TIMEOUT)\n Write-Log \"\"\n}\n\nfunction Step-Hostshare {\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n $script:config.HOST_SHARE_DIR = if ($env:AGENTTEAMS_HOST_SHARE_DIR) { $env:AGENTTEAMS_HOST_SHARE_DIR } else { $env:USERPROFILE }\n Write-Log \" $(Get-Msg 'host_share.label') = $($script:config.HOST_SHARE_DIR) (non-interactive, skipped)\"\n return\n }\n # ─────────────────────────────────────────────────────────────────\n $currentShare = $script:config.HOST_SHARE_DIR\n if ($currentShare) {\n $shareInput = Read-Host \"$(Get-Msg 'host_share.prompt' -f $env:USERPROFILE) [${currentShare}]\"\n } else {\n $shareInput = Read-Host (Get-Msg 'host_share.prompt' -f $env:USERPROFILE)\n }\n if ($shareInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $script:config.HOST_SHARE_DIR = if ($shareInput) { $shareInput } else { if ($currentShare) { $currentShare } else { $env:USERPROFILE } }\n}\n\n# ============================================================\n# Manager Installation\n# ============================================================\n\nfunction Install-Manager {\n Write-Log (Get-Msg \"install.title\")\n\n # Detect timezone\n $script:AGENTTEAMS_TIMEZONE = Get-AgentTeamsTimeZone\n\n # Language priority: env var > existing env file > timezone detection\n if ($env:AGENTTEAMS_LANGUAGE) {\n $script:AGENTTEAMS_LANGUAGE = $env:AGENTTEAMS_LANGUAGE\n } else {\n # Check existing env file for saved language preference (upgrade scenario)\n $_envFile = $script:AGENTTEAMS_ENV_FILE\n # Migrate from legacy location (current directory) if needed\n if (-not (Test-Path $_envFile) -and (Test-Path \".\\agentteams-manager.env\")) {\n Write-Log \"Migrating agentteams-manager.env to $_envFile...\"\n Move-Item \".\\agentteams-manager.env\" $_envFile -ErrorAction SilentlyContinue\n }\n if (Test-Path $_envFile) {\n $_savedLang = (Get-Content $_envFile | Select-String \"^AGENTTEAMS_LANGUAGE=\" | ForEach-Object {\n $_.Line -replace '^AGENTTEAMS_LANGUAGE=', ''\n } | Select-Object -First 1)\n if ($_savedLang) {\n $script:AGENTTEAMS_LANGUAGE = $_savedLang\n }\n }\n # Fall back to timezone-based detection\n if (-not $script:AGENTTEAMS_LANGUAGE) {\n $script:AGENTTEAMS_LANGUAGE = Get-AgentTeamsLanguage -Timezone $script:AGENTTEAMS_TIMEZONE\n }\n }\n $env:AGENTTEAMS_LANGUAGE = $script:AGENTTEAMS_LANGUAGE\n\n # Detect registry\n $script:AGENTTEAMS_REGISTRY = Get-Registry -Timezone $script:AGENTTEAMS_TIMEZONE\n\n # Set image names\n $script:MANAGER_IMAGE = if ($env:AGENTTEAMS_INSTALL_MANAGER_IMAGE) {\n $env:AGENTTEAMS_INSTALL_MANAGER_IMAGE\n } else {\n \"$($script:AGENTTEAMS_REGISTRY)/agentteams/agentteams-manager:$($script:AGENTTEAMS_VERSION)\"\n }\n\n $script:WORKER_IMAGE = if ($env:AGENTTEAMS_INSTALL_WORKER_IMAGE) {\n $env:AGENTTEAMS_INSTALL_WORKER_IMAGE\n } else {\n \"$($script:AGENTTEAMS_REGISTRY)/agentteams/agentteams-worker:$($script:AGENTTEAMS_VERSION)\"\n }\n\n $script:COPAW_WORKER_IMAGE = if ($env:AGENTTEAMS_INSTALL_COPAW_WORKER_IMAGE) {\n $env:AGENTTEAMS_INSTALL_COPAW_WORKER_IMAGE\n } else {\n \"$($script:AGENTTEAMS_REGISTRY)/agentteams/agentteams-copaw-worker:$($script:AGENTTEAMS_VERSION)\"\n }\n\n $script:QWENPAW_WORKER_IMAGE = if ($env:AGENTTEAMS_INSTALL_QWENPAW_WORKER_IMAGE) {\n $env:AGENTTEAMS_INSTALL_QWENPAW_WORKER_IMAGE\n } else {\n \"$($script:AGENTTEAMS_REGISTRY)/agentteams/agentteams-qwenpaw-worker:$($script:AGENTTEAMS_VERSION)\"\n }\n\n $script:HERMES_WORKER_IMAGE = if ($env:AGENTTEAMS_INSTALL_HERMES_WORKER_IMAGE) {\n $env:AGENTTEAMS_INSTALL_HERMES_WORKER_IMAGE\n } else {\n \"$($script:AGENTTEAMS_REGISTRY)/agentteams/agentteams-hermes-worker:$($script:AGENTTEAMS_VERSION)\"\n }\n\n $script:MANAGER_COPAW_IMAGE = if ($env:AGENTTEAMS_INSTALL_MANAGER_COPAW_IMAGE) {\n $env:AGENTTEAMS_INSTALL_MANAGER_COPAW_IMAGE\n } else {\n \"$($script:AGENTTEAMS_REGISTRY)/agentteams/agentteams-manager-copaw:$($script:AGENTTEAMS_VERSION)\"\n }\n\n $script:CONTROLLER_IMAGE = if ($env:AGENTTEAMS_INSTALL_CONTROLLER_IMAGE) {\n $env:AGENTTEAMS_INSTALL_CONTROLLER_IMAGE\n } else {\n \"$($script:AGENTTEAMS_REGISTRY)/agentteams/agentteams-controller:$($script:AGENTTEAMS_VERSION)\"\n }\n\n # Resolve embedded controller image (sets $script:EMBEDDED_IMAGE and\n # $script:AGENTTEAMS_USE_EMBEDDED). Errors out fast if no embedded image is available\n # for the requested version (mirrors the bash installer behavior).\n Resolve-EmbeddedImage\n\n Write-Log (Get-Msg \"install.registry\" -f $script:AGENTTEAMS_REGISTRY)\n Write-Log \"\"\n Write-Log (Get-Msg \"install.dir\" -f (Get-Location))\n Write-Log (Get-Msg \"install.dir_hint\")\n Write-Log (Get-Msg \"install.dir_hint2\")\n Write-Log \"\"\n\n # Check container runtime (docker or podman)\n if (-not (Get-Command \"docker\" -ErrorAction SilentlyContinue) -and\n -not (Get-Command \"podman\" -ErrorAction SilentlyContinue)) {\n Write-Host \"$($script:ESC)[31m[AgentTeams ERROR]$($script:ESC)[0m $(Get-Msg 'error.docker_not_found')\" -ForegroundColor Red\n Exit-Script 1\n }\n\n if (-not (Test-DockerRunning)) {\n Write-Host \"$($script:ESC)[31m[AgentTeams ERROR]$($script:ESC)[0m $(Get-Msg 'error.docker_not_running')\" -ForegroundColor Red\n Exit-Script 1\n }\n\n # Initialize shared config hashtable\n $script:config = @{}\n $config = $script:config # local alias so post-machine code can use $config\n\n # Orphan volume detection (env file gone but volume remains)\n if (-not (Test-Path $script:AGENTTEAMS_ENV_FILE)) {\n $dataVol = if ($env:AGENTTEAMS_DATA_DIR) { $env:AGENTTEAMS_DATA_DIR } else { \"agentteams-data\" }\n $volumeExists = docker volume ls -q 2>$null | Select-String \"^${dataVol}$\"\n if ($volumeExists) {\n Write-Host \"\"\n Write-Log (Get-Msg \"install.orphan_volume.detected\" -f $dataVol)\n Write-Log (Get-Msg \"install.orphan_volume.warn\")\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n Write-Log (Get-Msg \"install.orphan_volume.clean_noninteractive\")\n docker stop agentteams-manager *>$null\n docker rm agentteams-manager *>$null\n docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-worker-\" | ForEach-Object {\n docker stop $_.ToString().Trim() *>$null\n docker rm $_.ToString().Trim() *>$null\n }\n Write-Log (Get-Msg \"install.orphan_volume.cleaning\")\n docker volume rm $dataVol *>$null\n Write-Log (Get-Msg \"install.orphan_volume.cleaned\")\n } else {\n Write-Host \"\"\n Write-Host (Get-Msg \"install.orphan_volume.choose\")\n Write-Host (Get-Msg \"install.orphan_volume.clean\")\n Write-Host (Get-Msg \"install.orphan_volume.keep\")\n Write-Host \"\"\n $orphanChoice = Read-Host (Get-Msg \"install.orphan_volume.prompt\")\n if (-not $orphanChoice) { $orphanChoice = \"1\" }\n switch -Regex ($orphanChoice) {\n \"^(1|clean)$\" {\n docker stop agentteams-manager *>$null\n docker rm agentteams-manager *>$null\n docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-worker-\" | ForEach-Object {\n docker stop $_.ToString().Trim() *>$null\n docker rm $_.ToString().Trim() *>$null\n }\n Write-Log (Get-Msg \"install.orphan_volume.cleaning\")\n docker volume rm $dataVol *>$null\n Write-Log (Get-Msg \"install.orphan_volume.cleaned\")\n }\n \"^(2|keep)$\" {\n Write-Log (Get-Msg \"install.orphan_volume.keeping\")\n }\n }\n }\n }\n }\n\n # ── State machine ─────────────────────────────────────────────────────────\n $_steps = @(\"Step-Lang\", \"Step-Mode\", \"Step-Existing\", \"Step-Llm\", \"Step-ManagerRuntime\", \"Step-Runtime\", \"Step-Admin\",\n \"Step-Network\", \"Step-Ports\", \"Step-Domains\", \"Step-Github\", \"Step-Skills\",\n \"Step-Volume\", \"Step-Workspace\", \"Step-E2ee\", \"Step-DockerProxy\", \"Step-Idle\",\n \"Step-Hostshare\")\n $_stepHistory = [System.Collections.Generic.List[int]]::new()\n $_stepIdx = 0\n while ($_stepIdx -lt $_steps.Count) {\n $_stepFn = $_steps[$_stepIdx]\n if (Test-ShouldSkipStep $_stepFn) {\n $_stepIdx++\n continue\n }\n if ($_stepHistory.Count -gt 0) {\n Write-Log (Get-Msg \"nav.back_hint\")\n }\n $script:StepResult = \"\"\n & $_stepFn\n if ($script:StepResult -eq \"back\") {\n if ($_stepHistory.Count -gt 0) {\n $_stepIdx = $_stepHistory[$_stepHistory.Count - 1]\n $_stepHistory.RemoveAt($_stepHistory.Count - 1)\n Clear-StepVars $_steps[$_stepIdx]\n }\n # else: first step, ignore 'b'\n } else {\n $_stepHistory.Add($_stepIdx)\n $_stepIdx++\n }\n }\n # ── End state machine ──────────────────────────────────────────────────────\n\n # Post-machine defaults for any steps that were skipped\n if (-not $script:config.DATA_DIR) {\n $script:config.DATA_DIR = if ($env:AGENTTEAMS_DATA_DIR) { $env:AGENTTEAMS_DATA_DIR } else { \"agentteams-data\" }\n }\n Write-Log (Get-Msg \"data.volume_using\" -f $script:config.DATA_DIR)\n if (-not $script:config.WORKSPACE_DIR) {\n $_defaultWs = \"$env:USERPROFILE\\agentteams-manager\"\n $script:config.WORKSPACE_DIR = if ($env:AGENTTEAMS_WORKSPACE_DIR) { $env:AGENTTEAMS_WORKSPACE_DIR } else { $_defaultWs }\n if (-not (Test-Path $script:config.WORKSPACE_DIR)) {\n New-Item -ItemType Directory -Path $script:config.WORKSPACE_DIR -Force | Out-Null\n }\n Write-Log (Get-Msg \"workspace.dir_label\" -f $script:config.WORKSPACE_DIR)\n }\n if (-not $script:config.DEFAULT_WORKER_RUNTIME) {\n $script:config.DEFAULT_WORKER_RUNTIME = if ($env:AGENTTEAMS_DEFAULT_WORKER_RUNTIME) { $env:AGENTTEAMS_DEFAULT_WORKER_RUNTIME } else { \"copaw\" }\n Write-Log (Get-Msg \"worker_runtime.selected\" -f $script:config.DEFAULT_WORKER_RUNTIME)\n }\n if (-not $script:config.MATRIX_E2EE) {\n $script:config.MATRIX_E2EE = if ($env:AGENTTEAMS_MATRIX_E2EE) { $env:AGENTTEAMS_MATRIX_E2EE } else { \"0\" }\n }\n if (-not $script:config.WORKER_IDLE_TIMEOUT) {\n $script:config.WORKER_IDLE_TIMEOUT = if ($env:AGENTTEAMS_WORKER_IDLE_TIMEOUT) { $env:AGENTTEAMS_WORKER_IDLE_TIMEOUT } else { \"720\" }\n }\n if (-not $script:config.HOST_SHARE_DIR) {\n $script:config.HOST_SHARE_DIR = if ($env:AGENTTEAMS_HOST_SHARE_DIR) { $env:AGENTTEAMS_HOST_SHARE_DIR } else { $env:USERPROFILE }\n }\n if (-not $script:config.LOCAL_ONLY) {\n $script:config.LOCAL_ONLY = if ($env:AGENTTEAMS_LOCAL_ONLY) { $env:AGENTTEAMS_LOCAL_ONLY } else { \"1\" }\n }\n if (-not $script:config.MANAGER_RUNTIME) {\n $script:config.MANAGER_RUNTIME = if ($env:AGENTTEAMS_MANAGER_RUNTIME) { $env:AGENTTEAMS_MANAGER_RUNTIME } else { \"copaw\" }\n }\n if (-not $script:config.PORT_GATEWAY) {\n $script:config.PORT_GATEWAY = if ($env:AGENTTEAMS_PORT_GATEWAY) { $env:AGENTTEAMS_PORT_GATEWAY } else { \"18080\" }\n $script:config.PORT_CONSOLE = if ($env:AGENTTEAMS_PORT_CONSOLE) { $env:AGENTTEAMS_PORT_CONSOLE } else { \"18001\" }\n $script:config.PORT_ELEMENT_WEB = if ($env:AGENTTEAMS_PORT_ELEMENT_WEB) { $env:AGENTTEAMS_PORT_ELEMENT_WEB } else { \"18088\" }\n $script:config.PORT_MANAGER_CONSOLE = if ($env:AGENTTEAMS_PORT_MANAGER_CONSOLE) { $env:AGENTTEAMS_PORT_MANAGER_CONSOLE } else { \"18888\" }\n }\n if (-not $script:config.MATRIX_DOMAIN) {\n $script:config.MATRIX_DOMAIN = if ($env:AGENTTEAMS_MATRIX_DOMAIN) { $env:AGENTTEAMS_MATRIX_DOMAIN } else { \"matrix-local.agentteams.io:$($script:config.PORT_GATEWAY)\" }\n $script:config.MATRIX_CLIENT_DOMAIN = if ($env:AGENTTEAMS_MATRIX_CLIENT_DOMAIN) { $env:AGENTTEAMS_MATRIX_CLIENT_DOMAIN } else { \"matrix-client-local.agentteams.io\" }\n $script:config.AI_GATEWAY_DOMAIN = if ($env:AGENTTEAMS_AI_GATEWAY_DOMAIN) { $env:AGENTTEAMS_AI_GATEWAY_DOMAIN } else { \"aigw-local.agentteams.io\" }\n $script:config.FS_DOMAIN = if ($env:AGENTTEAMS_FS_DOMAIN) { $env:AGENTTEAMS_FS_DOMAIN } else { \"fs-local.agentteams.io\" }\n $script:config.CONSOLE_DOMAIN = if ($env:AGENTTEAMS_CONSOLE_DOMAIN) { $env:AGENTTEAMS_CONSOLE_DOMAIN } else { \"console-local.agentteams.io\" }\n }\n\n Write-Log \"\"\n\n # Generate secrets\n Write-Log (Get-Msg \"install.generating_secrets\")\n $config.MANAGER_PASSWORD = if ($env:AGENTTEAMS_MANAGER_PASSWORD) { $env:AGENTTEAMS_MANAGER_PASSWORD } else { New-RandomKey }\n $config.REGISTRATION_TOKEN = if ($env:AGENTTEAMS_REGISTRATION_TOKEN) { $env:AGENTTEAMS_REGISTRATION_TOKEN } else { New-RandomKey }\n $config.MINIO_USER = if ($env:AGENTTEAMS_MINIO_USER) { $env:AGENTTEAMS_MINIO_USER } else { $config.ADMIN_USER }\n $config.MINIO_PASSWORD = if ($env:AGENTTEAMS_MINIO_PASSWORD) { $env:AGENTTEAMS_MINIO_PASSWORD } else { $config.ADMIN_PASSWORD }\n $config.MANAGER_GATEWAY_KEY = if ($env:AGENTTEAMS_MANAGER_GATEWAY_KEY) { $env:AGENTTEAMS_MANAGER_GATEWAY_KEY } else { New-RandomKey }\n\n # Store additional config\n $config.LANGUAGE = $script:AGENTTEAMS_LANGUAGE\n $config.REGISTRY = $script:AGENTTEAMS_REGISTRY\n $config.WORKER_IMAGE = $script:WORKER_IMAGE\n $config.COPAW_WORKER_IMAGE = $script:COPAW_WORKER_IMAGE\n $config.QWENPAW_WORKER_IMAGE = $script:QWENPAW_WORKER_IMAGE\n $config.HERMES_WORKER_IMAGE = $script:HERMES_WORKER_IMAGE\n $config.MANAGER_COPAW_IMAGE = $script:MANAGER_COPAW_IMAGE\n\n # Write env file\n New-EnvFile -Config $config -Path $script:AGENTTEAMS_ENV_FILE\n\n # Manager image selection (used by both embedded — passed to controller via env —\n # and legacy — used directly as `docker run` target).\n $managerImage = if ($config.MANAGER_RUNTIME -eq \"copaw\") { $script:MANAGER_COPAW_IMAGE } else { $script:MANAGER_IMAGE }\n $portPrefix = if ($config.LOCAL_ONLY -eq \"1\") { \"127.0.0.1:\" } else { \"\" }\n\n # Ensure agentteams-net Docker network exists. Used in both modes — the embedded\n # controller and the spawned manager/worker containers all join it and rely on\n # network aliases for *-local.agentteams.io DNS resolution.\n if ($script:AGENTTEAMS_MOUNT_SOCKET) {\n $socketAvailable = Test-DockerRunning\n if ($socketAvailable) {\n docker network inspect agentteams-net *>$null\n if ($LASTEXITCODE -ne 0) { docker network create agentteams-net *>$null }\n } else {\n Write-Log (Get-Msg \"install.socket_not_found\")\n if (-not $script:AGENTTEAMS_NON_INTERACTIVE) {\n Write-Host \"\"\n Write-Host \"$($script:ESC)[33m$(Get-Msg 'install.socket_confirm.title')$($script:ESC)[0m\"\n Write-Host \"\"\n Write-Host (Get-Msg 'install.socket_confirm.message')\n Write-Host \"\"\n $confirm = Read-Host (Get-Msg 'install.socket_confirm.prompt')\n if ($confirm -ne 'y' -and $confirm -ne 'Y') {\n Write-Log (Get-Msg 'install.socket_confirm.cancelled')\n exit 0\n }\n }\n }\n }\n\n if ($script:AGENTTEAMS_USE_EMBEDDED -ne \"1\") {\n # ============================================================\n # Legacy architecture: all-in-one manager container\n # (only entered when AGENTTEAMS_FORCE_LEGACY=1 — broken with the slim manager\n # image shipped since PR #616; kept solely for AGENTTEAMS_VERSION <= v1.0.9)\n # ============================================================\n\n $dockerArgs = @(\n \"run\", \"-d\",\n \"--name\", \"agentteams-manager\",\n \"--env-file\", $script:AGENTTEAMS_ENV_FILE,\n \"-e\", \"HOME=/root/manager-workspace\",\n \"-w\", \"/root/manager-workspace\",\n \"-e\", \"HOST_ORIGINAL_HOME=$($config.HOST_SHARE_DIR)\",\n \"-e\", \"AGENTTEAMS_MANAGER_RUNTIME=$($config.MANAGER_RUNTIME)\"\n )\n\n $dockerArgs += @(\"-e\", \"TZ=$($script:AGENTTEAMS_TIMEZONE)\")\n\n if ($script:AGENTTEAMS_MOUNT_SOCKET -and (Test-DockerRunning)) {\n $dockerArgs += @(\"--network\", \"agentteams-net\")\n $dockerArgs += @(\"--network-alias\", \"matrix-local.agentteams.io\")\n $dockerArgs += @(\"--network-alias\", \"aigw-local.agentteams.io\")\n $dockerArgs += @(\"--network-alias\", \"fs-local.agentteams.io\")\n foreach ($domain in @($config.MATRIX_CLIENT_DOMAIN, $config.CONSOLE_DOMAIN)) {\n if ($domain -match '-local\\.agentteams\\.io$') {\n $dockerArgs += @(\"--network-alias\", $domain)\n }\n }\n\n if ($config.DOCKER_PROXY -eq \"1\") {\n $proxyImage = $script:CONTROLLER_IMAGE\n Write-Log \"Starting Docker API proxy...\"\n docker rm -f agentteams-controller *>$null\n docker run -d --name agentteams-controller `\n --network agentteams-net `\n -v \"//var/run/docker.sock:/var/run/docker.sock\" `\n --security-opt label=disable `\n -e \"AGENTTEAMS_WORKER_IMAGE=$($script:WORKER_IMAGE)\" `\n -e \"AGENTTEAMS_COPAW_WORKER_IMAGE=$($script:COPAW_WORKER_IMAGE)\" `\n -e \"AGENTTEAMS_QWENPAW_WORKER_IMAGE=$($script:QWENPAW_WORKER_IMAGE)\" `\n -e \"AGENTTEAMS_HERMES_WORKER_IMAGE=$($script:HERMES_WORKER_IMAGE)\" `\n -e \"AGENTTEAMS_DEFAULT_WORKER_RUNTIME=$($script:config.DEFAULT_WORKER_RUNTIME)\" `\n $(if ($config.PROXY_ALLOWED_REGISTRIES) { @(\"-e\", \"AGENTTEAMS_PROXY_ALLOWED_REGISTRIES=$($config.PROXY_ALLOWED_REGISTRIES)\") }) `\n --restart unless-stopped `\n $proxyImage\n $dockerArgs += @(\"-e\", \"AGENTTEAMS_CONTROLLER_URL=http://agentteams-controller:8090\")\n $dockerArgs += @(\"-e\", \"AGENTTEAMS_CONTAINER_API=http://agentteams-controller:8090\")\n Write-Log (Get-Msg \"docker_proxy.selected_enabled\")\n } else {\n $dockerArgs += @(\"-v\", \"//var/run/docker.sock:/var/run/docker.sock\")\n $dockerArgs += @(\"--security-opt\", \"label=disable\")\n Write-Log (Get-Msg \"install.socket_detected\" -f \"//var/run/docker.sock\")\n }\n }\n\n $dockerArgs += @(\"-p\", \"${portPrefix}$($config.PORT_GATEWAY):8080\")\n $dockerArgs += @(\"-p\", \"${portPrefix}$($config.PORT_CONSOLE):8001\")\n $dockerArgs += @(\"-p\", \"${portPrefix}$($config.PORT_ELEMENT_WEB):8088\")\n $dockerArgs += @(\"-p\", \"127.0.0.1:$($config.PORT_MANAGER_CONSOLE):18888\")\n\n $dockerArgs += @(\"-v\", \"$($config.DATA_DIR):/data\")\n\n $wsDockerPath = ConvertTo-DockerPath -Path $config.WORKSPACE_DIR\n $dockerArgs += @(\"-v\", \"${wsDockerPath}:/root/manager-workspace\")\n\n $shareDockerPath = ConvertTo-DockerPath -Path $config.HOST_SHARE_DIR\n $dockerArgs += @(\"-v\", \"${shareDockerPath}:/host-share\")\n Write-Log (Get-Msg \"host_share.sharing\" -f $config.HOST_SHARE_DIR)\n\n if ($env:AGENTTEAMS_YOLO -eq \"1\") {\n $dockerArgs += @(\"-e\", \"AGENTTEAMS_YOLO=1\")\n Write-Log (Get-Msg \"install.yolo\")\n }\n\n if ($env:AGENTTEAMS_MATRIX_DEBUG -eq \"1\") {\n $dockerArgs += @(\"-e\", \"AGENTTEAMS_MATRIX_DEBUG=1\")\n }\n\n $dockerArgs += @(\"--restart\", \"unless-stopped\")\n $dockerArgs += $managerImage\n }\n\n # Check if the Docker volume exists; create if not (reuse on reinstall) — both modes.\n $volumeExists = docker volume ls -q 2>$null | Select-String \"^$($config.DATA_DIR)$\"\n if (-not $volumeExists) {\n docker volume create $config.DATA_DIR | Out-Null\n }\n\n # Pull images (skip if already exists locally for local build tags).\n $LocalImagePattern = \"^agentteams/\"\n function Test-LocalImage {\n param([string]$Image)\n $imgExists = docker image inspect $Image 2>$null\n return $LASTEXITCODE -eq 0\n }\n if ($script:AGENTTEAMS_USE_EMBEDDED -eq \"1\") {\n # Embedded image was already pulled by Resolve-EmbeddedImage unless overridden;\n # for an explicit override we still need to ensure it is present locally.\n if ($env:AGENTTEAMS_INSTALL_EMBEDDED_IMAGE) {\n if ($script:EMBEDDED_IMAGE -match $LocalImagePattern) {\n if (-not (Test-LocalImage $script:EMBEDDED_IMAGE)) {\n Write-Log \"Pulling embedded image: $($script:EMBEDDED_IMAGE)\"\n & docker pull $script:EMBEDDED_IMAGE\n }\n } else {\n Write-Log \"Pulling embedded image: $($script:EMBEDDED_IMAGE)\"\n & docker pull $script:EMBEDDED_IMAGE\n }\n }\n # Manager image — controller will spawn it inside; pull here so the spawn doesn't\n # have to wait on the network.\n if ($managerImage -match $LocalImagePattern) {\n if (Test-LocalImage $managerImage) {\n Write-Log (Get-Msg \"install.image.exists\" -f $managerImage)\n } else {\n Write-Log (Get-Msg \"install.image.pulling_manager\" -f $managerImage)\n & docker pull $managerImage\n }\n } else {\n Write-Log (Get-Msg \"install.image.pulling_manager\" -f $managerImage)\n & docker pull $managerImage\n }\n } else {\n if ($managerImage -match $LocalImagePattern) {\n if (Test-LocalImage $managerImage) {\n Write-Log (Get-Msg \"install.image.exists\" -f $managerImage)\n } else {\n Write-Log (Get-Msg \"install.image.pulling_manager\" -f $managerImage)\n & docker pull $managerImage\n }\n } else {\n Write-Log (Get-Msg \"install.image.pulling_manager\" -f $managerImage)\n & docker pull $managerImage\n }\n }\n\n # Pull all worker runtime images (workers may use any runtime regardless of the default)\n $workerImages = @(\n $script:WORKER_IMAGE\n $script:COPAW_WORKER_IMAGE\n # Temporarily disabled until the QwenPaw worker image is published.\n # $script:QWENPAW_WORKER_IMAGE\n $script:HERMES_WORKER_IMAGE\n )\n foreach ($workerImg in $workerImages) {\n if ($workerImg -match $LocalImagePattern) {\n if (Test-LocalImage $workerImg) {\n Write-Log (Get-Msg \"install.image.worker_exists\" -f $workerImg)\n } else {\n Write-Log (Get-Msg \"install.image.pulling_worker\" -f $workerImg)\n & docker pull $workerImg\n }\n } else {\n Write-Log (Get-Msg \"install.image.pulling_worker\" -f $workerImg)\n & docker pull $workerImg\n }\n }\n\n # --- Pre-upgrade: extract Matrix passwords from old containers (old-arch -> embedded) ---\n $credsTmp = $null\n if ($script:AGENTTEAMS_UPGRADE -and $script:AGENTTEAMS_USE_EMBEDDED -eq \"1\") {\n $controllerNameHit = docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-controller$\"\n $isOldArch = $false\n if (-not $controllerNameHit) {\n $isOldArch = $true\n } else {\n $ctrlImgLine = docker ps -a --format \"{{.Names}} {{.Image}}\" 2>$null | Where-Object { $_ -match '^agentteams-controller ' } | Select-Object -First 1\n if ($ctrlImgLine -and ($ctrlImgLine -notmatch 'embedded')) {\n $isOldArch = $true\n }\n }\n\n if ($isOldArch) {\n $credsTmp = Join-Path ([System.IO.Path]::GetTempPath()) (\"agentteams-upgrade-creds-\" + [Guid]::NewGuid().ToString(\"n\"))\n New-Item -ItemType Directory -Path $credsTmp -Force | Out-Null\n $utf8NoBom = New-Object System.Text.UTF8Encoding $false\n\n $mgrCredsTempStart = $false\n $mgrExists = docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-manager$\"\n $mgrRunning = docker ps --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-manager$\"\n if ($mgrExists -and -not $mgrRunning) {\n Write-Log \"agentteams-manager is stopped; starting it temporarily to extract Matrix credentials for upgrade...\"\n docker start agentteams-manager 2>$null | Out-Null\n Wait-MatrixReady -Container \"agentteams-manager\"\n $mgrCredsTempStart = $true\n }\n\n $inspectLines = docker inspect agentteams-manager --format \"{{range .Config.Env}}{{println .}}{{end}}\" 2>$null\n $mgrPw = \"\"\n if ($inspectLines) {\n $envLine = $inspectLines -split \"`n\" | Where-Object { $_ -match '^AGENTTEAMS_MANAGER_PASSWORD=' } | Select-Object -First 1\n if ($envLine) {\n $mgrPw = ($envLine -replace '^AGENTTEAMS_MANAGER_PASSWORD=', \"\").Trim()\n }\n }\n $mgrRunningNow = docker ps --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-manager$\"\n if ([string]::IsNullOrEmpty($mgrPw) -and $mgrRunningNow) {\n $mgrPw = docker exec agentteams-manager bash -c 'source /data/agentteams-secrets.env 2>/dev/null && echo \"${AGENTTEAMS_MANAGER_PASSWORD}\"' 2>$null\n if ($mgrPw) { $mgrPw = $mgrPw.Trim() }\n }\n $dataVolPresent = docker volume ls -q 2>$null | Where-Object { $_ -eq $config.DATA_DIR }\n if ([string]::IsNullOrEmpty($mgrPw) -and $dataVolPresent) {\n $mgrPw = Read-AgentTeamsSecretFromDataVolume -VolumeName $config.DATA_DIR -Key \"AGENTTEAMS_MANAGER_PASSWORD\"\n }\n\n $envFilePath = $script:AGENTTEAMS_ENV_FILE\n $mgrRoom = \"\"\n if (-not [string]::IsNullOrEmpty($mgrPw)) {\n $adminPw = \"\"\n $adminUser = \"admin\"\n if (Test-Path $envFilePath) {\n $adminLine = Get-Content $envFilePath | Where-Object { $_ -match '^AGENTTEAMS_ADMIN_PASSWORD=' } | Select-Object -First 1\n if ($adminLine) { $adminPw = ($adminLine -replace '^AGENTTEAMS_ADMIN_PASSWORD=', \"\").Trim() }\n $userLine = Get-Content $envFilePath | Where-Object { $_ -match '^AGENTTEAMS_ADMIN_USER=' } | Select-Object -First 1\n if ($userLine) {\n $adminUser = ($userLine -replace '^AGENTTEAMS_ADMIN_USER=', \"\").Trim()\n if ([string]::IsNullOrEmpty($adminUser)) { $adminUser = \"admin\" }\n }\n }\n\n if (-not [string]::IsNullOrEmpty($adminPw) -and $mgrRunningNow) {\n $mgrRoom = Get-AgentTeamsAdminDmRoomViaMatrix -AdminUser $adminUser -AdminPassword $adminPw\n }\n if ([string]::IsNullOrEmpty($mgrRoom)) {\n $mgrRoom = Read-AgentTeamsAdminDmRoomFromWorkspace -WorkspaceDir $config.WORKSPACE_DIR\n }\n\n $gwKeyLine = if (Test-Path $envFilePath) {\n Get-Content $envFilePath | Where-Object { $_ -match '^AGENTTEAMS_MANAGER_GATEWAY_KEY=' } | Select-Object -First 1\n } else { $null }\n $gwKeyForDefault = if ($gwKeyLine) { ($gwKeyLine -replace '^AGENTTEAMS_MANAGER_GATEWAY_KEY=', \"\").Trim() } else { $config.MANAGER_GATEWAY_KEY }\n\n $defaultEnvPath = Join-Path $credsTmp \"default.env\"\n [System.IO.File]::WriteAllLines($defaultEnvPath, @(\n \"WORKER_PASSWORD=$mgrPw\"\n \"WORKER_MINIO_PASSWORD=$(Get-AgentTeamsRandomHex 24)\"\n \"WORKER_GATEWAY_KEY=$gwKeyForDefault\"\n \"WORKER_ROOM_ID=$mgrRoom\"\n ), $utf8NoBom)\n if ($mgrRoom) {\n Write-Log \"Extracted Manager Matrix password and room ID\"\n } else {\n Write-Log \"Extracted Manager Matrix password\"\n }\n }\n\n if ($mgrCredsTempStart) {\n Write-Log \"Stopping agentteams-manager after credential extraction (upgrade will recreate containers)...\"\n docker stop agentteams-manager 2>$null | Out-Null\n }\n }\n }\n\n # Stop and remove existing containers (deferred until after all\n # configuration is collected and images are pulled successfully)\n $existingProxy = docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-controller$\"\n if ($existingProxy) {\n docker stop agentteams-controller *>$null\n docker rm agentteams-controller *>$null\n }\n $existingContainer = docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-manager$\"\n if ($existingContainer) {\n Write-Log (Get-Msg \"install.removing_existing\")\n docker stop agentteams-manager *>$null\n docker rm agentteams-manager *>$null\n }\n\n # Stop and remove worker containers (controller will recreate via CR reconciliation)\n if ($script:UPGRADE_EXISTING_WORKERS) {\n Write-Log (Get-Msg \"install.existing.stopping_workers\")\n $script:UPGRADE_EXISTING_WORKERS | ForEach-Object {\n docker stop $_ *>$null\n docker rm $_ *>$null\n Write-Log (Get-Msg \"install.existing.removed\" -f $_)\n }\n }\n\n # --- Upgrade: inject extracted credentials into data volume (old-arch -> embedded) ---\n if ($credsTmp -and (Test-Path $credsTmp)) {\n $credsFiles = Get-ChildItem -Path $credsTmp -Filter \"*.env\" -ErrorAction SilentlyContinue\n if ($credsFiles -and $credsFiles.Count -gt 0) {\n $cleanupCtr = \"agentteams-upgrade-cleanup\"\n docker rm -f $cleanupCtr 2>$null | Out-Null\n $credsDockerPath = ConvertTo-DockerPath -Path $credsTmp\n $injectShell = \"rm -rf /data/worker-creds && mkdir -p /data/worker-creds && cp /creds/*.env /data/worker-creds/ 2>/dev/null || true && chmod 600 /data/worker-creds/*.env 2>/dev/null || true\"\n docker run --rm --name $cleanupCtr --entrypoint sh `\n -v \"$($config.DATA_DIR):/data\" `\n -v \"${credsDockerPath}:/creds:ro\" `\n $script:EMBEDDED_IMAGE `\n -c $injectShell 2>$null | Out-Null\n if ($LASTEXITCODE -eq 0) {\n Write-Log \"Injected credentials for upgrade\"\n } else {\n Write-Log \"Warning: credential injection failed, continuing\"\n }\n }\n Remove-Item -Path $credsTmp -Recurse -Force -ErrorAction SilentlyContinue\n }\n\n if ($script:AGENTTEAMS_USE_EMBEDDED -eq \"1\") {\n # ============================================================\n # New architecture: embedded controller + auto-created manager\n # (controller container hosts Higress / Tuwunel / MinIO / Element Web /\n # controller binary, then spawns the lightweight manager container)\n # ============================================================\n\n # Internal port: 8080 (Higress gateway inside the controller container).\n $internalGwPort = 8080\n\n $matrixDomain = if ($config.MATRIX_DOMAIN) {\n $config.MATRIX_DOMAIN\n } else {\n \"matrix-local.agentteams.io:$($config.PORT_GATEWAY)\"\n }\n $aigwDomain = if ($config.AI_GATEWAY_DOMAIN) { $config.AI_GATEWAY_DOMAIN } else { \"aigw-local.agentteams.io\" }\n if ($aigwDomain -notmatch \":\") { $aigwDomain = \"${aigwDomain}:${internalGwPort}\" }\n $fsDomain = if ($config.FS_DOMAIN) { $config.FS_DOMAIN } else { \"fs-local.agentteams.io\" }\n if ($fsDomain -notmatch \":\") { $fsDomain = \"${fsDomain}:${internalGwPort}\" }\n\n $ctrlArgs = @(\n \"run\", \"-d\",\n \"--name\", \"agentteams-controller\",\n \"--network\", \"agentteams-net\",\n \"--network-alias\", \"matrix-local.agentteams.io\",\n \"--network-alias\", \"aigw-local.agentteams.io\",\n \"--network-alias\", \"fs-local.agentteams.io\",\n \"-e\", \"AGENTTEAMS_ADMIN_USER=$($config.ADMIN_USER)\",\n \"-e\", \"AGENTTEAMS_ADMIN_PASSWORD=$($config.ADMIN_PASSWORD)\",\n \"-e\", \"AGENTTEAMS_MANAGER_PASSWORD=$($config.MANAGER_PASSWORD)\",\n \"-e\", \"AGENTTEAMS_REGISTRATION_TOKEN=$($config.REGISTRATION_TOKEN)\",\n \"-e\", \"AGENTTEAMS_MINIO_USER=$($config.MINIO_USER)\",\n \"-e\", \"AGENTTEAMS_MINIO_PASSWORD=$($config.MINIO_PASSWORD)\",\n \"-e\", \"AGENTTEAMS_LLM_PROVIDER=$($config.LLM_PROVIDER)\",\n \"-e\", \"AGENTTEAMS_LLM_API_KEY=$($config.LLM_API_KEY)\",\n \"-e\", \"AGENTTEAMS_DEFAULT_MODEL=$($config.DEFAULT_MODEL)\",\n \"-e\", \"AGENTTEAMS_MANAGER_GATEWAY_KEY=$($config.MANAGER_GATEWAY_KEY)\",\n \"-e\", \"AGENTTEAMS_MANAGER_RUNTIME=$($config.MANAGER_RUNTIME)\",\n \"-e\", \"AGENTTEAMS_MANAGER_IMAGE=$managerImage\",\n \"-e\", \"AGENTTEAMS_DEFAULT_WORKER_RUNTIME=$($config.DEFAULT_WORKER_RUNTIME)\",\n \"-e\", \"AGENTTEAMS_WORKER_IMAGE=$($script:WORKER_IMAGE)\",\n \"-e\", \"AGENTTEAMS_COPAW_WORKER_IMAGE=$($script:COPAW_WORKER_IMAGE)\",\n \"-e\", \"AGENTTEAMS_QWENPAW_WORKER_IMAGE=$($script:QWENPAW_WORKER_IMAGE)\",\n \"-e\", \"AGENTTEAMS_HERMES_WORKER_IMAGE=$($script:HERMES_WORKER_IMAGE)\",\n \"-e\", \"AGENTTEAMS_MATRIX_DOMAIN=$matrixDomain\",\n \"-e\", \"AGENTTEAMS_ELEMENT_HOMESERVER_URL=http://127.0.0.1:$($config.PORT_GATEWAY)\",\n \"-e\", \"AGENTTEAMS_MATRIX_URL=http://127.0.0.1:6167\",\n \"-e\", \"AGENTTEAMS_MATRIX_E2EE=$($config.MATRIX_E2EE)\",\n \"-e\", \"AGENTTEAMS_MINIO_ENDPOINT=http://127.0.0.1:9000\",\n \"-e\", \"AGENTTEAMS_MINIO_BUCKET=agentteams-storage\",\n \"-e\", \"AGENTTEAMS_STORAGE_PREFIX=agentteams/agentteams-storage\",\n \"-e\", \"AGENTTEAMS_FS_ENDPOINT=http://127.0.0.1:9000\",\n \"-e\", \"AGENTTEAMS_AI_GATEWAY_URL=http://$aigwDomain\",\n \"-e\", \"AGENTTEAMS_CONTROLLER_URL=http://agentteams-controller:8090\",\n \"-e\", \"AGENTTEAMS_DOCKER_NETWORK=agentteams-net\",\n \"-e\", \"AGENTTEAMS_WORKSPACE_DIR=$($config.WORKSPACE_DIR)\",\n \"-e\", \"AGENTTEAMS_HOST_SHARE_DIR=$($config.HOST_SHARE_DIR)\",\n \"-e\", \"AGENTTEAMS_MANAGER_ENABLED=true\",\n \"-e\", \"AGENTTEAMS_PORT_MANAGER_CONSOLE=$($config.PORT_MANAGER_CONSOLE)\"\n )\n\n if ($script:AGENTTEAMS_TIMEZONE) {\n $ctrlArgs += @(\"-e\", \"TZ=$($script:AGENTTEAMS_TIMEZONE)\")\n }\n if ($env:AGENTTEAMS_YOLO -eq \"1\") {\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_YOLO=1\")\n }\n if ($env:AGENTTEAMS_MATRIX_DEBUG -eq \"1\") {\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_MATRIX_DEBUG=1\")\n }\n if ($config.GITHUB_TOKEN) {\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_GITHUB_TOKEN=$($config.GITHUB_TOKEN)\")\n }\n if ($config.EMBEDDING_MODEL) {\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_EMBEDDING_MODEL=$($config.EMBEDDING_MODEL)\")\n }\n\n # Optional: custom model capability overrides (context window, max\n # tokens, vision, reasoning). These are read by the Controller's\n # config.go LoadConfig to annotate custom models not in the built-in\n # presets table.\n if ($config.MODEL_CONTEXT_WINDOW) {\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_MODEL_CONTEXT_WINDOW=$($config.MODEL_CONTEXT_WINDOW)\")\n }\n if ($config.MODEL_MAX_TOKENS) {\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_MODEL_MAX_TOKENS=$($config.MODEL_MAX_TOKENS)\")\n }\n if ($config.MODEL_VISION) {\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_MODEL_VISION=$($config.MODEL_VISION)\")\n }\n if ($config.MODEL_REASONING) {\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_MODEL_REASONING=$($config.MODEL_REASONING)\")\n }\n\n if ($config.OPENAI_BASE_URL) {\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_OPENAI_BASE_URL=$($config.OPENAI_BASE_URL)\")\n }\n if ($env:AGENTTEAMS_AI_STREAM_IDLE_TIMEOUT_SECONDS) {\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_AI_STREAM_IDLE_TIMEOUT_SECONDS=$($env:AGENTTEAMS_AI_STREAM_IDLE_TIMEOUT_SECONDS)\")\n }\n if ($script:AGENTTEAMS_LANGUAGE) {\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_LANGUAGE=$($script:AGENTTEAMS_LANGUAGE)\")\n }\n\n # Optional: CMS/ARMS observability. In embedded mode the controller\n # spawns the Manager and Workers, so it must receive these settings.\n $cmsTracesEnabled = if ($env:AGENTTEAMS_CMS_TRACES_ENABLED) { $env:AGENTTEAMS_CMS_TRACES_ENABLED } else { \"false\" }\n $cmsServiceName = if ($env:AGENTTEAMS_CMS_SERVICE_NAME) { $env:AGENTTEAMS_CMS_SERVICE_NAME } else { \"agentteams-manager\" }\n $cmsMetricsEnabled = if ($env:AGENTTEAMS_CMS_METRICS_ENABLED) { $env:AGENTTEAMS_CMS_METRICS_ENABLED } else { \"false\" }\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_CMS_TRACES_ENABLED=$cmsTracesEnabled\")\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_CMS_SERVICE_NAME=$cmsServiceName\")\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_CMS_METRICS_ENABLED=$cmsMetricsEnabled\")\n if ($env:AGENTTEAMS_CMS_ENDPOINT) {\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_CMS_ENDPOINT=$($env:AGENTTEAMS_CMS_ENDPOINT)\")\n }\n if ($env:AGENTTEAMS_CMS_LICENSE_KEY) {\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_CMS_LICENSE_KEY=$($env:AGENTTEAMS_CMS_LICENSE_KEY)\")\n }\n if ($env:AGENTTEAMS_CMS_PROJECT) {\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_CMS_PROJECT=$($env:AGENTTEAMS_CMS_PROJECT)\")\n }\n if ($env:AGENTTEAMS_CMS_WORKSPACE) {\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_CMS_WORKSPACE=$($env:AGENTTEAMS_CMS_WORKSPACE)\")\n }\n\n # Mount the docker socket so the controller can spawn manager + workers.\n $ctrlArgs += @(\"-v\", \"//var/run/docker.sock:/var/run/docker.sock\")\n $ctrlArgs += @(\"--security-opt\", \"label=disable\")\n\n # Persistent data + workspace mounts (manager workspace is bind-mounted under\n # /root/agentteams-fs/agents/manager so the controller can hand it to the spawned\n # manager container).\n $ctrlArgs += @(\"-v\", \"$($config.DATA_DIR):/data\")\n $wsDockerPath = ConvertTo-DockerPath -Path $config.WORKSPACE_DIR\n $ctrlArgs += @(\"-v\", \"${wsDockerPath}:/root/agentteams-fs/agents/manager\")\n Write-Log (Get-Msg \"host_share.sharing\" -f $config.HOST_SHARE_DIR)\n\n # Externally exposed ports — only the gateway / Higress console / Element Web,\n # since the manager console is now spawned inside its own container by the\n # controller (port mapping is handled there).\n $ctrlArgs += @(\"-p\", \"${portPrefix}$($config.PORT_GATEWAY):8080\")\n $ctrlArgs += @(\"-p\", \"${portPrefix}$($config.PORT_CONSOLE):8001\")\n $ctrlArgs += @(\"-p\", \"${portPrefix}$($config.PORT_ELEMENT_WEB):8088\")\n\n $ctrlArgs += @(\"--restart\", \"unless-stopped\")\n $ctrlArgs += $script:EMBEDDED_IMAGE\n\n Write-Log (Get-Msg \"install.starting_manager\")\n & docker $ctrlArgs\n Write-Log \"Embedded controller started: agentteams-controller\"\n\n # Wait for infra inside the controller container.\n function Wait-EmbeddedUrl {\n param([string]$Url, [string]$Container, [int]$MaxWait, [string]$Description)\n $elapsed = 0\n Write-Log \"Waiting for $Description...\"\n while ($elapsed -lt $MaxWait) {\n docker exec $Container curl -sf $Url *>$null\n if ($LASTEXITCODE -eq 0) {\n Write-Log \"$Description is ready (${elapsed}s)\"\n return $true\n }\n Start-Sleep -Seconds 2\n $elapsed += 2\n }\n Write-Host \"$($script:ESC)[31m[AgentTeams ERROR]$($script:ESC)[0m $Description not ready after ${MaxWait}s\" -ForegroundColor Red\n return $false\n }\n\n if (-not (Wait-EmbeddedUrl \"http://127.0.0.1:6167/_tuwunel/server_version\" \"agentteams-controller\" 120 \"Tuwunel (Matrix)\")) { Exit-Script 1 }\n if (-not (Wait-EmbeddedUrl \"http://127.0.0.1:9000/minio/health/live\" \"agentteams-controller\" 60 \"MinIO\")) { Exit-Script 1 }\n if (-not (Wait-EmbeddedUrl \"http://127.0.0.1:8080/status\" \"agentteams-controller\" 120 \"Higress Gateway\")) { Exit-Script 1 }\n\n # Wait for the controller to spawn the Manager Agent container.\n Write-Log \"Waiting for Manager Agent container...\"\n $mgrWait = 0\n $mgrMax = 300\n while ($mgrWait -lt $mgrMax) {\n $found = docker ps --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-manager$\"\n if ($found) {\n Write-Log \"Manager Agent container detected (${mgrWait}s)\"\n break\n }\n Start-Sleep -Seconds 3\n $mgrWait += 3\n }\n if ($mgrWait -ge $mgrMax) {\n Write-Host \"$($script:ESC)[31m[AgentTeams ERROR]$($script:ESC)[0m Manager Agent container not created after ${mgrMax}s\" -ForegroundColor Red\n Write-Log \"Controller logs:\"\n docker exec agentteams-controller tail -30 /var/log/agentteams/agentteams-controller-error.log 2>$null\n Exit-Script 1\n }\n\n # Wait for Manager Agent container to reach `running` state.\n Write-Log \"Waiting for Manager Agent to start...\"\n $agentWait = 0\n while ($agentWait -lt 120) {\n $state = docker inspect --format \"{{.State.Status}}\" agentteams-manager 2>$null\n if ($state -eq \"running\") {\n Write-Log \"Manager Agent is running\"\n break\n }\n Start-Sleep -Seconds 2\n $agentWait += 2\n }\n\n if ($env:AGENTTEAMS_YOLO -eq \"1\") {\n docker exec agentteams-manager touch /root/manager-workspace/yolo-mode 2>$null\n }\n\n # Wait for the Manager Agent's runtime + Matrix to be reachable. In embedded\n # mode Tuwunel lives in `agentteams-controller`, so the Matrix probe must target\n # the controller (the manager only exposes the agent runtime).\n Wait-ManagerReady -Container \"agentteams-manager\"\n Wait-MatrixReady -Container \"agentteams-controller\"\n\n # Wait for the controller to actually deliver the first-boot welcome\n # message. Gated by the controller on (a) Manager joining the DM room\n # and (b) Higress WASM key-auth propagating to /v1/chat/completions for\n # the Manager's gateway key — typically ~45-90s on a fresh install.\n # We poll Manager CR status.welcomeSent via the in-container AgentTeams\n # CLI, exec'd inside agentteams-controller (the source-of-truth container\n # — its bundled CLI binary is always in lockstep with the controller\n # binary serving the HTTP API, since they're the same `go build`\n # output. The agentteams-manager container's CLI may lag the controller\n # across image upgrades and silently drop the welcomeSent field,\n # leaving this loop hung). The controller mints a long-lived admin\n # SA token at startup and writes it to\n # AGENTTEAMS_AUTH_TOKEN_FILE=/var/run/agentteams/cli-token (set as a Dockerfile\n # ENV default), so a bare `docker exec agentteams-controller agt …`\n # auto-discovers both the endpoint and the token. The brief window\n # after container start before bootstrapAdminCLIToken completes is\n # absorbed by the loop's silent retry.\n $hasAgentTeamsCli = $false\n try {\n docker exec agentteams-controller sh -c 'command -v agt' *> $null\n if ($LASTEXITCODE -eq 0) { $hasAgentTeamsCli = $true }\n } catch {}\n\n if ($hasAgentTeamsCli) {\n Write-Log (Get-Msg \"install.welcome_msg.waiting\")\n $welcomeMax = if ($env:AGENTTEAMS_WELCOME_TIMEOUT) { [int]$env:AGENTTEAMS_WELCOME_TIMEOUT } else { 300 }\n $welcomeWait = 0\n $welcomeDone = $false\n while ($welcomeWait -lt $welcomeMax) {\n $wjson = \"\"\n try {\n $wjson = docker exec agentteams-controller `\n agt get managers default -o json 2>$null\n } catch {}\n if ($wjson -and ($wjson -replace '\\s', '') -match '\"welcomeSent\":true') {\n Write-Log (Get-Msg \"install.welcome_msg.confirmed\" $welcomeWait)\n $welcomeDone = $true\n break\n }\n Start-Sleep -Seconds 3\n $welcomeWait += 3\n }\n if (-not $welcomeDone) {\n # Non-fatal: install is still good. Keep going to the success\n # banner so the admin can use Element Web to nudge Manager into\n # onboarding manually (one DM message is enough).\n Write-Log (Get-Msg \"install.welcome_msg.timeout\" $welcomeMax)\n Write-Log (Get-Msg \"install.welcome_msg.timeout_hint\")\n Write-Log (Get-Msg \"install.welcome_msg.timeout_inspect\")\n }\n } else {\n Write-Log (Get-Msg \"install.welcome_msg.poll_unavailable\")\n }\n } else {\n # Run container (legacy path)\n Write-Log (Get-Msg \"install.starting_manager\")\n & docker $dockerArgs\n\n Wait-ManagerReady -Container \"agentteams-manager\"\n Wait-MatrixReady -Container \"agentteams-manager\"\n }\n\n # Create OpenAI-compatible provider if needed\n if ($config.LLM_PROVIDER -eq \"openai-compat\") {\n New-OpenAICompatProvider -BaseUrl $config.OPENAI_BASE_URL -ApiKey $config.LLM_API_KEY -ConsolePort ([int]$config.PORT_CONSOLE)\n }\n\n # Print success message\n Write-Log \"\"\n Write-Log (Get-Msg \"success.title\")\n Write-Log \"\"\n Write-Log (Get-Msg \"success.domains_configured\")\n Write-Log \" $($config.MATRIX_DOMAIN.Split(':')[0]) $($config.MATRIX_CLIENT_DOMAIN) $($config.AI_GATEWAY_DOMAIN) $($config.FS_DOMAIN) $($config.CONSOLE_DOMAIN)\"\n Write-Log \"\"\n\n $lanIP = Get-LanIP\n\n Write-Host \"$($script:ESC)[33m===============================================================$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.open_url')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[1;36m http://127.0.0.1:$($config.PORT_ELEMENT_WEB)/#/login$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.login_with')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.username' -f $config.ADMIN_USER)$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.password' -f $config.ADMIN_PASSWORD)$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.after_login')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.tell_it')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.manager_auto')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m --------------------------------------------------------------- $($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.mobile_title')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $($script:ESC)[0m\"\n if ($lanIP) {\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.mobile_step1')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.mobile_step2' -f \"http://${lanIP}:$($config.PORT_GATEWAY)\")$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.mobile_step3')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.mobile_username' -f $config.ADMIN_USER)$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.mobile_password' -f $config.ADMIN_PASSWORD)$($script:ESC)[0m\"\n } else {\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.mobile_step1')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.mobile_step2_noip' -f $config.PORT_GATEWAY)$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.mobile_noip_hint')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.mobile_step3')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.mobile_username' -f $config.ADMIN_USER)$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.mobile_password' -f $config.ADMIN_PASSWORD)$($script:ESC)[0m\"\n }\n Write-Host \"$($script:ESC)[33m $($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m===============================================================$($script:ESC)[0m\"\n\n Write-Log \"\"\n Write-Log (Get-Msg \"success.other_consoles\")\n Write-Log (Get-Msg \"success.higress_console\" -f $config.PORT_CONSOLE, $config.ADMIN_USER, $config.ADMIN_PASSWORD)\n if ($script:AGENTTEAMS_USE_EMBEDDED -ne \"1\") {\n # In embedded mode the manager runs in its own auto-spawned container with\n # its own console-port mapping handled by the controller, so don't print a\n # host-side URL/credentials hint here.\n Write-Log (Get-Msg \"success.manager_console\" -f $config.PORT_MANAGER_CONSOLE)\n Write-Log (Get-Msg \"success.manager_console_gateway\" -f $config.ADMIN_USER, $config.ADMIN_PASSWORD)\n }\n Write-Log \"\"\n Write-Log (Get-Msg \"success.switch_llm.title\")\n Write-Log (Get-Msg \"success.switch_llm.hint\")\n Write-Log (Get-Msg \"success.switch_llm.docs\")\n Write-Log (Get-Msg \"success.switch_llm.url\")\n Write-Log \"\"\n Write-Log (Get-Msg \"success.tip\")\n Write-Log \"\"\n if ($config.LOCAL_ONLY -ne \"1\") {\n Write-Host (Get-Msg \"port.local_only.https_hint\") -ForegroundColor Yellow\n Write-Log \"\"\n }\n Write-Log (Get-Msg \"success.config_file\" -f $script:AGENTTEAMS_ENV_FILE)\n\n Write-Log (Get-Msg \"success.data_volume\" -f $config.DATA_DIR)\n\n Write-Log (Get-Msg \"success.workspace\" -f $config.WORKSPACE_DIR)\n}\n\n# ============================================================\n# Worker Installation\n# ============================================================\n\nfunction Install-Worker {\n param(\n [string]$Name,\n [string]$Fs,\n [string]$FsKey,\n [string]$FsSecret,\n [switch]$Reset,\n [switch]$FindSkills,\n [string]$SkillsApiUrl\n )\n\n # Validate required parameters\n if (-not $Name) {\n Write-Error (Get-Msg \"error.name_required\")\n }\n if (-not $Fs) {\n Write-Error (Get-Msg \"error.fs_required\")\n }\n if (-not $FsKey) {\n Write-Error (Get-Msg \"error.fs_key_required\")\n }\n if (-not $FsSecret) {\n Write-Error (Get-Msg \"error.fs_secret_required\")\n }\n\n $containerName = \"agentteams-worker-$Name\"\n\n # Handle reset\n if ($Reset) {\n Write-Log (Get-Msg \"worker.resetting\" -f $Name)\n docker stop $containerName *>$null\n docker rm $containerName *>$null\n }\n\n # Check for existing container\n $existing = docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^$containerName$\"\n if ($existing) {\n Write-Error (Get-Msg \"worker.exists\" -f $containerName)\n }\n\n # Detect timezone and registry\n $timezone = Get-AgentTeamsTimeZone\n $registry = Get-Registry -Timezone $timezone\n $workerImage = if ($env:AGENTTEAMS_INSTALL_WORKER_IMAGE) {\n $env:AGENTTEAMS_INSTALL_WORKER_IMAGE\n } else {\n \"$registry/agentteams/agentteams-worker:$($script:AGENTTEAMS_VERSION)\"\n }\n\n Write-Log (Get-Msg \"worker.starting\" -f $Name)\n\n $dockerArgs = @(\n \"run\", \"-d\",\n \"--name\", $containerName,\n \"-e\", \"HOME=/root/agentteams-fs/agents/$Name\",\n \"-w\", \"/root/agentteams-fs/agents/$Name\",\n \"-e\", \"AGENTTEAMS_WORKER_NAME=$Name\",\n \"-e\", \"AGENTTEAMS_FS_ENDPOINT=$Fs\",\n \"-e\", \"AGENTTEAMS_FS_ACCESS_KEY=$FsKey\",\n \"-e\", \"AGENTTEAMS_FS_SECRET_KEY=$FsSecret\"\n )\n\n if (-not $SkillsApiUrl) {\n if ($env:AGENTTEAMS_SKILLS_API_URL) {\n $SkillsApiUrl = $env:AGENTTEAMS_SKILLS_API_URL\n } else {\n $SkillsApiUrl = \"nacos://market.agentteams.io:80/public\"\n }\n }\n\n if ($FindSkills -and $SkillsApiUrl) {\n $dockerArgs += @(\"-e\", \"SKILLS_API_URL=$SkillsApiUrl\")\n Write-Log (Get-Msg \"worker.skills_url\" -f $SkillsApiUrl)\n }\n if ($env:AGENTTEAMS_NACOS_USERNAME) {\n $dockerArgs += @(\"-e\", \"AGENTTEAMS_NACOS_USERNAME=$($env:AGENTTEAMS_NACOS_USERNAME)\")\n }\n if ($env:AGENTTEAMS_NACOS_PASSWORD) {\n $dockerArgs += @(\"-e\", \"AGENTTEAMS_NACOS_PASSWORD=$($env:AGENTTEAMS_NACOS_PASSWORD)\")\n }\n if ($env:AGENTTEAMS_NACOS_TOKEN) {\n $dockerArgs += @(\"-e\", \"AGENTTEAMS_NACOS_TOKEN=$($env:AGENTTEAMS_NACOS_TOKEN)\")\n }\n\n $dockerArgs += @(\"--restart\", \"unless-stopped\", $workerImage)\n\n & docker $dockerArgs\n\n Write-Log \"\"\n Write-Log (Get-Msg \"worker.started\" -f $Name)\n Write-Log (Get-Msg \"worker.container\" -f $containerName)\n Write-Log (Get-Msg \"worker.view_logs\" -f $containerName)\n}\n\n# ============================================================\n# Uninstall\n# ============================================================\n\nfunction Uninstall-AgentTeams {\n Write-Log (Get-Msg \"uninstall.title\")\n\n # Stop and remove manager\n $manager = docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-manager$\"\n if ($manager) {\n Write-Log (Get-Msg \"uninstall.stopping_manager\")\n docker stop agentteams-manager *>$null\n docker rm agentteams-manager *>$null\n }\n\n # Stop and remove workers\n $workers = docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-worker-\"\n if ($workers) {\n Write-Log (Get-Msg \"uninstall.stopping_workers\")\n $workers | ForEach-Object {\n docker stop $_ *>$null\n docker rm $_ *>$null\n Write-Log (Get-Msg \"uninstall.removed\" -f $_)\n }\n }\n\n # Stop and remove docker-proxy (legacy <= v1.0.x; current arch uses\n # agentteams-controller for the same role)\n $proxy = docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-docker-proxy$\"\n if ($proxy) {\n Write-Log (Get-Msg \"uninstall.removing_proxy\")\n docker stop agentteams-docker-proxy *>$null\n docker rm agentteams-docker-proxy *>$null\n }\n\n # Stop and remove the embedded controller container. MUST happen\n # before the `docker volume rm agentteams-data` step below -- in embedded\n # mode agentteams-controller mounts agentteams-data at /data (Tuwunel DB,\n # MinIO state, Higress state, all the room messages), and `volume rm`\n # against an in-use volume fails silently. Skipping this used to\n # leave room/message history behind across \"uninstall + reinstall\"\n # cycles. See PR #692.\n $controller = docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-controller$\"\n if ($controller) {\n Write-Log (Get-Msg \"uninstall.stopping_controller\")\n docker stop agentteams-controller *>$null\n docker rm agentteams-controller *>$null\n }\n\n # Remove Docker volume (read custom name from env file if available)\n $dataVolume = \"agentteams-data\"\n if (Test-Path $script:AGENTTEAMS_ENV_FILE) {\n $envContent = Get-Content $script:AGENTTEAMS_ENV_FILE -ErrorAction SilentlyContinue\n $dataLine = $envContent | Select-String \"^AGENTTEAMS_DATA_DIR=\"\n if ($dataLine) {\n $parsed = ($dataLine -split \"=\", 2)[1]\n if ($parsed) { $dataVolume = $parsed }\n }\n }\n $volume = docker volume ls -q 2>$null | Select-String \"^${dataVolume}$\"\n if ($volume) {\n Write-Log (Get-Msg \"uninstall.removing_volume\")\n docker volume rm $dataVolume *>$null\n }\n\n # Remove Docker network\n $network = docker network ls --format \"{{.Name}}\" 2>$null | Select-String \"^agentteams-net$\"\n if ($network) {\n Write-Log (Get-Msg \"uninstall.removing_network\")\n docker network rm agentteams-net *>$null\n }\n\n # Remove workspace directory\n $workspaceDir = \"$env:USERPROFILE\\agentteams-manager\"\n if (Test-Path $script:AGENTTEAMS_ENV_FILE) {\n $envContent = Get-Content $script:AGENTTEAMS_ENV_FILE -ErrorAction SilentlyContinue\n $wsLine = $envContent | Select-String \"^AGENTTEAMS_WORKSPACE_DIR=\"\n if ($wsLine) {\n $workspaceDir = ($wsLine -split \"=\", 2)[1]\n }\n }\n if ($workspaceDir -and (Test-Path $workspaceDir)) {\n Write-Log (Get-Msg \"uninstall.removing_workspace\" -f $workspaceDir)\n Remove-Item -Recurse -Force $workspaceDir -ErrorAction SilentlyContinue\n }\n\n # Remove env file\n if (Test-Path $script:AGENTTEAMS_ENV_FILE) {\n Write-Log (Get-Msg \"uninstall.removing_env\" -f $script:AGENTTEAMS_ENV_FILE)\n Remove-Item -Force $script:AGENTTEAMS_ENV_FILE\n }\n\n # Remove install log (stop transcript first to release the file)\n if (Test-Path $script:AGENTTEAMS_LOG_FILE) {\n Write-Log (Get-Msg \"uninstall.removing_log\" -f $script:AGENTTEAMS_LOG_FILE)\n try { Stop-Transcript *>$null } catch {}\n Remove-Item -Force $script:AGENTTEAMS_LOG_FILE -ErrorAction SilentlyContinue\n }\n\n Write-Log \"\"\n Write-Log (Get-Msg \"uninstall.done\")\n}\n\n# ============================================================\n# Main Entry Point\n# ============================================================\n\ntry {\n switch ($Command) {\n \"manager\" {\n Install-Manager\n }\n \"worker\" {\n Install-Worker -Name $Name -Fs $Fs -FsKey $FsKey -FsSecret $FsSecret -Reset:$Reset -FindSkills:$FindSkills -SkillsApiUrl $SkillsApiUrl\n }\n \"uninstall\" {\n Uninstall-AgentTeams\n }\n }\n} catch {\n Exit-Script 1\n}\n\n# Stop transcript logging\ntry {\n Stop-Transcript -ErrorAction SilentlyContinue | Out-Null\n} catch {\n # Ignore errors when stopping transcript\n}\n"}, "ce3a4770fb177d56db1ca445f92ae9030b9633b7": {"t": 1785764735.8637362, "n": 192747, "h": "0a314e12284e046a", "c": "#!/usr/bin/env powershell\n# agentteams-install.ps1 - One-click installation for AgentTeams Manager and Worker on Windows\n# Requires PowerShell 7.0+ (recommended)\n#\n# Usage:\n# .\\agentteams-install.ps1 # Interactive installation (choose Quick Start or Manual)\n# .\\agentteams-install.ps1 manager # Same as above (explicit)\n# .\\agentteams-install.ps1 worker --name <name> ... # Worker installation\n#\n# Onboarding Modes:\n# Quick Start - Fast installation with all default values (recommended)\n# Manual - Customize each option step by step\n#\n# Environment variables (for automation):\n# AGENTTEAMS_NON_INTERACTIVE Skip all prompts, use defaults (default: 0)\n# AGENTTEAMS_LLM_PROVIDER LLM provider (default: openai-compat for zh non-interactive Token Plan; qwen for en)\n# AGENTTEAMS_DEFAULT_MODEL Default model (default: qwen3.6-plus for zh Token Plan and en non-interactive)\n# AGENTTEAMS_OPENAI_BASE_URL OpenAI-compatible base URL (default for zh non-interactive: Alibaba Token Plan endpoint)\n# AGENTTEAMS_LLM_API_KEY LLM API key (required)\n# AGENTTEAMS_ADMIN_USER Admin username (default: admin)\n# AGENTTEAMS_ADMIN_PASSWORD Admin password (auto-generated if not set, min 8 chars)\n# AGENTTEAMS_MATRIX_DOMAIN Matrix domain (default: matrix-local.agentteams.io:18080)\n# AGENTTEAMS_MOUNT_SOCKET Mount container runtime socket (default: 1)\n# AGENTTEAMS_MATRIX_E2EE Matrix E2EE (default: 0, disabled)\n# AGENTTEAMS_DATA_DIR Docker volume name for persistent data (default: agentteams-data)\n# AGENTTEAMS_WORKSPACE_DIR Host directory for manager workspace (default: ~/agentteams-manager)\n# AGENTTEAMS_VERSION Image tag (default: latest)\n# AGENTTEAMS_REGISTRY Image registry (default: auto-detected by timezone)\n# AGENTTEAMS_INSTALL_MANAGER_IMAGE Override manager image (e.g., local build)\n# AGENTTEAMS_INSTALL_WORKER_IMAGE Override worker image (e.g., local build)\n# AGENTTEAMS_INSTALL_COPAW_WORKER_IMAGE Override copaw worker image (e.g., local build)\n# AGENTTEAMS_INSTALL_QWENPAW_WORKER_IMAGE Override qwenpaw worker image (e.g., local build)\n# AGENTTEAMS_INSTALL_HERMES_WORKER_IMAGE Override hermes worker image (e.g., local build)\n# AGENTTEAMS_PORT_GATEWAY Host port for Higress gateway (default: 18080)\n# AGENTTEAMS_PORT_CONSOLE Host port for Higress console (default: 18001)\n# AGENTTEAMS_PORT_ELEMENT_WEB Host port for Element Web direct access (default: 18088)\n\n#Requires -Version 5.1\n\n[CmdletBinding()]\nparam(\n [Parameter(Position = 0)]\n [ValidateSet(\"manager\", \"worker\", \"uninstall\")]\n [string]$Command = \"manager\",\n\n # Worker options\n [string]$Name,\n [string]$Fs,\n [string]$FsKey,\n [string]$FsSecret,\n [switch]$Reset,\n [switch]$FindSkills,\n [string]$SkillsApiUrl,\n\n # General options\n [switch]$NonInteractive,\n [string]$EnvFile\n)\n\n# ============================================================\n# Configuration\n# ============================================================\n\n$script:AGENTTEAMS_VERSION = if ($env:AGENTTEAMS_VERSION) { $env:AGENTTEAMS_VERSION } else { \"latest\" }\n$script:AGENTTEAMS_NON_INTERACTIVE = if ($env:AGENTTEAMS_NON_INTERACTIVE -eq \"1\" -or $NonInteractive) { $true } else { $false }\n$script:AGENTTEAMS_MOUNT_SOCKET = if ($env:AGENTTEAMS_MOUNT_SOCKET -eq \"0\") { $false } else { $true }\n$script:AGENTTEAMS_ENV_FILE = if ($EnvFile) { $EnvFile } elseif ($env:AGENTTEAMS_ENV_FILE) { $env:AGENTTEAMS_ENV_FILE } else { \"$env:USERPROFILE\\agentteams-manager.env\" }\n$script:StepResult = \"\" # Used by state machine to signal \"back\" navigation\n$script:config = @{} # Shared config hashtable for step functions\n\n# ANSI escape character for terminal colors\n$script:ESC = [char]0x1B\n\n# ============================================================\n# Log all output to file\n# ============================================================\n\n$script:AGENTTEAMS_LOG_FILE = \"$env:USERPROFILE\\agentteams-install.log\"\n\n# Start transcript for logging (PowerShell's built-in logging mechanism)\ntry {\n Start-Transcript -Path $script:AGENTTEAMS_LOG_FILE -Append -ErrorAction SilentlyContinue | Out-Null\n} catch {\n # If transcript fails, continue without logging\n}\n\nWrite-Host \"\"\nWrite-Host \"========================================\"\nWrite-Host \"AgentTeams Installation Log\"\nWrite-Host \"Started: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')\"\nWrite-Host \"User: $env:USERNAME\"\nWrite-Host \"System: $(hostname)\"\nWrite-Host \"Log file: $($script:AGENTTEAMS_LOG_FILE)\"\nWrite-Host \"========================================\"\nWrite-Host \"\"\n\n# ============================================================\n# Utility Functions\n# ============================================================\n\nfunction Write-Log {\n param([string]$Message)\n Write-Host \"$($script:ESC)[36m[AgentTeams]$($script:ESC)[0m $Message\"\n}\n\nfunction Write-Error {\n param([string]$Message)\n Write-Host \"$($script:ESC)[31m[AgentTeams ERROR]$($script:ESC)[0m $Message\" -ForegroundColor Red\n throw $Message\n}\n\nfunction Write-Warning {\n param([string]$Message)\n Write-Host \"$($script:ESC)[33m[AgentTeams WARNING]$($script:ESC)[0m $Message\"\n}\n\n# Pause before exit on error so user can read the message when running via double-click\nfunction Exit-Script {\n param([int]$ExitCode = 0)\n if ($ExitCode -ne 0 -and -not $script:AGENTTEAMS_NON_INTERACTIVE) {\n Write-Host \"\"\n Write-Host \"Press Enter to exit.\" -NoNewline\n $Host.UI.ReadLine()\n }\n exit $ExitCode\n}\n\nfunction Test-DockerRunning {\n try {\n $null = docker info 2>&1\n if ($LASTEXITCODE -ne 0) {\n return $false\n }\n return $true\n }\n catch {\n return $false\n }\n}\n\nfunction Get-AgentTeamsTimeZone {\n try {\n if ($env:AGENTTEAMS_TIMEZONE) {\n return $env:AGENTTEAMS_TIMEZONE\n }\n\n $tz = (Get-TimeZone).Id\n # Convert Windows timezone to IANA format\n $tzMap = @{\n \"China Standard Time\" = \"Asia/Shanghai\"\n \"Pacific Standard Time\" = \"America/Los_Angeles\"\n \"Mountain Standard Time\" = \"America/Denver\"\n \"Central Standard Time\" = \"America/Chicago\"\n \"Eastern Standard Time\" = \"America/New_York\"\n \"GMT Standard Time\" = \"Europe/London\"\n \"Central European Standard Time\" = \"Europe/Berlin\"\n \"Tokyo Standard Time\" = \"Asia/Tokyo\"\n \"Singapore Standard Time\" = \"Asia/Singapore\"\n \"Korea Standard Time\" = \"Asia/Seoul\"\n \"India Standard Time\" = \"Asia/Kolkata\"\n }\n\n if ($tzMap.ContainsKey($tz)) {\n return $tzMap[$tz]\n }\n return $tz\n }\n catch {\n return \"Asia/Shanghai\"\n }\n}\n\nfunction Get-Registry {\n param([string]$Timezone)\n\n if ($env:AGENTTEAMS_REGISTRY) {\n return $env:AGENTTEAMS_REGISTRY\n }\n\n # Americas\n if ($Timezone -match \"^America/\") {\n return \"higress-registry.us-west-1.cr.aliyuncs.com\"\n }\n\n # Southeast Asia\n if ($Timezone -match \"^(Asia/Singapore|Asia/Bangkok|Asia/Jakarta|Asia/Makassar|Asia/Jayapura|Asia/Kuala_Lumpur|Asia/Ho_Chi_Minh|Asia/Manila|Asia/Yangon|Asia/Vientiane|Asia/Phnom_Penh|Asia/Pontianak|Asia/Ujung_Pandang)\") {\n return \"higress-registry.ap-southeast-7.cr.aliyuncs.com\"\n }\n\n # Default: China\n return \"higress-registry.cn-hangzhou.cr.aliyuncs.com\"\n}\n\nfunction Get-AgentTeamsLanguage {\n param([string]$Timezone)\n $chineseZones = @(\n \"Asia/Shanghai\", \"Asia/Chongqing\", \"Asia/Harbin\", \"Asia/Urumqi\",\n \"Asia/Taipei\", \"Asia/Hong_Kong\", \"Asia/Macau\"\n )\n if ($chineseZones -contains $Timezone) { return \"zh\" }\n return \"en\"\n}\n\n# ============================================================\n# Centralized message dictionary and Get-Msg function\n# ============================================================\n\n$script:Messages = @{\n # --- Timezone detection messages ---\n \"tz.warning.title\" = @{ zh = \"无法自动检测时区。\"; en = \"Could not detect timezone automatically.\" }\n \"tz.warning.prompt\" = @{ zh = \"请输入您的时区(例如 Asia/Shanghai、America/New_York)。\"; en = \"Please enter your timezone (e.g., Asia/Shanghai, America/New_York).\" }\n \"tz.default\" = @{ zh = \"使用默认时区: {0}\"; en = \"Using default timezone: {0}\" }\n \"tz.input_prompt\" = @{ zh = \"时区\"; en = \"Timezone\" }\n\n # --- Installation title and info ---\n \"install.title\" = @{ zh = \"=== AgentTeams Manager 安装 ===\"; en = \"=== AgentTeams Manager Installation ===\" }\n \"install.registry\" = @{ zh = \"镜像仓库: {0}\"; en = \"Registry: {0}\" }\n \"install.dir\" = @{ zh = \"安装目录: {0}\"; en = \"Installation directory: {0}\" }\n \"install.dir_hint\" = @{ zh = \" (env 文件 'agentteams-manager.env' 将保存到 HOME 目录。)\"; en = \" (The env file 'agentteams-manager.env' will be saved to your HOME directory.)\" }\n \"install.dir_hint2\" = @{ zh = \" (请从您希望管理此安装的目录运行此脚本。)\"; en = \" (Run this script from the directory where you want to manage this installation.)\" }\n\n # --- Onboarding mode ---\n \"install.mode.title\" = @{ zh = \"--- Onboarding 模式 ---\"; en = \"--- Onboarding Mode ---\" }\n \"install.mode.choose\" = @{ zh = \"选择安装模式:\"; en = \"Choose your installation mode:\" }\n \"install.mode.quickstart\" = @{ zh = \" 1) 快速开始 - 使用阿里云通义 Token 套餐快速安装(推荐)\"; en = \" 1) Quick Start - Fast installation with Qwen Cloud (recommended)\" }\n \"install.mode.manual\" = @{ zh = \" 2) 手动配置 - 选择 LLM 提供商并自定义选项\"; en = \" 2) Manual - Choose LLM provider and customize options\" }\n \"install.mode.prompt\" = @{ zh = \"请选择 [1/2]\"; en = \"Enter choice [1/2]\" }\n \"install.mode.quickstart_selected\" = @{ zh = \"已选择快速开始模式 - 使用阿里云通义 Token 套餐\"; en = \"Quick Start mode selected - using Qwen Cloud\" }\n \"install.mode.manual_selected\" = @{ zh = \"已选择手动配置模式 - 您将选择 LLM 提供商并自定义选项\"; en = \"Manual mode selected - you will choose LLM provider and customize options\" }\n \"install.mode.invalid\" = @{ zh = \"无效选择,默认使用快速开始模式\"; en = \"Invalid choice, defaulting to Quick Start mode\" }\n\n # --- Existing installation detected ---\n \"install.existing.detected\" = @{ zh = \"检测到已有 Manager 安装(env 文件: {0})\"; en = \"Existing Manager installation detected (env file: {0})\" }\n \"install.existing.choose\" = @{ zh = \"选择操作:\"; en = \"Choose an action:\" }\n \"install.existing.upgrade\" = @{ zh = \" 1) 就地升级(保留数据、工作空间、env 文件)\"; en = \" 1) In-place upgrade (keep data, workspace, env file)\" }\n \"install.existing.reinstall\" = @{ zh = \" 2) 全新重装(删除所有数据,重新开始)\"; en = \" 2) Clean reinstall (remove all data, start fresh)\" }\n \"install.existing.cancel\" = @{ zh = \" 3) 取消\"; en = \" 3) Cancel\" }\n \"install.existing.prompt\" = @{ zh = \"请选择 [1/2/3]\"; en = \"Enter choice [1/2/3]\" }\n \"install.existing.upgrade_noninteractive\" = @{ zh = \"非交互模式: 执行就地升级...\"; en = \"Non-interactive mode: performing in-place upgrade...\" }\n \"install.existing.upgrading\" = @{ zh = \"执行就地升级...\"; en = \"Performing in-place upgrade...\" }\n \"install.existing.warn_manager_stop\" = @{ zh = \"警告: Manager 容器将被停止并重新创建。\"; en = \"WARNING: Manager container will be stopped and recreated.\" }\n \"install.existing.warn_worker_recreate\" = @{ zh = \"警告: Worker 容器也将被重新创建(以更新 Manager IP)。\"; en = \"WARNING: Worker containers will also be recreated (to update Manager IP in hosts).\" }\n \"install.existing.continue_prompt\" = @{ zh = \"继续?[y/N]\"; en = \"Continue? [y/N]\" }\n \"install.existing.cancelled\" = @{ zh = \"安装已取消。\"; en = \"Installation cancelled.\" }\n \"install.existing.stopping_manager\" = @{ zh = \"停止并移除现有 manager 容器...\"; en = \"Stopping and removing existing manager container...\" }\n \"install.existing.stopping_workers\" = @{ zh = \"停止并移除现有 worker 容器...\"; en = \"Stopping and removing existing worker containers...\" }\n \"install.existing.removed\" = @{ zh = \" 已移除: {0}\"; en = \" Removed: {0}\" }\n\n # --- Upgrade mode sub-menu ---\n \"upgrade.mode.prompt\" = @{ zh = \"请选择升级方式:\"; en = \"Select upgrade mode:\" }\n \"upgrade.mode.keep_all\" = @{ zh = \" 1) 保留所有参数,一键升级(推荐)\"; en = \" 1) Keep all parameters, quick upgrade (recommended)\" }\n \"upgrade.mode.confirm_each\" = @{ zh = \" 2) 逐个确认参数(可修改)\"; en = \" 2) Confirm each parameter (can modify)\" }\n \"upgrade.mode.back\" = @{ zh = \" 3) 返回\"; en = \" 3) Back\" }\n\n # --- Clean reinstall messages ---\n \"install.reinstall.performing\" = @{ zh = \"执行全新重装...\"; en = \"Performing clean reinstall...\" }\n \"install.reinstall.warn_stop\" = @{ zh = \"警告: 以下运行中的容器将被停止:\"; en = \"WARNING: The following running containers will be stopped:\" }\n \"install.reinstall.warn_delete\" = @{ zh = \"警告: 以下内容将被删除:\"; en = \"WARNING: This will DELETE the following:\" }\n \"install.reinstall.warn_volume\" = @{ zh = \" - Docker 卷: agentteams-data\"; en = \" - Docker volume: agentteams-data\" }\n \"install.reinstall.warn_env\" = @{ zh = \" - Env 文件: {0}\"; en = \" - Env file: {0}\" }\n \"install.reinstall.warn_workspace\" = @{ zh = \" - Manager 工作空间: {0}\"; en = \" - Manager workspace: {0}\" }\n \"install.reinstall.warn_workers\" = @{ zh = \" - 所有 worker 容器\"; en = \" - All worker containers\" }\n \"install.reinstall.warn_proxy\" = @{ zh = \" - Docker API 代理容器: agentteams-controller\"; en = \" - Docker API proxy container: agentteams-controller\" }\n \"install.reinstall.removing_proxy\" = @{ zh = \"正在移除 Docker API 代理容器: agentteams-controller\"; en = \"Removing Docker API proxy container: agentteams-controller\" }\n \"install.reinstall.warn_network\" = @{ zh = \" - Docker 网络: agentteams-net\"; en = \" - Docker network: agentteams-net\" }\n \"install.reinstall.removing_network\" = @{ zh = \"正在移除 Docker 网络: agentteams-net\"; en = \"Removing Docker network: agentteams-net\" }\n \"install.reinstall.confirm_type\" = @{ zh = \"请输入工作空间路径以确认删除(或按 Ctrl+C 取消):\"; en = \"To confirm deletion, please type the workspace path:\" }\n \"install.reinstall.confirm_path\" = @{ zh = \"输入路径以确认(或按 Ctrl+C 取消)\"; en = \"Type the path to confirm (or press Ctrl+C to cancel)\" }\n \"install.reinstall.path_mismatch\" = @{ zh = \"路径不匹配。中止重装。输入: '{0}',期望: '{1}'\"; en = \"Path mismatch. Aborting reinstall. Input: '{0}', Expected: '{1}'\" }\n \"install.reinstall.confirmed\" = @{ zh = \"已确认。正在清理...\"; en = \"Confirmed. Cleaning up...\" }\n \"install.reinstall.removed_worker\" = @{ zh = \" 已移除 worker: {0}\"; en = \" Removed worker: {0}\" }\n \"install.reinstall.removing_volume\" = @{ zh = \"正在移除 Docker 卷: agentteams-data\"; en = \"Removing Docker volume: agentteams-data\" }\n \"install.reinstall.warn_volume_fail\" = @{ zh = \" 警告: 无法移除卷(可能有引用)\"; en = \" Warning: Could not remove volume (may have references)\" }\n \"install.reinstall.removing_workspace\" = @{ zh = \"正在移除工作空间目录: {0}\"; en = \"Removing workspace directory: {0}\" }\n \"install.reinstall.removing_env\" = @{ zh = \"正在移除 env 文件: {0}\"; en = \"Removing env file: {0}\" }\n \"install.reinstall.cleanup_done\" = @{ zh = \"清理完成。开始全新安装...\"; en = \"Cleanup complete. Starting fresh installation...\" }\n \"install.reinstall.failed_rm_workspace\" = @{ zh = \"无法移除工作空间目录\"; en = \"Failed to remove workspace directory\" }\n\n # --- Orphan volume detection ---\n \"install.orphan_volume.detected\" = @{ zh = \"⚠️ 检测到残留数据卷 '{0}',但未找到对应的 env 配置文件。\"; en = \"⚠️ Found leftover data volume '{0}' but no matching env config file.\" }\n \"install.orphan_volume.warn\" = @{ zh = \"这可能是之前安装的残留数据,会导致新安装出现异常(如密码冲突、服务启动失败)。\"; en = \"This is likely leftover data from a previous installation and may cause issues (e.g., credential conflicts, service startup failures).\" }\n \"install.orphan_volume.choose\" = @{ zh = \"选择操作:\"; en = \"Choose an action:\" }\n \"install.orphan_volume.clean\" = @{ zh = \" 1) 清理残留数据卷后继续安装(推荐)\"; en = \" 1) Remove leftover volume and continue installation (recommended)\" }\n \"install.orphan_volume.keep\" = @{ zh = \" 2) 保留数据卷继续安装(可能出现异常)\"; en = \" 2) Keep the volume and continue installation (may cause issues)\" }\n \"install.orphan_volume.prompt\" = @{ zh = \"请选择 [1/2]\"; en = \"Enter choice [1/2]\" }\n \"install.orphan_volume.cleaning\" = @{ zh = \"正在清理残留数据卷...\"; en = \"Removing leftover data volume...\" }\n \"install.orphan_volume.cleaned\" = @{ zh = \"残留数据卷已清理。继续全新安装...\"; en = \"Leftover volume removed. Continuing with fresh installation...\" }\n \"install.orphan_volume.keeping\" = @{ zh = \"保留数据卷,继续安装。如遇异常请选择全新重装。\"; en = \"Keeping existing volume. If you encounter issues, consider a clean reinstall.\" }\n \"install.orphan_volume.clean_noninteractive\" = @{ zh = \"非交互模式: 自动清理残留数据卷...\"; en = \"Non-interactive mode: automatically removing leftover volume...\" }\n\n # --- Loading existing config ---\n \"install.loading_config\" = @{ zh = \"从 {0} 加载已有配置(shell 环境变量优先)...\"; en = \"Loading existing config from {0} (shell env vars take priority)...\" }\n\n # --- LLM Configuration ---\n \"llm.title\" = @{ zh = \"--- LLM 配置 ---\"; en = \"--- LLM Configuration ---\" }\n \"llm.provider.label\" = @{ zh = \" 提供商: {0}\"; en = \" Provider: {0}\" }\n \"llm.model.label\" = @{ zh = \" 模型: {0}\"; en = \" Model: {0}\" }\n \"llm.provider.qwen\" = @{ zh = \" 提供商: qwen(阿里云百炼)\"; en = \" Provider: qwen (Alibaba Cloud Bailian)\" }\n \"llm.provider.qwen_default\" = @{ zh = \" 提供商: {0}(默认)\"; en = \" Provider: {0} (default)\" }\n \"llm.model.default\" = @{ zh = \" 模型: {0}(默认)\"; en = \" Model: {0} (default)\" }\n \"llm.apikey_hint_bailian\" = @{ zh = \" 提示: 获取阿里云百炼(DashScope)API Key:\"; en = \" Hint: Get your Alibaba Cloud Bailian (DashScope) API Key:\" }\n \"llm.apikey_url_bailian\" = @{ zh = \" https://www.aliyun.com/product/bailian\"; en = \" https://www.aliyun.com/product/bailian\" }\n \"llm.apikey_hint_qwencloud\" = @{ zh = \" 提示: 从 Qwen Cloud(国际站)获取 DASHSCOPE_API_KEY:\"; en = \" Hint: Get your DASHSCOPE_API_KEY for Qwen Cloud (international) from:\" }\n \"llm.apikey_url_qwencloud\" = @{ zh = \" https://home.qwencloud.com/api-keys (文档: https://docs.qwencloud.com/)\"; en = \" https://home.qwencloud.com/api-keys | Docs: https://docs.qwencloud.com/\" }\n \"llm.apikey_hint_tokenplan\" = @{ zh = \" 提示: 获取 DashScope API Key 或开通通义 Token 套餐,请参考:\"; en = \" Hint: Get your DashScope or Token Plan API key (Alibaba Model Studio):\" }\n \"llm.apikey_url_tokenplan\" = @{ zh = \" https://help.aliyun.com/zh/model-studio/token-plan-quickstart\"; en = \" https://common-buy.aliyun.com/token-plan/ | https://help.aliyun.com/zh/model-studio/token-plan-quickstart\" }\n \"llm.apikey_hint_codingplan\" = @{ zh = \" 提示: 获取 DashScope API Key(Coding 套餐 / coding.dashscope 接口):\"; en = \" Hint: Get your DashScope API key for Coding Plan (coding.dashscope endpoint):\" }\n \"llm.apikey_url_codingplan\" = @{ zh = \" https://help.aliyun.com/zh/model-studio/get-api-key\"; en = \" https://help.aliyun.com/zh/model-studio/get-api-key\" }\n \"llm.apikey_prompt\" = @{ zh = \"LLM API Key\"; en = \"LLM API Key\" }\n \"llm.providers_title\" = @{ zh = \"可用 LLM 提供商:\"; en = \"Available LLM Providers:\" }\n \"llm.provider.alibaba\" = @{ zh = \" 1) 阿里云通义 Token 套餐 - 推荐中国用户使用\"; en = \" 1) Qwen Cloud - International (OpenAI-compatible API, recommended)\" }\n \"llm.provider.openai_compat\" = @{ zh = \" 2) OpenAI 兼容 API - 自定义 Base URL(OpenAI、DeepSeek 等)\"; en = \" 2) OpenAI-compatible API - Custom Base URL (OpenAI, DeepSeek, etc.)\" }\n \"llm.provider.select\" = @{ zh = \"选择提供商 [1/2]\"; en = \"Select provider [1/2]\" }\n \"llm.alibaba.models_title\" = @{ zh = \"选择阿里云模型接入方式:\"; en = \"Select Alibaba Cloud model access:\" }\n \"llm.alibaba.model.tokenplan\" = @{ zh = \" 1) 阿里云通义 Token 套餐 - 兼容模式(推荐)\"; en = \" 1) Alibaba Cloud Token Plan - compatible-mode (recommended)\" }\n \"llm.alibaba.model.bailian\" = @{ zh = \" 2) 阿里云百炼 - DashScope 通用兼容接口\"; en = \" 2) Alibaba Cloud Bailian - DashScope compatible mode\" }\n \"llm.alibaba.model.codingplan_legacy\" = @{ zh = \" 3) 阿里云 Coding 套餐 - 旧版端点(兼容保留)\"; en = \" 3) Alibaba Cloud Coding Plan - legacy endpoint (backward compatible)\" }\n \"llm.alibaba.model.select\" = @{ zh = \"选择接入方式 [1/2/3]\"; en = \"Select access option [1/2/3]\" }\n \"llm.alibaba.model.invalid\" = @{ zh = \"无效选择: {0}(请输入 1、2 或 3)\"; en = \"Invalid choice: {0} (please enter 1, 2, or 3)\" }\n \"llm.codingplan.models_title\" = @{ zh = \"选择通义 Token 套餐默认模型:\"; en = \"Select Qwen Cloud default model:\" }\n \"llm.codingplan.model.qwen36plus\" = @{ zh = \" 1) qwen3.6-plus - 千问 3.6(推荐)\"; en = \" 1) qwen3.6-plus - Qwen 3.6 (recommended)\" }\n \"llm.codingplan.model.glm5\" = @{ zh = \" 2) glm-5 - 智谱 GLM-5(编程推荐)\"; en = \" 2) glm-5 - Zhipu GLM-5 (recommended for coding)\" }\n \"llm.codingplan.model.kimi\" = @{ zh = \" 3) kimi-k2.5 - Moonshot Kimi K2.5\"; en = \" 3) kimi-k2.5 - Moonshot Kimi K2.5\" }\n \"llm.codingplan.model.minimax\" = @{ zh = \" 4) MiniMax-M2.5 - MiniMax M2.5\"; en = \" 4) MiniMax-M2.5 - MiniMax M2.5\" }\n \"llm.codingplan.model.select\" = @{ zh = \"选择模型 [1/2/3/4]\"; en = \"Select model [1/2/3/4]\" }\n \"llm.provider.selected_tokenplan\" = @{ zh = \" 提供商: 阿里云通义 Token 套餐(兼容模式)\"; en = \" Provider: Alibaba Cloud Token Plan (compatible mode)\" }\n \"llm.provider.selected_codingplan\" = @{ zh = \" 提供商: 阿里云通义 Token 套餐(alibaba-cloud)\"; en = \" Provider: Qwen Cloud (international) (alibaba-cloud)\" }\n \"llm.provider.selected_codingplan_legacy\" = @{ zh = \" 提供商: 阿里云 Coding 套餐(coding.dashscope)\"; en = \" Provider: Alibaba Cloud Coding Plan (coding.dashscope)\" }\n \"llm.provider.selected_qwen\" = @{ zh = \" 提供商: 阿里云百炼\"; en = \" Provider: Alibaba Cloud Bailian\" }\n \"llm.provider.selected_openai\" = @{ zh = \" 提供商: {0}(OpenAI 兼容)\"; en = \" Provider: {0} (OpenAI-compatible)\" }\n \"llm.provider.invalid\" = @{ zh = \"无效选择: {0}(请输入 1 或 2)\"; en = \"Invalid choice: {0} (please enter 1 or 2)\" }\n \"llm.qwen.model_prompt\" = @{ zh = \"默认模型 ID\"; en = \"Default Model ID\" }\n \"llm.openai.base_url_prompt\" = @{ zh = \"Base URL\"; en = \"Base URL\" }\n \"llm.openai.model_prompt\" = @{ zh = \"默认模型 ID\"; en = \"Default Model ID\" }\n \"llm.openai.base_url_label\" = @{ zh = \" Base URL: {0}\"; en = \" Base URL: {0}\" }\n\n # --- Custom model parameters ---\n \"llm.custom_model.detected\" = @{ zh = \" ⚠️ 模型 '{0}' 不在内置模型列表中,请配置模型参数:\"; en = \" ⚠️ Model '{0}' is not in the built-in model list. Please configure model parameters:\" }\n \"llm.custom_model.context_prompt\" = @{ zh = \"最大上下文长度(token 数)[150000]\"; en = \"Max context window (tokens) [150000]\" }\n \"llm.custom_model.max_tokens_prompt\" = @{ zh = \"最大输出长度(token 数)[128000]\"; en = \"Max output tokens [128000]\" }\n \"llm.custom_model.reasoning_prompt\" = @{ zh = \"是否支持推理/思考模式?[Y/n]\"; en = \"Does it support reasoning/thinking mode? [Y/n]\" }\n \"llm.custom_model.vision_prompt\" = @{ zh = \"是否支持图片输入?[y/N]\"; en = \"Does it support image input? [y/N]\" }\n \"llm.custom_model.summary\" = @{ zh = \" 自定义模型参数: 上下文={0}, 最大输出={1}, 推理={2}, 图片={3}\"; en = \" Custom model params: context={0}, maxTokens={1}, reasoning={2}, vision={3}\" }\n\n # --- Admin Credentials ---\n \"admin.title\" = @{ zh = \"--- 管理员凭据 ---\"; en = \"--- Admin Credentials ---\" }\n \"admin.username_prompt\" = @{ zh = \"管理员用户名\"; en = \"Admin Username\" }\n \"admin.password_prompt\" = @{ zh = \"管理员密码(留空自动生成,最少 8 位)\"; en = \"Admin Password (leave empty to auto-generate, min 8 chars)\" }\n \"admin.password_generated\" = @{ zh = \" 已自动生成管理员密码\"; en = \" Auto-generated admin password\" }\n \"admin.password_too_short\" = @{ zh = \"管理员密码至少需要 8 个字符(MinIO 要求)。当前长度: {0}\"; en = \"Admin password must be at least 8 characters (MinIO requirement). Current length: {0}\" }\n\n # --- Port Configuration ---\n \"port.title\" = @{ zh = \"--- 端口配置(按回车使用默认值)---\"; en = \"--- Port Configuration (press Enter for defaults) ---\" }\n \"port.gateway_prompt\" = @{ zh = \"网关主机端口(容器内 8080)\"; en = \"Host port for gateway (8080 inside container)\" }\n \"port.console_prompt\" = @{ zh = \"Higress 控制台主机端口(容器内 8001)\"; en = \"Host port for Higress console (8001 inside container)\" }\n \"port.element_prompt\" = @{ zh = \"Element Web 直接访问主机端口(容器内 8088)\"; en = \"Host port for Element Web direct access (8088 inside container)\" }\n \"port.manager_console_prompt\" = @{ zh = \"Manager 控制台主机端口(容器内 18888)\"; en = \"Host port for Manager console (18888 inside container)\" }\n \"port.local_only.title\" = @{ zh = \"--- 网络访问模式 ---\"; en = \"--- Network Access Mode ---\" }\n \"port.local_only.hint_yes\" = @{ zh = \" 仅本机使用,无需开放外部端口(推荐)\"; en = \" Local use only, no external port exposure (recommended)\" }\n \"port.local_only.hint_no\" = @{ zh = \" 允许外部访问(局域网 / 公网)\"; en = \" Allow external access (LAN / public network)\" }\n \"port.local_only.choice\" = @{ zh = \"请选择 [1/2]\"; en = \"Enter choice [1/2]\" }\n \"port.local_only.selected_local\" = @{ zh = \"端口已绑定到 127.0.0.1(仅本机访问)\"; en = \"Ports bound to 127.0.0.1 (localhost only)\" }\n \"port.local_only.selected_external\" = @{ zh = \"端口已绑定到所有网络接口(0.0.0.0)\"; en = \"Ports bound to all interfaces (0.0.0.0)\" }\n \"port.local_only.https_hint\" = @{ zh = \"警告: 建议在 Higress 控制台配置 TLS 证书并启用 HTTPS,避免明文传输。\"; en = \"WARNING: It is recommended to configure TLS certificates and enable HTTPS in the Higress Console to avoid plaintext transmission.\" }\n \"port.local_only.https_docs\" = @{ zh = \"\"; en = \"\" }\n\n # --- Domain Configuration ---\n \"domain.title\" = @{ zh = \"--- 域名配置(按回车使用默认值)---\"; en = \"--- Domain Configuration (press Enter for defaults) ---\" }\n \"domain.hint\" = @{ zh = \"提示: 自定义域名前必须事先做好 DNS 解析。单机 ECS 部署时无需修改 aigw、fs 等域名;Element Web 和 Matrix Server 也可通过 IP 直接访问。\"; en = \"Hint: Configure DNS resolution before customizing domains. For single ECS deployment, no need to change aigw, fs, etc.; Element Web and Matrix Server can also be accessed directly via IP.\" }\n \"domain.matrix_prompt\" = @{ zh = \"Matrix 域名\"; en = \"Matrix Domain\" }\n \"domain.element_prompt\" = @{ zh = \"Element Web 域名\"; en = \"Element Web Domain\" }\n \"domain.gateway_prompt\" = @{ zh = \"AI 网关域名\"; en = \"AI Gateway Domain\" }\n \"domain.fs_prompt\" = @{ zh = \"文件系统域名\"; en = \"File System Domain\" }\n \"domain.console_prompt\" = @{ zh = \"Manager 控制台域名\"; en = \"Manager Console Domain\" }\n\n # --- GitHub Integration ---\n \"github.title\" = @{ zh = \"--- GitHub 集成(可选,按回车跳过)---\"; en = \"--- GitHub Integration (optional, press Enter to skip) ---\" }\n \"github.token_prompt\" = @{ zh = \"GitHub 个人访问令牌(可选)\"; en = \"GitHub Personal Access Token (optional)\" }\n\n # --- Skills Registry ---\n \"skills.title\" = @{ zh = \"--- Skills 注册中心(可选,按回车使用默认 nacos://market.agentteams.io:80/public)---\"; en = \"--- Skills Registry (optional, press Enter for default nacos://market.agentteams.io:80/public) ---\" }\n \"skills.url_prompt\" = @{ zh = \"Skills 注册中心 URL(留空使用默认 nacos://market.agentteams.io:80/public)\"; en = \"Skills Registry URL (leave empty for default nacos://market.agentteams.io:80/public)\" }\n\n # --- Data Persistence ---\n \"data.title\" = @{ zh = \"--- 数据持久化 ---\"; en = \"--- Data Persistence ---\" }\n \"data.volume_prompt\" = @{ zh = \"Docker 卷名称 [agentteams-data]\"; en = \"Docker volume name for persistent data [agentteams-data]\" }\n \"data.volume_using\" = @{ zh = \" 使用 Docker 卷: {0}\"; en = \" Using Docker volume: {0}\" }\n\n # --- Manager Workspace ---\n \"workspace.title\" = @{ zh = \"--- Manager 工作空间 ---\"; en = \"--- Manager Workspace ---\" }\n \"workspace.dir_prompt\" = @{ zh = \"Manager 工作空间目录 [{0}]\"; en = \"Manager workspace directory [{0}]\" }\n \"workspace.dir_label\" = @{ zh = \" Manager 工作空间: {0}\"; en = \" Manager workspace: {0}\" }\n\n # --- Host directory sharing ---\n \"host_share.prompt\" = @{ zh = \"与 Agent 共享的主机目录(默认: {0})\"; en = \"Host directory to share with agents (default: {0})\" }\n \"host_share.sharing\" = @{ zh = \"共享主机目录: {0} -> 容器内 /host-share\"; en = \"Sharing host directory: {0} -> /host-share in container\" }\n \"host_share.not_exist\" = @{ zh = \"警告: 主机目录 {0} 不存在,跳过验证继续使用\"; en = \"WARNING: Host directory {0} does not exist, using without validation\" }\n\n # --- Default worker runtime ---\n \"worker_runtime.title\" = @{ zh = \"--- 默认 Worker 运行时 ---\"; en = \"--- Default Worker Runtime ---\" }\n \"worker_runtime.openclaw\" = @{ zh = \"OpenClaw\"; en = \"OpenClaw\" }\n \"worker_runtime.copaw\" = @{ zh = \"CoPaw\"; en = \"CoPaw\" }\n \"worker_runtime.hermes\" = @{ zh = \"Hermes\"; en = \"Hermes\" }\n \"worker_runtime.choice\" = @{ zh = \"请选择 [1/2/3]\"; en = \"Enter choice [1/2/3]\" }\n \"worker_runtime.selected\" = @{ zh = \"默认 Worker 运行时: {0}\"; en = \"Default Worker runtime: {0}\" }\n \"worker_runtime.title_short\" = @{ zh = \"默认 Worker 运行时\"; en = \"Default Worker Runtime\" }\n\n # --- Manager runtime ---\n \"manager_runtime.title\" = @{ zh = \"--- Manager 运行时 ---\"; en = \"--- Manager Runtime ---\" }\n \"manager_runtime.openclaw\" = @{ zh = \"OpenClaw\"; en = \"OpenClaw\" }\n \"manager_runtime.copaw\" = @{ zh = \"CoPaw\"; en = \"CoPaw\" }\n \"manager_runtime.choice\" = @{ zh = \"请选择 [1/2]\"; en = \"Enter choice [1/2]\" }\n \"manager_runtime.selected\" = @{ zh = \"Manager 运行时: {0}\"; en = \"Manager runtime: {0}\" }\n \"manager_runtime.title_short\" = @{ zh = \"Manager 运行时\"; en = \"Manager Runtime\" }\n\n # --- Matrix E2EE ---\n \"matrix_e2ee.title\" = @{ zh = \"--- Matrix 端到端加密(E2EE)---\"; en = \"--- Matrix End-to-End Encryption (E2EE) ---\" }\n \"matrix_e2ee.desc\" = @{\n zh = \"E2EE 会对 Manager 与 Worker 之间的 Matrix 消息进行端到端加密。`n 启用后,即使 Matrix 服务器被入侵,消息内容也无法被窃取。`n 但 E2EE 会增加首次握手耗时,且要求所有 Agent 都支持 matrix-sdk-crypto。`n 如果不确定,建议保持禁用。`n ⚠ 注意:禁用 E2EE 后,请勿在 Element 上创建默认启用加密的 Private 房间,`n 否则 Agent 将无法读取该房间中的加密消息。请改用 Public 房间或关闭房间加密。\"\n en = \"E2EE encrypts Matrix messages between Manager and Workers end-to-end.`n When enabled, message content stays private even if the Matrix server is compromised.`n However, E2EE adds overhead to the initial handshake and requires all Agents`n to support matrix-sdk-crypto. If unsure, keep it disabled.`n ⚠ Note: When E2EE is disabled, do NOT create Private rooms in Element (which`n enable encryption by default) — Agents cannot read encrypted messages without`n E2EE support. Use Public rooms or turn off room encryption instead.\"\n }\n \"matrix_e2ee.enable\" = @{ zh = \"启用 E2EE\"; en = \"Enable E2EE\" }\n \"matrix_e2ee.disable\" = @{ zh = \"禁用 E2EE(推荐)\"; en = \"Disable E2EE (recommended)\" }\n \"matrix_e2ee.choice\" = @{ zh = \"请选择 [1/2]\"; en = \"Enter choice [1/2]\" }\n \"matrix_e2ee.selected_enabled\" = @{ zh = \"Matrix E2EE: 已启用\"; en = \"Matrix E2EE: enabled\" }\n \"matrix_e2ee.selected_disabled\" = @{ zh = \"Matrix E2EE: 已禁用(默认)\"; en = \"Matrix E2EE: disabled (default)\" }\n \"matrix_e2ee.title_short\" = @{ zh = \"Matrix E2EE\"; en = \"Matrix E2EE\" }\n \"matrix_e2ee.val_enabled\" = @{ zh = \"已启用\"; en = \"enabled\" }\n \"matrix_e2ee.val_disabled\" = @{ zh = \"已禁用\"; en = \"disabled\" }\n\n # --- Docker API proxy ---\n \"docker_proxy.title\" = @{ zh = \"--- Docker API 安全代理 ---\"; en = \"--- Docker API Security Proxy ---\" }\n \"docker_proxy.desc\" = @{ zh = \"Docker API 代理可防止 AI Agent 通过 Docker API 越狱访问宿主机。`n 启用后,Manager 不再直接持有 Docker socket,所有容器操作经过安全校验。\"; en = \"Docker API proxy prevents AI Agents from escaping via Docker API to access the host.`n When enabled, Manager no longer has direct Docker socket access; all container operations go through security validation.\" }\n \"docker_proxy.enable\" = @{ zh = \"启用(推荐)\"; en = \"Enable (recommended)\" }\n \"docker_proxy.disable\" = @{ zh = \"禁用(直接挂载 Docker socket)\"; en = \"Disable (mount Docker socket directly)\" }\n \"docker_proxy.choice\" = @{ zh = \"请选择 [1/2]\"; en = \"Enter choice [1/2]\" }\n \"docker_proxy.selected_enabled\" = @{ zh = \"Docker API 代理: 已启用\"; en = \"Docker API proxy: enabled\" }\n \"docker_proxy.selected_disabled\" = @{ zh = \"Docker API 代理: 已禁用\"; en = \"Docker API proxy: disabled\" }\n \"docker_proxy.title_short\" = @{ zh = \"Docker API 代理\"; en = \"Docker API Proxy\" }\n \"docker_proxy.val_enabled\" = @{ zh = \"已启用\"; en = \"enabled\" }\n \"docker_proxy.val_disabled\" = @{ zh = \"已禁用\"; en = \"disabled\" }\n \"docker_proxy.registries_desc\" = @{ zh = \"默认放行的镜像来源:本地镜像、localhost、Higress 仓库(所有 region)。`n 如需放行其他镜像仓库,请输入逗号分隔的地址前缀。`n 示例: ghcr.io/myorg,registry.example.com/team\"; en = \"Default allowed image sources: local images, localhost, Higress registries (all regions).`n To allow additional image sources, enter comma-separated address prefixes.`n Example: ghcr.io/myorg,registry.example.com/team\" }\n \"docker_proxy.registries_prompt\" = @{ zh = \"额外放行的镜像来源(按回车跳过)\"; en = \"Additional allowed image sources (press Enter to skip)\" }\n \"docker_proxy.registries_label\" = @{ zh = \"额外放行的镜像来源\"; en = \"Additional allowed image sources\" }\n\n # --- Worker idle timeout ---\n \"idle_timeout.prompt\" = @{ zh = \"Worker 空闲自动停止超时(分钟)[720]\"; en = \"Worker idle auto-stop timeout in minutes [720]\" }\n \"idle_timeout.selected\" = @{ zh = \"Worker 空闲超时: {0} 分钟\"; en = \"Worker idle timeout: {0} minutes\" }\n \"idle_timeout.label\" = @{ zh = \"Worker 空闲超时(分钟)\"; en = \"Worker idle timeout (min)\" }\n\n # --- Secrets and config ---\n \"install.generating_secrets\" = @{ zh = \"正在生成密钥...\"; en = \"Generating secrets...\" }\n \"install.config_saved\" = @{ zh = \"配置已保存到 {0}\"; en = \"Configuration saved to {0}\" }\n\n # --- Container runtime socket ---\n \"install.socket_detected\" = @{ zh = \"容器运行时 socket: {0}(已启用直接创建 Worker)\"; en = \"Container runtime socket: {0} (direct Worker creation enabled)\" }\n \"install.socket_not_found\" = @{ zh = \"未找到容器运行时 socket(Manager 无法直接创建 Worker 容器,需要你手动执行 docker 命令创建)\"; en = \"No container runtime socket found (Manager cannot create Worker containers directly, you will need to create them manually using docker commands)\" }\n \"install.socket_confirm.title\" = @{ zh = \"警告: 未检测到容器运行时 Socket\"; en = \"WARNING: Container Runtime Socket Not Detected\" }\n \"install.socket_confirm.message\" = @{ zh = \"未找到 Docker/Podman socket,Manager 将无法自动创建 Worker 容器。`n你需要手动执行 docker run 命令来创建 Worker。`n`n是否继续安装?\"; en = \"Docker/Podman socket not found. Manager will not be able to create Worker containers automatically.`nYou will need to manually run docker commands to create Workers.`n`nContinue installation?\" }\n \"install.socket_confirm.prompt\" = @{ zh = \"继续安装? [y/N]: \"; en = \"Continue? [y/N]: \" }\n \"install.socket_confirm.cancelled\" = @{ zh = \"安装已取消。如需启用 Worker 自动创建,请确保 Docker/Podman 正在运行,然后重新运行安装脚本。\"; en = \"Installation cancelled. To enable automatic Worker creation, ensure Docker/Podman is running and re-run the installer.\" }\n\n # --- Container management ---\n \"install.removing_existing\" = @{ zh = \"正在移除现有 agentteams-manager 容器...\"; en = \"Removing existing agentteams-manager container...\" }\n\n # --- YOLO mode ---\n \"install.yolo\" = @{ zh = \"YOLO 模式已启用(自主决策,无交互提示)\"; en = \"YOLO mode enabled (autonomous decisions, no interactive prompts)\" }\n\n # --- Image pulling ---\n \"install.image.exists\" = @{ zh = \"Manager 镜像已存在: {0}\"; en = \"Manager image already exists locally: {0}\" }\n \"install.image.pulling_manager\" = @{ zh = \"正在拉取 Manager 镜像: {0}\"; en = \"Pulling Manager image: {0}\" }\n \"install.image.worker_exists\" = @{ zh = \"Worker 镜像已存在: {0}\"; en = \"Worker image already exists locally: {0}\" }\n \"install.image.pulling_worker\" = @{ zh = \"正在拉取 Worker 镜像: {0}\"; en = \"Pulling Worker image: {0}\" }\n\n # --- Starting container ---\n \"install.starting_manager\" = @{ zh = \"正在启动 Manager 容器...\"; en = \"Starting Manager container...\" }\n\n # --- Wait for Manager ready ---\n \"install.wait_ready\" = @{ zh = \"等待 Manager Agent 就绪(超时: {0}s)...\"; en = \"Waiting for Manager agent to be ready (timeout: {0}s)...\" }\n \"install.wait_ready.ok\" = @{ zh = \"Manager Agent 已就绪!\"; en = \"Manager agent is ready!\" }\n \"install.wait_ready.waiting\" = @{ zh = \"等待中... ({0}s/{1}s)\"; en = \"Waiting... ({0}s/{1}s)\" }\n \"install.wait_ready.timeout\" = @{ zh = \"Manager Agent 在 {0}s 内未就绪。请检查: docker logs {1}\"; en = \"Manager agent did not become ready within {0}s. Check: docker logs {1}\" }\n\n # --- Wait for Matrix ready ---\n \"install.wait_matrix\" = @{ zh = \"等待 Matrix 服务就绪(超时: {0}s)...\"; en = \"Waiting for Matrix server to be ready (timeout: {0}s)...\" }\n \"install.wait_matrix.ok\" = @{ zh = \"Matrix 服务已就绪!\"; en = \"Matrix server is ready!\" }\n \"install.wait_matrix.waiting\" = @{ zh = \"等待 Matrix 中... ({0}s/{1}s)\"; en = \"Waiting for Matrix... ({0}s/{1}s)\" }\n \"install.wait_matrix.timeout\" = @{ zh = \"Matrix 服务在 {0}s 内未就绪。请检查: docker logs {1}\"; en = \"Matrix server did not become ready within {0}s. Check: docker logs {1}\" }\n\n # --- OpenAI-compatible connectivity test ---\n \"llm.openai.test.testing\" = @{ zh = \"正在测试 API 联通性...\"; en = \"Testing API connectivity...\" }\n \"llm.openai.test.ok\" = @{ zh = \"API 联通性测试通过\"; en = \"API connectivity test passed\" }\n \"llm.openai.test.fail\" = @{ zh = \"API 联通性测试失败(HTTP {0})。响应内容:`n{1}`n请根据以上错误信息联系您的模型服务商解决。\"; en = \"API connectivity test failed (HTTP {0}). Response body:`n{1}`nPlease contact your model provider to resolve the issue.\" }\n \"llm.openai.test.fail.tokenplan\" = @{ zh = \"提示: 请确认 API Key 有效且已开通通义 Token 套餐。文档: https://help.aliyun.com/zh/model-studio/token-plan-quickstart\"; en = \"Hint: Verify your Token Plan API key and compatible-mode access. Docs: https://help.aliyun.com/zh/model-studio/token-plan-quickstart\" }\n \"llm.openai.test.fail.codingplan\" = @{ zh = \"提示: 请确认 API Key 有效且已开通通义 Token 套餐。文档: https://help.aliyun.com/zh/model-studio/token-plan-quickstart\"; en = \"Hint: Verify your DASHSCOPE_API_KEY for Qwen Cloud. API keys: https://home.qwencloud.com/api-keys Docs: https://docs.qwencloud.com/\" }\n \"llm.openai.test.fail.codingplan_legacy\" = @{ zh = \"提示: 请确认 API Key 有效且 Coding 套餐接口可用。文档: https://help.aliyun.com/zh/model-studio/get-api-key\"; en = \"Hint: Verify your DashScope API key and Coding Plan access. Docs: https://help.aliyun.com/zh/model-studio/get-api-key\" }\n \"llm.openai.test.confirm\" = @{ zh = \"是否仍要继续安装?[y/N/b]\"; en = \"Continue with installation anyway? [y/N/b]\" }\n \"llm.openai.test.aborted\" = @{ zh = \"安装已中止。\"; en = \"Installation aborted.\" }\n \"llm.embedding.title\" = @{ zh = \"📦 记忆搜索配置\"; en = \"📦 Memory Search Configuration\" }\n \"llm.embedding.hint\" = @{ zh = \" Embedding 模型可提升记忆搜索质量(语义匹配)。不启用也可正常使用记忆功能(关键词匹配)。\"; en = \" Embedding model improves memory search quality (semantic matching). Memory still works without it (keyword matching).\" }\n \"llm.embedding.option.default\" = @{ zh = \" 1) text-embedding-v4(推荐)\"; en = \" 1) text-embedding-v4 (Recommended)\" }\n \"llm.embedding.option.custom\" = @{ zh = \" 2) 自定义 Embedding 模型\"; en = \" 2) Custom embedding model\" }\n \"llm.embedding.option.disable\" = @{ zh = \" 3) 不启用\"; en = \" 3) Do not enable\" }\n \"llm.embedding.select\" = @{ zh = \"选择\"; en = \"Select\" }\n \"llm.embedding.custom_prompt\" = @{ zh = \" Embedding 模型名称\"; en = \" Embedding model name\" }\n \"llm.embedding.test.testing\" = @{ zh = \"正在测试 Embedding API 联通性...\"; en = \"Testing Embedding API connectivity...\" }\n \"llm.embedding.test.ok\" = @{ zh = \"✅ Embedding API 联通性测试通过\"; en = \"✅ Embedding API connectivity test passed\" }\n \"llm.embedding.test.fail\" = @{ zh = \"⚠️ Embedding API 测试失败(HTTP {0})。响应: {1}\"; en = \"⚠️ Embedding API test failed (HTTP {0}). Response: {1}\" }\n \"llm.embedding.auto_disabled\" = @{ zh = \"⚠️ Embedding 已自动禁用,记忆搜索将使用关键词匹配。您可以稍后在 agentteams-manager.env 中设置 AGENTTEAMS_EMBEDDING_MODEL 启用。\"; en = \"⚠️ Embedding auto-disabled. Memory search will use keyword matching. You can enable it later in agentteams-manager.env by setting AGENTTEAMS_EMBEDDING_MODEL.\" }\n \"llm.embedding.disabled\" = @{ zh = \"ℹ️ Embedding 已禁用,记忆搜索将使用关键词匹配。\"; en = \"ℹ️ Embedding disabled. Memory search will use keyword matching.\" }\n \"nav.back_hint\" = @{ zh = \"(输入 b 返回上一步)\"; en = \"(enter b to go back)\" }\n # --- OpenAI-compatible provider creation ---\n \"install.openai_compat.missing\" = @{ zh = \"警告: OpenAI Base URL 或 API Key 未设置,跳过提供商创建\"; en = \"WARNING: OpenAI Base URL or API Key not set, skipping provider creation\" }\n \"install.openai_compat.creating\" = @{ zh = \"正在创建 OpenAI 兼容提供商...\"; en = \"Creating OpenAI-compatible provider...\" }\n \"install.openai_compat.domain\" = @{ zh = \" 域名: {0}\"; en = \" Domain: {0}\" }\n \"install.openai_compat.port\" = @{ zh = \" 端口: {0}\"; en = \" Port: {0}\" }\n \"install.openai_compat.protocol\" = @{ zh = \" 协议: {0}\"; en = \" Protocol: {0}\" }\n \"install.openai_compat.service_fail\" = @{ zh = \"警告: 创建 DNS 服务源失败(可能已存在)\"; en = \"WARNING: Failed to create DNS service source (may already exist)\" }\n \"install.openai_compat.provider_fail\" = @{ zh = \"警告: 创建 AI 提供商失败(可能已存在)\"; en = \"WARNING: Failed to create AI provider (may already exist)\" }\n \"install.openai_compat.success\" = @{ zh = \"OpenAI 兼容提供商创建成功\"; en = \"OpenAI-compatible provider created successfully\" }\n\n # --- Welcome message ---\n \"install.welcome_msg.soul_configured\" = @{ zh = \"Soul 已配置(找到 soul-configured 标记),跳过 onboarding 消息\"; en = \"Soul already configured (soul-configured marker found), skipping onboarding message\" }\n \"install.welcome_msg.logging_in\" = @{ zh = \"正在以 {0} 身份登录以发送欢迎消息...\"; en = \"Logging in as {0} to send welcome message...\" }\n \"install.welcome_msg.login_failed\" = @{ zh = \"警告: 以 {0} 身份登录失败,跳过欢迎消息\"; en = \"WARNING: Failed to login as {0}, skipping welcome message\" }\n \"install.welcome_msg.finding_room\" = @{ zh = \"正在查找与 Manager 的 DM 房间...\"; en = \"Finding DM room with Manager...\" }\n \"install.welcome_msg.creating_room\" = @{ zh = \"正在创建与 Manager 的 DM 房间...\"; en = \"Creating DM room with Manager...\" }\n \"install.welcome_msg.no_room\" = @{ zh = \"警告: 无法找到或创建与 Manager 的 DM 房间\"; en = \"WARNING: Could not find or create DM room with Manager\" }\n \"install.welcome_msg.waiting_join\" = @{ zh = \"等待 Manager 加入房间...\"; en = \"Waiting for Manager to join the room...\" }\n \"install.welcome_msg.sending\" = @{ zh = \"正在向 Manager 发送欢迎消息...\"; en = \"Sending welcome message to Manager...\" }\n \"install.welcome_msg.send_failed\" = @{ zh = \"警告: 发送欢迎消息失败\"; en = \"WARNING: Failed to send welcome message\" }\n \"install.welcome_msg.sent\" = @{ zh = \"欢迎消息已发送给 Manager\"; en = \"Welcome message sent to Manager\" }\n \"install.welcome_msg.waiting\" = @{ zh = \"等待 Manager 发送欢迎消息(Higress 路由授权 + LLM 探活,约 45-90s)...\"; en = \"Waiting for Manager to send the welcome message (Higress route auth + LLM probe, ~45-90s)...\" }\n \"install.welcome_msg.confirmed\" = @{ zh = \"Manager 已确认发送欢迎消息(status.welcomeSent=true,用时 {0}s)\"; en = \"Manager confirmed welcome message sent (status.welcomeSent=true, {0}s elapsed)\" }\n \"install.welcome_msg.timeout\" = @{ zh = \"警告: 在 {0}s 内未观察到 Manager 发送欢迎消息(status.welcomeSent=true)。安装仍然成功,所有服务已就绪——可继续按下方提示登录 Element Web。\"; en = \"WARNING: Did not observe the Manager sending its welcome message (status.welcomeSent=true) within {0}s. Installation is still successful, all services are up — continue with the Element Web instructions below.\" }\n \"install.welcome_msg.timeout_hint\" = @{ zh = \"手动触发 onboarding: 登录 Element Web → 打开与 Manager 的 DM 房间 → 发送任意一句话(例如 `\"hi`\"),Manager 会接管对话并开始引导。\"; en = \"Manual onboarding: log in to Element Web -> open the DM with the Manager -> send any message (e.g. `\"hi`\") and the Manager will take over and start the guided setup.\" }\n \"install.welcome_msg.timeout_inspect\" = @{ zh = \"排查命令: docker exec agentteams-controller agt get managers default\"; en = \"Inspect status: docker exec agentteams-controller agt get managers default\" }\n \"install.welcome_msg.poll_unavailable\" = @{ zh = \"提示: agentteams-manager 内未找到 AgentTeams CLI,跳过 welcome 等待(旧镜像?)\"; en = \"Note: AgentTeams CLI not found inside agentteams-manager; skipping welcome wait (old image?)\" }\n\n # --- Final output panel ---\n \"success.title\" = @{ zh = \"=== AgentTeams Manager 已启动!===\"; en = \"=== AgentTeams Manager Started! ===\" }\n \"success.domains_configured\" = @{ zh = \"以下域名已配置解析到 127.0.0.1:\"; en = \"The following domains are configured to resolve to 127.0.0.1:\" }\n \"success.open_url\" = @{ zh = \" 在浏览器中打开以下 URL 开始使用: \"; en = \" Open the following URL in your browser to start: \" }\n \"success.login_with\" = @{ zh = \" 登录信息:\"; en = \" Login with:\" }\n \"success.username\" = @{ zh = \" 用户名: {0}\"; en = \" Username: {0}\" }\n \"success.password\" = @{ zh = \" 密码: {0}\"; en = \" Password: {0}\" }\n \"success.after_login\" = @{ zh = \" 登录后,开始与 Manager 聊天!\"; en = \" After login, start chatting with the Manager!\" }\n \"success.tell_it\" = @{ zh = \" 告诉它: `\"创建一个名为 alice 的前端开发 Worker`\"\"; en = \" Tell it: `\"Create a Worker named alice for frontend dev`\"\" }\n \"success.manager_auto\" = @{ zh = \" Manager 会自动处理一切。\"; en = \" The Manager will handle everything automatically.\" }\n \"success.mobile_title\" = @{ zh = \" 移动端访问(FluffyChat / Element Mobile):\"; en = \" Mobile access (FluffyChat / Element Mobile):\" }\n \"success.mobile_step1\" = @{ zh = \" 1. 在手机上下载 FluffyChat 或 Element\"; en = \" 1. Download FluffyChat or Element on your phone\" }\n \"success.mobile_step2\" = @{ zh = \" 2. 设置 homeserver 为: {0}\"; en = \" 2. Set homeserver to: {0}\" }\n \"success.mobile_step2_noip\" = @{ zh = \" 2. 设置 homeserver 为: http://<本机局域网IP>:{0}\"; en = \" 2. Set homeserver to: http://<this-machine-LAN-IP>:{0}\" }\n \"success.mobile_noip_hint\" = @{ zh = \" (无法自动检测局域网 IP — 请使用 ifconfig / ip addr 查看)\"; en = \" (Could not detect LAN IP automatically — check with: ifconfig / ip addr)\" }\n \"success.mobile_step3\" = @{ zh = \" 3. 登录信息:\"; en = \" 3. Login with:\" }\n \"success.mobile_username\" = @{ zh = \" 用户名: {0}\"; en = \" Username: {0}\" }\n \"success.mobile_password\" = @{ zh = \" 密码: {0}\"; en = \" Password: {0}\" }\n\n # --- Other consoles and tips ---\n \"success.other_consoles\" = @{ zh = \"--- 其他控制台 ---\"; en = \"--- Other Consoles ---\" }\n \"success.higress_console\" = @{ zh = \" Higress 控制台: http://localhost:{0}(用户名: {1} / 密码: {2})\"; en = \" Higress Console: http://localhost:{0} (Username: {1} / Password: {2})\" }\n \"success.manager_console\" = @{ zh = \" Manager 控制台(本地): http://localhost:{0}(无需登录)\"; en = \" Manager Console (local): http://localhost:{0} (no login required)\" }\n \"success.manager_console_gateway\" = @{ zh = \" Manager 控制台(网关): http://console-local.agentteams.io(用户名: {0} / 密码: {1})\"; en = \" Manager Console (gateway): http://console-local.agentteams.io (Username: {0} / Password: {1})\" }\n \"success.copaw_console\" = @{ zh = \" CoPaw 控制台(本地): http://localhost:{0}(无需登录)\"; en = \" CoPaw Console (local): http://localhost:{0} (no login required)\" }\n \"success.switch_llm.title\" = @{ zh = \"--- 切换 LLM 提供商 ---\"; en = \"--- Switch LLM Providers ---\" }\n \"success.switch_llm.hint\" = @{ zh = \" 您可以通过 Higress 控制台切换到其他 LLM 提供商(OpenAI、Anthropic 等)。\"; en = \" You can switch to other LLM providers (OpenAI, Anthropic, etc.) via Higress Console.\" }\n \"success.switch_llm.docs\" = @{ zh = \" 详细说明请参阅:\"; en = \" For detailed instructions, see:\" }\n \"success.switch_llm.url\" = @{ zh = \" https://higress.ai/en/docs/ai/scene-guide/multi-proxy#console-configuration\"; en = \" https://higress.ai/en/docs/ai/scene-guide/multi-proxy#console-configuration\" }\n \"success.tip\" = @{ zh = \"提示: 您也可以在聊天中让 Manager 为您配置 LLM 提供商。\"; en = \"Tip: You can also ask the Manager to configure LLM providers for you in the chat.\" }\n \"success.config_file\" = @{ zh = \"配置文件: {0}\"; en = \"Configuration file: {0}\" }\n \"success.data_volume\" = @{ zh = \"数据卷: {0}\"; en = \"Data volume: {0}\" }\n \"success.workspace\" = @{ zh = \"Manager 工作空间: {0}\"; en = \"Manager workspace: {0}\" }\n\n # --- Worker installation ---\n \"worker.resetting\" = @{ zh = \"正在重置 Worker: {0}...\"; en = \"Resetting Worker: {0}...\" }\n \"worker.exists\" = @{ zh = \"容器 '{0}' 已存在。使用 --reset 重新创建。\"; en = \"Container '{0}' already exists. Use --reset to recreate.\" }\n \"worker.starting\" = @{ zh = \"正在启动 Worker: {0}...\"; en = \"Starting Worker: {0}...\" }\n \"worker.skills_url\" = @{ zh = \" Skills API URL: {0}\"; en = \" Skills API URL: {0}\" }\n \"worker.started\" = @{ zh = \"=== Worker {0} 已启动!===\"; en = \"=== Worker {0} Started! ===\" }\n \"worker.container\" = @{ zh = \"容器: {0}\"; en = \"Container: {0}\" }\n \"worker.view_logs\" = @{ zh = \"查看日志: docker logs -f {0}\"; en = \"View logs: docker logs -f {0}\" }\n\n # --- Prompt function messages ---\n \"prompt.preset\" = @{ zh = \" {0} = (已通过环境变量预设)\"; en = \" {0} = (pre-set via env)\" }\n \"prompt.upgrade_keep\" = @{ zh = \" {0} = {1}(当前值,回车保留 / 输入新值覆盖)\"; en = \" {0} = {1} (current value, press Enter to keep / type new value to change)\" }\n \"prompt.upgrade_empty\" = @{ zh = \" {0} = (未设置,回车跳过 / 输入新值设置)\"; en = \" {0} = (not set, press Enter to skip / type new value to set)\" }\n \"prompt.default\" = @{ zh = \" {0} = {1}(默认)\"; en = \" {0} = {1} (default)\" }\n \"prompt.required\" = @{ zh = \"{0} 是必需的(在非交互模式下通过环境变量设置)\"; en = \"{0} is required (set via environment variable in non-interactive mode)\" }\n \"prompt.required_empty\" = @{ zh = \"{0} 是必需的\"; en = \"{0} is required\" }\n\n # --- Language switch prompt (bilingual by design) ---\n \"lang.detected.zh\" = @{ zh = \"检测到语言 / Detected language: 中文\"; en = \"检测到语言 / Detected language: 中文\" }\n \"lang.detected.en\" = @{ zh = \"检测到语言 / Detected language: English\"; en = \"检测到语言 / Detected language: English\" }\n \"lang.switch_title\" = @{ zh = \"切换语言 / Switch language:\"; en = \"切换语言 / Switch language:\" }\n \"lang.option_zh\" = @{ zh = \" 1) 中文\"; en = \" 1) 中文\" }\n \"lang.option_en\" = @{ zh = \" 2) English\"; en = \" 2) English\" }\n \"lang.prompt\" = @{ zh = \"请选择 / Enter choice\"; en = \"请选择 / Enter choice\" }\n\n # --- Error messages ---\n \"error.name_required\" = @{ zh = \"--name 是必需的\"; en = \"--name is required\" }\n \"error.fs_required\" = @{ zh = \"--fs 是必需的\"; en = \"--fs is required\" }\n \"error.fs_key_required\" = @{ zh = \"--fs-key 是必需的\"; en = \"--fs-key is required\" }\n \"error.fs_secret_required\" = @{ zh = \"--fs-secret 是必需的\"; en = \"--fs-secret is required\" }\n \"error.unknown_option\" = @{ zh = \"未知选项: {0}\"; en = \"Unknown option: {0}\" }\n \"error.docker_not_running\" = @{ zh = \"Docker 未运行。请先启动 Docker Desktop 或 Podman Desktop。\"; en = \"Docker is not running. Please start Docker Desktop or Podman Desktop first.\" }\n \"error.docker_not_found\" = @{ zh = \"未找到 docker 或 podman 命令。请先安装 Docker Desktop 或 Podman Desktop:`n Docker Desktop: https://www.docker.com/products/docker-desktop/`n Podman Desktop: https://podman-desktop.io/\"; en = \"docker or podman command not found. Please install Docker Desktop or Podman Desktop first:`n Docker Desktop: https://www.docker.com/products/docker-desktop/`n Podman Desktop: https://podman-desktop.io/\" }\n\n # --- Uninstall messages ---\n \"uninstall.title\" = @{ zh = \"正在卸载 AgentTeams...\"; en = \"Uninstalling AgentTeams...\" }\n \"uninstall.stopping_manager\" = @{ zh = \"正在停止并移除 agentteams-manager...\"; en = \"Stopping and removing agentteams-manager...\" }\n \"uninstall.stopping_workers\" = @{ zh = \"正在停止并移除 worker 容器...\"; en = \"Stopping and removing worker containers...\" }\n \"uninstall.removed\" = @{ zh = \" 已移除: {0}\"; en = \" Removed: {0}\" }\n \"uninstall.removing_volume\" = @{ zh = \"正在移除 Docker 卷: agentteams-data\"; en = \"Removing Docker volume: agentteams-data\" }\n \"uninstall.removing_env\" = @{ zh = \"正在移除 env 文件: {0}\"; en = \"Removing env file: {0}\" }\n \"uninstall.removing_proxy\" = @{ zh = \"正在停止并移除 Docker API 代理容器: agentteams-docker-proxy\"; en = \"Stopping and removing Docker API proxy container: agentteams-docker-proxy\" }\n \"uninstall.stopping_controller\" = @{ zh = \"正在停止并移除 agentteams-controller (内嵌 Tuwunel/MinIO/Higress)...\"; en = \"Stopping and removing agentteams-controller (embedded Tuwunel/MinIO/Higress)...\" }\n \"uninstall.removing_network\" = @{ zh = \"正在移除 Docker 网络: agentteams-net\"; en = \"Removing Docker network: agentteams-net\" }\n \"uninstall.removing_workspace\" = @{ zh = \"正在移除工作空间目录: {0}\"; en = \"Removing workspace directory: {0}\" }\n \"uninstall.removing_log\" = @{ zh = \"正在移除日志文件: {0}\"; en = \"Removing log file: {0}\" }\n \"uninstall.done\" = @{ zh = \"AgentTeams 已卸载。\"; en = \"AgentTeams has been uninstalled.\" }\n}\n\n# Get-Msg: look up message by key, with -f style argument substitution.\n# Falls back to English if the current language translation is missing.\nfunction Get-Msg {\n param(\n [Parameter(Mandatory)][string]$Key,\n [object[]]$f\n )\n $lang = $script:AGENTTEAMS_LANGUAGE\n if (-not $lang) { $lang = \"en\" }\n $entry = $script:Messages[$Key]\n if (-not $entry) { return $Key }\n $text = $entry[$lang]\n if (-not $text) { $text = $entry[\"en\"] }\n if (-not $text) { return $Key }\n if ($f) { return ($text -f $f) }\n return $text\n}\n\nfunction Get-LanIP {\n # Detect local LAN IP address on Windows\n try {\n # Get network adapters with IPv4 addresses, prefer connected/active interfaces\n $adapters = Get-NetIPAddress -AddressFamily IPv4 -ErrorAction SilentlyContinue |\n Where-Object {\n $_.IPAddress -notlike \"127.*\" -and\n $_.PrefixOrigin -ne \"WellKnown\" -and\n $_.InterfaceAlias -notlike \"*Loopback*\"\n } |\n Sort-Object {\n # PowerShell 5.1 compatible: use if-else instead of ternary operator\n if ($_.InterfaceAlias -like \"*Wi-Fi*\" -or $_.InterfaceAlias -like \"*Ethernet*\") { 0 } else { 1 }\n }\n\n if ($adapters) {\n return $adapters[0].IPAddress\n }\n\n # Fallback: use ipconfig\n $ipconfig = ipconfig 2>$null\n $ip = ($ipconfig | Select-String \"IPv4 Address.*?: (\\d+\\.\\d+\\.\\d+\\.\\d+)\" | Select-Object -First 1)\n if ($ip -match \"(\\d+\\.\\d+\\.\\d+\\.\\d+)\") {\n return $Matches[1]\n }\n }\n catch {\n # Ignore errors\n }\n\n return \"\"\n}\n\n# Known models list — used to detect custom models during install\n$script:KnownModels = @(\n \"gpt-5.4\", \"gpt-5.3-codex\", \"gpt-5-mini\", \"gpt-5-nano\",\n \"claude-opus-4-6\", \"claude-sonnet-4-6\", \"claude-haiku-4-5\",\n \"qwen3.6-plus\", \"qwen3.5-plus\", \"deepseek-chat\", \"deepseek-reasoner\",\n \"kimi-k2.5\", \"glm-5\", \"MiniMax-M2.7\", \"MiniMax-M2.7-highspeed\", \"MiniMax-M2.5\"\n)\n\nfunction Test-KnownModel {\n param([string]$Model)\n return $script:KnownModels -contains $Model\n}\n\nfunction Request-CustomModelParams {\n param([string]$Model)\n if (Test-KnownModel $Model) {\n $script:config.MODEL_CONTEXT_WINDOW = \"\"\n $script:config.MODEL_MAX_TOKENS = \"\"\n $script:config.MODEL_REASONING = \"\"\n $script:config.MODEL_VISION = \"\"\n return\n }\n Write-Host \"\"\n Write-Log (Get-Msg \"llm.custom_model.detected\" -f $Model)\n Write-Host \"\"\n $ctxInput = Read-Host \" $(Get-Msg 'llm.custom_model.context_prompt')\"\n if ($ctxInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $script:config.MODEL_CONTEXT_WINDOW = if ($ctxInput) { $ctxInput } else { \"150000\" }\n $maxInput = Read-Host \" $(Get-Msg 'llm.custom_model.max_tokens_prompt')\"\n if ($maxInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $script:config.MODEL_MAX_TOKENS = if ($maxInput) { $maxInput } else { \"128000\" }\n $reasoningInput = Read-Host \" $(Get-Msg 'llm.custom_model.reasoning_prompt')\"\n if ($reasoningInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $script:config.MODEL_REASONING = if ($reasoningInput -match \"^[nN]\") { \"false\" } else { \"true\" }\n $visionInput = Read-Host \" $(Get-Msg 'llm.custom_model.vision_prompt')\"\n if ($visionInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $script:config.MODEL_VISION = if ($visionInput -match \"^[yY]\") { \"true\" } else { \"false\" }\n Write-Log (Get-Msg \"llm.custom_model.summary\" -f $script:config.MODEL_CONTEXT_WINDOW, $script:config.MODEL_MAX_TOKENS, $script:config.MODEL_REASONING, $script:config.MODEL_VISION)\n}\n\nfunction New-RandomKey {\n # Generate 64 character hex string (32 bytes)\n $bytes = New-Object byte[] 32\n $rng = [System.Security.Cryptography.RandomNumberGenerator]::Create()\n $rng.GetBytes($bytes)\n return [BitConverter]::ToString($bytes).Replace(\"-\", \"\").ToLower()\n}\n\nfunction ConvertTo-DockerPath {\n param([string]$Path)\n\n # Convert Windows path to Docker mount format\n $fullPath = (Resolve-Path $Path -ErrorAction SilentlyContinue).Path\n if (-not $fullPath) {\n $fullPath = $Path\n }\n\n # Convert C:\\path to /c/path format for Docker\n if ($fullPath -match \"^([A-Za-z]):\") {\n $drive = $Matches[1].ToLower()\n $rest = $fullPath.Substring(2).Replace(\"\\\", \"/\")\n return \"/$drive$rest\"\n }\n return $fullPath.Replace(\"\\\", \"/\")\n}\n\n# Resolve the embedded controller image. Embedded mode is the only supported\n# architecture since PR #616 (manager image no longer bundles Higress/Tuwunel/MinIO).\n# Mirrors install/agentteams-install.sh::resolve_embedded_image — fail fast when the\n# embedded image is unavailable rather than silently falling back to the broken\n# legacy single-container path.\n# Sets $script:EMBEDDED_IMAGE and $script:AGENTTEAMS_USE_EMBEDDED.\nfunction Resolve-EmbeddedImage {\n $script:AGENTTEAMS_USE_EMBEDDED = \"1\"\n\n # Explicit override always wins (used by `make install-embedded` for local builds).\n if ($env:AGENTTEAMS_INSTALL_EMBEDDED_IMAGE) {\n $script:EMBEDDED_IMAGE = $env:AGENTTEAMS_INSTALL_EMBEDDED_IMAGE\n return\n }\n\n $versioned = \"$($script:AGENTTEAMS_REGISTRY)/agentteams/agentteams-embedded:$($script:AGENTTEAMS_VERSION)\"\n $latestTag = \"$($script:AGENTTEAMS_REGISTRY)/agentteams/agentteams-embedded:latest\"\n\n if ($script:AGENTTEAMS_VERSION -eq \"latest\") {\n $script:EMBEDDED_IMAGE = $latestTag\n return\n }\n\n docker pull $versioned *>$null\n if ($LASTEXITCODE -eq 0) {\n $script:EMBEDDED_IMAGE = $versioned\n return\n }\n docker pull $latestTag *>$null\n if ($LASTEXITCODE -eq 0) {\n Write-Log \"embedded $($script:AGENTTEAMS_VERSION) not found, using latest\"\n $script:EMBEDDED_IMAGE = $latestTag\n return\n }\n\n # Escape hatch for older versions (AGENTTEAMS_VERSION <= v1.0.9) whose manager image\n # still bundled the infrastructure — opt-in only, never silent.\n if ($env:AGENTTEAMS_FORCE_LEGACY -eq \"1\") {\n Write-Log \"WARNING: AGENTTEAMS_FORCE_LEGACY=1 - using legacy all-in-one manager architecture.\"\n Write-Log \"WARNING: This requires AGENTTEAMS_VERSION <= v1.0.9 (older bundled manager image).\"\n Write-Log \"WARNING: Newer slim manager images will hang on 'Waiting for Higress Gateway'.\"\n $script:AGENTTEAMS_USE_EMBEDDED = \"0\"\n return\n }\n\n Write-Host \"$($script:ESC)[31m[AgentTeams ERROR]$($script:ESC)[0m Embedded controller image is not available in the registry:\" -ForegroundColor Red\n Write-Host \" - tried: $versioned\" -ForegroundColor Red\n Write-Host \" - tried: $latestTag\" -ForegroundColor Red\n Write-Host \"\" -ForegroundColor Red\n Write-Host \"Embedded mode is the only supported architecture since PR #616.\" -ForegroundColor Red\n Write-Host \"How to resolve:\" -ForegroundColor Red\n Write-Host \" 1) Pin to a AGENTTEAMS_VERSION whose embedded image has been published, or\" -ForegroundColor Red\n Write-Host \" wait for the release pipeline to publish it.\" -ForegroundColor Red\n Write-Host \" 2) For a local build, run: make install-embedded\" -ForegroundColor Red\n Write-Host \" (builds and uses the local embedded image without touching the registry).\" -ForegroundColor Red\n Write-Host \" 3) Override with a custom image:\" -ForegroundColor Red\n Write-Host \" `$env:AGENTTEAMS_INSTALL_EMBEDDED_IMAGE=...; .\\agentteams-install.ps1\" -ForegroundColor Red\n Exit-Script 1\n}\n\nfunction Wait-ManagerReady {\n param(\n [string]$Container = \"agentteams-manager\",\n [int]$Timeout = $(if ($env:AGENTTEAMS_READY_TIMEOUT) { [int]$env:AGENTTEAMS_READY_TIMEOUT } else { 300 })\n )\n\n $elapsed = 0\n Write-Log (Get-Msg \"install.wait_ready\" -f $Timeout)\n\n $runtime = if ($script:config.MANAGER_RUNTIME) { $script:config.MANAGER_RUNTIME } else { \"copaw\" }\n\n while ($elapsed -lt $Timeout) {\n try {\n switch ($runtime) {\n \"copaw\" {\n $result = docker exec $Container curl -sf http://127.0.0.1:18799/api/agents 2>$null\n if ($result -match '\"agents\"') {\n Write-Log (Get-Msg \"install.wait_ready.ok\")\n return $true\n }\n }\n default {\n $result = docker exec $Container openclaw gateway health --json 2>$null\n if ($result -match '\"ok\"') {\n Write-Log (Get-Msg \"install.wait_ready.ok\")\n return $true\n }\n }\n }\n } catch {\n # Ignore errors during polling\n }\n\n Start-Sleep -Seconds 5\n $elapsed += 5\n Write-Host \"`r$($script:ESC)[36m[AgentTeams]$($script:ESC)[0m $(Get-Msg 'install.wait_ready.waiting' -f $elapsed, $Timeout)\" -NoNewline\n }\n\n Write-Host \"\"\n Write-Error (Get-Msg \"install.wait_ready.timeout\" -f $Timeout, $Container)\n}\n\nfunction Wait-MatrixReady {\n param(\n [string]$Container = \"agentteams-manager\",\n [int]$Timeout = $(if ($env:AGENTTEAMS_READY_TIMEOUT) { [int]$env:AGENTTEAMS_READY_TIMEOUT } else { 300 })\n )\n\n $elapsed = 0\n Write-Log (Get-Msg \"install.wait_matrix\" -f $Timeout)\n\n while ($elapsed -lt $Timeout) {\n try {\n $result = docker exec $Container curl -sf http://127.0.0.1:6167/_tuwunel/server_version 2>$null\n if ($result) {\n Write-Log (Get-Msg \"install.wait_matrix.ok\")\n return $true\n }\n } catch {\n # Ignore errors during polling\n }\n\n Start-Sleep -Seconds 5\n $elapsed += 5\n Write-Host \"`r$($script:ESC)[36m[AgentTeams]$($script:ESC)[0m $(Get-Msg 'install.wait_matrix.waiting' -f $elapsed, $Timeout)\" -NoNewline\n }\n\n Write-Host \"\"\n Write-Error (Get-Msg \"install.wait_matrix.timeout\" -f $Timeout, $Container)\n}\n\n# Read KEY=value from /data/agentteams-secrets.env on a Docker volume (manager container not required).\n# Requires $script:EMBEDDED_IMAGE (set by Resolve-EmbeddedImage before Install-Manager uses this).\nfunction Read-AgentTeamsSecretFromDataVolume {\n param(\n [string]$VolumeName,\n [string]$Key\n )\n if ([string]::IsNullOrEmpty($VolumeName) -or [string]::IsNullOrEmpty($Key) -or [string]::IsNullOrEmpty($script:EMBEDDED_IMAGE)) {\n return \"\"\n }\n $grepKey = [regex]::Escape($Key)\n $shCmd = \"grep \"\"^${grepKey}=\"\" /data/agentteams-secrets.env 2>/dev/null | cut -d= -f2- | head -1 | tr -d '\\r'\"\n $out = docker run --rm --entrypoint sh -v \"${VolumeName}:/data:ro\" $script:EMBEDDED_IMAGE -c $shCmd 2>$null\n if ($null -eq $out) { return \"\" }\n return ($out | Out-String).Trim()\n}\n\n# Read KEY=value from /data/worker-creds/<worker>.env on a Docker volume.\nfunction Read-AgentTeamsWorkerCredsFromVolume {\n param(\n [string]$VolumeName,\n [string]$WorkerName,\n [string]$Key\n )\n if ([string]::IsNullOrEmpty($VolumeName) -or [string]::IsNullOrEmpty($WorkerName) -or [string]::IsNullOrEmpty($Key) -or [string]::IsNullOrEmpty($script:EMBEDDED_IMAGE)) {\n return \"\"\n }\n $grepKey = [regex]::Escape($Key)\n $path = \"/data/worker-creds/${WorkerName}.env\"\n $shCmd = \"grep \"\"^${grepKey}=\"\" \"\"${path}\"\" 2>/dev/null | cut -d= -f2- | head -1 | tr -d '\\r'\"\n $out = docker run --rm --entrypoint sh -v \"${VolumeName}:/data:ro\" $script:EMBEDDED_IMAGE -c $shCmd 2>$null\n if ($null -eq $out) { return \"\" }\n return ($out | Out-String).Trim()\n}\n\n# Read admin_dm_room_id from host workspace state.json (fallback when Matrix API is unavailable).\nfunction Read-AgentTeamsAdminDmRoomFromWorkspace {\n param([string]$WorkspaceDir)\n if ([string]::IsNullOrEmpty($WorkspaceDir)) { return \"\" }\n $statePath = Join-Path $WorkspaceDir \"state.json\"\n if (-not (Test-Path $statePath)) { return \"\" }\n try {\n $j = Get-Content $statePath -Raw -ErrorAction Stop | ConvertFrom-Json\n $rid = $j.admin_dm_room_id\n if ($null -eq $rid) { return \"\" }\n $s = [string]$rid\n if ($s -eq \"\" -or $s -eq \"null\") { return \"\" }\n return $s.Trim()\n } catch {\n return \"\"\n }\n}\n\nfunction Get-AgentTeamsRandomHex {\n param([int]$ByteCount)\n $bytes = New-Object byte[] $ByteCount\n $rng = [System.Security.Cryptography.RandomNumberGenerator]::Create()\n $rng.GetBytes($bytes)\n return ([BitConverter]::ToString($bytes)).Replace(\"-\", \"\").ToLower()\n}\n\n# Resolve admin DM room with @manager (small room) via Matrix Client API inside agentteams-manager.\nfunction Get-AgentTeamsAdminDmRoomViaMatrix {\n param(\n [string]$AdminUser,\n [string]$AdminPassword\n )\n $running = docker ps --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-manager$\"\n if (-not $running) { return \"\" }\n if ([string]::IsNullOrEmpty($AdminPassword)) { return \"\" }\n\n $loginObj = @{\n type = \"m.login.password\"\n identifier = @{ type = \"m.id.user\"; user = $AdminUser }\n password = $AdminPassword\n }\n $loginJson = $loginObj | ConvertTo-Json -Compress -Depth 5\n try {\n $loginRaw = $loginJson | docker exec -i agentteams-manager sh -c \"curl -sf -X POST http://127.0.0.1:6167/_matrix/client/v3/login -H 'Content-Type: application/json' -d @-\" 2>$null\n if (-not $loginRaw) { return \"\" }\n $loginResp = $loginRaw | ConvertFrom-Json\n $token = [string]$loginResp.access_token\n if ([string]::IsNullOrEmpty($token)) { return \"\" }\n\n $roomsRaw = docker exec agentteams-manager curl -sf -X GET -H \"Authorization: Bearer $token\" `\n \"http://127.0.0.1:6167/_matrix/client/v3/joined_rooms\" 2>$null\n if (-not $roomsRaw) { return \"\" }\n $roomsObj = $roomsRaw | ConvertFrom-Json\n $roomList = @($roomsObj.joined_rooms)\n foreach ($roomId in $roomList) {\n if ([string]::IsNullOrEmpty($roomId)) { continue }\n $enc = $roomId.Replace(\"!\", \"%21\")\n $memRaw = docker exec agentteams-manager curl -sf -X GET -H \"Authorization: Bearer $token\" `\n \"http://127.0.0.1:6167/_matrix/client/v3/rooms/${enc}/members\" 2>$null\n if (-not $memRaw) { continue }\n $memObj = $memRaw | ConvertFrom-Json\n $chunk = @($memObj.chunk)\n $memberIds = @($chunk | ForEach-Object { $_.state_key })\n $match = $false\n foreach ($m in $memberIds) {\n $beforeColon = ($m -split \":\")[0]\n if ($m -like \"*manager*\" -and $beforeColon -notlike \"*admin*\") {\n $match = $true\n break\n }\n }\n if ($match -and $memberIds.Count -le 3) {\n return $roomId\n }\n }\n } catch {\n return \"\"\n }\n return \"\"\n}\n\nfunction New-EnvFile {\n param([hashtable]$Config, [string]$Path)\n\n $content = @\"\n# AgentTeams Manager Configuration\n# Generated by agentteams-install.ps1 on $(Get-Date -Format \"yyyy-MM-dd HH:mm:ss\")\n\n# Language\nAGENTTEAMS_LANGUAGE=$($Config.LANGUAGE)\n\n# LLM\nAGENTTEAMS_LLM_PROVIDER=$($Config.LLM_PROVIDER)\nAGENTTEAMS_DEFAULT_MODEL=$($Config.DEFAULT_MODEL)\nAGENTTEAMS_LLM_API_KEY=$($Config.LLM_API_KEY)\nAGENTTEAMS_OPENAI_BASE_URL=$($Config.OPENAI_BASE_URL)\nAGENTTEAMS_MODEL_CONTEXT_WINDOW=$($Config.MODEL_CONTEXT_WINDOW)\nAGENTTEAMS_MODEL_MAX_TOKENS=$($Config.MODEL_MAX_TOKENS)\nAGENTTEAMS_MODEL_REASONING=$($Config.MODEL_REASONING)\nAGENTTEAMS_MODEL_VISION=$($Config.MODEL_VISION)\n\n# Embedding model (empty = disabled, default: text-embedding-v4)\nAGENTTEAMS_EMBEDDING_MODEL=$($Config.EMBEDDING_MODEL)\n\n# Admin\nAGENTTEAMS_ADMIN_USER=$($Config.ADMIN_USER)\nAGENTTEAMS_ADMIN_PASSWORD=$($Config.ADMIN_PASSWORD)\n\n# Ports\nAGENTTEAMS_LOCAL_ONLY=$($Config.LOCAL_ONLY)\nAGENTTEAMS_PORT_GATEWAY=$($Config.PORT_GATEWAY)\nAGENTTEAMS_PORT_CONSOLE=$($Config.PORT_CONSOLE)\nAGENTTEAMS_PORT_ELEMENT_WEB=$($Config.PORT_ELEMENT_WEB)\nAGENTTEAMS_PORT_MANAGER_CONSOLE=$($Config.PORT_MANAGER_CONSOLE)\n\n# Matrix\nAGENTTEAMS_MATRIX_DOMAIN=$($Config.MATRIX_DOMAIN)\nAGENTTEAMS_MATRIX_CLIENT_DOMAIN=$($Config.MATRIX_CLIENT_DOMAIN)\n\n# Gateway\nAGENTTEAMS_AI_GATEWAY_DOMAIN=$($Config.AI_GATEWAY_DOMAIN)\nAGENTTEAMS_MANAGER_GATEWAY_KEY=$($Config.MANAGER_GATEWAY_KEY)\n\n# File System\nAGENTTEAMS_FS_DOMAIN=$($Config.FS_DOMAIN)\nAGENTTEAMS_CONSOLE_DOMAIN=$($Config.CONSOLE_DOMAIN)\nAGENTTEAMS_MINIO_USER=$($Config.MINIO_USER)\nAGENTTEAMS_MINIO_PASSWORD=$($Config.MINIO_PASSWORD)\n\n# Internal\nAGENTTEAMS_MANAGER_PASSWORD=$($Config.MANAGER_PASSWORD)\nAGENTTEAMS_REGISTRATION_TOKEN=$($Config.REGISTRATION_TOKEN)\n\n# GitHub (optional)\nAGENTTEAMS_GITHUB_TOKEN=$($Config.GITHUB_TOKEN)\n\n# Nacos package import defaults\nAGENTTEAMS_NACOS_REGISTRY_URI=$(if ($env:AGENTTEAMS_NACOS_REGISTRY_URI) { $env:AGENTTEAMS_NACOS_REGISTRY_URI } else { \"nacos://market.agentteams.io:80/public\" })\nAGENTTEAMS_NACOS_USERNAME=$($env:AGENTTEAMS_NACOS_USERNAME)\nAGENTTEAMS_NACOS_PASSWORD=$($env:AGENTTEAMS_NACOS_PASSWORD)\nAGENTTEAMS_NACOS_TOKEN=$($env:AGENTTEAMS_NACOS_TOKEN)\n\n# Skills Registry (optional, default: nacos://market.agentteams.io:80/public)\nAGENTTEAMS_SKILLS_API_URL=$(if ($Config.SKILLS_API_URL) { $Config.SKILLS_API_URL } else { \"nacos://market.agentteams.io:80/public\" })\n\n# OpenClaw CMS plugin (optional)\nAGENTTEAMS_CMS_TRACES_ENABLED=$(if ($env:AGENTTEAMS_CMS_TRACES_ENABLED) { $env:AGENTTEAMS_CMS_TRACES_ENABLED } else { \"false\" })\nAGENTTEAMS_CMS_ENDPOINT=$($env:AGENTTEAMS_CMS_ENDPOINT)\nAGENTTEAMS_CMS_LICENSE_KEY=$($env:AGENTTEAMS_CMS_LICENSE_KEY)\nAGENTTEAMS_CMS_PROJECT=$($env:AGENTTEAMS_CMS_PROJECT)\nAGENTTEAMS_CMS_WORKSPACE=$($env:AGENTTEAMS_CMS_WORKSPACE)\nAGENTTEAMS_CMS_SERVICE_NAME=$(if ($env:AGENTTEAMS_CMS_SERVICE_NAME) { $env:AGENTTEAMS_CMS_SERVICE_NAME } else { \"agentteams-manager\" })\nAGENTTEAMS_CMS_METRICS_ENABLED=$(if ($env:AGENTTEAMS_CMS_METRICS_ENABLED) { $env:AGENTTEAMS_CMS_METRICS_ENABLED } else { \"false\" })\n\n# Worker images (for direct container creation)\nAGENTTEAMS_WORKER_IMAGE=$($Config.WORKER_IMAGE)\nAGENTTEAMS_COPAW_WORKER_IMAGE=$($Config.COPAW_WORKER_IMAGE)\nAGENTTEAMS_QWENPAW_WORKER_IMAGE=$($Config.QWENPAW_WORKER_IMAGE)\nAGENTTEAMS_HERMES_WORKER_IMAGE=$($Config.HERMES_WORKER_IMAGE)\n\n# Manager runtime (openclaw | copaw)\nAGENTTEAMS_MANAGER_RUNTIME=$($Config.MANAGER_RUNTIME)\n\n# Default Worker runtime (openclaw | copaw | hermes)\nAGENTTEAMS_DEFAULT_WORKER_RUNTIME=$($Config.DEFAULT_WORKER_RUNTIME)\n\n# Matrix E2EE (0=disabled, 1=enabled; default: 0)\nAGENTTEAMS_MATRIX_E2EE=$($Config.MATRIX_E2EE)\n\n# Docker API proxy (0=disabled, 1=enabled; default: 1)\nAGENTTEAMS_DOCKER_PROXY=$($Config.DOCKER_PROXY)\n\n# Docker API proxy: additional allowed image sources (comma-separated)\nAGENTTEAMS_PROXY_ALLOWED_REGISTRIES=$($Config.PROXY_ALLOWED_REGISTRIES)\n\n# Worker idle timeout in minutes (default: 720 = 12 hours)\nAGENTTEAMS_WORKER_IDLE_TIMEOUT=$($Config.WORKER_IDLE_TIMEOUT)\n\n# JVM Args for Higress Console\nJVM_ARGS=$($env:JVM_ARGS)\n\n# Higress WASM plugin image registry (auto-selected by timezone)\nHIGRESS_ADMIN_WASM_PLUGIN_IMAGE_REGISTRY=$($Config.REGISTRY)\n\n# Data persistence\nAGENTTEAMS_DATA_DIR=$($Config.DATA_DIR)\n# Manager workspace (skills, memory, state - host-editable)\nAGENTTEAMS_WORKSPACE_DIR=$($Config.WORKSPACE_DIR)\n# Host directory sharing\nAGENTTEAMS_HOST_SHARE_DIR=$($Config.HOST_SHARE_DIR)\n\"@\n\n Set-Content -Path $Path -Value $content -Encoding UTF8\n Write-Log (Get-Msg \"install.config_saved\" -f $Path)\n}\n\n# ============================================================\n# Prompt Functions\n# ============================================================\n\nfunction Read-Prompt {\n param(\n [string]$VarName,\n [string]$PromptText,\n [string]$Default = \"\",\n [switch]$Secret,\n [switch]$Optional\n )\n\n # Check if already set in environment\n $envValue = [Environment]::GetEnvironmentVariable($VarName)\n if ($envValue) {\n if ($script:AGENTTEAMS_UPGRADE -and -not $script:AGENTTEAMS_NON_INTERACTIVE) {\n # Show current value (masked for secrets) and let user change it\n $displayValue = $envValue\n if ($Secret) {\n if ($envValue.Length -le 8) {\n $displayValue = \"****\"\n } else {\n $displayValue = $envValue.Substring(0, 4) + \"****\" + $envValue.Substring($envValue.Length - 4)\n }\n }\n Write-Log (Get-Msg \"prompt.upgrade_keep\" -f $PromptText, $displayValue)\n $prompt = $PromptText\n if ($Secret) {\n $newValue = Read-Host -Prompt $prompt -AsSecureString\n $newValue = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto(\n [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($newValue)\n )\n } else {\n $newValue = Read-Host -Prompt $prompt\n if ($newValue -eq \"b\") { $script:StepResult = \"back\"; return $null }\n }\n if ($newValue) {\n return $newValue\n }\n return $envValue\n }\n Write-Log (Get-Msg \"prompt.preset\" -f $PromptText)\n return $envValue\n }\n # Upgrade mode: optional fields with empty value — let user set a new value\n elseif ($Optional -and $script:AGENTTEAMS_UPGRADE -and -not $script:AGENTTEAMS_NON_INTERACTIVE) {\n Write-Log (Get-Msg \"prompt.upgrade_empty\" -f $PromptText)\n $prompt = $PromptText\n if ($Secret) {\n $newValue = Read-Host -Prompt $prompt -AsSecureString\n $newValue = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto(\n [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($newValue)\n )\n } else {\n $newValue = Read-Host -Prompt $prompt\n if ($newValue -eq \"b\") { $script:StepResult = \"back\"; return $null }\n }\n if ($newValue) {\n return $newValue\n }\n return \"\"\n }\n\n # Non-interactive or quickstart mode\n if ($script:AGENTTEAMS_NON_INTERACTIVE -or $script:AGENTTEAMS_QUICKSTART) {\n if ($Default) {\n Write-Log (Get-Msg \"prompt.default\" -f $PromptText, $Default)\n return $Default\n }\n elseif ($Optional) {\n return \"\"\n }\n elseif ($script:AGENTTEAMS_NON_INTERACTIVE) {\n # Only hard-error in fully non-interactive mode, not quickstart\n Write-Error (Get-Msg \"prompt.required\" -f $PromptText)\n }\n # quickstart + no default + not optional: fall through to interactive prompt\n }\n\n # Interactive prompt\n $prompt = if ($Default) { \"$PromptText [$Default]\" } else { $PromptText }\n\n if ($Secret) {\n $value = Read-Host -Prompt $prompt -AsSecureString\n $value = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto(\n [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($value)\n )\n }\n else {\n $value = Read-Host -Prompt $prompt\n if ($value -eq \"b\") { $script:StepResult = \"back\"; return $null }\n }\n\n if (-not $value -and $Default) {\n $value = $Default\n }\n\n if (-not $value -and -not $Optional) {\n Write-Error (Get-Msg \"prompt.required_empty\" -f $PromptText)\n }\n\n return $value\n}\n\n# Load current parameter values from env file for upgrade mode display\nfunction Load-CurrentParamsFromEnv {\n $envFile = $script:AGENTTEAMS_ENV_FILE\n if (Test-Path $envFile) {\n $content = Get-Content $envFile\n $content | ForEach-Object {\n if ($_ -match \"^AGENTTEAMS_LLM_PROVIDER=(.*)$\") {\n $script:config.LLM_PROVIDER = $Matches[1].Trim()\n }\n if ($_ -match \"^AGENTTEAMS_OPENAI_BASE_URL=(.*)$\") {\n $script:config.OPENAI_BASE_URL = $Matches[1].Trim()\n }\n if ($_ -match \"^AGENTTEAMS_DEFAULT_MODEL=(.*)$\") {\n $script:config.DEFAULT_MODEL = $Matches[1].Trim()\n }\n if ($_ -match \"^AGENTTEAMS_EMBEDDING_MODEL=(.*)$\") {\n $script:config.EMBEDDING_MODEL = $Matches[1].Trim()\n }\n if ($_ -match \"^AGENTTEAMS_WORKSPACE_DIR=(.*)$\") {\n $script:config.WORKSPACE_DIR = $Matches[1].Trim()\n }\n if ($_ -match \"^AGENTTEAMS_HOST_SHARE_DIR=(.*)$\") {\n $script:config.HOST_SHARE_DIR = $Matches[1].Trim()\n }\n }\n }\n}\n\n# ============================================================\n# OpenAI-Compatible Provider\n# ============================================================\n\nfunction Test-LlmConnectivity {\n param(\n [string]$BaseUrl,\n [string]$ApiKey,\n [string]$Model,\n [string]$Hint = \"\"\n )\n Write-Log (Get-Msg \"llm.openai.test.testing\")\n $uri = ($BaseUrl.TrimEnd('/')) + \"/chat/completions\"\n $bodyHash = @{\n model = $Model\n messages = @(@{ role = \"user\"; content = \"hi\" })\n }\n $body = $bodyHash | ConvertTo-Json -Compress\n try {\n $response = Invoke-WebRequest -Uri $uri -Method POST `\n -Headers @{ \"Authorization\" = \"Bearer $ApiKey\"; \"Content-Type\" = \"application/json\"; \"User-Agent\" = \"AgentTeams/$($script:AGENTTEAMS_VERSION)\" } `\n -Body $body -TimeoutSec 30 -ErrorAction Stop -UseBasicParsing\n Write-Log (Get-Msg \"llm.openai.test.ok\")\n } catch {\n $statusCode = if ($_.Exception.Response) { [int]$_.Exception.Response.StatusCode } else { 0 }\n $responseBody = \"\"\n if ($_.Exception.Response) {\n try {\n $stream = $_.Exception.Response.GetResponseStream()\n $reader = New-Object System.IO.StreamReader($stream)\n $responseBody = $reader.ReadToEnd()\n } catch {}\n }\n Write-Host (Get-Msg \"llm.openai.test.fail\" -f $statusCode, $responseBody) -ForegroundColor Yellow\n if ($Hint) {\n Write-Host $Hint -ForegroundColor Yellow\n }\n if (-not $script:AGENTTEAMS_NON_INTERACTIVE) {\n $confirm = Read-Host (Get-Msg \"llm.openai.test.confirm\")\n if ($confirm -eq \"b\" -or $confirm -eq \"B\") {\n $script:StepResult = \"back\"\n return\n }\n if ($confirm -ne \"y\" -and $confirm -ne \"Y\") {\n Write-Log (Get-Msg \"llm.openai.test.aborted\")\n Exit-Script 1\n }\n }\n }\n}\n\nfunction Test-EmbeddingConnectivity {\n param(\n [string]$BaseUrl,\n [string]$ApiKey,\n [string]$Model\n )\n Write-Log (Get-Msg \"llm.embedding.test.testing\")\n $uri = ($BaseUrl.TrimEnd('/')) + \"/embeddings\"\n $bodyHash = @{\n model = $Model\n input = \"test\"\n }\n $body = $bodyHash | ConvertTo-Json -Compress\n try {\n $response = Invoke-WebRequest -Uri $uri -Method POST `\n -Headers @{ \"Authorization\" = \"Bearer $ApiKey\"; \"Content-Type\" = \"application/json\"; \"User-Agent\" = \"AgentTeams/$($script:AGENTTEAMS_VERSION)\" } `\n -Body $body -TimeoutSec 30 -ErrorAction Stop -UseBasicParsing\n Write-Log (Get-Msg \"llm.embedding.test.ok\")\n return $true\n } catch {\n $statusCode = if ($_.Exception.Response) { [int]$_.Exception.Response.StatusCode } else { 0 }\n $responseBody = \"\"\n if ($_.Exception.Response) {\n try {\n $stream = $_.Exception.Response.GetResponseStream()\n $reader = New-Object System.IO.StreamReader($stream)\n $responseBody = $reader.ReadToEnd()\n } catch {}\n }\n Write-Host (Get-Msg \"llm.embedding.test.fail\" -f $statusCode, $responseBody) -ForegroundColor Yellow\n return $false\n }\n}\n\nfunction New-OpenAICompatProvider {\n param(\n [string]$BaseUrl,\n [string]$ApiKey,\n [int]$ConsolePort = 18001\n )\n\n if (-not $BaseUrl -or -not $ApiKey) {\n Write-Log (Get-Msg \"install.openai_compat.missing\")\n return $false\n }\n\n $consoleUrl = \"http://localhost:$ConsolePort\"\n\n # Parse base URL\n $protocol = \"https\"\n $port = 443\n $urlWithoutProto = $BaseUrl -replace \"^https?://\", \"\"\n\n if ($BaseUrl -match \"^http://\") {\n $protocol = \"http\"\n $port = 80\n }\n\n $domain = $urlWithoutProto.Split(\"/\")[0]\n\n if ($domain -match \":(\\d+)$\") {\n $port = [int]$Matches[1]\n $domain = $domain -replace \":\\d+$\", \"\"\n }\n\n Write-Log (Get-Msg \"install.openai_compat.creating\")\n Write-Log (Get-Msg \"install.openai_compat.domain\" -f $domain)\n Write-Log (Get-Msg \"install.openai_compat.port\" -f $port)\n Write-Log (Get-Msg \"install.openai_compat.protocol\" -f $protocol)\n\n $serviceName = \"openai-compat\"\n\n # Create DNS service source\n $serviceBody = @{\n type = \"dns\"\n name = $serviceName\n port = $port.ToString()\n protocol = $protocol\n proxyName = \"\"\n domain = $domain\n } | ConvertTo-Json -Compress\n\n try {\n Invoke-RestMethod -Uri \"$consoleUrl/v1/service-sources\" -Method POST -ContentType \"application/json\" -Body $serviceBody -ErrorAction SilentlyContinue | Out-Null\n }\n catch {\n Write-Log (Get-Msg \"install.openai_compat.service_fail\")\n }\n\n Start-Sleep -Seconds 2\n\n # Create AI provider\n $providerBody = @{\n type = \"openai\"\n name = \"openai-compat\"\n tokens = @($ApiKey)\n version = 0\n protocol = \"openai/v1\"\n tokenFailoverConfig = @{ enabled = $false }\n rawConfigs = @{\n openaiCustomUrl = $BaseUrl\n openaiCustomServiceName = \"$serviceName.dns\"\n openaiCustomServicePort = $port\n }\n } | ConvertTo-Json -Compress -Depth 3\n\n try {\n Invoke-RestMethod -Uri \"$consoleUrl/v1/ai/providers\" -Method POST -ContentType \"application/json\" -Body $providerBody -ErrorAction SilentlyContinue | Out-Null\n Write-Log (Get-Msg \"install.openai_compat.success\")\n return $true\n }\n catch {\n Write-Log (Get-Msg \"install.openai_compat.provider_fail\")\n return $false\n }\n}\n\n# ============================================================\n# State Machine Helpers\n# ============================================================\n\nfunction Test-ShouldSkipStep {\n param([string]$StepFn)\n switch ($StepFn) {\n { $_ -in @(\"Step-Lang\", \"Step-Mode\") } {\n return $script:AGENTTEAMS_NON_INTERACTIVE\n }\n \"Step-Existing\" {\n return (-not (Test-Path $script:AGENTTEAMS_ENV_FILE))\n }\n # Keep-All upgrade mode: skip all config steps, values are already loaded\n { $_ -in @(\"Step-Llm\", \"Step-Admin\", \"Step-Network\", \"Step-Ports\", \"Step-Domains\",\n \"Step-Github\", \"Step-Skills\",\n \"Step-Runtime\", \"Step-ManagerRuntime\", \"Step-E2ee\", \"Step-DockerProxy\",\n \"Step-Idle\", \"Step-Hostshare\") } {\n if ($script:AGENTTEAMS_UPGRADE -and $env:AGENTTEAMS_UPGRADE_KEEP_ALL -eq \"1\") { return $true }\n return $false\n }\n { $_ -in @(\"Step-Volume\", \"Step-Workspace\") } {\n if ($script:AGENTTEAMS_UPGRADE -and $env:AGENTTEAMS_UPGRADE_KEEP_ALL -eq \"1\") { return $true }\n if ($script:AGENTTEAMS_NON_INTERACTIVE) { return $true }\n if ($script:AGENTTEAMS_QUICKSTART) { return $true }\n return $false\n }\n { $_ -in @(\"Step-E2ee\", \"Step-Idle\") } {\n if ($script:AGENTTEAMS_NON_INTERACTIVE) { return $true }\n if ($script:AGENTTEAMS_QUICKSTART -and -not $script:AGENTTEAMS_UPGRADE) { return $true }\n return $false\n }\n \"Step-DockerProxy\" {\n # Embedded controller IS the proxy/orchestrator — no separate proxy step needed.\n if ($script:AGENTTEAMS_USE_EMBEDDED -eq \"1\") { return $true }\n if ($script:AGENTTEAMS_NON_INTERACTIVE) { return $true }\n if ($script:AGENTTEAMS_QUICKSTART -and -not $script:AGENTTEAMS_UPGRADE) { return $true }\n return $false\n }\n \"Step-ManagerRuntime\" {\n if ($script:AGENTTEAMS_NON_INTERACTIVE) { return $true }\n return $false\n }\n \"Step-Hostshare\" {\n if ($script:AGENTTEAMS_NON_INTERACTIVE) { return $true }\n if ($script:AGENTTEAMS_QUICKSTART) { return $true }\n return $false\n }\n default { return $false }\n }\n}\n\nfunction Clear-StepVars {\n param([string]$StepFn)\n switch ($StepFn) {\n \"Step-Mode\" { $script:AGENTTEAMS_QUICKSTART = $false }\n \"Step-Existing\" {\n $script:AGENTTEAMS_UPGRADE = $false\n $script:UPGRADE_EXISTING_WORKERS = $null\n }\n \"Step-Llm\" {\n foreach ($k in @(\"AGENTTEAMS_LLM_PROVIDER\",\"AGENTTEAMS_DEFAULT_MODEL\",\"AGENTTEAMS_OPENAI_BASE_URL\",\n \"AGENTTEAMS_LLM_API_KEY\",\"AGENTTEAMS_MODEL_CONTEXT_WINDOW\",\"AGENTTEAMS_MODEL_MAX_TOKENS\",\n \"AGENTTEAMS_MODEL_REASONING\",\"AGENTTEAMS_MODEL_VISION\")) {\n [Environment]::SetEnvironmentVariable($k, $null, \"Process\")\n }\n }\n \"Step-Admin\" {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_ADMIN_USER\", $null, \"Process\")\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_ADMIN_PASSWORD\", $null, \"Process\")\n }\n \"Step-Network\" {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_LOCAL_ONLY\", $null, \"Process\")\n }\n \"Step-Ports\" {\n foreach ($k in @(\"AGENTTEAMS_PORT_GATEWAY\",\"AGENTTEAMS_PORT_CONSOLE\",\"AGENTTEAMS_PORT_ELEMENT_WEB\",\"AGENTTEAMS_PORT_MANAGER_CONSOLE\")) {\n [Environment]::SetEnvironmentVariable($k, $null, \"Process\")\n }\n }\n \"Step-Domains\" {\n foreach ($k in @(\"AGENTTEAMS_MATRIX_DOMAIN\",\"AGENTTEAMS_MATRIX_CLIENT_DOMAIN\",\"AGENTTEAMS_AI_GATEWAY_DOMAIN\",\"AGENTTEAMS_FS_DOMAIN\",\"AGENTTEAMS_CONSOLE_DOMAIN\")) {\n [Environment]::SetEnvironmentVariable($k, $null, \"Process\")\n }\n }\n \"Step-Github\" {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_GITHUB_TOKEN\", $null, \"Process\")\n }\n \"Step-Skills\" {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_SKILLS_API_URL\", $null, \"Process\")\n }\n \"Step-Volume\" {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_DATA_DIR\", $null, \"Process\")\n }\n \"Step-Workspace\" {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_WORKSPACE_DIR\", $null, \"Process\")\n }\n \"Step-Runtime\" {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_DEFAULT_WORKER_RUNTIME\", $null, \"Process\")\n }\n \"Step-ManagerRuntime\" {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_MANAGER_RUNTIME\", $null, \"Process\")\n }\n \"Step-E2ee\" {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_MATRIX_E2EE\", $null, \"Process\")\n }\n \"Step-DockerProxy\" {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_DOCKER_PROXY\", $null, \"Process\")\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_PROXY_ALLOWED_REGISTRIES\", $null, \"Process\")\n }\n \"Step-Idle\" {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_WORKER_IDLE_TIMEOUT\", $null, \"Process\")\n }\n \"Step-Hostshare\" {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_HOST_SHARE_DIR\", $null, \"Process\")\n }\n }\n}\n\n# ============================================================\n# Individual step functions\n# ============================================================\n\nfunction Step-Lang {\n $langDefaultChoice = if ($script:AGENTTEAMS_LANGUAGE -eq \"zh\") { \"1\" } else { \"2\" }\n $langDetectedKey = \"lang.detected.$($script:AGENTTEAMS_LANGUAGE)\"\n Write-Log (Get-Msg $langDetectedKey)\n Write-Log (Get-Msg \"lang.switch_title\")\n Write-Host (Get-Msg \"lang.option_zh\")\n Write-Host (Get-Msg \"lang.option_en\")\n Write-Host \"\"\n $langChoice = Read-Host \"$(Get-Msg 'lang.prompt') [$langDefaultChoice]\"\n if (-not $langChoice) { $langChoice = $langDefaultChoice }\n if ($langChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n switch ($langChoice) {\n \"1\" { $script:AGENTTEAMS_LANGUAGE = \"zh\" }\n \"2\" { $script:AGENTTEAMS_LANGUAGE = \"en\" }\n }\n $env:AGENTTEAMS_LANGUAGE = $script:AGENTTEAMS_LANGUAGE\n Write-Log \"\"\n}\n\nfunction Step-Mode {\n Write-Log (Get-Msg \"install.mode.title\")\n Write-Host \"\"\n Write-Host (Get-Msg \"install.mode.choose\")\n Write-Host (Get-Msg \"install.mode.quickstart\")\n Write-Host (Get-Msg \"install.mode.manual\")\n Write-Host \"\"\n $choice = Read-Host (Get-Msg \"install.mode.prompt\")\n $choice = if ($choice) { $choice } else { \"1\" }\n if ($choice -eq \"b\") { $script:StepResult = \"back\"; return }\n switch -Regex ($choice) {\n \"^(1|quick|quickstart)$\" {\n Write-Log (Get-Msg \"install.mode.quickstart_selected\")\n $script:AGENTTEAMS_QUICKSTART = $true\n }\n \"^(2|manual)$\" {\n Write-Log (Get-Msg \"install.mode.manual_selected\")\n $script:AGENTTEAMS_QUICKSTART = $false\n }\n default {\n Write-Log (Get-Msg \"install.mode.invalid\")\n $script:AGENTTEAMS_QUICKSTART = $true\n }\n }\n Write-Log \"\"\n}\n\nfunction Step-Existing {\n # This step is skipped when env file doesn't exist\n Write-Log (Get-Msg \"install.existing.detected\" -f $script:AGENTTEAMS_ENV_FILE)\n\n $runningManager = docker ps --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-manager$\"\n $runningWorkers = docker ps --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-worker-\"\n $existingWorkers = docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-worker-\"\n\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n Write-Log (Get-Msg \"install.existing.upgrade_noninteractive\")\n $upgradeChoice = \"1\"\n } else {\n Write-Host \"\"\n Write-Host (Get-Msg \"install.existing.choose\")\n Write-Host (Get-Msg \"install.existing.upgrade\")\n Write-Host (Get-Msg \"install.existing.reinstall\")\n Write-Host (Get-Msg \"install.existing.cancel\")\n Write-Host \"\"\n $upgradeChoice = Read-Host (Get-Msg \"install.existing.prompt\")\n $upgradeChoice = if ($upgradeChoice) { $upgradeChoice } else { \"1\" }\n if ($upgradeChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n }\n\n switch -Regex ($upgradeChoice) {\n \"^(1|upgrade)$\" {\n $script:AGENTTEAMS_UPGRADE = $true\n Write-Log (Get-Msg \"install.existing.upgrading\")\n\n # Show upgrade mode sub-menu (unless already set via env var)\n if ($env:AGENTTEAMS_UPGRADE_KEEP_ALL -ne \"1\") {\n Write-Host \"\"\n Write-Host (Get-Msg \"upgrade.mode.prompt\")\n Write-Host (Get-Msg \"upgrade.mode.keep_all\")\n Write-Host (Get-Msg \"upgrade.mode.confirm_each\")\n Write-Host (Get-Msg \"upgrade.mode.back\")\n Write-Host \"\"\n $upgradeModeChoice = Read-Host (Get-Msg \"install.existing.prompt\")\n $upgradeModeChoice = if ($upgradeModeChoice) { $upgradeModeChoice } else { \"1\" }\n switch -Regex ($upgradeModeChoice) {\n \"^(1|keep)$\" {\n $env:AGENTTEAMS_UPGRADE_KEEP_ALL = \"1\"\n }\n \"^(2|confirm)$\" {\n $env:AGENTTEAMS_UPGRADE_KEEP_ALL = \"0\"\n }\n \"^(3|b)$\" {\n $script:StepResult = \"back\"\n return\n }\n default {\n $env:AGENTTEAMS_UPGRADE_KEEP_ALL = \"0\"\n }\n }\n }\n # Load current parameters for both Keep-All and confirm-each modes\n Load-CurrentParamsFromEnv\n\n if ($runningManager -or $runningWorkers) {\n Write-Host \"\"\n Write-Host \"$($script:ESC)[33m$(Get-Msg 'install.existing.warn_manager_stop')$($script:ESC)[0m\"\n if ($existingWorkers) {\n Write-Host \"$($script:ESC)[33m$(Get-Msg 'install.existing.warn_worker_recreate')$($script:ESC)[0m\"\n }\n if (-not $script:AGENTTEAMS_NON_INTERACTIVE) {\n $confirm = Read-Host (Get-Msg \"install.existing.continue_prompt\")\n if ($confirm -eq \"b\" -or $confirm -eq \"B\") { $script:StepResult = \"back\"; return }\n if ($confirm -ne \"y\" -and $confirm -ne \"Y\") {\n Write-Log (Get-Msg \"install.existing.cancelled\")\n exit 0\n }\n }\n }\n $script:UPGRADE_EXISTING_WORKERS = $existingWorkers\n }\n \"^(2|reinstall)$\" {\n Write-Log (Get-Msg \"install.reinstall.performing\")\n $existingWorkspace = \"$env:USERPROFILE\\agentteams-manager\"\n if (Test-Path $script:AGENTTEAMS_ENV_FILE) {\n $envContent = Get-Content $script:AGENTTEAMS_ENV_FILE\n $wsLine = $envContent | Select-String \"^AGENTTEAMS_WORKSPACE_DIR=\"\n if ($wsLine) {\n $existingWorkspace = $wsLine.Line.Substring(21)\n }\n }\n Write-Host \"\"\n Write-Host \"$($script:ESC)[33m$(Get-Msg 'install.reinstall.warn_stop')$($script:ESC)[0m\"\n if ($runningManager) { Write-Host \"$($script:ESC)[33m - agentteams-manager (manager)$($script:ESC)[0m\" }\n $runningWorkers | ForEach-Object { Write-Host \"$($script:ESC)[33m - $_ (worker)$($script:ESC)[0m\" }\n Write-Host \"\"\n Write-Host \"$($script:ESC)[31m$(Get-Msg 'install.reinstall.warn_delete')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[31m$(Get-Msg 'install.reinstall.warn_volume')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[31m$(Get-Msg 'install.reinstall.warn_env' -f $script:AGENTTEAMS_ENV_FILE)$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[31m$(Get-Msg 'install.reinstall.warn_workspace' -f $existingWorkspace)$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[31m$(Get-Msg 'install.reinstall.warn_workers')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[31m$(Get-Msg 'install.reinstall.warn_proxy')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[31m$(Get-Msg 'install.reinstall.warn_network')$($script:ESC)[0m\"\n Write-Host \"\"\n Write-Host \"$($script:ESC)[31m$(Get-Msg 'install.reinstall.confirm_type')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[31m $existingWorkspace$($script:ESC)[0m\"\n Write-Host \"\"\n $confirmPath = Read-Host (Get-Msg \"install.reinstall.confirm_path\")\n if ($confirmPath -ne $existingWorkspace) {\n Write-Error (Get-Msg \"install.reinstall.path_mismatch\" -f $confirmPath, $existingWorkspace)\n }\n Write-Log (Get-Msg \"install.reinstall.confirmed\")\n docker stop agentteams-manager *>$null\n docker rm agentteams-manager *>$null\n docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-worker-\" | ForEach-Object {\n docker stop $_ *>$null\n docker rm $_ *>$null\n Write-Log (Get-Msg \"install.reinstall.removed_worker\" -f $_)\n }\n $existingController = docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-controller$\"\n if ($existingController) {\n Write-Log (Get-Msg \"install.reinstall.removing_proxy\")\n docker stop agentteams-controller *>$null\n docker rm agentteams-controller *>$null\n }\n if (docker volume ls -q 2>$null | Select-String \"^agentteams-data$\") {\n Write-Log (Get-Msg \"install.reinstall.removing_volume\")\n docker volume rm agentteams-data *>$null\n }\n if (Test-Path $existingWorkspace) {\n Write-Log (Get-Msg \"install.reinstall.removing_workspace\" -f $existingWorkspace)\n Remove-Item -Recurse -Force $existingWorkspace\n }\n if (Test-Path $script:AGENTTEAMS_ENV_FILE) {\n Write-Log (Get-Msg \"install.reinstall.removing_env\" -f $script:AGENTTEAMS_ENV_FILE)\n Remove-Item -Force $script:AGENTTEAMS_ENV_FILE\n }\n $existingNetwork = docker network ls --format \"{{.Name}}\" 2>$null | Select-String \"^agentteams-net$\"\n if ($existingNetwork) {\n Write-Log (Get-Msg \"install.reinstall.removing_network\")\n docker network rm agentteams-net *>$null\n }\n Write-Log (Get-Msg \"install.reinstall.cleanup_done\")\n }\n default {\n Write-Log (Get-Msg \"install.existing.cancelled\")\n exit 0\n }\n }\n\n # Load existing env file (upgrade path)\n if (Test-Path $script:AGENTTEAMS_ENV_FILE) {\n Write-Log (Get-Msg \"install.loading_config\" -f $script:AGENTTEAMS_ENV_FILE)\n Get-Content $script:AGENTTEAMS_ENV_FILE | ForEach-Object {\n if ($_ -match \"^([^#=][^=]*)=(.*)$\") {\n $key = $Matches[1].Trim()\n $value = $Matches[2].Split(\"#\")[0].Trim()\n if (-not [Environment]::GetEnvironmentVariable($key)) {\n [Environment]::SetEnvironmentVariable($key, $value, \"Process\")\n }\n }\n }\n }\n}\n\nfunction Step-Llm {\n Write-Log (Get-Msg \"llm.title\")\n\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n if ($script:AGENTTEAMS_LANGUAGE -eq \"zh\") {\n $script:config.LLM_PROVIDER = if ($env:AGENTTEAMS_LLM_PROVIDER) { $env:AGENTTEAMS_LLM_PROVIDER } else { \"openai-compat\" }\n $script:config.DEFAULT_MODEL = if ($env:AGENTTEAMS_DEFAULT_MODEL) { $env:AGENTTEAMS_DEFAULT_MODEL } else { \"qwen3.6-plus\" }\n $script:config.OPENAI_BASE_URL = if ($env:AGENTTEAMS_OPENAI_BASE_URL) { $env:AGENTTEAMS_OPENAI_BASE_URL } else { \"https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1\" }\n Write-Log (Get-Msg \"llm.provider.label\" -f $script:config.LLM_PROVIDER)\n Write-Log (Get-Msg \"llm.openai.base_url_label\" -f $script:config.OPENAI_BASE_URL)\n } else {\n $script:config.LLM_PROVIDER = if ($env:AGENTTEAMS_LLM_PROVIDER) { $env:AGENTTEAMS_LLM_PROVIDER } else { \"qwen\" }\n $script:config.DEFAULT_MODEL = if ($env:AGENTTEAMS_DEFAULT_MODEL) { $env:AGENTTEAMS_DEFAULT_MODEL } else { \"qwen3.6-plus\" }\n $script:config.OPENAI_BASE_URL = if ($env:AGENTTEAMS_OPENAI_BASE_URL) { $env:AGENTTEAMS_OPENAI_BASE_URL } else { \"\" }\n Write-Log (Get-Msg \"llm.provider.qwen_default\" -f $script:config.LLM_PROVIDER)\n }\n Write-Log (Get-Msg \"llm.model.label\" -f $script:config.DEFAULT_MODEL)\n Write-Log \"\"\n $script:config.LLM_API_KEY = Read-Prompt -VarName \"AGENTTEAMS_LLM_API_KEY\" -PromptText (Get-Msg \"llm.apikey_prompt\") -Secret\n $script:config.EMBEDDING_MODEL = if ($null -ne $env:AGENTTEAMS_EMBEDDING_MODEL) { $env:AGENTTEAMS_EMBEDDING_MODEL } else { \"text-embedding-v4\" }\n return\n }\n\n Write-Host \"\"\n Write-Host (Get-Msg \"llm.providers_title\")\n Write-Host (Get-Msg \"llm.provider.alibaba\")\n Write-Host (Get-Msg \"llm.provider.openai_compat\")\n Write-Host \"\"\n\n # If upgrade mode with loaded provider, show current as default\n if ($script:AGENTTEAMS_UPGRADE -and $script:config.LLM_PROVIDER) {\n $defaultProvider = if ($script:config.LLM_PROVIDER -eq \"openai-compat\") { \"2\" } else { \"1\" }\n $providerChoice = Read-Host \"$(Get-Msg 'llm.provider.select') [${defaultProvider}]\"\n $providerChoice = if ($providerChoice) { $providerChoice } else { $defaultProvider }\n } elseif ($script:AGENTTEAMS_QUICKSTART) {\n $providerChoice = Read-Host \"$(Get-Msg 'llm.provider.select') [1]\"\n } else {\n $providerChoice = Read-Host (Get-Msg \"llm.provider.select\")\n $providerChoice = if ($providerChoice) { $providerChoice } else { \"1\" }\n }\n if ($providerChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n\n switch -Regex ($providerChoice) {\n \"^(1|alibaba-cloud)$\" {\n $alibabaAccess = $null\n if ($script:AGENTTEAMS_LANGUAGE -eq \"en\") {\n $script:config.LLM_PROVIDER = \"openai-compat\"\n $script:config.OPENAI_BASE_URL = \"https://dashscope-intl.aliyuncs.com/compatible-mode/v1\"\n $alibabaAccess = \"tokenplan\"\n Write-Host \"\"\n Write-Host (Get-Msg \"llm.codingplan.models_title\")\n Write-Host (Get-Msg \"llm.codingplan.model.qwen36plus\")\n Write-Host (Get-Msg \"llm.codingplan.model.glm5\")\n Write-Host (Get-Msg \"llm.codingplan.model.kimi\")\n Write-Host (Get-Msg \"llm.codingplan.model.minimax\")\n Write-Host \"\"\n # If upgrade with loaded model, show current as default\n if ($script:AGENTTEAMS_UPGRADE -and $script:config.DEFAULT_MODEL) {\n $defaultModel = switch ($script:config.DEFAULT_MODEL) {\n \"qwen3.6-plus\" { \"1\" }\n \"glm-5\" { \"2\" }\n \"kimi-k2.5\" { \"3\" }\n \"MiniMax-M2.5\" { \"4\" }\n default { \"1\" }\n }\n $codingPlanModelChoice = Read-Host \"$(Get-Msg 'llm.codingplan.model.select') [${defaultModel}]\"\n $codingPlanModelChoice = if ($codingPlanModelChoice) { $codingPlanModelChoice } else { $defaultModel }\n } elseif ($script:AGENTTEAMS_QUICKSTART) {\n $codingPlanModelChoice = Read-Host \"$(Get-Msg 'llm.codingplan.model.select') [1]\"\n } else {\n $codingPlanModelChoice = Read-Host (Get-Msg \"llm.codingplan.model.select\")\n }\n $codingPlanModelChoice = if ($codingPlanModelChoice) { $codingPlanModelChoice } else { \"1\" }\n if ($codingPlanModelChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n switch -Regex ($codingPlanModelChoice) {\n \"^(1|qwen3\\.6-plus)$\" { $script:config.DEFAULT_MODEL = \"qwen3.6-plus\" }\n \"^(2|glm-5)$\" { $script:config.DEFAULT_MODEL = \"glm-5\" }\n \"^(3|kimi-k2\\.5)$\" { $script:config.DEFAULT_MODEL = \"kimi-k2.5\" }\n \"^(4|MiniMax-M2\\.5)$\" { $script:config.DEFAULT_MODEL = \"MiniMax-M2.5\" }\n default { $script:config.DEFAULT_MODEL = \"qwen3.6-plus\" }\n }\n Write-Log (Get-Msg \"llm.provider.selected_codingplan\")\n } else {\n Write-Host \"\"\n Write-Host (Get-Msg \"llm.alibaba.models_title\")\n Write-Host (Get-Msg \"llm.alibaba.model.tokenplan\")\n Write-Host (Get-Msg \"llm.alibaba.model.bailian\")\n Write-Host (Get-Msg \"llm.alibaba.model.codingplan_legacy\")\n Write-Host \"\"\n $modelChoice = if ($script:AGENTTEAMS_QUICKSTART) {\n Read-Host \"$(Get-Msg 'llm.alibaba.model.select') [1]\"\n } else {\n Read-Host (Get-Msg \"llm.alibaba.model.select\")\n }\n $modelChoice = if ($modelChoice) { $modelChoice } else { \"1\" }\n if ($modelChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n\n if ($modelChoice -match \"^(1|token-plan|tokenplan)$\") {\n $alibabaAccess = \"tokenplan\"\n $script:config.LLM_PROVIDER = \"openai-compat\"\n $script:config.OPENAI_BASE_URL = \"https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1\"\n Write-Host \"\"\n Write-Host (Get-Msg \"llm.codingplan.models_title\")\n Write-Host (Get-Msg \"llm.codingplan.model.qwen36plus\")\n Write-Host (Get-Msg \"llm.codingplan.model.glm5\")\n Write-Host (Get-Msg \"llm.codingplan.model.kimi\")\n Write-Host (Get-Msg \"llm.codingplan.model.minimax\")\n Write-Host \"\"\n # If upgrade with loaded model, show current as default\n if ($script:AGENTTEAMS_UPGRADE -and $script:config.DEFAULT_MODEL) {\n $defaultModel = switch ($script:config.DEFAULT_MODEL) {\n \"qwen3.6-plus\" { \"1\" }\n \"glm-5\" { \"2\" }\n \"kimi-k2.5\" { \"3\" }\n \"MiniMax-M2.5\" { \"4\" }\n default { \"1\" }\n }\n $codingPlanModelChoice = Read-Host \"$(Get-Msg 'llm.codingplan.model.select') [${defaultModel}]\"\n $codingPlanModelChoice = if ($codingPlanModelChoice) { $codingPlanModelChoice } else { $defaultModel }\n } elseif ($script:AGENTTEAMS_QUICKSTART) {\n $codingPlanModelChoice = Read-Host \"$(Get-Msg 'llm.codingplan.model.select') [1]\"\n } else {\n $codingPlanModelChoice = Read-Host (Get-Msg \"llm.codingplan.model.select\")\n }\n $codingPlanModelChoice = if ($codingPlanModelChoice) { $codingPlanModelChoice } else { \"1\" }\n if ($codingPlanModelChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n switch -Regex ($codingPlanModelChoice) {\n \"^(1|qwen3\\.6-plus)$\" { $script:config.DEFAULT_MODEL = \"qwen3.6-plus\" }\n \"^(2|glm-5)$\" { $script:config.DEFAULT_MODEL = \"glm-5\" }\n \"^(3|kimi-k2\\.5)$\" { $script:config.DEFAULT_MODEL = \"kimi-k2.5\" }\n \"^(4|MiniMax-M2\\.5)$\" { $script:config.DEFAULT_MODEL = \"MiniMax-M2.5\" }\n default { $script:config.DEFAULT_MODEL = \"qwen3.6-plus\" }\n }\n Write-Log (Get-Msg \"llm.provider.selected_tokenplan\")\n } elseif ($modelChoice -match \"^(2|qwen|bailian)$\") {\n $alibabaAccess = \"bailian\"\n $script:config.LLM_PROVIDER = \"qwen\"\n $script:config.OPENAI_BASE_URL = \"\"\n Write-Host \"\"\n if ($script:AGENTTEAMS_UPGRADE -and $script:config.DEFAULT_MODEL) {\n $qwenModelInput = Read-Host \"$(Get-Msg 'llm.qwen.model_prompt') [${script:config.DEFAULT_MODEL}]\"\n } else {\n $qwenModelInput = Read-Host (Get-Msg \"llm.qwen.model_prompt\")\n }\n if ($qwenModelInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $script:config.DEFAULT_MODEL = if ($qwenModelInput) { $qwenModelInput } elseif ($script:config.DEFAULT_MODEL) { $script:config.DEFAULT_MODEL } else { \"qwen3.6-plus\" }\n Write-Log (Get-Msg \"llm.provider.selected_qwen\")\n Request-CustomModelParams $script:config.DEFAULT_MODEL\n if ($script:StepResult -eq \"back\") { return }\n } elseif ($modelChoice -match \"^(3|coding-plan|codingplan)$\") {\n $alibabaAccess = \"codingplan_legacy\"\n $script:config.LLM_PROVIDER = \"openai-compat\"\n $script:config.OPENAI_BASE_URL = \"https://coding.dashscope.aliyuncs.com/v1\"\n Write-Host \"\"\n if ($script:AGENTTEAMS_UPGRADE -and $script:config.DEFAULT_MODEL) {\n $codingModelInput = Read-Host \"$(Get-Msg 'llm.qwen.model_prompt') [${script:config.DEFAULT_MODEL}]\"\n } else {\n $codingModelInput = Read-Host (Get-Msg \"llm.qwen.model_prompt\")\n }\n if ($codingModelInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $script:config.DEFAULT_MODEL = if ($codingModelInput) { $codingModelInput } elseif ($script:config.DEFAULT_MODEL) { $script:config.DEFAULT_MODEL } else { \"qwen3.6-plus\" }\n Write-Log (Get-Msg \"llm.provider.selected_codingplan_legacy\")\n Request-CustomModelParams $script:config.DEFAULT_MODEL\n if ($script:StepResult -eq \"back\") { return }\n } else {\n Write-Error (Get-Msg \"llm.alibaba.model.invalid\" -f $modelChoice)\n }\n }\n\n Write-Log (Get-Msg \"llm.model.label\" -f $script:config.DEFAULT_MODEL)\n Write-Log \"\"\n if ($alibabaAccess -eq \"bailian\") {\n Write-Log (Get-Msg \"llm.apikey_hint_bailian\")\n Write-Log (Get-Msg \"llm.apikey_url_bailian\")\n } elseif ($script:AGENTTEAMS_LANGUAGE -eq \"en\") {\n Write-Log (Get-Msg \"llm.apikey_hint_qwencloud\")\n Write-Log (Get-Msg \"llm.apikey_url_qwencloud\")\n } elseif ($alibabaAccess -eq \"codingplan_legacy\") {\n Write-Log (Get-Msg \"llm.apikey_hint_codingplan\")\n Write-Log (Get-Msg \"llm.apikey_url_codingplan\")\n } else {\n Write-Log (Get-Msg \"llm.apikey_hint_tokenplan\")\n Write-Log (Get-Msg \"llm.apikey_url_tokenplan\")\n }\n Write-Log \"\"\n $script:config.LLM_API_KEY = Read-Prompt -VarName \"AGENTTEAMS_LLM_API_KEY\" -PromptText (Get-Msg \"llm.apikey_prompt\") -Secret\n if ($script:StepResult -eq \"back\") { return }\n if ($alibabaAccess -eq \"bailian\") {\n Test-LlmConnectivity -BaseUrl \"https://dashscope.aliyuncs.com/compatible-mode/v1\" -ApiKey $script:config.LLM_API_KEY -Model $script:config.DEFAULT_MODEL\n } elseif ($alibabaAccess -eq \"codingplan_legacy\") {\n Test-LlmConnectivity -BaseUrl \"https://coding.dashscope.aliyuncs.com/v1\" -ApiKey $script:config.LLM_API_KEY -Model $script:config.DEFAULT_MODEL -Hint (Get-Msg \"llm.openai.test.fail.codingplan_legacy\")\n } elseif ($script:AGENTTEAMS_LANGUAGE -eq \"en\") {\n Test-LlmConnectivity -BaseUrl $script:config.OPENAI_BASE_URL -ApiKey $script:config.LLM_API_KEY -Model $script:config.DEFAULT_MODEL -Hint (Get-Msg \"llm.openai.test.fail.codingplan\")\n } else {\n Test-LlmConnectivity -BaseUrl $script:config.OPENAI_BASE_URL -ApiKey $script:config.LLM_API_KEY -Model $script:config.DEFAULT_MODEL -Hint (Get-Msg \"llm.openai.test.fail.tokenplan\")\n }\n if ($script:StepResult -eq \"back\") { return }\n }\n \"^(2|openai-compat)$\" {\n $script:config.LLM_PROVIDER = \"openai-compat\"\n Write-Log (Get-Msg \"llm.provider.selected_openai\" -f $script:config.LLM_PROVIDER)\n Write-Host \"\"\n if ($script:AGENTTEAMS_UPGRADE -and $env:AGENTTEAMS_UPGRADE_KEEP_ALL -eq \"1\") {\n Write-Log (Get-Msg \"prompt.upgrade_keep\" -f (Get-Msg \"llm.openai.base_url_prompt\"), $script:config.OPENAI_BASE_URL)\n } else {\n $currentUrl = $script:config.OPENAI_BASE_URL\n # Use base prompt without embedded default when showing current value\n if ($currentUrl) {\n $urlInput = Read-Host \"$(Get-Msg 'llm.openai.base_url_prompt') [$currentUrl]\"\n } else {\n $urlInput = Read-Host \"$(Get-Msg 'llm.openai.base_url_prompt') [https://api.openai.com/v1]\"\n }\n if ($urlInput -eq \"b\") { $script:StepResult = \"back\"; return }\n if ($urlInput) {\n $script:config.OPENAI_BASE_URL = $urlInput\n } elseif ($currentUrl) {\n $script:config.OPENAI_BASE_URL = $currentUrl\n } else {\n $script:config.OPENAI_BASE_URL = \"https://api.openai.com/v1\"\n }\n }\n if ($script:AGENTTEAMS_UPGRADE -and $env:AGENTTEAMS_UPGRADE_KEEP_ALL -eq \"1\") {\n Write-Log (Get-Msg \"prompt.upgrade_keep\" -f (Get-Msg \"llm.openai.model_prompt\"), $script:config.DEFAULT_MODEL)\n } else {\n $currentModel = $script:config.DEFAULT_MODEL\n if ($currentModel) {\n $modelInput = Read-Host \"$(Get-Msg 'llm.openai.model_prompt') [${currentModel}]\"\n } else {\n $modelInput = Read-Host (Get-Msg 'llm.openai.model_prompt')\n }\n if ($modelInput -eq \"b\") { $script:StepResult = \"back\"; return }\n if ($modelInput) {\n $script:config.DEFAULT_MODEL = $modelInput\n } elseif ($currentModel) {\n $script:config.DEFAULT_MODEL = $currentModel\n } else {\n $script:config.DEFAULT_MODEL = \"gpt-5.4\"\n }\n }\n Write-Log (Get-Msg \"llm.openai.base_url_label\" -f $script:config.OPENAI_BASE_URL)\n Write-Log (Get-Msg \"llm.model.label\" -f $script:config.DEFAULT_MODEL)\n Request-CustomModelParams $script:config.DEFAULT_MODEL\n if ($script:StepResult -eq \"back\") { return }\n Write-Log \"\"\n $script:config.LLM_API_KEY = Read-Prompt -VarName \"AGENTTEAMS_LLM_API_KEY\" -PromptText (Get-Msg \"llm.apikey_prompt\") -Secret\n if ($script:StepResult -eq \"back\") { return }\n Test-LlmConnectivity -BaseUrl $script:config.OPENAI_BASE_URL -ApiKey $script:config.LLM_API_KEY -Model $script:config.DEFAULT_MODEL\n if ($script:StepResult -eq \"back\") { return }\n }\n default {\n Write-Error (Get-Msg \"llm.provider.invalid\" -f $providerChoice)\n }\n }\n\n # Skip to embedding if Keep-All mode handled LLM params\n if ($skipToEmbedding) {\n # Skip embedding selection, use loaded value (already in $script:config.EMBEDDING_MODEL)\n if ($script:config.EMBEDDING_MODEL) {\n Write-Log (Get-Msg \"llm.embedding.title\")\n Write-Log (Get-Msg \"llm.model.label\" -f $script:config.EMBEDDING_MODEL)\n Write-Log \"\"\n }\n return\n }\n\n # --- Embedding model (optional, auto-tested) ---\n Write-Host \"\"\n Write-Log (Get-Msg \"llm.embedding.title\")\n Write-Log (Get-Msg \"llm.embedding.hint\")\n Write-Host \"\"\n Write-Host (Get-Msg \"llm.embedding.option.default\")\n Write-Host (Get-Msg \"llm.embedding.option.custom\")\n Write-Host (Get-Msg \"llm.embedding.option.disable\")\n Write-Host \"\"\n $embChoice = Read-Host \"$(Get-Msg 'llm.embedding.select') [1]\"\n $embChoice = if ($embChoice) { $embChoice } else { \"1\" }\n if ($embChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n\n switch ($embChoice) {\n \"1\" {\n $script:config.EMBEDDING_MODEL = \"text-embedding-v4\"\n }\n \"2\" {\n if ($script:AGENTTEAMS_UPGRADE -and $env:AGENTTEAMS_UPGRADE_KEEP_ALL -eq \"1\") {\n Write-Log (Get-Msg \"prompt.upgrade_keep\" -f (Get-Msg \"llm.embedding.custom_prompt\"), $script:config.EMBEDDING_MODEL)\n } else {\n $currentEmb = $script:config.EMBEDDING_MODEL\n $embCustom = Read-Host (Get-Msg \"llm.embedding.custom_prompt\")\n if ($embCustom -eq \"b\") { $script:StepResult = \"back\"; return }\n if ($embCustom) {\n $script:config.EMBEDDING_MODEL = $embCustom\n } else {\n $script:config.EMBEDDING_MODEL = $currentEmb\n if (-not $script:config.EMBEDDING_MODEL) {\n Write-Log (Get-Msg \"llm.embedding.disabled\")\n }\n }\n }\n }\n \"3\" {\n $script:config.EMBEDDING_MODEL = \"\"\n Write-Log (Get-Msg \"llm.embedding.disabled\")\n }\n default {\n $script:config.EMBEDDING_MODEL = \"text-embedding-v4\"\n }\n }\n\n if ($script:config.EMBEDDING_MODEL) {\n # Qwen provider uses dashscope directly; others use OPENAI_BASE_URL\n $embBaseUrl = $script:config.OPENAI_BASE_URL\n if ($script:config.LLM_PROVIDER -eq \"qwen\") {\n $embBaseUrl = \"https://dashscope.aliyuncs.com/compatible-mode/v1\"\n }\n $embResult = Test-EmbeddingConnectivity -BaseUrl $embBaseUrl -ApiKey $script:config.LLM_API_KEY -Model $script:config.EMBEDDING_MODEL\n if (-not $embResult) {\n $script:config.EMBEDDING_MODEL = \"\"\n Write-Log (Get-Msg \"llm.embedding.auto_disabled\")\n }\n }\n\n Write-Log \"\"\n}\n\nfunction Step-Admin {\n Write-Log (Get-Msg \"admin.title\")\n $script:config.ADMIN_USER = Read-Prompt -VarName \"AGENTTEAMS_ADMIN_USER\" -PromptText (Get-Msg \"admin.username_prompt\") -Default \"admin\"\n if ($script:StepResult -eq \"back\") { return }\n $script:config.ADMIN_USER = $script:config.ADMIN_USER.ToLowerInvariant()\n\n # Pre-set via env var: validate; non-interactive fails fast,\n # interactive warns and falls through to the retry prompt.\n if ($env:AGENTTEAMS_ADMIN_PASSWORD) {\n $script:config.ADMIN_PASSWORD = $env:AGENTTEAMS_ADMIN_PASSWORD\n Write-Log (Get-Msg \"prompt.preset\" -f (Get-Msg \"admin.password_prompt\"))\n if ($script:config.ADMIN_PASSWORD.Length -ge 8) {\n Write-Log \"\"\n return\n }\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n Write-Error (Get-Msg \"admin.password_too_short\" -f $script:config.ADMIN_PASSWORD.Length)\n }\n Write-Host \"$($script:ESC)[31m[AgentTeams ERROR]$($script:ESC)[0m $(Get-Msg \"admin.password_too_short\" -f $script:config.ADMIN_PASSWORD.Length)\"\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_ADMIN_PASSWORD\", $null, \"Process\")\n }\n\n while ($true) {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_ADMIN_PASSWORD\", $null, \"Process\")\n $script:config.ADMIN_PASSWORD = Read-Prompt -VarName \"AGENTTEAMS_ADMIN_PASSWORD\" -PromptText (Get-Msg \"admin.password_prompt\") -Secret -Optional\n if ($script:StepResult -eq \"back\") { return }\n if (-not $script:config.ADMIN_PASSWORD) {\n $randomSuffix = (New-RandomKey).Substring(0, 12)\n $script:config.ADMIN_PASSWORD = \"admin$randomSuffix\"\n Write-Log (Get-Msg \"admin.password_generated\")\n break\n }\n if ($script:config.ADMIN_PASSWORD.Length -ge 8) {\n break\n }\n Write-Host \"$($script:ESC)[31m[AgentTeams ERROR]$($script:ESC)[0m $(Get-Msg \"admin.password_too_short\" -f $script:config.ADMIN_PASSWORD.Length)\"\n }\n\n Write-Log \"\"\n}\n\nfunction Step-Network {\n Write-Log (Get-Msg \"port.local_only.title\")\n Write-Host \"\"\n Write-Host \" 1) $(Get-Msg 'port.local_only.hint_yes')\"\n Write-Host \" 2) $(Get-Msg 'port.local_only.hint_no')\"\n Write-Host \"\"\n\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n $localOnly = if ($env:AGENTTEAMS_LOCAL_ONLY) { $env:AGENTTEAMS_LOCAL_ONLY } else { \"1\" }\n } elseif ($null -ne $env:AGENTTEAMS_LOCAL_ONLY) {\n $localOnly = $env:AGENTTEAMS_LOCAL_ONLY\n } else {\n $localChoice = Read-Host \"$(Get-Msg 'port.local_only.choice')\"\n if ($localChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n if (-not $localChoice) { $localChoice = \"1\" }\n $localOnly = if ($localChoice -match '^(2|n|N|no|NO)$') { \"0\" } else { \"1\" }\n }\n $script:config.LOCAL_ONLY = $localOnly\n\n if ($localOnly -eq \"1\") {\n Write-Log (Get-Msg \"port.local_only.selected_local\")\n } else {\n Write-Log (Get-Msg \"port.local_only.selected_external\")\n Write-Host \"\"\n Write-Host (Get-Msg \"port.local_only.https_hint\") -ForegroundColor Yellow\n }\n Write-Log \"\"\n}\n\nfunction Step-Ports {\n Write-Log (Get-Msg \"port.title\")\n $script:config.PORT_GATEWAY = Read-Prompt -VarName \"AGENTTEAMS_PORT_GATEWAY\" -PromptText (Get-Msg \"port.gateway_prompt\") -Default \"18080\"\n if ($script:StepResult -eq \"back\") { return }\n $script:config.PORT_CONSOLE = Read-Prompt -VarName \"AGENTTEAMS_PORT_CONSOLE\" -PromptText (Get-Msg \"port.console_prompt\") -Default \"18001\"\n if ($script:StepResult -eq \"back\") { return }\n $script:config.PORT_ELEMENT_WEB = Read-Prompt -VarName \"AGENTTEAMS_PORT_ELEMENT_WEB\" -PromptText (Get-Msg \"port.element_prompt\") -Default \"18088\"\n if ($script:StepResult -eq \"back\") { return }\n $script:config.PORT_MANAGER_CONSOLE = Read-Prompt -VarName \"AGENTTEAMS_PORT_MANAGER_CONSOLE\" -PromptText (Get-Msg \"port.manager_console_prompt\") -Default \"18888\"\n if ($script:StepResult -eq \"back\") { return }\n Write-Log \"\"\n}\n\nfunction Step-Domains {\n Write-Log (Get-Msg \"domain.title\")\n Write-Log (Get-Msg \"domain.hint\")\n $script:config.MATRIX_DOMAIN = Read-Prompt -VarName \"AGENTTEAMS_MATRIX_DOMAIN\" -PromptText (Get-Msg \"domain.matrix_prompt\") -Default \"matrix-local.agentteams.io:$($script:config.PORT_GATEWAY)\"\n if ($script:StepResult -eq \"back\") { return }\n $script:config.MATRIX_CLIENT_DOMAIN = Read-Prompt -VarName \"AGENTTEAMS_MATRIX_CLIENT_DOMAIN\" -PromptText (Get-Msg \"domain.element_prompt\") -Default \"matrix-client-local.agentteams.io\"\n if ($script:StepResult -eq \"back\") { return }\n $script:config.AI_GATEWAY_DOMAIN = Read-Prompt -VarName \"AGENTTEAMS_AI_GATEWAY_DOMAIN\" -PromptText (Get-Msg \"domain.gateway_prompt\") -Default \"aigw-local.agentteams.io\"\n if ($script:StepResult -eq \"back\") { return }\n $script:config.FS_DOMAIN = Read-Prompt -VarName \"AGENTTEAMS_FS_DOMAIN\" -PromptText (Get-Msg \"domain.fs_prompt\") -Default \"fs-local.agentteams.io\"\n if ($script:StepResult -eq \"back\") { return }\n $script:config.CONSOLE_DOMAIN = Read-Prompt -VarName \"AGENTTEAMS_CONSOLE_DOMAIN\" -PromptText (Get-Msg \"domain.console_prompt\") -Default \"console-local.agentteams.io\"\n if ($script:StepResult -eq \"back\") { return }\n Write-Log \"\"\n}\n\nfunction Step-Github {\n Write-Log (Get-Msg \"github.title\")\n $script:config.GITHUB_TOKEN = Read-Prompt -VarName \"AGENTTEAMS_GITHUB_TOKEN\" -PromptText (Get-Msg \"github.token_prompt\") -Secret -Optional\n}\n\nfunction Step-Skills {\n Write-Log \"\"\n Write-Log (Get-Msg \"skills.title\")\n $script:config.SKILLS_API_URL = Read-Prompt -VarName \"AGENTTEAMS_SKILLS_API_URL\" -PromptText (Get-Msg \"skills.url_prompt\") -Optional\n if ($script:StepResult -eq \"back\") { return }\n Write-Log \"\"\n}\n\nfunction Step-Volume {\n Write-Log (Get-Msg \"data.title\")\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n $script:config.DATA_DIR = if ($env:AGENTTEAMS_DATA_DIR) { $env:AGENTTEAMS_DATA_DIR } else { \"agentteams-data\" }\n Write-Log \" $(Get-Msg 'data.volume_using' -f $script:config.DATA_DIR) (non-interactive, skipped)\"\n return\n }\n # ─────────────────────────────────────────────────────────────────\n $dataDirInput = Read-Host (Get-Msg \"data.volume_prompt\")\n if ($dataDirInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $script:config.DATA_DIR = if ($dataDirInput) { $dataDirInput } else { \"agentteams-data\" }\n Write-Log (Get-Msg \"data.volume_using\" -f $script:config.DATA_DIR)\n}\n\nfunction Step-Workspace {\n Write-Log (Get-Msg \"workspace.title\")\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n $script:config.WORKSPACE_DIR = if ($env:AGENTTEAMS_WORKSPACE_DIR) { $env:AGENTTEAMS_WORKSPACE_DIR } else { \"$env:USERPROFILE\\agentteams-manager\" }\n if (-not (Test-Path $script:config.WORKSPACE_DIR)) {\n New-Item -ItemType Directory -Path $script:config.WORKSPACE_DIR -Force | Out-Null\n }\n Write-Log \" $(Get-Msg 'workspace.dir_label' -f $script:config.WORKSPACE_DIR) (non-interactive, skipped)\"\n return\n }\n # ─────────────────────────────────────────────────────────────────\n $defaultWorkspace = \"$env:USERPROFILE\\agentteams-manager\"\n if ($script:AGENTTEAMS_UPGRADE -and $env:AGENTTEAMS_UPGRADE_KEEP_ALL -eq \"1\") {\n Write-Log (Get-Msg \"prompt.upgrade_keep\" -f (Get-Msg \"workspace.dir_prompt\" -f $defaultWorkspace), $script:config.WORKSPACE_DIR)\n $wsInput = if ($script:config.WORKSPACE_DIR) { $script:config.WORKSPACE_DIR } else { $defaultWorkspace }\n } else {\n $currentWs = $script:config.WORKSPACE_DIR\n $wsInput = Read-Host (Get-Msg \"workspace.dir_prompt\" -f $defaultWorkspace)\n if ($wsInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $wsInput = if ($wsInput) { $wsInput } else { if ($currentWs) { $currentWs } else { $defaultWorkspace } }\n }\n $script:config.WORKSPACE_DIR = $wsInput\n if (-not (Test-Path $script:config.WORKSPACE_DIR)) {\n New-Item -ItemType Directory -Path $script:config.WORKSPACE_DIR -Force | Out-Null\n }\n Write-Log (Get-Msg \"workspace.dir_label\" -f $script:config.WORKSPACE_DIR)\n}\n\nfunction Step-Runtime {\n Write-Log (Get-Msg \"worker_runtime.title\")\n Write-Host \"\"\n Write-Host \" 1) $(Get-Msg 'worker_runtime.copaw')\"\n Write-Host \" 2) $(Get-Msg 'worker_runtime.openclaw')\"\n Write-Host \" 3) $(Get-Msg 'worker_runtime.hermes')\"\n Write-Host \"\"\n\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n $script:config.DEFAULT_WORKER_RUNTIME = if ($env:AGENTTEAMS_DEFAULT_WORKER_RUNTIME) { $env:AGENTTEAMS_DEFAULT_WORKER_RUNTIME } else { \"copaw\" }\n } elseif ($script:AGENTTEAMS_UPGRADE -and $env:AGENTTEAMS_DEFAULT_WORKER_RUNTIME) {\n Write-Log (Get-Msg \"prompt.upgrade_keep\" -f (Get-Msg \"worker_runtime.title_short\"), $env:AGENTTEAMS_DEFAULT_WORKER_RUNTIME)\n $rtChoice = Read-Host (Get-Msg \"worker_runtime.choice\")\n if ($rtChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n if ($rtChoice) {\n $script:config.DEFAULT_WORKER_RUNTIME = switch ($rtChoice) {\n \"2\" { \"openclaw\" }\n \"3\" { \"hermes\" }\n default { \"copaw\" }\n }\n } else {\n $script:config.DEFAULT_WORKER_RUNTIME = $env:AGENTTEAMS_DEFAULT_WORKER_RUNTIME\n }\n } elseif ($env:AGENTTEAMS_DEFAULT_WORKER_RUNTIME) {\n $script:config.DEFAULT_WORKER_RUNTIME = $env:AGENTTEAMS_DEFAULT_WORKER_RUNTIME\n } else {\n $rtChoice = Read-Host (Get-Msg \"worker_runtime.choice\")\n if ($rtChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n $rtChoice = if ($rtChoice) { $rtChoice } else { \"1\" }\n $script:config.DEFAULT_WORKER_RUNTIME = switch ($rtChoice) {\n \"2\" { \"openclaw\" }\n \"3\" { \"hermes\" }\n default { \"copaw\" }\n }\n }\n Write-Log (Get-Msg \"worker_runtime.selected\" -f $script:config.DEFAULT_WORKER_RUNTIME)\n}\n\nfunction Step-ManagerRuntime {\n Write-Log (Get-Msg \"manager_runtime.title\")\n Write-Host \"\"\n Write-Host \" 1) $(Get-Msg 'manager_runtime.copaw')\"\n Write-Host \" 2) $(Get-Msg 'manager_runtime.openclaw')\"\n Write-Host \"\"\n\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n $script:config.MANAGER_RUNTIME = if ($env:AGENTTEAMS_MANAGER_RUNTIME) { $env:AGENTTEAMS_MANAGER_RUNTIME } else { \"copaw\" }\n } elseif ($script:AGENTTEAMS_UPGRADE -and $env:AGENTTEAMS_MANAGER_RUNTIME) {\n Write-Log (Get-Msg \"prompt.upgrade_keep\" -f (Get-Msg \"manager_runtime.title_short\"), $env:AGENTTEAMS_MANAGER_RUNTIME)\n $mrChoice = Read-Host (Get-Msg \"manager_runtime.choice\")\n if ($mrChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n if ($mrChoice) {\n $script:config.MANAGER_RUNTIME = if ($mrChoice -eq \"2\") { \"openclaw\" } else { \"copaw\" }\n } else {\n $script:config.MANAGER_RUNTIME = $env:AGENTTEAMS_MANAGER_RUNTIME\n }\n } elseif ($env:AGENTTEAMS_MANAGER_RUNTIME) {\n $script:config.MANAGER_RUNTIME = $env:AGENTTEAMS_MANAGER_RUNTIME\n } else {\n $mrChoice = Read-Host (Get-Msg \"manager_runtime.choice\")\n if ($mrChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n $mrChoice = if ($mrChoice) { $mrChoice } else { \"1\" }\n $script:config.MANAGER_RUNTIME = if ($mrChoice -eq \"2\") { \"openclaw\" } else { \"copaw\" }\n }\n Write-Log (Get-Msg \"manager_runtime.selected\" -f $script:config.MANAGER_RUNTIME)\n}\n\nfunction Step-E2ee {\n Write-Host \"\"\n Write-Log (Get-Msg \"matrix_e2ee.title\")\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n $script:config.MATRIX_E2EE = if ($env:AGENTTEAMS_MATRIX_E2EE) { $env:AGENTTEAMS_MATRIX_E2EE } else { \"0\" }\n Write-Log \" $(Get-Msg 'matrix_e2ee.title_short') = $($script:config.MATRIX_E2EE) (non-interactive, skipped)\"\n return\n }\n # ─────────────────────────────────────────────────────────────────\n Write-Host \"\"\n Write-Host \" $(Get-Msg 'matrix_e2ee.desc')\"\n Write-Host \"\"\n Write-Host \" 1) $(Get-Msg 'matrix_e2ee.disable')\"\n Write-Host \" 2) $(Get-Msg 'matrix_e2ee.enable')\"\n Write-Host \"\"\n\n if ($script:AGENTTEAMS_UPGRADE -and $env:AGENTTEAMS_MATRIX_E2EE) {\n $e2eeDisplay = if ($env:AGENTTEAMS_MATRIX_E2EE -eq \"1\") { Get-Msg \"matrix_e2ee.val_enabled\" } else { Get-Msg \"matrix_e2ee.val_disabled\" }\n Write-Log (Get-Msg \"prompt.upgrade_keep\" -f (Get-Msg \"matrix_e2ee.title_short\"), $e2eeDisplay)\n $e2eeChoice = Read-Host (Get-Msg \"matrix_e2ee.choice\")\n if ($e2eeChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n if ($e2eeChoice) {\n $script:config.MATRIX_E2EE = if ($e2eeChoice -eq \"2\") { \"1\" } else { \"0\" }\n } else {\n $script:config.MATRIX_E2EE = $env:AGENTTEAMS_MATRIX_E2EE\n }\n } elseif (-not $env:AGENTTEAMS_MATRIX_E2EE) {\n $e2eeChoice = Read-Host (Get-Msg \"matrix_e2ee.choice\")\n if ($e2eeChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n $e2eeChoice = if ($e2eeChoice) { $e2eeChoice } else { \"1\" }\n $script:config.MATRIX_E2EE = if ($e2eeChoice -eq \"2\") { \"1\" } else { \"0\" }\n } else {\n $script:config.MATRIX_E2EE = $env:AGENTTEAMS_MATRIX_E2EE\n }\n\n if ($script:config.MATRIX_E2EE -eq \"1\") {\n Write-Log (Get-Msg \"matrix_e2ee.selected_enabled\")\n } else {\n Write-Log (Get-Msg \"matrix_e2ee.selected_disabled\")\n }\n}\n\nfunction Step-DockerProxy {\n if (-not $script:AGENTTEAMS_MOUNT_SOCKET) {\n $script:config.DOCKER_PROXY = \"0\"\n return\n }\n\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n $script:config.DOCKER_PROXY = if ($env:AGENTTEAMS_DOCKER_PROXY) { $env:AGENTTEAMS_DOCKER_PROXY } else { \"0\" }\n Write-Log \" $(Get-Msg 'docker_proxy.title_short') = $($script:config.DOCKER_PROXY) (non-interactive, skipped)\"\n return\n }\n # ─────────────────────────────────────────────────────────────────\n\n Write-Host \"\"\n Write-Log (Get-Msg \"docker_proxy.title\")\n Write-Host \"\"\n Write-Host \" $(Get-Msg 'docker_proxy.desc')\"\n Write-Host \"\"\n Write-Host \" 1) $(Get-Msg 'docker_proxy.enable')\"\n Write-Host \" 2) $(Get-Msg 'docker_proxy.disable')\"\n Write-Host \"\"\n\n if ($script:AGENTTEAMS_UPGRADE -and $env:AGENTTEAMS_DOCKER_PROXY) {\n $proxyDisplay = if ($env:AGENTTEAMS_DOCKER_PROXY -eq \"1\") { Get-Msg \"docker_proxy.val_enabled\" } else { Get-Msg \"docker_proxy.val_disabled\" }\n Write-Log (Get-Msg \"prompt.upgrade_keep\" -f (Get-Msg \"docker_proxy.title_short\"), $proxyDisplay)\n $proxyChoice = Read-Host (Get-Msg \"docker_proxy.choice\")\n if ($proxyChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n if ($proxyChoice) {\n $script:config.DOCKER_PROXY = if ($proxyChoice -eq \"2\") { \"0\" } else { \"1\" }\n } else {\n $script:config.DOCKER_PROXY = $env:AGENTTEAMS_DOCKER_PROXY\n }\n } elseif (-not $env:AGENTTEAMS_DOCKER_PROXY) {\n $proxyChoice = Read-Host (Get-Msg \"docker_proxy.choice\")\n if ($proxyChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n $proxyChoice = if ($proxyChoice) { $proxyChoice } else { \"1\" }\n $script:config.DOCKER_PROXY = if ($proxyChoice -eq \"2\") { \"0\" } else { \"1\" }\n } else {\n $script:config.DOCKER_PROXY = $env:AGENTTEAMS_DOCKER_PROXY\n }\n\n if ($script:config.DOCKER_PROXY -eq \"1\") {\n Write-Log (Get-Msg \"docker_proxy.selected_enabled\")\n\n # Prompt for additional allowed image sources\n Write-Host \"\"\n Write-Host \" $(Get-Msg 'docker_proxy.registries_desc')\"\n Write-Host \"\"\n if ($script:AGENTTEAMS_UPGRADE -and $env:AGENTTEAMS_PROXY_ALLOWED_REGISTRIES) {\n Write-Log (Get-Msg \"prompt.upgrade_keep\" -f (Get-Msg \"docker_proxy.registries_label\"), $env:AGENTTEAMS_PROXY_ALLOWED_REGISTRIES)\n $regInput = Read-Host (Get-Msg \"docker_proxy.registries_prompt\")\n if ($regInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $script:config.PROXY_ALLOWED_REGISTRIES = if ($regInput) { $regInput } else { $env:AGENTTEAMS_PROXY_ALLOWED_REGISTRIES }\n } elseif (-not $env:AGENTTEAMS_PROXY_ALLOWED_REGISTRIES) {\n $regInput = Read-Host (Get-Msg \"docker_proxy.registries_prompt\")\n if ($regInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $script:config.PROXY_ALLOWED_REGISTRIES = if ($regInput) { $regInput } else { \"\" }\n } else {\n $script:config.PROXY_ALLOWED_REGISTRIES = $env:AGENTTEAMS_PROXY_ALLOWED_REGISTRIES\n }\n } else {\n Write-Log (Get-Msg \"docker_proxy.selected_disabled\")\n }\n}\n\nfunction Step-Idle {\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n $script:config.WORKER_IDLE_TIMEOUT = if ($env:AGENTTEAMS_WORKER_IDLE_TIMEOUT) { $env:AGENTTEAMS_WORKER_IDLE_TIMEOUT } else { \"720\" }\n Write-Log \" $(Get-Msg 'idle_timeout.label') = $($script:config.WORKER_IDLE_TIMEOUT) (non-interactive, skipped)\"\n return\n }\n # ─────────────────────────────────────────────────────────────────\n if ($script:AGENTTEAMS_UPGRADE -and $env:AGENTTEAMS_WORKER_IDLE_TIMEOUT) {\n Write-Log (Get-Msg \"prompt.upgrade_keep\" -f (Get-Msg \"idle_timeout.label\"), $env:AGENTTEAMS_WORKER_IDLE_TIMEOUT)\n $idleInput = Read-Host (Get-Msg \"idle_timeout.prompt\")\n if ($idleInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $script:config.WORKER_IDLE_TIMEOUT = if ($idleInput) { $idleInput } else { $env:AGENTTEAMS_WORKER_IDLE_TIMEOUT }\n } elseif (-not $env:AGENTTEAMS_WORKER_IDLE_TIMEOUT) {\n $idleInput = Read-Host (Get-Msg \"idle_timeout.prompt\")\n if ($idleInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $script:config.WORKER_IDLE_TIMEOUT = if ($idleInput) { $idleInput } else { \"720\" }\n } else {\n $script:config.WORKER_IDLE_TIMEOUT = $env:AGENTTEAMS_WORKER_IDLE_TIMEOUT\n }\n Write-Log (Get-Msg \"idle_timeout.selected\" -f $script:config.WORKER_IDLE_TIMEOUT)\n Write-Log \"\"\n}\n\nfunction Step-Hostshare {\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n $script:config.HOST_SHARE_DIR = if ($env:AGENTTEAMS_HOST_SHARE_DIR) { $env:AGENTTEAMS_HOST_SHARE_DIR } else { $env:USERPROFILE }\n Write-Log \" $(Get-Msg 'host_share.label') = $($script:config.HOST_SHARE_DIR) (non-interactive, skipped)\"\n return\n }\n # ─────────────────────────────────────────────────────────────────\n $currentShare = $script:config.HOST_SHARE_DIR\n if ($currentShare) {\n $shareInput = Read-Host \"$(Get-Msg 'host_share.prompt' -f $env:USERPROFILE) [${currentShare}]\"\n } else {\n $shareInput = Read-Host (Get-Msg 'host_share.prompt' -f $env:USERPROFILE)\n }\n if ($shareInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $script:config.HOST_SHARE_DIR = if ($shareInput) { $shareInput } else { if ($currentShare) { $currentShare } else { $env:USERPROFILE } }\n}\n\n# ============================================================\n# Manager Installation\n# ============================================================\n\nfunction Install-Manager {\n Write-Log (Get-Msg \"install.title\")\n\n # Detect timezone\n $script:AGENTTEAMS_TIMEZONE = Get-AgentTeamsTimeZone\n\n # Language priority: env var > existing env file > timezone detection\n if ($env:AGENTTEAMS_LANGUAGE) {\n $script:AGENTTEAMS_LANGUAGE = $env:AGENTTEAMS_LANGUAGE\n } else {\n # Check existing env file for saved language preference (upgrade scenario)\n $_envFile = $script:AGENTTEAMS_ENV_FILE\n # Migrate from legacy location (current directory) if needed\n if (-not (Test-Path $_envFile) -and (Test-Path \".\\agentteams-manager.env\")) {\n Write-Log \"Migrating agentteams-manager.env to $_envFile...\"\n Move-Item \".\\agentteams-manager.env\" $_envFile -ErrorAction SilentlyContinue\n }\n if (Test-Path $_envFile) {\n $_savedLang = (Get-Content $_envFile | Select-String \"^AGENTTEAMS_LANGUAGE=\" | ForEach-Object {\n $_.Line -replace '^AGENTTEAMS_LANGUAGE=', ''\n } | Select-Object -First 1)\n if ($_savedLang) {\n $script:AGENTTEAMS_LANGUAGE = $_savedLang\n }\n }\n # Fall back to timezone-based detection\n if (-not $script:AGENTTEAMS_LANGUAGE) {\n $script:AGENTTEAMS_LANGUAGE = Get-AgentTeamsLanguage -Timezone $script:AGENTTEAMS_TIMEZONE\n }\n }\n $env:AGENTTEAMS_LANGUAGE = $script:AGENTTEAMS_LANGUAGE\n\n # Detect registry\n $script:AGENTTEAMS_REGISTRY = Get-Registry -Timezone $script:AGENTTEAMS_TIMEZONE\n\n # Set image names\n $script:MANAGER_IMAGE = if ($env:AGENTTEAMS_INSTALL_MANAGER_IMAGE) {\n $env:AGENTTEAMS_INSTALL_MANAGER_IMAGE\n } else {\n \"$($script:AGENTTEAMS_REGISTRY)/agentteams/agentteams-manager:$($script:AGENTTEAMS_VERSION)\"\n }\n\n $script:WORKER_IMAGE = if ($env:AGENTTEAMS_INSTALL_WORKER_IMAGE) {\n $env:AGENTTEAMS_INSTALL_WORKER_IMAGE\n } else {\n \"$($script:AGENTTEAMS_REGISTRY)/agentteams/agentteams-worker:$($script:AGENTTEAMS_VERSION)\"\n }\n\n $script:COPAW_WORKER_IMAGE = if ($env:AGENTTEAMS_INSTALL_COPAW_WORKER_IMAGE) {\n $env:AGENTTEAMS_INSTALL_COPAW_WORKER_IMAGE\n } else {\n \"$($script:AGENTTEAMS_REGISTRY)/agentteams/agentteams-copaw-worker:$($script:AGENTTEAMS_VERSION)\"\n }\n\n $script:QWENPAW_WORKER_IMAGE = if ($env:AGENTTEAMS_INSTALL_QWENPAW_WORKER_IMAGE) {\n $env:AGENTTEAMS_INSTALL_QWENPAW_WORKER_IMAGE\n } else {\n \"$($script:AGENTTEAMS_REGISTRY)/agentteams/agentteams-qwenpaw-worker:$($script:AGENTTEAMS_VERSION)\"\n }\n\n $script:HERMES_WORKER_IMAGE = if ($env:AGENTTEAMS_INSTALL_HERMES_WORKER_IMAGE) {\n $env:AGENTTEAMS_INSTALL_HERMES_WORKER_IMAGE\n } else {\n \"$($script:AGENTTEAMS_REGISTRY)/agentteams/agentteams-hermes-worker:$($script:AGENTTEAMS_VERSION)\"\n }\n\n $script:MANAGER_COPAW_IMAGE = if ($env:AGENTTEAMS_INSTALL_MANAGER_COPAW_IMAGE) {\n $env:AGENTTEAMS_INSTALL_MANAGER_COPAW_IMAGE\n } else {\n \"$($script:AGENTTEAMS_REGISTRY)/agentteams/agentteams-manager-copaw:$($script:AGENTTEAMS_VERSION)\"\n }\n\n $script:CONTROLLER_IMAGE = if ($env:AGENTTEAMS_INSTALL_CONTROLLER_IMAGE) {\n $env:AGENTTEAMS_INSTALL_CONTROLLER_IMAGE\n } else {\n \"$($script:AGENTTEAMS_REGISTRY)/agentteams/agentteams-controller:$($script:AGENTTEAMS_VERSION)\"\n }\n\n # Resolve embedded controller image (sets $script:EMBEDDED_IMAGE and\n # $script:AGENTTEAMS_USE_EMBEDDED). Errors out fast if no embedded image is available\n # for the requested version (mirrors the bash installer behavior).\n Resolve-EmbeddedImage\n\n Write-Log (Get-Msg \"install.registry\" -f $script:AGENTTEAMS_REGISTRY)\n Write-Log \"\"\n Write-Log (Get-Msg \"install.dir\" -f (Get-Location))\n Write-Log (Get-Msg \"install.dir_hint\")\n Write-Log (Get-Msg \"install.dir_hint2\")\n Write-Log \"\"\n\n # Check container runtime (docker or podman)\n if (-not (Get-Command \"docker\" -ErrorAction SilentlyContinue) -and\n -not (Get-Command \"podman\" -ErrorAction SilentlyContinue)) {\n Write-Host \"$($script:ESC)[31m[AgentTeams ERROR]$($script:ESC)[0m $(Get-Msg 'error.docker_not_found')\" -ForegroundColor Red\n Exit-Script 1\n }\n\n if (-not (Test-DockerRunning)) {\n Write-Host \"$($script:ESC)[31m[AgentTeams ERROR]$($script:ESC)[0m $(Get-Msg 'error.docker_not_running')\" -ForegroundColor Red\n Exit-Script 1\n }\n\n # Initialize shared config hashtable\n $script:config = @{}\n $config = $script:config # local alias so post-machine code can use $config\n\n # Orphan volume detection (env file gone but volume remains)\n if (-not (Test-Path $script:AGENTTEAMS_ENV_FILE)) {\n $dataVol = if ($env:AGENTTEAMS_DATA_DIR) { $env:AGENTTEAMS_DATA_DIR } else { \"agentteams-data\" }\n $volumeExists = docker volume ls -q 2>$null | Select-String \"^${dataVol}$\"\n if ($volumeExists) {\n Write-Host \"\"\n Write-Log (Get-Msg \"install.orphan_volume.detected\" -f $dataVol)\n Write-Log (Get-Msg \"install.orphan_volume.warn\")\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n Write-Log (Get-Msg \"install.orphan_volume.clean_noninteractive\")\n docker stop agentteams-manager *>$null\n docker rm agentteams-manager *>$null\n docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-worker-\" | ForEach-Object {\n docker stop $_.ToString().Trim() *>$null\n docker rm $_.ToString().Trim() *>$null\n }\n Write-Log (Get-Msg \"install.orphan_volume.cleaning\")\n docker volume rm $dataVol *>$null\n Write-Log (Get-Msg \"install.orphan_volume.cleaned\")\n } else {\n Write-Host \"\"\n Write-Host (Get-Msg \"install.orphan_volume.choose\")\n Write-Host (Get-Msg \"install.orphan_volume.clean\")\n Write-Host (Get-Msg \"install.orphan_volume.keep\")\n Write-Host \"\"\n $orphanChoice = Read-Host (Get-Msg \"install.orphan_volume.prompt\")\n if (-not $orphanChoice) { $orphanChoice = \"1\" }\n switch -Regex ($orphanChoice) {\n \"^(1|clean)$\" {\n docker stop agentteams-manager *>$null\n docker rm agentteams-manager *>$null\n docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-worker-\" | ForEach-Object {\n docker stop $_.ToString().Trim() *>$null\n docker rm $_.ToString().Trim() *>$null\n }\n Write-Log (Get-Msg \"install.orphan_volume.cleaning\")\n docker volume rm $dataVol *>$null\n Write-Log (Get-Msg \"install.orphan_volume.cleaned\")\n }\n \"^(2|keep)$\" {\n Write-Log (Get-Msg \"install.orphan_volume.keeping\")\n }\n }\n }\n }\n }\n\n # ── State machine ─────────────────────────────────────────────────────────\n $_steps = @(\"Step-Lang\", \"Step-Mode\", \"Step-Existing\", \"Step-Llm\", \"Step-ManagerRuntime\", \"Step-Runtime\", \"Step-Admin\",\n \"Step-Network\", \"Step-Ports\", \"Step-Domains\", \"Step-Github\", \"Step-Skills\",\n \"Step-Volume\", \"Step-Workspace\", \"Step-E2ee\", \"Step-DockerProxy\", \"Step-Idle\",\n \"Step-Hostshare\")\n $_stepHistory = [System.Collections.Generic.List[int]]::new()\n $_stepIdx = 0\n while ($_stepIdx -lt $_steps.Count) {\n $_stepFn = $_steps[$_stepIdx]\n if (Test-ShouldSkipStep $_stepFn) {\n $_stepIdx++\n continue\n }\n if ($_stepHistory.Count -gt 0) {\n Write-Log (Get-Msg \"nav.back_hint\")\n }\n $script:StepResult = \"\"\n & $_stepFn\n if ($script:StepResult -eq \"back\") {\n if ($_stepHistory.Count -gt 0) {\n $_stepIdx = $_stepHistory[$_stepHistory.Count - 1]\n $_stepHistory.RemoveAt($_stepHistory.Count - 1)\n Clear-StepVars $_steps[$_stepIdx]\n }\n # else: first step, ignore 'b'\n } else {\n $_stepHistory.Add($_stepIdx)\n $_stepIdx++\n }\n }\n # ── End state machine ──────────────────────────────────────────────────────\n\n # Post-machine defaults for any steps that were skipped\n if (-not $script:config.DATA_DIR) {\n $script:config.DATA_DIR = if ($env:AGENTTEAMS_DATA_DIR) { $env:AGENTTEAMS_DATA_DIR } else { \"agentteams-data\" }\n }\n Write-Log (Get-Msg \"data.volume_using\" -f $script:config.DATA_DIR)\n if (-not $script:config.WORKSPACE_DIR) {\n $_defaultWs = \"$env:USERPROFILE\\agentteams-manager\"\n $script:config.WORKSPACE_DIR = if ($env:AGENTTEAMS_WORKSPACE_DIR) { $env:AGENTTEAMS_WORKSPACE_DIR } else { $_defaultWs }\n if (-not (Test-Path $script:config.WORKSPACE_DIR)) {\n New-Item -ItemType Directory -Path $script:config.WORKSPACE_DIR -Force | Out-Null\n }\n Write-Log (Get-Msg \"workspace.dir_label\" -f $script:config.WORKSPACE_DIR)\n }\n if (-not $script:config.DEFAULT_WORKER_RUNTIME) {\n $script:config.DEFAULT_WORKER_RUNTIME = if ($env:AGENTTEAMS_DEFAULT_WORKER_RUNTIME) { $env:AGENTTEAMS_DEFAULT_WORKER_RUNTIME } else { \"copaw\" }\n Write-Log (Get-Msg \"worker_runtime.selected\" -f $script:config.DEFAULT_WORKER_RUNTIME)\n }\n if (-not $script:config.MATRIX_E2EE) {\n $script:config.MATRIX_E2EE = if ($env:AGENTTEAMS_MATRIX_E2EE) { $env:AGENTTEAMS_MATRIX_E2EE } else { \"0\" }\n }\n if (-not $script:config.WORKER_IDLE_TIMEOUT) {\n $script:config.WORKER_IDLE_TIMEOUT = if ($env:AGENTTEAMS_WORKER_IDLE_TIMEOUT) { $env:AGENTTEAMS_WORKER_IDLE_TIMEOUT } else { \"720\" }\n }\n if (-not $script:config.HOST_SHARE_DIR) {\n $script:config.HOST_SHARE_DIR = if ($env:AGENTTEAMS_HOST_SHARE_DIR) { $env:AGENTTEAMS_HOST_SHARE_DIR } else { $env:USERPROFILE }\n }\n if (-not $script:config.LOCAL_ONLY) {\n $script:config.LOCAL_ONLY = if ($env:AGENTTEAMS_LOCAL_ONLY) { $env:AGENTTEAMS_LOCAL_ONLY } else { \"1\" }\n }\n if (-not $script:config.MANAGER_RUNTIME) {\n $script:config.MANAGER_RUNTIME = if ($env:AGENTTEAMS_MANAGER_RUNTIME) { $env:AGENTTEAMS_MANAGER_RUNTIME } else { \"copaw\" }\n }\n if (-not $script:config.PORT_GATEWAY) {\n $script:config.PORT_GATEWAY = if ($env:AGENTTEAMS_PORT_GATEWAY) { $env:AGENTTEAMS_PORT_GATEWAY } else { \"18080\" }\n $script:config.PORT_CONSOLE = if ($env:AGENTTEAMS_PORT_CONSOLE) { $env:AGENTTEAMS_PORT_CONSOLE } else { \"18001\" }\n $script:config.PORT_ELEMENT_WEB = if ($env:AGENTTEAMS_PORT_ELEMENT_WEB) { $env:AGENTTEAMS_PORT_ELEMENT_WEB } else { \"18088\" }\n $script:config.PORT_MANAGER_CONSOLE = if ($env:AGENTTEAMS_PORT_MANAGER_CONSOLE) { $env:AGENTTEAMS_PORT_MANAGER_CONSOLE } else { \"18888\" }\n }\n if (-not $script:config.MATRIX_DOMAIN) {\n $script:config.MATRIX_DOMAIN = if ($env:AGENTTEAMS_MATRIX_DOMAIN) { $env:AGENTTEAMS_MATRIX_DOMAIN } else { \"matrix-local.agentteams.io:$($script:config.PORT_GATEWAY)\" }\n $script:config.MATRIX_CLIENT_DOMAIN = if ($env:AGENTTEAMS_MATRIX_CLIENT_DOMAIN) { $env:AGENTTEAMS_MATRIX_CLIENT_DOMAIN } else { \"matrix-client-local.agentteams.io\" }\n $script:config.AI_GATEWAY_DOMAIN = if ($env:AGENTTEAMS_AI_GATEWAY_DOMAIN) { $env:AGENTTEAMS_AI_GATEWAY_DOMAIN } else { \"aigw-local.agentteams.io\" }\n $script:config.FS_DOMAIN = if ($env:AGENTTEAMS_FS_DOMAIN) { $env:AGENTTEAMS_FS_DOMAIN } else { \"fs-local.agentteams.io\" }\n $script:config.CONSOLE_DOMAIN = if ($env:AGENTTEAMS_CONSOLE_DOMAIN) { $env:AGENTTEAMS_CONSOLE_DOMAIN } else { \"console-local.agentteams.io\" }\n }\n\n Write-Log \"\"\n\n # Generate secrets\n Write-Log (Get-Msg \"install.generating_secrets\")\n $config.MANAGER_PASSWORD = if ($env:AGENTTEAMS_MANAGER_PASSWORD) { $env:AGENTTEAMS_MANAGER_PASSWORD } else { New-RandomKey }\n $config.REGISTRATION_TOKEN = if ($env:AGENTTEAMS_REGISTRATION_TOKEN) { $env:AGENTTEAMS_REGISTRATION_TOKEN } else { New-RandomKey }\n $config.MINIO_USER = if ($env:AGENTTEAMS_MINIO_USER) { $env:AGENTTEAMS_MINIO_USER } else { $config.ADMIN_USER }\n $config.MINIO_PASSWORD = if ($env:AGENTTEAMS_MINIO_PASSWORD) { $env:AGENTTEAMS_MINIO_PASSWORD } else { $config.ADMIN_PASSWORD }\n $config.MANAGER_GATEWAY_KEY = if ($env:AGENTTEAMS_MANAGER_GATEWAY_KEY) { $env:AGENTTEAMS_MANAGER_GATEWAY_KEY } else { New-RandomKey }\n\n # Store additional config\n $config.LANGUAGE = $script:AGENTTEAMS_LANGUAGE\n $config.REGISTRY = $script:AGENTTEAMS_REGISTRY\n $config.WORKER_IMAGE = $script:WORKER_IMAGE\n $config.COPAW_WORKER_IMAGE = $script:COPAW_WORKER_IMAGE\n $config.QWENPAW_WORKER_IMAGE = $script:QWENPAW_WORKER_IMAGE\n $config.HERMES_WORKER_IMAGE = $script:HERMES_WORKER_IMAGE\n $config.MANAGER_COPAW_IMAGE = $script:MANAGER_COPAW_IMAGE\n\n # Write env file\n New-EnvFile -Config $config -Path $script:AGENTTEAMS_ENV_FILE\n\n # Manager image selection (used by both embedded — passed to controller via env —\n # and legacy — used directly as `docker run` target).\n $managerImage = if ($config.MANAGER_RUNTIME -eq \"copaw\") { $script:MANAGER_COPAW_IMAGE } else { $script:MANAGER_IMAGE }\n $portPrefix = if ($config.LOCAL_ONLY -eq \"1\") { \"127.0.0.1:\" } else { \"\" }\n\n # Ensure agentteams-net Docker network exists. Used in both modes — the embedded\n # controller and the spawned manager/worker containers all join it and rely on\n # network aliases for *-local.agentteams.io DNS resolution.\n if ($script:AGENTTEAMS_MOUNT_SOCKET) {\n $socketAvailable = Test-DockerRunning\n if ($socketAvailable) {\n docker network inspect agentteams-net *>$null\n if ($LASTEXITCODE -ne 0) { docker network create agentteams-net *>$null }\n } else {\n Write-Log (Get-Msg \"install.socket_not_found\")\n if (-not $script:AGENTTEAMS_NON_INTERACTIVE) {\n Write-Host \"\"\n Write-Host \"$($script:ESC)[33m$(Get-Msg 'install.socket_confirm.title')$($script:ESC)[0m\"\n Write-Host \"\"\n Write-Host (Get-Msg 'install.socket_confirm.message')\n Write-Host \"\"\n $confirm = Read-Host (Get-Msg 'install.socket_confirm.prompt')\n if ($confirm -ne 'y' -and $confirm -ne 'Y') {\n Write-Log (Get-Msg 'install.socket_confirm.cancelled')\n exit 0\n }\n }\n }\n }\n\n if ($script:AGENTTEAMS_USE_EMBEDDED -ne \"1\") {\n # ============================================================\n # Legacy architecture: all-in-one manager container\n # (only entered when AGENTTEAMS_FORCE_LEGACY=1 — broken with the slim manager\n # image shipped since PR #616; kept solely for AGENTTEAMS_VERSION <= v1.0.9)\n # ============================================================\n\n $dockerArgs = @(\n \"run\", \"-d\",\n \"--name\", \"agentteams-manager\",\n \"--env-file\", $script:AGENTTEAMS_ENV_FILE,\n \"-e\", \"HOME=/root/manager-workspace\",\n \"-w\", \"/root/manager-workspace\",\n \"-e\", \"HOST_ORIGINAL_HOME=$($config.HOST_SHARE_DIR)\",\n \"-e\", \"AGENTTEAMS_MANAGER_RUNTIME=$($config.MANAGER_RUNTIME)\"\n )\n\n $dockerArgs += @(\"-e\", \"TZ=$($script:AGENTTEAMS_TIMEZONE)\")\n\n if ($script:AGENTTEAMS_MOUNT_SOCKET -and (Test-DockerRunning)) {\n $dockerArgs += @(\"--network\", \"agentteams-net\")\n $dockerArgs += @(\"--network-alias\", \"matrix-local.agentteams.io\")\n $dockerArgs += @(\"--network-alias\", \"aigw-local.agentteams.io\")\n $dockerArgs += @(\"--network-alias\", \"fs-local.agentteams.io\")\n foreach ($domain in @($config.MATRIX_CLIENT_DOMAIN, $config.CONSOLE_DOMAIN)) {\n if ($domain -match '-local\\.agentteams\\.io$') {\n $dockerArgs += @(\"--network-alias\", $domain)\n }\n }\n\n if ($config.DOCKER_PROXY -eq \"1\") {\n $proxyImage = $script:CONTROLLER_IMAGE\n Write-Log \"Starting Docker API proxy...\"\n docker rm -f agentteams-controller *>$null\n docker run -d --name agentteams-controller `\n --network agentteams-net `\n -v \"//var/run/docker.sock:/var/run/docker.sock\" `\n --security-opt label=disable `\n -e \"AGENTTEAMS_WORKER_IMAGE=$($script:WORKER_IMAGE)\" `\n -e \"AGENTTEAMS_COPAW_WORKER_IMAGE=$($script:COPAW_WORKER_IMAGE)\" `\n -e \"AGENTTEAMS_QWENPAW_WORKER_IMAGE=$($script:QWENPAW_WORKER_IMAGE)\" `\n -e \"AGENTTEAMS_HERMES_WORKER_IMAGE=$($script:HERMES_WORKER_IMAGE)\" `\n -e \"AGENTTEAMS_DEFAULT_WORKER_RUNTIME=$($script:config.DEFAULT_WORKER_RUNTIME)\" `\n $(if ($config.PROXY_ALLOWED_REGISTRIES) { @(\"-e\", \"AGENTTEAMS_PROXY_ALLOWED_REGISTRIES=$($config.PROXY_ALLOWED_REGISTRIES)\") }) `\n --restart unless-stopped `\n $proxyImage\n $dockerArgs += @(\"-e\", \"AGENTTEAMS_CONTROLLER_URL=http://agentteams-controller:8090\")\n $dockerArgs += @(\"-e\", \"AGENTTEAMS_CONTAINER_API=http://agentteams-controller:8090\")\n Write-Log (Get-Msg \"docker_proxy.selected_enabled\")\n } else {\n $dockerArgs += @(\"-v\", \"//var/run/docker.sock:/var/run/docker.sock\")\n $dockerArgs += @(\"--security-opt\", \"label=disable\")\n Write-Log (Get-Msg \"install.socket_detected\" -f \"//var/run/docker.sock\")\n }\n }\n\n $dockerArgs += @(\"-p\", \"${portPrefix}$($config.PORT_GATEWAY):8080\")\n $dockerArgs += @(\"-p\", \"${portPrefix}$($config.PORT_CONSOLE):8001\")\n $dockerArgs += @(\"-p\", \"${portPrefix}$($config.PORT_ELEMENT_WEB):8088\")\n $dockerArgs += @(\"-p\", \"127.0.0.1:$($config.PORT_MANAGER_CONSOLE):18888\")\n\n $dockerArgs += @(\"-v\", \"$($config.DATA_DIR):/data\")\n\n $wsDockerPath = ConvertTo-DockerPath -Path $config.WORKSPACE_DIR\n $dockerArgs += @(\"-v\", \"${wsDockerPath}:/root/manager-workspace\")\n\n $shareDockerPath = ConvertTo-DockerPath -Path $config.HOST_SHARE_DIR\n $dockerArgs += @(\"-v\", \"${shareDockerPath}:/host-share\")\n Write-Log (Get-Msg \"host_share.sharing\" -f $config.HOST_SHARE_DIR)\n\n if ($env:AGENTTEAMS_YOLO -eq \"1\") {\n $dockerArgs += @(\"-e\", \"AGENTTEAMS_YOLO=1\")\n Write-Log (Get-Msg \"install.yolo\")\n }\n\n if ($env:AGENTTEAMS_MATRIX_DEBUG -eq \"1\") {\n $dockerArgs += @(\"-e\", \"AGENTTEAMS_MATRIX_DEBUG=1\")\n }\n\n $dockerArgs += @(\"--restart\", \"unless-stopped\")\n $dockerArgs += $managerImage\n }\n\n # Check if the Docker volume exists; create if not (reuse on reinstall) — both modes.\n $volumeExists = docker volume ls -q 2>$null | Select-String \"^$($config.DATA_DIR)$\"\n if (-not $volumeExists) {\n docker volume create $config.DATA_DIR | Out-Null\n }\n\n # Pull images (skip if already exists locally for local build tags).\n $LocalImagePattern = \"^agentteams/\"\n function Test-LocalImage {\n param([string]$Image)\n $imgExists = docker image inspect $Image 2>$null\n return $LASTEXITCODE -eq 0\n }\n if ($script:AGENTTEAMS_USE_EMBEDDED -eq \"1\") {\n # Embedded image was already pulled by Resolve-EmbeddedImage unless overridden;\n # for an explicit override we still need to ensure it is present locally.\n if ($env:AGENTTEAMS_INSTALL_EMBEDDED_IMAGE) {\n if ($script:EMBEDDED_IMAGE -match $LocalImagePattern) {\n if (-not (Test-LocalImage $script:EMBEDDED_IMAGE)) {\n Write-Log \"Pulling embedded image: $($script:EMBEDDED_IMAGE)\"\n & docker pull $script:EMBEDDED_IMAGE\n }\n } else {\n Write-Log \"Pulling embedded image: $($script:EMBEDDED_IMAGE)\"\n & docker pull $script:EMBEDDED_IMAGE\n }\n }\n # Manager image — controller will spawn it inside; pull here so the spawn doesn't\n # have to wait on the network.\n if ($managerImage -match $LocalImagePattern) {\n if (Test-LocalImage $managerImage) {\n Write-Log (Get-Msg \"install.image.exists\" -f $managerImage)\n } else {\n Write-Log (Get-Msg \"install.image.pulling_manager\" -f $managerImage)\n & docker pull $managerImage\n }\n } else {\n Write-Log (Get-Msg \"install.image.pulling_manager\" -f $managerImage)\n & docker pull $managerImage\n }\n } else {\n if ($managerImage -match $LocalImagePattern) {\n if (Test-LocalImage $managerImage) {\n Write-Log (Get-Msg \"install.image.exists\" -f $managerImage)\n } else {\n Write-Log (Get-Msg \"install.image.pulling_manager\" -f $managerImage)\n & docker pull $managerImage\n }\n } else {\n Write-Log (Get-Msg \"install.image.pulling_manager\" -f $managerImage)\n & docker pull $managerImage\n }\n }\n\n # Pull all worker runtime images (workers may use any runtime regardless of the default)\n $workerImages = @(\n $script:WORKER_IMAGE\n $script:COPAW_WORKER_IMAGE\n # Temporarily disabled until the QwenPaw worker image is published.\n # $script:QWENPAW_WORKER_IMAGE\n $script:HERMES_WORKER_IMAGE\n )\n foreach ($workerImg in $workerImages) {\n if ($workerImg -match $LocalImagePattern) {\n if (Test-LocalImage $workerImg) {\n Write-Log (Get-Msg \"install.image.worker_exists\" -f $workerImg)\n } else {\n Write-Log (Get-Msg \"install.image.pulling_worker\" -f $workerImg)\n & docker pull $workerImg\n }\n } else {\n Write-Log (Get-Msg \"install.image.pulling_worker\" -f $workerImg)\n & docker pull $workerImg\n }\n }\n\n # --- Pre-upgrade: extract Matrix passwords from old containers (old-arch -> embedded) ---\n $credsTmp = $null\n if ($script:AGENTTEAMS_UPGRADE -and $script:AGENTTEAMS_USE_EMBEDDED -eq \"1\") {\n $controllerNameHit = docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-controller$\"\n $isOldArch = $false\n if (-not $controllerNameHit) {\n $isOldArch = $true\n } else {\n $ctrlImgLine = docker ps -a --format \"{{.Names}} {{.Image}}\" 2>$null | Where-Object { $_ -match '^agentteams-controller ' } | Select-Object -First 1\n if ($ctrlImgLine -and ($ctrlImgLine -notmatch 'embedded')) {\n $isOldArch = $true\n }\n }\n\n if ($isOldArch) {\n $credsTmp = Join-Path ([System.IO.Path]::GetTempPath()) (\"agentteams-upgrade-creds-\" + [Guid]::NewGuid().ToString(\"n\"))\n New-Item -ItemType Directory -Path $credsTmp -Force | Out-Null\n $utf8NoBom = New-Object System.Text.UTF8Encoding $false\n\n $mgrCredsTempStart = $false\n $mgrExists = docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-manager$\"\n $mgrRunning = docker ps --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-manager$\"\n if ($mgrExists -and -not $mgrRunning) {\n Write-Log \"agentteams-manager is stopped; starting it temporarily to extract Matrix credentials for upgrade...\"\n docker start agentteams-manager 2>$null | Out-Null\n Wait-MatrixReady -Container \"agentteams-manager\"\n $mgrCredsTempStart = $true\n }\n\n $inspectLines = docker inspect agentteams-manager --format \"{{range .Config.Env}}{{println .}}{{end}}\" 2>$null\n $mgrPw = \"\"\n if ($inspectLines) {\n $envLine = $inspectLines -split \"`n\" | Where-Object { $_ -match '^AGENTTEAMS_MANAGER_PASSWORD=' } | Select-Object -First 1\n if ($envLine) {\n $mgrPw = ($envLine -replace '^AGENTTEAMS_MANAGER_PASSWORD=', \"\").Trim()\n }\n }\n $mgrRunningNow = docker ps --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-manager$\"\n if ([string]::IsNullOrEmpty($mgrPw) -and $mgrRunningNow) {\n $mgrPw = docker exec agentteams-manager bash -c 'source /data/agentteams-secrets.env 2>/dev/null && echo \"${AGENTTEAMS_MANAGER_PASSWORD}\"' 2>$null\n if ($mgrPw) { $mgrPw = $mgrPw.Trim() }\n }\n $dataVolPresent = docker volume ls -q 2>$null | Where-Object { $_ -eq $config.DATA_DIR }\n if ([string]::IsNullOrEmpty($mgrPw) -and $dataVolPresent) {\n $mgrPw = Read-AgentTeamsSecretFromDataVolume -VolumeName $config.DATA_DIR -Key \"AGENTTEAMS_MANAGER_PASSWORD\"\n }\n\n $envFilePath = $script:AGENTTEAMS_ENV_FILE\n $mgrRoom = \"\"\n if (-not [string]::IsNullOrEmpty($mgrPw)) {\n $adminPw = \"\"\n $adminUser = \"admin\"\n if (Test-Path $envFilePath) {\n $adminLine = Get-Content $envFilePath | Where-Object { $_ -match '^AGENTTEAMS_ADMIN_PASSWORD=' } | Select-Object -First 1\n if ($adminLine) { $adminPw = ($adminLine -replace '^AGENTTEAMS_ADMIN_PASSWORD=', \"\").Trim() }\n $userLine = Get-Content $envFilePath | Where-Object { $_ -match '^AGENTTEAMS_ADMIN_USER=' } | Select-Object -First 1\n if ($userLine) {\n $adminUser = ($userLine -replace '^AGENTTEAMS_ADMIN_USER=', \"\").Trim()\n if ([string]::IsNullOrEmpty($adminUser)) { $adminUser = \"admin\" }\n }\n }\n\n if (-not [string]::IsNullOrEmpty($adminPw) -and $mgrRunningNow) {\n $mgrRoom = Get-AgentTeamsAdminDmRoomViaMatrix -AdminUser $adminUser -AdminPassword $adminPw\n }\n if ([string]::IsNullOrEmpty($mgrRoom)) {\n $mgrRoom = Read-AgentTeamsAdminDmRoomFromWorkspace -WorkspaceDir $config.WORKSPACE_DIR\n }\n\n $gwKeyLine = if (Test-Path $envFilePath) {\n Get-Content $envFilePath | Where-Object { $_ -match '^AGENTTEAMS_MANAGER_GATEWAY_KEY=' } | Select-Object -First 1\n } else { $null }\n $gwKeyForDefault = if ($gwKeyLine) { ($gwKeyLine -replace '^AGENTTEAMS_MANAGER_GATEWAY_KEY=', \"\").Trim() } else { $config.MANAGER_GATEWAY_KEY }\n\n $defaultEnvPath = Join-Path $credsTmp \"default.env\"\n [System.IO.File]::WriteAllLines($defaultEnvPath, @(\n \"WORKER_PASSWORD=$mgrPw\"\n \"WORKER_MINIO_PASSWORD=$(Get-AgentTeamsRandomHex 24)\"\n \"WORKER_GATEWAY_KEY=$gwKeyForDefault\"\n \"WORKER_ROOM_ID=$mgrRoom\"\n ), $utf8NoBom)\n if ($mgrRoom) {\n Write-Log \"Extracted Manager Matrix password and room ID\"\n } else {\n Write-Log \"Extracted Manager Matrix password\"\n }\n }\n\n if ($mgrCredsTempStart) {\n Write-Log \"Stopping agentteams-manager after credential extraction (upgrade will recreate containers)...\"\n docker stop agentteams-manager 2>$null | Out-Null\n }\n }\n }\n\n # Stop and remove existing containers (deferred until after all\n # configuration is collected and images are pulled successfully)\n $existingProxy = docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-controller$\"\n if ($existingProxy) {\n docker stop agentteams-controller *>$null\n docker rm agentteams-controller *>$null\n }\n $existingContainer = docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-manager$\"\n if ($existingContainer) {\n Write-Log (Get-Msg \"install.removing_existing\")\n docker stop agentteams-manager *>$null\n docker rm agentteams-manager *>$null\n }\n\n # Stop and remove worker containers (controller will recreate via CR reconciliation)\n if ($script:UPGRADE_EXISTING_WORKERS) {\n Write-Log (Get-Msg \"install.existing.stopping_workers\")\n $script:UPGRADE_EXISTING_WORKERS | ForEach-Object {\n docker stop $_ *>$null\n docker rm $_ *>$null\n Write-Log (Get-Msg \"install.existing.removed\" -f $_)\n }\n }\n\n # --- Upgrade: inject extracted credentials into data volume (old-arch -> embedded) ---\n if ($credsTmp -and (Test-Path $credsTmp)) {\n $credsFiles = Get-ChildItem -Path $credsTmp -Filter \"*.env\" -ErrorAction SilentlyContinue\n if ($credsFiles -and $credsFiles.Count -gt 0) {\n $cleanupCtr = \"agentteams-upgrade-cleanup\"\n docker rm -f $cleanupCtr 2>$null | Out-Null\n $credsDockerPath = ConvertTo-DockerPath -Path $credsTmp\n $injectShell = \"rm -rf /data/worker-creds && mkdir -p /data/worker-creds && cp /creds/*.env /data/worker-creds/ 2>/dev/null || true && chmod 600 /data/worker-creds/*.env 2>/dev/null || true\"\n docker run --rm --name $cleanupCtr --entrypoint sh `\n -v \"$($config.DATA_DIR):/data\" `\n -v \"${credsDockerPath}:/creds:ro\" `\n $script:EMBEDDED_IMAGE `\n -c $injectShell 2>$null | Out-Null\n if ($LASTEXITCODE -eq 0) {\n Write-Log \"Injected credentials for upgrade\"\n } else {\n Write-Log \"Warning: credential injection failed, continuing\"\n }\n }\n Remove-Item -Path $credsTmp -Recurse -Force -ErrorAction SilentlyContinue\n }\n\n if ($script:AGENTTEAMS_USE_EMBEDDED -eq \"1\") {\n # ============================================================\n # New architecture: embedded controller + auto-created manager\n # (controller container hosts Higress / Tuwunel / MinIO / Element Web /\n # controller binary, then spawns the lightweight manager container)\n # ============================================================\n\n # Internal port: 8080 (Higress gateway inside the controller container).\n $internalGwPort = 8080\n\n $matrixDomain = if ($config.MATRIX_DOMAIN) {\n $config.MATRIX_DOMAIN\n } else {\n \"matrix-local.agentteams.io:$($config.PORT_GATEWAY)\"\n }\n $aigwDomain = if ($config.AI_GATEWAY_DOMAIN) { $config.AI_GATEWAY_DOMAIN } else { \"aigw-local.agentteams.io\" }\n if ($aigwDomain -notmatch \":\") { $aigwDomain = \"${aigwDomain}:${internalGwPort}\" }\n $fsDomain = if ($config.FS_DOMAIN) { $config.FS_DOMAIN } else { \"fs-local.agentteams.io\" }\n if ($fsDomain -notmatch \":\") { $fsDomain = \"${fsDomain}:${internalGwPort}\" }\n\n $ctrlArgs = @(\n \"run\", \"-d\",\n \"--name\", \"agentteams-controller\",\n \"--network\", \"agentteams-net\",\n \"--network-alias\", \"matrix-local.agentteams.io\",\n \"--network-alias\", \"aigw-local.agentteams.io\",\n \"--network-alias\", \"fs-local.agentteams.io\",\n \"-e\", \"AGENTTEAMS_ADMIN_USER=$($config.ADMIN_USER)\",\n \"-e\", \"AGENTTEAMS_ADMIN_PASSWORD=$($config.ADMIN_PASSWORD)\",\n \"-e\", \"AGENTTEAMS_MANAGER_PASSWORD=$($config.MANAGER_PASSWORD)\",\n \"-e\", \"AGENTTEAMS_REGISTRATION_TOKEN=$($config.REGISTRATION_TOKEN)\",\n \"-e\", \"AGENTTEAMS_MINIO_USER=$($config.MINIO_USER)\",\n \"-e\", \"AGENTTEAMS_MINIO_PASSWORD=$($config.MINIO_PASSWORD)\",\n \"-e\", \"AGENTTEAMS_LLM_PROVIDER=$($config.LLM_PROVIDER)\",\n \"-e\", \"AGENTTEAMS_LLM_API_KEY=$($config.LLM_API_KEY)\",\n \"-e\", \"AGENTTEAMS_DEFAULT_MODEL=$($config.DEFAULT_MODEL)\",\n \"-e\", \"AGENTTEAMS_MANAGER_GATEWAY_KEY=$($config.MANAGER_GATEWAY_KEY)\",\n \"-e\", \"AGENTTEAMS_MANAGER_RUNTIME=$($config.MANAGER_RUNTIME)\",\n \"-e\", \"AGENTTEAMS_MANAGER_IMAGE=$managerImage\",\n \"-e\", \"AGENTTEAMS_DEFAULT_WORKER_RUNTIME=$($config.DEFAULT_WORKER_RUNTIME)\",\n \"-e\", \"AGENTTEAMS_WORKER_IMAGE=$($script:WORKER_IMAGE)\",\n \"-e\", \"AGENTTEAMS_COPAW_WORKER_IMAGE=$($script:COPAW_WORKER_IMAGE)\",\n \"-e\", \"AGENTTEAMS_QWENPAW_WORKER_IMAGE=$($script:QWENPAW_WORKER_IMAGE)\",\n \"-e\", \"AGENTTEAMS_HERMES_WORKER_IMAGE=$($script:HERMES_WORKER_IMAGE)\",\n \"-e\", \"AGENTTEAMS_MATRIX_DOMAIN=$matrixDomain\",\n \"-e\", \"AGENTTEAMS_ELEMENT_HOMESERVER_URL=http://127.0.0.1:$($config.PORT_GATEWAY)\",\n \"-e\", \"AGENTTEAMS_MATRIX_URL=http://127.0.0.1:6167\",\n \"-e\", \"AGENTTEAMS_MATRIX_E2EE=$($config.MATRIX_E2EE)\",\n \"-e\", \"AGENTTEAMS_MINIO_ENDPOINT=http://127.0.0.1:9000\",\n \"-e\", \"AGENTTEAMS_MINIO_BUCKET=agentteams-storage\",\n \"-e\", \"AGENTTEAMS_STORAGE_PREFIX=agentteams/agentteams-storage\",\n \"-e\", \"AGENTTEAMS_FS_ENDPOINT=http://127.0.0.1:9000\",\n \"-e\", \"AGENTTEAMS_AI_GATEWAY_URL=http://$aigwDomain\",\n \"-e\", \"AGENTTEAMS_CONTROLLER_URL=http://agentteams-controller:8090\",\n \"-e\", \"AGENTTEAMS_DOCKER_NETWORK=agentteams-net\",\n \"-e\", \"AGENTTEAMS_WORKSPACE_DIR=$($config.WORKSPACE_DIR)\",\n \"-e\", \"AGENTTEAMS_HOST_SHARE_DIR=$($config.HOST_SHARE_DIR)\",\n \"-e\", \"AGENTTEAMS_MANAGER_ENABLED=true\",\n \"-e\", \"AGENTTEAMS_PORT_MANAGER_CONSOLE=$($config.PORT_MANAGER_CONSOLE)\"\n )\n\n if ($script:AGENTTEAMS_TIMEZONE) {\n $ctrlArgs += @(\"-e\", \"TZ=$($script:AGENTTEAMS_TIMEZONE)\")\n }\n if ($env:AGENTTEAMS_YOLO -eq \"1\") {\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_YOLO=1\")\n }\n if ($env:AGENTTEAMS_MATRIX_DEBUG -eq \"1\") {\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_MATRIX_DEBUG=1\")\n }\n if ($config.GITHUB_TOKEN) {\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_GITHUB_TOKEN=$($config.GITHUB_TOKEN)\")\n }\n if ($config.EMBEDDING_MODEL) {\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_EMBEDDING_MODEL=$($config.EMBEDDING_MODEL)\")\n }\n if ($config.OPENAI_BASE_URL) {\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_OPENAI_BASE_URL=$($config.OPENAI_BASE_URL)\")\n }\n if ($env:AGENTTEAMS_AI_STREAM_IDLE_TIMEOUT_SECONDS) {\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_AI_STREAM_IDLE_TIMEOUT_SECONDS=$($env:AGENTTEAMS_AI_STREAM_IDLE_TIMEOUT_SECONDS)\")\n }\n if ($script:AGENTTEAMS_LANGUAGE) {\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_LANGUAGE=$($script:AGENTTEAMS_LANGUAGE)\")\n }\n\n # Optional: CMS/ARMS observability. In embedded mode the controller\n # spawns the Manager and Workers, so it must receive these settings.\n $cmsTracesEnabled = if ($env:AGENTTEAMS_CMS_TRACES_ENABLED) { $env:AGENTTEAMS_CMS_TRACES_ENABLED } else { \"false\" }\n $cmsServiceName = if ($env:AGENTTEAMS_CMS_SERVICE_NAME) { $env:AGENTTEAMS_CMS_SERVICE_NAME } else { \"agentteams-manager\" }\n $cmsMetricsEnabled = if ($env:AGENTTEAMS_CMS_METRICS_ENABLED) { $env:AGENTTEAMS_CMS_METRICS_ENABLED } else { \"false\" }\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_CMS_TRACES_ENABLED=$cmsTracesEnabled\")\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_CMS_SERVICE_NAME=$cmsServiceName\")\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_CMS_METRICS_ENABLED=$cmsMetricsEnabled\")\n if ($env:AGENTTEAMS_CMS_ENDPOINT) {\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_CMS_ENDPOINT=$($env:AGENTTEAMS_CMS_ENDPOINT)\")\n }\n if ($env:AGENTTEAMS_CMS_LICENSE_KEY) {\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_CMS_LICENSE_KEY=$($env:AGENTTEAMS_CMS_LICENSE_KEY)\")\n }\n if ($env:AGENTTEAMS_CMS_PROJECT) {\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_CMS_PROJECT=$($env:AGENTTEAMS_CMS_PROJECT)\")\n }\n if ($env:AGENTTEAMS_CMS_WORKSPACE) {\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_CMS_WORKSPACE=$($env:AGENTTEAMS_CMS_WORKSPACE)\")\n }\n\n # Mount the docker socket so the controller can spawn manager + workers.\n $ctrlArgs += @(\"-v\", \"//var/run/docker.sock:/var/run/docker.sock\")\n $ctrlArgs += @(\"--security-opt\", \"label=disable\")\n\n # Persistent data + workspace mounts (manager workspace is bind-mounted under\n # /root/agentteams-fs/agents/manager so the controller can hand it to the spawned\n # manager container).\n $ctrlArgs += @(\"-v\", \"$($config.DATA_DIR):/data\")\n $wsDockerPath = ConvertTo-DockerPath -Path $config.WORKSPACE_DIR\n $ctrlArgs += @(\"-v\", \"${wsDockerPath}:/root/agentteams-fs/agents/manager\")\n Write-Log (Get-Msg \"host_share.sharing\" -f $config.HOST_SHARE_DIR)\n\n # Externally exposed ports — only the gateway / Higress console / Element Web,\n # since the manager console is now spawned inside its own container by the\n # controller (port mapping is handled there).\n $ctrlArgs += @(\"-p\", \"${portPrefix}$($config.PORT_GATEWAY):8080\")\n $ctrlArgs += @(\"-p\", \"${portPrefix}$($config.PORT_CONSOLE):8001\")\n $ctrlArgs += @(\"-p\", \"${portPrefix}$($config.PORT_ELEMENT_WEB):8088\")\n\n $ctrlArgs += @(\"--restart\", \"unless-stopped\")\n $ctrlArgs += $script:EMBEDDED_IMAGE\n\n Write-Log (Get-Msg \"install.starting_manager\")\n & docker $ctrlArgs\n Write-Log \"Embedded controller started: agentteams-controller\"\n\n # Wait for infra inside the controller container.\n function Wait-EmbeddedUrl {\n param([string]$Url, [string]$Container, [int]$MaxWait, [string]$Description)\n $elapsed = 0\n Write-Log \"Waiting for $Description...\"\n while ($elapsed -lt $MaxWait) {\n docker exec $Container curl -sf $Url *>$null\n if ($LASTEXITCODE -eq 0) {\n Write-Log \"$Description is ready (${elapsed}s)\"\n return $true\n }\n Start-Sleep -Seconds 2\n $elapsed += 2\n }\n Write-Host \"$($script:ESC)[31m[AgentTeams ERROR]$($script:ESC)[0m $Description not ready after ${MaxWait}s\" -ForegroundColor Red\n return $false\n }\n\n if (-not (Wait-EmbeddedUrl \"http://127.0.0.1:6167/_tuwunel/server_version\" \"agentteams-controller\" 120 \"Tuwunel (Matrix)\")) { Exit-Script 1 }\n if (-not (Wait-EmbeddedUrl \"http://127.0.0.1:9000/minio/health/live\" \"agentteams-controller\" 60 \"MinIO\")) { Exit-Script 1 }\n if (-not (Wait-EmbeddedUrl \"http://127.0.0.1:8080/status\" \"agentteams-controller\" 120 \"Higress Gateway\")) { Exit-Script 1 }\n\n # Wait for the controller to spawn the Manager Agent container.\n Write-Log \"Waiting for Manager Agent container...\"\n $mgrWait = 0\n $mgrMax = 300\n while ($mgrWait -lt $mgrMax) {\n $found = docker ps --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-manager$\"\n if ($found) {\n Write-Log \"Manager Agent container detected (${mgrWait}s)\"\n break\n }\n Start-Sleep -Seconds 3\n $mgrWait += 3\n }\n if ($mgrWait -ge $mgrMax) {\n Write-Host \"$($script:ESC)[31m[AgentTeams ERROR]$($script:ESC)[0m Manager Agent container not created after ${mgrMax}s\" -ForegroundColor Red\n Write-Log \"Controller logs:\"\n docker exec agentteams-controller tail -30 /var/log/agentteams/agentteams-controller-error.log 2>$null\n Exit-Script 1\n }\n\n # Wait for Manager Agent container to reach `running` state.\n Write-Log \"Waiting for Manager Agent to start...\"\n $agentWait = 0\n while ($agentWait -lt 120) {\n $state = docker inspect --format \"{{.State.Status}}\" agentteams-manager 2>$null\n if ($state -eq \"running\") {\n Write-Log \"Manager Agent is running\"\n break\n }\n Start-Sleep -Seconds 2\n $agentWait += 2\n }\n\n if ($env:AGENTTEAMS_YOLO -eq \"1\") {\n docker exec agentteams-manager touch /root/manager-workspace/yolo-mode 2>$null\n }\n\n # Wait for the Manager Agent's runtime + Matrix to be reachable. In embedded\n # mode Tuwunel lives in `agentteams-controller`, so the Matrix probe must target\n # the controller (the manager only exposes the agent runtime).\n Wait-ManagerReady -Container \"agentteams-manager\"\n Wait-MatrixReady -Container \"agentteams-controller\"\n\n # Wait for the controller to actually deliver the first-boot welcome\n # message. Gated by the controller on (a) Manager joining the DM room\n # and (b) Higress WASM key-auth propagating to /v1/chat/completions for\n # the Manager's gateway key — typically ~45-90s on a fresh install.\n # We poll Manager CR status.welcomeSent via the in-container AgentTeams\n # CLI, exec'd inside agentteams-controller (the source-of-truth container\n # — its bundled CLI binary is always in lockstep with the controller\n # binary serving the HTTP API, since they're the same `go build`\n # output. The agentteams-manager container's CLI may lag the controller\n # across image upgrades and silently drop the welcomeSent field,\n # leaving this loop hung). The controller mints a long-lived admin\n # SA token at startup and writes it to\n # AGENTTEAMS_AUTH_TOKEN_FILE=/var/run/agentteams/cli-token (set as a Dockerfile\n # ENV default), so a bare `docker exec agentteams-controller agt …`\n # auto-discovers both the endpoint and the token. The brief window\n # after container start before bootstrapAdminCLIToken completes is\n # absorbed by the loop's silent retry.\n $hasAgentTeamsCli = $false\n try {\n docker exec agentteams-controller sh -c 'command -v agt' *> $null\n if ($LASTEXITCODE -eq 0) { $hasAgentTeamsCli = $true }\n } catch {}\n\n if ($hasAgentTeamsCli) {\n Write-Log (Get-Msg \"install.welcome_msg.waiting\")\n $welcomeMax = if ($env:AGENTTEAMS_WELCOME_TIMEOUT) { [int]$env:AGENTTEAMS_WELCOME_TIMEOUT } else { 300 }\n $welcomeWait = 0\n $welcomeDone = $false\n while ($welcomeWait -lt $welcomeMax) {\n $wjson = \"\"\n try {\n $wjson = docker exec agentteams-controller `\n agt get managers default -o json 2>$null\n } catch {}\n if ($wjson -and ($wjson -replace '\\s', '') -match '\"welcomeSent\":true') {\n Write-Log (Get-Msg \"install.welcome_msg.confirmed\" $welcomeWait)\n $welcomeDone = $true\n break\n }\n Start-Sleep -Seconds 3\n $welcomeWait += 3\n }\n if (-not $welcomeDone) {\n # Non-fatal: install is still good. Keep going to the success\n # banner so the admin can use Element Web to nudge Manager into\n # onboarding manually (one DM message is enough).\n Write-Log (Get-Msg \"install.welcome_msg.timeout\" $welcomeMax)\n Write-Log (Get-Msg \"install.welcome_msg.timeout_hint\")\n Write-Log (Get-Msg \"install.welcome_msg.timeout_inspect\")\n }\n } else {\n Write-Log (Get-Msg \"install.welcome_msg.poll_unavailable\")\n }\n } else {\n # Run container (legacy path)\n Write-Log (Get-Msg \"install.starting_manager\")\n & docker $dockerArgs\n\n Wait-ManagerReady -Container \"agentteams-manager\"\n Wait-MatrixReady -Container \"agentteams-manager\"\n }\n\n # Create OpenAI-compatible provider if needed\n if ($config.LLM_PROVIDER -eq \"openai-compat\") {\n New-OpenAICompatProvider -BaseUrl $config.OPENAI_BASE_URL -ApiKey $config.LLM_API_KEY -ConsolePort ([int]$config.PORT_CONSOLE)\n }\n\n # Print success message\n Write-Log \"\"\n Write-Log (Get-Msg \"success.title\")\n Write-Log \"\"\n Write-Log (Get-Msg \"success.domains_configured\")\n Write-Log \" $($config.MATRIX_DOMAIN.Split(':')[0]) $($config.MATRIX_CLIENT_DOMAIN) $($config.AI_GATEWAY_DOMAIN) $($config.FS_DOMAIN) $($config.CONSOLE_DOMAIN)\"\n Write-Log \"\"\n\n $lanIP = Get-LanIP\n\n Write-Host \"$($script:ESC)[33m===============================================================$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.open_url')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[1;36m http://127.0.0.1:$($config.PORT_ELEMENT_WEB)/#/login$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.login_with')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.username' -f $config.ADMIN_USER)$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.password' -f $config.ADMIN_PASSWORD)$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.after_login')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.tell_it')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.manager_auto')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m --------------------------------------------------------------- $($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.mobile_title')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $($script:ESC)[0m\"\n if ($lanIP) {\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.mobile_step1')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.mobile_step2' -f \"http://${lanIP}:$($config.PORT_GATEWAY)\")$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.mobile_step3')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.mobile_username' -f $config.ADMIN_USER)$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.mobile_password' -f $config.ADMIN_PASSWORD)$($script:ESC)[0m\"\n } else {\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.mobile_step1')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.mobile_step2_noip' -f $config.PORT_GATEWAY)$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.mobile_noip_hint')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.mobile_step3')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.mobile_username' -f $config.ADMIN_USER)$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.mobile_password' -f $config.ADMIN_PASSWORD)$($script:ESC)[0m\"\n }\n Write-Host \"$($script:ESC)[33m $($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m===============================================================$($script:ESC)[0m\"\n\n Write-Log \"\"\n Write-Log (Get-Msg \"success.other_consoles\")\n Write-Log (Get-Msg \"success.higress_console\" -f $config.PORT_CONSOLE, $config.ADMIN_USER, $config.ADMIN_PASSWORD)\n if ($script:AGENTTEAMS_USE_EMBEDDED -ne \"1\") {\n # In embedded mode the manager runs in its own auto-spawned container with\n # its own console-port mapping handled by the controller, so don't print a\n # host-side URL/credentials hint here.\n Write-Log (Get-Msg \"success.manager_console\" -f $config.PORT_MANAGER_CONSOLE)\n Write-Log (Get-Msg \"success.manager_console_gateway\" -f $config.ADMIN_USER, $config.ADMIN_PASSWORD)\n }\n Write-Log \"\"\n Write-Log (Get-Msg \"success.switch_llm.title\")\n Write-Log (Get-Msg \"success.switch_llm.hint\")\n Write-Log (Get-Msg \"success.switch_llm.docs\")\n Write-Log (Get-Msg \"success.switch_llm.url\")\n Write-Log \"\"\n Write-Log (Get-Msg \"success.tip\")\n Write-Log \"\"\n if ($config.LOCAL_ONLY -ne \"1\") {\n Write-Host (Get-Msg \"port.local_only.https_hint\") -ForegroundColor Yellow\n Write-Log \"\"\n }\n Write-Log (Get-Msg \"success.config_file\" -f $script:AGENTTEAMS_ENV_FILE)\n\n Write-Log (Get-Msg \"success.data_volume\" -f $config.DATA_DIR)\n\n Write-Log (Get-Msg \"success.workspace\" -f $config.WORKSPACE_DIR)\n}\n\n# ============================================================\n# Worker Installation\n# ============================================================\n\nfunction Install-Worker {\n param(\n [string]$Name,\n [string]$Fs,\n [string]$FsKey,\n [string]$FsSecret,\n [switch]$Reset,\n [switch]$FindSkills,\n [string]$SkillsApiUrl\n )\n\n # Validate required parameters\n if (-not $Name) {\n Write-Error (Get-Msg \"error.name_required\")\n }\n if (-not $Fs) {\n Write-Error (Get-Msg \"error.fs_required\")\n }\n if (-not $FsKey) {\n Write-Error (Get-Msg \"error.fs_key_required\")\n }\n if (-not $FsSecret) {\n Write-Error (Get-Msg \"error.fs_secret_required\")\n }\n\n $containerName = \"agentteams-worker-$Name\"\n\n # Handle reset\n if ($Reset) {\n Write-Log (Get-Msg \"worker.resetting\" -f $Name)\n docker stop $containerName *>$null\n docker rm $containerName *>$null\n }\n\n # Check for existing container\n $existing = docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^$containerName$\"\n if ($existing) {\n Write-Error (Get-Msg \"worker.exists\" -f $containerName)\n }\n\n # Detect timezone and registry\n $timezone = Get-AgentTeamsTimeZone\n $registry = Get-Registry -Timezone $timezone\n $workerImage = if ($env:AGENTTEAMS_INSTALL_WORKER_IMAGE) {\n $env:AGENTTEAMS_INSTALL_WORKER_IMAGE\n } else {\n \"$registry/agentteams/agentteams-worker:$($script:AGENTTEAMS_VERSION)\"\n }\n\n Write-Log (Get-Msg \"worker.starting\" -f $Name)\n\n $dockerArgs = @(\n \"run\", \"-d\",\n \"--name\", $containerName,\n \"-e\", \"HOME=/root/agentteams-fs/agents/$Name\",\n \"-w\", \"/root/agentteams-fs/agents/$Name\",\n \"-e\", \"AGENTTEAMS_WORKER_NAME=$Name\",\n \"-e\", \"AGENTTEAMS_FS_ENDPOINT=$Fs\",\n \"-e\", \"AGENTTEAMS_FS_ACCESS_KEY=$FsKey\",\n \"-e\", \"AGENTTEAMS_FS_SECRET_KEY=$FsSecret\"\n )\n\n if (-not $SkillsApiUrl) {\n if ($env:AGENTTEAMS_SKILLS_API_URL) {\n $SkillsApiUrl = $env:AGENTTEAMS_SKILLS_API_URL\n } else {\n $SkillsApiUrl = \"nacos://market.agentteams.io:80/public\"\n }\n }\n\n if ($FindSkills -and $SkillsApiUrl) {\n $dockerArgs += @(\"-e\", \"SKILLS_API_URL=$SkillsApiUrl\")\n Write-Log (Get-Msg \"worker.skills_url\" -f $SkillsApiUrl)\n }\n if ($env:AGENTTEAMS_NACOS_USERNAME) {\n $dockerArgs += @(\"-e\", \"AGENTTEAMS_NACOS_USERNAME=$($env:AGENTTEAMS_NACOS_USERNAME)\")\n }\n if ($env:AGENTTEAMS_NACOS_PASSWORD) {\n $dockerArgs += @(\"-e\", \"AGENTTEAMS_NACOS_PASSWORD=$($env:AGENTTEAMS_NACOS_PASSWORD)\")\n }\n if ($env:AGENTTEAMS_NACOS_TOKEN) {\n $dockerArgs += @(\"-e\", \"AGENTTEAMS_NACOS_TOKEN=$($env:AGENTTEAMS_NACOS_TOKEN)\")\n }\n\n $dockerArgs += @(\"--restart\", \"unless-stopped\", $workerImage)\n\n & docker $dockerArgs\n\n Write-Log \"\"\n Write-Log (Get-Msg \"worker.started\" -f $Name)\n Write-Log (Get-Msg \"worker.container\" -f $containerName)\n Write-Log (Get-Msg \"worker.view_logs\" -f $containerName)\n}\n\n# ============================================================\n# Uninstall\n# ============================================================\n\nfunction Uninstall-AgentTeams {\n Write-Log (Get-Msg \"uninstall.title\")\n\n # Stop and remove manager\n $manager = docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-manager$\"\n if ($manager) {\n Write-Log (Get-Msg \"uninstall.stopping_manager\")\n docker stop agentteams-manager *>$null\n docker rm agentteams-manager *>$null\n }\n\n # Stop and remove workers\n $workers = docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-worker-\"\n if ($workers) {\n Write-Log (Get-Msg \"uninstall.stopping_workers\")\n $workers | ForEach-Object {\n docker stop $_ *>$null\n docker rm $_ *>$null\n Write-Log (Get-Msg \"uninstall.removed\" -f $_)\n }\n }\n\n # Stop and remove docker-proxy (legacy <= v1.0.x; current arch uses\n # agentteams-controller for the same role)\n $proxy = docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-docker-proxy$\"\n if ($proxy) {\n Write-Log (Get-Msg \"uninstall.removing_proxy\")\n docker stop agentteams-docker-proxy *>$null\n docker rm agentteams-docker-proxy *>$null\n }\n\n # Stop and remove the embedded controller container. MUST happen\n # before the `docker volume rm agentteams-data` step below -- in embedded\n # mode agentteams-controller mounts agentteams-data at /data (Tuwunel DB,\n # MinIO state, Higress state, all the room messages), and `volume rm`\n # against an in-use volume fails silently. Skipping this used to\n # leave room/message history behind across \"uninstall + reinstall\"\n # cycles. See PR #692.\n $controller = docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-controller$\"\n if ($controller) {\n Write-Log (Get-Msg \"uninstall.stopping_controller\")\n docker stop agentteams-controller *>$null\n docker rm agentteams-controller *>$null\n }\n\n # Remove Docker volume (read custom name from env file if available)\n $dataVolume = \"agentteams-data\"\n if (Test-Path $script:AGENTTEAMS_ENV_FILE) {\n $envContent = Get-Content $script:AGENTTEAMS_ENV_FILE -ErrorAction SilentlyContinue\n $dataLine = $envContent | Select-String \"^AGENTTEAMS_DATA_DIR=\"\n if ($dataLine) {\n $parsed = ($dataLine -split \"=\", 2)[1]\n if ($parsed) { $dataVolume = $parsed }\n }\n }\n $volume = docker volume ls -q 2>$null | Select-String \"^${dataVolume}$\"\n if ($volume) {\n Write-Log (Get-Msg \"uninstall.removing_volume\")\n docker volume rm $dataVolume *>$null\n }\n\n # Remove Docker network\n $network = docker network ls --format \"{{.Name}}\" 2>$null | Select-String \"^agentteams-net$\"\n if ($network) {\n Write-Log (Get-Msg \"uninstall.removing_network\")\n docker network rm agentteams-net *>$null\n }\n\n # Remove workspace directory\n $workspaceDir = \"$env:USERPROFILE\\agentteams-manager\"\n if (Test-Path $script:AGENTTEAMS_ENV_FILE) {\n $envContent = Get-Content $script:AGENTTEAMS_ENV_FILE -ErrorAction SilentlyContinue\n $wsLine = $envContent | Select-String \"^AGENTTEAMS_WORKSPACE_DIR=\"\n if ($wsLine) {\n $workspaceDir = ($wsLine -split \"=\", 2)[1]\n }\n }\n if ($workspaceDir -and (Test-Path $workspaceDir)) {\n Write-Log (Get-Msg \"uninstall.removing_workspace\" -f $workspaceDir)\n Remove-Item -Recurse -Force $workspaceDir -ErrorAction SilentlyContinue\n }\n\n # Remove env file\n if (Test-Path $script:AGENTTEAMS_ENV_FILE) {\n Write-Log (Get-Msg \"uninstall.removing_env\" -f $script:AGENTTEAMS_ENV_FILE)\n Remove-Item -Force $script:AGENTTEAMS_ENV_FILE\n }\n\n # Remove install log (stop transcript first to release the file)\n if (Test-Path $script:AGENTTEAMS_LOG_FILE) {\n Write-Log (Get-Msg \"uninstall.removing_log\" -f $script:AGENTTEAMS_LOG_FILE)\n try { Stop-Transcript *>$null } catch {}\n Remove-Item -Force $script:AGENTTEAMS_LOG_FILE -ErrorAction SilentlyContinue\n }\n\n Write-Log \"\"\n Write-Log (Get-Msg \"uninstall.done\")\n}\n\n# ============================================================\n# Main Entry Point\n# ============================================================\n\ntry {\n switch ($Command) {\n \"manager\" {\n Install-Manager\n }\n \"worker\" {\n Install-Worker -Name $Name -Fs $Fs -FsKey $FsKey -FsSecret $FsSecret -Reset:$Reset -FindSkills:$FindSkills -SkillsApiUrl $SkillsApiUrl\n }\n \"uninstall\" {\n Uninstall-AgentTeams\n }\n }\n} catch {\n Exit-Script 1\n}\n\n# Stop transcript logging\ntry {\n Stop-Transcript -ErrorAction SilentlyContinue | Out-Null\n} catch {\n # Ignore errors when stopping transcript\n}\n"}, "fb3a40be1f005bd584f45544fc73bd4601d5c52a": {"t": 1785764737.8650856, "n": 192612, "h": "ab2c7c06e92d5f51", "c": "#!/usr/bin/env powershell\n# agentteams-install.ps1 - One-click installation for AgentTeams Manager and Worker on Windows\n# Requires PowerShell 7.0+ (recommended)\n#\n# Usage:\n# .\\agentteams-install.ps1 # Interactive installation (choose Quick Start or Manual)\n# .\\agentteams-install.ps1 manager # Same as above (explicit)\n# .\\agentteams-install.ps1 worker --name <name> ... # Worker installation\n#\n# Onboarding Modes:\n# Quick Start - Fast installation with all default values (recommended)\n# Manual - Customize each option step by step\n#\n# Environment variables (for automation):\n# AGENTTEAMS_NON_INTERACTIVE Skip all prompts, use defaults (default: 0)\n# AGENTTEAMS_LLM_PROVIDER LLM provider (default: openai-compat for zh non-interactive Token Plan; qwen for en)\n# AGENTTEAMS_DEFAULT_MODEL Default model (default: qwen3.6-plus for zh Token Plan and en non-interactive)\n# AGENTTEAMS_OPENAI_BASE_URL OpenAI-compatible base URL (default for zh non-interactive: Alibaba Token Plan endpoint)\n# AGENTTEAMS_LLM_API_KEY LLM API key (required)\n# AGENTTEAMS_ADMIN_USER Admin username (default: admin)\n# AGENTTEAMS_ADMIN_PASSWORD Admin password (auto-generated if not set, min 8 chars)\n# AGENTTEAMS_MATRIX_DOMAIN Matrix domain (default: matrix-local.agentteams.io:18080)\n# AGENTTEAMS_MOUNT_SOCKET Mount container runtime socket (default: 1)\n# AGENTTEAMS_MATRIX_E2EE Matrix E2EE (default: 0, disabled)\n# AGENTTEAMS_DATA_DIR Docker volume name for persistent data (default: agentteams-data)\n# AGENTTEAMS_WORKSPACE_DIR Host directory for manager workspace (default: ~/agentteams-manager)\n# AGENTTEAMS_VERSION Image tag (default: latest)\n# AGENTTEAMS_REGISTRY Image registry (default: auto-detected by timezone)\n# AGENTTEAMS_INSTALL_MANAGER_IMAGE Override manager image (e.g., local build)\n# AGENTTEAMS_INSTALL_WORKER_IMAGE Override worker image (e.g., local build)\n# AGENTTEAMS_INSTALL_COPAW_WORKER_IMAGE Override copaw worker image (e.g., local build)\n# AGENTTEAMS_INSTALL_QWENPAW_WORKER_IMAGE Override qwenpaw worker image (e.g., local build)\n# AGENTTEAMS_INSTALL_HERMES_WORKER_IMAGE Override hermes worker image (e.g., local build)\n# AGENTTEAMS_PORT_GATEWAY Host port for Higress gateway (default: 18080)\n# AGENTTEAMS_PORT_CONSOLE Host port for Higress console (default: 18001)\n# AGENTTEAMS_PORT_ELEMENT_WEB Host port for Element Web direct access (default: 18088)\n\n#Requires -Version 5.1\n\n[CmdletBinding()]\nparam(\n [Parameter(Position = 0)]\n [ValidateSet(\"manager\", \"worker\", \"uninstall\")]\n [string]$Command = \"manager\",\n\n # Worker options\n [string]$Name,\n [string]$Fs,\n [string]$FsKey,\n [string]$FsSecret,\n [switch]$Reset,\n [switch]$FindSkills,\n [string]$SkillsApiUrl,\n\n # General options\n [switch]$NonInteractive,\n [string]$EnvFile\n)\n\n# ============================================================\n# Configuration\n# ============================================================\n\n$script:AGENTTEAMS_VERSION = if ($env:AGENTTEAMS_VERSION) { $env:AGENTTEAMS_VERSION } else { \"latest\" }\n$script:AGENTTEAMS_NON_INTERACTIVE = if ($env:AGENTTEAMS_NON_INTERACTIVE -eq \"1\" -or $NonInteractive) { $true } else { $false }\n$script:AGENTTEAMS_MOUNT_SOCKET = if ($env:AGENTTEAMS_MOUNT_SOCKET -eq \"0\") { $false } else { $true }\n$script:AGENTTEAMS_ENV_FILE = if ($EnvFile) { $EnvFile } elseif ($env:AGENTTEAMS_ENV_FILE) { $env:AGENTTEAMS_ENV_FILE } else { \"$env:USERPROFILE\\agentteams-manager.env\" }\n$script:StepResult = \"\" # Used by state machine to signal \"back\" navigation\n$script:config = @{} # Shared config hashtable for step functions\n\n# ANSI escape character for terminal colors\n$script:ESC = [char]0x1B\n\n# ============================================================\n# Log all output to file\n# ============================================================\n\n$script:AGENTTEAMS_LOG_FILE = \"$env:USERPROFILE\\agentteams-install.log\"\n\n# Start transcript for logging (PowerShell's built-in logging mechanism)\ntry {\n Start-Transcript -Path $script:AGENTTEAMS_LOG_FILE -Append -ErrorAction SilentlyContinue | Out-Null\n} catch {\n # If transcript fails, continue without logging\n}\n\nWrite-Host \"\"\nWrite-Host \"========================================\"\nWrite-Host \"AgentTeams Installation Log\"\nWrite-Host \"Started: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')\"\nWrite-Host \"User: $env:USERNAME\"\nWrite-Host \"System: $(hostname)\"\nWrite-Host \"Log file: $($script:AGENTTEAMS_LOG_FILE)\"\nWrite-Host \"========================================\"\nWrite-Host \"\"\n\n# ============================================================\n# Utility Functions\n# ============================================================\n\nfunction Write-Log {\n param([string]$Message)\n Write-Host \"$($script:ESC)[36m[AgentTeams]$($script:ESC)[0m $Message\"\n}\n\nfunction Write-Error {\n param([string]$Message)\n Write-Host \"$($script:ESC)[31m[AgentTeams ERROR]$($script:ESC)[0m $Message\" -ForegroundColor Red\n throw $Message\n}\n\nfunction Write-Warning {\n param([string]$Message)\n Write-Host \"$($script:ESC)[33m[AgentTeams WARNING]$($script:ESC)[0m $Message\"\n}\n\n# Pause before exit on error so user can read the message when running via double-click\nfunction Exit-Script {\n param([int]$ExitCode = 0)\n if ($ExitCode -ne 0 -and -not $script:AGENTTEAMS_NON_INTERACTIVE) {\n Write-Host \"\"\n Write-Host \"Press Enter to exit.\" -NoNewline\n $Host.UI.ReadLine()\n }\n exit $ExitCode\n}\n\nfunction Test-DockerRunning {\n try {\n $null = docker info 2>&1\n if ($LASTEXITCODE -ne 0) {\n return $false\n }\n return $true\n }\n catch {\n return $false\n }\n}\n\nfunction Get-AgentTeamsTimeZone {\n try {\n if ($env:AGENTTEAMS_TIMEZONE) {\n return $env:AGENTTEAMS_TIMEZONE\n }\n\n $tz = (Get-TimeZone).Id\n # Convert Windows timezone to IANA format\n $tzMap = @{\n \"China Standard Time\" = \"Asia/Shanghai\"\n \"Pacific Standard Time\" = \"America/Los_Angeles\"\n \"Mountain Standard Time\" = \"America/Denver\"\n \"Central Standard Time\" = \"America/Chicago\"\n \"Eastern Standard Time\" = \"America/New_York\"\n \"GMT Standard Time\" = \"Europe/London\"\n \"Central European Standard Time\" = \"Europe/Berlin\"\n \"Tokyo Standard Time\" = \"Asia/Tokyo\"\n \"Singapore Standard Time\" = \"Asia/Singapore\"\n \"Korea Standard Time\" = \"Asia/Seoul\"\n \"India Standard Time\" = \"Asia/Kolkata\"\n }\n\n if ($tzMap.ContainsKey($tz)) {\n return $tzMap[$tz]\n }\n return $tz\n }\n catch {\n return \"Asia/Shanghai\"\n }\n}\n\nfunction Get-Registry {\n param([string]$Timezone)\n\n if ($env:AGENTTEAMS_REGISTRY) {\n return $env:AGENTTEAMS_REGISTRY\n }\n\n # Americas\n if ($Timezone -match \"^America/\") {\n return \"higress-registry.us-west-1.cr.aliyuncs.com\"\n }\n\n # Southeast Asia\n if ($Timezone -match \"^(Asia/Singapore|Asia/Bangkok|Asia/Jakarta|Asia/Makassar|Asia/Jayapura|Asia/Kuala_Lumpur|Asia/Ho_Chi_Minh|Asia/Manila|Asia/Yangon|Asia/Vientiane|Asia/Phnom_Penh|Asia/Pontianak|Asia/Ujung_Pandang)\") {\n return \"higress-registry.ap-southeast-7.cr.aliyuncs.com\"\n }\n\n # Default: China\n return \"higress-registry.cn-hangzhou.cr.aliyuncs.com\"\n}\n\nfunction Get-AgentTeamsLanguage {\n param([string]$Timezone)\n $chineseZones = @(\n \"Asia/Shanghai\", \"Asia/Chongqing\", \"Asia/Harbin\", \"Asia/Urumqi\",\n \"Asia/Taipei\", \"Asia/Hong_Kong\", \"Asia/Macau\"\n )\n if ($chineseZones -contains $Timezone) { return \"zh\" }\n return \"en\"\n}\n\n# ============================================================\n# Centralized message dictionary and Get-Msg function\n# ============================================================\n\n$script:Messages = @{\n # --- Timezone detection messages ---\n \"tz.warning.title\" = @{ zh = \"无法自动检测时区。\"; en = \"Could not detect timezone automatically.\" }\n \"tz.warning.prompt\" = @{ zh = \"请输入您的时区(例如 Asia/Shanghai、America/New_York)。\"; en = \"Please enter your timezone (e.g., Asia/Shanghai, America/New_York).\" }\n \"tz.default\" = @{ zh = \"使用默认时区: {0}\"; en = \"Using default timezone: {0}\" }\n \"tz.input_prompt\" = @{ zh = \"时区\"; en = \"Timezone\" }\n\n # --- Installation title and info ---\n \"install.title\" = @{ zh = \"=== AgentTeams Manager 安装 ===\"; en = \"=== AgentTeams Manager Installation ===\" }\n \"install.registry\" = @{ zh = \"镜像仓库: {0}\"; en = \"Registry: {0}\" }\n \"install.dir\" = @{ zh = \"安装目录: {0}\"; en = \"Installation directory: {0}\" }\n \"install.dir_hint\" = @{ zh = \" (env 文件 'agentteams-manager.env' 将保存到 HOME 目录。)\"; en = \" (The env file 'agentteams-manager.env' will be saved to your HOME directory.)\" }\n \"install.dir_hint2\" = @{ zh = \" (请从您希望管理此安装的目录运行此脚本。)\"; en = \" (Run this script from the directory where you want to manage this installation.)\" }\n\n # --- Onboarding mode ---\n \"install.mode.title\" = @{ zh = \"--- Onboarding 模式 ---\"; en = \"--- Onboarding Mode ---\" }\n \"install.mode.choose\" = @{ zh = \"选择安装模式:\"; en = \"Choose your installation mode:\" }\n \"install.mode.quickstart\" = @{ zh = \" 1) 快速开始 - 使用阿里云通义 Token 套餐快速安装(推荐)\"; en = \" 1) Quick Start - Fast installation with Qwen Cloud (recommended)\" }\n \"install.mode.manual\" = @{ zh = \" 2) 手动配置 - 选择 LLM 提供商并自定义选项\"; en = \" 2) Manual - Choose LLM provider and customize options\" }\n \"install.mode.prompt\" = @{ zh = \"请选择 [1/2]\"; en = \"Enter choice [1/2]\" }\n \"install.mode.quickstart_selected\" = @{ zh = \"已选择快速开始模式 - 使用阿里云通义 Token 套餐\"; en = \"Quick Start mode selected - using Qwen Cloud\" }\n \"install.mode.manual_selected\" = @{ zh = \"已选择手动配置模式 - 您将选择 LLM 提供商并自定义选项\"; en = \"Manual mode selected - you will choose LLM provider and customize options\" }\n \"install.mode.invalid\" = @{ zh = \"无效选择,默认使用快速开始模式\"; en = \"Invalid choice, defaulting to Quick Start mode\" }\n\n # --- Existing installation detected ---\n \"install.existing.detected\" = @{ zh = \"检测到已有 Manager 安装(env 文件: {0})\"; en = \"Existing Manager installation detected (env file: {0})\" }\n \"install.existing.choose\" = @{ zh = \"选择操作:\"; en = \"Choose an action:\" }\n \"install.existing.upgrade\" = @{ zh = \" 1) 就地升级(保留数据、工作空间、env 文件)\"; en = \" 1) In-place upgrade (keep data, workspace, env file)\" }\n \"install.existing.reinstall\" = @{ zh = \" 2) 全新重装(删除所有数据,重新开始)\"; en = \" 2) Clean reinstall (remove all data, start fresh)\" }\n \"install.existing.cancel\" = @{ zh = \" 3) 取消\"; en = \" 3) Cancel\" }\n \"install.existing.prompt\" = @{ zh = \"请选择 [1/2/3]\"; en = \"Enter choice [1/2/3]\" }\n \"install.existing.upgrade_noninteractive\" = @{ zh = \"非交互模式: 执行就地升级...\"; en = \"Non-interactive mode: performing in-place upgrade...\" }\n \"install.existing.upgrading\" = @{ zh = \"执行就地升级...\"; en = \"Performing in-place upgrade...\" }\n \"install.existing.warn_manager_stop\" = @{ zh = \"警告: Manager 容器将被停止并重新创建。\"; en = \"WARNING: Manager container will be stopped and recreated.\" }\n \"install.existing.warn_worker_recreate\" = @{ zh = \"警告: Worker 容器也将被重新创建(以更新 Manager IP)。\"; en = \"WARNING: Worker containers will also be recreated (to update Manager IP in hosts).\" }\n \"install.existing.continue_prompt\" = @{ zh = \"继续?[y/N]\"; en = \"Continue? [y/N]\" }\n \"install.existing.cancelled\" = @{ zh = \"安装已取消。\"; en = \"Installation cancelled.\" }\n \"install.existing.stopping_manager\" = @{ zh = \"停止并移除现有 manager 容器...\"; en = \"Stopping and removing existing manager container...\" }\n \"install.existing.stopping_workers\" = @{ zh = \"停止并移除现有 worker 容器...\"; en = \"Stopping and removing existing worker containers...\" }\n \"install.existing.removed\" = @{ zh = \" 已移除: {0}\"; en = \" Removed: {0}\" }\n\n # --- Upgrade mode sub-menu ---\n \"upgrade.mode.prompt\" = @{ zh = \"请选择升级方式:\"; en = \"Select upgrade mode:\" }\n \"upgrade.mode.keep_all\" = @{ zh = \" 1) 保留所有参数,一键升级(推荐)\"; en = \" 1) Keep all parameters, quick upgrade (recommended)\" }\n \"upgrade.mode.confirm_each\" = @{ zh = \" 2) 逐个确认参数(可修改)\"; en = \" 2) Confirm each parameter (can modify)\" }\n \"upgrade.mode.back\" = @{ zh = \" 3) 返回\"; en = \" 3) Back\" }\n\n # --- Clean reinstall messages ---\n \"install.reinstall.performing\" = @{ zh = \"执行全新重装...\"; en = \"Performing clean reinstall...\" }\n \"install.reinstall.warn_stop\" = @{ zh = \"警告: 以下运行中的容器将被停止:\"; en = \"WARNING: The following running containers will be stopped:\" }\n \"install.reinstall.warn_delete\" = @{ zh = \"警告: 以下内容将被删除:\"; en = \"WARNING: This will DELETE the following:\" }\n \"install.reinstall.warn_volume\" = @{ zh = \" - Docker 卷: agentteams-data\"; en = \" - Docker volume: agentteams-data\" }\n \"install.reinstall.warn_env\" = @{ zh = \" - Env 文件: {0}\"; en = \" - Env file: {0}\" }\n \"install.reinstall.warn_workspace\" = @{ zh = \" - Manager 工作空间: {0}\"; en = \" - Manager workspace: {0}\" }\n \"install.reinstall.warn_workers\" = @{ zh = \" - 所有 worker 容器\"; en = \" - All worker containers\" }\n \"install.reinstall.warn_proxy\" = @{ zh = \" - Docker API 代理容器: agentteams-controller\"; en = \" - Docker API proxy container: agentteams-controller\" }\n \"install.reinstall.removing_proxy\" = @{ zh = \"正在移除 Docker API 代理容器: agentteams-controller\"; en = \"Removing Docker API proxy container: agentteams-controller\" }\n \"install.reinstall.warn_network\" = @{ zh = \" - Docker 网络: agentteams-net\"; en = \" - Docker network: agentteams-net\" }\n \"install.reinstall.removing_network\" = @{ zh = \"正在移除 Docker 网络: agentteams-net\"; en = \"Removing Docker network: agentteams-net\" }\n \"install.reinstall.confirm_type\" = @{ zh = \"请输入工作空间路径以确认删除(或按 Ctrl+C 取消):\"; en = \"To confirm deletion, please type the workspace path:\" }\n \"install.reinstall.confirm_path\" = @{ zh = \"输入路径以确认(或按 Ctrl+C 取消)\"; en = \"Type the path to confirm (or press Ctrl+C to cancel)\" }\n \"install.reinstall.path_mismatch\" = @{ zh = \"路径不匹配。中止重装。输入: '{0}',期望: '{1}'\"; en = \"Path mismatch. Aborting reinstall. Input: '{0}', Expected: '{1}'\" }\n \"install.reinstall.confirmed\" = @{ zh = \"已确认。正在清理...\"; en = \"Confirmed. Cleaning up...\" }\n \"install.reinstall.removed_worker\" = @{ zh = \" 已移除 worker: {0}\"; en = \" Removed worker: {0}\" }\n \"install.reinstall.removing_volume\" = @{ zh = \"正在移除 Docker 卷: agentteams-data\"; en = \"Removing Docker volume: agentteams-data\" }\n \"install.reinstall.warn_volume_fail\" = @{ zh = \" 警告: 无法移除卷(可能有引用)\"; en = \" Warning: Could not remove volume (may have references)\" }\n \"install.reinstall.removing_workspace\" = @{ zh = \"正在移除工作空间目录: {0}\"; en = \"Removing workspace directory: {0}\" }\n \"install.reinstall.removing_env\" = @{ zh = \"正在移除 env 文件: {0}\"; en = \"Removing env file: {0}\" }\n \"install.reinstall.cleanup_done\" = @{ zh = \"清理完成。开始全新安装...\"; en = \"Cleanup complete. Starting fresh installation...\" }\n \"install.reinstall.failed_rm_workspace\" = @{ zh = \"无法移除工作空间目录\"; en = \"Failed to remove workspace directory\" }\n\n # --- Orphan volume detection ---\n \"install.orphan_volume.detected\" = @{ zh = \"⚠️ 检测到残留数据卷 '{0}',但未找到对应的 env 配置文件。\"; en = \"⚠️ Found leftover data volume '{0}' but no matching env config file.\" }\n \"install.orphan_volume.warn\" = @{ zh = \"这可能是之前安装的残留数据,会导致新安装出现异常(如密码冲突、服务启动失败)。\"; en = \"This is likely leftover data from a previous installation and may cause issues (e.g., credential conflicts, service startup failures).\" }\n \"install.orphan_volume.choose\" = @{ zh = \"选择操作:\"; en = \"Choose an action:\" }\n \"install.orphan_volume.clean\" = @{ zh = \" 1) 清理残留数据卷后继续安装(推荐)\"; en = \" 1) Remove leftover volume and continue installation (recommended)\" }\n \"install.orphan_volume.keep\" = @{ zh = \" 2) 保留数据卷继续安装(可能出现异常)\"; en = \" 2) Keep the volume and continue installation (may cause issues)\" }\n \"install.orphan_volume.prompt\" = @{ zh = \"请选择 [1/2]\"; en = \"Enter choice [1/2]\" }\n \"install.orphan_volume.cleaning\" = @{ zh = \"正在清理残留数据卷...\"; en = \"Removing leftover data volume...\" }\n \"install.orphan_volume.cleaned\" = @{ zh = \"残留数据卷已清理。继续全新安装...\"; en = \"Leftover volume removed. Continuing with fresh installation...\" }\n \"install.orphan_volume.keeping\" = @{ zh = \"保留数据卷,继续安装。如遇异常请选择全新重装。\"; en = \"Keeping existing volume. If you encounter issues, consider a clean reinstall.\" }\n \"install.orphan_volume.clean_noninteractive\" = @{ zh = \"非交互模式: 自动清理残留数据卷...\"; en = \"Non-interactive mode: automatically removing leftover volume...\" }\n\n # --- Loading existing config ---\n \"install.loading_config\" = @{ zh = \"从 {0} 加载已有配置(shell 环境变量优先)...\"; en = \"Loading existing config from {0} (shell env vars take priority)...\" }\n\n # --- LLM Configuration ---\n \"llm.title\" = @{ zh = \"--- LLM 配置 ---\"; en = \"--- LLM Configuration ---\" }\n \"llm.provider.label\" = @{ zh = \" 提供商: {0}\"; en = \" Provider: {0}\" }\n \"llm.model.label\" = @{ zh = \" 模型: {0}\"; en = \" Model: {0}\" }\n \"llm.provider.qwen\" = @{ zh = \" 提供商: qwen(阿里云百炼)\"; en = \" Provider: qwen (Alibaba Cloud Bailian)\" }\n \"llm.provider.qwen_default\" = @{ zh = \" 提供商: {0}(默认)\"; en = \" Provider: {0} (default)\" }\n \"llm.model.default\" = @{ zh = \" 模型: {0}(默认)\"; en = \" Model: {0} (default)\" }\n \"llm.apikey_hint_bailian\" = @{ zh = \" 提示: 获取阿里云百炼(DashScope)API Key:\"; en = \" Hint: Get your Alibaba Cloud Bailian (DashScope) API Key:\" }\n \"llm.apikey_url_bailian\" = @{ zh = \" https://www.aliyun.com/product/bailian\"; en = \" https://www.aliyun.com/product/bailian\" }\n \"llm.apikey_hint_qwencloud\" = @{ zh = \" 提示: 从 Qwen Cloud(国际站)获取 DASHSCOPE_API_KEY:\"; en = \" Hint: Get your DASHSCOPE_API_KEY for Qwen Cloud (international) from:\" }\n \"llm.apikey_url_qwencloud\" = @{ zh = \" https://home.qwencloud.com/api-keys (文档: https://docs.qwencloud.com/)\"; en = \" https://home.qwencloud.com/api-keys | Docs: https://docs.qwencloud.com/\" }\n \"llm.apikey_hint_tokenplan\" = @{ zh = \" 提示: 获取 DashScope API Key 或开通通义 Token 套餐,请参考:\"; en = \" Hint: Get your DashScope or Token Plan API key (Alibaba Model Studio):\" }\n \"llm.apikey_url_tokenplan\" = @{ zh = \" https://help.aliyun.com/zh/model-studio/token-plan-quickstart\"; en = \" https://common-buy.aliyun.com/token-plan/ | https://help.aliyun.com/zh/model-studio/token-plan-quickstart\" }\n \"llm.apikey_hint_codingplan\" = @{ zh = \" 提示: 获取 DashScope API Key(Coding 套餐 / coding.dashscope 接口):\"; en = \" Hint: Get your DashScope API key for Coding Plan (coding.dashscope endpoint):\" }\n \"llm.apikey_url_codingplan\" = @{ zh = \" https://help.aliyun.com/zh/model-studio/get-api-key\"; en = \" https://help.aliyun.com/zh/model-studio/get-api-key\" }\n \"llm.apikey_prompt\" = @{ zh = \"LLM API Key\"; en = \"LLM API Key\" }\n \"llm.providers_title\" = @{ zh = \"可用 LLM 提供商:\"; en = \"Available LLM Providers:\" }\n \"llm.provider.alibaba\" = @{ zh = \" 1) 阿里云通义 Token 套餐 - 推荐中国用户使用\"; en = \" 1) Qwen Cloud - International (OpenAI-compatible API, recommended)\" }\n \"llm.provider.openai_compat\" = @{ zh = \" 2) OpenAI 兼容 API - 自定义 Base URL(OpenAI、DeepSeek 等)\"; en = \" 2) OpenAI-compatible API - Custom Base URL (OpenAI, DeepSeek, etc.)\" }\n \"llm.provider.select\" = @{ zh = \"选择提供商 [1/2]\"; en = \"Select provider [1/2]\" }\n \"llm.alibaba.models_title\" = @{ zh = \"选择阿里云模型接入方式:\"; en = \"Select Alibaba Cloud model access:\" }\n \"llm.alibaba.model.tokenplan\" = @{ zh = \" 1) 阿里云通义 Token 套餐 - 兼容模式(推荐)\"; en = \" 1) Alibaba Cloud Token Plan - compatible-mode (recommended)\" }\n \"llm.alibaba.model.bailian\" = @{ zh = \" 2) 阿里云百炼 - DashScope 通用兼容接口\"; en = \" 2) Alibaba Cloud Bailian - DashScope compatible mode\" }\n \"llm.alibaba.model.codingplan_legacy\" = @{ zh = \" 3) 阿里云 Coding 套餐 - 旧版端点(兼容保留)\"; en = \" 3) Alibaba Cloud Coding Plan - legacy endpoint (backward compatible)\" }\n \"llm.alibaba.model.select\" = @{ zh = \"选择接入方式 [1/2/3]\"; en = \"Select access option [1/2/3]\" }\n \"llm.alibaba.model.invalid\" = @{ zh = \"无效选择: {0}(请输入 1、2 或 3)\"; en = \"Invalid choice: {0} (please enter 1, 2, or 3)\" }\n \"llm.codingplan.models_title\" = @{ zh = \"选择通义 Token 套餐默认模型:\"; en = \"Select Qwen Cloud default model:\" }\n \"llm.codingplan.model.qwen36plus\" = @{ zh = \" 1) qwen3.6-plus - 千问 3.6(推荐)\"; en = \" 1) qwen3.6-plus - Qwen 3.6 (recommended)\" }\n \"llm.codingplan.model.glm5\" = @{ zh = \" 2) glm-5 - 智谱 GLM-5(编程推荐)\"; en = \" 2) glm-5 - Zhipu GLM-5 (recommended for coding)\" }\n \"llm.codingplan.model.kimi\" = @{ zh = \" 3) kimi-k2.5 - Moonshot Kimi K2.5\"; en = \" 3) kimi-k2.5 - Moonshot Kimi K2.5\" }\n \"llm.codingplan.model.minimax\" = @{ zh = \" 4) MiniMax-M2.5 - MiniMax M2.5\"; en = \" 4) MiniMax-M2.5 - MiniMax M2.5\" }\n \"llm.codingplan.model.select\" = @{ zh = \"选择模型 [1/2/3/4]\"; en = \"Select model [1/2/3/4]\" }\n \"llm.provider.selected_tokenplan\" = @{ zh = \" 提供商: 阿里云通义 Token 套餐(兼容模式)\"; en = \" Provider: Alibaba Cloud Token Plan (compatible mode)\" }\n \"llm.provider.selected_codingplan\" = @{ zh = \" 提供商: 阿里云通义 Token 套餐(alibaba-cloud)\"; en = \" Provider: Qwen Cloud (international) (alibaba-cloud)\" }\n \"llm.provider.selected_codingplan_legacy\" = @{ zh = \" 提供商: 阿里云 Coding 套餐(coding.dashscope)\"; en = \" Provider: Alibaba Cloud Coding Plan (coding.dashscope)\" }\n \"llm.provider.selected_qwen\" = @{ zh = \" 提供商: 阿里云百炼\"; en = \" Provider: Alibaba Cloud Bailian\" }\n \"llm.provider.selected_openai\" = @{ zh = \" 提供商: {0}(OpenAI 兼容)\"; en = \" Provider: {0} (OpenAI-compatible)\" }\n \"llm.provider.invalid\" = @{ zh = \"无效选择: {0}(请输入 1 或 2)\"; en = \"Invalid choice: {0} (please enter 1 or 2)\" }\n \"llm.qwen.model_prompt\" = @{ zh = \"默认模型 ID\"; en = \"Default Model ID\" }\n \"llm.openai.base_url_prompt\" = @{ zh = \"Base URL\"; en = \"Base URL\" }\n \"llm.openai.model_prompt\" = @{ zh = \"默认模型 ID\"; en = \"Default Model ID\" }\n \"llm.openai.base_url_label\" = @{ zh = \" Base URL: {0}\"; en = \" Base URL: {0}\" }\n\n # --- Custom model parameters ---\n \"llm.custom_model.detected\" = @{ zh = \" ⚠️ 模型 '{0}' 不在内置模型列表中,请配置模型参数:\"; en = \" ⚠️ Model '{0}' is not in the built-in model list. Please configure model parameters:\" }\n \"llm.custom_model.context_prompt\" = @{ zh = \"最大上下文长度(token 数)[150000]\"; en = \"Max context window (tokens) [150000]\" }\n \"llm.custom_model.max_tokens_prompt\" = @{ zh = \"最大输出长度(token 数)[128000]\"; en = \"Max output tokens [128000]\" }\n \"llm.custom_model.reasoning_prompt\" = @{ zh = \"是否支持推理/思考模式?[Y/n]\"; en = \"Does it support reasoning/thinking mode? [Y/n]\" }\n \"llm.custom_model.vision_prompt\" = @{ zh = \"是否支持图片输入?[y/N]\"; en = \"Does it support image input? [y/N]\" }\n \"llm.custom_model.summary\" = @{ zh = \" 自定义模型参数: 上下文={0}, 最大输出={1}, 推理={2}, 图片={3}\"; en = \" Custom model params: context={0}, maxTokens={1}, reasoning={2}, vision={3}\" }\n\n # --- Admin Credentials ---\n \"admin.title\" = @{ zh = \"--- 管理员凭据 ---\"; en = \"--- Admin Credentials ---\" }\n \"admin.username_prompt\" = @{ zh = \"管理员用户名\"; en = \"Admin Username\" }\n \"admin.password_prompt\" = @{ zh = \"管理员密码(留空自动生成,最少 8 位)\"; en = \"Admin Password (leave empty to auto-generate, min 8 chars)\" }\n \"admin.password_generated\" = @{ zh = \" 已自动生成管理员密码\"; en = \" Auto-generated admin password\" }\n \"admin.password_too_short\" = @{ zh = \"管理员密码至少需要 8 个字符(MinIO 要求)。当前长度: {0}\"; en = \"Admin password must be at least 8 characters (MinIO requirement). Current length: {0}\" }\n\n # --- Port Configuration ---\n \"port.title\" = @{ zh = \"--- 端口配置(按回车使用默认值)---\"; en = \"--- Port Configuration (press Enter for defaults) ---\" }\n \"port.gateway_prompt\" = @{ zh = \"网关主机端口(容器内 8080)\"; en = \"Host port for gateway (8080 inside container)\" }\n \"port.console_prompt\" = @{ zh = \"Higress 控制台主机端口(容器内 8001)\"; en = \"Host port for Higress console (8001 inside container)\" }\n \"port.element_prompt\" = @{ zh = \"Element Web 直接访问主机端口(容器内 8088)\"; en = \"Host port for Element Web direct access (8088 inside container)\" }\n \"port.manager_console_prompt\" = @{ zh = \"Manager 控制台主机端口(容器内 18888)\"; en = \"Host port for Manager console (18888 inside container)\" }\n \"port.local_only.title\" = @{ zh = \"--- 网络访问模式 ---\"; en = \"--- Network Access Mode ---\" }\n \"port.local_only.hint_yes\" = @{ zh = \" 仅本机使用,无需开放外部端口(推荐)\"; en = \" Local use only, no external port exposure (recommended)\" }\n \"port.local_only.hint_no\" = @{ zh = \" 允许外部访问(局域网 / 公网)\"; en = \" Allow external access (LAN / public network)\" }\n \"port.local_only.choice\" = @{ zh = \"请选择 [1/2]\"; en = \"Enter choice [1/2]\" }\n \"port.local_only.selected_local\" = @{ zh = \"端口已绑定到 127.0.0.1(仅本机访问)\"; en = \"Ports bound to 127.0.0.1 (localhost only)\" }\n \"port.local_only.selected_external\" = @{ zh = \"端口已绑定到所有网络接口(0.0.0.0)\"; en = \"Ports bound to all interfaces (0.0.0.0)\" }\n \"port.local_only.https_hint\" = @{ zh = \"警告: 建议在 Higress 控制台配置 TLS 证书并启用 HTTPS,避免明文传输。\"; en = \"WARNING: It is recommended to configure TLS certificates and enable HTTPS in the Higress Console to avoid plaintext transmission.\" }\n \"port.local_only.https_docs\" = @{ zh = \"\"; en = \"\" }\n\n # --- Domain Configuration ---\n \"domain.title\" = @{ zh = \"--- 域名配置(按回车使用默认值)---\"; en = \"--- Domain Configuration (press Enter for defaults) ---\" }\n \"domain.hint\" = @{ zh = \"提示: 自定义域名前必须事先做好 DNS 解析。单机 ECS 部署时无需修改 aigw、fs 等域名;Element Web 和 Matrix Server 也可通过 IP 直接访问。\"; en = \"Hint: Configure DNS resolution before customizing domains. For single ECS deployment, no need to change aigw, fs, etc.; Element Web and Matrix Server can also be accessed directly via IP.\" }\n \"domain.matrix_prompt\" = @{ zh = \"Matrix 域名\"; en = \"Matrix Domain\" }\n \"domain.element_prompt\" = @{ zh = \"Element Web 域名\"; en = \"Element Web Domain\" }\n \"domain.gateway_prompt\" = @{ zh = \"AI 网关域名\"; en = \"AI Gateway Domain\" }\n \"domain.fs_prompt\" = @{ zh = \"文件系统域名\"; en = \"File System Domain\" }\n \"domain.console_prompt\" = @{ zh = \"Manager 控制台域名\"; en = \"Manager Console Domain\" }\n\n # --- GitHub Integration ---\n \"github.title\" = @{ zh = \"--- GitHub 集成(可选,按回车跳过)---\"; en = \"--- GitHub Integration (optional, press Enter to skip) ---\" }\n \"github.token_prompt\" = @{ zh = \"GitHub 个人访问令牌(可选)\"; en = \"GitHub Personal Access Token (optional)\" }\n\n # --- Skills Registry ---\n \"skills.title\" = @{ zh = \"--- Skills 注册中心(可选,按回车使用默认 nacos://market.agentteams.io:80/public)---\"; en = \"--- Skills Registry (optional, press Enter for default nacos://market.agentteams.io:80/public) ---\" }\n \"skills.url_prompt\" = @{ zh = \"Skills 注册中心 URL(留空使用默认 nacos://market.agentteams.io:80/public)\"; en = \"Skills Registry URL (leave empty for default nacos://market.agentteams.io:80/public)\" }\n\n # --- Data Persistence ---\n \"data.title\" = @{ zh = \"--- 数据持久化 ---\"; en = \"--- Data Persistence ---\" }\n \"data.volume_prompt\" = @{ zh = \"Docker 卷名称 [agentteams-data]\"; en = \"Docker volume name for persistent data [agentteams-data]\" }\n \"data.volume_using\" = @{ zh = \" 使用 Docker 卷: {0}\"; en = \" Using Docker volume: {0}\" }\n\n # --- Manager Workspace ---\n \"workspace.title\" = @{ zh = \"--- Manager 工作空间 ---\"; en = \"--- Manager Workspace ---\" }\n \"workspace.dir_prompt\" = @{ zh = \"Manager 工作空间目录 [{0}]\"; en = \"Manager workspace directory [{0}]\" }\n \"workspace.dir_label\" = @{ zh = \" Manager 工作空间: {0}\"; en = \" Manager workspace: {0}\" }\n\n # --- Host directory sharing ---\n \"host_share.prompt\" = @{ zh = \"与 Agent 共享的主机目录(默认: {0})\"; en = \"Host directory to share with agents (default: {0})\" }\n \"host_share.sharing\" = @{ zh = \"共享主机目录: {0} -> 容器内 /host-share\"; en = \"Sharing host directory: {0} -> /host-share in container\" }\n \"host_share.not_exist\" = @{ zh = \"警告: 主机目录 {0} 不存在,跳过验证继续使用\"; en = \"WARNING: Host directory {0} does not exist, using without validation\" }\n\n # --- Default worker runtime ---\n \"worker_runtime.title\" = @{ zh = \"--- 默认 Worker 运行时 ---\"; en = \"--- Default Worker Runtime ---\" }\n \"worker_runtime.openclaw\" = @{ zh = \"OpenClaw\"; en = \"OpenClaw\" }\n \"worker_runtime.copaw\" = @{ zh = \"QwenPaw\"; en = \"QwenPaw\" }\n \"worker_runtime.hermes\" = @{ zh = \"Hermes\"; en = \"Hermes\" }\n \"worker_runtime.choice\" = @{ zh = \"请选择 [1/2/3]\"; en = \"Enter choice [1/2/3]\" }\n \"worker_runtime.selected\" = @{ zh = \"默认 Worker 运行时: {0}\"; en = \"Default Worker runtime: {0}\" }\n \"worker_runtime.title_short\" = @{ zh = \"默认 Worker 运行时\"; en = \"Default Worker Runtime\" }\n\n # --- Manager runtime ---\n \"manager_runtime.title\" = @{ zh = \"--- Manager 运行时 ---\"; en = \"--- Manager Runtime ---\" }\n \"manager_runtime.openclaw\" = @{ zh = \"OpenClaw\"; en = \"OpenClaw\" }\n \"manager_runtime.copaw\" = @{ zh = \"QwenPaw\"; en = \"QwenPaw\" }\n \"manager_runtime.choice\" = @{ zh = \"请选择 [1/2]\"; en = \"Enter choice [1/2]\" }\n \"manager_runtime.selected\" = @{ zh = \"Manager 运行时: {0}\"; en = \"Manager runtime: {0}\" }\n \"manager_runtime.title_short\" = @{ zh = \"Manager 运行时\"; en = \"Manager Runtime\" }\n\n # --- Matrix E2EE ---\n \"matrix_e2ee.title\" = @{ zh = \"--- Matrix 端到端加密(E2EE)---\"; en = \"--- Matrix End-to-End Encryption (E2EE) ---\" }\n \"matrix_e2ee.desc\" = @{\n zh = \"E2EE 会对 Manager 与 Worker 之间的 Matrix 消息进行端到端加密。`n 启用后,即使 Matrix 服务器被入侵,消息内容也无法被窃取。`n 但 E2EE 会增加首次握手耗时,且要求所有 Agent 都支持 matrix-sdk-crypto。`n 如果不确定,建议保持禁用。`n ⚠ 注意:禁用 E2EE 后,请勿在 Element 上创建默认启用加密的 Private 房间,`n 否则 Agent 将无法读取该房间中的加密消息。请改用 Public 房间或关闭房间加密。\"\n en = \"E2EE encrypts Matrix messages between Manager and Workers end-to-end.`n When enabled, message content stays private even if the Matrix server is compromised.`n However, E2EE adds overhead to the initial handshake and requires all Agents`n to support matrix-sdk-crypto. If unsure, keep it disabled.`n ⚠ Note: When E2EE is disabled, do NOT create Private rooms in Element (which`n enable encryption by default) — Agents cannot read encrypted messages without`n E2EE support. Use Public rooms or turn off room encryption instead.\"\n }\n \"matrix_e2ee.enable\" = @{ zh = \"启用 E2EE\"; en = \"Enable E2EE\" }\n \"matrix_e2ee.disable\" = @{ zh = \"禁用 E2EE(推荐)\"; en = \"Disable E2EE (recommended)\" }\n \"matrix_e2ee.choice\" = @{ zh = \"请选择 [1/2]\"; en = \"Enter choice [1/2]\" }\n \"matrix_e2ee.selected_enabled\" = @{ zh = \"Matrix E2EE: 已启用\"; en = \"Matrix E2EE: enabled\" }\n \"matrix_e2ee.selected_disabled\" = @{ zh = \"Matrix E2EE: 已禁用(默认)\"; en = \"Matrix E2EE: disabled (default)\" }\n \"matrix_e2ee.title_short\" = @{ zh = \"Matrix E2EE\"; en = \"Matrix E2EE\" }\n \"matrix_e2ee.val_enabled\" = @{ zh = \"已启用\"; en = \"enabled\" }\n \"matrix_e2ee.val_disabled\" = @{ zh = \"已禁用\"; en = \"disabled\" }\n\n # --- Docker API proxy ---\n \"docker_proxy.title\" = @{ zh = \"--- Docker API 安全代理 ---\"; en = \"--- Docker API Security Proxy ---\" }\n \"docker_proxy.desc\" = @{ zh = \"Docker API 代理可防止 AI Agent 通过 Docker API 越狱访问宿主机。`n 启用后,Manager 不再直接持有 Docker socket,所有容器操作经过安全校验。\"; en = \"Docker API proxy prevents AI Agents from escaping via Docker API to access the host.`n When enabled, Manager no longer has direct Docker socket access; all container operations go through security validation.\" }\n \"docker_proxy.enable\" = @{ zh = \"启用(推荐)\"; en = \"Enable (recommended)\" }\n \"docker_proxy.disable\" = @{ zh = \"禁用(直接挂载 Docker socket)\"; en = \"Disable (mount Docker socket directly)\" }\n \"docker_proxy.choice\" = @{ zh = \"请选择 [1/2]\"; en = \"Enter choice [1/2]\" }\n \"docker_proxy.selected_enabled\" = @{ zh = \"Docker API 代理: 已启用\"; en = \"Docker API proxy: enabled\" }\n \"docker_proxy.selected_disabled\" = @{ zh = \"Docker API 代理: 已禁用\"; en = \"Docker API proxy: disabled\" }\n \"docker_proxy.title_short\" = @{ zh = \"Docker API 代理\"; en = \"Docker API Proxy\" }\n \"docker_proxy.val_enabled\" = @{ zh = \"已启用\"; en = \"enabled\" }\n \"docker_proxy.val_disabled\" = @{ zh = \"已禁用\"; en = \"disabled\" }\n \"docker_proxy.registries_desc\" = @{ zh = \"默认放行的镜像来源:本地镜像、localhost、Higress 仓库(所有 region)。`n 如需放行其他镜像仓库,请输入逗号分隔的地址前缀。`n 示例: ghcr.io/myorg,registry.example.com/team\"; en = \"Default allowed image sources: local images, localhost, Higress registries (all regions).`n To allow additional image sources, enter comma-separated address prefixes.`n Example: ghcr.io/myorg,registry.example.com/team\" }\n \"docker_proxy.registries_prompt\" = @{ zh = \"额外放行的镜像来源(按回车跳过)\"; en = \"Additional allowed image sources (press Enter to skip)\" }\n \"docker_proxy.registries_label\" = @{ zh = \"额外放行的镜像来源\"; en = \"Additional allowed image sources\" }\n\n # --- Worker idle timeout ---\n \"idle_timeout.prompt\" = @{ zh = \"Worker 空闲自动停止超时(分钟)[720]\"; en = \"Worker idle auto-stop timeout in minutes [720]\" }\n \"idle_timeout.selected\" = @{ zh = \"Worker 空闲超时: {0} 分钟\"; en = \"Worker idle timeout: {0} minutes\" }\n \"idle_timeout.label\" = @{ zh = \"Worker 空闲超时(分钟)\"; en = \"Worker idle timeout (min)\" }\n\n # --- Secrets and config ---\n \"install.generating_secrets\" = @{ zh = \"正在生成密钥...\"; en = \"Generating secrets...\" }\n \"install.config_saved\" = @{ zh = \"配置已保存到 {0}\"; en = \"Configuration saved to {0}\" }\n\n # --- Container runtime socket ---\n \"install.socket_detected\" = @{ zh = \"容器运行时 socket: {0}(已启用直接创建 Worker)\"; en = \"Container runtime socket: {0} (direct Worker creation enabled)\" }\n \"install.socket_not_found\" = @{ zh = \"未找到容器运行时 socket(Manager 无法直接创建 Worker 容器,需要你手动执行 docker 命令创建)\"; en = \"No container runtime socket found (Manager cannot create Worker containers directly, you will need to create them manually using docker commands)\" }\n \"install.socket_confirm.title\" = @{ zh = \"警告: 未检测到容器运行时 Socket\"; en = \"WARNING: Container Runtime Socket Not Detected\" }\n \"install.socket_confirm.message\" = @{ zh = \"未找到 Docker/Podman socket,Manager 将无法自动创建 Worker 容器。`n你需要手动执行 docker run 命令来创建 Worker。`n`n是否继续安装?\"; en = \"Docker/Podman socket not found. Manager will not be able to create Worker containers automatically.`nYou will need to manually run docker commands to create Workers.`n`nContinue installation?\" }\n \"install.socket_confirm.prompt\" = @{ zh = \"继续安装? [y/N]: \"; en = \"Continue? [y/N]: \" }\n \"install.socket_confirm.cancelled\" = @{ zh = \"安装已取消。如需启用 Worker 自动创建,请确保 Docker/Podman 正在运行,然后重新运行安装脚本。\"; en = \"Installation cancelled. To enable automatic Worker creation, ensure Docker/Podman is running and re-run the installer.\" }\n\n # --- Container management ---\n \"install.removing_existing\" = @{ zh = \"正在移除现有 agentteams-manager 容器...\"; en = \"Removing existing agentteams-manager container...\" }\n\n # --- YOLO mode ---\n \"install.yolo\" = @{ zh = \"YOLO 模式已启用(自主决策,无交互提示)\"; en = \"YOLO mode enabled (autonomous decisions, no interactive prompts)\" }\n\n # --- Image pulling ---\n \"install.image.exists\" = @{ zh = \"Manager 镜像已存在: {0}\"; en = \"Manager image already exists locally: {0}\" }\n \"install.image.pulling_manager\" = @{ zh = \"正在拉取 Manager 镜像: {0}\"; en = \"Pulling Manager image: {0}\" }\n \"install.image.worker_exists\" = @{ zh = \"Worker 镜像已存在: {0}\"; en = \"Worker image already exists locally: {0}\" }\n \"install.image.pulling_worker\" = @{ zh = \"正在拉取 Worker 镜像: {0}\"; en = \"Pulling Worker image: {0}\" }\n\n # --- Starting container ---\n \"install.starting_manager\" = @{ zh = \"正在启动 Manager 容器...\"; en = \"Starting Manager container...\" }\n\n # --- Wait for Manager ready ---\n \"install.wait_ready\" = @{ zh = \"等待 Manager Agent 就绪(超时: {0}s)...\"; en = \"Waiting for Manager agent to be ready (timeout: {0}s)...\" }\n \"install.wait_ready.ok\" = @{ zh = \"Manager Agent 已就绪!\"; en = \"Manager agent is ready!\" }\n \"install.wait_ready.waiting\" = @{ zh = \"等待中... ({0}s/{1}s)\"; en = \"Waiting... ({0}s/{1}s)\" }\n \"install.wait_ready.timeout\" = @{ zh = \"Manager Agent 在 {0}s 内未就绪。请检查: docker logs {1}\"; en = \"Manager agent did not become ready within {0}s. Check: docker logs {1}\" }\n\n # --- Wait for Matrix ready ---\n \"install.wait_matrix\" = @{ zh = \"等待 Matrix 服务就绪(超时: {0}s)...\"; en = \"Waiting for Matrix server to be ready (timeout: {0}s)...\" }\n \"install.wait_matrix.ok\" = @{ zh = \"Matrix 服务已就绪!\"; en = \"Matrix server is ready!\" }\n \"install.wait_matrix.waiting\" = @{ zh = \"等待 Matrix 中... ({0}s/{1}s)\"; en = \"Waiting for Matrix... ({0}s/{1}s)\" }\n \"install.wait_matrix.timeout\" = @{ zh = \"Matrix 服务在 {0}s 内未就绪。请检查: docker logs {1}\"; en = \"Matrix server did not become ready within {0}s. Check: docker logs {1}\" }\n\n # --- OpenAI-compatible connectivity test ---\n \"llm.openai.test.testing\" = @{ zh = \"正在测试 API 联通性...\"; en = \"Testing API connectivity...\" }\n \"llm.openai.test.ok\" = @{ zh = \"API 联通性测试通过\"; en = \"API connectivity test passed\" }\n \"llm.openai.test.fail\" = @{ zh = \"API 联通性测试失败(HTTP {0})。响应内容:`n{1}`n请根据以上错误信息联系您的模型服务商解决。\"; en = \"API connectivity test failed (HTTP {0}). Response body:`n{1}`nPlease contact your model provider to resolve the issue.\" }\n \"llm.openai.test.fail.tokenplan\" = @{ zh = \"提示: 请确认 API Key 有效且已开通通义 Token 套餐。文档: https://help.aliyun.com/zh/model-studio/token-plan-quickstart\"; en = \"Hint: Verify your Token Plan API key and compatible-mode access. Docs: https://help.aliyun.com/zh/model-studio/token-plan-quickstart\" }\n \"llm.openai.test.fail.codingplan\" = @{ zh = \"提示: 请确认 API Key 有效且已开通通义 Token 套餐。文档: https://help.aliyun.com/zh/model-studio/token-plan-quickstart\"; en = \"Hint: Verify your DASHSCOPE_API_KEY for Qwen Cloud. API keys: https://home.qwencloud.com/api-keys Docs: https://docs.qwencloud.com/\" }\n \"llm.openai.test.fail.codingplan_legacy\" = @{ zh = \"提示: 请确认 API Key 有效且 Coding 套餐接口可用。文档: https://help.aliyun.com/zh/model-studio/get-api-key\"; en = \"Hint: Verify your DashScope API key and Coding Plan access. Docs: https://help.aliyun.com/zh/model-studio/get-api-key\" }\n \"llm.openai.test.confirm\" = @{ zh = \"是否仍要继续安装?[y/N/b]\"; en = \"Continue with installation anyway? [y/N/b]\" }\n \"llm.openai.test.aborted\" = @{ zh = \"安装已中止。\"; en = \"Installation aborted.\" }\n \"llm.embedding.title\" = @{ zh = \"📦 记忆搜索配置\"; en = \"📦 Memory Search Configuration\" }\n \"llm.embedding.hint\" = @{ zh = \" Embedding 模型可提升记忆搜索质量(语义匹配)。不启用也可正常使用记忆功能(关键词匹配)。\"; en = \" Embedding model improves memory search quality (semantic matching). Memory still works without it (keyword matching).\" }\n \"llm.embedding.option.default\" = @{ zh = \" 1) text-embedding-v4(推荐)\"; en = \" 1) text-embedding-v4 (Recommended)\" }\n \"llm.embedding.option.custom\" = @{ zh = \" 2) 自定义 Embedding 模型\"; en = \" 2) Custom embedding model\" }\n \"llm.embedding.option.disable\" = @{ zh = \" 3) 不启用\"; en = \" 3) Do not enable\" }\n \"llm.embedding.select\" = @{ zh = \"选择\"; en = \"Select\" }\n \"llm.embedding.custom_prompt\" = @{ zh = \" Embedding 模型名称\"; en = \" Embedding model name\" }\n \"llm.embedding.test.testing\" = @{ zh = \"正在测试 Embedding API 联通性...\"; en = \"Testing Embedding API connectivity...\" }\n \"llm.embedding.test.ok\" = @{ zh = \"✅ Embedding API 联通性测试通过\"; en = \"✅ Embedding API connectivity test passed\" }\n \"llm.embedding.test.fail\" = @{ zh = \"⚠️ Embedding API 测试失败(HTTP {0})。响应: {1}\"; en = \"⚠️ Embedding API test failed (HTTP {0}). Response: {1}\" }\n \"llm.embedding.auto_disabled\" = @{ zh = \"⚠️ Embedding 已自动禁用,记忆搜索将使用关键词匹配。您可以稍后在 agentteams-manager.env 中设置 AGENTTEAMS_EMBEDDING_MODEL 启用。\"; en = \"⚠️ Embedding auto-disabled. Memory search will use keyword matching. You can enable it later in agentteams-manager.env by setting AGENTTEAMS_EMBEDDING_MODEL.\" }\n \"llm.embedding.disabled\" = @{ zh = \"ℹ️ Embedding 已禁用,记忆搜索将使用关键词匹配。\"; en = \"ℹ️ Embedding disabled. Memory search will use keyword matching.\" }\n \"nav.back_hint\" = @{ zh = \"(输入 b 返回上一步)\"; en = \"(enter b to go back)\" }\n # --- OpenAI-compatible provider creation ---\n \"install.openai_compat.missing\" = @{ zh = \"警告: OpenAI Base URL 或 API Key 未设置,跳过提供商创建\"; en = \"WARNING: OpenAI Base URL or API Key not set, skipping provider creation\" }\n \"install.openai_compat.creating\" = @{ zh = \"正在创建 OpenAI 兼容提供商...\"; en = \"Creating OpenAI-compatible provider...\" }\n \"install.openai_compat.domain\" = @{ zh = \" 域名: {0}\"; en = \" Domain: {0}\" }\n \"install.openai_compat.port\" = @{ zh = \" 端口: {0}\"; en = \" Port: {0}\" }\n \"install.openai_compat.protocol\" = @{ zh = \" 协议: {0}\"; en = \" Protocol: {0}\" }\n \"install.openai_compat.service_fail\" = @{ zh = \"警告: 创建 DNS 服务源失败(可能已存在)\"; en = \"WARNING: Failed to create DNS service source (may already exist)\" }\n \"install.openai_compat.provider_fail\" = @{ zh = \"警告: 创建 AI 提供商失败(可能已存在)\"; en = \"WARNING: Failed to create AI provider (may already exist)\" }\n \"install.openai_compat.success\" = @{ zh = \"OpenAI 兼容提供商创建成功\"; en = \"OpenAI-compatible provider created successfully\" }\n\n # --- Welcome message ---\n \"install.welcome_msg.soul_configured\" = @{ zh = \"Soul 已配置(找到 soul-configured 标记),跳过 onboarding 消息\"; en = \"Soul already configured (soul-configured marker found), skipping onboarding message\" }\n \"install.welcome_msg.logging_in\" = @{ zh = \"正在以 {0} 身份登录以发送欢迎消息...\"; en = \"Logging in as {0} to send welcome message...\" }\n \"install.welcome_msg.login_failed\" = @{ zh = \"警告: 以 {0} 身份登录失败,跳过欢迎消息\"; en = \"WARNING: Failed to login as {0}, skipping welcome message\" }\n \"install.welcome_msg.finding_room\" = @{ zh = \"正在查找与 Manager 的 DM 房间...\"; en = \"Finding DM room with Manager...\" }\n \"install.welcome_msg.creating_room\" = @{ zh = \"正在创建与 Manager 的 DM 房间...\"; en = \"Creating DM room with Manager...\" }\n \"install.welcome_msg.no_room\" = @{ zh = \"警告: 无法找到或创建与 Manager 的 DM 房间\"; en = \"WARNING: Could not find or create DM room with Manager\" }\n \"install.welcome_msg.waiting_join\" = @{ zh = \"等待 Manager 加入房间...\"; en = \"Waiting for Manager to join the room...\" }\n \"install.welcome_msg.sending\" = @{ zh = \"正在向 Manager 发送欢迎消息...\"; en = \"Sending welcome message to Manager...\" }\n \"install.welcome_msg.send_failed\" = @{ zh = \"警告: 发送欢迎消息失败\"; en = \"WARNING: Failed to send welcome message\" }\n \"install.welcome_msg.sent\" = @{ zh = \"欢迎消息已发送给 Manager\"; en = \"Welcome message sent to Manager\" }\n \"install.welcome_msg.waiting\" = @{ zh = \"等待 Manager 发送欢迎消息(Higress 路由授权 + LLM 探活,约 45-90s)...\"; en = \"Waiting for Manager to send the welcome message (Higress route auth + LLM probe, ~45-90s)...\" }\n \"install.welcome_msg.confirmed\" = @{ zh = \"Manager 已确认发送欢迎消息(status.welcomeSent=true,用时 {0}s)\"; en = \"Manager confirmed welcome message sent (status.welcomeSent=true, {0}s elapsed)\" }\n \"install.welcome_msg.timeout\" = @{ zh = \"警告: 在 {0}s 内未观察到 Manager 发送欢迎消息(status.welcomeSent=true)。安装仍然成功,所有服务已就绪——可继续按下方提示登录 Element Web。\"; en = \"WARNING: Did not observe the Manager sending its welcome message (status.welcomeSent=true) within {0}s. Installation is still successful, all services are up — continue with the Element Web instructions below.\" }\n \"install.welcome_msg.timeout_hint\" = @{ zh = \"手动触发 onboarding: 登录 Element Web → 打开与 Manager 的 DM 房间 → 发送任意一句话(例如 `\"hi`\"),Manager 会接管对话并开始引导。\"; en = \"Manual onboarding: log in to Element Web -> open the DM with the Manager -> send any message (e.g. `\"hi`\") and the Manager will take over and start the guided setup.\" }\n \"install.welcome_msg.timeout_inspect\" = @{ zh = \"排查命令: docker exec agentteams-controller agt get managers default\"; en = \"Inspect status: docker exec agentteams-controller agt get managers default\" }\n \"install.welcome_msg.poll_unavailable\" = @{ zh = \"提示: agentteams-manager 内未找到 AgentTeams CLI,跳过 welcome 等待(旧镜像?)\"; en = \"Note: AgentTeams CLI not found inside agentteams-manager; skipping welcome wait (old image?)\" }\n\n # --- Final output panel ---\n \"success.title\" = @{ zh = \"=== AgentTeams Manager 已启动!===\"; en = \"=== AgentTeams Manager Started! ===\" }\n \"success.domains_configured\" = @{ zh = \"以下域名已配置解析到 127.0.0.1:\"; en = \"The following domains are configured to resolve to 127.0.0.1:\" }\n \"success.open_url\" = @{ zh = \" 在浏览器中打开以下 URL 开始使用: \"; en = \" Open the following URL in your browser to start: \" }\n \"success.login_with\" = @{ zh = \" 登录信息:\"; en = \" Login with:\" }\n \"success.username\" = @{ zh = \" 用户名: {0}\"; en = \" Username: {0}\" }\n \"success.password\" = @{ zh = \" 密码: {0}\"; en = \" Password: {0}\" }\n \"success.after_login\" = @{ zh = \" 登录后,开始与 Manager 聊天!\"; en = \" After login, start chatting with the Manager!\" }\n \"success.tell_it\" = @{ zh = \" 告诉它: `\"创建一个名为 alice 的前端开发 Worker`\"\"; en = \" Tell it: `\"Create a Worker named alice for frontend dev`\"\" }\n \"success.manager_auto\" = @{ zh = \" Manager 会自动处理一切。\"; en = \" The Manager will handle everything automatically.\" }\n \"success.mobile_title\" = @{ zh = \" 移动端访问(FluffyChat / Element Mobile):\"; en = \" Mobile access (FluffyChat / Element Mobile):\" }\n \"success.mobile_step1\" = @{ zh = \" 1. 在手机上下载 FluffyChat 或 Element\"; en = \" 1. Download FluffyChat or Element on your phone\" }\n \"success.mobile_step2\" = @{ zh = \" 2. 设置 homeserver 为: {0}\"; en = \" 2. Set homeserver to: {0}\" }\n \"success.mobile_step2_noip\" = @{ zh = \" 2. 设置 homeserver 为: http://<本机局域网IP>:{0}\"; en = \" 2. Set homeserver to: http://<this-machine-LAN-IP>:{0}\" }\n \"success.mobile_noip_hint\" = @{ zh = \" (无法自动检测局域网 IP — 请使用 ifconfig / ip addr 查看)\"; en = \" (Could not detect LAN IP automatically — check with: ifconfig / ip addr)\" }\n \"success.mobile_step3\" = @{ zh = \" 3. 登录信息:\"; en = \" 3. Login with:\" }\n \"success.mobile_username\" = @{ zh = \" 用户名: {0}\"; en = \" Username: {0}\" }\n \"success.mobile_password\" = @{ zh = \" 密码: {0}\"; en = \" Password: {0}\" }\n\n # --- Other consoles and tips ---\n \"success.other_consoles\" = @{ zh = \"--- 其他控制台 ---\"; en = \"--- Other Consoles ---\" }\n \"success.higress_console\" = @{ zh = \" Higress 控制台: http://localhost:{0}(用户名: {1} / 密码: {2})\"; en = \" Higress Console: http://localhost:{0} (Username: {1} / Password: {2})\" }\n \"success.manager_console\" = @{ zh = \" Manager 控制台(本地): http://localhost:{0}(无需登录)\"; en = \" Manager Console (local): http://localhost:{0} (no login required)\" }\n \"success.manager_console_gateway\" = @{ zh = \" Manager 控制台(网关): http://console-local.agentteams.io(用户名: {0} / 密码: {1})\"; en = \" Manager Console (gateway): http://console-local.agentteams.io (Username: {0} / Password: {1})\" }\n \"success.copaw_console\" = @{ zh = \" QwenPaw 控制台(本地): http://localhost:{0}(无需登录)\"; en = \" QwenPaw Console (local): http://localhost:{0} (no login required)\" }\n \"success.switch_llm.title\" = @{ zh = \"--- 切换 LLM 提供商 ---\"; en = \"--- Switch LLM Providers ---\" }\n \"success.switch_llm.hint\" = @{ zh = \" 您可以通过 Higress 控制台切换到其他 LLM 提供商(OpenAI、Anthropic 等)。\"; en = \" You can switch to other LLM providers (OpenAI, Anthropic, etc.) via Higress Console.\" }\n \"success.switch_llm.docs\" = @{ zh = \" 详细说明请参阅:\"; en = \" For detailed instructions, see:\" }\n \"success.switch_llm.url\" = @{ zh = \" https://higress.ai/en/docs/ai/scene-guide/multi-proxy#console-configuration\"; en = \" https://higress.ai/en/docs/ai/scene-guide/multi-proxy#console-configuration\" }\n \"success.tip\" = @{ zh = \"提示: 您也可以在聊天中让 Manager 为您配置 LLM 提供商。\"; en = \"Tip: You can also ask the Manager to configure LLM providers for you in the chat.\" }\n \"success.config_file\" = @{ zh = \"配置文件: {0}\"; en = \"Configuration file: {0}\" }\n \"success.data_volume\" = @{ zh = \"数据卷: {0}\"; en = \"Data volume: {0}\" }\n \"success.workspace\" = @{ zh = \"Manager 工作空间: {0}\"; en = \"Manager workspace: {0}\" }\n\n # --- Worker installation ---\n \"worker.resetting\" = @{ zh = \"正在重置 Worker: {0}...\"; en = \"Resetting Worker: {0}...\" }\n \"worker.exists\" = @{ zh = \"容器 '{0}' 已存在。使用 --reset 重新创建。\"; en = \"Container '{0}' already exists. Use --reset to recreate.\" }\n \"worker.starting\" = @{ zh = \"正在启动 Worker: {0}...\"; en = \"Starting Worker: {0}...\" }\n \"worker.skills_url\" = @{ zh = \" Skills API URL: {0}\"; en = \" Skills API URL: {0}\" }\n \"worker.started\" = @{ zh = \"=== Worker {0} 已启动!===\"; en = \"=== Worker {0} Started! ===\" }\n \"worker.container\" = @{ zh = \"容器: {0}\"; en = \"Container: {0}\" }\n \"worker.view_logs\" = @{ zh = \"查看日志: docker logs -f {0}\"; en = \"View logs: docker logs -f {0}\" }\n\n # --- Prompt function messages ---\n \"prompt.preset\" = @{ zh = \" {0} = (已通过环境变量预设)\"; en = \" {0} = (pre-set via env)\" }\n \"prompt.upgrade_keep\" = @{ zh = \" {0} = {1}(当前值,回车保留 / 输入新值覆盖)\"; en = \" {0} = {1} (current value, press Enter to keep / type new value to change)\" }\n \"prompt.upgrade_empty\" = @{ zh = \" {0} = (未设置,回车跳过 / 输入新值设置)\"; en = \" {0} = (not set, press Enter to skip / type new value to set)\" }\n \"prompt.default\" = @{ zh = \" {0} = {1}(默认)\"; en = \" {0} = {1} (default)\" }\n \"prompt.required\" = @{ zh = \"{0} 是必需的(在非交互模式下通过环境变量设置)\"; en = \"{0} is required (set via environment variable in non-interactive mode)\" }\n \"prompt.required_empty\" = @{ zh = \"{0} 是必需的\"; en = \"{0} is required\" }\n\n # --- Language switch prompt (bilingual by design) ---\n \"lang.detected.zh\" = @{ zh = \"检测到语言 / Detected language: 中文\"; en = \"检测到语言 / Detected language: 中文\" }\n \"lang.detected.en\" = @{ zh = \"检测到语言 / Detected language: English\"; en = \"检测到语言 / Detected language: English\" }\n \"lang.switch_title\" = @{ zh = \"切换语言 / Switch language:\"; en = \"切换语言 / Switch language:\" }\n \"lang.option_zh\" = @{ zh = \" 1) 中文\"; en = \" 1) 中文\" }\n \"lang.option_en\" = @{ zh = \" 2) English\"; en = \" 2) English\" }\n \"lang.prompt\" = @{ zh = \"请选择 / Enter choice\"; en = \"请选择 / Enter choice\" }\n\n # --- Error messages ---\n \"error.name_required\" = @{ zh = \"--name 是必需的\"; en = \"--name is required\" }\n \"error.fs_required\" = @{ zh = \"--fs 是必需的\"; en = \"--fs is required\" }\n \"error.fs_key_required\" = @{ zh = \"--fs-key 是必需的\"; en = \"--fs-key is required\" }\n \"error.fs_secret_required\" = @{ zh = \"--fs-secret 是必需的\"; en = \"--fs-secret is required\" }\n \"error.unknown_option\" = @{ zh = \"未知选项: {0}\"; en = \"Unknown option: {0}\" }\n \"error.docker_not_running\" = @{ zh = \"Docker 未运行。请先启动 Docker Desktop 或 Podman Desktop。\"; en = \"Docker is not running. Please start Docker Desktop or Podman Desktop first.\" }\n \"error.docker_not_found\" = @{ zh = \"未找到 docker 或 podman 命令。请先安装 Docker Desktop 或 Podman Desktop:`n Docker Desktop: https://www.docker.com/products/docker-desktop/`n Podman Desktop: https://podman-desktop.io/\"; en = \"docker or podman command not found. Please install Docker Desktop or Podman Desktop first:`n Docker Desktop: https://www.docker.com/products/docker-desktop/`n Podman Desktop: https://podman-desktop.io/\" }\n\n # --- Uninstall messages ---\n \"uninstall.title\" = @{ zh = \"正在卸载 AgentTeams...\"; en = \"Uninstalling AgentTeams...\" }\n \"uninstall.stopping_manager\" = @{ zh = \"正在停止并移除 agentteams-manager...\"; en = \"Stopping and removing agentteams-manager...\" }\n \"uninstall.stopping_workers\" = @{ zh = \"正在停止并移除 worker 容器...\"; en = \"Stopping and removing worker containers...\" }\n \"uninstall.removed\" = @{ zh = \" 已移除: {0}\"; en = \" Removed: {0}\" }\n \"uninstall.removing_volume\" = @{ zh = \"正在移除 Docker 卷: agentteams-data\"; en = \"Removing Docker volume: agentteams-data\" }\n \"uninstall.removing_env\" = @{ zh = \"正在移除 env 文件: {0}\"; en = \"Removing env file: {0}\" }\n \"uninstall.removing_proxy\" = @{ zh = \"正在停止并移除 Docker API 代理容器: agentteams-docker-proxy\"; en = \"Stopping and removing Docker API proxy container: agentteams-docker-proxy\" }\n \"uninstall.stopping_controller\" = @{ zh = \"正在停止并移除 agentteams-controller (内嵌 Tuwunel/MinIO/Higress)...\"; en = \"Stopping and removing agentteams-controller (embedded Tuwunel/MinIO/Higress)...\" }\n \"uninstall.removing_network\" = @{ zh = \"正在移除 Docker 网络: agentteams-net\"; en = \"Removing Docker network: agentteams-net\" }\n \"uninstall.removing_workspace\" = @{ zh = \"正在移除工作空间目录: {0}\"; en = \"Removing workspace directory: {0}\" }\n \"uninstall.removing_log\" = @{ zh = \"正在移除日志文件: {0}\"; en = \"Removing log file: {0}\" }\n \"uninstall.done\" = @{ zh = \"AgentTeams 已卸载。\"; en = \"AgentTeams has been uninstalled.\" }\n}\n\n# Get-Msg: look up message by key, with -f style argument substitution.\n# Falls back to English if the current language translation is missing.\nfunction Get-Msg {\n param(\n [Parameter(Mandatory)][string]$Key,\n [object[]]$f\n )\n $lang = $script:AGENTTEAMS_LANGUAGE\n if (-not $lang) { $lang = \"en\" }\n $entry = $script:Messages[$Key]\n if (-not $entry) { return $Key }\n $text = $entry[$lang]\n if (-not $text) { $text = $entry[\"en\"] }\n if (-not $text) { return $Key }\n if ($f) { return ($text -f $f) }\n return $text\n}\n\nfunction Get-LanIP {\n # Detect local LAN IP address on Windows\n try {\n # Get network adapters with IPv4 addresses, prefer connected/active interfaces\n $adapters = Get-NetIPAddress -AddressFamily IPv4 -ErrorAction SilentlyContinue |\n Where-Object {\n $_.IPAddress -notlike \"127.*\" -and\n $_.PrefixOrigin -ne \"WellKnown\" -and\n $_.InterfaceAlias -notlike \"*Loopback*\"\n } |\n Sort-Object {\n # PowerShell 5.1 compatible: use if-else instead of ternary operator\n if ($_.InterfaceAlias -like \"*Wi-Fi*\" -or $_.InterfaceAlias -like \"*Ethernet*\") { 0 } else { 1 }\n }\n\n if ($adapters) {\n return $adapters[0].IPAddress\n }\n\n # Fallback: use ipconfig\n $ipconfig = ipconfig 2>$null\n $ip = ($ipconfig | Select-String \"IPv4 Address.*?: (\\d+\\.\\d+\\.\\d+\\.\\d+)\" | Select-Object -First 1)\n if ($ip -match \"(\\d+\\.\\d+\\.\\d+\\.\\d+)\") {\n return $Matches[1]\n }\n }\n catch {\n # Ignore errors\n }\n\n return \"\"\n}\n\n# Known models list — used to detect custom models during install\n$script:KnownModels = @(\n \"gpt-5.4\", \"gpt-5.3-codex\", \"gpt-5-mini\", \"gpt-5-nano\",\n \"claude-opus-4-6\", \"claude-sonnet-4-6\", \"claude-haiku-4-5\",\n \"qwen3.6-plus\", \"qwen3.5-plus\", \"deepseek-chat\", \"deepseek-reasoner\",\n \"kimi-k2.5\", \"glm-5\", \"MiniMax-M2.7\", \"MiniMax-M2.7-highspeed\", \"MiniMax-M2.5\"\n)\n\nfunction Test-KnownModel {\n param([string]$Model)\n return $script:KnownModels -contains $Model\n}\n\nfunction Request-CustomModelParams {\n param([string]$Model)\n if (Test-KnownModel $Model) {\n $script:config.MODEL_CONTEXT_WINDOW = \"\"\n $script:config.MODEL_MAX_TOKENS = \"\"\n $script:config.MODEL_REASONING = \"\"\n $script:config.MODEL_VISION = \"\"\n return\n }\n Write-Host \"\"\n Write-Log (Get-Msg \"llm.custom_model.detected\" -f $Model)\n Write-Host \"\"\n $ctxInput = Read-Host \" $(Get-Msg 'llm.custom_model.context_prompt')\"\n if ($ctxInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $script:config.MODEL_CONTEXT_WINDOW = if ($ctxInput) { $ctxInput } else { \"150000\" }\n $maxInput = Read-Host \" $(Get-Msg 'llm.custom_model.max_tokens_prompt')\"\n if ($maxInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $script:config.MODEL_MAX_TOKENS = if ($maxInput) { $maxInput } else { \"128000\" }\n $reasoningInput = Read-Host \" $(Get-Msg 'llm.custom_model.reasoning_prompt')\"\n if ($reasoningInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $script:config.MODEL_REASONING = if ($reasoningInput -match \"^[nN]\") { \"false\" } else { \"true\" }\n $visionInput = Read-Host \" $(Get-Msg 'llm.custom_model.vision_prompt')\"\n if ($visionInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $script:config.MODEL_VISION = if ($visionInput -match \"^[yY]\") { \"true\" } else { \"false\" }\n Write-Log (Get-Msg \"llm.custom_model.summary\" -f $script:config.MODEL_CONTEXT_WINDOW, $script:config.MODEL_MAX_TOKENS, $script:config.MODEL_REASONING, $script:config.MODEL_VISION)\n}\n\nfunction New-RandomKey {\n # Generate 64 character hex string (32 bytes)\n $bytes = New-Object byte[] 32\n $rng = [System.Security.Cryptography.RandomNumberGenerator]::Create()\n $rng.GetBytes($bytes)\n return [BitConverter]::ToString($bytes).Replace(\"-\", \"\").ToLower()\n}\n\nfunction ConvertTo-DockerPath {\n param([string]$Path)\n\n # Convert Windows path to Docker mount format\n $fullPath = (Resolve-Path $Path -ErrorAction SilentlyContinue).Path\n if (-not $fullPath) {\n $fullPath = $Path\n }\n\n # Convert C:\\path to /c/path format for Docker\n if ($fullPath -match \"^([A-Za-z]):\") {\n $drive = $Matches[1].ToLower()\n $rest = $fullPath.Substring(2).Replace(\"\\\", \"/\")\n return \"/$drive$rest\"\n }\n return $fullPath.Replace(\"\\\", \"/\")\n}\n\n# Resolve the embedded controller image. Embedded mode is the only supported\n# architecture since PR #616 (manager image no longer bundles Higress/Tuwunel/MinIO).\n# Mirrors install/agentteams-install.sh::resolve_embedded_image — fail fast when the\n# embedded image is unavailable rather than silently falling back to the broken\n# legacy single-container path.\n# Sets $script:EMBEDDED_IMAGE and $script:AGENTTEAMS_USE_EMBEDDED.\nfunction Resolve-EmbeddedImage {\n $script:AGENTTEAMS_USE_EMBEDDED = \"1\"\n\n # Explicit override always wins (used by `make install-embedded` for local builds).\n if ($env:AGENTTEAMS_INSTALL_EMBEDDED_IMAGE) {\n $script:EMBEDDED_IMAGE = $env:AGENTTEAMS_INSTALL_EMBEDDED_IMAGE\n return\n }\n\n $versioned = \"$($script:AGENTTEAMS_REGISTRY)/agentteams/agentteams-embedded:$($script:AGENTTEAMS_VERSION)\"\n $latestTag = \"$($script:AGENTTEAMS_REGISTRY)/agentteams/agentteams-embedded:latest\"\n\n if ($script:AGENTTEAMS_VERSION -eq \"latest\") {\n $script:EMBEDDED_IMAGE = $latestTag\n return\n }\n\n docker pull $versioned *>$null\n if ($LASTEXITCODE -eq 0) {\n $script:EMBEDDED_IMAGE = $versioned\n return\n }\n docker pull $latestTag *>$null\n if ($LASTEXITCODE -eq 0) {\n Write-Log \"embedded $($script:AGENTTEAMS_VERSION) not found, using latest\"\n $script:EMBEDDED_IMAGE = $latestTag\n return\n }\n\n # Escape hatch for older versions (AGENTTEAMS_VERSION <= v1.0.9) whose manager image\n # still bundled the infrastructure — opt-in only, never silent.\n if ($env:AGENTTEAMS_FORCE_LEGACY -eq \"1\") {\n Write-Log \"WARNING: AGENTTEAMS_FORCE_LEGACY=1 - using legacy all-in-one manager architecture.\"\n Write-Log \"WARNING: This requires AGENTTEAMS_VERSION <= v1.0.9 (older bundled manager image).\"\n Write-Log \"WARNING: Newer slim manager images will hang on 'Waiting for Higress Gateway'.\"\n $script:AGENTTEAMS_USE_EMBEDDED = \"0\"\n return\n }\n\n Write-Host \"$($script:ESC)[31m[AgentTeams ERROR]$($script:ESC)[0m Embedded controller image is not available in the registry:\" -ForegroundColor Red\n Write-Host \" - tried: $versioned\" -ForegroundColor Red\n Write-Host \" - tried: $latestTag\" -ForegroundColor Red\n Write-Host \"\" -ForegroundColor Red\n Write-Host \"Embedded mode is the only supported architecture since PR #616.\" -ForegroundColor Red\n Write-Host \"How to resolve:\" -ForegroundColor Red\n Write-Host \" 1) Pin to a AGENTTEAMS_VERSION whose embedded image has been published, or\" -ForegroundColor Red\n Write-Host \" wait for the release pipeline to publish it.\" -ForegroundColor Red\n Write-Host \" 2) For a local build, run: make install-embedded\" -ForegroundColor Red\n Write-Host \" (builds and uses the local embedded image without touching the registry).\" -ForegroundColor Red\n Write-Host \" 3) Override with a custom image:\" -ForegroundColor Red\n Write-Host \" `$env:AGENTTEAMS_INSTALL_EMBEDDED_IMAGE=...; .\\agentteams-install.ps1\" -ForegroundColor Red\n Exit-Script 1\n}\n\nfunction Wait-ManagerReady {\n param(\n [string]$Container = \"agentteams-manager\",\n [int]$Timeout = $(if ($env:AGENTTEAMS_READY_TIMEOUT) { [int]$env:AGENTTEAMS_READY_TIMEOUT } else { 300 })\n )\n\n $elapsed = 0\n Write-Log (Get-Msg \"install.wait_ready\" -f $Timeout)\n\n $runtime = if ($script:config.MANAGER_RUNTIME) { $script:config.MANAGER_RUNTIME } else { \"copaw\" }\n\n while ($elapsed -lt $Timeout) {\n try {\n switch ($runtime) {\n \"copaw\" {\n $result = docker exec $Container curl -sf http://127.0.0.1:18799/api/agents 2>$null\n if ($result -match '\"agents\"') {\n Write-Log (Get-Msg \"install.wait_ready.ok\")\n return $true\n }\n }\n default {\n $result = docker exec $Container openclaw gateway health --json 2>$null\n if ($result -match '\"ok\"') {\n Write-Log (Get-Msg \"install.wait_ready.ok\")\n return $true\n }\n }\n }\n } catch {\n # Ignore errors during polling\n }\n\n Start-Sleep -Seconds 5\n $elapsed += 5\n Write-Host \"`r$($script:ESC)[36m[AgentTeams]$($script:ESC)[0m $(Get-Msg 'install.wait_ready.waiting' -f $elapsed, $Timeout)\" -NoNewline\n }\n\n Write-Host \"\"\n Write-Error (Get-Msg \"install.wait_ready.timeout\" -f $Timeout, $Container)\n}\n\nfunction Wait-MatrixReady {\n param(\n [string]$Container = \"agentteams-manager\",\n [int]$Timeout = $(if ($env:AGENTTEAMS_READY_TIMEOUT) { [int]$env:AGENTTEAMS_READY_TIMEOUT } else { 300 })\n )\n\n $elapsed = 0\n Write-Log (Get-Msg \"install.wait_matrix\" -f $Timeout)\n\n while ($elapsed -lt $Timeout) {\n try {\n $result = docker exec $Container curl -sf http://127.0.0.1:6167/_tuwunel/server_version 2>$null\n if ($result) {\n Write-Log (Get-Msg \"install.wait_matrix.ok\")\n return $true\n }\n } catch {\n # Ignore errors during polling\n }\n\n Start-Sleep -Seconds 5\n $elapsed += 5\n Write-Host \"`r$($script:ESC)[36m[AgentTeams]$($script:ESC)[0m $(Get-Msg 'install.wait_matrix.waiting' -f $elapsed, $Timeout)\" -NoNewline\n }\n\n Write-Host \"\"\n Write-Error (Get-Msg \"install.wait_matrix.timeout\" -f $Timeout, $Container)\n}\n\n# Read KEY=value from /data/agentteams-secrets.env on a Docker volume (manager container not required).\n# Requires $script:EMBEDDED_IMAGE (set by Resolve-EmbeddedImage before Install-Manager uses this).\nfunction Read-AgentTeamsSecretFromDataVolume {\n param(\n [string]$VolumeName,\n [string]$Key\n )\n if ([string]::IsNullOrEmpty($VolumeName) -or [string]::IsNullOrEmpty($Key) -or [string]::IsNullOrEmpty($script:EMBEDDED_IMAGE)) {\n return \"\"\n }\n $grepKey = [regex]::Escape($Key)\n $shCmd = \"grep \"\"^${grepKey}=\"\" /data/agentteams-secrets.env 2>/dev/null | cut -d= -f2- | head -1 | tr -d '\\r'\"\n $out = docker run --rm --entrypoint sh -v \"${VolumeName}:/data:ro\" $script:EMBEDDED_IMAGE -c $shCmd 2>$null\n if ($null -eq $out) { return \"\" }\n return ($out | Out-String).Trim()\n}\n\n# Read KEY=value from /data/worker-creds/<worker>.env on a Docker volume.\nfunction Read-AgentTeamsWorkerCredsFromVolume {\n param(\n [string]$VolumeName,\n [string]$WorkerName,\n [string]$Key\n )\n if ([string]::IsNullOrEmpty($VolumeName) -or [string]::IsNullOrEmpty($WorkerName) -or [string]::IsNullOrEmpty($Key) -or [string]::IsNullOrEmpty($script:EMBEDDED_IMAGE)) {\n return \"\"\n }\n $grepKey = [regex]::Escape($Key)\n $path = \"/data/worker-creds/${WorkerName}.env\"\n $shCmd = \"grep \"\"^${grepKey}=\"\" \"\"${path}\"\" 2>/dev/null | cut -d= -f2- | head -1 | tr -d '\\r'\"\n $out = docker run --rm --entrypoint sh -v \"${VolumeName}:/data:ro\" $script:EMBEDDED_IMAGE -c $shCmd 2>$null\n if ($null -eq $out) { return \"\" }\n return ($out | Out-String).Trim()\n}\n\n# Read admin_dm_room_id from host workspace state.json (fallback when Matrix API is unavailable).\nfunction Read-AgentTeamsAdminDmRoomFromWorkspace {\n param([string]$WorkspaceDir)\n if ([string]::IsNullOrEmpty($WorkspaceDir)) { return \"\" }\n $statePath = Join-Path $WorkspaceDir \"state.json\"\n if (-not (Test-Path $statePath)) { return \"\" }\n try {\n $j = Get-Content $statePath -Raw -ErrorAction Stop | ConvertFrom-Json\n $rid = $j.admin_dm_room_id\n if ($null -eq $rid) { return \"\" }\n $s = [string]$rid\n if ($s -eq \"\" -or $s -eq \"null\") { return \"\" }\n return $s.Trim()\n } catch {\n return \"\"\n }\n}\n\nfunction Get-AgentTeamsRandomHex {\n param([int]$ByteCount)\n $bytes = New-Object byte[] $ByteCount\n $rng = [System.Security.Cryptography.RandomNumberGenerator]::Create()\n $rng.GetBytes($bytes)\n return ([BitConverter]::ToString($bytes)).Replace(\"-\", \"\").ToLower()\n}\n\n# Resolve admin DM room with @manager (small room) via Matrix Client API inside agentteams-manager.\nfunction Get-AgentTeamsAdminDmRoomViaMatrix {\n param(\n [string]$AdminUser,\n [string]$AdminPassword\n )\n $running = docker ps --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-manager$\"\n if (-not $running) { return \"\" }\n if ([string]::IsNullOrEmpty($AdminPassword)) { return \"\" }\n\n $loginObj = @{\n type = \"m.login.password\"\n identifier = @{ type = \"m.id.user\"; user = $AdminUser }\n password = $AdminPassword\n }\n $loginJson = $loginObj | ConvertTo-Json -Compress -Depth 5\n try {\n $loginRaw = $loginJson | docker exec -i agentteams-manager sh -c \"curl -sf -X POST http://127.0.0.1:6167/_matrix/client/v3/login -H 'Content-Type: application/json' -d @-\" 2>$null\n if (-not $loginRaw) { return \"\" }\n $loginResp = $loginRaw | ConvertFrom-Json\n $token = [string]$loginResp.access_token\n if ([string]::IsNullOrEmpty($token)) { return \"\" }\n\n $roomsRaw = docker exec agentteams-manager curl -sf -X GET -H \"Authorization: Bearer $token\" `\n \"http://127.0.0.1:6167/_matrix/client/v3/joined_rooms\" 2>$null\n if (-not $roomsRaw) { return \"\" }\n $roomsObj = $roomsRaw | ConvertFrom-Json\n $roomList = @($roomsObj.joined_rooms)\n foreach ($roomId in $roomList) {\n if ([string]::IsNullOrEmpty($roomId)) { continue }\n $enc = $roomId.Replace(\"!\", \"%21\")\n $memRaw = docker exec agentteams-manager curl -sf -X GET -H \"Authorization: Bearer $token\" `\n \"http://127.0.0.1:6167/_matrix/client/v3/rooms/${enc}/members\" 2>$null\n if (-not $memRaw) { continue }\n $memObj = $memRaw | ConvertFrom-Json\n $chunk = @($memObj.chunk)\n $memberIds = @($chunk | ForEach-Object { $_.state_key })\n $match = $false\n foreach ($m in $memberIds) {\n $beforeColon = ($m -split \":\")[0]\n if ($m -like \"*manager*\" -and $beforeColon -notlike \"*admin*\") {\n $match = $true\n break\n }\n }\n if ($match -and $memberIds.Count -le 3) {\n return $roomId\n }\n }\n } catch {\n return \"\"\n }\n return \"\"\n}\n\nfunction New-EnvFile {\n param([hashtable]$Config, [string]$Path)\n\n $content = @\"\n# AgentTeams Manager Configuration\n# Generated by agentteams-install.ps1 on $(Get-Date -Format \"yyyy-MM-dd HH:mm:ss\")\n\n# Language\nAGENTTEAMS_LANGUAGE=$($Config.LANGUAGE)\n\n# LLM\nAGENTTEAMS_LLM_PROVIDER=$($Config.LLM_PROVIDER)\nAGENTTEAMS_DEFAULT_MODEL=$($Config.DEFAULT_MODEL)\nAGENTTEAMS_LLM_API_KEY=$($Config.LLM_API_KEY)\nAGENTTEAMS_OPENAI_BASE_URL=$($Config.OPENAI_BASE_URL)\nAGENTTEAMS_MODEL_CONTEXT_WINDOW=$($Config.MODEL_CONTEXT_WINDOW)\nAGENTTEAMS_MODEL_MAX_TOKENS=$($Config.MODEL_MAX_TOKENS)\nAGENTTEAMS_MODEL_REASONING=$($Config.MODEL_REASONING)\nAGENTTEAMS_MODEL_VISION=$($Config.MODEL_VISION)\n\n# Embedding model (empty = disabled, default: text-embedding-v4)\nAGENTTEAMS_EMBEDDING_MODEL=$($Config.EMBEDDING_MODEL)\n\n# Admin\nAGENTTEAMS_ADMIN_USER=$($Config.ADMIN_USER)\nAGENTTEAMS_ADMIN_PASSWORD=$($Config.ADMIN_PASSWORD)\n\n# Ports\nAGENTTEAMS_LOCAL_ONLY=$($Config.LOCAL_ONLY)\nAGENTTEAMS_PORT_GATEWAY=$($Config.PORT_GATEWAY)\nAGENTTEAMS_PORT_CONSOLE=$($Config.PORT_CONSOLE)\nAGENTTEAMS_PORT_ELEMENT_WEB=$($Config.PORT_ELEMENT_WEB)\nAGENTTEAMS_PORT_MANAGER_CONSOLE=$($Config.PORT_MANAGER_CONSOLE)\n\n# Matrix\nAGENTTEAMS_MATRIX_DOMAIN=$($Config.MATRIX_DOMAIN)\nAGENTTEAMS_MATRIX_CLIENT_DOMAIN=$($Config.MATRIX_CLIENT_DOMAIN)\n\n# Gateway\nAGENTTEAMS_AI_GATEWAY_DOMAIN=$($Config.AI_GATEWAY_DOMAIN)\nAGENTTEAMS_MANAGER_GATEWAY_KEY=$($Config.MANAGER_GATEWAY_KEY)\n\n# File System\nAGENTTEAMS_FS_DOMAIN=$($Config.FS_DOMAIN)\nAGENTTEAMS_CONSOLE_DOMAIN=$($Config.CONSOLE_DOMAIN)\nAGENTTEAMS_MINIO_USER=$($Config.MINIO_USER)\nAGENTTEAMS_MINIO_PASSWORD=$($Config.MINIO_PASSWORD)\n\n# Internal\nAGENTTEAMS_MANAGER_PASSWORD=$($Config.MANAGER_PASSWORD)\nAGENTTEAMS_REGISTRATION_TOKEN=$($Config.REGISTRATION_TOKEN)\n\n# GitHub (optional)\nAGENTTEAMS_GITHUB_TOKEN=$($Config.GITHUB_TOKEN)\n\n# Nacos package import defaults\nAGENTTEAMS_NACOS_REGISTRY_URI=$(if ($env:AGENTTEAMS_NACOS_REGISTRY_URI) { $env:AGENTTEAMS_NACOS_REGISTRY_URI } else { \"nacos://market.agentteams.io:80/public\" })\nAGENTTEAMS_NACOS_USERNAME=$($env:AGENTTEAMS_NACOS_USERNAME)\nAGENTTEAMS_NACOS_PASSWORD=$($env:AGENTTEAMS_NACOS_PASSWORD)\nAGENTTEAMS_NACOS_TOKEN=$($env:AGENTTEAMS_NACOS_TOKEN)\n\n# Skills Registry (optional, default: nacos://market.agentteams.io:80/public)\nAGENTTEAMS_SKILLS_API_URL=$(if ($Config.SKILLS_API_URL) { $Config.SKILLS_API_URL } else { \"nacos://market.agentteams.io:80/public\" })\n\n# OpenClaw CMS plugin (optional)\nAGENTTEAMS_CMS_TRACES_ENABLED=$(if ($env:AGENTTEAMS_CMS_TRACES_ENABLED) { $env:AGENTTEAMS_CMS_TRACES_ENABLED } else { \"false\" })\nAGENTTEAMS_CMS_ENDPOINT=$($env:AGENTTEAMS_CMS_ENDPOINT)\nAGENTTEAMS_CMS_LICENSE_KEY=$($env:AGENTTEAMS_CMS_LICENSE_KEY)\nAGENTTEAMS_CMS_PROJECT=$($env:AGENTTEAMS_CMS_PROJECT)\nAGENTTEAMS_CMS_WORKSPACE=$($env:AGENTTEAMS_CMS_WORKSPACE)\nAGENTTEAMS_CMS_SERVICE_NAME=$(if ($env:AGENTTEAMS_CMS_SERVICE_NAME) { $env:AGENTTEAMS_CMS_SERVICE_NAME } else { \"agentteams-manager\" })\nAGENTTEAMS_CMS_METRICS_ENABLED=$(if ($env:AGENTTEAMS_CMS_METRICS_ENABLED) { $env:AGENTTEAMS_CMS_METRICS_ENABLED } else { \"false\" })\n\n# Worker images (for direct container creation)\nAGENTTEAMS_WORKER_IMAGE=$($Config.WORKER_IMAGE)\nAGENTTEAMS_COPAW_WORKER_IMAGE=$($Config.COPAW_WORKER_IMAGE)\nAGENTTEAMS_QWENPAW_WORKER_IMAGE=$($Config.QWENPAW_WORKER_IMAGE)\nAGENTTEAMS_HERMES_WORKER_IMAGE=$($Config.HERMES_WORKER_IMAGE)\n\n# Manager runtime (openclaw | copaw)\nAGENTTEAMS_MANAGER_RUNTIME=$($Config.MANAGER_RUNTIME)\n\n# Default Worker runtime (openclaw | copaw | hermes)\nAGENTTEAMS_DEFAULT_WORKER_RUNTIME=$($Config.DEFAULT_WORKER_RUNTIME)\n\n# Matrix E2EE (0=disabled, 1=enabled; default: 0)\nAGENTTEAMS_MATRIX_E2EE=$($Config.MATRIX_E2EE)\n\n# Docker API proxy (0=disabled, 1=enabled; default: 1)\nAGENTTEAMS_DOCKER_PROXY=$($Config.DOCKER_PROXY)\n\n# Docker API proxy: additional allowed image sources (comma-separated)\nAGENTTEAMS_PROXY_ALLOWED_REGISTRIES=$($Config.PROXY_ALLOWED_REGISTRIES)\n\n# Worker idle timeout in minutes (default: 720 = 12 hours)\nAGENTTEAMS_WORKER_IDLE_TIMEOUT=$($Config.WORKER_IDLE_TIMEOUT)\n\n# JVM Args for Higress Console\nJVM_ARGS=$($env:JVM_ARGS)\n\n# Higress WASM plugin image registry (auto-selected by timezone)\nHIGRESS_ADMIN_WASM_PLUGIN_IMAGE_REGISTRY=$($Config.REGISTRY)\n\n# Data persistence\nAGENTTEAMS_DATA_DIR=$($Config.DATA_DIR)\n# Manager workspace (skills, memory, state - host-editable)\nAGENTTEAMS_WORKSPACE_DIR=$($Config.WORKSPACE_DIR)\n# Host directory sharing\nAGENTTEAMS_HOST_SHARE_DIR=$($Config.HOST_SHARE_DIR)\n\"@\n\n Set-Content -Path $Path -Value $content -Encoding UTF8\n Write-Log (Get-Msg \"install.config_saved\" -f $Path)\n}\n\n# ============================================================\n# Prompt Functions\n# ============================================================\n\nfunction Read-Prompt {\n param(\n [string]$VarName,\n [string]$PromptText,\n [string]$Default = \"\",\n [switch]$Secret,\n [switch]$Optional\n )\n\n # Check if already set in environment\n $envValue = [Environment]::GetEnvironmentVariable($VarName)\n if ($envValue) {\n if ($script:AGENTTEAMS_UPGRADE -and -not $script:AGENTTEAMS_NON_INTERACTIVE) {\n # Show current value (masked for secrets) and let user change it\n $displayValue = $envValue\n if ($Secret) {\n if ($envValue.Length -le 8) {\n $displayValue = \"****\"\n } else {\n $displayValue = $envValue.Substring(0, 4) + \"****\" + $envValue.Substring($envValue.Length - 4)\n }\n }\n Write-Log (Get-Msg \"prompt.upgrade_keep\" -f $PromptText, $displayValue)\n $prompt = $PromptText\n if ($Secret) {\n $newValue = Read-Host -Prompt $prompt -AsSecureString\n $newValue = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto(\n [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($newValue)\n )\n } else {\n $newValue = Read-Host -Prompt $prompt\n if ($newValue -eq \"b\") { $script:StepResult = \"back\"; return $null }\n }\n if ($newValue) {\n return $newValue\n }\n return $envValue\n }\n Write-Log (Get-Msg \"prompt.preset\" -f $PromptText)\n return $envValue\n }\n # Upgrade mode: optional fields with empty value — let user set a new value\n elseif ($Optional -and $script:AGENTTEAMS_UPGRADE -and -not $script:AGENTTEAMS_NON_INTERACTIVE) {\n Write-Log (Get-Msg \"prompt.upgrade_empty\" -f $PromptText)\n $prompt = $PromptText\n if ($Secret) {\n $newValue = Read-Host -Prompt $prompt -AsSecureString\n $newValue = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto(\n [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($newValue)\n )\n } else {\n $newValue = Read-Host -Prompt $prompt\n if ($newValue -eq \"b\") { $script:StepResult = \"back\"; return $null }\n }\n if ($newValue) {\n return $newValue\n }\n return \"\"\n }\n\n # Non-interactive or quickstart mode\n if ($script:AGENTTEAMS_NON_INTERACTIVE -or $script:AGENTTEAMS_QUICKSTART) {\n if ($Default) {\n Write-Log (Get-Msg \"prompt.default\" -f $PromptText, $Default)\n return $Default\n }\n elseif ($Optional) {\n return \"\"\n }\n elseif ($script:AGENTTEAMS_NON_INTERACTIVE) {\n # Only hard-error in fully non-interactive mode, not quickstart\n Write-Error (Get-Msg \"prompt.required\" -f $PromptText)\n }\n # quickstart + no default + not optional: fall through to interactive prompt\n }\n\n # Interactive prompt\n $prompt = if ($Default) { \"$PromptText [$Default]\" } else { $PromptText }\n\n if ($Secret) {\n $value = Read-Host -Prompt $prompt -AsSecureString\n $value = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto(\n [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($value)\n )\n }\n else {\n $value = Read-Host -Prompt $prompt\n if ($value -eq \"b\") { $script:StepResult = \"back\"; return $null }\n }\n\n if (-not $value -and $Default) {\n $value = $Default\n }\n\n if (-not $value -and -not $Optional) {\n Write-Error (Get-Msg \"prompt.required_empty\" -f $PromptText)\n }\n\n return $value\n}\n\n# Load current parameter values from env file for upgrade mode display\nfunction Load-CurrentParamsFromEnv {\n $envFile = $script:AGENTTEAMS_ENV_FILE\n if (Test-Path $envFile) {\n $content = Get-Content $envFile\n $content | ForEach-Object {\n if ($_ -match \"^AGENTTEAMS_LLM_PROVIDER=(.*)$\") {\n $script:config.LLM_PROVIDER = $Matches[1].Trim()\n }\n if ($_ -match \"^AGENTTEAMS_OPENAI_BASE_URL=(.*)$\") {\n $script:config.OPENAI_BASE_URL = $Matches[1].Trim()\n }\n if ($_ -match \"^AGENTTEAMS_DEFAULT_MODEL=(.*)$\") {\n $script:config.DEFAULT_MODEL = $Matches[1].Trim()\n }\n if ($_ -match \"^AGENTTEAMS_EMBEDDING_MODEL=(.*)$\") {\n $script:config.EMBEDDING_MODEL = $Matches[1].Trim()\n }\n if ($_ -match \"^AGENTTEAMS_WORKSPACE_DIR=(.*)$\") {\n $script:config.WORKSPACE_DIR = $Matches[1].Trim()\n }\n if ($_ -match \"^AGENTTEAMS_HOST_SHARE_DIR=(.*)$\") {\n $script:config.HOST_SHARE_DIR = $Matches[1].Trim()\n }\n }\n }\n}\n\n# ============================================================\n# OpenAI-Compatible Provider\n# ============================================================\n\nfunction Test-LlmConnectivity {\n param(\n [string]$BaseUrl,\n [string]$ApiKey,\n [string]$Model,\n [string]$Hint = \"\"\n )\n Write-Log (Get-Msg \"llm.openai.test.testing\")\n $uri = ($BaseUrl.TrimEnd('/')) + \"/chat/completions\"\n $bodyHash = @{\n model = $Model\n messages = @(@{ role = \"user\"; content = \"hi\" })\n }\n $body = $bodyHash | ConvertTo-Json -Compress\n try {\n $response = Invoke-WebRequest -Uri $uri -Method POST `\n -Headers @{ \"Authorization\" = \"Bearer $ApiKey\"; \"Content-Type\" = \"application/json\"; \"User-Agent\" = \"AgentTeams/$($script:AGENTTEAMS_VERSION)\" } `\n -Body $body -TimeoutSec 30 -ErrorAction Stop -UseBasicParsing\n Write-Log (Get-Msg \"llm.openai.test.ok\")\n } catch {\n $statusCode = if ($_.Exception.Response) { [int]$_.Exception.Response.StatusCode } else { 0 }\n $responseBody = \"\"\n if ($_.Exception.Response) {\n try {\n $stream = $_.Exception.Response.GetResponseStream()\n $reader = New-Object System.IO.StreamReader($stream)\n $responseBody = $reader.ReadToEnd()\n } catch {}\n }\n Write-Host (Get-Msg \"llm.openai.test.fail\" -f $statusCode, $responseBody) -ForegroundColor Yellow\n if ($Hint) {\n Write-Host $Hint -ForegroundColor Yellow\n }\n if (-not $script:AGENTTEAMS_NON_INTERACTIVE) {\n $confirm = Read-Host (Get-Msg \"llm.openai.test.confirm\")\n if ($confirm -eq \"b\" -or $confirm -eq \"B\") {\n $script:StepResult = \"back\"\n return\n }\n if ($confirm -ne \"y\" -and $confirm -ne \"Y\") {\n Write-Log (Get-Msg \"llm.openai.test.aborted\")\n Exit-Script 1\n }\n }\n }\n}\n\nfunction Test-EmbeddingConnectivity {\n param(\n [string]$BaseUrl,\n [string]$ApiKey,\n [string]$Model\n )\n Write-Log (Get-Msg \"llm.embedding.test.testing\")\n $uri = ($BaseUrl.TrimEnd('/')) + \"/embeddings\"\n $bodyHash = @{\n model = $Model\n input = \"test\"\n }\n $body = $bodyHash | ConvertTo-Json -Compress\n try {\n $response = Invoke-WebRequest -Uri $uri -Method POST `\n -Headers @{ \"Authorization\" = \"Bearer $ApiKey\"; \"Content-Type\" = \"application/json\"; \"User-Agent\" = \"AgentTeams/$($script:AGENTTEAMS_VERSION)\" } `\n -Body $body -TimeoutSec 30 -ErrorAction Stop -UseBasicParsing\n Write-Log (Get-Msg \"llm.embedding.test.ok\")\n return $true\n } catch {\n $statusCode = if ($_.Exception.Response) { [int]$_.Exception.Response.StatusCode } else { 0 }\n $responseBody = \"\"\n if ($_.Exception.Response) {\n try {\n $stream = $_.Exception.Response.GetResponseStream()\n $reader = New-Object System.IO.StreamReader($stream)\n $responseBody = $reader.ReadToEnd()\n } catch {}\n }\n Write-Host (Get-Msg \"llm.embedding.test.fail\" -f $statusCode, $responseBody) -ForegroundColor Yellow\n return $false\n }\n}\n\nfunction New-OpenAICompatProvider {\n param(\n [string]$BaseUrl,\n [string]$ApiKey,\n [int]$ConsolePort = 18001\n )\n\n if (-not $BaseUrl -or -not $ApiKey) {\n Write-Log (Get-Msg \"install.openai_compat.missing\")\n return $false\n }\n\n $consoleUrl = \"http://localhost:$ConsolePort\"\n\n # Parse base URL\n $protocol = \"https\"\n $port = 443\n $urlWithoutProto = $BaseUrl -replace \"^https?://\", \"\"\n\n if ($BaseUrl -match \"^http://\") {\n $protocol = \"http\"\n $port = 80\n }\n\n $domain = $urlWithoutProto.Split(\"/\")[0]\n\n if ($domain -match \":(\\d+)$\") {\n $port = [int]$Matches[1]\n $domain = $domain -replace \":\\d+$\", \"\"\n }\n\n Write-Log (Get-Msg \"install.openai_compat.creating\")\n Write-Log (Get-Msg \"install.openai_compat.domain\" -f $domain)\n Write-Log (Get-Msg \"install.openai_compat.port\" -f $port)\n Write-Log (Get-Msg \"install.openai_compat.protocol\" -f $protocol)\n\n $serviceName = \"openai-compat\"\n\n # Create DNS service source\n $serviceBody = @{\n type = \"dns\"\n name = $serviceName\n port = $port.ToString()\n protocol = $protocol\n proxyName = \"\"\n domain = $domain\n } | ConvertTo-Json -Compress\n\n try {\n Invoke-RestMethod -Uri \"$consoleUrl/v1/service-sources\" -Method POST -ContentType \"application/json\" -Body $serviceBody -ErrorAction SilentlyContinue | Out-Null\n }\n catch {\n Write-Log (Get-Msg \"install.openai_compat.service_fail\")\n }\n\n Start-Sleep -Seconds 2\n\n # Create AI provider\n $providerBody = @{\n type = \"openai\"\n name = \"openai-compat\"\n tokens = @($ApiKey)\n version = 0\n protocol = \"openai/v1\"\n tokenFailoverConfig = @{ enabled = $false }\n rawConfigs = @{\n openaiCustomUrl = $BaseUrl\n openaiCustomServiceName = \"$serviceName.dns\"\n openaiCustomServicePort = $port\n }\n } | ConvertTo-Json -Compress -Depth 3\n\n try {\n Invoke-RestMethod -Uri \"$consoleUrl/v1/ai/providers\" -Method POST -ContentType \"application/json\" -Body $providerBody -ErrorAction SilentlyContinue | Out-Null\n Write-Log (Get-Msg \"install.openai_compat.success\")\n return $true\n }\n catch {\n Write-Log (Get-Msg \"install.openai_compat.provider_fail\")\n return $false\n }\n}\n\n# ============================================================\n# State Machine Helpers\n# ============================================================\n\nfunction Test-ShouldSkipStep {\n param([string]$StepFn)\n switch ($StepFn) {\n { $_ -in @(\"Step-Lang\", \"Step-Mode\") } {\n return $script:AGENTTEAMS_NON_INTERACTIVE\n }\n \"Step-Existing\" {\n return (-not (Test-Path $script:AGENTTEAMS_ENV_FILE))\n }\n # Keep-All upgrade mode: skip all config steps, values are already loaded\n { $_ -in @(\"Step-Llm\", \"Step-Admin\", \"Step-Network\", \"Step-Ports\", \"Step-Domains\",\n \"Step-Github\", \"Step-Skills\",\n \"Step-Runtime\", \"Step-ManagerRuntime\", \"Step-E2ee\", \"Step-DockerProxy\",\n \"Step-Idle\", \"Step-Hostshare\") } {\n if ($script:AGENTTEAMS_UPGRADE -and $env:AGENTTEAMS_UPGRADE_KEEP_ALL -eq \"1\") { return $true }\n return $false\n }\n { $_ -in @(\"Step-Volume\", \"Step-Workspace\") } {\n if ($script:AGENTTEAMS_UPGRADE -and $env:AGENTTEAMS_UPGRADE_KEEP_ALL -eq \"1\") { return $true }\n if ($script:AGENTTEAMS_NON_INTERACTIVE) { return $true }\n if ($script:AGENTTEAMS_QUICKSTART) { return $true }\n return $false\n }\n { $_ -in @(\"Step-E2ee\", \"Step-Idle\") } {\n if ($script:AGENTTEAMS_NON_INTERACTIVE) { return $true }\n if ($script:AGENTTEAMS_QUICKSTART -and -not $script:AGENTTEAMS_UPGRADE) { return $true }\n return $false\n }\n \"Step-DockerProxy\" {\n # Embedded controller IS the proxy/orchestrator — no separate proxy step needed.\n if ($script:AGENTTEAMS_USE_EMBEDDED -eq \"1\") { return $true }\n if ($script:AGENTTEAMS_NON_INTERACTIVE) { return $true }\n if ($script:AGENTTEAMS_QUICKSTART -and -not $script:AGENTTEAMS_UPGRADE) { return $true }\n return $false\n }\n \"Step-ManagerRuntime\" {\n if ($script:AGENTTEAMS_NON_INTERACTIVE) { return $true }\n return $false\n }\n \"Step-Hostshare\" {\n if ($script:AGENTTEAMS_NON_INTERACTIVE) { return $true }\n if ($script:AGENTTEAMS_QUICKSTART) { return $true }\n return $false\n }\n default { return $false }\n }\n}\n\nfunction Clear-StepVars {\n param([string]$StepFn)\n switch ($StepFn) {\n \"Step-Mode\" { $script:AGENTTEAMS_QUICKSTART = $false }\n \"Step-Existing\" {\n $script:AGENTTEAMS_UPGRADE = $false\n $script:UPGRADE_EXISTING_WORKERS = $null\n }\n \"Step-Llm\" {\n foreach ($k in @(\"AGENTTEAMS_LLM_PROVIDER\",\"AGENTTEAMS_DEFAULT_MODEL\",\"AGENTTEAMS_OPENAI_BASE_URL\",\n \"AGENTTEAMS_LLM_API_KEY\",\"AGENTTEAMS_MODEL_CONTEXT_WINDOW\",\"AGENTTEAMS_MODEL_MAX_TOKENS\",\n \"AGENTTEAMS_MODEL_REASONING\",\"AGENTTEAMS_MODEL_VISION\")) {\n [Environment]::SetEnvironmentVariable($k, $null, \"Process\")\n }\n }\n \"Step-Admin\" {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_ADMIN_USER\", $null, \"Process\")\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_ADMIN_PASSWORD\", $null, \"Process\")\n }\n \"Step-Network\" {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_LOCAL_ONLY\", $null, \"Process\")\n }\n \"Step-Ports\" {\n foreach ($k in @(\"AGENTTEAMS_PORT_GATEWAY\",\"AGENTTEAMS_PORT_CONSOLE\",\"AGENTTEAMS_PORT_ELEMENT_WEB\",\"AGENTTEAMS_PORT_MANAGER_CONSOLE\")) {\n [Environment]::SetEnvironmentVariable($k, $null, \"Process\")\n }\n }\n \"Step-Domains\" {\n foreach ($k in @(\"AGENTTEAMS_MATRIX_DOMAIN\",\"AGENTTEAMS_MATRIX_CLIENT_DOMAIN\",\"AGENTTEAMS_AI_GATEWAY_DOMAIN\",\"AGENTTEAMS_FS_DOMAIN\",\"AGENTTEAMS_CONSOLE_DOMAIN\")) {\n [Environment]::SetEnvironmentVariable($k, $null, \"Process\")\n }\n }\n \"Step-Github\" {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_GITHUB_TOKEN\", $null, \"Process\")\n }\n \"Step-Skills\" {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_SKILLS_API_URL\", $null, \"Process\")\n }\n \"Step-Volume\" {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_DATA_DIR\", $null, \"Process\")\n }\n \"Step-Workspace\" {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_WORKSPACE_DIR\", $null, \"Process\")\n }\n \"Step-Runtime\" {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_DEFAULT_WORKER_RUNTIME\", $null, \"Process\")\n }\n \"Step-ManagerRuntime\" {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_MANAGER_RUNTIME\", $null, \"Process\")\n }\n \"Step-E2ee\" {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_MATRIX_E2EE\", $null, \"Process\")\n }\n \"Step-DockerProxy\" {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_DOCKER_PROXY\", $null, \"Process\")\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_PROXY_ALLOWED_REGISTRIES\", $null, \"Process\")\n }\n \"Step-Idle\" {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_WORKER_IDLE_TIMEOUT\", $null, \"Process\")\n }\n \"Step-Hostshare\" {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_HOST_SHARE_DIR\", $null, \"Process\")\n }\n }\n}\n\n# ============================================================\n# Individual step functions\n# ============================================================\n\nfunction Step-Lang {\n $langDefaultChoice = if ($script:AGENTTEAMS_LANGUAGE -eq \"zh\") { \"1\" } else { \"2\" }\n $langDetectedKey = \"lang.detected.$($script:AGENTTEAMS_LANGUAGE)\"\n Write-Log (Get-Msg $langDetectedKey)\n Write-Log (Get-Msg \"lang.switch_title\")\n Write-Host (Get-Msg \"lang.option_zh\")\n Write-Host (Get-Msg \"lang.option_en\")\n Write-Host \"\"\n $langChoice = Read-Host \"$(Get-Msg 'lang.prompt') [$langDefaultChoice]\"\n if (-not $langChoice) { $langChoice = $langDefaultChoice }\n if ($langChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n switch ($langChoice) {\n \"1\" { $script:AGENTTEAMS_LANGUAGE = \"zh\" }\n \"2\" { $script:AGENTTEAMS_LANGUAGE = \"en\" }\n }\n $env:AGENTTEAMS_LANGUAGE = $script:AGENTTEAMS_LANGUAGE\n Write-Log \"\"\n}\n\nfunction Step-Mode {\n Write-Log (Get-Msg \"install.mode.title\")\n Write-Host \"\"\n Write-Host (Get-Msg \"install.mode.choose\")\n Write-Host (Get-Msg \"install.mode.quickstart\")\n Write-Host (Get-Msg \"install.mode.manual\")\n Write-Host \"\"\n $choice = Read-Host (Get-Msg \"install.mode.prompt\")\n $choice = if ($choice) { $choice } else { \"1\" }\n if ($choice -eq \"b\") { $script:StepResult = \"back\"; return }\n switch -Regex ($choice) {\n \"^(1|quick|quickstart)$\" {\n Write-Log (Get-Msg \"install.mode.quickstart_selected\")\n $script:AGENTTEAMS_QUICKSTART = $true\n }\n \"^(2|manual)$\" {\n Write-Log (Get-Msg \"install.mode.manual_selected\")\n $script:AGENTTEAMS_QUICKSTART = $false\n }\n default {\n Write-Log (Get-Msg \"install.mode.invalid\")\n $script:AGENTTEAMS_QUICKSTART = $true\n }\n }\n Write-Log \"\"\n}\n\nfunction Step-Existing {\n # This step is skipped when env file doesn't exist\n Write-Log (Get-Msg \"install.existing.detected\" -f $script:AGENTTEAMS_ENV_FILE)\n\n $runningManager = docker ps --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-manager$\"\n $runningWorkers = docker ps --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-worker-\"\n $existingWorkers = docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-worker-\"\n\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n Write-Log (Get-Msg \"install.existing.upgrade_noninteractive\")\n $upgradeChoice = \"1\"\n } else {\n Write-Host \"\"\n Write-Host (Get-Msg \"install.existing.choose\")\n Write-Host (Get-Msg \"install.existing.upgrade\")\n Write-Host (Get-Msg \"install.existing.reinstall\")\n Write-Host (Get-Msg \"install.existing.cancel\")\n Write-Host \"\"\n $upgradeChoice = Read-Host (Get-Msg \"install.existing.prompt\")\n $upgradeChoice = if ($upgradeChoice) { $upgradeChoice } else { \"1\" }\n if ($upgradeChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n }\n\n switch -Regex ($upgradeChoice) {\n \"^(1|upgrade)$\" {\n $script:AGENTTEAMS_UPGRADE = $true\n Write-Log (Get-Msg \"install.existing.upgrading\")\n\n # Show upgrade mode sub-menu (unless already set via env var)\n if ($env:AGENTTEAMS_UPGRADE_KEEP_ALL -ne \"1\") {\n Write-Host \"\"\n Write-Host (Get-Msg \"upgrade.mode.prompt\")\n Write-Host (Get-Msg \"upgrade.mode.keep_all\")\n Write-Host (Get-Msg \"upgrade.mode.confirm_each\")\n Write-Host (Get-Msg \"upgrade.mode.back\")\n Write-Host \"\"\n $upgradeModeChoice = Read-Host (Get-Msg \"install.existing.prompt\")\n $upgradeModeChoice = if ($upgradeModeChoice) { $upgradeModeChoice } else { \"1\" }\n switch -Regex ($upgradeModeChoice) {\n \"^(1|keep)$\" {\n $env:AGENTTEAMS_UPGRADE_KEEP_ALL = \"1\"\n }\n \"^(2|confirm)$\" {\n $env:AGENTTEAMS_UPGRADE_KEEP_ALL = \"0\"\n }\n \"^(3|b)$\" {\n $script:StepResult = \"back\"\n return\n }\n default {\n $env:AGENTTEAMS_UPGRADE_KEEP_ALL = \"0\"\n }\n }\n }\n # Load current parameters for both Keep-All and confirm-each modes\n Load-CurrentParamsFromEnv\n\n if ($runningManager -or $runningWorkers) {\n Write-Host \"\"\n Write-Host \"$($script:ESC)[33m$(Get-Msg 'install.existing.warn_manager_stop')$($script:ESC)[0m\"\n if ($existingWorkers) {\n Write-Host \"$($script:ESC)[33m$(Get-Msg 'install.existing.warn_worker_recreate')$($script:ESC)[0m\"\n }\n if (-not $script:AGENTTEAMS_NON_INTERACTIVE) {\n $confirm = Read-Host (Get-Msg \"install.existing.continue_prompt\")\n if ($confirm -eq \"b\" -or $confirm -eq \"B\") { $script:StepResult = \"back\"; return }\n if ($confirm -ne \"y\" -and $confirm -ne \"Y\") {\n Write-Log (Get-Msg \"install.existing.cancelled\")\n exit 0\n }\n }\n }\n $script:UPGRADE_EXISTING_WORKERS = $existingWorkers\n }\n \"^(2|reinstall)$\" {\n Write-Log (Get-Msg \"install.reinstall.performing\")\n $existingWorkspace = \"$env:USERPROFILE\\agentteams-manager\"\n if (Test-Path $script:AGENTTEAMS_ENV_FILE) {\n $envContent = Get-Content $script:AGENTTEAMS_ENV_FILE\n $wsLine = $envContent | Select-String \"^AGENTTEAMS_WORKSPACE_DIR=\"\n if ($wsLine) {\n $existingWorkspace = $wsLine.Line.Substring(21)\n }\n }\n Write-Host \"\"\n Write-Host \"$($script:ESC)[33m$(Get-Msg 'install.reinstall.warn_stop')$($script:ESC)[0m\"\n if ($runningManager) { Write-Host \"$($script:ESC)[33m - agentteams-manager (manager)$($script:ESC)[0m\" }\n $runningWorkers | ForEach-Object { Write-Host \"$($script:ESC)[33m - $_ (worker)$($script:ESC)[0m\" }\n Write-Host \"\"\n Write-Host \"$($script:ESC)[31m$(Get-Msg 'install.reinstall.warn_delete')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[31m$(Get-Msg 'install.reinstall.warn_volume')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[31m$(Get-Msg 'install.reinstall.warn_env' -f $script:AGENTTEAMS_ENV_FILE)$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[31m$(Get-Msg 'install.reinstall.warn_workspace' -f $existingWorkspace)$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[31m$(Get-Msg 'install.reinstall.warn_workers')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[31m$(Get-Msg 'install.reinstall.warn_proxy')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[31m$(Get-Msg 'install.reinstall.warn_network')$($script:ESC)[0m\"\n Write-Host \"\"\n Write-Host \"$($script:ESC)[31m$(Get-Msg 'install.reinstall.confirm_type')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[31m $existingWorkspace$($script:ESC)[0m\"\n Write-Host \"\"\n $confirmPath = Read-Host (Get-Msg \"install.reinstall.confirm_path\")\n if ($confirmPath -ne $existingWorkspace) {\n Write-Error (Get-Msg \"install.reinstall.path_mismatch\" -f $confirmPath, $existingWorkspace)\n }\n Write-Log (Get-Msg \"install.reinstall.confirmed\")\n docker stop agentteams-manager *>$null\n docker rm agentteams-manager *>$null\n docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-worker-\" | ForEach-Object {\n docker stop $_ *>$null\n docker rm $_ *>$null\n Write-Log (Get-Msg \"install.reinstall.removed_worker\" -f $_)\n }\n $existingController = docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-controller$\"\n if ($existingController) {\n Write-Log (Get-Msg \"install.reinstall.removing_proxy\")\n docker stop agentteams-controller *>$null\n docker rm agentteams-controller *>$null\n }\n if (docker volume ls -q 2>$null | Select-String \"^agentteams-data$\") {\n Write-Log (Get-Msg \"install.reinstall.removing_volume\")\n docker volume rm agentteams-data *>$null\n }\n if (Test-Path $existingWorkspace) {\n Write-Log (Get-Msg \"install.reinstall.removing_workspace\" -f $existingWorkspace)\n Remove-Item -Recurse -Force $existingWorkspace\n }\n if (Test-Path $script:AGENTTEAMS_ENV_FILE) {\n Write-Log (Get-Msg \"install.reinstall.removing_env\" -f $script:AGENTTEAMS_ENV_FILE)\n Remove-Item -Force $script:AGENTTEAMS_ENV_FILE\n }\n $existingNetwork = docker network ls --format \"{{.Name}}\" 2>$null | Select-String \"^agentteams-net$\"\n if ($existingNetwork) {\n Write-Log (Get-Msg \"install.reinstall.removing_network\")\n docker network rm agentteams-net *>$null\n }\n Write-Log (Get-Msg \"install.reinstall.cleanup_done\")\n }\n default {\n Write-Log (Get-Msg \"install.existing.cancelled\")\n exit 0\n }\n }\n\n # Load existing env file (upgrade path)\n if (Test-Path $script:AGENTTEAMS_ENV_FILE) {\n Write-Log (Get-Msg \"install.loading_config\" -f $script:AGENTTEAMS_ENV_FILE)\n Get-Content $script:AGENTTEAMS_ENV_FILE | ForEach-Object {\n if ($_ -match \"^([^#=][^=]*)=(.*)$\") {\n $key = $Matches[1].Trim()\n $value = $Matches[2].Split(\"#\")[0].Trim()\n if (-not [Environment]::GetEnvironmentVariable($key)) {\n [Environment]::SetEnvironmentVariable($key, $value, \"Process\")\n }\n }\n }\n }\n}\n\nfunction Step-Llm {\n Write-Log (Get-Msg \"llm.title\")\n\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n if ($script:AGENTTEAMS_LANGUAGE -eq \"zh\") {\n $script:config.LLM_PROVIDER = if ($env:AGENTTEAMS_LLM_PROVIDER) { $env:AGENTTEAMS_LLM_PROVIDER } else { \"openai-compat\" }\n $script:config.DEFAULT_MODEL = if ($env:AGENTTEAMS_DEFAULT_MODEL) { $env:AGENTTEAMS_DEFAULT_MODEL } else { \"qwen3.6-plus\" }\n $script:config.OPENAI_BASE_URL = if ($env:AGENTTEAMS_OPENAI_BASE_URL) { $env:AGENTTEAMS_OPENAI_BASE_URL } else { \"https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1\" }\n Write-Log (Get-Msg \"llm.provider.label\" -f $script:config.LLM_PROVIDER)\n Write-Log (Get-Msg \"llm.openai.base_url_label\" -f $script:config.OPENAI_BASE_URL)\n } else {\n $script:config.LLM_PROVIDER = if ($env:AGENTTEAMS_LLM_PROVIDER) { $env:AGENTTEAMS_LLM_PROVIDER } else { \"qwen\" }\n $script:config.DEFAULT_MODEL = if ($env:AGENTTEAMS_DEFAULT_MODEL) { $env:AGENTTEAMS_DEFAULT_MODEL } else { \"qwen3.6-plus\" }\n $script:config.OPENAI_BASE_URL = if ($env:AGENTTEAMS_OPENAI_BASE_URL) { $env:AGENTTEAMS_OPENAI_BASE_URL } else { \"\" }\n Write-Log (Get-Msg \"llm.provider.qwen_default\" -f $script:config.LLM_PROVIDER)\n }\n Write-Log (Get-Msg \"llm.model.label\" -f $script:config.DEFAULT_MODEL)\n Write-Log \"\"\n $script:config.LLM_API_KEY = Read-Prompt -VarName \"AGENTTEAMS_LLM_API_KEY\" -PromptText (Get-Msg \"llm.apikey_prompt\") -Secret\n $script:config.EMBEDDING_MODEL = if ($null -ne $env:AGENTTEAMS_EMBEDDING_MODEL) { $env:AGENTTEAMS_EMBEDDING_MODEL } else { \"text-embedding-v4\" }\n return\n }\n\n Write-Host \"\"\n Write-Host (Get-Msg \"llm.providers_title\")\n Write-Host (Get-Msg \"llm.provider.alibaba\")\n Write-Host (Get-Msg \"llm.provider.openai_compat\")\n Write-Host \"\"\n\n # If upgrade mode with loaded provider, show current as default\n if ($script:AGENTTEAMS_UPGRADE -and $script:config.LLM_PROVIDER) {\n $defaultProvider = if ($script:config.LLM_PROVIDER -eq \"openai-compat\") { \"2\" } else { \"1\" }\n $providerChoice = Read-Host \"$(Get-Msg 'llm.provider.select') [${defaultProvider}]\"\n $providerChoice = if ($providerChoice) { $providerChoice } else { $defaultProvider }\n } elseif ($script:AGENTTEAMS_QUICKSTART) {\n $providerChoice = Read-Host \"$(Get-Msg 'llm.provider.select') [1]\"\n } else {\n $providerChoice = Read-Host (Get-Msg \"llm.provider.select\")\n $providerChoice = if ($providerChoice) { $providerChoice } else { \"1\" }\n }\n if ($providerChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n\n switch -Regex ($providerChoice) {\n \"^(1|alibaba-cloud)$\" {\n $alibabaAccess = $null\n if ($script:AGENTTEAMS_LANGUAGE -eq \"en\") {\n $script:config.LLM_PROVIDER = \"openai-compat\"\n $script:config.OPENAI_BASE_URL = \"https://dashscope-intl.aliyuncs.com/compatible-mode/v1\"\n $alibabaAccess = \"tokenplan\"\n Write-Host \"\"\n Write-Host (Get-Msg \"llm.codingplan.models_title\")\n Write-Host (Get-Msg \"llm.codingplan.model.qwen36plus\")\n Write-Host (Get-Msg \"llm.codingplan.model.glm5\")\n Write-Host (Get-Msg \"llm.codingplan.model.kimi\")\n Write-Host (Get-Msg \"llm.codingplan.model.minimax\")\n Write-Host \"\"\n # If upgrade with loaded model, show current as default\n if ($script:AGENTTEAMS_UPGRADE -and $script:config.DEFAULT_MODEL) {\n $defaultModel = switch ($script:config.DEFAULT_MODEL) {\n \"qwen3.6-plus\" { \"1\" }\n \"glm-5\" { \"2\" }\n \"kimi-k2.5\" { \"3\" }\n \"MiniMax-M2.5\" { \"4\" }\n default { \"1\" }\n }\n $codingPlanModelChoice = Read-Host \"$(Get-Msg 'llm.codingplan.model.select') [${defaultModel}]\"\n $codingPlanModelChoice = if ($codingPlanModelChoice) { $codingPlanModelChoice } else { $defaultModel }\n } elseif ($script:AGENTTEAMS_QUICKSTART) {\n $codingPlanModelChoice = Read-Host \"$(Get-Msg 'llm.codingplan.model.select') [1]\"\n } else {\n $codingPlanModelChoice = Read-Host (Get-Msg \"llm.codingplan.model.select\")\n }\n $codingPlanModelChoice = if ($codingPlanModelChoice) { $codingPlanModelChoice } else { \"1\" }\n if ($codingPlanModelChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n switch -Regex ($codingPlanModelChoice) {\n \"^(1|qwen3\\.6-plus)$\" { $script:config.DEFAULT_MODEL = \"qwen3.6-plus\" }\n \"^(2|glm-5)$\" { $script:config.DEFAULT_MODEL = \"glm-5\" }\n \"^(3|kimi-k2\\.5)$\" { $script:config.DEFAULT_MODEL = \"kimi-k2.5\" }\n \"^(4|MiniMax-M2\\.5)$\" { $script:config.DEFAULT_MODEL = \"MiniMax-M2.5\" }\n default { $script:config.DEFAULT_MODEL = \"qwen3.6-plus\" }\n }\n Write-Log (Get-Msg \"llm.provider.selected_codingplan\")\n } else {\n Write-Host \"\"\n Write-Host (Get-Msg \"llm.alibaba.models_title\")\n Write-Host (Get-Msg \"llm.alibaba.model.tokenplan\")\n Write-Host (Get-Msg \"llm.alibaba.model.bailian\")\n Write-Host (Get-Msg \"llm.alibaba.model.codingplan_legacy\")\n Write-Host \"\"\n $modelChoice = if ($script:AGENTTEAMS_QUICKSTART) {\n Read-Host \"$(Get-Msg 'llm.alibaba.model.select') [1]\"\n } else {\n Read-Host (Get-Msg \"llm.alibaba.model.select\")\n }\n $modelChoice = if ($modelChoice) { $modelChoice } else { \"1\" }\n if ($modelChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n\n if ($modelChoice -match \"^(1|token-plan|tokenplan)$\") {\n $alibabaAccess = \"tokenplan\"\n $script:config.LLM_PROVIDER = \"openai-compat\"\n $script:config.OPENAI_BASE_URL = \"https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1\"\n Write-Host \"\"\n Write-Host (Get-Msg \"llm.codingplan.models_title\")\n Write-Host (Get-Msg \"llm.codingplan.model.qwen36plus\")\n Write-Host (Get-Msg \"llm.codingplan.model.glm5\")\n Write-Host (Get-Msg \"llm.codingplan.model.kimi\")\n Write-Host (Get-Msg \"llm.codingplan.model.minimax\")\n Write-Host \"\"\n # If upgrade with loaded model, show current as default\n if ($script:AGENTTEAMS_UPGRADE -and $script:config.DEFAULT_MODEL) {\n $defaultModel = switch ($script:config.DEFAULT_MODEL) {\n \"qwen3.6-plus\" { \"1\" }\n \"glm-5\" { \"2\" }\n \"kimi-k2.5\" { \"3\" }\n \"MiniMax-M2.5\" { \"4\" }\n default { \"1\" }\n }\n $codingPlanModelChoice = Read-Host \"$(Get-Msg 'llm.codingplan.model.select') [${defaultModel}]\"\n $codingPlanModelChoice = if ($codingPlanModelChoice) { $codingPlanModelChoice } else { $defaultModel }\n } elseif ($script:AGENTTEAMS_QUICKSTART) {\n $codingPlanModelChoice = Read-Host \"$(Get-Msg 'llm.codingplan.model.select') [1]\"\n } else {\n $codingPlanModelChoice = Read-Host (Get-Msg \"llm.codingplan.model.select\")\n }\n $codingPlanModelChoice = if ($codingPlanModelChoice) { $codingPlanModelChoice } else { \"1\" }\n if ($codingPlanModelChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n switch -Regex ($codingPlanModelChoice) {\n \"^(1|qwen3\\.6-plus)$\" { $script:config.DEFAULT_MODEL = \"qwen3.6-plus\" }\n \"^(2|glm-5)$\" { $script:config.DEFAULT_MODEL = \"glm-5\" }\n \"^(3|kimi-k2\\.5)$\" { $script:config.DEFAULT_MODEL = \"kimi-k2.5\" }\n \"^(4|MiniMax-M2\\.5)$\" { $script:config.DEFAULT_MODEL = \"MiniMax-M2.5\" }\n default { $script:config.DEFAULT_MODEL = \"qwen3.6-plus\" }\n }\n Write-Log (Get-Msg \"llm.provider.selected_tokenplan\")\n } elseif ($modelChoice -match \"^(2|qwen|bailian)$\") {\n $alibabaAccess = \"bailian\"\n $script:config.LLM_PROVIDER = \"qwen\"\n $script:config.OPENAI_BASE_URL = \"\"\n Write-Host \"\"\n if ($script:AGENTTEAMS_UPGRADE -and $script:config.DEFAULT_MODEL) {\n $qwenModelInput = Read-Host \"$(Get-Msg 'llm.qwen.model_prompt') [${script:config.DEFAULT_MODEL}]\"\n } else {\n $qwenModelInput = Read-Host (Get-Msg \"llm.qwen.model_prompt\")\n }\n if ($qwenModelInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $script:config.DEFAULT_MODEL = if ($qwenModelInput) { $qwenModelInput } elseif ($script:config.DEFAULT_MODEL) { $script:config.DEFAULT_MODEL } else { \"qwen3.6-plus\" }\n Write-Log (Get-Msg \"llm.provider.selected_qwen\")\n Request-CustomModelParams $script:config.DEFAULT_MODEL\n if ($script:StepResult -eq \"back\") { return }\n } elseif ($modelChoice -match \"^(3|coding-plan|codingplan)$\") {\n $alibabaAccess = \"codingplan_legacy\"\n $script:config.LLM_PROVIDER = \"openai-compat\"\n $script:config.OPENAI_BASE_URL = \"https://coding.dashscope.aliyuncs.com/v1\"\n Write-Host \"\"\n if ($script:AGENTTEAMS_UPGRADE -and $script:config.DEFAULT_MODEL) {\n $codingModelInput = Read-Host \"$(Get-Msg 'llm.qwen.model_prompt') [${script:config.DEFAULT_MODEL}]\"\n } else {\n $codingModelInput = Read-Host (Get-Msg \"llm.qwen.model_prompt\")\n }\n if ($codingModelInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $script:config.DEFAULT_MODEL = if ($codingModelInput) { $codingModelInput } elseif ($script:config.DEFAULT_MODEL) { $script:config.DEFAULT_MODEL } else { \"qwen3.6-plus\" }\n Write-Log (Get-Msg \"llm.provider.selected_codingplan_legacy\")\n Request-CustomModelParams $script:config.DEFAULT_MODEL\n if ($script:StepResult -eq \"back\") { return }\n } else {\n Write-Error (Get-Msg \"llm.alibaba.model.invalid\" -f $modelChoice)\n }\n }\n\n Write-Log (Get-Msg \"llm.model.label\" -f $script:config.DEFAULT_MODEL)\n Write-Log \"\"\n if ($alibabaAccess -eq \"bailian\") {\n Write-Log (Get-Msg \"llm.apikey_hint_bailian\")\n Write-Log (Get-Msg \"llm.apikey_url_bailian\")\n } elseif ($script:AGENTTEAMS_LANGUAGE -eq \"en\") {\n Write-Log (Get-Msg \"llm.apikey_hint_qwencloud\")\n Write-Log (Get-Msg \"llm.apikey_url_qwencloud\")\n } elseif ($alibabaAccess -eq \"codingplan_legacy\") {\n Write-Log (Get-Msg \"llm.apikey_hint_codingplan\")\n Write-Log (Get-Msg \"llm.apikey_url_codingplan\")\n } else {\n Write-Log (Get-Msg \"llm.apikey_hint_tokenplan\")\n Write-Log (Get-Msg \"llm.apikey_url_tokenplan\")\n }\n Write-Log \"\"\n $script:config.LLM_API_KEY = Read-Prompt -VarName \"AGENTTEAMS_LLM_API_KEY\" -PromptText (Get-Msg \"llm.apikey_prompt\") -Secret\n if ($script:StepResult -eq \"back\") { return }\n if ($alibabaAccess -eq \"bailian\") {\n Test-LlmConnectivity -BaseUrl \"https://dashscope.aliyuncs.com/compatible-mode/v1\" -ApiKey $script:config.LLM_API_KEY -Model $script:config.DEFAULT_MODEL\n } elseif ($alibabaAccess -eq \"codingplan_legacy\") {\n Test-LlmConnectivity -BaseUrl \"https://coding.dashscope.aliyuncs.com/v1\" -ApiKey $script:config.LLM_API_KEY -Model $script:config.DEFAULT_MODEL -Hint (Get-Msg \"llm.openai.test.fail.codingplan_legacy\")\n } elseif ($script:AGENTTEAMS_LANGUAGE -eq \"en\") {\n Test-LlmConnectivity -BaseUrl $script:config.OPENAI_BASE_URL -ApiKey $script:config.LLM_API_KEY -Model $script:config.DEFAULT_MODEL -Hint (Get-Msg \"llm.openai.test.fail.codingplan\")\n } else {\n Test-LlmConnectivity -BaseUrl $script:config.OPENAI_BASE_URL -ApiKey $script:config.LLM_API_KEY -Model $script:config.DEFAULT_MODEL -Hint (Get-Msg \"llm.openai.test.fail.tokenplan\")\n }\n if ($script:StepResult -eq \"back\") { return }\n }\n \"^(2|openai-compat)$\" {\n $script:config.LLM_PROVIDER = \"openai-compat\"\n Write-Log (Get-Msg \"llm.provider.selected_openai\" -f $script:config.LLM_PROVIDER)\n Write-Host \"\"\n if ($script:AGENTTEAMS_UPGRADE -and $env:AGENTTEAMS_UPGRADE_KEEP_ALL -eq \"1\") {\n Write-Log (Get-Msg \"prompt.upgrade_keep\" -f (Get-Msg \"llm.openai.base_url_prompt\"), $script:config.OPENAI_BASE_URL)\n } else {\n $currentUrl = $script:config.OPENAI_BASE_URL\n # Use base prompt without embedded default when showing current value\n if ($currentUrl) {\n $urlInput = Read-Host \"$(Get-Msg 'llm.openai.base_url_prompt') [$currentUrl]\"\n } else {\n $urlInput = Read-Host \"$(Get-Msg 'llm.openai.base_url_prompt') [https://api.openai.com/v1]\"\n }\n if ($urlInput -eq \"b\") { $script:StepResult = \"back\"; return }\n if ($urlInput) {\n $script:config.OPENAI_BASE_URL = $urlInput\n } elseif ($currentUrl) {\n $script:config.OPENAI_BASE_URL = $currentUrl\n } else {\n $script:config.OPENAI_BASE_URL = \"https://api.openai.com/v1\"\n }\n }\n if ($script:AGENTTEAMS_UPGRADE -and $env:AGENTTEAMS_UPGRADE_KEEP_ALL -eq \"1\") {\n Write-Log (Get-Msg \"prompt.upgrade_keep\" -f (Get-Msg \"llm.openai.model_prompt\"), $script:config.DEFAULT_MODEL)\n } else {\n $currentModel = $script:config.DEFAULT_MODEL\n if ($currentModel) {\n $modelInput = Read-Host \"$(Get-Msg 'llm.openai.model_prompt') [${currentModel}]\"\n } else {\n $modelInput = Read-Host (Get-Msg 'llm.openai.model_prompt')\n }\n if ($modelInput -eq \"b\") { $script:StepResult = \"back\"; return }\n if ($modelInput) {\n $script:config.DEFAULT_MODEL = $modelInput\n } elseif ($currentModel) {\n $script:config.DEFAULT_MODEL = $currentModel\n } else {\n $script:config.DEFAULT_MODEL = \"gpt-5.4\"\n }\n }\n Write-Log (Get-Msg \"llm.openai.base_url_label\" -f $script:config.OPENAI_BASE_URL)\n Write-Log (Get-Msg \"llm.model.label\" -f $script:config.DEFAULT_MODEL)\n Request-CustomModelParams $script:config.DEFAULT_MODEL\n if ($script:StepResult -eq \"back\") { return }\n Write-Log \"\"\n $script:config.LLM_API_KEY = Read-Prompt -VarName \"AGENTTEAMS_LLM_API_KEY\" -PromptText (Get-Msg \"llm.apikey_prompt\") -Secret\n if ($script:StepResult -eq \"back\") { return }\n Test-LlmConnectivity -BaseUrl $script:config.OPENAI_BASE_URL -ApiKey $script:config.LLM_API_KEY -Model $script:config.DEFAULT_MODEL\n if ($script:StepResult -eq \"back\") { return }\n }\n default {\n Write-Error (Get-Msg \"llm.provider.invalid\" -f $providerChoice)\n }\n }\n\n # Skip to embedding if Keep-All mode handled LLM params\n if ($skipToEmbedding) {\n # Skip embedding selection, use loaded value (already in $script:config.EMBEDDING_MODEL)\n if ($script:config.EMBEDDING_MODEL) {\n Write-Log (Get-Msg \"llm.embedding.title\")\n Write-Log (Get-Msg \"llm.model.label\" -f $script:config.EMBEDDING_MODEL)\n Write-Log \"\"\n }\n return\n }\n\n # --- Embedding model (optional, auto-tested) ---\n Write-Host \"\"\n Write-Log (Get-Msg \"llm.embedding.title\")\n Write-Log (Get-Msg \"llm.embedding.hint\")\n Write-Host \"\"\n Write-Host (Get-Msg \"llm.embedding.option.default\")\n Write-Host (Get-Msg \"llm.embedding.option.custom\")\n Write-Host (Get-Msg \"llm.embedding.option.disable\")\n Write-Host \"\"\n $embChoice = Read-Host \"$(Get-Msg 'llm.embedding.select') [1]\"\n $embChoice = if ($embChoice) { $embChoice } else { \"1\" }\n if ($embChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n\n switch ($embChoice) {\n \"1\" {\n $script:config.EMBEDDING_MODEL = \"text-embedding-v4\"\n }\n \"2\" {\n if ($script:AGENTTEAMS_UPGRADE -and $env:AGENTTEAMS_UPGRADE_KEEP_ALL -eq \"1\") {\n Write-Log (Get-Msg \"prompt.upgrade_keep\" -f (Get-Msg \"llm.embedding.custom_prompt\"), $script:config.EMBEDDING_MODEL)\n } else {\n $currentEmb = $script:config.EMBEDDING_MODEL\n $embCustom = Read-Host (Get-Msg \"llm.embedding.custom_prompt\")\n if ($embCustom -eq \"b\") { $script:StepResult = \"back\"; return }\n if ($embCustom) {\n $script:config.EMBEDDING_MODEL = $embCustom\n } else {\n $script:config.EMBEDDING_MODEL = $currentEmb\n if (-not $script:config.EMBEDDING_MODEL) {\n Write-Log (Get-Msg \"llm.embedding.disabled\")\n }\n }\n }\n }\n \"3\" {\n $script:config.EMBEDDING_MODEL = \"\"\n Write-Log (Get-Msg \"llm.embedding.disabled\")\n }\n default {\n $script:config.EMBEDDING_MODEL = \"text-embedding-v4\"\n }\n }\n\n if ($script:config.EMBEDDING_MODEL) {\n # Qwen provider uses dashscope directly; others use OPENAI_BASE_URL\n $embBaseUrl = $script:config.OPENAI_BASE_URL\n if ($script:config.LLM_PROVIDER -eq \"qwen\") {\n $embBaseUrl = \"https://dashscope.aliyuncs.com/compatible-mode/v1\"\n }\n $embResult = Test-EmbeddingConnectivity -BaseUrl $embBaseUrl -ApiKey $script:config.LLM_API_KEY -Model $script:config.EMBEDDING_MODEL\n if (-not $embResult) {\n $script:config.EMBEDDING_MODEL = \"\"\n Write-Log (Get-Msg \"llm.embedding.auto_disabled\")\n }\n }\n\n Write-Log \"\"\n}\n\nfunction Step-Admin {\n Write-Log (Get-Msg \"admin.title\")\n $script:config.ADMIN_USER = Read-Prompt -VarName \"AGENTTEAMS_ADMIN_USER\" -PromptText (Get-Msg \"admin.username_prompt\") -Default \"admin\"\n if ($script:StepResult -eq \"back\") { return }\n $script:config.ADMIN_USER = $script:config.ADMIN_USER.ToLowerInvariant()\n\n # Pre-set via env var: validate; non-interactive fails fast,\n # interactive warns and falls through to the retry prompt.\n if ($env:AGENTTEAMS_ADMIN_PASSWORD) {\n $script:config.ADMIN_PASSWORD = $env:AGENTTEAMS_ADMIN_PASSWORD\n Write-Log (Get-Msg \"prompt.preset\" -f (Get-Msg \"admin.password_prompt\"))\n if ($script:config.ADMIN_PASSWORD.Length -ge 8) {\n Write-Log \"\"\n return\n }\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n Write-Error (Get-Msg \"admin.password_too_short\" -f $script:config.ADMIN_PASSWORD.Length)\n }\n Write-Host \"$($script:ESC)[31m[AgentTeams ERROR]$($script:ESC)[0m $(Get-Msg \"admin.password_too_short\" -f $script:config.ADMIN_PASSWORD.Length)\"\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_ADMIN_PASSWORD\", $null, \"Process\")\n }\n\n while ($true) {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_ADMIN_PASSWORD\", $null, \"Process\")\n $script:config.ADMIN_PASSWORD = Read-Prompt -VarName \"AGENTTEAMS_ADMIN_PASSWORD\" -PromptText (Get-Msg \"admin.password_prompt\") -Secret -Optional\n if ($script:StepResult -eq \"back\") { return }\n if (-not $script:config.ADMIN_PASSWORD) {\n $randomSuffix = (New-RandomKey).Substring(0, 12)\n $script:config.ADMIN_PASSWORD = \"admin$randomSuffix\"\n Write-Log (Get-Msg \"admin.password_generated\")\n break\n }\n if ($script:config.ADMIN_PASSWORD.Length -ge 8) {\n break\n }\n Write-Host \"$($script:ESC)[31m[AgentTeams ERROR]$($script:ESC)[0m $(Get-Msg \"admin.password_too_short\" -f $script:config.ADMIN_PASSWORD.Length)\"\n }\n\n Write-Log \"\"\n}\n\nfunction Step-Network {\n Write-Log (Get-Msg \"port.local_only.title\")\n Write-Host \"\"\n Write-Host \" 1) $(Get-Msg 'port.local_only.hint_yes')\"\n Write-Host \" 2) $(Get-Msg 'port.local_only.hint_no')\"\n Write-Host \"\"\n\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n $localOnly = if ($env:AGENTTEAMS_LOCAL_ONLY) { $env:AGENTTEAMS_LOCAL_ONLY } else { \"1\" }\n } elseif ($null -ne $env:AGENTTEAMS_LOCAL_ONLY) {\n $localOnly = $env:AGENTTEAMS_LOCAL_ONLY\n } else {\n $localChoice = Read-Host \"$(Get-Msg 'port.local_only.choice')\"\n if ($localChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n if (-not $localChoice) { $localChoice = \"1\" }\n $localOnly = if ($localChoice -match '^(2|n|N|no|NO)$') { \"0\" } else { \"1\" }\n }\n $script:config.LOCAL_ONLY = $localOnly\n\n if ($localOnly -eq \"1\") {\n Write-Log (Get-Msg \"port.local_only.selected_local\")\n } else {\n Write-Log (Get-Msg \"port.local_only.selected_external\")\n Write-Host \"\"\n Write-Host (Get-Msg \"port.local_only.https_hint\") -ForegroundColor Yellow\n }\n Write-Log \"\"\n}\n\nfunction Step-Ports {\n Write-Log (Get-Msg \"port.title\")\n $script:config.PORT_GATEWAY = Read-Prompt -VarName \"AGENTTEAMS_PORT_GATEWAY\" -PromptText (Get-Msg \"port.gateway_prompt\") -Default \"18080\"\n if ($script:StepResult -eq \"back\") { return }\n $script:config.PORT_CONSOLE = Read-Prompt -VarName \"AGENTTEAMS_PORT_CONSOLE\" -PromptText (Get-Msg \"port.console_prompt\") -Default \"18001\"\n if ($script:StepResult -eq \"back\") { return }\n $script:config.PORT_ELEMENT_WEB = Read-Prompt -VarName \"AGENTTEAMS_PORT_ELEMENT_WEB\" -PromptText (Get-Msg \"port.element_prompt\") -Default \"18088\"\n if ($script:StepResult -eq \"back\") { return }\n $script:config.PORT_MANAGER_CONSOLE = Read-Prompt -VarName \"AGENTTEAMS_PORT_MANAGER_CONSOLE\" -PromptText (Get-Msg \"port.manager_console_prompt\") -Default \"18888\"\n if ($script:StepResult -eq \"back\") { return }\n Write-Log \"\"\n}\n\nfunction Step-Domains {\n Write-Log (Get-Msg \"domain.title\")\n Write-Log (Get-Msg \"domain.hint\")\n $script:config.MATRIX_DOMAIN = Read-Prompt -VarName \"AGENTTEAMS_MATRIX_DOMAIN\" -PromptText (Get-Msg \"domain.matrix_prompt\") -Default \"matrix-local.agentteams.io:$($script:config.PORT_GATEWAY)\"\n if ($script:StepResult -eq \"back\") { return }\n $script:config.MATRIX_CLIENT_DOMAIN = Read-Prompt -VarName \"AGENTTEAMS_MATRIX_CLIENT_DOMAIN\" -PromptText (Get-Msg \"domain.element_prompt\") -Default \"matrix-client-local.agentteams.io\"\n if ($script:StepResult -eq \"back\") { return }\n $script:config.AI_GATEWAY_DOMAIN = Read-Prompt -VarName \"AGENTTEAMS_AI_GATEWAY_DOMAIN\" -PromptText (Get-Msg \"domain.gateway_prompt\") -Default \"aigw-local.agentteams.io\"\n if ($script:StepResult -eq \"back\") { return }\n $script:config.FS_DOMAIN = Read-Prompt -VarName \"AGENTTEAMS_FS_DOMAIN\" -PromptText (Get-Msg \"domain.fs_prompt\") -Default \"fs-local.agentteams.io\"\n if ($script:StepResult -eq \"back\") { return }\n $script:config.CONSOLE_DOMAIN = Read-Prompt -VarName \"AGENTTEAMS_CONSOLE_DOMAIN\" -PromptText (Get-Msg \"domain.console_prompt\") -Default \"console-local.agentteams.io\"\n if ($script:StepResult -eq \"back\") { return }\n Write-Log \"\"\n}\n\nfunction Step-Github {\n Write-Log (Get-Msg \"github.title\")\n $script:config.GITHUB_TOKEN = Read-Prompt -VarName \"AGENTTEAMS_GITHUB_TOKEN\" -PromptText (Get-Msg \"github.token_prompt\") -Secret -Optional\n}\n\nfunction Step-Skills {\n Write-Log \"\"\n Write-Log (Get-Msg \"skills.title\")\n $script:config.SKILLS_API_URL = Read-Prompt -VarName \"AGENTTEAMS_SKILLS_API_URL\" -PromptText (Get-Msg \"skills.url_prompt\") -Optional\n if ($script:StepResult -eq \"back\") { return }\n Write-Log \"\"\n}\n\nfunction Step-Volume {\n Write-Log (Get-Msg \"data.title\")\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n $script:config.DATA_DIR = if ($env:AGENTTEAMS_DATA_DIR) { $env:AGENTTEAMS_DATA_DIR } else { \"agentteams-data\" }\n Write-Log \" $(Get-Msg 'data.volume_using' -f $script:config.DATA_DIR) (non-interactive, skipped)\"\n return\n }\n # ─────────────────────────────────────────────────────────────────\n $dataDirInput = Read-Host (Get-Msg \"data.volume_prompt\")\n if ($dataDirInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $script:config.DATA_DIR = if ($dataDirInput) { $dataDirInput } else { \"agentteams-data\" }\n Write-Log (Get-Msg \"data.volume_using\" -f $script:config.DATA_DIR)\n}\n\nfunction Step-Workspace {\n Write-Log (Get-Msg \"workspace.title\")\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n $script:config.WORKSPACE_DIR = if ($env:AGENTTEAMS_WORKSPACE_DIR) { $env:AGENTTEAMS_WORKSPACE_DIR } else { \"$env:USERPROFILE\\agentteams-manager\" }\n if (-not (Test-Path $script:config.WORKSPACE_DIR)) {\n New-Item -ItemType Directory -Path $script:config.WORKSPACE_DIR -Force | Out-Null\n }\n Write-Log \" $(Get-Msg 'workspace.dir_label' -f $script:config.WORKSPACE_DIR) (non-interactive, skipped)\"\n return\n }\n # ─────────────────────────────────────────────────────────────────\n $defaultWorkspace = \"$env:USERPROFILE\\agentteams-manager\"\n if ($script:AGENTTEAMS_UPGRADE -and $env:AGENTTEAMS_UPGRADE_KEEP_ALL -eq \"1\") {\n Write-Log (Get-Msg \"prompt.upgrade_keep\" -f (Get-Msg \"workspace.dir_prompt\" -f $defaultWorkspace), $script:config.WORKSPACE_DIR)\n $wsInput = if ($script:config.WORKSPACE_DIR) { $script:config.WORKSPACE_DIR } else { $defaultWorkspace }\n } else {\n $currentWs = $script:config.WORKSPACE_DIR\n $wsInput = Read-Host (Get-Msg \"workspace.dir_prompt\" -f $defaultWorkspace)\n if ($wsInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $wsInput = if ($wsInput) { $wsInput } else { if ($currentWs) { $currentWs } else { $defaultWorkspace } }\n }\n $script:config.WORKSPACE_DIR = $wsInput\n if (-not (Test-Path $script:config.WORKSPACE_DIR)) {\n New-Item -ItemType Directory -Path $script:config.WORKSPACE_DIR -Force | Out-Null\n }\n Write-Log (Get-Msg \"workspace.dir_label\" -f $script:config.WORKSPACE_DIR)\n}\n\nfunction Step-Runtime {\n Write-Log (Get-Msg \"worker_runtime.title\")\n Write-Host \"\"\n Write-Host \" 1) $(Get-Msg 'worker_runtime.copaw')\"\n Write-Host \" 2) $(Get-Msg 'worker_runtime.openclaw')\"\n Write-Host \" 3) $(Get-Msg 'worker_runtime.hermes')\"\n Write-Host \"\"\n\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n $script:config.DEFAULT_WORKER_RUNTIME = if ($env:AGENTTEAMS_DEFAULT_WORKER_RUNTIME) { $env:AGENTTEAMS_DEFAULT_WORKER_RUNTIME } else { \"copaw\" }\n } elseif ($script:AGENTTEAMS_UPGRADE -and $env:AGENTTEAMS_DEFAULT_WORKER_RUNTIME) {\n Write-Log (Get-Msg \"prompt.upgrade_keep\" -f (Get-Msg \"worker_runtime.title_short\"), $env:AGENTTEAMS_DEFAULT_WORKER_RUNTIME)\n $rtChoice = Read-Host (Get-Msg \"worker_runtime.choice\")\n if ($rtChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n if ($rtChoice) {\n $script:config.DEFAULT_WORKER_RUNTIME = switch ($rtChoice) {\n \"2\" { \"openclaw\" }\n \"3\" { \"hermes\" }\n default { \"copaw\" }\n }\n } else {\n $script:config.DEFAULT_WORKER_RUNTIME = $env:AGENTTEAMS_DEFAULT_WORKER_RUNTIME\n }\n } elseif ($env:AGENTTEAMS_DEFAULT_WORKER_RUNTIME) {\n $script:config.DEFAULT_WORKER_RUNTIME = $env:AGENTTEAMS_DEFAULT_WORKER_RUNTIME\n } else {\n $rtChoice = Read-Host (Get-Msg \"worker_runtime.choice\")\n if ($rtChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n $rtChoice = if ($rtChoice) { $rtChoice } else { \"1\" }\n $script:config.DEFAULT_WORKER_RUNTIME = switch ($rtChoice) {\n \"2\" { \"openclaw\" }\n \"3\" { \"hermes\" }\n default { \"copaw\" }\n }\n }\n Write-Log (Get-Msg \"worker_runtime.selected\" -f $script:config.DEFAULT_WORKER_RUNTIME)\n}\n\nfunction Step-ManagerRuntime {\n Write-Log (Get-Msg \"manager_runtime.title\")\n Write-Host \"\"\n Write-Host \" 1) $(Get-Msg 'manager_runtime.copaw')\"\n Write-Host \" 2) $(Get-Msg 'manager_runtime.openclaw')\"\n Write-Host \"\"\n\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n $script:config.MANAGER_RUNTIME = if ($env:AGENTTEAMS_MANAGER_RUNTIME) { $env:AGENTTEAMS_MANAGER_RUNTIME } else { \"copaw\" }\n } elseif ($script:AGENTTEAMS_UPGRADE -and $env:AGENTTEAMS_MANAGER_RUNTIME) {\n Write-Log (Get-Msg \"prompt.upgrade_keep\" -f (Get-Msg \"manager_runtime.title_short\"), $env:AGENTTEAMS_MANAGER_RUNTIME)\n $mrChoice = Read-Host (Get-Msg \"manager_runtime.choice\")\n if ($mrChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n if ($mrChoice) {\n $script:config.MANAGER_RUNTIME = if ($mrChoice -eq \"2\") { \"openclaw\" } else { \"copaw\" }\n } else {\n $script:config.MANAGER_RUNTIME = $env:AGENTTEAMS_MANAGER_RUNTIME\n }\n } elseif ($env:AGENTTEAMS_MANAGER_RUNTIME) {\n $script:config.MANAGER_RUNTIME = $env:AGENTTEAMS_MANAGER_RUNTIME\n } else {\n $mrChoice = Read-Host (Get-Msg \"manager_runtime.choice\")\n if ($mrChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n $mrChoice = if ($mrChoice) { $mrChoice } else { \"1\" }\n $script:config.MANAGER_RUNTIME = if ($mrChoice -eq \"2\") { \"openclaw\" } else { \"copaw\" }\n }\n Write-Log (Get-Msg \"manager_runtime.selected\" -f $script:config.MANAGER_RUNTIME)\n}\n\nfunction Step-E2ee {\n Write-Host \"\"\n Write-Log (Get-Msg \"matrix_e2ee.title\")\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n $script:config.MATRIX_E2EE = if ($env:AGENTTEAMS_MATRIX_E2EE) { $env:AGENTTEAMS_MATRIX_E2EE } else { \"0\" }\n Write-Log \" $(Get-Msg 'matrix_e2ee.title_short') = $($script:config.MATRIX_E2EE) (non-interactive, skipped)\"\n return\n }\n # ─────────────────────────────────────────────────────────────────\n Write-Host \"\"\n Write-Host \" $(Get-Msg 'matrix_e2ee.desc')\"\n Write-Host \"\"\n Write-Host \" 1) $(Get-Msg 'matrix_e2ee.disable')\"\n Write-Host \" 2) $(Get-Msg 'matrix_e2ee.enable')\"\n Write-Host \"\"\n\n if ($script:AGENTTEAMS_UPGRADE -and $env:AGENTTEAMS_MATRIX_E2EE) {\n $e2eeDisplay = if ($env:AGENTTEAMS_MATRIX_E2EE -eq \"1\") { Get-Msg \"matrix_e2ee.val_enabled\" } else { Get-Msg \"matrix_e2ee.val_disabled\" }\n Write-Log (Get-Msg \"prompt.upgrade_keep\" -f (Get-Msg \"matrix_e2ee.title_short\"), $e2eeDisplay)\n $e2eeChoice = Read-Host (Get-Msg \"matrix_e2ee.choice\")\n if ($e2eeChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n if ($e2eeChoice) {\n $script:config.MATRIX_E2EE = if ($e2eeChoice -eq \"2\") { \"1\" } else { \"0\" }\n } else {\n $script:config.MATRIX_E2EE = $env:AGENTTEAMS_MATRIX_E2EE\n }\n } elseif (-not $env:AGENTTEAMS_MATRIX_E2EE) {\n $e2eeChoice = Read-Host (Get-Msg \"matrix_e2ee.choice\")\n if ($e2eeChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n $e2eeChoice = if ($e2eeChoice) { $e2eeChoice } else { \"1\" }\n $script:config.MATRIX_E2EE = if ($e2eeChoice -eq \"2\") { \"1\" } else { \"0\" }\n } else {\n $script:config.MATRIX_E2EE = $env:AGENTTEAMS_MATRIX_E2EE\n }\n\n if ($script:config.MATRIX_E2EE -eq \"1\") {\n Write-Log (Get-Msg \"matrix_e2ee.selected_enabled\")\n } else {\n Write-Log (Get-Msg \"matrix_e2ee.selected_disabled\")\n }\n}\n\nfunction Step-DockerProxy {\n if (-not $script:AGENTTEAMS_MOUNT_SOCKET) {\n $script:config.DOCKER_PROXY = \"0\"\n return\n }\n\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n $script:config.DOCKER_PROXY = if ($env:AGENTTEAMS_DOCKER_PROXY) { $env:AGENTTEAMS_DOCKER_PROXY } else { \"0\" }\n Write-Log \" $(Get-Msg 'docker_proxy.title_short') = $($script:config.DOCKER_PROXY) (non-interactive, skipped)\"\n return\n }\n # ─────────────────────────────────────────────────────────────────\n\n Write-Host \"\"\n Write-Log (Get-Msg \"docker_proxy.title\")\n Write-Host \"\"\n Write-Host \" $(Get-Msg 'docker_proxy.desc')\"\n Write-Host \"\"\n Write-Host \" 1) $(Get-Msg 'docker_proxy.enable')\"\n Write-Host \" 2) $(Get-Msg 'docker_proxy.disable')\"\n Write-Host \"\"\n\n if ($script:AGENTTEAMS_UPGRADE -and $env:AGENTTEAMS_DOCKER_PROXY) {\n $proxyDisplay = if ($env:AGENTTEAMS_DOCKER_PROXY -eq \"1\") { Get-Msg \"docker_proxy.val_enabled\" } else { Get-Msg \"docker_proxy.val_disabled\" }\n Write-Log (Get-Msg \"prompt.upgrade_keep\" -f (Get-Msg \"docker_proxy.title_short\"), $proxyDisplay)\n $proxyChoice = Read-Host (Get-Msg \"docker_proxy.choice\")\n if ($proxyChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n if ($proxyChoice) {\n $script:config.DOCKER_PROXY = if ($proxyChoice -eq \"2\") { \"0\" } else { \"1\" }\n } else {\n $script:config.DOCKER_PROXY = $env:AGENTTEAMS_DOCKER_PROXY\n }\n } elseif (-not $env:AGENTTEAMS_DOCKER_PROXY) {\n $proxyChoice = Read-Host (Get-Msg \"docker_proxy.choice\")\n if ($proxyChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n $proxyChoice = if ($proxyChoice) { $proxyChoice } else { \"1\" }\n $script:config.DOCKER_PROXY = if ($proxyChoice -eq \"2\") { \"0\" } else { \"1\" }\n } else {\n $script:config.DOCKER_PROXY = $env:AGENTTEAMS_DOCKER_PROXY\n }\n\n if ($script:config.DOCKER_PROXY -eq \"1\") {\n Write-Log (Get-Msg \"docker_proxy.selected_enabled\")\n\n # Prompt for additional allowed image sources\n Write-Host \"\"\n Write-Host \" $(Get-Msg 'docker_proxy.registries_desc')\"\n Write-Host \"\"\n if ($script:AGENTTEAMS_UPGRADE -and $env:AGENTTEAMS_PROXY_ALLOWED_REGISTRIES) {\n Write-Log (Get-Msg \"prompt.upgrade_keep\" -f (Get-Msg \"docker_proxy.registries_label\"), $env:AGENTTEAMS_PROXY_ALLOWED_REGISTRIES)\n $regInput = Read-Host (Get-Msg \"docker_proxy.registries_prompt\")\n if ($regInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $script:config.PROXY_ALLOWED_REGISTRIES = if ($regInput) { $regInput } else { $env:AGENTTEAMS_PROXY_ALLOWED_REGISTRIES }\n } elseif (-not $env:AGENTTEAMS_PROXY_ALLOWED_REGISTRIES) {\n $regInput = Read-Host (Get-Msg \"docker_proxy.registries_prompt\")\n if ($regInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $script:config.PROXY_ALLOWED_REGISTRIES = if ($regInput) { $regInput } else { \"\" }\n } else {\n $script:config.PROXY_ALLOWED_REGISTRIES = $env:AGENTTEAMS_PROXY_ALLOWED_REGISTRIES\n }\n } else {\n Write-Log (Get-Msg \"docker_proxy.selected_disabled\")\n }\n}\n\nfunction Step-Idle {\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n $script:config.WORKER_IDLE_TIMEOUT = if ($env:AGENTTEAMS_WORKER_IDLE_TIMEOUT) { $env:AGENTTEAMS_WORKER_IDLE_TIMEOUT } else { \"720\" }\n Write-Log \" $(Get-Msg 'idle_timeout.label') = $($script:config.WORKER_IDLE_TIMEOUT) (non-interactive, skipped)\"\n return\n }\n # ─────────────────────────────────────────────────────────────────\n if ($script:AGENTTEAMS_UPGRADE -and $env:AGENTTEAMS_WORKER_IDLE_TIMEOUT) {\n Write-Log (Get-Msg \"prompt.upgrade_keep\" -f (Get-Msg \"idle_timeout.label\"), $env:AGENTTEAMS_WORKER_IDLE_TIMEOUT)\n $idleInput = Read-Host (Get-Msg \"idle_timeout.prompt\")\n if ($idleInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $script:config.WORKER_IDLE_TIMEOUT = if ($idleInput) { $idleInput } else { $env:AGENTTEAMS_WORKER_IDLE_TIMEOUT }\n } elseif (-not $env:AGENTTEAMS_WORKER_IDLE_TIMEOUT) {\n $idleInput = Read-Host (Get-Msg \"idle_timeout.prompt\")\n if ($idleInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $script:config.WORKER_IDLE_TIMEOUT = if ($idleInput) { $idleInput } else { \"720\" }\n } else {\n $script:config.WORKER_IDLE_TIMEOUT = $env:AGENTTEAMS_WORKER_IDLE_TIMEOUT\n }\n Write-Log (Get-Msg \"idle_timeout.selected\" -f $script:config.WORKER_IDLE_TIMEOUT)\n Write-Log \"\"\n}\n\nfunction Step-Hostshare {\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n $script:config.HOST_SHARE_DIR = if ($env:AGENTTEAMS_HOST_SHARE_DIR) { $env:AGENTTEAMS_HOST_SHARE_DIR } else { $env:USERPROFILE }\n Write-Log \" $(Get-Msg 'host_share.label') = $($script:config.HOST_SHARE_DIR) (non-interactive, skipped)\"\n return\n }\n # ─────────────────────────────────────────────────────────────────\n $currentShare = $script:config.HOST_SHARE_DIR\n if ($currentShare) {\n $shareInput = Read-Host \"$(Get-Msg 'host_share.prompt' -f $env:USERPROFILE) [${currentShare}]\"\n } else {\n $shareInput = Read-Host (Get-Msg 'host_share.prompt' -f $env:USERPROFILE)\n }\n if ($shareInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $script:config.HOST_SHARE_DIR = if ($shareInput) { $shareInput } else { if ($currentShare) { $currentShare } else { $env:USERPROFILE } }\n}\n\n# ============================================================\n# Manager Installation\n# ============================================================\n\nfunction Install-Manager {\n Write-Log (Get-Msg \"install.title\")\n\n # Detect timezone\n $script:AGENTTEAMS_TIMEZONE = Get-AgentTeamsTimeZone\n\n # Language priority: env var > existing env file > timezone detection\n if ($env:AGENTTEAMS_LANGUAGE) {\n $script:AGENTTEAMS_LANGUAGE = $env:AGENTTEAMS_LANGUAGE\n } else {\n # Check existing env file for saved language preference (upgrade scenario)\n $_envFile = $script:AGENTTEAMS_ENV_FILE\n # Migrate from legacy location (current directory) if needed\n if (-not (Test-Path $_envFile) -and (Test-Path \".\\agentteams-manager.env\")) {\n Write-Log \"Migrating agentteams-manager.env to $_envFile...\"\n Move-Item \".\\agentteams-manager.env\" $_envFile -ErrorAction SilentlyContinue\n }\n if (Test-Path $_envFile) {\n $_savedLang = (Get-Content $_envFile | Select-String \"^AGENTTEAMS_LANGUAGE=\" | ForEach-Object {\n $_.Line -replace '^AGENTTEAMS_LANGUAGE=', ''\n } | Select-Object -First 1)\n if ($_savedLang) {\n $script:AGENTTEAMS_LANGUAGE = $_savedLang\n }\n }\n # Fall back to timezone-based detection\n if (-not $script:AGENTTEAMS_LANGUAGE) {\n $script:AGENTTEAMS_LANGUAGE = Get-AgentTeamsLanguage -Timezone $script:AGENTTEAMS_TIMEZONE\n }\n }\n $env:AGENTTEAMS_LANGUAGE = $script:AGENTTEAMS_LANGUAGE\n\n # Detect registry\n $script:AGENTTEAMS_REGISTRY = Get-Registry -Timezone $script:AGENTTEAMS_TIMEZONE\n\n # Set image names\n $script:MANAGER_IMAGE = if ($env:AGENTTEAMS_INSTALL_MANAGER_IMAGE) {\n $env:AGENTTEAMS_INSTALL_MANAGER_IMAGE\n } else {\n \"$($script:AGENTTEAMS_REGISTRY)/agentteams/agentteams-manager:$($script:AGENTTEAMS_VERSION)\"\n }\n\n $script:WORKER_IMAGE = if ($env:AGENTTEAMS_INSTALL_WORKER_IMAGE) {\n $env:AGENTTEAMS_INSTALL_WORKER_IMAGE\n } else {\n \"$($script:AGENTTEAMS_REGISTRY)/agentteams/agentteams-worker:$($script:AGENTTEAMS_VERSION)\"\n }\n\n $script:COPAW_WORKER_IMAGE = if ($env:AGENTTEAMS_INSTALL_COPAW_WORKER_IMAGE) {\n $env:AGENTTEAMS_INSTALL_COPAW_WORKER_IMAGE\n } else {\n \"$($script:AGENTTEAMS_REGISTRY)/agentteams/agentteams-copaw-worker:$($script:AGENTTEAMS_VERSION)\"\n }\n\n $script:QWENPAW_WORKER_IMAGE = if ($env:AGENTTEAMS_INSTALL_QWENPAW_WORKER_IMAGE) {\n $env:AGENTTEAMS_INSTALL_QWENPAW_WORKER_IMAGE\n } else {\n \"$($script:AGENTTEAMS_REGISTRY)/agentteams/agentteams-qwenpaw-worker:$($script:AGENTTEAMS_VERSION)\"\n }\n\n $script:HERMES_WORKER_IMAGE = if ($env:AGENTTEAMS_INSTALL_HERMES_WORKER_IMAGE) {\n $env:AGENTTEAMS_INSTALL_HERMES_WORKER_IMAGE\n } else {\n \"$($script:AGENTTEAMS_REGISTRY)/agentteams/agentteams-hermes-worker:$($script:AGENTTEAMS_VERSION)\"\n }\n\n $script:MANAGER_COPAW_IMAGE = if ($env:AGENTTEAMS_INSTALL_MANAGER_COPAW_IMAGE) {\n $env:AGENTTEAMS_INSTALL_MANAGER_COPAW_IMAGE\n } else {\n \"$($script:AGENTTEAMS_REGISTRY)/agentteams/agentteams-manager-copaw:$($script:AGENTTEAMS_VERSION)\"\n }\n\n $script:CONTROLLER_IMAGE = if ($env:AGENTTEAMS_INSTALL_CONTROLLER_IMAGE) {\n $env:AGENTTEAMS_INSTALL_CONTROLLER_IMAGE\n } else {\n \"$($script:AGENTTEAMS_REGISTRY)/agentteams/agentteams-controller:$($script:AGENTTEAMS_VERSION)\"\n }\n\n # Resolve embedded controller image (sets $script:EMBEDDED_IMAGE and\n # $script:AGENTTEAMS_USE_EMBEDDED). Errors out fast if no embedded image is available\n # for the requested version (mirrors the bash installer behavior).\n Resolve-EmbeddedImage\n\n Write-Log (Get-Msg \"install.registry\" -f $script:AGENTTEAMS_REGISTRY)\n Write-Log \"\"\n Write-Log (Get-Msg \"install.dir\" -f (Get-Location))\n Write-Log (Get-Msg \"install.dir_hint\")\n Write-Log (Get-Msg \"install.dir_hint2\")\n Write-Log \"\"\n\n # Check container runtime (docker or podman)\n if (-not (Get-Command \"docker\" -ErrorAction SilentlyContinue) -and\n -not (Get-Command \"podman\" -ErrorAction SilentlyContinue)) {\n Write-Host \"$($script:ESC)[31m[AgentTeams ERROR]$($script:ESC)[0m $(Get-Msg 'error.docker_not_found')\" -ForegroundColor Red\n Exit-Script 1\n }\n\n if (-not (Test-DockerRunning)) {\n Write-Host \"$($script:ESC)[31m[AgentTeams ERROR]$($script:ESC)[0m $(Get-Msg 'error.docker_not_running')\" -ForegroundColor Red\n Exit-Script 1\n }\n\n # Initialize shared config hashtable\n $script:config = @{}\n $config = $script:config # local alias so post-machine code can use $config\n\n # Orphan volume detection (env file gone but volume remains)\n if (-not (Test-Path $script:AGENTTEAMS_ENV_FILE)) {\n $dataVol = if ($env:AGENTTEAMS_DATA_DIR) { $env:AGENTTEAMS_DATA_DIR } else { \"agentteams-data\" }\n $volumeExists = docker volume ls -q 2>$null | Select-String \"^${dataVol}$\"\n if ($volumeExists) {\n Write-Host \"\"\n Write-Log (Get-Msg \"install.orphan_volume.detected\" -f $dataVol)\n Write-Log (Get-Msg \"install.orphan_volume.warn\")\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n Write-Log (Get-Msg \"install.orphan_volume.clean_noninteractive\")\n docker stop agentteams-manager *>$null\n docker rm agentteams-manager *>$null\n docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-worker-\" | ForEach-Object {\n docker stop $_.ToString().Trim() *>$null\n docker rm $_.ToString().Trim() *>$null\n }\n Write-Log (Get-Msg \"install.orphan_volume.cleaning\")\n docker volume rm $dataVol *>$null\n Write-Log (Get-Msg \"install.orphan_volume.cleaned\")\n } else {\n Write-Host \"\"\n Write-Host (Get-Msg \"install.orphan_volume.choose\")\n Write-Host (Get-Msg \"install.orphan_volume.clean\")\n Write-Host (Get-Msg \"install.orphan_volume.keep\")\n Write-Host \"\"\n $orphanChoice = Read-Host (Get-Msg \"install.orphan_volume.prompt\")\n if (-not $orphanChoice) { $orphanChoice = \"1\" }\n switch -Regex ($orphanChoice) {\n \"^(1|clean)$\" {\n docker stop agentteams-manager *>$null\n docker rm agentteams-manager *>$null\n docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-worker-\" | ForEach-Object {\n docker stop $_.ToString().Trim() *>$null\n docker rm $_.ToString().Trim() *>$null\n }\n Write-Log (Get-Msg \"install.orphan_volume.cleaning\")\n docker volume rm $dataVol *>$null\n Write-Log (Get-Msg \"install.orphan_volume.cleaned\")\n }\n \"^(2|keep)$\" {\n Write-Log (Get-Msg \"install.orphan_volume.keeping\")\n }\n }\n }\n }\n }\n\n # ── State machine ─────────────────────────────────────────────────────────\n $_steps = @(\"Step-Lang\", \"Step-Mode\", \"Step-Existing\", \"Step-Llm\", \"Step-ManagerRuntime\", \"Step-Runtime\", \"Step-Admin\",\n \"Step-Network\", \"Step-Ports\", \"Step-Domains\", \"Step-Github\", \"Step-Skills\",\n \"Step-Volume\", \"Step-Workspace\", \"Step-E2ee\", \"Step-DockerProxy\", \"Step-Idle\",\n \"Step-Hostshare\")\n $_stepHistory = [System.Collections.Generic.List[int]]::new()\n $_stepIdx = 0\n while ($_stepIdx -lt $_steps.Count) {\n $_stepFn = $_steps[$_stepIdx]\n if (Test-ShouldSkipStep $_stepFn) {\n $_stepIdx++\n continue\n }\n if ($_stepHistory.Count -gt 0) {\n Write-Log (Get-Msg \"nav.back_hint\")\n }\n $script:StepResult = \"\"\n & $_stepFn\n if ($script:StepResult -eq \"back\") {\n if ($_stepHistory.Count -gt 0) {\n $_stepIdx = $_stepHistory[$_stepHistory.Count - 1]\n $_stepHistory.RemoveAt($_stepHistory.Count - 1)\n Clear-StepVars $_steps[$_stepIdx]\n }\n # else: first step, ignore 'b'\n } else {\n $_stepHistory.Add($_stepIdx)\n $_stepIdx++\n }\n }\n # ── End state machine ──────────────────────────────────────────────────────\n\n # Post-machine defaults for any steps that were skipped\n if (-not $script:config.DATA_DIR) {\n $script:config.DATA_DIR = if ($env:AGENTTEAMS_DATA_DIR) { $env:AGENTTEAMS_DATA_DIR } else { \"agentteams-data\" }\n }\n Write-Log (Get-Msg \"data.volume_using\" -f $script:config.DATA_DIR)\n if (-not $script:config.WORKSPACE_DIR) {\n $_defaultWs = \"$env:USERPROFILE\\agentteams-manager\"\n $script:config.WORKSPACE_DIR = if ($env:AGENTTEAMS_WORKSPACE_DIR) { $env:AGENTTEAMS_WORKSPACE_DIR } else { $_defaultWs }\n if (-not (Test-Path $script:config.WORKSPACE_DIR)) {\n New-Item -ItemType Directory -Path $script:config.WORKSPACE_DIR -Force | Out-Null\n }\n Write-Log (Get-Msg \"workspace.dir_label\" -f $script:config.WORKSPACE_DIR)\n }\n if (-not $script:config.DEFAULT_WORKER_RUNTIME) {\n $script:config.DEFAULT_WORKER_RUNTIME = if ($env:AGENTTEAMS_DEFAULT_WORKER_RUNTIME) { $env:AGENTTEAMS_DEFAULT_WORKER_RUNTIME } else { \"copaw\" }\n Write-Log (Get-Msg \"worker_runtime.selected\" -f $script:config.DEFAULT_WORKER_RUNTIME)\n }\n if (-not $script:config.MATRIX_E2EE) {\n $script:config.MATRIX_E2EE = if ($env:AGENTTEAMS_MATRIX_E2EE) { $env:AGENTTEAMS_MATRIX_E2EE } else { \"0\" }\n }\n if (-not $script:config.WORKER_IDLE_TIMEOUT) {\n $script:config.WORKER_IDLE_TIMEOUT = if ($env:AGENTTEAMS_WORKER_IDLE_TIMEOUT) { $env:AGENTTEAMS_WORKER_IDLE_TIMEOUT } else { \"720\" }\n }\n if (-not $script:config.HOST_SHARE_DIR) {\n $script:config.HOST_SHARE_DIR = if ($env:AGENTTEAMS_HOST_SHARE_DIR) { $env:AGENTTEAMS_HOST_SHARE_DIR } else { $env:USERPROFILE }\n }\n if (-not $script:config.LOCAL_ONLY) {\n $script:config.LOCAL_ONLY = if ($env:AGENTTEAMS_LOCAL_ONLY) { $env:AGENTTEAMS_LOCAL_ONLY } else { \"1\" }\n }\n if (-not $script:config.MANAGER_RUNTIME) {\n $script:config.MANAGER_RUNTIME = if ($env:AGENTTEAMS_MANAGER_RUNTIME) { $env:AGENTTEAMS_MANAGER_RUNTIME } else { \"copaw\" }\n }\n if (-not $script:config.PORT_GATEWAY) {\n $script:config.PORT_GATEWAY = if ($env:AGENTTEAMS_PORT_GATEWAY) { $env:AGENTTEAMS_PORT_GATEWAY } else { \"18080\" }\n $script:config.PORT_CONSOLE = if ($env:AGENTTEAMS_PORT_CONSOLE) { $env:AGENTTEAMS_PORT_CONSOLE } else { \"18001\" }\n $script:config.PORT_ELEMENT_WEB = if ($env:AGENTTEAMS_PORT_ELEMENT_WEB) { $env:AGENTTEAMS_PORT_ELEMENT_WEB } else { \"18088\" }\n $script:config.PORT_MANAGER_CONSOLE = if ($env:AGENTTEAMS_PORT_MANAGER_CONSOLE) { $env:AGENTTEAMS_PORT_MANAGER_CONSOLE } else { \"18888\" }\n }\n if (-not $script:config.MATRIX_DOMAIN) {\n $script:config.MATRIX_DOMAIN = if ($env:AGENTTEAMS_MATRIX_DOMAIN) { $env:AGENTTEAMS_MATRIX_DOMAIN } else { \"matrix-local.agentteams.io:$($script:config.PORT_GATEWAY)\" }\n $script:config.MATRIX_CLIENT_DOMAIN = if ($env:AGENTTEAMS_MATRIX_CLIENT_DOMAIN) { $env:AGENTTEAMS_MATRIX_CLIENT_DOMAIN } else { \"matrix-client-local.agentteams.io\" }\n $script:config.AI_GATEWAY_DOMAIN = if ($env:AGENTTEAMS_AI_GATEWAY_DOMAIN) { $env:AGENTTEAMS_AI_GATEWAY_DOMAIN } else { \"aigw-local.agentteams.io\" }\n $script:config.FS_DOMAIN = if ($env:AGENTTEAMS_FS_DOMAIN) { $env:AGENTTEAMS_FS_DOMAIN } else { \"fs-local.agentteams.io\" }\n $script:config.CONSOLE_DOMAIN = if ($env:AGENTTEAMS_CONSOLE_DOMAIN) { $env:AGENTTEAMS_CONSOLE_DOMAIN } else { \"console-local.agentteams.io\" }\n }\n\n Write-Log \"\"\n\n # Generate secrets\n Write-Log (Get-Msg \"install.generating_secrets\")\n $config.MANAGER_PASSWORD = if ($env:AGENTTEAMS_MANAGER_PASSWORD) { $env:AGENTTEAMS_MANAGER_PASSWORD } else { New-RandomKey }\n $config.REGISTRATION_TOKEN = if ($env:AGENTTEAMS_REGISTRATION_TOKEN) { $env:AGENTTEAMS_REGISTRATION_TOKEN } else { New-RandomKey }\n $config.MINIO_USER = if ($env:AGENTTEAMS_MINIO_USER) { $env:AGENTTEAMS_MINIO_USER } else { $config.ADMIN_USER }\n $config.MINIO_PASSWORD = if ($env:AGENTTEAMS_MINIO_PASSWORD) { $env:AGENTTEAMS_MINIO_PASSWORD } else { $config.ADMIN_PASSWORD }\n $config.MANAGER_GATEWAY_KEY = if ($env:AGENTTEAMS_MANAGER_GATEWAY_KEY) { $env:AGENTTEAMS_MANAGER_GATEWAY_KEY } else { New-RandomKey }\n\n # Store additional config\n $config.LANGUAGE = $script:AGENTTEAMS_LANGUAGE\n $config.REGISTRY = $script:AGENTTEAMS_REGISTRY\n $config.WORKER_IMAGE = $script:WORKER_IMAGE\n $config.COPAW_WORKER_IMAGE = $script:COPAW_WORKER_IMAGE\n $config.QWENPAW_WORKER_IMAGE = $script:QWENPAW_WORKER_IMAGE\n $config.HERMES_WORKER_IMAGE = $script:HERMES_WORKER_IMAGE\n $config.MANAGER_COPAW_IMAGE = $script:MANAGER_COPAW_IMAGE\n\n # Write env file\n New-EnvFile -Config $config -Path $script:AGENTTEAMS_ENV_FILE\n\n # Manager image selection (used by both embedded — passed to controller via env —\n # and legacy — used directly as `docker run` target).\n $managerImage = if ($config.MANAGER_RUNTIME -eq \"copaw\") { $script:MANAGER_COPAW_IMAGE } else { $script:MANAGER_IMAGE }\n $portPrefix = if ($config.LOCAL_ONLY -eq \"1\") { \"127.0.0.1:\" } else { \"\" }\n\n # Ensure agentteams-net Docker network exists. Used in both modes — the embedded\n # controller and the spawned manager/worker containers all join it and rely on\n # network aliases for *-local.agentteams.io DNS resolution.\n if ($script:AGENTTEAMS_MOUNT_SOCKET) {\n $socketAvailable = Test-DockerRunning\n if ($socketAvailable) {\n docker network inspect agentteams-net *>$null\n if ($LASTEXITCODE -ne 0) { docker network create agentteams-net *>$null }\n } else {\n Write-Log (Get-Msg \"install.socket_not_found\")\n if (-not $script:AGENTTEAMS_NON_INTERACTIVE) {\n Write-Host \"\"\n Write-Host \"$($script:ESC)[33m$(Get-Msg 'install.socket_confirm.title')$($script:ESC)[0m\"\n Write-Host \"\"\n Write-Host (Get-Msg 'install.socket_confirm.message')\n Write-Host \"\"\n $confirm = Read-Host (Get-Msg 'install.socket_confirm.prompt')\n if ($confirm -ne 'y' -and $confirm -ne 'Y') {\n Write-Log (Get-Msg 'install.socket_confirm.cancelled')\n exit 0\n }\n }\n }\n }\n\n if ($script:AGENTTEAMS_USE_EMBEDDED -ne \"1\") {\n # ============================================================\n # Legacy architecture: all-in-one manager container\n # (only entered when AGENTTEAMS_FORCE_LEGACY=1 — broken with the slim manager\n # image shipped since PR #616; kept solely for AGENTTEAMS_VERSION <= v1.0.9)\n # ============================================================\n\n $dockerArgs = @(\n \"run\", \"-d\",\n \"--name\", \"agentteams-manager\",\n \"--env-file\", $script:AGENTTEAMS_ENV_FILE,\n \"-e\", \"HOME=/root/manager-workspace\",\n \"-w\", \"/root/manager-workspace\",\n \"-e\", \"HOST_ORIGINAL_HOME=$($config.HOST_SHARE_DIR)\",\n \"-e\", \"AGENTTEAMS_MANAGER_RUNTIME=$($config.MANAGER_RUNTIME)\"\n )\n\n $dockerArgs += @(\"-e\", \"TZ=$($script:AGENTTEAMS_TIMEZONE)\")\n\n if ($script:AGENTTEAMS_MOUNT_SOCKET -and (Test-DockerRunning)) {\n $dockerArgs += @(\"--network\", \"agentteams-net\")\n $dockerArgs += @(\"--network-alias\", \"matrix-local.agentteams.io\")\n $dockerArgs += @(\"--network-alias\", \"aigw-local.agentteams.io\")\n $dockerArgs += @(\"--network-alias\", \"fs-local.agentteams.io\")\n foreach ($domain in @($config.MATRIX_CLIENT_DOMAIN, $config.CONSOLE_DOMAIN)) {\n if ($domain -match '-local\\.agentteams\\.io$') {\n $dockerArgs += @(\"--network-alias\", $domain)\n }\n }\n\n if ($config.DOCKER_PROXY -eq \"1\") {\n $proxyImage = $script:CONTROLLER_IMAGE\n Write-Log \"Starting Docker API proxy...\"\n docker rm -f agentteams-controller *>$null\n docker run -d --name agentteams-controller `\n --network agentteams-net `\n -v \"//var/run/docker.sock:/var/run/docker.sock\" `\n --security-opt label=disable `\n -e \"AGENTTEAMS_WORKER_IMAGE=$($script:WORKER_IMAGE)\" `\n -e \"AGENTTEAMS_COPAW_WORKER_IMAGE=$($script:COPAW_WORKER_IMAGE)\" `\n -e \"AGENTTEAMS_QWENPAW_WORKER_IMAGE=$($script:QWENPAW_WORKER_IMAGE)\" `\n -e \"AGENTTEAMS_HERMES_WORKER_IMAGE=$($script:HERMES_WORKER_IMAGE)\" `\n -e \"AGENTTEAMS_DEFAULT_WORKER_RUNTIME=$($script:config.DEFAULT_WORKER_RUNTIME)\" `\n $(if ($config.PROXY_ALLOWED_REGISTRIES) { @(\"-e\", \"AGENTTEAMS_PROXY_ALLOWED_REGISTRIES=$($config.PROXY_ALLOWED_REGISTRIES)\") }) `\n --restart unless-stopped `\n $proxyImage\n $dockerArgs += @(\"-e\", \"AGENTTEAMS_CONTROLLER_URL=http://agentteams-controller:8090\")\n $dockerArgs += @(\"-e\", \"AGENTTEAMS_CONTAINER_API=http://agentteams-controller:8090\")\n Write-Log (Get-Msg \"docker_proxy.selected_enabled\")\n } else {\n $dockerArgs += @(\"-v\", \"//var/run/docker.sock:/var/run/docker.sock\")\n $dockerArgs += @(\"--security-opt\", \"label=disable\")\n Write-Log (Get-Msg \"install.socket_detected\" -f \"//var/run/docker.sock\")\n }\n }\n\n $dockerArgs += @(\"-p\", \"${portPrefix}$($config.PORT_GATEWAY):8080\")\n $dockerArgs += @(\"-p\", \"${portPrefix}$($config.PORT_CONSOLE):8001\")\n $dockerArgs += @(\"-p\", \"${portPrefix}$($config.PORT_ELEMENT_WEB):8088\")\n $dockerArgs += @(\"-p\", \"127.0.0.1:$($config.PORT_MANAGER_CONSOLE):18888\")\n\n $dockerArgs += @(\"-v\", \"$($config.DATA_DIR):/data\")\n\n $wsDockerPath = ConvertTo-DockerPath -Path $config.WORKSPACE_DIR\n $dockerArgs += @(\"-v\", \"${wsDockerPath}:/root/manager-workspace\")\n\n $shareDockerPath = ConvertTo-DockerPath -Path $config.HOST_SHARE_DIR\n $dockerArgs += @(\"-v\", \"${shareDockerPath}:/host-share\")\n Write-Log (Get-Msg \"host_share.sharing\" -f $config.HOST_SHARE_DIR)\n\n if ($env:AGENTTEAMS_YOLO -eq \"1\") {\n $dockerArgs += @(\"-e\", \"AGENTTEAMS_YOLO=1\")\n Write-Log (Get-Msg \"install.yolo\")\n }\n\n if ($env:AGENTTEAMS_MATRIX_DEBUG -eq \"1\") {\n $dockerArgs += @(\"-e\", \"AGENTTEAMS_MATRIX_DEBUG=1\")\n }\n\n $dockerArgs += @(\"--restart\", \"unless-stopped\")\n $dockerArgs += $managerImage\n }\n\n # Check if the Docker volume exists; create if not (reuse on reinstall) — both modes.\n $volumeExists = docker volume ls -q 2>$null | Select-String \"^$($config.DATA_DIR)$\"\n if (-not $volumeExists) {\n docker volume create $config.DATA_DIR | Out-Null\n }\n\n # Pull images (skip if already exists locally for local build tags).\n $LocalImagePattern = \"^agentteams/\"\n function Test-LocalImage {\n param([string]$Image)\n $imgExists = docker image inspect $Image 2>$null\n return $LASTEXITCODE -eq 0\n }\n if ($script:AGENTTEAMS_USE_EMBEDDED -eq \"1\") {\n # Embedded image was already pulled by Resolve-EmbeddedImage unless overridden;\n # for an explicit override we still need to ensure it is present locally.\n if ($env:AGENTTEAMS_INSTALL_EMBEDDED_IMAGE) {\n if ($script:EMBEDDED_IMAGE -match $LocalImagePattern) {\n if (-not (Test-LocalImage $script:EMBEDDED_IMAGE)) {\n Write-Log \"Pulling embedded image: $($script:EMBEDDED_IMAGE)\"\n & docker pull $script:EMBEDDED_IMAGE\n }\n } else {\n Write-Log \"Pulling embedded image: $($script:EMBEDDED_IMAGE)\"\n & docker pull $script:EMBEDDED_IMAGE\n }\n }\n # Manager image — controller will spawn it inside; pull here so the spawn doesn't\n # have to wait on the network.\n if ($managerImage -match $LocalImagePattern) {\n if (Test-LocalImage $managerImage) {\n Write-Log (Get-Msg \"install.image.exists\" -f $managerImage)\n } else {\n Write-Log (Get-Msg \"install.image.pulling_manager\" -f $managerImage)\n & docker pull $managerImage\n }\n } else {\n Write-Log (Get-Msg \"install.image.pulling_manager\" -f $managerImage)\n & docker pull $managerImage\n }\n } else {\n if ($managerImage -match $LocalImagePattern) {\n if (Test-LocalImage $managerImage) {\n Write-Log (Get-Msg \"install.image.exists\" -f $managerImage)\n } else {\n Write-Log (Get-Msg \"install.image.pulling_manager\" -f $managerImage)\n & docker pull $managerImage\n }\n } else {\n Write-Log (Get-Msg \"install.image.pulling_manager\" -f $managerImage)\n & docker pull $managerImage\n }\n }\n\n # Pull all worker runtime images (workers may use any runtime regardless of the default)\n foreach ($workerImg in @($script:WORKER_IMAGE, $script:COPAW_WORKER_IMAGE, $script:QWENPAW_WORKER_IMAGE, $script:HERMES_WORKER_IMAGE)) {\n if ($workerImg -match $LocalImagePattern) {\n if (Test-LocalImage $workerImg) {\n Write-Log (Get-Msg \"install.image.worker_exists\" -f $workerImg)\n } else {\n Write-Log (Get-Msg \"install.image.pulling_worker\" -f $workerImg)\n & docker pull $workerImg\n }\n } else {\n Write-Log (Get-Msg \"install.image.pulling_worker\" -f $workerImg)\n & docker pull $workerImg\n }\n }\n\n # --- Pre-upgrade: extract Matrix passwords from old containers (old-arch -> embedded) ---\n $credsTmp = $null\n if ($script:AGENTTEAMS_UPGRADE -and $script:AGENTTEAMS_USE_EMBEDDED -eq \"1\") {\n $controllerNameHit = docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-controller$\"\n $isOldArch = $false\n if (-not $controllerNameHit) {\n $isOldArch = $true\n } else {\n $ctrlImgLine = docker ps -a --format \"{{.Names}} {{.Image}}\" 2>$null | Where-Object { $_ -match '^agentteams-controller ' } | Select-Object -First 1\n if ($ctrlImgLine -and ($ctrlImgLine -notmatch 'embedded')) {\n $isOldArch = $true\n }\n }\n\n if ($isOldArch) {\n $credsTmp = Join-Path ([System.IO.Path]::GetTempPath()) (\"agentteams-upgrade-creds-\" + [Guid]::NewGuid().ToString(\"n\"))\n New-Item -ItemType Directory -Path $credsTmp -Force | Out-Null\n $utf8NoBom = New-Object System.Text.UTF8Encoding $false\n\n $mgrCredsTempStart = $false\n $mgrExists = docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-manager$\"\n $mgrRunning = docker ps --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-manager$\"\n if ($mgrExists -and -not $mgrRunning) {\n Write-Log \"agentteams-manager is stopped; starting it temporarily to extract Matrix credentials for upgrade...\"\n docker start agentteams-manager 2>$null | Out-Null\n Wait-MatrixReady -Container \"agentteams-manager\"\n $mgrCredsTempStart = $true\n }\n\n $inspectLines = docker inspect agentteams-manager --format \"{{range .Config.Env}}{{println .}}{{end}}\" 2>$null\n $mgrPw = \"\"\n if ($inspectLines) {\n $envLine = $inspectLines -split \"`n\" | Where-Object { $_ -match '^AGENTTEAMS_MANAGER_PASSWORD=' } | Select-Object -First 1\n if ($envLine) {\n $mgrPw = ($envLine -replace '^AGENTTEAMS_MANAGER_PASSWORD=', \"\").Trim()\n }\n }\n $mgrRunningNow = docker ps --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-manager$\"\n if ([string]::IsNullOrEmpty($mgrPw) -and $mgrRunningNow) {\n $mgrPw = docker exec agentteams-manager bash -c 'source /data/agentteams-secrets.env 2>/dev/null && echo \"${AGENTTEAMS_MANAGER_PASSWORD}\"' 2>$null\n if ($mgrPw) { $mgrPw = $mgrPw.Trim() }\n }\n $dataVolPresent = docker volume ls -q 2>$null | Where-Object { $_ -eq $config.DATA_DIR }\n if ([string]::IsNullOrEmpty($mgrPw) -and $dataVolPresent) {\n $mgrPw = Read-AgentTeamsSecretFromDataVolume -VolumeName $config.DATA_DIR -Key \"AGENTTEAMS_MANAGER_PASSWORD\"\n }\n\n $envFilePath = $script:AGENTTEAMS_ENV_FILE\n $mgrRoom = \"\"\n if (-not [string]::IsNullOrEmpty($mgrPw)) {\n $adminPw = \"\"\n $adminUser = \"admin\"\n if (Test-Path $envFilePath) {\n $adminLine = Get-Content $envFilePath | Where-Object { $_ -match '^AGENTTEAMS_ADMIN_PASSWORD=' } | Select-Object -First 1\n if ($adminLine) { $adminPw = ($adminLine -replace '^AGENTTEAMS_ADMIN_PASSWORD=', \"\").Trim() }\n $userLine = Get-Content $envFilePath | Where-Object { $_ -match '^AGENTTEAMS_ADMIN_USER=' } | Select-Object -First 1\n if ($userLine) {\n $adminUser = ($userLine -replace '^AGENTTEAMS_ADMIN_USER=', \"\").Trim()\n if ([string]::IsNullOrEmpty($adminUser)) { $adminUser = \"admin\" }\n }\n }\n\n if (-not [string]::IsNullOrEmpty($adminPw) -and $mgrRunningNow) {\n $mgrRoom = Get-AgentTeamsAdminDmRoomViaMatrix -AdminUser $adminUser -AdminPassword $adminPw\n }\n if ([string]::IsNullOrEmpty($mgrRoom)) {\n $mgrRoom = Read-AgentTeamsAdminDmRoomFromWorkspace -WorkspaceDir $config.WORKSPACE_DIR\n }\n\n $gwKeyLine = if (Test-Path $envFilePath) {\n Get-Content $envFilePath | Where-Object { $_ -match '^AGENTTEAMS_MANAGER_GATEWAY_KEY=' } | Select-Object -First 1\n } else { $null }\n $gwKeyForDefault = if ($gwKeyLine) { ($gwKeyLine -replace '^AGENTTEAMS_MANAGER_GATEWAY_KEY=', \"\").Trim() } else { $config.MANAGER_GATEWAY_KEY }\n\n $defaultEnvPath = Join-Path $credsTmp \"default.env\"\n [System.IO.File]::WriteAllLines($defaultEnvPath, @(\n \"WORKER_PASSWORD=$mgrPw\"\n \"WORKER_MINIO_PASSWORD=$(Get-AgentTeamsRandomHex 24)\"\n \"WORKER_GATEWAY_KEY=$gwKeyForDefault\"\n \"WORKER_ROOM_ID=$mgrRoom\"\n ), $utf8NoBom)\n if ($mgrRoom) {\n Write-Log \"Extracted Manager Matrix password and room ID\"\n } else {\n Write-Log \"Extracted Manager Matrix password\"\n }\n }\n\n if ($mgrCredsTempStart) {\n Write-Log \"Stopping agentteams-manager after credential extraction (upgrade will recreate containers)...\"\n docker stop agentteams-manager 2>$null | Out-Null\n }\n }\n }\n\n # Stop and remove existing containers (deferred until after all\n # configuration is collected and images are pulled successfully)\n $existingProxy = docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-controller$\"\n if ($existingProxy) {\n docker stop agentteams-controller *>$null\n docker rm agentteams-controller *>$null\n }\n $existingContainer = docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-manager$\"\n if ($existingContainer) {\n Write-Log (Get-Msg \"install.removing_existing\")\n docker stop agentteams-manager *>$null\n docker rm agentteams-manager *>$null\n }\n\n # Stop and remove worker containers (controller will recreate via CR reconciliation)\n if ($script:UPGRADE_EXISTING_WORKERS) {\n Write-Log (Get-Msg \"install.existing.stopping_workers\")\n $script:UPGRADE_EXISTING_WORKERS | ForEach-Object {\n docker stop $_ *>$null\n docker rm $_ *>$null\n Write-Log (Get-Msg \"install.existing.removed\" -f $_)\n }\n }\n\n # --- Upgrade: inject extracted credentials into data volume (old-arch -> embedded) ---\n if ($credsTmp -and (Test-Path $credsTmp)) {\n $credsFiles = Get-ChildItem -Path $credsTmp -Filter \"*.env\" -ErrorAction SilentlyContinue\n if ($credsFiles -and $credsFiles.Count -gt 0) {\n $cleanupCtr = \"agentteams-upgrade-cleanup\"\n docker rm -f $cleanupCtr 2>$null | Out-Null\n $credsDockerPath = ConvertTo-DockerPath -Path $credsTmp\n $injectShell = \"rm -rf /data/worker-creds && mkdir -p /data/worker-creds && cp /creds/*.env /data/worker-creds/ 2>/dev/null || true && chmod 600 /data/worker-creds/*.env 2>/dev/null || true\"\n docker run --rm --name $cleanupCtr --entrypoint sh `\n -v \"$($config.DATA_DIR):/data\" `\n -v \"${credsDockerPath}:/creds:ro\" `\n $script:EMBEDDED_IMAGE `\n -c $injectShell 2>$null | Out-Null\n if ($LASTEXITCODE -eq 0) {\n Write-Log \"Injected credentials for upgrade\"\n } else {\n Write-Log \"Warning: credential injection failed, continuing\"\n }\n }\n Remove-Item -Path $credsTmp -Recurse -Force -ErrorAction SilentlyContinue\n }\n\n if ($script:AGENTTEAMS_USE_EMBEDDED -eq \"1\") {\n # ============================================================\n # New architecture: embedded controller + auto-created manager\n # (controller container hosts Higress / Tuwunel / MinIO / Element Web /\n # controller binary, then spawns the lightweight manager container)\n # ============================================================\n\n # Internal port: 8080 (Higress gateway inside the controller container).\n $internalGwPort = 8080\n\n $matrixDomain = if ($config.MATRIX_DOMAIN) {\n $config.MATRIX_DOMAIN\n } else {\n \"matrix-local.agentteams.io:$($config.PORT_GATEWAY)\"\n }\n $aigwDomain = if ($config.AI_GATEWAY_DOMAIN) { $config.AI_GATEWAY_DOMAIN } else { \"aigw-local.agentteams.io\" }\n if ($aigwDomain -notmatch \":\") { $aigwDomain = \"${aigwDomain}:${internalGwPort}\" }\n $fsDomain = if ($config.FS_DOMAIN) { $config.FS_DOMAIN } else { \"fs-local.agentteams.io\" }\n if ($fsDomain -notmatch \":\") { $fsDomain = \"${fsDomain}:${internalGwPort}\" }\n\n $ctrlArgs = @(\n \"run\", \"-d\",\n \"--name\", \"agentteams-controller\",\n \"--network\", \"agentteams-net\",\n \"--network-alias\", \"matrix-local.agentteams.io\",\n \"--network-alias\", \"aigw-local.agentteams.io\",\n \"--network-alias\", \"fs-local.agentteams.io\",\n \"-e\", \"AGENTTEAMS_ADMIN_USER=$($config.ADMIN_USER)\",\n \"-e\", \"AGENTTEAMS_ADMIN_PASSWORD=$($config.ADMIN_PASSWORD)\",\n \"-e\", \"AGENTTEAMS_MANAGER_PASSWORD=$($config.MANAGER_PASSWORD)\",\n \"-e\", \"AGENTTEAMS_REGISTRATION_TOKEN=$($config.REGISTRATION_TOKEN)\",\n \"-e\", \"AGENTTEAMS_MINIO_USER=$($config.MINIO_USER)\",\n \"-e\", \"AGENTTEAMS_MINIO_PASSWORD=$($config.MINIO_PASSWORD)\",\n \"-e\", \"AGENTTEAMS_LLM_PROVIDER=$($config.LLM_PROVIDER)\",\n \"-e\", \"AGENTTEAMS_LLM_API_KEY=$($config.LLM_API_KEY)\",\n \"-e\", \"AGENTTEAMS_DEFAULT_MODEL=$($config.DEFAULT_MODEL)\",\n \"-e\", \"AGENTTEAMS_MANAGER_GATEWAY_KEY=$($config.MANAGER_GATEWAY_KEY)\",\n \"-e\", \"AGENTTEAMS_MANAGER_RUNTIME=$($config.MANAGER_RUNTIME)\",\n \"-e\", \"AGENTTEAMS_MANAGER_IMAGE=$managerImage\",\n \"-e\", \"AGENTTEAMS_DEFAULT_WORKER_RUNTIME=$($config.DEFAULT_WORKER_RUNTIME)\",\n \"-e\", \"AGENTTEAMS_WORKER_IMAGE=$($script:WORKER_IMAGE)\",\n \"-e\", \"AGENTTEAMS_COPAW_WORKER_IMAGE=$($script:COPAW_WORKER_IMAGE)\",\n \"-e\", \"AGENTTEAMS_QWENPAW_WORKER_IMAGE=$($script:QWENPAW_WORKER_IMAGE)\",\n \"-e\", \"AGENTTEAMS_HERMES_WORKER_IMAGE=$($script:HERMES_WORKER_IMAGE)\",\n \"-e\", \"AGENTTEAMS_MATRIX_DOMAIN=$matrixDomain\",\n \"-e\", \"AGENTTEAMS_ELEMENT_HOMESERVER_URL=http://127.0.0.1:$($config.PORT_GATEWAY)\",\n \"-e\", \"AGENTTEAMS_MATRIX_URL=http://127.0.0.1:6167\",\n \"-e\", \"AGENTTEAMS_MATRIX_E2EE=$($config.MATRIX_E2EE)\",\n \"-e\", \"AGENTTEAMS_MINIO_ENDPOINT=http://127.0.0.1:9000\",\n \"-e\", \"AGENTTEAMS_MINIO_BUCKET=agentteams-storage\",\n \"-e\", \"AGENTTEAMS_STORAGE_PREFIX=agentteams/agentteams-storage\",\n \"-e\", \"AGENTTEAMS_FS_ENDPOINT=http://127.0.0.1:9000\",\n \"-e\", \"AGENTTEAMS_AI_GATEWAY_URL=http://$aigwDomain\",\n \"-e\", \"AGENTTEAMS_CONTROLLER_URL=http://agentteams-controller:8090\",\n \"-e\", \"AGENTTEAMS_DOCKER_NETWORK=agentteams-net\",\n \"-e\", \"AGENTTEAMS_WORKSPACE_DIR=$($config.WORKSPACE_DIR)\",\n \"-e\", \"AGENTTEAMS_HOST_SHARE_DIR=$($config.HOST_SHARE_DIR)\",\n \"-e\", \"AGENTTEAMS_MANAGER_ENABLED=true\",\n \"-e\", \"AGENTTEAMS_PORT_MANAGER_CONSOLE=$($config.PORT_MANAGER_CONSOLE)\"\n )\n\n if ($script:AGENTTEAMS_TIMEZONE) {\n $ctrlArgs += @(\"-e\", \"TZ=$($script:AGENTTEAMS_TIMEZONE)\")\n }\n if ($env:AGENTTEAMS_YOLO -eq \"1\") {\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_YOLO=1\")\n }\n if ($env:AGENTTEAMS_MATRIX_DEBUG -eq \"1\") {\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_MATRIX_DEBUG=1\")\n }\n if ($config.GITHUB_TOKEN) {\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_GITHUB_TOKEN=$($config.GITHUB_TOKEN)\")\n }\n if ($config.EMBEDDING_MODEL) {\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_EMBEDDING_MODEL=$($config.EMBEDDING_MODEL)\")\n }\n if ($config.OPENAI_BASE_URL) {\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_OPENAI_BASE_URL=$($config.OPENAI_BASE_URL)\")\n }\n if ($env:AGENTTEAMS_AI_STREAM_IDLE_TIMEOUT_SECONDS) {\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_AI_STREAM_IDLE_TIMEOUT_SECONDS=$($env:AGENTTEAMS_AI_STREAM_IDLE_TIMEOUT_SECONDS)\")\n }\n if ($script:AGENTTEAMS_LANGUAGE) {\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_LANGUAGE=$($script:AGENTTEAMS_LANGUAGE)\")\n }\n\n # Optional: CMS/ARMS observability. In embedded mode the controller\n # spawns the Manager and Workers, so it must receive these settings.\n $cmsTracesEnabled = if ($env:AGENTTEAMS_CMS_TRACES_ENABLED) { $env:AGENTTEAMS_CMS_TRACES_ENABLED } else { \"false\" }\n $cmsServiceName = if ($env:AGENTTEAMS_CMS_SERVICE_NAME) { $env:AGENTTEAMS_CMS_SERVICE_NAME } else { \"agentteams-manager\" }\n $cmsMetricsEnabled = if ($env:AGENTTEAMS_CMS_METRICS_ENABLED) { $env:AGENTTEAMS_CMS_METRICS_ENABLED } else { \"false\" }\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_CMS_TRACES_ENABLED=$cmsTracesEnabled\")\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_CMS_SERVICE_NAME=$cmsServiceName\")\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_CMS_METRICS_ENABLED=$cmsMetricsEnabled\")\n if ($env:AGENTTEAMS_CMS_ENDPOINT) {\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_CMS_ENDPOINT=$($env:AGENTTEAMS_CMS_ENDPOINT)\")\n }\n if ($env:AGENTTEAMS_CMS_LICENSE_KEY) {\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_CMS_LICENSE_KEY=$($env:AGENTTEAMS_CMS_LICENSE_KEY)\")\n }\n if ($env:AGENTTEAMS_CMS_PROJECT) {\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_CMS_PROJECT=$($env:AGENTTEAMS_CMS_PROJECT)\")\n }\n if ($env:AGENTTEAMS_CMS_WORKSPACE) {\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_CMS_WORKSPACE=$($env:AGENTTEAMS_CMS_WORKSPACE)\")\n }\n\n # Mount the docker socket so the controller can spawn manager + workers.\n $ctrlArgs += @(\"-v\", \"//var/run/docker.sock:/var/run/docker.sock\")\n $ctrlArgs += @(\"--security-opt\", \"label=disable\")\n\n # Persistent data + workspace mounts (manager workspace is bind-mounted under\n # /root/agentteams-fs/agents/manager so the controller can hand it to the spawned\n # manager container).\n $ctrlArgs += @(\"-v\", \"$($config.DATA_DIR):/data\")\n $wsDockerPath = ConvertTo-DockerPath -Path $config.WORKSPACE_DIR\n $ctrlArgs += @(\"-v\", \"${wsDockerPath}:/root/agentteams-fs/agents/manager\")\n Write-Log (Get-Msg \"host_share.sharing\" -f $config.HOST_SHARE_DIR)\n\n # Externally exposed ports — only the gateway / Higress console / Element Web,\n # since the manager console is now spawned inside its own container by the\n # controller (port mapping is handled there).\n $ctrlArgs += @(\"-p\", \"${portPrefix}$($config.PORT_GATEWAY):8080\")\n $ctrlArgs += @(\"-p\", \"${portPrefix}$($config.PORT_CONSOLE):8001\")\n $ctrlArgs += @(\"-p\", \"${portPrefix}$($config.PORT_ELEMENT_WEB):8088\")\n\n $ctrlArgs += @(\"--restart\", \"unless-stopped\")\n $ctrlArgs += $script:EMBEDDED_IMAGE\n\n Write-Log (Get-Msg \"install.starting_manager\")\n & docker $ctrlArgs\n Write-Log \"Embedded controller started: agentteams-controller\"\n\n # Wait for infra inside the controller container.\n function Wait-EmbeddedUrl {\n param([string]$Url, [string]$Container, [int]$MaxWait, [string]$Description)\n $elapsed = 0\n Write-Log \"Waiting for $Description...\"\n while ($elapsed -lt $MaxWait) {\n docker exec $Container curl -sf $Url *>$null\n if ($LASTEXITCODE -eq 0) {\n Write-Log \"$Description is ready (${elapsed}s)\"\n return $true\n }\n Start-Sleep -Seconds 2\n $elapsed += 2\n }\n Write-Host \"$($script:ESC)[31m[AgentTeams ERROR]$($script:ESC)[0m $Description not ready after ${MaxWait}s\" -ForegroundColor Red\n return $false\n }\n\n if (-not (Wait-EmbeddedUrl \"http://127.0.0.1:6167/_tuwunel/server_version\" \"agentteams-controller\" 120 \"Tuwunel (Matrix)\")) { Exit-Script 1 }\n if (-not (Wait-EmbeddedUrl \"http://127.0.0.1:9000/minio/health/live\" \"agentteams-controller\" 60 \"MinIO\")) { Exit-Script 1 }\n if (-not (Wait-EmbeddedUrl \"http://127.0.0.1:8080/status\" \"agentteams-controller\" 120 \"Higress Gateway\")) { Exit-Script 1 }\n\n # Wait for the controller to spawn the Manager Agent container.\n Write-Log \"Waiting for Manager Agent container...\"\n $mgrWait = 0\n $mgrMax = 300\n while ($mgrWait -lt $mgrMax) {\n $found = docker ps --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-manager$\"\n if ($found) {\n Write-Log \"Manager Agent container detected (${mgrWait}s)\"\n break\n }\n Start-Sleep -Seconds 3\n $mgrWait += 3\n }\n if ($mgrWait -ge $mgrMax) {\n Write-Host \"$($script:ESC)[31m[AgentTeams ERROR]$($script:ESC)[0m Manager Agent container not created after ${mgrMax}s\" -ForegroundColor Red\n Write-Log \"Controller logs:\"\n docker exec agentteams-controller tail -30 /var/log/agentteams/agentteams-controller-error.log 2>$null\n Exit-Script 1\n }\n\n # Wait for Manager Agent container to reach `running` state.\n Write-Log \"Waiting for Manager Agent to start...\"\n $agentWait = 0\n while ($agentWait -lt 120) {\n $state = docker inspect --format \"{{.State.Status}}\" agentteams-manager 2>$null\n if ($state -eq \"running\") {\n Write-Log \"Manager Agent is running\"\n break\n }\n Start-Sleep -Seconds 2\n $agentWait += 2\n }\n\n if ($env:AGENTTEAMS_YOLO -eq \"1\") {\n docker exec agentteams-manager touch /root/manager-workspace/yolo-mode 2>$null\n }\n\n # Wait for the Manager Agent's runtime + Matrix to be reachable. In embedded\n # mode Tuwunel lives in `agentteams-controller`, so the Matrix probe must target\n # the controller (the manager only exposes the agent runtime).\n Wait-ManagerReady -Container \"agentteams-manager\"\n Wait-MatrixReady -Container \"agentteams-controller\"\n\n # Wait for the controller to actually deliver the first-boot welcome\n # message. Gated by the controller on (a) Manager joining the DM room\n # and (b) Higress WASM key-auth propagating to /v1/chat/completions for\n # the Manager's gateway key — typically ~45-90s on a fresh install.\n # We poll Manager CR status.welcomeSent via the in-container AgentTeams\n # CLI, exec'd inside agentteams-controller (the source-of-truth container\n # — its bundled CLI binary is always in lockstep with the controller\n # binary serving the HTTP API, since they're the same `go build`\n # output. The agentteams-manager container's CLI may lag the controller\n # across image upgrades and silently drop the welcomeSent field,\n # leaving this loop hung). The controller mints a long-lived admin\n # SA token at startup and writes it to\n # AGENTTEAMS_AUTH_TOKEN_FILE=/var/run/agentteams/cli-token (set as a Dockerfile\n # ENV default), so a bare `docker exec agentteams-controller agt …`\n # auto-discovers both the endpoint and the token. The brief window\n # after container start before bootstrapAdminCLIToken completes is\n # absorbed by the loop's silent retry.\n $hasAgentTeamsCli = $false\n try {\n docker exec agentteams-controller sh -c 'command -v agt' *> $null\n if ($LASTEXITCODE -eq 0) { $hasAgentTeamsCli = $true }\n } catch {}\n\n if ($hasAgentTeamsCli) {\n Write-Log (Get-Msg \"install.welcome_msg.waiting\")\n $welcomeMax = if ($env:AGENTTEAMS_WELCOME_TIMEOUT) { [int]$env:AGENTTEAMS_WELCOME_TIMEOUT } else { 300 }\n $welcomeWait = 0\n $welcomeDone = $false\n while ($welcomeWait -lt $welcomeMax) {\n $wjson = \"\"\n try {\n $wjson = docker exec agentteams-controller `\n agt get managers default -o json 2>$null\n } catch {}\n if ($wjson -and ($wjson -replace '\\s', '') -match '\"welcomeSent\":true') {\n Write-Log (Get-Msg \"install.welcome_msg.confirmed\" $welcomeWait)\n $welcomeDone = $true\n break\n }\n Start-Sleep -Seconds 3\n $welcomeWait += 3\n }\n if (-not $welcomeDone) {\n # Non-fatal: install is still good. Keep going to the success\n # banner so the admin can use Element Web to nudge Manager into\n # onboarding manually (one DM message is enough).\n Write-Log (Get-Msg \"install.welcome_msg.timeout\" $welcomeMax)\n Write-Log (Get-Msg \"install.welcome_msg.timeout_hint\")\n Write-Log (Get-Msg \"install.welcome_msg.timeout_inspect\")\n }\n } else {\n Write-Log (Get-Msg \"install.welcome_msg.poll_unavailable\")\n }\n } else {\n # Run container (legacy path)\n Write-Log (Get-Msg \"install.starting_manager\")\n & docker $dockerArgs\n\n Wait-ManagerReady -Container \"agentteams-manager\"\n Wait-MatrixReady -Container \"agentteams-manager\"\n }\n\n # Create OpenAI-compatible provider if needed\n if ($config.LLM_PROVIDER -eq \"openai-compat\") {\n New-OpenAICompatProvider -BaseUrl $config.OPENAI_BASE_URL -ApiKey $config.LLM_API_KEY -ConsolePort ([int]$config.PORT_CONSOLE)\n }\n\n # Print success message\n Write-Log \"\"\n Write-Log (Get-Msg \"success.title\")\n Write-Log \"\"\n Write-Log (Get-Msg \"success.domains_configured\")\n Write-Log \" $($config.MATRIX_DOMAIN.Split(':')[0]) $($config.MATRIX_CLIENT_DOMAIN) $($config.AI_GATEWAY_DOMAIN) $($config.FS_DOMAIN) $($config.CONSOLE_DOMAIN)\"\n Write-Log \"\"\n\n $lanIP = Get-LanIP\n\n Write-Host \"$($script:ESC)[33m===============================================================$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.open_url')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[1;36m http://127.0.0.1:$($config.PORT_ELEMENT_WEB)/#/login$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.login_with')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.username' -f $config.ADMIN_USER)$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.password' -f $config.ADMIN_PASSWORD)$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.after_login')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.tell_it')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.manager_auto')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m --------------------------------------------------------------- $($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.mobile_title')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $($script:ESC)[0m\"\n if ($lanIP) {\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.mobile_step1')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.mobile_step2' -f \"http://${lanIP}:$($config.PORT_GATEWAY)\")$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.mobile_step3')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.mobile_username' -f $config.ADMIN_USER)$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.mobile_password' -f $config.ADMIN_PASSWORD)$($script:ESC)[0m\"\n } else {\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.mobile_step1')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.mobile_step2_noip' -f $config.PORT_GATEWAY)$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.mobile_noip_hint')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.mobile_step3')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.mobile_username' -f $config.ADMIN_USER)$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.mobile_password' -f $config.ADMIN_PASSWORD)$($script:ESC)[0m\"\n }\n Write-Host \"$($script:ESC)[33m $($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m===============================================================$($script:ESC)[0m\"\n\n Write-Log \"\"\n Write-Log (Get-Msg \"success.other_consoles\")\n Write-Log (Get-Msg \"success.higress_console\" -f $config.PORT_CONSOLE, $config.ADMIN_USER, $config.ADMIN_PASSWORD)\n if ($script:AGENTTEAMS_USE_EMBEDDED -ne \"1\") {\n # In embedded mode the manager runs in its own auto-spawned container with\n # its own console-port mapping handled by the controller, so don't print a\n # host-side URL/credentials hint here.\n Write-Log (Get-Msg \"success.manager_console\" -f $config.PORT_MANAGER_CONSOLE)\n Write-Log (Get-Msg \"success.manager_console_gateway\" -f $config.ADMIN_USER, $config.ADMIN_PASSWORD)\n }\n Write-Log \"\"\n Write-Log (Get-Msg \"success.switch_llm.title\")\n Write-Log (Get-Msg \"success.switch_llm.hint\")\n Write-Log (Get-Msg \"success.switch_llm.docs\")\n Write-Log (Get-Msg \"success.switch_llm.url\")\n Write-Log \"\"\n Write-Log (Get-Msg \"success.tip\")\n Write-Log \"\"\n if ($config.LOCAL_ONLY -ne \"1\") {\n Write-Host (Get-Msg \"port.local_only.https_hint\") -ForegroundColor Yellow\n Write-Log \"\"\n }\n Write-Log (Get-Msg \"success.config_file\" -f $script:AGENTTEAMS_ENV_FILE)\n\n Write-Log (Get-Msg \"success.data_volume\" -f $config.DATA_DIR)\n\n Write-Log (Get-Msg \"success.workspace\" -f $config.WORKSPACE_DIR)\n}\n\n# ============================================================\n# Worker Installation\n# ============================================================\n\nfunction Install-Worker {\n param(\n [string]$Name,\n [string]$Fs,\n [string]$FsKey,\n [string]$FsSecret,\n [switch]$Reset,\n [switch]$FindSkills,\n [string]$SkillsApiUrl\n )\n\n # Validate required parameters\n if (-not $Name) {\n Write-Error (Get-Msg \"error.name_required\")\n }\n if (-not $Fs) {\n Write-Error (Get-Msg \"error.fs_required\")\n }\n if (-not $FsKey) {\n Write-Error (Get-Msg \"error.fs_key_required\")\n }\n if (-not $FsSecret) {\n Write-Error (Get-Msg \"error.fs_secret_required\")\n }\n\n $containerName = \"agentteams-worker-$Name\"\n\n # Handle reset\n if ($Reset) {\n Write-Log (Get-Msg \"worker.resetting\" -f $Name)\n docker stop $containerName *>$null\n docker rm $containerName *>$null\n }\n\n # Check for existing container\n $existing = docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^$containerName$\"\n if ($existing) {\n Write-Error (Get-Msg \"worker.exists\" -f $containerName)\n }\n\n # Detect timezone and registry\n $timezone = Get-AgentTeamsTimeZone\n $registry = Get-Registry -Timezone $timezone\n $workerImage = if ($env:AGENTTEAMS_INSTALL_WORKER_IMAGE) {\n $env:AGENTTEAMS_INSTALL_WORKER_IMAGE\n } else {\n \"$registry/agentteams/agentteams-worker:$($script:AGENTTEAMS_VERSION)\"\n }\n\n Write-Log (Get-Msg \"worker.starting\" -f $Name)\n\n $dockerArgs = @(\n \"run\", \"-d\",\n \"--name\", $containerName,\n \"-e\", \"HOME=/root/agentteams-fs/agents/$Name\",\n \"-w\", \"/root/agentteams-fs/agents/$Name\",\n \"-e\", \"AGENTTEAMS_WORKER_NAME=$Name\",\n \"-e\", \"AGENTTEAMS_FS_ENDPOINT=$Fs\",\n \"-e\", \"AGENTTEAMS_FS_ACCESS_KEY=$FsKey\",\n \"-e\", \"AGENTTEAMS_FS_SECRET_KEY=$FsSecret\"\n )\n\n if (-not $SkillsApiUrl) {\n if ($env:AGENTTEAMS_SKILLS_API_URL) {\n $SkillsApiUrl = $env:AGENTTEAMS_SKILLS_API_URL\n } else {\n $SkillsApiUrl = \"nacos://market.agentteams.io:80/public\"\n }\n }\n\n if ($FindSkills -and $SkillsApiUrl) {\n $dockerArgs += @(\"-e\", \"SKILLS_API_URL=$SkillsApiUrl\")\n Write-Log (Get-Msg \"worker.skills_url\" -f $SkillsApiUrl)\n }\n if ($env:AGENTTEAMS_NACOS_USERNAME) {\n $dockerArgs += @(\"-e\", \"AGENTTEAMS_NACOS_USERNAME=$($env:AGENTTEAMS_NACOS_USERNAME)\")\n }\n if ($env:AGENTTEAMS_NACOS_PASSWORD) {\n $dockerArgs += @(\"-e\", \"AGENTTEAMS_NACOS_PASSWORD=$($env:AGENTTEAMS_NACOS_PASSWORD)\")\n }\n if ($env:AGENTTEAMS_NACOS_TOKEN) {\n $dockerArgs += @(\"-e\", \"AGENTTEAMS_NACOS_TOKEN=$($env:AGENTTEAMS_NACOS_TOKEN)\")\n }\n\n $dockerArgs += @(\"--restart\", \"unless-stopped\", $workerImage)\n\n & docker $dockerArgs\n\n Write-Log \"\"\n Write-Log (Get-Msg \"worker.started\" -f $Name)\n Write-Log (Get-Msg \"worker.container\" -f $containerName)\n Write-Log (Get-Msg \"worker.view_logs\" -f $containerName)\n}\n\n# ============================================================\n# Uninstall\n# ============================================================\n\nfunction Uninstall-AgentTeams {\n Write-Log (Get-Msg \"uninstall.title\")\n\n # Stop and remove manager\n $manager = docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-manager$\"\n if ($manager) {\n Write-Log (Get-Msg \"uninstall.stopping_manager\")\n docker stop agentteams-manager *>$null\n docker rm agentteams-manager *>$null\n }\n\n # Stop and remove workers\n $workers = docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-worker-\"\n if ($workers) {\n Write-Log (Get-Msg \"uninstall.stopping_workers\")\n $workers | ForEach-Object {\n docker stop $_ *>$null\n docker rm $_ *>$null\n Write-Log (Get-Msg \"uninstall.removed\" -f $_)\n }\n }\n\n # Stop and remove docker-proxy (legacy <= v1.0.x; current arch uses\n # agentteams-controller for the same role)\n $proxy = docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-docker-proxy$\"\n if ($proxy) {\n Write-Log (Get-Msg \"uninstall.removing_proxy\")\n docker stop agentteams-docker-proxy *>$null\n docker rm agentteams-docker-proxy *>$null\n }\n\n # Stop and remove the embedded controller container. MUST happen\n # before the `docker volume rm agentteams-data` step below -- in embedded\n # mode agentteams-controller mounts agentteams-data at /data (Tuwunel DB,\n # MinIO state, Higress state, all the room messages), and `volume rm`\n # against an in-use volume fails silently. Skipping this used to\n # leave room/message history behind across \"uninstall + reinstall\"\n # cycles. See PR #692.\n $controller = docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-controller$\"\n if ($controller) {\n Write-Log (Get-Msg \"uninstall.stopping_controller\")\n docker stop agentteams-controller *>$null\n docker rm agentteams-controller *>$null\n }\n\n # Remove Docker volume (read custom name from env file if available)\n $dataVolume = \"agentteams-data\"\n if (Test-Path $script:AGENTTEAMS_ENV_FILE) {\n $envContent = Get-Content $script:AGENTTEAMS_ENV_FILE -ErrorAction SilentlyContinue\n $dataLine = $envContent | Select-String \"^AGENTTEAMS_DATA_DIR=\"\n if ($dataLine) {\n $parsed = ($dataLine -split \"=\", 2)[1]\n if ($parsed) { $dataVolume = $parsed }\n }\n }\n $volume = docker volume ls -q 2>$null | Select-String \"^${dataVolume}$\"\n if ($volume) {\n Write-Log (Get-Msg \"uninstall.removing_volume\")\n docker volume rm $dataVolume *>$null\n }\n\n # Remove Docker network\n $network = docker network ls --format \"{{.Name}}\" 2>$null | Select-String \"^agentteams-net$\"\n if ($network) {\n Write-Log (Get-Msg \"uninstall.removing_network\")\n docker network rm agentteams-net *>$null\n }\n\n # Remove workspace directory\n $workspaceDir = \"$env:USERPROFILE\\agentteams-manager\"\n if (Test-Path $script:AGENTTEAMS_ENV_FILE) {\n $envContent = Get-Content $script:AGENTTEAMS_ENV_FILE -ErrorAction SilentlyContinue\n $wsLine = $envContent | Select-String \"^AGENTTEAMS_WORKSPACE_DIR=\"\n if ($wsLine) {\n $workspaceDir = ($wsLine -split \"=\", 2)[1]\n }\n }\n if ($workspaceDir -and (Test-Path $workspaceDir)) {\n Write-Log (Get-Msg \"uninstall.removing_workspace\" -f $workspaceDir)\n Remove-Item -Recurse -Force $workspaceDir -ErrorAction SilentlyContinue\n }\n\n # Remove env file\n if (Test-Path $script:AGENTTEAMS_ENV_FILE) {\n Write-Log (Get-Msg \"uninstall.removing_env\" -f $script:AGENTTEAMS_ENV_FILE)\n Remove-Item -Force $script:AGENTTEAMS_ENV_FILE\n }\n\n # Remove install log (stop transcript first to release the file)\n if (Test-Path $script:AGENTTEAMS_LOG_FILE) {\n Write-Log (Get-Msg \"uninstall.removing_log\" -f $script:AGENTTEAMS_LOG_FILE)\n try { Stop-Transcript *>$null } catch {}\n Remove-Item -Force $script:AGENTTEAMS_LOG_FILE -ErrorAction SilentlyContinue\n }\n\n Write-Log \"\"\n Write-Log (Get-Msg \"uninstall.done\")\n}\n\n# ============================================================\n# Main Entry Point\n# ============================================================\n\ntry {\n switch ($Command) {\n \"manager\" {\n Install-Manager\n }\n \"worker\" {\n Install-Worker -Name $Name -Fs $Fs -FsKey $FsKey -FsSecret $FsSecret -Reset:$Reset -FindSkills:$FindSkills -SkillsApiUrl $SkillsApiUrl\n }\n \"uninstall\" {\n Uninstall-AgentTeams\n }\n }\n} catch {\n Exit-Script 1\n}\n\n# Stop transcript logging\ntry {\n Stop-Transcript -ErrorAction SilentlyContinue | Out-Null\n} catch {\n # Ignore errors when stopping transcript\n}\n"}, "37c31b77d4e88ca87a1270c61a1e6f659e8023e1": {"t": 1785764739.9410555, "n": 191916, "h": "fcd032347ea6e8a1", "c": "#!/usr/bin/env powershell\n# agentteams-install.ps1 - One-click installation for AgentTeams Manager and Worker on Windows\n# Requires PowerShell 7.0+ (recommended)\n#\n# Usage:\n# .\\agentteams-install.ps1 # Interactive installation (choose Quick Start or Manual)\n# .\\agentteams-install.ps1 manager # Same as above (explicit)\n# .\\agentteams-install.ps1 worker --name <name> ... # Worker installation\n#\n# Onboarding Modes:\n# Quick Start - Fast installation with all default values (recommended)\n# Manual - Customize each option step by step\n#\n# Environment variables (for automation):\n# AGENTTEAMS_NON_INTERACTIVE Skip all prompts, use defaults (default: 0)\n# AGENTTEAMS_LLM_PROVIDER LLM provider (default: openai-compat for zh non-interactive Token Plan; qwen for en)\n# AGENTTEAMS_DEFAULT_MODEL Default model (default: qwen3.6-plus for zh Token Plan and en non-interactive)\n# AGENTTEAMS_OPENAI_BASE_URL OpenAI-compatible base URL (default for zh non-interactive: Alibaba Token Plan endpoint)\n# AGENTTEAMS_LLM_API_KEY LLM API key (required)\n# AGENTTEAMS_ADMIN_USER Admin username (default: admin)\n# AGENTTEAMS_ADMIN_PASSWORD Admin password (auto-generated if not set, min 8 chars)\n# AGENTTEAMS_MATRIX_DOMAIN Matrix domain (default: matrix-local.agentteams.io:18080)\n# AGENTTEAMS_MOUNT_SOCKET Mount container runtime socket (default: 1)\n# AGENTTEAMS_MATRIX_E2EE Matrix E2EE (default: 0, disabled)\n# AGENTTEAMS_DATA_DIR Docker volume name for persistent data (default: agentteams-data)\n# AGENTTEAMS_WORKSPACE_DIR Host directory for manager workspace (default: ~/agentteams-manager)\n# AGENTTEAMS_VERSION Image tag (default: latest)\n# AGENTTEAMS_REGISTRY Image registry (default: auto-detected by timezone)\n# AGENTTEAMS_INSTALL_MANAGER_IMAGE Override manager image (e.g., local build)\n# AGENTTEAMS_INSTALL_WORKER_IMAGE Override worker image (e.g., local build)\n# AGENTTEAMS_INSTALL_COPAW_WORKER_IMAGE Override copaw worker image (e.g., local build)\n# AGENTTEAMS_INSTALL_HERMES_WORKER_IMAGE Override hermes worker image (e.g., local build)\n# AGENTTEAMS_PORT_GATEWAY Host port for Higress gateway (default: 18080)\n# AGENTTEAMS_PORT_CONSOLE Host port for Higress console (default: 18001)\n# AGENTTEAMS_PORT_ELEMENT_WEB Host port for Element Web direct access (default: 18088)\n\n#Requires -Version 5.1\n\n[CmdletBinding()]\nparam(\n [Parameter(Position = 0)]\n [ValidateSet(\"manager\", \"worker\", \"uninstall\")]\n [string]$Command = \"manager\",\n\n # Worker options\n [string]$Name,\n [string]$Fs,\n [string]$FsKey,\n [string]$FsSecret,\n [switch]$Reset,\n [switch]$FindSkills,\n [string]$SkillsApiUrl,\n\n # General options\n [switch]$NonInteractive,\n [string]$EnvFile\n)\n\n# ============================================================\n# Configuration\n# ============================================================\n\n$script:AGENTTEAMS_VERSION = if ($env:AGENTTEAMS_VERSION) { $env:AGENTTEAMS_VERSION } else { \"latest\" }\n$script:AGENTTEAMS_NON_INTERACTIVE = if ($env:AGENTTEAMS_NON_INTERACTIVE -eq \"1\" -or $NonInteractive) { $true } else { $false }\n$script:AGENTTEAMS_MOUNT_SOCKET = if ($env:AGENTTEAMS_MOUNT_SOCKET -eq \"0\") { $false } else { $true }\n$script:AGENTTEAMS_ENV_FILE = if ($EnvFile) { $EnvFile } elseif ($env:AGENTTEAMS_ENV_FILE) { $env:AGENTTEAMS_ENV_FILE } else { \"$env:USERPROFILE\\agentteams-manager.env\" }\n$script:StepResult = \"\" # Used by state machine to signal \"back\" navigation\n$script:config = @{} # Shared config hashtable for step functions\n\n# ANSI escape character for terminal colors\n$script:ESC = [char]0x1B\n\n# ============================================================\n# Log all output to file\n# ============================================================\n\n$script:AGENTTEAMS_LOG_FILE = \"$env:USERPROFILE\\agentteams-install.log\"\n\n# Start transcript for logging (PowerShell's built-in logging mechanism)\ntry {\n Start-Transcript -Path $script:AGENTTEAMS_LOG_FILE -Append -ErrorAction SilentlyContinue | Out-Null\n} catch {\n # If transcript fails, continue without logging\n}\n\nWrite-Host \"\"\nWrite-Host \"========================================\"\nWrite-Host \"AgentTeams Installation Log\"\nWrite-Host \"Started: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')\"\nWrite-Host \"User: $env:USERNAME\"\nWrite-Host \"System: $(hostname)\"\nWrite-Host \"Log file: $($script:AGENTTEAMS_LOG_FILE)\"\nWrite-Host \"========================================\"\nWrite-Host \"\"\n\n# ============================================================\n# Utility Functions\n# ============================================================\n\nfunction Write-Log {\n param([string]$Message)\n Write-Host \"$($script:ESC)[36m[AgentTeams]$($script:ESC)[0m $Message\"\n}\n\nfunction Write-Error {\n param([string]$Message)\n Write-Host \"$($script:ESC)[31m[AgentTeams ERROR]$($script:ESC)[0m $Message\" -ForegroundColor Red\n throw $Message\n}\n\nfunction Write-Warning {\n param([string]$Message)\n Write-Host \"$($script:ESC)[33m[AgentTeams WARNING]$($script:ESC)[0m $Message\"\n}\n\n# Pause before exit on error so user can read the message when running via double-click\nfunction Exit-Script {\n param([int]$ExitCode = 0)\n if ($ExitCode -ne 0 -and -not $script:AGENTTEAMS_NON_INTERACTIVE) {\n Write-Host \"\"\n Write-Host \"Press Enter to exit.\" -NoNewline\n $Host.UI.ReadLine()\n }\n exit $ExitCode\n}\n\nfunction Test-DockerRunning {\n try {\n $null = docker info 2>&1\n if ($LASTEXITCODE -ne 0) {\n return $false\n }\n return $true\n }\n catch {\n return $false\n }\n}\n\nfunction Get-AgentTeamsTimeZone {\n try {\n if ($env:AGENTTEAMS_TIMEZONE) {\n return $env:AGENTTEAMS_TIMEZONE\n }\n\n $tz = (Get-TimeZone).Id\n # Convert Windows timezone to IANA format\n $tzMap = @{\n \"China Standard Time\" = \"Asia/Shanghai\"\n \"Pacific Standard Time\" = \"America/Los_Angeles\"\n \"Mountain Standard Time\" = \"America/Denver\"\n \"Central Standard Time\" = \"America/Chicago\"\n \"Eastern Standard Time\" = \"America/New_York\"\n \"GMT Standard Time\" = \"Europe/London\"\n \"Central European Standard Time\" = \"Europe/Berlin\"\n \"Tokyo Standard Time\" = \"Asia/Tokyo\"\n \"Singapore Standard Time\" = \"Asia/Singapore\"\n \"Korea Standard Time\" = \"Asia/Seoul\"\n \"India Standard Time\" = \"Asia/Kolkata\"\n }\n\n if ($tzMap.ContainsKey($tz)) {\n return $tzMap[$tz]\n }\n return $tz\n }\n catch {\n return \"Asia/Shanghai\"\n }\n}\n\nfunction Get-Registry {\n param([string]$Timezone)\n\n if ($env:AGENTTEAMS_REGISTRY) {\n return $env:AGENTTEAMS_REGISTRY\n }\n\n # Americas\n if ($Timezone -match \"^America/\") {\n return \"higress-registry.us-west-1.cr.aliyuncs.com\"\n }\n\n # Southeast Asia\n if ($Timezone -match \"^(Asia/Singapore|Asia/Bangkok|Asia/Jakarta|Asia/Makassar|Asia/Jayapura|Asia/Kuala_Lumpur|Asia/Ho_Chi_Minh|Asia/Manila|Asia/Yangon|Asia/Vientiane|Asia/Phnom_Penh|Asia/Pontianak|Asia/Ujung_Pandang)\") {\n return \"higress-registry.ap-southeast-7.cr.aliyuncs.com\"\n }\n\n # Default: China\n return \"higress-registry.cn-hangzhou.cr.aliyuncs.com\"\n}\n\nfunction Get-AgentTeamsLanguage {\n param([string]$Timezone)\n $chineseZones = @(\n \"Asia/Shanghai\", \"Asia/Chongqing\", \"Asia/Harbin\", \"Asia/Urumqi\",\n \"Asia/Taipei\", \"Asia/Hong_Kong\", \"Asia/Macau\"\n )\n if ($chineseZones -contains $Timezone) { return \"zh\" }\n return \"en\"\n}\n\n# ============================================================\n# Centralized message dictionary and Get-Msg function\n# ============================================================\n\n$script:Messages = @{\n # --- Timezone detection messages ---\n \"tz.warning.title\" = @{ zh = \"无法自动检测时区。\"; en = \"Could not detect timezone automatically.\" }\n \"tz.warning.prompt\" = @{ zh = \"请输入您的时区(例如 Asia/Shanghai、America/New_York)。\"; en = \"Please enter your timezone (e.g., Asia/Shanghai, America/New_York).\" }\n \"tz.default\" = @{ zh = \"使用默认时区: {0}\"; en = \"Using default timezone: {0}\" }\n \"tz.input_prompt\" = @{ zh = \"时区\"; en = \"Timezone\" }\n\n # --- Installation title and info ---\n \"install.title\" = @{ zh = \"=== AgentTeams Manager 安装 ===\"; en = \"=== AgentTeams Manager Installation ===\" }\n \"install.registry\" = @{ zh = \"镜像仓库: {0}\"; en = \"Registry: {0}\" }\n \"install.dir\" = @{ zh = \"安装目录: {0}\"; en = \"Installation directory: {0}\" }\n \"install.dir_hint\" = @{ zh = \" (env 文件 'agentteams-manager.env' 将保存到 HOME 目录。)\"; en = \" (The env file 'agentteams-manager.env' will be saved to your HOME directory.)\" }\n \"install.dir_hint2\" = @{ zh = \" (请从您希望管理此安装的目录运行此脚本。)\"; en = \" (Run this script from the directory where you want to manage this installation.)\" }\n\n # --- Onboarding mode ---\n \"install.mode.title\" = @{ zh = \"--- Onboarding 模式 ---\"; en = \"--- Onboarding Mode ---\" }\n \"install.mode.choose\" = @{ zh = \"选择安装模式:\"; en = \"Choose your installation mode:\" }\n \"install.mode.quickstart\" = @{ zh = \" 1) 快速开始 - 使用阿里云通义 Token 套餐快速安装(推荐)\"; en = \" 1) Quick Start - Fast installation with Qwen Cloud (recommended)\" }\n \"install.mode.manual\" = @{ zh = \" 2) 手动配置 - 选择 LLM 提供商并自定义选项\"; en = \" 2) Manual - Choose LLM provider and customize options\" }\n \"install.mode.prompt\" = @{ zh = \"请选择 [1/2]\"; en = \"Enter choice [1/2]\" }\n \"install.mode.quickstart_selected\" = @{ zh = \"已选择快速开始模式 - 使用阿里云通义 Token 套餐\"; en = \"Quick Start mode selected - using Qwen Cloud\" }\n \"install.mode.manual_selected\" = @{ zh = \"已选择手动配置模式 - 您将选择 LLM 提供商并自定义选项\"; en = \"Manual mode selected - you will choose LLM provider and customize options\" }\n \"install.mode.invalid\" = @{ zh = \"无效选择,默认使用快速开始模式\"; en = \"Invalid choice, defaulting to Quick Start mode\" }\n\n # --- Existing installation detected ---\n \"install.existing.detected\" = @{ zh = \"检测到已有 Manager 安装(env 文件: {0})\"; en = \"Existing Manager installation detected (env file: {0})\" }\n \"install.existing.choose\" = @{ zh = \"选择操作:\"; en = \"Choose an action:\" }\n \"install.existing.upgrade\" = @{ zh = \" 1) 就地升级(保留数据、工作空间、env 文件)\"; en = \" 1) In-place upgrade (keep data, workspace, env file)\" }\n \"install.existing.reinstall\" = @{ zh = \" 2) 全新重装(删除所有数据,重新开始)\"; en = \" 2) Clean reinstall (remove all data, start fresh)\" }\n \"install.existing.cancel\" = @{ zh = \" 3) 取消\"; en = \" 3) Cancel\" }\n \"install.existing.prompt\" = @{ zh = \"请选择 [1/2/3]\"; en = \"Enter choice [1/2/3]\" }\n \"install.existing.upgrade_noninteractive\" = @{ zh = \"非交互模式: 执行就地升级...\"; en = \"Non-interactive mode: performing in-place upgrade...\" }\n \"install.existing.upgrading\" = @{ zh = \"执行就地升级...\"; en = \"Performing in-place upgrade...\" }\n \"install.existing.warn_manager_stop\" = @{ zh = \"警告: Manager 容器将被停止并重新创建。\"; en = \"WARNING: Manager container will be stopped and recreated.\" }\n \"install.existing.warn_worker_recreate\" = @{ zh = \"警告: Worker 容器也将被重新创建(以更新 Manager IP)。\"; en = \"WARNING: Worker containers will also be recreated (to update Manager IP in hosts).\" }\n \"install.existing.continue_prompt\" = @{ zh = \"继续?[y/N]\"; en = \"Continue? [y/N]\" }\n \"install.existing.cancelled\" = @{ zh = \"安装已取消。\"; en = \"Installation cancelled.\" }\n \"install.existing.stopping_manager\" = @{ zh = \"停止并移除现有 manager 容器...\"; en = \"Stopping and removing existing manager container...\" }\n \"install.existing.stopping_workers\" = @{ zh = \"停止并移除现有 worker 容器...\"; en = \"Stopping and removing existing worker containers...\" }\n \"install.existing.removed\" = @{ zh = \" 已移除: {0}\"; en = \" Removed: {0}\" }\n\n # --- Upgrade mode sub-menu ---\n \"upgrade.mode.prompt\" = @{ zh = \"请选择升级方式:\"; en = \"Select upgrade mode:\" }\n \"upgrade.mode.keep_all\" = @{ zh = \" 1) 保留所有参数,一键升级(推荐)\"; en = \" 1) Keep all parameters, quick upgrade (recommended)\" }\n \"upgrade.mode.confirm_each\" = @{ zh = \" 2) 逐个确认参数(可修改)\"; en = \" 2) Confirm each parameter (can modify)\" }\n \"upgrade.mode.back\" = @{ zh = \" 3) 返回\"; en = \" 3) Back\" }\n\n # --- Clean reinstall messages ---\n \"install.reinstall.performing\" = @{ zh = \"执行全新重装...\"; en = \"Performing clean reinstall...\" }\n \"install.reinstall.warn_stop\" = @{ zh = \"警告: 以下运行中的容器将被停止:\"; en = \"WARNING: The following running containers will be stopped:\" }\n \"install.reinstall.warn_delete\" = @{ zh = \"警告: 以下内容将被删除:\"; en = \"WARNING: This will DELETE the following:\" }\n \"install.reinstall.warn_volume\" = @{ zh = \" - Docker 卷: agentteams-data\"; en = \" - Docker volume: agentteams-data\" }\n \"install.reinstall.warn_env\" = @{ zh = \" - Env 文件: {0}\"; en = \" - Env file: {0}\" }\n \"install.reinstall.warn_workspace\" = @{ zh = \" - Manager 工作空间: {0}\"; en = \" - Manager workspace: {0}\" }\n \"install.reinstall.warn_workers\" = @{ zh = \" - 所有 worker 容器\"; en = \" - All worker containers\" }\n \"install.reinstall.warn_proxy\" = @{ zh = \" - Docker API 代理容器: agentteams-controller\"; en = \" - Docker API proxy container: agentteams-controller\" }\n \"install.reinstall.removing_proxy\" = @{ zh = \"正在移除 Docker API 代理容器: agentteams-controller\"; en = \"Removing Docker API proxy container: agentteams-controller\" }\n \"install.reinstall.warn_network\" = @{ zh = \" - Docker 网络: agentteams-net\"; en = \" - Docker network: agentteams-net\" }\n \"install.reinstall.removing_network\" = @{ zh = \"正在移除 Docker 网络: agentteams-net\"; en = \"Removing Docker network: agentteams-net\" }\n \"install.reinstall.confirm_type\" = @{ zh = \"请输入工作空间路径以确认删除(或按 Ctrl+C 取消):\"; en = \"To confirm deletion, please type the workspace path:\" }\n \"install.reinstall.confirm_path\" = @{ zh = \"输入路径以确认(或按 Ctrl+C 取消)\"; en = \"Type the path to confirm (or press Ctrl+C to cancel)\" }\n \"install.reinstall.path_mismatch\" = @{ zh = \"路径不匹配。中止重装。输入: '{0}',期望: '{1}'\"; en = \"Path mismatch. Aborting reinstall. Input: '{0}', Expected: '{1}'\" }\n \"install.reinstall.confirmed\" = @{ zh = \"已确认。正在清理...\"; en = \"Confirmed. Cleaning up...\" }\n \"install.reinstall.removed_worker\" = @{ zh = \" 已移除 worker: {0}\"; en = \" Removed worker: {0}\" }\n \"install.reinstall.removing_volume\" = @{ zh = \"正在移除 Docker 卷: agentteams-data\"; en = \"Removing Docker volume: agentteams-data\" }\n \"install.reinstall.warn_volume_fail\" = @{ zh = \" 警告: 无法移除卷(可能有引用)\"; en = \" Warning: Could not remove volume (may have references)\" }\n \"install.reinstall.removing_workspace\" = @{ zh = \"正在移除工作空间目录: {0}\"; en = \"Removing workspace directory: {0}\" }\n \"install.reinstall.removing_env\" = @{ zh = \"正在移除 env 文件: {0}\"; en = \"Removing env file: {0}\" }\n \"install.reinstall.cleanup_done\" = @{ zh = \"清理完成。开始全新安装...\"; en = \"Cleanup complete. Starting fresh installation...\" }\n \"install.reinstall.failed_rm_workspace\" = @{ zh = \"无法移除工作空间目录\"; en = \"Failed to remove workspace directory\" }\n\n # --- Orphan volume detection ---\n \"install.orphan_volume.detected\" = @{ zh = \"⚠️ 检测到残留数据卷 '{0}',但未找到对应的 env 配置文件。\"; en = \"⚠️ Found leftover data volume '{0}' but no matching env config file.\" }\n \"install.orphan_volume.warn\" = @{ zh = \"这可能是之前安装的残留数据,会导致新安装出现异常(如密码冲突、服务启动失败)。\"; en = \"This is likely leftover data from a previous installation and may cause issues (e.g., credential conflicts, service startup failures).\" }\n \"install.orphan_volume.choose\" = @{ zh = \"选择操作:\"; en = \"Choose an action:\" }\n \"install.orphan_volume.clean\" = @{ zh = \" 1) 清理残留数据卷后继续安装(推荐)\"; en = \" 1) Remove leftover volume and continue installation (recommended)\" }\n \"install.orphan_volume.keep\" = @{ zh = \" 2) 保留数据卷继续安装(可能出现异常)\"; en = \" 2) Keep the volume and continue installation (may cause issues)\" }\n \"install.orphan_volume.prompt\" = @{ zh = \"请选择 [1/2]\"; en = \"Enter choice [1/2]\" }\n \"install.orphan_volume.cleaning\" = @{ zh = \"正在清理残留数据卷...\"; en = \"Removing leftover data volume...\" }\n \"install.orphan_volume.cleaned\" = @{ zh = \"残留数据卷已清理。继续全新安装...\"; en = \"Leftover volume removed. Continuing with fresh installation...\" }\n \"install.orphan_volume.keeping\" = @{ zh = \"保留数据卷,继续安装。如遇异常请选择全新重装。\"; en = \"Keeping existing volume. If you encounter issues, consider a clean reinstall.\" }\n \"install.orphan_volume.clean_noninteractive\" = @{ zh = \"非交互模式: 自动清理残留数据卷...\"; en = \"Non-interactive mode: automatically removing leftover volume...\" }\n\n # --- Loading existing config ---\n \"install.loading_config\" = @{ zh = \"从 {0} 加载已有配置(shell 环境变量优先)...\"; en = \"Loading existing config from {0} (shell env vars take priority)...\" }\n\n # --- LLM Configuration ---\n \"llm.title\" = @{ zh = \"--- LLM 配置 ---\"; en = \"--- LLM Configuration ---\" }\n \"llm.provider.label\" = @{ zh = \" 提供商: {0}\"; en = \" Provider: {0}\" }\n \"llm.model.label\" = @{ zh = \" 模型: {0}\"; en = \" Model: {0}\" }\n \"llm.provider.qwen\" = @{ zh = \" 提供商: qwen(阿里云百炼)\"; en = \" Provider: qwen (Alibaba Cloud Bailian)\" }\n \"llm.provider.qwen_default\" = @{ zh = \" 提供商: {0}(默认)\"; en = \" Provider: {0} (default)\" }\n \"llm.model.default\" = @{ zh = \" 模型: {0}(默认)\"; en = \" Model: {0} (default)\" }\n \"llm.apikey_hint_bailian\" = @{ zh = \" 提示: 获取阿里云百炼(DashScope)API Key:\"; en = \" Hint: Get your Alibaba Cloud Bailian (DashScope) API Key:\" }\n \"llm.apikey_url_bailian\" = @{ zh = \" https://www.aliyun.com/product/bailian\"; en = \" https://www.aliyun.com/product/bailian\" }\n \"llm.apikey_hint_qwencloud\" = @{ zh = \" 提示: 从 Qwen Cloud(国际站)获取 DASHSCOPE_API_KEY:\"; en = \" Hint: Get your DASHSCOPE_API_KEY for Qwen Cloud (international) from:\" }\n \"llm.apikey_url_qwencloud\" = @{ zh = \" https://home.qwencloud.com/api-keys (文档: https://docs.qwencloud.com/)\"; en = \" https://home.qwencloud.com/api-keys | Docs: https://docs.qwencloud.com/\" }\n \"llm.apikey_hint_tokenplan\" = @{ zh = \" 提示: 获取 DashScope API Key 或开通通义 Token 套餐,请参考:\"; en = \" Hint: Get your DashScope or Token Plan API key (Alibaba Model Studio):\" }\n \"llm.apikey_url_tokenplan\" = @{ zh = \" https://help.aliyun.com/zh/model-studio/token-plan-quickstart\"; en = \" https://common-buy.aliyun.com/token-plan/ | https://help.aliyun.com/zh/model-studio/token-plan-quickstart\" }\n \"llm.apikey_hint_codingplan\" = @{ zh = \" 提示: 获取 DashScope API Key(Coding 套餐 / coding.dashscope 接口):\"; en = \" Hint: Get your DashScope API key for Coding Plan (coding.dashscope endpoint):\" }\n \"llm.apikey_url_codingplan\" = @{ zh = \" https://help.aliyun.com/zh/model-studio/get-api-key\"; en = \" https://help.aliyun.com/zh/model-studio/get-api-key\" }\n \"llm.apikey_prompt\" = @{ zh = \"LLM API Key\"; en = \"LLM API Key\" }\n \"llm.providers_title\" = @{ zh = \"可用 LLM 提供商:\"; en = \"Available LLM Providers:\" }\n \"llm.provider.alibaba\" = @{ zh = \" 1) 阿里云通义 Token 套餐 - 推荐中国用户使用\"; en = \" 1) Qwen Cloud - International (OpenAI-compatible API, recommended)\" }\n \"llm.provider.openai_compat\" = @{ zh = \" 2) OpenAI 兼容 API - 自定义 Base URL(OpenAI、DeepSeek 等)\"; en = \" 2) OpenAI-compatible API - Custom Base URL (OpenAI, DeepSeek, etc.)\" }\n \"llm.provider.select\" = @{ zh = \"选择提供商 [1/2]\"; en = \"Select provider [1/2]\" }\n \"llm.alibaba.models_title\" = @{ zh = \"选择阿里云模型接入方式:\"; en = \"Select Alibaba Cloud model access:\" }\n \"llm.alibaba.model.tokenplan\" = @{ zh = \" 1) 阿里云通义 Token 套餐 - 兼容模式(推荐)\"; en = \" 1) Alibaba Cloud Token Plan - compatible-mode (recommended)\" }\n \"llm.alibaba.model.bailian\" = @{ zh = \" 2) 阿里云百炼 - DashScope 通用兼容接口\"; en = \" 2) Alibaba Cloud Bailian - DashScope compatible mode\" }\n \"llm.alibaba.model.codingplan_legacy\" = @{ zh = \" 3) 阿里云 Coding 套餐 - 旧版端点(兼容保留)\"; en = \" 3) Alibaba Cloud Coding Plan - legacy endpoint (backward compatible)\" }\n \"llm.alibaba.model.select\" = @{ zh = \"选择接入方式 [1/2/3]\"; en = \"Select access option [1/2/3]\" }\n \"llm.alibaba.model.invalid\" = @{ zh = \"无效选择: {0}(请输入 1、2 或 3)\"; en = \"Invalid choice: {0} (please enter 1, 2, or 3)\" }\n \"llm.codingplan.models_title\" = @{ zh = \"选择通义 Token 套餐默认模型:\"; en = \"Select Qwen Cloud default model:\" }\n \"llm.codingplan.model.qwen36plus\" = @{ zh = \" 1) qwen3.6-plus - 千问 3.6(推荐)\"; en = \" 1) qwen3.6-plus - Qwen 3.6 (recommended)\" }\n \"llm.codingplan.model.glm5\" = @{ zh = \" 2) glm-5 - 智谱 GLM-5(编程推荐)\"; en = \" 2) glm-5 - Zhipu GLM-5 (recommended for coding)\" }\n \"llm.codingplan.model.kimi\" = @{ zh = \" 3) kimi-k2.5 - Moonshot Kimi K2.5\"; en = \" 3) kimi-k2.5 - Moonshot Kimi K2.5\" }\n \"llm.codingplan.model.minimax\" = @{ zh = \" 4) MiniMax-M2.5 - MiniMax M2.5\"; en = \" 4) MiniMax-M2.5 - MiniMax M2.5\" }\n \"llm.codingplan.model.select\" = @{ zh = \"选择模型 [1/2/3/4]\"; en = \"Select model [1/2/3/4]\" }\n \"llm.provider.selected_tokenplan\" = @{ zh = \" 提供商: 阿里云通义 Token 套餐(兼容模式)\"; en = \" Provider: Alibaba Cloud Token Plan (compatible mode)\" }\n \"llm.provider.selected_codingplan\" = @{ zh = \" 提供商: 阿里云通义 Token 套餐(alibaba-cloud)\"; en = \" Provider: Qwen Cloud (international) (alibaba-cloud)\" }\n \"llm.provider.selected_codingplan_legacy\" = @{ zh = \" 提供商: 阿里云 Coding 套餐(coding.dashscope)\"; en = \" Provider: Alibaba Cloud Coding Plan (coding.dashscope)\" }\n \"llm.provider.selected_qwen\" = @{ zh = \" 提供商: 阿里云百炼\"; en = \" Provider: Alibaba Cloud Bailian\" }\n \"llm.provider.selected_openai\" = @{ zh = \" 提供商: {0}(OpenAI 兼容)\"; en = \" Provider: {0} (OpenAI-compatible)\" }\n \"llm.provider.invalid\" = @{ zh = \"无效选择: {0}(请输入 1 或 2)\"; en = \"Invalid choice: {0} (please enter 1 or 2)\" }\n \"llm.qwen.model_prompt\" = @{ zh = \"默认模型 ID\"; en = \"Default Model ID\" }\n \"llm.openai.base_url_prompt\" = @{ zh = \"Base URL\"; en = \"Base URL\" }\n \"llm.openai.model_prompt\" = @{ zh = \"默认模型 ID\"; en = \"Default Model ID\" }\n \"llm.openai.base_url_label\" = @{ zh = \" Base URL: {0}\"; en = \" Base URL: {0}\" }\n\n # --- Custom model parameters ---\n \"llm.custom_model.detected\" = @{ zh = \" ⚠️ 模型 '{0}' 不在内置模型列表中,请配置模型参数:\"; en = \" ⚠️ Model '{0}' is not in the built-in model list. Please configure model parameters:\" }\n \"llm.custom_model.context_prompt\" = @{ zh = \"最大上下文长度(token 数)[150000]\"; en = \"Max context window (tokens) [150000]\" }\n \"llm.custom_model.max_tokens_prompt\" = @{ zh = \"最大输出长度(token 数)[128000]\"; en = \"Max output tokens [128000]\" }\n \"llm.custom_model.reasoning_prompt\" = @{ zh = \"是否支持推理/思考模式?[Y/n]\"; en = \"Does it support reasoning/thinking mode? [Y/n]\" }\n \"llm.custom_model.vision_prompt\" = @{ zh = \"是否支持图片输入?[y/N]\"; en = \"Does it support image input? [y/N]\" }\n \"llm.custom_model.summary\" = @{ zh = \" 自定义模型参数: 上下文={0}, 最大输出={1}, 推理={2}, 图片={3}\"; en = \" Custom model params: context={0}, maxTokens={1}, reasoning={2}, vision={3}\" }\n\n # --- Admin Credentials ---\n \"admin.title\" = @{ zh = \"--- 管理员凭据 ---\"; en = \"--- Admin Credentials ---\" }\n \"admin.username_prompt\" = @{ zh = \"管理员用户名\"; en = \"Admin Username\" }\n \"admin.password_prompt\" = @{ zh = \"管理员密码(留空自动生成,最少 8 位)\"; en = \"Admin Password (leave empty to auto-generate, min 8 chars)\" }\n \"admin.password_generated\" = @{ zh = \" 已自动生成管理员密码\"; en = \" Auto-generated admin password\" }\n \"admin.password_too_short\" = @{ zh = \"管理员密码至少需要 8 个字符(MinIO 要求)。当前长度: {0}\"; en = \"Admin password must be at least 8 characters (MinIO requirement). Current length: {0}\" }\n\n # --- Port Configuration ---\n \"port.title\" = @{ zh = \"--- 端口配置(按回车使用默认值)---\"; en = \"--- Port Configuration (press Enter for defaults) ---\" }\n \"port.gateway_prompt\" = @{ zh = \"网关主机端口(容器内 8080)\"; en = \"Host port for gateway (8080 inside container)\" }\n \"port.console_prompt\" = @{ zh = \"Higress 控制台主机端口(容器内 8001)\"; en = \"Host port for Higress console (8001 inside container)\" }\n \"port.element_prompt\" = @{ zh = \"Element Web 直接访问主机端口(容器内 8088)\"; en = \"Host port for Element Web direct access (8088 inside container)\" }\n \"port.manager_console_prompt\" = @{ zh = \"Manager 控制台主机端口(容器内 18888)\"; en = \"Host port for Manager console (18888 inside container)\" }\n \"port.local_only.title\" = @{ zh = \"--- 网络访问模式 ---\"; en = \"--- Network Access Mode ---\" }\n \"port.local_only.hint_yes\" = @{ zh = \" 仅本机使用,无需开放外部端口(推荐)\"; en = \" Local use only, no external port exposure (recommended)\" }\n \"port.local_only.hint_no\" = @{ zh = \" 允许外部访问(局域网 / 公网)\"; en = \" Allow external access (LAN / public network)\" }\n \"port.local_only.choice\" = @{ zh = \"请选择 [1/2]\"; en = \"Enter choice [1/2]\" }\n \"port.local_only.selected_local\" = @{ zh = \"端口已绑定到 127.0.0.1(仅本机访问)\"; en = \"Ports bound to 127.0.0.1 (localhost only)\" }\n \"port.local_only.selected_external\" = @{ zh = \"端口已绑定到所有网络接口(0.0.0.0)\"; en = \"Ports bound to all interfaces (0.0.0.0)\" }\n \"port.local_only.https_hint\" = @{ zh = \"警告: 建议在 Higress 控制台配置 TLS 证书并启用 HTTPS,避免明文传输。\"; en = \"WARNING: It is recommended to configure TLS certificates and enable HTTPS in the Higress Console to avoid plaintext transmission.\" }\n \"port.local_only.https_docs\" = @{ zh = \"\"; en = \"\" }\n\n # --- Domain Configuration ---\n \"domain.title\" = @{ zh = \"--- 域名配置(按回车使用默认值)---\"; en = \"--- Domain Configuration (press Enter for defaults) ---\" }\n \"domain.hint\" = @{ zh = \"提示: 自定义域名前必须事先做好 DNS 解析。单机 ECS 部署时无需修改 aigw、fs 等域名;Element Web 和 Matrix Server 也可通过 IP 直接访问。\"; en = \"Hint: Configure DNS resolution before customizing domains. For single ECS deployment, no need to change aigw, fs, etc.; Element Web and Matrix Server can also be accessed directly via IP.\" }\n \"domain.matrix_prompt\" = @{ zh = \"Matrix 域名\"; en = \"Matrix Domain\" }\n \"domain.element_prompt\" = @{ zh = \"Element Web 域名\"; en = \"Element Web Domain\" }\n \"domain.gateway_prompt\" = @{ zh = \"AI 网关域名\"; en = \"AI Gateway Domain\" }\n \"domain.fs_prompt\" = @{ zh = \"文件系统域名\"; en = \"File System Domain\" }\n \"domain.console_prompt\" = @{ zh = \"Manager 控制台域名\"; en = \"Manager Console Domain\" }\n\n # --- GitHub Integration ---\n \"github.title\" = @{ zh = \"--- GitHub 集成(可选,按回车跳过)---\"; en = \"--- GitHub Integration (optional, press Enter to skip) ---\" }\n \"github.token_prompt\" = @{ zh = \"GitHub 个人访问令牌(可选)\"; en = \"GitHub Personal Access Token (optional)\" }\n\n # --- Skills Registry ---\n \"skills.title\" = @{ zh = \"--- Skills 注册中心(可选,按回车使用默认 nacos://market.agentteams.io:80/public)---\"; en = \"--- Skills Registry (optional, press Enter for default nacos://market.agentteams.io:80/public) ---\" }\n \"skills.url_prompt\" = @{ zh = \"Skills 注册中心 URL(留空使用默认 nacos://market.agentteams.io:80/public)\"; en = \"Skills Registry URL (leave empty for default nacos://market.agentteams.io:80/public)\" }\n\n # --- Data Persistence ---\n \"data.title\" = @{ zh = \"--- 数据持久化 ---\"; en = \"--- Data Persistence ---\" }\n \"data.volume_prompt\" = @{ zh = \"Docker 卷名称 [agentteams-data]\"; en = \"Docker volume name for persistent data [agentteams-data]\" }\n \"data.volume_using\" = @{ zh = \" 使用 Docker 卷: {0}\"; en = \" Using Docker volume: {0}\" }\n\n # --- Manager Workspace ---\n \"workspace.title\" = @{ zh = \"--- Manager 工作空间 ---\"; en = \"--- Manager Workspace ---\" }\n \"workspace.dir_prompt\" = @{ zh = \"Manager 工作空间目录 [{0}]\"; en = \"Manager workspace directory [{0}]\" }\n \"workspace.dir_label\" = @{ zh = \" Manager 工作空间: {0}\"; en = \" Manager workspace: {0}\" }\n\n # --- Host directory sharing ---\n \"host_share.prompt\" = @{ zh = \"与 Agent 共享的主机目录(默认: {0})\"; en = \"Host directory to share with agents (default: {0})\" }\n \"host_share.sharing\" = @{ zh = \"共享主机目录: {0} -> 容器内 /host-share\"; en = \"Sharing host directory: {0} -> /host-share in container\" }\n \"host_share.not_exist\" = @{ zh = \"警告: 主机目录 {0} 不存在,跳过验证继续使用\"; en = \"WARNING: Host directory {0} does not exist, using without validation\" }\n\n # --- Default worker runtime ---\n \"worker_runtime.title\" = @{ zh = \"--- 默认 Worker 运行时 ---\"; en = \"--- Default Worker Runtime ---\" }\n \"worker_runtime.openclaw\" = @{ zh = \"OpenClaw\"; en = \"OpenClaw\" }\n \"worker_runtime.copaw\" = @{ zh = \"QwenPaw\"; en = \"QwenPaw\" }\n \"worker_runtime.hermes\" = @{ zh = \"Hermes\"; en = \"Hermes\" }\n \"worker_runtime.choice\" = @{ zh = \"请选择 [1/2/3]\"; en = \"Enter choice [1/2/3]\" }\n \"worker_runtime.selected\" = @{ zh = \"默认 Worker 运行时: {0}\"; en = \"Default Worker runtime: {0}\" }\n \"worker_runtime.title_short\" = @{ zh = \"默认 Worker 运行时\"; en = \"Default Worker Runtime\" }\n\n # --- Manager runtime ---\n \"manager_runtime.title\" = @{ zh = \"--- Manager 运行时 ---\"; en = \"--- Manager Runtime ---\" }\n \"manager_runtime.openclaw\" = @{ zh = \"OpenClaw\"; en = \"OpenClaw\" }\n \"manager_runtime.copaw\" = @{ zh = \"QwenPaw\"; en = \"QwenPaw\" }\n \"manager_runtime.choice\" = @{ zh = \"请选择 [1/2]\"; en = \"Enter choice [1/2]\" }\n \"manager_runtime.selected\" = @{ zh = \"Manager 运行时: {0}\"; en = \"Manager runtime: {0}\" }\n \"manager_runtime.title_short\" = @{ zh = \"Manager 运行时\"; en = \"Manager Runtime\" }\n\n # --- Matrix E2EE ---\n \"matrix_e2ee.title\" = @{ zh = \"--- Matrix 端到端加密(E2EE)---\"; en = \"--- Matrix End-to-End Encryption (E2EE) ---\" }\n \"matrix_e2ee.desc\" = @{\n zh = \"E2EE 会对 Manager 与 Worker 之间的 Matrix 消息进行端到端加密。`n 启用后,即使 Matrix 服务器被入侵,消息内容也无法被窃取。`n 但 E2EE 会增加首次握手耗时,且要求所有 Agent 都支持 matrix-sdk-crypto。`n 如果不确定,建议保持禁用。`n ⚠ 注意:禁用 E2EE 后,请勿在 Element 上创建默认启用加密的 Private 房间,`n 否则 Agent 将无法读取该房间中的加密消息。请改用 Public 房间或关闭房间加密。\"\n en = \"E2EE encrypts Matrix messages between Manager and Workers end-to-end.`n When enabled, message content stays private even if the Matrix server is compromised.`n However, E2EE adds overhead to the initial handshake and requires all Agents`n to support matrix-sdk-crypto. If unsure, keep it disabled.`n ⚠ Note: When E2EE is disabled, do NOT create Private rooms in Element (which`n enable encryption by default) — Agents cannot read encrypted messages without`n E2EE support. Use Public rooms or turn off room encryption instead.\"\n }\n \"matrix_e2ee.enable\" = @{ zh = \"启用 E2EE\"; en = \"Enable E2EE\" }\n \"matrix_e2ee.disable\" = @{ zh = \"禁用 E2EE(推荐)\"; en = \"Disable E2EE (recommended)\" }\n \"matrix_e2ee.choice\" = @{ zh = \"请选择 [1/2]\"; en = \"Enter choice [1/2]\" }\n \"matrix_e2ee.selected_enabled\" = @{ zh = \"Matrix E2EE: 已启用\"; en = \"Matrix E2EE: enabled\" }\n \"matrix_e2ee.selected_disabled\" = @{ zh = \"Matrix E2EE: 已禁用(默认)\"; en = \"Matrix E2EE: disabled (default)\" }\n \"matrix_e2ee.title_short\" = @{ zh = \"Matrix E2EE\"; en = \"Matrix E2EE\" }\n \"matrix_e2ee.val_enabled\" = @{ zh = \"已启用\"; en = \"enabled\" }\n \"matrix_e2ee.val_disabled\" = @{ zh = \"已禁用\"; en = \"disabled\" }\n\n # --- Docker API proxy ---\n \"docker_proxy.title\" = @{ zh = \"--- Docker API 安全代理 ---\"; en = \"--- Docker API Security Proxy ---\" }\n \"docker_proxy.desc\" = @{ zh = \"Docker API 代理可防止 AI Agent 通过 Docker API 越狱访问宿主机。`n 启用后,Manager 不再直接持有 Docker socket,所有容器操作经过安全校验。\"; en = \"Docker API proxy prevents AI Agents from escaping via Docker API to access the host.`n When enabled, Manager no longer has direct Docker socket access; all container operations go through security validation.\" }\n \"docker_proxy.enable\" = @{ zh = \"启用(推荐)\"; en = \"Enable (recommended)\" }\n \"docker_proxy.disable\" = @{ zh = \"禁用(直接挂载 Docker socket)\"; en = \"Disable (mount Docker socket directly)\" }\n \"docker_proxy.choice\" = @{ zh = \"请选择 [1/2]\"; en = \"Enter choice [1/2]\" }\n \"docker_proxy.selected_enabled\" = @{ zh = \"Docker API 代理: 已启用\"; en = \"Docker API proxy: enabled\" }\n \"docker_proxy.selected_disabled\" = @{ zh = \"Docker API 代理: 已禁用\"; en = \"Docker API proxy: disabled\" }\n \"docker_proxy.title_short\" = @{ zh = \"Docker API 代理\"; en = \"Docker API Proxy\" }\n \"docker_proxy.val_enabled\" = @{ zh = \"已启用\"; en = \"enabled\" }\n \"docker_proxy.val_disabled\" = @{ zh = \"已禁用\"; en = \"disabled\" }\n \"docker_proxy.registries_desc\" = @{ zh = \"默认放行的镜像来源:本地镜像、localhost、Higress 仓库(所有 region)。`n 如需放行其他镜像仓库,请输入逗号分隔的地址前缀。`n 示例: ghcr.io/myorg,registry.example.com/team\"; en = \"Default allowed image sources: local images, localhost, Higress registries (all regions).`n To allow additional image sources, enter comma-separated address prefixes.`n Example: ghcr.io/myorg,registry.example.com/team\" }\n \"docker_proxy.registries_prompt\" = @{ zh = \"额外放行的镜像来源(按回车跳过)\"; en = \"Additional allowed image sources (press Enter to skip)\" }\n \"docker_proxy.registries_label\" = @{ zh = \"额外放行的镜像来源\"; en = \"Additional allowed image sources\" }\n\n # --- Worker idle timeout ---\n \"idle_timeout.prompt\" = @{ zh = \"Worker 空闲自动停止超时(分钟)[720]\"; en = \"Worker idle auto-stop timeout in minutes [720]\" }\n \"idle_timeout.selected\" = @{ zh = \"Worker 空闲超时: {0} 分钟\"; en = \"Worker idle timeout: {0} minutes\" }\n \"idle_timeout.label\" = @{ zh = \"Worker 空闲超时(分钟)\"; en = \"Worker idle timeout (min)\" }\n\n # --- Secrets and config ---\n \"install.generating_secrets\" = @{ zh = \"正在生成密钥...\"; en = \"Generating secrets...\" }\n \"install.config_saved\" = @{ zh = \"配置已保存到 {0}\"; en = \"Configuration saved to {0}\" }\n\n # --- Container runtime socket ---\n \"install.socket_detected\" = @{ zh = \"容器运行时 socket: {0}(已启用直接创建 Worker)\"; en = \"Container runtime socket: {0} (direct Worker creation enabled)\" }\n \"install.socket_not_found\" = @{ zh = \"未找到容器运行时 socket(Manager 无法直接创建 Worker 容器,需要你手动执行 docker 命令创建)\"; en = \"No container runtime socket found (Manager cannot create Worker containers directly, you will need to create them manually using docker commands)\" }\n \"install.socket_confirm.title\" = @{ zh = \"警告: 未检测到容器运行时 Socket\"; en = \"WARNING: Container Runtime Socket Not Detected\" }\n \"install.socket_confirm.message\" = @{ zh = \"未找到 Docker/Podman socket,Manager 将无法自动创建 Worker 容器。`n你需要手动执行 docker run 命令来创建 Worker。`n`n是否继续安装?\"; en = \"Docker/Podman socket not found. Manager will not be able to create Worker containers automatically.`nYou will need to manually run docker commands to create Workers.`n`nContinue installation?\" }\n \"install.socket_confirm.prompt\" = @{ zh = \"继续安装? [y/N]: \"; en = \"Continue? [y/N]: \" }\n \"install.socket_confirm.cancelled\" = @{ zh = \"安装已取消。如需启用 Worker 自动创建,请确保 Docker/Podman 正在运行,然后重新运行安装脚本。\"; en = \"Installation cancelled. To enable automatic Worker creation, ensure Docker/Podman is running and re-run the installer.\" }\n\n # --- Container management ---\n \"install.removing_existing\" = @{ zh = \"正在移除现有 agentteams-manager 容器...\"; en = \"Removing existing agentteams-manager container...\" }\n\n # --- YOLO mode ---\n \"install.yolo\" = @{ zh = \"YOLO 模式已启用(自主决策,无交互提示)\"; en = \"YOLO mode enabled (autonomous decisions, no interactive prompts)\" }\n\n # --- Image pulling ---\n \"install.image.exists\" = @{ zh = \"Manager 镜像已存在: {0}\"; en = \"Manager image already exists locally: {0}\" }\n \"install.image.pulling_manager\" = @{ zh = \"正在拉取 Manager 镜像: {0}\"; en = \"Pulling Manager image: {0}\" }\n \"install.image.worker_exists\" = @{ zh = \"Worker 镜像已存在: {0}\"; en = \"Worker image already exists locally: {0}\" }\n \"install.image.pulling_worker\" = @{ zh = \"正在拉取 Worker 镜像: {0}\"; en = \"Pulling Worker image: {0}\" }\n\n # --- Starting container ---\n \"install.starting_manager\" = @{ zh = \"正在启动 Manager 容器...\"; en = \"Starting Manager container...\" }\n\n # --- Wait for Manager ready ---\n \"install.wait_ready\" = @{ zh = \"等待 Manager Agent 就绪(超时: {0}s)...\"; en = \"Waiting for Manager agent to be ready (timeout: {0}s)...\" }\n \"install.wait_ready.ok\" = @{ zh = \"Manager Agent 已就绪!\"; en = \"Manager agent is ready!\" }\n \"install.wait_ready.waiting\" = @{ zh = \"等待中... ({0}s/{1}s)\"; en = \"Waiting... ({0}s/{1}s)\" }\n \"install.wait_ready.timeout\" = @{ zh = \"Manager Agent 在 {0}s 内未就绪。请检查: docker logs {1}\"; en = \"Manager agent did not become ready within {0}s. Check: docker logs {1}\" }\n\n # --- Wait for Matrix ready ---\n \"install.wait_matrix\" = @{ zh = \"等待 Matrix 服务就绪(超时: {0}s)...\"; en = \"Waiting for Matrix server to be ready (timeout: {0}s)...\" }\n \"install.wait_matrix.ok\" = @{ zh = \"Matrix 服务已就绪!\"; en = \"Matrix server is ready!\" }\n \"install.wait_matrix.waiting\" = @{ zh = \"等待 Matrix 中... ({0}s/{1}s)\"; en = \"Waiting for Matrix... ({0}s/{1}s)\" }\n \"install.wait_matrix.timeout\" = @{ zh = \"Matrix 服务在 {0}s 内未就绪。请检查: docker logs {1}\"; en = \"Matrix server did not become ready within {0}s. Check: docker logs {1}\" }\n\n # --- OpenAI-compatible connectivity test ---\n \"llm.openai.test.testing\" = @{ zh = \"正在测试 API 联通性...\"; en = \"Testing API connectivity...\" }\n \"llm.openai.test.ok\" = @{ zh = \"API 联通性测试通过\"; en = \"API connectivity test passed\" }\n \"llm.openai.test.fail\" = @{ zh = \"API 联通性测试失败(HTTP {0})。响应内容:`n{1}`n请根据以上错误信息联系您的模型服务商解决。\"; en = \"API connectivity test failed (HTTP {0}). Response body:`n{1}`nPlease contact your model provider to resolve the issue.\" }\n \"llm.openai.test.fail.tokenplan\" = @{ zh = \"提示: 请确认 API Key 有效且已开通通义 Token 套餐。文档: https://help.aliyun.com/zh/model-studio/token-plan-quickstart\"; en = \"Hint: Verify your Token Plan API key and compatible-mode access. Docs: https://help.aliyun.com/zh/model-studio/token-plan-quickstart\" }\n \"llm.openai.test.fail.codingplan\" = @{ zh = \"提示: 请确认 API Key 有效且已开通通义 Token 套餐。文档: https://help.aliyun.com/zh/model-studio/token-plan-quickstart\"; en = \"Hint: Verify your DASHSCOPE_API_KEY for Qwen Cloud. API keys: https://home.qwencloud.com/api-keys Docs: https://docs.qwencloud.com/\" }\n \"llm.openai.test.fail.codingplan_legacy\" = @{ zh = \"提示: 请确认 API Key 有效且 Coding 套餐接口可用。文档: https://help.aliyun.com/zh/model-studio/get-api-key\"; en = \"Hint: Verify your DashScope API key and Coding Plan access. Docs: https://help.aliyun.com/zh/model-studio/get-api-key\" }\n \"llm.openai.test.confirm\" = @{ zh = \"是否仍要继续安装?[y/N/b]\"; en = \"Continue with installation anyway? [y/N/b]\" }\n \"llm.openai.test.aborted\" = @{ zh = \"安装已中止。\"; en = \"Installation aborted.\" }\n \"llm.embedding.title\" = @{ zh = \"📦 记忆搜索配置\"; en = \"📦 Memory Search Configuration\" }\n \"llm.embedding.hint\" = @{ zh = \" Embedding 模型可提升记忆搜索质量(语义匹配)。不启用也可正常使用记忆功能(关键词匹配)。\"; en = \" Embedding model improves memory search quality (semantic matching). Memory still works without it (keyword matching).\" }\n \"llm.embedding.option.default\" = @{ zh = \" 1) text-embedding-v4(推荐)\"; en = \" 1) text-embedding-v4 (Recommended)\" }\n \"llm.embedding.option.custom\" = @{ zh = \" 2) 自定义 Embedding 模型\"; en = \" 2) Custom embedding model\" }\n \"llm.embedding.option.disable\" = @{ zh = \" 3) 不启用\"; en = \" 3) Do not enable\" }\n \"llm.embedding.select\" = @{ zh = \"选择\"; en = \"Select\" }\n \"llm.embedding.custom_prompt\" = @{ zh = \" Embedding 模型名称\"; en = \" Embedding model name\" }\n \"llm.embedding.test.testing\" = @{ zh = \"正在测试 Embedding API 联通性...\"; en = \"Testing Embedding API connectivity...\" }\n \"llm.embedding.test.ok\" = @{ zh = \"✅ Embedding API 联通性测试通过\"; en = \"✅ Embedding API connectivity test passed\" }\n \"llm.embedding.test.fail\" = @{ zh = \"⚠️ Embedding API 测试失败(HTTP {0})。响应: {1}\"; en = \"⚠️ Embedding API test failed (HTTP {0}). Response: {1}\" }\n \"llm.embedding.auto_disabled\" = @{ zh = \"⚠️ Embedding 已自动禁用,记忆搜索将使用关键词匹配。您可以稍后在 agentteams-manager.env 中设置 AGENTTEAMS_EMBEDDING_MODEL 启用。\"; en = \"⚠️ Embedding auto-disabled. Memory search will use keyword matching. You can enable it later in agentteams-manager.env by setting AGENTTEAMS_EMBEDDING_MODEL.\" }\n \"llm.embedding.disabled\" = @{ zh = \"ℹ️ Embedding 已禁用,记忆搜索将使用关键词匹配。\"; en = \"ℹ️ Embedding disabled. Memory search will use keyword matching.\" }\n \"nav.back_hint\" = @{ zh = \"(输入 b 返回上一步)\"; en = \"(enter b to go back)\" }\n # --- OpenAI-compatible provider creation ---\n \"install.openai_compat.missing\" = @{ zh = \"警告: OpenAI Base URL 或 API Key 未设置,跳过提供商创建\"; en = \"WARNING: OpenAI Base URL or API Key not set, skipping provider creation\" }\n \"install.openai_compat.creating\" = @{ zh = \"正在创建 OpenAI 兼容提供商...\"; en = \"Creating OpenAI-compatible provider...\" }\n \"install.openai_compat.domain\" = @{ zh = \" 域名: {0}\"; en = \" Domain: {0}\" }\n \"install.openai_compat.port\" = @{ zh = \" 端口: {0}\"; en = \" Port: {0}\" }\n \"install.openai_compat.protocol\" = @{ zh = \" 协议: {0}\"; en = \" Protocol: {0}\" }\n \"install.openai_compat.service_fail\" = @{ zh = \"警告: 创建 DNS 服务源失败(可能已存在)\"; en = \"WARNING: Failed to create DNS service source (may already exist)\" }\n \"install.openai_compat.provider_fail\" = @{ zh = \"警告: 创建 AI 提供商失败(可能已存在)\"; en = \"WARNING: Failed to create AI provider (may already exist)\" }\n \"install.openai_compat.success\" = @{ zh = \"OpenAI 兼容提供商创建成功\"; en = \"OpenAI-compatible provider created successfully\" }\n\n # --- Welcome message ---\n \"install.welcome_msg.soul_configured\" = @{ zh = \"Soul 已配置(找到 soul-configured 标记),跳过 onboarding 消息\"; en = \"Soul already configured (soul-configured marker found), skipping onboarding message\" }\n \"install.welcome_msg.logging_in\" = @{ zh = \"正在以 {0} 身份登录以发送欢迎消息...\"; en = \"Logging in as {0} to send welcome message...\" }\n \"install.welcome_msg.login_failed\" = @{ zh = \"警告: 以 {0} 身份登录失败,跳过欢迎消息\"; en = \"WARNING: Failed to login as {0}, skipping welcome message\" }\n \"install.welcome_msg.finding_room\" = @{ zh = \"正在查找与 Manager 的 DM 房间...\"; en = \"Finding DM room with Manager...\" }\n \"install.welcome_msg.creating_room\" = @{ zh = \"正在创建与 Manager 的 DM 房间...\"; en = \"Creating DM room with Manager...\" }\n \"install.welcome_msg.no_room\" = @{ zh = \"警告: 无法找到或创建与 Manager 的 DM 房间\"; en = \"WARNING: Could not find or create DM room with Manager\" }\n \"install.welcome_msg.waiting_join\" = @{ zh = \"等待 Manager 加入房间...\"; en = \"Waiting for Manager to join the room...\" }\n \"install.welcome_msg.sending\" = @{ zh = \"正在向 Manager 发送欢迎消息...\"; en = \"Sending welcome message to Manager...\" }\n \"install.welcome_msg.send_failed\" = @{ zh = \"警告: 发送欢迎消息失败\"; en = \"WARNING: Failed to send welcome message\" }\n \"install.welcome_msg.sent\" = @{ zh = \"欢迎消息已发送给 Manager\"; en = \"Welcome message sent to Manager\" }\n \"install.welcome_msg.waiting\" = @{ zh = \"等待 Manager 发送欢迎消息(Higress 路由授权 + LLM 探活,约 45-90s)...\"; en = \"Waiting for Manager to send the welcome message (Higress route auth + LLM probe, ~45-90s)...\" }\n \"install.welcome_msg.confirmed\" = @{ zh = \"Manager 已确认发送欢迎消息(status.welcomeSent=true,用时 {0}s)\"; en = \"Manager confirmed welcome message sent (status.welcomeSent=true, {0}s elapsed)\" }\n \"install.welcome_msg.timeout\" = @{ zh = \"警告: 在 {0}s 内未观察到 Manager 发送欢迎消息(status.welcomeSent=true)。安装仍然成功,所有服务已就绪——可继续按下方提示登录 Element Web。\"; en = \"WARNING: Did not observe the Manager sending its welcome message (status.welcomeSent=true) within {0}s. Installation is still successful, all services are up — continue with the Element Web instructions below.\" }\n \"install.welcome_msg.timeout_hint\" = @{ zh = \"手动触发 onboarding: 登录 Element Web → 打开与 Manager 的 DM 房间 → 发送任意一句话(例如 `\"hi`\"),Manager 会接管对话并开始引导。\"; en = \"Manual onboarding: log in to Element Web -> open the DM with the Manager -> send any message (e.g. `\"hi`\") and the Manager will take over and start the guided setup.\" }\n \"install.welcome_msg.timeout_inspect\" = @{ zh = \"排查命令: docker exec agentteams-controller agt get managers default\"; en = \"Inspect status: docker exec agentteams-controller agt get managers default\" }\n \"install.welcome_msg.poll_unavailable\" = @{ zh = \"提示: agentteams-manager 内未找到 AgentTeams CLI,跳过 welcome 等待(旧镜像?)\"; en = \"Note: AgentTeams CLI not found inside agentteams-manager; skipping welcome wait (old image?)\" }\n\n # --- Final output panel ---\n \"success.title\" = @{ zh = \"=== AgentTeams Manager 已启动!===\"; en = \"=== AgentTeams Manager Started! ===\" }\n \"success.domains_configured\" = @{ zh = \"以下域名已配置解析到 127.0.0.1:\"; en = \"The following domains are configured to resolve to 127.0.0.1:\" }\n \"success.open_url\" = @{ zh = \" 在浏览器中打开以下 URL 开始使用: \"; en = \" Open the following URL in your browser to start: \" }\n \"success.login_with\" = @{ zh = \" 登录信息:\"; en = \" Login with:\" }\n \"success.username\" = @{ zh = \" 用户名: {0}\"; en = \" Username: {0}\" }\n \"success.password\" = @{ zh = \" 密码: {0}\"; en = \" Password: {0}\" }\n \"success.after_login\" = @{ zh = \" 登录后,开始与 Manager 聊天!\"; en = \" After login, start chatting with the Manager!\" }\n \"success.tell_it\" = @{ zh = \" 告诉它: `\"创建一个名为 alice 的前端开发 Worker`\"\"; en = \" Tell it: `\"Create a Worker named alice for frontend dev`\"\" }\n \"success.manager_auto\" = @{ zh = \" Manager 会自动处理一切。\"; en = \" The Manager will handle everything automatically.\" }\n \"success.mobile_title\" = @{ zh = \" 移动端访问(FluffyChat / Element Mobile):\"; en = \" Mobile access (FluffyChat / Element Mobile):\" }\n \"success.mobile_step1\" = @{ zh = \" 1. 在手机上下载 FluffyChat 或 Element\"; en = \" 1. Download FluffyChat or Element on your phone\" }\n \"success.mobile_step2\" = @{ zh = \" 2. 设置 homeserver 为: {0}\"; en = \" 2. Set homeserver to: {0}\" }\n \"success.mobile_step2_noip\" = @{ zh = \" 2. 设置 homeserver 为: http://<本机局域网IP>:{0}\"; en = \" 2. Set homeserver to: http://<this-machine-LAN-IP>:{0}\" }\n \"success.mobile_noip_hint\" = @{ zh = \" (无法自动检测局域网 IP — 请使用 ifconfig / ip addr 查看)\"; en = \" (Could not detect LAN IP automatically — check with: ifconfig / ip addr)\" }\n \"success.mobile_step3\" = @{ zh = \" 3. 登录信息:\"; en = \" 3. Login with:\" }\n \"success.mobile_username\" = @{ zh = \" 用户名: {0}\"; en = \" Username: {0}\" }\n \"success.mobile_password\" = @{ zh = \" 密码: {0}\"; en = \" Password: {0}\" }\n\n # --- Other consoles and tips ---\n \"success.other_consoles\" = @{ zh = \"--- 其他控制台 ---\"; en = \"--- Other Consoles ---\" }\n \"success.higress_console\" = @{ zh = \" Higress 控制台: http://localhost:{0}(用户名: {1} / 密码: {2})\"; en = \" Higress Console: http://localhost:{0} (Username: {1} / Password: {2})\" }\n \"success.manager_console\" = @{ zh = \" Manager 控制台(本地): http://localhost:{0}(无需登录)\"; en = \" Manager Console (local): http://localhost:{0} (no login required)\" }\n \"success.manager_console_gateway\" = @{ zh = \" Manager 控制台(网关): http://console-local.agentteams.io(用户名: {0} / 密码: {1})\"; en = \" Manager Console (gateway): http://console-local.agentteams.io (Username: {0} / Password: {1})\" }\n \"success.copaw_console\" = @{ zh = \" QwenPaw 控制台(本地): http://localhost:{0}(无需登录)\"; en = \" QwenPaw Console (local): http://localhost:{0} (no login required)\" }\n \"success.switch_llm.title\" = @{ zh = \"--- 切换 LLM 提供商 ---\"; en = \"--- Switch LLM Providers ---\" }\n \"success.switch_llm.hint\" = @{ zh = \" 您可以通过 Higress 控制台切换到其他 LLM 提供商(OpenAI、Anthropic 等)。\"; en = \" You can switch to other LLM providers (OpenAI, Anthropic, etc.) via Higress Console.\" }\n \"success.switch_llm.docs\" = @{ zh = \" 详细说明请参阅:\"; en = \" For detailed instructions, see:\" }\n \"success.switch_llm.url\" = @{ zh = \" https://higress.ai/en/docs/ai/scene-guide/multi-proxy#console-configuration\"; en = \" https://higress.ai/en/docs/ai/scene-guide/multi-proxy#console-configuration\" }\n \"success.tip\" = @{ zh = \"提示: 您也可以在聊天中让 Manager 为您配置 LLM 提供商。\"; en = \"Tip: You can also ask the Manager to configure LLM providers for you in the chat.\" }\n \"success.config_file\" = @{ zh = \"配置文件: {0}\"; en = \"Configuration file: {0}\" }\n \"success.data_volume\" = @{ zh = \"数据卷: {0}\"; en = \"Data volume: {0}\" }\n \"success.workspace\" = @{ zh = \"Manager 工作空间: {0}\"; en = \"Manager workspace: {0}\" }\n\n # --- Worker installation ---\n \"worker.resetting\" = @{ zh = \"正在重置 Worker: {0}...\"; en = \"Resetting Worker: {0}...\" }\n \"worker.exists\" = @{ zh = \"容器 '{0}' 已存在。使用 --reset 重新创建。\"; en = \"Container '{0}' already exists. Use --reset to recreate.\" }\n \"worker.starting\" = @{ zh = \"正在启动 Worker: {0}...\"; en = \"Starting Worker: {0}...\" }\n \"worker.skills_url\" = @{ zh = \" Skills API URL: {0}\"; en = \" Skills API URL: {0}\" }\n \"worker.started\" = @{ zh = \"=== Worker {0} 已启动!===\"; en = \"=== Worker {0} Started! ===\" }\n \"worker.container\" = @{ zh = \"容器: {0}\"; en = \"Container: {0}\" }\n \"worker.view_logs\" = @{ zh = \"查看日志: docker logs -f {0}\"; en = \"View logs: docker logs -f {0}\" }\n\n # --- Prompt function messages ---\n \"prompt.preset\" = @{ zh = \" {0} = (已通过环境变量预设)\"; en = \" {0} = (pre-set via env)\" }\n \"prompt.upgrade_keep\" = @{ zh = \" {0} = {1}(当前值,回车保留 / 输入新值覆盖)\"; en = \" {0} = {1} (current value, press Enter to keep / type new value to change)\" }\n \"prompt.upgrade_empty\" = @{ zh = \" {0} = (未设置,回车跳过 / 输入新值设置)\"; en = \" {0} = (not set, press Enter to skip / type new value to set)\" }\n \"prompt.default\" = @{ zh = \" {0} = {1}(默认)\"; en = \" {0} = {1} (default)\" }\n \"prompt.required\" = @{ zh = \"{0} 是必需的(在非交互模式下通过环境变量设置)\"; en = \"{0} is required (set via environment variable in non-interactive mode)\" }\n \"prompt.required_empty\" = @{ zh = \"{0} 是必需的\"; en = \"{0} is required\" }\n\n # --- Language switch prompt (bilingual by design) ---\n \"lang.detected.zh\" = @{ zh = \"检测到语言 / Detected language: 中文\"; en = \"检测到语言 / Detected language: 中文\" }\n \"lang.detected.en\" = @{ zh = \"检测到语言 / Detected language: English\"; en = \"检测到语言 / Detected language: English\" }\n \"lang.switch_title\" = @{ zh = \"切换语言 / Switch language:\"; en = \"切换语言 / Switch language:\" }\n \"lang.option_zh\" = @{ zh = \" 1) 中文\"; en = \" 1) 中文\" }\n \"lang.option_en\" = @{ zh = \" 2) English\"; en = \" 2) English\" }\n \"lang.prompt\" = @{ zh = \"请选择 / Enter choice\"; en = \"请选择 / Enter choice\" }\n\n # --- Error messages ---\n \"error.name_required\" = @{ zh = \"--name 是必需的\"; en = \"--name is required\" }\n \"error.fs_required\" = @{ zh = \"--fs 是必需的\"; en = \"--fs is required\" }\n \"error.fs_key_required\" = @{ zh = \"--fs-key 是必需的\"; en = \"--fs-key is required\" }\n \"error.fs_secret_required\" = @{ zh = \"--fs-secret 是必需的\"; en = \"--fs-secret is required\" }\n \"error.unknown_option\" = @{ zh = \"未知选项: {0}\"; en = \"Unknown option: {0}\" }\n \"error.docker_not_running\" = @{ zh = \"Docker 未运行。请先启动 Docker Desktop 或 Podman Desktop。\"; en = \"Docker is not running. Please start Docker Desktop or Podman Desktop first.\" }\n \"error.docker_not_found\" = @{ zh = \"未找到 docker 或 podman 命令。请先安装 Docker Desktop 或 Podman Desktop:`n Docker Desktop: https://www.docker.com/products/docker-desktop/`n Podman Desktop: https://podman-desktop.io/\"; en = \"docker or podman command not found. Please install Docker Desktop or Podman Desktop first:`n Docker Desktop: https://www.docker.com/products/docker-desktop/`n Podman Desktop: https://podman-desktop.io/\" }\n\n # --- Uninstall messages ---\n \"uninstall.title\" = @{ zh = \"正在卸载 AgentTeams...\"; en = \"Uninstalling AgentTeams...\" }\n \"uninstall.stopping_manager\" = @{ zh = \"正在停止并移除 agentteams-manager...\"; en = \"Stopping and removing agentteams-manager...\" }\n \"uninstall.stopping_workers\" = @{ zh = \"正在停止并移除 worker 容器...\"; en = \"Stopping and removing worker containers...\" }\n \"uninstall.removed\" = @{ zh = \" 已移除: {0}\"; en = \" Removed: {0}\" }\n \"uninstall.removing_volume\" = @{ zh = \"正在移除 Docker 卷: agentteams-data\"; en = \"Removing Docker volume: agentteams-data\" }\n \"uninstall.removing_env\" = @{ zh = \"正在移除 env 文件: {0}\"; en = \"Removing env file: {0}\" }\n \"uninstall.removing_proxy\" = @{ zh = \"正在停止并移除 Docker API 代理容器: agentteams-docker-proxy\"; en = \"Stopping and removing Docker API proxy container: agentteams-docker-proxy\" }\n \"uninstall.stopping_controller\" = @{ zh = \"正在停止并移除 agentteams-controller (内嵌 Tuwunel/MinIO/Higress)...\"; en = \"Stopping and removing agentteams-controller (embedded Tuwunel/MinIO/Higress)...\" }\n \"uninstall.removing_network\" = @{ zh = \"正在移除 Docker 网络: agentteams-net\"; en = \"Removing Docker network: agentteams-net\" }\n \"uninstall.removing_workspace\" = @{ zh = \"正在移除工作空间目录: {0}\"; en = \"Removing workspace directory: {0}\" }\n \"uninstall.removing_log\" = @{ zh = \"正在移除日志文件: {0}\"; en = \"Removing log file: {0}\" }\n \"uninstall.done\" = @{ zh = \"AgentTeams 已卸载。\"; en = \"AgentTeams has been uninstalled.\" }\n}\n\n# Get-Msg: look up message by key, with -f style argument substitution.\n# Falls back to English if the current language translation is missing.\nfunction Get-Msg {\n param(\n [Parameter(Mandatory)][string]$Key,\n [object[]]$f\n )\n $lang = $script:AGENTTEAMS_LANGUAGE\n if (-not $lang) { $lang = \"en\" }\n $entry = $script:Messages[$Key]\n if (-not $entry) { return $Key }\n $text = $entry[$lang]\n if (-not $text) { $text = $entry[\"en\"] }\n if (-not $text) { return $Key }\n if ($f) { return ($text -f $f) }\n return $text\n}\n\nfunction Get-LanIP {\n # Detect local LAN IP address on Windows\n try {\n # Get network adapters with IPv4 addresses, prefer connected/active interfaces\n $adapters = Get-NetIPAddress -AddressFamily IPv4 -ErrorAction SilentlyContinue |\n Where-Object {\n $_.IPAddress -notlike \"127.*\" -and\n $_.PrefixOrigin -ne \"WellKnown\" -and\n $_.InterfaceAlias -notlike \"*Loopback*\"\n } |\n Sort-Object {\n # PowerShell 5.1 compatible: use if-else instead of ternary operator\n if ($_.InterfaceAlias -like \"*Wi-Fi*\" -or $_.InterfaceAlias -like \"*Ethernet*\") { 0 } else { 1 }\n }\n\n if ($adapters) {\n return $adapters[0].IPAddress\n }\n\n # Fallback: use ipconfig\n $ipconfig = ipconfig 2>$null\n $ip = ($ipconfig | Select-String \"IPv4 Address.*?: (\\d+\\.\\d+\\.\\d+\\.\\d+)\" | Select-Object -First 1)\n if ($ip -match \"(\\d+\\.\\d+\\.\\d+\\.\\d+)\") {\n return $Matches[1]\n }\n }\n catch {\n # Ignore errors\n }\n\n return \"\"\n}\n\n# Known models list — used to detect custom models during install\n$script:KnownModels = @(\n \"gpt-5.4\", \"gpt-5.3-codex\", \"gpt-5-mini\", \"gpt-5-nano\",\n \"claude-opus-4-6\", \"claude-sonnet-4-6\", \"claude-haiku-4-5\",\n \"qwen3.6-plus\", \"qwen3.5-plus\", \"deepseek-chat\", \"deepseek-reasoner\",\n \"kimi-k2.5\", \"glm-5\", \"MiniMax-M2.7\", \"MiniMax-M2.7-highspeed\", \"MiniMax-M2.5\"\n)\n\nfunction Test-KnownModel {\n param([string]$Model)\n return $script:KnownModels -contains $Model\n}\n\nfunction Request-CustomModelParams {\n param([string]$Model)\n if (Test-KnownModel $Model) {\n $script:config.MODEL_CONTEXT_WINDOW = \"\"\n $script:config.MODEL_MAX_TOKENS = \"\"\n $script:config.MODEL_REASONING = \"\"\n $script:config.MODEL_VISION = \"\"\n return\n }\n Write-Host \"\"\n Write-Log (Get-Msg \"llm.custom_model.detected\" -f $Model)\n Write-Host \"\"\n $ctxInput = Read-Host \" $(Get-Msg 'llm.custom_model.context_prompt')\"\n if ($ctxInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $script:config.MODEL_CONTEXT_WINDOW = if ($ctxInput) { $ctxInput } else { \"150000\" }\n $maxInput = Read-Host \" $(Get-Msg 'llm.custom_model.max_tokens_prompt')\"\n if ($maxInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $script:config.MODEL_MAX_TOKENS = if ($maxInput) { $maxInput } else { \"128000\" }\n $reasoningInput = Read-Host \" $(Get-Msg 'llm.custom_model.reasoning_prompt')\"\n if ($reasoningInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $script:config.MODEL_REASONING = if ($reasoningInput -match \"^[nN]\") { \"false\" } else { \"true\" }\n $visionInput = Read-Host \" $(Get-Msg 'llm.custom_model.vision_prompt')\"\n if ($visionInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $script:config.MODEL_VISION = if ($visionInput -match \"^[yY]\") { \"true\" } else { \"false\" }\n Write-Log (Get-Msg \"llm.custom_model.summary\" -f $script:config.MODEL_CONTEXT_WINDOW, $script:config.MODEL_MAX_TOKENS, $script:config.MODEL_REASONING, $script:config.MODEL_VISION)\n}\n\nfunction New-RandomKey {\n # Generate 64 character hex string (32 bytes)\n $bytes = New-Object byte[] 32\n $rng = [System.Security.Cryptography.RandomNumberGenerator]::Create()\n $rng.GetBytes($bytes)\n return [BitConverter]::ToString($bytes).Replace(\"-\", \"\").ToLower()\n}\n\nfunction ConvertTo-DockerPath {\n param([string]$Path)\n\n # Convert Windows path to Docker mount format\n $fullPath = (Resolve-Path $Path -ErrorAction SilentlyContinue).Path\n if (-not $fullPath) {\n $fullPath = $Path\n }\n\n # Convert C:\\path to /c/path format for Docker\n if ($fullPath -match \"^([A-Za-z]):\") {\n $drive = $Matches[1].ToLower()\n $rest = $fullPath.Substring(2).Replace(\"\\\", \"/\")\n return \"/$drive$rest\"\n }\n return $fullPath.Replace(\"\\\", \"/\")\n}\n\n# Resolve the embedded controller image. Embedded mode is the only supported\n# architecture since PR #616 (manager image no longer bundles Higress/Tuwunel/MinIO).\n# Mirrors install/agentteams-install.sh::resolve_embedded_image — fail fast when the\n# embedded image is unavailable rather than silently falling back to the broken\n# legacy single-container path.\n# Sets $script:EMBEDDED_IMAGE and $script:AGENTTEAMS_USE_EMBEDDED.\nfunction Resolve-EmbeddedImage {\n $script:AGENTTEAMS_USE_EMBEDDED = \"1\"\n\n # Explicit override always wins (used by `make install-embedded` for local builds).\n if ($env:AGENTTEAMS_INSTALL_EMBEDDED_IMAGE) {\n $script:EMBEDDED_IMAGE = $env:AGENTTEAMS_INSTALL_EMBEDDED_IMAGE\n return\n }\n\n $versioned = \"$($script:AGENTTEAMS_REGISTRY)/agentteams/agentteams-embedded:$($script:AGENTTEAMS_VERSION)\"\n $latestTag = \"$($script:AGENTTEAMS_REGISTRY)/agentteams/agentteams-embedded:latest\"\n\n if ($script:AGENTTEAMS_VERSION -eq \"latest\") {\n $script:EMBEDDED_IMAGE = $latestTag\n return\n }\n\n docker pull $versioned *>$null\n if ($LASTEXITCODE -eq 0) {\n $script:EMBEDDED_IMAGE = $versioned\n return\n }\n docker pull $latestTag *>$null\n if ($LASTEXITCODE -eq 0) {\n Write-Log \"embedded $($script:AGENTTEAMS_VERSION) not found, using latest\"\n $script:EMBEDDED_IMAGE = $latestTag\n return\n }\n\n # Escape hatch for older versions (AGENTTEAMS_VERSION <= v1.0.9) whose manager image\n # still bundled the infrastructure — opt-in only, never silent.\n if ($env:AGENTTEAMS_FORCE_LEGACY -eq \"1\") {\n Write-Log \"WARNING: AGENTTEAMS_FORCE_LEGACY=1 - using legacy all-in-one manager architecture.\"\n Write-Log \"WARNING: This requires AGENTTEAMS_VERSION <= v1.0.9 (older bundled manager image).\"\n Write-Log \"WARNING: Newer slim manager images will hang on 'Waiting for Higress Gateway'.\"\n $script:AGENTTEAMS_USE_EMBEDDED = \"0\"\n return\n }\n\n Write-Host \"$($script:ESC)[31m[AgentTeams ERROR]$($script:ESC)[0m Embedded controller image is not available in the registry:\" -ForegroundColor Red\n Write-Host \" - tried: $versioned\" -ForegroundColor Red\n Write-Host \" - tried: $latestTag\" -ForegroundColor Red\n Write-Host \"\" -ForegroundColor Red\n Write-Host \"Embedded mode is the only supported architecture since PR #616.\" -ForegroundColor Red\n Write-Host \"How to resolve:\" -ForegroundColor Red\n Write-Host \" 1) Pin to a AGENTTEAMS_VERSION whose embedded image has been published, or\" -ForegroundColor Red\n Write-Host \" wait for the release pipeline to publish it.\" -ForegroundColor Red\n Write-Host \" 2) For a local build, run: make install-embedded\" -ForegroundColor Red\n Write-Host \" (builds and uses the local embedded image without touching the registry).\" -ForegroundColor Red\n Write-Host \" 3) Override with a custom image:\" -ForegroundColor Red\n Write-Host \" `$env:AGENTTEAMS_INSTALL_EMBEDDED_IMAGE=...; .\\agentteams-install.ps1\" -ForegroundColor Red\n Exit-Script 1\n}\n\nfunction Wait-ManagerReady {\n param(\n [string]$Container = \"agentteams-manager\",\n [int]$Timeout = $(if ($env:AGENTTEAMS_READY_TIMEOUT) { [int]$env:AGENTTEAMS_READY_TIMEOUT } else { 300 })\n )\n\n $elapsed = 0\n Write-Log (Get-Msg \"install.wait_ready\" -f $Timeout)\n\n $runtime = if ($script:config.MANAGER_RUNTIME) { $script:config.MANAGER_RUNTIME } else { \"copaw\" }\n\n while ($elapsed -lt $Timeout) {\n try {\n switch ($runtime) {\n \"copaw\" {\n $result = docker exec $Container curl -sf http://127.0.0.1:18799/api/agents 2>$null\n if ($result -match '\"agents\"') {\n Write-Log (Get-Msg \"install.wait_ready.ok\")\n return $true\n }\n }\n default {\n $result = docker exec $Container openclaw gateway health --json 2>$null\n if ($result -match '\"ok\"') {\n Write-Log (Get-Msg \"install.wait_ready.ok\")\n return $true\n }\n }\n }\n } catch {\n # Ignore errors during polling\n }\n\n Start-Sleep -Seconds 5\n $elapsed += 5\n Write-Host \"`r$($script:ESC)[36m[AgentTeams]$($script:ESC)[0m $(Get-Msg 'install.wait_ready.waiting' -f $elapsed, $Timeout)\" -NoNewline\n }\n\n Write-Host \"\"\n Write-Error (Get-Msg \"install.wait_ready.timeout\" -f $Timeout, $Container)\n}\n\nfunction Wait-MatrixReady {\n param(\n [string]$Container = \"agentteams-manager\",\n [int]$Timeout = $(if ($env:AGENTTEAMS_READY_TIMEOUT) { [int]$env:AGENTTEAMS_READY_TIMEOUT } else { 300 })\n )\n\n $elapsed = 0\n Write-Log (Get-Msg \"install.wait_matrix\" -f $Timeout)\n\n while ($elapsed -lt $Timeout) {\n try {\n $result = docker exec $Container curl -sf http://127.0.0.1:6167/_tuwunel/server_version 2>$null\n if ($result) {\n Write-Log (Get-Msg \"install.wait_matrix.ok\")\n return $true\n }\n } catch {\n # Ignore errors during polling\n }\n\n Start-Sleep -Seconds 5\n $elapsed += 5\n Write-Host \"`r$($script:ESC)[36m[AgentTeams]$($script:ESC)[0m $(Get-Msg 'install.wait_matrix.waiting' -f $elapsed, $Timeout)\" -NoNewline\n }\n\n Write-Host \"\"\n Write-Error (Get-Msg \"install.wait_matrix.timeout\" -f $Timeout, $Container)\n}\n\n# Read KEY=value from /data/agentteams-secrets.env on a Docker volume (manager container not required).\n# Requires $script:EMBEDDED_IMAGE (set by Resolve-EmbeddedImage before Install-Manager uses this).\nfunction Read-AgentTeamsSecretFromDataVolume {\n param(\n [string]$VolumeName,\n [string]$Key\n )\n if ([string]::IsNullOrEmpty($VolumeName) -or [string]::IsNullOrEmpty($Key) -or [string]::IsNullOrEmpty($script:EMBEDDED_IMAGE)) {\n return \"\"\n }\n $grepKey = [regex]::Escape($Key)\n $shCmd = \"grep \"\"^${grepKey}=\"\" /data/agentteams-secrets.env 2>/dev/null | cut -d= -f2- | head -1 | tr -d '\\r'\"\n $out = docker run --rm --entrypoint sh -v \"${VolumeName}:/data:ro\" $script:EMBEDDED_IMAGE -c $shCmd 2>$null\n if ($null -eq $out) { return \"\" }\n return ($out | Out-String).Trim()\n}\n\n# Read KEY=value from /data/worker-creds/<worker>.env on a Docker volume.\nfunction Read-AgentTeamsWorkerCredsFromVolume {\n param(\n [string]$VolumeName,\n [string]$WorkerName,\n [string]$Key\n )\n if ([string]::IsNullOrEmpty($VolumeName) -or [string]::IsNullOrEmpty($WorkerName) -or [string]::IsNullOrEmpty($Key) -or [string]::IsNullOrEmpty($script:EMBEDDED_IMAGE)) {\n return \"\"\n }\n $grepKey = [regex]::Escape($Key)\n $path = \"/data/worker-creds/${WorkerName}.env\"\n $shCmd = \"grep \"\"^${grepKey}=\"\" \"\"${path}\"\" 2>/dev/null | cut -d= -f2- | head -1 | tr -d '\\r'\"\n $out = docker run --rm --entrypoint sh -v \"${VolumeName}:/data:ro\" $script:EMBEDDED_IMAGE -c $shCmd 2>$null\n if ($null -eq $out) { return \"\" }\n return ($out | Out-String).Trim()\n}\n\n# Read admin_dm_room_id from host workspace state.json (fallback when Matrix API is unavailable).\nfunction Read-AgentTeamsAdminDmRoomFromWorkspace {\n param([string]$WorkspaceDir)\n if ([string]::IsNullOrEmpty($WorkspaceDir)) { return \"\" }\n $statePath = Join-Path $WorkspaceDir \"state.json\"\n if (-not (Test-Path $statePath)) { return \"\" }\n try {\n $j = Get-Content $statePath -Raw -ErrorAction Stop | ConvertFrom-Json\n $rid = $j.admin_dm_room_id\n if ($null -eq $rid) { return \"\" }\n $s = [string]$rid\n if ($s -eq \"\" -or $s -eq \"null\") { return \"\" }\n return $s.Trim()\n } catch {\n return \"\"\n }\n}\n\nfunction Get-AgentTeamsRandomHex {\n param([int]$ByteCount)\n $bytes = New-Object byte[] $ByteCount\n $rng = [System.Security.Cryptography.RandomNumberGenerator]::Create()\n $rng.GetBytes($bytes)\n return ([BitConverter]::ToString($bytes)).Replace(\"-\", \"\").ToLower()\n}\n\n# Resolve admin DM room with @manager (small room) via Matrix Client API inside agentteams-manager.\nfunction Get-AgentTeamsAdminDmRoomViaMatrix {\n param(\n [string]$AdminUser,\n [string]$AdminPassword\n )\n $running = docker ps --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-manager$\"\n if (-not $running) { return \"\" }\n if ([string]::IsNullOrEmpty($AdminPassword)) { return \"\" }\n\n $loginObj = @{\n type = \"m.login.password\"\n identifier = @{ type = \"m.id.user\"; user = $AdminUser }\n password = $AdminPassword\n }\n $loginJson = $loginObj | ConvertTo-Json -Compress -Depth 5\n try {\n $loginRaw = $loginJson | docker exec -i agentteams-manager sh -c \"curl -sf -X POST http://127.0.0.1:6167/_matrix/client/v3/login -H 'Content-Type: application/json' -d @-\" 2>$null\n if (-not $loginRaw) { return \"\" }\n $loginResp = $loginRaw | ConvertFrom-Json\n $token = [string]$loginResp.access_token\n if ([string]::IsNullOrEmpty($token)) { return \"\" }\n\n $roomsRaw = docker exec agentteams-manager curl -sf -X GET -H \"Authorization: Bearer $token\" `\n \"http://127.0.0.1:6167/_matrix/client/v3/joined_rooms\" 2>$null\n if (-not $roomsRaw) { return \"\" }\n $roomsObj = $roomsRaw | ConvertFrom-Json\n $roomList = @($roomsObj.joined_rooms)\n foreach ($roomId in $roomList) {\n if ([string]::IsNullOrEmpty($roomId)) { continue }\n $enc = $roomId.Replace(\"!\", \"%21\")\n $memRaw = docker exec agentteams-manager curl -sf -X GET -H \"Authorization: Bearer $token\" `\n \"http://127.0.0.1:6167/_matrix/client/v3/rooms/${enc}/members\" 2>$null\n if (-not $memRaw) { continue }\n $memObj = $memRaw | ConvertFrom-Json\n $chunk = @($memObj.chunk)\n $memberIds = @($chunk | ForEach-Object { $_.state_key })\n $match = $false\n foreach ($m in $memberIds) {\n $beforeColon = ($m -split \":\")[0]\n if ($m -like \"*manager*\" -and $beforeColon -notlike \"*admin*\") {\n $match = $true\n break\n }\n }\n if ($match -and $memberIds.Count -le 3) {\n return $roomId\n }\n }\n } catch {\n return \"\"\n }\n return \"\"\n}\n\nfunction New-EnvFile {\n param([hashtable]$Config, [string]$Path)\n\n $content = @\"\n# AgentTeams Manager Configuration\n# Generated by agentteams-install.ps1 on $(Get-Date -Format \"yyyy-MM-dd HH:mm:ss\")\n\n# Language\nAGENTTEAMS_LANGUAGE=$($Config.LANGUAGE)\n\n# LLM\nAGENTTEAMS_LLM_PROVIDER=$($Config.LLM_PROVIDER)\nAGENTTEAMS_DEFAULT_MODEL=$($Config.DEFAULT_MODEL)\nAGENTTEAMS_LLM_API_KEY=$($Config.LLM_API_KEY)\nAGENTTEAMS_OPENAI_BASE_URL=$($Config.OPENAI_BASE_URL)\nAGENTTEAMS_MODEL_CONTEXT_WINDOW=$($Config.MODEL_CONTEXT_WINDOW)\nAGENTTEAMS_MODEL_MAX_TOKENS=$($Config.MODEL_MAX_TOKENS)\nAGENTTEAMS_MODEL_REASONING=$($Config.MODEL_REASONING)\nAGENTTEAMS_MODEL_VISION=$($Config.MODEL_VISION)\n\n# Embedding model (empty = disabled, default: text-embedding-v4)\nAGENTTEAMS_EMBEDDING_MODEL=$($Config.EMBEDDING_MODEL)\n\n# Admin\nAGENTTEAMS_ADMIN_USER=$($Config.ADMIN_USER)\nAGENTTEAMS_ADMIN_PASSWORD=$($Config.ADMIN_PASSWORD)\n\n# Ports\nAGENTTEAMS_LOCAL_ONLY=$($Config.LOCAL_ONLY)\nAGENTTEAMS_PORT_GATEWAY=$($Config.PORT_GATEWAY)\nAGENTTEAMS_PORT_CONSOLE=$($Config.PORT_CONSOLE)\nAGENTTEAMS_PORT_ELEMENT_WEB=$($Config.PORT_ELEMENT_WEB)\nAGENTTEAMS_PORT_MANAGER_CONSOLE=$($Config.PORT_MANAGER_CONSOLE)\n\n# Matrix\nAGENTTEAMS_MATRIX_DOMAIN=$($Config.MATRIX_DOMAIN)\nAGENTTEAMS_MATRIX_CLIENT_DOMAIN=$($Config.MATRIX_CLIENT_DOMAIN)\n\n# Gateway\nAGENTTEAMS_AI_GATEWAY_DOMAIN=$($Config.AI_GATEWAY_DOMAIN)\nAGENTTEAMS_MANAGER_GATEWAY_KEY=$($Config.MANAGER_GATEWAY_KEY)\n\n# File System\nAGENTTEAMS_FS_DOMAIN=$($Config.FS_DOMAIN)\nAGENTTEAMS_CONSOLE_DOMAIN=$($Config.CONSOLE_DOMAIN)\nAGENTTEAMS_MINIO_USER=$($Config.MINIO_USER)\nAGENTTEAMS_MINIO_PASSWORD=$($Config.MINIO_PASSWORD)\n\n# Internal\nAGENTTEAMS_MANAGER_PASSWORD=$($Config.MANAGER_PASSWORD)\nAGENTTEAMS_REGISTRATION_TOKEN=$($Config.REGISTRATION_TOKEN)\n\n# GitHub (optional)\nAGENTTEAMS_GITHUB_TOKEN=$($Config.GITHUB_TOKEN)\n\n# Nacos package import defaults\nAGENTTEAMS_NACOS_REGISTRY_URI=$(if ($env:AGENTTEAMS_NACOS_REGISTRY_URI) { $env:AGENTTEAMS_NACOS_REGISTRY_URI } else { \"nacos://market.agentteams.io:80/public\" })\nAGENTTEAMS_NACOS_USERNAME=$($env:AGENTTEAMS_NACOS_USERNAME)\nAGENTTEAMS_NACOS_PASSWORD=$($env:AGENTTEAMS_NACOS_PASSWORD)\nAGENTTEAMS_NACOS_TOKEN=$($env:AGENTTEAMS_NACOS_TOKEN)\n\n# Skills Registry (optional, default: nacos://market.agentteams.io:80/public)\nAGENTTEAMS_SKILLS_API_URL=$(if ($Config.SKILLS_API_URL) { $Config.SKILLS_API_URL } else { \"nacos://market.agentteams.io:80/public\" })\n\n# OpenClaw CMS plugin (optional)\nAGENTTEAMS_CMS_TRACES_ENABLED=$(if ($env:AGENTTEAMS_CMS_TRACES_ENABLED) { $env:AGENTTEAMS_CMS_TRACES_ENABLED } else { \"false\" })\nAGENTTEAMS_CMS_ENDPOINT=$($env:AGENTTEAMS_CMS_ENDPOINT)\nAGENTTEAMS_CMS_LICENSE_KEY=$($env:AGENTTEAMS_CMS_LICENSE_KEY)\nAGENTTEAMS_CMS_PROJECT=$($env:AGENTTEAMS_CMS_PROJECT)\nAGENTTEAMS_CMS_WORKSPACE=$($env:AGENTTEAMS_CMS_WORKSPACE)\nAGENTTEAMS_CMS_SERVICE_NAME=$(if ($env:AGENTTEAMS_CMS_SERVICE_NAME) { $env:AGENTTEAMS_CMS_SERVICE_NAME } else { \"agentteams-manager\" })\nAGENTTEAMS_CMS_METRICS_ENABLED=$(if ($env:AGENTTEAMS_CMS_METRICS_ENABLED) { $env:AGENTTEAMS_CMS_METRICS_ENABLED } else { \"false\" })\n\n# Worker images (for direct container creation)\nAGENTTEAMS_WORKER_IMAGE=$($Config.WORKER_IMAGE)\nAGENTTEAMS_COPAW_WORKER_IMAGE=$($Config.COPAW_WORKER_IMAGE)\nAGENTTEAMS_HERMES_WORKER_IMAGE=$($Config.HERMES_WORKER_IMAGE)\n\n# Manager runtime (openclaw | copaw)\nAGENTTEAMS_MANAGER_RUNTIME=$($Config.MANAGER_RUNTIME)\n\n# Default Worker runtime (openclaw | copaw | hermes)\nAGENTTEAMS_DEFAULT_WORKER_RUNTIME=$($Config.DEFAULT_WORKER_RUNTIME)\n\n# Matrix E2EE (0=disabled, 1=enabled; default: 0)\nAGENTTEAMS_MATRIX_E2EE=$($Config.MATRIX_E2EE)\n\n# Docker API proxy (0=disabled, 1=enabled; default: 1)\nAGENTTEAMS_DOCKER_PROXY=$($Config.DOCKER_PROXY)\n\n# Docker API proxy: additional allowed image sources (comma-separated)\nAGENTTEAMS_PROXY_ALLOWED_REGISTRIES=$($Config.PROXY_ALLOWED_REGISTRIES)\n\n# Worker idle timeout in minutes (default: 720 = 12 hours)\nAGENTTEAMS_WORKER_IDLE_TIMEOUT=$($Config.WORKER_IDLE_TIMEOUT)\n\n# JVM Args for Higress Console\nJVM_ARGS=$($env:JVM_ARGS)\n\n# Higress WASM plugin image registry (auto-selected by timezone)\nHIGRESS_ADMIN_WASM_PLUGIN_IMAGE_REGISTRY=$($Config.REGISTRY)\n\n# Data persistence\nAGENTTEAMS_DATA_DIR=$($Config.DATA_DIR)\n# Manager workspace (skills, memory, state - host-editable)\nAGENTTEAMS_WORKSPACE_DIR=$($Config.WORKSPACE_DIR)\n# Host directory sharing\nAGENTTEAMS_HOST_SHARE_DIR=$($Config.HOST_SHARE_DIR)\n\"@\n\n Set-Content -Path $Path -Value $content -Encoding UTF8\n Write-Log (Get-Msg \"install.config_saved\" -f $Path)\n}\n\n# ============================================================\n# Prompt Functions\n# ============================================================\n\nfunction Read-Prompt {\n param(\n [string]$VarName,\n [string]$PromptText,\n [string]$Default = \"\",\n [switch]$Secret,\n [switch]$Optional\n )\n\n # Check if already set in environment\n $envValue = [Environment]::GetEnvironmentVariable($VarName)\n if ($envValue) {\n if ($script:AGENTTEAMS_UPGRADE -and -not $script:AGENTTEAMS_NON_INTERACTIVE) {\n # Show current value (masked for secrets) and let user change it\n $displayValue = $envValue\n if ($Secret) {\n if ($envValue.Length -le 8) {\n $displayValue = \"****\"\n } else {\n $displayValue = $envValue.Substring(0, 4) + \"****\" + $envValue.Substring($envValue.Length - 4)\n }\n }\n Write-Log (Get-Msg \"prompt.upgrade_keep\" -f $PromptText, $displayValue)\n $prompt = $PromptText\n if ($Secret) {\n $newValue = Read-Host -Prompt $prompt -AsSecureString\n $newValue = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto(\n [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($newValue)\n )\n } else {\n $newValue = Read-Host -Prompt $prompt\n if ($newValue -eq \"b\") { $script:StepResult = \"back\"; return $null }\n }\n if ($newValue) {\n return $newValue\n }\n return $envValue\n }\n Write-Log (Get-Msg \"prompt.preset\" -f $PromptText)\n return $envValue\n }\n # Upgrade mode: optional fields with empty value — let user set a new value\n elseif ($Optional -and $script:AGENTTEAMS_UPGRADE -and -not $script:AGENTTEAMS_NON_INTERACTIVE) {\n Write-Log (Get-Msg \"prompt.upgrade_empty\" -f $PromptText)\n $prompt = $PromptText\n if ($Secret) {\n $newValue = Read-Host -Prompt $prompt -AsSecureString\n $newValue = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto(\n [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($newValue)\n )\n } else {\n $newValue = Read-Host -Prompt $prompt\n if ($newValue -eq \"b\") { $script:StepResult = \"back\"; return $null }\n }\n if ($newValue) {\n return $newValue\n }\n return \"\"\n }\n\n # Non-interactive or quickstart mode\n if ($script:AGENTTEAMS_NON_INTERACTIVE -or $script:AGENTTEAMS_QUICKSTART) {\n if ($Default) {\n Write-Log (Get-Msg \"prompt.default\" -f $PromptText, $Default)\n return $Default\n }\n elseif ($Optional) {\n return \"\"\n }\n elseif ($script:AGENTTEAMS_NON_INTERACTIVE) {\n # Only hard-error in fully non-interactive mode, not quickstart\n Write-Error (Get-Msg \"prompt.required\" -f $PromptText)\n }\n # quickstart + no default + not optional: fall through to interactive prompt\n }\n\n # Interactive prompt\n $prompt = if ($Default) { \"$PromptText [$Default]\" } else { $PromptText }\n\n if ($Secret) {\n $value = Read-Host -Prompt $prompt -AsSecureString\n $value = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto(\n [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($value)\n )\n }\n else {\n $value = Read-Host -Prompt $prompt\n if ($value -eq \"b\") { $script:StepResult = \"back\"; return $null }\n }\n\n if (-not $value -and $Default) {\n $value = $Default\n }\n\n if (-not $value -and -not $Optional) {\n Write-Error (Get-Msg \"prompt.required_empty\" -f $PromptText)\n }\n\n return $value\n}\n\n# Load current parameter values from env file for upgrade mode display\nfunction Load-CurrentParamsFromEnv {\n $envFile = $script:AGENTTEAMS_ENV_FILE\n if (Test-Path $envFile) {\n $content = Get-Content $envFile\n $content | ForEach-Object {\n if ($_ -match \"^AGENTTEAMS_LLM_PROVIDER=(.*)$\") {\n $script:config.LLM_PROVIDER = $Matches[1].Trim()\n }\n if ($_ -match \"^AGENTTEAMS_OPENAI_BASE_URL=(.*)$\") {\n $script:config.OPENAI_BASE_URL = $Matches[1].Trim()\n }\n if ($_ -match \"^AGENTTEAMS_DEFAULT_MODEL=(.*)$\") {\n $script:config.DEFAULT_MODEL = $Matches[1].Trim()\n }\n if ($_ -match \"^AGENTTEAMS_EMBEDDING_MODEL=(.*)$\") {\n $script:config.EMBEDDING_MODEL = $Matches[1].Trim()\n }\n if ($_ -match \"^AGENTTEAMS_WORKSPACE_DIR=(.*)$\") {\n $script:config.WORKSPACE_DIR = $Matches[1].Trim()\n }\n if ($_ -match \"^AGENTTEAMS_HOST_SHARE_DIR=(.*)$\") {\n $script:config.HOST_SHARE_DIR = $Matches[1].Trim()\n }\n }\n }\n}\n\n# ============================================================\n# OpenAI-Compatible Provider\n# ============================================================\n\nfunction Test-LlmConnectivity {\n param(\n [string]$BaseUrl,\n [string]$ApiKey,\n [string]$Model,\n [string]$Hint = \"\"\n )\n Write-Log (Get-Msg \"llm.openai.test.testing\")\n $uri = ($BaseUrl.TrimEnd('/')) + \"/chat/completions\"\n $bodyHash = @{\n model = $Model\n messages = @(@{ role = \"user\"; content = \"hi\" })\n }\n $body = $bodyHash | ConvertTo-Json -Compress\n try {\n $response = Invoke-WebRequest -Uri $uri -Method POST `\n -Headers @{ \"Authorization\" = \"Bearer $ApiKey\"; \"Content-Type\" = \"application/json\"; \"User-Agent\" = \"AgentTeams/$($script:AGENTTEAMS_VERSION)\" } `\n -Body $body -TimeoutSec 30 -ErrorAction Stop -UseBasicParsing\n Write-Log (Get-Msg \"llm.openai.test.ok\")\n } catch {\n $statusCode = if ($_.Exception.Response) { [int]$_.Exception.Response.StatusCode } else { 0 }\n $responseBody = \"\"\n if ($_.Exception.Response) {\n try {\n $stream = $_.Exception.Response.GetResponseStream()\n $reader = New-Object System.IO.StreamReader($stream)\n $responseBody = $reader.ReadToEnd()\n } catch {}\n }\n Write-Host (Get-Msg \"llm.openai.test.fail\" -f $statusCode, $responseBody) -ForegroundColor Yellow\n if ($Hint) {\n Write-Host $Hint -ForegroundColor Yellow\n }\n if (-not $script:AGENTTEAMS_NON_INTERACTIVE) {\n $confirm = Read-Host (Get-Msg \"llm.openai.test.confirm\")\n if ($confirm -eq \"b\" -or $confirm -eq \"B\") {\n $script:StepResult = \"back\"\n return\n }\n if ($confirm -ne \"y\" -and $confirm -ne \"Y\") {\n Write-Log (Get-Msg \"llm.openai.test.aborted\")\n Exit-Script 1\n }\n }\n }\n}\n\nfunction Test-EmbeddingConnectivity {\n param(\n [string]$BaseUrl,\n [string]$ApiKey,\n [string]$Model\n )\n Write-Log (Get-Msg \"llm.embedding.test.testing\")\n $uri = ($BaseUrl.TrimEnd('/')) + \"/embeddings\"\n $bodyHash = @{\n model = $Model\n input = \"test\"\n }\n $body = $bodyHash | ConvertTo-Json -Compress\n try {\n $response = Invoke-WebRequest -Uri $uri -Method POST `\n -Headers @{ \"Authorization\" = \"Bearer $ApiKey\"; \"Content-Type\" = \"application/json\"; \"User-Agent\" = \"AgentTeams/$($script:AGENTTEAMS_VERSION)\" } `\n -Body $body -TimeoutSec 30 -ErrorAction Stop -UseBasicParsing\n Write-Log (Get-Msg \"llm.embedding.test.ok\")\n return $true\n } catch {\n $statusCode = if ($_.Exception.Response) { [int]$_.Exception.Response.StatusCode } else { 0 }\n $responseBody = \"\"\n if ($_.Exception.Response) {\n try {\n $stream = $_.Exception.Response.GetResponseStream()\n $reader = New-Object System.IO.StreamReader($stream)\n $responseBody = $reader.ReadToEnd()\n } catch {}\n }\n Write-Host (Get-Msg \"llm.embedding.test.fail\" -f $statusCode, $responseBody) -ForegroundColor Yellow\n return $false\n }\n}\n\nfunction New-OpenAICompatProvider {\n param(\n [string]$BaseUrl,\n [string]$ApiKey,\n [int]$ConsolePort = 18001\n )\n\n if (-not $BaseUrl -or -not $ApiKey) {\n Write-Log (Get-Msg \"install.openai_compat.missing\")\n return $false\n }\n\n $consoleUrl = \"http://localhost:$ConsolePort\"\n\n # Parse base URL\n $protocol = \"https\"\n $port = 443\n $urlWithoutProto = $BaseUrl -replace \"^https?://\", \"\"\n\n if ($BaseUrl -match \"^http://\") {\n $protocol = \"http\"\n $port = 80\n }\n\n $domain = $urlWithoutProto.Split(\"/\")[0]\n\n if ($domain -match \":(\\d+)$\") {\n $port = [int]$Matches[1]\n $domain = $domain -replace \":\\d+$\", \"\"\n }\n\n Write-Log (Get-Msg \"install.openai_compat.creating\")\n Write-Log (Get-Msg \"install.openai_compat.domain\" -f $domain)\n Write-Log (Get-Msg \"install.openai_compat.port\" -f $port)\n Write-Log (Get-Msg \"install.openai_compat.protocol\" -f $protocol)\n\n $serviceName = \"openai-compat\"\n\n # Create DNS service source\n $serviceBody = @{\n type = \"dns\"\n name = $serviceName\n port = $port.ToString()\n protocol = $protocol\n proxyName = \"\"\n domain = $domain\n } | ConvertTo-Json -Compress\n\n try {\n Invoke-RestMethod -Uri \"$consoleUrl/v1/service-sources\" -Method POST -ContentType \"application/json\" -Body $serviceBody -ErrorAction SilentlyContinue | Out-Null\n }\n catch {\n Write-Log (Get-Msg \"install.openai_compat.service_fail\")\n }\n\n Start-Sleep -Seconds 2\n\n # Create AI provider\n $providerBody = @{\n type = \"openai\"\n name = \"openai-compat\"\n tokens = @($ApiKey)\n version = 0\n protocol = \"openai/v1\"\n tokenFailoverConfig = @{ enabled = $false }\n rawConfigs = @{\n openaiCustomUrl = $BaseUrl\n openaiCustomServiceName = \"$serviceName.dns\"\n openaiCustomServicePort = $port\n }\n } | ConvertTo-Json -Compress -Depth 3\n\n try {\n Invoke-RestMethod -Uri \"$consoleUrl/v1/ai/providers\" -Method POST -ContentType \"application/json\" -Body $providerBody -ErrorAction SilentlyContinue | Out-Null\n Write-Log (Get-Msg \"install.openai_compat.success\")\n return $true\n }\n catch {\n Write-Log (Get-Msg \"install.openai_compat.provider_fail\")\n return $false\n }\n}\n\n# ============================================================\n# State Machine Helpers\n# ============================================================\n\nfunction Test-ShouldSkipStep {\n param([string]$StepFn)\n switch ($StepFn) {\n { $_ -in @(\"Step-Lang\", \"Step-Mode\") } {\n return $script:AGENTTEAMS_NON_INTERACTIVE\n }\n \"Step-Existing\" {\n return (-not (Test-Path $script:AGENTTEAMS_ENV_FILE))\n }\n # Keep-All upgrade mode: skip all config steps, values are already loaded\n { $_ -in @(\"Step-Llm\", \"Step-Admin\", \"Step-Network\", \"Step-Ports\", \"Step-Domains\",\n \"Step-Github\", \"Step-Skills\",\n \"Step-Runtime\", \"Step-ManagerRuntime\", \"Step-E2ee\", \"Step-DockerProxy\",\n \"Step-Idle\", \"Step-Hostshare\") } {\n if ($script:AGENTTEAMS_UPGRADE -and $env:AGENTTEAMS_UPGRADE_KEEP_ALL -eq \"1\") { return $true }\n return $false\n }\n { $_ -in @(\"Step-Volume\", \"Step-Workspace\") } {\n if ($script:AGENTTEAMS_UPGRADE -and $env:AGENTTEAMS_UPGRADE_KEEP_ALL -eq \"1\") { return $true }\n if ($script:AGENTTEAMS_NON_INTERACTIVE) { return $true }\n if ($script:AGENTTEAMS_QUICKSTART) { return $true }\n return $false\n }\n { $_ -in @(\"Step-E2ee\", \"Step-Idle\") } {\n if ($script:AGENTTEAMS_NON_INTERACTIVE) { return $true }\n if ($script:AGENTTEAMS_QUICKSTART -and -not $script:AGENTTEAMS_UPGRADE) { return $true }\n return $false\n }\n \"Step-DockerProxy\" {\n # Embedded controller IS the proxy/orchestrator — no separate proxy step needed.\n if ($script:AGENTTEAMS_USE_EMBEDDED -eq \"1\") { return $true }\n if ($script:AGENTTEAMS_NON_INTERACTIVE) { return $true }\n if ($script:AGENTTEAMS_QUICKSTART -and -not $script:AGENTTEAMS_UPGRADE) { return $true }\n return $false\n }\n \"Step-ManagerRuntime\" {\n if ($script:AGENTTEAMS_NON_INTERACTIVE) { return $true }\n return $false\n }\n \"Step-Hostshare\" {\n if ($script:AGENTTEAMS_NON_INTERACTIVE) { return $true }\n if ($script:AGENTTEAMS_QUICKSTART) { return $true }\n return $false\n }\n default { return $false }\n }\n}\n\nfunction Clear-StepVars {\n param([string]$StepFn)\n switch ($StepFn) {\n \"Step-Mode\" { $script:AGENTTEAMS_QUICKSTART = $false }\n \"Step-Existing\" {\n $script:AGENTTEAMS_UPGRADE = $false\n $script:UPGRADE_EXISTING_WORKERS = $null\n }\n \"Step-Llm\" {\n foreach ($k in @(\"AGENTTEAMS_LLM_PROVIDER\",\"AGENTTEAMS_DEFAULT_MODEL\",\"AGENTTEAMS_OPENAI_BASE_URL\",\n \"AGENTTEAMS_LLM_API_KEY\",\"AGENTTEAMS_MODEL_CONTEXT_WINDOW\",\"AGENTTEAMS_MODEL_MAX_TOKENS\",\n \"AGENTTEAMS_MODEL_REASONING\",\"AGENTTEAMS_MODEL_VISION\")) {\n [Environment]::SetEnvironmentVariable($k, $null, \"Process\")\n }\n }\n \"Step-Admin\" {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_ADMIN_USER\", $null, \"Process\")\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_ADMIN_PASSWORD\", $null, \"Process\")\n }\n \"Step-Network\" {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_LOCAL_ONLY\", $null, \"Process\")\n }\n \"Step-Ports\" {\n foreach ($k in @(\"AGENTTEAMS_PORT_GATEWAY\",\"AGENTTEAMS_PORT_CONSOLE\",\"AGENTTEAMS_PORT_ELEMENT_WEB\",\"AGENTTEAMS_PORT_MANAGER_CONSOLE\")) {\n [Environment]::SetEnvironmentVariable($k, $null, \"Process\")\n }\n }\n \"Step-Domains\" {\n foreach ($k in @(\"AGENTTEAMS_MATRIX_DOMAIN\",\"AGENTTEAMS_MATRIX_CLIENT_DOMAIN\",\"AGENTTEAMS_AI_GATEWAY_DOMAIN\",\"AGENTTEAMS_FS_DOMAIN\",\"AGENTTEAMS_CONSOLE_DOMAIN\")) {\n [Environment]::SetEnvironmentVariable($k, $null, \"Process\")\n }\n }\n \"Step-Github\" {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_GITHUB_TOKEN\", $null, \"Process\")\n }\n \"Step-Skills\" {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_SKILLS_API_URL\", $null, \"Process\")\n }\n \"Step-Volume\" {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_DATA_DIR\", $null, \"Process\")\n }\n \"Step-Workspace\" {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_WORKSPACE_DIR\", $null, \"Process\")\n }\n \"Step-Runtime\" {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_DEFAULT_WORKER_RUNTIME\", $null, \"Process\")\n }\n \"Step-ManagerRuntime\" {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_MANAGER_RUNTIME\", $null, \"Process\")\n }\n \"Step-E2ee\" {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_MATRIX_E2EE\", $null, \"Process\")\n }\n \"Step-DockerProxy\" {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_DOCKER_PROXY\", $null, \"Process\")\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_PROXY_ALLOWED_REGISTRIES\", $null, \"Process\")\n }\n \"Step-Idle\" {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_WORKER_IDLE_TIMEOUT\", $null, \"Process\")\n }\n \"Step-Hostshare\" {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_HOST_SHARE_DIR\", $null, \"Process\")\n }\n }\n}\n\n# ============================================================\n# Individual step functions\n# ============================================================\n\nfunction Step-Lang {\n $langDefaultChoice = if ($script:AGENTTEAMS_LANGUAGE -eq \"zh\") { \"1\" } else { \"2\" }\n $langDetectedKey = \"lang.detected.$($script:AGENTTEAMS_LANGUAGE)\"\n Write-Log (Get-Msg $langDetectedKey)\n Write-Log (Get-Msg \"lang.switch_title\")\n Write-Host (Get-Msg \"lang.option_zh\")\n Write-Host (Get-Msg \"lang.option_en\")\n Write-Host \"\"\n $langChoice = Read-Host \"$(Get-Msg 'lang.prompt') [$langDefaultChoice]\"\n if (-not $langChoice) { $langChoice = $langDefaultChoice }\n if ($langChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n switch ($langChoice) {\n \"1\" { $script:AGENTTEAMS_LANGUAGE = \"zh\" }\n \"2\" { $script:AGENTTEAMS_LANGUAGE = \"en\" }\n }\n $env:AGENTTEAMS_LANGUAGE = $script:AGENTTEAMS_LANGUAGE\n Write-Log \"\"\n}\n\nfunction Step-Mode {\n Write-Log (Get-Msg \"install.mode.title\")\n Write-Host \"\"\n Write-Host (Get-Msg \"install.mode.choose\")\n Write-Host (Get-Msg \"install.mode.quickstart\")\n Write-Host (Get-Msg \"install.mode.manual\")\n Write-Host \"\"\n $choice = Read-Host (Get-Msg \"install.mode.prompt\")\n $choice = if ($choice) { $choice } else { \"1\" }\n if ($choice -eq \"b\") { $script:StepResult = \"back\"; return }\n switch -Regex ($choice) {\n \"^(1|quick|quickstart)$\" {\n Write-Log (Get-Msg \"install.mode.quickstart_selected\")\n $script:AGENTTEAMS_QUICKSTART = $true\n }\n \"^(2|manual)$\" {\n Write-Log (Get-Msg \"install.mode.manual_selected\")\n $script:AGENTTEAMS_QUICKSTART = $false\n }\n default {\n Write-Log (Get-Msg \"install.mode.invalid\")\n $script:AGENTTEAMS_QUICKSTART = $true\n }\n }\n Write-Log \"\"\n}\n\nfunction Step-Existing {\n # This step is skipped when env file doesn't exist\n Write-Log (Get-Msg \"install.existing.detected\" -f $script:AGENTTEAMS_ENV_FILE)\n\n $runningManager = docker ps --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-manager$\"\n $runningWorkers = docker ps --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-worker-\"\n $existingWorkers = docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-worker-\"\n\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n Write-Log (Get-Msg \"install.existing.upgrade_noninteractive\")\n $upgradeChoice = \"1\"\n } else {\n Write-Host \"\"\n Write-Host (Get-Msg \"install.existing.choose\")\n Write-Host (Get-Msg \"install.existing.upgrade\")\n Write-Host (Get-Msg \"install.existing.reinstall\")\n Write-Host (Get-Msg \"install.existing.cancel\")\n Write-Host \"\"\n $upgradeChoice = Read-Host (Get-Msg \"install.existing.prompt\")\n $upgradeChoice = if ($upgradeChoice) { $upgradeChoice } else { \"1\" }\n if ($upgradeChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n }\n\n switch -Regex ($upgradeChoice) {\n \"^(1|upgrade)$\" {\n $script:AGENTTEAMS_UPGRADE = $true\n Write-Log (Get-Msg \"install.existing.upgrading\")\n\n # Show upgrade mode sub-menu (unless already set via env var)\n if ($env:AGENTTEAMS_UPGRADE_KEEP_ALL -ne \"1\") {\n Write-Host \"\"\n Write-Host (Get-Msg \"upgrade.mode.prompt\")\n Write-Host (Get-Msg \"upgrade.mode.keep_all\")\n Write-Host (Get-Msg \"upgrade.mode.confirm_each\")\n Write-Host (Get-Msg \"upgrade.mode.back\")\n Write-Host \"\"\n $upgradeModeChoice = Read-Host (Get-Msg \"install.existing.prompt\")\n $upgradeModeChoice = if ($upgradeModeChoice) { $upgradeModeChoice } else { \"1\" }\n switch -Regex ($upgradeModeChoice) {\n \"^(1|keep)$\" {\n $env:AGENTTEAMS_UPGRADE_KEEP_ALL = \"1\"\n }\n \"^(2|confirm)$\" {\n $env:AGENTTEAMS_UPGRADE_KEEP_ALL = \"0\"\n }\n \"^(3|b)$\" {\n $script:StepResult = \"back\"\n return\n }\n default {\n $env:AGENTTEAMS_UPGRADE_KEEP_ALL = \"0\"\n }\n }\n }\n # Load current parameters for both Keep-All and confirm-each modes\n Load-CurrentParamsFromEnv\n\n if ($runningManager -or $runningWorkers) {\n Write-Host \"\"\n Write-Host \"$($script:ESC)[33m$(Get-Msg 'install.existing.warn_manager_stop')$($script:ESC)[0m\"\n if ($existingWorkers) {\n Write-Host \"$($script:ESC)[33m$(Get-Msg 'install.existing.warn_worker_recreate')$($script:ESC)[0m\"\n }\n if (-not $script:AGENTTEAMS_NON_INTERACTIVE) {\n $confirm = Read-Host (Get-Msg \"install.existing.continue_prompt\")\n if ($confirm -eq \"b\" -or $confirm -eq \"B\") { $script:StepResult = \"back\"; return }\n if ($confirm -ne \"y\" -and $confirm -ne \"Y\") {\n Write-Log (Get-Msg \"install.existing.cancelled\")\n exit 0\n }\n }\n }\n $script:UPGRADE_EXISTING_WORKERS = $existingWorkers\n }\n \"^(2|reinstall)$\" {\n Write-Log (Get-Msg \"install.reinstall.performing\")\n $existingWorkspace = \"$env:USERPROFILE\\agentteams-manager\"\n if (Test-Path $script:AGENTTEAMS_ENV_FILE) {\n $envContent = Get-Content $script:AGENTTEAMS_ENV_FILE\n $wsLine = $envContent | Select-String \"^AGENTTEAMS_WORKSPACE_DIR=\"\n if ($wsLine) {\n $existingWorkspace = $wsLine.Line.Substring(21)\n }\n }\n Write-Host \"\"\n Write-Host \"$($script:ESC)[33m$(Get-Msg 'install.reinstall.warn_stop')$($script:ESC)[0m\"\n if ($runningManager) { Write-Host \"$($script:ESC)[33m - agentteams-manager (manager)$($script:ESC)[0m\" }\n $runningWorkers | ForEach-Object { Write-Host \"$($script:ESC)[33m - $_ (worker)$($script:ESC)[0m\" }\n Write-Host \"\"\n Write-Host \"$($script:ESC)[31m$(Get-Msg 'install.reinstall.warn_delete')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[31m$(Get-Msg 'install.reinstall.warn_volume')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[31m$(Get-Msg 'install.reinstall.warn_env' -f $script:AGENTTEAMS_ENV_FILE)$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[31m$(Get-Msg 'install.reinstall.warn_workspace' -f $existingWorkspace)$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[31m$(Get-Msg 'install.reinstall.warn_workers')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[31m$(Get-Msg 'install.reinstall.warn_proxy')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[31m$(Get-Msg 'install.reinstall.warn_network')$($script:ESC)[0m\"\n Write-Host \"\"\n Write-Host \"$($script:ESC)[31m$(Get-Msg 'install.reinstall.confirm_type')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[31m $existingWorkspace$($script:ESC)[0m\"\n Write-Host \"\"\n $confirmPath = Read-Host (Get-Msg \"install.reinstall.confirm_path\")\n if ($confirmPath -ne $existingWorkspace) {\n Write-Error (Get-Msg \"install.reinstall.path_mismatch\" -f $confirmPath, $existingWorkspace)\n }\n Write-Log (Get-Msg \"install.reinstall.confirmed\")\n docker stop agentteams-manager *>$null\n docker rm agentteams-manager *>$null\n docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-worker-\" | ForEach-Object {\n docker stop $_ *>$null\n docker rm $_ *>$null\n Write-Log (Get-Msg \"install.reinstall.removed_worker\" -f $_)\n }\n $existingController = docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-controller$\"\n if ($existingController) {\n Write-Log (Get-Msg \"install.reinstall.removing_proxy\")\n docker stop agentteams-controller *>$null\n docker rm agentteams-controller *>$null\n }\n if (docker volume ls -q 2>$null | Select-String \"^agentteams-data$\") {\n Write-Log (Get-Msg \"install.reinstall.removing_volume\")\n docker volume rm agentteams-data *>$null\n }\n if (Test-Path $existingWorkspace) {\n Write-Log (Get-Msg \"install.reinstall.removing_workspace\" -f $existingWorkspace)\n Remove-Item -Recurse -Force $existingWorkspace\n }\n if (Test-Path $script:AGENTTEAMS_ENV_FILE) {\n Write-Log (Get-Msg \"install.reinstall.removing_env\" -f $script:AGENTTEAMS_ENV_FILE)\n Remove-Item -Force $script:AGENTTEAMS_ENV_FILE\n }\n $existingNetwork = docker network ls --format \"{{.Name}}\" 2>$null | Select-String \"^agentteams-net$\"\n if ($existingNetwork) {\n Write-Log (Get-Msg \"install.reinstall.removing_network\")\n docker network rm agentteams-net *>$null\n }\n Write-Log (Get-Msg \"install.reinstall.cleanup_done\")\n }\n default {\n Write-Log (Get-Msg \"install.existing.cancelled\")\n exit 0\n }\n }\n\n # Load existing env file (upgrade path)\n if (Test-Path $script:AGENTTEAMS_ENV_FILE) {\n Write-Log (Get-Msg \"install.loading_config\" -f $script:AGENTTEAMS_ENV_FILE)\n Get-Content $script:AGENTTEAMS_ENV_FILE | ForEach-Object {\n if ($_ -match \"^([^#=][^=]*)=(.*)$\") {\n $key = $Matches[1].Trim()\n $value = $Matches[2].Split(\"#\")[0].Trim()\n if (-not [Environment]::GetEnvironmentVariable($key)) {\n [Environment]::SetEnvironmentVariable($key, $value, \"Process\")\n }\n }\n }\n }\n}\n\nfunction Step-Llm {\n Write-Log (Get-Msg \"llm.title\")\n\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n if ($script:AGENTTEAMS_LANGUAGE -eq \"zh\") {\n $script:config.LLM_PROVIDER = if ($env:AGENTTEAMS_LLM_PROVIDER) { $env:AGENTTEAMS_LLM_PROVIDER } else { \"openai-compat\" }\n $script:config.DEFAULT_MODEL = if ($env:AGENTTEAMS_DEFAULT_MODEL) { $env:AGENTTEAMS_DEFAULT_MODEL } else { \"qwen3.6-plus\" }\n $script:config.OPENAI_BASE_URL = if ($env:AGENTTEAMS_OPENAI_BASE_URL) { $env:AGENTTEAMS_OPENAI_BASE_URL } else { \"https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1\" }\n Write-Log (Get-Msg \"llm.provider.label\" -f $script:config.LLM_PROVIDER)\n Write-Log (Get-Msg \"llm.openai.base_url_label\" -f $script:config.OPENAI_BASE_URL)\n } else {\n $script:config.LLM_PROVIDER = if ($env:AGENTTEAMS_LLM_PROVIDER) { $env:AGENTTEAMS_LLM_PROVIDER } else { \"qwen\" }\n $script:config.DEFAULT_MODEL = if ($env:AGENTTEAMS_DEFAULT_MODEL) { $env:AGENTTEAMS_DEFAULT_MODEL } else { \"qwen3.6-plus\" }\n $script:config.OPENAI_BASE_URL = if ($env:AGENTTEAMS_OPENAI_BASE_URL) { $env:AGENTTEAMS_OPENAI_BASE_URL } else { \"\" }\n Write-Log (Get-Msg \"llm.provider.qwen_default\" -f $script:config.LLM_PROVIDER)\n }\n Write-Log (Get-Msg \"llm.model.label\" -f $script:config.DEFAULT_MODEL)\n Write-Log \"\"\n $script:config.LLM_API_KEY = Read-Prompt -VarName \"AGENTTEAMS_LLM_API_KEY\" -PromptText (Get-Msg \"llm.apikey_prompt\") -Secret\n $script:config.EMBEDDING_MODEL = if ($null -ne $env:AGENTTEAMS_EMBEDDING_MODEL) { $env:AGENTTEAMS_EMBEDDING_MODEL } else { \"text-embedding-v4\" }\n return\n }\n\n Write-Host \"\"\n Write-Host (Get-Msg \"llm.providers_title\")\n Write-Host (Get-Msg \"llm.provider.alibaba\")\n Write-Host (Get-Msg \"llm.provider.openai_compat\")\n Write-Host \"\"\n\n # If upgrade mode with loaded provider, show current as default\n if ($script:AGENTTEAMS_UPGRADE -and $script:config.LLM_PROVIDER) {\n $defaultProvider = if ($script:config.LLM_PROVIDER -eq \"openai-compat\") { \"2\" } else { \"1\" }\n $providerChoice = Read-Host \"$(Get-Msg 'llm.provider.select') [${defaultProvider}]\"\n $providerChoice = if ($providerChoice) { $providerChoice } else { $defaultProvider }\n } elseif ($script:AGENTTEAMS_QUICKSTART) {\n $providerChoice = Read-Host \"$(Get-Msg 'llm.provider.select') [1]\"\n } else {\n $providerChoice = Read-Host (Get-Msg \"llm.provider.select\")\n $providerChoice = if ($providerChoice) { $providerChoice } else { \"1\" }\n }\n if ($providerChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n\n switch -Regex ($providerChoice) {\n \"^(1|alibaba-cloud)$\" {\n $alibabaAccess = $null\n if ($script:AGENTTEAMS_LANGUAGE -eq \"en\") {\n $script:config.LLM_PROVIDER = \"openai-compat\"\n $script:config.OPENAI_BASE_URL = \"https://dashscope-intl.aliyuncs.com/compatible-mode/v1\"\n $alibabaAccess = \"tokenplan\"\n Write-Host \"\"\n Write-Host (Get-Msg \"llm.codingplan.models_title\")\n Write-Host (Get-Msg \"llm.codingplan.model.qwen36plus\")\n Write-Host (Get-Msg \"llm.codingplan.model.glm5\")\n Write-Host (Get-Msg \"llm.codingplan.model.kimi\")\n Write-Host (Get-Msg \"llm.codingplan.model.minimax\")\n Write-Host \"\"\n # If upgrade with loaded model, show current as default\n if ($script:AGENTTEAMS_UPGRADE -and $script:config.DEFAULT_MODEL) {\n $defaultModel = switch ($script:config.DEFAULT_MODEL) {\n \"qwen3.6-plus\" { \"1\" }\n \"glm-5\" { \"2\" }\n \"kimi-k2.5\" { \"3\" }\n \"MiniMax-M2.5\" { \"4\" }\n default { \"1\" }\n }\n $codingPlanModelChoice = Read-Host \"$(Get-Msg 'llm.codingplan.model.select') [${defaultModel}]\"\n $codingPlanModelChoice = if ($codingPlanModelChoice) { $codingPlanModelChoice } else { $defaultModel }\n } elseif ($script:AGENTTEAMS_QUICKSTART) {\n $codingPlanModelChoice = Read-Host \"$(Get-Msg 'llm.codingplan.model.select') [1]\"\n } else {\n $codingPlanModelChoice = Read-Host (Get-Msg \"llm.codingplan.model.select\")\n }\n $codingPlanModelChoice = if ($codingPlanModelChoice) { $codingPlanModelChoice } else { \"1\" }\n if ($codingPlanModelChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n switch -Regex ($codingPlanModelChoice) {\n \"^(1|qwen3\\.6-plus)$\" { $script:config.DEFAULT_MODEL = \"qwen3.6-plus\" }\n \"^(2|glm-5)$\" { $script:config.DEFAULT_MODEL = \"glm-5\" }\n \"^(3|kimi-k2\\.5)$\" { $script:config.DEFAULT_MODEL = \"kimi-k2.5\" }\n \"^(4|MiniMax-M2\\.5)$\" { $script:config.DEFAULT_MODEL = \"MiniMax-M2.5\" }\n default { $script:config.DEFAULT_MODEL = \"qwen3.6-plus\" }\n }\n Write-Log (Get-Msg \"llm.provider.selected_codingplan\")\n } else {\n Write-Host \"\"\n Write-Host (Get-Msg \"llm.alibaba.models_title\")\n Write-Host (Get-Msg \"llm.alibaba.model.tokenplan\")\n Write-Host (Get-Msg \"llm.alibaba.model.bailian\")\n Write-Host (Get-Msg \"llm.alibaba.model.codingplan_legacy\")\n Write-Host \"\"\n $modelChoice = if ($script:AGENTTEAMS_QUICKSTART) {\n Read-Host \"$(Get-Msg 'llm.alibaba.model.select') [1]\"\n } else {\n Read-Host (Get-Msg \"llm.alibaba.model.select\")\n }\n $modelChoice = if ($modelChoice) { $modelChoice } else { \"1\" }\n if ($modelChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n\n if ($modelChoice -match \"^(1|token-plan|tokenplan)$\") {\n $alibabaAccess = \"tokenplan\"\n $script:config.LLM_PROVIDER = \"openai-compat\"\n $script:config.OPENAI_BASE_URL = \"https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1\"\n Write-Host \"\"\n Write-Host (Get-Msg \"llm.codingplan.models_title\")\n Write-Host (Get-Msg \"llm.codingplan.model.qwen36plus\")\n Write-Host (Get-Msg \"llm.codingplan.model.glm5\")\n Write-Host (Get-Msg \"llm.codingplan.model.kimi\")\n Write-Host (Get-Msg \"llm.codingplan.model.minimax\")\n Write-Host \"\"\n # If upgrade with loaded model, show current as default\n if ($script:AGENTTEAMS_UPGRADE -and $script:config.DEFAULT_MODEL) {\n $defaultModel = switch ($script:config.DEFAULT_MODEL) {\n \"qwen3.6-plus\" { \"1\" }\n \"glm-5\" { \"2\" }\n \"kimi-k2.5\" { \"3\" }\n \"MiniMax-M2.5\" { \"4\" }\n default { \"1\" }\n }\n $codingPlanModelChoice = Read-Host \"$(Get-Msg 'llm.codingplan.model.select') [${defaultModel}]\"\n $codingPlanModelChoice = if ($codingPlanModelChoice) { $codingPlanModelChoice } else { $defaultModel }\n } elseif ($script:AGENTTEAMS_QUICKSTART) {\n $codingPlanModelChoice = Read-Host \"$(Get-Msg 'llm.codingplan.model.select') [1]\"\n } else {\n $codingPlanModelChoice = Read-Host (Get-Msg \"llm.codingplan.model.select\")\n }\n $codingPlanModelChoice = if ($codingPlanModelChoice) { $codingPlanModelChoice } else { \"1\" }\n if ($codingPlanModelChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n switch -Regex ($codingPlanModelChoice) {\n \"^(1|qwen3\\.6-plus)$\" { $script:config.DEFAULT_MODEL = \"qwen3.6-plus\" }\n \"^(2|glm-5)$\" { $script:config.DEFAULT_MODEL = \"glm-5\" }\n \"^(3|kimi-k2\\.5)$\" { $script:config.DEFAULT_MODEL = \"kimi-k2.5\" }\n \"^(4|MiniMax-M2\\.5)$\" { $script:config.DEFAULT_MODEL = \"MiniMax-M2.5\" }\n default { $script:config.DEFAULT_MODEL = \"qwen3.6-plus\" }\n }\n Write-Log (Get-Msg \"llm.provider.selected_tokenplan\")\n } elseif ($modelChoice -match \"^(2|qwen|bailian)$\") {\n $alibabaAccess = \"bailian\"\n $script:config.LLM_PROVIDER = \"qwen\"\n $script:config.OPENAI_BASE_URL = \"\"\n Write-Host \"\"\n if ($script:AGENTTEAMS_UPGRADE -and $script:config.DEFAULT_MODEL) {\n $qwenModelInput = Read-Host \"$(Get-Msg 'llm.qwen.model_prompt') [${script:config.DEFAULT_MODEL}]\"\n } else {\n $qwenModelInput = Read-Host (Get-Msg \"llm.qwen.model_prompt\")\n }\n if ($qwenModelInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $script:config.DEFAULT_MODEL = if ($qwenModelInput) { $qwenModelInput } elseif ($script:config.DEFAULT_MODEL) { $script:config.DEFAULT_MODEL } else { \"qwen3.6-plus\" }\n Write-Log (Get-Msg \"llm.provider.selected_qwen\")\n Request-CustomModelParams $script:config.DEFAULT_MODEL\n if ($script:StepResult -eq \"back\") { return }\n } elseif ($modelChoice -match \"^(3|coding-plan|codingplan)$\") {\n $alibabaAccess = \"codingplan_legacy\"\n $script:config.LLM_PROVIDER = \"openai-compat\"\n $script:config.OPENAI_BASE_URL = \"https://coding.dashscope.aliyuncs.com/v1\"\n Write-Host \"\"\n if ($script:AGENTTEAMS_UPGRADE -and $script:config.DEFAULT_MODEL) {\n $codingModelInput = Read-Host \"$(Get-Msg 'llm.qwen.model_prompt') [${script:config.DEFAULT_MODEL}]\"\n } else {\n $codingModelInput = Read-Host (Get-Msg \"llm.qwen.model_prompt\")\n }\n if ($codingModelInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $script:config.DEFAULT_MODEL = if ($codingModelInput) { $codingModelInput } elseif ($script:config.DEFAULT_MODEL) { $script:config.DEFAULT_MODEL } else { \"qwen3.6-plus\" }\n Write-Log (Get-Msg \"llm.provider.selected_codingplan_legacy\")\n Request-CustomModelParams $script:config.DEFAULT_MODEL\n if ($script:StepResult -eq \"back\") { return }\n } else {\n Write-Error (Get-Msg \"llm.alibaba.model.invalid\" -f $modelChoice)\n }\n }\n\n Write-Log (Get-Msg \"llm.model.label\" -f $script:config.DEFAULT_MODEL)\n Write-Log \"\"\n if ($alibabaAccess -eq \"bailian\") {\n Write-Log (Get-Msg \"llm.apikey_hint_bailian\")\n Write-Log (Get-Msg \"llm.apikey_url_bailian\")\n } elseif ($script:AGENTTEAMS_LANGUAGE -eq \"en\") {\n Write-Log (Get-Msg \"llm.apikey_hint_qwencloud\")\n Write-Log (Get-Msg \"llm.apikey_url_qwencloud\")\n } elseif ($alibabaAccess -eq \"codingplan_legacy\") {\n Write-Log (Get-Msg \"llm.apikey_hint_codingplan\")\n Write-Log (Get-Msg \"llm.apikey_url_codingplan\")\n } else {\n Write-Log (Get-Msg \"llm.apikey_hint_tokenplan\")\n Write-Log (Get-Msg \"llm.apikey_url_tokenplan\")\n }\n Write-Log \"\"\n $script:config.LLM_API_KEY = Read-Prompt -VarName \"AGENTTEAMS_LLM_API_KEY\" -PromptText (Get-Msg \"llm.apikey_prompt\") -Secret\n if ($script:StepResult -eq \"back\") { return }\n if ($alibabaAccess -eq \"bailian\") {\n Test-LlmConnectivity -BaseUrl \"https://dashscope.aliyuncs.com/compatible-mode/v1\" -ApiKey $script:config.LLM_API_KEY -Model $script:config.DEFAULT_MODEL\n } elseif ($alibabaAccess -eq \"codingplan_legacy\") {\n Test-LlmConnectivity -BaseUrl \"https://coding.dashscope.aliyuncs.com/v1\" -ApiKey $script:config.LLM_API_KEY -Model $script:config.DEFAULT_MODEL -Hint (Get-Msg \"llm.openai.test.fail.codingplan_legacy\")\n } elseif ($script:AGENTTEAMS_LANGUAGE -eq \"en\") {\n Test-LlmConnectivity -BaseUrl $script:config.OPENAI_BASE_URL -ApiKey $script:config.LLM_API_KEY -Model $script:config.DEFAULT_MODEL -Hint (Get-Msg \"llm.openai.test.fail.codingplan\")\n } else {\n Test-LlmConnectivity -BaseUrl $script:config.OPENAI_BASE_URL -ApiKey $script:config.LLM_API_KEY -Model $script:config.DEFAULT_MODEL -Hint (Get-Msg \"llm.openai.test.fail.tokenplan\")\n }\n if ($script:StepResult -eq \"back\") { return }\n }\n \"^(2|openai-compat)$\" {\n $script:config.LLM_PROVIDER = \"openai-compat\"\n Write-Log (Get-Msg \"llm.provider.selected_openai\" -f $script:config.LLM_PROVIDER)\n Write-Host \"\"\n if ($script:AGENTTEAMS_UPGRADE -and $env:AGENTTEAMS_UPGRADE_KEEP_ALL -eq \"1\") {\n Write-Log (Get-Msg \"prompt.upgrade_keep\" -f (Get-Msg \"llm.openai.base_url_prompt\"), $script:config.OPENAI_BASE_URL)\n } else {\n $currentUrl = $script:config.OPENAI_BASE_URL\n # Use base prompt without embedded default when showing current value\n if ($currentUrl) {\n $urlInput = Read-Host \"$(Get-Msg 'llm.openai.base_url_prompt') [$currentUrl]\"\n } else {\n $urlInput = Read-Host \"$(Get-Msg 'llm.openai.base_url_prompt') [https://api.openai.com/v1]\"\n }\n if ($urlInput -eq \"b\") { $script:StepResult = \"back\"; return }\n if ($urlInput) {\n $script:config.OPENAI_BASE_URL = $urlInput\n } elseif ($currentUrl) {\n $script:config.OPENAI_BASE_URL = $currentUrl\n } else {\n $script:config.OPENAI_BASE_URL = \"https://api.openai.com/v1\"\n }\n }\n if ($script:AGENTTEAMS_UPGRADE -and $env:AGENTTEAMS_UPGRADE_KEEP_ALL -eq \"1\") {\n Write-Log (Get-Msg \"prompt.upgrade_keep\" -f (Get-Msg \"llm.openai.model_prompt\"), $script:config.DEFAULT_MODEL)\n } else {\n $currentModel = $script:config.DEFAULT_MODEL\n if ($currentModel) {\n $modelInput = Read-Host \"$(Get-Msg 'llm.openai.model_prompt') [${currentModel}]\"\n } else {\n $modelInput = Read-Host (Get-Msg 'llm.openai.model_prompt')\n }\n if ($modelInput -eq \"b\") { $script:StepResult = \"back\"; return }\n if ($modelInput) {\n $script:config.DEFAULT_MODEL = $modelInput\n } elseif ($currentModel) {\n $script:config.DEFAULT_MODEL = $currentModel\n } else {\n $script:config.DEFAULT_MODEL = \"gpt-5.4\"\n }\n }\n Write-Log (Get-Msg \"llm.openai.base_url_label\" -f $script:config.OPENAI_BASE_URL)\n Write-Log (Get-Msg \"llm.model.label\" -f $script:config.DEFAULT_MODEL)\n Request-CustomModelParams $script:config.DEFAULT_MODEL\n if ($script:StepResult -eq \"back\") { return }\n Write-Log \"\"\n $script:config.LLM_API_KEY = Read-Prompt -VarName \"AGENTTEAMS_LLM_API_KEY\" -PromptText (Get-Msg \"llm.apikey_prompt\") -Secret\n if ($script:StepResult -eq \"back\") { return }\n Test-LlmConnectivity -BaseUrl $script:config.OPENAI_BASE_URL -ApiKey $script:config.LLM_API_KEY -Model $script:config.DEFAULT_MODEL\n if ($script:StepResult -eq \"back\") { return }\n }\n default {\n Write-Error (Get-Msg \"llm.provider.invalid\" -f $providerChoice)\n }\n }\n\n # Skip to embedding if Keep-All mode handled LLM params\n if ($skipToEmbedding) {\n # Skip embedding selection, use loaded value (already in $script:config.EMBEDDING_MODEL)\n if ($script:config.EMBEDDING_MODEL) {\n Write-Log (Get-Msg \"llm.embedding.title\")\n Write-Log (Get-Msg \"llm.model.label\" -f $script:config.EMBEDDING_MODEL)\n Write-Log \"\"\n }\n return\n }\n\n # --- Embedding model (optional, auto-tested) ---\n Write-Host \"\"\n Write-Log (Get-Msg \"llm.embedding.title\")\n Write-Log (Get-Msg \"llm.embedding.hint\")\n Write-Host \"\"\n Write-Host (Get-Msg \"llm.embedding.option.default\")\n Write-Host (Get-Msg \"llm.embedding.option.custom\")\n Write-Host (Get-Msg \"llm.embedding.option.disable\")\n Write-Host \"\"\n $embChoice = Read-Host \"$(Get-Msg 'llm.embedding.select') [1]\"\n $embChoice = if ($embChoice) { $embChoice } else { \"1\" }\n if ($embChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n\n switch ($embChoice) {\n \"1\" {\n $script:config.EMBEDDING_MODEL = \"text-embedding-v4\"\n }\n \"2\" {\n if ($script:AGENTTEAMS_UPGRADE -and $env:AGENTTEAMS_UPGRADE_KEEP_ALL -eq \"1\") {\n Write-Log (Get-Msg \"prompt.upgrade_keep\" -f (Get-Msg \"llm.embedding.custom_prompt\"), $script:config.EMBEDDING_MODEL)\n } else {\n $currentEmb = $script:config.EMBEDDING_MODEL\n $embCustom = Read-Host (Get-Msg \"llm.embedding.custom_prompt\")\n if ($embCustom -eq \"b\") { $script:StepResult = \"back\"; return }\n if ($embCustom) {\n $script:config.EMBEDDING_MODEL = $embCustom\n } else {\n $script:config.EMBEDDING_MODEL = $currentEmb\n if (-not $script:config.EMBEDDING_MODEL) {\n Write-Log (Get-Msg \"llm.embedding.disabled\")\n }\n }\n }\n }\n \"3\" {\n $script:config.EMBEDDING_MODEL = \"\"\n Write-Log (Get-Msg \"llm.embedding.disabled\")\n }\n default {\n $script:config.EMBEDDING_MODEL = \"text-embedding-v4\"\n }\n }\n\n if ($script:config.EMBEDDING_MODEL) {\n # Qwen provider uses dashscope directly; others use OPENAI_BASE_URL\n $embBaseUrl = $script:config.OPENAI_BASE_URL\n if ($script:config.LLM_PROVIDER -eq \"qwen\") {\n $embBaseUrl = \"https://dashscope.aliyuncs.com/compatible-mode/v1\"\n }\n $embResult = Test-EmbeddingConnectivity -BaseUrl $embBaseUrl -ApiKey $script:config.LLM_API_KEY -Model $script:config.EMBEDDING_MODEL\n if (-not $embResult) {\n $script:config.EMBEDDING_MODEL = \"\"\n Write-Log (Get-Msg \"llm.embedding.auto_disabled\")\n }\n }\n\n Write-Log \"\"\n}\n\nfunction Step-Admin {\n Write-Log (Get-Msg \"admin.title\")\n $script:config.ADMIN_USER = Read-Prompt -VarName \"AGENTTEAMS_ADMIN_USER\" -PromptText (Get-Msg \"admin.username_prompt\") -Default \"admin\"\n if ($script:StepResult -eq \"back\") { return }\n $script:config.ADMIN_USER = $script:config.ADMIN_USER.ToLowerInvariant()\n\n # Pre-set via env var: validate; non-interactive fails fast,\n # interactive warns and falls through to the retry prompt.\n if ($env:AGENTTEAMS_ADMIN_PASSWORD) {\n $script:config.ADMIN_PASSWORD = $env:AGENTTEAMS_ADMIN_PASSWORD\n Write-Log (Get-Msg \"prompt.preset\" -f (Get-Msg \"admin.password_prompt\"))\n if ($script:config.ADMIN_PASSWORD.Length -ge 8) {\n Write-Log \"\"\n return\n }\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n Write-Error (Get-Msg \"admin.password_too_short\" -f $script:config.ADMIN_PASSWORD.Length)\n }\n Write-Host \"$($script:ESC)[31m[AgentTeams ERROR]$($script:ESC)[0m $(Get-Msg \"admin.password_too_short\" -f $script:config.ADMIN_PASSWORD.Length)\"\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_ADMIN_PASSWORD\", $null, \"Process\")\n }\n\n while ($true) {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_ADMIN_PASSWORD\", $null, \"Process\")\n $script:config.ADMIN_PASSWORD = Read-Prompt -VarName \"AGENTTEAMS_ADMIN_PASSWORD\" -PromptText (Get-Msg \"admin.password_prompt\") -Secret -Optional\n if ($script:StepResult -eq \"back\") { return }\n if (-not $script:config.ADMIN_PASSWORD) {\n $randomSuffix = (New-RandomKey).Substring(0, 12)\n $script:config.ADMIN_PASSWORD = \"admin$randomSuffix\"\n Write-Log (Get-Msg \"admin.password_generated\")\n break\n }\n if ($script:config.ADMIN_PASSWORD.Length -ge 8) {\n break\n }\n Write-Host \"$($script:ESC)[31m[AgentTeams ERROR]$($script:ESC)[0m $(Get-Msg \"admin.password_too_short\" -f $script:config.ADMIN_PASSWORD.Length)\"\n }\n\n Write-Log \"\"\n}\n\nfunction Step-Network {\n Write-Log (Get-Msg \"port.local_only.title\")\n Write-Host \"\"\n Write-Host \" 1) $(Get-Msg 'port.local_only.hint_yes')\"\n Write-Host \" 2) $(Get-Msg 'port.local_only.hint_no')\"\n Write-Host \"\"\n\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n $localOnly = if ($env:AGENTTEAMS_LOCAL_ONLY) { $env:AGENTTEAMS_LOCAL_ONLY } else { \"1\" }\n } elseif ($null -ne $env:AGENTTEAMS_LOCAL_ONLY) {\n $localOnly = $env:AGENTTEAMS_LOCAL_ONLY\n } else {\n $localChoice = Read-Host \"$(Get-Msg 'port.local_only.choice')\"\n if ($localChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n if (-not $localChoice) { $localChoice = \"1\" }\n $localOnly = if ($localChoice -match '^(2|n|N|no|NO)$') { \"0\" } else { \"1\" }\n }\n $script:config.LOCAL_ONLY = $localOnly\n\n if ($localOnly -eq \"1\") {\n Write-Log (Get-Msg \"port.local_only.selected_local\")\n } else {\n Write-Log (Get-Msg \"port.local_only.selected_external\")\n Write-Host \"\"\n Write-Host (Get-Msg \"port.local_only.https_hint\") -ForegroundColor Yellow\n }\n Write-Log \"\"\n}\n\nfunction Step-Ports {\n Write-Log (Get-Msg \"port.title\")\n $script:config.PORT_GATEWAY = Read-Prompt -VarName \"AGENTTEAMS_PORT_GATEWAY\" -PromptText (Get-Msg \"port.gateway_prompt\") -Default \"18080\"\n if ($script:StepResult -eq \"back\") { return }\n $script:config.PORT_CONSOLE = Read-Prompt -VarName \"AGENTTEAMS_PORT_CONSOLE\" -PromptText (Get-Msg \"port.console_prompt\") -Default \"18001\"\n if ($script:StepResult -eq \"back\") { return }\n $script:config.PORT_ELEMENT_WEB = Read-Prompt -VarName \"AGENTTEAMS_PORT_ELEMENT_WEB\" -PromptText (Get-Msg \"port.element_prompt\") -Default \"18088\"\n if ($script:StepResult -eq \"back\") { return }\n $script:config.PORT_MANAGER_CONSOLE = Read-Prompt -VarName \"AGENTTEAMS_PORT_MANAGER_CONSOLE\" -PromptText (Get-Msg \"port.manager_console_prompt\") -Default \"18888\"\n if ($script:StepResult -eq \"back\") { return }\n Write-Log \"\"\n}\n\nfunction Step-Domains {\n Write-Log (Get-Msg \"domain.title\")\n Write-Log (Get-Msg \"domain.hint\")\n $script:config.MATRIX_DOMAIN = Read-Prompt -VarName \"AGENTTEAMS_MATRIX_DOMAIN\" -PromptText (Get-Msg \"domain.matrix_prompt\") -Default \"matrix-local.agentteams.io:$($script:config.PORT_GATEWAY)\"\n if ($script:StepResult -eq \"back\") { return }\n $script:config.MATRIX_CLIENT_DOMAIN = Read-Prompt -VarName \"AGENTTEAMS_MATRIX_CLIENT_DOMAIN\" -PromptText (Get-Msg \"domain.element_prompt\") -Default \"matrix-client-local.agentteams.io\"\n if ($script:StepResult -eq \"back\") { return }\n $script:config.AI_GATEWAY_DOMAIN = Read-Prompt -VarName \"AGENTTEAMS_AI_GATEWAY_DOMAIN\" -PromptText (Get-Msg \"domain.gateway_prompt\") -Default \"aigw-local.agentteams.io\"\n if ($script:StepResult -eq \"back\") { return }\n $script:config.FS_DOMAIN = Read-Prompt -VarName \"AGENTTEAMS_FS_DOMAIN\" -PromptText (Get-Msg \"domain.fs_prompt\") -Default \"fs-local.agentteams.io\"\n if ($script:StepResult -eq \"back\") { return }\n $script:config.CONSOLE_DOMAIN = Read-Prompt -VarName \"AGENTTEAMS_CONSOLE_DOMAIN\" -PromptText (Get-Msg \"domain.console_prompt\") -Default \"console-local.agentteams.io\"\n if ($script:StepResult -eq \"back\") { return }\n Write-Log \"\"\n}\n\nfunction Step-Github {\n Write-Log (Get-Msg \"github.title\")\n $script:config.GITHUB_TOKEN = Read-Prompt -VarName \"AGENTTEAMS_GITHUB_TOKEN\" -PromptText (Get-Msg \"github.token_prompt\") -Secret -Optional\n}\n\nfunction Step-Skills {\n Write-Log \"\"\n Write-Log (Get-Msg \"skills.title\")\n $script:config.SKILLS_API_URL = Read-Prompt -VarName \"AGENTTEAMS_SKILLS_API_URL\" -PromptText (Get-Msg \"skills.url_prompt\") -Optional\n if ($script:StepResult -eq \"back\") { return }\n Write-Log \"\"\n}\n\nfunction Step-Volume {\n Write-Log (Get-Msg \"data.title\")\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n $script:config.DATA_DIR = if ($env:AGENTTEAMS_DATA_DIR) { $env:AGENTTEAMS_DATA_DIR } else { \"agentteams-data\" }\n Write-Log \" $(Get-Msg 'data.volume_using' -f $script:config.DATA_DIR) (non-interactive, skipped)\"\n return\n }\n # ─────────────────────────────────────────────────────────────────\n $dataDirInput = Read-Host (Get-Msg \"data.volume_prompt\")\n if ($dataDirInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $script:config.DATA_DIR = if ($dataDirInput) { $dataDirInput } else { \"agentteams-data\" }\n Write-Log (Get-Msg \"data.volume_using\" -f $script:config.DATA_DIR)\n}\n\nfunction Step-Workspace {\n Write-Log (Get-Msg \"workspace.title\")\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n $script:config.WORKSPACE_DIR = if ($env:AGENTTEAMS_WORKSPACE_DIR) { $env:AGENTTEAMS_WORKSPACE_DIR } else { \"$env:USERPROFILE\\agentteams-manager\" }\n if (-not (Test-Path $script:config.WORKSPACE_DIR)) {\n New-Item -ItemType Directory -Path $script:config.WORKSPACE_DIR -Force | Out-Null\n }\n Write-Log \" $(Get-Msg 'workspace.dir_label' -f $script:config.WORKSPACE_DIR) (non-interactive, skipped)\"\n return\n }\n # ─────────────────────────────────────────────────────────────────\n $defaultWorkspace = \"$env:USERPROFILE\\agentteams-manager\"\n if ($script:AGENTTEAMS_UPGRADE -and $env:AGENTTEAMS_UPGRADE_KEEP_ALL -eq \"1\") {\n Write-Log (Get-Msg \"prompt.upgrade_keep\" -f (Get-Msg \"workspace.dir_prompt\" -f $defaultWorkspace), $script:config.WORKSPACE_DIR)\n $wsInput = if ($script:config.WORKSPACE_DIR) { $script:config.WORKSPACE_DIR } else { $defaultWorkspace }\n } else {\n $currentWs = $script:config.WORKSPACE_DIR\n $wsInput = Read-Host (Get-Msg \"workspace.dir_prompt\" -f $defaultWorkspace)\n if ($wsInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $wsInput = if ($wsInput) { $wsInput } else { if ($currentWs) { $currentWs } else { $defaultWorkspace } }\n }\n $script:config.WORKSPACE_DIR = $wsInput\n if (-not (Test-Path $script:config.WORKSPACE_DIR)) {\n New-Item -ItemType Directory -Path $script:config.WORKSPACE_DIR -Force | Out-Null\n }\n Write-Log (Get-Msg \"workspace.dir_label\" -f $script:config.WORKSPACE_DIR)\n}\n\nfunction Step-Runtime {\n Write-Log (Get-Msg \"worker_runtime.title\")\n Write-Host \"\"\n Write-Host \" 1) $(Get-Msg 'worker_runtime.copaw')\"\n Write-Host \" 2) $(Get-Msg 'worker_runtime.openclaw')\"\n Write-Host \" 3) $(Get-Msg 'worker_runtime.hermes')\"\n Write-Host \"\"\n\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n $script:config.DEFAULT_WORKER_RUNTIME = if ($env:AGENTTEAMS_DEFAULT_WORKER_RUNTIME) { $env:AGENTTEAMS_DEFAULT_WORKER_RUNTIME } else { \"copaw\" }\n } elseif ($script:AGENTTEAMS_UPGRADE -and $env:AGENTTEAMS_DEFAULT_WORKER_RUNTIME) {\n Write-Log (Get-Msg \"prompt.upgrade_keep\" -f (Get-Msg \"worker_runtime.title_short\"), $env:AGENTTEAMS_DEFAULT_WORKER_RUNTIME)\n $rtChoice = Read-Host (Get-Msg \"worker_runtime.choice\")\n if ($rtChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n if ($rtChoice) {\n $script:config.DEFAULT_WORKER_RUNTIME = switch ($rtChoice) {\n \"2\" { \"openclaw\" }\n \"3\" { \"hermes\" }\n default { \"copaw\" }\n }\n } else {\n $script:config.DEFAULT_WORKER_RUNTIME = $env:AGENTTEAMS_DEFAULT_WORKER_RUNTIME\n }\n } elseif ($env:AGENTTEAMS_DEFAULT_WORKER_RUNTIME) {\n $script:config.DEFAULT_WORKER_RUNTIME = $env:AGENTTEAMS_DEFAULT_WORKER_RUNTIME\n } else {\n $rtChoice = Read-Host (Get-Msg \"worker_runtime.choice\")\n if ($rtChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n $rtChoice = if ($rtChoice) { $rtChoice } else { \"1\" }\n $script:config.DEFAULT_WORKER_RUNTIME = switch ($rtChoice) {\n \"2\" { \"openclaw\" }\n \"3\" { \"hermes\" }\n default { \"copaw\" }\n }\n }\n Write-Log (Get-Msg \"worker_runtime.selected\" -f $script:config.DEFAULT_WORKER_RUNTIME)\n}\n\nfunction Step-ManagerRuntime {\n Write-Log (Get-Msg \"manager_runtime.title\")\n Write-Host \"\"\n Write-Host \" 1) $(Get-Msg 'manager_runtime.copaw')\"\n Write-Host \" 2) $(Get-Msg 'manager_runtime.openclaw')\"\n Write-Host \"\"\n\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n $script:config.MANAGER_RUNTIME = if ($env:AGENTTEAMS_MANAGER_RUNTIME) { $env:AGENTTEAMS_MANAGER_RUNTIME } else { \"copaw\" }\n } elseif ($script:AGENTTEAMS_UPGRADE -and $env:AGENTTEAMS_MANAGER_RUNTIME) {\n Write-Log (Get-Msg \"prompt.upgrade_keep\" -f (Get-Msg \"manager_runtime.title_short\"), $env:AGENTTEAMS_MANAGER_RUNTIME)\n $mrChoice = Read-Host (Get-Msg \"manager_runtime.choice\")\n if ($mrChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n if ($mrChoice) {\n $script:config.MANAGER_RUNTIME = if ($mrChoice -eq \"2\") { \"openclaw\" } else { \"copaw\" }\n } else {\n $script:config.MANAGER_RUNTIME = $env:AGENTTEAMS_MANAGER_RUNTIME\n }\n } elseif ($env:AGENTTEAMS_MANAGER_RUNTIME) {\n $script:config.MANAGER_RUNTIME = $env:AGENTTEAMS_MANAGER_RUNTIME\n } else {\n $mrChoice = Read-Host (Get-Msg \"manager_runtime.choice\")\n if ($mrChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n $mrChoice = if ($mrChoice) { $mrChoice } else { \"1\" }\n $script:config.MANAGER_RUNTIME = if ($mrChoice -eq \"2\") { \"openclaw\" } else { \"copaw\" }\n }\n Write-Log (Get-Msg \"manager_runtime.selected\" -f $script:config.MANAGER_RUNTIME)\n}\n\nfunction Step-E2ee {\n Write-Host \"\"\n Write-Log (Get-Msg \"matrix_e2ee.title\")\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n $script:config.MATRIX_E2EE = if ($env:AGENTTEAMS_MATRIX_E2EE) { $env:AGENTTEAMS_MATRIX_E2EE } else { \"0\" }\n Write-Log \" $(Get-Msg 'matrix_e2ee.title_short') = $($script:config.MATRIX_E2EE) (non-interactive, skipped)\"\n return\n }\n # ─────────────────────────────────────────────────────────────────\n Write-Host \"\"\n Write-Host \" $(Get-Msg 'matrix_e2ee.desc')\"\n Write-Host \"\"\n Write-Host \" 1) $(Get-Msg 'matrix_e2ee.disable')\"\n Write-Host \" 2) $(Get-Msg 'matrix_e2ee.enable')\"\n Write-Host \"\"\n\n if ($script:AGENTTEAMS_UPGRADE -and $env:AGENTTEAMS_MATRIX_E2EE) {\n $e2eeDisplay = if ($env:AGENTTEAMS_MATRIX_E2EE -eq \"1\") { Get-Msg \"matrix_e2ee.val_enabled\" } else { Get-Msg \"matrix_e2ee.val_disabled\" }\n Write-Log (Get-Msg \"prompt.upgrade_keep\" -f (Get-Msg \"matrix_e2ee.title_short\"), $e2eeDisplay)\n $e2eeChoice = Read-Host (Get-Msg \"matrix_e2ee.choice\")\n if ($e2eeChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n if ($e2eeChoice) {\n $script:config.MATRIX_E2EE = if ($e2eeChoice -eq \"2\") { \"1\" } else { \"0\" }\n } else {\n $script:config.MATRIX_E2EE = $env:AGENTTEAMS_MATRIX_E2EE\n }\n } elseif (-not $env:AGENTTEAMS_MATRIX_E2EE) {\n $e2eeChoice = Read-Host (Get-Msg \"matrix_e2ee.choice\")\n if ($e2eeChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n $e2eeChoice = if ($e2eeChoice) { $e2eeChoice } else { \"1\" }\n $script:config.MATRIX_E2EE = if ($e2eeChoice -eq \"2\") { \"1\" } else { \"0\" }\n } else {\n $script:config.MATRIX_E2EE = $env:AGENTTEAMS_MATRIX_E2EE\n }\n\n if ($script:config.MATRIX_E2EE -eq \"1\") {\n Write-Log (Get-Msg \"matrix_e2ee.selected_enabled\")\n } else {\n Write-Log (Get-Msg \"matrix_e2ee.selected_disabled\")\n }\n}\n\nfunction Step-DockerProxy {\n if (-not $script:AGENTTEAMS_MOUNT_SOCKET) {\n $script:config.DOCKER_PROXY = \"0\"\n return\n }\n\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n $script:config.DOCKER_PROXY = if ($env:AGENTTEAMS_DOCKER_PROXY) { $env:AGENTTEAMS_DOCKER_PROXY } else { \"0\" }\n Write-Log \" $(Get-Msg 'docker_proxy.title_short') = $($script:config.DOCKER_PROXY) (non-interactive, skipped)\"\n return\n }\n # ─────────────────────────────────────────────────────────────────\n\n Write-Host \"\"\n Write-Log (Get-Msg \"docker_proxy.title\")\n Write-Host \"\"\n Write-Host \" $(Get-Msg 'docker_proxy.desc')\"\n Write-Host \"\"\n Write-Host \" 1) $(Get-Msg 'docker_proxy.enable')\"\n Write-Host \" 2) $(Get-Msg 'docker_proxy.disable')\"\n Write-Host \"\"\n\n if ($script:AGENTTEAMS_UPGRADE -and $env:AGENTTEAMS_DOCKER_PROXY) {\n $proxyDisplay = if ($env:AGENTTEAMS_DOCKER_PROXY -eq \"1\") { Get-Msg \"docker_proxy.val_enabled\" } else { Get-Msg \"docker_proxy.val_disabled\" }\n Write-Log (Get-Msg \"prompt.upgrade_keep\" -f (Get-Msg \"docker_proxy.title_short\"), $proxyDisplay)\n $proxyChoice = Read-Host (Get-Msg \"docker_proxy.choice\")\n if ($proxyChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n if ($proxyChoice) {\n $script:config.DOCKER_PROXY = if ($proxyChoice -eq \"2\") { \"0\" } else { \"1\" }\n } else {\n $script:config.DOCKER_PROXY = $env:AGENTTEAMS_DOCKER_PROXY\n }\n } elseif (-not $env:AGENTTEAMS_DOCKER_PROXY) {\n $proxyChoice = Read-Host (Get-Msg \"docker_proxy.choice\")\n if ($proxyChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n $proxyChoice = if ($proxyChoice) { $proxyChoice } else { \"1\" }\n $script:config.DOCKER_PROXY = if ($proxyChoice -eq \"2\") { \"0\" } else { \"1\" }\n } else {\n $script:config.DOCKER_PROXY = $env:AGENTTEAMS_DOCKER_PROXY\n }\n\n if ($script:config.DOCKER_PROXY -eq \"1\") {\n Write-Log (Get-Msg \"docker_proxy.selected_enabled\")\n\n # Prompt for additional allowed image sources\n Write-Host \"\"\n Write-Host \" $(Get-Msg 'docker_proxy.registries_desc')\"\n Write-Host \"\"\n if ($script:AGENTTEAMS_UPGRADE -and $env:AGENTTEAMS_PROXY_ALLOWED_REGISTRIES) {\n Write-Log (Get-Msg \"prompt.upgrade_keep\" -f (Get-Msg \"docker_proxy.registries_label\"), $env:AGENTTEAMS_PROXY_ALLOWED_REGISTRIES)\n $regInput = Read-Host (Get-Msg \"docker_proxy.registries_prompt\")\n if ($regInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $script:config.PROXY_ALLOWED_REGISTRIES = if ($regInput) { $regInput } else { $env:AGENTTEAMS_PROXY_ALLOWED_REGISTRIES }\n } elseif (-not $env:AGENTTEAMS_PROXY_ALLOWED_REGISTRIES) {\n $regInput = Read-Host (Get-Msg \"docker_proxy.registries_prompt\")\n if ($regInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $script:config.PROXY_ALLOWED_REGISTRIES = if ($regInput) { $regInput } else { \"\" }\n } else {\n $script:config.PROXY_ALLOWED_REGISTRIES = $env:AGENTTEAMS_PROXY_ALLOWED_REGISTRIES\n }\n } else {\n Write-Log (Get-Msg \"docker_proxy.selected_disabled\")\n }\n}\n\nfunction Step-Idle {\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n $script:config.WORKER_IDLE_TIMEOUT = if ($env:AGENTTEAMS_WORKER_IDLE_TIMEOUT) { $env:AGENTTEAMS_WORKER_IDLE_TIMEOUT } else { \"720\" }\n Write-Log \" $(Get-Msg 'idle_timeout.label') = $($script:config.WORKER_IDLE_TIMEOUT) (non-interactive, skipped)\"\n return\n }\n # ─────────────────────────────────────────────────────────────────\n if ($script:AGENTTEAMS_UPGRADE -and $env:AGENTTEAMS_WORKER_IDLE_TIMEOUT) {\n Write-Log (Get-Msg \"prompt.upgrade_keep\" -f (Get-Msg \"idle_timeout.label\"), $env:AGENTTEAMS_WORKER_IDLE_TIMEOUT)\n $idleInput = Read-Host (Get-Msg \"idle_timeout.prompt\")\n if ($idleInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $script:config.WORKER_IDLE_TIMEOUT = if ($idleInput) { $idleInput } else { $env:AGENTTEAMS_WORKER_IDLE_TIMEOUT }\n } elseif (-not $env:AGENTTEAMS_WORKER_IDLE_TIMEOUT) {\n $idleInput = Read-Host (Get-Msg \"idle_timeout.prompt\")\n if ($idleInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $script:config.WORKER_IDLE_TIMEOUT = if ($idleInput) { $idleInput } else { \"720\" }\n } else {\n $script:config.WORKER_IDLE_TIMEOUT = $env:AGENTTEAMS_WORKER_IDLE_TIMEOUT\n }\n Write-Log (Get-Msg \"idle_timeout.selected\" -f $script:config.WORKER_IDLE_TIMEOUT)\n Write-Log \"\"\n}\n\nfunction Step-Hostshare {\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n $script:config.HOST_SHARE_DIR = if ($env:AGENTTEAMS_HOST_SHARE_DIR) { $env:AGENTTEAMS_HOST_SHARE_DIR } else { $env:USERPROFILE }\n Write-Log \" $(Get-Msg 'host_share.label') = $($script:config.HOST_SHARE_DIR) (non-interactive, skipped)\"\n return\n }\n # ─────────────────────────────────────────────────────────────────\n $currentShare = $script:config.HOST_SHARE_DIR\n if ($currentShare) {\n $shareInput = Read-Host \"$(Get-Msg 'host_share.prompt' -f $env:USERPROFILE) [${currentShare}]\"\n } else {\n $shareInput = Read-Host (Get-Msg 'host_share.prompt' -f $env:USERPROFILE)\n }\n if ($shareInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $script:config.HOST_SHARE_DIR = if ($shareInput) { $shareInput } else { if ($currentShare) { $currentShare } else { $env:USERPROFILE } }\n}\n\n# ============================================================\n# Manager Installation\n# ============================================================\n\nfunction Install-Manager {\n Write-Log (Get-Msg \"install.title\")\n\n # Detect timezone\n $script:AGENTTEAMS_TIMEZONE = Get-AgentTeamsTimeZone\n\n # Language priority: env var > existing env file > timezone detection\n if ($env:AGENTTEAMS_LANGUAGE) {\n $script:AGENTTEAMS_LANGUAGE = $env:AGENTTEAMS_LANGUAGE\n } else {\n # Check existing env file for saved language preference (upgrade scenario)\n $_envFile = $script:AGENTTEAMS_ENV_FILE\n # Migrate from legacy location (current directory) if needed\n if (-not (Test-Path $_envFile) -and (Test-Path \".\\agentteams-manager.env\")) {\n Write-Log \"Migrating agentteams-manager.env to $_envFile...\"\n Move-Item \".\\agentteams-manager.env\" $_envFile -ErrorAction SilentlyContinue\n }\n if (Test-Path $_envFile) {\n $_savedLang = (Get-Content $_envFile | Select-String \"^AGENTTEAMS_LANGUAGE=\" | ForEach-Object {\n $_.Line -replace '^AGENTTEAMS_LANGUAGE=', ''\n } | Select-Object -First 1)\n if ($_savedLang) {\n $script:AGENTTEAMS_LANGUAGE = $_savedLang\n }\n }\n # Fall back to timezone-based detection\n if (-not $script:AGENTTEAMS_LANGUAGE) {\n $script:AGENTTEAMS_LANGUAGE = Get-AgentTeamsLanguage -Timezone $script:AGENTTEAMS_TIMEZONE\n }\n }\n $env:AGENTTEAMS_LANGUAGE = $script:AGENTTEAMS_LANGUAGE\n\n # Detect registry\n $script:AGENTTEAMS_REGISTRY = Get-Registry -Timezone $script:AGENTTEAMS_TIMEZONE\n\n # Set image names\n $script:MANAGER_IMAGE = if ($env:AGENTTEAMS_INSTALL_MANAGER_IMAGE) {\n $env:AGENTTEAMS_INSTALL_MANAGER_IMAGE\n } else {\n \"$($script:AGENTTEAMS_REGISTRY)/agentteams/agentteams-manager:$($script:AGENTTEAMS_VERSION)\"\n }\n\n $script:WORKER_IMAGE = if ($env:AGENTTEAMS_INSTALL_WORKER_IMAGE) {\n $env:AGENTTEAMS_INSTALL_WORKER_IMAGE\n } else {\n \"$($script:AGENTTEAMS_REGISTRY)/agentteams/agentteams-worker:$($script:AGENTTEAMS_VERSION)\"\n }\n\n $script:COPAW_WORKER_IMAGE = if ($env:AGENTTEAMS_INSTALL_COPAW_WORKER_IMAGE) {\n $env:AGENTTEAMS_INSTALL_COPAW_WORKER_IMAGE\n } else {\n \"$($script:AGENTTEAMS_REGISTRY)/agentteams/agentteams-copaw-worker:$($script:AGENTTEAMS_VERSION)\"\n }\n\n $script:HERMES_WORKER_IMAGE = if ($env:AGENTTEAMS_INSTALL_HERMES_WORKER_IMAGE) {\n $env:AGENTTEAMS_INSTALL_HERMES_WORKER_IMAGE\n } else {\n \"$($script:AGENTTEAMS_REGISTRY)/agentteams/agentteams-hermes-worker:$($script:AGENTTEAMS_VERSION)\"\n }\n\n $script:MANAGER_COPAW_IMAGE = if ($env:AGENTTEAMS_INSTALL_MANAGER_COPAW_IMAGE) {\n $env:AGENTTEAMS_INSTALL_MANAGER_COPAW_IMAGE\n } else {\n \"$($script:AGENTTEAMS_REGISTRY)/agentteams/agentteams-manager-copaw:$($script:AGENTTEAMS_VERSION)\"\n }\n\n $script:CONTROLLER_IMAGE = if ($env:AGENTTEAMS_INSTALL_CONTROLLER_IMAGE) {\n $env:AGENTTEAMS_INSTALL_CONTROLLER_IMAGE\n } else {\n \"$($script:AGENTTEAMS_REGISTRY)/agentteams/agentteams-controller:$($script:AGENTTEAMS_VERSION)\"\n }\n\n # Resolve embedded controller image (sets $script:EMBEDDED_IMAGE and\n # $script:AGENTTEAMS_USE_EMBEDDED). Errors out fast if no embedded image is available\n # for the requested version (mirrors the bash installer behavior).\n Resolve-EmbeddedImage\n\n Write-Log (Get-Msg \"install.registry\" -f $script:AGENTTEAMS_REGISTRY)\n Write-Log \"\"\n Write-Log (Get-Msg \"install.dir\" -f (Get-Location))\n Write-Log (Get-Msg \"install.dir_hint\")\n Write-Log (Get-Msg \"install.dir_hint2\")\n Write-Log \"\"\n\n # Check container runtime (docker or podman)\n if (-not (Get-Command \"docker\" -ErrorAction SilentlyContinue) -and\n -not (Get-Command \"podman\" -ErrorAction SilentlyContinue)) {\n Write-Host \"$($script:ESC)[31m[AgentTeams ERROR]$($script:ESC)[0m $(Get-Msg 'error.docker_not_found')\" -ForegroundColor Red\n Exit-Script 1\n }\n\n if (-not (Test-DockerRunning)) {\n Write-Host \"$($script:ESC)[31m[AgentTeams ERROR]$($script:ESC)[0m $(Get-Msg 'error.docker_not_running')\" -ForegroundColor Red\n Exit-Script 1\n }\n\n # Initialize shared config hashtable\n $script:config = @{}\n $config = $script:config # local alias so post-machine code can use $config\n\n # Orphan volume detection (env file gone but volume remains)\n if (-not (Test-Path $script:AGENTTEAMS_ENV_FILE)) {\n $dataVol = if ($env:AGENTTEAMS_DATA_DIR) { $env:AGENTTEAMS_DATA_DIR } else { \"agentteams-data\" }\n $volumeExists = docker volume ls -q 2>$null | Select-String \"^${dataVol}$\"\n if ($volumeExists) {\n Write-Host \"\"\n Write-Log (Get-Msg \"install.orphan_volume.detected\" -f $dataVol)\n Write-Log (Get-Msg \"install.orphan_volume.warn\")\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n Write-Log (Get-Msg \"install.orphan_volume.clean_noninteractive\")\n docker stop agentteams-manager *>$null\n docker rm agentteams-manager *>$null\n docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-worker-\" | ForEach-Object {\n docker stop $_.ToString().Trim() *>$null\n docker rm $_.ToString().Trim() *>$null\n }\n Write-Log (Get-Msg \"install.orphan_volume.cleaning\")\n docker volume rm $dataVol *>$null\n Write-Log (Get-Msg \"install.orphan_volume.cleaned\")\n } else {\n Write-Host \"\"\n Write-Host (Get-Msg \"install.orphan_volume.choose\")\n Write-Host (Get-Msg \"install.orphan_volume.clean\")\n Write-Host (Get-Msg \"install.orphan_volume.keep\")\n Write-Host \"\"\n $orphanChoice = Read-Host (Get-Msg \"install.orphan_volume.prompt\")\n if (-not $orphanChoice) { $orphanChoice = \"1\" }\n switch -Regex ($orphanChoice) {\n \"^(1|clean)$\" {\n docker stop agentteams-manager *>$null\n docker rm agentteams-manager *>$null\n docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-worker-\" | ForEach-Object {\n docker stop $_.ToString().Trim() *>$null\n docker rm $_.ToString().Trim() *>$null\n }\n Write-Log (Get-Msg \"install.orphan_volume.cleaning\")\n docker volume rm $dataVol *>$null\n Write-Log (Get-Msg \"install.orphan_volume.cleaned\")\n }\n \"^(2|keep)$\" {\n Write-Log (Get-Msg \"install.orphan_volume.keeping\")\n }\n }\n }\n }\n }\n\n # ── State machine ─────────────────────────────────────────────────────────\n $_steps = @(\"Step-Lang\", \"Step-Mode\", \"Step-Existing\", \"Step-Llm\", \"Step-ManagerRuntime\", \"Step-Runtime\", \"Step-Admin\",\n \"Step-Network\", \"Step-Ports\", \"Step-Domains\", \"Step-Github\", \"Step-Skills\",\n \"Step-Volume\", \"Step-Workspace\", \"Step-E2ee\", \"Step-DockerProxy\", \"Step-Idle\",\n \"Step-Hostshare\")\n $_stepHistory = [System.Collections.Generic.List[int]]::new()\n $_stepIdx = 0\n while ($_stepIdx -lt $_steps.Count) {\n $_stepFn = $_steps[$_stepIdx]\n if (Test-ShouldSkipStep $_stepFn) {\n $_stepIdx++\n continue\n }\n if ($_stepHistory.Count -gt 0) {\n Write-Log (Get-Msg \"nav.back_hint\")\n }\n $script:StepResult = \"\"\n & $_stepFn\n if ($script:StepResult -eq \"back\") {\n if ($_stepHistory.Count -gt 0) {\n $_stepIdx = $_stepHistory[$_stepHistory.Count - 1]\n $_stepHistory.RemoveAt($_stepHistory.Count - 1)\n Clear-StepVars $_steps[$_stepIdx]\n }\n # else: first step, ignore 'b'\n } else {\n $_stepHistory.Add($_stepIdx)\n $_stepIdx++\n }\n }\n # ── End state machine ──────────────────────────────────────────────────────\n\n # Post-machine defaults for any steps that were skipped\n if (-not $script:config.DATA_DIR) {\n $script:config.DATA_DIR = if ($env:AGENTTEAMS_DATA_DIR) { $env:AGENTTEAMS_DATA_DIR } else { \"agentteams-data\" }\n }\n Write-Log (Get-Msg \"data.volume_using\" -f $script:config.DATA_DIR)\n if (-not $script:config.WORKSPACE_DIR) {\n $_defaultWs = \"$env:USERPROFILE\\agentteams-manager\"\n $script:config.WORKSPACE_DIR = if ($env:AGENTTEAMS_WORKSPACE_DIR) { $env:AGENTTEAMS_WORKSPACE_DIR } else { $_defaultWs }\n if (-not (Test-Path $script:config.WORKSPACE_DIR)) {\n New-Item -ItemType Directory -Path $script:config.WORKSPACE_DIR -Force | Out-Null\n }\n Write-Log (Get-Msg \"workspace.dir_label\" -f $script:config.WORKSPACE_DIR)\n }\n if (-not $script:config.DEFAULT_WORKER_RUNTIME) {\n $script:config.DEFAULT_WORKER_RUNTIME = if ($env:AGENTTEAMS_DEFAULT_WORKER_RUNTIME) { $env:AGENTTEAMS_DEFAULT_WORKER_RUNTIME } else { \"copaw\" }\n Write-Log (Get-Msg \"worker_runtime.selected\" -f $script:config.DEFAULT_WORKER_RUNTIME)\n }\n if (-not $script:config.MATRIX_E2EE) {\n $script:config.MATRIX_E2EE = if ($env:AGENTTEAMS_MATRIX_E2EE) { $env:AGENTTEAMS_MATRIX_E2EE } else { \"0\" }\n }\n if (-not $script:config.WORKER_IDLE_TIMEOUT) {\n $script:config.WORKER_IDLE_TIMEOUT = if ($env:AGENTTEAMS_WORKER_IDLE_TIMEOUT) { $env:AGENTTEAMS_WORKER_IDLE_TIMEOUT } else { \"720\" }\n }\n if (-not $script:config.HOST_SHARE_DIR) {\n $script:config.HOST_SHARE_DIR = if ($env:AGENTTEAMS_HOST_SHARE_DIR) { $env:AGENTTEAMS_HOST_SHARE_DIR } else { $env:USERPROFILE }\n }\n if (-not $script:config.LOCAL_ONLY) {\n $script:config.LOCAL_ONLY = if ($env:AGENTTEAMS_LOCAL_ONLY) { $env:AGENTTEAMS_LOCAL_ONLY } else { \"1\" }\n }\n if (-not $script:config.MANAGER_RUNTIME) {\n $script:config.MANAGER_RUNTIME = if ($env:AGENTTEAMS_MANAGER_RUNTIME) { $env:AGENTTEAMS_MANAGER_RUNTIME } else { \"copaw\" }\n }\n if (-not $script:config.PORT_GATEWAY) {\n $script:config.PORT_GATEWAY = if ($env:AGENTTEAMS_PORT_GATEWAY) { $env:AGENTTEAMS_PORT_GATEWAY } else { \"18080\" }\n $script:config.PORT_CONSOLE = if ($env:AGENTTEAMS_PORT_CONSOLE) { $env:AGENTTEAMS_PORT_CONSOLE } else { \"18001\" }\n $script:config.PORT_ELEMENT_WEB = if ($env:AGENTTEAMS_PORT_ELEMENT_WEB) { $env:AGENTTEAMS_PORT_ELEMENT_WEB } else { \"18088\" }\n $script:config.PORT_MANAGER_CONSOLE = if ($env:AGENTTEAMS_PORT_MANAGER_CONSOLE) { $env:AGENTTEAMS_PORT_MANAGER_CONSOLE } else { \"18888\" }\n }\n if (-not $script:config.MATRIX_DOMAIN) {\n $script:config.MATRIX_DOMAIN = if ($env:AGENTTEAMS_MATRIX_DOMAIN) { $env:AGENTTEAMS_MATRIX_DOMAIN } else { \"matrix-local.agentteams.io:$($script:config.PORT_GATEWAY)\" }\n $script:config.MATRIX_CLIENT_DOMAIN = if ($env:AGENTTEAMS_MATRIX_CLIENT_DOMAIN) { $env:AGENTTEAMS_MATRIX_CLIENT_DOMAIN } else { \"matrix-client-local.agentteams.io\" }\n $script:config.AI_GATEWAY_DOMAIN = if ($env:AGENTTEAMS_AI_GATEWAY_DOMAIN) { $env:AGENTTEAMS_AI_GATEWAY_DOMAIN } else { \"aigw-local.agentteams.io\" }\n $script:config.FS_DOMAIN = if ($env:AGENTTEAMS_FS_DOMAIN) { $env:AGENTTEAMS_FS_DOMAIN } else { \"fs-local.agentteams.io\" }\n $script:config.CONSOLE_DOMAIN = if ($env:AGENTTEAMS_CONSOLE_DOMAIN) { $env:AGENTTEAMS_CONSOLE_DOMAIN } else { \"console-local.agentteams.io\" }\n }\n\n Write-Log \"\"\n\n # Generate secrets\n Write-Log (Get-Msg \"install.generating_secrets\")\n $config.MANAGER_PASSWORD = if ($env:AGENTTEAMS_MANAGER_PASSWORD) { $env:AGENTTEAMS_MANAGER_PASSWORD } else { New-RandomKey }\n $config.REGISTRATION_TOKEN = if ($env:AGENTTEAMS_REGISTRATION_TOKEN) { $env:AGENTTEAMS_REGISTRATION_TOKEN } else { New-RandomKey }\n $config.MINIO_USER = if ($env:AGENTTEAMS_MINIO_USER) { $env:AGENTTEAMS_MINIO_USER } else { $config.ADMIN_USER }\n $config.MINIO_PASSWORD = if ($env:AGENTTEAMS_MINIO_PASSWORD) { $env:AGENTTEAMS_MINIO_PASSWORD } else { $config.ADMIN_PASSWORD }\n $config.MANAGER_GATEWAY_KEY = if ($env:AGENTTEAMS_MANAGER_GATEWAY_KEY) { $env:AGENTTEAMS_MANAGER_GATEWAY_KEY } else { New-RandomKey }\n\n # Store additional config\n $config.LANGUAGE = $script:AGENTTEAMS_LANGUAGE\n $config.REGISTRY = $script:AGENTTEAMS_REGISTRY\n $config.WORKER_IMAGE = $script:WORKER_IMAGE\n $config.COPAW_WORKER_IMAGE = $script:COPAW_WORKER_IMAGE\n $config.HERMES_WORKER_IMAGE = $script:HERMES_WORKER_IMAGE\n $config.MANAGER_COPAW_IMAGE = $script:MANAGER_COPAW_IMAGE\n\n # Write env file\n New-EnvFile -Config $config -Path $script:AGENTTEAMS_ENV_FILE\n\n # Manager image selection (used by both embedded — passed to controller via env —\n # and legacy — used directly as `docker run` target).\n $managerImage = if ($config.MANAGER_RUNTIME -eq \"copaw\") { $script:MANAGER_COPAW_IMAGE } else { $script:MANAGER_IMAGE }\n $portPrefix = if ($config.LOCAL_ONLY -eq \"1\") { \"127.0.0.1:\" } else { \"\" }\n\n # Ensure agentteams-net Docker network exists. Used in both modes — the embedded\n # controller and the spawned manager/worker containers all join it and rely on\n # network aliases for *-local.agentteams.io DNS resolution.\n if ($script:AGENTTEAMS_MOUNT_SOCKET) {\n $socketAvailable = Test-DockerRunning\n if ($socketAvailable) {\n docker network inspect agentteams-net *>$null\n if ($LASTEXITCODE -ne 0) { docker network create agentteams-net *>$null }\n } else {\n Write-Log (Get-Msg \"install.socket_not_found\")\n if (-not $script:AGENTTEAMS_NON_INTERACTIVE) {\n Write-Host \"\"\n Write-Host \"$($script:ESC)[33m$(Get-Msg 'install.socket_confirm.title')$($script:ESC)[0m\"\n Write-Host \"\"\n Write-Host (Get-Msg 'install.socket_confirm.message')\n Write-Host \"\"\n $confirm = Read-Host (Get-Msg 'install.socket_confirm.prompt')\n if ($confirm -ne 'y' -and $confirm -ne 'Y') {\n Write-Log (Get-Msg 'install.socket_confirm.cancelled')\n exit 0\n }\n }\n }\n }\n\n if ($script:AGENTTEAMS_USE_EMBEDDED -ne \"1\") {\n # ============================================================\n # Legacy architecture: all-in-one manager container\n # (only entered when AGENTTEAMS_FORCE_LEGACY=1 — broken with the slim manager\n # image shipped since PR #616; kept solely for AGENTTEAMS_VERSION <= v1.0.9)\n # ============================================================\n\n $dockerArgs = @(\n \"run\", \"-d\",\n \"--name\", \"agentteams-manager\",\n \"--env-file\", $script:AGENTTEAMS_ENV_FILE,\n \"-e\", \"HOME=/root/manager-workspace\",\n \"-w\", \"/root/manager-workspace\",\n \"-e\", \"HOST_ORIGINAL_HOME=$($config.HOST_SHARE_DIR)\",\n \"-e\", \"AGENTTEAMS_MANAGER_RUNTIME=$($config.MANAGER_RUNTIME)\"\n )\n\n $dockerArgs += @(\"-e\", \"TZ=$($script:AGENTTEAMS_TIMEZONE)\")\n\n if ($script:AGENTTEAMS_MOUNT_SOCKET -and (Test-DockerRunning)) {\n $dockerArgs += @(\"--network\", \"agentteams-net\")\n $dockerArgs += @(\"--network-alias\", \"matrix-local.agentteams.io\")\n $dockerArgs += @(\"--network-alias\", \"aigw-local.agentteams.io\")\n $dockerArgs += @(\"--network-alias\", \"fs-local.agentteams.io\")\n foreach ($domain in @($config.MATRIX_CLIENT_DOMAIN, $config.CONSOLE_DOMAIN)) {\n if ($domain -match '-local\\.agentteams\\.io$') {\n $dockerArgs += @(\"--network-alias\", $domain)\n }\n }\n\n if ($config.DOCKER_PROXY -eq \"1\") {\n $proxyImage = $script:CONTROLLER_IMAGE\n Write-Log \"Starting Docker API proxy...\"\n docker rm -f agentteams-controller *>$null\n docker run -d --name agentteams-controller `\n --network agentteams-net `\n -v \"//var/run/docker.sock:/var/run/docker.sock\" `\n --security-opt label=disable `\n -e \"AGENTTEAMS_WORKER_IMAGE=$($script:WORKER_IMAGE)\" `\n -e \"AGENTTEAMS_COPAW_WORKER_IMAGE=$($script:COPAW_WORKER_IMAGE)\" `\n -e \"AGENTTEAMS_HERMES_WORKER_IMAGE=$($script:HERMES_WORKER_IMAGE)\" `\n -e \"AGENTTEAMS_DEFAULT_WORKER_RUNTIME=$($script:config.DEFAULT_WORKER_RUNTIME)\" `\n $(if ($config.PROXY_ALLOWED_REGISTRIES) { @(\"-e\", \"AGENTTEAMS_PROXY_ALLOWED_REGISTRIES=$($config.PROXY_ALLOWED_REGISTRIES)\") }) `\n --restart unless-stopped `\n $proxyImage\n $dockerArgs += @(\"-e\", \"AGENTTEAMS_CONTROLLER_URL=http://agentteams-controller:8090\")\n $dockerArgs += @(\"-e\", \"AGENTTEAMS_CONTAINER_API=http://agentteams-controller:8090\")\n Write-Log (Get-Msg \"docker_proxy.selected_enabled\")\n } else {\n $dockerArgs += @(\"-v\", \"//var/run/docker.sock:/var/run/docker.sock\")\n $dockerArgs += @(\"--security-opt\", \"label=disable\")\n Write-Log (Get-Msg \"install.socket_detected\" -f \"//var/run/docker.sock\")\n }\n }\n\n $dockerArgs += @(\"-p\", \"${portPrefix}$($config.PORT_GATEWAY):8080\")\n $dockerArgs += @(\"-p\", \"${portPrefix}$($config.PORT_CONSOLE):8001\")\n $dockerArgs += @(\"-p\", \"${portPrefix}$($config.PORT_ELEMENT_WEB):8088\")\n $dockerArgs += @(\"-p\", \"127.0.0.1:$($config.PORT_MANAGER_CONSOLE):18888\")\n\n $dockerArgs += @(\"-v\", \"$($config.DATA_DIR):/data\")\n\n $wsDockerPath = ConvertTo-DockerPath -Path $config.WORKSPACE_DIR\n $dockerArgs += @(\"-v\", \"${wsDockerPath}:/root/manager-workspace\")\n\n $shareDockerPath = ConvertTo-DockerPath -Path $config.HOST_SHARE_DIR\n $dockerArgs += @(\"-v\", \"${shareDockerPath}:/host-share\")\n Write-Log (Get-Msg \"host_share.sharing\" -f $config.HOST_SHARE_DIR)\n\n if ($env:AGENTTEAMS_YOLO -eq \"1\") {\n $dockerArgs += @(\"-e\", \"AGENTTEAMS_YOLO=1\")\n Write-Log (Get-Msg \"install.yolo\")\n }\n\n if ($env:AGENTTEAMS_MATRIX_DEBUG -eq \"1\") {\n $dockerArgs += @(\"-e\", \"AGENTTEAMS_MATRIX_DEBUG=1\")\n }\n\n $dockerArgs += @(\"--restart\", \"unless-stopped\")\n $dockerArgs += $managerImage\n }\n\n # Check if the Docker volume exists; create if not (reuse on reinstall) — both modes.\n $volumeExists = docker volume ls -q 2>$null | Select-String \"^$($config.DATA_DIR)$\"\n if (-not $volumeExists) {\n docker volume create $config.DATA_DIR | Out-Null\n }\n\n # Pull images (skip if already exists locally for local build tags).\n $LocalImagePattern = \"^agentteams/\"\n function Test-LocalImage {\n param([string]$Image)\n $imgExists = docker image inspect $Image 2>$null\n return $LASTEXITCODE -eq 0\n }\n if ($script:AGENTTEAMS_USE_EMBEDDED -eq \"1\") {\n # Embedded image was already pulled by Resolve-EmbeddedImage unless overridden;\n # for an explicit override we still need to ensure it is present locally.\n if ($env:AGENTTEAMS_INSTALL_EMBEDDED_IMAGE) {\n if ($script:EMBEDDED_IMAGE -match $LocalImagePattern) {\n if (-not (Test-LocalImage $script:EMBEDDED_IMAGE)) {\n Write-Log \"Pulling embedded image: $($script:EMBEDDED_IMAGE)\"\n & docker pull $script:EMBEDDED_IMAGE\n }\n } else {\n Write-Log \"Pulling embedded image: $($script:EMBEDDED_IMAGE)\"\n & docker pull $script:EMBEDDED_IMAGE\n }\n }\n # Manager image — controller will spawn it inside; pull here so the spawn doesn't\n # have to wait on the network.\n if ($managerImage -match $LocalImagePattern) {\n if (Test-LocalImage $managerImage) {\n Write-Log (Get-Msg \"install.image.exists\" -f $managerImage)\n } else {\n Write-Log (Get-Msg \"install.image.pulling_manager\" -f $managerImage)\n & docker pull $managerImage\n }\n } else {\n Write-Log (Get-Msg \"install.image.pulling_manager\" -f $managerImage)\n & docker pull $managerImage\n }\n } else {\n if ($managerImage -match $LocalImagePattern) {\n if (Test-LocalImage $managerImage) {\n Write-Log (Get-Msg \"install.image.exists\" -f $managerImage)\n } else {\n Write-Log (Get-Msg \"install.image.pulling_manager\" -f $managerImage)\n & docker pull $managerImage\n }\n } else {\n Write-Log (Get-Msg \"install.image.pulling_manager\" -f $managerImage)\n & docker pull $managerImage\n }\n }\n\n # Pull all worker runtime images (workers may use any runtime regardless of the default)\n foreach ($workerImg in @($script:WORKER_IMAGE, $script:COPAW_WORKER_IMAGE, $script:HERMES_WORKER_IMAGE)) {\n if ($workerImg -match $LocalImagePattern) {\n if (Test-LocalImage $workerImg) {\n Write-Log (Get-Msg \"install.image.worker_exists\" -f $workerImg)\n } else {\n Write-Log (Get-Msg \"install.image.pulling_worker\" -f $workerImg)\n & docker pull $workerImg\n }\n } else {\n Write-Log (Get-Msg \"install.image.pulling_worker\" -f $workerImg)\n & docker pull $workerImg\n }\n }\n\n # --- Pre-upgrade: extract Matrix passwords from old containers (old-arch -> embedded) ---\n $credsTmp = $null\n if ($script:AGENTTEAMS_UPGRADE -and $script:AGENTTEAMS_USE_EMBEDDED -eq \"1\") {\n $controllerNameHit = docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-controller$\"\n $isOldArch = $false\n if (-not $controllerNameHit) {\n $isOldArch = $true\n } else {\n $ctrlImgLine = docker ps -a --format \"{{.Names}} {{.Image}}\" 2>$null | Where-Object { $_ -match '^agentteams-controller ' } | Select-Object -First 1\n if ($ctrlImgLine -and ($ctrlImgLine -notmatch 'embedded')) {\n $isOldArch = $true\n }\n }\n\n if ($isOldArch) {\n $credsTmp = Join-Path ([System.IO.Path]::GetTempPath()) (\"agentteams-upgrade-creds-\" + [Guid]::NewGuid().ToString(\"n\"))\n New-Item -ItemType Directory -Path $credsTmp -Force | Out-Null\n $utf8NoBom = New-Object System.Text.UTF8Encoding $false\n\n $mgrCredsTempStart = $false\n $mgrExists = docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-manager$\"\n $mgrRunning = docker ps --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-manager$\"\n if ($mgrExists -and -not $mgrRunning) {\n Write-Log \"agentteams-manager is stopped; starting it temporarily to extract Matrix credentials for upgrade...\"\n docker start agentteams-manager 2>$null | Out-Null\n Wait-MatrixReady -Container \"agentteams-manager\"\n $mgrCredsTempStart = $true\n }\n\n $inspectLines = docker inspect agentteams-manager --format \"{{range .Config.Env}}{{println .}}{{end}}\" 2>$null\n $mgrPw = \"\"\n if ($inspectLines) {\n $envLine = $inspectLines -split \"`n\" | Where-Object { $_ -match '^AGENTTEAMS_MANAGER_PASSWORD=' } | Select-Object -First 1\n if ($envLine) {\n $mgrPw = ($envLine -replace '^AGENTTEAMS_MANAGER_PASSWORD=', \"\").Trim()\n }\n }\n $mgrRunningNow = docker ps --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-manager$\"\n if ([string]::IsNullOrEmpty($mgrPw) -and $mgrRunningNow) {\n $mgrPw = docker exec agentteams-manager bash -c 'source /data/agentteams-secrets.env 2>/dev/null && echo \"${AGENTTEAMS_MANAGER_PASSWORD}\"' 2>$null\n if ($mgrPw) { $mgrPw = $mgrPw.Trim() }\n }\n $dataVolPresent = docker volume ls -q 2>$null | Where-Object { $_ -eq $config.DATA_DIR }\n if ([string]::IsNullOrEmpty($mgrPw) -and $dataVolPresent) {\n $mgrPw = Read-AgentTeamsSecretFromDataVolume -VolumeName $config.DATA_DIR -Key \"AGENTTEAMS_MANAGER_PASSWORD\"\n }\n\n $envFilePath = $script:AGENTTEAMS_ENV_FILE\n $mgrRoom = \"\"\n if (-not [string]::IsNullOrEmpty($mgrPw)) {\n $adminPw = \"\"\n $adminUser = \"admin\"\n if (Test-Path $envFilePath) {\n $adminLine = Get-Content $envFilePath | Where-Object { $_ -match '^AGENTTEAMS_ADMIN_PASSWORD=' } | Select-Object -First 1\n if ($adminLine) { $adminPw = ($adminLine -replace '^AGENTTEAMS_ADMIN_PASSWORD=', \"\").Trim() }\n $userLine = Get-Content $envFilePath | Where-Object { $_ -match '^AGENTTEAMS_ADMIN_USER=' } | Select-Object -First 1\n if ($userLine) {\n $adminUser = ($userLine -replace '^AGENTTEAMS_ADMIN_USER=', \"\").Trim()\n if ([string]::IsNullOrEmpty($adminUser)) { $adminUser = \"admin\" }\n }\n }\n\n if (-not [string]::IsNullOrEmpty($adminPw) -and $mgrRunningNow) {\n $mgrRoom = Get-AgentTeamsAdminDmRoomViaMatrix -AdminUser $adminUser -AdminPassword $adminPw\n }\n if ([string]::IsNullOrEmpty($mgrRoom)) {\n $mgrRoom = Read-AgentTeamsAdminDmRoomFromWorkspace -WorkspaceDir $config.WORKSPACE_DIR\n }\n\n $gwKeyLine = if (Test-Path $envFilePath) {\n Get-Content $envFilePath | Where-Object { $_ -match '^AGENTTEAMS_MANAGER_GATEWAY_KEY=' } | Select-Object -First 1\n } else { $null }\n $gwKeyForDefault = if ($gwKeyLine) { ($gwKeyLine -replace '^AGENTTEAMS_MANAGER_GATEWAY_KEY=', \"\").Trim() } else { $config.MANAGER_GATEWAY_KEY }\n\n $defaultEnvPath = Join-Path $credsTmp \"default.env\"\n [System.IO.File]::WriteAllLines($defaultEnvPath, @(\n \"WORKER_PASSWORD=$mgrPw\"\n \"WORKER_MINIO_PASSWORD=$(Get-AgentTeamsRandomHex 24)\"\n \"WORKER_GATEWAY_KEY=$gwKeyForDefault\"\n \"WORKER_ROOM_ID=$mgrRoom\"\n ), $utf8NoBom)\n if ($mgrRoom) {\n Write-Log \"Extracted Manager Matrix password and room ID\"\n } else {\n Write-Log \"Extracted Manager Matrix password\"\n }\n }\n\n if ($mgrCredsTempStart) {\n Write-Log \"Stopping agentteams-manager after credential extraction (upgrade will recreate containers)...\"\n docker stop agentteams-manager 2>$null | Out-Null\n }\n }\n }\n\n # Stop and remove existing containers (deferred until after all\n # configuration is collected and images are pulled successfully)\n $existingProxy = docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-controller$\"\n if ($existingProxy) {\n docker stop agentteams-controller *>$null\n docker rm agentteams-controller *>$null\n }\n $existingContainer = docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-manager$\"\n if ($existingContainer) {\n Write-Log (Get-Msg \"install.removing_existing\")\n docker stop agentteams-manager *>$null\n docker rm agentteams-manager *>$null\n }\n\n # Stop and remove worker containers (controller will recreate via CR reconciliation)\n if ($script:UPGRADE_EXISTING_WORKERS) {\n Write-Log (Get-Msg \"install.existing.stopping_workers\")\n $script:UPGRADE_EXISTING_WORKERS | ForEach-Object {\n docker stop $_ *>$null\n docker rm $_ *>$null\n Write-Log (Get-Msg \"install.existing.removed\" -f $_)\n }\n }\n\n # --- Upgrade: inject extracted credentials into data volume (old-arch -> embedded) ---\n if ($credsTmp -and (Test-Path $credsTmp)) {\n $credsFiles = Get-ChildItem -Path $credsTmp -Filter \"*.env\" -ErrorAction SilentlyContinue\n if ($credsFiles -and $credsFiles.Count -gt 0) {\n $cleanupCtr = \"agentteams-upgrade-cleanup\"\n docker rm -f $cleanupCtr 2>$null | Out-Null\n $credsDockerPath = ConvertTo-DockerPath -Path $credsTmp\n $injectShell = \"rm -rf /data/worker-creds && mkdir -p /data/worker-creds && cp /creds/*.env /data/worker-creds/ 2>/dev/null || true && chmod 600 /data/worker-creds/*.env 2>/dev/null || true\"\n docker run --rm --name $cleanupCtr --entrypoint sh `\n -v \"$($config.DATA_DIR):/data\" `\n -v \"${credsDockerPath}:/creds:ro\" `\n $script:EMBEDDED_IMAGE `\n -c $injectShell 2>$null | Out-Null\n if ($LASTEXITCODE -eq 0) {\n Write-Log \"Injected credentials for upgrade\"\n } else {\n Write-Log \"Warning: credential injection failed, continuing\"\n }\n }\n Remove-Item -Path $credsTmp -Recurse -Force -ErrorAction SilentlyContinue\n }\n\n if ($script:AGENTTEAMS_USE_EMBEDDED -eq \"1\") {\n # ============================================================\n # New architecture: embedded controller + auto-created manager\n # (controller container hosts Higress / Tuwunel / MinIO / Element Web /\n # controller binary, then spawns the lightweight manager container)\n # ============================================================\n\n # Internal port: 8080 (Higress gateway inside the controller container).\n $internalGwPort = 8080\n\n $matrixDomain = if ($config.MATRIX_DOMAIN) {\n $config.MATRIX_DOMAIN\n } else {\n \"matrix-local.agentteams.io:$($config.PORT_GATEWAY)\"\n }\n $aigwDomain = if ($config.AI_GATEWAY_DOMAIN) { $config.AI_GATEWAY_DOMAIN } else { \"aigw-local.agentteams.io\" }\n if ($aigwDomain -notmatch \":\") { $aigwDomain = \"${aigwDomain}:${internalGwPort}\" }\n $fsDomain = if ($config.FS_DOMAIN) { $config.FS_DOMAIN } else { \"fs-local.agentteams.io\" }\n if ($fsDomain -notmatch \":\") { $fsDomain = \"${fsDomain}:${internalGwPort}\" }\n\n $ctrlArgs = @(\n \"run\", \"-d\",\n \"--name\", \"agentteams-controller\",\n \"--network\", \"agentteams-net\",\n \"--network-alias\", \"matrix-local.agentteams.io\",\n \"--network-alias\", \"aigw-local.agentteams.io\",\n \"--network-alias\", \"fs-local.agentteams.io\",\n \"-e\", \"AGENTTEAMS_ADMIN_USER=$($config.ADMIN_USER)\",\n \"-e\", \"AGENTTEAMS_ADMIN_PASSWORD=$($config.ADMIN_PASSWORD)\",\n \"-e\", \"AGENTTEAMS_MANAGER_PASSWORD=$($config.MANAGER_PASSWORD)\",\n \"-e\", \"AGENTTEAMS_REGISTRATION_TOKEN=$($config.REGISTRATION_TOKEN)\",\n \"-e\", \"AGENTTEAMS_MINIO_USER=$($config.MINIO_USER)\",\n \"-e\", \"AGENTTEAMS_MINIO_PASSWORD=$($config.MINIO_PASSWORD)\",\n \"-e\", \"AGENTTEAMS_LLM_PROVIDER=$($config.LLM_PROVIDER)\",\n \"-e\", \"AGENTTEAMS_LLM_API_KEY=$($config.LLM_API_KEY)\",\n \"-e\", \"AGENTTEAMS_DEFAULT_MODEL=$($config.DEFAULT_MODEL)\",\n \"-e\", \"AGENTTEAMS_MANAGER_GATEWAY_KEY=$($config.MANAGER_GATEWAY_KEY)\",\n \"-e\", \"AGENTTEAMS_MANAGER_RUNTIME=$($config.MANAGER_RUNTIME)\",\n \"-e\", \"AGENTTEAMS_MANAGER_IMAGE=$managerImage\",\n \"-e\", \"AGENTTEAMS_DEFAULT_WORKER_RUNTIME=$($config.DEFAULT_WORKER_RUNTIME)\",\n \"-e\", \"AGENTTEAMS_WORKER_IMAGE=$($script:WORKER_IMAGE)\",\n \"-e\", \"AGENTTEAMS_COPAW_WORKER_IMAGE=$($script:COPAW_WORKER_IMAGE)\",\n \"-e\", \"AGENTTEAMS_HERMES_WORKER_IMAGE=$($script:HERMES_WORKER_IMAGE)\",\n \"-e\", \"AGENTTEAMS_MATRIX_DOMAIN=$matrixDomain\",\n \"-e\", \"AGENTTEAMS_ELEMENT_HOMESERVER_URL=http://127.0.0.1:$($config.PORT_GATEWAY)\",\n \"-e\", \"AGENTTEAMS_MATRIX_URL=http://127.0.0.1:6167\",\n \"-e\", \"AGENTTEAMS_MATRIX_E2EE=$($config.MATRIX_E2EE)\",\n \"-e\", \"AGENTTEAMS_MINIO_ENDPOINT=http://127.0.0.1:9000\",\n \"-e\", \"AGENTTEAMS_MINIO_BUCKET=agentteams-storage\",\n \"-e\", \"AGENTTEAMS_STORAGE_PREFIX=agentteams/agentteams-storage\",\n \"-e\", \"AGENTTEAMS_FS_ENDPOINT=http://127.0.0.1:9000\",\n \"-e\", \"AGENTTEAMS_AI_GATEWAY_URL=http://$aigwDomain\",\n \"-e\", \"AGENTTEAMS_CONTROLLER_URL=http://agentteams-controller:8090\",\n \"-e\", \"AGENTTEAMS_DOCKER_NETWORK=agentteams-net\",\n \"-e\", \"AGENTTEAMS_WORKSPACE_DIR=$($config.WORKSPACE_DIR)\",\n \"-e\", \"AGENTTEAMS_HOST_SHARE_DIR=$($config.HOST_SHARE_DIR)\",\n \"-e\", \"AGENTTEAMS_MANAGER_ENABLED=true\",\n \"-e\", \"AGENTTEAMS_PORT_MANAGER_CONSOLE=$($config.PORT_MANAGER_CONSOLE)\"\n )\n\n if ($script:AGENTTEAMS_TIMEZONE) {\n $ctrlArgs += @(\"-e\", \"TZ=$($script:AGENTTEAMS_TIMEZONE)\")\n }\n if ($env:AGENTTEAMS_YOLO -eq \"1\") {\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_YOLO=1\")\n }\n if ($env:AGENTTEAMS_MATRIX_DEBUG -eq \"1\") {\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_MATRIX_DEBUG=1\")\n }\n if ($config.GITHUB_TOKEN) {\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_GITHUB_TOKEN=$($config.GITHUB_TOKEN)\")\n }\n if ($config.EMBEDDING_MODEL) {\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_EMBEDDING_MODEL=$($config.EMBEDDING_MODEL)\")\n }\n if ($config.OPENAI_BASE_URL) {\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_OPENAI_BASE_URL=$($config.OPENAI_BASE_URL)\")\n }\n if ($env:AGENTTEAMS_AI_STREAM_IDLE_TIMEOUT_SECONDS) {\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_AI_STREAM_IDLE_TIMEOUT_SECONDS=$($env:AGENTTEAMS_AI_STREAM_IDLE_TIMEOUT_SECONDS)\")\n }\n if ($script:AGENTTEAMS_LANGUAGE) {\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_LANGUAGE=$($script:AGENTTEAMS_LANGUAGE)\")\n }\n\n # Optional: CMS/ARMS observability. In embedded mode the controller\n # spawns the Manager and Workers, so it must receive these settings.\n $cmsTracesEnabled = if ($env:AGENTTEAMS_CMS_TRACES_ENABLED) { $env:AGENTTEAMS_CMS_TRACES_ENABLED } else { \"false\" }\n $cmsServiceName = if ($env:AGENTTEAMS_CMS_SERVICE_NAME) { $env:AGENTTEAMS_CMS_SERVICE_NAME } else { \"agentteams-manager\" }\n $cmsMetricsEnabled = if ($env:AGENTTEAMS_CMS_METRICS_ENABLED) { $env:AGENTTEAMS_CMS_METRICS_ENABLED } else { \"false\" }\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_CMS_TRACES_ENABLED=$cmsTracesEnabled\")\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_CMS_SERVICE_NAME=$cmsServiceName\")\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_CMS_METRICS_ENABLED=$cmsMetricsEnabled\")\n if ($env:AGENTTEAMS_CMS_ENDPOINT) {\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_CMS_ENDPOINT=$($env:AGENTTEAMS_CMS_ENDPOINT)\")\n }\n if ($env:AGENTTEAMS_CMS_LICENSE_KEY) {\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_CMS_LICENSE_KEY=$($env:AGENTTEAMS_CMS_LICENSE_KEY)\")\n }\n if ($env:AGENTTEAMS_CMS_PROJECT) {\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_CMS_PROJECT=$($env:AGENTTEAMS_CMS_PROJECT)\")\n }\n if ($env:AGENTTEAMS_CMS_WORKSPACE) {\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_CMS_WORKSPACE=$($env:AGENTTEAMS_CMS_WORKSPACE)\")\n }\n\n # Mount the docker socket so the controller can spawn manager + workers.\n $ctrlArgs += @(\"-v\", \"//var/run/docker.sock:/var/run/docker.sock\")\n $ctrlArgs += @(\"--security-opt\", \"label=disable\")\n\n # Persistent data + workspace mounts (manager workspace is bind-mounted under\n # /root/agentteams-fs/agents/manager so the controller can hand it to the spawned\n # manager container).\n $ctrlArgs += @(\"-v\", \"$($config.DATA_DIR):/data\")\n $wsDockerPath = ConvertTo-DockerPath -Path $config.WORKSPACE_DIR\n $ctrlArgs += @(\"-v\", \"${wsDockerPath}:/root/agentteams-fs/agents/manager\")\n Write-Log (Get-Msg \"host_share.sharing\" -f $config.HOST_SHARE_DIR)\n\n # Externally exposed ports — only the gateway / Higress console / Element Web,\n # since the manager console is now spawned inside its own container by the\n # controller (port mapping is handled there).\n $ctrlArgs += @(\"-p\", \"${portPrefix}$($config.PORT_GATEWAY):8080\")\n $ctrlArgs += @(\"-p\", \"${portPrefix}$($config.PORT_CONSOLE):8001\")\n $ctrlArgs += @(\"-p\", \"${portPrefix}$($config.PORT_ELEMENT_WEB):8088\")\n\n $ctrlArgs += @(\"--restart\", \"unless-stopped\")\n $ctrlArgs += $script:EMBEDDED_IMAGE\n\n Write-Log (Get-Msg \"install.starting_manager\")\n & docker $ctrlArgs\n Write-Log \"Embedded controller started: agentteams-controller\"\n\n # Wait for infra inside the controller container.\n function Wait-EmbeddedUrl {\n param([string]$Url, [string]$Container, [int]$MaxWait, [string]$Description)\n $elapsed = 0\n Write-Log \"Waiting for $Description...\"\n while ($elapsed -lt $MaxWait) {\n docker exec $Container curl -sf $Url *>$null\n if ($LASTEXITCODE -eq 0) {\n Write-Log \"$Description is ready (${elapsed}s)\"\n return $true\n }\n Start-Sleep -Seconds 2\n $elapsed += 2\n }\n Write-Host \"$($script:ESC)[31m[AgentTeams ERROR]$($script:ESC)[0m $Description not ready after ${MaxWait}s\" -ForegroundColor Red\n return $false\n }\n\n if (-not (Wait-EmbeddedUrl \"http://127.0.0.1:6167/_tuwunel/server_version\" \"agentteams-controller\" 120 \"Tuwunel (Matrix)\")) { Exit-Script 1 }\n if (-not (Wait-EmbeddedUrl \"http://127.0.0.1:9000/minio/health/live\" \"agentteams-controller\" 60 \"MinIO\")) { Exit-Script 1 }\n if (-not (Wait-EmbeddedUrl \"http://127.0.0.1:8080/status\" \"agentteams-controller\" 120 \"Higress Gateway\")) { Exit-Script 1 }\n\n # Wait for the controller to spawn the Manager Agent container.\n Write-Log \"Waiting for Manager Agent container...\"\n $mgrWait = 0\n $mgrMax = 300\n while ($mgrWait -lt $mgrMax) {\n $found = docker ps --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-manager$\"\n if ($found) {\n Write-Log \"Manager Agent container detected (${mgrWait}s)\"\n break\n }\n Start-Sleep -Seconds 3\n $mgrWait += 3\n }\n if ($mgrWait -ge $mgrMax) {\n Write-Host \"$($script:ESC)[31m[AgentTeams ERROR]$($script:ESC)[0m Manager Agent container not created after ${mgrMax}s\" -ForegroundColor Red\n Write-Log \"Controller logs:\"\n docker exec agentteams-controller tail -30 /var/log/agentteams/agentteams-controller-error.log 2>$null\n Exit-Script 1\n }\n\n # Wait for Manager Agent container to reach `running` state.\n Write-Log \"Waiting for Manager Agent to start...\"\n $agentWait = 0\n while ($agentWait -lt 120) {\n $state = docker inspect --format \"{{.State.Status}}\" agentteams-manager 2>$null\n if ($state -eq \"running\") {\n Write-Log \"Manager Agent is running\"\n break\n }\n Start-Sleep -Seconds 2\n $agentWait += 2\n }\n\n if ($env:AGENTTEAMS_YOLO -eq \"1\") {\n docker exec agentteams-manager touch /root/manager-workspace/yolo-mode 2>$null\n }\n\n # Wait for the Manager Agent's runtime + Matrix to be reachable. In embedded\n # mode Tuwunel lives in `agentteams-controller`, so the Matrix probe must target\n # the controller (the manager only exposes the agent runtime).\n Wait-ManagerReady -Container \"agentteams-manager\"\n Wait-MatrixReady -Container \"agentteams-controller\"\n\n # Wait for the controller to actually deliver the first-boot welcome\n # message. Gated by the controller on (a) Manager joining the DM room\n # and (b) Higress WASM key-auth propagating to /v1/chat/completions for\n # the Manager's gateway key — typically ~45-90s on a fresh install.\n # We poll Manager CR status.welcomeSent via the in-container AgentTeams\n # CLI, exec'd inside agentteams-controller (the source-of-truth container\n # — its bundled CLI binary is always in lockstep with the controller\n # binary serving the HTTP API, since they're the same `go build`\n # output. The agentteams-manager container's CLI may lag the controller\n # across image upgrades and silently drop the welcomeSent field,\n # leaving this loop hung). The controller mints a long-lived admin\n # SA token at startup and writes it to\n # AGENTTEAMS_AUTH_TOKEN_FILE=/var/run/agentteams/cli-token (set as a Dockerfile\n # ENV default), so a bare `docker exec agentteams-controller agt …`\n # auto-discovers both the endpoint and the token. The brief window\n # after container start before bootstrapAdminCLIToken completes is\n # absorbed by the loop's silent retry.\n $hasAgentTeamsCli = $false\n try {\n docker exec agentteams-controller sh -c 'command -v agt' *> $null\n if ($LASTEXITCODE -eq 0) { $hasAgentTeamsCli = $true }\n } catch {}\n\n if ($hasAgentTeamsCli) {\n Write-Log (Get-Msg \"install.welcome_msg.waiting\")\n $welcomeMax = if ($env:AGENTTEAMS_WELCOME_TIMEOUT) { [int]$env:AGENTTEAMS_WELCOME_TIMEOUT } else { 300 }\n $welcomeWait = 0\n $welcomeDone = $false\n while ($welcomeWait -lt $welcomeMax) {\n $wjson = \"\"\n try {\n $wjson = docker exec agentteams-controller `\n agt get managers default -o json 2>$null\n } catch {}\n if ($wjson -and ($wjson -replace '\\s', '') -match '\"welcomeSent\":true') {\n Write-Log (Get-Msg \"install.welcome_msg.confirmed\" $welcomeWait)\n $welcomeDone = $true\n break\n }\n Start-Sleep -Seconds 3\n $welcomeWait += 3\n }\n if (-not $welcomeDone) {\n # Non-fatal: install is still good. Keep going to the success\n # banner so the admin can use Element Web to nudge Manager into\n # onboarding manually (one DM message is enough).\n Write-Log (Get-Msg \"install.welcome_msg.timeout\" $welcomeMax)\n Write-Log (Get-Msg \"install.welcome_msg.timeout_hint\")\n Write-Log (Get-Msg \"install.welcome_msg.timeout_inspect\")\n }\n } else {\n Write-Log (Get-Msg \"install.welcome_msg.poll_unavailable\")\n }\n } else {\n # Run container (legacy path)\n Write-Log (Get-Msg \"install.starting_manager\")\n & docker $dockerArgs\n\n Wait-ManagerReady -Container \"agentteams-manager\"\n Wait-MatrixReady -Container \"agentteams-manager\"\n }\n\n # Create OpenAI-compatible provider if needed\n if ($config.LLM_PROVIDER -eq \"openai-compat\") {\n New-OpenAICompatProvider -BaseUrl $config.OPENAI_BASE_URL -ApiKey $config.LLM_API_KEY -ConsolePort ([int]$config.PORT_CONSOLE)\n }\n\n # Print success message\n Write-Log \"\"\n Write-Log (Get-Msg \"success.title\")\n Write-Log \"\"\n Write-Log (Get-Msg \"success.domains_configured\")\n Write-Log \" $($config.MATRIX_DOMAIN.Split(':')[0]) $($config.MATRIX_CLIENT_DOMAIN) $($config.AI_GATEWAY_DOMAIN) $($config.FS_DOMAIN) $($config.CONSOLE_DOMAIN)\"\n Write-Log \"\"\n\n $lanIP = Get-LanIP\n\n Write-Host \"$($script:ESC)[33m===============================================================$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.open_url')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[1;36m http://127.0.0.1:$($config.PORT_ELEMENT_WEB)/#/login$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.login_with')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.username' -f $config.ADMIN_USER)$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.password' -f $config.ADMIN_PASSWORD)$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.after_login')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.tell_it')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.manager_auto')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m --------------------------------------------------------------- $($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.mobile_title')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $($script:ESC)[0m\"\n if ($lanIP) {\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.mobile_step1')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.mobile_step2' -f \"http://${lanIP}:$($config.PORT_GATEWAY)\")$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.mobile_step3')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.mobile_username' -f $config.ADMIN_USER)$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.mobile_password' -f $config.ADMIN_PASSWORD)$($script:ESC)[0m\"\n } else {\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.mobile_step1')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.mobile_step2_noip' -f $config.PORT_GATEWAY)$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.mobile_noip_hint')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.mobile_step3')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.mobile_username' -f $config.ADMIN_USER)$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.mobile_password' -f $config.ADMIN_PASSWORD)$($script:ESC)[0m\"\n }\n Write-Host \"$($script:ESC)[33m $($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m===============================================================$($script:ESC)[0m\"\n\n Write-Log \"\"\n Write-Log (Get-Msg \"success.other_consoles\")\n Write-Log (Get-Msg \"success.higress_console\" -f $config.PORT_CONSOLE, $config.ADMIN_USER, $config.ADMIN_PASSWORD)\n if ($script:AGENTTEAMS_USE_EMBEDDED -ne \"1\") {\n # In embedded mode the manager runs in its own auto-spawned container with\n # its own console-port mapping handled by the controller, so don't print a\n # host-side URL/credentials hint here.\n Write-Log (Get-Msg \"success.manager_console\" -f $config.PORT_MANAGER_CONSOLE)\n Write-Log (Get-Msg \"success.manager_console_gateway\" -f $config.ADMIN_USER, $config.ADMIN_PASSWORD)\n }\n Write-Log \"\"\n Write-Log (Get-Msg \"success.switch_llm.title\")\n Write-Log (Get-Msg \"success.switch_llm.hint\")\n Write-Log (Get-Msg \"success.switch_llm.docs\")\n Write-Log (Get-Msg \"success.switch_llm.url\")\n Write-Log \"\"\n Write-Log (Get-Msg \"success.tip\")\n Write-Log \"\"\n if ($config.LOCAL_ONLY -ne \"1\") {\n Write-Host (Get-Msg \"port.local_only.https_hint\") -ForegroundColor Yellow\n Write-Log \"\"\n }\n Write-Log (Get-Msg \"success.config_file\" -f $script:AGENTTEAMS_ENV_FILE)\n\n Write-Log (Get-Msg \"success.data_volume\" -f $config.DATA_DIR)\n\n Write-Log (Get-Msg \"success.workspace\" -f $config.WORKSPACE_DIR)\n}\n\n# ============================================================\n# Worker Installation\n# ============================================================\n\nfunction Install-Worker {\n param(\n [string]$Name,\n [string]$Fs,\n [string]$FsKey,\n [string]$FsSecret,\n [switch]$Reset,\n [switch]$FindSkills,\n [string]$SkillsApiUrl\n )\n\n # Validate required parameters\n if (-not $Name) {\n Write-Error (Get-Msg \"error.name_required\")\n }\n if (-not $Fs) {\n Write-Error (Get-Msg \"error.fs_required\")\n }\n if (-not $FsKey) {\n Write-Error (Get-Msg \"error.fs_key_required\")\n }\n if (-not $FsSecret) {\n Write-Error (Get-Msg \"error.fs_secret_required\")\n }\n\n $containerName = \"agentteams-worker-$Name\"\n\n # Handle reset\n if ($Reset) {\n Write-Log (Get-Msg \"worker.resetting\" -f $Name)\n docker stop $containerName *>$null\n docker rm $containerName *>$null\n }\n\n # Check for existing container\n $existing = docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^$containerName$\"\n if ($existing) {\n Write-Error (Get-Msg \"worker.exists\" -f $containerName)\n }\n\n # Detect timezone and registry\n $timezone = Get-AgentTeamsTimeZone\n $registry = Get-Registry -Timezone $timezone\n $workerImage = if ($env:AGENTTEAMS_INSTALL_WORKER_IMAGE) {\n $env:AGENTTEAMS_INSTALL_WORKER_IMAGE\n } else {\n \"$registry/agentteams/agentteams-worker:$($script:AGENTTEAMS_VERSION)\"\n }\n\n Write-Log (Get-Msg \"worker.starting\" -f $Name)\n\n $dockerArgs = @(\n \"run\", \"-d\",\n \"--name\", $containerName,\n \"-e\", \"HOME=/root/agentteams-fs/agents/$Name\",\n \"-w\", \"/root/agentteams-fs/agents/$Name\",\n \"-e\", \"AGENTTEAMS_WORKER_NAME=$Name\",\n \"-e\", \"AGENTTEAMS_FS_ENDPOINT=$Fs\",\n \"-e\", \"AGENTTEAMS_FS_ACCESS_KEY=$FsKey\",\n \"-e\", \"AGENTTEAMS_FS_SECRET_KEY=$FsSecret\"\n )\n\n if (-not $SkillsApiUrl) {\n if ($env:AGENTTEAMS_SKILLS_API_URL) {\n $SkillsApiUrl = $env:AGENTTEAMS_SKILLS_API_URL\n } else {\n $SkillsApiUrl = \"nacos://market.agentteams.io:80/public\"\n }\n }\n\n if ($FindSkills -and $SkillsApiUrl) {\n $dockerArgs += @(\"-e\", \"SKILLS_API_URL=$SkillsApiUrl\")\n Write-Log (Get-Msg \"worker.skills_url\" -f $SkillsApiUrl)\n }\n if ($env:AGENTTEAMS_NACOS_USERNAME) {\n $dockerArgs += @(\"-e\", \"AGENTTEAMS_NACOS_USERNAME=$($env:AGENTTEAMS_NACOS_USERNAME)\")\n }\n if ($env:AGENTTEAMS_NACOS_PASSWORD) {\n $dockerArgs += @(\"-e\", \"AGENTTEAMS_NACOS_PASSWORD=$($env:AGENTTEAMS_NACOS_PASSWORD)\")\n }\n if ($env:AGENTTEAMS_NACOS_TOKEN) {\n $dockerArgs += @(\"-e\", \"AGENTTEAMS_NACOS_TOKEN=$($env:AGENTTEAMS_NACOS_TOKEN)\")\n }\n\n $dockerArgs += @(\"--restart\", \"unless-stopped\", $workerImage)\n\n & docker $dockerArgs\n\n Write-Log \"\"\n Write-Log (Get-Msg \"worker.started\" -f $Name)\n Write-Log (Get-Msg \"worker.container\" -f $containerName)\n Write-Log (Get-Msg \"worker.view_logs\" -f $containerName)\n}\n\n# ============================================================\n# Uninstall\n# ============================================================\n\nfunction Uninstall-AgentTeams {\n Write-Log (Get-Msg \"uninstall.title\")\n\n # Stop and remove manager\n $manager = docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-manager$\"\n if ($manager) {\n Write-Log (Get-Msg \"uninstall.stopping_manager\")\n docker stop agentteams-manager *>$null\n docker rm agentteams-manager *>$null\n }\n\n # Stop and remove workers\n $workers = docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-worker-\"\n if ($workers) {\n Write-Log (Get-Msg \"uninstall.stopping_workers\")\n $workers | ForEach-Object {\n docker stop $_ *>$null\n docker rm $_ *>$null\n Write-Log (Get-Msg \"uninstall.removed\" -f $_)\n }\n }\n\n # Stop and remove docker-proxy (legacy <= v1.0.x; current arch uses\n # agentteams-controller for the same role)\n $proxy = docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-docker-proxy$\"\n if ($proxy) {\n Write-Log (Get-Msg \"uninstall.removing_proxy\")\n docker stop agentteams-docker-proxy *>$null\n docker rm agentteams-docker-proxy *>$null\n }\n\n # Stop and remove the embedded controller container. MUST happen\n # before the `docker volume rm agentteams-data` step below -- in embedded\n # mode agentteams-controller mounts agentteams-data at /data (Tuwunel DB,\n # MinIO state, Higress state, all the room messages), and `volume rm`\n # against an in-use volume fails silently. Skipping this used to\n # leave room/message history behind across \"uninstall + reinstall\"\n # cycles. See PR #692.\n $controller = docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-controller$\"\n if ($controller) {\n Write-Log (Get-Msg \"uninstall.stopping_controller\")\n docker stop agentteams-controller *>$null\n docker rm agentteams-controller *>$null\n }\n\n # Remove Docker volume (read custom name from env file if available)\n $dataVolume = \"agentteams-data\"\n if (Test-Path $script:AGENTTEAMS_ENV_FILE) {\n $envContent = Get-Content $script:AGENTTEAMS_ENV_FILE -ErrorAction SilentlyContinue\n $dataLine = $envContent | Select-String \"^AGENTTEAMS_DATA_DIR=\"\n if ($dataLine) {\n $parsed = ($dataLine -split \"=\", 2)[1]\n if ($parsed) { $dataVolume = $parsed }\n }\n }\n $volume = docker volume ls -q 2>$null | Select-String \"^${dataVolume}$\"\n if ($volume) {\n Write-Log (Get-Msg \"uninstall.removing_volume\")\n docker volume rm $dataVolume *>$null\n }\n\n # Remove Docker network\n $network = docker network ls --format \"{{.Name}}\" 2>$null | Select-String \"^agentteams-net$\"\n if ($network) {\n Write-Log (Get-Msg \"uninstall.removing_network\")\n docker network rm agentteams-net *>$null\n }\n\n # Remove workspace directory\n $workspaceDir = \"$env:USERPROFILE\\agentteams-manager\"\n if (Test-Path $script:AGENTTEAMS_ENV_FILE) {\n $envContent = Get-Content $script:AGENTTEAMS_ENV_FILE -ErrorAction SilentlyContinue\n $wsLine = $envContent | Select-String \"^AGENTTEAMS_WORKSPACE_DIR=\"\n if ($wsLine) {\n $workspaceDir = ($wsLine -split \"=\", 2)[1]\n }\n }\n if ($workspaceDir -and (Test-Path $workspaceDir)) {\n Write-Log (Get-Msg \"uninstall.removing_workspace\" -f $workspaceDir)\n Remove-Item -Recurse -Force $workspaceDir -ErrorAction SilentlyContinue\n }\n\n # Remove env file\n if (Test-Path $script:AGENTTEAMS_ENV_FILE) {\n Write-Log (Get-Msg \"uninstall.removing_env\" -f $script:AGENTTEAMS_ENV_FILE)\n Remove-Item -Force $script:AGENTTEAMS_ENV_FILE\n }\n\n # Remove install log (stop transcript first to release the file)\n if (Test-Path $script:AGENTTEAMS_LOG_FILE) {\n Write-Log (Get-Msg \"uninstall.removing_log\" -f $script:AGENTTEAMS_LOG_FILE)\n try { Stop-Transcript *>$null } catch {}\n Remove-Item -Force $script:AGENTTEAMS_LOG_FILE -ErrorAction SilentlyContinue\n }\n\n Write-Log \"\"\n Write-Log (Get-Msg \"uninstall.done\")\n}\n\n# ============================================================\n# Main Entry Point\n# ============================================================\n\ntry {\n switch ($Command) {\n \"manager\" {\n Install-Manager\n }\n \"worker\" {\n Install-Worker -Name $Name -Fs $Fs -FsKey $FsKey -FsSecret $FsSecret -Reset:$Reset -FindSkills:$FindSkills -SkillsApiUrl $SkillsApiUrl\n }\n \"uninstall\" {\n Uninstall-AgentTeams\n }\n }\n} catch {\n Exit-Script 1\n}\n\n# Stop transcript logging\ntry {\n Stop-Transcript -ErrorAction SilentlyContinue | Out-Null\n} catch {\n # Ignore errors when stopping transcript\n}\n"}, "687b6d94ba04e894f5982150293c188158189d3e": {"t": 1785764741.9854271, "n": 194495, "h": "bf0928e566fbd9b6", "c": "#!/usr/bin/env powershell\n# agentteams-install.ps1 - One-click installation for AgentTeams Manager and Worker on Windows\n# Requires PowerShell 7.0+ (recommended)\n#\n# Usage:\n# .\\agentteams-install.ps1 # Interactive installation (choose Quick Start or Manual)\n# .\\agentteams-install.ps1 manager # Same as above (explicit)\n# .\\agentteams-install.ps1 worker --name <name> ... # Worker installation\n#\n# Onboarding Modes:\n# Quick Start - Fast installation with all default values (recommended)\n# Manual - Customize each option step by step\n#\n# Environment variables (for automation):\n# AGENTTEAMS_NON_INTERACTIVE Skip all prompts, use defaults (default: 0)\n# AGENTTEAMS_LLM_PROVIDER LLM provider (default: openai-compat for zh non-interactive Token Plan; qwen for en)\n# AGENTTEAMS_DEFAULT_MODEL Default model (default: qwen3.6-plus for zh Token Plan and en non-interactive)\n# AGENTTEAMS_OPENAI_BASE_URL OpenAI-compatible base URL (default for zh non-interactive: Alibaba Token Plan endpoint)\n# AGENTTEAMS_LLM_API_KEY LLM API key (required)\n# AGENTTEAMS_ADMIN_USER Admin username (default: admin)\n# AGENTTEAMS_ADMIN_PASSWORD Admin password (auto-generated if not set, min 8 chars)\n# AGENTTEAMS_MATRIX_DOMAIN Matrix domain (default: matrix-local.agentteams.io:18080)\n# AGENTTEAMS_MOUNT_SOCKET Mount container runtime socket (default: 1)\n# AGENTTEAMS_MATRIX_E2EE Matrix E2EE (default: 0, disabled)\n# AGENTTEAMS_DATA_DIR Docker volume name for persistent data (default: agentteams-data)\n# AGENTTEAMS_WORKSPACE_DIR Host directory for manager workspace (default: ~/agentteams-manager)\n# AGENTTEAMS_VERSION Image tag (default: latest)\n# AGENTTEAMS_REGISTRY Image registry (default: auto-detected by timezone)\n# AGENTTEAMS_INSTALL_MANAGER_IMAGE Override manager image (e.g., local build)\n# AGENTTEAMS_INSTALL_WORKER_IMAGE Override worker image (e.g., local build)\n# AGENTTEAMS_INSTALL_COPAW_WORKER_IMAGE Override copaw worker image (e.g., local build)\n# AGENTTEAMS_INSTALL_HERMES_WORKER_IMAGE Override hermes worker image (e.g., local build)\n# AGENTTEAMS_PORT_GATEWAY Host port for Higress gateway (default: 18080)\n# AGENTTEAMS_PORT_CONSOLE Host port for Higress console (default: 18001)\n# AGENTTEAMS_PORT_ELEMENT_WEB Host port for Element Web direct access (default: 18088)\n\n#Requires -Version 5.1\n\n[CmdletBinding()]\nparam(\n [Parameter(Position = 0)]\n [ValidateSet(\"manager\", \"worker\", \"uninstall\")]\n [string]$Command = \"manager\",\n\n # Worker options\n [string]$Name,\n [string]$Fs,\n [string]$FsKey,\n [string]$FsSecret,\n [switch]$Reset,\n [switch]$FindSkills,\n [string]$SkillsApiUrl,\n\n # General options\n [switch]$NonInteractive,\n [string]$EnvFile\n)\n\n# ============================================================\n# Configuration\n# ============================================================\n\n$script:AGENTTEAMS_VERSION = if ($env:AGENTTEAMS_VERSION) { $env:AGENTTEAMS_VERSION } else { \"latest\" }\n$script:AGENTTEAMS_NON_INTERACTIVE = if ($env:AGENTTEAMS_NON_INTERACTIVE -eq \"1\" -or $NonInteractive) { $true } else { $false }\n$script:AGENTTEAMS_MOUNT_SOCKET = if ($env:AGENTTEAMS_MOUNT_SOCKET -eq \"0\") { $false } else { $true }\n$script:AGENTTEAMS_ENV_FILE = if ($EnvFile) { $EnvFile } elseif ($env:AGENTTEAMS_ENV_FILE) { $env:AGENTTEAMS_ENV_FILE } else { \"$env:USERPROFILE\\agentteams-manager.env\" }\n$script:StepResult = \"\" # Used by state machine to signal \"back\" navigation\n$script:config = @{} # Shared config hashtable for step functions\n\n# ANSI escape character for terminal colors\n$script:ESC = [char]0x1B\n\n# ============================================================\n# Log all output to file\n# ============================================================\n\n$script:AGENTTEAMS_LOG_FILE = \"$env:USERPROFILE\\agentteams-install.log\"\n\n# Start transcript for logging (PowerShell's built-in logging mechanism)\ntry {\n Start-Transcript -Path $script:AGENTTEAMS_LOG_FILE -Append -ErrorAction SilentlyContinue | Out-Null\n} catch {\n # If transcript fails, continue without logging\n}\n\nWrite-Host \"\"\nWrite-Host \"========================================\"\nWrite-Host \"AgentTeams Installation Log\"\nWrite-Host \"Started: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')\"\nWrite-Host \"User: $env:USERNAME\"\nWrite-Host \"System: $(hostname)\"\nWrite-Host \"Log file: $($script:AGENTTEAMS_LOG_FILE)\"\nWrite-Host \"========================================\"\nWrite-Host \"\"\n\n# ============================================================\n# Utility Functions\n# ============================================================\n\nfunction Write-Log {\n param([string]$Message)\n Write-Host \"$($script:ESC)[36m[AgentTeams]$($script:ESC)[0m $Message\"\n}\n\nfunction Write-Error {\n param([string]$Message)\n Write-Host \"$($script:ESC)[31m[AgentTeams ERROR]$($script:ESC)[0m $Message\" -ForegroundColor Red\n throw $Message\n}\n\nfunction Write-Warning {\n param([string]$Message)\n Write-Host \"$($script:ESC)[33m[AgentTeams WARNING]$($script:ESC)[0m $Message\"\n}\n\n# Pause before exit on error so user can read the message when running via double-click\nfunction Exit-Script {\n param([int]$ExitCode = 0)\n if ($ExitCode -ne 0 -and -not $script:AGENTTEAMS_NON_INTERACTIVE) {\n Write-Host \"\"\n Write-Host \"Press Enter to exit.\" -NoNewline\n $Host.UI.ReadLine()\n }\n exit $ExitCode\n}\n\nfunction Test-DockerRunning {\n try {\n $null = docker info 2>&1\n if ($LASTEXITCODE -ne 0) {\n return $false\n }\n return $true\n }\n catch {\n return $false\n }\n}\n\nfunction Get-AgentTeamsTimeZone {\n try {\n if ($env:AGENTTEAMS_TIMEZONE) {\n return $env:AGENTTEAMS_TIMEZONE\n }\n\n $tz = (Get-TimeZone).Id\n # Convert Windows timezone to IANA format\n $tzMap = @{\n \"China Standard Time\" = \"Asia/Shanghai\"\n \"Pacific Standard Time\" = \"America/Los_Angeles\"\n \"Mountain Standard Time\" = \"America/Denver\"\n \"Central Standard Time\" = \"America/Chicago\"\n \"Eastern Standard Time\" = \"America/New_York\"\n \"GMT Standard Time\" = \"Europe/London\"\n \"Central European Standard Time\" = \"Europe/Berlin\"\n \"Tokyo Standard Time\" = \"Asia/Tokyo\"\n \"Singapore Standard Time\" = \"Asia/Singapore\"\n \"Korea Standard Time\" = \"Asia/Seoul\"\n \"India Standard Time\" = \"Asia/Kolkata\"\n }\n\n if ($tzMap.ContainsKey($tz)) {\n return $tzMap[$tz]\n }\n return $tz\n }\n catch {\n return \"Asia/Shanghai\"\n }\n}\n\nfunction Get-Registry {\n param([string]$Timezone)\n\n if ($env:AGENTTEAMS_REGISTRY) {\n return $env:AGENTTEAMS_REGISTRY\n }\n\n # Americas\n if ($Timezone -match \"^America/\") {\n return \"higress-registry.us-west-1.cr.aliyuncs.com\"\n }\n\n # Southeast Asia\n if ($Timezone -match \"^(Asia/Singapore|Asia/Bangkok|Asia/Jakarta|Asia/Makassar|Asia/Jayapura|Asia/Kuala_Lumpur|Asia/Ho_Chi_Minh|Asia/Manila|Asia/Yangon|Asia/Vientiane|Asia/Phnom_Penh|Asia/Pontianak|Asia/Ujung_Pandang)\") {\n return \"higress-registry.ap-southeast-7.cr.aliyuncs.com\"\n }\n\n # Default: China\n return \"higress-registry.cn-hangzhou.cr.aliyuncs.com\"\n}\n\nfunction Get-AgentTeamsLanguage {\n param([string]$Timezone)\n $chineseZones = @(\n \"Asia/Shanghai\", \"Asia/Chongqing\", \"Asia/Harbin\", \"Asia/Urumqi\",\n \"Asia/Taipei\", \"Asia/Hong_Kong\", \"Asia/Macau\"\n )\n if ($chineseZones -contains $Timezone) { return \"zh\" }\n return \"en\"\n}\n\n# ============================================================\n# Centralized message dictionary and Get-Msg function\n# ============================================================\n\n$script:Messages = @{\n # --- Timezone detection messages ---\n \"tz.warning.title\" = @{ zh = \"无法自动检测时区。\"; en = \"Could not detect timezone automatically.\" }\n \"tz.warning.prompt\" = @{ zh = \"请输入您的时区(例如 Asia/Shanghai、America/New_York)。\"; en = \"Please enter your timezone (e.g., Asia/Shanghai, America/New_York).\" }\n \"tz.default\" = @{ zh = \"使用默认时区: {0}\"; en = \"Using default timezone: {0}\" }\n \"tz.input_prompt\" = @{ zh = \"时区\"; en = \"Timezone\" }\n\n # --- Installation title and info ---\n \"install.title\" = @{ zh = \"=== AgentTeams Manager 安装 ===\"; en = \"=== AgentTeams Manager Installation ===\" }\n \"install.registry\" = @{ zh = \"镜像仓库: {0}\"; en = \"Registry: {0}\" }\n \"install.dir\" = @{ zh = \"安装目录: {0}\"; en = \"Installation directory: {0}\" }\n \"install.dir_hint\" = @{ zh = \" (env 文件 'agentteams-manager.env' 将保存到 HOME 目录。)\"; en = \" (The env file 'agentteams-manager.env' will be saved to your HOME directory.)\" }\n \"install.dir_hint2\" = @{ zh = \" (请从您希望管理此安装的目录运行此脚本。)\"; en = \" (Run this script from the directory where you want to manage this installation.)\" }\n\n # --- Onboarding mode ---\n \"install.mode.title\" = @{ zh = \"--- Onboarding 模式 ---\"; en = \"--- Onboarding Mode ---\" }\n \"install.mode.choose\" = @{ zh = \"选择安装模式:\"; en = \"Choose your installation mode:\" }\n \"install.mode.quickstart\" = @{ zh = \" 1) 快速开始 - 使用阿里云通义 Token 套餐快速安装(推荐)\"; en = \" 1) Quick Start - Fast installation with Qwen Cloud (recommended)\" }\n \"install.mode.manual\" = @{ zh = \" 2) 手动配置 - 选择 LLM 提供商并自定义选项\"; en = \" 2) Manual - Choose LLM provider and customize options\" }\n \"install.mode.prompt\" = @{ zh = \"请选择 [1/2]\"; en = \"Enter choice [1/2]\" }\n \"install.mode.quickstart_selected\" = @{ zh = \"已选择快速开始模式 - 使用阿里云通义 Token 套餐\"; en = \"Quick Start mode selected - using Qwen Cloud\" }\n \"install.mode.manual_selected\" = @{ zh = \"已选择手动配置模式 - 您将选择 LLM 提供商并自定义选项\"; en = \"Manual mode selected - you will choose LLM provider and customize options\" }\n \"install.mode.invalid\" = @{ zh = \"无效选择,默认使用快速开始模式\"; en = \"Invalid choice, defaulting to Quick Start mode\" }\n\n # --- Existing installation detected ---\n \"install.existing.detected\" = @{ zh = \"检测到已有 Manager 安装(env 文件: {0})\"; en = \"Existing Manager installation detected (env file: {0})\" }\n \"install.existing.choose\" = @{ zh = \"选择操作:\"; en = \"Choose an action:\" }\n \"install.existing.upgrade\" = @{ zh = \" 1) 就地升级(保留数据、工作空间、env 文件)\"; en = \" 1) In-place upgrade (keep data, workspace, env file)\" }\n \"install.existing.reinstall\" = @{ zh = \" 2) 全新重装(删除所有数据,重新开始)\"; en = \" 2) Clean reinstall (remove all data, start fresh)\" }\n \"install.existing.cancel\" = @{ zh = \" 3) 取消\"; en = \" 3) Cancel\" }\n \"install.existing.prompt\" = @{ zh = \"请选择 [1/2/3]\"; en = \"Enter choice [1/2/3]\" }\n \"install.existing.upgrade_noninteractive\" = @{ zh = \"非交互模式: 执行就地升级...\"; en = \"Non-interactive mode: performing in-place upgrade...\" }\n \"install.existing.upgrading\" = @{ zh = \"执行就地升级...\"; en = \"Performing in-place upgrade...\" }\n \"install.existing.warn_manager_stop\" = @{ zh = \"警告: Manager 容器将被停止并重新创建。\"; en = \"WARNING: Manager container will be stopped and recreated.\" }\n \"install.existing.warn_worker_recreate\" = @{ zh = \"警告: Worker 容器也将被重新创建(以更新 Manager IP)。\"; en = \"WARNING: Worker containers will also be recreated (to update Manager IP in hosts).\" }\n \"install.existing.continue_prompt\" = @{ zh = \"继续?[y/N]\"; en = \"Continue? [y/N]\" }\n \"install.existing.cancelled\" = @{ zh = \"安装已取消。\"; en = \"Installation cancelled.\" }\n \"install.existing.stopping_manager\" = @{ zh = \"停止并移除现有 manager 容器...\"; en = \"Stopping and removing existing manager container...\" }\n \"install.existing.stopping_workers\" = @{ zh = \"停止并移除现有 worker 容器...\"; en = \"Stopping and removing existing worker containers...\" }\n \"install.existing.removed\" = @{ zh = \" 已移除: {0}\"; en = \" Removed: {0}\" }\n\n # --- Upgrade mode sub-menu ---\n \"upgrade.mode.prompt\" = @{ zh = \"请选择升级方式:\"; en = \"Select upgrade mode:\" }\n \"upgrade.mode.keep_all\" = @{ zh = \" 1) 保留所有参数,一键升级(推荐)\"; en = \" 1) Keep all parameters, quick upgrade (recommended)\" }\n \"upgrade.mode.confirm_each\" = @{ zh = \" 2) 逐个确认参数(可修改)\"; en = \" 2) Confirm each parameter (can modify)\" }\n \"upgrade.mode.back\" = @{ zh = \" 3) 返回\"; en = \" 3) Back\" }\n\n # --- Clean reinstall messages ---\n \"install.reinstall.performing\" = @{ zh = \"执行全新重装...\"; en = \"Performing clean reinstall...\" }\n \"install.reinstall.warn_stop\" = @{ zh = \"警告: 以下运行中的容器将被停止:\"; en = \"WARNING: The following running containers will be stopped:\" }\n \"install.reinstall.warn_delete\" = @{ zh = \"警告: 以下内容将被删除:\"; en = \"WARNING: This will DELETE the following:\" }\n \"install.reinstall.warn_volume\" = @{ zh = \" - Docker 卷: agentteams-data\"; en = \" - Docker volume: agentteams-data\" }\n \"install.reinstall.warn_env\" = @{ zh = \" - Env 文件: {0}\"; en = \" - Env file: {0}\" }\n \"install.reinstall.warn_workspace\" = @{ zh = \" - Manager 工作空间: {0}\"; en = \" - Manager workspace: {0}\" }\n \"install.reinstall.warn_workers\" = @{ zh = \" - 所有 worker 容器\"; en = \" - All worker containers\" }\n \"install.reinstall.warn_proxy\" = @{ zh = \" - Docker API 代理容器: agentteams-controller\"; en = \" - Docker API proxy container: agentteams-controller\" }\n \"install.reinstall.removing_proxy\" = @{ zh = \"正在移除 Docker API 代理容器: agentteams-controller\"; en = \"Removing Docker API proxy container: agentteams-controller\" }\n \"install.reinstall.warn_network\" = @{ zh = \" - Docker 网络: agentteams-net\"; en = \" - Docker network: agentteams-net\" }\n \"install.reinstall.removing_network\" = @{ zh = \"正在移除 Docker 网络: agentteams-net\"; en = \"Removing Docker network: agentteams-net\" }\n \"install.reinstall.confirm_type\" = @{ zh = \"请输入工作空间路径以确认删除(或按 Ctrl+C 取消):\"; en = \"To confirm deletion, please type the workspace path:\" }\n \"install.reinstall.confirm_path\" = @{ zh = \"输入路径以确认(或按 Ctrl+C 取消)\"; en = \"Type the path to confirm (or press Ctrl+C to cancel)\" }\n \"install.reinstall.path_mismatch\" = @{ zh = \"路径不匹配。中止重装。输入: '{0}',期望: '{1}'\"; en = \"Path mismatch. Aborting reinstall. Input: '{0}', Expected: '{1}'\" }\n \"install.reinstall.confirmed\" = @{ zh = \"已确认。正在清理...\"; en = \"Confirmed. Cleaning up...\" }\n \"install.reinstall.removed_worker\" = @{ zh = \" 已移除 worker: {0}\"; en = \" Removed worker: {0}\" }\n \"install.reinstall.removing_volume\" = @{ zh = \"正在移除 Docker 卷: agentteams-data\"; en = \"Removing Docker volume: agentteams-data\" }\n \"install.reinstall.warn_volume_fail\" = @{ zh = \" 警告: 无法移除卷(可能有引用)\"; en = \" Warning: Could not remove volume (may have references)\" }\n \"install.reinstall.removing_workspace\" = @{ zh = \"正在移除工作空间目录: {0}\"; en = \"Removing workspace directory: {0}\" }\n \"install.reinstall.removing_env\" = @{ zh = \"正在移除 env 文件: {0}\"; en = \"Removing env file: {0}\" }\n \"install.reinstall.cleanup_done\" = @{ zh = \"清理完成。开始全新安装...\"; en = \"Cleanup complete. Starting fresh installation...\" }\n \"install.reinstall.failed_rm_workspace\" = @{ zh = \"无法移除工作空间目录\"; en = \"Failed to remove workspace directory\" }\n\n # --- Orphan volume detection ---\n \"install.orphan_volume.detected\" = @{ zh = \"⚠️ 检测到残留数据卷 '{0}',但未找到对应的 env 配置文件。\"; en = \"⚠️ Found leftover data volume '{0}' but no matching env config file.\" }\n \"install.orphan_volume.warn\" = @{ zh = \"这可能是之前安装的残留数据,会导致新安装出现异常(如密码冲突、服务启动失败)。\"; en = \"This is likely leftover data from a previous installation and may cause issues (e.g., credential conflicts, service startup failures).\" }\n \"install.orphan_volume.choose\" = @{ zh = \"选择操作:\"; en = \"Choose an action:\" }\n \"install.orphan_volume.clean\" = @{ zh = \" 1) 清理残留数据卷后继续安装(推荐)\"; en = \" 1) Remove leftover volume and continue installation (recommended)\" }\n \"install.orphan_volume.keep\" = @{ zh = \" 2) 保留数据卷继续安装(可能出现异常)\"; en = \" 2) Keep the volume and continue installation (may cause issues)\" }\n \"install.orphan_volume.prompt\" = @{ zh = \"请选择 [1/2]\"; en = \"Enter choice [1/2]\" }\n \"install.orphan_volume.cleaning\" = @{ zh = \"正在清理残留数据卷...\"; en = \"Removing leftover data volume...\" }\n \"install.orphan_volume.cleaned\" = @{ zh = \"残留数据卷已清理。继续全新安装...\"; en = \"Leftover volume removed. Continuing with fresh installation...\" }\n \"install.orphan_volume.keeping\" = @{ zh = \"保留数据卷,继续安装。如遇异常请选择全新重装。\"; en = \"Keeping existing volume. If you encounter issues, consider a clean reinstall.\" }\n \"install.orphan_volume.clean_noninteractive\" = @{ zh = \"非交互模式: 自动清理残留数据卷...\"; en = \"Non-interactive mode: automatically removing leftover volume...\" }\n\n # --- Loading existing config ---\n \"install.loading_config\" = @{ zh = \"从 {0} 加载已有配置(shell 环境变量优先)...\"; en = \"Loading existing config from {0} (shell env vars take priority)...\" }\n\n # --- LLM Configuration ---\n \"llm.title\" = @{ zh = \"--- LLM 配置 ---\"; en = \"--- LLM Configuration ---\" }\n \"llm.provider.label\" = @{ zh = \" 提供商: {0}\"; en = \" Provider: {0}\" }\n \"llm.model.label\" = @{ zh = \" 模型: {0}\"; en = \" Model: {0}\" }\n \"llm.provider.qwen\" = @{ zh = \" 提供商: qwen(阿里云百炼)\"; en = \" Provider: qwen (Alibaba Cloud Bailian)\" }\n \"llm.provider.qwen_default\" = @{ zh = \" 提供商: {0}(默认)\"; en = \" Provider: {0} (default)\" }\n \"llm.model.default\" = @{ zh = \" 模型: {0}(默认)\"; en = \" Model: {0} (default)\" }\n \"llm.apikey_hint_bailian\" = @{ zh = \" 提示: 获取阿里云百炼(DashScope)API Key:\"; en = \" Hint: Get your Alibaba Cloud Bailian (DashScope) API Key:\" }\n \"llm.apikey_url_bailian\" = @{ zh = \" https://www.aliyun.com/product/bailian\"; en = \" https://www.aliyun.com/product/bailian\" }\n \"llm.apikey_hint_qwencloud\" = @{ zh = \" 提示: 从 Qwen Cloud(国际站)获取 DASHSCOPE_API_KEY:\"; en = \" Hint: Get your DASHSCOPE_API_KEY for Qwen Cloud (international) from:\" }\n \"llm.apikey_url_qwencloud\" = @{ zh = \" https://home.qwencloud.com/api-keys (文档: https://docs.qwencloud.com/)\"; en = \" https://home.qwencloud.com/api-keys | Docs: https://docs.qwencloud.com/\" }\n \"llm.apikey_hint_tokenplan\" = @{ zh = \" 提示: 获取 DashScope API Key 或开通通义 Token 套餐,请参考:\"; en = \" Hint: Get your DashScope or Token Plan API key (Alibaba Model Studio):\" }\n \"llm.apikey_url_tokenplan\" = @{ zh = \" https://help.aliyun.com/zh/model-studio/token-plan-quickstart\"; en = \" https://common-buy.aliyun.com/token-plan/ | https://help.aliyun.com/zh/model-studio/token-plan-quickstart\" }\n \"llm.apikey_hint_codingplan\" = @{ zh = \" 提示: 获取 DashScope API Key(Coding 套餐 / coding.dashscope 接口):\"; en = \" Hint: Get your DashScope API key for Coding Plan (coding.dashscope endpoint):\" }\n \"llm.apikey_url_codingplan\" = @{ zh = \" https://help.aliyun.com/zh/model-studio/get-api-key\"; en = \" https://help.aliyun.com/zh/model-studio/get-api-key\" }\n \"llm.apikey_prompt\" = @{ zh = \"LLM API Key\"; en = \"LLM API Key\" }\n \"llm.providers_title\" = @{ zh = \"可用 LLM 提供商:\"; en = \"Available LLM Providers:\" }\n \"llm.provider.alibaba\" = @{ zh = \" 1) 阿里云通义 Token 套餐 - 推荐中国用户使用\"; en = \" 1) Qwen Cloud - International (OpenAI-compatible API, recommended)\" }\n \"llm.provider.openai_compat\" = @{ zh = \" 2) OpenAI 兼容 API - 自定义 Base URL(OpenAI、DeepSeek 等)\"; en = \" 2) OpenAI-compatible API - Custom Base URL (OpenAI, DeepSeek, etc.)\" }\n \"llm.provider.select\" = @{ zh = \"选择提供商 [1/2]\"; en = \"Select provider [1/2]\" }\n \"llm.alibaba.models_title\" = @{ zh = \"选择阿里云模型接入方式:\"; en = \"Select Alibaba Cloud model access:\" }\n \"llm.alibaba.model.tokenplan\" = @{ zh = \" 1) 阿里云通义 Token 套餐 - 兼容模式(推荐)\"; en = \" 1) Alibaba Cloud Token Plan - compatible-mode (recommended)\" }\n \"llm.alibaba.model.bailian\" = @{ zh = \" 2) 阿里云百炼 - DashScope 通用兼容接口\"; en = \" 2) Alibaba Cloud Bailian - DashScope compatible mode\" }\n \"llm.alibaba.model.codingplan_legacy\" = @{ zh = \" 3) 阿里云 Coding 套餐 - 旧版端点(兼容保留)\"; en = \" 3) Alibaba Cloud Coding Plan - legacy endpoint (backward compatible)\" }\n \"llm.alibaba.model.select\" = @{ zh = \"选择接入方式 [1/2/3]\"; en = \"Select access option [1/2/3]\" }\n \"llm.alibaba.model.invalid\" = @{ zh = \"无效选择: {0}(请输入 1、2 或 3)\"; en = \"Invalid choice: {0} (please enter 1, 2, or 3)\" }\n \"llm.codingplan.models_title\" = @{ zh = \"选择通义 Token 套餐默认模型:\"; en = \"Select Qwen Cloud default model:\" }\n \"llm.codingplan.model.qwen36plus\" = @{ zh = \" 1) qwen3.6-plus - 千问 3.6(推荐)\"; en = \" 1) qwen3.6-plus - Qwen 3.6 (recommended)\" }\n \"llm.codingplan.model.glm5\" = @{ zh = \" 2) glm-5 - 智谱 GLM-5(编程推荐)\"; en = \" 2) glm-5 - Zhipu GLM-5 (recommended for coding)\" }\n \"llm.codingplan.model.kimi\" = @{ zh = \" 3) kimi-k2.5 - Moonshot Kimi K2.5\"; en = \" 3) kimi-k2.5 - Moonshot Kimi K2.5\" }\n \"llm.codingplan.model.minimax\" = @{ zh = \" 4) MiniMax-M2.5 - MiniMax M2.5\"; en = \" 4) MiniMax-M2.5 - MiniMax M2.5\" }\n \"llm.codingplan.model.select\" = @{ zh = \"选择模型 [1/2/3/4]\"; en = \"Select model [1/2/3/4]\" }\n \"llm.provider.selected_tokenplan\" = @{ zh = \" 提供商: 阿里云通义 Token 套餐(兼容模式)\"; en = \" Provider: Alibaba Cloud Token Plan (compatible mode)\" }\n \"llm.provider.selected_codingplan\" = @{ zh = \" 提供商: 阿里云通义 Token 套餐(alibaba-cloud)\"; en = \" Provider: Qwen Cloud (international) (alibaba-cloud)\" }\n \"llm.provider.selected_codingplan_legacy\" = @{ zh = \" 提供商: 阿里云 Coding 套餐(coding.dashscope)\"; en = \" Provider: Alibaba Cloud Coding Plan (coding.dashscope)\" }\n \"llm.provider.selected_qwen\" = @{ zh = \" 提供商: 阿里云百炼\"; en = \" Provider: Alibaba Cloud Bailian\" }\n \"llm.provider.selected_openai\" = @{ zh = \" 提供商: {0}(OpenAI 兼容)\"; en = \" Provider: {0} (OpenAI-compatible)\" }\n \"llm.provider.invalid\" = @{ zh = \"无效选择: {0}(请输入 1 或 2)\"; en = \"Invalid choice: {0} (please enter 1 or 2)\" }\n \"llm.qwen.model_prompt\" = @{ zh = \"默认模型 ID\"; en = \"Default Model ID\" }\n \"llm.openai.base_url_prompt\" = @{ zh = \"Base URL\"; en = \"Base URL\" }\n \"llm.openai.model_prompt\" = @{ zh = \"默认模型 ID\"; en = \"Default Model ID\" }\n \"llm.openai.base_url_label\" = @{ zh = \" Base URL: {0}\"; en = \" Base URL: {0}\" }\n\n # --- Custom model parameters ---\n \"llm.custom_model.detected\" = @{ zh = \" ⚠️ 模型 '{0}' 不在内置模型列表中,请配置模型参数:\"; en = \" ⚠️ Model '{0}' is not in the built-in model list. Please configure model parameters:\" }\n \"llm.custom_model.context_prompt\" = @{ zh = \"最大上下文长度(token 数)[150000]\"; en = \"Max context window (tokens) [150000]\" }\n \"llm.custom_model.max_tokens_prompt\" = @{ zh = \"最大输出长度(token 数)[128000]\"; en = \"Max output tokens [128000]\" }\n \"llm.custom_model.reasoning_prompt\" = @{ zh = \"是否支持推理/思考模式?[Y/n]\"; en = \"Does it support reasoning/thinking mode? [Y/n]\" }\n \"llm.custom_model.vision_prompt\" = @{ zh = \"是否支持图片输入?[y/N]\"; en = \"Does it support image input? [y/N]\" }\n \"llm.custom_model.summary\" = @{ zh = \" 自定义模型参数: 上下文={0}, 最大输出={1}, 推理={2}, 图片={3}\"; en = \" Custom model params: context={0}, maxTokens={1}, reasoning={2}, vision={3}\" }\n\n # --- Admin Credentials ---\n \"admin.title\" = @{ zh = \"--- 管理员凭据 ---\"; en = \"--- Admin Credentials ---\" }\n \"admin.username_prompt\" = @{ zh = \"管理员用户名\"; en = \"Admin Username\" }\n \"admin.password_prompt\" = @{ zh = \"管理员密码(留空自动生成,最少 8 位)\"; en = \"Admin Password (leave empty to auto-generate, min 8 chars)\" }\n \"admin.password_generated\" = @{ zh = \" 已自动生成管理员密码\"; en = \" Auto-generated admin password\" }\n \"admin.password_too_short\" = @{ zh = \"管理员密码至少需要 8 个字符(MinIO 要求)。当前长度: {0}\"; en = \"Admin password must be at least 8 characters (MinIO requirement). Current length: {0}\" }\n\n # --- Port Configuration ---\n \"port.title\" = @{ zh = \"--- 端口配置(按回车使用默认值)---\"; en = \"--- Port Configuration (press Enter for defaults) ---\" }\n \"port.gateway_prompt\" = @{ zh = \"网关主机端口(容器内 8080)\"; en = \"Host port for gateway (8080 inside container)\" }\n \"port.console_prompt\" = @{ zh = \"Higress 控制台主机端口(容器内 8001)\"; en = \"Host port for Higress console (8001 inside container)\" }\n \"port.element_prompt\" = @{ zh = \"Element Web 直接访问主机端口(容器内 8088)\"; en = \"Host port for Element Web direct access (8088 inside container)\" }\n \"port.manager_console_prompt\" = @{ zh = \"Manager 控制台主机端口(容器内 18888)\"; en = \"Host port for Manager console (18888 inside container)\" }\n \"port.local_only.title\" = @{ zh = \"--- 网络访问模式 ---\"; en = \"--- Network Access Mode ---\" }\n \"port.local_only.hint_yes\" = @{ zh = \" 仅本机使用,无需开放外部端口(推荐)\"; en = \" Local use only, no external port exposure (recommended)\" }\n \"port.local_only.hint_no\" = @{ zh = \" 允许外部访问(局域网 / 公网)\"; en = \" Allow external access (LAN / public network)\" }\n \"port.local_only.choice\" = @{ zh = \"请选择 [1/2]\"; en = \"Enter choice [1/2]\" }\n \"port.local_only.selected_local\" = @{ zh = \"端口已绑定到 127.0.0.1(仅本机访问)\"; en = \"Ports bound to 127.0.0.1 (localhost only)\" }\n \"port.local_only.selected_external\" = @{ zh = \"端口已绑定到所有网络接口(0.0.0.0)\"; en = \"Ports bound to all interfaces (0.0.0.0)\" }\n \"port.local_only.https_hint\" = @{ zh = \"警告: 建议在 Higress 控制台配置 TLS 证书并启用 HTTPS,避免明文传输。\"; en = \"WARNING: It is recommended to configure TLS certificates and enable HTTPS in the Higress Console to avoid plaintext transmission.\" }\n \"port.local_only.https_docs\" = @{ zh = \"\"; en = \"\" }\n\n # --- Domain Configuration ---\n \"domain.title\" = @{ zh = \"--- 域名配置(按回车使用默认值)---\"; en = \"--- Domain Configuration (press Enter for defaults) ---\" }\n \"domain.hint\" = @{ zh = \"提示: 自定义域名前必须事先做好 DNS 解析。单机 ECS 部署时无需修改 aigw、fs 等域名;Element Web 和 Matrix Server 也可通过 IP 直接访问。\"; en = \"Hint: Configure DNS resolution before customizing domains. For single ECS deployment, no need to change aigw, fs, etc.; Element Web and Matrix Server can also be accessed directly via IP.\" }\n \"domain.matrix_prompt\" = @{ zh = \"Matrix 域名\"; en = \"Matrix Domain\" }\n \"domain.element_prompt\" = @{ zh = \"Element Web 域名\"; en = \"Element Web Domain\" }\n \"domain.gateway_prompt\" = @{ zh = \"AI 网关域名\"; en = \"AI Gateway Domain\" }\n \"domain.fs_prompt\" = @{ zh = \"文件系统域名\"; en = \"File System Domain\" }\n \"domain.console_prompt\" = @{ zh = \"Manager 控制台域名\"; en = \"Manager Console Domain\" }\n\n # --- GitHub Integration ---\n \"github.title\" = @{ zh = \"--- GitHub 集成(可选,按回车跳过)---\"; en = \"--- GitHub Integration (optional, press Enter to skip) ---\" }\n \"github.token_prompt\" = @{ zh = \"GitHub 个人访问令牌(可选)\"; en = \"GitHub Personal Access Token (optional)\" }\n\n # --- Skills Registry ---\n \"skills.title\" = @{ zh = \"--- Skills 注册中心(可选,按回车使用默认 nacos://market.agentteams.io:80/public)---\"; en = \"--- Skills Registry (optional, press Enter for default nacos://market.agentteams.io:80/public) ---\" }\n \"skills.url_prompt\" = @{ zh = \"Skills 注册中心 URL(留空使用默认 nacos://market.agentteams.io:80/public)\"; en = \"Skills Registry URL (leave empty for default nacos://market.agentteams.io:80/public)\" }\n\n # --- Data Persistence ---\n \"data.title\" = @{ zh = \"--- 数据持久化 ---\"; en = \"--- Data Persistence ---\" }\n \"data.volume_prompt\" = @{ zh = \"Docker 卷名称 [agentteams-data]\"; en = \"Docker volume name for persistent data [agentteams-data]\" }\n \"data.volume_using\" = @{ zh = \" 使用 Docker 卷: {0}\"; en = \" Using Docker volume: {0}\" }\n\n # --- Manager Workspace ---\n \"workspace.title\" = @{ zh = \"--- Manager 工作空间 ---\"; en = \"--- Manager Workspace ---\" }\n \"workspace.dir_prompt\" = @{ zh = \"Manager 工作空间目录 [{0}]\"; en = \"Manager workspace directory [{0}]\" }\n \"workspace.dir_label\" = @{ zh = \" Manager 工作空间: {0}\"; en = \" Manager workspace: {0}\" }\n\n # --- Host directory sharing ---\n \"host_share.prompt\" = @{ zh = \"与 Agent 共享的主机目录(默认: {0})\"; en = \"Host directory to share with agents (default: {0})\" }\n \"host_share.sharing\" = @{ zh = \"共享主机目录: {0} -> 容器内 /host-share\"; en = \"Sharing host directory: {0} -> /host-share in container\" }\n \"host_share.not_exist\" = @{ zh = \"警告: 主机目录 {0} 不存在,跳过验证继续使用\"; en = \"WARNING: Host directory {0} does not exist, using without validation\" }\n\n # --- Default worker runtime ---\n \"worker_runtime.title\" = @{ zh = \"--- 默认 Worker 运行时 ---\"; en = \"--- Default Worker Runtime ---\" }\n \"worker_runtime.openclaw\" = @{ zh = \"OpenClaw\"; en = \"OpenClaw\" }\n \"worker_runtime.copaw\" = @{ zh = \"QwenPaw\"; en = \"QwenPaw\" }\n \"worker_runtime.hermes\" = @{ zh = \"Hermes\"; en = \"Hermes\" }\n \"worker_runtime.choice\" = @{ zh = \"请选择 [1/2/3]\"; en = \"Enter choice [1/2/3]\" }\n \"worker_runtime.selected\" = @{ zh = \"默认 Worker 运行时: {0}\"; en = \"Default Worker runtime: {0}\" }\n \"worker_runtime.title_short\" = @{ zh = \"默认 Worker 运行时\"; en = \"Default Worker Runtime\" }\n\n # --- Manager runtime ---\n \"manager_runtime.title\" = @{ zh = \"--- Manager 运行时 ---\"; en = \"--- Manager Runtime ---\" }\n \"manager_runtime.openclaw\" = @{ zh = \"OpenClaw\"; en = \"OpenClaw\" }\n \"manager_runtime.copaw\" = @{ zh = \"QwenPaw\"; en = \"QwenPaw\" }\n \"manager_runtime.choice\" = @{ zh = \"请选择 [1/2]\"; en = \"Enter choice [1/2]\" }\n \"manager_runtime.selected\" = @{ zh = \"Manager 运行时: {0}\"; en = \"Manager runtime: {0}\" }\n \"manager_runtime.title_short\" = @{ zh = \"Manager 运行时\"; en = \"Manager Runtime\" }\n\n # --- Matrix E2EE ---\n \"matrix_e2ee.title\" = @{ zh = \"--- Matrix 端到端加密(E2EE)---\"; en = \"--- Matrix End-to-End Encryption (E2EE) ---\" }\n \"matrix_e2ee.desc\" = @{\n zh = \"E2EE 会对 Manager 与 Worker 之间的 Matrix 消息进行端到端加密。`n 启用后,即使 Matrix 服务器被入侵,消息内容也无法被窃取。`n 但 E2EE 会增加首次握手耗时,且要求所有 Agent 都支持 matrix-sdk-crypto。`n 如果不确定,建议保持禁用。`n ⚠ 注意:禁用 E2EE 后,请勿在 Element 上创建默认启用加密的 Private 房间,`n 否则 Agent 将无法读取该房间中的加密消息。请改用 Public 房间或关闭房间加密。\"\n en = \"E2EE encrypts Matrix messages between Manager and Workers end-to-end.`n When enabled, message content stays private even if the Matrix server is compromised.`n However, E2EE adds overhead to the initial handshake and requires all Agents`n to support matrix-sdk-crypto. If unsure, keep it disabled.`n ⚠ Note: When E2EE is disabled, do NOT create Private rooms in Element (which`n enable encryption by default) — Agents cannot read encrypted messages without`n E2EE support. Use Public rooms or turn off room encryption instead.\"\n }\n \"matrix_e2ee.enable\" = @{ zh = \"启用 E2EE\"; en = \"Enable E2EE\" }\n \"matrix_e2ee.disable\" = @{ zh = \"禁用 E2EE(推荐)\"; en = \"Disable E2EE (recommended)\" }\n \"matrix_e2ee.choice\" = @{ zh = \"请选择 [1/2]\"; en = \"Enter choice [1/2]\" }\n \"matrix_e2ee.selected_enabled\" = @{ zh = \"Matrix E2EE: 已启用\"; en = \"Matrix E2EE: enabled\" }\n \"matrix_e2ee.selected_disabled\" = @{ zh = \"Matrix E2EE: 已禁用(默认)\"; en = \"Matrix E2EE: disabled (default)\" }\n \"matrix_e2ee.title_short\" = @{ zh = \"Matrix E2EE\"; en = \"Matrix E2EE\" }\n \"matrix_e2ee.val_enabled\" = @{ zh = \"已启用\"; en = \"enabled\" }\n \"matrix_e2ee.val_disabled\" = @{ zh = \"已禁用\"; en = \"disabled\" }\n\n # --- Docker API proxy ---\n \"docker_proxy.title\" = @{ zh = \"--- Docker API 安全代理 ---\"; en = \"--- Docker API Security Proxy ---\" }\n \"docker_proxy.desc\" = @{ zh = \"Docker API 代理可防止 AI Agent 通过 Docker API 越狱访问宿主机。`n 启用后,Manager 不再直接持有 Docker socket,所有容器操作经过安全校验。\"; en = \"Docker API proxy prevents AI Agents from escaping via Docker API to access the host.`n When enabled, Manager no longer has direct Docker socket access; all container operations go through security validation.\" }\n \"docker_proxy.enable\" = @{ zh = \"启用(推荐)\"; en = \"Enable (recommended)\" }\n \"docker_proxy.disable\" = @{ zh = \"禁用(直接挂载 Docker socket)\"; en = \"Disable (mount Docker socket directly)\" }\n \"docker_proxy.choice\" = @{ zh = \"请选择 [1/2]\"; en = \"Enter choice [1/2]\" }\n \"docker_proxy.selected_enabled\" = @{ zh = \"Docker API 代理: 已启用\"; en = \"Docker API proxy: enabled\" }\n \"docker_proxy.selected_disabled\" = @{ zh = \"Docker API 代理: 已禁用\"; en = \"Docker API proxy: disabled\" }\n \"docker_proxy.title_short\" = @{ zh = \"Docker API 代理\"; en = \"Docker API Proxy\" }\n \"docker_proxy.val_enabled\" = @{ zh = \"已启用\"; en = \"enabled\" }\n \"docker_proxy.val_disabled\" = @{ zh = \"已禁用\"; en = \"disabled\" }\n \"docker_proxy.registries_desc\" = @{ zh = \"默认放行的镜像来源:本地镜像、localhost、Higress 仓库(所有 region)。`n 如需放行其他镜像仓库,请输入逗号分隔的地址前缀。`n 示例: ghcr.io/myorg,registry.example.com/team\"; en = \"Default allowed image sources: local images, localhost, Higress registries (all regions).`n To allow additional image sources, enter comma-separated address prefixes.`n Example: ghcr.io/myorg,registry.example.com/team\" }\n \"docker_proxy.registries_prompt\" = @{ zh = \"额外放行的镜像来源(按回车跳过)\"; en = \"Additional allowed image sources (press Enter to skip)\" }\n \"docker_proxy.registries_label\" = @{ zh = \"额外放行的镜像来源\"; en = \"Additional allowed image sources\" }\n\n # --- Worker idle timeout ---\n \"idle_timeout.prompt\" = @{ zh = \"Worker 空闲自动停止超时(分钟)[720]\"; en = \"Worker idle auto-stop timeout in minutes [720]\" }\n \"idle_timeout.selected\" = @{ zh = \"Worker 空闲超时: {0} 分钟\"; en = \"Worker idle timeout: {0} minutes\" }\n \"idle_timeout.label\" = @{ zh = \"Worker 空闲超时(分钟)\"; en = \"Worker idle timeout (min)\" }\n\n # --- Secrets and config ---\n \"install.generating_secrets\" = @{ zh = \"正在生成密钥...\"; en = \"Generating secrets...\" }\n \"install.config_saved\" = @{ zh = \"配置已保存到 {0}\"; en = \"Configuration saved to {0}\" }\n\n # --- Container runtime socket ---\n \"install.socket_detected\" = @{ zh = \"容器运行时 socket: {0}(已启用直接创建 Worker)\"; en = \"Container runtime socket: {0} (direct Worker creation enabled)\" }\n \"install.socket_not_found\" = @{ zh = \"未找到容器运行时 socket(Manager 无法直接创建 Worker 容器,需要你手动执行 docker 命令创建)\"; en = \"No container runtime socket found (Manager cannot create Worker containers directly, you will need to create them manually using docker commands)\" }\n \"install.socket_confirm.title\" = @{ zh = \"警告: 未检测到容器运行时 Socket\"; en = \"WARNING: Container Runtime Socket Not Detected\" }\n \"install.socket_confirm.message\" = @{ zh = \"未找到 Docker/Podman socket,Manager 将无法自动创建 Worker 容器。`n你需要手动执行 docker run 命令来创建 Worker。`n`n是否继续安装?\"; en = \"Docker/Podman socket not found. Manager will not be able to create Worker containers automatically.`nYou will need to manually run docker commands to create Workers.`n`nContinue installation?\" }\n \"install.socket_confirm.prompt\" = @{ zh = \"继续安装? [y/N]: \"; en = \"Continue? [y/N]: \" }\n \"install.socket_confirm.cancelled\" = @{ zh = \"安装已取消。如需启用 Worker 自动创建,请确保 Docker/Podman 正在运行,然后重新运行安装脚本。\"; en = \"Installation cancelled. To enable automatic Worker creation, ensure Docker/Podman is running and re-run the installer.\" }\n\n # --- Container management ---\n \"install.removing_existing\" = @{ zh = \"正在移除现有 agentteams-manager 容器...\"; en = \"Removing existing agentteams-manager container...\" }\n\n # --- YOLO mode ---\n \"install.yolo\" = @{ zh = \"YOLO 模式已启用(自主决策,无交互提示)\"; en = \"YOLO mode enabled (autonomous decisions, no interactive prompts)\" }\n\n # --- Image pulling ---\n \"install.image.exists\" = @{ zh = \"Manager 镜像已存在: {0}\"; en = \"Manager image already exists locally: {0}\" }\n \"install.image.pulling_manager\" = @{ zh = \"正在拉取 Manager 镜像: {0}\"; en = \"Pulling Manager image: {0}\" }\n \"install.image.worker_exists\" = @{ zh = \"Worker 镜像已存在: {0}\"; en = \"Worker image already exists locally: {0}\" }\n \"install.image.pulling_worker\" = @{ zh = \"正在拉取 Worker 镜像: {0}\"; en = \"Pulling Worker image: {0}\" }\n\n # --- Starting container ---\n \"install.starting_manager\" = @{ zh = \"正在启动 Manager 容器...\"; en = \"Starting Manager container...\" }\n\n # --- Wait for Manager ready ---\n \"install.wait_ready\" = @{ zh = \"等待 Manager Agent 就绪(超时: {0}s)...\"; en = \"Waiting for Manager agent to be ready (timeout: {0}s)...\" }\n \"install.wait_ready.ok\" = @{ zh = \"Manager Agent 已就绪!\"; en = \"Manager agent is ready!\" }\n \"install.wait_ready.waiting\" = @{ zh = \"等待中... ({0}s/{1}s)\"; en = \"Waiting... ({0}s/{1}s)\" }\n \"install.wait_ready.timeout\" = @{ zh = \"Manager Agent 在 {0}s 内未就绪。请检查: docker logs {1}\"; en = \"Manager agent did not become ready within {0}s. Check: docker logs {1}\" }\n\n # --- Wait for Matrix ready ---\n \"install.wait_matrix\" = @{ zh = \"等待 Matrix 服务就绪(超时: {0}s)...\"; en = \"Waiting for Matrix server to be ready (timeout: {0}s)...\" }\n \"install.wait_matrix.ok\" = @{ zh = \"Matrix 服务已就绪!\"; en = \"Matrix server is ready!\" }\n \"install.wait_matrix.waiting\" = @{ zh = \"等待 Matrix 中... ({0}s/{1}s)\"; en = \"Waiting for Matrix... ({0}s/{1}s)\" }\n \"install.wait_matrix.timeout\" = @{ zh = \"Matrix 服务在 {0}s 内未就绪。请检查: docker logs {1}\"; en = \"Matrix server did not become ready within {0}s. Check: docker logs {1}\" }\n\n # --- OpenAI-compatible connectivity test ---\n \"llm.openai.test.testing\" = @{ zh = \"正在测试 API 联通性...\"; en = \"Testing API connectivity...\" }\n \"llm.openai.test.ok\" = @{ zh = \"API 联通性测试通过\"; en = \"API connectivity test passed\" }\n \"llm.openai.test.fail\" = @{ zh = \"API 联通性测试失败(HTTP {0})。响应内容:`n{1}`n请根据以上错误信息联系您的模型服务商解决。\"; en = \"API connectivity test failed (HTTP {0}). Response body:`n{1}`nPlease contact your model provider to resolve the issue.\" }\n \"llm.openai.test.fail.tokenplan\" = @{ zh = \"提示: 请确认 API Key 有效且已开通通义 Token 套餐。文档: https://help.aliyun.com/zh/model-studio/token-plan-quickstart\"; en = \"Hint: Verify your Token Plan API key and compatible-mode access. Docs: https://help.aliyun.com/zh/model-studio/token-plan-quickstart\" }\n \"llm.openai.test.fail.codingplan\" = @{ zh = \"提示: 请确认 API Key 有效且已开通通义 Token 套餐。文档: https://help.aliyun.com/zh/model-studio/token-plan-quickstart\"; en = \"Hint: Verify your DASHSCOPE_API_KEY for Qwen Cloud. API keys: https://home.qwencloud.com/api-keys Docs: https://docs.qwencloud.com/\" }\n \"llm.openai.test.fail.codingplan_legacy\" = @{ zh = \"提示: 请确认 API Key 有效且 Coding 套餐接口可用。文档: https://help.aliyun.com/zh/model-studio/get-api-key\"; en = \"Hint: Verify your DashScope API key and Coding Plan access. Docs: https://help.aliyun.com/zh/model-studio/get-api-key\" }\n \"llm.openai.test.confirm\" = @{ zh = \"是否仍要继续安装?[y/N/b]\"; en = \"Continue with installation anyway? [y/N/b]\" }\n \"llm.openai.test.aborted\" = @{ zh = \"安装已中止。\"; en = \"Installation aborted.\" }\n \"llm.embedding.title\" = @{ zh = \"📦 记忆搜索配置\"; en = \"📦 Memory Search Configuration\" }\n \"llm.embedding.hint\" = @{ zh = \" Embedding 模型可提升记忆搜索质量(语义匹配)。不启用也可正常使用记忆功能(关键词匹配)。\"; en = \" Embedding model improves memory search quality (semantic matching). Memory still works without it (keyword matching).\" }\n \"llm.embedding.option.default\" = @{ zh = \" 1) text-embedding-v4(推荐)\"; en = \" 1) text-embedding-v4 (Recommended)\" }\n \"llm.embedding.option.custom\" = @{ zh = \" 2) 自定义 Embedding 模型\"; en = \" 2) Custom embedding model\" }\n \"llm.embedding.option.disable\" = @{ zh = \" 3) 不启用\"; en = \" 3) Do not enable\" }\n \"llm.embedding.select\" = @{ zh = \"选择\"; en = \"Select\" }\n \"llm.embedding.custom_prompt\" = @{ zh = \" Embedding 模型名称\"; en = \" Embedding model name\" }\n \"llm.embedding.test.testing\" = @{ zh = \"正在测试 Embedding API 联通性...\"; en = \"Testing Embedding API connectivity...\" }\n \"llm.embedding.test.ok\" = @{ zh = \"✅ Embedding API 联通性测试通过\"; en = \"✅ Embedding API connectivity test passed\" }\n \"llm.embedding.test.fail\" = @{ zh = \"⚠️ Embedding API 测试失败(HTTP {0})。响应: {1}\"; en = \"⚠️ Embedding API test failed (HTTP {0}). Response: {1}\" }\n \"llm.embedding.auto_disabled\" = @{ zh = \"⚠️ Embedding 已自动禁用,记忆搜索将使用关键词匹配。您可以稍后在 agentteams-manager.env 中设置 AGENTTEAMS_EMBEDDING_MODEL 启用。\"; en = \"⚠️ Embedding auto-disabled. Memory search will use keyword matching. You can enable it later in agentteams-manager.env by setting AGENTTEAMS_EMBEDDING_MODEL.\" }\n \"llm.embedding.disabled\" = @{ zh = \"ℹ️ Embedding 已禁用,记忆搜索将使用关键词匹配。\"; en = \"ℹ️ Embedding disabled. Memory search will use keyword matching.\" }\n \"nav.back_hint\" = @{ zh = \"(输入 b 返回上一步)\"; en = \"(enter b to go back)\" }\n # --- OpenAI-compatible provider creation ---\n \"install.openai_compat.missing\" = @{ zh = \"警告: OpenAI Base URL 或 API Key 未设置,跳过提供商创建\"; en = \"WARNING: OpenAI Base URL or API Key not set, skipping provider creation\" }\n \"install.openai_compat.creating\" = @{ zh = \"正在创建 OpenAI 兼容提供商...\"; en = \"Creating OpenAI-compatible provider...\" }\n \"install.openai_compat.domain\" = @{ zh = \" 域名: {0}\"; en = \" Domain: {0}\" }\n \"install.openai_compat.port\" = @{ zh = \" 端口: {0}\"; en = \" Port: {0}\" }\n \"install.openai_compat.protocol\" = @{ zh = \" 协议: {0}\"; en = \" Protocol: {0}\" }\n \"install.openai_compat.service_fail\" = @{ zh = \"警告: 创建 DNS 服务源失败(可能已存在)\"; en = \"WARNING: Failed to create DNS service source (may already exist)\" }\n \"install.openai_compat.provider_fail\" = @{ zh = \"警告: 创建 AI 提供商失败(可能已存在)\"; en = \"WARNING: Failed to create AI provider (may already exist)\" }\n \"install.openai_compat.success\" = @{ zh = \"OpenAI 兼容提供商创建成功\"; en = \"OpenAI-compatible provider created successfully\" }\n\n # --- Welcome message ---\n \"install.welcome_msg.soul_configured\" = @{ zh = \"Soul 已配置(找到 soul-configured 标记),跳过 onboarding 消息\"; en = \"Soul already configured (soul-configured marker found), skipping onboarding message\" }\n \"install.welcome_msg.logging_in\" = @{ zh = \"正在以 {0} 身份登录以发送欢迎消息...\"; en = \"Logging in as {0} to send welcome message...\" }\n \"install.welcome_msg.login_failed\" = @{ zh = \"警告: 以 {0} 身份登录失败,跳过欢迎消息\"; en = \"WARNING: Failed to login as {0}, skipping welcome message\" }\n \"install.welcome_msg.finding_room\" = @{ zh = \"正在查找与 Manager 的 DM 房间...\"; en = \"Finding DM room with Manager...\" }\n \"install.welcome_msg.creating_room\" = @{ zh = \"正在创建与 Manager 的 DM 房间...\"; en = \"Creating DM room with Manager...\" }\n \"install.welcome_msg.no_room\" = @{ zh = \"警告: 无法找到或创建与 Manager 的 DM 房间\"; en = \"WARNING: Could not find or create DM room with Manager\" }\n \"install.welcome_msg.waiting_join\" = @{ zh = \"等待 Manager 加入房间...\"; en = \"Waiting for Manager to join the room...\" }\n \"install.welcome_msg.sending\" = @{ zh = \"正在向 Manager 发送欢迎消息...\"; en = \"Sending welcome message to Manager...\" }\n \"install.welcome_msg.send_failed\" = @{ zh = \"警告: 发送欢迎消息失败\"; en = \"WARNING: Failed to send welcome message\" }\n \"install.welcome_msg.sent\" = @{ zh = \"欢迎消息已发送给 Manager\"; en = \"Welcome message sent to Manager\" }\n \"install.welcome_msg.waiting\" = @{ zh = \"等待 Manager 发送欢迎消息(Higress 路由授权 + LLM 探活,约 45-90s)...\"; en = \"Waiting for Manager to send the welcome message (Higress route auth + LLM probe, ~45-90s)...\" }\n \"install.welcome_msg.confirmed\" = @{ zh = \"Manager 已确认发送欢迎消息(status.welcomeSent=true,用时 {0}s)\"; en = \"Manager confirmed welcome message sent (status.welcomeSent=true, {0}s elapsed)\" }\n \"install.welcome_msg.timeout\" = @{ zh = \"警告: 在 {0}s 内未观察到 Manager 发送欢迎消息(status.welcomeSent=true)。安装仍然成功,所有服务已就绪——可继续按下方提示登录 Element Web。\"; en = \"WARNING: Did not observe the Manager sending its welcome message (status.welcomeSent=true) within {0}s. Installation is still successful, all services are up — continue with the Element Web instructions below.\" }\n \"install.welcome_msg.timeout_hint\" = @{ zh = \"手动触发 onboarding: 登录 Element Web → 打开与 Manager 的 DM 房间 → 发送任意一句话(例如 `\"hi`\"),Manager 会接管对话并开始引导。\"; en = \"Manual onboarding: log in to Element Web -> open the DM with the Manager -> send any message (e.g. `\"hi`\") and the Manager will take over and start the guided setup.\" }\n \"install.welcome_msg.timeout_inspect\" = @{ zh = \"排查命令: docker exec agentteams-controller agt get managers default\"; en = \"Inspect status: docker exec agentteams-controller agt get managers default\" }\n \"install.welcome_msg.poll_unavailable\" = @{ zh = \"提示: agentteams-manager 内未找到 AgentTeams CLI,跳过 welcome 等待(旧镜像?)\"; en = \"Note: AgentTeams CLI not found inside agentteams-manager; skipping welcome wait (old image?)\" }\n\n # --- Final output panel ---\n \"success.title\" = @{ zh = \"=== AgentTeams Manager 已启动!===\"; en = \"=== AgentTeams Manager Started! ===\" }\n \"success.domains_configured\" = @{ zh = \"以下域名已配置解析到 127.0.0.1:\"; en = \"The following domains are configured to resolve to 127.0.0.1:\" }\n \"success.open_url\" = @{ zh = \" 在浏览器中打开以下 URL 开始使用: \"; en = \" Open the following URL in your browser to start: \" }\n \"success.login_with\" = @{ zh = \" 登录信息:\"; en = \" Login with:\" }\n \"success.username\" = @{ zh = \" 用户名: {0}\"; en = \" Username: {0}\" }\n \"success.password\" = @{ zh = \" 密码: {0}\"; en = \" Password: {0}\" }\n \"success.after_login\" = @{ zh = \" 登录后,开始与 Manager 聊天!\"; en = \" After login, start chatting with the Manager!\" }\n \"success.tell_it\" = @{ zh = \" 告诉它: `\"创建一个名为 alice 的前端开发 Worker`\"\"; en = \" Tell it: `\"Create a Worker named alice for frontend dev`\"\" }\n \"success.manager_auto\" = @{ zh = \" Manager 会自动处理一切。\"; en = \" The Manager will handle everything automatically.\" }\n \"success.mobile_title\" = @{ zh = \" 移动端访问(FluffyChat / Element Mobile):\"; en = \" Mobile access (FluffyChat / Element Mobile):\" }\n \"success.mobile_step1\" = @{ zh = \" 1. 在手机上下载 FluffyChat 或 Element\"; en = \" 1. Download FluffyChat or Element on your phone\" }\n \"success.mobile_step2\" = @{ zh = \" 2. 设置 homeserver 为: {0}\"; en = \" 2. Set homeserver to: {0}\" }\n \"success.mobile_step2_noip\" = @{ zh = \" 2. 设置 homeserver 为: http://<本机局域网IP>:{0}\"; en = \" 2. Set homeserver to: http://<this-machine-LAN-IP>:{0}\" }\n \"success.mobile_noip_hint\" = @{ zh = \" (无法自动检测局域网 IP — 请使用 ifconfig / ip addr 查看)\"; en = \" (Could not detect LAN IP automatically — check with: ifconfig / ip addr)\" }\n \"success.mobile_step3\" = @{ zh = \" 3. 登录信息:\"; en = \" 3. Login with:\" }\n \"success.mobile_username\" = @{ zh = \" 用户名: {0}\"; en = \" Username: {0}\" }\n \"success.mobile_password\" = @{ zh = \" 密码: {0}\"; en = \" Password: {0}\" }\n\n # --- Other consoles and tips ---\n \"success.other_consoles\" = @{ zh = \"--- 其他控制台 ---\"; en = \"--- Other Consoles ---\" }\n \"success.higress_console\" = @{ zh = \" Higress 控制台: http://localhost:{0}(用户名: {1} / 密码: {2})\"; en = \" Higress Console: http://localhost:{0} (Username: {1} / Password: {2})\" }\n \"success.manager_console\" = @{ zh = \" Manager 控制台(本地): http://localhost:{0}(无需登录)\"; en = \" Manager Console (local): http://localhost:{0} (no login required)\" }\n \"success.manager_console_gateway\" = @{ zh = \" Manager 控制台(网关): http://console-local.agentteams.io(用户名: {0} / 密码: {1})\"; en = \" Manager Console (gateway): http://console-local.agentteams.io (Username: {0} / Password: {1})\" }\n \"success.copaw_console\" = @{ zh = \" QwenPaw 控制台(本地): http://localhost:{0}(无需登录)\"; en = \" QwenPaw Console (local): http://localhost:{0} (no login required)\" }\n \"success.switch_llm.title\" = @{ zh = \"--- 切换 LLM 提供商 ---\"; en = \"--- Switch LLM Providers ---\" }\n \"success.switch_llm.hint\" = @{ zh = \" 您可以通过 Higress 控制台切换到其他 LLM 提供商(OpenAI、Anthropic 等)。\"; en = \" You can switch to other LLM providers (OpenAI, Anthropic, etc.) via Higress Console.\" }\n \"success.switch_llm.docs\" = @{ zh = \" 详细说明请参阅:\"; en = \" For detailed instructions, see:\" }\n \"success.switch_llm.url\" = @{ zh = \" https://higress.ai/en/docs/ai/scene-guide/multi-proxy#console-configuration\"; en = \" https://higress.ai/en/docs/ai/scene-guide/multi-proxy#console-configuration\" }\n \"success.tip\" = @{ zh = \"提示: 您也可以在聊天中让 Manager 为您配置 LLM 提供商。\"; en = \"Tip: You can also ask the Manager to configure LLM providers for you in the chat.\" }\n \"success.config_file\" = @{ zh = \"配置文件: {0}\"; en = \"Configuration file: {0}\" }\n \"success.data_volume\" = @{ zh = \"数据卷: {0}\"; en = \"Data volume: {0}\" }\n \"success.workspace\" = @{ zh = \"Manager 工作空间: {0}\"; en = \"Manager workspace: {0}\" }\n\n # --- Worker installation ---\n \"worker.resetting\" = @{ zh = \"正在重置 Worker: {0}...\"; en = \"Resetting Worker: {0}...\" }\n \"worker.exists\" = @{ zh = \"容器 '{0}' 已存在。使用 --reset 重新创建。\"; en = \"Container '{0}' already exists. Use --reset to recreate.\" }\n \"worker.starting\" = @{ zh = \"正在启动 Worker: {0}...\"; en = \"Starting Worker: {0}...\" }\n \"worker.skills_url\" = @{ zh = \" Skills API URL: {0}\"; en = \" Skills API URL: {0}\" }\n \"worker.started\" = @{ zh = \"=== Worker {0} 已启动!===\"; en = \"=== Worker {0} Started! ===\" }\n \"worker.container\" = @{ zh = \"容器: {0}\"; en = \"Container: {0}\" }\n \"worker.view_logs\" = @{ zh = \"查看日志: docker logs -f {0}\"; en = \"View logs: docker logs -f {0}\" }\n\n # --- Prompt function messages ---\n \"prompt.preset\" = @{ zh = \" {0} = (已通过环境变量预设)\"; en = \" {0} = (pre-set via env)\" }\n \"prompt.upgrade_keep\" = @{ zh = \" {0} = {1}(当前值,回车保留 / 输入新值覆盖)\"; en = \" {0} = {1} (current value, press Enter to keep / type new value to change)\" }\n \"prompt.upgrade_empty\" = @{ zh = \" {0} = (未设置,回车跳过 / 输入新值设置)\"; en = \" {0} = (not set, press Enter to skip / type new value to set)\" }\n \"prompt.default\" = @{ zh = \" {0} = {1}(默认)\"; en = \" {0} = {1} (default)\" }\n \"prompt.required\" = @{ zh = \"{0} 是必需的(在非交互模式下通过环境变量设置)\"; en = \"{0} is required (set via environment variable in non-interactive mode)\" }\n \"prompt.required_empty\" = @{ zh = \"{0} 是必需的\"; en = \"{0} is required\" }\n\n # --- Language switch prompt (bilingual by design) ---\n \"lang.detected.zh\" = @{ zh = \"检测到语言 / Detected language: 中文\"; en = \"检测到语言 / Detected language: 中文\" }\n \"lang.detected.en\" = @{ zh = \"检测到语言 / Detected language: English\"; en = \"检测到语言 / Detected language: English\" }\n \"lang.switch_title\" = @{ zh = \"切换语言 / Switch language:\"; en = \"切换语言 / Switch language:\" }\n \"lang.option_zh\" = @{ zh = \" 1) 中文\"; en = \" 1) 中文\" }\n \"lang.option_en\" = @{ zh = \" 2) English\"; en = \" 2) English\" }\n \"lang.prompt\" = @{ zh = \"请选择 / Enter choice\"; en = \"请选择 / Enter choice\" }\n\n # --- Error messages ---\n \"error.name_required\" = @{ zh = \"--name 是必需的\"; en = \"--name is required\" }\n \"error.fs_required\" = @{ zh = \"--fs 是必需的\"; en = \"--fs is required\" }\n \"error.fs_key_required\" = @{ zh = \"--fs-key 是必需的\"; en = \"--fs-key is required\" }\n \"error.fs_secret_required\" = @{ zh = \"--fs-secret 是必需的\"; en = \"--fs-secret is required\" }\n \"error.unknown_option\" = @{ zh = \"未知选项: {0}\"; en = \"Unknown option: {0}\" }\n \"error.docker_not_running\" = @{ zh = \"Docker 未运行。请先启动 Docker Desktop 或 Podman Desktop。\"; en = \"Docker is not running. Please start Docker Desktop or Podman Desktop first.\" }\n \"error.docker_not_found\" = @{ zh = \"未找到 docker 或 podman 命令。请先安装 Docker Desktop 或 Podman Desktop:`n Docker Desktop: https://www.docker.com/products/docker-desktop/`n Podman Desktop: https://podman-desktop.io/\"; en = \"docker or podman command not found. Please install Docker Desktop or Podman Desktop first:`n Docker Desktop: https://www.docker.com/products/docker-desktop/`n Podman Desktop: https://podman-desktop.io/\" }\n\n # --- Uninstall messages ---\n \"uninstall.title\" = @{ zh = \"正在卸载 AgentTeams...\"; en = \"Uninstalling AgentTeams...\" }\n \"uninstall.stopping_manager\" = @{ zh = \"正在停止并移除 agentteams-manager...\"; en = \"Stopping and removing agentteams-manager...\" }\n \"uninstall.stopping_workers\" = @{ zh = \"正在停止并移除 worker 容器...\"; en = \"Stopping and removing worker containers...\" }\n \"uninstall.removed\" = @{ zh = \" 已移除: {0}\"; en = \" Removed: {0}\" }\n \"uninstall.removing_volume\" = @{ zh = \"正在移除 Docker 卷: agentteams-data\"; en = \"Removing Docker volume: agentteams-data\" }\n \"uninstall.removing_env\" = @{ zh = \"正在移除 env 文件: {0}\"; en = \"Removing env file: {0}\" }\n \"uninstall.removing_proxy\" = @{ zh = \"正在停止并移除 Docker API 代理容器: agentteams-docker-proxy\"; en = \"Stopping and removing Docker API proxy container: agentteams-docker-proxy\" }\n \"uninstall.stopping_controller\" = @{ zh = \"正在停止并移除 agentteams-controller (内嵌 Tuwunel/MinIO/Higress)...\"; en = \"Stopping and removing agentteams-controller (embedded Tuwunel/MinIO/Higress)...\" }\n \"uninstall.removing_network\" = @{ zh = \"正在移除 Docker 网络: agentteams-net\"; en = \"Removing Docker network: agentteams-net\" }\n \"uninstall.removing_workspace\" = @{ zh = \"正在移除工作空间目录: {0}\"; en = \"Removing workspace directory: {0}\" }\n \"uninstall.removing_log\" = @{ zh = \"正在移除日志文件: {0}\"; en = \"Removing log file: {0}\" }\n \"uninstall.done\" = @{ zh = \"AgentTeams 已卸载。\"; en = \"AgentTeams has been uninstalled.\" }\n}\n\n# Get-Msg: look up message by key, with -f style argument substitution.\n# Falls back to English if the current language translation is missing.\nfunction Get-Msg {\n param(\n [Parameter(Mandatory)][string]$Key,\n [object[]]$f\n )\n $lang = $script:AGENTTEAMS_LANGUAGE\n if (-not $lang) { $lang = \"en\" }\n $entry = $script:Messages[$Key]\n if (-not $entry) { return $Key }\n $text = $entry[$lang]\n if (-not $text) { $text = $entry[\"en\"] }\n if (-not $text) { return $Key }\n if ($f) { return ($text -f $f) }\n return $text\n}\n\nfunction Get-LanIP {\n # Detect local LAN IP address on Windows\n try {\n # Get network adapters with IPv4 addresses, prefer connected/active interfaces\n $adapters = Get-NetIPAddress -AddressFamily IPv4 -ErrorAction SilentlyContinue |\n Where-Object {\n $_.IPAddress -notlike \"127.*\" -and\n $_.PrefixOrigin -ne \"WellKnown\" -and\n $_.InterfaceAlias -notlike \"*Loopback*\"\n } |\n Sort-Object {\n # PowerShell 5.1 compatible: use if-else instead of ternary operator\n if ($_.InterfaceAlias -like \"*Wi-Fi*\" -or $_.InterfaceAlias -like \"*Ethernet*\") { 0 } else { 1 }\n }\n\n if ($adapters) {\n return $adapters[0].IPAddress\n }\n\n # Fallback: use ipconfig\n $ipconfig = ipconfig 2>$null\n $ip = ($ipconfig | Select-String \"IPv4 Address.*?: (\\d+\\.\\d+\\.\\d+\\.\\d+)\" | Select-Object -First 1)\n if ($ip -match \"(\\d+\\.\\d+\\.\\d+\\.\\d+)\") {\n return $Matches[1]\n }\n }\n catch {\n # Ignore errors\n }\n\n return \"\"\n}\n\n# Known models list — used to detect custom models during install\n$script:KnownModels = @(\n \"gpt-5.4\", \"gpt-5.3-codex\", \"gpt-5-mini\", \"gpt-5-nano\",\n \"claude-opus-4-6\", \"claude-sonnet-4-6\", \"claude-haiku-4-5\",\n \"qwen3.6-plus\", \"qwen3.5-plus\", \"deepseek-chat\", \"deepseek-reasoner\",\n \"kimi-k2.5\", \"glm-5\", \"MiniMax-M2.7\", \"MiniMax-M2.7-highspeed\", \"MiniMax-M2.5\"\n)\n\nfunction Test-KnownModel {\n param([string]$Model)\n return $script:KnownModels -contains $Model\n}\n\nfunction Request-CustomModelParams {\n param([string]$Model)\n if (Test-KnownModel $Model) {\n $script:config.MODEL_CONTEXT_WINDOW = \"\"\n $script:config.MODEL_MAX_TOKENS = \"\"\n $script:config.MODEL_REASONING = \"\"\n $script:config.MODEL_VISION = \"\"\n return\n }\n Write-Host \"\"\n Write-Log (Get-Msg \"llm.custom_model.detected\" -f $Model)\n Write-Host \"\"\n $ctxInput = Read-Host \" $(Get-Msg 'llm.custom_model.context_prompt')\"\n if ($ctxInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $script:config.MODEL_CONTEXT_WINDOW = if ($ctxInput) { $ctxInput } else { \"150000\" }\n $maxInput = Read-Host \" $(Get-Msg 'llm.custom_model.max_tokens_prompt')\"\n if ($maxInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $script:config.MODEL_MAX_TOKENS = if ($maxInput) { $maxInput } else { \"128000\" }\n $reasoningInput = Read-Host \" $(Get-Msg 'llm.custom_model.reasoning_prompt')\"\n if ($reasoningInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $script:config.MODEL_REASONING = if ($reasoningInput -match \"^[nN]\") { \"false\" } else { \"true\" }\n $visionInput = Read-Host \" $(Get-Msg 'llm.custom_model.vision_prompt')\"\n if ($visionInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $script:config.MODEL_VISION = if ($visionInput -match \"^[yY]\") { \"true\" } else { \"false\" }\n Write-Log (Get-Msg \"llm.custom_model.summary\" -f $script:config.MODEL_CONTEXT_WINDOW, $script:config.MODEL_MAX_TOKENS, $script:config.MODEL_REASONING, $script:config.MODEL_VISION)\n}\n\nfunction New-RandomKey {\n # Generate 64 character hex string (32 bytes)\n $bytes = New-Object byte[] 32\n $rng = [System.Security.Cryptography.RandomNumberGenerator]::Create()\n $rng.GetBytes($bytes)\n return [BitConverter]::ToString($bytes).Replace(\"-\", \"\").ToLower()\n}\n\nfunction ConvertTo-DockerPath {\n param([string]$Path)\n\n # Convert Windows path to Docker mount format\n $fullPath = (Resolve-Path $Path -ErrorAction SilentlyContinue).Path\n if (-not $fullPath) {\n $fullPath = $Path\n }\n\n # Convert C:\\path to /c/path format for Docker\n if ($fullPath -match \"^([A-Za-z]):\") {\n $drive = $Matches[1].ToLower()\n $rest = $fullPath.Substring(2).Replace(\"\\\", \"/\")\n return \"/$drive$rest\"\n }\n return $fullPath.Replace(\"\\\", \"/\")\n}\n\n# Resolve the embedded controller image. Embedded mode is the only supported\n# architecture since PR #616 (manager image no longer bundles Higress/Tuwunel/MinIO).\n# Mirrors install/agentteams-install.sh::resolve_embedded_image — fail fast when the\n# embedded image is unavailable rather than silently falling back to the broken\n# legacy single-container path.\n# Sets $script:EMBEDDED_IMAGE and $script:AGENTTEAMS_USE_EMBEDDED.\nfunction Resolve-EmbeddedImage {\n $script:AGENTTEAMS_USE_EMBEDDED = \"1\"\n\n # Explicit override always wins (used by `make install-embedded` for local builds).\n if ($env:AGENTTEAMS_INSTALL_EMBEDDED_IMAGE) {\n $script:EMBEDDED_IMAGE = $env:AGENTTEAMS_INSTALL_EMBEDDED_IMAGE\n return\n }\n\n $versioned = \"$($script:AGENTTEAMS_REGISTRY)/agentteams/agentteams-embedded:$($script:AGENTTEAMS_VERSION)\"\n $latestTag = \"$($script:AGENTTEAMS_REGISTRY)/agentteams/agentteams-embedded:latest\"\n\n if ($script:AGENTTEAMS_VERSION -eq \"latest\") {\n $script:EMBEDDED_IMAGE = $latestTag\n return\n }\n\n docker pull $versioned *>$null\n if ($LASTEXITCODE -eq 0) {\n $script:EMBEDDED_IMAGE = $versioned\n return\n }\n docker pull $latestTag *>$null\n if ($LASTEXITCODE -eq 0) {\n Write-Log \"embedded $($script:AGENTTEAMS_VERSION) not found, using latest\"\n $script:EMBEDDED_IMAGE = $latestTag\n return\n }\n\n # Escape hatch for older versions (AGENTTEAMS_VERSION <= v1.0.9) whose manager image\n # still bundled the infrastructure — opt-in only, never silent.\n if ($env:AGENTTEAMS_FORCE_LEGACY -eq \"1\") {\n Write-Log \"WARNING: AGENTTEAMS_FORCE_LEGACY=1 - using legacy all-in-one manager architecture.\"\n Write-Log \"WARNING: This requires AGENTTEAMS_VERSION <= v1.0.9 (older bundled manager image).\"\n Write-Log \"WARNING: Newer slim manager images will hang on 'Waiting for Higress Gateway'.\"\n $script:AGENTTEAMS_USE_EMBEDDED = \"0\"\n return\n }\n\n Write-Host \"$($script:ESC)[31m[AgentTeams ERROR]$($script:ESC)[0m Embedded controller image is not available in the registry:\" -ForegroundColor Red\n Write-Host \" - tried: $versioned\" -ForegroundColor Red\n Write-Host \" - tried: $latestTag\" -ForegroundColor Red\n Write-Host \"\" -ForegroundColor Red\n Write-Host \"Embedded mode is the only supported architecture since PR #616.\" -ForegroundColor Red\n Write-Host \"How to resolve:\" -ForegroundColor Red\n Write-Host \" 1) Pin to a AGENTTEAMS_VERSION whose embedded image has been published, or\" -ForegroundColor Red\n Write-Host \" wait for the release pipeline to publish it.\" -ForegroundColor Red\n Write-Host \" 2) For a local build, run: make install-embedded\" -ForegroundColor Red\n Write-Host \" (builds and uses the local embedded image without touching the registry).\" -ForegroundColor Red\n Write-Host \" 3) Override with a custom image:\" -ForegroundColor Red\n Write-Host \" `$env:AGENTTEAMS_INSTALL_EMBEDDED_IMAGE=...; .\\agentteams-install.ps1\" -ForegroundColor Red\n Exit-Script 1\n}\n\nfunction Wait-ManagerReady {\n param(\n [string]$Container = \"agentteams-manager\",\n [int]$Timeout = $(if ($env:AGENTTEAMS_READY_TIMEOUT) { [int]$env:AGENTTEAMS_READY_TIMEOUT } else { 300 })\n )\n\n $elapsed = 0\n Write-Log (Get-Msg \"install.wait_ready\" -f $Timeout)\n\n $runtime = if ($script:config.MANAGER_RUNTIME) { $script:config.MANAGER_RUNTIME } else { \"copaw\" }\n\n while ($elapsed -lt $Timeout) {\n try {\n switch ($runtime) {\n \"copaw\" {\n $result = docker exec $Container curl -sf http://127.0.0.1:18799/api/agents 2>$null\n if ($result -match '\"agents\"') {\n Write-Log (Get-Msg \"install.wait_ready.ok\")\n return $true\n }\n }\n default {\n $result = docker exec $Container openclaw gateway health --json 2>$null\n if ($result -match '\"ok\"') {\n Write-Log (Get-Msg \"install.wait_ready.ok\")\n return $true\n }\n }\n }\n } catch {\n # Ignore errors during polling\n }\n\n Start-Sleep -Seconds 5\n $elapsed += 5\n Write-Host \"`r$($script:ESC)[36m[AgentTeams]$($script:ESC)[0m $(Get-Msg 'install.wait_ready.waiting' -f $elapsed, $Timeout)\" -NoNewline\n }\n\n Write-Host \"\"\n Write-Error (Get-Msg \"install.wait_ready.timeout\" -f $Timeout, $Container)\n}\n\nfunction Wait-MatrixReady {\n param(\n [string]$Container = \"agentteams-manager\",\n [int]$Timeout = $(if ($env:AGENTTEAMS_READY_TIMEOUT) { [int]$env:AGENTTEAMS_READY_TIMEOUT } else { 300 })\n )\n\n $elapsed = 0\n Write-Log (Get-Msg \"install.wait_matrix\" -f $Timeout)\n\n while ($elapsed -lt $Timeout) {\n try {\n $result = docker exec $Container curl -sf http://127.0.0.1:6167/_tuwunel/server_version 2>$null\n if ($result) {\n Write-Log (Get-Msg \"install.wait_matrix.ok\")\n return $true\n }\n } catch {\n # Ignore errors during polling\n }\n\n Start-Sleep -Seconds 5\n $elapsed += 5\n Write-Host \"`r$($script:ESC)[36m[AgentTeams]$($script:ESC)[0m $(Get-Msg 'install.wait_matrix.waiting' -f $elapsed, $Timeout)\" -NoNewline\n }\n\n Write-Host \"\"\n Write-Error (Get-Msg \"install.wait_matrix.timeout\" -f $Timeout, $Container)\n}\n\n# Read KEY=value from /data/agentteams-secrets.env on a Docker volume (manager container not required).\n# Requires $script:EMBEDDED_IMAGE (set by Resolve-EmbeddedImage before Install-Manager uses this).\nfunction Read-AgentTeamsSecretFromDataVolume {\n param(\n [string]$VolumeName,\n [string]$Key\n )\n if ([string]::IsNullOrEmpty($VolumeName) -or [string]::IsNullOrEmpty($Key) -or [string]::IsNullOrEmpty($script:EMBEDDED_IMAGE)) {\n return \"\"\n }\n $grepKey = [regex]::Escape($Key)\n $shCmd = \"grep \"\"^${grepKey}=\"\" /data/agentteams-secrets.env 2>/dev/null | cut -d= -f2- | head -1 | tr -d '\\r'\"\n $out = docker run --rm --entrypoint sh -v \"${VolumeName}:/data:ro\" $script:EMBEDDED_IMAGE -c $shCmd 2>$null\n if ($null -eq $out) { return \"\" }\n return ($out | Out-String).Trim()\n}\n\n# Read KEY=value from /data/worker-creds/<worker>.env on a Docker volume.\nfunction Read-AgentTeamsWorkerCredsFromVolume {\n param(\n [string]$VolumeName,\n [string]$WorkerName,\n [string]$Key\n )\n if ([string]::IsNullOrEmpty($VolumeName) -or [string]::IsNullOrEmpty($WorkerName) -or [string]::IsNullOrEmpty($Key) -or [string]::IsNullOrEmpty($script:EMBEDDED_IMAGE)) {\n return \"\"\n }\n $grepKey = [regex]::Escape($Key)\n $path = \"/data/worker-creds/${WorkerName}.env\"\n $shCmd = \"grep \"\"^${grepKey}=\"\" \"\"${path}\"\" 2>/dev/null | cut -d= -f2- | head -1 | tr -d '\\r'\"\n $out = docker run --rm --entrypoint sh -v \"${VolumeName}:/data:ro\" $script:EMBEDDED_IMAGE -c $shCmd 2>$null\n if ($null -eq $out) { return \"\" }\n return ($out | Out-String).Trim()\n}\n\n# Read admin_dm_room_id from host workspace state.json (fallback when Matrix API is unavailable).\nfunction Read-AgentTeamsAdminDmRoomFromWorkspace {\n param([string]$WorkspaceDir)\n if ([string]::IsNullOrEmpty($WorkspaceDir)) { return \"\" }\n $statePath = Join-Path $WorkspaceDir \"state.json\"\n if (-not (Test-Path $statePath)) { return \"\" }\n try {\n $j = Get-Content $statePath -Raw -ErrorAction Stop | ConvertFrom-Json\n $rid = $j.admin_dm_room_id\n if ($null -eq $rid) { return \"\" }\n $s = [string]$rid\n if ($s -eq \"\" -or $s -eq \"null\") { return \"\" }\n return $s.Trim()\n } catch {\n return \"\"\n }\n}\n\nfunction Get-AgentTeamsRandomHex {\n param([int]$ByteCount)\n $bytes = New-Object byte[] $ByteCount\n $rng = [System.Security.Cryptography.RandomNumberGenerator]::Create()\n $rng.GetBytes($bytes)\n return ([BitConverter]::ToString($bytes)).Replace(\"-\", \"\").ToLower()\n}\n\n# Resolve admin DM room with @manager (small room) via Matrix Client API inside agentteams-manager.\nfunction Get-AgentTeamsAdminDmRoomViaMatrix {\n param(\n [string]$AdminUser,\n [string]$AdminPassword\n )\n $running = docker ps --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-manager$\"\n if (-not $running) { return \"\" }\n if ([string]::IsNullOrEmpty($AdminPassword)) { return \"\" }\n\n $loginObj = @{\n type = \"m.login.password\"\n identifier = @{ type = \"m.id.user\"; user = $AdminUser }\n password = $AdminPassword\n }\n $loginJson = $loginObj | ConvertTo-Json -Compress -Depth 5\n try {\n $loginRaw = $loginJson | docker exec -i agentteams-manager sh -c \"curl -sf -X POST http://127.0.0.1:6167/_matrix/client/v3/login -H 'Content-Type: application/json' -d @-\" 2>$null\n if (-not $loginRaw) { return \"\" }\n $loginResp = $loginRaw | ConvertFrom-Json\n $token = [string]$loginResp.access_token\n if ([string]::IsNullOrEmpty($token)) { return \"\" }\n\n $roomsRaw = docker exec agentteams-manager curl -sf -X GET -H \"Authorization: Bearer $token\" `\n \"http://127.0.0.1:6167/_matrix/client/v3/joined_rooms\" 2>$null\n if (-not $roomsRaw) { return \"\" }\n $roomsObj = $roomsRaw | ConvertFrom-Json\n $roomList = @($roomsObj.joined_rooms)\n foreach ($roomId in $roomList) {\n if ([string]::IsNullOrEmpty($roomId)) { continue }\n $enc = $roomId.Replace(\"!\", \"%21\")\n $memRaw = docker exec agentteams-manager curl -sf -X GET -H \"Authorization: Bearer $token\" `\n \"http://127.0.0.1:6167/_matrix/client/v3/rooms/${enc}/members\" 2>$null\n if (-not $memRaw) { continue }\n $memObj = $memRaw | ConvertFrom-Json\n $chunk = @($memObj.chunk)\n $memberIds = @($chunk | ForEach-Object { $_.state_key })\n $match = $false\n foreach ($m in $memberIds) {\n $beforeColon = ($m -split \":\")[0]\n if ($m -like \"*manager*\" -and $beforeColon -notlike \"*admin*\") {\n $match = $true\n break\n }\n }\n if ($match -and $memberIds.Count -le 3) {\n return $roomId\n }\n }\n } catch {\n return \"\"\n }\n return \"\"\n}\n\nfunction New-EnvFile {\n param([hashtable]$Config, [string]$Path)\n\n $content = @\"\n# AgentTeams Manager Configuration\n# Generated by agentteams-install.ps1 on $(Get-Date -Format \"yyyy-MM-dd HH:mm:ss\")\n\n# Language\nAGENTTEAMS_LANGUAGE=$($Config.LANGUAGE)\n\n# LLM\nAGENTTEAMS_LLM_PROVIDER=$($Config.LLM_PROVIDER)\nAGENTTEAMS_DEFAULT_MODEL=$($Config.DEFAULT_MODEL)\nAGENTTEAMS_LLM_API_KEY=$($Config.LLM_API_KEY)\nAGENTTEAMS_OPENAI_BASE_URL=$($Config.OPENAI_BASE_URL)\nAGENTTEAMS_MODEL_CONTEXT_WINDOW=$($Config.MODEL_CONTEXT_WINDOW)\nAGENTTEAMS_MODEL_MAX_TOKENS=$($Config.MODEL_MAX_TOKENS)\nAGENTTEAMS_MODEL_REASONING=$($Config.MODEL_REASONING)\nAGENTTEAMS_MODEL_VISION=$($Config.MODEL_VISION)\n\n# Embedding model (empty = disabled, default: text-embedding-v4)\nAGENTTEAMS_EMBEDDING_MODEL=$($Config.EMBEDDING_MODEL)\n\n# Admin\nAGENTTEAMS_ADMIN_USER=$($Config.ADMIN_USER)\nAGENTTEAMS_ADMIN_PASSWORD=$($Config.ADMIN_PASSWORD)\n\n# Ports\nAGENTTEAMS_LOCAL_ONLY=$($Config.LOCAL_ONLY)\nAGENTTEAMS_PORT_GATEWAY=$($Config.PORT_GATEWAY)\nAGENTTEAMS_PORT_CONSOLE=$($Config.PORT_CONSOLE)\nAGENTTEAMS_PORT_ELEMENT_WEB=$($Config.PORT_ELEMENT_WEB)\nAGENTTEAMS_PORT_MANAGER_CONSOLE=$($Config.PORT_MANAGER_CONSOLE)\n\n# Matrix\nAGENTTEAMS_MATRIX_DOMAIN=$($Config.MATRIX_DOMAIN)\nAGENTTEAMS_MATRIX_CLIENT_DOMAIN=$($Config.MATRIX_CLIENT_DOMAIN)\n\n# Gateway\nAGENTTEAMS_AI_GATEWAY_DOMAIN=$($Config.AI_GATEWAY_DOMAIN)\nAGENTTEAMS_MANAGER_GATEWAY_KEY=$($Config.MANAGER_GATEWAY_KEY)\n\n# File System\nAGENTTEAMS_FS_DOMAIN=$($Config.FS_DOMAIN)\nAGENTTEAMS_CONSOLE_DOMAIN=$($Config.CONSOLE_DOMAIN)\nAGENTTEAMS_MINIO_USER=$($Config.MINIO_USER)\nAGENTTEAMS_MINIO_PASSWORD=$($Config.MINIO_PASSWORD)\n\n# Internal\nAGENTTEAMS_MANAGER_PASSWORD=$($Config.MANAGER_PASSWORD)\nAGENTTEAMS_REGISTRATION_TOKEN=$($Config.REGISTRATION_TOKEN)\n\n# GitHub (optional)\nAGENTTEAMS_GITHUB_TOKEN=$($Config.GITHUB_TOKEN)\n\n# Nacos package import defaults\nAGENTTEAMS_NACOS_REGISTRY_URI=$(if ($env:AGENTTEAMS_NACOS_REGISTRY_URI) { $env:AGENTTEAMS_NACOS_REGISTRY_URI } else { \"nacos://market.agentteams.io:80/public\" })\nAGENTTEAMS_NACOS_USERNAME=$($env:AGENTTEAMS_NACOS_USERNAME)\nAGENTTEAMS_NACOS_PASSWORD=$($env:AGENTTEAMS_NACOS_PASSWORD)\nAGENTTEAMS_NACOS_TOKEN=$($env:AGENTTEAMS_NACOS_TOKEN)\n\n# Skills Registry (optional, default: nacos://market.agentteams.io:80/public)\nAGENTTEAMS_SKILLS_API_URL=$(if ($Config.SKILLS_API_URL) { $Config.SKILLS_API_URL } else { \"nacos://market.agentteams.io:80/public\" })\n\n# OpenClaw CMS plugin (optional)\nAGENTTEAMS_CMS_TRACES_ENABLED=$(if ($env:AGENTTEAMS_CMS_TRACES_ENABLED) { $env:AGENTTEAMS_CMS_TRACES_ENABLED } else { \"false\" })\nAGENTTEAMS_CMS_ENDPOINT=$($env:AGENTTEAMS_CMS_ENDPOINT)\nAGENTTEAMS_CMS_LICENSE_KEY=$($env:AGENTTEAMS_CMS_LICENSE_KEY)\nAGENTTEAMS_CMS_PROJECT=$($env:AGENTTEAMS_CMS_PROJECT)\nAGENTTEAMS_CMS_WORKSPACE=$($env:AGENTTEAMS_CMS_WORKSPACE)\nAGENTTEAMS_CMS_SERVICE_NAME=$(if ($env:AGENTTEAMS_CMS_SERVICE_NAME) { $env:AGENTTEAMS_CMS_SERVICE_NAME } else { \"agentteams-manager\" })\nAGENTTEAMS_CMS_METRICS_ENABLED=$(if ($env:AGENTTEAMS_CMS_METRICS_ENABLED) { $env:AGENTTEAMS_CMS_METRICS_ENABLED } else { \"false\" })\n\n# Worker images (for direct container creation)\nAGENTTEAMS_WORKER_IMAGE=$($Config.WORKER_IMAGE)\nAGENTTEAMS_COPAW_WORKER_IMAGE=$($Config.COPAW_WORKER_IMAGE)\nAGENTTEAMS_HERMES_WORKER_IMAGE=$($Config.HERMES_WORKER_IMAGE)\n\n# Manager runtime (openclaw | copaw)\nAGENTTEAMS_MANAGER_RUNTIME=$($Config.MANAGER_RUNTIME)\n\n# Default Worker runtime (openclaw | copaw | hermes)\nAGENTTEAMS_DEFAULT_WORKER_RUNTIME=$($Config.DEFAULT_WORKER_RUNTIME)\n\n# Matrix E2EE (0=disabled, 1=enabled; default: 0)\nAGENTTEAMS_MATRIX_E2EE=$($Config.MATRIX_E2EE)\n\n# Docker API proxy (0=disabled, 1=enabled; default: 1)\nAGENTTEAMS_DOCKER_PROXY=$($Config.DOCKER_PROXY)\n\n# Docker API proxy: additional allowed image sources (comma-separated)\nAGENTTEAMS_PROXY_ALLOWED_REGISTRIES=$($Config.PROXY_ALLOWED_REGISTRIES)\n\n# Worker idle timeout in minutes (default: 720 = 12 hours)\nAGENTTEAMS_WORKER_IDLE_TIMEOUT=$($Config.WORKER_IDLE_TIMEOUT)\n\n# JVM Args for Higress Console\nJVM_ARGS=$($env:JVM_ARGS)\n\n# Higress WASM plugin image registry (auto-selected by timezone)\nHIGRESS_ADMIN_WASM_PLUGIN_IMAGE_REGISTRY=$($Config.REGISTRY)\n\n# Data persistence\nAGENTTEAMS_DATA_DIR=$($Config.DATA_DIR)\n# Manager workspace (skills, memory, state - host-editable)\nAGENTTEAMS_WORKSPACE_DIR=$($Config.WORKSPACE_DIR)\n# Host directory sharing\nAGENTTEAMS_HOST_SHARE_DIR=$($Config.HOST_SHARE_DIR)\n\"@\n\n Set-Content -Path $Path -Value $content -Encoding UTF8\n Write-Log (Get-Msg \"install.config_saved\" -f $Path)\n}\n\n# ============================================================\n# Prompt Functions\n# ============================================================\n\nfunction Read-Prompt {\n param(\n [string]$VarName,\n [string]$PromptText,\n [string]$Default = \"\",\n [switch]$Secret,\n [switch]$Optional\n )\n\n # Check if already set in environment\n $envValue = [Environment]::GetEnvironmentVariable($VarName)\n if ($envValue) {\n if ($script:AGENTTEAMS_UPGRADE -and -not $script:AGENTTEAMS_NON_INTERACTIVE) {\n # Show current value (masked for secrets) and let user change it\n $displayValue = $envValue\n if ($Secret) {\n if ($envValue.Length -le 8) {\n $displayValue = \"****\"\n } else {\n $displayValue = $envValue.Substring(0, 4) + \"****\" + $envValue.Substring($envValue.Length - 4)\n }\n }\n Write-Log (Get-Msg \"prompt.upgrade_keep\" -f $PromptText, $displayValue)\n $prompt = $PromptText\n if ($Secret) {\n $newValue = Read-Host -Prompt $prompt -AsSecureString\n $newValue = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto(\n [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($newValue)\n )\n } else {\n $newValue = Read-Host -Prompt $prompt\n if ($newValue -eq \"b\") { $script:StepResult = \"back\"; return $null }\n }\n if ($newValue) {\n return $newValue\n }\n return $envValue\n }\n Write-Log (Get-Msg \"prompt.preset\" -f $PromptText)\n return $envValue\n }\n # Upgrade mode: optional fields with empty value — let user set a new value\n elseif ($Optional -and $script:AGENTTEAMS_UPGRADE -and -not $script:AGENTTEAMS_NON_INTERACTIVE) {\n Write-Log (Get-Msg \"prompt.upgrade_empty\" -f $PromptText)\n $prompt = $PromptText\n if ($Secret) {\n $newValue = Read-Host -Prompt $prompt -AsSecureString\n $newValue = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto(\n [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($newValue)\n )\n } else {\n $newValue = Read-Host -Prompt $prompt\n if ($newValue -eq \"b\") { $script:StepResult = \"back\"; return $null }\n }\n if ($newValue) {\n return $newValue\n }\n return \"\"\n }\n\n # Non-interactive or quickstart mode\n if ($script:AGENTTEAMS_NON_INTERACTIVE -or $script:AGENTTEAMS_QUICKSTART) {\n if ($Default) {\n Write-Log (Get-Msg \"prompt.default\" -f $PromptText, $Default)\n return $Default\n }\n elseif ($Optional) {\n return \"\"\n }\n elseif ($script:AGENTTEAMS_NON_INTERACTIVE) {\n # Only hard-error in fully non-interactive mode, not quickstart\n Write-Error (Get-Msg \"prompt.required\" -f $PromptText)\n }\n # quickstart + no default + not optional: fall through to interactive prompt\n }\n\n # Interactive prompt\n $prompt = if ($Default) { \"$PromptText [$Default]\" } else { $PromptText }\n\n if ($Secret) {\n $value = Read-Host -Prompt $prompt -AsSecureString\n $value = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto(\n [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($value)\n )\n }\n else {\n $value = Read-Host -Prompt $prompt\n if ($value -eq \"b\") { $script:StepResult = \"back\"; return $null }\n }\n\n if (-not $value -and $Default) {\n $value = $Default\n }\n\n if (-not $value -and -not $Optional) {\n Write-Error (Get-Msg \"prompt.required_empty\" -f $PromptText)\n }\n\n return $value\n}\n\n# Load current parameter values from env file for upgrade mode display\nfunction Load-CurrentParamsFromEnv {\n $envFile = $script:AGENTTEAMS_ENV_FILE\n if (Test-Path $envFile) {\n $content = Get-Content $envFile\n $content | ForEach-Object {\n if ($_ -match \"^AGENTTEAMS_LLM_PROVIDER=(.*)$\") {\n $script:config.LLM_PROVIDER = $Matches[1].Trim()\n }\n if ($_ -match \"^AGENTTEAMS_OPENAI_BASE_URL=(.*)$\") {\n $script:config.OPENAI_BASE_URL = $Matches[1].Trim()\n }\n if ($_ -match \"^AGENTTEAMS_DEFAULT_MODEL=(.*)$\") {\n $script:config.DEFAULT_MODEL = $Matches[1].Trim()\n }\n if ($_ -match \"^AGENTTEAMS_EMBEDDING_MODEL=(.*)$\") {\n $script:config.EMBEDDING_MODEL = $Matches[1].Trim()\n }\n if ($_ -match \"^AGENTTEAMS_WORKSPACE_DIR=(.*)$\") {\n $script:config.WORKSPACE_DIR = $Matches[1].Trim()\n }\n if ($_ -match \"^AGENTTEAMS_HOST_SHARE_DIR=(.*)$\") {\n $script:config.HOST_SHARE_DIR = $Matches[1].Trim()\n }\n }\n }\n}\n\n# ============================================================\n# OpenAI-Compatible Provider\n# ============================================================\n\nfunction Test-LlmConnectivity {\n param(\n [string]$BaseUrl,\n [string]$ApiKey,\n [string]$Model,\n [string]$Hint = \"\"\n )\n Write-Log (Get-Msg \"llm.openai.test.testing\")\n $uri = ($BaseUrl.TrimEnd('/')) + \"/chat/completions\"\n $bodyHash = @{\n model = $Model\n messages = @(@{ role = \"user\"; content = \"hi\" })\n }\n $body = $bodyHash | ConvertTo-Json -Compress\n try {\n $response = Invoke-WebRequest -Uri $uri -Method POST `\n -Headers @{ \"Authorization\" = \"Bearer $ApiKey\"; \"Content-Type\" = \"application/json\"; \"User-Agent\" = \"AgentTeams/$($script:AGENTTEAMS_VERSION)\" } `\n -Body $body -TimeoutSec 30 -ErrorAction Stop -UseBasicParsing\n Write-Log (Get-Msg \"llm.openai.test.ok\")\n } catch {\n $statusCode = if ($_.Exception.Response) { [int]$_.Exception.Response.StatusCode } else { 0 }\n $responseBody = \"\"\n if ($_.Exception.Response) {\n try {\n $stream = $_.Exception.Response.GetResponseStream()\n $reader = New-Object System.IO.StreamReader($stream)\n $responseBody = $reader.ReadToEnd()\n } catch {}\n }\n Write-Host (Get-Msg \"llm.openai.test.fail\" -f $statusCode, $responseBody) -ForegroundColor Yellow\n if ($Hint) {\n Write-Host $Hint -ForegroundColor Yellow\n }\n if (-not $script:AGENTTEAMS_NON_INTERACTIVE) {\n $confirm = Read-Host (Get-Msg \"llm.openai.test.confirm\")\n if ($confirm -eq \"b\" -or $confirm -eq \"B\") {\n $script:StepResult = \"back\"\n return\n }\n if ($confirm -ne \"y\" -and $confirm -ne \"Y\") {\n Write-Log (Get-Msg \"llm.openai.test.aborted\")\n Exit-Script 1\n }\n }\n }\n}\n\nfunction Test-EmbeddingConnectivity {\n param(\n [string]$BaseUrl,\n [string]$ApiKey,\n [string]$Model\n )\n Write-Log (Get-Msg \"llm.embedding.test.testing\")\n $uri = ($BaseUrl.TrimEnd('/')) + \"/embeddings\"\n $bodyHash = @{\n model = $Model\n input = \"test\"\n }\n $body = $bodyHash | ConvertTo-Json -Compress\n try {\n $response = Invoke-WebRequest -Uri $uri -Method POST `\n -Headers @{ \"Authorization\" = \"Bearer $ApiKey\"; \"Content-Type\" = \"application/json\"; \"User-Agent\" = \"AgentTeams/$($script:AGENTTEAMS_VERSION)\" } `\n -Body $body -TimeoutSec 30 -ErrorAction Stop -UseBasicParsing\n Write-Log (Get-Msg \"llm.embedding.test.ok\")\n return $true\n } catch {\n $statusCode = if ($_.Exception.Response) { [int]$_.Exception.Response.StatusCode } else { 0 }\n $responseBody = \"\"\n if ($_.Exception.Response) {\n try {\n $stream = $_.Exception.Response.GetResponseStream()\n $reader = New-Object System.IO.StreamReader($stream)\n $responseBody = $reader.ReadToEnd()\n } catch {}\n }\n Write-Host (Get-Msg \"llm.embedding.test.fail\" -f $statusCode, $responseBody) -ForegroundColor Yellow\n return $false\n }\n}\n\nfunction New-OpenAICompatProvider {\n param(\n [string]$BaseUrl,\n [string]$ApiKey,\n [int]$ConsolePort = 18001\n )\n\n if (-not $BaseUrl -or -not $ApiKey) {\n Write-Log (Get-Msg \"install.openai_compat.missing\")\n return $false\n }\n\n $consoleUrl = \"http://localhost:$ConsolePort\"\n\n # Parse base URL\n $protocol = \"https\"\n $port = 443\n $urlWithoutProto = $BaseUrl -replace \"^https?://\", \"\"\n\n if ($BaseUrl -match \"^http://\") {\n $protocol = \"http\"\n $port = 80\n }\n\n $domain = $urlWithoutProto.Split(\"/\")[0]\n\n if ($domain -match \":(\\d+)$\") {\n $port = [int]$Matches[1]\n $domain = $domain -replace \":\\d+$\", \"\"\n }\n\n Write-Log (Get-Msg \"install.openai_compat.creating\")\n Write-Log (Get-Msg \"install.openai_compat.domain\" -f $domain)\n Write-Log (Get-Msg \"install.openai_compat.port\" -f $port)\n Write-Log (Get-Msg \"install.openai_compat.protocol\" -f $protocol)\n\n $serviceName = \"openai-compat\"\n\n # Create DNS service source\n $serviceBody = @{\n type = \"dns\"\n name = $serviceName\n port = $port.ToString()\n protocol = $protocol\n proxyName = \"\"\n domain = $domain\n } | ConvertTo-Json -Compress\n\n try {\n Invoke-RestMethod -Uri \"$consoleUrl/v1/service-sources\" -Method POST -ContentType \"application/json\" -Body $serviceBody -ErrorAction SilentlyContinue | Out-Null\n }\n catch {\n Write-Log (Get-Msg \"install.openai_compat.service_fail\")\n }\n\n Start-Sleep -Seconds 2\n\n # Create AI provider\n $providerBody = @{\n type = \"openai\"\n name = \"openai-compat\"\n tokens = @($ApiKey)\n version = 0\n protocol = \"openai/v1\"\n tokenFailoverConfig = @{ enabled = $false }\n rawConfigs = @{\n openaiCustomUrl = $BaseUrl\n openaiCustomServiceName = \"$serviceName.dns\"\n openaiCustomServicePort = $port\n }\n } | ConvertTo-Json -Compress -Depth 3\n\n try {\n Invoke-RestMethod -Uri \"$consoleUrl/v1/ai/providers\" -Method POST -ContentType \"application/json\" -Body $providerBody -ErrorAction SilentlyContinue | Out-Null\n Write-Log (Get-Msg \"install.openai_compat.success\")\n return $true\n }\n catch {\n Write-Log (Get-Msg \"install.openai_compat.provider_fail\")\n return $false\n }\n}\n\n# ============================================================\n# State Machine Helpers\n# ============================================================\n\nfunction Test-ShouldSkipStep {\n param([string]$StepFn)\n switch ($StepFn) {\n { $_ -in @(\"Step-Lang\", \"Step-Mode\") } {\n return $script:AGENTTEAMS_NON_INTERACTIVE\n }\n \"Step-Existing\" {\n return (-not (Test-Path $script:AGENTTEAMS_ENV_FILE))\n }\n # Keep-All upgrade mode: skip all config steps, values are already loaded\n { $_ -in @(\"Step-Llm\", \"Step-Admin\", \"Step-Network\", \"Step-Ports\", \"Step-Domains\",\n \"Step-Github\", \"Step-Skills\",\n \"Step-Runtime\", \"Step-ManagerRuntime\", \"Step-E2ee\", \"Step-DockerProxy\",\n \"Step-Idle\", \"Step-Hostshare\") } {\n if ($script:AGENTTEAMS_UPGRADE -and $env:AGENTTEAMS_UPGRADE_KEEP_ALL -eq \"1\") { return $true }\n return $false\n }\n { $_ -in @(\"Step-Volume\", \"Step-Workspace\") } {\n if ($script:AGENTTEAMS_UPGRADE -and $env:AGENTTEAMS_UPGRADE_KEEP_ALL -eq \"1\") { return $true }\n if ($script:AGENTTEAMS_NON_INTERACTIVE) { return $true }\n if ($script:AGENTTEAMS_QUICKSTART) { return $true }\n return $false\n }\n { $_ -in @(\"Step-E2ee\", \"Step-Idle\") } {\n if ($script:AGENTTEAMS_NON_INTERACTIVE) { return $true }\n if ($script:AGENTTEAMS_QUICKSTART -and -not $script:AGENTTEAMS_UPGRADE) { return $true }\n return $false\n }\n \"Step-DockerProxy\" {\n # Embedded controller IS the proxy/orchestrator — no separate proxy step needed.\n if ($script:AGENTTEAMS_USE_EMBEDDED -eq \"1\") { return $true }\n if ($script:AGENTTEAMS_NON_INTERACTIVE) { return $true }\n if ($script:AGENTTEAMS_QUICKSTART -and -not $script:AGENTTEAMS_UPGRADE) { return $true }\n return $false\n }\n \"Step-ManagerRuntime\" {\n if ($script:AGENTTEAMS_NON_INTERACTIVE) { return $true }\n return $false\n }\n \"Step-Hostshare\" {\n if ($script:AGENTTEAMS_NON_INTERACTIVE) { return $true }\n if ($script:AGENTTEAMS_QUICKSTART) { return $true }\n return $false\n }\n default { return $false }\n }\n}\n\nfunction Clear-StepVars {\n param([string]$StepFn)\n switch ($StepFn) {\n \"Step-Mode\" { $script:AGENTTEAMS_QUICKSTART = $false }\n \"Step-Existing\" {\n $script:AGENTTEAMS_UPGRADE = $false\n $script:UPGRADE_EXISTING_WORKERS = $null\n }\n \"Step-Llm\" {\n foreach ($k in @(\"AGENTTEAMS_LLM_PROVIDER\",\"AGENTTEAMS_DEFAULT_MODEL\",\"AGENTTEAMS_OPENAI_BASE_URL\",\n \"AGENTTEAMS_LLM_API_KEY\",\"AGENTTEAMS_MODEL_CONTEXT_WINDOW\",\"AGENTTEAMS_MODEL_MAX_TOKENS\",\n \"AGENTTEAMS_MODEL_REASONING\",\"AGENTTEAMS_MODEL_VISION\")) {\n [Environment]::SetEnvironmentVariable($k, $null, \"Process\")\n }\n }\n \"Step-Admin\" {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_ADMIN_USER\", $null, \"Process\")\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_ADMIN_PASSWORD\", $null, \"Process\")\n }\n \"Step-Network\" {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_LOCAL_ONLY\", $null, \"Process\")\n }\n \"Step-Ports\" {\n foreach ($k in @(\"AGENTTEAMS_PORT_GATEWAY\",\"AGENTTEAMS_PORT_CONSOLE\",\"AGENTTEAMS_PORT_ELEMENT_WEB\",\"AGENTTEAMS_PORT_MANAGER_CONSOLE\")) {\n [Environment]::SetEnvironmentVariable($k, $null, \"Process\")\n }\n }\n \"Step-Domains\" {\n foreach ($k in @(\"AGENTTEAMS_MATRIX_DOMAIN\",\"AGENTTEAMS_MATRIX_CLIENT_DOMAIN\",\"AGENTTEAMS_AI_GATEWAY_DOMAIN\",\"AGENTTEAMS_FS_DOMAIN\",\"AGENTTEAMS_CONSOLE_DOMAIN\")) {\n [Environment]::SetEnvironmentVariable($k, $null, \"Process\")\n }\n }\n \"Step-Github\" {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_GITHUB_TOKEN\", $null, \"Process\")\n }\n \"Step-Skills\" {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_SKILLS_API_URL\", $null, \"Process\")\n }\n \"Step-Volume\" {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_DATA_DIR\", $null, \"Process\")\n }\n \"Step-Workspace\" {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_WORKSPACE_DIR\", $null, \"Process\")\n }\n \"Step-Runtime\" {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_DEFAULT_WORKER_RUNTIME\", $null, \"Process\")\n }\n \"Step-ManagerRuntime\" {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_MANAGER_RUNTIME\", $null, \"Process\")\n }\n \"Step-E2ee\" {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_MATRIX_E2EE\", $null, \"Process\")\n }\n \"Step-DockerProxy\" {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_DOCKER_PROXY\", $null, \"Process\")\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_PROXY_ALLOWED_REGISTRIES\", $null, \"Process\")\n }\n \"Step-Idle\" {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_WORKER_IDLE_TIMEOUT\", $null, \"Process\")\n }\n \"Step-Hostshare\" {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_HOST_SHARE_DIR\", $null, \"Process\")\n }\n }\n}\n\n# ============================================================\n# Individual step functions\n# ============================================================\n\nfunction Step-Lang {\n $langDefaultChoice = if ($script:AGENTTEAMS_LANGUAGE -eq \"zh\") { \"1\" } else { \"2\" }\n $langDetectedKey = \"lang.detected.$($script:AGENTTEAMS_LANGUAGE)\"\n Write-Log (Get-Msg $langDetectedKey)\n Write-Log (Get-Msg \"lang.switch_title\")\n Write-Host (Get-Msg \"lang.option_zh\")\n Write-Host (Get-Msg \"lang.option_en\")\n Write-Host \"\"\n $langChoice = Read-Host \"$(Get-Msg 'lang.prompt') [$langDefaultChoice]\"\n if (-not $langChoice) { $langChoice = $langDefaultChoice }\n if ($langChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n switch ($langChoice) {\n \"1\" { $script:AGENTTEAMS_LANGUAGE = \"zh\" }\n \"2\" { $script:AGENTTEAMS_LANGUAGE = \"en\" }\n }\n $env:AGENTTEAMS_LANGUAGE = $script:AGENTTEAMS_LANGUAGE\n Write-Log \"\"\n}\n\nfunction Step-Mode {\n Write-Log (Get-Msg \"install.mode.title\")\n Write-Host \"\"\n Write-Host (Get-Msg \"install.mode.choose\")\n Write-Host (Get-Msg \"install.mode.quickstart\")\n Write-Host (Get-Msg \"install.mode.manual\")\n Write-Host \"\"\n $choice = Read-Host (Get-Msg \"install.mode.prompt\")\n $choice = if ($choice) { $choice } else { \"1\" }\n if ($choice -eq \"b\") { $script:StepResult = \"back\"; return }\n switch -Regex ($choice) {\n \"^(1|quick|quickstart)$\" {\n Write-Log (Get-Msg \"install.mode.quickstart_selected\")\n $script:AGENTTEAMS_QUICKSTART = $true\n }\n \"^(2|manual)$\" {\n Write-Log (Get-Msg \"install.mode.manual_selected\")\n $script:AGENTTEAMS_QUICKSTART = $false\n }\n default {\n Write-Log (Get-Msg \"install.mode.invalid\")\n $script:AGENTTEAMS_QUICKSTART = $true\n }\n }\n Write-Log \"\"\n}\n\nfunction Step-Existing {\n # This step is skipped when env file doesn't exist\n Write-Log (Get-Msg \"install.existing.detected\" -f $script:AGENTTEAMS_ENV_FILE)\n\n $runningManager = docker ps --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-manager$\"\n $runningWorkers = docker ps --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-worker-\"\n $existingWorkers = docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-worker-\"\n\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n Write-Log (Get-Msg \"install.existing.upgrade_noninteractive\")\n $upgradeChoice = \"1\"\n } else {\n Write-Host \"\"\n Write-Host (Get-Msg \"install.existing.choose\")\n Write-Host (Get-Msg \"install.existing.upgrade\")\n Write-Host (Get-Msg \"install.existing.reinstall\")\n Write-Host (Get-Msg \"install.existing.cancel\")\n Write-Host \"\"\n $upgradeChoice = Read-Host (Get-Msg \"install.existing.prompt\")\n $upgradeChoice = if ($upgradeChoice) { $upgradeChoice } else { \"1\" }\n if ($upgradeChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n }\n\n switch -Regex ($upgradeChoice) {\n \"^(1|upgrade)$\" {\n $script:AGENTTEAMS_UPGRADE = $true\n Write-Log (Get-Msg \"install.existing.upgrading\")\n\n # Show upgrade mode sub-menu (unless already set via env var)\n if ($env:AGENTTEAMS_UPGRADE_KEEP_ALL -ne \"1\") {\n Write-Host \"\"\n Write-Host (Get-Msg \"upgrade.mode.prompt\")\n Write-Host (Get-Msg \"upgrade.mode.keep_all\")\n Write-Host (Get-Msg \"upgrade.mode.confirm_each\")\n Write-Host (Get-Msg \"upgrade.mode.back\")\n Write-Host \"\"\n $upgradeModeChoice = Read-Host (Get-Msg \"install.existing.prompt\")\n $upgradeModeChoice = if ($upgradeModeChoice) { $upgradeModeChoice } else { \"1\" }\n switch -Regex ($upgradeModeChoice) {\n \"^(1|keep)$\" {\n $env:AGENTTEAMS_UPGRADE_KEEP_ALL = \"1\"\n }\n \"^(2|confirm)$\" {\n $env:AGENTTEAMS_UPGRADE_KEEP_ALL = \"0\"\n }\n \"^(3|b)$\" {\n $script:StepResult = \"back\"\n return\n }\n default {\n $env:AGENTTEAMS_UPGRADE_KEEP_ALL = \"0\"\n }\n }\n }\n # Load current parameters for both Keep-All and confirm-each modes\n Load-CurrentParamsFromEnv\n\n if ($runningManager -or $runningWorkers) {\n Write-Host \"\"\n Write-Host \"$($script:ESC)[33m$(Get-Msg 'install.existing.warn_manager_stop')$($script:ESC)[0m\"\n if ($existingWorkers) {\n Write-Host \"$($script:ESC)[33m$(Get-Msg 'install.existing.warn_worker_recreate')$($script:ESC)[0m\"\n }\n if (-not $script:AGENTTEAMS_NON_INTERACTIVE) {\n $confirm = Read-Host (Get-Msg \"install.existing.continue_prompt\")\n if ($confirm -eq \"b\" -or $confirm -eq \"B\") { $script:StepResult = \"back\"; return }\n if ($confirm -ne \"y\" -and $confirm -ne \"Y\") {\n Write-Log (Get-Msg \"install.existing.cancelled\")\n exit 0\n }\n }\n }\n $script:UPGRADE_EXISTING_WORKERS = $existingWorkers\n }\n \"^(2|reinstall)$\" {\n Write-Log (Get-Msg \"install.reinstall.performing\")\n $existingWorkspace = \"$env:USERPROFILE\\agentteams-manager\"\n if (Test-Path $script:AGENTTEAMS_ENV_FILE) {\n $envContent = Get-Content $script:AGENTTEAMS_ENV_FILE\n $wsLine = $envContent | Select-String \"^AGENTTEAMS_WORKSPACE_DIR=\"\n if ($wsLine) {\n $existingWorkspace = $wsLine.Line.Substring(21)\n }\n }\n Write-Host \"\"\n Write-Host \"$($script:ESC)[33m$(Get-Msg 'install.reinstall.warn_stop')$($script:ESC)[0m\"\n if ($runningManager) { Write-Host \"$($script:ESC)[33m - agentteams-manager (manager)$($script:ESC)[0m\" }\n $runningWorkers | ForEach-Object { Write-Host \"$($script:ESC)[33m - $_ (worker)$($script:ESC)[0m\" }\n Write-Host \"\"\n Write-Host \"$($script:ESC)[31m$(Get-Msg 'install.reinstall.warn_delete')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[31m$(Get-Msg 'install.reinstall.warn_volume')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[31m$(Get-Msg 'install.reinstall.warn_env' -f $script:AGENTTEAMS_ENV_FILE)$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[31m$(Get-Msg 'install.reinstall.warn_workspace' -f $existingWorkspace)$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[31m$(Get-Msg 'install.reinstall.warn_workers')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[31m$(Get-Msg 'install.reinstall.warn_proxy')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[31m$(Get-Msg 'install.reinstall.warn_network')$($script:ESC)[0m\"\n Write-Host \"\"\n Write-Host \"$($script:ESC)[31m$(Get-Msg 'install.reinstall.confirm_type')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[31m $existingWorkspace$($script:ESC)[0m\"\n Write-Host \"\"\n $confirmPath = Read-Host (Get-Msg \"install.reinstall.confirm_path\")\n if ($confirmPath -ne $existingWorkspace) {\n Write-Error (Get-Msg \"install.reinstall.path_mismatch\" -f $confirmPath, $existingWorkspace)\n }\n Write-Log (Get-Msg \"install.reinstall.confirmed\")\n docker stop agentteams-manager *>$null\n docker rm agentteams-manager *>$null\n docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-worker-\" | ForEach-Object {\n docker stop $_ *>$null\n docker rm $_ *>$null\n Write-Log (Get-Msg \"install.reinstall.removed_worker\" -f $_)\n }\n $existingController = docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-controller$\"\n if ($existingController) {\n Write-Log (Get-Msg \"install.reinstall.removing_proxy\")\n docker stop agentteams-controller *>$null\n docker rm agentteams-controller *>$null\n }\n if (docker volume ls -q 2>$null | Select-String \"^agentteams-data$\") {\n Write-Log (Get-Msg \"install.reinstall.removing_volume\")\n docker volume rm agentteams-data *>$null\n }\n if (Test-Path $existingWorkspace) {\n Write-Log (Get-Msg \"install.reinstall.removing_workspace\" -f $existingWorkspace)\n Remove-Item -Recurse -Force $existingWorkspace\n }\n if (Test-Path $script:AGENTTEAMS_ENV_FILE) {\n Write-Log (Get-Msg \"install.reinstall.removing_env\" -f $script:AGENTTEAMS_ENV_FILE)\n Remove-Item -Force $script:AGENTTEAMS_ENV_FILE\n }\n $existingNetwork = docker network ls --format \"{{.Name}}\" 2>$null | Select-String \"^agentteams-net$\"\n if ($existingNetwork) {\n Write-Log (Get-Msg \"install.reinstall.removing_network\")\n docker network rm agentteams-net *>$null\n }\n Write-Log (Get-Msg \"install.reinstall.cleanup_done\")\n }\n default {\n Write-Log (Get-Msg \"install.existing.cancelled\")\n exit 0\n }\n }\n\n # Load existing env file (upgrade path)\n if (Test-Path $script:AGENTTEAMS_ENV_FILE) {\n Write-Log (Get-Msg \"install.loading_config\" -f $script:AGENTTEAMS_ENV_FILE)\n Get-Content $script:AGENTTEAMS_ENV_FILE | ForEach-Object {\n if ($_ -match \"^([^#=][^=]*)=(.*)$\") {\n $key = $Matches[1].Trim()\n $value = $Matches[2].Split(\"#\")[0].Trim()\n if (-not [Environment]::GetEnvironmentVariable($key)) {\n [Environment]::SetEnvironmentVariable($key, $value, \"Process\")\n }\n }\n }\n }\n}\n\nfunction Step-Llm {\n Write-Log (Get-Msg \"llm.title\")\n\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n if ($script:AGENTTEAMS_LANGUAGE -eq \"zh\") {\n $script:config.LLM_PROVIDER = if ($env:AGENTTEAMS_LLM_PROVIDER) { $env:AGENTTEAMS_LLM_PROVIDER } else { \"openai-compat\" }\n $script:config.DEFAULT_MODEL = if ($env:AGENTTEAMS_DEFAULT_MODEL) { $env:AGENTTEAMS_DEFAULT_MODEL } else { \"qwen3.6-plus\" }\n $script:config.OPENAI_BASE_URL = if ($env:AGENTTEAMS_OPENAI_BASE_URL) { $env:AGENTTEAMS_OPENAI_BASE_URL } else { \"https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1\" }\n Write-Log (Get-Msg \"llm.provider.label\" -f $script:config.LLM_PROVIDER)\n Write-Log (Get-Msg \"llm.openai.base_url_label\" -f $script:config.OPENAI_BASE_URL)\n } else {\n $script:config.LLM_PROVIDER = if ($env:AGENTTEAMS_LLM_PROVIDER) { $env:AGENTTEAMS_LLM_PROVIDER } else { \"qwen\" }\n $script:config.DEFAULT_MODEL = if ($env:AGENTTEAMS_DEFAULT_MODEL) { $env:AGENTTEAMS_DEFAULT_MODEL } else { \"qwen3.6-plus\" }\n $script:config.OPENAI_BASE_URL = if ($env:AGENTTEAMS_OPENAI_BASE_URL) { $env:AGENTTEAMS_OPENAI_BASE_URL } else { \"\" }\n Write-Log (Get-Msg \"llm.provider.qwen_default\" -f $script:config.LLM_PROVIDER)\n }\n Write-Log (Get-Msg \"llm.model.label\" -f $script:config.DEFAULT_MODEL)\n Write-Log \"\"\n $script:config.LLM_API_KEY = Read-Prompt -VarName \"AGENTTEAMS_LLM_API_KEY\" -PromptText (Get-Msg \"llm.apikey_prompt\") -Secret\n $script:config.EMBEDDING_MODEL = if ($null -ne $env:AGENTTEAMS_EMBEDDING_MODEL) { $env:AGENTTEAMS_EMBEDDING_MODEL } else { \"text-embedding-v4\" }\n return\n }\n\n Write-Host \"\"\n Write-Host (Get-Msg \"llm.providers_title\")\n Write-Host (Get-Msg \"llm.provider.alibaba\")\n Write-Host (Get-Msg \"llm.provider.openai_compat\")\n Write-Host \"\"\n\n # If upgrade mode with loaded provider, show current as default\n if ($script:AGENTTEAMS_UPGRADE -and $script:config.LLM_PROVIDER) {\n $defaultProvider = if ($script:config.LLM_PROVIDER -eq \"openai-compat\") { \"2\" } else { \"1\" }\n $providerChoice = Read-Host \"$(Get-Msg 'llm.provider.select') [${defaultProvider}]\"\n $providerChoice = if ($providerChoice) { $providerChoice } else { $defaultProvider }\n } elseif ($script:AGENTTEAMS_QUICKSTART) {\n $providerChoice = Read-Host \"$(Get-Msg 'llm.provider.select') [1]\"\n } else {\n $providerChoice = Read-Host (Get-Msg \"llm.provider.select\")\n $providerChoice = if ($providerChoice) { $providerChoice } else { \"1\" }\n }\n if ($providerChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n\n switch -Regex ($providerChoice) {\n \"^(1|alibaba-cloud)$\" {\n $alibabaAccess = $null\n if ($script:AGENTTEAMS_LANGUAGE -eq \"en\") {\n $script:config.LLM_PROVIDER = \"openai-compat\"\n $script:config.OPENAI_BASE_URL = \"https://dashscope-intl.aliyuncs.com/compatible-mode/v1\"\n $alibabaAccess = \"tokenplan\"\n Write-Host \"\"\n Write-Host (Get-Msg \"llm.codingplan.models_title\")\n Write-Host (Get-Msg \"llm.codingplan.model.qwen36plus\")\n Write-Host (Get-Msg \"llm.codingplan.model.glm5\")\n Write-Host (Get-Msg \"llm.codingplan.model.kimi\")\n Write-Host (Get-Msg \"llm.codingplan.model.minimax\")\n Write-Host \"\"\n # If upgrade with loaded model, show current as default\n if ($script:AGENTTEAMS_UPGRADE -and $script:config.DEFAULT_MODEL) {\n $defaultModel = switch ($script:config.DEFAULT_MODEL) {\n \"qwen3.6-plus\" { \"1\" }\n \"glm-5\" { \"2\" }\n \"kimi-k2.5\" { \"3\" }\n \"MiniMax-M2.5\" { \"4\" }\n default { \"1\" }\n }\n $codingPlanModelChoice = Read-Host \"$(Get-Msg 'llm.codingplan.model.select') [${defaultModel}]\"\n $codingPlanModelChoice = if ($codingPlanModelChoice) { $codingPlanModelChoice } else { $defaultModel }\n } elseif ($script:AGENTTEAMS_QUICKSTART) {\n $codingPlanModelChoice = Read-Host \"$(Get-Msg 'llm.codingplan.model.select') [1]\"\n } else {\n $codingPlanModelChoice = Read-Host (Get-Msg \"llm.codingplan.model.select\")\n }\n $codingPlanModelChoice = if ($codingPlanModelChoice) { $codingPlanModelChoice } else { \"1\" }\n if ($codingPlanModelChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n switch -Regex ($codingPlanModelChoice) {\n \"^(1|qwen3\\.6-plus)$\" { $script:config.DEFAULT_MODEL = \"qwen3.6-plus\" }\n \"^(2|glm-5)$\" { $script:config.DEFAULT_MODEL = \"glm-5\" }\n \"^(3|kimi-k2\\.5)$\" { $script:config.DEFAULT_MODEL = \"kimi-k2.5\" }\n \"^(4|MiniMax-M2\\.5)$\" { $script:config.DEFAULT_MODEL = \"MiniMax-M2.5\" }\n default { $script:config.DEFAULT_MODEL = \"qwen3.6-plus\" }\n }\n Write-Log (Get-Msg \"llm.provider.selected_codingplan\")\n } else {\n Write-Host \"\"\n Write-Host (Get-Msg \"llm.alibaba.models_title\")\n Write-Host (Get-Msg \"llm.alibaba.model.tokenplan\")\n Write-Host (Get-Msg \"llm.alibaba.model.bailian\")\n Write-Host (Get-Msg \"llm.alibaba.model.codingplan_legacy\")\n Write-Host \"\"\n $modelChoice = if ($script:AGENTTEAMS_QUICKSTART) {\n Read-Host \"$(Get-Msg 'llm.alibaba.model.select') [1]\"\n } else {\n Read-Host (Get-Msg \"llm.alibaba.model.select\")\n }\n $modelChoice = if ($modelChoice) { $modelChoice } else { \"1\" }\n if ($modelChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n\n if ($modelChoice -match \"^(1|token-plan|tokenplan)$\") {\n $alibabaAccess = \"tokenplan\"\n $script:config.LLM_PROVIDER = \"openai-compat\"\n $script:config.OPENAI_BASE_URL = \"https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1\"\n Write-Host \"\"\n Write-Host (Get-Msg \"llm.codingplan.models_title\")\n Write-Host (Get-Msg \"llm.codingplan.model.qwen36plus\")\n Write-Host (Get-Msg \"llm.codingplan.model.glm5\")\n Write-Host (Get-Msg \"llm.codingplan.model.kimi\")\n Write-Host (Get-Msg \"llm.codingplan.model.minimax\")\n Write-Host \"\"\n # If upgrade with loaded model, show current as default\n if ($script:AGENTTEAMS_UPGRADE -and $script:config.DEFAULT_MODEL) {\n $defaultModel = switch ($script:config.DEFAULT_MODEL) {\n \"qwen3.6-plus\" { \"1\" }\n \"glm-5\" { \"2\" }\n \"kimi-k2.5\" { \"3\" }\n \"MiniMax-M2.5\" { \"4\" }\n default { \"1\" }\n }\n $codingPlanModelChoice = Read-Host \"$(Get-Msg 'llm.codingplan.model.select') [${defaultModel}]\"\n $codingPlanModelChoice = if ($codingPlanModelChoice) { $codingPlanModelChoice } else { $defaultModel }\n } elseif ($script:AGENTTEAMS_QUICKSTART) {\n $codingPlanModelChoice = Read-Host \"$(Get-Msg 'llm.codingplan.model.select') [1]\"\n } else {\n $codingPlanModelChoice = Read-Host (Get-Msg \"llm.codingplan.model.select\")\n }\n $codingPlanModelChoice = if ($codingPlanModelChoice) { $codingPlanModelChoice } else { \"1\" }\n if ($codingPlanModelChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n switch -Regex ($codingPlanModelChoice) {\n \"^(1|qwen3\\.6-plus)$\" { $script:config.DEFAULT_MODEL = \"qwen3.6-plus\" }\n \"^(2|glm-5)$\" { $script:config.DEFAULT_MODEL = \"glm-5\" }\n \"^(3|kimi-k2\\.5)$\" { $script:config.DEFAULT_MODEL = \"kimi-k2.5\" }\n \"^(4|MiniMax-M2\\.5)$\" { $script:config.DEFAULT_MODEL = \"MiniMax-M2.5\" }\n default { $script:config.DEFAULT_MODEL = \"qwen3.6-plus\" }\n }\n Write-Log (Get-Msg \"llm.provider.selected_tokenplan\")\n } elseif ($modelChoice -match \"^(2|qwen|bailian)$\") {\n $alibabaAccess = \"bailian\"\n $script:config.LLM_PROVIDER = \"qwen\"\n $script:config.OPENAI_BASE_URL = \"\"\n Write-Host \"\"\n if ($script:AGENTTEAMS_UPGRADE -and $script:config.DEFAULT_MODEL) {\n $qwenModelInput = Read-Host \"$(Get-Msg 'llm.qwen.model_prompt') [${script:config.DEFAULT_MODEL}]\"\n } else {\n $qwenModelInput = Read-Host (Get-Msg \"llm.qwen.model_prompt\")\n }\n if ($qwenModelInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $script:config.DEFAULT_MODEL = if ($qwenModelInput) { $qwenModelInput } elseif ($script:config.DEFAULT_MODEL) { $script:config.DEFAULT_MODEL } else { \"qwen3.6-plus\" }\n Write-Log (Get-Msg \"llm.provider.selected_qwen\")\n Request-CustomModelParams $script:config.DEFAULT_MODEL\n if ($script:StepResult -eq \"back\") { return }\n } elseif ($modelChoice -match \"^(3|coding-plan|codingplan)$\") {\n $alibabaAccess = \"codingplan_legacy\"\n $script:config.LLM_PROVIDER = \"openai-compat\"\n $script:config.OPENAI_BASE_URL = \"https://coding.dashscope.aliyuncs.com/v1\"\n Write-Host \"\"\n if ($script:AGENTTEAMS_UPGRADE -and $script:config.DEFAULT_MODEL) {\n $codingModelInput = Read-Host \"$(Get-Msg 'llm.qwen.model_prompt') [${script:config.DEFAULT_MODEL}]\"\n } else {\n $codingModelInput = Read-Host (Get-Msg \"llm.qwen.model_prompt\")\n }\n if ($codingModelInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $script:config.DEFAULT_MODEL = if ($codingModelInput) { $codingModelInput } elseif ($script:config.DEFAULT_MODEL) { $script:config.DEFAULT_MODEL } else { \"qwen3.6-plus\" }\n Write-Log (Get-Msg \"llm.provider.selected_codingplan_legacy\")\n Request-CustomModelParams $script:config.DEFAULT_MODEL\n if ($script:StepResult -eq \"back\") { return }\n } else {\n Write-Error (Get-Msg \"llm.alibaba.model.invalid\" -f $modelChoice)\n }\n }\n\n Write-Log (Get-Msg \"llm.model.label\" -f $script:config.DEFAULT_MODEL)\n Write-Log \"\"\n if ($alibabaAccess -eq \"bailian\") {\n Write-Log (Get-Msg \"llm.apikey_hint_bailian\")\n Write-Log (Get-Msg \"llm.apikey_url_bailian\")\n } elseif ($script:AGENTTEAMS_LANGUAGE -eq \"en\") {\n Write-Log (Get-Msg \"llm.apikey_hint_qwencloud\")\n Write-Log (Get-Msg \"llm.apikey_url_qwencloud\")\n } elseif ($alibabaAccess -eq \"codingplan_legacy\") {\n Write-Log (Get-Msg \"llm.apikey_hint_codingplan\")\n Write-Log (Get-Msg \"llm.apikey_url_codingplan\")\n } else {\n Write-Log (Get-Msg \"llm.apikey_hint_tokenplan\")\n Write-Log (Get-Msg \"llm.apikey_url_tokenplan\")\n }\n Write-Log \"\"\n $script:config.LLM_API_KEY = Read-Prompt -VarName \"AGENTTEAMS_LLM_API_KEY\" -PromptText (Get-Msg \"llm.apikey_prompt\") -Secret\n if ($script:StepResult -eq \"back\") { return }\n if ($alibabaAccess -eq \"bailian\") {\n Test-LlmConnectivity -BaseUrl \"https://dashscope.aliyuncs.com/compatible-mode/v1\" -ApiKey $script:config.LLM_API_KEY -Model $script:config.DEFAULT_MODEL\n } elseif ($alibabaAccess -eq \"codingplan_legacy\") {\n Test-LlmConnectivity -BaseUrl \"https://coding.dashscope.aliyuncs.com/v1\" -ApiKey $script:config.LLM_API_KEY -Model $script:config.DEFAULT_MODEL -Hint (Get-Msg \"llm.openai.test.fail.codingplan_legacy\")\n } elseif ($script:AGENTTEAMS_LANGUAGE -eq \"en\") {\n Test-LlmConnectivity -BaseUrl $script:config.OPENAI_BASE_URL -ApiKey $script:config.LLM_API_KEY -Model $script:config.DEFAULT_MODEL -Hint (Get-Msg \"llm.openai.test.fail.codingplan\")\n } else {\n Test-LlmConnectivity -BaseUrl $script:config.OPENAI_BASE_URL -ApiKey $script:config.LLM_API_KEY -Model $script:config.DEFAULT_MODEL -Hint (Get-Msg \"llm.openai.test.fail.tokenplan\")\n }\n if ($script:StepResult -eq \"back\") { return }\n }\n \"^(2|openai-compat)$\" {\n $script:config.LLM_PROVIDER = \"openai-compat\"\n Write-Log (Get-Msg \"llm.provider.selected_openai\" -f $script:config.LLM_PROVIDER)\n Write-Host \"\"\n if ($script:AGENTTEAMS_UPGRADE -and $env:AGENTTEAMS_UPGRADE_KEEP_ALL -eq \"1\") {\n Write-Log (Get-Msg \"prompt.upgrade_keep\" -f (Get-Msg \"llm.openai.base_url_prompt\"), $script:config.OPENAI_BASE_URL)\n } else {\n $currentUrl = $script:config.OPENAI_BASE_URL\n # Use base prompt without embedded default when showing current value\n if ($currentUrl) {\n $urlInput = Read-Host \"$(Get-Msg 'llm.openai.base_url_prompt') [$currentUrl]\"\n } else {\n $urlInput = Read-Host \"$(Get-Msg 'llm.openai.base_url_prompt') [https://api.openai.com/v1]\"\n }\n if ($urlInput -eq \"b\") { $script:StepResult = \"back\"; return }\n if ($urlInput) {\n $script:config.OPENAI_BASE_URL = $urlInput\n } elseif ($currentUrl) {\n $script:config.OPENAI_BASE_URL = $currentUrl\n } else {\n $script:config.OPENAI_BASE_URL = \"https://api.openai.com/v1\"\n }\n }\n if ($script:AGENTTEAMS_UPGRADE -and $env:AGENTTEAMS_UPGRADE_KEEP_ALL -eq \"1\") {\n Write-Log (Get-Msg \"prompt.upgrade_keep\" -f (Get-Msg \"llm.openai.model_prompt\"), $script:config.DEFAULT_MODEL)\n } else {\n $currentModel = $script:config.DEFAULT_MODEL\n if ($currentModel) {\n $modelInput = Read-Host \"$(Get-Msg 'llm.openai.model_prompt') [${currentModel}]\"\n } else {\n $modelInput = Read-Host (Get-Msg 'llm.openai.model_prompt')\n }\n if ($modelInput -eq \"b\") { $script:StepResult = \"back\"; return }\n if ($modelInput) {\n $script:config.DEFAULT_MODEL = $modelInput\n } elseif ($currentModel) {\n $script:config.DEFAULT_MODEL = $currentModel\n } else {\n $script:config.DEFAULT_MODEL = \"gpt-5.4\"\n }\n }\n Write-Log (Get-Msg \"llm.openai.base_url_label\" -f $script:config.OPENAI_BASE_URL)\n Write-Log (Get-Msg \"llm.model.label\" -f $script:config.DEFAULT_MODEL)\n Request-CustomModelParams $script:config.DEFAULT_MODEL\n if ($script:StepResult -eq \"back\") { return }\n Write-Log \"\"\n $script:config.LLM_API_KEY = Read-Prompt -VarName \"AGENTTEAMS_LLM_API_KEY\" -PromptText (Get-Msg \"llm.apikey_prompt\") -Secret\n if ($script:StepResult -eq \"back\") { return }\n Test-LlmConnectivity -BaseUrl $script:config.OPENAI_BASE_URL -ApiKey $script:config.LLM_API_KEY -Model $script:config.DEFAULT_MODEL\n if ($script:StepResult -eq \"back\") { return }\n }\n default {\n Write-Error (Get-Msg \"llm.provider.invalid\" -f $providerChoice)\n }\n }\n\n # Skip to embedding if Keep-All mode handled LLM params\n if ($skipToEmbedding) {\n # Skip embedding selection, use loaded value (already in $script:config.EMBEDDING_MODEL)\n if ($script:config.EMBEDDING_MODEL) {\n Write-Log (Get-Msg \"llm.embedding.title\")\n Write-Log (Get-Msg \"llm.model.label\" -f $script:config.EMBEDDING_MODEL)\n Write-Log \"\"\n }\n return\n }\n\n # --- Embedding model (optional, auto-tested) ---\n Write-Host \"\"\n Write-Log (Get-Msg \"llm.embedding.title\")\n Write-Log (Get-Msg \"llm.embedding.hint\")\n Write-Host \"\"\n Write-Host (Get-Msg \"llm.embedding.option.default\")\n Write-Host (Get-Msg \"llm.embedding.option.custom\")\n Write-Host (Get-Msg \"llm.embedding.option.disable\")\n Write-Host \"\"\n $embChoice = Read-Host \"$(Get-Msg 'llm.embedding.select') [1]\"\n $embChoice = if ($embChoice) { $embChoice } else { \"1\" }\n if ($embChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n\n switch ($embChoice) {\n \"1\" {\n $script:config.EMBEDDING_MODEL = \"text-embedding-v4\"\n }\n \"2\" {\n if ($script:AGENTTEAMS_UPGRADE -and $env:AGENTTEAMS_UPGRADE_KEEP_ALL -eq \"1\") {\n Write-Log (Get-Msg \"prompt.upgrade_keep\" -f (Get-Msg \"llm.embedding.custom_prompt\"), $script:config.EMBEDDING_MODEL)\n } else {\n $currentEmb = $script:config.EMBEDDING_MODEL\n $embCustom = Read-Host (Get-Msg \"llm.embedding.custom_prompt\")\n if ($embCustom -eq \"b\") { $script:StepResult = \"back\"; return }\n if ($embCustom) {\n $script:config.EMBEDDING_MODEL = $embCustom\n } else {\n $script:config.EMBEDDING_MODEL = $currentEmb\n if (-not $script:config.EMBEDDING_MODEL) {\n Write-Log (Get-Msg \"llm.embedding.disabled\")\n }\n }\n }\n }\n \"3\" {\n $script:config.EMBEDDING_MODEL = \"\"\n Write-Log (Get-Msg \"llm.embedding.disabled\")\n }\n default {\n $script:config.EMBEDDING_MODEL = \"text-embedding-v4\"\n }\n }\n\n if ($script:config.EMBEDDING_MODEL) {\n # Qwen provider uses dashscope directly; others use OPENAI_BASE_URL\n $embBaseUrl = $script:config.OPENAI_BASE_URL\n if ($script:config.LLM_PROVIDER -eq \"qwen\") {\n $embBaseUrl = \"https://dashscope.aliyuncs.com/compatible-mode/v1\"\n }\n $embResult = Test-EmbeddingConnectivity -BaseUrl $embBaseUrl -ApiKey $script:config.LLM_API_KEY -Model $script:config.EMBEDDING_MODEL\n if (-not $embResult) {\n $script:config.EMBEDDING_MODEL = \"\"\n Write-Log (Get-Msg \"llm.embedding.auto_disabled\")\n }\n }\n\n Write-Log \"\"\n}\n\nfunction Step-Admin {\n Write-Log (Get-Msg \"admin.title\")\n $script:config.ADMIN_USER = Read-Prompt -VarName \"AGENTTEAMS_ADMIN_USER\" -PromptText (Get-Msg \"admin.username_prompt\") -Default \"admin\"\n if ($script:StepResult -eq \"back\") { return }\n $script:config.ADMIN_USER = $script:config.ADMIN_USER.ToLowerInvariant()\n\n # Pre-set via env var: validate; non-interactive fails fast,\n # interactive warns and falls through to the retry prompt.\n if ($env:AGENTTEAMS_ADMIN_PASSWORD) {\n $script:config.ADMIN_PASSWORD = $env:AGENTTEAMS_ADMIN_PASSWORD\n Write-Log (Get-Msg \"prompt.preset\" -f (Get-Msg \"admin.password_prompt\"))\n if ($script:config.ADMIN_PASSWORD.Length -ge 8) {\n Write-Log \"\"\n return\n }\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n Write-Error (Get-Msg \"admin.password_too_short\" -f $script:config.ADMIN_PASSWORD.Length)\n }\n Write-Host \"$($script:ESC)[31m[AgentTeams ERROR]$($script:ESC)[0m $(Get-Msg \"admin.password_too_short\" -f $script:config.ADMIN_PASSWORD.Length)\"\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_ADMIN_PASSWORD\", $null, \"Process\")\n }\n\n while ($true) {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_ADMIN_PASSWORD\", $null, \"Process\")\n $script:config.ADMIN_PASSWORD = Read-Prompt -VarName \"AGENTTEAMS_ADMIN_PASSWORD\" -PromptText (Get-Msg \"admin.password_prompt\") -Secret -Optional\n if ($script:StepResult -eq \"back\") { return }\n if (-not $script:config.ADMIN_PASSWORD) {\n $randomSuffix = (New-RandomKey).Substring(0, 12)\n $script:config.ADMIN_PASSWORD = \"admin$randomSuffix\"\n Write-Log (Get-Msg \"admin.password_generated\")\n break\n }\n if ($script:config.ADMIN_PASSWORD.Length -ge 8) {\n break\n }\n Write-Host \"$($script:ESC)[31m[AgentTeams ERROR]$($script:ESC)[0m $(Get-Msg \"admin.password_too_short\" -f $script:config.ADMIN_PASSWORD.Length)\"\n }\n\n Write-Log \"\"\n}\n\nfunction Step-Network {\n Write-Log (Get-Msg \"port.local_only.title\")\n Write-Host \"\"\n Write-Host \" 1) $(Get-Msg 'port.local_only.hint_yes')\"\n Write-Host \" 2) $(Get-Msg 'port.local_only.hint_no')\"\n Write-Host \"\"\n\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n $localOnly = if ($env:AGENTTEAMS_LOCAL_ONLY) { $env:AGENTTEAMS_LOCAL_ONLY } else { \"1\" }\n } elseif ($null -ne $env:AGENTTEAMS_LOCAL_ONLY) {\n $localOnly = $env:AGENTTEAMS_LOCAL_ONLY\n } else {\n $localChoice = Read-Host \"$(Get-Msg 'port.local_only.choice')\"\n if ($localChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n if (-not $localChoice) { $localChoice = \"1\" }\n $localOnly = if ($localChoice -match '^(2|n|N|no|NO)$') { \"0\" } else { \"1\" }\n }\n $script:config.LOCAL_ONLY = $localOnly\n\n if ($localOnly -eq \"1\") {\n Write-Log (Get-Msg \"port.local_only.selected_local\")\n } else {\n Write-Log (Get-Msg \"port.local_only.selected_external\")\n Write-Host \"\"\n Write-Host (Get-Msg \"port.local_only.https_hint\") -ForegroundColor Yellow\n }\n Write-Log \"\"\n}\n\nfunction Step-Ports {\n Write-Log (Get-Msg \"port.title\")\n $script:config.PORT_GATEWAY = Read-Prompt -VarName \"AGENTTEAMS_PORT_GATEWAY\" -PromptText (Get-Msg \"port.gateway_prompt\") -Default \"18080\"\n if ($script:StepResult -eq \"back\") { return }\n $script:config.PORT_CONSOLE = Read-Prompt -VarName \"AGENTTEAMS_PORT_CONSOLE\" -PromptText (Get-Msg \"port.console_prompt\") -Default \"18001\"\n if ($script:StepResult -eq \"back\") { return }\n $script:config.PORT_ELEMENT_WEB = Read-Prompt -VarName \"AGENTTEAMS_PORT_ELEMENT_WEB\" -PromptText (Get-Msg \"port.element_prompt\") -Default \"18088\"\n if ($script:StepResult -eq \"back\") { return }\n $script:config.PORT_MANAGER_CONSOLE = Read-Prompt -VarName \"AGENTTEAMS_PORT_MANAGER_CONSOLE\" -PromptText (Get-Msg \"port.manager_console_prompt\") -Default \"18888\"\n if ($script:StepResult -eq \"back\") { return }\n Write-Log \"\"\n}\n\nfunction Step-Domains {\n Write-Log (Get-Msg \"domain.title\")\n Write-Log (Get-Msg \"domain.hint\")\n $script:config.MATRIX_DOMAIN = Read-Prompt -VarName \"AGENTTEAMS_MATRIX_DOMAIN\" -PromptText (Get-Msg \"domain.matrix_prompt\") -Default \"matrix-local.agentteams.io:$($script:config.PORT_GATEWAY)\"\n if ($script:StepResult -eq \"back\") { return }\n $script:config.MATRIX_CLIENT_DOMAIN = Read-Prompt -VarName \"AGENTTEAMS_MATRIX_CLIENT_DOMAIN\" -PromptText (Get-Msg \"domain.element_prompt\") -Default \"matrix-client-local.agentteams.io\"\n if ($script:StepResult -eq \"back\") { return }\n $script:config.AI_GATEWAY_DOMAIN = Read-Prompt -VarName \"AGENTTEAMS_AI_GATEWAY_DOMAIN\" -PromptText (Get-Msg \"domain.gateway_prompt\") -Default \"aigw-local.agentteams.io\"\n if ($script:StepResult -eq \"back\") { return }\n $script:config.FS_DOMAIN = Read-Prompt -VarName \"AGENTTEAMS_FS_DOMAIN\" -PromptText (Get-Msg \"domain.fs_prompt\") -Default \"fs-local.agentteams.io\"\n if ($script:StepResult -eq \"back\") { return }\n $script:config.CONSOLE_DOMAIN = Read-Prompt -VarName \"AGENTTEAMS_CONSOLE_DOMAIN\" -PromptText (Get-Msg \"domain.console_prompt\") -Default \"console-local.agentteams.io\"\n if ($script:StepResult -eq \"back\") { return }\n Write-Log \"\"\n}\n\nfunction Step-Github {\n Write-Log (Get-Msg \"github.title\")\n $script:config.GITHUB_TOKEN = Read-Prompt -VarName \"AGENTTEAMS_GITHUB_TOKEN\" -PromptText (Get-Msg \"github.token_prompt\") -Secret -Optional\n}\n\nfunction Step-Skills {\n Write-Log \"\"\n Write-Log (Get-Msg \"skills.title\")\n $script:config.SKILLS_API_URL = Read-Prompt -VarName \"AGENTTEAMS_SKILLS_API_URL\" -PromptText (Get-Msg \"skills.url_prompt\") -Optional\n if ($script:StepResult -eq \"back\") { return }\n Write-Log \"\"\n}\n\nfunction Step-Volume {\n Write-Log (Get-Msg \"data.title\")\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n $script:config.DATA_DIR = if ($env:AGENTTEAMS_DATA_DIR) { $env:AGENTTEAMS_DATA_DIR } else { \"agentteams-data\" }\n Write-Log \" $(Get-Msg 'data.volume_using' -f $script:config.DATA_DIR) (non-interactive, skipped)\"\n return\n }\n # ─────────────────────────────────────────────────────────────────\n $dataDirInput = Read-Host (Get-Msg \"data.volume_prompt\")\n if ($dataDirInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $script:config.DATA_DIR = if ($dataDirInput) { $dataDirInput } else { \"agentteams-data\" }\n Write-Log (Get-Msg \"data.volume_using\" -f $script:config.DATA_DIR)\n}\n\nfunction Step-Workspace {\n Write-Log (Get-Msg \"workspace.title\")\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n $script:config.WORKSPACE_DIR = if ($env:AGENTTEAMS_WORKSPACE_DIR) { $env:AGENTTEAMS_WORKSPACE_DIR } else { \"$env:USERPROFILE\\agentteams-manager\" }\n if (-not (Test-Path $script:config.WORKSPACE_DIR)) {\n New-Item -ItemType Directory -Path $script:config.WORKSPACE_DIR -Force | Out-Null\n }\n Write-Log \" $(Get-Msg 'workspace.dir_label' -f $script:config.WORKSPACE_DIR) (non-interactive, skipped)\"\n return\n }\n # ─────────────────────────────────────────────────────────────────\n $defaultWorkspace = \"$env:USERPROFILE\\agentteams-manager\"\n if ($script:AGENTTEAMS_UPGRADE -and $env:AGENTTEAMS_UPGRADE_KEEP_ALL -eq \"1\") {\n Write-Log (Get-Msg \"prompt.upgrade_keep\" -f (Get-Msg \"workspace.dir_prompt\" -f $defaultWorkspace), $script:config.WORKSPACE_DIR)\n $wsInput = if ($script:config.WORKSPACE_DIR) { $script:config.WORKSPACE_DIR } else { $defaultWorkspace }\n } else {\n $currentWs = $script:config.WORKSPACE_DIR\n $wsInput = Read-Host (Get-Msg \"workspace.dir_prompt\" -f $defaultWorkspace)\n if ($wsInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $wsInput = if ($wsInput) { $wsInput } else { if ($currentWs) { $currentWs } else { $defaultWorkspace } }\n }\n $script:config.WORKSPACE_DIR = $wsInput\n if (-not (Test-Path $script:config.WORKSPACE_DIR)) {\n New-Item -ItemType Directory -Path $script:config.WORKSPACE_DIR -Force | Out-Null\n }\n Write-Log (Get-Msg \"workspace.dir_label\" -f $script:config.WORKSPACE_DIR)\n}\n\nfunction Step-Runtime {\n Write-Log (Get-Msg \"worker_runtime.title\")\n Write-Host \"\"\n Write-Host \" 1) $(Get-Msg 'worker_runtime.copaw')\"\n Write-Host \" 2) $(Get-Msg 'worker_runtime.openclaw')\"\n Write-Host \" 3) $(Get-Msg 'worker_runtime.hermes')\"\n Write-Host \"\"\n\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n $script:config.DEFAULT_WORKER_RUNTIME = if ($env:AGENTTEAMS_DEFAULT_WORKER_RUNTIME) { $env:AGENTTEAMS_DEFAULT_WORKER_RUNTIME } else { \"copaw\" }\n } elseif ($script:AGENTTEAMS_UPGRADE -and $env:AGENTTEAMS_DEFAULT_WORKER_RUNTIME) {\n Write-Log (Get-Msg \"prompt.upgrade_keep\" -f (Get-Msg \"worker_runtime.title_short\"), $env:AGENTTEAMS_DEFAULT_WORKER_RUNTIME)\n $rtChoice = Read-Host (Get-Msg \"worker_runtime.choice\")\n if ($rtChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n if ($rtChoice) {\n $script:config.DEFAULT_WORKER_RUNTIME = switch ($rtChoice) {\n \"2\" { \"openclaw\" }\n \"3\" { \"hermes\" }\n default { \"copaw\" }\n }\n } else {\n $script:config.DEFAULT_WORKER_RUNTIME = $env:AGENTTEAMS_DEFAULT_WORKER_RUNTIME\n }\n } elseif ($env:AGENTTEAMS_DEFAULT_WORKER_RUNTIME) {\n $script:config.DEFAULT_WORKER_RUNTIME = $env:AGENTTEAMS_DEFAULT_WORKER_RUNTIME\n } else {\n $rtChoice = Read-Host (Get-Msg \"worker_runtime.choice\")\n if ($rtChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n $rtChoice = if ($rtChoice) { $rtChoice } else { \"1\" }\n $script:config.DEFAULT_WORKER_RUNTIME = switch ($rtChoice) {\n \"2\" { \"openclaw\" }\n \"3\" { \"hermes\" }\n default { \"copaw\" }\n }\n }\n Write-Log (Get-Msg \"worker_runtime.selected\" -f $script:config.DEFAULT_WORKER_RUNTIME)\n}\n\nfunction Step-ManagerRuntime {\n Write-Log (Get-Msg \"manager_runtime.title\")\n Write-Host \"\"\n Write-Host \" 1) $(Get-Msg 'manager_runtime.copaw')\"\n Write-Host \" 2) $(Get-Msg 'manager_runtime.openclaw')\"\n Write-Host \"\"\n\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n $script:config.MANAGER_RUNTIME = if ($env:AGENTTEAMS_MANAGER_RUNTIME) { $env:AGENTTEAMS_MANAGER_RUNTIME } else { \"copaw\" }\n } elseif ($script:AGENTTEAMS_UPGRADE -and $env:AGENTTEAMS_MANAGER_RUNTIME) {\n Write-Log (Get-Msg \"prompt.upgrade_keep\" -f (Get-Msg \"manager_runtime.title_short\"), $env:AGENTTEAMS_MANAGER_RUNTIME)\n $mrChoice = Read-Host (Get-Msg \"manager_runtime.choice\")\n if ($mrChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n if ($mrChoice) {\n $script:config.MANAGER_RUNTIME = if ($mrChoice -eq \"2\") { \"openclaw\" } else { \"copaw\" }\n } else {\n $script:config.MANAGER_RUNTIME = $env:AGENTTEAMS_MANAGER_RUNTIME\n }\n } elseif ($env:AGENTTEAMS_MANAGER_RUNTIME) {\n $script:config.MANAGER_RUNTIME = $env:AGENTTEAMS_MANAGER_RUNTIME\n } else {\n $mrChoice = Read-Host (Get-Msg \"manager_runtime.choice\")\n if ($mrChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n $mrChoice = if ($mrChoice) { $mrChoice } else { \"1\" }\n $script:config.MANAGER_RUNTIME = if ($mrChoice -eq \"2\") { \"openclaw\" } else { \"copaw\" }\n }\n Write-Log (Get-Msg \"manager_runtime.selected\" -f $script:config.MANAGER_RUNTIME)\n}\n\nfunction Step-E2ee {\n Write-Host \"\"\n Write-Log (Get-Msg \"matrix_e2ee.title\")\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n $script:config.MATRIX_E2EE = if ($env:AGENTTEAMS_MATRIX_E2EE) { $env:AGENTTEAMS_MATRIX_E2EE } else { \"0\" }\n Write-Log \" $(Get-Msg 'matrix_e2ee.title_short') = $($script:config.MATRIX_E2EE) (non-interactive, skipped)\"\n return\n }\n # ─────────────────────────────────────────────────────────────────\n Write-Host \"\"\n Write-Host \" $(Get-Msg 'matrix_e2ee.desc')\"\n Write-Host \"\"\n Write-Host \" 1) $(Get-Msg 'matrix_e2ee.disable')\"\n Write-Host \" 2) $(Get-Msg 'matrix_e2ee.enable')\"\n Write-Host \"\"\n\n if ($script:AGENTTEAMS_UPGRADE -and $env:AGENTTEAMS_MATRIX_E2EE) {\n $e2eeDisplay = if ($env:AGENTTEAMS_MATRIX_E2EE -eq \"1\") { Get-Msg \"matrix_e2ee.val_enabled\" } else { Get-Msg \"matrix_e2ee.val_disabled\" }\n Write-Log (Get-Msg \"prompt.upgrade_keep\" -f (Get-Msg \"matrix_e2ee.title_short\"), $e2eeDisplay)\n $e2eeChoice = Read-Host (Get-Msg \"matrix_e2ee.choice\")\n if ($e2eeChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n if ($e2eeChoice) {\n $script:config.MATRIX_E2EE = if ($e2eeChoice -eq \"2\") { \"1\" } else { \"0\" }\n } else {\n $script:config.MATRIX_E2EE = $env:AGENTTEAMS_MATRIX_E2EE\n }\n } elseif (-not $env:AGENTTEAMS_MATRIX_E2EE) {\n $e2eeChoice = Read-Host (Get-Msg \"matrix_e2ee.choice\")\n if ($e2eeChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n $e2eeChoice = if ($e2eeChoice) { $e2eeChoice } else { \"1\" }\n $script:config.MATRIX_E2EE = if ($e2eeChoice -eq \"2\") { \"1\" } else { \"0\" }\n } else {\n $script:config.MATRIX_E2EE = $env:AGENTTEAMS_MATRIX_E2EE\n }\n\n if ($script:config.MATRIX_E2EE -eq \"1\") {\n Write-Log (Get-Msg \"matrix_e2ee.selected_enabled\")\n } else {\n Write-Log (Get-Msg \"matrix_e2ee.selected_disabled\")\n }\n}\n\nfunction Step-DockerProxy {\n if (-not $script:AGENTTEAMS_MOUNT_SOCKET) {\n $script:config.DOCKER_PROXY = \"0\"\n return\n }\n\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n $script:config.DOCKER_PROXY = if ($env:AGENTTEAMS_DOCKER_PROXY) { $env:AGENTTEAMS_DOCKER_PROXY } else { \"0\" }\n Write-Log \" $(Get-Msg 'docker_proxy.title_short') = $($script:config.DOCKER_PROXY) (non-interactive, skipped)\"\n return\n }\n # ─────────────────────────────────────────────────────────────────\n\n Write-Host \"\"\n Write-Log (Get-Msg \"docker_proxy.title\")\n Write-Host \"\"\n Write-Host \" $(Get-Msg 'docker_proxy.desc')\"\n Write-Host \"\"\n Write-Host \" 1) $(Get-Msg 'docker_proxy.enable')\"\n Write-Host \" 2) $(Get-Msg 'docker_proxy.disable')\"\n Write-Host \"\"\n\n if ($script:AGENTTEAMS_UPGRADE -and $env:AGENTTEAMS_DOCKER_PROXY) {\n $proxyDisplay = if ($env:AGENTTEAMS_DOCKER_PROXY -eq \"1\") { Get-Msg \"docker_proxy.val_enabled\" } else { Get-Msg \"docker_proxy.val_disabled\" }\n Write-Log (Get-Msg \"prompt.upgrade_keep\" -f (Get-Msg \"docker_proxy.title_short\"), $proxyDisplay)\n $proxyChoice = Read-Host (Get-Msg \"docker_proxy.choice\")\n if ($proxyChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n if ($proxyChoice) {\n $script:config.DOCKER_PROXY = if ($proxyChoice -eq \"2\") { \"0\" } else { \"1\" }\n } else {\n $script:config.DOCKER_PROXY = $env:AGENTTEAMS_DOCKER_PROXY\n }\n } elseif (-not $env:AGENTTEAMS_DOCKER_PROXY) {\n $proxyChoice = Read-Host (Get-Msg \"docker_proxy.choice\")\n if ($proxyChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n $proxyChoice = if ($proxyChoice) { $proxyChoice } else { \"1\" }\n $script:config.DOCKER_PROXY = if ($proxyChoice -eq \"2\") { \"0\" } else { \"1\" }\n } else {\n $script:config.DOCKER_PROXY = $env:AGENTTEAMS_DOCKER_PROXY\n }\n\n if ($script:config.DOCKER_PROXY -eq \"1\") {\n Write-Log (Get-Msg \"docker_proxy.selected_enabled\")\n\n # Prompt for additional allowed image sources\n Write-Host \"\"\n Write-Host \" $(Get-Msg 'docker_proxy.registries_desc')\"\n Write-Host \"\"\n if ($script:AGENTTEAMS_UPGRADE -and $env:AGENTTEAMS_PROXY_ALLOWED_REGISTRIES) {\n Write-Log (Get-Msg \"prompt.upgrade_keep\" -f (Get-Msg \"docker_proxy.registries_label\"), $env:AGENTTEAMS_PROXY_ALLOWED_REGISTRIES)\n $regInput = Read-Host (Get-Msg \"docker_proxy.registries_prompt\")\n if ($regInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $script:config.PROXY_ALLOWED_REGISTRIES = if ($regInput) { $regInput } else { $env:AGENTTEAMS_PROXY_ALLOWED_REGISTRIES }\n } elseif (-not $env:AGENTTEAMS_PROXY_ALLOWED_REGISTRIES) {\n $regInput = Read-Host (Get-Msg \"docker_proxy.registries_prompt\")\n if ($regInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $script:config.PROXY_ALLOWED_REGISTRIES = if ($regInput) { $regInput } else { \"\" }\n } else {\n $script:config.PROXY_ALLOWED_REGISTRIES = $env:AGENTTEAMS_PROXY_ALLOWED_REGISTRIES\n }\n } else {\n Write-Log (Get-Msg \"docker_proxy.selected_disabled\")\n }\n}\n\nfunction Step-Idle {\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n $script:config.WORKER_IDLE_TIMEOUT = if ($env:AGENTTEAMS_WORKER_IDLE_TIMEOUT) { $env:AGENTTEAMS_WORKER_IDLE_TIMEOUT } else { \"720\" }\n Write-Log \" $(Get-Msg 'idle_timeout.label') = $($script:config.WORKER_IDLE_TIMEOUT) (non-interactive, skipped)\"\n return\n }\n # ─────────────────────────────────────────────────────────────────\n if ($script:AGENTTEAMS_UPGRADE -and $env:AGENTTEAMS_WORKER_IDLE_TIMEOUT) {\n Write-Log (Get-Msg \"prompt.upgrade_keep\" -f (Get-Msg \"idle_timeout.label\"), $env:AGENTTEAMS_WORKER_IDLE_TIMEOUT)\n $idleInput = Read-Host (Get-Msg \"idle_timeout.prompt\")\n if ($idleInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $script:config.WORKER_IDLE_TIMEOUT = if ($idleInput) { $idleInput } else { $env:AGENTTEAMS_WORKER_IDLE_TIMEOUT }\n } elseif (-not $env:AGENTTEAMS_WORKER_IDLE_TIMEOUT) {\n $idleInput = Read-Host (Get-Msg \"idle_timeout.prompt\")\n if ($idleInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $script:config.WORKER_IDLE_TIMEOUT = if ($idleInput) { $idleInput } else { \"720\" }\n } else {\n $script:config.WORKER_IDLE_TIMEOUT = $env:AGENTTEAMS_WORKER_IDLE_TIMEOUT\n }\n Write-Log (Get-Msg \"idle_timeout.selected\" -f $script:config.WORKER_IDLE_TIMEOUT)\n Write-Log \"\"\n}\n\nfunction Step-Hostshare {\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n $script:config.HOST_SHARE_DIR = if ($env:AGENTTEAMS_HOST_SHARE_DIR) { $env:AGENTTEAMS_HOST_SHARE_DIR } else { $env:USERPROFILE }\n Write-Log \" $(Get-Msg 'host_share.label') = $($script:config.HOST_SHARE_DIR) (non-interactive, skipped)\"\n return\n }\n # ─────────────────────────────────────────────────────────────────\n $currentShare = $script:config.HOST_SHARE_DIR\n if ($currentShare) {\n $shareInput = Read-Host \"$(Get-Msg 'host_share.prompt' -f $env:USERPROFILE) [${currentShare}]\"\n } else {\n $shareInput = Read-Host (Get-Msg 'host_share.prompt' -f $env:USERPROFILE)\n }\n if ($shareInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $script:config.HOST_SHARE_DIR = if ($shareInput) { $shareInput } else { if ($currentShare) { $currentShare } else { $env:USERPROFILE } }\n}\n\n# ============================================================\n# Manager Installation\n# ============================================================\n\nfunction Install-Manager {\n Write-Log (Get-Msg \"install.title\")\n\n # Detect timezone\n $script:AGENTTEAMS_TIMEZONE = Get-AgentTeamsTimeZone\n\n # Language priority: env var > existing env file > timezone detection\n if ($env:AGENTTEAMS_LANGUAGE) {\n $script:AGENTTEAMS_LANGUAGE = $env:AGENTTEAMS_LANGUAGE\n } else {\n # Check existing env file for saved language preference (upgrade scenario)\n $_envFile = $script:AGENTTEAMS_ENV_FILE\n # Migrate from legacy location (current directory) if needed\n if (-not (Test-Path $_envFile) -and (Test-Path \".\\agentteams-manager.env\")) {\n Write-Log \"Migrating agentteams-manager.env to $_envFile...\"\n Move-Item \".\\agentteams-manager.env\" $_envFile -ErrorAction SilentlyContinue\n }\n if (Test-Path $_envFile) {\n $_savedLang = (Get-Content $_envFile | Select-String \"^AGENTTEAMS_LANGUAGE=\" | ForEach-Object {\n $_.Line -replace '^AGENTTEAMS_LANGUAGE=', ''\n } | Select-Object -First 1)\n if ($_savedLang) {\n $script:AGENTTEAMS_LANGUAGE = $_savedLang\n }\n }\n # Fall back to timezone-based detection\n if (-not $script:AGENTTEAMS_LANGUAGE) {\n $script:AGENTTEAMS_LANGUAGE = Get-AgentTeamsLanguage -Timezone $script:AGENTTEAMS_TIMEZONE\n }\n }\n $env:AGENTTEAMS_LANGUAGE = $script:AGENTTEAMS_LANGUAGE\n\n # Detect registry\n $script:AGENTTEAMS_REGISTRY = Get-Registry -Timezone $script:AGENTTEAMS_TIMEZONE\n\n # Set image names\n $script:MANAGER_IMAGE = if ($env:AGENTTEAMS_INSTALL_MANAGER_IMAGE) {\n $env:AGENTTEAMS_INSTALL_MANAGER_IMAGE\n } else {\n \"$($script:AGENTTEAMS_REGISTRY)/agentteams/agentteams-manager:$($script:AGENTTEAMS_VERSION)\"\n }\n\n $script:WORKER_IMAGE = if ($env:AGENTTEAMS_INSTALL_WORKER_IMAGE) {\n $env:AGENTTEAMS_INSTALL_WORKER_IMAGE\n } else {\n \"$($script:AGENTTEAMS_REGISTRY)/agentteams/agentteams-worker:$($script:AGENTTEAMS_VERSION)\"\n }\n\n $script:COPAW_WORKER_IMAGE = if ($env:AGENTTEAMS_INSTALL_COPAW_WORKER_IMAGE) {\n $env:AGENTTEAMS_INSTALL_COPAW_WORKER_IMAGE\n } else {\n \"$($script:AGENTTEAMS_REGISTRY)/agentteams/agentteams-copaw-worker:$($script:AGENTTEAMS_VERSION)\"\n }\n\n $script:HERMES_WORKER_IMAGE = if ($env:AGENTTEAMS_INSTALL_HERMES_WORKER_IMAGE) {\n $env:AGENTTEAMS_INSTALL_HERMES_WORKER_IMAGE\n } else {\n \"$($script:AGENTTEAMS_REGISTRY)/agentteams/agentteams-hermes-worker:$($script:AGENTTEAMS_VERSION)\"\n }\n\n $script:MANAGER_COPAW_IMAGE = if ($env:AGENTTEAMS_INSTALL_MANAGER_COPAW_IMAGE) {\n $env:AGENTTEAMS_INSTALL_MANAGER_COPAW_IMAGE\n } else {\n \"$($script:AGENTTEAMS_REGISTRY)/agentteams/agentteams-manager-copaw:$($script:AGENTTEAMS_VERSION)\"\n }\n\n $script:CONTROLLER_IMAGE = if ($env:AGENTTEAMS_INSTALL_CONTROLLER_IMAGE) {\n $env:AGENTTEAMS_INSTALL_CONTROLLER_IMAGE\n } else {\n \"$($script:AGENTTEAMS_REGISTRY)/agentteams/agentteams-controller:$($script:AGENTTEAMS_VERSION)\"\n }\n\n # Resolve embedded controller image (sets $script:EMBEDDED_IMAGE and\n # $script:AGENTTEAMS_USE_EMBEDDED). Errors out fast if no embedded image is available\n # for the requested version (mirrors the bash installer behavior).\n Resolve-EmbeddedImage\n\n Write-Log (Get-Msg \"install.registry\" -f $script:AGENTTEAMS_REGISTRY)\n Write-Log \"\"\n Write-Log (Get-Msg \"install.dir\" -f (Get-Location))\n Write-Log (Get-Msg \"install.dir_hint\")\n Write-Log (Get-Msg \"install.dir_hint2\")\n Write-Log \"\"\n\n # Check container runtime (docker or podman)\n if (-not (Get-Command \"docker\" -ErrorAction SilentlyContinue) -and\n -not (Get-Command \"podman\" -ErrorAction SilentlyContinue)) {\n Write-Host \"$($script:ESC)[31m[AgentTeams ERROR]$($script:ESC)[0m $(Get-Msg 'error.docker_not_found')\" -ForegroundColor Red\n Exit-Script 1\n }\n\n if (-not (Test-DockerRunning)) {\n Write-Host \"$($script:ESC)[31m[AgentTeams ERROR]$($script:ESC)[0m $(Get-Msg 'error.docker_not_running')\" -ForegroundColor Red\n Exit-Script 1\n }\n\n # Initialize shared config hashtable\n $script:config = @{}\n $config = $script:config # local alias so post-machine code can use $config\n\n # Orphan volume detection (env file gone but volume remains)\n if (-not (Test-Path $script:AGENTTEAMS_ENV_FILE)) {\n $dataVol = if ($env:AGENTTEAMS_DATA_DIR) { $env:AGENTTEAMS_DATA_DIR } else { \"agentteams-data\" }\n $volumeExists = docker volume ls -q 2>$null | Select-String \"^${dataVol}$\"\n if ($volumeExists) {\n Write-Host \"\"\n Write-Log (Get-Msg \"install.orphan_volume.detected\" -f $dataVol)\n Write-Log (Get-Msg \"install.orphan_volume.warn\")\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n Write-Log (Get-Msg \"install.orphan_volume.clean_noninteractive\")\n docker stop agentteams-manager *>$null\n docker rm agentteams-manager *>$null\n docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-worker-\" | ForEach-Object {\n docker stop $_.ToString().Trim() *>$null\n docker rm $_.ToString().Trim() *>$null\n }\n Write-Log (Get-Msg \"install.orphan_volume.cleaning\")\n docker volume rm $dataVol *>$null\n Write-Log (Get-Msg \"install.orphan_volume.cleaned\")\n } else {\n Write-Host \"\"\n Write-Host (Get-Msg \"install.orphan_volume.choose\")\n Write-Host (Get-Msg \"install.orphan_volume.clean\")\n Write-Host (Get-Msg \"install.orphan_volume.keep\")\n Write-Host \"\"\n $orphanChoice = Read-Host (Get-Msg \"install.orphan_volume.prompt\")\n if (-not $orphanChoice) { $orphanChoice = \"1\" }\n switch -Regex ($orphanChoice) {\n \"^(1|clean)$\" {\n docker stop agentteams-manager *>$null\n docker rm agentteams-manager *>$null\n docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-worker-\" | ForEach-Object {\n docker stop $_.ToString().Trim() *>$null\n docker rm $_.ToString().Trim() *>$null\n }\n Write-Log (Get-Msg \"install.orphan_volume.cleaning\")\n docker volume rm $dataVol *>$null\n Write-Log (Get-Msg \"install.orphan_volume.cleaned\")\n }\n \"^(2|keep)$\" {\n Write-Log (Get-Msg \"install.orphan_volume.keeping\")\n }\n }\n }\n }\n }\n\n # ── State machine ─────────────────────────────────────────────────────────\n $_steps = @(\"Step-Lang\", \"Step-Mode\", \"Step-Existing\", \"Step-Llm\", \"Step-ManagerRuntime\", \"Step-Runtime\", \"Step-Admin\",\n \"Step-Network\", \"Step-Ports\", \"Step-Domains\", \"Step-Github\", \"Step-Skills\",\n \"Step-Volume\", \"Step-Workspace\", \"Step-E2ee\", \"Step-DockerProxy\", \"Step-Idle\",\n \"Step-Hostshare\")\n $_stepHistory = [System.Collections.Generic.List[int]]::new()\n $_stepIdx = 0\n while ($_stepIdx -lt $_steps.Count) {\n $_stepFn = $_steps[$_stepIdx]\n if (Test-ShouldSkipStep $_stepFn) {\n $_stepIdx++\n continue\n }\n if ($_stepHistory.Count -gt 0) {\n Write-Log (Get-Msg \"nav.back_hint\")\n }\n $script:StepResult = \"\"\n & $_stepFn\n if ($script:StepResult -eq \"back\") {\n if ($_stepHistory.Count -gt 0) {\n $_stepIdx = $_stepHistory[$_stepHistory.Count - 1]\n $_stepHistory.RemoveAt($_stepHistory.Count - 1)\n Clear-StepVars $_steps[$_stepIdx]\n }\n # else: first step, ignore 'b'\n } else {\n $_stepHistory.Add($_stepIdx)\n $_stepIdx++\n }\n }\n # ── End state machine ──────────────────────────────────────────────────────\n\n # Post-machine defaults for any steps that were skipped\n if (-not $script:config.DATA_DIR) {\n $script:config.DATA_DIR = if ($env:AGENTTEAMS_DATA_DIR) { $env:AGENTTEAMS_DATA_DIR } else { \"agentteams-data\" }\n }\n Write-Log (Get-Msg \"data.volume_using\" -f $script:config.DATA_DIR)\n if (-not $script:config.WORKSPACE_DIR) {\n $_defaultWs = \"$env:USERPROFILE\\agentteams-manager\"\n $script:config.WORKSPACE_DIR = if ($env:AGENTTEAMS_WORKSPACE_DIR) { $env:AGENTTEAMS_WORKSPACE_DIR } else { $_defaultWs }\n if (-not (Test-Path $script:config.WORKSPACE_DIR)) {\n New-Item -ItemType Directory -Path $script:config.WORKSPACE_DIR -Force | Out-Null\n }\n Write-Log (Get-Msg \"workspace.dir_label\" -f $script:config.WORKSPACE_DIR)\n }\n if (-not $script:config.DEFAULT_WORKER_RUNTIME) {\n $script:config.DEFAULT_WORKER_RUNTIME = if ($env:AGENTTEAMS_DEFAULT_WORKER_RUNTIME) { $env:AGENTTEAMS_DEFAULT_WORKER_RUNTIME } else { \"copaw\" }\n Write-Log (Get-Msg \"worker_runtime.selected\" -f $script:config.DEFAULT_WORKER_RUNTIME)\n }\n if (-not $script:config.MATRIX_E2EE) {\n $script:config.MATRIX_E2EE = if ($env:AGENTTEAMS_MATRIX_E2EE) { $env:AGENTTEAMS_MATRIX_E2EE } else { \"0\" }\n }\n if (-not $script:config.WORKER_IDLE_TIMEOUT) {\n $script:config.WORKER_IDLE_TIMEOUT = if ($env:AGENTTEAMS_WORKER_IDLE_TIMEOUT) { $env:AGENTTEAMS_WORKER_IDLE_TIMEOUT } else { \"720\" }\n }\n if (-not $script:config.HOST_SHARE_DIR) {\n $script:config.HOST_SHARE_DIR = if ($env:AGENTTEAMS_HOST_SHARE_DIR) { $env:AGENTTEAMS_HOST_SHARE_DIR } else { $env:USERPROFILE }\n }\n if (-not $script:config.LOCAL_ONLY) {\n $script:config.LOCAL_ONLY = if ($env:AGENTTEAMS_LOCAL_ONLY) { $env:AGENTTEAMS_LOCAL_ONLY } else { \"1\" }\n }\n if (-not $script:config.MANAGER_RUNTIME) {\n $script:config.MANAGER_RUNTIME = if ($env:AGENTTEAMS_MANAGER_RUNTIME) { $env:AGENTTEAMS_MANAGER_RUNTIME } else { \"copaw\" }\n }\n if (-not $script:config.PORT_GATEWAY) {\n $script:config.PORT_GATEWAY = if ($env:AGENTTEAMS_PORT_GATEWAY) { $env:AGENTTEAMS_PORT_GATEWAY } else { \"18080\" }\n $script:config.PORT_CONSOLE = if ($env:AGENTTEAMS_PORT_CONSOLE) { $env:AGENTTEAMS_PORT_CONSOLE } else { \"18001\" }\n $script:config.PORT_ELEMENT_WEB = if ($env:AGENTTEAMS_PORT_ELEMENT_WEB) { $env:AGENTTEAMS_PORT_ELEMENT_WEB } else { \"18088\" }\n $script:config.PORT_MANAGER_CONSOLE = if ($env:AGENTTEAMS_PORT_MANAGER_CONSOLE) { $env:AGENTTEAMS_PORT_MANAGER_CONSOLE } else { \"18888\" }\n }\n if (-not $script:config.MATRIX_DOMAIN) {\n $script:config.MATRIX_DOMAIN = if ($env:AGENTTEAMS_MATRIX_DOMAIN) { $env:AGENTTEAMS_MATRIX_DOMAIN } else { \"matrix-local.agentteams.io:$($script:config.PORT_GATEWAY)\" }\n $script:config.MATRIX_CLIENT_DOMAIN = if ($env:AGENTTEAMS_MATRIX_CLIENT_DOMAIN) { $env:AGENTTEAMS_MATRIX_CLIENT_DOMAIN } else { \"matrix-client-local.agentteams.io\" }\n $script:config.AI_GATEWAY_DOMAIN = if ($env:AGENTTEAMS_AI_GATEWAY_DOMAIN) { $env:AGENTTEAMS_AI_GATEWAY_DOMAIN } else { \"aigw-local.agentteams.io\" }\n $script:config.FS_DOMAIN = if ($env:AGENTTEAMS_FS_DOMAIN) { $env:AGENTTEAMS_FS_DOMAIN } else { \"fs-local.agentteams.io\" }\n $script:config.CONSOLE_DOMAIN = if ($env:AGENTTEAMS_CONSOLE_DOMAIN) { $env:AGENTTEAMS_CONSOLE_DOMAIN } else { \"console-local.agentteams.io\" }\n }\n\n Write-Log \"\"\n\n # Generate secrets\n Write-Log (Get-Msg \"install.generating_secrets\")\n $config.MANAGER_PASSWORD = if ($env:AGENTTEAMS_MANAGER_PASSWORD) { $env:AGENTTEAMS_MANAGER_PASSWORD } else { New-RandomKey }\n $config.REGISTRATION_TOKEN = if ($env:AGENTTEAMS_REGISTRATION_TOKEN) { $env:AGENTTEAMS_REGISTRATION_TOKEN } else { New-RandomKey }\n $config.MINIO_USER = if ($env:AGENTTEAMS_MINIO_USER) { $env:AGENTTEAMS_MINIO_USER } else { $config.ADMIN_USER }\n $config.MINIO_PASSWORD = if ($env:AGENTTEAMS_MINIO_PASSWORD) { $env:AGENTTEAMS_MINIO_PASSWORD } else { $config.ADMIN_PASSWORD }\n $config.MANAGER_GATEWAY_KEY = if ($env:AGENTTEAMS_MANAGER_GATEWAY_KEY) { $env:AGENTTEAMS_MANAGER_GATEWAY_KEY } else { New-RandomKey }\n\n # Store additional config\n $config.LANGUAGE = $script:AGENTTEAMS_LANGUAGE\n $config.REGISTRY = $script:AGENTTEAMS_REGISTRY\n $config.WORKER_IMAGE = $script:WORKER_IMAGE\n $config.COPAW_WORKER_IMAGE = $script:COPAW_WORKER_IMAGE\n $config.HERMES_WORKER_IMAGE = $script:HERMES_WORKER_IMAGE\n $config.MANAGER_COPAW_IMAGE = $script:MANAGER_COPAW_IMAGE\n\n # Write env file\n New-EnvFile -Config $config -Path $script:AGENTTEAMS_ENV_FILE\n\n # Manager image selection (used by both embedded — passed to controller via env —\n # and legacy — used directly as `docker run` target).\n $managerImage = if ($config.MANAGER_RUNTIME -eq \"copaw\") { $script:MANAGER_COPAW_IMAGE } else { $script:MANAGER_IMAGE }\n $portPrefix = if ($config.LOCAL_ONLY -eq \"1\") { \"127.0.0.1:\" } else { \"\" }\n\n # Ensure agentteams-net Docker network exists. Used in both modes — the embedded\n # controller and the spawned manager/worker containers all join it and rely on\n # network aliases for *-local.agentteams.io DNS resolution.\n if ($script:AGENTTEAMS_MOUNT_SOCKET) {\n $socketAvailable = Test-DockerRunning\n if ($socketAvailable) {\n docker network inspect agentteams-net *>$null\n if ($LASTEXITCODE -ne 0) { docker network create agentteams-net *>$null }\n } else {\n Write-Log (Get-Msg \"install.socket_not_found\")\n if (-not $script:AGENTTEAMS_NON_INTERACTIVE) {\n Write-Host \"\"\n Write-Host \"$($script:ESC)[33m$(Get-Msg 'install.socket_confirm.title')$($script:ESC)[0m\"\n Write-Host \"\"\n Write-Host (Get-Msg 'install.socket_confirm.message')\n Write-Host \"\"\n $confirm = Read-Host (Get-Msg 'install.socket_confirm.prompt')\n if ($confirm -ne 'y' -and $confirm -ne 'Y') {\n Write-Log (Get-Msg 'install.socket_confirm.cancelled')\n exit 0\n }\n }\n }\n }\n\n if ($script:AGENTTEAMS_USE_EMBEDDED -ne \"1\") {\n # ============================================================\n # Legacy architecture: all-in-one manager container\n # (only entered when AGENTTEAMS_FORCE_LEGACY=1 — broken with the slim manager\n # image shipped since PR #616; kept solely for AGENTTEAMS_VERSION <= v1.0.9)\n # ============================================================\n\n $dockerArgs = @(\n \"run\", \"-d\",\n \"--name\", \"agentteams-manager\",\n \"--env-file\", $script:AGENTTEAMS_ENV_FILE,\n \"-e\", \"HOME=/root/manager-workspace\",\n \"-w\", \"/root/manager-workspace\",\n \"-e\", \"HOST_ORIGINAL_HOME=$($config.HOST_SHARE_DIR)\",\n \"-e\", \"AGENTTEAMS_MANAGER_RUNTIME=$($config.MANAGER_RUNTIME)\"\n )\n\n $dockerArgs += @(\"-e\", \"TZ=$($script:AGENTTEAMS_TIMEZONE)\")\n\n if ($script:AGENTTEAMS_MOUNT_SOCKET -and (Test-DockerRunning)) {\n $dockerArgs += @(\"--network\", \"agentteams-net\")\n $dockerArgs += @(\"--network-alias\", \"matrix-local.agentteams.io\")\n $dockerArgs += @(\"--network-alias\", \"aigw-local.agentteams.io\")\n $dockerArgs += @(\"--network-alias\", \"fs-local.agentteams.io\")\n foreach ($domain in @($config.MATRIX_CLIENT_DOMAIN, $config.CONSOLE_DOMAIN)) {\n if ($domain -match '-local\\.agentteams\\.io$') {\n $dockerArgs += @(\"--network-alias\", $domain)\n }\n }\n\n if ($config.DOCKER_PROXY -eq \"1\") {\n $proxyImage = $script:CONTROLLER_IMAGE\n Write-Log \"Starting Docker API proxy...\"\n docker rm -f agentteams-controller *>$null\n docker run -d --name agentteams-controller `\n --network agentteams-net `\n -v \"//var/run/docker.sock:/var/run/docker.sock\" `\n --security-opt label=disable `\n -e \"AGENTTEAMS_WORKER_IMAGE=$($script:WORKER_IMAGE)\" `\n -e \"AGENTTEAMS_COPAW_WORKER_IMAGE=$($script:COPAW_WORKER_IMAGE)\" `\n -e \"AGENTTEAMS_HERMES_WORKER_IMAGE=$($script:HERMES_WORKER_IMAGE)\" `\n -e \"AGENTTEAMS_DEFAULT_WORKER_RUNTIME=$($script:config.DEFAULT_WORKER_RUNTIME)\" `\n $(if ($config.PROXY_ALLOWED_REGISTRIES) { @(\"-e\", \"AGENTTEAMS_PROXY_ALLOWED_REGISTRIES=$($config.PROXY_ALLOWED_REGISTRIES)\") }) `\n --restart unless-stopped `\n $proxyImage\n $dockerArgs += @(\"-e\", \"AGENTTEAMS_CONTROLLER_URL=http://agentteams-controller:8090\")\n $dockerArgs += @(\"-e\", \"AGENTTEAMS_CONTAINER_API=http://agentteams-controller:8090\")\n Write-Log (Get-Msg \"docker_proxy.selected_enabled\")\n } else {\n $dockerArgs += @(\"-v\", \"//var/run/docker.sock:/var/run/docker.sock\")\n $dockerArgs += @(\"--security-opt\", \"label=disable\")\n Write-Log (Get-Msg \"install.socket_detected\" -f \"//var/run/docker.sock\")\n }\n }\n\n $dockerArgs += @(\"-p\", \"${portPrefix}$($config.PORT_GATEWAY):8080\")\n $dockerArgs += @(\"-p\", \"${portPrefix}$($config.PORT_CONSOLE):8001\")\n $dockerArgs += @(\"-p\", \"${portPrefix}$($config.PORT_ELEMENT_WEB):8088\")\n $dockerArgs += @(\"-p\", \"127.0.0.1:$($config.PORT_MANAGER_CONSOLE):18888\")\n\n $dockerArgs += @(\"-v\", \"$($config.DATA_DIR):/data\")\n\n $wsDockerPath = ConvertTo-DockerPath -Path $config.WORKSPACE_DIR\n $dockerArgs += @(\"-v\", \"${wsDockerPath}:/root/manager-workspace\")\n\n $shareDockerPath = ConvertTo-DockerPath -Path $config.HOST_SHARE_DIR\n $dockerArgs += @(\"-v\", \"${shareDockerPath}:/host-share\")\n Write-Log (Get-Msg \"host_share.sharing\" -f $config.HOST_SHARE_DIR)\n\n if ($env:AGENTTEAMS_YOLO -eq \"1\") {\n $dockerArgs += @(\"-e\", \"AGENTTEAMS_YOLO=1\")\n Write-Log (Get-Msg \"install.yolo\")\n }\n\n if ($env:AGENTTEAMS_MATRIX_DEBUG -eq \"1\") {\n $dockerArgs += @(\"-e\", \"AGENTTEAMS_MATRIX_DEBUG=1\")\n }\n\n $dockerArgs += @(\"--restart\", \"unless-stopped\")\n $dockerArgs += $managerImage\n }\n\n # Check if the Docker volume exists; create if not (reuse on reinstall) — both modes.\n $volumeExists = docker volume ls -q 2>$null | Select-String \"^$($config.DATA_DIR)$\"\n if (-not $volumeExists) {\n docker volume create $config.DATA_DIR | Out-Null\n }\n\n # Pull images (skip if already exists locally for local build tags).\n $LocalImagePattern = \"^agentteams/\"\n function Test-LocalImage {\n param([string]$Image)\n $imgExists = docker image inspect $Image 2>$null\n return $LASTEXITCODE -eq 0\n }\n if ($script:AGENTTEAMS_USE_EMBEDDED -eq \"1\") {\n # Embedded image was already pulled by Resolve-EmbeddedImage unless overridden;\n # for an explicit override we still need to ensure it is present locally.\n if ($env:AGENTTEAMS_INSTALL_EMBEDDED_IMAGE) {\n if ($script:EMBEDDED_IMAGE -match $LocalImagePattern) {\n if (-not (Test-LocalImage $script:EMBEDDED_IMAGE)) {\n Write-Log \"Pulling embedded image: $($script:EMBEDDED_IMAGE)\"\n & docker pull $script:EMBEDDED_IMAGE\n }\n } else {\n Write-Log \"Pulling embedded image: $($script:EMBEDDED_IMAGE)\"\n & docker pull $script:EMBEDDED_IMAGE\n }\n }\n # Manager image — controller will spawn it inside; pull here so the spawn doesn't\n # have to wait on the network.\n if ($managerImage -match $LocalImagePattern) {\n if (Test-LocalImage $managerImage) {\n Write-Log (Get-Msg \"install.image.exists\" -f $managerImage)\n } else {\n Write-Log (Get-Msg \"install.image.pulling_manager\" -f $managerImage)\n & docker pull $managerImage\n }\n } else {\n Write-Log (Get-Msg \"install.image.pulling_manager\" -f $managerImage)\n & docker pull $managerImage\n }\n } else {\n if ($managerImage -match $LocalImagePattern) {\n if (Test-LocalImage $managerImage) {\n Write-Log (Get-Msg \"install.image.exists\" -f $managerImage)\n } else {\n Write-Log (Get-Msg \"install.image.pulling_manager\" -f $managerImage)\n & docker pull $managerImage\n }\n } else {\n Write-Log (Get-Msg \"install.image.pulling_manager\" -f $managerImage)\n & docker pull $managerImage\n }\n }\n\n # Pull all worker runtime images (workers may use any runtime regardless of the default)\n foreach ($workerImg in @($script:WORKER_IMAGE, $script:COPAW_WORKER_IMAGE, $script:HERMES_WORKER_IMAGE)) {\n if ($workerImg -match $LocalImagePattern) {\n if (Test-LocalImage $workerImg) {\n Write-Log (Get-Msg \"install.image.worker_exists\" -f $workerImg)\n } else {\n Write-Log (Get-Msg \"install.image.pulling_worker\" -f $workerImg)\n & docker pull $workerImg\n }\n } else {\n Write-Log (Get-Msg \"install.image.pulling_worker\" -f $workerImg)\n & docker pull $workerImg\n }\n }\n\n # --- Pre-upgrade: extract Matrix passwords from old containers (old-arch -> embedded) ---\n $credsTmp = $null\n if ($script:AGENTTEAMS_UPGRADE -and $script:AGENTTEAMS_USE_EMBEDDED -eq \"1\") {\n $controllerNameHit = docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-controller$\"\n $isOldArch = $false\n if (-not $controllerNameHit) {\n $isOldArch = $true\n } else {\n $ctrlImgLine = docker ps -a --format \"{{.Names}} {{.Image}}\" 2>$null | Where-Object { $_ -match '^agentteams-controller ' } | Select-Object -First 1\n if ($ctrlImgLine -and ($ctrlImgLine -notmatch 'embedded')) {\n $isOldArch = $true\n }\n }\n\n if ($isOldArch) {\n $credsTmp = Join-Path ([System.IO.Path]::GetTempPath()) (\"agentteams-upgrade-creds-\" + [Guid]::NewGuid().ToString(\"n\"))\n New-Item -ItemType Directory -Path $credsTmp -Force | Out-Null\n $utf8NoBom = New-Object System.Text.UTF8Encoding $false\n\n $mgrCredsTempStart = $false\n $mgrExists = docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-manager$\"\n $mgrRunning = docker ps --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-manager$\"\n if ($mgrExists -and -not $mgrRunning) {\n Write-Log \"agentteams-manager is stopped; starting it temporarily to extract Matrix credentials for upgrade...\"\n docker start agentteams-manager 2>$null | Out-Null\n Wait-MatrixReady -Container \"agentteams-manager\"\n $mgrCredsTempStart = $true\n }\n\n $inspectLines = docker inspect agentteams-manager --format \"{{range .Config.Env}}{{println .}}{{end}}\" 2>$null\n $mgrPw = \"\"\n if ($inspectLines) {\n $envLine = $inspectLines -split \"`n\" | Where-Object { $_ -match '^AGENTTEAMS_MANAGER_PASSWORD=' } | Select-Object -First 1\n if ($envLine) {\n $mgrPw = ($envLine -replace '^AGENTTEAMS_MANAGER_PASSWORD=', \"\").Trim()\n }\n }\n $mgrRunningNow = docker ps --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-manager$\"\n if ([string]::IsNullOrEmpty($mgrPw) -and $mgrRunningNow) {\n $mgrPw = docker exec agentteams-manager bash -c 'source /data/agentteams-secrets.env 2>/dev/null && echo \"${AGENTTEAMS_MANAGER_PASSWORD}\"' 2>$null\n if ($mgrPw) { $mgrPw = $mgrPw.Trim() }\n }\n $dataVolPresent = docker volume ls -q 2>$null | Where-Object { $_ -eq $config.DATA_DIR }\n if ([string]::IsNullOrEmpty($mgrPw) -and $dataVolPresent) {\n $mgrPw = Read-AgentTeamsSecretFromDataVolume -VolumeName $config.DATA_DIR -Key \"AGENTTEAMS_MANAGER_PASSWORD\"\n }\n\n $envFilePath = $script:AGENTTEAMS_ENV_FILE\n $mgrRoom = \"\"\n if (-not [string]::IsNullOrEmpty($mgrPw)) {\n $adminPw = \"\"\n $adminUser = \"admin\"\n if (Test-Path $envFilePath) {\n $adminLine = Get-Content $envFilePath | Where-Object { $_ -match '^AGENTTEAMS_ADMIN_PASSWORD=' } | Select-Object -First 1\n if ($adminLine) { $adminPw = ($adminLine -replace '^AGENTTEAMS_ADMIN_PASSWORD=', \"\").Trim() }\n $userLine = Get-Content $envFilePath | Where-Object { $_ -match '^AGENTTEAMS_ADMIN_USER=' } | Select-Object -First 1\n if ($userLine) {\n $adminUser = ($userLine -replace '^AGENTTEAMS_ADMIN_USER=', \"\").Trim()\n if ([string]::IsNullOrEmpty($adminUser)) { $adminUser = \"admin\" }\n }\n }\n\n if (-not [string]::IsNullOrEmpty($adminPw) -and $mgrRunningNow) {\n $mgrRoom = Get-AgentTeamsAdminDmRoomViaMatrix -AdminUser $adminUser -AdminPassword $adminPw\n }\n if ([string]::IsNullOrEmpty($mgrRoom)) {\n $mgrRoom = Read-AgentTeamsAdminDmRoomFromWorkspace -WorkspaceDir $config.WORKSPACE_DIR\n }\n\n $gwKeyLine = if (Test-Path $envFilePath) {\n Get-Content $envFilePath | Where-Object { $_ -match '^AGENTTEAMS_MANAGER_GATEWAY_KEY=' } | Select-Object -First 1\n } else { $null }\n $gwKeyForDefault = if ($gwKeyLine) { ($gwKeyLine -replace '^AGENTTEAMS_MANAGER_GATEWAY_KEY=', \"\").Trim() } else { $config.MANAGER_GATEWAY_KEY }\n\n $defaultEnvPath = Join-Path $credsTmp \"default.env\"\n [System.IO.File]::WriteAllLines($defaultEnvPath, @(\n \"WORKER_PASSWORD=$mgrPw\"\n \"WORKER_MINIO_PASSWORD=$(Get-AgentTeamsRandomHex 24)\"\n \"WORKER_GATEWAY_KEY=$gwKeyForDefault\"\n \"WORKER_ROOM_ID=$mgrRoom\"\n ), $utf8NoBom)\n if ($mgrRoom) {\n Write-Log \"Extracted Manager Matrix password and room ID\"\n } else {\n Write-Log \"Extracted Manager Matrix password\"\n }\n }\n\n $registryPath = Join-Path $config.WORKSPACE_DIR \"workers-registry.json\"\n if (Test-Path $registryPath) {\n try {\n $wreg = Get-Content $registryPath -Raw | ConvertFrom-Json\n $workerNames = @()\n if ($null -ne $wreg.workers) {\n $wreg.workers.PSObject.Properties | ForEach-Object { $workerNames += $_.Name }\n }\n foreach ($wname in $workerNames) {\n $wpw = \"\"\n if ($mgrRunningNow) {\n $wpw = docker exec agentteams-manager cat \"/root/agentteams-fs/agents/${wname}/credentials/matrix/password\" 2>$null\n if ($wpw) { $wpw = $wpw.Trim() }\n }\n if ([string]::IsNullOrEmpty($wpw) -and $dataVolPresent) {\n $wpw = Read-AgentTeamsWorkerCredsFromVolume -VolumeName $config.DATA_DIR -WorkerName $wname -Key \"WORKER_PASSWORD\"\n }\n $wroom = \"\"\n $wEntry = $wreg.workers.$wname\n if ($wEntry -and $wEntry.room_id) {\n $wroom = [string]$wEntry.room_id\n }\n if ([string]::IsNullOrEmpty($wroom) -and $dataVolPresent) {\n $wroom = Read-AgentTeamsWorkerCredsFromVolume -VolumeName $config.DATA_DIR -WorkerName $wname -Key \"WORKER_ROOM_ID\"\n }\n if (-not [string]::IsNullOrEmpty($wpw)) {\n $wEnvPath = Join-Path $credsTmp \"${wname}.env\"\n [System.IO.File]::WriteAllLines($wEnvPath, @(\n \"WORKER_PASSWORD=$wpw\"\n \"WORKER_MINIO_PASSWORD=$(Get-AgentTeamsRandomHex 24)\"\n \"WORKER_GATEWAY_KEY=$(Get-AgentTeamsRandomHex 32)\"\n \"WORKER_ROOM_ID=$wroom\"\n ), $utf8NoBom)\n if ($wroom) {\n Write-Log \"Extracted ${wname} Matrix password and room ID\"\n } else {\n Write-Log \"Extracted ${wname} Matrix password\"\n }\n }\n }\n } catch {\n Write-Log \"Warning: could not read workers-registry.json for credential extraction\"\n }\n }\n\n if ($mgrCredsTempStart) {\n Write-Log \"Stopping agentteams-manager after credential extraction (upgrade will recreate containers)...\"\n docker stop agentteams-manager 2>$null | Out-Null\n }\n }\n }\n\n # Stop and remove existing containers (deferred until after all\n # configuration is collected and images are pulled successfully)\n $existingProxy = docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-controller$\"\n if ($existingProxy) {\n docker stop agentteams-controller *>$null\n docker rm agentteams-controller *>$null\n }\n $existingContainer = docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-manager$\"\n if ($existingContainer) {\n Write-Log (Get-Msg \"install.removing_existing\")\n docker stop agentteams-manager *>$null\n docker rm agentteams-manager *>$null\n }\n\n # Stop and remove worker containers (controller will recreate via CR reconciliation)\n if ($script:UPGRADE_EXISTING_WORKERS) {\n Write-Log (Get-Msg \"install.existing.stopping_workers\")\n $script:UPGRADE_EXISTING_WORKERS | ForEach-Object {\n docker stop $_ *>$null\n docker rm $_ *>$null\n Write-Log (Get-Msg \"install.existing.removed\" -f $_)\n }\n }\n\n # --- Upgrade: inject extracted credentials into data volume (old-arch -> embedded) ---\n if ($credsTmp -and (Test-Path $credsTmp)) {\n $credsFiles = Get-ChildItem -Path $credsTmp -Filter \"*.env\" -ErrorAction SilentlyContinue\n if ($credsFiles -and $credsFiles.Count -gt 0) {\n $cleanupCtr = \"agentteams-upgrade-cleanup\"\n docker rm -f $cleanupCtr 2>$null | Out-Null\n $credsDockerPath = ConvertTo-DockerPath -Path $credsTmp\n $injectShell = \"rm -rf /data/worker-creds && mkdir -p /data/worker-creds && cp /creds/*.env /data/worker-creds/ 2>/dev/null || true && chmod 600 /data/worker-creds/*.env 2>/dev/null || true\"\n docker run --rm --name $cleanupCtr --entrypoint sh `\n -v \"$($config.DATA_DIR):/data\" `\n -v \"${credsDockerPath}:/creds:ro\" `\n $script:EMBEDDED_IMAGE `\n -c $injectShell 2>$null | Out-Null\n if ($LASTEXITCODE -eq 0) {\n Write-Log \"Injected credentials for upgrade\"\n } else {\n Write-Log \"Warning: credential injection failed, continuing\"\n }\n }\n Remove-Item -Path $credsTmp -Recurse -Force -ErrorAction SilentlyContinue\n }\n\n if ($script:AGENTTEAMS_USE_EMBEDDED -eq \"1\") {\n # ============================================================\n # New architecture: embedded controller + auto-created manager\n # (controller container hosts Higress / Tuwunel / MinIO / Element Web /\n # controller binary, then spawns the lightweight manager container)\n # ============================================================\n\n # Internal port: 8080 (Higress gateway inside the controller container).\n $internalGwPort = 8080\n\n $matrixDomain = if ($config.MATRIX_DOMAIN) {\n $config.MATRIX_DOMAIN\n } else {\n \"matrix-local.agentteams.io:$($config.PORT_GATEWAY)\"\n }\n $aigwDomain = if ($config.AI_GATEWAY_DOMAIN) { $config.AI_GATEWAY_DOMAIN } else { \"aigw-local.agentteams.io\" }\n if ($aigwDomain -notmatch \":\") { $aigwDomain = \"${aigwDomain}:${internalGwPort}\" }\n $fsDomain = if ($config.FS_DOMAIN) { $config.FS_DOMAIN } else { \"fs-local.agentteams.io\" }\n if ($fsDomain -notmatch \":\") { $fsDomain = \"${fsDomain}:${internalGwPort}\" }\n\n $ctrlArgs = @(\n \"run\", \"-d\",\n \"--name\", \"agentteams-controller\",\n \"--network\", \"agentteams-net\",\n \"--network-alias\", \"matrix-local.agentteams.io\",\n \"--network-alias\", \"aigw-local.agentteams.io\",\n \"--network-alias\", \"fs-local.agentteams.io\",\n \"-e\", \"AGENTTEAMS_ADMIN_USER=$($config.ADMIN_USER)\",\n \"-e\", \"AGENTTEAMS_ADMIN_PASSWORD=$($config.ADMIN_PASSWORD)\",\n \"-e\", \"AGENTTEAMS_MANAGER_PASSWORD=$($config.MANAGER_PASSWORD)\",\n \"-e\", \"AGENTTEAMS_REGISTRATION_TOKEN=$($config.REGISTRATION_TOKEN)\",\n \"-e\", \"AGENTTEAMS_MINIO_USER=$($config.MINIO_USER)\",\n \"-e\", \"AGENTTEAMS_MINIO_PASSWORD=$($config.MINIO_PASSWORD)\",\n \"-e\", \"AGENTTEAMS_LLM_PROVIDER=$($config.LLM_PROVIDER)\",\n \"-e\", \"AGENTTEAMS_LLM_API_KEY=$($config.LLM_API_KEY)\",\n \"-e\", \"AGENTTEAMS_DEFAULT_MODEL=$($config.DEFAULT_MODEL)\",\n \"-e\", \"AGENTTEAMS_MANAGER_GATEWAY_KEY=$($config.MANAGER_GATEWAY_KEY)\",\n \"-e\", \"AGENTTEAMS_MANAGER_RUNTIME=$($config.MANAGER_RUNTIME)\",\n \"-e\", \"AGENTTEAMS_MANAGER_IMAGE=$managerImage\",\n \"-e\", \"AGENTTEAMS_DEFAULT_WORKER_RUNTIME=$($config.DEFAULT_WORKER_RUNTIME)\",\n \"-e\", \"AGENTTEAMS_WORKER_IMAGE=$($script:WORKER_IMAGE)\",\n \"-e\", \"AGENTTEAMS_COPAW_WORKER_IMAGE=$($script:COPAW_WORKER_IMAGE)\",\n \"-e\", \"AGENTTEAMS_HERMES_WORKER_IMAGE=$($script:HERMES_WORKER_IMAGE)\",\n \"-e\", \"AGENTTEAMS_MATRIX_DOMAIN=$matrixDomain\",\n \"-e\", \"AGENTTEAMS_ELEMENT_HOMESERVER_URL=http://127.0.0.1:$($config.PORT_GATEWAY)\",\n \"-e\", \"AGENTTEAMS_MATRIX_URL=http://127.0.0.1:6167\",\n \"-e\", \"AGENTTEAMS_MATRIX_E2EE=$($config.MATRIX_E2EE)\",\n \"-e\", \"AGENTTEAMS_MINIO_ENDPOINT=http://127.0.0.1:9000\",\n \"-e\", \"AGENTTEAMS_MINIO_BUCKET=agentteams-storage\",\n \"-e\", \"AGENTTEAMS_STORAGE_PREFIX=agentteams/agentteams-storage\",\n \"-e\", \"AGENTTEAMS_FS_ENDPOINT=http://127.0.0.1:9000\",\n \"-e\", \"AGENTTEAMS_AI_GATEWAY_URL=http://$aigwDomain\",\n \"-e\", \"AGENTTEAMS_CONTROLLER_URL=http://agentteams-controller:8090\",\n \"-e\", \"AGENTTEAMS_DOCKER_NETWORK=agentteams-net\",\n \"-e\", \"AGENTTEAMS_WORKSPACE_DIR=$($config.WORKSPACE_DIR)\",\n \"-e\", \"AGENTTEAMS_HOST_SHARE_DIR=$($config.HOST_SHARE_DIR)\",\n \"-e\", \"AGENTTEAMS_MANAGER_ENABLED=true\",\n \"-e\", \"AGENTTEAMS_PORT_MANAGER_CONSOLE=$($config.PORT_MANAGER_CONSOLE)\"\n )\n\n if ($script:AGENTTEAMS_TIMEZONE) {\n $ctrlArgs += @(\"-e\", \"TZ=$($script:AGENTTEAMS_TIMEZONE)\")\n }\n if ($env:AGENTTEAMS_YOLO -eq \"1\") {\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_YOLO=1\")\n }\n if ($env:AGENTTEAMS_MATRIX_DEBUG -eq \"1\") {\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_MATRIX_DEBUG=1\")\n }\n if ($config.GITHUB_TOKEN) {\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_GITHUB_TOKEN=$($config.GITHUB_TOKEN)\")\n }\n if ($config.EMBEDDING_MODEL) {\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_EMBEDDING_MODEL=$($config.EMBEDDING_MODEL)\")\n }\n if ($config.OPENAI_BASE_URL) {\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_OPENAI_BASE_URL=$($config.OPENAI_BASE_URL)\")\n }\n if ($env:AGENTTEAMS_AI_STREAM_IDLE_TIMEOUT_SECONDS) {\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_AI_STREAM_IDLE_TIMEOUT_SECONDS=$($env:AGENTTEAMS_AI_STREAM_IDLE_TIMEOUT_SECONDS)\")\n }\n if ($script:AGENTTEAMS_LANGUAGE) {\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_LANGUAGE=$($script:AGENTTEAMS_LANGUAGE)\")\n }\n\n # Optional: CMS/ARMS observability. In embedded mode the controller\n # spawns the Manager and Workers, so it must receive these settings.\n $cmsTracesEnabled = if ($env:AGENTTEAMS_CMS_TRACES_ENABLED) { $env:AGENTTEAMS_CMS_TRACES_ENABLED } else { \"false\" }\n $cmsServiceName = if ($env:AGENTTEAMS_CMS_SERVICE_NAME) { $env:AGENTTEAMS_CMS_SERVICE_NAME } else { \"agentteams-manager\" }\n $cmsMetricsEnabled = if ($env:AGENTTEAMS_CMS_METRICS_ENABLED) { $env:AGENTTEAMS_CMS_METRICS_ENABLED } else { \"false\" }\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_CMS_TRACES_ENABLED=$cmsTracesEnabled\")\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_CMS_SERVICE_NAME=$cmsServiceName\")\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_CMS_METRICS_ENABLED=$cmsMetricsEnabled\")\n if ($env:AGENTTEAMS_CMS_ENDPOINT) {\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_CMS_ENDPOINT=$($env:AGENTTEAMS_CMS_ENDPOINT)\")\n }\n if ($env:AGENTTEAMS_CMS_LICENSE_KEY) {\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_CMS_LICENSE_KEY=$($env:AGENTTEAMS_CMS_LICENSE_KEY)\")\n }\n if ($env:AGENTTEAMS_CMS_PROJECT) {\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_CMS_PROJECT=$($env:AGENTTEAMS_CMS_PROJECT)\")\n }\n if ($env:AGENTTEAMS_CMS_WORKSPACE) {\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_CMS_WORKSPACE=$($env:AGENTTEAMS_CMS_WORKSPACE)\")\n }\n\n # Mount the docker socket so the controller can spawn manager + workers.\n $ctrlArgs += @(\"-v\", \"//var/run/docker.sock:/var/run/docker.sock\")\n $ctrlArgs += @(\"--security-opt\", \"label=disable\")\n\n # Persistent data + workspace mounts (manager workspace is bind-mounted under\n # /root/agentteams-fs/agents/manager so the controller can hand it to the spawned\n # manager container).\n $ctrlArgs += @(\"-v\", \"$($config.DATA_DIR):/data\")\n $wsDockerPath = ConvertTo-DockerPath -Path $config.WORKSPACE_DIR\n $ctrlArgs += @(\"-v\", \"${wsDockerPath}:/root/agentteams-fs/agents/manager\")\n Write-Log (Get-Msg \"host_share.sharing\" -f $config.HOST_SHARE_DIR)\n\n # Externally exposed ports — only the gateway / Higress console / Element Web,\n # since the manager console is now spawned inside its own container by the\n # controller (port mapping is handled there).\n $ctrlArgs += @(\"-p\", \"${portPrefix}$($config.PORT_GATEWAY):8080\")\n $ctrlArgs += @(\"-p\", \"${portPrefix}$($config.PORT_CONSOLE):8001\")\n $ctrlArgs += @(\"-p\", \"${portPrefix}$($config.PORT_ELEMENT_WEB):8088\")\n\n $ctrlArgs += @(\"--restart\", \"unless-stopped\")\n $ctrlArgs += $script:EMBEDDED_IMAGE\n\n Write-Log (Get-Msg \"install.starting_manager\")\n & docker $ctrlArgs\n Write-Log \"Embedded controller started: agentteams-controller\"\n\n # Wait for infra inside the controller container.\n function Wait-EmbeddedUrl {\n param([string]$Url, [string]$Container, [int]$MaxWait, [string]$Description)\n $elapsed = 0\n Write-Log \"Waiting for $Description...\"\n while ($elapsed -lt $MaxWait) {\n docker exec $Container curl -sf $Url *>$null\n if ($LASTEXITCODE -eq 0) {\n Write-Log \"$Description is ready (${elapsed}s)\"\n return $true\n }\n Start-Sleep -Seconds 2\n $elapsed += 2\n }\n Write-Host \"$($script:ESC)[31m[AgentTeams ERROR]$($script:ESC)[0m $Description not ready after ${MaxWait}s\" -ForegroundColor Red\n return $false\n }\n\n if (-not (Wait-EmbeddedUrl \"http://127.0.0.1:6167/_tuwunel/server_version\" \"agentteams-controller\" 120 \"Tuwunel (Matrix)\")) { Exit-Script 1 }\n if (-not (Wait-EmbeddedUrl \"http://127.0.0.1:9000/minio/health/live\" \"agentteams-controller\" 60 \"MinIO\")) { Exit-Script 1 }\n if (-not (Wait-EmbeddedUrl \"http://127.0.0.1:8080/status\" \"agentteams-controller\" 120 \"Higress Gateway\")) { Exit-Script 1 }\n\n # Wait for the controller to spawn the Manager Agent container.\n Write-Log \"Waiting for Manager Agent container...\"\n $mgrWait = 0\n $mgrMax = 300\n while ($mgrWait -lt $mgrMax) {\n $found = docker ps --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-manager$\"\n if ($found) {\n Write-Log \"Manager Agent container detected (${mgrWait}s)\"\n break\n }\n Start-Sleep -Seconds 3\n $mgrWait += 3\n }\n if ($mgrWait -ge $mgrMax) {\n Write-Host \"$($script:ESC)[31m[AgentTeams ERROR]$($script:ESC)[0m Manager Agent container not created after ${mgrMax}s\" -ForegroundColor Red\n Write-Log \"Controller logs:\"\n docker exec agentteams-controller tail -30 /var/log/agentteams/agentteams-controller-error.log 2>$null\n Exit-Script 1\n }\n\n # Wait for Manager Agent container to reach `running` state.\n Write-Log \"Waiting for Manager Agent to start...\"\n $agentWait = 0\n while ($agentWait -lt 120) {\n $state = docker inspect --format \"{{.State.Status}}\" agentteams-manager 2>$null\n if ($state -eq \"running\") {\n Write-Log \"Manager Agent is running\"\n break\n }\n Start-Sleep -Seconds 2\n $agentWait += 2\n }\n\n if ($env:AGENTTEAMS_YOLO -eq \"1\") {\n docker exec agentteams-manager touch /root/manager-workspace/yolo-mode 2>$null\n }\n\n # Wait for the Manager Agent's runtime + Matrix to be reachable. In embedded\n # mode Tuwunel lives in `agentteams-controller`, so the Matrix probe must target\n # the controller (the manager only exposes the agent runtime).\n Wait-ManagerReady -Container \"agentteams-manager\"\n Wait-MatrixReady -Container \"agentteams-controller\"\n\n # Wait for the controller to actually deliver the first-boot welcome\n # message. Gated by the controller on (a) Manager joining the DM room\n # and (b) Higress WASM key-auth propagating to /v1/chat/completions for\n # the Manager's gateway key — typically ~45-90s on a fresh install.\n # We poll Manager CR status.welcomeSent via the in-container AgentTeams\n # CLI, exec'd inside agentteams-controller (the source-of-truth container\n # — its bundled CLI binary is always in lockstep with the controller\n # binary serving the HTTP API, since they're the same `go build`\n # output. The agentteams-manager container's CLI may lag the controller\n # across image upgrades and silently drop the welcomeSent field,\n # leaving this loop hung). The controller mints a long-lived admin\n # SA token at startup and writes it to\n # AGENTTEAMS_AUTH_TOKEN_FILE=/var/run/agentteams/cli-token (set as a Dockerfile\n # ENV default), so a bare `docker exec agentteams-controller agt …`\n # auto-discovers both the endpoint and the token. The brief window\n # after container start before bootstrapAdminCLIToken completes is\n # absorbed by the loop's silent retry.\n $hasAgentTeamsCli = $false\n try {\n docker exec agentteams-controller sh -c 'command -v agt' *> $null\n if ($LASTEXITCODE -eq 0) { $hasAgentTeamsCli = $true }\n } catch {}\n\n if ($hasAgentTeamsCli) {\n Write-Log (Get-Msg \"install.welcome_msg.waiting\")\n $welcomeMax = if ($env:AGENTTEAMS_WELCOME_TIMEOUT) { [int]$env:AGENTTEAMS_WELCOME_TIMEOUT } else { 300 }\n $welcomeWait = 0\n $welcomeDone = $false\n while ($welcomeWait -lt $welcomeMax) {\n $wjson = \"\"\n try {\n $wjson = docker exec agentteams-controller `\n agt get managers default -o json 2>$null\n } catch {}\n if ($wjson -and ($wjson -replace '\\s', '') -match '\"welcomeSent\":true') {\n Write-Log (Get-Msg \"install.welcome_msg.confirmed\" $welcomeWait)\n $welcomeDone = $true\n break\n }\n Start-Sleep -Seconds 3\n $welcomeWait += 3\n }\n if (-not $welcomeDone) {\n # Non-fatal: install is still good. Keep going to the success\n # banner so the admin can use Element Web to nudge Manager into\n # onboarding manually (one DM message is enough).\n Write-Log (Get-Msg \"install.welcome_msg.timeout\" $welcomeMax)\n Write-Log (Get-Msg \"install.welcome_msg.timeout_hint\")\n Write-Log (Get-Msg \"install.welcome_msg.timeout_inspect\")\n }\n } else {\n Write-Log (Get-Msg \"install.welcome_msg.poll_unavailable\")\n }\n } else {\n # Run container (legacy path)\n Write-Log (Get-Msg \"install.starting_manager\")\n & docker $dockerArgs\n\n Wait-ManagerReady -Container \"agentteams-manager\"\n Wait-MatrixReady -Container \"agentteams-manager\"\n }\n\n # Create OpenAI-compatible provider if needed\n if ($config.LLM_PROVIDER -eq \"openai-compat\") {\n New-OpenAICompatProvider -BaseUrl $config.OPENAI_BASE_URL -ApiKey $config.LLM_API_KEY -ConsolePort ([int]$config.PORT_CONSOLE)\n }\n\n # Print success message\n Write-Log \"\"\n Write-Log (Get-Msg \"success.title\")\n Write-Log \"\"\n Write-Log (Get-Msg \"success.domains_configured\")\n Write-Log \" $($config.MATRIX_DOMAIN.Split(':')[0]) $($config.MATRIX_CLIENT_DOMAIN) $($config.AI_GATEWAY_DOMAIN) $($config.FS_DOMAIN) $($config.CONSOLE_DOMAIN)\"\n Write-Log \"\"\n\n $lanIP = Get-LanIP\n\n Write-Host \"$($script:ESC)[33m===============================================================$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.open_url')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[1;36m http://127.0.0.1:$($config.PORT_ELEMENT_WEB)/#/login$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.login_with')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.username' -f $config.ADMIN_USER)$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.password' -f $config.ADMIN_PASSWORD)$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.after_login')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.tell_it')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.manager_auto')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m --------------------------------------------------------------- $($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.mobile_title')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $($script:ESC)[0m\"\n if ($lanIP) {\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.mobile_step1')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.mobile_step2' -f \"http://${lanIP}:$($config.PORT_GATEWAY)\")$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.mobile_step3')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.mobile_username' -f $config.ADMIN_USER)$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.mobile_password' -f $config.ADMIN_PASSWORD)$($script:ESC)[0m\"\n } else {\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.mobile_step1')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.mobile_step2_noip' -f $config.PORT_GATEWAY)$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.mobile_noip_hint')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.mobile_step3')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.mobile_username' -f $config.ADMIN_USER)$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.mobile_password' -f $config.ADMIN_PASSWORD)$($script:ESC)[0m\"\n }\n Write-Host \"$($script:ESC)[33m $($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m===============================================================$($script:ESC)[0m\"\n\n Write-Log \"\"\n Write-Log (Get-Msg \"success.other_consoles\")\n Write-Log (Get-Msg \"success.higress_console\" -f $config.PORT_CONSOLE, $config.ADMIN_USER, $config.ADMIN_PASSWORD)\n if ($script:AGENTTEAMS_USE_EMBEDDED -ne \"1\") {\n # In embedded mode the manager runs in its own auto-spawned container with\n # its own console-port mapping handled by the controller, so don't print a\n # host-side URL/credentials hint here.\n Write-Log (Get-Msg \"success.manager_console\" -f $config.PORT_MANAGER_CONSOLE)\n Write-Log (Get-Msg \"success.manager_console_gateway\" -f $config.ADMIN_USER, $config.ADMIN_PASSWORD)\n }\n Write-Log \"\"\n Write-Log (Get-Msg \"success.switch_llm.title\")\n Write-Log (Get-Msg \"success.switch_llm.hint\")\n Write-Log (Get-Msg \"success.switch_llm.docs\")\n Write-Log (Get-Msg \"success.switch_llm.url\")\n Write-Log \"\"\n Write-Log (Get-Msg \"success.tip\")\n Write-Log \"\"\n if ($config.LOCAL_ONLY -ne \"1\") {\n Write-Host (Get-Msg \"port.local_only.https_hint\") -ForegroundColor Yellow\n Write-Log \"\"\n }\n Write-Log (Get-Msg \"success.config_file\" -f $script:AGENTTEAMS_ENV_FILE)\n\n Write-Log (Get-Msg \"success.data_volume\" -f $config.DATA_DIR)\n\n Write-Log (Get-Msg \"success.workspace\" -f $config.WORKSPACE_DIR)\n}\n\n# ============================================================\n# Worker Installation\n# ============================================================\n\nfunction Install-Worker {\n param(\n [string]$Name,\n [string]$Fs,\n [string]$FsKey,\n [string]$FsSecret,\n [switch]$Reset,\n [switch]$FindSkills,\n [string]$SkillsApiUrl\n )\n\n # Validate required parameters\n if (-not $Name) {\n Write-Error (Get-Msg \"error.name_required\")\n }\n if (-not $Fs) {\n Write-Error (Get-Msg \"error.fs_required\")\n }\n if (-not $FsKey) {\n Write-Error (Get-Msg \"error.fs_key_required\")\n }\n if (-not $FsSecret) {\n Write-Error (Get-Msg \"error.fs_secret_required\")\n }\n\n $containerName = \"agentteams-worker-$Name\"\n\n # Handle reset\n if ($Reset) {\n Write-Log (Get-Msg \"worker.resetting\" -f $Name)\n docker stop $containerName *>$null\n docker rm $containerName *>$null\n }\n\n # Check for existing container\n $existing = docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^$containerName$\"\n if ($existing) {\n Write-Error (Get-Msg \"worker.exists\" -f $containerName)\n }\n\n # Detect timezone and registry\n $timezone = Get-AgentTeamsTimeZone\n $registry = Get-Registry -Timezone $timezone\n $workerImage = if ($env:AGENTTEAMS_INSTALL_WORKER_IMAGE) {\n $env:AGENTTEAMS_INSTALL_WORKER_IMAGE\n } else {\n \"$registry/agentteams/agentteams-worker:$($script:AGENTTEAMS_VERSION)\"\n }\n\n Write-Log (Get-Msg \"worker.starting\" -f $Name)\n\n $dockerArgs = @(\n \"run\", \"-d\",\n \"--name\", $containerName,\n \"-e\", \"HOME=/root/agentteams-fs/agents/$Name\",\n \"-w\", \"/root/agentteams-fs/agents/$Name\",\n \"-e\", \"AGENTTEAMS_WORKER_NAME=$Name\",\n \"-e\", \"AGENTTEAMS_FS_ENDPOINT=$Fs\",\n \"-e\", \"AGENTTEAMS_FS_ACCESS_KEY=$FsKey\",\n \"-e\", \"AGENTTEAMS_FS_SECRET_KEY=$FsSecret\"\n )\n\n if (-not $SkillsApiUrl) {\n if ($env:AGENTTEAMS_SKILLS_API_URL) {\n $SkillsApiUrl = $env:AGENTTEAMS_SKILLS_API_URL\n } else {\n $SkillsApiUrl = \"nacos://market.agentteams.io:80/public\"\n }\n }\n\n if ($FindSkills -and $SkillsApiUrl) {\n $dockerArgs += @(\"-e\", \"SKILLS_API_URL=$SkillsApiUrl\")\n Write-Log (Get-Msg \"worker.skills_url\" -f $SkillsApiUrl)\n }\n if ($env:AGENTTEAMS_NACOS_USERNAME) {\n $dockerArgs += @(\"-e\", \"AGENTTEAMS_NACOS_USERNAME=$($env:AGENTTEAMS_NACOS_USERNAME)\")\n }\n if ($env:AGENTTEAMS_NACOS_PASSWORD) {\n $dockerArgs += @(\"-e\", \"AGENTTEAMS_NACOS_PASSWORD=$($env:AGENTTEAMS_NACOS_PASSWORD)\")\n }\n if ($env:AGENTTEAMS_NACOS_TOKEN) {\n $dockerArgs += @(\"-e\", \"AGENTTEAMS_NACOS_TOKEN=$($env:AGENTTEAMS_NACOS_TOKEN)\")\n }\n\n $dockerArgs += @(\"--restart\", \"unless-stopped\", $workerImage)\n\n & docker $dockerArgs\n\n Write-Log \"\"\n Write-Log (Get-Msg \"worker.started\" -f $Name)\n Write-Log (Get-Msg \"worker.container\" -f $containerName)\n Write-Log (Get-Msg \"worker.view_logs\" -f $containerName)\n}\n\n# ============================================================\n# Uninstall\n# ============================================================\n\nfunction Uninstall-AgentTeams {\n Write-Log (Get-Msg \"uninstall.title\")\n\n # Stop and remove manager\n $manager = docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-manager$\"\n if ($manager) {\n Write-Log (Get-Msg \"uninstall.stopping_manager\")\n docker stop agentteams-manager *>$null\n docker rm agentteams-manager *>$null\n }\n\n # Stop and remove workers\n $workers = docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-worker-\"\n if ($workers) {\n Write-Log (Get-Msg \"uninstall.stopping_workers\")\n $workers | ForEach-Object {\n docker stop $_ *>$null\n docker rm $_ *>$null\n Write-Log (Get-Msg \"uninstall.removed\" -f $_)\n }\n }\n\n # Stop and remove docker-proxy (legacy <= v1.0.x; current arch uses\n # agentteams-controller for the same role)\n $proxy = docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-docker-proxy$\"\n if ($proxy) {\n Write-Log (Get-Msg \"uninstall.removing_proxy\")\n docker stop agentteams-docker-proxy *>$null\n docker rm agentteams-docker-proxy *>$null\n }\n\n # Stop and remove the embedded controller container. MUST happen\n # before the `docker volume rm agentteams-data` step below -- in embedded\n # mode agentteams-controller mounts agentteams-data at /data (Tuwunel DB,\n # MinIO state, Higress state, all the room messages), and `volume rm`\n # against an in-use volume fails silently. Skipping this used to\n # leave room/message history behind across \"uninstall + reinstall\"\n # cycles. See PR #692.\n $controller = docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-controller$\"\n if ($controller) {\n Write-Log (Get-Msg \"uninstall.stopping_controller\")\n docker stop agentteams-controller *>$null\n docker rm agentteams-controller *>$null\n }\n\n # Remove Docker volume (read custom name from env file if available)\n $dataVolume = \"agentteams-data\"\n if (Test-Path $script:AGENTTEAMS_ENV_FILE) {\n $envContent = Get-Content $script:AGENTTEAMS_ENV_FILE -ErrorAction SilentlyContinue\n $dataLine = $envContent | Select-String \"^AGENTTEAMS_DATA_DIR=\"\n if ($dataLine) {\n $parsed = ($dataLine -split \"=\", 2)[1]\n if ($parsed) { $dataVolume = $parsed }\n }\n }\n $volume = docker volume ls -q 2>$null | Select-String \"^${dataVolume}$\"\n if ($volume) {\n Write-Log (Get-Msg \"uninstall.removing_volume\")\n docker volume rm $dataVolume *>$null\n }\n\n # Remove Docker network\n $network = docker network ls --format \"{{.Name}}\" 2>$null | Select-String \"^agentteams-net$\"\n if ($network) {\n Write-Log (Get-Msg \"uninstall.removing_network\")\n docker network rm agentteams-net *>$null\n }\n\n # Remove workspace directory\n $workspaceDir = \"$env:USERPROFILE\\agentteams-manager\"\n if (Test-Path $script:AGENTTEAMS_ENV_FILE) {\n $envContent = Get-Content $script:AGENTTEAMS_ENV_FILE -ErrorAction SilentlyContinue\n $wsLine = $envContent | Select-String \"^AGENTTEAMS_WORKSPACE_DIR=\"\n if ($wsLine) {\n $workspaceDir = ($wsLine -split \"=\", 2)[1]\n }\n }\n if ($workspaceDir -and (Test-Path $workspaceDir)) {\n Write-Log (Get-Msg \"uninstall.removing_workspace\" -f $workspaceDir)\n Remove-Item -Recurse -Force $workspaceDir -ErrorAction SilentlyContinue\n }\n\n # Remove env file\n if (Test-Path $script:AGENTTEAMS_ENV_FILE) {\n Write-Log (Get-Msg \"uninstall.removing_env\" -f $script:AGENTTEAMS_ENV_FILE)\n Remove-Item -Force $script:AGENTTEAMS_ENV_FILE\n }\n\n # Remove install log (stop transcript first to release the file)\n if (Test-Path $script:AGENTTEAMS_LOG_FILE) {\n Write-Log (Get-Msg \"uninstall.removing_log\" -f $script:AGENTTEAMS_LOG_FILE)\n try { Stop-Transcript *>$null } catch {}\n Remove-Item -Force $script:AGENTTEAMS_LOG_FILE -ErrorAction SilentlyContinue\n }\n\n Write-Log \"\"\n Write-Log (Get-Msg \"uninstall.done\")\n}\n\n# ============================================================\n# Main Entry Point\n# ============================================================\n\ntry {\n switch ($Command) {\n \"manager\" {\n Install-Manager\n }\n \"worker\" {\n Install-Worker -Name $Name -Fs $Fs -FsKey $FsKey -FsSecret $FsSecret -Reset:$Reset -FindSkills:$FindSkills -SkillsApiUrl $SkillsApiUrl\n }\n \"uninstall\" {\n Uninstall-AgentTeams\n }\n }\n} catch {\n Exit-Script 1\n}\n\n# Stop transcript logging\ntry {\n Stop-Transcript -ErrorAction SilentlyContinue | Out-Null\n} catch {\n # Ignore errors when stopping transcript\n}\n"}, "e6fa64c0cb7b14600c6abbf9b4a25f47bf544c1f": {"t": 1785764743.7514598, "n": 197190, "h": "2cc737c032a9e10a", "c": "#!/usr/bin/env powershell\n# agentteams-install.ps1 - One-click installation for AgentTeams Manager and Worker on Windows\n# Requires PowerShell 7.0+ (recommended)\n#\n# Usage:\n# .\\agentteams-install.ps1 # Interactive installation (choose Quick Start or Manual)\n# .\\agentteams-install.ps1 manager # Same as above (explicit)\n# .\\agentteams-install.ps1 worker --name <name> ... # Worker installation\n#\n# Onboarding Modes:\n# Quick Start - Fast installation with all default values (recommended)\n# Manual - Customize each option step by step\n#\n# Environment variables (for automation):\n# AGENTTEAMS_NON_INTERACTIVE Skip all prompts, use defaults (default: 0)\n# AGENTTEAMS_LLM_PROVIDER LLM provider (default: openai-compat for zh non-interactive Token Plan; qwen for en)\n# AGENTTEAMS_DEFAULT_MODEL Default model (default: qwen3.6-plus for zh Token Plan and en non-interactive)\n# AGENTTEAMS_OPENAI_BASE_URL OpenAI-compatible base URL (default for zh non-interactive: Alibaba Token Plan endpoint)\n# AGENTTEAMS_LLM_API_KEY LLM API key (required)\n# AGENTTEAMS_ADMIN_USER Admin username (default: admin)\n# AGENTTEAMS_ADMIN_PASSWORD Admin password (auto-generated if not set, min 8 chars)\n# AGENTTEAMS_MATRIX_DOMAIN Matrix domain (default: matrix-local.agentteams.io:18080)\n# AGENTTEAMS_MOUNT_SOCKET Mount container runtime socket (default: 1)\n# AGENTTEAMS_MATRIX_E2EE Matrix E2EE (default: 0, disabled)\n# AGENTTEAMS_DATA_DIR Docker volume name for persistent data (default: agentteams-data)\n# AGENTTEAMS_WORKSPACE_DIR Host directory for manager workspace (default: ~/agentteams-manager)\n# AGENTTEAMS_VERSION Image tag (default: latest)\n# AGENTTEAMS_REGISTRY Image registry (default: auto-detected by timezone)\n# AGENTTEAMS_INSTALL_MANAGER_IMAGE Override manager image (e.g., local build)\n# AGENTTEAMS_INSTALL_WORKER_IMAGE Override worker image (e.g., local build)\n# AGENTTEAMS_INSTALL_COPAW_WORKER_IMAGE Override copaw worker image (e.g., local build)\n# AGENTTEAMS_INSTALL_HERMES_WORKER_IMAGE Override hermes worker image (e.g., local build)\n# AGENTTEAMS_PORT_GATEWAY Host port for Higress gateway (default: 18080)\n# AGENTTEAMS_PORT_CONSOLE Host port for Higress console (default: 18001)\n# AGENTTEAMS_PORT_ELEMENT_WEB Host port for Element Web direct access (default: 18088)\n\n#Requires -Version 5.1\n\n[CmdletBinding()]\nparam(\n [Parameter(Position = 0)]\n [ValidateSet(\"manager\", \"worker\", \"uninstall\")]\n [string]$Command = \"manager\",\n\n # Worker options\n [string]$Name,\n [string]$Fs,\n [string]$FsKey,\n [string]$FsSecret,\n [switch]$Reset,\n [switch]$FindSkills,\n [string]$SkillsApiUrl,\n\n # General options\n [switch]$NonInteractive,\n [string]$EnvFile\n)\n\n# ============================================================\n# Configuration\n# ============================================================\n\n$script:AGENTTEAMS_VERSION = if ($env:AGENTTEAMS_VERSION) { $env:AGENTTEAMS_VERSION } else { \"latest\" }\n$script:AGENTTEAMS_NON_INTERACTIVE = if ($env:AGENTTEAMS_NON_INTERACTIVE -eq \"1\" -or $NonInteractive) { $true } else { $false }\n$script:AGENTTEAMS_MOUNT_SOCKET = if ($env:AGENTTEAMS_MOUNT_SOCKET -eq \"0\") { $false } else { $true }\n$script:AGENTTEAMS_ENV_FILE = if ($EnvFile) { $EnvFile } elseif ($env:AGENTTEAMS_ENV_FILE) { $env:AGENTTEAMS_ENV_FILE } else { \"$env:USERPROFILE\\agentteams-manager.env\" }\n$script:StepResult = \"\" # Used by state machine to signal \"back\" navigation\n$script:config = @{} # Shared config hashtable for step functions\n\nfunction Move-LegacyAgentTeamsEnv {\n if ($EnvFile -or $env:AGENTTEAMS_ENV_FILE -or (Test-Path $script:AGENTTEAMS_ENV_FILE)) {\n return\n }\n\n $legacyCandidates = @(\n \"$env:USERPROFILE\\hiclaw-manager.env\",\n \".\\hiclaw-manager.env\",\n \".\\agentteams-manager.env\"\n )\n foreach ($legacy in $legacyCandidates) {\n if (Test-Path $legacy) {\n Write-Log \"Migrating legacy env file $legacy to $script:AGENTTEAMS_ENV_FILE...\"\n Move-Item $legacy $script:AGENTTEAMS_ENV_FILE -ErrorAction Stop\n return\n }\n }\n}\n\n# ANSI escape character for terminal colors\n$script:ESC = [char]0x1B\n\n# ============================================================\n# Log all output to file\n# ============================================================\n\n$script:AGENTTEAMS_LOG_FILE = \"$env:USERPROFILE\\agentteams-install.log\"\n\n# Start transcript for logging (PowerShell's built-in logging mechanism)\ntry {\n Start-Transcript -Path $script:AGENTTEAMS_LOG_FILE -Append -ErrorAction SilentlyContinue | Out-Null\n} catch {\n # If transcript fails, continue without logging\n}\n\nWrite-Host \"\"\nWrite-Host \"========================================\"\nWrite-Host \"AgentTeams Installation Log\"\nWrite-Host \"Started: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')\"\nWrite-Host \"User: $env:USERNAME\"\nWrite-Host \"System: $(hostname)\"\nWrite-Host \"Log file: $($script:AGENTTEAMS_LOG_FILE)\"\nWrite-Host \"========================================\"\nWrite-Host \"\"\n\n# ============================================================\n# Utility Functions\n# ============================================================\n\nfunction Write-Log {\n param([string]$Message)\n Write-Host \"$($script:ESC)[36m[AgentTeams]$($script:ESC)[0m $Message\"\n}\n\nfunction Write-Error {\n param([string]$Message)\n Write-Host \"$($script:ESC)[31m[AgentTeams ERROR]$($script:ESC)[0m $Message\" -ForegroundColor Red\n throw $Message\n}\n\nfunction Write-Warning {\n param([string]$Message)\n Write-Host \"$($script:ESC)[33m[AgentTeams WARNING]$($script:ESC)[0m $Message\"\n}\n\n# Pause before exit on error so user can read the message when running via double-click\nfunction Exit-Script {\n param([int]$ExitCode = 0)\n if ($ExitCode -ne 0 -and -not $script:AGENTTEAMS_NON_INTERACTIVE) {\n Write-Host \"\"\n Write-Host \"Press Enter to exit.\" -NoNewline\n $Host.UI.ReadLine()\n }\n exit $ExitCode\n}\n\nfunction Test-DockerRunning {\n try {\n $null = docker info 2>&1\n if ($LASTEXITCODE -ne 0) {\n return $false\n }\n return $true\n }\n catch {\n return $false\n }\n}\n\nfunction Get-AgentTeamsTimeZone {\n try {\n if ($env:AGENTTEAMS_TIMEZONE) {\n return $env:AGENTTEAMS_TIMEZONE\n }\n\n $tz = (Get-TimeZone).Id\n # Convert Windows timezone to IANA format\n $tzMap = @{\n \"China Standard Time\" = \"Asia/Shanghai\"\n \"Pacific Standard Time\" = \"America/Los_Angeles\"\n \"Mountain Standard Time\" = \"America/Denver\"\n \"Central Standard Time\" = \"America/Chicago\"\n \"Eastern Standard Time\" = \"America/New_York\"\n \"GMT Standard Time\" = \"Europe/London\"\n \"Central European Standard Time\" = \"Europe/Berlin\"\n \"Tokyo Standard Time\" = \"Asia/Tokyo\"\n \"Singapore Standard Time\" = \"Asia/Singapore\"\n \"Korea Standard Time\" = \"Asia/Seoul\"\n \"India Standard Time\" = \"Asia/Kolkata\"\n }\n\n if ($tzMap.ContainsKey($tz)) {\n return $tzMap[$tz]\n }\n return $tz\n }\n catch {\n return \"Asia/Shanghai\"\n }\n}\n\nfunction Get-Registry {\n param([string]$Timezone)\n\n if ($env:AGENTTEAMS_REGISTRY) {\n return $env:AGENTTEAMS_REGISTRY\n }\n\n # Americas\n if ($Timezone -match \"^America/\") {\n return \"higress-registry.us-west-1.cr.aliyuncs.com\"\n }\n\n # Southeast Asia\n if ($Timezone -match \"^(Asia/Singapore|Asia/Bangkok|Asia/Jakarta|Asia/Makassar|Asia/Jayapura|Asia/Kuala_Lumpur|Asia/Ho_Chi_Minh|Asia/Manila|Asia/Yangon|Asia/Vientiane|Asia/Phnom_Penh|Asia/Pontianak|Asia/Ujung_Pandang)\") {\n return \"higress-registry.ap-southeast-7.cr.aliyuncs.com\"\n }\n\n # Default: China\n return \"higress-registry.cn-hangzhou.cr.aliyuncs.com\"\n}\n\nfunction Get-AgentTeamsLanguage {\n param([string]$Timezone)\n $chineseZones = @(\n \"Asia/Shanghai\", \"Asia/Chongqing\", \"Asia/Harbin\", \"Asia/Urumqi\",\n \"Asia/Taipei\", \"Asia/Hong_Kong\", \"Asia/Macau\"\n )\n if ($chineseZones -contains $Timezone) { return \"zh\" }\n return \"en\"\n}\n\n# ============================================================\n# Centralized message dictionary and Get-Msg function\n# ============================================================\n\n$script:Messages = @{\n # --- Timezone detection messages ---\n \"tz.warning.title\" = @{ zh = \"无法自动检测时区。\"; en = \"Could not detect timezone automatically.\" }\n \"tz.warning.prompt\" = @{ zh = \"请输入您的时区(例如 Asia/Shanghai、America/New_York)。\"; en = \"Please enter your timezone (e.g., Asia/Shanghai, America/New_York).\" }\n \"tz.default\" = @{ zh = \"使用默认时区: {0}\"; en = \"Using default timezone: {0}\" }\n \"tz.input_prompt\" = @{ zh = \"时区\"; en = \"Timezone\" }\n\n # --- Installation title and info ---\n \"install.title\" = @{ zh = \"=== AgentTeams Manager 安装 ===\"; en = \"=== AgentTeams Manager Installation ===\" }\n \"install.registry\" = @{ zh = \"镜像仓库: {0}\"; en = \"Registry: {0}\" }\n \"install.dir\" = @{ zh = \"安装目录: {0}\"; en = \"Installation directory: {0}\" }\n \"install.dir_hint\" = @{ zh = \" (env 文件 'agentteams-manager.env' 将保存到 HOME 目录。)\"; en = \" (The env file 'agentteams-manager.env' will be saved to your HOME directory.)\" }\n \"install.dir_hint2\" = @{ zh = \" (请从您希望管理此安装的目录运行此脚本。)\"; en = \" (Run this script from the directory where you want to manage this installation.)\" }\n\n # --- Onboarding mode ---\n \"install.mode.title\" = @{ zh = \"--- Onboarding 模式 ---\"; en = \"--- Onboarding Mode ---\" }\n \"install.mode.choose\" = @{ zh = \"选择安装模式:\"; en = \"Choose your installation mode:\" }\n \"install.mode.quickstart\" = @{ zh = \" 1) 快速开始 - 使用阿里云通义 Token 套餐快速安装(推荐)\"; en = \" 1) Quick Start - Fast installation with Qwen Cloud (recommended)\" }\n \"install.mode.manual\" = @{ zh = \" 2) 手动配置 - 选择 LLM 提供商并自定义选项\"; en = \" 2) Manual - Choose LLM provider and customize options\" }\n \"install.mode.prompt\" = @{ zh = \"请选择 [1/2]\"; en = \"Enter choice [1/2]\" }\n \"install.mode.quickstart_selected\" = @{ zh = \"已选择快速开始模式 - 使用阿里云通义 Token 套餐\"; en = \"Quick Start mode selected - using Qwen Cloud\" }\n \"install.mode.manual_selected\" = @{ zh = \"已选择手动配置模式 - 您将选择 LLM 提供商并自定义选项\"; en = \"Manual mode selected - you will choose LLM provider and customize options\" }\n \"install.mode.invalid\" = @{ zh = \"无效选择,默认使用快速开始模式\"; en = \"Invalid choice, defaulting to Quick Start mode\" }\n\n # --- Existing installation detected ---\n \"install.existing.detected\" = @{ zh = \"检测到已有 Manager 安装(env 文件: {0})\"; en = \"Existing Manager installation detected (env file: {0})\" }\n \"install.existing.choose\" = @{ zh = \"选择操作:\"; en = \"Choose an action:\" }\n \"install.existing.upgrade\" = @{ zh = \" 1) 就地升级(保留数据、工作空间、env 文件)\"; en = \" 1) In-place upgrade (keep data, workspace, env file)\" }\n \"install.existing.reinstall\" = @{ zh = \" 2) 全新重装(删除所有数据,重新开始)\"; en = \" 2) Clean reinstall (remove all data, start fresh)\" }\n \"install.existing.cancel\" = @{ zh = \" 3) 取消\"; en = \" 3) Cancel\" }\n \"install.existing.prompt\" = @{ zh = \"请选择 [1/2/3]\"; en = \"Enter choice [1/2/3]\" }\n \"install.existing.upgrade_noninteractive\" = @{ zh = \"非交互模式: 执行就地升级...\"; en = \"Non-interactive mode: performing in-place upgrade...\" }\n \"install.existing.upgrading\" = @{ zh = \"执行就地升级...\"; en = \"Performing in-place upgrade...\" }\n \"install.existing.warn_manager_stop\" = @{ zh = \"警告: Manager 容器将被停止并重新创建。\"; en = \"WARNING: Manager container will be stopped and recreated.\" }\n \"install.existing.warn_worker_recreate\" = @{ zh = \"警告: Worker 容器也将被重新创建(以更新 Manager IP)。\"; en = \"WARNING: Worker containers will also be recreated (to update Manager IP in hosts).\" }\n \"install.existing.continue_prompt\" = @{ zh = \"继续?[y/N]\"; en = \"Continue? [y/N]\" }\n \"install.existing.cancelled\" = @{ zh = \"安装已取消。\"; en = \"Installation cancelled.\" }\n \"install.existing.stopping_manager\" = @{ zh = \"停止并移除现有 manager 容器...\"; en = \"Stopping and removing existing manager container...\" }\n \"install.existing.stopping_workers\" = @{ zh = \"停止并移除现有 worker 容器...\"; en = \"Stopping and removing existing worker containers...\" }\n \"install.existing.removed\" = @{ zh = \" 已移除: {0}\"; en = \" Removed: {0}\" }\n\n # --- Upgrade mode sub-menu ---\n \"upgrade.mode.prompt\" = @{ zh = \"请选择升级方式:\"; en = \"Select upgrade mode:\" }\n \"upgrade.mode.keep_all\" = @{ zh = \" 1) 保留所有参数,一键升级(推荐)\"; en = \" 1) Keep all parameters, quick upgrade (recommended)\" }\n \"upgrade.mode.confirm_each\" = @{ zh = \" 2) 逐个确认参数(可修改)\"; en = \" 2) Confirm each parameter (can modify)\" }\n \"upgrade.mode.back\" = @{ zh = \" 3) 返回\"; en = \" 3) Back\" }\n\n # --- Clean reinstall messages ---\n \"install.reinstall.performing\" = @{ zh = \"执行全新重装...\"; en = \"Performing clean reinstall...\" }\n \"install.reinstall.warn_stop\" = @{ zh = \"警告: 以下运行中的容器将被停止:\"; en = \"WARNING: The following running containers will be stopped:\" }\n \"install.reinstall.warn_delete\" = @{ zh = \"警告: 以下内容将被删除:\"; en = \"WARNING: This will DELETE the following:\" }\n \"install.reinstall.warn_volume\" = @{ zh = \" - Docker 卷: agentteams-data\"; en = \" - Docker volume: agentteams-data\" }\n \"install.reinstall.warn_env\" = @{ zh = \" - Env 文件: {0}\"; en = \" - Env file: {0}\" }\n \"install.reinstall.warn_workspace\" = @{ zh = \" - Manager 工作空间: {0}\"; en = \" - Manager workspace: {0}\" }\n \"install.reinstall.warn_workers\" = @{ zh = \" - 所有 worker 容器\"; en = \" - All worker containers\" }\n \"install.reinstall.warn_proxy\" = @{ zh = \" - Docker API 代理容器: agentteams-controller\"; en = \" - Docker API proxy container: agentteams-controller\" }\n \"install.reinstall.removing_proxy\" = @{ zh = \"正在移除 Docker API 代理容器: agentteams-controller\"; en = \"Removing Docker API proxy container: agentteams-controller\" }\n \"install.reinstall.warn_network\" = @{ zh = \" - Docker 网络: agentteams-net\"; en = \" - Docker network: agentteams-net\" }\n \"install.reinstall.removing_network\" = @{ zh = \"正在移除 Docker 网络: agentteams-net\"; en = \"Removing Docker network: agentteams-net\" }\n \"install.reinstall.confirm_type\" = @{ zh = \"请输入工作空间路径以确认删除(或按 Ctrl+C 取消):\"; en = \"To confirm deletion, please type the workspace path:\" }\n \"install.reinstall.confirm_path\" = @{ zh = \"输入路径以确认(或按 Ctrl+C 取消)\"; en = \"Type the path to confirm (or press Ctrl+C to cancel)\" }\n \"install.reinstall.path_mismatch\" = @{ zh = \"路径不匹配。中止重装。输入: '{0}',期望: '{1}'\"; en = \"Path mismatch. Aborting reinstall. Input: '{0}', Expected: '{1}'\" }\n \"install.reinstall.confirmed\" = @{ zh = \"已确认。正在清理...\"; en = \"Confirmed. Cleaning up...\" }\n \"install.reinstall.removed_worker\" = @{ zh = \" 已移除 worker: {0}\"; en = \" Removed worker: {0}\" }\n \"install.reinstall.removing_volume\" = @{ zh = \"正在移除 Docker 卷: agentteams-data\"; en = \"Removing Docker volume: agentteams-data\" }\n \"install.reinstall.warn_volume_fail\" = @{ zh = \" 警告: 无法移除卷(可能有引用)\"; en = \" Warning: Could not remove volume (may have references)\" }\n \"install.reinstall.removing_workspace\" = @{ zh = \"正在移除工作空间目录: {0}\"; en = \"Removing workspace directory: {0}\" }\n \"install.reinstall.removing_env\" = @{ zh = \"正在移除 env 文件: {0}\"; en = \"Removing env file: {0}\" }\n \"install.reinstall.cleanup_done\" = @{ zh = \"清理完成。开始全新安装...\"; en = \"Cleanup complete. Starting fresh installation...\" }\n \"install.reinstall.failed_rm_workspace\" = @{ zh = \"无法移除工作空间目录\"; en = \"Failed to remove workspace directory\" }\n\n # --- Orphan volume detection ---\n \"install.orphan_volume.detected\" = @{ zh = \"⚠️ 检测到残留数据卷 '{0}',但未找到对应的 env 配置文件。\"; en = \"⚠️ Found leftover data volume '{0}' but no matching env config file.\" }\n \"install.orphan_volume.warn\" = @{ zh = \"这可能是之前安装的残留数据,会导致新安装出现异常(如密码冲突、服务启动失败)。\"; en = \"This is likely leftover data from a previous installation and may cause issues (e.g., credential conflicts, service startup failures).\" }\n \"install.orphan_volume.choose\" = @{ zh = \"选择操作:\"; en = \"Choose an action:\" }\n \"install.orphan_volume.clean\" = @{ zh = \" 1) 清理残留数据卷后继续安装(推荐)\"; en = \" 1) Remove leftover volume and continue installation (recommended)\" }\n \"install.orphan_volume.keep\" = @{ zh = \" 2) 保留数据卷继续安装(可能出现异常)\"; en = \" 2) Keep the volume and continue installation (may cause issues)\" }\n \"install.orphan_volume.prompt\" = @{ zh = \"请选择 [1/2]\"; en = \"Enter choice [1/2]\" }\n \"install.orphan_volume.cleaning\" = @{ zh = \"正在清理残留数据卷...\"; en = \"Removing leftover data volume...\" }\n \"install.orphan_volume.cleaned\" = @{ zh = \"残留数据卷已清理。继续全新安装...\"; en = \"Leftover volume removed. Continuing with fresh installation...\" }\n \"install.orphan_volume.keeping\" = @{ zh = \"保留数据卷,继续安装。如遇异常请选择全新重装。\"; en = \"Keeping existing volume. If you encounter issues, consider a clean reinstall.\" }\n \"install.orphan_volume.clean_noninteractive\" = @{ zh = \"非交互模式: 自动清理残留数据卷...\"; en = \"Non-interactive mode: automatically removing leftover volume...\" }\n\n # --- Loading existing config ---\n \"install.loading_config\" = @{ zh = \"从 {0} 加载已有配置(shell 环境变量优先)...\"; en = \"Loading existing config from {0} (shell env vars take priority)...\" }\n\n # --- LLM Configuration ---\n \"llm.title\" = @{ zh = \"--- LLM 配置 ---\"; en = \"--- LLM Configuration ---\" }\n \"llm.provider.label\" = @{ zh = \" 提供商: {0}\"; en = \" Provider: {0}\" }\n \"llm.model.label\" = @{ zh = \" 模型: {0}\"; en = \" Model: {0}\" }\n \"llm.provider.qwen\" = @{ zh = \" 提供商: qwen(阿里云百炼)\"; en = \" Provider: qwen (Alibaba Cloud Bailian)\" }\n \"llm.provider.qwen_default\" = @{ zh = \" 提供商: {0}(默认)\"; en = \" Provider: {0} (default)\" }\n \"llm.model.default\" = @{ zh = \" 模型: {0}(默认)\"; en = \" Model: {0} (default)\" }\n \"llm.apikey_hint_bailian\" = @{ zh = \" 提示: 获取阿里云百炼(DashScope)API Key:\"; en = \" Hint: Get your Alibaba Cloud Bailian (DashScope) API Key:\" }\n \"llm.apikey_url_bailian\" = @{ zh = \" https://www.aliyun.com/product/bailian\"; en = \" https://www.aliyun.com/product/bailian\" }\n \"llm.apikey_hint_qwencloud\" = @{ zh = \" 提示: 从 Qwen Cloud(国际站)获取 DASHSCOPE_API_KEY:\"; en = \" Hint: Get your DASHSCOPE_API_KEY for Qwen Cloud (international) from:\" }\n \"llm.apikey_url_qwencloud\" = @{ zh = \" https://home.qwencloud.com/api-keys (文档: https://docs.qwencloud.com/)\"; en = \" https://home.qwencloud.com/api-keys | Docs: https://docs.qwencloud.com/\" }\n \"llm.apikey_hint_tokenplan\" = @{ zh = \" 提示: 获取 DashScope API Key 或开通通义 Token 套餐,请参考:\"; en = \" Hint: Get your DashScope or Token Plan API key (Alibaba Model Studio):\" }\n \"llm.apikey_url_tokenplan\" = @{ zh = \" https://help.aliyun.com/zh/model-studio/token-plan-quickstart\"; en = \" https://common-buy.aliyun.com/token-plan/ | https://help.aliyun.com/zh/model-studio/token-plan-quickstart\" }\n \"llm.apikey_hint_codingplan\" = @{ zh = \" 提示: 获取 DashScope API Key(Coding 套餐 / coding.dashscope 接口):\"; en = \" Hint: Get your DashScope API key for Coding Plan (coding.dashscope endpoint):\" }\n \"llm.apikey_url_codingplan\" = @{ zh = \" https://help.aliyun.com/zh/model-studio/get-api-key\"; en = \" https://help.aliyun.com/zh/model-studio/get-api-key\" }\n \"llm.apikey_prompt\" = @{ zh = \"LLM API Key\"; en = \"LLM API Key\" }\n \"llm.providers_title\" = @{ zh = \"可用 LLM 提供商:\"; en = \"Available LLM Providers:\" }\n \"llm.provider.alibaba\" = @{ zh = \" 1) 阿里云通义 Token 套餐 - 推荐中国用户使用\"; en = \" 1) Qwen Cloud - International (OpenAI-compatible API, recommended)\" }\n \"llm.provider.openai_compat\" = @{ zh = \" 2) OpenAI 兼容 API - 自定义 Base URL(OpenAI、DeepSeek 等)\"; en = \" 2) OpenAI-compatible API - Custom Base URL (OpenAI, DeepSeek, etc.)\" }\n \"llm.provider.select\" = @{ zh = \"选择提供商 [1/2]\"; en = \"Select provider [1/2]\" }\n \"llm.alibaba.models_title\" = @{ zh = \"选择阿里云模型接入方式:\"; en = \"Select Alibaba Cloud model access:\" }\n \"llm.alibaba.model.tokenplan\" = @{ zh = \" 1) 阿里云通义 Token 套餐 - 兼容模式(推荐)\"; en = \" 1) Alibaba Cloud Token Plan - compatible-mode (recommended)\" }\n \"llm.alibaba.model.bailian\" = @{ zh = \" 2) 阿里云百炼 - DashScope 通用兼容接口\"; en = \" 2) Alibaba Cloud Bailian - DashScope compatible mode\" }\n \"llm.alibaba.model.codingplan_legacy\" = @{ zh = \" 3) 阿里云 Coding 套餐 - 旧版端点(兼容保留)\"; en = \" 3) Alibaba Cloud Coding Plan - legacy endpoint (backward compatible)\" }\n \"llm.alibaba.model.select\" = @{ zh = \"选择接入方式 [1/2/3]\"; en = \"Select access option [1/2/3]\" }\n \"llm.alibaba.model.invalid\" = @{ zh = \"无效选择: {0}(请输入 1、2 或 3)\"; en = \"Invalid choice: {0} (please enter 1, 2, or 3)\" }\n \"llm.codingplan.models_title\" = @{ zh = \"选择通义 Token 套餐默认模型:\"; en = \"Select Qwen Cloud default model:\" }\n \"llm.codingplan.model.qwen36plus\" = @{ zh = \" 1) qwen3.6-plus - 千问 3.6(推荐)\"; en = \" 1) qwen3.6-plus - Qwen 3.6 (recommended)\" }\n \"llm.codingplan.model.glm5\" = @{ zh = \" 2) glm-5 - 智谱 GLM-5(编程推荐)\"; en = \" 2) glm-5 - Zhipu GLM-5 (recommended for coding)\" }\n \"llm.codingplan.model.kimi\" = @{ zh = \" 3) kimi-k2.5 - Moonshot Kimi K2.5\"; en = \" 3) kimi-k2.5 - Moonshot Kimi K2.5\" }\n \"llm.codingplan.model.minimax\" = @{ zh = \" 4) MiniMax-M2.5 - MiniMax M2.5\"; en = \" 4) MiniMax-M2.5 - MiniMax M2.5\" }\n \"llm.codingplan.model.select\" = @{ zh = \"选择模型 [1/2/3/4]\"; en = \"Select model [1/2/3/4]\" }\n \"llm.provider.selected_tokenplan\" = @{ zh = \" 提供商: 阿里云通义 Token 套餐(兼容模式)\"; en = \" Provider: Alibaba Cloud Token Plan (compatible mode)\" }\n \"llm.provider.selected_codingplan\" = @{ zh = \" 提供商: 阿里云通义 Token 套餐(alibaba-cloud)\"; en = \" Provider: Qwen Cloud (international) (alibaba-cloud)\" }\n \"llm.provider.selected_codingplan_legacy\" = @{ zh = \" 提供商: 阿里云 Coding 套餐(coding.dashscope)\"; en = \" Provider: Alibaba Cloud Coding Plan (coding.dashscope)\" }\n \"llm.provider.selected_qwen\" = @{ zh = \" 提供商: 阿里云百炼\"; en = \" Provider: Alibaba Cloud Bailian\" }\n \"llm.provider.selected_openai\" = @{ zh = \" 提供商: {0}(OpenAI 兼容)\"; en = \" Provider: {0} (OpenAI-compatible)\" }\n \"llm.provider.invalid\" = @{ zh = \"无效选择: {0}(请输入 1 或 2)\"; en = \"Invalid choice: {0} (please enter 1 or 2)\" }\n \"llm.qwen.model_prompt\" = @{ zh = \"默认模型 ID\"; en = \"Default Model ID\" }\n \"llm.openai.base_url_prompt\" = @{ zh = \"Base URL\"; en = \"Base URL\" }\n \"llm.openai.model_prompt\" = @{ zh = \"默认模型 ID\"; en = \"Default Model ID\" }\n \"llm.openai.base_url_label\" = @{ zh = \" Base URL: {0}\"; en = \" Base URL: {0}\" }\n\n # --- Custom model parameters ---\n \"llm.custom_model.detected\" = @{ zh = \" ⚠️ 模型 '{0}' 不在内置模型列表中,请配置模型参数:\"; en = \" ⚠️ Model '{0}' is not in the built-in model list. Please configure model parameters:\" }\n \"llm.custom_model.context_prompt\" = @{ zh = \"最大上下文长度(token 数)[150000]\"; en = \"Max context window (tokens) [150000]\" }\n \"llm.custom_model.max_tokens_prompt\" = @{ zh = \"最大输出长度(token 数)[128000]\"; en = \"Max output tokens [128000]\" }\n \"llm.custom_model.reasoning_prompt\" = @{ zh = \"是否支持推理/思考模式?[Y/n]\"; en = \"Does it support reasoning/thinking mode? [Y/n]\" }\n \"llm.custom_model.vision_prompt\" = @{ zh = \"是否支持图片输入?[y/N]\"; en = \"Does it support image input? [y/N]\" }\n \"llm.custom_model.summary\" = @{ zh = \" 自定义模型参数: 上下文={0}, 最大输出={1}, 推理={2}, 图片={3}\"; en = \" Custom model params: context={0}, maxTokens={1}, reasoning={2}, vision={3}\" }\n\n # --- Admin Credentials ---\n \"admin.title\" = @{ zh = \"--- 管理员凭据 ---\"; en = \"--- Admin Credentials ---\" }\n \"admin.username_prompt\" = @{ zh = \"管理员用户名\"; en = \"Admin Username\" }\n \"admin.password_prompt\" = @{ zh = \"管理员密码(留空自动生成,最少 8 位)\"; en = \"Admin Password (leave empty to auto-generate, min 8 chars)\" }\n \"admin.password_generated\" = @{ zh = \" 已自动生成管理员密码\"; en = \" Auto-generated admin password\" }\n \"admin.password_too_short\" = @{ zh = \"管理员密码至少需要 8 个字符(MinIO 要求)。当前长度: {0}\"; en = \"Admin password must be at least 8 characters (MinIO requirement). Current length: {0}\" }\n\n # --- Port Configuration ---\n \"port.title\" = @{ zh = \"--- 端口配置(按回车使用默认值)---\"; en = \"--- Port Configuration (press Enter for defaults) ---\" }\n \"port.gateway_prompt\" = @{ zh = \"网关主机端口(容器内 8080)\"; en = \"Host port for gateway (8080 inside container)\" }\n \"port.console_prompt\" = @{ zh = \"Higress 控制台主机端口(容器内 8001)\"; en = \"Host port for Higress console (8001 inside container)\" }\n \"port.element_prompt\" = @{ zh = \"Element Web 直接访问主机端口(容器内 8088)\"; en = \"Host port for Element Web direct access (8088 inside container)\" }\n \"port.manager_console_prompt\" = @{ zh = \"Manager 控制台主机端口(容器内 18888)\"; en = \"Host port for Manager console (18888 inside container)\" }\n \"port.local_only.title\" = @{ zh = \"--- 网络访问模式 ---\"; en = \"--- Network Access Mode ---\" }\n \"port.local_only.hint_yes\" = @{ zh = \" 仅本机使用,无需开放外部端口(推荐)\"; en = \" Local use only, no external port exposure (recommended)\" }\n \"port.local_only.hint_no\" = @{ zh = \" 允许外部访问(局域网 / 公网)\"; en = \" Allow external access (LAN / public network)\" }\n \"port.local_only.choice\" = @{ zh = \"请选择 [1/2]\"; en = \"Enter choice [1/2]\" }\n \"port.local_only.selected_local\" = @{ zh = \"端口已绑定到 127.0.0.1(仅本机访问)\"; en = \"Ports bound to 127.0.0.1 (localhost only)\" }\n \"port.local_only.selected_external\" = @{ zh = \"端口已绑定到所有网络接口(0.0.0.0)\"; en = \"Ports bound to all interfaces (0.0.0.0)\" }\n \"port.local_only.https_hint\" = @{ zh = \"警告: 建议在 Higress 控制台配置 TLS 证书并启用 HTTPS,避免明文传输。\"; en = \"WARNING: It is recommended to configure TLS certificates and enable HTTPS in the Higress Console to avoid plaintext transmission.\" }\n \"port.local_only.https_docs\" = @{ zh = \"\"; en = \"\" }\n\n # --- Domain Configuration ---\n \"domain.title\" = @{ zh = \"--- 域名配置(按回车使用默认值)---\"; en = \"--- Domain Configuration (press Enter for defaults) ---\" }\n \"domain.hint\" = @{ zh = \"提示: 自定义域名前必须事先做好 DNS 解析。单机 ECS 部署时无需修改 aigw、fs 等域名;Element Web 和 Matrix Server 也可通过 IP 直接访问。\"; en = \"Hint: Configure DNS resolution before customizing domains. For single ECS deployment, no need to change aigw, fs, etc.; Element Web and Matrix Server can also be accessed directly via IP.\" }\n \"domain.matrix_prompt\" = @{ zh = \"Matrix 域名\"; en = \"Matrix Domain\" }\n \"domain.element_prompt\" = @{ zh = \"Element Web 域名\"; en = \"Element Web Domain\" }\n \"domain.gateway_prompt\" = @{ zh = \"AI 网关域名\"; en = \"AI Gateway Domain\" }\n \"domain.fs_prompt\" = @{ zh = \"文件系统域名\"; en = \"File System Domain\" }\n \"domain.console_prompt\" = @{ zh = \"Manager 控制台域名\"; en = \"Manager Console Domain\" }\n\n # --- GitHub Integration ---\n \"github.title\" = @{ zh = \"--- GitHub 集成(可选,按回车跳过)---\"; en = \"--- GitHub Integration (optional, press Enter to skip) ---\" }\n \"github.token_prompt\" = @{ zh = \"GitHub 个人访问令牌(可选)\"; en = \"GitHub Personal Access Token (optional)\" }\n\n # --- Skills Registry ---\n \"skills.title\" = @{ zh = \"--- Skills 注册中心(可选,按回车使用默认 nacos://market.agentteams.io:80/public)---\"; en = \"--- Skills Registry (optional, press Enter for default nacos://market.agentteams.io:80/public) ---\" }\n \"skills.url_prompt\" = @{ zh = \"Skills 注册中心 URL(留空使用默认 nacos://market.agentteams.io:80/public)\"; en = \"Skills Registry URL (leave empty for default nacos://market.agentteams.io:80/public)\" }\n\n # --- Data Persistence ---\n \"data.title\" = @{ zh = \"--- 数据持久化 ---\"; en = \"--- Data Persistence ---\" }\n \"data.volume_prompt\" = @{ zh = \"Docker 卷名称 [agentteams-data]\"; en = \"Docker volume name for persistent data [agentteams-data]\" }\n \"data.volume_using\" = @{ zh = \" 使用 Docker 卷: {0}\"; en = \" Using Docker volume: {0}\" }\n\n # --- Manager Workspace ---\n \"workspace.title\" = @{ zh = \"--- Manager 工作空间 ---\"; en = \"--- Manager Workspace ---\" }\n \"workspace.dir_prompt\" = @{ zh = \"Manager 工作空间目录 [{0}]\"; en = \"Manager workspace directory [{0}]\" }\n \"workspace.dir_label\" = @{ zh = \" Manager 工作空间: {0}\"; en = \" Manager workspace: {0}\" }\n\n # --- Host directory sharing ---\n \"host_share.prompt\" = @{ zh = \"与 Agent 共享的主机目录(默认: {0})\"; en = \"Host directory to share with agents (default: {0})\" }\n \"host_share.sharing\" = @{ zh = \"共享主机目录: {0} -> 容器内 /host-share\"; en = \"Sharing host directory: {0} -> /host-share in container\" }\n \"host_share.not_exist\" = @{ zh = \"警告: 主机目录 {0} 不存在,跳过验证继续使用\"; en = \"WARNING: Host directory {0} does not exist, using without validation\" }\n\n # --- Default worker runtime ---\n \"worker_runtime.title\" = @{ zh = \"--- 默认 Worker 运行时 ---\"; en = \"--- Default Worker Runtime ---\" }\n \"worker_runtime.openclaw\" = @{ zh = \"OpenClaw\"; en = \"OpenClaw\" }\n \"worker_runtime.copaw\" = @{ zh = \"QwenPaw\"; en = \"QwenPaw\" }\n \"worker_runtime.hermes\" = @{ zh = \"Hermes\"; en = \"Hermes\" }\n \"worker_runtime.choice\" = @{ zh = \"请选择 [1/2/3]\"; en = \"Enter choice [1/2/3]\" }\n \"worker_runtime.selected\" = @{ zh = \"默认 Worker 运行时: {0}\"; en = \"Default Worker runtime: {0}\" }\n \"worker_runtime.title_short\" = @{ zh = \"默认 Worker 运行时\"; en = \"Default Worker Runtime\" }\n\n # --- Manager runtime ---\n \"manager_runtime.title\" = @{ zh = \"--- Manager 运行时 ---\"; en = \"--- Manager Runtime ---\" }\n \"manager_runtime.openclaw\" = @{ zh = \"OpenClaw\"; en = \"OpenClaw\" }\n \"manager_runtime.copaw\" = @{ zh = \"QwenPaw\"; en = \"QwenPaw\" }\n \"manager_runtime.choice\" = @{ zh = \"请选择 [1/2]\"; en = \"Enter choice [1/2]\" }\n \"manager_runtime.selected\" = @{ zh = \"Manager 运行时: {0}\"; en = \"Manager runtime: {0}\" }\n \"manager_runtime.title_short\" = @{ zh = \"Manager 运行时\"; en = \"Manager Runtime\" }\n\n # --- Matrix E2EE ---\n \"matrix_e2ee.title\" = @{ zh = \"--- Matrix 端到端加密(E2EE)---\"; en = \"--- Matrix End-to-End Encryption (E2EE) ---\" }\n \"matrix_e2ee.desc\" = @{\n zh = \"E2EE 会对 Manager 与 Worker 之间的 Matrix 消息进行端到端加密。`n 启用后,即使 Matrix 服务器被入侵,消息内容也无法被窃取。`n 但 E2EE 会增加首次握手耗时,且要求所有 Agent 都支持 matrix-sdk-crypto。`n 如果不确定,建议保持禁用。`n ⚠ 注意:禁用 E2EE 后,请勿在 Element 上创建默认启用加密的 Private 房间,`n 否则 Agent 将无法读取该房间中的加密消息。请改用 Public 房间或关闭房间加密。\"\n en = \"E2EE encrypts Matrix messages between Manager and Workers end-to-end.`n When enabled, message content stays private even if the Matrix server is compromised.`n However, E2EE adds overhead to the initial handshake and requires all Agents`n to support matrix-sdk-crypto. If unsure, keep it disabled.`n ⚠ Note: When E2EE is disabled, do NOT create Private rooms in Element (which`n enable encryption by default) — Agents cannot read encrypted messages without`n E2EE support. Use Public rooms or turn off room encryption instead.\"\n }\n \"matrix_e2ee.enable\" = @{ zh = \"启用 E2EE\"; en = \"Enable E2EE\" }\n \"matrix_e2ee.disable\" = @{ zh = \"禁用 E2EE(推荐)\"; en = \"Disable E2EE (recommended)\" }\n \"matrix_e2ee.choice\" = @{ zh = \"请选择 [1/2]\"; en = \"Enter choice [1/2]\" }\n \"matrix_e2ee.selected_enabled\" = @{ zh = \"Matrix E2EE: 已启用\"; en = \"Matrix E2EE: enabled\" }\n \"matrix_e2ee.selected_disabled\" = @{ zh = \"Matrix E2EE: 已禁用(默认)\"; en = \"Matrix E2EE: disabled (default)\" }\n \"matrix_e2ee.title_short\" = @{ zh = \"Matrix E2EE\"; en = \"Matrix E2EE\" }\n \"matrix_e2ee.val_enabled\" = @{ zh = \"已启用\"; en = \"enabled\" }\n \"matrix_e2ee.val_disabled\" = @{ zh = \"已禁用\"; en = \"disabled\" }\n\n # --- Docker API proxy ---\n \"docker_proxy.title\" = @{ zh = \"--- Docker API 安全代理 ---\"; en = \"--- Docker API Security Proxy ---\" }\n \"docker_proxy.desc\" = @{ zh = \"Docker API 代理可防止 AI Agent 通过 Docker API 越狱访问宿主机。`n 启用后,Manager 不再直接持有 Docker socket,所有容器操作经过安全校验。\"; en = \"Docker API proxy prevents AI Agents from escaping via Docker API to access the host.`n When enabled, Manager no longer has direct Docker socket access; all container operations go through security validation.\" }\n \"docker_proxy.enable\" = @{ zh = \"启用(推荐)\"; en = \"Enable (recommended)\" }\n \"docker_proxy.disable\" = @{ zh = \"禁用(直接挂载 Docker socket)\"; en = \"Disable (mount Docker socket directly)\" }\n \"docker_proxy.choice\" = @{ zh = \"请选择 [1/2]\"; en = \"Enter choice [1/2]\" }\n \"docker_proxy.selected_enabled\" = @{ zh = \"Docker API 代理: 已启用\"; en = \"Docker API proxy: enabled\" }\n \"docker_proxy.selected_disabled\" = @{ zh = \"Docker API 代理: 已禁用\"; en = \"Docker API proxy: disabled\" }\n \"docker_proxy.title_short\" = @{ zh = \"Docker API 代理\"; en = \"Docker API Proxy\" }\n \"docker_proxy.val_enabled\" = @{ zh = \"已启用\"; en = \"enabled\" }\n \"docker_proxy.val_disabled\" = @{ zh = \"已禁用\"; en = \"disabled\" }\n \"docker_proxy.registries_desc\" = @{ zh = \"默认放行的镜像来源:本地镜像、localhost、Higress 仓库(所有 region)。`n 如需放行其他镜像仓库,请输入逗号分隔的地址前缀。`n 示例: ghcr.io/myorg,registry.example.com/team\"; en = \"Default allowed image sources: local images, localhost, Higress registries (all regions).`n To allow additional image sources, enter comma-separated address prefixes.`n Example: ghcr.io/myorg,registry.example.com/team\" }\n \"docker_proxy.registries_prompt\" = @{ zh = \"额外放行的镜像来源(按回车跳过)\"; en = \"Additional allowed image sources (press Enter to skip)\" }\n \"docker_proxy.registries_label\" = @{ zh = \"额外放行的镜像来源\"; en = \"Additional allowed image sources\" }\n\n # --- Worker idle timeout ---\n \"idle_timeout.prompt\" = @{ zh = \"Worker 空闲自动停止超时(分钟)[720]\"; en = \"Worker idle auto-stop timeout in minutes [720]\" }\n \"idle_timeout.selected\" = @{ zh = \"Worker 空闲超时: {0} 分钟\"; en = \"Worker idle timeout: {0} minutes\" }\n \"idle_timeout.label\" = @{ zh = \"Worker 空闲超时(分钟)\"; en = \"Worker idle timeout (min)\" }\n\n # --- Secrets and config ---\n \"install.generating_secrets\" = @{ zh = \"正在生成密钥...\"; en = \"Generating secrets...\" }\n \"install.config_saved\" = @{ zh = \"配置已保存到 {0}\"; en = \"Configuration saved to {0}\" }\n\n # --- Container runtime socket ---\n \"install.socket_detected\" = @{ zh = \"容器运行时 socket: {0}(已启用直接创建 Worker)\"; en = \"Container runtime socket: {0} (direct Worker creation enabled)\" }\n \"install.socket_not_found\" = @{ zh = \"未找到容器运行时 socket(Manager 无法直接创建 Worker 容器,需要你手动执行 docker 命令创建)\"; en = \"No container runtime socket found (Manager cannot create Worker containers directly, you will need to create them manually using docker commands)\" }\n \"install.socket_confirm.title\" = @{ zh = \"警告: 未检测到容器运行时 Socket\"; en = \"WARNING: Container Runtime Socket Not Detected\" }\n \"install.socket_confirm.message\" = @{ zh = \"未找到 Docker/Podman socket,Manager 将无法自动创建 Worker 容器。`n你需要手动执行 docker run 命令来创建 Worker。`n`n是否继续安装?\"; en = \"Docker/Podman socket not found. Manager will not be able to create Worker containers automatically.`nYou will need to manually run docker commands to create Workers.`n`nContinue installation?\" }\n \"install.socket_confirm.prompt\" = @{ zh = \"继续安装? [y/N]: \"; en = \"Continue? [y/N]: \" }\n \"install.socket_confirm.cancelled\" = @{ zh = \"安装已取消。如需启用 Worker 自动创建,请确保 Docker/Podman 正在运行,然后重新运行安装脚本。\"; en = \"Installation cancelled. To enable automatic Worker creation, ensure Docker/Podman is running and re-run the installer.\" }\n\n # --- Container management ---\n \"install.removing_existing\" = @{ zh = \"正在移除现有 agentteams-manager 容器...\"; en = \"Removing existing agentteams-manager container...\" }\n\n # --- YOLO mode ---\n \"install.yolo\" = @{ zh = \"YOLO 模式已启用(自主决策,无交互提示)\"; en = \"YOLO mode enabled (autonomous decisions, no interactive prompts)\" }\n\n # --- Image pulling ---\n \"install.image.exists\" = @{ zh = \"Manager 镜像已存在: {0}\"; en = \"Manager image already exists locally: {0}\" }\n \"install.image.pulling_manager\" = @{ zh = \"正在拉取 Manager 镜像: {0}\"; en = \"Pulling Manager image: {0}\" }\n \"install.image.worker_exists\" = @{ zh = \"Worker 镜像已存在: {0}\"; en = \"Worker image already exists locally: {0}\" }\n \"install.image.pulling_worker\" = @{ zh = \"正在拉取 Worker 镜像: {0}\"; en = \"Pulling Worker image: {0}\" }\n\n # --- Starting container ---\n \"install.starting_manager\" = @{ zh = \"正在启动 Manager 容器...\"; en = \"Starting Manager container...\" }\n\n # --- Wait for Manager ready ---\n \"install.wait_ready\" = @{ zh = \"等待 Manager Agent 就绪(超时: {0}s)...\"; en = \"Waiting for Manager agent to be ready (timeout: {0}s)...\" }\n \"install.wait_ready.ok\" = @{ zh = \"Manager Agent 已就绪!\"; en = \"Manager agent is ready!\" }\n \"install.wait_ready.waiting\" = @{ zh = \"等待中... ({0}s/{1}s)\"; en = \"Waiting... ({0}s/{1}s)\" }\n \"install.wait_ready.timeout\" = @{ zh = \"Manager Agent 在 {0}s 内未就绪。请检查: docker logs {1}\"; en = \"Manager agent did not become ready within {0}s. Check: docker logs {1}\" }\n\n # --- Wait for Matrix ready ---\n \"install.wait_matrix\" = @{ zh = \"等待 Matrix 服务就绪(超时: {0}s)...\"; en = \"Waiting for Matrix server to be ready (timeout: {0}s)...\" }\n \"install.wait_matrix.ok\" = @{ zh = \"Matrix 服务已就绪!\"; en = \"Matrix server is ready!\" }\n \"install.wait_matrix.waiting\" = @{ zh = \"等待 Matrix 中... ({0}s/{1}s)\"; en = \"Waiting for Matrix... ({0}s/{1}s)\" }\n \"install.wait_matrix.timeout\" = @{ zh = \"Matrix 服务在 {0}s 内未就绪。请检查: docker logs {1}\"; en = \"Matrix server did not become ready within {0}s. Check: docker logs {1}\" }\n\n # --- OpenAI-compatible connectivity test ---\n \"llm.openai.test.testing\" = @{ zh = \"正在测试 API 联通性...\"; en = \"Testing API connectivity...\" }\n \"llm.openai.test.ok\" = @{ zh = \"API 联通性测试通过\"; en = \"API connectivity test passed\" }\n \"llm.openai.test.fail\" = @{ zh = \"API 联通性测试失败(HTTP {0})。响应内容:`n{1}`n请根据以上错误信息联系您的模型服务商解决。\"; en = \"API connectivity test failed (HTTP {0}). Response body:`n{1}`nPlease contact your model provider to resolve the issue.\" }\n \"llm.openai.test.fail.tokenplan\" = @{ zh = \"提示: 请确认 API Key 有效且已开通通义 Token 套餐。文档: https://help.aliyun.com/zh/model-studio/token-plan-quickstart\"; en = \"Hint: Verify your Token Plan API key and compatible-mode access. Docs: https://help.aliyun.com/zh/model-studio/token-plan-quickstart\" }\n \"llm.openai.test.fail.codingplan\" = @{ zh = \"提示: 请确认 API Key 有效且已开通通义 Token 套餐。文档: https://help.aliyun.com/zh/model-studio/token-plan-quickstart\"; en = \"Hint: Verify your DASHSCOPE_API_KEY for Qwen Cloud. API keys: https://home.qwencloud.com/api-keys Docs: https://docs.qwencloud.com/\" }\n \"llm.openai.test.fail.codingplan_legacy\" = @{ zh = \"提示: 请确认 API Key 有效且 Coding 套餐接口可用。文档: https://help.aliyun.com/zh/model-studio/get-api-key\"; en = \"Hint: Verify your DashScope API key and Coding Plan access. Docs: https://help.aliyun.com/zh/model-studio/get-api-key\" }\n \"llm.openai.test.confirm\" = @{ zh = \"是否仍要继续安装?[y/N/b]\"; en = \"Continue with installation anyway? [y/N/b]\" }\n \"llm.openai.test.aborted\" = @{ zh = \"安装已中止。\"; en = \"Installation aborted.\" }\n \"llm.embedding.title\" = @{ zh = \"📦 记忆搜索配置\"; en = \"📦 Memory Search Configuration\" }\n \"llm.embedding.hint\" = @{ zh = \" Embedding 模型可提升记忆搜索质量(语义匹配)。不启用也可正常使用记忆功能(关键词匹配)。\"; en = \" Embedding model improves memory search quality (semantic matching). Memory still works without it (keyword matching).\" }\n \"llm.embedding.option.default\" = @{ zh = \" 1) text-embedding-v4(推荐)\"; en = \" 1) text-embedding-v4 (Recommended)\" }\n \"llm.embedding.option.custom\" = @{ zh = \" 2) 自定义 Embedding 模型\"; en = \" 2) Custom embedding model\" }\n \"llm.embedding.option.disable\" = @{ zh = \" 3) 不启用\"; en = \" 3) Do not enable\" }\n \"llm.embedding.select\" = @{ zh = \"选择\"; en = \"Select\" }\n \"llm.embedding.custom_prompt\" = @{ zh = \" Embedding 模型名称\"; en = \" Embedding model name\" }\n \"llm.embedding.test.testing\" = @{ zh = \"正在测试 Embedding API 联通性...\"; en = \"Testing Embedding API connectivity...\" }\n \"llm.embedding.test.ok\" = @{ zh = \"✅ Embedding API 联通性测试通过\"; en = \"✅ Embedding API connectivity test passed\" }\n \"llm.embedding.test.fail\" = @{ zh = \"⚠️ Embedding API 测试失败(HTTP {0})。响应: {1}\"; en = \"⚠️ Embedding API test failed (HTTP {0}). Response: {1}\" }\n \"llm.embedding.auto_disabled\" = @{ zh = \"⚠️ Embedding 已自动禁用,记忆搜索将使用关键词匹配。您可以稍后在 agentteams-manager.env 中设置 AGENTTEAMS_EMBEDDING_MODEL 启用。\"; en = \"⚠️ Embedding auto-disabled. Memory search will use keyword matching. You can enable it later in agentteams-manager.env by setting AGENTTEAMS_EMBEDDING_MODEL.\" }\n \"llm.embedding.disabled\" = @{ zh = \"ℹ️ Embedding 已禁用,记忆搜索将使用关键词匹配。\"; en = \"ℹ️ Embedding disabled. Memory search will use keyword matching.\" }\n \"nav.back_hint\" = @{ zh = \"(输入 b 返回上一步)\"; en = \"(enter b to go back)\" }\n # --- OpenAI-compatible provider creation ---\n \"install.openai_compat.missing\" = @{ zh = \"警告: OpenAI Base URL 或 API Key 未设置,跳过提供商创建\"; en = \"WARNING: OpenAI Base URL or API Key not set, skipping provider creation\" }\n \"install.openai_compat.creating\" = @{ zh = \"正在创建 OpenAI 兼容提供商...\"; en = \"Creating OpenAI-compatible provider...\" }\n \"install.openai_compat.domain\" = @{ zh = \" 域名: {0}\"; en = \" Domain: {0}\" }\n \"install.openai_compat.port\" = @{ zh = \" 端口: {0}\"; en = \" Port: {0}\" }\n \"install.openai_compat.protocol\" = @{ zh = \" 协议: {0}\"; en = \" Protocol: {0}\" }\n \"install.openai_compat.service_fail\" = @{ zh = \"警告: 创建 DNS 服务源失败(可能已存在)\"; en = \"WARNING: Failed to create DNS service source (may already exist)\" }\n \"install.openai_compat.provider_fail\" = @{ zh = \"警告: 创建 AI 提供商失败(可能已存在)\"; en = \"WARNING: Failed to create AI provider (may already exist)\" }\n \"install.openai_compat.success\" = @{ zh = \"OpenAI 兼容提供商创建成功\"; en = \"OpenAI-compatible provider created successfully\" }\n\n # --- Welcome message ---\n \"install.welcome_msg.soul_configured\" = @{ zh = \"Soul 已配置(找到 soul-configured 标记),跳过 onboarding 消息\"; en = \"Soul already configured (soul-configured marker found), skipping onboarding message\" }\n \"install.welcome_msg.logging_in\" = @{ zh = \"正在以 {0} 身份登录以发送欢迎消息...\"; en = \"Logging in as {0} to send welcome message...\" }\n \"install.welcome_msg.login_failed\" = @{ zh = \"警告: 以 {0} 身份登录失败,跳过欢迎消息\"; en = \"WARNING: Failed to login as {0}, skipping welcome message\" }\n \"install.welcome_msg.finding_room\" = @{ zh = \"正在查找与 Manager 的 DM 房间...\"; en = \"Finding DM room with Manager...\" }\n \"install.welcome_msg.creating_room\" = @{ zh = \"正在创建与 Manager 的 DM 房间...\"; en = \"Creating DM room with Manager...\" }\n \"install.welcome_msg.no_room\" = @{ zh = \"警告: 无法找到或创建与 Manager 的 DM 房间\"; en = \"WARNING: Could not find or create DM room with Manager\" }\n \"install.welcome_msg.waiting_join\" = @{ zh = \"等待 Manager 加入房间...\"; en = \"Waiting for Manager to join the room...\" }\n \"install.welcome_msg.sending\" = @{ zh = \"正在向 Manager 发送欢迎消息...\"; en = \"Sending welcome message to Manager...\" }\n \"install.welcome_msg.send_failed\" = @{ zh = \"警告: 发送欢迎消息失败\"; en = \"WARNING: Failed to send welcome message\" }\n \"install.welcome_msg.sent\" = @{ zh = \"欢迎消息已发送给 Manager\"; en = \"Welcome message sent to Manager\" }\n \"install.welcome_msg.waiting\" = @{ zh = \"等待 Manager 发送欢迎消息(Higress 路由授权 + LLM 探活,约 45-90s)...\"; en = \"Waiting for Manager to send the welcome message (Higress route auth + LLM probe, ~45-90s)...\" }\n \"install.welcome_msg.confirmed\" = @{ zh = \"Manager 已确认发送欢迎消息(status.welcomeSent=true,用时 {0}s)\"; en = \"Manager confirmed welcome message sent (status.welcomeSent=true, {0}s elapsed)\" }\n \"install.welcome_msg.timeout\" = @{ zh = \"警告: 在 {0}s 内未观察到 Manager 发送欢迎消息(status.welcomeSent=true)。安装仍然成功,所有服务已就绪——可继续按下方提示登录 Element Web。\"; en = \"WARNING: Did not observe the Manager sending its welcome message (status.welcomeSent=true) within {0}s. Installation is still successful, all services are up — continue with the Element Web instructions below.\" }\n \"install.welcome_msg.timeout_hint\" = @{ zh = \"手动触发 onboarding: 登录 Element Web → 打开与 Manager 的 DM 房间 → 发送任意一句话(例如 `\"hi`\"),Manager 会接管对话并开始引导。\"; en = \"Manual onboarding: log in to Element Web -> open the DM with the Manager -> send any message (e.g. `\"hi`\") and the Manager will take over and start the guided setup.\" }\n \"install.welcome_msg.timeout_inspect\" = @{ zh = \"排查命令: docker exec agentteams-controller agt get managers default\"; en = \"Inspect status: docker exec agentteams-controller agt get managers default\" }\n \"install.welcome_msg.poll_unavailable\" = @{ zh = \"提示: agentteams-manager 内未找到 AgentTeams CLI,跳过 welcome 等待(旧镜像?)\"; en = \"Note: AgentTeams CLI not found inside agentteams-manager; skipping welcome wait (old image?)\" }\n\n # --- Final output panel ---\n \"success.title\" = @{ zh = \"=== AgentTeams Manager 已启动!===\"; en = \"=== AgentTeams Manager Started! ===\" }\n \"success.domains_configured\" = @{ zh = \"以下域名已配置解析到 127.0.0.1:\"; en = \"The following domains are configured to resolve to 127.0.0.1:\" }\n \"success.open_url\" = @{ zh = \" 在浏览器中打开以下 URL 开始使用: \"; en = \" Open the following URL in your browser to start: \" }\n \"success.login_with\" = @{ zh = \" 登录信息:\"; en = \" Login with:\" }\n \"success.username\" = @{ zh = \" 用户名: {0}\"; en = \" Username: {0}\" }\n \"success.password\" = @{ zh = \" 密码: {0}\"; en = \" Password: {0}\" }\n \"success.after_login\" = @{ zh = \" 登录后,开始与 Manager 聊天!\"; en = \" After login, start chatting with the Manager!\" }\n \"success.tell_it\" = @{ zh = \" 告诉它: `\"创建一个名为 alice 的前端开发 Worker`\"\"; en = \" Tell it: `\"Create a Worker named alice for frontend dev`\"\" }\n \"success.manager_auto\" = @{ zh = \" Manager 会自动处理一切。\"; en = \" The Manager will handle everything automatically.\" }\n \"success.mobile_title\" = @{ zh = \" 移动端访问(FluffyChat / Element Mobile):\"; en = \" Mobile access (FluffyChat / Element Mobile):\" }\n \"success.mobile_step1\" = @{ zh = \" 1. 在手机上下载 FluffyChat 或 Element\"; en = \" 1. Download FluffyChat or Element on your phone\" }\n \"success.mobile_step2\" = @{ zh = \" 2. 设置 homeserver 为: {0}\"; en = \" 2. Set homeserver to: {0}\" }\n \"success.mobile_step2_noip\" = @{ zh = \" 2. 设置 homeserver 为: http://<本机局域网IP>:{0}\"; en = \" 2. Set homeserver to: http://<this-machine-LAN-IP>:{0}\" }\n \"success.mobile_noip_hint\" = @{ zh = \" (无法自动检测局域网 IP — 请使用 ifconfig / ip addr 查看)\"; en = \" (Could not detect LAN IP automatically — check with: ifconfig / ip addr)\" }\n \"success.mobile_step3\" = @{ zh = \" 3. 登录信息:\"; en = \" 3. Login with:\" }\n \"success.mobile_username\" = @{ zh = \" 用户名: {0}\"; en = \" Username: {0}\" }\n \"success.mobile_password\" = @{ zh = \" 密码: {0}\"; en = \" Password: {0}\" }\n\n # --- Other consoles and tips ---\n \"success.other_consoles\" = @{ zh = \"--- 其他控制台 ---\"; en = \"--- Other Consoles ---\" }\n \"success.higress_console\" = @{ zh = \" Higress 控制台: http://localhost:{0}(用户名: {1} / 密码: {2})\"; en = \" Higress Console: http://localhost:{0} (Username: {1} / Password: {2})\" }\n \"success.manager_console\" = @{ zh = \" Manager 控制台(本地): http://localhost:{0}(无需登录)\"; en = \" Manager Console (local): http://localhost:{0} (no login required)\" }\n \"success.manager_console_gateway\" = @{ zh = \" Manager 控制台(网关): http://console-local.agentteams.io(用户名: {0} / 密码: {1})\"; en = \" Manager Console (gateway): http://console-local.agentteams.io (Username: {0} / Password: {1})\" }\n \"success.copaw_console\" = @{ zh = \" QwenPaw 控制台(本地): http://localhost:{0}(无需登录)\"; en = \" QwenPaw Console (local): http://localhost:{0} (no login required)\" }\n \"success.switch_llm.title\" = @{ zh = \"--- 切换 LLM 提供商 ---\"; en = \"--- Switch LLM Providers ---\" }\n \"success.switch_llm.hint\" = @{ zh = \" 您可以通过 Higress 控制台切换到其他 LLM 提供商(OpenAI、Anthropic 等)。\"; en = \" You can switch to other LLM providers (OpenAI, Anthropic, etc.) via Higress Console.\" }\n \"success.switch_llm.docs\" = @{ zh = \" 详细说明请参阅:\"; en = \" For detailed instructions, see:\" }\n \"success.switch_llm.url\" = @{ zh = \" https://higress.ai/en/docs/ai/scene-guide/multi-proxy#console-configuration\"; en = \" https://higress.ai/en/docs/ai/scene-guide/multi-proxy#console-configuration\" }\n \"success.tip\" = @{ zh = \"提示: 您也可以在聊天中让 Manager 为您配置 LLM 提供商。\"; en = \"Tip: You can also ask the Manager to configure LLM providers for you in the chat.\" }\n \"success.config_file\" = @{ zh = \"配置文件: {0}\"; en = \"Configuration file: {0}\" }\n \"success.data_volume\" = @{ zh = \"数据卷: {0}\"; en = \"Data volume: {0}\" }\n \"success.workspace\" = @{ zh = \"Manager 工作空间: {0}\"; en = \"Manager workspace: {0}\" }\n\n # --- Worker installation ---\n \"worker.resetting\" = @{ zh = \"正在重置 Worker: {0}...\"; en = \"Resetting Worker: {0}...\" }\n \"worker.exists\" = @{ zh = \"容器 '{0}' 已存在。使用 --reset 重新创建。\"; en = \"Container '{0}' already exists. Use --reset to recreate.\" }\n \"worker.starting\" = @{ zh = \"正在启动 Worker: {0}...\"; en = \"Starting Worker: {0}...\" }\n \"worker.skills_url\" = @{ zh = \" Skills API URL: {0}\"; en = \" Skills API URL: {0}\" }\n \"worker.started\" = @{ zh = \"=== Worker {0} 已启动!===\"; en = \"=== Worker {0} Started! ===\" }\n \"worker.container\" = @{ zh = \"容器: {0}\"; en = \"Container: {0}\" }\n \"worker.view_logs\" = @{ zh = \"查看日志: docker logs -f {0}\"; en = \"View logs: docker logs -f {0}\" }\n\n # --- Prompt function messages ---\n \"prompt.preset\" = @{ zh = \" {0} = (已通过环境变量预设)\"; en = \" {0} = (pre-set via env)\" }\n \"prompt.upgrade_keep\" = @{ zh = \" {0} = {1}(当前值,回车保留 / 输入新值覆盖)\"; en = \" {0} = {1} (current value, press Enter to keep / type new value to change)\" }\n \"prompt.upgrade_empty\" = @{ zh = \" {0} = (未设置,回车跳过 / 输入新值设置)\"; en = \" {0} = (not set, press Enter to skip / type new value to set)\" }\n \"prompt.default\" = @{ zh = \" {0} = {1}(默认)\"; en = \" {0} = {1} (default)\" }\n \"prompt.required\" = @{ zh = \"{0} 是必需的(在非交互模式下通过环境变量设置)\"; en = \"{0} is required (set via environment variable in non-interactive mode)\" }\n \"prompt.required_empty\" = @{ zh = \"{0} 是必需的\"; en = \"{0} is required\" }\n\n # --- Language switch prompt (bilingual by design) ---\n \"lang.detected.zh\" = @{ zh = \"检测到语言 / Detected language: 中文\"; en = \"检测到语言 / Detected language: 中文\" }\n \"lang.detected.en\" = @{ zh = \"检测到语言 / Detected language: English\"; en = \"检测到语言 / Detected language: English\" }\n \"lang.switch_title\" = @{ zh = \"切换语言 / Switch language:\"; en = \"切换语言 / Switch language:\" }\n \"lang.option_zh\" = @{ zh = \" 1) 中文\"; en = \" 1) 中文\" }\n \"lang.option_en\" = @{ zh = \" 2) English\"; en = \" 2) English\" }\n \"lang.prompt\" = @{ zh = \"请选择 / Enter choice\"; en = \"请选择 / Enter choice\" }\n\n # --- Error messages ---\n \"error.name_required\" = @{ zh = \"--name 是必需的\"; en = \"--name is required\" }\n \"error.fs_required\" = @{ zh = \"--fs 是必需的\"; en = \"--fs is required\" }\n \"error.fs_key_required\" = @{ zh = \"--fs-key 是必需的\"; en = \"--fs-key is required\" }\n \"error.fs_secret_required\" = @{ zh = \"--fs-secret 是必需的\"; en = \"--fs-secret is required\" }\n \"error.unknown_option\" = @{ zh = \"未知选项: {0}\"; en = \"Unknown option: {0}\" }\n \"error.docker_not_running\" = @{ zh = \"Docker 未运行。请先启动 Docker Desktop 或 Podman Desktop。\"; en = \"Docker is not running. Please start Docker Desktop or Podman Desktop first.\" }\n \"error.docker_not_found\" = @{ zh = \"未找到 docker 或 podman 命令。请先安装 Docker Desktop 或 Podman Desktop:`n Docker Desktop: https://www.docker.com/products/docker-desktop/`n Podman Desktop: https://podman-desktop.io/\"; en = \"docker or podman command not found. Please install Docker Desktop or Podman Desktop first:`n Docker Desktop: https://www.docker.com/products/docker-desktop/`n Podman Desktop: https://podman-desktop.io/\" }\n\n # --- Uninstall messages ---\n \"uninstall.title\" = @{ zh = \"正在卸载 AgentTeams...\"; en = \"Uninstalling AgentTeams...\" }\n \"uninstall.stopping_manager\" = @{ zh = \"正在停止并移除 agentteams-manager...\"; en = \"Stopping and removing agentteams-manager...\" }\n \"uninstall.stopping_workers\" = @{ zh = \"正在停止并移除 worker 容器...\"; en = \"Stopping and removing worker containers...\" }\n \"uninstall.removed\" = @{ zh = \" 已移除: {0}\"; en = \" Removed: {0}\" }\n \"uninstall.removing_volume\" = @{ zh = \"正在移除 Docker 卷: agentteams-data\"; en = \"Removing Docker volume: agentteams-data\" }\n \"uninstall.removing_env\" = @{ zh = \"正在移除 env 文件: {0}\"; en = \"Removing env file: {0}\" }\n \"uninstall.removing_proxy\" = @{ zh = \"正在停止并移除 Docker API 代理容器: hiclaw-docker-proxy\"; en = \"Stopping and removing Docker API proxy container: hiclaw-docker-proxy\" }\n \"uninstall.stopping_controller\" = @{ zh = \"正在停止并移除 agentteams-controller (内嵌 Tuwunel/MinIO/Higress)...\"; en = \"Stopping and removing agentteams-controller (embedded Tuwunel/MinIO/Higress)...\" }\n \"uninstall.removing_network\" = @{ zh = \"正在移除 Docker 网络: agentteams-net\"; en = \"Removing Docker network: agentteams-net\" }\n \"uninstall.removing_workspace\" = @{ zh = \"正在移除工作空间目录: {0}\"; en = \"Removing workspace directory: {0}\" }\n \"uninstall.removing_log\" = @{ zh = \"正在移除日志文件: {0}\"; en = \"Removing log file: {0}\" }\n \"uninstall.done\" = @{ zh = \"AgentTeams 已卸载。\"; en = \"AgentTeams has been uninstalled.\" }\n}\n\n# Get-Msg: look up message by key, with -f style argument substitution.\n# Falls back to English if the current language translation is missing.\nfunction Get-Msg {\n param(\n [Parameter(Mandatory)][string]$Key,\n [object[]]$f\n )\n $lang = $script:AGENTTEAMS_LANGUAGE\n if (-not $lang) { $lang = \"en\" }\n $entry = $script:Messages[$Key]\n if (-not $entry) { return $Key }\n $text = $entry[$lang]\n if (-not $text) { $text = $entry[\"en\"] }\n if (-not $text) { return $Key }\n if ($f) { return ($text -f $f) }\n return $text\n}\n\nfunction Get-LanIP {\n # Detect local LAN IP address on Windows\n try {\n # Get network adapters with IPv4 addresses, prefer connected/active interfaces\n $adapters = Get-NetIPAddress -AddressFamily IPv4 -ErrorAction SilentlyContinue |\n Where-Object {\n $_.IPAddress -notlike \"127.*\" -and\n $_.PrefixOrigin -ne \"WellKnown\" -and\n $_.InterfaceAlias -notlike \"*Loopback*\"\n } |\n Sort-Object {\n # PowerShell 5.1 compatible: use if-else instead of ternary operator\n if ($_.InterfaceAlias -like \"*Wi-Fi*\" -or $_.InterfaceAlias -like \"*Ethernet*\") { 0 } else { 1 }\n }\n\n if ($adapters) {\n return $adapters[0].IPAddress\n }\n\n # Fallback: use ipconfig\n $ipconfig = ipconfig 2>$null\n $ip = ($ipconfig | Select-String \"IPv4 Address.*?: (\\d+\\.\\d+\\.\\d+\\.\\d+)\" | Select-Object -First 1)\n if ($ip -match \"(\\d+\\.\\d+\\.\\d+\\.\\d+)\") {\n return $Matches[1]\n }\n }\n catch {\n # Ignore errors\n }\n\n return \"\"\n}\n\n# Known models list — used to detect custom models during install\n$script:KnownModels = @(\n \"gpt-5.4\", \"gpt-5.3-codex\", \"gpt-5-mini\", \"gpt-5-nano\",\n \"claude-opus-4-6\", \"claude-sonnet-4-6\", \"claude-haiku-4-5\",\n \"qwen3.6-plus\", \"qwen3.5-plus\", \"deepseek-chat\", \"deepseek-reasoner\",\n \"kimi-k2.5\", \"glm-5\", \"MiniMax-M2.7\", \"MiniMax-M2.7-highspeed\", \"MiniMax-M2.5\"\n)\n\nfunction Test-KnownModel {\n param([string]$Model)\n return $script:KnownModels -contains $Model\n}\n\nfunction Request-CustomModelParams {\n param([string]$Model)\n if (Test-KnownModel $Model) {\n $script:config.MODEL_CONTEXT_WINDOW = \"\"\n $script:config.MODEL_MAX_TOKENS = \"\"\n $script:config.MODEL_REASONING = \"\"\n $script:config.MODEL_VISION = \"\"\n return\n }\n Write-Host \"\"\n Write-Log (Get-Msg \"llm.custom_model.detected\" -f $Model)\n Write-Host \"\"\n $ctxInput = Read-Host \" $(Get-Msg 'llm.custom_model.context_prompt')\"\n if ($ctxInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $script:config.MODEL_CONTEXT_WINDOW = if ($ctxInput) { $ctxInput } else { \"150000\" }\n $maxInput = Read-Host \" $(Get-Msg 'llm.custom_model.max_tokens_prompt')\"\n if ($maxInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $script:config.MODEL_MAX_TOKENS = if ($maxInput) { $maxInput } else { \"128000\" }\n $reasoningInput = Read-Host \" $(Get-Msg 'llm.custom_model.reasoning_prompt')\"\n if ($reasoningInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $script:config.MODEL_REASONING = if ($reasoningInput -match \"^[nN]\") { \"false\" } else { \"true\" }\n $visionInput = Read-Host \" $(Get-Msg 'llm.custom_model.vision_prompt')\"\n if ($visionInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $script:config.MODEL_VISION = if ($visionInput -match \"^[yY]\") { \"true\" } else { \"false\" }\n Write-Log (Get-Msg \"llm.custom_model.summary\" -f $script:config.MODEL_CONTEXT_WINDOW, $script:config.MODEL_MAX_TOKENS, $script:config.MODEL_REASONING, $script:config.MODEL_VISION)\n}\n\nfunction New-RandomKey {\n # Generate 64 character hex string (32 bytes)\n $bytes = New-Object byte[] 32\n $rng = [System.Security.Cryptography.RandomNumberGenerator]::Create()\n $rng.GetBytes($bytes)\n return [BitConverter]::ToString($bytes).Replace(\"-\", \"\").ToLower()\n}\n\nfunction ConvertTo-DockerPath {\n param([string]$Path)\n\n # Convert Windows path to Docker mount format\n $fullPath = (Resolve-Path $Path -ErrorAction SilentlyContinue).Path\n if (-not $fullPath) {\n $fullPath = $Path\n }\n\n # Convert C:\\path to /c/path format for Docker\n if ($fullPath -match \"^([A-Za-z]):\") {\n $drive = $Matches[1].ToLower()\n $rest = $fullPath.Substring(2).Replace(\"\\\", \"/\")\n return \"/$drive$rest\"\n }\n return $fullPath.Replace(\"\\\", \"/\")\n}\n\n# Resolve the embedded controller image. Embedded mode is the only supported\n# architecture since PR #616 (manager image no longer bundles Higress/Tuwunel/MinIO).\n# Mirrors install/agentteams-install.sh::resolve_embedded_image — fail fast when the\n# embedded image is unavailable rather than silently falling back to the broken\n# legacy single-container path.\n# Sets $script:EMBEDDED_IMAGE and $script:AGENTTEAMS_USE_EMBEDDED.\nfunction Resolve-EmbeddedImage {\n $script:AGENTTEAMS_USE_EMBEDDED = \"1\"\n\n # Explicit override always wins (used by `make install-embedded` for local builds).\n if ($env:AGENTTEAMS_INSTALL_EMBEDDED_IMAGE) {\n $script:EMBEDDED_IMAGE = $env:AGENTTEAMS_INSTALL_EMBEDDED_IMAGE\n return\n }\n\n $versioned = \"$($script:AGENTTEAMS_REGISTRY)/agentteams/agentteams-embedded:$($script:AGENTTEAMS_VERSION)\"\n $latestTag = \"$($script:AGENTTEAMS_REGISTRY)/agentteams/agentteams-embedded:latest\"\n\n if ($script:AGENTTEAMS_VERSION -eq \"latest\") {\n $script:EMBEDDED_IMAGE = $latestTag\n return\n }\n\n docker pull $versioned *>$null\n if ($LASTEXITCODE -eq 0) {\n $script:EMBEDDED_IMAGE = $versioned\n return\n }\n docker pull $latestTag *>$null\n if ($LASTEXITCODE -eq 0) {\n Write-Log \"embedded $($script:AGENTTEAMS_VERSION) not found, using latest\"\n $script:EMBEDDED_IMAGE = $latestTag\n return\n }\n\n # Escape hatch for older versions (AGENTTEAMS_VERSION <= v1.0.9) whose manager image\n # still bundled the infrastructure — opt-in only, never silent.\n if ($env:AGENTTEAMS_FORCE_LEGACY -eq \"1\") {\n Write-Log \"WARNING: AGENTTEAMS_FORCE_LEGACY=1 - using legacy all-in-one manager architecture.\"\n Write-Log \"WARNING: This requires AGENTTEAMS_VERSION <= v1.0.9 (older bundled manager image).\"\n Write-Log \"WARNING: Newer slim manager images will hang on 'Waiting for Higress Gateway'.\"\n $script:AGENTTEAMS_USE_EMBEDDED = \"0\"\n return\n }\n\n Write-Host \"$($script:ESC)[31m[AgentTeams ERROR]$($script:ESC)[0m Embedded controller image is not available in the registry:\" -ForegroundColor Red\n Write-Host \" - tried: $versioned\" -ForegroundColor Red\n Write-Host \" - tried: $latestTag\" -ForegroundColor Red\n Write-Host \"\" -ForegroundColor Red\n Write-Host \"Embedded mode is the only supported architecture since PR #616.\" -ForegroundColor Red\n Write-Host \"How to resolve:\" -ForegroundColor Red\n Write-Host \" 1) Pin to a AGENTTEAMS_VERSION whose embedded image has been published, or\" -ForegroundColor Red\n Write-Host \" wait for the release pipeline to publish it.\" -ForegroundColor Red\n Write-Host \" 2) For a local build, run: make install-embedded\" -ForegroundColor Red\n Write-Host \" (builds and uses the local embedded image without touching the registry).\" -ForegroundColor Red\n Write-Host \" 3) Override with a custom image:\" -ForegroundColor Red\n Write-Host \" `$env:AGENTTEAMS_INSTALL_EMBEDDED_IMAGE=...; .\\agentteams-install.ps1\" -ForegroundColor Red\n Exit-Script 1\n}\n\nfunction Wait-ManagerReady {\n param(\n [string]$Container = \"agentteams-manager\",\n [int]$Timeout = $(if ($env:AGENTTEAMS_READY_TIMEOUT) { [int]$env:AGENTTEAMS_READY_TIMEOUT } else { 300 })\n )\n\n $elapsed = 0\n Write-Log (Get-Msg \"install.wait_ready\" -f $Timeout)\n\n $runtime = if ($script:config.MANAGER_RUNTIME) { $script:config.MANAGER_RUNTIME } else { \"copaw\" }\n\n while ($elapsed -lt $Timeout) {\n try {\n switch ($runtime) {\n \"copaw\" {\n $result = docker exec $Container curl -sf http://127.0.0.1:18799/api/agents 2>$null\n if ($result -match '\"agents\"') {\n Write-Log (Get-Msg \"install.wait_ready.ok\")\n return $true\n }\n }\n default {\n $result = docker exec $Container openclaw gateway health --json 2>$null\n if ($result -match '\"ok\"') {\n Write-Log (Get-Msg \"install.wait_ready.ok\")\n return $true\n }\n }\n }\n } catch {\n # Ignore errors during polling\n }\n\n Start-Sleep -Seconds 5\n $elapsed += 5\n Write-Host \"`r$($script:ESC)[36m[AgentTeams]$($script:ESC)[0m $(Get-Msg 'install.wait_ready.waiting' -f $elapsed, $Timeout)\" -NoNewline\n }\n\n Write-Host \"\"\n Write-Error (Get-Msg \"install.wait_ready.timeout\" -f $Timeout, $Container)\n}\n\nfunction Wait-MatrixReady {\n param(\n [string]$Container = \"agentteams-manager\",\n [int]$Timeout = $(if ($env:AGENTTEAMS_READY_TIMEOUT) { [int]$env:AGENTTEAMS_READY_TIMEOUT } else { 300 })\n )\n\n $elapsed = 0\n Write-Log (Get-Msg \"install.wait_matrix\" -f $Timeout)\n\n while ($elapsed -lt $Timeout) {\n try {\n $result = docker exec $Container curl -sf http://127.0.0.1:6167/_tuwunel/server_version 2>$null\n if ($result) {\n Write-Log (Get-Msg \"install.wait_matrix.ok\")\n return $true\n }\n } catch {\n # Ignore errors during polling\n }\n\n Start-Sleep -Seconds 5\n $elapsed += 5\n Write-Host \"`r$($script:ESC)[36m[AgentTeams]$($script:ESC)[0m $(Get-Msg 'install.wait_matrix.waiting' -f $elapsed, $Timeout)\" -NoNewline\n }\n\n Write-Host \"\"\n Write-Error (Get-Msg \"install.wait_matrix.timeout\" -f $Timeout, $Container)\n}\n\n# Read KEY=value from /data/agentteams-secrets.env on a Docker volume (manager container not required).\n# Requires $script:EMBEDDED_IMAGE (set by Resolve-EmbeddedImage before Install-Manager uses this).\nfunction Read-AgentTeamsSecretFromDataVolume {\n param(\n [string]$VolumeName,\n [string]$Key\n )\n if ([string]::IsNullOrEmpty($VolumeName) -or [string]::IsNullOrEmpty($Key) -or [string]::IsNullOrEmpty($script:EMBEDDED_IMAGE)) {\n return \"\"\n }\n $grepKey = [regex]::Escape($Key)\n $shCmd = \"grep \"\"^${grepKey}=\"\" /data/agentteams-secrets.env /data/hiclaw-secrets.env 2>/dev/null | cut -d= -f2- | head -1 | tr -d '\\r'\"\n $out = docker run --rm --entrypoint sh -v \"${VolumeName}:/data:ro\" $script:EMBEDDED_IMAGE -c $shCmd 2>$null\n if ($null -eq $out) { return \"\" }\n return ($out | Out-String).Trim()\n}\n\n# Read KEY=value from /data/worker-creds/<worker>.env on a Docker volume.\nfunction Read-AgentTeamsWorkerCredsFromVolume {\n param(\n [string]$VolumeName,\n [string]$WorkerName,\n [string]$Key\n )\n if ([string]::IsNullOrEmpty($VolumeName) -or [string]::IsNullOrEmpty($WorkerName) -or [string]::IsNullOrEmpty($Key) -or [string]::IsNullOrEmpty($script:EMBEDDED_IMAGE)) {\n return \"\"\n }\n $grepKey = [regex]::Escape($Key)\n $path = \"/data/worker-creds/${WorkerName}.env\"\n $shCmd = \"grep \"\"^${grepKey}=\"\" \"\"${path}\"\" 2>/dev/null | cut -d= -f2- | head -1 | tr -d '\\r'\"\n $out = docker run --rm --entrypoint sh -v \"${VolumeName}:/data:ro\" $script:EMBEDDED_IMAGE -c $shCmd 2>$null\n if ($null -eq $out) { return \"\" }\n return ($out | Out-String).Trim()\n}\n\n# Read admin_dm_room_id from host workspace state.json (fallback when Matrix API is unavailable).\nfunction Read-AgentTeamsAdminDmRoomFromWorkspace {\n param([string]$WorkspaceDir)\n if ([string]::IsNullOrEmpty($WorkspaceDir)) { return \"\" }\n $statePath = Join-Path $WorkspaceDir \"state.json\"\n if (-not (Test-Path $statePath)) { return \"\" }\n try {\n $j = Get-Content $statePath -Raw -ErrorAction Stop | ConvertFrom-Json\n $rid = $j.admin_dm_room_id\n if ($null -eq $rid) { return \"\" }\n $s = [string]$rid\n if ($s -eq \"\" -or $s -eq \"null\") { return \"\" }\n return $s.Trim()\n } catch {\n return \"\"\n }\n}\n\nfunction Get-AgentTeamsRandomHex {\n param([int]$ByteCount)\n $bytes = New-Object byte[] $ByteCount\n $rng = [System.Security.Cryptography.RandomNumberGenerator]::Create()\n $rng.GetBytes($bytes)\n return ([BitConverter]::ToString($bytes)).Replace(\"-\", \"\").ToLower()\n}\n\n# Resolve admin DM room with @manager (small room) via Matrix Client API inside agentteams-manager.\nfunction Get-AgentTeamsAdminDmRoomViaMatrix {\n param(\n [string]$AdminUser,\n [string]$AdminPassword\n )\n $running = docker ps --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-manager$\"\n if (-not $running) { return \"\" }\n if ([string]::IsNullOrEmpty($AdminPassword)) { return \"\" }\n\n $loginObj = @{\n type = \"m.login.password\"\n identifier = @{ type = \"m.id.user\"; user = $AdminUser }\n password = $AdminPassword\n }\n $loginJson = $loginObj | ConvertTo-Json -Compress -Depth 5\n try {\n $loginRaw = $loginJson | docker exec -i agentteams-manager sh -c \"curl -sf -X POST http://127.0.0.1:6167/_matrix/client/v3/login -H 'Content-Type: application/json' -d @-\" 2>$null\n if (-not $loginRaw) { return \"\" }\n $loginResp = $loginRaw | ConvertFrom-Json\n $token = [string]$loginResp.access_token\n if ([string]::IsNullOrEmpty($token)) { return \"\" }\n\n $roomsRaw = docker exec agentteams-manager curl -sf -X GET -H \"Authorization: Bearer $token\" `\n \"http://127.0.0.1:6167/_matrix/client/v3/joined_rooms\" 2>$null\n if (-not $roomsRaw) { return \"\" }\n $roomsObj = $roomsRaw | ConvertFrom-Json\n $roomList = @($roomsObj.joined_rooms)\n foreach ($roomId in $roomList) {\n if ([string]::IsNullOrEmpty($roomId)) { continue }\n $enc = $roomId.Replace(\"!\", \"%21\")\n $memRaw = docker exec agentteams-manager curl -sf -X GET -H \"Authorization: Bearer $token\" `\n \"http://127.0.0.1:6167/_matrix/client/v3/rooms/${enc}/members\" 2>$null\n if (-not $memRaw) { continue }\n $memObj = $memRaw | ConvertFrom-Json\n $chunk = @($memObj.chunk)\n $memberIds = @($chunk | ForEach-Object { $_.state_key })\n $match = $false\n foreach ($m in $memberIds) {\n $beforeColon = ($m -split \":\")[0]\n if ($m -like \"*manager*\" -and $beforeColon -notlike \"*admin*\") {\n $match = $true\n break\n }\n }\n if ($match -and $memberIds.Count -le 3) {\n return $roomId\n }\n }\n } catch {\n return \"\"\n }\n return \"\"\n}\n\nfunction New-EnvFile {\n param([hashtable]$Config, [string]$Path)\n\n $content = @\"\n# AgentTeams Manager Configuration\n# Generated by agentteams-install.ps1 on $(Get-Date -Format \"yyyy-MM-dd HH:mm:ss\")\n\n# Language\nAGENTTEAMS_LANGUAGE=$($Config.LANGUAGE)\n\n# LLM\nAGENTTEAMS_LLM_PROVIDER=$($Config.LLM_PROVIDER)\nAGENTTEAMS_DEFAULT_MODEL=$($Config.DEFAULT_MODEL)\nAGENTTEAMS_LLM_API_KEY=$($Config.LLM_API_KEY)\nAGENTTEAMS_OPENAI_BASE_URL=$($Config.OPENAI_BASE_URL)\nAGENTTEAMS_MODEL_CONTEXT_WINDOW=$($Config.MODEL_CONTEXT_WINDOW)\nAGENTTEAMS_MODEL_MAX_TOKENS=$($Config.MODEL_MAX_TOKENS)\nAGENTTEAMS_MODEL_REASONING=$($Config.MODEL_REASONING)\nAGENTTEAMS_MODEL_VISION=$($Config.MODEL_VISION)\n\n# Embedding model (empty = disabled, default: text-embedding-v4)\nAGENTTEAMS_EMBEDDING_MODEL=$($Config.EMBEDDING_MODEL)\n\n# Admin\nAGENTTEAMS_ADMIN_USER=$($Config.ADMIN_USER)\nAGENTTEAMS_ADMIN_PASSWORD=$($Config.ADMIN_PASSWORD)\n\n# Ports\nAGENTTEAMS_LOCAL_ONLY=$($Config.LOCAL_ONLY)\nAGENTTEAMS_PORT_GATEWAY=$($Config.PORT_GATEWAY)\nAGENTTEAMS_PORT_CONSOLE=$($Config.PORT_CONSOLE)\nAGENTTEAMS_PORT_ELEMENT_WEB=$($Config.PORT_ELEMENT_WEB)\nAGENTTEAMS_PORT_MANAGER_CONSOLE=$($Config.PORT_MANAGER_CONSOLE)\n\n# Matrix\nAGENTTEAMS_MATRIX_DOMAIN=$($Config.MATRIX_DOMAIN)\nAGENTTEAMS_MATRIX_CLIENT_DOMAIN=$($Config.MATRIX_CLIENT_DOMAIN)\n\n# Gateway\nAGENTTEAMS_AI_GATEWAY_DOMAIN=$($Config.AI_GATEWAY_DOMAIN)\nAGENTTEAMS_MANAGER_GATEWAY_KEY=$($Config.MANAGER_GATEWAY_KEY)\n\n# File System\nAGENTTEAMS_FS_DOMAIN=$($Config.FS_DOMAIN)\nAGENTTEAMS_CONSOLE_DOMAIN=$($Config.CONSOLE_DOMAIN)\nAGENTTEAMS_MINIO_USER=$($Config.MINIO_USER)\nAGENTTEAMS_MINIO_PASSWORD=$($Config.MINIO_PASSWORD)\n\n# Internal\nAGENTTEAMS_MANAGER_PASSWORD=$($Config.MANAGER_PASSWORD)\nAGENTTEAMS_REGISTRATION_TOKEN=$($Config.REGISTRATION_TOKEN)\n\n# GitHub (optional)\nAGENTTEAMS_GITHUB_TOKEN=$($Config.GITHUB_TOKEN)\n\n# Nacos package import defaults\nAGENTTEAMS_NACOS_REGISTRY_URI=$(if ($env:AGENTTEAMS_NACOS_REGISTRY_URI) { $env:AGENTTEAMS_NACOS_REGISTRY_URI } else { \"nacos://market.agentteams.io:80/public\" })\nAGENTTEAMS_NACOS_USERNAME=$($env:AGENTTEAMS_NACOS_USERNAME)\nAGENTTEAMS_NACOS_PASSWORD=$($env:AGENTTEAMS_NACOS_PASSWORD)\nAGENTTEAMS_NACOS_TOKEN=$($env:AGENTTEAMS_NACOS_TOKEN)\n\n# Skills Registry (optional, default: nacos://market.agentteams.io:80/public)\nAGENTTEAMS_SKILLS_API_URL=$(if ($Config.SKILLS_API_URL) { $Config.SKILLS_API_URL } else { \"nacos://market.agentteams.io:80/public\" })\n\n# OpenClaw CMS plugin (optional)\nAGENTTEAMS_CMS_TRACES_ENABLED=$(if ($env:AGENTTEAMS_CMS_TRACES_ENABLED) { $env:AGENTTEAMS_CMS_TRACES_ENABLED } else { \"false\" })\nAGENTTEAMS_CMS_ENDPOINT=$($env:AGENTTEAMS_CMS_ENDPOINT)\nAGENTTEAMS_CMS_LICENSE_KEY=$($env:AGENTTEAMS_CMS_LICENSE_KEY)\nAGENTTEAMS_CMS_PROJECT=$($env:AGENTTEAMS_CMS_PROJECT)\nAGENTTEAMS_CMS_WORKSPACE=$($env:AGENTTEAMS_CMS_WORKSPACE)\nAGENTTEAMS_CMS_SERVICE_NAME=$(if ($env:AGENTTEAMS_CMS_SERVICE_NAME) { $env:AGENTTEAMS_CMS_SERVICE_NAME } else { \"agentteams-manager\" })\nAGENTTEAMS_CMS_METRICS_ENABLED=$(if ($env:AGENTTEAMS_CMS_METRICS_ENABLED) { $env:AGENTTEAMS_CMS_METRICS_ENABLED } else { \"false\" })\n\n# Worker images (for direct container creation)\nAGENTTEAMS_WORKER_IMAGE=$($Config.WORKER_IMAGE)\nAGENTTEAMS_COPAW_WORKER_IMAGE=$($Config.COPAW_WORKER_IMAGE)\nAGENTTEAMS_HERMES_WORKER_IMAGE=$($Config.HERMES_WORKER_IMAGE)\n\n# Manager runtime (openclaw | copaw)\nAGENTTEAMS_MANAGER_RUNTIME=$($Config.MANAGER_RUNTIME)\n\n# Default Worker runtime (openclaw | copaw | hermes)\nAGENTTEAMS_DEFAULT_WORKER_RUNTIME=$($Config.DEFAULT_WORKER_RUNTIME)\n\n# Matrix E2EE (0=disabled, 1=enabled; default: 0)\nAGENTTEAMS_MATRIX_E2EE=$($Config.MATRIX_E2EE)\n\n# Docker API proxy (0=disabled, 1=enabled; default: 1)\nAGENTTEAMS_DOCKER_PROXY=$($Config.DOCKER_PROXY)\n\n# Docker API proxy: additional allowed image sources (comma-separated)\nAGENTTEAMS_PROXY_ALLOWED_REGISTRIES=$($Config.PROXY_ALLOWED_REGISTRIES)\n\n# Worker idle timeout in minutes (default: 720 = 12 hours)\nAGENTTEAMS_WORKER_IDLE_TIMEOUT=$($Config.WORKER_IDLE_TIMEOUT)\n\n# JVM Args for Higress Console\nJVM_ARGS=$($env:JVM_ARGS)\n\n# Higress WASM plugin image registry (auto-selected by timezone)\nHIGRESS_ADMIN_WASM_PLUGIN_IMAGE_REGISTRY=$($Config.REGISTRY)\n\n# Data persistence\nAGENTTEAMS_DATA_DIR=$($Config.DATA_DIR)\n# Manager workspace (skills, memory, state - host-editable)\nAGENTTEAMS_WORKSPACE_DIR=$($Config.WORKSPACE_DIR)\n# Host directory sharing\nAGENTTEAMS_HOST_SHARE_DIR=$($Config.HOST_SHARE_DIR)\n\"@\n\n Set-Content -Path $Path -Value $content -Encoding UTF8\n Write-Log (Get-Msg \"install.config_saved\" -f $Path)\n}\n\n# ============================================================\n# Prompt Functions\n# ============================================================\n\nfunction Read-Prompt {\n param(\n [string]$VarName,\n [string]$PromptText,\n [string]$Default = \"\",\n [switch]$Secret,\n [switch]$Optional\n )\n\n # Check if already set in environment\n $envValue = [Environment]::GetEnvironmentVariable($VarName)\n if ($envValue) {\n if ($script:AGENTTEAMS_UPGRADE -and -not $script:AGENTTEAMS_NON_INTERACTIVE) {\n # Show current value (masked for secrets) and let user change it\n $displayValue = $envValue\n if ($Secret) {\n if ($envValue.Length -le 8) {\n $displayValue = \"****\"\n } else {\n $displayValue = $envValue.Substring(0, 4) + \"****\" + $envValue.Substring($envValue.Length - 4)\n }\n }\n Write-Log (Get-Msg \"prompt.upgrade_keep\" -f $PromptText, $displayValue)\n $prompt = $PromptText\n if ($Secret) {\n $newValue = Read-Host -Prompt $prompt -AsSecureString\n $newValue = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto(\n [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($newValue)\n )\n } else {\n $newValue = Read-Host -Prompt $prompt\n if ($newValue -eq \"b\") { $script:StepResult = \"back\"; return $null }\n }\n if ($newValue) {\n return $newValue\n }\n return $envValue\n }\n Write-Log (Get-Msg \"prompt.preset\" -f $PromptText)\n return $envValue\n }\n # Upgrade mode: optional fields with empty value — let user set a new value\n elseif ($Optional -and $script:AGENTTEAMS_UPGRADE -and -not $script:AGENTTEAMS_NON_INTERACTIVE) {\n Write-Log (Get-Msg \"prompt.upgrade_empty\" -f $PromptText)\n $prompt = $PromptText\n if ($Secret) {\n $newValue = Read-Host -Prompt $prompt -AsSecureString\n $newValue = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto(\n [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($newValue)\n )\n } else {\n $newValue = Read-Host -Prompt $prompt\n if ($newValue -eq \"b\") { $script:StepResult = \"back\"; return $null }\n }\n if ($newValue) {\n return $newValue\n }\n return \"\"\n }\n\n # Non-interactive or quickstart mode\n if ($script:AGENTTEAMS_NON_INTERACTIVE -or $script:AGENTTEAMS_QUICKSTART) {\n if ($Default) {\n Write-Log (Get-Msg \"prompt.default\" -f $PromptText, $Default)\n return $Default\n }\n elseif ($Optional) {\n return \"\"\n }\n elseif ($script:AGENTTEAMS_NON_INTERACTIVE) {\n # Only hard-error in fully non-interactive mode, not quickstart\n Write-Error (Get-Msg \"prompt.required\" -f $PromptText)\n }\n # quickstart + no default + not optional: fall through to interactive prompt\n }\n\n # Interactive prompt\n $prompt = if ($Default) { \"$PromptText [$Default]\" } else { $PromptText }\n\n if ($Secret) {\n $value = Read-Host -Prompt $prompt -AsSecureString\n $value = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto(\n [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($value)\n )\n }\n else {\n $value = Read-Host -Prompt $prompt\n if ($value -eq \"b\") { $script:StepResult = \"back\"; return $null }\n }\n\n if (-not $value -and $Default) {\n $value = $Default\n }\n\n if (-not $value -and -not $Optional) {\n Write-Error (Get-Msg \"prompt.required_empty\" -f $PromptText)\n }\n\n return $value\n}\n\n# Load current parameter values from env file for upgrade mode display\nfunction Load-CurrentParamsFromEnv {\n $envFile = $script:AGENTTEAMS_ENV_FILE\n if (Test-Path $envFile) {\n $content = Get-Content $envFile\n $content | ForEach-Object {\n if ($_ -match \"^AGENTTEAMS_LLM_PROVIDER=(.*)$\") {\n $script:config.LLM_PROVIDER = $Matches[1].Trim()\n }\n if ($_ -match \"^AGENTTEAMS_OPENAI_BASE_URL=(.*)$\") {\n $script:config.OPENAI_BASE_URL = $Matches[1].Trim()\n }\n if ($_ -match \"^AGENTTEAMS_DEFAULT_MODEL=(.*)$\") {\n $script:config.DEFAULT_MODEL = $Matches[1].Trim()\n }\n if ($_ -match \"^AGENTTEAMS_EMBEDDING_MODEL=(.*)$\") {\n $script:config.EMBEDDING_MODEL = $Matches[1].Trim()\n }\n if ($_ -match \"^AGENTTEAMS_WORKSPACE_DIR=(.*)$\") {\n $script:config.WORKSPACE_DIR = $Matches[1].Trim()\n }\n if ($_ -match \"^AGENTTEAMS_HOST_SHARE_DIR=(.*)$\") {\n $script:config.HOST_SHARE_DIR = $Matches[1].Trim()\n }\n }\n }\n}\n\n# ============================================================\n# OpenAI-Compatible Provider\n# ============================================================\n\nfunction Test-LlmConnectivity {\n param(\n [string]$BaseUrl,\n [string]$ApiKey,\n [string]$Model,\n [string]$Hint = \"\"\n )\n Write-Log (Get-Msg \"llm.openai.test.testing\")\n $uri = ($BaseUrl.TrimEnd('/')) + \"/chat/completions\"\n $bodyHash = @{\n model = $Model\n messages = @(@{ role = \"user\"; content = \"hi\" })\n }\n $body = $bodyHash | ConvertTo-Json -Compress\n try {\n $response = Invoke-WebRequest -Uri $uri -Method POST `\n -Headers @{ \"Authorization\" = \"Bearer $ApiKey\"; \"Content-Type\" = \"application/json\"; \"User-Agent\" = \"AgentTeams/$($script:AGENTTEAMS_VERSION)\" } `\n -Body $body -TimeoutSec 30 -ErrorAction Stop -UseBasicParsing\n Write-Log (Get-Msg \"llm.openai.test.ok\")\n } catch {\n $statusCode = if ($_.Exception.Response) { [int]$_.Exception.Response.StatusCode } else { 0 }\n $responseBody = \"\"\n if ($_.Exception.Response) {\n try {\n $stream = $_.Exception.Response.GetResponseStream()\n $reader = New-Object System.IO.StreamReader($stream)\n $responseBody = $reader.ReadToEnd()\n } catch {}\n }\n Write-Host (Get-Msg \"llm.openai.test.fail\" -f $statusCode, $responseBody) -ForegroundColor Yellow\n if ($Hint) {\n Write-Host $Hint -ForegroundColor Yellow\n }\n if (-not $script:AGENTTEAMS_NON_INTERACTIVE) {\n $confirm = Read-Host (Get-Msg \"llm.openai.test.confirm\")\n if ($confirm -eq \"b\" -or $confirm -eq \"B\") {\n $script:StepResult = \"back\"\n return\n }\n if ($confirm -ne \"y\" -and $confirm -ne \"Y\") {\n Write-Log (Get-Msg \"llm.openai.test.aborted\")\n Exit-Script 1\n }\n }\n }\n}\n\nfunction Test-EmbeddingConnectivity {\n param(\n [string]$BaseUrl,\n [string]$ApiKey,\n [string]$Model\n )\n Write-Log (Get-Msg \"llm.embedding.test.testing\")\n $uri = ($BaseUrl.TrimEnd('/')) + \"/embeddings\"\n $bodyHash = @{\n model = $Model\n input = \"test\"\n }\n $body = $bodyHash | ConvertTo-Json -Compress\n try {\n $response = Invoke-WebRequest -Uri $uri -Method POST `\n -Headers @{ \"Authorization\" = \"Bearer $ApiKey\"; \"Content-Type\" = \"application/json\"; \"User-Agent\" = \"AgentTeams/$($script:AGENTTEAMS_VERSION)\" } `\n -Body $body -TimeoutSec 30 -ErrorAction Stop -UseBasicParsing\n Write-Log (Get-Msg \"llm.embedding.test.ok\")\n return $true\n } catch {\n $statusCode = if ($_.Exception.Response) { [int]$_.Exception.Response.StatusCode } else { 0 }\n $responseBody = \"\"\n if ($_.Exception.Response) {\n try {\n $stream = $_.Exception.Response.GetResponseStream()\n $reader = New-Object System.IO.StreamReader($stream)\n $responseBody = $reader.ReadToEnd()\n } catch {}\n }\n Write-Host (Get-Msg \"llm.embedding.test.fail\" -f $statusCode, $responseBody) -ForegroundColor Yellow\n return $false\n }\n}\n\nfunction New-OpenAICompatProvider {\n param(\n [string]$BaseUrl,\n [string]$ApiKey,\n [int]$ConsolePort = 18001\n )\n\n if (-not $BaseUrl -or -not $ApiKey) {\n Write-Log (Get-Msg \"install.openai_compat.missing\")\n return $false\n }\n\n $consoleUrl = \"http://localhost:$ConsolePort\"\n\n # Parse base URL\n $protocol = \"https\"\n $port = 443\n $urlWithoutProto = $BaseUrl -replace \"^https?://\", \"\"\n\n if ($BaseUrl -match \"^http://\") {\n $protocol = \"http\"\n $port = 80\n }\n\n $domain = $urlWithoutProto.Split(\"/\")[0]\n\n if ($domain -match \":(\\d+)$\") {\n $port = [int]$Matches[1]\n $domain = $domain -replace \":\\d+$\", \"\"\n }\n\n Write-Log (Get-Msg \"install.openai_compat.creating\")\n Write-Log (Get-Msg \"install.openai_compat.domain\" -f $domain)\n Write-Log (Get-Msg \"install.openai_compat.port\" -f $port)\n Write-Log (Get-Msg \"install.openai_compat.protocol\" -f $protocol)\n\n $serviceName = \"openai-compat\"\n\n # Create DNS service source\n $serviceBody = @{\n type = \"dns\"\n name = $serviceName\n port = $port.ToString()\n protocol = $protocol\n proxyName = \"\"\n domain = $domain\n } | ConvertTo-Json -Compress\n\n try {\n Invoke-RestMethod -Uri \"$consoleUrl/v1/service-sources\" -Method POST -ContentType \"application/json\" -Body $serviceBody -ErrorAction SilentlyContinue | Out-Null\n }\n catch {\n Write-Log (Get-Msg \"install.openai_compat.service_fail\")\n }\n\n Start-Sleep -Seconds 2\n\n # Create AI provider\n $providerBody = @{\n type = \"openai\"\n name = \"openai-compat\"\n tokens = @($ApiKey)\n version = 0\n protocol = \"openai/v1\"\n tokenFailoverConfig = @{ enabled = $false }\n rawConfigs = @{\n openaiCustomUrl = $BaseUrl\n openaiCustomServiceName = \"$serviceName.dns\"\n openaiCustomServicePort = $port\n }\n } | ConvertTo-Json -Compress -Depth 3\n\n try {\n Invoke-RestMethod -Uri \"$consoleUrl/v1/ai/providers\" -Method POST -ContentType \"application/json\" -Body $providerBody -ErrorAction SilentlyContinue | Out-Null\n Write-Log (Get-Msg \"install.openai_compat.success\")\n return $true\n }\n catch {\n Write-Log (Get-Msg \"install.openai_compat.provider_fail\")\n return $false\n }\n}\n\n# ============================================================\n# State Machine Helpers\n# ============================================================\n\nfunction Test-ShouldSkipStep {\n param([string]$StepFn)\n switch ($StepFn) {\n { $_ -in @(\"Step-Lang\", \"Step-Mode\") } {\n return $script:AGENTTEAMS_NON_INTERACTIVE\n }\n \"Step-Existing\" {\n return (-not (Test-Path $script:AGENTTEAMS_ENV_FILE))\n }\n # Keep-All upgrade mode: skip all config steps, values are already loaded\n { $_ -in @(\"Step-Llm\", \"Step-Admin\", \"Step-Network\", \"Step-Ports\", \"Step-Domains\",\n \"Step-Github\", \"Step-Skills\",\n \"Step-Runtime\", \"Step-ManagerRuntime\", \"Step-E2ee\", \"Step-DockerProxy\",\n \"Step-Idle\", \"Step-Hostshare\") } {\n if ($script:AGENTTEAMS_UPGRADE -and $env:AGENTTEAMS_UPGRADE_KEEP_ALL -eq \"1\") { return $true }\n return $false\n }\n { $_ -in @(\"Step-Volume\", \"Step-Workspace\") } {\n if ($script:AGENTTEAMS_UPGRADE -and $env:AGENTTEAMS_UPGRADE_KEEP_ALL -eq \"1\") { return $true }\n if ($script:AGENTTEAMS_NON_INTERACTIVE) { return $true }\n if ($script:AGENTTEAMS_QUICKSTART) { return $true }\n return $false\n }\n { $_ -in @(\"Step-E2ee\", \"Step-Idle\") } {\n if ($script:AGENTTEAMS_NON_INTERACTIVE) { return $true }\n if ($script:AGENTTEAMS_QUICKSTART -and -not $script:AGENTTEAMS_UPGRADE) { return $true }\n return $false\n }\n \"Step-DockerProxy\" {\n # Embedded controller IS the proxy/orchestrator — no separate proxy step needed.\n if ($script:AGENTTEAMS_USE_EMBEDDED -eq \"1\") { return $true }\n if ($script:AGENTTEAMS_NON_INTERACTIVE) { return $true }\n if ($script:AGENTTEAMS_QUICKSTART -and -not $script:AGENTTEAMS_UPGRADE) { return $true }\n return $false\n }\n \"Step-ManagerRuntime\" {\n if ($script:AGENTTEAMS_NON_INTERACTIVE) { return $true }\n return $false\n }\n \"Step-Hostshare\" {\n if ($script:AGENTTEAMS_NON_INTERACTIVE) { return $true }\n if ($script:AGENTTEAMS_QUICKSTART) { return $true }\n return $false\n }\n default { return $false }\n }\n}\n\nfunction Clear-StepVars {\n param([string]$StepFn)\n switch ($StepFn) {\n \"Step-Mode\" { $script:AGENTTEAMS_QUICKSTART = $false }\n \"Step-Existing\" {\n $script:AGENTTEAMS_UPGRADE = $false\n $script:UPGRADE_EXISTING_WORKERS = $null\n }\n \"Step-Llm\" {\n foreach ($k in @(\"AGENTTEAMS_LLM_PROVIDER\",\"AGENTTEAMS_DEFAULT_MODEL\",\"AGENTTEAMS_OPENAI_BASE_URL\",\n \"AGENTTEAMS_LLM_API_KEY\",\"AGENTTEAMS_MODEL_CONTEXT_WINDOW\",\"AGENTTEAMS_MODEL_MAX_TOKENS\",\n \"AGENTTEAMS_MODEL_REASONING\",\"AGENTTEAMS_MODEL_VISION\")) {\n [Environment]::SetEnvironmentVariable($k, $null, \"Process\")\n }\n }\n \"Step-Admin\" {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_ADMIN_USER\", $null, \"Process\")\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_ADMIN_PASSWORD\", $null, \"Process\")\n }\n \"Step-Network\" {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_LOCAL_ONLY\", $null, \"Process\")\n }\n \"Step-Ports\" {\n foreach ($k in @(\"AGENTTEAMS_PORT_GATEWAY\",\"AGENTTEAMS_PORT_CONSOLE\",\"AGENTTEAMS_PORT_ELEMENT_WEB\",\"AGENTTEAMS_PORT_MANAGER_CONSOLE\")) {\n [Environment]::SetEnvironmentVariable($k, $null, \"Process\")\n }\n }\n \"Step-Domains\" {\n foreach ($k in @(\"AGENTTEAMS_MATRIX_DOMAIN\",\"AGENTTEAMS_MATRIX_CLIENT_DOMAIN\",\"AGENTTEAMS_AI_GATEWAY_DOMAIN\",\"AGENTTEAMS_FS_DOMAIN\",\"AGENTTEAMS_CONSOLE_DOMAIN\")) {\n [Environment]::SetEnvironmentVariable($k, $null, \"Process\")\n }\n }\n \"Step-Github\" {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_GITHUB_TOKEN\", $null, \"Process\")\n }\n \"Step-Skills\" {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_SKILLS_API_URL\", $null, \"Process\")\n }\n \"Step-Volume\" {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_DATA_DIR\", $null, \"Process\")\n }\n \"Step-Workspace\" {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_WORKSPACE_DIR\", $null, \"Process\")\n }\n \"Step-Runtime\" {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_DEFAULT_WORKER_RUNTIME\", $null, \"Process\")\n }\n \"Step-ManagerRuntime\" {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_MANAGER_RUNTIME\", $null, \"Process\")\n }\n \"Step-E2ee\" {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_MATRIX_E2EE\", $null, \"Process\")\n }\n \"Step-DockerProxy\" {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_DOCKER_PROXY\", $null, \"Process\")\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_PROXY_ALLOWED_REGISTRIES\", $null, \"Process\")\n }\n \"Step-Idle\" {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_WORKER_IDLE_TIMEOUT\", $null, \"Process\")\n }\n \"Step-Hostshare\" {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_HOST_SHARE_DIR\", $null, \"Process\")\n }\n }\n}\n\n# ============================================================\n# Individual step functions\n# ============================================================\n\nfunction Step-Lang {\n $langDefaultChoice = if ($script:AGENTTEAMS_LANGUAGE -eq \"zh\") { \"1\" } else { \"2\" }\n $langDetectedKey = \"lang.detected.$($script:AGENTTEAMS_LANGUAGE)\"\n Write-Log (Get-Msg $langDetectedKey)\n Write-Log (Get-Msg \"lang.switch_title\")\n Write-Host (Get-Msg \"lang.option_zh\")\n Write-Host (Get-Msg \"lang.option_en\")\n Write-Host \"\"\n $langChoice = Read-Host \"$(Get-Msg 'lang.prompt') [$langDefaultChoice]\"\n if (-not $langChoice) { $langChoice = $langDefaultChoice }\n if ($langChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n switch ($langChoice) {\n \"1\" { $script:AGENTTEAMS_LANGUAGE = \"zh\" }\n \"2\" { $script:AGENTTEAMS_LANGUAGE = \"en\" }\n }\n $env:AGENTTEAMS_LANGUAGE = $script:AGENTTEAMS_LANGUAGE\n Write-Log \"\"\n}\n\nfunction Step-Mode {\n Write-Log (Get-Msg \"install.mode.title\")\n Write-Host \"\"\n Write-Host (Get-Msg \"install.mode.choose\")\n Write-Host (Get-Msg \"install.mode.quickstart\")\n Write-Host (Get-Msg \"install.mode.manual\")\n Write-Host \"\"\n $choice = Read-Host (Get-Msg \"install.mode.prompt\")\n $choice = if ($choice) { $choice } else { \"1\" }\n if ($choice -eq \"b\") { $script:StepResult = \"back\"; return }\n switch -Regex ($choice) {\n \"^(1|quick|quickstart)$\" {\n Write-Log (Get-Msg \"install.mode.quickstart_selected\")\n $script:AGENTTEAMS_QUICKSTART = $true\n }\n \"^(2|manual)$\" {\n Write-Log (Get-Msg \"install.mode.manual_selected\")\n $script:AGENTTEAMS_QUICKSTART = $false\n }\n default {\n Write-Log (Get-Msg \"install.mode.invalid\")\n $script:AGENTTEAMS_QUICKSTART = $true\n }\n }\n Write-Log \"\"\n}\n\nfunction Step-Existing {\n # This step is skipped when env file doesn't exist\n Write-Log (Get-Msg \"install.existing.detected\" -f $script:AGENTTEAMS_ENV_FILE)\n\n $runningManager = docker ps --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-manager$\"\n $runningWorkers = docker ps --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-worker-\"\n $existingWorkers = docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-worker-\"\n\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n Write-Log (Get-Msg \"install.existing.upgrade_noninteractive\")\n $upgradeChoice = \"1\"\n } else {\n Write-Host \"\"\n Write-Host (Get-Msg \"install.existing.choose\")\n Write-Host (Get-Msg \"install.existing.upgrade\")\n Write-Host (Get-Msg \"install.existing.reinstall\")\n Write-Host (Get-Msg \"install.existing.cancel\")\n Write-Host \"\"\n $upgradeChoice = Read-Host (Get-Msg \"install.existing.prompt\")\n $upgradeChoice = if ($upgradeChoice) { $upgradeChoice } else { \"1\" }\n if ($upgradeChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n }\n\n switch -Regex ($upgradeChoice) {\n \"^(1|upgrade)$\" {\n $script:AGENTTEAMS_UPGRADE = $true\n Write-Log (Get-Msg \"install.existing.upgrading\")\n\n # Show upgrade mode sub-menu (unless already set via env var)\n if ($env:AGENTTEAMS_UPGRADE_KEEP_ALL -ne \"1\") {\n Write-Host \"\"\n Write-Host (Get-Msg \"upgrade.mode.prompt\")\n Write-Host (Get-Msg \"upgrade.mode.keep_all\")\n Write-Host (Get-Msg \"upgrade.mode.confirm_each\")\n Write-Host (Get-Msg \"upgrade.mode.back\")\n Write-Host \"\"\n $upgradeModeChoice = Read-Host (Get-Msg \"install.existing.prompt\")\n $upgradeModeChoice = if ($upgradeModeChoice) { $upgradeModeChoice } else { \"1\" }\n switch -Regex ($upgradeModeChoice) {\n \"^(1|keep)$\" {\n $env:AGENTTEAMS_UPGRADE_KEEP_ALL = \"1\"\n }\n \"^(2|confirm)$\" {\n $env:AGENTTEAMS_UPGRADE_KEEP_ALL = \"0\"\n }\n \"^(3|b)$\" {\n $script:StepResult = \"back\"\n return\n }\n default {\n $env:AGENTTEAMS_UPGRADE_KEEP_ALL = \"0\"\n }\n }\n }\n # Load current parameters for both Keep-All and confirm-each modes\n Load-CurrentParamsFromEnv\n\n if ($runningManager -or $runningWorkers) {\n Write-Host \"\"\n Write-Host \"$($script:ESC)[33m$(Get-Msg 'install.existing.warn_manager_stop')$($script:ESC)[0m\"\n if ($existingWorkers) {\n Write-Host \"$($script:ESC)[33m$(Get-Msg 'install.existing.warn_worker_recreate')$($script:ESC)[0m\"\n }\n if (-not $script:AGENTTEAMS_NON_INTERACTIVE) {\n $confirm = Read-Host (Get-Msg \"install.existing.continue_prompt\")\n if ($confirm -eq \"b\" -or $confirm -eq \"B\") { $script:StepResult = \"back\"; return }\n if ($confirm -ne \"y\" -and $confirm -ne \"Y\") {\n Write-Log (Get-Msg \"install.existing.cancelled\")\n exit 0\n }\n }\n }\n $script:UPGRADE_EXISTING_WORKERS = $existingWorkers\n }\n \"^(2|reinstall)$\" {\n Write-Log (Get-Msg \"install.reinstall.performing\")\n $existingWorkspace = \"$env:USERPROFILE\\agentteams-manager\"\n if (Test-Path $script:AGENTTEAMS_ENV_FILE) {\n $envContent = Get-Content $script:AGENTTEAMS_ENV_FILE\n $wsLine = $envContent | Select-String \"^AGENTTEAMS_WORKSPACE_DIR=\"\n if ($wsLine) {\n $existingWorkspace = $wsLine.Line.Substring(21)\n }\n }\n Write-Host \"\"\n Write-Host \"$($script:ESC)[33m$(Get-Msg 'install.reinstall.warn_stop')$($script:ESC)[0m\"\n if ($runningManager) { Write-Host \"$($script:ESC)[33m - agentteams-manager (manager)$($script:ESC)[0m\" }\n $runningWorkers | ForEach-Object { Write-Host \"$($script:ESC)[33m - $_ (worker)$($script:ESC)[0m\" }\n Write-Host \"\"\n Write-Host \"$($script:ESC)[31m$(Get-Msg 'install.reinstall.warn_delete')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[31m$(Get-Msg 'install.reinstall.warn_volume')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[31m$(Get-Msg 'install.reinstall.warn_env' -f $script:AGENTTEAMS_ENV_FILE)$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[31m$(Get-Msg 'install.reinstall.warn_workspace' -f $existingWorkspace)$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[31m$(Get-Msg 'install.reinstall.warn_workers')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[31m$(Get-Msg 'install.reinstall.warn_proxy')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[31m$(Get-Msg 'install.reinstall.warn_network')$($script:ESC)[0m\"\n Write-Host \"\"\n Write-Host \"$($script:ESC)[31m$(Get-Msg 'install.reinstall.confirm_type')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[31m $existingWorkspace$($script:ESC)[0m\"\n Write-Host \"\"\n $confirmPath = Read-Host (Get-Msg \"install.reinstall.confirm_path\")\n if ($confirmPath -ne $existingWorkspace) {\n Write-Error (Get-Msg \"install.reinstall.path_mismatch\" -f $confirmPath, $existingWorkspace)\n }\n Write-Log (Get-Msg \"install.reinstall.confirmed\")\n docker stop agentteams-manager *>$null\n docker rm agentteams-manager *>$null\n docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-worker-\" | ForEach-Object {\n docker stop $_ *>$null\n docker rm $_ *>$null\n Write-Log (Get-Msg \"install.reinstall.removed_worker\" -f $_)\n }\n $existingController = docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-controller$\"\n if ($existingController) {\n Write-Log (Get-Msg \"install.reinstall.removing_proxy\")\n docker stop agentteams-controller *>$null\n docker rm agentteams-controller *>$null\n }\n if (docker volume ls -q 2>$null | Select-String \"^agentteams-data$\") {\n Write-Log (Get-Msg \"install.reinstall.removing_volume\")\n docker volume rm agentteams-data *>$null\n }\n if (Test-Path $existingWorkspace) {\n Write-Log (Get-Msg \"install.reinstall.removing_workspace\" -f $existingWorkspace)\n Remove-Item -Recurse -Force $existingWorkspace\n }\n if (Test-Path $script:AGENTTEAMS_ENV_FILE) {\n Write-Log (Get-Msg \"install.reinstall.removing_env\" -f $script:AGENTTEAMS_ENV_FILE)\n Remove-Item -Force $script:AGENTTEAMS_ENV_FILE\n }\n $existingNetwork = docker network ls --format \"{{.Name}}\" 2>$null | Select-String \"^agentteams-net$\"\n if ($existingNetwork) {\n Write-Log (Get-Msg \"install.reinstall.removing_network\")\n docker network rm agentteams-net *>$null\n }\n Write-Log (Get-Msg \"install.reinstall.cleanup_done\")\n }\n default {\n Write-Log (Get-Msg \"install.existing.cancelled\")\n exit 0\n }\n }\n\n # Load existing env file (upgrade path)\n if (Test-Path $script:AGENTTEAMS_ENV_FILE) {\n Write-Log (Get-Msg \"install.loading_config\" -f $script:AGENTTEAMS_ENV_FILE)\n Get-Content $script:AGENTTEAMS_ENV_FILE | ForEach-Object {\n if ($_ -match \"^([^#=][^=]*)=(.*)$\") {\n $key = $Matches[1].Trim()\n $value = $Matches[2].Split(\"#\")[0].Trim()\n if (-not [Environment]::GetEnvironmentVariable($key)) {\n [Environment]::SetEnvironmentVariable($key, $value, \"Process\")\n }\n }\n }\n }\n}\n\nfunction Step-Llm {\n Write-Log (Get-Msg \"llm.title\")\n\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n if ($script:AGENTTEAMS_LANGUAGE -eq \"zh\") {\n $script:config.LLM_PROVIDER = if ($env:AGENTTEAMS_LLM_PROVIDER) { $env:AGENTTEAMS_LLM_PROVIDER } else { \"openai-compat\" }\n $script:config.DEFAULT_MODEL = if ($env:AGENTTEAMS_DEFAULT_MODEL) { $env:AGENTTEAMS_DEFAULT_MODEL } else { \"qwen3.6-plus\" }\n $script:config.OPENAI_BASE_URL = if ($env:AGENTTEAMS_OPENAI_BASE_URL) { $env:AGENTTEAMS_OPENAI_BASE_URL } else { \"https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1\" }\n Write-Log (Get-Msg \"llm.provider.label\" -f $script:config.LLM_PROVIDER)\n Write-Log (Get-Msg \"llm.openai.base_url_label\" -f $script:config.OPENAI_BASE_URL)\n } else {\n $script:config.LLM_PROVIDER = if ($env:AGENTTEAMS_LLM_PROVIDER) { $env:AGENTTEAMS_LLM_PROVIDER } else { \"qwen\" }\n $script:config.DEFAULT_MODEL = if ($env:AGENTTEAMS_DEFAULT_MODEL) { $env:AGENTTEAMS_DEFAULT_MODEL } else { \"qwen3.6-plus\" }\n $script:config.OPENAI_BASE_URL = if ($env:AGENTTEAMS_OPENAI_BASE_URL) { $env:AGENTTEAMS_OPENAI_BASE_URL } else { \"\" }\n Write-Log (Get-Msg \"llm.provider.qwen_default\" -f $script:config.LLM_PROVIDER)\n }\n Write-Log (Get-Msg \"llm.model.label\" -f $script:config.DEFAULT_MODEL)\n Write-Log \"\"\n $script:config.LLM_API_KEY = Read-Prompt -VarName \"AGENTTEAMS_LLM_API_KEY\" -PromptText (Get-Msg \"llm.apikey_prompt\") -Secret\n $script:config.EMBEDDING_MODEL = if ($null -ne $env:AGENTTEAMS_EMBEDDING_MODEL) { $env:AGENTTEAMS_EMBEDDING_MODEL } else { \"text-embedding-v4\" }\n return\n }\n\n Write-Host \"\"\n Write-Host (Get-Msg \"llm.providers_title\")\n Write-Host (Get-Msg \"llm.provider.alibaba\")\n Write-Host (Get-Msg \"llm.provider.openai_compat\")\n Write-Host \"\"\n\n # If upgrade mode with loaded provider, show current as default\n if ($script:AGENTTEAMS_UPGRADE -and $script:config.LLM_PROVIDER) {\n $defaultProvider = if ($script:config.LLM_PROVIDER -eq \"openai-compat\") { \"2\" } else { \"1\" }\n $providerChoice = Read-Host \"$(Get-Msg 'llm.provider.select') [${defaultProvider}]\"\n $providerChoice = if ($providerChoice) { $providerChoice } else { $defaultProvider }\n } elseif ($script:AGENTTEAMS_QUICKSTART) {\n $providerChoice = Read-Host \"$(Get-Msg 'llm.provider.select') [1]\"\n } else {\n $providerChoice = Read-Host (Get-Msg \"llm.provider.select\")\n $providerChoice = if ($providerChoice) { $providerChoice } else { \"1\" }\n }\n if ($providerChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n\n switch -Regex ($providerChoice) {\n \"^(1|alibaba-cloud)$\" {\n $alibabaAccess = $null\n if ($script:AGENTTEAMS_LANGUAGE -eq \"en\") {\n $script:config.LLM_PROVIDER = \"openai-compat\"\n $script:config.OPENAI_BASE_URL = \"https://dashscope-intl.aliyuncs.com/compatible-mode/v1\"\n $alibabaAccess = \"tokenplan\"\n Write-Host \"\"\n Write-Host (Get-Msg \"llm.codingplan.models_title\")\n Write-Host (Get-Msg \"llm.codingplan.model.qwen36plus\")\n Write-Host (Get-Msg \"llm.codingplan.model.glm5\")\n Write-Host (Get-Msg \"llm.codingplan.model.kimi\")\n Write-Host (Get-Msg \"llm.codingplan.model.minimax\")\n Write-Host \"\"\n # If upgrade with loaded model, show current as default\n if ($script:AGENTTEAMS_UPGRADE -and $script:config.DEFAULT_MODEL) {\n $defaultModel = switch ($script:config.DEFAULT_MODEL) {\n \"qwen3.6-plus\" { \"1\" }\n \"glm-5\" { \"2\" }\n \"kimi-k2.5\" { \"3\" }\n \"MiniMax-M2.5\" { \"4\" }\n default { \"1\" }\n }\n $codingPlanModelChoice = Read-Host \"$(Get-Msg 'llm.codingplan.model.select') [${defaultModel}]\"\n $codingPlanModelChoice = if ($codingPlanModelChoice) { $codingPlanModelChoice } else { $defaultModel }\n } elseif ($script:AGENTTEAMS_QUICKSTART) {\n $codingPlanModelChoice = Read-Host \"$(Get-Msg 'llm.codingplan.model.select') [1]\"\n } else {\n $codingPlanModelChoice = Read-Host (Get-Msg \"llm.codingplan.model.select\")\n }\n $codingPlanModelChoice = if ($codingPlanModelChoice) { $codingPlanModelChoice } else { \"1\" }\n if ($codingPlanModelChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n switch -Regex ($codingPlanModelChoice) {\n \"^(1|qwen3\\.6-plus)$\" { $script:config.DEFAULT_MODEL = \"qwen3.6-plus\" }\n \"^(2|glm-5)$\" { $script:config.DEFAULT_MODEL = \"glm-5\" }\n \"^(3|kimi-k2\\.5)$\" { $script:config.DEFAULT_MODEL = \"kimi-k2.5\" }\n \"^(4|MiniMax-M2\\.5)$\" { $script:config.DEFAULT_MODEL = \"MiniMax-M2.5\" }\n default { $script:config.DEFAULT_MODEL = \"qwen3.6-plus\" }\n }\n Write-Log (Get-Msg \"llm.provider.selected_codingplan\")\n } else {\n Write-Host \"\"\n Write-Host (Get-Msg \"llm.alibaba.models_title\")\n Write-Host (Get-Msg \"llm.alibaba.model.tokenplan\")\n Write-Host (Get-Msg \"llm.alibaba.model.bailian\")\n Write-Host (Get-Msg \"llm.alibaba.model.codingplan_legacy\")\n Write-Host \"\"\n $modelChoice = if ($script:AGENTTEAMS_QUICKSTART) {\n Read-Host \"$(Get-Msg 'llm.alibaba.model.select') [1]\"\n } else {\n Read-Host (Get-Msg \"llm.alibaba.model.select\")\n }\n $modelChoice = if ($modelChoice) { $modelChoice } else { \"1\" }\n if ($modelChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n\n if ($modelChoice -match \"^(1|token-plan|tokenplan)$\") {\n $alibabaAccess = \"tokenplan\"\n $script:config.LLM_PROVIDER = \"openai-compat\"\n $script:config.OPENAI_BASE_URL = \"https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1\"\n Write-Host \"\"\n Write-Host (Get-Msg \"llm.codingplan.models_title\")\n Write-Host (Get-Msg \"llm.codingplan.model.qwen36plus\")\n Write-Host (Get-Msg \"llm.codingplan.model.glm5\")\n Write-Host (Get-Msg \"llm.codingplan.model.kimi\")\n Write-Host (Get-Msg \"llm.codingplan.model.minimax\")\n Write-Host \"\"\n # If upgrade with loaded model, show current as default\n if ($script:AGENTTEAMS_UPGRADE -and $script:config.DEFAULT_MODEL) {\n $defaultModel = switch ($script:config.DEFAULT_MODEL) {\n \"qwen3.6-plus\" { \"1\" }\n \"glm-5\" { \"2\" }\n \"kimi-k2.5\" { \"3\" }\n \"MiniMax-M2.5\" { \"4\" }\n default { \"1\" }\n }\n $codingPlanModelChoice = Read-Host \"$(Get-Msg 'llm.codingplan.model.select') [${defaultModel}]\"\n $codingPlanModelChoice = if ($codingPlanModelChoice) { $codingPlanModelChoice } else { $defaultModel }\n } elseif ($script:AGENTTEAMS_QUICKSTART) {\n $codingPlanModelChoice = Read-Host \"$(Get-Msg 'llm.codingplan.model.select') [1]\"\n } else {\n $codingPlanModelChoice = Read-Host (Get-Msg \"llm.codingplan.model.select\")\n }\n $codingPlanModelChoice = if ($codingPlanModelChoice) { $codingPlanModelChoice } else { \"1\" }\n if ($codingPlanModelChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n switch -Regex ($codingPlanModelChoice) {\n \"^(1|qwen3\\.6-plus)$\" { $script:config.DEFAULT_MODEL = \"qwen3.6-plus\" }\n \"^(2|glm-5)$\" { $script:config.DEFAULT_MODEL = \"glm-5\" }\n \"^(3|kimi-k2\\.5)$\" { $script:config.DEFAULT_MODEL = \"kimi-k2.5\" }\n \"^(4|MiniMax-M2\\.5)$\" { $script:config.DEFAULT_MODEL = \"MiniMax-M2.5\" }\n default { $script:config.DEFAULT_MODEL = \"qwen3.6-plus\" }\n }\n Write-Log (Get-Msg \"llm.provider.selected_tokenplan\")\n } elseif ($modelChoice -match \"^(2|qwen|bailian)$\") {\n $alibabaAccess = \"bailian\"\n $script:config.LLM_PROVIDER = \"qwen\"\n $script:config.OPENAI_BASE_URL = \"\"\n Write-Host \"\"\n if ($script:AGENTTEAMS_UPGRADE -and $script:config.DEFAULT_MODEL) {\n $qwenModelInput = Read-Host \"$(Get-Msg 'llm.qwen.model_prompt') [${script:config.DEFAULT_MODEL}]\"\n } else {\n $qwenModelInput = Read-Host (Get-Msg \"llm.qwen.model_prompt\")\n }\n if ($qwenModelInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $script:config.DEFAULT_MODEL = if ($qwenModelInput) { $qwenModelInput } elseif ($script:config.DEFAULT_MODEL) { $script:config.DEFAULT_MODEL } else { \"qwen3.6-plus\" }\n Write-Log (Get-Msg \"llm.provider.selected_qwen\")\n Request-CustomModelParams $script:config.DEFAULT_MODEL\n if ($script:StepResult -eq \"back\") { return }\n } elseif ($modelChoice -match \"^(3|coding-plan|codingplan)$\") {\n $alibabaAccess = \"codingplan_legacy\"\n $script:config.LLM_PROVIDER = \"openai-compat\"\n $script:config.OPENAI_BASE_URL = \"https://coding.dashscope.aliyuncs.com/v1\"\n Write-Host \"\"\n if ($script:AGENTTEAMS_UPGRADE -and $script:config.DEFAULT_MODEL) {\n $codingModelInput = Read-Host \"$(Get-Msg 'llm.qwen.model_prompt') [${script:config.DEFAULT_MODEL}]\"\n } else {\n $codingModelInput = Read-Host (Get-Msg \"llm.qwen.model_prompt\")\n }\n if ($codingModelInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $script:config.DEFAULT_MODEL = if ($codingModelInput) { $codingModelInput } elseif ($script:config.DEFAULT_MODEL) { $script:config.DEFAULT_MODEL } else { \"qwen3.6-plus\" }\n Write-Log (Get-Msg \"llm.provider.selected_codingplan_legacy\")\n Request-CustomModelParams $script:config.DEFAULT_MODEL\n if ($script:StepResult -eq \"back\") { return }\n } else {\n Write-Error (Get-Msg \"llm.alibaba.model.invalid\" -f $modelChoice)\n }\n }\n\n Write-Log (Get-Msg \"llm.model.label\" -f $script:config.DEFAULT_MODEL)\n Write-Log \"\"\n if ($alibabaAccess -eq \"bailian\") {\n Write-Log (Get-Msg \"llm.apikey_hint_bailian\")\n Write-Log (Get-Msg \"llm.apikey_url_bailian\")\n } elseif ($script:AGENTTEAMS_LANGUAGE -eq \"en\") {\n Write-Log (Get-Msg \"llm.apikey_hint_qwencloud\")\n Write-Log (Get-Msg \"llm.apikey_url_qwencloud\")\n } elseif ($alibabaAccess -eq \"codingplan_legacy\") {\n Write-Log (Get-Msg \"llm.apikey_hint_codingplan\")\n Write-Log (Get-Msg \"llm.apikey_url_codingplan\")\n } else {\n Write-Log (Get-Msg \"llm.apikey_hint_tokenplan\")\n Write-Log (Get-Msg \"llm.apikey_url_tokenplan\")\n }\n Write-Log \"\"\n $script:config.LLM_API_KEY = Read-Prompt -VarName \"AGENTTEAMS_LLM_API_KEY\" -PromptText (Get-Msg \"llm.apikey_prompt\") -Secret\n if ($script:StepResult -eq \"back\") { return }\n if ($alibabaAccess -eq \"bailian\") {\n Test-LlmConnectivity -BaseUrl \"https://dashscope.aliyuncs.com/compatible-mode/v1\" -ApiKey $script:config.LLM_API_KEY -Model $script:config.DEFAULT_MODEL\n } elseif ($alibabaAccess -eq \"codingplan_legacy\") {\n Test-LlmConnectivity -BaseUrl \"https://coding.dashscope.aliyuncs.com/v1\" -ApiKey $script:config.LLM_API_KEY -Model $script:config.DEFAULT_MODEL -Hint (Get-Msg \"llm.openai.test.fail.codingplan_legacy\")\n } elseif ($script:AGENTTEAMS_LANGUAGE -eq \"en\") {\n Test-LlmConnectivity -BaseUrl $script:config.OPENAI_BASE_URL -ApiKey $script:config.LLM_API_KEY -Model $script:config.DEFAULT_MODEL -Hint (Get-Msg \"llm.openai.test.fail.codingplan\")\n } else {\n Test-LlmConnectivity -BaseUrl $script:config.OPENAI_BASE_URL -ApiKey $script:config.LLM_API_KEY -Model $script:config.DEFAULT_MODEL -Hint (Get-Msg \"llm.openai.test.fail.tokenplan\")\n }\n if ($script:StepResult -eq \"back\") { return }\n }\n \"^(2|openai-compat)$\" {\n $script:config.LLM_PROVIDER = \"openai-compat\"\n Write-Log (Get-Msg \"llm.provider.selected_openai\" -f $script:config.LLM_PROVIDER)\n Write-Host \"\"\n if ($script:AGENTTEAMS_UPGRADE -and $env:AGENTTEAMS_UPGRADE_KEEP_ALL -eq \"1\") {\n Write-Log (Get-Msg \"prompt.upgrade_keep\" -f (Get-Msg \"llm.openai.base_url_prompt\"), $script:config.OPENAI_BASE_URL)\n } else {\n $currentUrl = $script:config.OPENAI_BASE_URL\n # Use base prompt without embedded default when showing current value\n if ($currentUrl) {\n $urlInput = Read-Host \"$(Get-Msg 'llm.openai.base_url_prompt') [$currentUrl]\"\n } else {\n $urlInput = Read-Host \"$(Get-Msg 'llm.openai.base_url_prompt') [https://api.openai.com/v1]\"\n }\n if ($urlInput -eq \"b\") { $script:StepResult = \"back\"; return }\n if ($urlInput) {\n $script:config.OPENAI_BASE_URL = $urlInput\n } elseif ($currentUrl) {\n $script:config.OPENAI_BASE_URL = $currentUrl\n } else {\n $script:config.OPENAI_BASE_URL = \"https://api.openai.com/v1\"\n }\n }\n if ($script:AGENTTEAMS_UPGRADE -and $env:AGENTTEAMS_UPGRADE_KEEP_ALL -eq \"1\") {\n Write-Log (Get-Msg \"prompt.upgrade_keep\" -f (Get-Msg \"llm.openai.model_prompt\"), $script:config.DEFAULT_MODEL)\n } else {\n $currentModel = $script:config.DEFAULT_MODEL\n if ($currentModel) {\n $modelInput = Read-Host \"$(Get-Msg 'llm.openai.model_prompt') [${currentModel}]\"\n } else {\n $modelInput = Read-Host (Get-Msg 'llm.openai.model_prompt')\n }\n if ($modelInput -eq \"b\") { $script:StepResult = \"back\"; return }\n if ($modelInput) {\n $script:config.DEFAULT_MODEL = $modelInput\n } elseif ($currentModel) {\n $script:config.DEFAULT_MODEL = $currentModel\n } else {\n $script:config.DEFAULT_MODEL = \"gpt-5.4\"\n }\n }\n Write-Log (Get-Msg \"llm.openai.base_url_label\" -f $script:config.OPENAI_BASE_URL)\n Write-Log (Get-Msg \"llm.model.label\" -f $script:config.DEFAULT_MODEL)\n Request-CustomModelParams $script:config.DEFAULT_MODEL\n if ($script:StepResult -eq \"back\") { return }\n Write-Log \"\"\n $script:config.LLM_API_KEY = Read-Prompt -VarName \"AGENTTEAMS_LLM_API_KEY\" -PromptText (Get-Msg \"llm.apikey_prompt\") -Secret\n if ($script:StepResult -eq \"back\") { return }\n Test-LlmConnectivity -BaseUrl $script:config.OPENAI_BASE_URL -ApiKey $script:config.LLM_API_KEY -Model $script:config.DEFAULT_MODEL\n if ($script:StepResult -eq \"back\") { return }\n }\n default {\n Write-Error (Get-Msg \"llm.provider.invalid\" -f $providerChoice)\n }\n }\n\n # Skip to embedding if Keep-All mode handled LLM params\n if ($skipToEmbedding) {\n # Skip embedding selection, use loaded value (already in $script:config.EMBEDDING_MODEL)\n if ($script:config.EMBEDDING_MODEL) {\n Write-Log (Get-Msg \"llm.embedding.title\")\n Write-Log (Get-Msg \"llm.model.label\" -f $script:config.EMBEDDING_MODEL)\n Write-Log \"\"\n }\n return\n }\n\n # --- Embedding model (optional, auto-tested) ---\n Write-Host \"\"\n Write-Log (Get-Msg \"llm.embedding.title\")\n Write-Log (Get-Msg \"llm.embedding.hint\")\n Write-Host \"\"\n Write-Host (Get-Msg \"llm.embedding.option.default\")\n Write-Host (Get-Msg \"llm.embedding.option.custom\")\n Write-Host (Get-Msg \"llm.embedding.option.disable\")\n Write-Host \"\"\n $embChoice = Read-Host \"$(Get-Msg 'llm.embedding.select') [1]\"\n $embChoice = if ($embChoice) { $embChoice } else { \"1\" }\n if ($embChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n\n switch ($embChoice) {\n \"1\" {\n $script:config.EMBEDDING_MODEL = \"text-embedding-v4\"\n }\n \"2\" {\n if ($script:AGENTTEAMS_UPGRADE -and $env:AGENTTEAMS_UPGRADE_KEEP_ALL -eq \"1\") {\n Write-Log (Get-Msg \"prompt.upgrade_keep\" -f (Get-Msg \"llm.embedding.custom_prompt\"), $script:config.EMBEDDING_MODEL)\n } else {\n $currentEmb = $script:config.EMBEDDING_MODEL\n $embCustom = Read-Host (Get-Msg \"llm.embedding.custom_prompt\")\n if ($embCustom -eq \"b\") { $script:StepResult = \"back\"; return }\n if ($embCustom) {\n $script:config.EMBEDDING_MODEL = $embCustom\n } else {\n $script:config.EMBEDDING_MODEL = $currentEmb\n if (-not $script:config.EMBEDDING_MODEL) {\n Write-Log (Get-Msg \"llm.embedding.disabled\")\n }\n }\n }\n }\n \"3\" {\n $script:config.EMBEDDING_MODEL = \"\"\n Write-Log (Get-Msg \"llm.embedding.disabled\")\n }\n default {\n $script:config.EMBEDDING_MODEL = \"text-embedding-v4\"\n }\n }\n\n if ($script:config.EMBEDDING_MODEL) {\n # Qwen provider uses dashscope directly; others use OPENAI_BASE_URL\n $embBaseUrl = $script:config.OPENAI_BASE_URL\n if ($script:config.LLM_PROVIDER -eq \"qwen\") {\n $embBaseUrl = \"https://dashscope.aliyuncs.com/compatible-mode/v1\"\n }\n $embResult = Test-EmbeddingConnectivity -BaseUrl $embBaseUrl -ApiKey $script:config.LLM_API_KEY -Model $script:config.EMBEDDING_MODEL\n if (-not $embResult) {\n $script:config.EMBEDDING_MODEL = \"\"\n Write-Log (Get-Msg \"llm.embedding.auto_disabled\")\n }\n }\n\n Write-Log \"\"\n}\n\nfunction Step-Admin {\n Write-Log (Get-Msg \"admin.title\")\n $script:config.ADMIN_USER = Read-Prompt -VarName \"AGENTTEAMS_ADMIN_USER\" -PromptText (Get-Msg \"admin.username_prompt\") -Default \"admin\"\n if ($script:StepResult -eq \"back\") { return }\n $script:config.ADMIN_USER = $script:config.ADMIN_USER.ToLowerInvariant()\n\n # Pre-set via env var: validate; non-interactive fails fast,\n # interactive warns and falls through to the retry prompt.\n if ($env:AGENTTEAMS_ADMIN_PASSWORD) {\n $script:config.ADMIN_PASSWORD = $env:AGENTTEAMS_ADMIN_PASSWORD\n Write-Log (Get-Msg \"prompt.preset\" -f (Get-Msg \"admin.password_prompt\"))\n if ($script:config.ADMIN_PASSWORD.Length -ge 8) {\n Write-Log \"\"\n return\n }\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n Write-Error (Get-Msg \"admin.password_too_short\" -f $script:config.ADMIN_PASSWORD.Length)\n }\n Write-Host \"$($script:ESC)[31m[AgentTeams ERROR]$($script:ESC)[0m $(Get-Msg \"admin.password_too_short\" -f $script:config.ADMIN_PASSWORD.Length)\"\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_ADMIN_PASSWORD\", $null, \"Process\")\n }\n\n while ($true) {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_ADMIN_PASSWORD\", $null, \"Process\")\n $script:config.ADMIN_PASSWORD = Read-Prompt -VarName \"AGENTTEAMS_ADMIN_PASSWORD\" -PromptText (Get-Msg \"admin.password_prompt\") -Secret -Optional\n if ($script:StepResult -eq \"back\") { return }\n if (-not $script:config.ADMIN_PASSWORD) {\n $randomSuffix = (New-RandomKey).Substring(0, 12)\n $script:config.ADMIN_PASSWORD = \"admin$randomSuffix\"\n Write-Log (Get-Msg \"admin.password_generated\")\n break\n }\n if ($script:config.ADMIN_PASSWORD.Length -ge 8) {\n break\n }\n Write-Host \"$($script:ESC)[31m[AgentTeams ERROR]$($script:ESC)[0m $(Get-Msg \"admin.password_too_short\" -f $script:config.ADMIN_PASSWORD.Length)\"\n }\n\n Write-Log \"\"\n}\n\nfunction Step-Network {\n Write-Log (Get-Msg \"port.local_only.title\")\n Write-Host \"\"\n Write-Host \" 1) $(Get-Msg 'port.local_only.hint_yes')\"\n Write-Host \" 2) $(Get-Msg 'port.local_only.hint_no')\"\n Write-Host \"\"\n\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n $localOnly = if ($env:AGENTTEAMS_LOCAL_ONLY) { $env:AGENTTEAMS_LOCAL_ONLY } else { \"1\" }\n } elseif ($null -ne $env:AGENTTEAMS_LOCAL_ONLY) {\n $localOnly = $env:AGENTTEAMS_LOCAL_ONLY\n } else {\n $localChoice = Read-Host \"$(Get-Msg 'port.local_only.choice')\"\n if ($localChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n if (-not $localChoice) { $localChoice = \"1\" }\n $localOnly = if ($localChoice -match '^(2|n|N|no|NO)$') { \"0\" } else { \"1\" }\n }\n $script:config.LOCAL_ONLY = $localOnly\n\n if ($localOnly -eq \"1\") {\n Write-Log (Get-Msg \"port.local_only.selected_local\")\n } else {\n Write-Log (Get-Msg \"port.local_only.selected_external\")\n Write-Host \"\"\n Write-Host (Get-Msg \"port.local_only.https_hint\") -ForegroundColor Yellow\n }\n Write-Log \"\"\n}\n\nfunction Step-Ports {\n Write-Log (Get-Msg \"port.title\")\n $script:config.PORT_GATEWAY = Read-Prompt -VarName \"AGENTTEAMS_PORT_GATEWAY\" -PromptText (Get-Msg \"port.gateway_prompt\") -Default \"18080\"\n if ($script:StepResult -eq \"back\") { return }\n $script:config.PORT_CONSOLE = Read-Prompt -VarName \"AGENTTEAMS_PORT_CONSOLE\" -PromptText (Get-Msg \"port.console_prompt\") -Default \"18001\"\n if ($script:StepResult -eq \"back\") { return }\n $script:config.PORT_ELEMENT_WEB = Read-Prompt -VarName \"AGENTTEAMS_PORT_ELEMENT_WEB\" -PromptText (Get-Msg \"port.element_prompt\") -Default \"18088\"\n if ($script:StepResult -eq \"back\") { return }\n $script:config.PORT_MANAGER_CONSOLE = Read-Prompt -VarName \"AGENTTEAMS_PORT_MANAGER_CONSOLE\" -PromptText (Get-Msg \"port.manager_console_prompt\") -Default \"18888\"\n if ($script:StepResult -eq \"back\") { return }\n Write-Log \"\"\n}\n\nfunction Step-Domains {\n Write-Log (Get-Msg \"domain.title\")\n Write-Log (Get-Msg \"domain.hint\")\n $script:config.MATRIX_DOMAIN = Read-Prompt -VarName \"AGENTTEAMS_MATRIX_DOMAIN\" -PromptText (Get-Msg \"domain.matrix_prompt\") -Default \"matrix-local.agentteams.io:$($script:config.PORT_GATEWAY)\"\n if ($script:StepResult -eq \"back\") { return }\n $script:config.MATRIX_CLIENT_DOMAIN = Read-Prompt -VarName \"AGENTTEAMS_MATRIX_CLIENT_DOMAIN\" -PromptText (Get-Msg \"domain.element_prompt\") -Default \"matrix-client-local.agentteams.io\"\n if ($script:StepResult -eq \"back\") { return }\n $script:config.AI_GATEWAY_DOMAIN = Read-Prompt -VarName \"AGENTTEAMS_AI_GATEWAY_DOMAIN\" -PromptText (Get-Msg \"domain.gateway_prompt\") -Default \"aigw-local.agentteams.io\"\n if ($script:StepResult -eq \"back\") { return }\n $script:config.FS_DOMAIN = Read-Prompt -VarName \"AGENTTEAMS_FS_DOMAIN\" -PromptText (Get-Msg \"domain.fs_prompt\") -Default \"fs-local.agentteams.io\"\n if ($script:StepResult -eq \"back\") { return }\n $script:config.CONSOLE_DOMAIN = Read-Prompt -VarName \"AGENTTEAMS_CONSOLE_DOMAIN\" -PromptText (Get-Msg \"domain.console_prompt\") -Default \"console-local.agentteams.io\"\n if ($script:StepResult -eq \"back\") { return }\n Write-Log \"\"\n}\n\nfunction Step-Github {\n Write-Log (Get-Msg \"github.title\")\n $script:config.GITHUB_TOKEN = Read-Prompt -VarName \"AGENTTEAMS_GITHUB_TOKEN\" -PromptText (Get-Msg \"github.token_prompt\") -Secret -Optional\n}\n\nfunction Step-Skills {\n Write-Log \"\"\n Write-Log (Get-Msg \"skills.title\")\n $script:config.SKILLS_API_URL = Read-Prompt -VarName \"AGENTTEAMS_SKILLS_API_URL\" -PromptText (Get-Msg \"skills.url_prompt\") -Optional\n if ($script:StepResult -eq \"back\") { return }\n Write-Log \"\"\n}\n\nfunction Step-Volume {\n Write-Log (Get-Msg \"data.title\")\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n $script:config.DATA_DIR = if ($env:AGENTTEAMS_DATA_DIR) { $env:AGENTTEAMS_DATA_DIR } else { \"agentteams-data\" }\n Write-Log \" $(Get-Msg 'data.volume_using' -f $script:config.DATA_DIR) (non-interactive, skipped)\"\n return\n }\n # ─────────────────────────────────────────────────────────────────\n $dataDirInput = Read-Host (Get-Msg \"data.volume_prompt\")\n if ($dataDirInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $script:config.DATA_DIR = if ($dataDirInput) { $dataDirInput } else { \"agentteams-data\" }\n Write-Log (Get-Msg \"data.volume_using\" -f $script:config.DATA_DIR)\n}\n\nfunction Step-Workspace {\n Write-Log (Get-Msg \"workspace.title\")\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n $script:config.WORKSPACE_DIR = if ($env:AGENTTEAMS_WORKSPACE_DIR) { $env:AGENTTEAMS_WORKSPACE_DIR } else { \"$env:USERPROFILE\\agentteams-manager\" }\n if (-not (Test-Path $script:config.WORKSPACE_DIR)) {\n New-Item -ItemType Directory -Path $script:config.WORKSPACE_DIR -Force | Out-Null\n }\n Write-Log \" $(Get-Msg 'workspace.dir_label' -f $script:config.WORKSPACE_DIR) (non-interactive, skipped)\"\n return\n }\n # ─────────────────────────────────────────────────────────────────\n $defaultWorkspace = \"$env:USERPROFILE\\agentteams-manager\"\n if ($script:AGENTTEAMS_UPGRADE -and $env:AGENTTEAMS_UPGRADE_KEEP_ALL -eq \"1\") {\n Write-Log (Get-Msg \"prompt.upgrade_keep\" -f (Get-Msg \"workspace.dir_prompt\" -f $defaultWorkspace), $script:config.WORKSPACE_DIR)\n $wsInput = if ($script:config.WORKSPACE_DIR) { $script:config.WORKSPACE_DIR } else { $defaultWorkspace }\n } else {\n $currentWs = $script:config.WORKSPACE_DIR\n $wsInput = Read-Host (Get-Msg \"workspace.dir_prompt\" -f $defaultWorkspace)\n if ($wsInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $wsInput = if ($wsInput) { $wsInput } else { if ($currentWs) { $currentWs } else { $defaultWorkspace } }\n }\n $script:config.WORKSPACE_DIR = $wsInput\n if (-not (Test-Path $script:config.WORKSPACE_DIR)) {\n New-Item -ItemType Directory -Path $script:config.WORKSPACE_DIR -Force | Out-Null\n }\n Write-Log (Get-Msg \"workspace.dir_label\" -f $script:config.WORKSPACE_DIR)\n}\n\nfunction Step-Runtime {\n Write-Log (Get-Msg \"worker_runtime.title\")\n Write-Host \"\"\n Write-Host \" 1) $(Get-Msg 'worker_runtime.copaw')\"\n Write-Host \" 2) $(Get-Msg 'worker_runtime.openclaw')\"\n Write-Host \" 3) $(Get-Msg 'worker_runtime.hermes')\"\n Write-Host \"\"\n\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n $script:config.DEFAULT_WORKER_RUNTIME = if ($env:AGENTTEAMS_DEFAULT_WORKER_RUNTIME) { $env:AGENTTEAMS_DEFAULT_WORKER_RUNTIME } else { \"copaw\" }\n } elseif ($script:AGENTTEAMS_UPGRADE -and $env:AGENTTEAMS_DEFAULT_WORKER_RUNTIME) {\n Write-Log (Get-Msg \"prompt.upgrade_keep\" -f (Get-Msg \"worker_runtime.title_short\"), $env:AGENTTEAMS_DEFAULT_WORKER_RUNTIME)\n $rtChoice = Read-Host (Get-Msg \"worker_runtime.choice\")\n if ($rtChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n if ($rtChoice) {\n $script:config.DEFAULT_WORKER_RUNTIME = switch ($rtChoice) {\n \"2\" { \"openclaw\" }\n \"3\" { \"hermes\" }\n default { \"copaw\" }\n }\n } else {\n $script:config.DEFAULT_WORKER_RUNTIME = $env:AGENTTEAMS_DEFAULT_WORKER_RUNTIME\n }\n } elseif ($env:AGENTTEAMS_DEFAULT_WORKER_RUNTIME) {\n $script:config.DEFAULT_WORKER_RUNTIME = $env:AGENTTEAMS_DEFAULT_WORKER_RUNTIME\n } else {\n $rtChoice = Read-Host (Get-Msg \"worker_runtime.choice\")\n if ($rtChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n $rtChoice = if ($rtChoice) { $rtChoice } else { \"1\" }\n $script:config.DEFAULT_WORKER_RUNTIME = switch ($rtChoice) {\n \"2\" { \"openclaw\" }\n \"3\" { \"hermes\" }\n default { \"copaw\" }\n }\n }\n Write-Log (Get-Msg \"worker_runtime.selected\" -f $script:config.DEFAULT_WORKER_RUNTIME)\n}\n\nfunction Step-ManagerRuntime {\n Write-Log (Get-Msg \"manager_runtime.title\")\n Write-Host \"\"\n Write-Host \" 1) $(Get-Msg 'manager_runtime.copaw')\"\n Write-Host \" 2) $(Get-Msg 'manager_runtime.openclaw')\"\n Write-Host \"\"\n\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n $script:config.MANAGER_RUNTIME = if ($env:AGENTTEAMS_MANAGER_RUNTIME) { $env:AGENTTEAMS_MANAGER_RUNTIME } else { \"copaw\" }\n } elseif ($script:AGENTTEAMS_UPGRADE -and $env:AGENTTEAMS_MANAGER_RUNTIME) {\n Write-Log (Get-Msg \"prompt.upgrade_keep\" -f (Get-Msg \"manager_runtime.title_short\"), $env:AGENTTEAMS_MANAGER_RUNTIME)\n $mrChoice = Read-Host (Get-Msg \"manager_runtime.choice\")\n if ($mrChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n if ($mrChoice) {\n $script:config.MANAGER_RUNTIME = if ($mrChoice -eq \"2\") { \"openclaw\" } else { \"copaw\" }\n } else {\n $script:config.MANAGER_RUNTIME = $env:AGENTTEAMS_MANAGER_RUNTIME\n }\n } elseif ($env:AGENTTEAMS_MANAGER_RUNTIME) {\n $script:config.MANAGER_RUNTIME = $env:AGENTTEAMS_MANAGER_RUNTIME\n } else {\n $mrChoice = Read-Host (Get-Msg \"manager_runtime.choice\")\n if ($mrChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n $mrChoice = if ($mrChoice) { $mrChoice } else { \"1\" }\n $script:config.MANAGER_RUNTIME = if ($mrChoice -eq \"2\") { \"openclaw\" } else { \"copaw\" }\n }\n Write-Log (Get-Msg \"manager_runtime.selected\" -f $script:config.MANAGER_RUNTIME)\n}\n\nfunction Step-E2ee {\n Write-Host \"\"\n Write-Log (Get-Msg \"matrix_e2ee.title\")\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n $script:config.MATRIX_E2EE = if ($env:AGENTTEAMS_MATRIX_E2EE) { $env:AGENTTEAMS_MATRIX_E2EE } else { \"0\" }\n Write-Log \" $(Get-Msg 'matrix_e2ee.title_short') = $($script:config.MATRIX_E2EE) (non-interactive, skipped)\"\n return\n }\n # ─────────────────────────────────────────────────────────────────\n Write-Host \"\"\n Write-Host \" $(Get-Msg 'matrix_e2ee.desc')\"\n Write-Host \"\"\n Write-Host \" 1) $(Get-Msg 'matrix_e2ee.disable')\"\n Write-Host \" 2) $(Get-Msg 'matrix_e2ee.enable')\"\n Write-Host \"\"\n\n if ($script:AGENTTEAMS_UPGRADE -and $env:AGENTTEAMS_MATRIX_E2EE) {\n $e2eeDisplay = if ($env:AGENTTEAMS_MATRIX_E2EE -eq \"1\") { Get-Msg \"matrix_e2ee.val_enabled\" } else { Get-Msg \"matrix_e2ee.val_disabled\" }\n Write-Log (Get-Msg \"prompt.upgrade_keep\" -f (Get-Msg \"matrix_e2ee.title_short\"), $e2eeDisplay)\n $e2eeChoice = Read-Host (Get-Msg \"matrix_e2ee.choice\")\n if ($e2eeChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n if ($e2eeChoice) {\n $script:config.MATRIX_E2EE = if ($e2eeChoice -eq \"2\") { \"1\" } else { \"0\" }\n } else {\n $script:config.MATRIX_E2EE = $env:AGENTTEAMS_MATRIX_E2EE\n }\n } elseif (-not $env:AGENTTEAMS_MATRIX_E2EE) {\n $e2eeChoice = Read-Host (Get-Msg \"matrix_e2ee.choice\")\n if ($e2eeChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n $e2eeChoice = if ($e2eeChoice) { $e2eeChoice } else { \"1\" }\n $script:config.MATRIX_E2EE = if ($e2eeChoice -eq \"2\") { \"1\" } else { \"0\" }\n } else {\n $script:config.MATRIX_E2EE = $env:AGENTTEAMS_MATRIX_E2EE\n }\n\n if ($script:config.MATRIX_E2EE -eq \"1\") {\n Write-Log (Get-Msg \"matrix_e2ee.selected_enabled\")\n } else {\n Write-Log (Get-Msg \"matrix_e2ee.selected_disabled\")\n }\n}\n\nfunction Step-DockerProxy {\n if (-not $script:AGENTTEAMS_MOUNT_SOCKET) {\n $script:config.DOCKER_PROXY = \"0\"\n return\n }\n\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n $script:config.DOCKER_PROXY = if ($env:AGENTTEAMS_DOCKER_PROXY) { $env:AGENTTEAMS_DOCKER_PROXY } else { \"0\" }\n Write-Log \" $(Get-Msg 'docker_proxy.title_short') = $($script:config.DOCKER_PROXY) (non-interactive, skipped)\"\n return\n }\n # ─────────────────────────────────────────────────────────────────\n\n Write-Host \"\"\n Write-Log (Get-Msg \"docker_proxy.title\")\n Write-Host \"\"\n Write-Host \" $(Get-Msg 'docker_proxy.desc')\"\n Write-Host \"\"\n Write-Host \" 1) $(Get-Msg 'docker_proxy.enable')\"\n Write-Host \" 2) $(Get-Msg 'docker_proxy.disable')\"\n Write-Host \"\"\n\n if ($script:AGENTTEAMS_UPGRADE -and $env:AGENTTEAMS_DOCKER_PROXY) {\n $proxyDisplay = if ($env:AGENTTEAMS_DOCKER_PROXY -eq \"1\") { Get-Msg \"docker_proxy.val_enabled\" } else { Get-Msg \"docker_proxy.val_disabled\" }\n Write-Log (Get-Msg \"prompt.upgrade_keep\" -f (Get-Msg \"docker_proxy.title_short\"), $proxyDisplay)\n $proxyChoice = Read-Host (Get-Msg \"docker_proxy.choice\")\n if ($proxyChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n if ($proxyChoice) {\n $script:config.DOCKER_PROXY = if ($proxyChoice -eq \"2\") { \"0\" } else { \"1\" }\n } else {\n $script:config.DOCKER_PROXY = $env:AGENTTEAMS_DOCKER_PROXY\n }\n } elseif (-not $env:AGENTTEAMS_DOCKER_PROXY) {\n $proxyChoice = Read-Host (Get-Msg \"docker_proxy.choice\")\n if ($proxyChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n $proxyChoice = if ($proxyChoice) { $proxyChoice } else { \"1\" }\n $script:config.DOCKER_PROXY = if ($proxyChoice -eq \"2\") { \"0\" } else { \"1\" }\n } else {\n $script:config.DOCKER_PROXY = $env:AGENTTEAMS_DOCKER_PROXY\n }\n\n if ($script:config.DOCKER_PROXY -eq \"1\") {\n Write-Log (Get-Msg \"docker_proxy.selected_enabled\")\n\n # Prompt for additional allowed image sources\n Write-Host \"\"\n Write-Host \" $(Get-Msg 'docker_proxy.registries_desc')\"\n Write-Host \"\"\n if ($script:AGENTTEAMS_UPGRADE -and $env:AGENTTEAMS_PROXY_ALLOWED_REGISTRIES) {\n Write-Log (Get-Msg \"prompt.upgrade_keep\" -f (Get-Msg \"docker_proxy.registries_label\"), $env:AGENTTEAMS_PROXY_ALLOWED_REGISTRIES)\n $regInput = Read-Host (Get-Msg \"docker_proxy.registries_prompt\")\n if ($regInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $script:config.PROXY_ALLOWED_REGISTRIES = if ($regInput) { $regInput } else { $env:AGENTTEAMS_PROXY_ALLOWED_REGISTRIES }\n } elseif (-not $env:AGENTTEAMS_PROXY_ALLOWED_REGISTRIES) {\n $regInput = Read-Host (Get-Msg \"docker_proxy.registries_prompt\")\n if ($regInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $script:config.PROXY_ALLOWED_REGISTRIES = if ($regInput) { $regInput } else { \"\" }\n } else {\n $script:config.PROXY_ALLOWED_REGISTRIES = $env:AGENTTEAMS_PROXY_ALLOWED_REGISTRIES\n }\n } else {\n Write-Log (Get-Msg \"docker_proxy.selected_disabled\")\n }\n}\n\nfunction Step-Idle {\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n $script:config.WORKER_IDLE_TIMEOUT = if ($env:AGENTTEAMS_WORKER_IDLE_TIMEOUT) { $env:AGENTTEAMS_WORKER_IDLE_TIMEOUT } else { \"720\" }\n Write-Log \" $(Get-Msg 'idle_timeout.label') = $($script:config.WORKER_IDLE_TIMEOUT) (non-interactive, skipped)\"\n return\n }\n # ─────────────────────────────────────────────────────────────────\n if ($script:AGENTTEAMS_UPGRADE -and $env:AGENTTEAMS_WORKER_IDLE_TIMEOUT) {\n Write-Log (Get-Msg \"prompt.upgrade_keep\" -f (Get-Msg \"idle_timeout.label\"), $env:AGENTTEAMS_WORKER_IDLE_TIMEOUT)\n $idleInput = Read-Host (Get-Msg \"idle_timeout.prompt\")\n if ($idleInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $script:config.WORKER_IDLE_TIMEOUT = if ($idleInput) { $idleInput } else { $env:AGENTTEAMS_WORKER_IDLE_TIMEOUT }\n } elseif (-not $env:AGENTTEAMS_WORKER_IDLE_TIMEOUT) {\n $idleInput = Read-Host (Get-Msg \"idle_timeout.prompt\")\n if ($idleInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $script:config.WORKER_IDLE_TIMEOUT = if ($idleInput) { $idleInput } else { \"720\" }\n } else {\n $script:config.WORKER_IDLE_TIMEOUT = $env:AGENTTEAMS_WORKER_IDLE_TIMEOUT\n }\n Write-Log (Get-Msg \"idle_timeout.selected\" -f $script:config.WORKER_IDLE_TIMEOUT)\n Write-Log \"\"\n}\n\nfunction Step-Hostshare {\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n $script:config.HOST_SHARE_DIR = if ($env:AGENTTEAMS_HOST_SHARE_DIR) { $env:AGENTTEAMS_HOST_SHARE_DIR } else { $env:USERPROFILE }\n Write-Log \" $(Get-Msg 'host_share.label') = $($script:config.HOST_SHARE_DIR) (non-interactive, skipped)\"\n return\n }\n # ─────────────────────────────────────────────────────────────────\n $currentShare = $script:config.HOST_SHARE_DIR\n if ($currentShare) {\n $shareInput = Read-Host \"$(Get-Msg 'host_share.prompt' -f $env:USERPROFILE) [${currentShare}]\"\n } else {\n $shareInput = Read-Host (Get-Msg 'host_share.prompt' -f $env:USERPROFILE)\n }\n if ($shareInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $script:config.HOST_SHARE_DIR = if ($shareInput) { $shareInput } else { if ($currentShare) { $currentShare } else { $env:USERPROFILE } }\n}\n\n# ============================================================\n# Manager Installation\n# ============================================================\n\nfunction Install-Manager {\n Write-Log (Get-Msg \"install.title\")\n\n Move-LegacyAgentTeamsEnv\n\n # Detect timezone\n $script:AGENTTEAMS_TIMEZONE = Get-AgentTeamsTimeZone\n\n # Language priority: env var > existing env file > timezone detection\n if ($env:AGENTTEAMS_LANGUAGE) {\n $script:AGENTTEAMS_LANGUAGE = $env:AGENTTEAMS_LANGUAGE\n } else {\n # Check existing env file for saved language preference (upgrade scenario)\n $_envFile = $script:AGENTTEAMS_ENV_FILE\n # Migrate from legacy location (current directory) if needed\n if (-not (Test-Path $_envFile) -and (Test-Path \".\\agentteams-manager.env\")) {\n Write-Log \"Migrating agentteams-manager.env to $_envFile...\"\n Move-Item \".\\agentteams-manager.env\" $_envFile -ErrorAction SilentlyContinue\n }\n if (Test-Path $_envFile) {\n $_savedLang = (Get-Content $_envFile | Select-String \"^AGENTTEAMS_LANGUAGE=\" | ForEach-Object {\n $_.Line -replace '^AGENTTEAMS_LANGUAGE=', ''\n } | Select-Object -First 1)\n if ($_savedLang) {\n $script:AGENTTEAMS_LANGUAGE = $_savedLang\n }\n }\n # Fall back to timezone-based detection\n if (-not $script:AGENTTEAMS_LANGUAGE) {\n $script:AGENTTEAMS_LANGUAGE = Get-AgentTeamsLanguage -Timezone $script:AGENTTEAMS_TIMEZONE\n }\n }\n $env:AGENTTEAMS_LANGUAGE = $script:AGENTTEAMS_LANGUAGE\n\n # Detect registry\n $script:AGENTTEAMS_REGISTRY = Get-Registry -Timezone $script:AGENTTEAMS_TIMEZONE\n\n # Set image names\n $script:MANAGER_IMAGE = if ($env:AGENTTEAMS_INSTALL_MANAGER_IMAGE) {\n $env:AGENTTEAMS_INSTALL_MANAGER_IMAGE\n } else {\n \"$($script:AGENTTEAMS_REGISTRY)/agentteams/agentteams-manager:$($script:AGENTTEAMS_VERSION)\"\n }\n\n $script:WORKER_IMAGE = if ($env:AGENTTEAMS_INSTALL_WORKER_IMAGE) {\n $env:AGENTTEAMS_INSTALL_WORKER_IMAGE\n } else {\n \"$($script:AGENTTEAMS_REGISTRY)/agentteams/agentteams-worker:$($script:AGENTTEAMS_VERSION)\"\n }\n\n $script:COPAW_WORKER_IMAGE = if ($env:AGENTTEAMS_INSTALL_COPAW_WORKER_IMAGE) {\n $env:AGENTTEAMS_INSTALL_COPAW_WORKER_IMAGE\n } else {\n \"$($script:AGENTTEAMS_REGISTRY)/agentteams/agentteams-copaw-worker:$($script:AGENTTEAMS_VERSION)\"\n }\n\n $script:HERMES_WORKER_IMAGE = if ($env:AGENTTEAMS_INSTALL_HERMES_WORKER_IMAGE) {\n $env:AGENTTEAMS_INSTALL_HERMES_WORKER_IMAGE\n } else {\n \"$($script:AGENTTEAMS_REGISTRY)/agentteams/agentteams-hermes-worker:$($script:AGENTTEAMS_VERSION)\"\n }\n\n $script:MANAGER_COPAW_IMAGE = if ($env:AGENTTEAMS_INSTALL_MANAGER_COPAW_IMAGE) {\n $env:AGENTTEAMS_INSTALL_MANAGER_COPAW_IMAGE\n } else {\n \"$($script:AGENTTEAMS_REGISTRY)/agentteams/agentteams-manager-copaw:$($script:AGENTTEAMS_VERSION)\"\n }\n\n # Backward compatibility: accept old env var name from previous versions\n $controllerImageOverride = if ($env:AGENTTEAMS_INSTALL_CONTROLLER_IMAGE) { $env:AGENTTEAMS_INSTALL_CONTROLLER_IMAGE } elseif ($env:AGENTTEAMS_INSTALL_DOCKER_PROXY_IMAGE) { $env:AGENTTEAMS_INSTALL_DOCKER_PROXY_IMAGE } else { $null }\n $script:CONTROLLER_IMAGE = if ($controllerImageOverride) {\n $controllerImageOverride\n } else {\n \"$($script:AGENTTEAMS_REGISTRY)/agentteams/agentteams-controller:$($script:AGENTTEAMS_VERSION)\"\n }\n\n # Resolve embedded controller image (sets $script:EMBEDDED_IMAGE and\n # $script:AGENTTEAMS_USE_EMBEDDED). Errors out fast if no embedded image is available\n # for the requested version (mirrors the bash installer behavior).\n Resolve-EmbeddedImage\n\n Write-Log (Get-Msg \"install.registry\" -f $script:AGENTTEAMS_REGISTRY)\n Write-Log \"\"\n Write-Log (Get-Msg \"install.dir\" -f (Get-Location))\n Write-Log (Get-Msg \"install.dir_hint\")\n Write-Log (Get-Msg \"install.dir_hint2\")\n Write-Log \"\"\n\n # Check container runtime (docker or podman)\n if (-not (Get-Command \"docker\" -ErrorAction SilentlyContinue) -and\n -not (Get-Command \"podman\" -ErrorAction SilentlyContinue)) {\n Write-Host \"$($script:ESC)[31m[AgentTeams ERROR]$($script:ESC)[0m $(Get-Msg 'error.docker_not_found')\" -ForegroundColor Red\n Exit-Script 1\n }\n\n if (-not (Test-DockerRunning)) {\n Write-Host \"$($script:ESC)[31m[AgentTeams ERROR]$($script:ESC)[0m $(Get-Msg 'error.docker_not_running')\" -ForegroundColor Red\n Exit-Script 1\n }\n\n # Initialize shared config hashtable\n $script:config = @{}\n $config = $script:config # local alias so post-machine code can use $config\n\n # Orphan volume detection (env file gone but volume remains)\n if (-not (Test-Path $script:AGENTTEAMS_ENV_FILE)) {\n $dataVol = if ($env:AGENTTEAMS_DATA_DIR) { $env:AGENTTEAMS_DATA_DIR } else { \"agentteams-data\" }\n $volumeExists = docker volume ls -q 2>$null | Select-String \"^${dataVol}$\"\n if ($volumeExists) {\n Write-Host \"\"\n Write-Log (Get-Msg \"install.orphan_volume.detected\" -f $dataVol)\n Write-Log (Get-Msg \"install.orphan_volume.warn\")\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n Write-Log (Get-Msg \"install.orphan_volume.clean_noninteractive\")\n docker stop agentteams-manager *>$null\n docker rm agentteams-manager *>$null\n docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-worker-\" | ForEach-Object {\n docker stop $_.ToString().Trim() *>$null\n docker rm $_.ToString().Trim() *>$null\n }\n Write-Log (Get-Msg \"install.orphan_volume.cleaning\")\n docker volume rm $dataVol *>$null\n Write-Log (Get-Msg \"install.orphan_volume.cleaned\")\n } else {\n Write-Host \"\"\n Write-Host (Get-Msg \"install.orphan_volume.choose\")\n Write-Host (Get-Msg \"install.orphan_volume.clean\")\n Write-Host (Get-Msg \"install.orphan_volume.keep\")\n Write-Host \"\"\n $orphanChoice = Read-Host (Get-Msg \"install.orphan_volume.prompt\")\n if (-not $orphanChoice) { $orphanChoice = \"1\" }\n switch -Regex ($orphanChoice) {\n \"^(1|clean)$\" {\n docker stop agentteams-manager *>$null\n docker rm agentteams-manager *>$null\n docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-worker-\" | ForEach-Object {\n docker stop $_.ToString().Trim() *>$null\n docker rm $_.ToString().Trim() *>$null\n }\n Write-Log (Get-Msg \"install.orphan_volume.cleaning\")\n docker volume rm $dataVol *>$null\n Write-Log (Get-Msg \"install.orphan_volume.cleaned\")\n }\n \"^(2|keep)$\" {\n Write-Log (Get-Msg \"install.orphan_volume.keeping\")\n }\n }\n }\n }\n }\n\n # ── State machine ─────────────────────────────────────────────────────────\n $_steps = @(\"Step-Lang\", \"Step-Mode\", \"Step-Existing\", \"Step-Llm\", \"Step-ManagerRuntime\", \"Step-Runtime\", \"Step-Admin\",\n \"Step-Network\", \"Step-Ports\", \"Step-Domains\", \"Step-Github\", \"Step-Skills\",\n \"Step-Volume\", \"Step-Workspace\", \"Step-E2ee\", \"Step-DockerProxy\", \"Step-Idle\",\n \"Step-Hostshare\")\n $_stepHistory = [System.Collections.Generic.List[int]]::new()\n $_stepIdx = 0\n while ($_stepIdx -lt $_steps.Count) {\n $_stepFn = $_steps[$_stepIdx]\n if (Test-ShouldSkipStep $_stepFn) {\n $_stepIdx++\n continue\n }\n if ($_stepHistory.Count -gt 0) {\n Write-Log (Get-Msg \"nav.back_hint\")\n }\n $script:StepResult = \"\"\n & $_stepFn\n if ($script:StepResult -eq \"back\") {\n if ($_stepHistory.Count -gt 0) {\n $_stepIdx = $_stepHistory[$_stepHistory.Count - 1]\n $_stepHistory.RemoveAt($_stepHistory.Count - 1)\n Clear-StepVars $_steps[$_stepIdx]\n }\n # else: first step, ignore 'b'\n } else {\n $_stepHistory.Add($_stepIdx)\n $_stepIdx++\n }\n }\n # ── End state machine ──────────────────────────────────────────────────────\n\n # Post-machine defaults for any steps that were skipped\n if (-not $script:config.DATA_DIR) {\n $script:config.DATA_DIR = if ($env:AGENTTEAMS_DATA_DIR) { $env:AGENTTEAMS_DATA_DIR } else { \"agentteams-data\" }\n }\n Write-Log (Get-Msg \"data.volume_using\" -f $script:config.DATA_DIR)\n if (-not $script:config.WORKSPACE_DIR) {\n $_defaultWs = \"$env:USERPROFILE\\agentteams-manager\"\n $script:config.WORKSPACE_DIR = if ($env:AGENTTEAMS_WORKSPACE_DIR) { $env:AGENTTEAMS_WORKSPACE_DIR } else { $_defaultWs }\n if (-not (Test-Path $script:config.WORKSPACE_DIR)) {\n New-Item -ItemType Directory -Path $script:config.WORKSPACE_DIR -Force | Out-Null\n }\n Write-Log (Get-Msg \"workspace.dir_label\" -f $script:config.WORKSPACE_DIR)\n }\n if (-not $script:config.DEFAULT_WORKER_RUNTIME) {\n $script:config.DEFAULT_WORKER_RUNTIME = if ($env:AGENTTEAMS_DEFAULT_WORKER_RUNTIME) { $env:AGENTTEAMS_DEFAULT_WORKER_RUNTIME } else { \"copaw\" }\n Write-Log (Get-Msg \"worker_runtime.selected\" -f $script:config.DEFAULT_WORKER_RUNTIME)\n }\n if (-not $script:config.MATRIX_E2EE) {\n $script:config.MATRIX_E2EE = if ($env:AGENTTEAMS_MATRIX_E2EE) { $env:AGENTTEAMS_MATRIX_E2EE } else { \"0\" }\n }\n if (-not $script:config.WORKER_IDLE_TIMEOUT) {\n $script:config.WORKER_IDLE_TIMEOUT = if ($env:AGENTTEAMS_WORKER_IDLE_TIMEOUT) { $env:AGENTTEAMS_WORKER_IDLE_TIMEOUT } else { \"720\" }\n }\n if (-not $script:config.HOST_SHARE_DIR) {\n $script:config.HOST_SHARE_DIR = if ($env:AGENTTEAMS_HOST_SHARE_DIR) { $env:AGENTTEAMS_HOST_SHARE_DIR } else { $env:USERPROFILE }\n }\n if (-not $script:config.LOCAL_ONLY) {\n $script:config.LOCAL_ONLY = if ($env:AGENTTEAMS_LOCAL_ONLY) { $env:AGENTTEAMS_LOCAL_ONLY } else { \"1\" }\n }\n if (-not $script:config.MANAGER_RUNTIME) {\n $script:config.MANAGER_RUNTIME = if ($env:AGENTTEAMS_MANAGER_RUNTIME) { $env:AGENTTEAMS_MANAGER_RUNTIME } else { \"copaw\" }\n }\n if (-not $script:config.PORT_GATEWAY) {\n $script:config.PORT_GATEWAY = if ($env:AGENTTEAMS_PORT_GATEWAY) { $env:AGENTTEAMS_PORT_GATEWAY } else { \"18080\" }\n $script:config.PORT_CONSOLE = if ($env:AGENTTEAMS_PORT_CONSOLE) { $env:AGENTTEAMS_PORT_CONSOLE } else { \"18001\" }\n $script:config.PORT_ELEMENT_WEB = if ($env:AGENTTEAMS_PORT_ELEMENT_WEB) { $env:AGENTTEAMS_PORT_ELEMENT_WEB } else { \"18088\" }\n $script:config.PORT_MANAGER_CONSOLE = if ($env:AGENTTEAMS_PORT_MANAGER_CONSOLE) { $env:AGENTTEAMS_PORT_MANAGER_CONSOLE } else { \"18888\" }\n }\n if (-not $script:config.MATRIX_DOMAIN) {\n $script:config.MATRIX_DOMAIN = if ($env:AGENTTEAMS_MATRIX_DOMAIN) { $env:AGENTTEAMS_MATRIX_DOMAIN } else { \"matrix-local.agentteams.io:$($script:config.PORT_GATEWAY)\" }\n $script:config.MATRIX_CLIENT_DOMAIN = if ($env:AGENTTEAMS_MATRIX_CLIENT_DOMAIN) { $env:AGENTTEAMS_MATRIX_CLIENT_DOMAIN } else { \"matrix-client-local.agentteams.io\" }\n $script:config.AI_GATEWAY_DOMAIN = if ($env:AGENTTEAMS_AI_GATEWAY_DOMAIN) { $env:AGENTTEAMS_AI_GATEWAY_DOMAIN } else { \"aigw-local.agentteams.io\" }\n $script:config.FS_DOMAIN = if ($env:AGENTTEAMS_FS_DOMAIN) { $env:AGENTTEAMS_FS_DOMAIN } else { \"fs-local.agentteams.io\" }\n $script:config.CONSOLE_DOMAIN = if ($env:AGENTTEAMS_CONSOLE_DOMAIN) { $env:AGENTTEAMS_CONSOLE_DOMAIN } else { \"console-local.agentteams.io\" }\n }\n\n Write-Log \"\"\n\n # Generate secrets\n Write-Log (Get-Msg \"install.generating_secrets\")\n $config.MANAGER_PASSWORD = if ($env:AGENTTEAMS_MANAGER_PASSWORD) { $env:AGENTTEAMS_MANAGER_PASSWORD } else { New-RandomKey }\n $config.REGISTRATION_TOKEN = if ($env:AGENTTEAMS_REGISTRATION_TOKEN) { $env:AGENTTEAMS_REGISTRATION_TOKEN } else { New-RandomKey }\n $config.MINIO_USER = if ($env:AGENTTEAMS_MINIO_USER) { $env:AGENTTEAMS_MINIO_USER } else { $config.ADMIN_USER }\n $config.MINIO_PASSWORD = if ($env:AGENTTEAMS_MINIO_PASSWORD) { $env:AGENTTEAMS_MINIO_PASSWORD } else { $config.ADMIN_PASSWORD }\n $config.MANAGER_GATEWAY_KEY = if ($env:AGENTTEAMS_MANAGER_GATEWAY_KEY) { $env:AGENTTEAMS_MANAGER_GATEWAY_KEY } else { New-RandomKey }\n\n # Store additional config\n $config.LANGUAGE = $script:AGENTTEAMS_LANGUAGE\n $config.REGISTRY = $script:AGENTTEAMS_REGISTRY\n $config.WORKER_IMAGE = $script:WORKER_IMAGE\n $config.COPAW_WORKER_IMAGE = $script:COPAW_WORKER_IMAGE\n $config.HERMES_WORKER_IMAGE = $script:HERMES_WORKER_IMAGE\n $config.MANAGER_COPAW_IMAGE = $script:MANAGER_COPAW_IMAGE\n\n # Write env file\n New-EnvFile -Config $config -Path $script:AGENTTEAMS_ENV_FILE\n\n # Manager image selection (used by both embedded — passed to controller via env —\n # and legacy — used directly as `docker run` target).\n $managerImage = if ($config.MANAGER_RUNTIME -eq \"copaw\") { $script:MANAGER_COPAW_IMAGE } else { $script:MANAGER_IMAGE }\n $portPrefix = if ($config.LOCAL_ONLY -eq \"1\") { \"127.0.0.1:\" } else { \"\" }\n\n # Ensure agentteams-net Docker network exists. Used in both modes — the embedded\n # controller and the spawned manager/worker containers all join it and rely on\n # network aliases for *-local.agentteams.io DNS resolution.\n if ($script:AGENTTEAMS_MOUNT_SOCKET) {\n $socketAvailable = Test-DockerRunning\n if ($socketAvailable) {\n docker network inspect agentteams-net *>$null\n if ($LASTEXITCODE -ne 0) { docker network create agentteams-net *>$null }\n } else {\n Write-Log (Get-Msg \"install.socket_not_found\")\n if (-not $script:AGENTTEAMS_NON_INTERACTIVE) {\n Write-Host \"\"\n Write-Host \"$($script:ESC)[33m$(Get-Msg 'install.socket_confirm.title')$($script:ESC)[0m\"\n Write-Host \"\"\n Write-Host (Get-Msg 'install.socket_confirm.message')\n Write-Host \"\"\n $confirm = Read-Host (Get-Msg 'install.socket_confirm.prompt')\n if ($confirm -ne 'y' -and $confirm -ne 'Y') {\n Write-Log (Get-Msg 'install.socket_confirm.cancelled')\n exit 0\n }\n }\n }\n }\n\n if ($script:AGENTTEAMS_USE_EMBEDDED -ne \"1\") {\n # ============================================================\n # Legacy architecture: all-in-one manager container\n # (only entered when AGENTTEAMS_FORCE_LEGACY=1 — broken with the slim manager\n # image shipped since PR #616; kept solely for AGENTTEAMS_VERSION <= v1.0.9)\n # ============================================================\n\n $dockerArgs = @(\n \"run\", \"-d\",\n \"--name\", \"agentteams-manager\",\n \"--env-file\", $script:AGENTTEAMS_ENV_FILE,\n \"-e\", \"HOME=/root/manager-workspace\",\n \"-w\", \"/root/manager-workspace\",\n \"-e\", \"HOST_ORIGINAL_HOME=$($config.HOST_SHARE_DIR)\",\n \"-e\", \"AGENTTEAMS_MANAGER_RUNTIME=$($config.MANAGER_RUNTIME)\"\n )\n\n $dockerArgs += @(\"-e\", \"TZ=$($script:AGENTTEAMS_TIMEZONE)\")\n\n if ($script:AGENTTEAMS_MOUNT_SOCKET -and (Test-DockerRunning)) {\n $dockerArgs += @(\"--network\", \"agentteams-net\")\n $dockerArgs += @(\"--network-alias\", \"matrix-local.agentteams.io\")\n $dockerArgs += @(\"--network-alias\", \"aigw-local.agentteams.io\")\n $dockerArgs += @(\"--network-alias\", \"fs-local.agentteams.io\")\n foreach ($domain in @($config.MATRIX_CLIENT_DOMAIN, $config.CONSOLE_DOMAIN)) {\n if ($domain -match '-local\\.hiclaw\\.io$') {\n $dockerArgs += @(\"--network-alias\", $domain)\n }\n }\n\n if ($config.DOCKER_PROXY -eq \"1\") {\n $proxyImage = $script:CONTROLLER_IMAGE\n Write-Log \"Starting Docker API proxy...\"\n docker rm -f agentteams-controller *>$null\n docker run -d --name agentteams-controller `\n --network agentteams-net `\n -v \"//var/run/docker.sock:/var/run/docker.sock\" `\n --security-opt label=disable `\n -e \"AGENTTEAMS_WORKER_IMAGE=$($script:WORKER_IMAGE)\" `\n -e \"AGENTTEAMS_COPAW_WORKER_IMAGE=$($script:COPAW_WORKER_IMAGE)\" `\n -e \"AGENTTEAMS_HERMES_WORKER_IMAGE=$($script:HERMES_WORKER_IMAGE)\" `\n -e \"AGENTTEAMS_DEFAULT_WORKER_RUNTIME=$($script:config.DEFAULT_WORKER_RUNTIME)\" `\n $(if ($config.PROXY_ALLOWED_REGISTRIES) { @(\"-e\", \"AGENTTEAMS_PROXY_ALLOWED_REGISTRIES=$($config.PROXY_ALLOWED_REGISTRIES)\") }) `\n --restart unless-stopped `\n $proxyImage\n $dockerArgs += @(\"-e\", \"AGENTTEAMS_CONTROLLER_URL=http://agentteams-controller:8090\")\n $dockerArgs += @(\"-e\", \"AGENTTEAMS_CONTAINER_API=http://agentteams-controller:8090\")\n Write-Log (Get-Msg \"docker_proxy.selected_enabled\")\n } else {\n $dockerArgs += @(\"-v\", \"//var/run/docker.sock:/var/run/docker.sock\")\n $dockerArgs += @(\"--security-opt\", \"label=disable\")\n Write-Log (Get-Msg \"install.socket_detected\" -f \"//var/run/docker.sock\")\n }\n }\n\n $dockerArgs += @(\"-p\", \"${portPrefix}$($config.PORT_GATEWAY):8080\")\n $dockerArgs += @(\"-p\", \"${portPrefix}$($config.PORT_CONSOLE):8001\")\n $dockerArgs += @(\"-p\", \"${portPrefix}$($config.PORT_ELEMENT_WEB):8088\")\n $dockerArgs += @(\"-p\", \"127.0.0.1:$($config.PORT_MANAGER_CONSOLE):18888\")\n\n $dockerArgs += @(\"-v\", \"$($config.DATA_DIR):/data\")\n\n $wsDockerPath = ConvertTo-DockerPath -Path $config.WORKSPACE_DIR\n $dockerArgs += @(\"-v\", \"${wsDockerPath}:/root/manager-workspace\")\n\n $shareDockerPath = ConvertTo-DockerPath -Path $config.HOST_SHARE_DIR\n $dockerArgs += @(\"-v\", \"${shareDockerPath}:/host-share\")\n Write-Log (Get-Msg \"host_share.sharing\" -f $config.HOST_SHARE_DIR)\n\n if ($env:AGENTTEAMS_YOLO -eq \"1\") {\n $dockerArgs += @(\"-e\", \"AGENTTEAMS_YOLO=1\")\n Write-Log (Get-Msg \"install.yolo\")\n }\n\n if ($env:AGENTTEAMS_MATRIX_DEBUG -eq \"1\") {\n $dockerArgs += @(\"-e\", \"AGENTTEAMS_MATRIX_DEBUG=1\")\n }\n\n $dockerArgs += @(\"--restart\", \"unless-stopped\")\n $dockerArgs += $managerImage\n }\n\n # Check if the Docker volume exists; create if not (reuse on reinstall) — both modes.\n $volumeExists = docker volume ls -q 2>$null | Select-String \"^$($config.DATA_DIR)$\"\n if (-not $volumeExists) {\n docker volume create $config.DATA_DIR | Out-Null\n }\n\n # Pull images (skip if already exists locally for local build tags).\n $LocalImagePattern = \"^(hiclaw|agentteams)/\"\n function Resolve-LegacyLocalImage {\n param([string]$Image)\n switch -Regex ($Image) {\n '^agentteams/manager:(.+)$' { return \"hiclaw/hiclaw-manager:$($Matches[1])\" }\n '^agentteams/manager-copaw:(.+)$' { return \"hiclaw/hiclaw-manager-copaw:$($Matches[1])\" }\n '^agentteams/worker-agent:(.+)$' { return \"hiclaw/worker-agent:$($Matches[1])\" }\n '^agentteams/copaw-worker:(.+)$' { return \"hiclaw/copaw-worker:$($Matches[1])\" }\n '^agentteams/hermes-worker:(.+)$' { return \"hiclaw/hermes-worker:$($Matches[1])\" }\n '^agentteams/qwenpaw-worker:(.+)$' { return \"hiclaw/qwenpaw-worker:$($Matches[1])\" }\n '^agentteams/agentteams-embedded:(.+)$' { return \"hiclaw/hiclaw-embedded:$($Matches[1])\" }\n '^agentteams/agentteams-controller:(.+)$' { return \"hiclaw/hiclaw-controller:$($Matches[1])\" }\n default { return $null }\n }\n }\n function Test-OrTagLocalImage {\n param([string]$Image)\n $imgExists = docker image inspect $Image 2>$null\n if ($LASTEXITCODE -eq 0) {\n return $true\n }\n $legacyImage = Resolve-LegacyLocalImage $Image\n if ($legacyImage) {\n $legacyExists = docker image inspect $legacyImage 2>$null\n if ($LASTEXITCODE -eq 0) {\n docker tag $legacyImage $Image\n return $true\n }\n }\n return $false\n }\n if ($script:AGENTTEAMS_USE_EMBEDDED -eq \"1\") {\n # Embedded image was already pulled by Resolve-EmbeddedImage unless overridden;\n # for an explicit override we still need to ensure it is present locally.\n if ($env:AGENTTEAMS_INSTALL_EMBEDDED_IMAGE) {\n if ($script:EMBEDDED_IMAGE -match $LocalImagePattern) {\n if (-not (Test-OrTagLocalImage $script:EMBEDDED_IMAGE)) {\n Write-Log \"Pulling embedded image: $($script:EMBEDDED_IMAGE)\"\n & docker pull $script:EMBEDDED_IMAGE\n }\n } else {\n Write-Log \"Pulling embedded image: $($script:EMBEDDED_IMAGE)\"\n & docker pull $script:EMBEDDED_IMAGE\n }\n }\n # Manager image — controller will spawn it inside; pull here so the spawn doesn't\n # have to wait on the network.\n if ($managerImage -match $LocalImagePattern) {\n if (Test-OrTagLocalImage $managerImage) {\n Write-Log (Get-Msg \"install.image.exists\" -f $managerImage)\n } else {\n Write-Log (Get-Msg \"install.image.pulling_manager\" -f $managerImage)\n & docker pull $managerImage\n }\n } else {\n Write-Log (Get-Msg \"install.image.pulling_manager\" -f $managerImage)\n & docker pull $managerImage\n }\n } else {\n if ($managerImage -match $LocalImagePattern) {\n if (Test-OrTagLocalImage $managerImage) {\n Write-Log (Get-Msg \"install.image.exists\" -f $managerImage)\n } else {\n Write-Log (Get-Msg \"install.image.pulling_manager\" -f $managerImage)\n & docker pull $managerImage\n }\n } else {\n Write-Log (Get-Msg \"install.image.pulling_manager\" -f $managerImage)\n & docker pull $managerImage\n }\n }\n\n # Pull all worker runtime images (workers may use any runtime regardless of the default)\n foreach ($workerImg in @($script:WORKER_IMAGE, $script:COPAW_WORKER_IMAGE, $script:HERMES_WORKER_IMAGE)) {\n if ($workerImg -match $LocalImagePattern) {\n if (Test-OrTagLocalImage $workerImg) {\n Write-Log (Get-Msg \"install.image.worker_exists\" -f $workerImg)\n } else {\n Write-Log (Get-Msg \"install.image.pulling_worker\" -f $workerImg)\n & docker pull $workerImg\n }\n } else {\n Write-Log (Get-Msg \"install.image.pulling_worker\" -f $workerImg)\n & docker pull $workerImg\n }\n }\n\n # --- Pre-upgrade: extract Matrix passwords from old containers (old-arch -> embedded) ---\n $credsTmp = $null\n if ($script:AGENTTEAMS_UPGRADE -and $script:AGENTTEAMS_USE_EMBEDDED -eq \"1\") {\n $controllerNameHit = docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-controller$\"\n $isOldArch = $false\n if (-not $controllerNameHit) {\n $isOldArch = $true\n } else {\n $ctrlImgLine = docker ps -a --format \"{{.Names}} {{.Image}}\" 2>$null | Where-Object { $_ -match '^agentteams-controller ' } | Select-Object -First 1\n if ($ctrlImgLine -and ($ctrlImgLine -notmatch 'embedded')) {\n $isOldArch = $true\n }\n }\n\n if ($isOldArch) {\n $credsTmp = Join-Path ([System.IO.Path]::GetTempPath()) (\"agentteams-upgrade-creds-\" + [Guid]::NewGuid().ToString(\"n\"))\n New-Item -ItemType Directory -Path $credsTmp -Force | Out-Null\n $utf8NoBom = New-Object System.Text.UTF8Encoding $false\n\n $mgrCredsTempStart = $false\n $mgrExists = docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-manager$\"\n $mgrRunning = docker ps --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-manager$\"\n if ($mgrExists -and -not $mgrRunning) {\n Write-Log \"agentteams-manager is stopped; starting it temporarily to extract Matrix credentials for upgrade...\"\n docker start agentteams-manager 2>$null | Out-Null\n Wait-MatrixReady -Container \"agentteams-manager\"\n $mgrCredsTempStart = $true\n }\n\n $inspectLines = docker inspect agentteams-manager --format \"{{range .Config.Env}}{{println .}}{{end}}\" 2>$null\n $mgrPw = \"\"\n if ($inspectLines) {\n $envLine = $inspectLines -split \"`n\" | Where-Object { $_ -match '^AGENTTEAMS_MANAGER_PASSWORD=' } | Select-Object -First 1\n if ($envLine) {\n $mgrPw = ($envLine -replace '^AGENTTEAMS_MANAGER_PASSWORD=', \"\").Trim()\n }\n }\n $mgrRunningNow = docker ps --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-manager$\"\n if ([string]::IsNullOrEmpty($mgrPw) -and $mgrRunningNow) {\n $mgrPw = docker exec agentteams-manager bash -c 'source /data/agentteams-secrets.env 2>/dev/null && echo \"${AGENTTEAMS_MANAGER_PASSWORD}\"' 2>$null\n if ($mgrPw) { $mgrPw = $mgrPw.Trim() }\n }\n $dataVolPresent = docker volume ls -q 2>$null | Where-Object { $_ -eq $config.DATA_DIR }\n if ([string]::IsNullOrEmpty($mgrPw) -and $dataVolPresent) {\n $mgrPw = Read-AgentTeamsSecretFromDataVolume -VolumeName $config.DATA_DIR -Key \"AGENTTEAMS_MANAGER_PASSWORD\"\n }\n\n $envFilePath = $script:AGENTTEAMS_ENV_FILE\n $mgrRoom = \"\"\n if (-not [string]::IsNullOrEmpty($mgrPw)) {\n $adminPw = \"\"\n $adminUser = \"admin\"\n if (Test-Path $envFilePath) {\n $adminLine = Get-Content $envFilePath | Where-Object { $_ -match '^AGENTTEAMS_ADMIN_PASSWORD=' } | Select-Object -First 1\n if ($adminLine) { $adminPw = ($adminLine -replace '^AGENTTEAMS_ADMIN_PASSWORD=', \"\").Trim() }\n $userLine = Get-Content $envFilePath | Where-Object { $_ -match '^AGENTTEAMS_ADMIN_USER=' } | Select-Object -First 1\n if ($userLine) {\n $adminUser = ($userLine -replace '^AGENTTEAMS_ADMIN_USER=', \"\").Trim()\n if ([string]::IsNullOrEmpty($adminUser)) { $adminUser = \"admin\" }\n }\n }\n\n if (-not [string]::IsNullOrEmpty($adminPw) -and $mgrRunningNow) {\n $mgrRoom = Get-AgentTeamsAdminDmRoomViaMatrix -AdminUser $adminUser -AdminPassword $adminPw\n }\n if ([string]::IsNullOrEmpty($mgrRoom)) {\n $mgrRoom = Read-AgentTeamsAdminDmRoomFromWorkspace -WorkspaceDir $config.WORKSPACE_DIR\n }\n\n $gwKeyLine = if (Test-Path $envFilePath) {\n Get-Content $envFilePath | Where-Object { $_ -match '^AGENTTEAMS_MANAGER_GATEWAY_KEY=' } | Select-Object -First 1\n } else { $null }\n $gwKeyForDefault = if ($gwKeyLine) { ($gwKeyLine -replace '^AGENTTEAMS_MANAGER_GATEWAY_KEY=', \"\").Trim() } else { $config.MANAGER_GATEWAY_KEY }\n\n $defaultEnvPath = Join-Path $credsTmp \"default.env\"\n [System.IO.File]::WriteAllLines($defaultEnvPath, @(\n \"WORKER_PASSWORD=$mgrPw\"\n \"WORKER_MINIO_PASSWORD=$(Get-AgentTeamsRandomHex 24)\"\n \"WORKER_GATEWAY_KEY=$gwKeyForDefault\"\n \"WORKER_ROOM_ID=$mgrRoom\"\n ), $utf8NoBom)\n if ($mgrRoom) {\n Write-Log \"Extracted Manager Matrix password and room ID\"\n } else {\n Write-Log \"Extracted Manager Matrix password\"\n }\n }\n\n $registryPath = Join-Path $config.WORKSPACE_DIR \"workers-registry.json\"\n if (Test-Path $registryPath) {\n try {\n $wreg = Get-Content $registryPath -Raw | ConvertFrom-Json\n $workerNames = @()\n if ($null -ne $wreg.workers) {\n $wreg.workers.PSObject.Properties | ForEach-Object { $workerNames += $_.Name }\n }\n foreach ($wname in $workerNames) {\n $wpw = \"\"\n if ($mgrRunningNow) {\n $wpw = docker exec agentteams-manager cat \"/root/agentteams-fs/agents/${wname}/credentials/matrix/password\" 2>$null\n if ($wpw) { $wpw = $wpw.Trim() }\n }\n if ([string]::IsNullOrEmpty($wpw) -and $dataVolPresent) {\n $wpw = Read-AgentTeamsWorkerCredsFromVolume -VolumeName $config.DATA_DIR -WorkerName $wname -Key \"WORKER_PASSWORD\"\n }\n $wroom = \"\"\n $wEntry = $wreg.workers.$wname\n if ($wEntry -and $wEntry.room_id) {\n $wroom = [string]$wEntry.room_id\n }\n if ([string]::IsNullOrEmpty($wroom) -and $dataVolPresent) {\n $wroom = Read-AgentTeamsWorkerCredsFromVolume -VolumeName $config.DATA_DIR -WorkerName $wname -Key \"WORKER_ROOM_ID\"\n }\n if (-not [string]::IsNullOrEmpty($wpw)) {\n $wEnvPath = Join-Path $credsTmp \"${wname}.env\"\n [System.IO.File]::WriteAllLines($wEnvPath, @(\n \"WORKER_PASSWORD=$wpw\"\n \"WORKER_MINIO_PASSWORD=$(Get-AgentTeamsRandomHex 24)\"\n \"WORKER_GATEWAY_KEY=$(Get-AgentTeamsRandomHex 32)\"\n \"WORKER_ROOM_ID=$wroom\"\n ), $utf8NoBom)\n if ($wroom) {\n Write-Log \"Extracted ${wname} Matrix password and room ID\"\n } else {\n Write-Log \"Extracted ${wname} Matrix password\"\n }\n }\n }\n } catch {\n Write-Log \"Warning: could not read workers-registry.json for credential extraction\"\n }\n }\n\n if ($mgrCredsTempStart) {\n Write-Log \"Stopping agentteams-manager after credential extraction (upgrade will recreate containers)...\"\n docker stop agentteams-manager 2>$null | Out-Null\n }\n }\n }\n\n # Stop and remove existing containers (deferred until after all\n # configuration is collected and images are pulled successfully)\n $existingProxy = docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-controller$\"\n if ($existingProxy) {\n docker stop agentteams-controller *>$null\n docker rm agentteams-controller *>$null\n }\n $existingContainer = docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-manager$\"\n if ($existingContainer) {\n Write-Log (Get-Msg \"install.removing_existing\")\n docker stop agentteams-manager *>$null\n docker rm agentteams-manager *>$null\n }\n\n # Stop and remove worker containers (controller will recreate via CR reconciliation)\n if ($script:UPGRADE_EXISTING_WORKERS) {\n Write-Log (Get-Msg \"install.existing.stopping_workers\")\n $script:UPGRADE_EXISTING_WORKERS | ForEach-Object {\n docker stop $_ *>$null\n docker rm $_ *>$null\n Write-Log (Get-Msg \"install.existing.removed\" -f $_)\n }\n }\n\n # Clean up legacy containers (e.g. hiclaw-docker-proxy from v1.0.x)\n $legacyContainers = docker ps -a --format \"{{.Names}}\" 2>$null |\n Select-String \"^hiclaw-\" |\n Where-Object { $_.Line -notmatch \"^(agentteams-controller|agentteams-manager|agentteams-worker-)\" }\n foreach ($legacy in $legacyContainers) {\n Write-Log \"Removing legacy container: $($legacy.Line)\"\n docker stop $legacy.Line *>$null\n docker rm -f $legacy.Line *>$null\n }\n\n # --- Upgrade: inject extracted credentials into data volume (old-arch -> embedded) ---\n if ($credsTmp -and (Test-Path $credsTmp)) {\n $credsFiles = Get-ChildItem -Path $credsTmp -Filter \"*.env\" -ErrorAction SilentlyContinue\n if ($credsFiles -and $credsFiles.Count -gt 0) {\n $cleanupCtr = \"agentteams-upgrade-cleanup\"\n docker rm -f $cleanupCtr 2>$null | Out-Null\n $credsDockerPath = ConvertTo-DockerPath -Path $credsTmp\n $injectShell = \"rm -rf /data/worker-creds && mkdir -p /data/worker-creds && cp /creds/*.env /data/worker-creds/ 2>/dev/null || true && chmod 600 /data/worker-creds/*.env 2>/dev/null || true\"\n docker run --rm --name $cleanupCtr --entrypoint sh `\n -v \"$($config.DATA_DIR):/data\" `\n -v \"${credsDockerPath}:/creds:ro\" `\n $script:EMBEDDED_IMAGE `\n -c $injectShell 2>$null | Out-Null\n if ($LASTEXITCODE -eq 0) {\n Write-Log \"Injected credentials for upgrade\"\n } else {\n Write-Log \"Warning: credential injection failed, continuing\"\n }\n }\n Remove-Item -Path $credsTmp -Recurse -Force -ErrorAction SilentlyContinue\n }\n\n if ($script:AGENTTEAMS_USE_EMBEDDED -eq \"1\") {\n # ============================================================\n # New architecture: embedded controller + auto-created manager\n # (controller container hosts Higress / Tuwunel / MinIO / Element Web /\n # controller binary, then spawns the lightweight manager container)\n # ============================================================\n\n # Internal port: 8080 (Higress gateway inside the controller container).\n $internalGwPort = 8080\n\n $matrixDomain = if ($config.MATRIX_DOMAIN) {\n $config.MATRIX_DOMAIN\n } else {\n \"matrix-local.agentteams.io:$($config.PORT_GATEWAY)\"\n }\n $aigwDomain = if ($config.AI_GATEWAY_DOMAIN) { $config.AI_GATEWAY_DOMAIN } else { \"aigw-local.agentteams.io\" }\n if ($aigwDomain -notmatch \":\") { $aigwDomain = \"${aigwDomain}:${internalGwPort}\" }\n $fsDomain = if ($config.FS_DOMAIN) { $config.FS_DOMAIN } else { \"fs-local.agentteams.io\" }\n if ($fsDomain -notmatch \":\") { $fsDomain = \"${fsDomain}:${internalGwPort}\" }\n\n $ctrlArgs = @(\n \"run\", \"-d\",\n \"--name\", \"agentteams-controller\",\n \"--network\", \"agentteams-net\",\n \"--network-alias\", \"matrix-local.agentteams.io\",\n \"--network-alias\", \"aigw-local.agentteams.io\",\n \"--network-alias\", \"fs-local.agentteams.io\",\n \"-e\", \"AGENTTEAMS_ADMIN_USER=$($config.ADMIN_USER)\",\n \"-e\", \"AGENTTEAMS_ADMIN_PASSWORD=$($config.ADMIN_PASSWORD)\",\n \"-e\", \"AGENTTEAMS_MANAGER_PASSWORD=$($config.MANAGER_PASSWORD)\",\n \"-e\", \"AGENTTEAMS_REGISTRATION_TOKEN=$($config.REGISTRATION_TOKEN)\",\n \"-e\", \"AGENTTEAMS_MINIO_USER=$($config.MINIO_USER)\",\n \"-e\", \"AGENTTEAMS_MINIO_PASSWORD=$($config.MINIO_PASSWORD)\",\n \"-e\", \"AGENTTEAMS_LLM_PROVIDER=$($config.LLM_PROVIDER)\",\n \"-e\", \"AGENTTEAMS_LLM_API_KEY=$($config.LLM_API_KEY)\",\n \"-e\", \"AGENTTEAMS_DEFAULT_MODEL=$($config.DEFAULT_MODEL)\",\n \"-e\", \"AGENTTEAMS_MANAGER_GATEWAY_KEY=$($config.MANAGER_GATEWAY_KEY)\",\n \"-e\", \"AGENTTEAMS_MANAGER_RUNTIME=$($config.MANAGER_RUNTIME)\",\n \"-e\", \"AGENTTEAMS_MANAGER_IMAGE=$managerImage\",\n \"-e\", \"AGENTTEAMS_DEFAULT_WORKER_RUNTIME=$($config.DEFAULT_WORKER_RUNTIME)\",\n \"-e\", \"AGENTTEAMS_WORKER_IMAGE=$($script:WORKER_IMAGE)\",\n \"-e\", \"AGENTTEAMS_COPAW_WORKER_IMAGE=$($script:COPAW_WORKER_IMAGE)\",\n \"-e\", \"AGENTTEAMS_HERMES_WORKER_IMAGE=$($script:HERMES_WORKER_IMAGE)\",\n \"-e\", \"AGENTTEAMS_MATRIX_DOMAIN=$matrixDomain\",\n \"-e\", \"AGENTTEAMS_ELEMENT_HOMESERVER_URL=http://127.0.0.1:$($config.PORT_GATEWAY)\",\n \"-e\", \"AGENTTEAMS_MATRIX_URL=http://127.0.0.1:6167\",\n \"-e\", \"AGENTTEAMS_MATRIX_E2EE=$($config.MATRIX_E2EE)\",\n \"-e\", \"AGENTTEAMS_MINIO_ENDPOINT=http://127.0.0.1:9000\",\n \"-e\", \"AGENTTEAMS_MINIO_BUCKET=agentteams-storage\",\n \"-e\", \"AGENTTEAMS_STORAGE_PREFIX=agentteams/agentteams-storage\",\n \"-e\", \"AGENTTEAMS_FS_ENDPOINT=http://127.0.0.1:9000\",\n \"-e\", \"AGENTTEAMS_AI_GATEWAY_URL=http://$aigwDomain\",\n \"-e\", \"AGENTTEAMS_CONTROLLER_URL=http://agentteams-controller:8090\",\n \"-e\", \"AGENTTEAMS_DOCKER_NETWORK=agentteams-net\",\n \"-e\", \"AGENTTEAMS_WORKSPACE_DIR=$($config.WORKSPACE_DIR)\",\n \"-e\", \"AGENTTEAMS_HOST_SHARE_DIR=$($config.HOST_SHARE_DIR)\",\n \"-e\", \"AGENTTEAMS_MANAGER_ENABLED=true\",\n \"-e\", \"AGENTTEAMS_PORT_MANAGER_CONSOLE=$($config.PORT_MANAGER_CONSOLE)\"\n )\n\n if ($script:AGENTTEAMS_TIMEZONE) {\n $ctrlArgs += @(\"-e\", \"TZ=$($script:AGENTTEAMS_TIMEZONE)\")\n }\n if ($env:AGENTTEAMS_YOLO -eq \"1\") {\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_YOLO=1\")\n }\n if ($env:AGENTTEAMS_MATRIX_DEBUG -eq \"1\") {\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_MATRIX_DEBUG=1\")\n }\n if ($config.GITHUB_TOKEN) {\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_GITHUB_TOKEN=$($config.GITHUB_TOKEN)\")\n }\n if ($config.EMBEDDING_MODEL) {\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_EMBEDDING_MODEL=$($config.EMBEDDING_MODEL)\")\n }\n if ($config.OPENAI_BASE_URL) {\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_OPENAI_BASE_URL=$($config.OPENAI_BASE_URL)\")\n }\n if ($env:AGENTTEAMS_AI_STREAM_IDLE_TIMEOUT_SECONDS) {\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_AI_STREAM_IDLE_TIMEOUT_SECONDS=$($env:AGENTTEAMS_AI_STREAM_IDLE_TIMEOUT_SECONDS)\")\n }\n if ($script:AGENTTEAMS_LANGUAGE) {\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_LANGUAGE=$($script:AGENTTEAMS_LANGUAGE)\")\n }\n\n # Optional: CMS/ARMS observability. In embedded mode the controller\n # spawns the Manager and Workers, so it must receive these settings.\n $cmsTracesEnabled = if ($env:AGENTTEAMS_CMS_TRACES_ENABLED) { $env:AGENTTEAMS_CMS_TRACES_ENABLED } else { \"false\" }\n $cmsServiceName = if ($env:AGENTTEAMS_CMS_SERVICE_NAME) { $env:AGENTTEAMS_CMS_SERVICE_NAME } else { \"agentteams-manager\" }\n $cmsMetricsEnabled = if ($env:AGENTTEAMS_CMS_METRICS_ENABLED) { $env:AGENTTEAMS_CMS_METRICS_ENABLED } else { \"false\" }\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_CMS_TRACES_ENABLED=$cmsTracesEnabled\")\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_CMS_SERVICE_NAME=$cmsServiceName\")\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_CMS_METRICS_ENABLED=$cmsMetricsEnabled\")\n if ($env:AGENTTEAMS_CMS_ENDPOINT) {\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_CMS_ENDPOINT=$($env:AGENTTEAMS_CMS_ENDPOINT)\")\n }\n if ($env:AGENTTEAMS_CMS_LICENSE_KEY) {\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_CMS_LICENSE_KEY=$($env:AGENTTEAMS_CMS_LICENSE_KEY)\")\n }\n if ($env:AGENTTEAMS_CMS_PROJECT) {\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_CMS_PROJECT=$($env:AGENTTEAMS_CMS_PROJECT)\")\n }\n if ($env:AGENTTEAMS_CMS_WORKSPACE) {\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_CMS_WORKSPACE=$($env:AGENTTEAMS_CMS_WORKSPACE)\")\n }\n\n # Mount the docker socket so the controller can spawn manager + workers.\n $ctrlArgs += @(\"-v\", \"//var/run/docker.sock:/var/run/docker.sock\")\n $ctrlArgs += @(\"--security-opt\", \"label=disable\")\n\n # Persistent data + workspace mounts (manager workspace is bind-mounted under\n # /root/agentteams-fs/agents/manager so the controller can hand it to the spawned\n # manager container).\n $ctrlArgs += @(\"-v\", \"$($config.DATA_DIR):/data\")\n $wsDockerPath = ConvertTo-DockerPath -Path $config.WORKSPACE_DIR\n $ctrlArgs += @(\"-v\", \"${wsDockerPath}:/root/agentteams-fs/agents/manager\")\n Write-Log (Get-Msg \"host_share.sharing\" -f $config.HOST_SHARE_DIR)\n\n # Externally exposed ports — only the gateway / Higress console / Element Web,\n # since the manager console is now spawned inside its own container by the\n # controller (port mapping is handled there).\n $ctrlArgs += @(\"-p\", \"${portPrefix}$($config.PORT_GATEWAY):8080\")\n $ctrlArgs += @(\"-p\", \"${portPrefix}$($config.PORT_CONSOLE):8001\")\n $ctrlArgs += @(\"-p\", \"${portPrefix}$($config.PORT_ELEMENT_WEB):8088\")\n\n $ctrlArgs += @(\"--restart\", \"unless-stopped\")\n $ctrlArgs += $script:EMBEDDED_IMAGE\n\n Write-Log (Get-Msg \"install.starting_manager\")\n & docker $ctrlArgs\n Write-Log \"Embedded controller started: agentteams-controller\"\n\n # Wait for infra inside the controller container.\n function Wait-EmbeddedUrl {\n param([string]$Url, [string]$Container, [int]$MaxWait, [string]$Description)\n $elapsed = 0\n Write-Log \"Waiting for $Description...\"\n while ($elapsed -lt $MaxWait) {\n docker exec $Container curl -sf $Url *>$null\n if ($LASTEXITCODE -eq 0) {\n Write-Log \"$Description is ready (${elapsed}s)\"\n return $true\n }\n Start-Sleep -Seconds 2\n $elapsed += 2\n }\n Write-Host \"$($script:ESC)[31m[AgentTeams ERROR]$($script:ESC)[0m $Description not ready after ${MaxWait}s\" -ForegroundColor Red\n return $false\n }\n\n if (-not (Wait-EmbeddedUrl \"http://127.0.0.1:6167/_tuwunel/server_version\" \"agentteams-controller\" 120 \"Tuwunel (Matrix)\")) { Exit-Script 1 }\n if (-not (Wait-EmbeddedUrl \"http://127.0.0.1:9000/minio/health/live\" \"agentteams-controller\" 60 \"MinIO\")) { Exit-Script 1 }\n if (-not (Wait-EmbeddedUrl \"http://127.0.0.1:8080/status\" \"agentteams-controller\" 120 \"Higress Gateway\")) { Exit-Script 1 }\n\n # Wait for the controller to spawn the Manager Agent container.\n Write-Log \"Waiting for Manager Agent container...\"\n $mgrWait = 0\n $mgrMax = 300\n while ($mgrWait -lt $mgrMax) {\n $found = docker ps --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-manager$\"\n if ($found) {\n Write-Log \"Manager Agent container detected (${mgrWait}s)\"\n break\n }\n Start-Sleep -Seconds 3\n $mgrWait += 3\n }\n if ($mgrWait -ge $mgrMax) {\n Write-Host \"$($script:ESC)[31m[AgentTeams ERROR]$($script:ESC)[0m Manager Agent container not created after ${mgrMax}s\" -ForegroundColor Red\n Write-Log \"Controller logs:\"\n docker exec agentteams-controller tail -30 /var/log/agentteams/agentteams-controller-error.log 2>$null\n Exit-Script 1\n }\n\n # Wait for Manager Agent container to reach `running` state.\n Write-Log \"Waiting for Manager Agent to start...\"\n $agentWait = 0\n while ($agentWait -lt 120) {\n $state = docker inspect --format \"{{.State.Status}}\" agentteams-manager 2>$null\n if ($state -eq \"running\") {\n Write-Log \"Manager Agent is running\"\n break\n }\n Start-Sleep -Seconds 2\n $agentWait += 2\n }\n\n if ($env:AGENTTEAMS_YOLO -eq \"1\") {\n docker exec agentteams-manager touch /root/manager-workspace/yolo-mode 2>$null\n }\n\n # Wait for the Manager Agent's runtime + Matrix to be reachable. In embedded\n # mode Tuwunel lives in `agentteams-controller`, so the Matrix probe must target\n # the controller (the manager only exposes the agent runtime).\n Wait-ManagerReady -Container \"agentteams-manager\"\n Wait-MatrixReady -Container \"agentteams-controller\"\n\n # Wait for the controller to actually deliver the first-boot welcome\n # message. Gated by the controller on (a) Manager joining the DM room\n # and (b) Higress WASM key-auth propagating to /v1/chat/completions for\n # the Manager's gateway key — typically ~45-90s on a fresh install.\n # We poll Manager CR status.welcomeSent via the in-container AgentTeams\n # CLI, exec'd inside agentteams-controller (the source-of-truth container\n # — its bundled CLI binary is always in lockstep with the controller\n # binary serving the HTTP API, since they're the same `go build`\n # output. The agentteams-manager container's CLI may lag the controller\n # across image upgrades and silently drop the welcomeSent field,\n # leaving this loop hung). The controller mints a long-lived admin\n # SA token at startup and writes it to\n # AGENTTEAMS_AUTH_TOKEN_FILE=/var/run/agentteams/cli-token (set as a Dockerfile\n # ENV default), so a bare `docker exec agentteams-controller agt …`\n # auto-discovers both the endpoint and the token. The brief window\n # after container start before bootstrapAdminCLIToken completes is\n # absorbed by the loop's silent retry.\n $hasAgentTeamsCli = $false\n try {\n docker exec agentteams-controller sh -c 'command -v agt' *> $null\n if ($LASTEXITCODE -eq 0) { $hasAgentTeamsCli = $true }\n } catch {}\n\n if ($hasAgentTeamsCli) {\n Write-Log (Get-Msg \"install.welcome_msg.waiting\")\n $welcomeMax = if ($env:AGENTTEAMS_WELCOME_TIMEOUT) { [int]$env:AGENTTEAMS_WELCOME_TIMEOUT } else { 300 }\n $welcomeWait = 0\n $welcomeDone = $false\n while ($welcomeWait -lt $welcomeMax) {\n $wjson = \"\"\n try {\n $wjson = docker exec agentteams-controller `\n agt get managers default -o json 2>$null\n } catch {}\n if ($wjson -and ($wjson -replace '\\s', '') -match '\"welcomeSent\":true') {\n Write-Log (Get-Msg \"install.welcome_msg.confirmed\" $welcomeWait)\n $welcomeDone = $true\n break\n }\n Start-Sleep -Seconds 3\n $welcomeWait += 3\n }\n if (-not $welcomeDone) {\n # Non-fatal: install is still good. Keep going to the success\n # banner so the admin can use Element Web to nudge Manager into\n # onboarding manually (one DM message is enough).\n Write-Log (Get-Msg \"install.welcome_msg.timeout\" $welcomeMax)\n Write-Log (Get-Msg \"install.welcome_msg.timeout_hint\")\n Write-Log (Get-Msg \"install.welcome_msg.timeout_inspect\")\n }\n } else {\n Write-Log (Get-Msg \"install.welcome_msg.poll_unavailable\")\n }\n } else {\n # Run container (legacy path)\n Write-Log (Get-Msg \"install.starting_manager\")\n & docker $dockerArgs\n\n Wait-ManagerReady -Container \"agentteams-manager\"\n Wait-MatrixReady -Container \"agentteams-manager\"\n }\n\n # Create OpenAI-compatible provider if needed\n if ($config.LLM_PROVIDER -eq \"openai-compat\") {\n New-OpenAICompatProvider -BaseUrl $config.OPENAI_BASE_URL -ApiKey $config.LLM_API_KEY -ConsolePort ([int]$config.PORT_CONSOLE)\n }\n\n # Print success message\n Write-Log \"\"\n Write-Log (Get-Msg \"success.title\")\n Write-Log \"\"\n Write-Log (Get-Msg \"success.domains_configured\")\n Write-Log \" $($config.MATRIX_DOMAIN.Split(':')[0]) $($config.MATRIX_CLIENT_DOMAIN) $($config.AI_GATEWAY_DOMAIN) $($config.FS_DOMAIN) $($config.CONSOLE_DOMAIN)\"\n Write-Log \"\"\n\n $lanIP = Get-LanIP\n\n Write-Host \"$($script:ESC)[33m===============================================================$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.open_url')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[1;36m http://127.0.0.1:$($config.PORT_ELEMENT_WEB)/#/login$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.login_with')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.username' -f $config.ADMIN_USER)$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.password' -f $config.ADMIN_PASSWORD)$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.after_login')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.tell_it')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.manager_auto')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m --------------------------------------------------------------- $($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.mobile_title')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $($script:ESC)[0m\"\n if ($lanIP) {\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.mobile_step1')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.mobile_step2' -f \"http://${lanIP}:$($config.PORT_GATEWAY)\")$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.mobile_step3')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.mobile_username' -f $config.ADMIN_USER)$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.mobile_password' -f $config.ADMIN_PASSWORD)$($script:ESC)[0m\"\n } else {\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.mobile_step1')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.mobile_step2_noip' -f $config.PORT_GATEWAY)$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.mobile_noip_hint')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.mobile_step3')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.mobile_username' -f $config.ADMIN_USER)$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.mobile_password' -f $config.ADMIN_PASSWORD)$($script:ESC)[0m\"\n }\n Write-Host \"$($script:ESC)[33m $($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m===============================================================$($script:ESC)[0m\"\n\n Write-Log \"\"\n Write-Log (Get-Msg \"success.other_consoles\")\n Write-Log (Get-Msg \"success.higress_console\" -f $config.PORT_CONSOLE, $config.ADMIN_USER, $config.ADMIN_PASSWORD)\n if ($script:AGENTTEAMS_USE_EMBEDDED -ne \"1\") {\n # In embedded mode the manager runs in its own auto-spawned container with\n # its own console-port mapping handled by the controller, so don't print a\n # host-side URL/credentials hint here.\n Write-Log (Get-Msg \"success.manager_console\" -f $config.PORT_MANAGER_CONSOLE)\n Write-Log (Get-Msg \"success.manager_console_gateway\" -f $config.ADMIN_USER, $config.ADMIN_PASSWORD)\n }\n Write-Log \"\"\n Write-Log (Get-Msg \"success.switch_llm.title\")\n Write-Log (Get-Msg \"success.switch_llm.hint\")\n Write-Log (Get-Msg \"success.switch_llm.docs\")\n Write-Log (Get-Msg \"success.switch_llm.url\")\n Write-Log \"\"\n Write-Log (Get-Msg \"success.tip\")\n Write-Log \"\"\n if ($config.LOCAL_ONLY -ne \"1\") {\n Write-Host (Get-Msg \"port.local_only.https_hint\") -ForegroundColor Yellow\n Write-Log \"\"\n }\n Write-Log (Get-Msg \"success.config_file\" -f $script:AGENTTEAMS_ENV_FILE)\n\n Write-Log (Get-Msg \"success.data_volume\" -f $config.DATA_DIR)\n\n Write-Log (Get-Msg \"success.workspace\" -f $config.WORKSPACE_DIR)\n}\n\n# ============================================================\n# Worker Installation\n# ============================================================\n\nfunction Install-Worker {\n param(\n [string]$Name,\n [string]$Fs,\n [string]$FsKey,\n [string]$FsSecret,\n [switch]$Reset,\n [switch]$FindSkills,\n [string]$SkillsApiUrl\n )\n\n # Validate required parameters\n if (-not $Name) {\n Write-Error (Get-Msg \"error.name_required\")\n }\n if (-not $Fs) {\n Write-Error (Get-Msg \"error.fs_required\")\n }\n if (-not $FsKey) {\n Write-Error (Get-Msg \"error.fs_key_required\")\n }\n if (-not $FsSecret) {\n Write-Error (Get-Msg \"error.fs_secret_required\")\n }\n\n $containerName = \"agentteams-worker-$Name\"\n\n # Handle reset\n if ($Reset) {\n Write-Log (Get-Msg \"worker.resetting\" -f $Name)\n docker stop $containerName *>$null\n docker rm $containerName *>$null\n }\n\n # Check for existing container\n $existing = docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^$containerName$\"\n if ($existing) {\n Write-Error (Get-Msg \"worker.exists\" -f $containerName)\n }\n\n # Detect timezone and registry\n $timezone = Get-AgentTeamsTimeZone\n $registry = Get-Registry -Timezone $timezone\n $workerImage = if ($env:AGENTTEAMS_INSTALL_WORKER_IMAGE) {\n $env:AGENTTEAMS_INSTALL_WORKER_IMAGE\n } else {\n \"$registry/agentteams/agentteams-worker:$($script:AGENTTEAMS_VERSION)\"\n }\n\n Write-Log (Get-Msg \"worker.starting\" -f $Name)\n\n $dockerArgs = @(\n \"run\", \"-d\",\n \"--name\", $containerName,\n \"-e\", \"HOME=/root/agentteams-fs/agents/$Name\",\n \"-w\", \"/root/agentteams-fs/agents/$Name\",\n \"-e\", \"AGENTTEAMS_WORKER_NAME=$Name\",\n \"-e\", \"AGENTTEAMS_FS_ENDPOINT=$Fs\",\n \"-e\", \"AGENTTEAMS_FS_ACCESS_KEY=$FsKey\",\n \"-e\", \"AGENTTEAMS_FS_SECRET_KEY=$FsSecret\"\n )\n\n if (-not $SkillsApiUrl) {\n if ($env:AGENTTEAMS_SKILLS_API_URL) {\n $SkillsApiUrl = $env:AGENTTEAMS_SKILLS_API_URL\n } else {\n $SkillsApiUrl = \"nacos://market.agentteams.io:80/public\"\n }\n }\n\n if ($FindSkills -and $SkillsApiUrl) {\n $dockerArgs += @(\"-e\", \"SKILLS_API_URL=$SkillsApiUrl\")\n Write-Log (Get-Msg \"worker.skills_url\" -f $SkillsApiUrl)\n }\n if ($env:AGENTTEAMS_NACOS_USERNAME) {\n $dockerArgs += @(\"-e\", \"AGENTTEAMS_NACOS_USERNAME=$($env:AGENTTEAMS_NACOS_USERNAME)\")\n }\n if ($env:AGENTTEAMS_NACOS_PASSWORD) {\n $dockerArgs += @(\"-e\", \"AGENTTEAMS_NACOS_PASSWORD=$($env:AGENTTEAMS_NACOS_PASSWORD)\")\n }\n if ($env:AGENTTEAMS_NACOS_TOKEN) {\n $dockerArgs += @(\"-e\", \"AGENTTEAMS_NACOS_TOKEN=$($env:AGENTTEAMS_NACOS_TOKEN)\")\n }\n\n $dockerArgs += @(\"--restart\", \"unless-stopped\", $workerImage)\n\n & docker $dockerArgs\n\n Write-Log \"\"\n Write-Log (Get-Msg \"worker.started\" -f $Name)\n Write-Log (Get-Msg \"worker.container\" -f $containerName)\n Write-Log (Get-Msg \"worker.view_logs\" -f $containerName)\n}\n\n# ============================================================\n# Uninstall\n# ============================================================\n\nfunction Uninstall-AgentTeams {\n Write-Log (Get-Msg \"uninstall.title\")\n\n # Stop and remove manager\n $manager = docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-manager$\"\n if ($manager) {\n Write-Log (Get-Msg \"uninstall.stopping_manager\")\n docker stop agentteams-manager *>$null\n docker rm agentteams-manager *>$null\n }\n\n # Stop and remove workers\n $workers = docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-worker-\"\n if ($workers) {\n Write-Log (Get-Msg \"uninstall.stopping_workers\")\n $workers | ForEach-Object {\n docker stop $_ *>$null\n docker rm $_ *>$null\n Write-Log (Get-Msg \"uninstall.removed\" -f $_)\n }\n }\n\n # Stop and remove docker-proxy (legacy <= v1.0.x; current arch uses\n # agentteams-controller for the same role)\n $proxy = docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^hiclaw-docker-proxy$\"\n if ($proxy) {\n Write-Log (Get-Msg \"uninstall.removing_proxy\")\n docker stop hiclaw-docker-proxy *>$null\n docker rm hiclaw-docker-proxy *>$null\n }\n\n # Stop and remove the embedded controller container. MUST happen\n # before the `docker volume rm agentteams-data` step below -- in embedded\n # mode agentteams-controller mounts agentteams-data at /data (Tuwunel DB,\n # MinIO state, Higress state, all the room messages), and `volume rm`\n # against an in-use volume fails silently. Skipping this used to\n # leave room/message history behind across \"uninstall + reinstall\"\n # cycles. See PR #692.\n $controller = docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-controller$\"\n if ($controller) {\n Write-Log (Get-Msg \"uninstall.stopping_controller\")\n docker stop agentteams-controller *>$null\n docker rm agentteams-controller *>$null\n }\n\n # Remove Docker volume (read custom name from env file if available)\n $dataVolume = \"agentteams-data\"\n if (Test-Path $script:AGENTTEAMS_ENV_FILE) {\n $envContent = Get-Content $script:AGENTTEAMS_ENV_FILE -ErrorAction SilentlyContinue\n $dataLine = $envContent | Select-String \"^AGENTTEAMS_DATA_DIR=\"\n if ($dataLine) {\n $parsed = ($dataLine -split \"=\", 2)[1]\n if ($parsed) { $dataVolume = $parsed }\n }\n }\n $volume = docker volume ls -q 2>$null | Select-String \"^${dataVolume}$\"\n if ($volume) {\n Write-Log (Get-Msg \"uninstall.removing_volume\")\n docker volume rm $dataVolume *>$null\n }\n\n # Remove Docker network\n $network = docker network ls --format \"{{.Name}}\" 2>$null | Select-String \"^agentteams-net$\"\n if ($network) {\n Write-Log (Get-Msg \"uninstall.removing_network\")\n docker network rm agentteams-net *>$null\n }\n\n # Remove workspace directory\n $workspaceDir = \"$env:USERPROFILE\\agentteams-manager\"\n if (Test-Path $script:AGENTTEAMS_ENV_FILE) {\n $envContent = Get-Content $script:AGENTTEAMS_ENV_FILE -ErrorAction SilentlyContinue\n $wsLine = $envContent | Select-String \"^AGENTTEAMS_WORKSPACE_DIR=\"\n if ($wsLine) {\n $workspaceDir = ($wsLine -split \"=\", 2)[1]\n }\n }\n if ($workspaceDir -and (Test-Path $workspaceDir)) {\n Write-Log (Get-Msg \"uninstall.removing_workspace\" -f $workspaceDir)\n Remove-Item -Recurse -Force $workspaceDir -ErrorAction SilentlyContinue\n }\n\n # Remove env file\n if (Test-Path $script:AGENTTEAMS_ENV_FILE) {\n Write-Log (Get-Msg \"uninstall.removing_env\" -f $script:AGENTTEAMS_ENV_FILE)\n Remove-Item -Force $script:AGENTTEAMS_ENV_FILE\n }\n\n # Remove install log (stop transcript first to release the file)\n if (Test-Path $script:AGENTTEAMS_LOG_FILE) {\n Write-Log (Get-Msg \"uninstall.removing_log\" -f $script:AGENTTEAMS_LOG_FILE)\n try { Stop-Transcript *>$null } catch {}\n Remove-Item -Force $script:AGENTTEAMS_LOG_FILE -ErrorAction SilentlyContinue\n }\n\n Write-Log \"\"\n Write-Log (Get-Msg \"uninstall.done\")\n}\n\n# ============================================================\n# Main Entry Point\n# ============================================================\n\ntry {\n switch ($Command) {\n \"manager\" {\n Install-Manager\n }\n \"worker\" {\n Install-Worker -Name $Name -Fs $Fs -FsKey $FsKey -FsSecret $FsSecret -Reset:$Reset -FindSkills:$FindSkills -SkillsApiUrl $SkillsApiUrl\n }\n \"uninstall\" {\n Uninstall-AgentTeams\n }\n }\n} catch {\n Exit-Script 1\n}\n\n# Stop transcript logging\ntry {\n Stop-Transcript -ErrorAction SilentlyContinue | Out-Null\n} catch {\n # Ignore errors when stopping transcript\n}\n"}, "cf9bc5ea7e37e5d96dddbaf5cd7f2c6b4ea40207": {"t": 1786068456.9937928, "n": 193564, "h": "6fc90f14bd01f519", "c": "#!/usr/bin/env powershell\n# agentteams-install.ps1 - One-click installation for AgentTeams Manager and Worker on Windows\n# Requires PowerShell 7.0+ (recommended)\n#\n# Usage:\n# .\\agentteams-install.ps1 # Interactive installation (choose Quick Start or Manual)\n# .\\agentteams-install.ps1 manager # Same as above (explicit)\n# .\\agentteams-install.ps1 worker --name <name> ... # Worker installation\n#\n# Onboarding Modes:\n# Quick Start - Fast installation with all default values (recommended)\n# Manual - Customize each option step by step\n#\n# Environment variables (for automation):\n# AGENTTEAMS_NON_INTERACTIVE Skip all prompts, use defaults (default: 0)\n# AGENTTEAMS_LLM_PROVIDER LLM provider (default: openai-compat for zh non-interactive Token Plan; qwen for en)\n# AGENTTEAMS_DEFAULT_MODEL Default model (default: qwen3.6-plus for zh Token Plan and en non-interactive)\n# AGENTTEAMS_OPENAI_BASE_URL OpenAI-compatible base URL (default for zh non-interactive: Alibaba Token Plan endpoint)\n# AGENTTEAMS_LLM_API_KEY LLM API key (required)\n# AGENTTEAMS_ADMIN_USER Admin username (default: admin)\n# AGENTTEAMS_ADMIN_PASSWORD Admin password (auto-generated if not set, min 8 chars)\n# AGENTTEAMS_MATRIX_DOMAIN Matrix domain (default: matrix-local.agentteams.io:18080)\n# AGENTTEAMS_MOUNT_SOCKET Mount container runtime socket (default: 1)\n# AGENTTEAMS_MATRIX_E2EE Matrix E2EE (default: 0, disabled)\n# AGENTTEAMS_DATA_DIR Docker volume name for persistent data (default: agentteams-data)\n# AGENTTEAMS_WORKSPACE_DIR Host directory for manager workspace (default: ~/agentteams-manager)\n# AGENTTEAMS_VERSION Image tag (default: latest)\n# AGENTTEAMS_REGISTRY Image registry (default: auto-detected by timezone)\n# AGENTTEAMS_INSTALL_MANAGER_IMAGE Override manager image (e.g., local build)\n# AGENTTEAMS_INSTALL_WORKER_IMAGE Override worker image (e.g., local build)\n# AGENTTEAMS_INSTALL_COPAW_WORKER_IMAGE Override copaw worker image (e.g., local build)\n# AGENTTEAMS_INSTALL_QWENPAW_WORKER_IMAGE Override qwenpaw worker image (e.g., local build)\n# AGENTTEAMS_INSTALL_HERMES_WORKER_IMAGE Override hermes worker image (e.g., local build)\n# AGENTTEAMS_PORT_GATEWAY Host port for Higress gateway (default: 18080)\n# AGENTTEAMS_PORT_CONSOLE Host port for Higress console (default: 18001)\n# AGENTTEAMS_PORT_ELEMENT_WEB Host port for Element Web direct access (default: 18088)\n\n#Requires -Version 5.1\n\n[CmdletBinding()]\nparam(\n [Parameter(Position = 0)]\n [ValidateSet(\"manager\", \"worker\", \"uninstall\")]\n [string]$Command = \"manager\",\n\n # Worker options\n [string]$Name,\n [string]$Fs,\n [string]$FsKey,\n [string]$FsSecret,\n [switch]$Reset,\n [switch]$FindSkills,\n [string]$SkillsApiUrl,\n\n # General options\n [switch]$NonInteractive,\n [string]$EnvFile\n)\n\n# ============================================================\n# Configuration\n# ============================================================\n\n$script:AGENTTEAMS_VERSION = if ($env:AGENTTEAMS_VERSION) { $env:AGENTTEAMS_VERSION } else { \"latest\" }\n$script:AGENTTEAMS_NON_INTERACTIVE = if ($env:AGENTTEAMS_NON_INTERACTIVE -eq \"1\" -or $NonInteractive) { $true } else { $false }\n$script:AGENTTEAMS_MOUNT_SOCKET = if ($env:AGENTTEAMS_MOUNT_SOCKET -eq \"0\") { $false } else { $true }\n$script:AGENTTEAMS_ENV_FILE = if ($EnvFile) { $EnvFile } elseif ($env:AGENTTEAMS_ENV_FILE) { $env:AGENTTEAMS_ENV_FILE } else { \"$env:USERPROFILE\\agentteams-manager.env\" }\n$script:StepResult = \"\" # Used by state machine to signal \"back\" navigation\n$script:config = @{} # Shared config hashtable for step functions\n\n# ANSI escape character for terminal colors\n$script:ESC = [char]0x1B\n\n# ============================================================\n# Log all output to file\n# ============================================================\n\n$script:AGENTTEAMS_LOG_FILE = \"$env:USERPROFILE\\agentteams-install.log\"\n\n# Start transcript for logging (PowerShell's built-in logging mechanism)\ntry {\n Start-Transcript -Path $script:AGENTTEAMS_LOG_FILE -Append -ErrorAction SilentlyContinue | Out-Null\n} catch {\n # If transcript fails, continue without logging\n}\n\nWrite-Host \"\"\nWrite-Host \"========================================\"\nWrite-Host \"AgentTeams Installation Log\"\nWrite-Host \"Started: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')\"\nWrite-Host \"User: $env:USERNAME\"\nWrite-Host \"System: $(hostname)\"\nWrite-Host \"Log file: $($script:AGENTTEAMS_LOG_FILE)\"\nWrite-Host \"========================================\"\nWrite-Host \"\"\n\n# ============================================================\n# Utility Functions\n# ============================================================\n\nfunction Write-Log {\n param([string]$Message)\n Write-Host \"$($script:ESC)[36m[AgentTeams]$($script:ESC)[0m $Message\"\n}\n\nfunction Write-Error {\n param([string]$Message)\n Write-Host \"$($script:ESC)[31m[AgentTeams ERROR]$($script:ESC)[0m $Message\" -ForegroundColor Red\n throw $Message\n}\n\nfunction Write-Warning {\n param([string]$Message)\n Write-Host \"$($script:ESC)[33m[AgentTeams WARNING]$($script:ESC)[0m $Message\"\n}\n\n# Pause before exit on error so user can read the message when running via double-click\nfunction Exit-Script {\n param([int]$ExitCode = 0)\n if ($ExitCode -ne 0 -and -not $script:AGENTTEAMS_NON_INTERACTIVE) {\n Write-Host \"\"\n Write-Host \"Press Enter to exit.\" -NoNewline\n $Host.UI.ReadLine()\n }\n exit $ExitCode\n}\n\nfunction Test-DockerRunning {\n try {\n $null = docker info 2>&1\n if ($LASTEXITCODE -ne 0) {\n return $false\n }\n return $true\n }\n catch {\n return $false\n }\n}\n\nfunction Get-AgentTeamsTimeZone {\n try {\n if ($env:AGENTTEAMS_TIMEZONE) {\n return $env:AGENTTEAMS_TIMEZONE\n }\n\n $tz = (Get-TimeZone).Id\n # Convert Windows timezone to IANA format\n $tzMap = @{\n \"China Standard Time\" = \"Asia/Shanghai\"\n \"Pacific Standard Time\" = \"America/Los_Angeles\"\n \"Mountain Standard Time\" = \"America/Denver\"\n \"Central Standard Time\" = \"America/Chicago\"\n \"Eastern Standard Time\" = \"America/New_York\"\n \"GMT Standard Time\" = \"Europe/London\"\n \"Central European Standard Time\" = \"Europe/Berlin\"\n \"Tokyo Standard Time\" = \"Asia/Tokyo\"\n \"Singapore Standard Time\" = \"Asia/Singapore\"\n \"Korea Standard Time\" = \"Asia/Seoul\"\n \"India Standard Time\" = \"Asia/Kolkata\"\n }\n\n if ($tzMap.ContainsKey($tz)) {\n return $tzMap[$tz]\n }\n return $tz\n }\n catch {\n return \"Asia/Shanghai\"\n }\n}\n\nfunction Get-Registry {\n param([string]$Timezone)\n\n if ($env:AGENTTEAMS_REGISTRY) {\n return $env:AGENTTEAMS_REGISTRY\n }\n\n # Americas\n if ($Timezone -match \"^America/\") {\n return \"higress-registry.us-west-1.cr.aliyuncs.com\"\n }\n\n # Southeast Asia\n if ($Timezone -match \"^(Asia/Singapore|Asia/Bangkok|Asia/Jakarta|Asia/Makassar|Asia/Jayapura|Asia/Kuala_Lumpur|Asia/Ho_Chi_Minh|Asia/Manila|Asia/Yangon|Asia/Vientiane|Asia/Phnom_Penh|Asia/Pontianak|Asia/Ujung_Pandang)\") {\n return \"higress-registry.ap-southeast-7.cr.aliyuncs.com\"\n }\n\n # Default: China\n return \"higress-registry.cn-hangzhou.cr.aliyuncs.com\"\n}\n\nfunction Get-AgentTeamsLanguage {\n param([string]$Timezone)\n $chineseZones = @(\n \"Asia/Shanghai\", \"Asia/Chongqing\", \"Asia/Harbin\", \"Asia/Urumqi\",\n \"Asia/Taipei\", \"Asia/Hong_Kong\", \"Asia/Macau\"\n )\n if ($chineseZones -contains $Timezone) { return \"zh\" }\n return \"en\"\n}\n\n# ============================================================\n# Centralized message dictionary and Get-Msg function\n# ============================================================\n\n$script:Messages = @{\n # --- Timezone detection messages ---\n \"tz.warning.title\" = @{ zh = \"无法自动检测时区。\"; en = \"Could not detect timezone automatically.\" }\n \"tz.warning.prompt\" = @{ zh = \"请输入您的时区(例如 Asia/Shanghai、America/New_York)。\"; en = \"Please enter your timezone (e.g., Asia/Shanghai, America/New_York).\" }\n \"tz.default\" = @{ zh = \"使用默认时区: {0}\"; en = \"Using default timezone: {0}\" }\n \"tz.input_prompt\" = @{ zh = \"时区\"; en = \"Timezone\" }\n\n # --- Installation title and info ---\n \"install.title\" = @{ zh = \"=== AgentTeams Manager 安装 ===\"; en = \"=== AgentTeams Manager Installation ===\" }\n \"install.registry\" = @{ zh = \"镜像仓库: {0}\"; en = \"Registry: {0}\" }\n \"install.dir\" = @{ zh = \"安装目录: {0}\"; en = \"Installation directory: {0}\" }\n \"install.dir_hint\" = @{ zh = \" (env 文件 'agentteams-manager.env' 将保存到 HOME 目录。)\"; en = \" (The env file 'agentteams-manager.env' will be saved to your HOME directory.)\" }\n \"install.dir_hint2\" = @{ zh = \" (请从您希望管理此安装的目录运行此脚本。)\"; en = \" (Run this script from the directory where you want to manage this installation.)\" }\n\n # --- Onboarding mode ---\n \"install.mode.title\" = @{ zh = \"--- Onboarding 模式 ---\"; en = \"--- Onboarding Mode ---\" }\n \"install.mode.choose\" = @{ zh = \"选择安装模式:\"; en = \"Choose your installation mode:\" }\n \"install.mode.quickstart\" = @{ zh = \" 1) 快速开始 - 使用阿里云通义 Token 套餐快速安装(推荐)\"; en = \" 1) Quick Start - Fast installation with Qwen Cloud (recommended)\" }\n \"install.mode.manual\" = @{ zh = \" 2) 手动配置 - 选择 LLM 提供商并自定义选项\"; en = \" 2) Manual - Choose LLM provider and customize options\" }\n \"install.mode.prompt\" = @{ zh = \"请选择 [1/2]\"; en = \"Enter choice [1/2]\" }\n \"install.mode.quickstart_selected\" = @{ zh = \"已选择快速开始模式 - 使用阿里云通义 Token 套餐\"; en = \"Quick Start mode selected - using Qwen Cloud\" }\n \"install.mode.manual_selected\" = @{ zh = \"已选择手动配置模式 - 您将选择 LLM 提供商并自定义选项\"; en = \"Manual mode selected - you will choose LLM provider and customize options\" }\n \"install.mode.invalid\" = @{ zh = \"无效选择,默认使用快速开始模式\"; en = \"Invalid choice, defaulting to Quick Start mode\" }\n\n # --- Existing installation detected ---\n \"install.existing.detected\" = @{ zh = \"检测到已有 Manager 安装(env 文件: {0})\"; en = \"Existing Manager installation detected (env file: {0})\" }\n \"install.existing.choose\" = @{ zh = \"选择操作:\"; en = \"Choose an action:\" }\n \"install.existing.upgrade\" = @{ zh = \" 1) 就地升级(保留数据、工作空间、env 文件)\"; en = \" 1) In-place upgrade (keep data, workspace, env file)\" }\n \"install.existing.reinstall\" = @{ zh = \" 2) 全新重装(删除所有数据,重新开始)\"; en = \" 2) Clean reinstall (remove all data, start fresh)\" }\n \"install.existing.cancel\" = @{ zh = \" 3) 取消\"; en = \" 3) Cancel\" }\n \"install.existing.prompt\" = @{ zh = \"请选择 [1/2/3]\"; en = \"Enter choice [1/2/3]\" }\n \"install.existing.upgrade_noninteractive\" = @{ zh = \"非交互模式: 执行就地升级...\"; en = \"Non-interactive mode: performing in-place upgrade...\" }\n \"install.existing.upgrading\" = @{ zh = \"执行就地升级...\"; en = \"Performing in-place upgrade...\" }\n \"install.existing.warn_manager_stop\" = @{ zh = \"警告: Manager 容器将被停止并重新创建。\"; en = \"WARNING: Manager container will be stopped and recreated.\" }\n \"install.existing.warn_worker_recreate\" = @{ zh = \"警告: Worker 容器也将被重新创建(以更新 Manager IP)。\"; en = \"WARNING: Worker containers will also be recreated (to update Manager IP in hosts).\" }\n \"install.existing.continue_prompt\" = @{ zh = \"继续?[y/N]\"; en = \"Continue? [y/N]\" }\n \"install.existing.cancelled\" = @{ zh = \"安装已取消。\"; en = \"Installation cancelled.\" }\n \"install.existing.stopping_manager\" = @{ zh = \"停止并移除现有 manager 容器...\"; en = \"Stopping and removing existing manager container...\" }\n \"install.existing.stopping_workers\" = @{ zh = \"停止并移除现有 worker 容器...\"; en = \"Stopping and removing existing worker containers...\" }\n \"install.existing.removed\" = @{ zh = \" 已移除: {0}\"; en = \" Removed: {0}\" }\n\n # --- Upgrade mode sub-menu ---\n \"upgrade.mode.prompt\" = @{ zh = \"请选择升级方式:\"; en = \"Select upgrade mode:\" }\n \"upgrade.mode.keep_all\" = @{ zh = \" 1) 保留所有参数,一键升级(推荐)\"; en = \" 1) Keep all parameters, quick upgrade (recommended)\" }\n \"upgrade.mode.confirm_each\" = @{ zh = \" 2) 逐个确认参数(可修改)\"; en = \" 2) Confirm each parameter (can modify)\" }\n \"upgrade.mode.back\" = @{ zh = \" 3) 返回\"; en = \" 3) Back\" }\n\n # --- Clean reinstall messages ---\n \"install.reinstall.performing\" = @{ zh = \"执行全新重装...\"; en = \"Performing clean reinstall...\" }\n \"install.reinstall.warn_stop\" = @{ zh = \"警告: 以下运行中的容器将被停止:\"; en = \"WARNING: The following running containers will be stopped:\" }\n \"install.reinstall.warn_delete\" = @{ zh = \"警告: 以下内容将被删除:\"; en = \"WARNING: This will DELETE the following:\" }\n \"install.reinstall.warn_volume\" = @{ zh = \" - Docker 卷: agentteams-data\"; en = \" - Docker volume: agentteams-data\" }\n \"install.reinstall.warn_env\" = @{ zh = \" - Env 文件: {0}\"; en = \" - Env file: {0}\" }\n \"install.reinstall.warn_workspace\" = @{ zh = \" - Manager 工作空间: {0}\"; en = \" - Manager workspace: {0}\" }\n \"install.reinstall.warn_workers\" = @{ zh = \" - 所有 worker 容器\"; en = \" - All worker containers\" }\n \"install.reinstall.warn_proxy\" = @{ zh = \" - Docker API 代理容器: agentteams-controller\"; en = \" - Docker API proxy container: agentteams-controller\" }\n \"install.reinstall.removing_proxy\" = @{ zh = \"正在移除 Docker API 代理容器: agentteams-controller\"; en = \"Removing Docker API proxy container: agentteams-controller\" }\n \"install.reinstall.warn_network\" = @{ zh = \" - Docker 网络: agentteams-net\"; en = \" - Docker network: agentteams-net\" }\n \"install.reinstall.removing_network\" = @{ zh = \"正在移除 Docker 网络: agentteams-net\"; en = \"Removing Docker network: agentteams-net\" }\n \"install.reinstall.confirm_type\" = @{ zh = \"请输入工作空间路径以确认删除(或按 Ctrl+C 取消):\"; en = \"To confirm deletion, please type the workspace path:\" }\n \"install.reinstall.confirm_path\" = @{ zh = \"输入路径以确认(或按 Ctrl+C 取消)\"; en = \"Type the path to confirm (or press Ctrl+C to cancel)\" }\n \"install.reinstall.path_mismatch\" = @{ zh = \"路径不匹配。中止重装。输入: '{0}',期望: '{1}'\"; en = \"Path mismatch. Aborting reinstall. Input: '{0}', Expected: '{1}'\" }\n \"install.reinstall.confirmed\" = @{ zh = \"已确认。正在清理...\"; en = \"Confirmed. Cleaning up...\" }\n \"install.reinstall.removed_worker\" = @{ zh = \" 已移除 worker: {0}\"; en = \" Removed worker: {0}\" }\n \"install.reinstall.removing_volume\" = @{ zh = \"正在移除 Docker 卷: agentteams-data\"; en = \"Removing Docker volume: agentteams-data\" }\n \"install.reinstall.warn_volume_fail\" = @{ zh = \" 警告: 无法移除卷(可能有引用)\"; en = \" Warning: Could not remove volume (may have references)\" }\n \"install.reinstall.removing_workspace\" = @{ zh = \"正在移除工作空间目录: {0}\"; en = \"Removing workspace directory: {0}\" }\n \"install.reinstall.removing_env\" = @{ zh = \"正在移除 env 文件: {0}\"; en = \"Removing env file: {0}\" }\n \"install.reinstall.cleanup_done\" = @{ zh = \"清理完成。开始全新安装...\"; en = \"Cleanup complete. Starting fresh installation...\" }\n \"install.reinstall.failed_rm_workspace\" = @{ zh = \"无法移除工作空间目录\"; en = \"Failed to remove workspace directory\" }\n\n # --- Orphan volume detection ---\n \"install.orphan_volume.detected\" = @{ zh = \"⚠️ 检测到残留数据卷 '{0}',但未找到对应的 env 配置文件。\"; en = \"⚠️ Found leftover data volume '{0}' but no matching env config file.\" }\n \"install.orphan_volume.warn\" = @{ zh = \"这可能是之前安装的残留数据,会导致新安装出现异常(如密码冲突、服务启动失败)。\"; en = \"This is likely leftover data from a previous installation and may cause issues (e.g., credential conflicts, service startup failures).\" }\n \"install.orphan_volume.choose\" = @{ zh = \"选择操作:\"; en = \"Choose an action:\" }\n \"install.orphan_volume.clean\" = @{ zh = \" 1) 清理残留数据卷后继续安装(推荐)\"; en = \" 1) Remove leftover volume and continue installation (recommended)\" }\n \"install.orphan_volume.keep\" = @{ zh = \" 2) 保留数据卷继续安装(可能出现异常)\"; en = \" 2) Keep the volume and continue installation (may cause issues)\" }\n \"install.orphan_volume.prompt\" = @{ zh = \"请选择 [1/2]\"; en = \"Enter choice [1/2]\" }\n \"install.orphan_volume.cleaning\" = @{ zh = \"正在清理残留数据卷...\"; en = \"Removing leftover data volume...\" }\n \"install.orphan_volume.cleaned\" = @{ zh = \"残留数据卷已清理。继续全新安装...\"; en = \"Leftover volume removed. Continuing with fresh installation...\" }\n \"install.orphan_volume.keeping\" = @{ zh = \"保留数据卷,继续安装。如遇异常请选择全新重装。\"; en = \"Keeping existing volume. If you encounter issues, consider a clean reinstall.\" }\n \"install.orphan_volume.clean_noninteractive\" = @{ zh = \"非交互模式: 自动清理残留数据卷...\"; en = \"Non-interactive mode: automatically removing leftover volume...\" }\n\n # --- Loading existing config ---\n \"install.loading_config\" = @{ zh = \"从 {0} 加载已有配置(shell 环境变量优先)...\"; en = \"Loading existing config from {0} (shell env vars take priority)...\" }\n\n # --- LLM Configuration ---\n \"llm.title\" = @{ zh = \"--- LLM 配置 ---\"; en = \"--- LLM Configuration ---\" }\n \"llm.provider.label\" = @{ zh = \" 提供商: {0}\"; en = \" Provider: {0}\" }\n \"llm.model.label\" = @{ zh = \" 模型: {0}\"; en = \" Model: {0}\" }\n \"llm.provider.qwen\" = @{ zh = \" 提供商: qwen(阿里云百炼)\"; en = \" Provider: qwen (Alibaba Cloud Bailian)\" }\n \"llm.provider.qwen_default\" = @{ zh = \" 提供商: {0}(默认)\"; en = \" Provider: {0} (default)\" }\n \"llm.model.default\" = @{ zh = \" 模型: {0}(默认)\"; en = \" Model: {0} (default)\" }\n \"llm.apikey_hint_bailian\" = @{ zh = \" 提示: 获取阿里云百炼(DashScope)API Key:\"; en = \" Hint: Get your Alibaba Cloud Bailian (DashScope) API Key:\" }\n \"llm.apikey_url_bailian\" = @{ zh = \" https://www.aliyun.com/product/bailian\"; en = \" https://www.aliyun.com/product/bailian\" }\n \"llm.apikey_hint_qwencloud\" = @{ zh = \" 提示: 从 Qwen Cloud(国际站)获取 DASHSCOPE_API_KEY:\"; en = \" Hint: Get your DASHSCOPE_API_KEY for Qwen Cloud (international) from:\" }\n \"llm.apikey_url_qwencloud\" = @{ zh = \" https://home.qwencloud.com/api-keys (文档: https://docs.qwencloud.com/)\"; en = \" https://home.qwencloud.com/api-keys | Docs: https://docs.qwencloud.com/\" }\n \"llm.apikey_hint_tokenplan\" = @{ zh = \" 提示: 获取 DashScope API Key 或开通通义 Token 套餐,请参考:\"; en = \" Hint: Get your DashScope or Token Plan API key (Alibaba Model Studio):\" }\n \"llm.apikey_url_tokenplan\" = @{ zh = \" https://help.aliyun.com/zh/model-studio/token-plan-quickstart\"; en = \" https://common-buy.aliyun.com/token-plan/ | https://help.aliyun.com/zh/model-studio/token-plan-quickstart\" }\n \"llm.apikey_hint_codingplan\" = @{ zh = \" 提示: 获取 DashScope API Key(Coding 套餐 / coding.dashscope 接口):\"; en = \" Hint: Get your DashScope API key for Coding Plan (coding.dashscope endpoint):\" }\n \"llm.apikey_url_codingplan\" = @{ zh = \" https://help.aliyun.com/zh/model-studio/get-api-key\"; en = \" https://help.aliyun.com/zh/model-studio/get-api-key\" }\n \"llm.apikey_prompt\" = @{ zh = \"LLM API Key\"; en = \"LLM API Key\" }\n \"llm.providers_title\" = @{ zh = \"可用 LLM 提供商:\"; en = \"Available LLM Providers:\" }\n \"llm.provider.alibaba\" = @{ zh = \" 1) 阿里云通义 Token 套餐 - 推荐中国用户使用\"; en = \" 1) Qwen Cloud - International (OpenAI-compatible API, recommended)\" }\n \"llm.provider.openai_compat\" = @{ zh = \" 2) OpenAI 兼容 API - 自定义 Base URL(OpenAI、DeepSeek 等)\"; en = \" 2) OpenAI-compatible API - Custom Base URL (OpenAI, DeepSeek, etc.)\" }\n \"llm.provider.select\" = @{ zh = \"选择提供商 [1/2]\"; en = \"Select provider [1/2]\" }\n \"llm.alibaba.models_title\" = @{ zh = \"选择阿里云模型接入方式:\"; en = \"Select Alibaba Cloud model access:\" }\n \"llm.alibaba.model.tokenplan\" = @{ zh = \" 1) 阿里云通义 Token 套餐 - 兼容模式(推荐)\"; en = \" 1) Alibaba Cloud Token Plan - compatible-mode (recommended)\" }\n \"llm.alibaba.model.bailian\" = @{ zh = \" 2) 阿里云百炼 - DashScope 通用兼容接口\"; en = \" 2) Alibaba Cloud Bailian - DashScope compatible mode\" }\n \"llm.alibaba.model.codingplan_legacy\" = @{ zh = \" 3) 阿里云 Coding 套餐 - 旧版端点(兼容保留)\"; en = \" 3) Alibaba Cloud Coding Plan - legacy endpoint (backward compatible)\" }\n \"llm.alibaba.model.select\" = @{ zh = \"选择接入方式 [1/2/3]\"; en = \"Select access option [1/2/3]\" }\n \"llm.alibaba.model.invalid\" = @{ zh = \"无效选择: {0}(请输入 1、2 或 3)\"; en = \"Invalid choice: {0} (please enter 1, 2, or 3)\" }\n \"llm.codingplan.models_title\" = @{ zh = \"选择通义 Token 套餐默认模型:\"; en = \"Select Qwen Cloud default model:\" }\n \"llm.codingplan.model.qwen36plus\" = @{ zh = \" 1) qwen3.6-plus - 千问 3.6(推荐)\"; en = \" 1) qwen3.6-plus - Qwen 3.6 (recommended)\" }\n \"llm.codingplan.model.glm5\" = @{ zh = \" 2) glm-5 - 智谱 GLM-5(编程推荐)\"; en = \" 2) glm-5 - Zhipu GLM-5 (recommended for coding)\" }\n \"llm.codingplan.model.kimi\" = @{ zh = \" 3) kimi-k2.5 - Moonshot Kimi K2.5\"; en = \" 3) kimi-k2.5 - Moonshot Kimi K2.5\" }\n \"llm.codingplan.model.minimax\" = @{ zh = \" 4) MiniMax-M2.5 - MiniMax M2.5\"; en = \" 4) MiniMax-M2.5 - MiniMax M2.5\" }\n \"llm.codingplan.model.select\" = @{ zh = \"选择模型 [1/2/3/4]\"; en = \"Select model [1/2/3/4]\" }\n \"llm.provider.selected_tokenplan\" = @{ zh = \" 提供商: 阿里云通义 Token 套餐(兼容模式)\"; en = \" Provider: Alibaba Cloud Token Plan (compatible mode)\" }\n \"llm.provider.selected_codingplan\" = @{ zh = \" 提供商: 阿里云通义 Token 套餐(alibaba-cloud)\"; en = \" Provider: Qwen Cloud (international) (alibaba-cloud)\" }\n \"llm.provider.selected_codingplan_legacy\" = @{ zh = \" 提供商: 阿里云 Coding 套餐(coding.dashscope)\"; en = \" Provider: Alibaba Cloud Coding Plan (coding.dashscope)\" }\n \"llm.provider.selected_qwen\" = @{ zh = \" 提供商: 阿里云百炼\"; en = \" Provider: Alibaba Cloud Bailian\" }\n \"llm.provider.selected_openai\" = @{ zh = \" 提供商: {0}(OpenAI 兼容)\"; en = \" Provider: {0} (OpenAI-compatible)\" }\n \"llm.provider.invalid\" = @{ zh = \"无效选择: {0}(请输入 1 或 2)\"; en = \"Invalid choice: {0} (please enter 1 or 2)\" }\n \"llm.qwen.model_prompt\" = @{ zh = \"默认模型 ID\"; en = \"Default Model ID\" }\n \"llm.openai.base_url_prompt\" = @{ zh = \"Base URL\"; en = \"Base URL\" }\n \"llm.openai.model_prompt\" = @{ zh = \"默认模型 ID\"; en = \"Default Model ID\" }\n \"llm.openai.base_url_label\" = @{ zh = \" Base URL: {0}\"; en = \" Base URL: {0}\" }\n\n # --- Custom model parameters ---\n \"llm.custom_model.detected\" = @{ zh = \" ⚠️ 模型 '{0}' 不在内置模型列表中,请配置模型参数:\"; en = \" ⚠️ Model '{0}' is not in the built-in model list. Please configure model parameters:\" }\n \"llm.custom_model.context_prompt\" = @{ zh = \"最大上下文长度(token 数)[150000]\"; en = \"Max context window (tokens) [150000]\" }\n \"llm.custom_model.max_tokens_prompt\" = @{ zh = \"最大输出长度(token 数)[128000]\"; en = \"Max output tokens [128000]\" }\n \"llm.custom_model.reasoning_prompt\" = @{ zh = \"是否支持推理/思考模式?[Y/n]\"; en = \"Does it support reasoning/thinking mode? [Y/n]\" }\n \"llm.custom_model.vision_prompt\" = @{ zh = \"是否支持图片输入?[y/N]\"; en = \"Does it support image input? [y/N]\" }\n \"llm.custom_model.summary\" = @{ zh = \" 自定义模型参数: 上下文={0}, 最大输出={1}, 推理={2}, 图片={3}\"; en = \" Custom model params: context={0}, maxTokens={1}, reasoning={2}, vision={3}\" }\n\n # --- Admin Credentials ---\n \"admin.title\" = @{ zh = \"--- 管理员凭据 ---\"; en = \"--- Admin Credentials ---\" }\n \"admin.username_prompt\" = @{ zh = \"管理员用户名\"; en = \"Admin Username\" }\n \"admin.password_prompt\" = @{ zh = \"管理员密码(留空自动生成,最少 8 位)\"; en = \"Admin Password (leave empty to auto-generate, min 8 chars)\" }\n \"admin.password_generated\" = @{ zh = \" 已自动生成管理员密码\"; en = \" Auto-generated admin password\" }\n \"admin.password_too_short\" = @{ zh = \"管理员密码至少需要 8 个字符(MinIO 要求)。当前长度: {0}\"; en = \"Admin password must be at least 8 characters (MinIO requirement). Current length: {0}\" }\n\n # --- Port Configuration ---\n \"port.title\" = @{ zh = \"--- 端口配置(按回车使用默认值)---\"; en = \"--- Port Configuration (press Enter for defaults) ---\" }\n \"port.gateway_prompt\" = @{ zh = \"网关主机端口(容器内 8080)\"; en = \"Host port for gateway (8080 inside container)\" }\n \"port.console_prompt\" = @{ zh = \"Higress 控制台主机端口(容器内 8001)\"; en = \"Host port for Higress console (8001 inside container)\" }\n \"port.element_prompt\" = @{ zh = \"Element Web 直接访问主机端口(容器内 8088)\"; en = \"Host port for Element Web direct access (8088 inside container)\" }\n \"port.manager_console_prompt\" = @{ zh = \"Manager 控制台主机端口(容器内 18888)\"; en = \"Host port for Manager console (18888 inside container)\" }\n \"port.local_only.title\" = @{ zh = \"--- 网络访问模式 ---\"; en = \"--- Network Access Mode ---\" }\n \"port.local_only.hint_yes\" = @{ zh = \" 仅本机使用,无需开放外部端口(推荐)\"; en = \" Local use only, no external port exposure (recommended)\" }\n \"port.local_only.hint_no\" = @{ zh = \" 允许外部访问(局域网 / 公网)\"; en = \" Allow external access (LAN / public network)\" }\n \"port.local_only.choice\" = @{ zh = \"请选择 [1/2]\"; en = \"Enter choice [1/2]\" }\n \"port.local_only.selected_local\" = @{ zh = \"端口已绑定到 127.0.0.1(仅本机访问)\"; en = \"Ports bound to 127.0.0.1 (localhost only)\" }\n \"port.local_only.selected_external\" = @{ zh = \"端口已绑定到所有网络接口(0.0.0.0)\"; en = \"Ports bound to all interfaces (0.0.0.0)\" }\n \"port.local_only.https_hint\" = @{ zh = \"警告: 建议在 Higress 控制台配置 TLS 证书并启用 HTTPS,避免明文传输。\"; en = \"WARNING: It is recommended to configure TLS certificates and enable HTTPS in the Higress Console to avoid plaintext transmission.\" }\n \"port.local_only.https_docs\" = @{ zh = \"\"; en = \"\" }\n\n # --- Domain Configuration ---\n \"domain.title\" = @{ zh = \"--- 域名配置(按回车使用默认值)---\"; en = \"--- Domain Configuration (press Enter for defaults) ---\" }\n \"domain.hint\" = @{ zh = \"提示: 自定义域名前必须事先做好 DNS 解析。单机 ECS 部署时无需修改 aigw、fs 等域名;Element Web 和 Matrix Server 也可通过 IP 直接访问。\"; en = \"Hint: Configure DNS resolution before customizing domains. For single ECS deployment, no need to change aigw, fs, etc.; Element Web and Matrix Server can also be accessed directly via IP.\" }\n \"domain.matrix_prompt\" = @{ zh = \"Matrix 域名\"; en = \"Matrix Domain\" }\n \"domain.element_prompt\" = @{ zh = \"Element Web 域名\"; en = \"Element Web Domain\" }\n \"domain.gateway_prompt\" = @{ zh = \"AI 网关域名\"; en = \"AI Gateway Domain\" }\n \"domain.fs_prompt\" = @{ zh = \"文件系统域名\"; en = \"File System Domain\" }\n \"domain.console_prompt\" = @{ zh = \"Manager 控制台域名\"; en = \"Manager Console Domain\" }\n\n # --- GitHub Integration ---\n \"github.title\" = @{ zh = \"--- GitHub 集成(可选,按回车跳过)---\"; en = \"--- GitHub Integration (optional, press Enter to skip) ---\" }\n \"github.token_prompt\" = @{ zh = \"GitHub 个人访问令牌(可选)\"; en = \"GitHub Personal Access Token (optional)\" }\n\n # --- Skills Registry ---\n \"skills.title\" = @{ zh = \"--- Skills 注册中心(可选,按回车使用默认 nacos://market.agentteams.io:80/public)---\"; en = \"--- Skills Registry (optional, press Enter for default nacos://market.agentteams.io:80/public) ---\" }\n \"skills.url_prompt\" = @{ zh = \"Skills 注册中心 URL(留空使用默认 nacos://market.agentteams.io:80/public)\"; en = \"Skills Registry URL (leave empty for default nacos://market.agentteams.io:80/public)\" }\n\n # --- Data Persistence ---\n \"data.title\" = @{ zh = \"--- 数据持久化 ---\"; en = \"--- Data Persistence ---\" }\n \"data.volume_prompt\" = @{ zh = \"Docker 卷名称 [agentteams-data]\"; en = \"Docker volume name for persistent data [agentteams-data]\" }\n \"data.volume_using\" = @{ zh = \" 使用 Docker 卷: {0}\"; en = \" Using Docker volume: {0}\" }\n\n # --- Manager Workspace ---\n \"workspace.title\" = @{ zh = \"--- Manager 工作空间 ---\"; en = \"--- Manager Workspace ---\" }\n \"workspace.dir_prompt\" = @{ zh = \"Manager 工作空间目录 [{0}]\"; en = \"Manager workspace directory [{0}]\" }\n \"workspace.dir_label\" = @{ zh = \" Manager 工作空间: {0}\"; en = \" Manager workspace: {0}\" }\n\n # --- Host directory sharing ---\n \"host_share.prompt\" = @{ zh = \"与 Agent 共享的主机目录(默认: {0})\"; en = \"Host directory to share with agents (default: {0})\" }\n \"host_share.sharing\" = @{ zh = \"共享主机目录: {0} -> 容器内 /host-share\"; en = \"Sharing host directory: {0} -> /host-share in container\" }\n \"host_share.not_exist\" = @{ zh = \"警告: 主机目录 {0} 不存在,跳过验证继续使用\"; en = \"WARNING: Host directory {0} does not exist, using without validation\" }\n\n # --- Default worker runtime ---\n \"worker_runtime.title\" = @{ zh = \"--- 默认 Worker 运行时 ---\"; en = \"--- Default Worker Runtime ---\" }\n \"worker_runtime.openclaw\" = @{ zh = \"OpenClaw\"; en = \"OpenClaw\" }\n \"worker_runtime.copaw\" = @{ zh = \"CoPaw\"; en = \"CoPaw\" }\n \"worker_runtime.hermes\" = @{ zh = \"Hermes\"; en = \"Hermes\" }\n \"worker_runtime.choice\" = @{ zh = \"请选择 [1/2/3]\"; en = \"Enter choice [1/2/3]\" }\n \"worker_runtime.selected\" = @{ zh = \"默认 Worker 运行时: {0}\"; en = \"Default Worker runtime: {0}\" }\n \"worker_runtime.title_short\" = @{ zh = \"默认 Worker 运行时\"; en = \"Default Worker Runtime\" }\n\n # --- Manager runtime ---\n \"manager_runtime.title\" = @{ zh = \"--- Manager 运行时 ---\"; en = \"--- Manager Runtime ---\" }\n \"manager_runtime.openclaw\" = @{ zh = \"OpenClaw\"; en = \"OpenClaw\" }\n \"manager_runtime.copaw\" = @{ zh = \"CoPaw\"; en = \"CoPaw\" }\n \"manager_runtime.choice\" = @{ zh = \"请选择 [1/2]\"; en = \"Enter choice [1/2]\" }\n \"manager_runtime.selected\" = @{ zh = \"Manager 运行时: {0}\"; en = \"Manager runtime: {0}\" }\n \"manager_runtime.title_short\" = @{ zh = \"Manager 运行时\"; en = \"Manager Runtime\" }\n\n # --- Matrix E2EE ---\n \"matrix_e2ee.title\" = @{ zh = \"--- Matrix 端到端加密(E2EE)---\"; en = \"--- Matrix End-to-End Encryption (E2EE) ---\" }\n \"matrix_e2ee.desc\" = @{\n zh = \"E2EE 会对 Manager 与 Worker 之间的 Matrix 消息进行端到端加密。`n 启用后,即使 Matrix 服务器被入侵,消息内容也无法被窃取。`n 但 E2EE 会增加首次握手耗时,且要求所有 Agent 都支持 matrix-sdk-crypto。`n 如果不确定,建议保持禁用。`n ⚠ 注意:禁用 E2EE 后,请勿在 Element 上创建默认启用加密的 Private 房间,`n 否则 Agent 将无法读取该房间中的加密消息。请改用 Public 房间或关闭房间加密。\"\n en = \"E2EE encrypts Matrix messages between Manager and Workers end-to-end.`n When enabled, message content stays private even if the Matrix server is compromised.`n However, E2EE adds overhead to the initial handshake and requires all Agents`n to support matrix-sdk-crypto. If unsure, keep it disabled.`n ⚠ Note: When E2EE is disabled, do NOT create Private rooms in Element (which`n enable encryption by default) — Agents cannot read encrypted messages without`n E2EE support. Use Public rooms or turn off room encryption instead.\"\n }\n \"matrix_e2ee.enable\" = @{ zh = \"启用 E2EE\"; en = \"Enable E2EE\" }\n \"matrix_e2ee.disable\" = @{ zh = \"禁用 E2EE(推荐)\"; en = \"Disable E2EE (recommended)\" }\n \"matrix_e2ee.choice\" = @{ zh = \"请选择 [1/2]\"; en = \"Enter choice [1/2]\" }\n \"matrix_e2ee.selected_enabled\" = @{ zh = \"Matrix E2EE: 已启用\"; en = \"Matrix E2EE: enabled\" }\n \"matrix_e2ee.selected_disabled\" = @{ zh = \"Matrix E2EE: 已禁用(默认)\"; en = \"Matrix E2EE: disabled (default)\" }\n \"matrix_e2ee.title_short\" = @{ zh = \"Matrix E2EE\"; en = \"Matrix E2EE\" }\n \"matrix_e2ee.val_enabled\" = @{ zh = \"已启用\"; en = \"enabled\" }\n \"matrix_e2ee.val_disabled\" = @{ zh = \"已禁用\"; en = \"disabled\" }\n\n # --- Docker API proxy ---\n \"docker_proxy.title\" = @{ zh = \"--- Docker API 安全代理 ---\"; en = \"--- Docker API Security Proxy ---\" }\n \"docker_proxy.desc\" = @{ zh = \"Docker API 代理可防止 AI Agent 通过 Docker API 越狱访问宿主机。`n 启用后,Manager 不再直接持有 Docker socket,所有容器操作经过安全校验。\"; en = \"Docker API proxy prevents AI Agents from escaping via Docker API to access the host.`n When enabled, Manager no longer has direct Docker socket access; all container operations go through security validation.\" }\n \"docker_proxy.enable\" = @{ zh = \"启用(推荐)\"; en = \"Enable (recommended)\" }\n \"docker_proxy.disable\" = @{ zh = \"禁用(直接挂载 Docker socket)\"; en = \"Disable (mount Docker socket directly)\" }\n \"docker_proxy.choice\" = @{ zh = \"请选择 [1/2]\"; en = \"Enter choice [1/2]\" }\n \"docker_proxy.selected_enabled\" = @{ zh = \"Docker API 代理: 已启用\"; en = \"Docker API proxy: enabled\" }\n \"docker_proxy.selected_disabled\" = @{ zh = \"Docker API 代理: 已禁用\"; en = \"Docker API proxy: disabled\" }\n \"docker_proxy.title_short\" = @{ zh = \"Docker API 代理\"; en = \"Docker API Proxy\" }\n \"docker_proxy.val_enabled\" = @{ zh = \"已启用\"; en = \"enabled\" }\n \"docker_proxy.val_disabled\" = @{ zh = \"已禁用\"; en = \"disabled\" }\n \"docker_proxy.registries_desc\" = @{ zh = \"默认放行的镜像来源:本地镜像、localhost、Higress 仓库(所有 region)。`n 如需放行其他镜像仓库,请输入逗号分隔的地址前缀。`n 示例: ghcr.io/myorg,registry.example.com/team\"; en = \"Default allowed image sources: local images, localhost, Higress registries (all regions).`n To allow additional image sources, enter comma-separated address prefixes.`n Example: ghcr.io/myorg,registry.example.com/team\" }\n \"docker_proxy.registries_prompt\" = @{ zh = \"额外放行的镜像来源(按回车跳过)\"; en = \"Additional allowed image sources (press Enter to skip)\" }\n \"docker_proxy.registries_label\" = @{ zh = \"额外放行的镜像来源\"; en = \"Additional allowed image sources\" }\n\n # --- Worker idle timeout ---\n \"idle_timeout.prompt\" = @{ zh = \"Worker 空闲自动停止超时(分钟)[720]\"; en = \"Worker idle auto-stop timeout in minutes [720]\" }\n \"idle_timeout.selected\" = @{ zh = \"Worker 空闲超时: {0} 分钟\"; en = \"Worker idle timeout: {0} minutes\" }\n \"idle_timeout.label\" = @{ zh = \"Worker 空闲超时(分钟)\"; en = \"Worker idle timeout (min)\" }\n\n # --- Secrets and config ---\n \"install.generating_secrets\" = @{ zh = \"正在生成密钥...\"; en = \"Generating secrets...\" }\n \"install.config_saved\" = @{ zh = \"配置已保存到 {0}\"; en = \"Configuration saved to {0}\" }\n\n # --- Container runtime socket ---\n \"install.socket_detected\" = @{ zh = \"容器运行时 socket: {0}(已启用直接创建 Worker)\"; en = \"Container runtime socket: {0} (direct Worker creation enabled)\" }\n \"install.socket_not_found\" = @{ zh = \"未找到容器运行时 socket(Manager 无法直接创建 Worker 容器,需要你手动执行 docker 命令创建)\"; en = \"No container runtime socket found (Manager cannot create Worker containers directly, you will need to create them manually using docker commands)\" }\n \"install.socket_confirm.title\" = @{ zh = \"警告: 未检测到容器运行时 Socket\"; en = \"WARNING: Container Runtime Socket Not Detected\" }\n \"install.socket_confirm.message\" = @{ zh = \"未找到 Docker/Podman socket,Manager 将无法自动创建 Worker 容器。`n你需要手动执行 docker run 命令来创建 Worker。`n`n是否继续安装?\"; en = \"Docker/Podman socket not found. Manager will not be able to create Worker containers automatically.`nYou will need to manually run docker commands to create Workers.`n`nContinue installation?\" }\n \"install.socket_confirm.prompt\" = @{ zh = \"继续安装? [y/N]: \"; en = \"Continue? [y/N]: \" }\n \"install.socket_confirm.cancelled\" = @{ zh = \"安装已取消。如需启用 Worker 自动创建,请确保 Docker/Podman 正在运行,然后重新运行安装脚本。\"; en = \"Installation cancelled. To enable automatic Worker creation, ensure Docker/Podman is running and re-run the installer.\" }\n\n # --- Container management ---\n \"install.removing_existing\" = @{ zh = \"正在移除现有 agentteams-manager 容器...\"; en = \"Removing existing agentteams-manager container...\" }\n\n # --- YOLO mode ---\n \"install.yolo\" = @{ zh = \"YOLO 模式已启用(自主决策,无交互提示)\"; en = \"YOLO mode enabled (autonomous decisions, no interactive prompts)\" }\n\n # --- Image pulling ---\n \"install.image.exists\" = @{ zh = \"Manager 镜像已存在: {0}\"; en = \"Manager image already exists locally: {0}\" }\n \"install.image.pulling_manager\" = @{ zh = \"正在拉取 Manager 镜像: {0}\"; en = \"Pulling Manager image: {0}\" }\n \"install.image.worker_exists\" = @{ zh = \"Worker 镜像已存在: {0}\"; en = \"Worker image already exists locally: {0}\" }\n \"install.image.pulling_worker\" = @{ zh = \"正在拉取 Worker 镜像: {0}\"; en = \"Pulling Worker image: {0}\" }\n\n # --- Starting container ---\n \"install.starting_manager\" = @{ zh = \"正在启动 Manager 容器...\"; en = \"Starting Manager container...\" }\n\n # --- Wait for Manager ready ---\n \"install.wait_ready\" = @{ zh = \"等待 Manager Agent 就绪(超时: {0}s)...\"; en = \"Waiting for Manager agent to be ready (timeout: {0}s)...\" }\n \"install.wait_ready.ok\" = @{ zh = \"Manager Agent 已就绪!\"; en = \"Manager agent is ready!\" }\n \"install.wait_ready.waiting\" = @{ zh = \"等待中... ({0}s/{1}s)\"; en = \"Waiting... ({0}s/{1}s)\" }\n \"install.wait_ready.timeout\" = @{ zh = \"Manager Agent 在 {0}s 内未就绪。请检查: docker logs {1}\"; en = \"Manager agent did not become ready within {0}s. Check: docker logs {1}\" }\n\n # --- Wait for Matrix ready ---\n \"install.wait_matrix\" = @{ zh = \"等待 Matrix 服务就绪(超时: {0}s)...\"; en = \"Waiting for Matrix server to be ready (timeout: {0}s)...\" }\n \"install.wait_matrix.ok\" = @{ zh = \"Matrix 服务已就绪!\"; en = \"Matrix server is ready!\" }\n \"install.wait_matrix.waiting\" = @{ zh = \"等待 Matrix 中... ({0}s/{1}s)\"; en = \"Waiting for Matrix... ({0}s/{1}s)\" }\n \"install.wait_matrix.timeout\" = @{ zh = \"Matrix 服务在 {0}s 内未就绪。请检查: docker logs {1}\"; en = \"Matrix server did not become ready within {0}s. Check: docker logs {1}\" }\n\n # --- OpenAI-compatible connectivity test ---\n \"llm.openai.test.testing\" = @{ zh = \"正在测试 API 联通性...\"; en = \"Testing API connectivity...\" }\n \"llm.openai.test.ok\" = @{ zh = \"API 联通性测试通过\"; en = \"API connectivity test passed\" }\n \"llm.openai.test.fail\" = @{ zh = \"API 联通性测试失败(HTTP {0})。响应内容:`n{1}`n请根据以上错误信息联系您的模型服务商解决。\"; en = \"API connectivity test failed (HTTP {0}). Response body:`n{1}`nPlease contact your model provider to resolve the issue.\" }\n \"llm.openai.test.fail.tokenplan\" = @{ zh = \"提示: 请确认 API Key 有效且已开通通义 Token 套餐。文档: https://help.aliyun.com/zh/model-studio/token-plan-quickstart\"; en = \"Hint: Verify your Token Plan API key and compatible-mode access. Docs: https://help.aliyun.com/zh/model-studio/token-plan-quickstart\" }\n \"llm.openai.test.fail.codingplan\" = @{ zh = \"提示: 请确认 API Key 有效且已开通通义 Token 套餐。文档: https://help.aliyun.com/zh/model-studio/token-plan-quickstart\"; en = \"Hint: Verify your DASHSCOPE_API_KEY for Qwen Cloud. API keys: https://home.qwencloud.com/api-keys Docs: https://docs.qwencloud.com/\" }\n \"llm.openai.test.fail.codingplan_legacy\" = @{ zh = \"提示: 请确认 API Key 有效且 Coding 套餐接口可用。文档: https://help.aliyun.com/zh/model-studio/get-api-key\"; en = \"Hint: Verify your DashScope API key and Coding Plan access. Docs: https://help.aliyun.com/zh/model-studio/get-api-key\" }\n \"llm.openai.test.confirm\" = @{ zh = \"是否仍要继续安装?[y/N/b]\"; en = \"Continue with installation anyway? [y/N/b]\" }\n \"llm.openai.test.aborted\" = @{ zh = \"安装已中止。\"; en = \"Installation aborted.\" }\n \"llm.embedding.title\" = @{ zh = \"📦 记忆搜索配置\"; en = \"📦 Memory Search Configuration\" }\n \"llm.embedding.hint\" = @{ zh = \" Embedding 模型可提升记忆搜索质量(语义匹配)。不启用也可正常使用记忆功能(关键词匹配)。\"; en = \" Embedding model improves memory search quality (semantic matching). Memory still works without it (keyword matching).\" }\n \"llm.embedding.option.default\" = @{ zh = \" 1) text-embedding-v4(推荐)\"; en = \" 1) text-embedding-v4 (Recommended)\" }\n \"llm.embedding.option.custom\" = @{ zh = \" 2) 自定义 Embedding 模型\"; en = \" 2) Custom embedding model\" }\n \"llm.embedding.option.disable\" = @{ zh = \" 3) 不启用\"; en = \" 3) Do not enable\" }\n \"llm.embedding.select\" = @{ zh = \"选择\"; en = \"Select\" }\n \"llm.embedding.custom_prompt\" = @{ zh = \" Embedding 模型名称\"; en = \" Embedding model name\" }\n \"llm.embedding.test.testing\" = @{ zh = \"正在测试 Embedding API 联通性...\"; en = \"Testing Embedding API connectivity...\" }\n \"llm.embedding.test.ok\" = @{ zh = \"✅ Embedding API 联通性测试通过\"; en = \"✅ Embedding API connectivity test passed\" }\n \"llm.embedding.test.fail\" = @{ zh = \"⚠️ Embedding API 测试失败(HTTP {0})。响应: {1}\"; en = \"⚠️ Embedding API test failed (HTTP {0}). Response: {1}\" }\n \"llm.embedding.auto_disabled\" = @{ zh = \"⚠️ Embedding 已自动禁用,记忆搜索将使用关键词匹配。您可以稍后在 agentteams-manager.env 中设置 AGENTTEAMS_EMBEDDING_MODEL 启用。\"; en = \"⚠️ Embedding auto-disabled. Memory search will use keyword matching. You can enable it later in agentteams-manager.env by setting AGENTTEAMS_EMBEDDING_MODEL.\" }\n \"llm.embedding.disabled\" = @{ zh = \"ℹ️ Embedding 已禁用,记忆搜索将使用关键词匹配。\"; en = \"ℹ️ Embedding disabled. Memory search will use keyword matching.\" }\n \"nav.back_hint\" = @{ zh = \"(输入 b 返回上一步)\"; en = \"(enter b to go back)\" }\n # --- OpenAI-compatible provider creation ---\n \"install.openai_compat.missing\" = @{ zh = \"警告: OpenAI Base URL 或 API Key 未设置,跳过提供商创建\"; en = \"WARNING: OpenAI Base URL or API Key not set, skipping provider creation\" }\n \"install.openai_compat.creating\" = @{ zh = \"正在创建 OpenAI 兼容提供商...\"; en = \"Creating OpenAI-compatible provider...\" }\n \"install.openai_compat.domain\" = @{ zh = \" 域名: {0}\"; en = \" Domain: {0}\" }\n \"install.openai_compat.port\" = @{ zh = \" 端口: {0}\"; en = \" Port: {0}\" }\n \"install.openai_compat.protocol\" = @{ zh = \" 协议: {0}\"; en = \" Protocol: {0}\" }\n \"install.openai_compat.service_fail\" = @{ zh = \"警告: 创建 DNS 服务源失败(可能已存在)\"; en = \"WARNING: Failed to create DNS service source (may already exist)\" }\n \"install.openai_compat.provider_fail\" = @{ zh = \"警告: 创建 AI 提供商失败(可能已存在)\"; en = \"WARNING: Failed to create AI provider (may already exist)\" }\n \"install.openai_compat.success\" = @{ zh = \"OpenAI 兼容提供商创建成功\"; en = \"OpenAI-compatible provider created successfully\" }\n\n # --- Welcome message ---\n \"install.welcome_msg.soul_configured\" = @{ zh = \"Soul 已配置(找到 soul-configured 标记),跳过 onboarding 消息\"; en = \"Soul already configured (soul-configured marker found), skipping onboarding message\" }\n \"install.welcome_msg.logging_in\" = @{ zh = \"正在以 {0} 身份登录以发送欢迎消息...\"; en = \"Logging in as {0} to send welcome message...\" }\n \"install.welcome_msg.login_failed\" = @{ zh = \"警告: 以 {0} 身份登录失败,跳过欢迎消息\"; en = \"WARNING: Failed to login as {0}, skipping welcome message\" }\n \"install.welcome_msg.finding_room\" = @{ zh = \"正在查找与 Manager 的 DM 房间...\"; en = \"Finding DM room with Manager...\" }\n \"install.welcome_msg.creating_room\" = @{ zh = \"正在创建与 Manager 的 DM 房间...\"; en = \"Creating DM room with Manager...\" }\n \"install.welcome_msg.no_room\" = @{ zh = \"警告: 无法找到或创建与 Manager 的 DM 房间\"; en = \"WARNING: Could not find or create DM room with Manager\" }\n \"install.welcome_msg.waiting_join\" = @{ zh = \"等待 Manager 加入房间...\"; en = \"Waiting for Manager to join the room...\" }\n \"install.welcome_msg.sending\" = @{ zh = \"正在向 Manager 发送欢迎消息...\"; en = \"Sending welcome message to Manager...\" }\n \"install.welcome_msg.send_failed\" = @{ zh = \"警告: 发送欢迎消息失败\"; en = \"WARNING: Failed to send welcome message\" }\n \"install.welcome_msg.sent\" = @{ zh = \"欢迎消息已发送给 Manager\"; en = \"Welcome message sent to Manager\" }\n \"install.welcome_msg.waiting\" = @{ zh = \"等待 Manager 发送欢迎消息(Higress 路由授权 + LLM 探活,约 45-90s)...\"; en = \"Waiting for Manager to send the welcome message (Higress route auth + LLM probe, ~45-90s)...\" }\n \"install.welcome_msg.confirmed\" = @{ zh = \"Manager 已确认发送欢迎消息(status.welcomeSent=true,用时 {0}s)\"; en = \"Manager confirmed welcome message sent (status.welcomeSent=true, {0}s elapsed)\" }\n \"install.welcome_msg.timeout\" = @{ zh = \"警告: 在 {0}s 内未观察到 Manager 发送欢迎消息(status.welcomeSent=true)。安装仍然成功,所有服务已就绪——可继续按下方提示登录 Element Web。\"; en = \"WARNING: Did not observe the Manager sending its welcome message (status.welcomeSent=true) within {0}s. Installation is still successful, all services are up — continue with the Element Web instructions below.\" }\n \"install.welcome_msg.timeout_hint\" = @{ zh = \"手动触发 onboarding: 登录 Element Web → 打开与 Manager 的 DM 房间 → 发送任意一句话(例如 `\"hi`\"),Manager 会接管对话并开始引导。\"; en = \"Manual onboarding: log in to Element Web -> open the DM with the Manager -> send any message (e.g. `\"hi`\") and the Manager will take over and start the guided setup.\" }\n \"install.welcome_msg.timeout_inspect\" = @{ zh = \"排查命令: docker exec agentteams-controller agt get managers default\"; en = \"Inspect status: docker exec agentteams-controller agt get managers default\" }\n \"install.welcome_msg.poll_unavailable\" = @{ zh = \"提示: agentteams-manager 内未找到 AgentTeams CLI,跳过 welcome 等待(旧镜像?)\"; en = \"Note: AgentTeams CLI not found inside agentteams-manager; skipping welcome wait (old image?)\" }\n\n # --- Final output panel ---\n \"success.title\" = @{ zh = \"=== AgentTeams Manager 已启动!===\"; en = \"=== AgentTeams Manager Started! ===\" }\n \"success.domains_configured\" = @{ zh = \"以下域名已配置解析到 127.0.0.1:\"; en = \"The following domains are configured to resolve to 127.0.0.1:\" }\n \"success.open_url\" = @{ zh = \" 在浏览器中打开以下 URL 开始使用: \"; en = \" Open the following URL in your browser to start: \" }\n \"success.login_with\" = @{ zh = \" 登录信息:\"; en = \" Login with:\" }\n \"success.username\" = @{ zh = \" 用户名: {0}\"; en = \" Username: {0}\" }\n \"success.password\" = @{ zh = \" 密码: {0}\"; en = \" Password: {0}\" }\n \"success.after_login\" = @{ zh = \" 登录后,开始与 Manager 聊天!\"; en = \" After login, start chatting with the Manager!\" }\n \"success.tell_it\" = @{ zh = \" 告诉它: `\"创建一个名为 alice 的前端开发 Worker`\"\"; en = \" Tell it: `\"Create a Worker named alice for frontend dev`\"\" }\n \"success.manager_auto\" = @{ zh = \" Manager 会自动处理一切。\"; en = \" The Manager will handle everything automatically.\" }\n \"success.mobile_title\" = @{ zh = \" 移动端访问(FluffyChat / Element Mobile):\"; en = \" Mobile access (FluffyChat / Element Mobile):\" }\n \"success.mobile_step1\" = @{ zh = \" 1. 在手机上下载 FluffyChat 或 Element\"; en = \" 1. Download FluffyChat or Element on your phone\" }\n \"success.mobile_step2\" = @{ zh = \" 2. 设置 homeserver 为: {0}\"; en = \" 2. Set homeserver to: {0}\" }\n \"success.mobile_step2_noip\" = @{ zh = \" 2. 设置 homeserver 为: http://<本机局域网IP>:{0}\"; en = \" 2. Set homeserver to: http://<this-machine-LAN-IP>:{0}\" }\n \"success.mobile_noip_hint\" = @{ zh = \" (无法自动检测局域网 IP — 请使用 ifconfig / ip addr 查看)\"; en = \" (Could not detect LAN IP automatically — check with: ifconfig / ip addr)\" }\n \"success.mobile_step3\" = @{ zh = \" 3. 登录信息:\"; en = \" 3. Login with:\" }\n \"success.mobile_username\" = @{ zh = \" 用户名: {0}\"; en = \" Username: {0}\" }\n \"success.mobile_password\" = @{ zh = \" 密码: {0}\"; en = \" Password: {0}\" }\n\n # --- Other consoles and tips ---\n \"success.other_consoles\" = @{ zh = \"--- 其他控制台 ---\"; en = \"--- Other Consoles ---\" }\n \"success.higress_console\" = @{ zh = \" Higress 控制台: http://localhost:{0}(用户名: {1} / 密码: {2})\"; en = \" Higress Console: http://localhost:{0} (Username: {1} / Password: {2})\" }\n \"success.manager_console\" = @{ zh = \" Manager 控制台(本地): http://localhost:{0}(无需登录)\"; en = \" Manager Console (local): http://localhost:{0} (no login required)\" }\n \"success.manager_console_gateway\" = @{ zh = \" Manager 控制台(网关): http://console-local.agentteams.io(用户名: {0} / 密码: {1})\"; en = \" Manager Console (gateway): http://console-local.agentteams.io (Username: {0} / Password: {1})\" }\n \"success.copaw_console\" = @{ zh = \" CoPaw 控制台(本地): http://localhost:{0}(无需登录)\"; en = \" CoPaw Console (local): http://localhost:{0} (no login required)\" }\n \"success.switch_llm.title\" = @{ zh = \"--- 切换 LLM 提供商 ---\"; en = \"--- Switch LLM Providers ---\" }\n \"success.switch_llm.hint\" = @{ zh = \" 您可以通过 Higress 控制台切换到其他 LLM 提供商(OpenAI、Anthropic 等)。\"; en = \" You can switch to other LLM providers (OpenAI, Anthropic, etc.) via Higress Console.\" }\n \"success.switch_llm.docs\" = @{ zh = \" 详细说明请参阅:\"; en = \" For detailed instructions, see:\" }\n \"success.switch_llm.url\" = @{ zh = \" https://higress.ai/en/docs/ai/scene-guide/multi-proxy#console-configuration\"; en = \" https://higress.ai/en/docs/ai/scene-guide/multi-proxy#console-configuration\" }\n \"success.tip\" = @{ zh = \"提示: 您也可以在聊天中让 Manager 为您配置 LLM 提供商。\"; en = \"Tip: You can also ask the Manager to configure LLM providers for you in the chat.\" }\n \"success.config_file\" = @{ zh = \"配置文件: {0}\"; en = \"Configuration file: {0}\" }\n \"success.data_volume\" = @{ zh = \"数据卷: {0}\"; en = \"Data volume: {0}\" }\n \"success.workspace\" = @{ zh = \"Manager 工作空间: {0}\"; en = \"Manager workspace: {0}\" }\n\n # --- Worker installation ---\n \"worker.resetting\" = @{ zh = \"正在重置 Worker: {0}...\"; en = \"Resetting Worker: {0}...\" }\n \"worker.exists\" = @{ zh = \"容器 '{0}' 已存在。使用 --reset 重新创建。\"; en = \"Container '{0}' already exists. Use --reset to recreate.\" }\n \"worker.starting\" = @{ zh = \"正在启动 Worker: {0}...\"; en = \"Starting Worker: {0}...\" }\n \"worker.skills_url\" = @{ zh = \" Skills API URL: {0}\"; en = \" Skills API URL: {0}\" }\n \"worker.started\" = @{ zh = \"=== Worker {0} 已启动!===\"; en = \"=== Worker {0} Started! ===\" }\n \"worker.container\" = @{ zh = \"容器: {0}\"; en = \"Container: {0}\" }\n \"worker.view_logs\" = @{ zh = \"查看日志: docker logs -f {0}\"; en = \"View logs: docker logs -f {0}\" }\n\n # --- Prompt function messages ---\n \"prompt.preset\" = @{ zh = \" {0} = (已通过环境变量预设)\"; en = \" {0} = (pre-set via env)\" }\n \"prompt.upgrade_keep\" = @{ zh = \" {0} = {1}(当前值,回车保留 / 输入新值覆盖)\"; en = \" {0} = {1} (current value, press Enter to keep / type new value to change)\" }\n \"prompt.upgrade_empty\" = @{ zh = \" {0} = (未设置,回车跳过 / 输入新值设置)\"; en = \" {0} = (not set, press Enter to skip / type new value to set)\" }\n \"prompt.default\" = @{ zh = \" {0} = {1}(默认)\"; en = \" {0} = {1} (default)\" }\n \"prompt.required\" = @{ zh = \"{0} 是必需的(在非交互模式下通过环境变量设置)\"; en = \"{0} is required (set via environment variable in non-interactive mode)\" }\n \"prompt.required_empty\" = @{ zh = \"{0} 是必需的\"; en = \"{0} is required\" }\n\n # --- Language switch prompt (bilingual by design) ---\n \"lang.detected.zh\" = @{ zh = \"检测到语言 / Detected language: 中文\"; en = \"检测到语言 / Detected language: 中文\" }\n \"lang.detected.en\" = @{ zh = \"检测到语言 / Detected language: English\"; en = \"检测到语言 / Detected language: English\" }\n \"lang.switch_title\" = @{ zh = \"切换语言 / Switch language:\"; en = \"切换语言 / Switch language:\" }\n \"lang.option_zh\" = @{ zh = \" 1) 中文\"; en = \" 1) 中文\" }\n \"lang.option_en\" = @{ zh = \" 2) English\"; en = \" 2) English\" }\n \"lang.prompt\" = @{ zh = \"请选择 / Enter choice\"; en = \"请选择 / Enter choice\" }\n\n # --- Error messages ---\n \"error.name_required\" = @{ zh = \"--name 是必需的\"; en = \"--name is required\" }\n \"error.fs_required\" = @{ zh = \"--fs 是必需的\"; en = \"--fs is required\" }\n \"error.fs_key_required\" = @{ zh = \"--fs-key 是必需的\"; en = \"--fs-key is required\" }\n \"error.fs_secret_required\" = @{ zh = \"--fs-secret 是必需的\"; en = \"--fs-secret is required\" }\n \"error.unknown_option\" = @{ zh = \"未知选项: {0}\"; en = \"Unknown option: {0}\" }\n \"error.docker_not_running\" = @{ zh = \"Docker 未运行。请先启动 Docker Desktop 或 Podman Desktop。\"; en = \"Docker is not running. Please start Docker Desktop or Podman Desktop first.\" }\n \"error.docker_not_found\" = @{ zh = \"未找到 docker 或 podman 命令。请先安装 Docker Desktop 或 Podman Desktop:`n Docker Desktop: https://www.docker.com/products/docker-desktop/`n Podman Desktop: https://podman-desktop.io/\"; en = \"docker or podman command not found. Please install Docker Desktop or Podman Desktop first:`n Docker Desktop: https://www.docker.com/products/docker-desktop/`n Podman Desktop: https://podman-desktop.io/\" }\n\n # --- Uninstall messages ---\n \"uninstall.title\" = @{ zh = \"正在卸载 AgentTeams...\"; en = \"Uninstalling AgentTeams...\" }\n \"uninstall.stopping_manager\" = @{ zh = \"正在停止并移除 agentteams-manager...\"; en = \"Stopping and removing agentteams-manager...\" }\n \"uninstall.stopping_workers\" = @{ zh = \"正在停止并移除 worker 容器...\"; en = \"Stopping and removing worker containers...\" }\n \"uninstall.removed\" = @{ zh = \" 已移除: {0}\"; en = \" Removed: {0}\" }\n \"uninstall.removing_volume\" = @{ zh = \"正在移除 Docker 卷: agentteams-data\"; en = \"Removing Docker volume: agentteams-data\" }\n \"uninstall.removing_env\" = @{ zh = \"正在移除 env 文件: {0}\"; en = \"Removing env file: {0}\" }\n \"uninstall.removing_proxy\" = @{ zh = \"正在停止并移除 Docker API 代理容器: agentteams-docker-proxy\"; en = \"Stopping and removing Docker API proxy container: agentteams-docker-proxy\" }\n \"uninstall.stopping_controller\" = @{ zh = \"正在停止并移除 agentteams-controller (内嵌 Tuwunel/MinIO/Higress)...\"; en = \"Stopping and removing agentteams-controller (embedded Tuwunel/MinIO/Higress)...\" }\n \"uninstall.removing_network\" = @{ zh = \"正在移除 Docker 网络: agentteams-net\"; en = \"Removing Docker network: agentteams-net\" }\n \"uninstall.removing_workspace\" = @{ zh = \"正在移除工作空间目录: {0}\"; en = \"Removing workspace directory: {0}\" }\n \"uninstall.removing_log\" = @{ zh = \"正在移除日志文件: {0}\"; en = \"Removing log file: {0}\" }\n \"uninstall.done\" = @{ zh = \"AgentTeams 已卸载。\"; en = \"AgentTeams has been uninstalled.\" }\n}\n\n# Get-Msg: look up message by key, with -f style argument substitution.\n# Falls back to English if the current language translation is missing.\nfunction Get-Msg {\n param(\n [Parameter(Mandatory)][string]$Key,\n [object[]]$f\n )\n $lang = $script:AGENTTEAMS_LANGUAGE\n if (-not $lang) { $lang = \"en\" }\n $entry = $script:Messages[$Key]\n if (-not $entry) { return $Key }\n $text = $entry[$lang]\n if (-not $text) { $text = $entry[\"en\"] }\n if (-not $text) { return $Key }\n if ($f) { return ($text -f $f) }\n return $text\n}\n\nfunction Get-LanIP {\n # Detect local LAN IP address on Windows\n try {\n # Get network adapters with IPv4 addresses, prefer connected/active interfaces\n $adapters = Get-NetIPAddress -AddressFamily IPv4 -ErrorAction SilentlyContinue |\n Where-Object {\n $_.IPAddress -notlike \"127.*\" -and\n $_.PrefixOrigin -ne \"WellKnown\" -and\n $_.InterfaceAlias -notlike \"*Loopback*\"\n } |\n Sort-Object {\n # PowerShell 5.1 compatible: use if-else instead of ternary operator\n if ($_.InterfaceAlias -like \"*Wi-Fi*\" -or $_.InterfaceAlias -like \"*Ethernet*\") { 0 } else { 1 }\n }\n\n if ($adapters) {\n return $adapters[0].IPAddress\n }\n\n # Fallback: use ipconfig\n $ipconfig = ipconfig 2>$null\n $ip = ($ipconfig | Select-String \"IPv4 Address.*?: (\\d+\\.\\d+\\.\\d+\\.\\d+)\" | Select-Object -First 1)\n if ($ip -match \"(\\d+\\.\\d+\\.\\d+\\.\\d+)\") {\n return $Matches[1]\n }\n }\n catch {\n # Ignore errors\n }\n\n return \"\"\n}\n\n# Known models list — used to detect custom models during install\n$script:KnownModels = @(\n \"gpt-5.4\", \"gpt-5.3-codex\", \"gpt-5-mini\", \"gpt-5-nano\",\n \"claude-opus-4-6\", \"claude-sonnet-4-6\", \"claude-haiku-4-5\",\n \"qwen3.6-plus\", \"qwen3.5-plus\", \"deepseek-chat\", \"deepseek-reasoner\",\n \"kimi-k2.5\", \"glm-5\", \"MiniMax-M2.7\", \"MiniMax-M2.7-highspeed\", \"MiniMax-M2.5\"\n)\n\nfunction Test-KnownModel {\n param([string]$Model)\n return $script:KnownModels -contains $Model\n}\n\nfunction Request-CustomModelParams {\n param([string]$Model)\n if (Test-KnownModel $Model) {\n $script:config.MODEL_CONTEXT_WINDOW = \"\"\n $script:config.MODEL_MAX_TOKENS = \"\"\n $script:config.MODEL_REASONING = \"\"\n $script:config.MODEL_VISION = \"\"\n return\n }\n Write-Host \"\"\n Write-Log (Get-Msg \"llm.custom_model.detected\" -f $Model)\n Write-Host \"\"\n $ctxInput = Read-Host \" $(Get-Msg 'llm.custom_model.context_prompt')\"\n if ($ctxInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $script:config.MODEL_CONTEXT_WINDOW = if ($ctxInput) { $ctxInput } else { \"150000\" }\n $maxInput = Read-Host \" $(Get-Msg 'llm.custom_model.max_tokens_prompt')\"\n if ($maxInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $script:config.MODEL_MAX_TOKENS = if ($maxInput) { $maxInput } else { \"128000\" }\n $reasoningInput = Read-Host \" $(Get-Msg 'llm.custom_model.reasoning_prompt')\"\n if ($reasoningInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $script:config.MODEL_REASONING = if ($reasoningInput -match \"^[nN]\") { \"false\" } else { \"true\" }\n $visionInput = Read-Host \" $(Get-Msg 'llm.custom_model.vision_prompt')\"\n if ($visionInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $script:config.MODEL_VISION = if ($visionInput -match \"^[yY]\") { \"true\" } else { \"false\" }\n Write-Log (Get-Msg \"llm.custom_model.summary\" -f $script:config.MODEL_CONTEXT_WINDOW, $script:config.MODEL_MAX_TOKENS, $script:config.MODEL_REASONING, $script:config.MODEL_VISION)\n}\n\nfunction New-RandomKey {\n # Generate 64 character hex string (32 bytes)\n $bytes = New-Object byte[] 32\n $rng = [System.Security.Cryptography.RandomNumberGenerator]::Create()\n $rng.GetBytes($bytes)\n return [BitConverter]::ToString($bytes).Replace(\"-\", \"\").ToLower()\n}\n\nfunction ConvertTo-DockerPath {\n param([string]$Path)\n\n # Convert Windows path to Docker mount format\n $fullPath = (Resolve-Path $Path -ErrorAction SilentlyContinue).Path\n if (-not $fullPath) {\n $fullPath = $Path\n }\n\n # Convert C:\\path to /c/path format for Docker\n if ($fullPath -match \"^([A-Za-z]):\") {\n $drive = $Matches[1].ToLower()\n $rest = $fullPath.Substring(2).Replace(\"\\\", \"/\")\n return \"/$drive$rest\"\n }\n return $fullPath.Replace(\"\\\", \"/\")\n}\n\n# Resolve the embedded controller image. Embedded mode is the only supported\n# architecture since PR #616 (manager image no longer bundles Higress/Tuwunel/MinIO).\n# Mirrors install/agentteams-install.sh::resolve_embedded_image — fail fast when the\n# embedded image is unavailable rather than silently falling back to the broken\n# legacy single-container path.\n# Sets $script:EMBEDDED_IMAGE and $script:AGENTTEAMS_USE_EMBEDDED.\nfunction Resolve-EmbeddedImage {\n $script:AGENTTEAMS_USE_EMBEDDED = \"1\"\n\n # Explicit override always wins (used by `make install-embedded` for local builds).\n if ($env:AGENTTEAMS_INSTALL_EMBEDDED_IMAGE) {\n $script:EMBEDDED_IMAGE = $env:AGENTTEAMS_INSTALL_EMBEDDED_IMAGE\n return\n }\n\n $versioned = \"$($script:AGENTTEAMS_REGISTRY)/agentteams/agentteams-embedded:$($script:AGENTTEAMS_VERSION)\"\n $latestTag = \"$($script:AGENTTEAMS_REGISTRY)/agentteams/agentteams-embedded:latest\"\n\n if ($script:AGENTTEAMS_VERSION -eq \"latest\") {\n $script:EMBEDDED_IMAGE = $latestTag\n return\n }\n\n docker pull $versioned *>$null\n if ($LASTEXITCODE -eq 0) {\n $script:EMBEDDED_IMAGE = $versioned\n return\n }\n docker pull $latestTag *>$null\n if ($LASTEXITCODE -eq 0) {\n Write-Log \"embedded $($script:AGENTTEAMS_VERSION) not found, using latest\"\n $script:EMBEDDED_IMAGE = $latestTag\n return\n }\n\n # Escape hatch for older versions (AGENTTEAMS_VERSION <= v1.0.9) whose manager image\n # still bundled the infrastructure — opt-in only, never silent.\n if ($env:AGENTTEAMS_FORCE_LEGACY -eq \"1\") {\n Write-Log \"WARNING: AGENTTEAMS_FORCE_LEGACY=1 - using legacy all-in-one manager architecture.\"\n Write-Log \"WARNING: This requires AGENTTEAMS_VERSION <= v1.0.9 (older bundled manager image).\"\n Write-Log \"WARNING: Newer slim manager images will hang on 'Waiting for Higress Gateway'.\"\n $script:AGENTTEAMS_USE_EMBEDDED = \"0\"\n return\n }\n\n Write-Host \"$($script:ESC)[31m[AgentTeams ERROR]$($script:ESC)[0m Embedded controller image is not available in the registry:\" -ForegroundColor Red\n Write-Host \" - tried: $versioned\" -ForegroundColor Red\n Write-Host \" - tried: $latestTag\" -ForegroundColor Red\n Write-Host \"\" -ForegroundColor Red\n Write-Host \"Embedded mode is the only supported architecture since PR #616.\" -ForegroundColor Red\n Write-Host \"How to resolve:\" -ForegroundColor Red\n Write-Host \" 1) Pin to a AGENTTEAMS_VERSION whose embedded image has been published, or\" -ForegroundColor Red\n Write-Host \" wait for the release pipeline to publish it.\" -ForegroundColor Red\n Write-Host \" 2) For a local build, run: make install-embedded\" -ForegroundColor Red\n Write-Host \" (builds and uses the local embedded image without touching the registry).\" -ForegroundColor Red\n Write-Host \" 3) Override with a custom image:\" -ForegroundColor Red\n Write-Host \" `$env:AGENTTEAMS_INSTALL_EMBEDDED_IMAGE=...; .\\agentteams-install.ps1\" -ForegroundColor Red\n Exit-Script 1\n}\n\nfunction Wait-ManagerReady {\n param(\n [string]$Container = \"agentteams-manager\",\n [int]$Timeout = $(if ($env:AGENTTEAMS_READY_TIMEOUT) { [int]$env:AGENTTEAMS_READY_TIMEOUT } else { 300 })\n )\n\n $elapsed = 0\n Write-Log (Get-Msg \"install.wait_ready\" -f $Timeout)\n\n $runtime = if ($script:config.MANAGER_RUNTIME) { $script:config.MANAGER_RUNTIME } else { \"copaw\" }\n\n while ($elapsed -lt $Timeout) {\n try {\n switch ($runtime) {\n \"copaw\" {\n $result = docker exec $Container curl -sf http://127.0.0.1:18799/api/agents 2>$null\n if ($result -match '\"agents\"') {\n Write-Log (Get-Msg \"install.wait_ready.ok\")\n return $true\n }\n }\n default {\n $result = docker exec $Container openclaw gateway health --json 2>$null\n if ($result -match '\"ok\"') {\n Write-Log (Get-Msg \"install.wait_ready.ok\")\n return $true\n }\n }\n }\n } catch {\n # Ignore errors during polling\n }\n\n Start-Sleep -Seconds 5\n $elapsed += 5\n Write-Host \"`r$($script:ESC)[36m[AgentTeams]$($script:ESC)[0m $(Get-Msg 'install.wait_ready.waiting' -f $elapsed, $Timeout)\" -NoNewline\n }\n\n Write-Host \"\"\n Write-Error (Get-Msg \"install.wait_ready.timeout\" -f $Timeout, $Container)\n}\n\nfunction Wait-MatrixReady {\n param(\n [string]$Container = \"agentteams-manager\",\n [int]$Timeout = $(if ($env:AGENTTEAMS_READY_TIMEOUT) { [int]$env:AGENTTEAMS_READY_TIMEOUT } else { 300 })\n )\n\n $elapsed = 0\n Write-Log (Get-Msg \"install.wait_matrix\" -f $Timeout)\n\n while ($elapsed -lt $Timeout) {\n try {\n $result = docker exec $Container curl -sf http://127.0.0.1:6167/_tuwunel/server_version 2>$null\n if ($result) {\n Write-Log (Get-Msg \"install.wait_matrix.ok\")\n return $true\n }\n } catch {\n # Ignore errors during polling\n }\n\n Start-Sleep -Seconds 5\n $elapsed += 5\n Write-Host \"`r$($script:ESC)[36m[AgentTeams]$($script:ESC)[0m $(Get-Msg 'install.wait_matrix.waiting' -f $elapsed, $Timeout)\" -NoNewline\n }\n\n Write-Host \"\"\n Write-Error (Get-Msg \"install.wait_matrix.timeout\" -f $Timeout, $Container)\n}\n\n# Read KEY=value from /data/agentteams-secrets.env on a Docker volume (manager container not required).\n# Requires $script:EMBEDDED_IMAGE (set by Resolve-EmbeddedImage before Install-Manager uses this).\nfunction Read-AgentTeamsSecretFromDataVolume {\n param(\n [string]$VolumeName,\n [string]$Key\n )\n if ([string]::IsNullOrEmpty($VolumeName) -or [string]::IsNullOrEmpty($Key) -or [string]::IsNullOrEmpty($script:EMBEDDED_IMAGE)) {\n return \"\"\n }\n $grepKey = [regex]::Escape($Key)\n $shCmd = \"grep \"\"^${grepKey}=\"\" /data/agentteams-secrets.env 2>/dev/null | cut -d= -f2- | head -1 | tr -d '\\r'\"\n $out = docker run --rm --entrypoint sh -v \"${VolumeName}:/data:ro\" $script:EMBEDDED_IMAGE -c $shCmd 2>$null\n if ($null -eq $out) { return \"\" }\n return ($out | Out-String).Trim()\n}\n\n# Read KEY=value from /data/worker-creds/<worker>.env on a Docker volume.\nfunction Read-AgentTeamsWorkerCredsFromVolume {\n param(\n [string]$VolumeName,\n [string]$WorkerName,\n [string]$Key\n )\n if ([string]::IsNullOrEmpty($VolumeName) -or [string]::IsNullOrEmpty($WorkerName) -or [string]::IsNullOrEmpty($Key) -or [string]::IsNullOrEmpty($script:EMBEDDED_IMAGE)) {\n return \"\"\n }\n $grepKey = [regex]::Escape($Key)\n $path = \"/data/worker-creds/${WorkerName}.env\"\n $shCmd = \"grep \"\"^${grepKey}=\"\" \"\"${path}\"\" 2>/dev/null | cut -d= -f2- | head -1 | tr -d '\\r'\"\n $out = docker run --rm --entrypoint sh -v \"${VolumeName}:/data:ro\" $script:EMBEDDED_IMAGE -c $shCmd 2>$null\n if ($null -eq $out) { return \"\" }\n return ($out | Out-String).Trim()\n}\n\n# Read admin_dm_room_id from host workspace state.json (fallback when Matrix API is unavailable).\nfunction Read-AgentTeamsAdminDmRoomFromWorkspace {\n param([string]$WorkspaceDir)\n if ([string]::IsNullOrEmpty($WorkspaceDir)) { return \"\" }\n $statePath = Join-Path $WorkspaceDir \"state.json\"\n if (-not (Test-Path $statePath)) { return \"\" }\n try {\n $j = Get-Content $statePath -Raw -ErrorAction Stop | ConvertFrom-Json\n $rid = $j.admin_dm_room_id\n if ($null -eq $rid) { return \"\" }\n $s = [string]$rid\n if ($s -eq \"\" -or $s -eq \"null\") { return \"\" }\n return $s.Trim()\n } catch {\n return \"\"\n }\n}\n\nfunction Get-AgentTeamsRandomHex {\n param([int]$ByteCount)\n $bytes = New-Object byte[] $ByteCount\n $rng = [System.Security.Cryptography.RandomNumberGenerator]::Create()\n $rng.GetBytes($bytes)\n return ([BitConverter]::ToString($bytes)).Replace(\"-\", \"\").ToLower()\n}\n\n# Resolve admin DM room with @manager (small room) via Matrix Client API inside agentteams-manager.\nfunction Get-AgentTeamsAdminDmRoomViaMatrix {\n param(\n [string]$AdminUser,\n [string]$AdminPassword\n )\n $running = docker ps --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-manager$\"\n if (-not $running) { return \"\" }\n if ([string]::IsNullOrEmpty($AdminPassword)) { return \"\" }\n\n $loginObj = @{\n type = \"m.login.password\"\n identifier = @{ type = \"m.id.user\"; user = $AdminUser }\n password = $AdminPassword\n }\n $loginJson = $loginObj | ConvertTo-Json -Compress -Depth 5\n try {\n $loginRaw = $loginJson | docker exec -i agentteams-manager sh -c \"curl -sf -X POST http://127.0.0.1:6167/_matrix/client/v3/login -H 'Content-Type: application/json' -d @-\" 2>$null\n if (-not $loginRaw) { return \"\" }\n $loginResp = $loginRaw | ConvertFrom-Json\n $token = [string]$loginResp.access_token\n if ([string]::IsNullOrEmpty($token)) { return \"\" }\n\n $roomsRaw = docker exec agentteams-manager curl -sf -X GET -H \"Authorization: Bearer $token\" `\n \"http://127.0.0.1:6167/_matrix/client/v3/joined_rooms\" 2>$null\n if (-not $roomsRaw) { return \"\" }\n $roomsObj = $roomsRaw | ConvertFrom-Json\n $roomList = @($roomsObj.joined_rooms)\n foreach ($roomId in $roomList) {\n if ([string]::IsNullOrEmpty($roomId)) { continue }\n $enc = $roomId.Replace(\"!\", \"%21\")\n $memRaw = docker exec agentteams-manager curl -sf -X GET -H \"Authorization: Bearer $token\" `\n \"http://127.0.0.1:6167/_matrix/client/v3/rooms/${enc}/members\" 2>$null\n if (-not $memRaw) { continue }\n $memObj = $memRaw | ConvertFrom-Json\n $chunk = @($memObj.chunk)\n $memberIds = @($chunk | ForEach-Object { $_.state_key })\n $match = $false\n foreach ($m in $memberIds) {\n $beforeColon = ($m -split \":\")[0]\n if ($m -like \"*manager*\" -and $beforeColon -notlike \"*admin*\") {\n $match = $true\n break\n }\n }\n if ($match -and $memberIds.Count -le 3) {\n return $roomId\n }\n }\n } catch {\n return \"\"\n }\n return \"\"\n}\n\nfunction New-EnvFile {\n param([hashtable]$Config, [string]$Path)\n\n $content = @\"\n# AgentTeams Manager Configuration\n# Generated by agentteams-install.ps1 on $(Get-Date -Format \"yyyy-MM-dd HH:mm:ss\")\n\n# Language\nAGENTTEAMS_LANGUAGE=$($Config.LANGUAGE)\n\n# LLM\nAGENTTEAMS_LLM_PROVIDER=$($Config.LLM_PROVIDER)\nAGENTTEAMS_DEFAULT_MODEL=$($Config.DEFAULT_MODEL)\nAGENTTEAMS_LLM_API_KEY=$($Config.LLM_API_KEY)\nAGENTTEAMS_OPENAI_BASE_URL=$($Config.OPENAI_BASE_URL)\nAGENTTEAMS_MODEL_CONTEXT_WINDOW=$($Config.MODEL_CONTEXT_WINDOW)\nAGENTTEAMS_MODEL_MAX_TOKENS=$($Config.MODEL_MAX_TOKENS)\nAGENTTEAMS_MODEL_REASONING=$($Config.MODEL_REASONING)\nAGENTTEAMS_MODEL_VISION=$($Config.MODEL_VISION)\n\n# Embedding model (empty = disabled, default: text-embedding-v4)\nAGENTTEAMS_EMBEDDING_MODEL=$($Config.EMBEDDING_MODEL)\n\n# Admin\nAGENTTEAMS_ADMIN_USER=$($Config.ADMIN_USER)\nAGENTTEAMS_ADMIN_PASSWORD=$($Config.ADMIN_PASSWORD)\n\n# Ports\nAGENTTEAMS_LOCAL_ONLY=$($Config.LOCAL_ONLY)\nAGENTTEAMS_PORT_GATEWAY=$($Config.PORT_GATEWAY)\nAGENTTEAMS_PORT_CONSOLE=$($Config.PORT_CONSOLE)\nAGENTTEAMS_PORT_ELEMENT_WEB=$($Config.PORT_ELEMENT_WEB)\nAGENTTEAMS_PORT_MANAGER_CONSOLE=$($Config.PORT_MANAGER_CONSOLE)\n\n# Matrix\nAGENTTEAMS_MATRIX_DOMAIN=$($Config.MATRIX_DOMAIN)\nAGENTTEAMS_MATRIX_CLIENT_DOMAIN=$($Config.MATRIX_CLIENT_DOMAIN)\n\n# Gateway\nAGENTTEAMS_AI_GATEWAY_DOMAIN=$($Config.AI_GATEWAY_DOMAIN)\nAGENTTEAMS_MANAGER_GATEWAY_KEY=$($Config.MANAGER_GATEWAY_KEY)\n\n# File System\nAGENTTEAMS_FS_DOMAIN=$($Config.FS_DOMAIN)\nAGENTTEAMS_CONSOLE_DOMAIN=$($Config.CONSOLE_DOMAIN)\nAGENTTEAMS_MINIO_USER=$($Config.MINIO_USER)\nAGENTTEAMS_MINIO_PASSWORD=$($Config.MINIO_PASSWORD)\n\n# Internal\nAGENTTEAMS_MANAGER_PASSWORD=$($Config.MANAGER_PASSWORD)\nAGENTTEAMS_REGISTRATION_TOKEN=$($Config.REGISTRATION_TOKEN)\n\n# GitHub (optional)\nAGENTTEAMS_GITHUB_TOKEN=$($Config.GITHUB_TOKEN)\n\n# Nacos package import defaults\nAGENTTEAMS_NACOS_REGISTRY_URI=$(if ($env:AGENTTEAMS_NACOS_REGISTRY_URI) { $env:AGENTTEAMS_NACOS_REGISTRY_URI } else { \"nacos://market.agentteams.io:80/public\" })\nAGENTTEAMS_NACOS_USERNAME=$($env:AGENTTEAMS_NACOS_USERNAME)\nAGENTTEAMS_NACOS_PASSWORD=$($env:AGENTTEAMS_NACOS_PASSWORD)\nAGENTTEAMS_NACOS_TOKEN=$($env:AGENTTEAMS_NACOS_TOKEN)\n\n# Skills Registry (optional, default: nacos://market.agentteams.io:80/public)\nAGENTTEAMS_SKILLS_API_URL=$(if ($Config.SKILLS_API_URL) { $Config.SKILLS_API_URL } else { \"nacos://market.agentteams.io:80/public\" })\n\n# OpenClaw CMS plugin (optional)\nAGENTTEAMS_CMS_TRACES_ENABLED=$(if ($env:AGENTTEAMS_CMS_TRACES_ENABLED) { $env:AGENTTEAMS_CMS_TRACES_ENABLED } else { \"false\" })\nAGENTTEAMS_CMS_ENDPOINT=$($env:AGENTTEAMS_CMS_ENDPOINT)\nAGENTTEAMS_CMS_LICENSE_KEY=$($env:AGENTTEAMS_CMS_LICENSE_KEY)\nAGENTTEAMS_CMS_PROJECT=$($env:AGENTTEAMS_CMS_PROJECT)\nAGENTTEAMS_CMS_WORKSPACE=$($env:AGENTTEAMS_CMS_WORKSPACE)\nAGENTTEAMS_CMS_SERVICE_NAME=$(if ($env:AGENTTEAMS_CMS_SERVICE_NAME) { $env:AGENTTEAMS_CMS_SERVICE_NAME } else { \"agentteams-manager\" })\nAGENTTEAMS_CMS_METRICS_ENABLED=$(if ($env:AGENTTEAMS_CMS_METRICS_ENABLED) { $env:AGENTTEAMS_CMS_METRICS_ENABLED } else { \"false\" })\n\n# Worker images (for direct container creation)\nAGENTTEAMS_WORKER_IMAGE=$($Config.WORKER_IMAGE)\nAGENTTEAMS_COPAW_WORKER_IMAGE=$($Config.COPAW_WORKER_IMAGE)\nAGENTTEAMS_QWENPAW_WORKER_IMAGE=$($Config.QWENPAW_WORKER_IMAGE)\nAGENTTEAMS_HERMES_WORKER_IMAGE=$($Config.HERMES_WORKER_IMAGE)\n\n# Manager runtime (openclaw | copaw)\nAGENTTEAMS_MANAGER_RUNTIME=$($Config.MANAGER_RUNTIME)\n\n# Default Worker runtime (openclaw | copaw | hermes)\nAGENTTEAMS_DEFAULT_WORKER_RUNTIME=$($Config.DEFAULT_WORKER_RUNTIME)\n\n# Matrix E2EE (0=disabled, 1=enabled; default: 0)\nAGENTTEAMS_MATRIX_E2EE=$($Config.MATRIX_E2EE)\n\n# Docker API proxy (0=disabled, 1=enabled; default: 1)\nAGENTTEAMS_DOCKER_PROXY=$($Config.DOCKER_PROXY)\n\n# Docker API proxy: additional allowed image sources (comma-separated)\nAGENTTEAMS_PROXY_ALLOWED_REGISTRIES=$($Config.PROXY_ALLOWED_REGISTRIES)\n\n# Worker idle timeout in minutes (default: 720 = 12 hours)\nAGENTTEAMS_WORKER_IDLE_TIMEOUT=$($Config.WORKER_IDLE_TIMEOUT)\n\n# JVM Args for Higress Console\nJVM_ARGS=$($env:JVM_ARGS)\n\n# Higress WASM plugin image registry (auto-selected by timezone)\nHIGRESS_ADMIN_WASM_PLUGIN_IMAGE_REGISTRY=$($Config.REGISTRY)\n\n# Data persistence\nAGENTTEAMS_DATA_DIR=$($Config.DATA_DIR)\n# Manager workspace (skills, memory, state - host-editable)\nAGENTTEAMS_WORKSPACE_DIR=$($Config.WORKSPACE_DIR)\n# Host directory sharing\nAGENTTEAMS_HOST_SHARE_DIR=$($Config.HOST_SHARE_DIR)\n\"@\n\n Set-Content -Path $Path -Value $content -Encoding UTF8\n Write-Log (Get-Msg \"install.config_saved\" -f $Path)\n}\n\n# ============================================================\n# Prompt Functions\n# ============================================================\n\nfunction Read-Prompt {\n param(\n [string]$VarName,\n [string]$PromptText,\n [string]$Default = \"\",\n [switch]$Secret,\n [switch]$Optional\n )\n\n # Check if already set in environment\n $envValue = [Environment]::GetEnvironmentVariable($VarName)\n if ($envValue) {\n if ($script:AGENTTEAMS_UPGRADE -and -not $script:AGENTTEAMS_NON_INTERACTIVE) {\n # Show current value (masked for secrets) and let user change it\n $displayValue = $envValue\n if ($Secret) {\n if ($envValue.Length -le 8) {\n $displayValue = \"****\"\n } else {\n $displayValue = $envValue.Substring(0, 4) + \"****\" + $envValue.Substring($envValue.Length - 4)\n }\n }\n Write-Log (Get-Msg \"prompt.upgrade_keep\" -f $PromptText, $displayValue)\n $prompt = $PromptText\n if ($Secret) {\n $newValue = Read-Host -Prompt $prompt -AsSecureString\n $newValue = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto(\n [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($newValue)\n )\n } else {\n $newValue = Read-Host -Prompt $prompt\n if ($newValue -eq \"b\") { $script:StepResult = \"back\"; return $null }\n }\n if ($newValue) {\n return $newValue\n }\n return $envValue\n }\n Write-Log (Get-Msg \"prompt.preset\" -f $PromptText)\n return $envValue\n }\n # Upgrade mode: optional fields with empty value — let user set a new value\n elseif ($Optional -and $script:AGENTTEAMS_UPGRADE -and -not $script:AGENTTEAMS_NON_INTERACTIVE) {\n Write-Log (Get-Msg \"prompt.upgrade_empty\" -f $PromptText)\n $prompt = $PromptText\n if ($Secret) {\n $newValue = Read-Host -Prompt $prompt -AsSecureString\n $newValue = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto(\n [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($newValue)\n )\n } else {\n $newValue = Read-Host -Prompt $prompt\n if ($newValue -eq \"b\") { $script:StepResult = \"back\"; return $null }\n }\n if ($newValue) {\n return $newValue\n }\n return \"\"\n }\n\n # Non-interactive or quickstart mode\n if ($script:AGENTTEAMS_NON_INTERACTIVE -or $script:AGENTTEAMS_QUICKSTART) {\n if ($Default) {\n Write-Log (Get-Msg \"prompt.default\" -f $PromptText, $Default)\n return $Default\n }\n elseif ($Optional) {\n return \"\"\n }\n elseif ($script:AGENTTEAMS_NON_INTERACTIVE) {\n # Only hard-error in fully non-interactive mode, not quickstart\n Write-Error (Get-Msg \"prompt.required\" -f $PromptText)\n }\n # quickstart + no default + not optional: fall through to interactive prompt\n }\n\n # Interactive prompt\n $prompt = if ($Default) { \"$PromptText [$Default]\" } else { $PromptText }\n\n if ($Secret) {\n $value = Read-Host -Prompt $prompt -AsSecureString\n $value = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto(\n [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($value)\n )\n }\n else {\n $value = Read-Host -Prompt $prompt\n if ($value -eq \"b\") { $script:StepResult = \"back\"; return $null }\n }\n\n if (-not $value -and $Default) {\n $value = $Default\n }\n\n if (-not $value -and -not $Optional) {\n Write-Error (Get-Msg \"prompt.required_empty\" -f $PromptText)\n }\n\n return $value\n}\n\n# Load current parameter values from env file for upgrade mode display\nfunction Load-CurrentParamsFromEnv {\n $envFile = $script:AGENTTEAMS_ENV_FILE\n if (Test-Path $envFile) {\n $content = Get-Content $envFile\n $content | ForEach-Object {\n if ($_ -match \"^AGENTTEAMS_LLM_PROVIDER=(.*)$\") {\n $script:config.LLM_PROVIDER = $Matches[1].Trim()\n }\n if ($_ -match \"^AGENTTEAMS_OPENAI_BASE_URL=(.*)$\") {\n $script:config.OPENAI_BASE_URL = $Matches[1].Trim()\n }\n if ($_ -match \"^AGENTTEAMS_DEFAULT_MODEL=(.*)$\") {\n $script:config.DEFAULT_MODEL = $Matches[1].Trim()\n }\n if ($_ -match \"^AGENTTEAMS_EMBEDDING_MODEL=(.*)$\") {\n $script:config.EMBEDDING_MODEL = $Matches[1].Trim()\n }\n if ($_ -match \"^AGENTTEAMS_WORKSPACE_DIR=(.*)$\") {\n $script:config.WORKSPACE_DIR = $Matches[1].Trim()\n }\n if ($_ -match \"^AGENTTEAMS_HOST_SHARE_DIR=(.*)$\") {\n $script:config.HOST_SHARE_DIR = $Matches[1].Trim()\n }\n }\n }\n}\n\n# ============================================================\n# OpenAI-Compatible Provider\n# ============================================================\n\nfunction Test-LlmConnectivity {\n param(\n [string]$BaseUrl,\n [string]$ApiKey,\n [string]$Model,\n [string]$Hint = \"\"\n )\n Write-Log (Get-Msg \"llm.openai.test.testing\")\n $uri = ($BaseUrl.TrimEnd('/')) + \"/chat/completions\"\n $bodyHash = @{\n model = $Model\n messages = @(@{ role = \"user\"; content = \"hi\" })\n }\n $body = $bodyHash | ConvertTo-Json -Compress\n try {\n $response = Invoke-WebRequest -Uri $uri -Method POST `\n -Headers @{ \"Authorization\" = \"Bearer $ApiKey\"; \"Content-Type\" = \"application/json\"; \"User-Agent\" = \"AgentTeams/$($script:AGENTTEAMS_VERSION)\" } `\n -Body $body -TimeoutSec 30 -ErrorAction Stop -UseBasicParsing\n Write-Log (Get-Msg \"llm.openai.test.ok\")\n } catch {\n $statusCode = if ($_.Exception.Response) { [int]$_.Exception.Response.StatusCode } else { 0 }\n $responseBody = \"\"\n if ($_.Exception.Response) {\n try {\n $stream = $_.Exception.Response.GetResponseStream()\n $reader = New-Object System.IO.StreamReader($stream)\n $responseBody = $reader.ReadToEnd()\n } catch {}\n }\n Write-Host (Get-Msg \"llm.openai.test.fail\" -f $statusCode, $responseBody) -ForegroundColor Yellow\n if ($Hint) {\n Write-Host $Hint -ForegroundColor Yellow\n }\n if (-not $script:AGENTTEAMS_NON_INTERACTIVE) {\n $confirm = Read-Host (Get-Msg \"llm.openai.test.confirm\")\n if ($confirm -eq \"b\" -or $confirm -eq \"B\") {\n $script:StepResult = \"back\"\n return\n }\n if ($confirm -ne \"y\" -and $confirm -ne \"Y\") {\n Write-Log (Get-Msg \"llm.openai.test.aborted\")\n Exit-Script 1\n }\n }\n }\n}\n\nfunction Test-EmbeddingConnectivity {\n param(\n [string]$BaseUrl,\n [string]$ApiKey,\n [string]$Model\n )\n Write-Log (Get-Msg \"llm.embedding.test.testing\")\n $uri = ($BaseUrl.TrimEnd('/')) + \"/embeddings\"\n $bodyHash = @{\n model = $Model\n input = \"test\"\n }\n $body = $bodyHash | ConvertTo-Json -Compress\n try {\n $response = Invoke-WebRequest -Uri $uri -Method POST `\n -Headers @{ \"Authorization\" = \"Bearer $ApiKey\"; \"Content-Type\" = \"application/json\"; \"User-Agent\" = \"AgentTeams/$($script:AGENTTEAMS_VERSION)\" } `\n -Body $body -TimeoutSec 30 -ErrorAction Stop -UseBasicParsing\n Write-Log (Get-Msg \"llm.embedding.test.ok\")\n return $true\n } catch {\n $statusCode = if ($_.Exception.Response) { [int]$_.Exception.Response.StatusCode } else { 0 }\n $responseBody = \"\"\n if ($_.Exception.Response) {\n try {\n $stream = $_.Exception.Response.GetResponseStream()\n $reader = New-Object System.IO.StreamReader($stream)\n $responseBody = $reader.ReadToEnd()\n } catch {}\n }\n Write-Host (Get-Msg \"llm.embedding.test.fail\" -f $statusCode, $responseBody) -ForegroundColor Yellow\n return $false\n }\n}\n\nfunction New-OpenAICompatProvider {\n param(\n [string]$BaseUrl,\n [string]$ApiKey,\n [int]$ConsolePort = 18001\n )\n\n if (-not $BaseUrl -or -not $ApiKey) {\n Write-Log (Get-Msg \"install.openai_compat.missing\")\n return $false\n }\n\n $consoleUrl = \"http://localhost:$ConsolePort\"\n\n # Parse base URL\n $protocol = \"https\"\n $port = 443\n $urlWithoutProto = $BaseUrl -replace \"^https?://\", \"\"\n\n if ($BaseUrl -match \"^http://\") {\n $protocol = \"http\"\n $port = 80\n }\n\n $domain = $urlWithoutProto.Split(\"/\")[0]\n\n if ($domain -match \":(\\d+)$\") {\n $port = [int]$Matches[1]\n $domain = $domain -replace \":\\d+$\", \"\"\n }\n\n Write-Log (Get-Msg \"install.openai_compat.creating\")\n Write-Log (Get-Msg \"install.openai_compat.domain\" -f $domain)\n Write-Log (Get-Msg \"install.openai_compat.port\" -f $port)\n Write-Log (Get-Msg \"install.openai_compat.protocol\" -f $protocol)\n\n $serviceName = \"openai-compat\"\n\n # Create DNS service source\n $serviceBody = @{\n type = \"dns\"\n name = $serviceName\n port = $port.ToString()\n protocol = $protocol\n proxyName = \"\"\n domain = $domain\n } | ConvertTo-Json -Compress\n\n try {\n Invoke-RestMethod -Uri \"$consoleUrl/v1/service-sources\" -Method POST -ContentType \"application/json\" -Body $serviceBody -ErrorAction SilentlyContinue | Out-Null\n }\n catch {\n Write-Log (Get-Msg \"install.openai_compat.service_fail\")\n }\n\n Start-Sleep -Seconds 2\n\n # Create AI provider\n $providerBody = @{\n type = \"openai\"\n name = \"openai-compat\"\n tokens = @($ApiKey)\n version = 0\n protocol = \"openai/v1\"\n tokenFailoverConfig = @{ enabled = $false }\n rawConfigs = @{\n openaiCustomUrl = $BaseUrl\n openaiCustomServiceName = \"$serviceName.dns\"\n openaiCustomServicePort = $port\n }\n } | ConvertTo-Json -Compress -Depth 3\n\n try {\n Invoke-RestMethod -Uri \"$consoleUrl/v1/ai/providers\" -Method POST -ContentType \"application/json\" -Body $providerBody -ErrorAction SilentlyContinue | Out-Null\n Write-Log (Get-Msg \"install.openai_compat.success\")\n return $true\n }\n catch {\n Write-Log (Get-Msg \"install.openai_compat.provider_fail\")\n return $false\n }\n}\n\n# ============================================================\n# State Machine Helpers\n# ============================================================\n\nfunction Test-ShouldSkipStep {\n param([string]$StepFn)\n switch ($StepFn) {\n { $_ -in @(\"Step-Lang\", \"Step-Mode\") } {\n return $script:AGENTTEAMS_NON_INTERACTIVE\n }\n \"Step-Existing\" {\n return (-not (Test-Path $script:AGENTTEAMS_ENV_FILE))\n }\n # Keep-All upgrade mode: skip all config steps, values are already loaded\n { $_ -in @(\"Step-Llm\", \"Step-Admin\", \"Step-Network\", \"Step-Ports\", \"Step-Domains\",\n \"Step-Github\", \"Step-Skills\",\n \"Step-Runtime\", \"Step-ManagerRuntime\", \"Step-E2ee\", \"Step-DockerProxy\",\n \"Step-Idle\", \"Step-Hostshare\") } {\n if ($script:AGENTTEAMS_UPGRADE -and $env:AGENTTEAMS_UPGRADE_KEEP_ALL -eq \"1\") { return $true }\n return $false\n }\n { $_ -in @(\"Step-Volume\", \"Step-Workspace\") } {\n if ($script:AGENTTEAMS_UPGRADE -and $env:AGENTTEAMS_UPGRADE_KEEP_ALL -eq \"1\") { return $true }\n if ($script:AGENTTEAMS_NON_INTERACTIVE) { return $true }\n if ($script:AGENTTEAMS_QUICKSTART) { return $true }\n return $false\n }\n { $_ -in @(\"Step-E2ee\", \"Step-Idle\") } {\n if ($script:AGENTTEAMS_NON_INTERACTIVE) { return $true }\n if ($script:AGENTTEAMS_QUICKSTART -and -not $script:AGENTTEAMS_UPGRADE) { return $true }\n return $false\n }\n \"Step-DockerProxy\" {\n # Embedded controller IS the proxy/orchestrator — no separate proxy step needed.\n if ($script:AGENTTEAMS_USE_EMBEDDED -eq \"1\") { return $true }\n if ($script:AGENTTEAMS_NON_INTERACTIVE) { return $true }\n if ($script:AGENTTEAMS_QUICKSTART -and -not $script:AGENTTEAMS_UPGRADE) { return $true }\n return $false\n }\n \"Step-ManagerRuntime\" {\n if ($script:AGENTTEAMS_NON_INTERACTIVE) { return $true }\n return $false\n }\n \"Step-Hostshare\" {\n if ($script:AGENTTEAMS_NON_INTERACTIVE) { return $true }\n if ($script:AGENTTEAMS_QUICKSTART) { return $true }\n return $false\n }\n default { return $false }\n }\n}\n\nfunction Clear-StepVars {\n param([string]$StepFn)\n switch ($StepFn) {\n \"Step-Mode\" { $script:AGENTTEAMS_QUICKSTART = $false }\n \"Step-Existing\" {\n $script:AGENTTEAMS_UPGRADE = $false\n $script:UPGRADE_EXISTING_WORKERS = $null\n }\n \"Step-Llm\" {\n foreach ($k in @(\"AGENTTEAMS_LLM_PROVIDER\",\"AGENTTEAMS_DEFAULT_MODEL\",\"AGENTTEAMS_OPENAI_BASE_URL\",\n \"AGENTTEAMS_LLM_API_KEY\",\"AGENTTEAMS_MODEL_CONTEXT_WINDOW\",\"AGENTTEAMS_MODEL_MAX_TOKENS\",\n \"AGENTTEAMS_MODEL_REASONING\",\"AGENTTEAMS_MODEL_VISION\")) {\n [Environment]::SetEnvironmentVariable($k, $null, \"Process\")\n }\n }\n \"Step-Admin\" {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_ADMIN_USER\", $null, \"Process\")\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_ADMIN_PASSWORD\", $null, \"Process\")\n }\n \"Step-Network\" {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_LOCAL_ONLY\", $null, \"Process\")\n }\n \"Step-Ports\" {\n foreach ($k in @(\"AGENTTEAMS_PORT_GATEWAY\",\"AGENTTEAMS_PORT_CONSOLE\",\"AGENTTEAMS_PORT_ELEMENT_WEB\",\"AGENTTEAMS_PORT_MANAGER_CONSOLE\")) {\n [Environment]::SetEnvironmentVariable($k, $null, \"Process\")\n }\n }\n \"Step-Domains\" {\n foreach ($k in @(\"AGENTTEAMS_MATRIX_DOMAIN\",\"AGENTTEAMS_MATRIX_CLIENT_DOMAIN\",\"AGENTTEAMS_AI_GATEWAY_DOMAIN\",\"AGENTTEAMS_FS_DOMAIN\",\"AGENTTEAMS_CONSOLE_DOMAIN\")) {\n [Environment]::SetEnvironmentVariable($k, $null, \"Process\")\n }\n }\n \"Step-Github\" {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_GITHUB_TOKEN\", $null, \"Process\")\n }\n \"Step-Skills\" {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_SKILLS_API_URL\", $null, \"Process\")\n }\n \"Step-Volume\" {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_DATA_DIR\", $null, \"Process\")\n }\n \"Step-Workspace\" {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_WORKSPACE_DIR\", $null, \"Process\")\n }\n \"Step-Runtime\" {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_DEFAULT_WORKER_RUNTIME\", $null, \"Process\")\n }\n \"Step-ManagerRuntime\" {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_MANAGER_RUNTIME\", $null, \"Process\")\n }\n \"Step-E2ee\" {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_MATRIX_E2EE\", $null, \"Process\")\n }\n \"Step-DockerProxy\" {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_DOCKER_PROXY\", $null, \"Process\")\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_PROXY_ALLOWED_REGISTRIES\", $null, \"Process\")\n }\n \"Step-Idle\" {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_WORKER_IDLE_TIMEOUT\", $null, \"Process\")\n }\n \"Step-Hostshare\" {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_HOST_SHARE_DIR\", $null, \"Process\")\n }\n }\n}\n\n# ============================================================\n# Individual step functions\n# ============================================================\n\nfunction Step-Lang {\n $langDefaultChoice = if ($script:AGENTTEAMS_LANGUAGE -eq \"zh\") { \"1\" } else { \"2\" }\n $langDetectedKey = \"lang.detected.$($script:AGENTTEAMS_LANGUAGE)\"\n Write-Log (Get-Msg $langDetectedKey)\n Write-Log (Get-Msg \"lang.switch_title\")\n Write-Host (Get-Msg \"lang.option_zh\")\n Write-Host (Get-Msg \"lang.option_en\")\n Write-Host \"\"\n $langChoice = Read-Host \"$(Get-Msg 'lang.prompt') [$langDefaultChoice]\"\n if (-not $langChoice) { $langChoice = $langDefaultChoice }\n if ($langChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n switch ($langChoice) {\n \"1\" { $script:AGENTTEAMS_LANGUAGE = \"zh\" }\n \"2\" { $script:AGENTTEAMS_LANGUAGE = \"en\" }\n }\n $env:AGENTTEAMS_LANGUAGE = $script:AGENTTEAMS_LANGUAGE\n Write-Log \"\"\n}\n\nfunction Step-Mode {\n Write-Log (Get-Msg \"install.mode.title\")\n Write-Host \"\"\n Write-Host (Get-Msg \"install.mode.choose\")\n Write-Host (Get-Msg \"install.mode.quickstart\")\n Write-Host (Get-Msg \"install.mode.manual\")\n Write-Host \"\"\n $choice = Read-Host (Get-Msg \"install.mode.prompt\")\n $choice = if ($choice) { $choice } else { \"1\" }\n if ($choice -eq \"b\") { $script:StepResult = \"back\"; return }\n switch -Regex ($choice) {\n \"^(1|quick|quickstart)$\" {\n Write-Log (Get-Msg \"install.mode.quickstart_selected\")\n $script:AGENTTEAMS_QUICKSTART = $true\n }\n \"^(2|manual)$\" {\n Write-Log (Get-Msg \"install.mode.manual_selected\")\n $script:AGENTTEAMS_QUICKSTART = $false\n }\n default {\n Write-Log (Get-Msg \"install.mode.invalid\")\n $script:AGENTTEAMS_QUICKSTART = $true\n }\n }\n Write-Log \"\"\n}\n\nfunction Step-Existing {\n # This step is skipped when env file doesn't exist\n Write-Log (Get-Msg \"install.existing.detected\" -f $script:AGENTTEAMS_ENV_FILE)\n\n $runningManager = docker ps --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-manager$\"\n $runningWorkers = docker ps --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-worker-\"\n $existingWorkers = docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-worker-\"\n\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n Write-Log (Get-Msg \"install.existing.upgrade_noninteractive\")\n $upgradeChoice = \"1\"\n } else {\n Write-Host \"\"\n Write-Host (Get-Msg \"install.existing.choose\")\n Write-Host (Get-Msg \"install.existing.upgrade\")\n Write-Host (Get-Msg \"install.existing.reinstall\")\n Write-Host (Get-Msg \"install.existing.cancel\")\n Write-Host \"\"\n $upgradeChoice = Read-Host (Get-Msg \"install.existing.prompt\")\n $upgradeChoice = if ($upgradeChoice) { $upgradeChoice } else { \"1\" }\n if ($upgradeChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n }\n\n switch -Regex ($upgradeChoice) {\n \"^(1|upgrade)$\" {\n $script:AGENTTEAMS_UPGRADE = $true\n Write-Log (Get-Msg \"install.existing.upgrading\")\n\n # Show upgrade mode sub-menu (unless already set via env var)\n if ($env:AGENTTEAMS_UPGRADE_KEEP_ALL -ne \"1\") {\n Write-Host \"\"\n Write-Host (Get-Msg \"upgrade.mode.prompt\")\n Write-Host (Get-Msg \"upgrade.mode.keep_all\")\n Write-Host (Get-Msg \"upgrade.mode.confirm_each\")\n Write-Host (Get-Msg \"upgrade.mode.back\")\n Write-Host \"\"\n $upgradeModeChoice = Read-Host (Get-Msg \"install.existing.prompt\")\n $upgradeModeChoice = if ($upgradeModeChoice) { $upgradeModeChoice } else { \"1\" }\n switch -Regex ($upgradeModeChoice) {\n \"^(1|keep)$\" {\n $env:AGENTTEAMS_UPGRADE_KEEP_ALL = \"1\"\n }\n \"^(2|confirm)$\" {\n $env:AGENTTEAMS_UPGRADE_KEEP_ALL = \"0\"\n }\n \"^(3|b)$\" {\n $script:StepResult = \"back\"\n return\n }\n default {\n $env:AGENTTEAMS_UPGRADE_KEEP_ALL = \"0\"\n }\n }\n }\n # Load current parameters for both Keep-All and confirm-each modes\n Load-CurrentParamsFromEnv\n\n if ($runningManager -or $runningWorkers) {\n Write-Host \"\"\n Write-Host \"$($script:ESC)[33m$(Get-Msg 'install.existing.warn_manager_stop')$($script:ESC)[0m\"\n if ($existingWorkers) {\n Write-Host \"$($script:ESC)[33m$(Get-Msg 'install.existing.warn_worker_recreate')$($script:ESC)[0m\"\n }\n if (-not $script:AGENTTEAMS_NON_INTERACTIVE) {\n $confirm = Read-Host (Get-Msg \"install.existing.continue_prompt\")\n if ($confirm -eq \"b\" -or $confirm -eq \"B\") { $script:StepResult = \"back\"; return }\n if ($confirm -ne \"y\" -and $confirm -ne \"Y\") {\n Write-Log (Get-Msg \"install.existing.cancelled\")\n exit 0\n }\n }\n }\n $script:UPGRADE_EXISTING_WORKERS = $existingWorkers\n }\n \"^(2|reinstall)$\" {\n Write-Log (Get-Msg \"install.reinstall.performing\")\n $existingWorkspace = \"$env:USERPROFILE\\agentteams-manager\"\n if (Test-Path $script:AGENTTEAMS_ENV_FILE) {\n $envContent = Get-Content $script:AGENTTEAMS_ENV_FILE\n $wsLine = $envContent | Select-String \"^AGENTTEAMS_WORKSPACE_DIR=\"\n if ($wsLine) {\n $existingWorkspace = $wsLine.Line.Substring(21)\n }\n }\n Write-Host \"\"\n Write-Host \"$($script:ESC)[33m$(Get-Msg 'install.reinstall.warn_stop')$($script:ESC)[0m\"\n if ($runningManager) { Write-Host \"$($script:ESC)[33m - agentteams-manager (manager)$($script:ESC)[0m\" }\n $runningWorkers | ForEach-Object { Write-Host \"$($script:ESC)[33m - $_ (worker)$($script:ESC)[0m\" }\n Write-Host \"\"\n Write-Host \"$($script:ESC)[31m$(Get-Msg 'install.reinstall.warn_delete')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[31m$(Get-Msg 'install.reinstall.warn_volume')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[31m$(Get-Msg 'install.reinstall.warn_env' -f $script:AGENTTEAMS_ENV_FILE)$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[31m$(Get-Msg 'install.reinstall.warn_workspace' -f $existingWorkspace)$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[31m$(Get-Msg 'install.reinstall.warn_workers')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[31m$(Get-Msg 'install.reinstall.warn_proxy')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[31m$(Get-Msg 'install.reinstall.warn_network')$($script:ESC)[0m\"\n Write-Host \"\"\n Write-Host \"$($script:ESC)[31m$(Get-Msg 'install.reinstall.confirm_type')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[31m $existingWorkspace$($script:ESC)[0m\"\n Write-Host \"\"\n $confirmPath = Read-Host (Get-Msg \"install.reinstall.confirm_path\")\n if ($confirmPath -ne $existingWorkspace) {\n Write-Error (Get-Msg \"install.reinstall.path_mismatch\" -f $confirmPath, $existingWorkspace)\n }\n Write-Log (Get-Msg \"install.reinstall.confirmed\")\n docker stop agentteams-manager *>$null\n docker rm agentteams-manager *>$null\n docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-worker-\" | ForEach-Object {\n docker stop $_ *>$null\n docker rm $_ *>$null\n Write-Log (Get-Msg \"install.reinstall.removed_worker\" -f $_)\n }\n $existingController = docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-controller$\"\n if ($existingController) {\n Write-Log (Get-Msg \"install.reinstall.removing_proxy\")\n docker stop agentteams-controller *>$null\n docker rm agentteams-controller *>$null\n }\n if (docker volume ls -q 2>$null | Select-String \"^agentteams-data$\") {\n Write-Log (Get-Msg \"install.reinstall.removing_volume\")\n docker volume rm agentteams-data *>$null\n }\n if (Test-Path $existingWorkspace) {\n Write-Log (Get-Msg \"install.reinstall.removing_workspace\" -f $existingWorkspace)\n Remove-Item -Recurse -Force $existingWorkspace\n }\n if (Test-Path $script:AGENTTEAMS_ENV_FILE) {\n Write-Log (Get-Msg \"install.reinstall.removing_env\" -f $script:AGENTTEAMS_ENV_FILE)\n Remove-Item -Force $script:AGENTTEAMS_ENV_FILE\n }\n $existingNetwork = docker network ls --format \"{{.Name}}\" 2>$null | Select-String \"^agentteams-net$\"\n if ($existingNetwork) {\n Write-Log (Get-Msg \"install.reinstall.removing_network\")\n docker network rm agentteams-net *>$null\n }\n Write-Log (Get-Msg \"install.reinstall.cleanup_done\")\n }\n default {\n Write-Log (Get-Msg \"install.existing.cancelled\")\n exit 0\n }\n }\n\n # Load existing env file (upgrade path)\n if (Test-Path $script:AGENTTEAMS_ENV_FILE) {\n Write-Log (Get-Msg \"install.loading_config\" -f $script:AGENTTEAMS_ENV_FILE)\n Get-Content $script:AGENTTEAMS_ENV_FILE | ForEach-Object {\n if ($_ -match \"^([^#=][^=]*)=(.*)$\") {\n $key = $Matches[1].Trim()\n $value = $Matches[2].Split(\"#\")[0].Trim()\n if (-not [Environment]::GetEnvironmentVariable($key)) {\n [Environment]::SetEnvironmentVariable($key, $value, \"Process\")\n }\n }\n }\n }\n}\n\nfunction Step-Llm {\n Write-Log (Get-Msg \"llm.title\")\n\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n if ($script:AGENTTEAMS_LANGUAGE -eq \"zh\") {\n $script:config.LLM_PROVIDER = if ($env:AGENTTEAMS_LLM_PROVIDER) { $env:AGENTTEAMS_LLM_PROVIDER } else { \"openai-compat\" }\n $script:config.DEFAULT_MODEL = if ($env:AGENTTEAMS_DEFAULT_MODEL) { $env:AGENTTEAMS_DEFAULT_MODEL } else { \"qwen3.6-plus\" }\n $script:config.OPENAI_BASE_URL = if ($env:AGENTTEAMS_OPENAI_BASE_URL) { $env:AGENTTEAMS_OPENAI_BASE_URL } else { \"https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1\" }\n Write-Log (Get-Msg \"llm.provider.label\" -f $script:config.LLM_PROVIDER)\n Write-Log (Get-Msg \"llm.openai.base_url_label\" -f $script:config.OPENAI_BASE_URL)\n } else {\n $script:config.LLM_PROVIDER = if ($env:AGENTTEAMS_LLM_PROVIDER) { $env:AGENTTEAMS_LLM_PROVIDER } else { \"qwen\" }\n $script:config.DEFAULT_MODEL = if ($env:AGENTTEAMS_DEFAULT_MODEL) { $env:AGENTTEAMS_DEFAULT_MODEL } else { \"qwen3.6-plus\" }\n $script:config.OPENAI_BASE_URL = if ($env:AGENTTEAMS_OPENAI_BASE_URL) { $env:AGENTTEAMS_OPENAI_BASE_URL } else { \"\" }\n Write-Log (Get-Msg \"llm.provider.qwen_default\" -f $script:config.LLM_PROVIDER)\n }\n Write-Log (Get-Msg \"llm.model.label\" -f $script:config.DEFAULT_MODEL)\n Write-Log \"\"\n $script:config.LLM_API_KEY = Read-Prompt -VarName \"AGENTTEAMS_LLM_API_KEY\" -PromptText (Get-Msg \"llm.apikey_prompt\") -Secret\n $script:config.EMBEDDING_MODEL = if ($null -ne $env:AGENTTEAMS_EMBEDDING_MODEL) { $env:AGENTTEAMS_EMBEDDING_MODEL } else { \"text-embedding-v4\" }\n return\n }\n\n Write-Host \"\"\n Write-Host (Get-Msg \"llm.providers_title\")\n Write-Host (Get-Msg \"llm.provider.alibaba\")\n Write-Host (Get-Msg \"llm.provider.openai_compat\")\n Write-Host \"\"\n\n # If upgrade mode with loaded provider, show current as default\n if ($script:AGENTTEAMS_UPGRADE -and $script:config.LLM_PROVIDER) {\n $defaultProvider = if ($script:config.LLM_PROVIDER -eq \"openai-compat\") { \"2\" } else { \"1\" }\n $providerChoice = Read-Host \"$(Get-Msg 'llm.provider.select') [${defaultProvider}]\"\n $providerChoice = if ($providerChoice) { $providerChoice } else { $defaultProvider }\n } elseif ($script:AGENTTEAMS_QUICKSTART) {\n $providerChoice = Read-Host \"$(Get-Msg 'llm.provider.select') [1]\"\n } else {\n $providerChoice = Read-Host (Get-Msg \"llm.provider.select\")\n $providerChoice = if ($providerChoice) { $providerChoice } else { \"1\" }\n }\n if ($providerChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n\n switch -Regex ($providerChoice) {\n \"^(1|alibaba-cloud)$\" {\n $alibabaAccess = $null\n if ($script:AGENTTEAMS_LANGUAGE -eq \"en\") {\n $script:config.LLM_PROVIDER = \"openai-compat\"\n $script:config.OPENAI_BASE_URL = \"https://dashscope-intl.aliyuncs.com/compatible-mode/v1\"\n $alibabaAccess = \"tokenplan\"\n Write-Host \"\"\n Write-Host (Get-Msg \"llm.codingplan.models_title\")\n Write-Host (Get-Msg \"llm.codingplan.model.qwen36plus\")\n Write-Host (Get-Msg \"llm.codingplan.model.glm5\")\n Write-Host (Get-Msg \"llm.codingplan.model.kimi\")\n Write-Host (Get-Msg \"llm.codingplan.model.minimax\")\n Write-Host \"\"\n # If upgrade with loaded model, show current as default\n if ($script:AGENTTEAMS_UPGRADE -and $script:config.DEFAULT_MODEL) {\n $defaultModel = switch ($script:config.DEFAULT_MODEL) {\n \"qwen3.6-plus\" { \"1\" }\n \"glm-5\" { \"2\" }\n \"kimi-k2.5\" { \"3\" }\n \"MiniMax-M2.5\" { \"4\" }\n default { \"1\" }\n }\n $codingPlanModelChoice = Read-Host \"$(Get-Msg 'llm.codingplan.model.select') [${defaultModel}]\"\n $codingPlanModelChoice = if ($codingPlanModelChoice) { $codingPlanModelChoice } else { $defaultModel }\n } elseif ($script:AGENTTEAMS_QUICKSTART) {\n $codingPlanModelChoice = Read-Host \"$(Get-Msg 'llm.codingplan.model.select') [1]\"\n } else {\n $codingPlanModelChoice = Read-Host (Get-Msg \"llm.codingplan.model.select\")\n }\n $codingPlanModelChoice = if ($codingPlanModelChoice) { $codingPlanModelChoice } else { \"1\" }\n if ($codingPlanModelChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n switch -Regex ($codingPlanModelChoice) {\n \"^(1|qwen3\\.6-plus)$\" { $script:config.DEFAULT_MODEL = \"qwen3.6-plus\" }\n \"^(2|glm-5)$\" { $script:config.DEFAULT_MODEL = \"glm-5\" }\n \"^(3|kimi-k2\\.5)$\" { $script:config.DEFAULT_MODEL = \"kimi-k2.5\" }\n \"^(4|MiniMax-M2\\.5)$\" { $script:config.DEFAULT_MODEL = \"MiniMax-M2.5\" }\n default { $script:config.DEFAULT_MODEL = \"qwen3.6-plus\" }\n }\n Write-Log (Get-Msg \"llm.provider.selected_codingplan\")\n } else {\n Write-Host \"\"\n Write-Host (Get-Msg \"llm.alibaba.models_title\")\n Write-Host (Get-Msg \"llm.alibaba.model.tokenplan\")\n Write-Host (Get-Msg \"llm.alibaba.model.bailian\")\n Write-Host (Get-Msg \"llm.alibaba.model.codingplan_legacy\")\n Write-Host \"\"\n $modelChoice = if ($script:AGENTTEAMS_QUICKSTART) {\n Read-Host \"$(Get-Msg 'llm.alibaba.model.select') [1]\"\n } else {\n Read-Host (Get-Msg \"llm.alibaba.model.select\")\n }\n $modelChoice = if ($modelChoice) { $modelChoice } else { \"1\" }\n if ($modelChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n\n if ($modelChoice -match \"^(1|token-plan|tokenplan)$\") {\n $alibabaAccess = \"tokenplan\"\n $script:config.LLM_PROVIDER = \"openai-compat\"\n $script:config.OPENAI_BASE_URL = \"https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1\"\n Write-Host \"\"\n Write-Host (Get-Msg \"llm.codingplan.models_title\")\n Write-Host (Get-Msg \"llm.codingplan.model.qwen36plus\")\n Write-Host (Get-Msg \"llm.codingplan.model.glm5\")\n Write-Host (Get-Msg \"llm.codingplan.model.kimi\")\n Write-Host (Get-Msg \"llm.codingplan.model.minimax\")\n Write-Host \"\"\n # If upgrade with loaded model, show current as default\n if ($script:AGENTTEAMS_UPGRADE -and $script:config.DEFAULT_MODEL) {\n $defaultModel = switch ($script:config.DEFAULT_MODEL) {\n \"qwen3.6-plus\" { \"1\" }\n \"glm-5\" { \"2\" }\n \"kimi-k2.5\" { \"3\" }\n \"MiniMax-M2.5\" { \"4\" }\n default { \"1\" }\n }\n $codingPlanModelChoice = Read-Host \"$(Get-Msg 'llm.codingplan.model.select') [${defaultModel}]\"\n $codingPlanModelChoice = if ($codingPlanModelChoice) { $codingPlanModelChoice } else { $defaultModel }\n } elseif ($script:AGENTTEAMS_QUICKSTART) {\n $codingPlanModelChoice = Read-Host \"$(Get-Msg 'llm.codingplan.model.select') [1]\"\n } else {\n $codingPlanModelChoice = Read-Host (Get-Msg \"llm.codingplan.model.select\")\n }\n $codingPlanModelChoice = if ($codingPlanModelChoice) { $codingPlanModelChoice } else { \"1\" }\n if ($codingPlanModelChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n switch -Regex ($codingPlanModelChoice) {\n \"^(1|qwen3\\.6-plus)$\" { $script:config.DEFAULT_MODEL = \"qwen3.6-plus\" }\n \"^(2|glm-5)$\" { $script:config.DEFAULT_MODEL = \"glm-5\" }\n \"^(3|kimi-k2\\.5)$\" { $script:config.DEFAULT_MODEL = \"kimi-k2.5\" }\n \"^(4|MiniMax-M2\\.5)$\" { $script:config.DEFAULT_MODEL = \"MiniMax-M2.5\" }\n default { $script:config.DEFAULT_MODEL = \"qwen3.6-plus\" }\n }\n Write-Log (Get-Msg \"llm.provider.selected_tokenplan\")\n } elseif ($modelChoice -match \"^(2|qwen|bailian)$\") {\n $alibabaAccess = \"bailian\"\n $script:config.LLM_PROVIDER = \"qwen\"\n $script:config.OPENAI_BASE_URL = \"\"\n Write-Host \"\"\n if ($script:AGENTTEAMS_UPGRADE -and $script:config.DEFAULT_MODEL) {\n $qwenModelInput = Read-Host \"$(Get-Msg 'llm.qwen.model_prompt') [${script:config.DEFAULT_MODEL}]\"\n } else {\n $qwenModelInput = Read-Host (Get-Msg \"llm.qwen.model_prompt\")\n }\n if ($qwenModelInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $script:config.DEFAULT_MODEL = if ($qwenModelInput) { $qwenModelInput } elseif ($script:config.DEFAULT_MODEL) { $script:config.DEFAULT_MODEL } else { \"qwen3.6-plus\" }\n Write-Log (Get-Msg \"llm.provider.selected_qwen\")\n Request-CustomModelParams $script:config.DEFAULT_MODEL\n if ($script:StepResult -eq \"back\") { return }\n } elseif ($modelChoice -match \"^(3|coding-plan|codingplan)$\") {\n $alibabaAccess = \"codingplan_legacy\"\n $script:config.LLM_PROVIDER = \"openai-compat\"\n $script:config.OPENAI_BASE_URL = \"https://coding.dashscope.aliyuncs.com/v1\"\n Write-Host \"\"\n if ($script:AGENTTEAMS_UPGRADE -and $script:config.DEFAULT_MODEL) {\n $codingModelInput = Read-Host \"$(Get-Msg 'llm.qwen.model_prompt') [${script:config.DEFAULT_MODEL}]\"\n } else {\n $codingModelInput = Read-Host (Get-Msg \"llm.qwen.model_prompt\")\n }\n if ($codingModelInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $script:config.DEFAULT_MODEL = if ($codingModelInput) { $codingModelInput } elseif ($script:config.DEFAULT_MODEL) { $script:config.DEFAULT_MODEL } else { \"qwen3.6-plus\" }\n Write-Log (Get-Msg \"llm.provider.selected_codingplan_legacy\")\n Request-CustomModelParams $script:config.DEFAULT_MODEL\n if ($script:StepResult -eq \"back\") { return }\n } else {\n Write-Error (Get-Msg \"llm.alibaba.model.invalid\" -f $modelChoice)\n }\n }\n\n Write-Log (Get-Msg \"llm.model.label\" -f $script:config.DEFAULT_MODEL)\n Write-Log \"\"\n if ($alibabaAccess -eq \"bailian\") {\n Write-Log (Get-Msg \"llm.apikey_hint_bailian\")\n Write-Log (Get-Msg \"llm.apikey_url_bailian\")\n } elseif ($script:AGENTTEAMS_LANGUAGE -eq \"en\") {\n Write-Log (Get-Msg \"llm.apikey_hint_qwencloud\")\n Write-Log (Get-Msg \"llm.apikey_url_qwencloud\")\n } elseif ($alibabaAccess -eq \"codingplan_legacy\") {\n Write-Log (Get-Msg \"llm.apikey_hint_codingplan\")\n Write-Log (Get-Msg \"llm.apikey_url_codingplan\")\n } else {\n Write-Log (Get-Msg \"llm.apikey_hint_tokenplan\")\n Write-Log (Get-Msg \"llm.apikey_url_tokenplan\")\n }\n Write-Log \"\"\n $script:config.LLM_API_KEY = Read-Prompt -VarName \"AGENTTEAMS_LLM_API_KEY\" -PromptText (Get-Msg \"llm.apikey_prompt\") -Secret\n if ($script:StepResult -eq \"back\") { return }\n if ($alibabaAccess -eq \"bailian\") {\n Test-LlmConnectivity -BaseUrl \"https://dashscope.aliyuncs.com/compatible-mode/v1\" -ApiKey $script:config.LLM_API_KEY -Model $script:config.DEFAULT_MODEL\n } elseif ($alibabaAccess -eq \"codingplan_legacy\") {\n Test-LlmConnectivity -BaseUrl \"https://coding.dashscope.aliyuncs.com/v1\" -ApiKey $script:config.LLM_API_KEY -Model $script:config.DEFAULT_MODEL -Hint (Get-Msg \"llm.openai.test.fail.codingplan_legacy\")\n } elseif ($script:AGENTTEAMS_LANGUAGE -eq \"en\") {\n Test-LlmConnectivity -BaseUrl $script:config.OPENAI_BASE_URL -ApiKey $script:config.LLM_API_KEY -Model $script:config.DEFAULT_MODEL -Hint (Get-Msg \"llm.openai.test.fail.codingplan\")\n } else {\n Test-LlmConnectivity -BaseUrl $script:config.OPENAI_BASE_URL -ApiKey $script:config.LLM_API_KEY -Model $script:config.DEFAULT_MODEL -Hint (Get-Msg \"llm.openai.test.fail.tokenplan\")\n }\n if ($script:StepResult -eq \"back\") { return }\n }\n \"^(2|openai-compat)$\" {\n $script:config.LLM_PROVIDER = \"openai-compat\"\n Write-Log (Get-Msg \"llm.provider.selected_openai\" -f $script:config.LLM_PROVIDER)\n Write-Host \"\"\n if ($script:AGENTTEAMS_UPGRADE -and $env:AGENTTEAMS_UPGRADE_KEEP_ALL -eq \"1\") {\n Write-Log (Get-Msg \"prompt.upgrade_keep\" -f (Get-Msg \"llm.openai.base_url_prompt\"), $script:config.OPENAI_BASE_URL)\n } else {\n $currentUrl = $script:config.OPENAI_BASE_URL\n # Use base prompt without embedded default when showing current value\n if ($currentUrl) {\n $urlInput = Read-Host \"$(Get-Msg 'llm.openai.base_url_prompt') [$currentUrl]\"\n } else {\n $urlInput = Read-Host \"$(Get-Msg 'llm.openai.base_url_prompt') [https://api.openai.com/v1]\"\n }\n if ($urlInput -eq \"b\") { $script:StepResult = \"back\"; return }\n if ($urlInput) {\n $script:config.OPENAI_BASE_URL = $urlInput\n } elseif ($currentUrl) {\n $script:config.OPENAI_BASE_URL = $currentUrl\n } else {\n $script:config.OPENAI_BASE_URL = \"https://api.openai.com/v1\"\n }\n }\n if ($script:AGENTTEAMS_UPGRADE -and $env:AGENTTEAMS_UPGRADE_KEEP_ALL -eq \"1\") {\n Write-Log (Get-Msg \"prompt.upgrade_keep\" -f (Get-Msg \"llm.openai.model_prompt\"), $script:config.DEFAULT_MODEL)\n } else {\n $currentModel = $script:config.DEFAULT_MODEL\n if ($currentModel) {\n $modelInput = Read-Host \"$(Get-Msg 'llm.openai.model_prompt') [${currentModel}]\"\n } else {\n $modelInput = Read-Host (Get-Msg 'llm.openai.model_prompt')\n }\n if ($modelInput -eq \"b\") { $script:StepResult = \"back\"; return }\n if ($modelInput) {\n $script:config.DEFAULT_MODEL = $modelInput\n } elseif ($currentModel) {\n $script:config.DEFAULT_MODEL = $currentModel\n } else {\n $script:config.DEFAULT_MODEL = \"gpt-5.4\"\n }\n }\n Write-Log (Get-Msg \"llm.openai.base_url_label\" -f $script:config.OPENAI_BASE_URL)\n Write-Log (Get-Msg \"llm.model.label\" -f $script:config.DEFAULT_MODEL)\n Request-CustomModelParams $script:config.DEFAULT_MODEL\n if ($script:StepResult -eq \"back\") { return }\n Write-Log \"\"\n $script:config.LLM_API_KEY = Read-Prompt -VarName \"AGENTTEAMS_LLM_API_KEY\" -PromptText (Get-Msg \"llm.apikey_prompt\") -Secret\n if ($script:StepResult -eq \"back\") { return }\n Test-LlmConnectivity -BaseUrl $script:config.OPENAI_BASE_URL -ApiKey $script:config.LLM_API_KEY -Model $script:config.DEFAULT_MODEL\n if ($script:StepResult -eq \"back\") { return }\n }\n default {\n Write-Error (Get-Msg \"llm.provider.invalid\" -f $providerChoice)\n }\n }\n\n # Skip to embedding if Keep-All mode handled LLM params\n if ($skipToEmbedding) {\n # Skip embedding selection, use loaded value (already in $script:config.EMBEDDING_MODEL)\n if ($script:config.EMBEDDING_MODEL) {\n Write-Log (Get-Msg \"llm.embedding.title\")\n Write-Log (Get-Msg \"llm.model.label\" -f $script:config.EMBEDDING_MODEL)\n Write-Log \"\"\n }\n return\n }\n\n # --- Embedding model (optional, auto-tested) ---\n Write-Host \"\"\n Write-Log (Get-Msg \"llm.embedding.title\")\n Write-Log (Get-Msg \"llm.embedding.hint\")\n Write-Host \"\"\n Write-Host (Get-Msg \"llm.embedding.option.default\")\n Write-Host (Get-Msg \"llm.embedding.option.custom\")\n Write-Host (Get-Msg \"llm.embedding.option.disable\")\n Write-Host \"\"\n $embChoice = Read-Host \"$(Get-Msg 'llm.embedding.select') [1]\"\n $embChoice = if ($embChoice) { $embChoice } else { \"1\" }\n if ($embChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n\n switch ($embChoice) {\n \"1\" {\n $script:config.EMBEDDING_MODEL = \"text-embedding-v4\"\n }\n \"2\" {\n if ($script:AGENTTEAMS_UPGRADE -and $env:AGENTTEAMS_UPGRADE_KEEP_ALL -eq \"1\") {\n Write-Log (Get-Msg \"prompt.upgrade_keep\" -f (Get-Msg \"llm.embedding.custom_prompt\"), $script:config.EMBEDDING_MODEL)\n } else {\n $currentEmb = $script:config.EMBEDDING_MODEL\n $embCustom = Read-Host (Get-Msg \"llm.embedding.custom_prompt\")\n if ($embCustom -eq \"b\") { $script:StepResult = \"back\"; return }\n if ($embCustom) {\n $script:config.EMBEDDING_MODEL = $embCustom\n } else {\n $script:config.EMBEDDING_MODEL = $currentEmb\n if (-not $script:config.EMBEDDING_MODEL) {\n Write-Log (Get-Msg \"llm.embedding.disabled\")\n }\n }\n }\n }\n \"3\" {\n $script:config.EMBEDDING_MODEL = \"\"\n Write-Log (Get-Msg \"llm.embedding.disabled\")\n }\n default {\n $script:config.EMBEDDING_MODEL = \"text-embedding-v4\"\n }\n }\n\n if ($script:config.EMBEDDING_MODEL) {\n # Qwen provider uses dashscope directly; others use OPENAI_BASE_URL\n $embBaseUrl = $script:config.OPENAI_BASE_URL\n if ($script:config.LLM_PROVIDER -eq \"qwen\") {\n $embBaseUrl = \"https://dashscope.aliyuncs.com/compatible-mode/v1\"\n }\n $embResult = Test-EmbeddingConnectivity -BaseUrl $embBaseUrl -ApiKey $script:config.LLM_API_KEY -Model $script:config.EMBEDDING_MODEL\n if (-not $embResult) {\n $script:config.EMBEDDING_MODEL = \"\"\n Write-Log (Get-Msg \"llm.embedding.auto_disabled\")\n }\n }\n\n Write-Log \"\"\n}\n\nfunction Step-Admin {\n Write-Log (Get-Msg \"admin.title\")\n $script:config.ADMIN_USER = Read-Prompt -VarName \"AGENTTEAMS_ADMIN_USER\" -PromptText (Get-Msg \"admin.username_prompt\") -Default \"admin\"\n if ($script:StepResult -eq \"back\") { return }\n $script:config.ADMIN_USER = $script:config.ADMIN_USER.ToLowerInvariant()\n\n # Pre-set via env var: validate; non-interactive fails fast,\n # interactive warns and falls through to the retry prompt.\n if ($env:AGENTTEAMS_ADMIN_PASSWORD) {\n $script:config.ADMIN_PASSWORD = $env:AGENTTEAMS_ADMIN_PASSWORD\n Write-Log (Get-Msg \"prompt.preset\" -f (Get-Msg \"admin.password_prompt\"))\n if ($script:config.ADMIN_PASSWORD.Length -ge 8) {\n Write-Log \"\"\n return\n }\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n Write-Error (Get-Msg \"admin.password_too_short\" -f $script:config.ADMIN_PASSWORD.Length)\n }\n Write-Host \"$($script:ESC)[31m[AgentTeams ERROR]$($script:ESC)[0m $(Get-Msg \"admin.password_too_short\" -f $script:config.ADMIN_PASSWORD.Length)\"\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_ADMIN_PASSWORD\", $null, \"Process\")\n }\n\n while ($true) {\n [Environment]::SetEnvironmentVariable(\"AGENTTEAMS_ADMIN_PASSWORD\", $null, \"Process\")\n $script:config.ADMIN_PASSWORD = Read-Prompt -VarName \"AGENTTEAMS_ADMIN_PASSWORD\" -PromptText (Get-Msg \"admin.password_prompt\") -Secret -Optional\n if ($script:StepResult -eq \"back\") { return }\n if (-not $script:config.ADMIN_PASSWORD) {\n $randomSuffix = (New-RandomKey).Substring(0, 12)\n $script:config.ADMIN_PASSWORD = \"admin$randomSuffix\"\n Write-Log (Get-Msg \"admin.password_generated\")\n break\n }\n if ($script:config.ADMIN_PASSWORD.Length -ge 8) {\n break\n }\n Write-Host \"$($script:ESC)[31m[AgentTeams ERROR]$($script:ESC)[0m $(Get-Msg \"admin.password_too_short\" -f $script:config.ADMIN_PASSWORD.Length)\"\n }\n\n Write-Log \"\"\n}\n\nfunction Step-Network {\n Write-Log (Get-Msg \"port.local_only.title\")\n Write-Host \"\"\n Write-Host \" 1) $(Get-Msg 'port.local_only.hint_yes')\"\n Write-Host \" 2) $(Get-Msg 'port.local_only.hint_no')\"\n Write-Host \"\"\n\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n $localOnly = if ($env:AGENTTEAMS_LOCAL_ONLY) { $env:AGENTTEAMS_LOCAL_ONLY } else { \"1\" }\n } elseif ($null -ne $env:AGENTTEAMS_LOCAL_ONLY) {\n $localOnly = $env:AGENTTEAMS_LOCAL_ONLY\n } else {\n $localChoice = Read-Host \"$(Get-Msg 'port.local_only.choice')\"\n if ($localChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n if (-not $localChoice) { $localChoice = \"1\" }\n $localOnly = if ($localChoice -match '^(2|n|N|no|NO)$') { \"0\" } else { \"1\" }\n }\n $script:config.LOCAL_ONLY = $localOnly\n\n if ($localOnly -eq \"1\") {\n Write-Log (Get-Msg \"port.local_only.selected_local\")\n } else {\n Write-Log (Get-Msg \"port.local_only.selected_external\")\n Write-Host \"\"\n Write-Host (Get-Msg \"port.local_only.https_hint\") -ForegroundColor Yellow\n }\n Write-Log \"\"\n}\n\nfunction Step-Ports {\n Write-Log (Get-Msg \"port.title\")\n $script:config.PORT_GATEWAY = Read-Prompt -VarName \"AGENTTEAMS_PORT_GATEWAY\" -PromptText (Get-Msg \"port.gateway_prompt\") -Default \"18080\"\n if ($script:StepResult -eq \"back\") { return }\n $script:config.PORT_CONSOLE = Read-Prompt -VarName \"AGENTTEAMS_PORT_CONSOLE\" -PromptText (Get-Msg \"port.console_prompt\") -Default \"18001\"\n if ($script:StepResult -eq \"back\") { return }\n $script:config.PORT_ELEMENT_WEB = Read-Prompt -VarName \"AGENTTEAMS_PORT_ELEMENT_WEB\" -PromptText (Get-Msg \"port.element_prompt\") -Default \"18088\"\n if ($script:StepResult -eq \"back\") { return }\n $script:config.PORT_MANAGER_CONSOLE = Read-Prompt -VarName \"AGENTTEAMS_PORT_MANAGER_CONSOLE\" -PromptText (Get-Msg \"port.manager_console_prompt\") -Default \"18888\"\n if ($script:StepResult -eq \"back\") { return }\n Write-Log \"\"\n}\n\nfunction Step-Domains {\n Write-Log (Get-Msg \"domain.title\")\n Write-Log (Get-Msg \"domain.hint\")\n $script:config.MATRIX_DOMAIN = Read-Prompt -VarName \"AGENTTEAMS_MATRIX_DOMAIN\" -PromptText (Get-Msg \"domain.matrix_prompt\") -Default \"matrix-local.agentteams.io:$($script:config.PORT_GATEWAY)\"\n if ($script:StepResult -eq \"back\") { return }\n $script:config.MATRIX_CLIENT_DOMAIN = Read-Prompt -VarName \"AGENTTEAMS_MATRIX_CLIENT_DOMAIN\" -PromptText (Get-Msg \"domain.element_prompt\") -Default \"matrix-client-local.agentteams.io\"\n if ($script:StepResult -eq \"back\") { return }\n $script:config.AI_GATEWAY_DOMAIN = Read-Prompt -VarName \"AGENTTEAMS_AI_GATEWAY_DOMAIN\" -PromptText (Get-Msg \"domain.gateway_prompt\") -Default \"aigw-local.agentteams.io\"\n if ($script:StepResult -eq \"back\") { return }\n $script:config.FS_DOMAIN = Read-Prompt -VarName \"AGENTTEAMS_FS_DOMAIN\" -PromptText (Get-Msg \"domain.fs_prompt\") -Default \"fs-local.agentteams.io\"\n if ($script:StepResult -eq \"back\") { return }\n $script:config.CONSOLE_DOMAIN = Read-Prompt -VarName \"AGENTTEAMS_CONSOLE_DOMAIN\" -PromptText (Get-Msg \"domain.console_prompt\") -Default \"console-local.agentteams.io\"\n if ($script:StepResult -eq \"back\") { return }\n Write-Log \"\"\n}\n\nfunction Step-Github {\n Write-Log (Get-Msg \"github.title\")\n $script:config.GITHUB_TOKEN = Read-Prompt -VarName \"AGENTTEAMS_GITHUB_TOKEN\" -PromptText (Get-Msg \"github.token_prompt\") -Secret -Optional\n}\n\nfunction Step-Skills {\n Write-Log \"\"\n Write-Log (Get-Msg \"skills.title\")\n $script:config.SKILLS_API_URL = Read-Prompt -VarName \"AGENTTEAMS_SKILLS_API_URL\" -PromptText (Get-Msg \"skills.url_prompt\") -Optional\n if ($script:StepResult -eq \"back\") { return }\n Write-Log \"\"\n}\n\nfunction Step-Volume {\n Write-Log (Get-Msg \"data.title\")\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n $script:config.DATA_DIR = if ($env:AGENTTEAMS_DATA_DIR) { $env:AGENTTEAMS_DATA_DIR } else { \"agentteams-data\" }\n Write-Log \" $(Get-Msg 'data.volume_using' -f $script:config.DATA_DIR) (non-interactive, skipped)\"\n return\n }\n # ─────────────────────────────────────────────────────────────────\n $dataDirInput = Read-Host (Get-Msg \"data.volume_prompt\")\n if ($dataDirInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $script:config.DATA_DIR = if ($dataDirInput) { $dataDirInput } else { \"agentteams-data\" }\n Write-Log (Get-Msg \"data.volume_using\" -f $script:config.DATA_DIR)\n}\n\nfunction Step-Workspace {\n Write-Log (Get-Msg \"workspace.title\")\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n $script:config.WORKSPACE_DIR = if ($env:AGENTTEAMS_WORKSPACE_DIR) { $env:AGENTTEAMS_WORKSPACE_DIR } else { \"$env:USERPROFILE\\agentteams-manager\" }\n if (-not (Test-Path $script:config.WORKSPACE_DIR)) {\n New-Item -ItemType Directory -Path $script:config.WORKSPACE_DIR -Force | Out-Null\n }\n Write-Log \" $(Get-Msg 'workspace.dir_label' -f $script:config.WORKSPACE_DIR) (non-interactive, skipped)\"\n return\n }\n # ─────────────────────────────────────────────────────────────────\n $defaultWorkspace = \"$env:USERPROFILE\\agentteams-manager\"\n if ($script:AGENTTEAMS_UPGRADE -and $env:AGENTTEAMS_UPGRADE_KEEP_ALL -eq \"1\") {\n Write-Log (Get-Msg \"prompt.upgrade_keep\" -f (Get-Msg \"workspace.dir_prompt\" -f $defaultWorkspace), $script:config.WORKSPACE_DIR)\n $wsInput = if ($script:config.WORKSPACE_DIR) { $script:config.WORKSPACE_DIR } else { $defaultWorkspace }\n } else {\n $currentWs = $script:config.WORKSPACE_DIR\n $wsInput = Read-Host (Get-Msg \"workspace.dir_prompt\" -f $defaultWorkspace)\n if ($wsInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $wsInput = if ($wsInput) { $wsInput } else { if ($currentWs) { $currentWs } else { $defaultWorkspace } }\n }\n $script:config.WORKSPACE_DIR = $wsInput\n if (-not (Test-Path $script:config.WORKSPACE_DIR)) {\n New-Item -ItemType Directory -Path $script:config.WORKSPACE_DIR -Force | Out-Null\n }\n Write-Log (Get-Msg \"workspace.dir_label\" -f $script:config.WORKSPACE_DIR)\n}\n\nfunction Step-Runtime {\n Write-Log (Get-Msg \"worker_runtime.title\")\n Write-Host \"\"\n Write-Host \" 1) $(Get-Msg 'worker_runtime.copaw')\"\n Write-Host \" 2) $(Get-Msg 'worker_runtime.openclaw')\"\n Write-Host \" 3) $(Get-Msg 'worker_runtime.hermes')\"\n Write-Host \"\"\n\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n $script:config.DEFAULT_WORKER_RUNTIME = if ($env:AGENTTEAMS_DEFAULT_WORKER_RUNTIME) { $env:AGENTTEAMS_DEFAULT_WORKER_RUNTIME } else { \"copaw\" }\n } elseif ($script:AGENTTEAMS_UPGRADE -and $env:AGENTTEAMS_DEFAULT_WORKER_RUNTIME) {\n Write-Log (Get-Msg \"prompt.upgrade_keep\" -f (Get-Msg \"worker_runtime.title_short\"), $env:AGENTTEAMS_DEFAULT_WORKER_RUNTIME)\n $rtChoice = Read-Host (Get-Msg \"worker_runtime.choice\")\n if ($rtChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n if ($rtChoice) {\n $script:config.DEFAULT_WORKER_RUNTIME = switch ($rtChoice) {\n \"2\" { \"openclaw\" }\n \"3\" { \"hermes\" }\n default { \"copaw\" }\n }\n } else {\n $script:config.DEFAULT_WORKER_RUNTIME = $env:AGENTTEAMS_DEFAULT_WORKER_RUNTIME\n }\n } elseif ($env:AGENTTEAMS_DEFAULT_WORKER_RUNTIME) {\n $script:config.DEFAULT_WORKER_RUNTIME = $env:AGENTTEAMS_DEFAULT_WORKER_RUNTIME\n } else {\n $rtChoice = Read-Host (Get-Msg \"worker_runtime.choice\")\n if ($rtChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n $rtChoice = if ($rtChoice) { $rtChoice } else { \"1\" }\n $script:config.DEFAULT_WORKER_RUNTIME = switch ($rtChoice) {\n \"2\" { \"openclaw\" }\n \"3\" { \"hermes\" }\n default { \"copaw\" }\n }\n }\n Write-Log (Get-Msg \"worker_runtime.selected\" -f $script:config.DEFAULT_WORKER_RUNTIME)\n}\n\nfunction Step-ManagerRuntime {\n Write-Log (Get-Msg \"manager_runtime.title\")\n Write-Host \"\"\n Write-Host \" 1) $(Get-Msg 'manager_runtime.copaw')\"\n Write-Host \" 2) $(Get-Msg 'manager_runtime.openclaw')\"\n Write-Host \"\"\n\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n $script:config.MANAGER_RUNTIME = if ($env:AGENTTEAMS_MANAGER_RUNTIME) { $env:AGENTTEAMS_MANAGER_RUNTIME } else { \"copaw\" }\n } elseif ($script:AGENTTEAMS_UPGRADE -and $env:AGENTTEAMS_MANAGER_RUNTIME) {\n Write-Log (Get-Msg \"prompt.upgrade_keep\" -f (Get-Msg \"manager_runtime.title_short\"), $env:AGENTTEAMS_MANAGER_RUNTIME)\n $mrChoice = Read-Host (Get-Msg \"manager_runtime.choice\")\n if ($mrChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n if ($mrChoice) {\n $script:config.MANAGER_RUNTIME = if ($mrChoice -eq \"2\") { \"openclaw\" } else { \"copaw\" }\n } else {\n $script:config.MANAGER_RUNTIME = $env:AGENTTEAMS_MANAGER_RUNTIME\n }\n } elseif ($env:AGENTTEAMS_MANAGER_RUNTIME) {\n $script:config.MANAGER_RUNTIME = $env:AGENTTEAMS_MANAGER_RUNTIME\n } else {\n $mrChoice = Read-Host (Get-Msg \"manager_runtime.choice\")\n if ($mrChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n $mrChoice = if ($mrChoice) { $mrChoice } else { \"1\" }\n $script:config.MANAGER_RUNTIME = if ($mrChoice -eq \"2\") { \"openclaw\" } else { \"copaw\" }\n }\n Write-Log (Get-Msg \"manager_runtime.selected\" -f $script:config.MANAGER_RUNTIME)\n}\n\nfunction Step-E2ee {\n Write-Host \"\"\n Write-Log (Get-Msg \"matrix_e2ee.title\")\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n $script:config.MATRIX_E2EE = if ($env:AGENTTEAMS_MATRIX_E2EE) { $env:AGENTTEAMS_MATRIX_E2EE } else { \"0\" }\n Write-Log \" $(Get-Msg 'matrix_e2ee.title_short') = $($script:config.MATRIX_E2EE) (non-interactive, skipped)\"\n return\n }\n # ─────────────────────────────────────────────────────────────────\n Write-Host \"\"\n Write-Host \" $(Get-Msg 'matrix_e2ee.desc')\"\n Write-Host \"\"\n Write-Host \" 1) $(Get-Msg 'matrix_e2ee.disable')\"\n Write-Host \" 2) $(Get-Msg 'matrix_e2ee.enable')\"\n Write-Host \"\"\n\n if ($script:AGENTTEAMS_UPGRADE -and $env:AGENTTEAMS_MATRIX_E2EE) {\n $e2eeDisplay = if ($env:AGENTTEAMS_MATRIX_E2EE -eq \"1\") { Get-Msg \"matrix_e2ee.val_enabled\" } else { Get-Msg \"matrix_e2ee.val_disabled\" }\n Write-Log (Get-Msg \"prompt.upgrade_keep\" -f (Get-Msg \"matrix_e2ee.title_short\"), $e2eeDisplay)\n $e2eeChoice = Read-Host (Get-Msg \"matrix_e2ee.choice\")\n if ($e2eeChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n if ($e2eeChoice) {\n $script:config.MATRIX_E2EE = if ($e2eeChoice -eq \"2\") { \"1\" } else { \"0\" }\n } else {\n $script:config.MATRIX_E2EE = $env:AGENTTEAMS_MATRIX_E2EE\n }\n } elseif (-not $env:AGENTTEAMS_MATRIX_E2EE) {\n $e2eeChoice = Read-Host (Get-Msg \"matrix_e2ee.choice\")\n if ($e2eeChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n $e2eeChoice = if ($e2eeChoice) { $e2eeChoice } else { \"1\" }\n $script:config.MATRIX_E2EE = if ($e2eeChoice -eq \"2\") { \"1\" } else { \"0\" }\n } else {\n $script:config.MATRIX_E2EE = $env:AGENTTEAMS_MATRIX_E2EE\n }\n\n if ($script:config.MATRIX_E2EE -eq \"1\") {\n Write-Log (Get-Msg \"matrix_e2ee.selected_enabled\")\n } else {\n Write-Log (Get-Msg \"matrix_e2ee.selected_disabled\")\n }\n}\n\nfunction Step-DockerProxy {\n if (-not $script:AGENTTEAMS_MOUNT_SOCKET) {\n $script:config.DOCKER_PROXY = \"0\"\n return\n }\n\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n $script:config.DOCKER_PROXY = if ($env:AGENTTEAMS_DOCKER_PROXY) { $env:AGENTTEAMS_DOCKER_PROXY } else { \"0\" }\n Write-Log \" $(Get-Msg 'docker_proxy.title_short') = $($script:config.DOCKER_PROXY) (non-interactive, skipped)\"\n return\n }\n # ─────────────────────────────────────────────────────────────────\n\n Write-Host \"\"\n Write-Log (Get-Msg \"docker_proxy.title\")\n Write-Host \"\"\n Write-Host \" $(Get-Msg 'docker_proxy.desc')\"\n Write-Host \"\"\n Write-Host \" 1) $(Get-Msg 'docker_proxy.enable')\"\n Write-Host \" 2) $(Get-Msg 'docker_proxy.disable')\"\n Write-Host \"\"\n\n if ($script:AGENTTEAMS_UPGRADE -and $env:AGENTTEAMS_DOCKER_PROXY) {\n $proxyDisplay = if ($env:AGENTTEAMS_DOCKER_PROXY -eq \"1\") { Get-Msg \"docker_proxy.val_enabled\" } else { Get-Msg \"docker_proxy.val_disabled\" }\n Write-Log (Get-Msg \"prompt.upgrade_keep\" -f (Get-Msg \"docker_proxy.title_short\"), $proxyDisplay)\n $proxyChoice = Read-Host (Get-Msg \"docker_proxy.choice\")\n if ($proxyChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n if ($proxyChoice) {\n $script:config.DOCKER_PROXY = if ($proxyChoice -eq \"2\") { \"0\" } else { \"1\" }\n } else {\n $script:config.DOCKER_PROXY = $env:AGENTTEAMS_DOCKER_PROXY\n }\n } elseif (-not $env:AGENTTEAMS_DOCKER_PROXY) {\n $proxyChoice = Read-Host (Get-Msg \"docker_proxy.choice\")\n if ($proxyChoice -eq \"b\") { $script:StepResult = \"back\"; return }\n $proxyChoice = if ($proxyChoice) { $proxyChoice } else { \"1\" }\n $script:config.DOCKER_PROXY = if ($proxyChoice -eq \"2\") { \"0\" } else { \"1\" }\n } else {\n $script:config.DOCKER_PROXY = $env:AGENTTEAMS_DOCKER_PROXY\n }\n\n if ($script:config.DOCKER_PROXY -eq \"1\") {\n Write-Log (Get-Msg \"docker_proxy.selected_enabled\")\n\n # Prompt for additional allowed image sources\n Write-Host \"\"\n Write-Host \" $(Get-Msg 'docker_proxy.registries_desc')\"\n Write-Host \"\"\n if ($script:AGENTTEAMS_UPGRADE -and $env:AGENTTEAMS_PROXY_ALLOWED_REGISTRIES) {\n Write-Log (Get-Msg \"prompt.upgrade_keep\" -f (Get-Msg \"docker_proxy.registries_label\"), $env:AGENTTEAMS_PROXY_ALLOWED_REGISTRIES)\n $regInput = Read-Host (Get-Msg \"docker_proxy.registries_prompt\")\n if ($regInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $script:config.PROXY_ALLOWED_REGISTRIES = if ($regInput) { $regInput } else { $env:AGENTTEAMS_PROXY_ALLOWED_REGISTRIES }\n } elseif (-not $env:AGENTTEAMS_PROXY_ALLOWED_REGISTRIES) {\n $regInput = Read-Host (Get-Msg \"docker_proxy.registries_prompt\")\n if ($regInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $script:config.PROXY_ALLOWED_REGISTRIES = if ($regInput) { $regInput } else { \"\" }\n } else {\n $script:config.PROXY_ALLOWED_REGISTRIES = $env:AGENTTEAMS_PROXY_ALLOWED_REGISTRIES\n }\n } else {\n Write-Log (Get-Msg \"docker_proxy.selected_disabled\")\n }\n}\n\nfunction Step-Idle {\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n $script:config.WORKER_IDLE_TIMEOUT = if ($env:AGENTTEAMS_WORKER_IDLE_TIMEOUT) { $env:AGENTTEAMS_WORKER_IDLE_TIMEOUT } else { \"720\" }\n Write-Log \" $(Get-Msg 'idle_timeout.label') = $($script:config.WORKER_IDLE_TIMEOUT) (non-interactive, skipped)\"\n return\n }\n # ─────────────────────────────────────────────────────────────────\n if ($script:AGENTTEAMS_UPGRADE -and $env:AGENTTEAMS_WORKER_IDLE_TIMEOUT) {\n Write-Log (Get-Msg \"prompt.upgrade_keep\" -f (Get-Msg \"idle_timeout.label\"), $env:AGENTTEAMS_WORKER_IDLE_TIMEOUT)\n $idleInput = Read-Host (Get-Msg \"idle_timeout.prompt\")\n if ($idleInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $script:config.WORKER_IDLE_TIMEOUT = if ($idleInput) { $idleInput } else { $env:AGENTTEAMS_WORKER_IDLE_TIMEOUT }\n } elseif (-not $env:AGENTTEAMS_WORKER_IDLE_TIMEOUT) {\n $idleInput = Read-Host (Get-Msg \"idle_timeout.prompt\")\n if ($idleInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $script:config.WORKER_IDLE_TIMEOUT = if ($idleInput) { $idleInput } else { \"720\" }\n } else {\n $script:config.WORKER_IDLE_TIMEOUT = $env:AGENTTEAMS_WORKER_IDLE_TIMEOUT\n }\n Write-Log (Get-Msg \"idle_timeout.selected\" -f $script:config.WORKER_IDLE_TIMEOUT)\n Write-Log \"\"\n}\n\nfunction Step-Hostshare {\n # ── Non-interactive guard (deep defense) ──────────────────────────\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n $script:config.HOST_SHARE_DIR = if ($env:AGENTTEAMS_HOST_SHARE_DIR) { $env:AGENTTEAMS_HOST_SHARE_DIR } else { $env:USERPROFILE }\n Write-Log \" $(Get-Msg 'host_share.label') = $($script:config.HOST_SHARE_DIR) (non-interactive, skipped)\"\n return\n }\n # ─────────────────────────────────────────────────────────────────\n $currentShare = $script:config.HOST_SHARE_DIR\n if ($currentShare) {\n $shareInput = Read-Host \"$(Get-Msg 'host_share.prompt' -f $env:USERPROFILE) [${currentShare}]\"\n } else {\n $shareInput = Read-Host (Get-Msg 'host_share.prompt' -f $env:USERPROFILE)\n }\n if ($shareInput -eq \"b\") { $script:StepResult = \"back\"; return }\n $script:config.HOST_SHARE_DIR = if ($shareInput) { $shareInput } else { if ($currentShare) { $currentShare } else { $env:USERPROFILE } }\n}\n\n# ============================================================\n# Manager Installation\n# ============================================================\n\nfunction Install-Manager {\n Write-Log (Get-Msg \"install.title\")\n\n # Detect timezone\n $script:AGENTTEAMS_TIMEZONE = Get-AgentTeamsTimeZone\n\n # Language priority: env var > existing env file > timezone detection\n if ($env:AGENTTEAMS_LANGUAGE) {\n $script:AGENTTEAMS_LANGUAGE = $env:AGENTTEAMS_LANGUAGE\n } else {\n # Check existing env file for saved language preference (upgrade scenario)\n $_envFile = $script:AGENTTEAMS_ENV_FILE\n # Migrate from legacy location (current directory) if needed\n if (-not (Test-Path $_envFile) -and (Test-Path \".\\agentteams-manager.env\")) {\n Write-Log \"Migrating agentteams-manager.env to $_envFile...\"\n Move-Item \".\\agentteams-manager.env\" $_envFile -ErrorAction SilentlyContinue\n }\n if (Test-Path $_envFile) {\n $_savedLang = (Get-Content $_envFile | Select-String \"^AGENTTEAMS_LANGUAGE=\" | ForEach-Object {\n $_.Line -replace '^AGENTTEAMS_LANGUAGE=', ''\n } | Select-Object -First 1)\n if ($_savedLang) {\n $script:AGENTTEAMS_LANGUAGE = $_savedLang\n }\n }\n # Fall back to timezone-based detection\n if (-not $script:AGENTTEAMS_LANGUAGE) {\n $script:AGENTTEAMS_LANGUAGE = Get-AgentTeamsLanguage -Timezone $script:AGENTTEAMS_TIMEZONE\n }\n }\n $env:AGENTTEAMS_LANGUAGE = $script:AGENTTEAMS_LANGUAGE\n\n # Detect registry\n $script:AGENTTEAMS_REGISTRY = Get-Registry -Timezone $script:AGENTTEAMS_TIMEZONE\n\n # Set image names\n $script:MANAGER_IMAGE = if ($env:AGENTTEAMS_INSTALL_MANAGER_IMAGE) {\n $env:AGENTTEAMS_INSTALL_MANAGER_IMAGE\n } else {\n \"$($script:AGENTTEAMS_REGISTRY)/agentteams/agentteams-manager:$($script:AGENTTEAMS_VERSION)\"\n }\n\n $script:WORKER_IMAGE = if ($env:AGENTTEAMS_INSTALL_WORKER_IMAGE) {\n $env:AGENTTEAMS_INSTALL_WORKER_IMAGE\n } else {\n \"$($script:AGENTTEAMS_REGISTRY)/agentteams/agentteams-worker:$($script:AGENTTEAMS_VERSION)\"\n }\n\n $script:COPAW_WORKER_IMAGE = if ($env:AGENTTEAMS_INSTALL_COPAW_WORKER_IMAGE) {\n $env:AGENTTEAMS_INSTALL_COPAW_WORKER_IMAGE\n } else {\n \"$($script:AGENTTEAMS_REGISTRY)/agentteams/agentteams-copaw-worker:$($script:AGENTTEAMS_VERSION)\"\n }\n\n $script:QWENPAW_WORKER_IMAGE = if ($env:AGENTTEAMS_INSTALL_QWENPAW_WORKER_IMAGE) {\n $env:AGENTTEAMS_INSTALL_QWENPAW_WORKER_IMAGE\n } else {\n \"$($script:AGENTTEAMS_REGISTRY)/agentteams/agentteams-qwenpaw-worker:$($script:AGENTTEAMS_VERSION)\"\n }\n\n $script:HERMES_WORKER_IMAGE = if ($env:AGENTTEAMS_INSTALL_HERMES_WORKER_IMAGE) {\n $env:AGENTTEAMS_INSTALL_HERMES_WORKER_IMAGE\n } else {\n \"$($script:AGENTTEAMS_REGISTRY)/agentteams/agentteams-hermes-worker:$($script:AGENTTEAMS_VERSION)\"\n }\n\n $script:MANAGER_COPAW_IMAGE = if ($env:AGENTTEAMS_INSTALL_MANAGER_COPAW_IMAGE) {\n $env:AGENTTEAMS_INSTALL_MANAGER_COPAW_IMAGE\n } else {\n \"$($script:AGENTTEAMS_REGISTRY)/agentteams/agentteams-manager-copaw:$($script:AGENTTEAMS_VERSION)\"\n }\n\n $script:CONTROLLER_IMAGE = if ($env:AGENTTEAMS_INSTALL_CONTROLLER_IMAGE) {\n $env:AGENTTEAMS_INSTALL_CONTROLLER_IMAGE\n } else {\n \"$($script:AGENTTEAMS_REGISTRY)/agentteams/agentteams-controller:$($script:AGENTTEAMS_VERSION)\"\n }\n\n # Resolve embedded controller image (sets $script:EMBEDDED_IMAGE and\n # $script:AGENTTEAMS_USE_EMBEDDED). Errors out fast if no embedded image is available\n # for the requested version (mirrors the bash installer behavior).\n Resolve-EmbeddedImage\n\n Write-Log (Get-Msg \"install.registry\" -f $script:AGENTTEAMS_REGISTRY)\n Write-Log \"\"\n Write-Log (Get-Msg \"install.dir\" -f (Get-Location))\n Write-Log (Get-Msg \"install.dir_hint\")\n Write-Log (Get-Msg \"install.dir_hint2\")\n Write-Log \"\"\n\n # Check container runtime (docker or podman)\n if (-not (Get-Command \"docker\" -ErrorAction SilentlyContinue) -and\n -not (Get-Command \"podman\" -ErrorAction SilentlyContinue)) {\n Write-Host \"$($script:ESC)[31m[AgentTeams ERROR]$($script:ESC)[0m $(Get-Msg 'error.docker_not_found')\" -ForegroundColor Red\n Exit-Script 1\n }\n\n if (-not (Test-DockerRunning)) {\n Write-Host \"$($script:ESC)[31m[AgentTeams ERROR]$($script:ESC)[0m $(Get-Msg 'error.docker_not_running')\" -ForegroundColor Red\n Exit-Script 1\n }\n\n # Initialize shared config hashtable\n $script:config = @{}\n $config = $script:config # local alias so post-machine code can use $config\n\n # Orphan volume detection (env file gone but volume remains)\n if (-not (Test-Path $script:AGENTTEAMS_ENV_FILE)) {\n $dataVol = if ($env:AGENTTEAMS_DATA_DIR) { $env:AGENTTEAMS_DATA_DIR } else { \"agentteams-data\" }\n $volumeExists = docker volume ls -q 2>$null | Select-String \"^${dataVol}$\"\n if ($volumeExists) {\n Write-Host \"\"\n Write-Log (Get-Msg \"install.orphan_volume.detected\" -f $dataVol)\n Write-Log (Get-Msg \"install.orphan_volume.warn\")\n if ($script:AGENTTEAMS_NON_INTERACTIVE) {\n Write-Log (Get-Msg \"install.orphan_volume.clean_noninteractive\")\n docker stop agentteams-manager *>$null\n docker rm agentteams-manager *>$null\n docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-worker-\" | ForEach-Object {\n docker stop $_.ToString().Trim() *>$null\n docker rm $_.ToString().Trim() *>$null\n }\n Write-Log (Get-Msg \"install.orphan_volume.cleaning\")\n docker volume rm $dataVol *>$null\n Write-Log (Get-Msg \"install.orphan_volume.cleaned\")\n } else {\n Write-Host \"\"\n Write-Host (Get-Msg \"install.orphan_volume.choose\")\n Write-Host (Get-Msg \"install.orphan_volume.clean\")\n Write-Host (Get-Msg \"install.orphan_volume.keep\")\n Write-Host \"\"\n $orphanChoice = Read-Host (Get-Msg \"install.orphan_volume.prompt\")\n if (-not $orphanChoice) { $orphanChoice = \"1\" }\n switch -Regex ($orphanChoice) {\n \"^(1|clean)$\" {\n docker stop agentteams-manager *>$null\n docker rm agentteams-manager *>$null\n docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-worker-\" | ForEach-Object {\n docker stop $_.ToString().Trim() *>$null\n docker rm $_.ToString().Trim() *>$null\n }\n Write-Log (Get-Msg \"install.orphan_volume.cleaning\")\n docker volume rm $dataVol *>$null\n Write-Log (Get-Msg \"install.orphan_volume.cleaned\")\n }\n \"^(2|keep)$\" {\n Write-Log (Get-Msg \"install.orphan_volume.keeping\")\n }\n }\n }\n }\n }\n\n # ── State machine ─────────────────────────────────────────────────────────\n $_steps = @(\"Step-Lang\", \"Step-Mode\", \"Step-Existing\", \"Step-Llm\", \"Step-ManagerRuntime\", \"Step-Runtime\", \"Step-Admin\",\n \"Step-Network\", \"Step-Ports\", \"Step-Domains\", \"Step-Github\", \"Step-Skills\",\n \"Step-Volume\", \"Step-Workspace\", \"Step-E2ee\", \"Step-DockerProxy\", \"Step-Idle\",\n \"Step-Hostshare\")\n $_stepHistory = [System.Collections.Generic.List[int]]::new()\n $_stepIdx = 0\n while ($_stepIdx -lt $_steps.Count) {\n $_stepFn = $_steps[$_stepIdx]\n if (Test-ShouldSkipStep $_stepFn) {\n $_stepIdx++\n continue\n }\n if ($_stepHistory.Count -gt 0) {\n Write-Log (Get-Msg \"nav.back_hint\")\n }\n $script:StepResult = \"\"\n & $_stepFn\n if ($script:StepResult -eq \"back\") {\n if ($_stepHistory.Count -gt 0) {\n $_stepIdx = $_stepHistory[$_stepHistory.Count - 1]\n $_stepHistory.RemoveAt($_stepHistory.Count - 1)\n Clear-StepVars $_steps[$_stepIdx]\n }\n # else: first step, ignore 'b'\n } else {\n $_stepHistory.Add($_stepIdx)\n $_stepIdx++\n }\n }\n # ── End state machine ──────────────────────────────────────────────────────\n\n # Post-machine defaults for any steps that were skipped\n if (-not $script:config.DATA_DIR) {\n $script:config.DATA_DIR = if ($env:AGENTTEAMS_DATA_DIR) { $env:AGENTTEAMS_DATA_DIR } else { \"agentteams-data\" }\n }\n Write-Log (Get-Msg \"data.volume_using\" -f $script:config.DATA_DIR)\n if (-not $script:config.WORKSPACE_DIR) {\n $_defaultWs = \"$env:USERPROFILE\\agentteams-manager\"\n $script:config.WORKSPACE_DIR = if ($env:AGENTTEAMS_WORKSPACE_DIR) { $env:AGENTTEAMS_WORKSPACE_DIR } else { $_defaultWs }\n if (-not (Test-Path $script:config.WORKSPACE_DIR)) {\n New-Item -ItemType Directory -Path $script:config.WORKSPACE_DIR -Force | Out-Null\n }\n Write-Log (Get-Msg \"workspace.dir_label\" -f $script:config.WORKSPACE_DIR)\n }\n if (-not $script:config.DEFAULT_WORKER_RUNTIME) {\n $script:config.DEFAULT_WORKER_RUNTIME = if ($env:AGENTTEAMS_DEFAULT_WORKER_RUNTIME) { $env:AGENTTEAMS_DEFAULT_WORKER_RUNTIME } else { \"copaw\" }\n Write-Log (Get-Msg \"worker_runtime.selected\" -f $script:config.DEFAULT_WORKER_RUNTIME)\n }\n if (-not $script:config.MATRIX_E2EE) {\n $script:config.MATRIX_E2EE = if ($env:AGENTTEAMS_MATRIX_E2EE) { $env:AGENTTEAMS_MATRIX_E2EE } else { \"0\" }\n }\n if (-not $script:config.WORKER_IDLE_TIMEOUT) {\n $script:config.WORKER_IDLE_TIMEOUT = if ($env:AGENTTEAMS_WORKER_IDLE_TIMEOUT) { $env:AGENTTEAMS_WORKER_IDLE_TIMEOUT } else { \"720\" }\n }\n if (-not $script:config.HOST_SHARE_DIR) {\n $script:config.HOST_SHARE_DIR = if ($env:AGENTTEAMS_HOST_SHARE_DIR) { $env:AGENTTEAMS_HOST_SHARE_DIR } else { $env:USERPROFILE }\n }\n if (-not $script:config.LOCAL_ONLY) {\n $script:config.LOCAL_ONLY = if ($env:AGENTTEAMS_LOCAL_ONLY) { $env:AGENTTEAMS_LOCAL_ONLY } else { \"1\" }\n }\n if (-not $script:config.MANAGER_RUNTIME) {\n $script:config.MANAGER_RUNTIME = if ($env:AGENTTEAMS_MANAGER_RUNTIME) { $env:AGENTTEAMS_MANAGER_RUNTIME } else { \"copaw\" }\n }\n if (-not $script:config.PORT_GATEWAY) {\n $script:config.PORT_GATEWAY = if ($env:AGENTTEAMS_PORT_GATEWAY) { $env:AGENTTEAMS_PORT_GATEWAY } else { \"18080\" }\n $script:config.PORT_CONSOLE = if ($env:AGENTTEAMS_PORT_CONSOLE) { $env:AGENTTEAMS_PORT_CONSOLE } else { \"18001\" }\n $script:config.PORT_ELEMENT_WEB = if ($env:AGENTTEAMS_PORT_ELEMENT_WEB) { $env:AGENTTEAMS_PORT_ELEMENT_WEB } else { \"18088\" }\n $script:config.PORT_MANAGER_CONSOLE = if ($env:AGENTTEAMS_PORT_MANAGER_CONSOLE) { $env:AGENTTEAMS_PORT_MANAGER_CONSOLE } else { \"18888\" }\n }\n if (-not $script:config.MATRIX_DOMAIN) {\n $script:config.MATRIX_DOMAIN = if ($env:AGENTTEAMS_MATRIX_DOMAIN) { $env:AGENTTEAMS_MATRIX_DOMAIN } else { \"matrix-local.agentteams.io:$($script:config.PORT_GATEWAY)\" }\n $script:config.MATRIX_CLIENT_DOMAIN = if ($env:AGENTTEAMS_MATRIX_CLIENT_DOMAIN) { $env:AGENTTEAMS_MATRIX_CLIENT_DOMAIN } else { \"matrix-client-local.agentteams.io\" }\n $script:config.AI_GATEWAY_DOMAIN = if ($env:AGENTTEAMS_AI_GATEWAY_DOMAIN) { $env:AGENTTEAMS_AI_GATEWAY_DOMAIN } else { \"aigw-local.agentteams.io\" }\n $script:config.FS_DOMAIN = if ($env:AGENTTEAMS_FS_DOMAIN) { $env:AGENTTEAMS_FS_DOMAIN } else { \"fs-local.agentteams.io\" }\n $script:config.CONSOLE_DOMAIN = if ($env:AGENTTEAMS_CONSOLE_DOMAIN) { $env:AGENTTEAMS_CONSOLE_DOMAIN } else { \"console-local.agentteams.io\" }\n }\n\n Write-Log \"\"\n\n # Generate secrets\n Write-Log (Get-Msg \"install.generating_secrets\")\n $config.MANAGER_PASSWORD = if ($env:AGENTTEAMS_MANAGER_PASSWORD) { $env:AGENTTEAMS_MANAGER_PASSWORD } else { New-RandomKey }\n $config.REGISTRATION_TOKEN = if ($env:AGENTTEAMS_REGISTRATION_TOKEN) { $env:AGENTTEAMS_REGISTRATION_TOKEN } else { New-RandomKey }\n $config.MINIO_USER = if ($env:AGENTTEAMS_MINIO_USER) { $env:AGENTTEAMS_MINIO_USER } else { $config.ADMIN_USER }\n $config.MINIO_PASSWORD = if ($env:AGENTTEAMS_MINIO_PASSWORD) { $env:AGENTTEAMS_MINIO_PASSWORD } else { $config.ADMIN_PASSWORD }\n $config.MANAGER_GATEWAY_KEY = if ($env:AGENTTEAMS_MANAGER_GATEWAY_KEY) { $env:AGENTTEAMS_MANAGER_GATEWAY_KEY } else { New-RandomKey }\n\n # Store additional config\n $config.LANGUAGE = $script:AGENTTEAMS_LANGUAGE\n $config.REGISTRY = $script:AGENTTEAMS_REGISTRY\n $config.WORKER_IMAGE = $script:WORKER_IMAGE\n $config.COPAW_WORKER_IMAGE = $script:COPAW_WORKER_IMAGE\n $config.QWENPAW_WORKER_IMAGE = $script:QWENPAW_WORKER_IMAGE\n $config.HERMES_WORKER_IMAGE = $script:HERMES_WORKER_IMAGE\n $config.MANAGER_COPAW_IMAGE = $script:MANAGER_COPAW_IMAGE\n\n # Write env file\n New-EnvFile -Config $config -Path $script:AGENTTEAMS_ENV_FILE\n\n # Manager image selection (used by both embedded — passed to controller via env —\n # and legacy — used directly as `docker run` target).\n $managerImage = if ($config.MANAGER_RUNTIME -eq \"copaw\") { $script:MANAGER_COPAW_IMAGE } else { $script:MANAGER_IMAGE }\n $portPrefix = if ($config.LOCAL_ONLY -eq \"1\") { \"127.0.0.1:\" } else { \"\" }\n\n # Ensure agentteams-net Docker network exists. Used in both modes — the embedded\n # controller and the spawned manager/worker containers all join it and rely on\n # network aliases for *-local.agentteams.io DNS resolution.\n if ($script:AGENTTEAMS_MOUNT_SOCKET) {\n $socketAvailable = Test-DockerRunning\n if ($socketAvailable) {\n docker network inspect agentteams-net *>$null\n if ($LASTEXITCODE -ne 0) { docker network create agentteams-net *>$null }\n } else {\n Write-Log (Get-Msg \"install.socket_not_found\")\n if (-not $script:AGENTTEAMS_NON_INTERACTIVE) {\n Write-Host \"\"\n Write-Host \"$($script:ESC)[33m$(Get-Msg 'install.socket_confirm.title')$($script:ESC)[0m\"\n Write-Host \"\"\n Write-Host (Get-Msg 'install.socket_confirm.message')\n Write-Host \"\"\n $confirm = Read-Host (Get-Msg 'install.socket_confirm.prompt')\n if ($confirm -ne 'y' -and $confirm -ne 'Y') {\n Write-Log (Get-Msg 'install.socket_confirm.cancelled')\n exit 0\n }\n }\n }\n }\n\n if ($script:AGENTTEAMS_USE_EMBEDDED -ne \"1\") {\n # ============================================================\n # Legacy architecture: all-in-one manager container\n # (only entered when AGENTTEAMS_FORCE_LEGACY=1 — broken with the slim manager\n # image shipped since PR #616; kept solely for AGENTTEAMS_VERSION <= v1.0.9)\n # ============================================================\n\n $dockerArgs = @(\n \"run\", \"-d\",\n \"--name\", \"agentteams-manager\",\n \"--env-file\", $script:AGENTTEAMS_ENV_FILE,\n \"-e\", \"HOME=/root/manager-workspace\",\n \"-w\", \"/root/manager-workspace\",\n \"-e\", \"HOST_ORIGINAL_HOME=$($config.HOST_SHARE_DIR)\",\n \"-e\", \"AGENTTEAMS_MANAGER_RUNTIME=$($config.MANAGER_RUNTIME)\"\n )\n\n $dockerArgs += @(\"-e\", \"TZ=$($script:AGENTTEAMS_TIMEZONE)\")\n\n if ($script:AGENTTEAMS_MOUNT_SOCKET -and (Test-DockerRunning)) {\n $dockerArgs += @(\"--network\", \"agentteams-net\")\n $dockerArgs += @(\"--network-alias\", \"matrix-local.agentteams.io\")\n $dockerArgs += @(\"--network-alias\", \"aigw-local.agentteams.io\")\n $dockerArgs += @(\"--network-alias\", \"fs-local.agentteams.io\")\n foreach ($domain in @($config.MATRIX_CLIENT_DOMAIN, $config.CONSOLE_DOMAIN)) {\n if ($domain -match '-local\\.agentteams\\.io$') {\n $dockerArgs += @(\"--network-alias\", $domain)\n }\n }\n\n if ($config.DOCKER_PROXY -eq \"1\") {\n $proxyImage = $script:CONTROLLER_IMAGE\n Write-Log \"Starting Docker API proxy...\"\n docker rm -f agentteams-controller *>$null\n docker run -d --name agentteams-controller `\n --network agentteams-net `\n -v \"//var/run/docker.sock:/var/run/docker.sock\" `\n --security-opt label=disable `\n -e \"AGENTTEAMS_WORKER_IMAGE=$($script:WORKER_IMAGE)\" `\n -e \"AGENTTEAMS_COPAW_WORKER_IMAGE=$($script:COPAW_WORKER_IMAGE)\" `\n -e \"AGENTTEAMS_QWENPAW_WORKER_IMAGE=$($script:QWENPAW_WORKER_IMAGE)\" `\n -e \"AGENTTEAMS_HERMES_WORKER_IMAGE=$($script:HERMES_WORKER_IMAGE)\" `\n -e \"AGENTTEAMS_DEFAULT_WORKER_RUNTIME=$($script:config.DEFAULT_WORKER_RUNTIME)\" `\n $(if ($config.PROXY_ALLOWED_REGISTRIES) { @(\"-e\", \"AGENTTEAMS_PROXY_ALLOWED_REGISTRIES=$($config.PROXY_ALLOWED_REGISTRIES)\") }) `\n --restart unless-stopped `\n $proxyImage\n $dockerArgs += @(\"-e\", \"AGENTTEAMS_CONTROLLER_URL=http://agentteams-controller:8090\")\n $dockerArgs += @(\"-e\", \"AGENTTEAMS_CONTAINER_API=http://agentteams-controller:8090\")\n Write-Log (Get-Msg \"docker_proxy.selected_enabled\")\n } else {\n $dockerArgs += @(\"-v\", \"//var/run/docker.sock:/var/run/docker.sock\")\n $dockerArgs += @(\"--security-opt\", \"label=disable\")\n Write-Log (Get-Msg \"install.socket_detected\" -f \"//var/run/docker.sock\")\n }\n }\n\n $dockerArgs += @(\"-p\", \"${portPrefix}$($config.PORT_GATEWAY):8080\")\n $dockerArgs += @(\"-p\", \"${portPrefix}$($config.PORT_CONSOLE):8001\")\n $dockerArgs += @(\"-p\", \"${portPrefix}$($config.PORT_ELEMENT_WEB):8088\")\n $dockerArgs += @(\"-p\", \"127.0.0.1:$($config.PORT_MANAGER_CONSOLE):18888\")\n\n $dockerArgs += @(\"-v\", \"$($config.DATA_DIR):/data\")\n\n $wsDockerPath = ConvertTo-DockerPath -Path $config.WORKSPACE_DIR\n $dockerArgs += @(\"-v\", \"${wsDockerPath}:/root/manager-workspace\")\n\n $shareDockerPath = ConvertTo-DockerPath -Path $config.HOST_SHARE_DIR\n $dockerArgs += @(\"-v\", \"${shareDockerPath}:/host-share\")\n Write-Log (Get-Msg \"host_share.sharing\" -f $config.HOST_SHARE_DIR)\n\n if ($env:AGENTTEAMS_YOLO -eq \"1\") {\n $dockerArgs += @(\"-e\", \"AGENTTEAMS_YOLO=1\")\n Write-Log (Get-Msg \"install.yolo\")\n }\n\n if ($env:AGENTTEAMS_MATRIX_DEBUG -eq \"1\") {\n $dockerArgs += @(\"-e\", \"AGENTTEAMS_MATRIX_DEBUG=1\")\n }\n\n $dockerArgs += @(\"--restart\", \"unless-stopped\")\n $dockerArgs += $managerImage\n }\n\n # Check if the Docker volume exists; create if not (reuse on reinstall) — both modes.\n $volumeExists = docker volume ls -q 2>$null | Select-String \"^$($config.DATA_DIR)$\"\n if (-not $volumeExists) {\n docker volume create $config.DATA_DIR | Out-Null\n }\n\n # Pull images (skip if already exists locally for local build tags).\n $LocalImagePattern = \"^agentteams/\"\n function Test-LocalImage {\n param([string]$Image)\n $imgExists = docker image inspect $Image 2>$null\n return $LASTEXITCODE -eq 0\n }\n if ($script:AGENTTEAMS_USE_EMBEDDED -eq \"1\") {\n # Embedded image was already pulled by Resolve-EmbeddedImage unless overridden;\n # for an explicit override we still need to ensure it is present locally.\n if ($env:AGENTTEAMS_INSTALL_EMBEDDED_IMAGE) {\n if ($script:EMBEDDED_IMAGE -match $LocalImagePattern) {\n if (-not (Test-LocalImage $script:EMBEDDED_IMAGE)) {\n Write-Log \"Pulling embedded image: $($script:EMBEDDED_IMAGE)\"\n & docker pull $script:EMBEDDED_IMAGE\n }\n } else {\n Write-Log \"Pulling embedded image: $($script:EMBEDDED_IMAGE)\"\n & docker pull $script:EMBEDDED_IMAGE\n }\n }\n # Manager image — controller will spawn it inside; pull here so the spawn doesn't\n # have to wait on the network.\n if ($managerImage -match $LocalImagePattern) {\n if (Test-LocalImage $managerImage) {\n Write-Log (Get-Msg \"install.image.exists\" -f $managerImage)\n } else {\n Write-Log (Get-Msg \"install.image.pulling_manager\" -f $managerImage)\n & docker pull $managerImage\n }\n } else {\n Write-Log (Get-Msg \"install.image.pulling_manager\" -f $managerImage)\n & docker pull $managerImage\n }\n } else {\n if ($managerImage -match $LocalImagePattern) {\n if (Test-LocalImage $managerImage) {\n Write-Log (Get-Msg \"install.image.exists\" -f $managerImage)\n } else {\n Write-Log (Get-Msg \"install.image.pulling_manager\" -f $managerImage)\n & docker pull $managerImage\n }\n } else {\n Write-Log (Get-Msg \"install.image.pulling_manager\" -f $managerImage)\n & docker pull $managerImage\n }\n }\n\n # Pull all worker runtime images (workers may use any runtime regardless of the default)\n $workerImages = @(\n $script:WORKER_IMAGE\n $script:COPAW_WORKER_IMAGE\n # Temporarily disabled until the QwenPaw worker image is published.\n # $script:QWENPAW_WORKER_IMAGE\n $script:HERMES_WORKER_IMAGE\n )\n foreach ($workerImg in $workerImages) {\n if ($workerImg -match $LocalImagePattern) {\n if (Test-LocalImage $workerImg) {\n Write-Log (Get-Msg \"install.image.worker_exists\" -f $workerImg)\n } else {\n Write-Log (Get-Msg \"install.image.pulling_worker\" -f $workerImg)\n & docker pull $workerImg\n }\n } else {\n Write-Log (Get-Msg \"install.image.pulling_worker\" -f $workerImg)\n & docker pull $workerImg\n }\n }\n\n # --- Pre-upgrade: extract Matrix passwords from old containers (old-arch -> embedded) ---\n $credsTmp = $null\n if ($script:AGENTTEAMS_UPGRADE -and $script:AGENTTEAMS_USE_EMBEDDED -eq \"1\") {\n $controllerNameHit = docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-controller$\"\n $isOldArch = $false\n if (-not $controllerNameHit) {\n $isOldArch = $true\n } else {\n $ctrlImgLine = docker ps -a --format \"{{.Names}} {{.Image}}\" 2>$null | Where-Object { $_ -match '^agentteams-controller ' } | Select-Object -First 1\n if ($ctrlImgLine -and ($ctrlImgLine -notmatch 'embedded')) {\n $isOldArch = $true\n }\n }\n\n if ($isOldArch) {\n $credsTmp = Join-Path ([System.IO.Path]::GetTempPath()) (\"agentteams-upgrade-creds-\" + [Guid]::NewGuid().ToString(\"n\"))\n New-Item -ItemType Directory -Path $credsTmp -Force | Out-Null\n $utf8NoBom = New-Object System.Text.UTF8Encoding $false\n\n $mgrCredsTempStart = $false\n $mgrExists = docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-manager$\"\n $mgrRunning = docker ps --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-manager$\"\n if ($mgrExists -and -not $mgrRunning) {\n Write-Log \"agentteams-manager is stopped; starting it temporarily to extract Matrix credentials for upgrade...\"\n docker start agentteams-manager 2>$null | Out-Null\n Wait-MatrixReady -Container \"agentteams-manager\"\n $mgrCredsTempStart = $true\n }\n\n $inspectLines = docker inspect agentteams-manager --format \"{{range .Config.Env}}{{println .}}{{end}}\" 2>$null\n $mgrPw = \"\"\n if ($inspectLines) {\n $envLine = $inspectLines -split \"`n\" | Where-Object { $_ -match '^AGENTTEAMS_MANAGER_PASSWORD=' } | Select-Object -First 1\n if ($envLine) {\n $mgrPw = ($envLine -replace '^AGENTTEAMS_MANAGER_PASSWORD=', \"\").Trim()\n }\n }\n $mgrRunningNow = docker ps --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-manager$\"\n if ([string]::IsNullOrEmpty($mgrPw) -and $mgrRunningNow) {\n $mgrPw = docker exec agentteams-manager bash -c 'source /data/agentteams-secrets.env 2>/dev/null && echo \"${AGENTTEAMS_MANAGER_PASSWORD}\"' 2>$null\n if ($mgrPw) { $mgrPw = $mgrPw.Trim() }\n }\n $dataVolPresent = docker volume ls -q 2>$null | Where-Object { $_ -eq $config.DATA_DIR }\n if ([string]::IsNullOrEmpty($mgrPw) -and $dataVolPresent) {\n $mgrPw = Read-AgentTeamsSecretFromDataVolume -VolumeName $config.DATA_DIR -Key \"AGENTTEAMS_MANAGER_PASSWORD\"\n }\n\n $envFilePath = $script:AGENTTEAMS_ENV_FILE\n $mgrRoom = \"\"\n if (-not [string]::IsNullOrEmpty($mgrPw)) {\n $adminPw = \"\"\n $adminUser = \"admin\"\n if (Test-Path $envFilePath) {\n $adminLine = Get-Content $envFilePath | Where-Object { $_ -match '^AGENTTEAMS_ADMIN_PASSWORD=' } | Select-Object -First 1\n if ($adminLine) { $adminPw = ($adminLine -replace '^AGENTTEAMS_ADMIN_PASSWORD=', \"\").Trim() }\n $userLine = Get-Content $envFilePath | Where-Object { $_ -match '^AGENTTEAMS_ADMIN_USER=' } | Select-Object -First 1\n if ($userLine) {\n $adminUser = ($userLine -replace '^AGENTTEAMS_ADMIN_USER=', \"\").Trim()\n if ([string]::IsNullOrEmpty($adminUser)) { $adminUser = \"admin\" }\n }\n }\n\n if (-not [string]::IsNullOrEmpty($adminPw) -and $mgrRunningNow) {\n $mgrRoom = Get-AgentTeamsAdminDmRoomViaMatrix -AdminUser $adminUser -AdminPassword $adminPw\n }\n if ([string]::IsNullOrEmpty($mgrRoom)) {\n $mgrRoom = Read-AgentTeamsAdminDmRoomFromWorkspace -WorkspaceDir $config.WORKSPACE_DIR\n }\n\n $gwKeyLine = if (Test-Path $envFilePath) {\n Get-Content $envFilePath | Where-Object { $_ -match '^AGENTTEAMS_MANAGER_GATEWAY_KEY=' } | Select-Object -First 1\n } else { $null }\n $gwKeyForDefault = if ($gwKeyLine) { ($gwKeyLine -replace '^AGENTTEAMS_MANAGER_GATEWAY_KEY=', \"\").Trim() } else { $config.MANAGER_GATEWAY_KEY }\n\n $defaultEnvPath = Join-Path $credsTmp \"default.env\"\n [System.IO.File]::WriteAllLines($defaultEnvPath, @(\n \"WORKER_PASSWORD=$mgrPw\"\n \"WORKER_MINIO_PASSWORD=$(Get-AgentTeamsRandomHex 24)\"\n \"WORKER_GATEWAY_KEY=$gwKeyForDefault\"\n \"WORKER_ROOM_ID=$mgrRoom\"\n ), $utf8NoBom)\n if ($mgrRoom) {\n Write-Log \"Extracted Manager Matrix password and room ID\"\n } else {\n Write-Log \"Extracted Manager Matrix password\"\n }\n }\n\n if ($mgrCredsTempStart) {\n Write-Log \"Stopping agentteams-manager after credential extraction (upgrade will recreate containers)...\"\n docker stop agentteams-manager 2>$null | Out-Null\n }\n }\n }\n\n # Stop and remove existing containers (deferred until after all\n # configuration is collected and images are pulled successfully)\n $existingProxy = docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-controller$\"\n if ($existingProxy) {\n docker stop agentteams-controller *>$null\n docker rm agentteams-controller *>$null\n }\n $existingContainer = docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-manager$\"\n if ($existingContainer) {\n Write-Log (Get-Msg \"install.removing_existing\")\n docker stop agentteams-manager *>$null\n docker rm agentteams-manager *>$null\n }\n\n # Stop and remove worker containers (controller will recreate via CR reconciliation)\n if ($script:UPGRADE_EXISTING_WORKERS) {\n Write-Log (Get-Msg \"install.existing.stopping_workers\")\n $script:UPGRADE_EXISTING_WORKERS | ForEach-Object {\n docker stop $_ *>$null\n docker rm $_ *>$null\n Write-Log (Get-Msg \"install.existing.removed\" -f $_)\n }\n }\n\n # --- Upgrade: inject extracted credentials into data volume (old-arch -> embedded) ---\n if ($credsTmp -and (Test-Path $credsTmp)) {\n $credsFiles = Get-ChildItem -Path $credsTmp -Filter \"*.env\" -ErrorAction SilentlyContinue\n if ($credsFiles -and $credsFiles.Count -gt 0) {\n $cleanupCtr = \"agentteams-upgrade-cleanup\"\n docker rm -f $cleanupCtr 2>$null | Out-Null\n $credsDockerPath = ConvertTo-DockerPath -Path $credsTmp\n $injectShell = \"rm -rf /data/worker-creds && mkdir -p /data/worker-creds && cp /creds/*.env /data/worker-creds/ 2>/dev/null || true && chmod 600 /data/worker-creds/*.env 2>/dev/null || true\"\n docker run --rm --name $cleanupCtr --entrypoint sh `\n -v \"$($config.DATA_DIR):/data\" `\n -v \"${credsDockerPath}:/creds:ro\" `\n $script:EMBEDDED_IMAGE `\n -c $injectShell 2>$null | Out-Null\n if ($LASTEXITCODE -eq 0) {\n Write-Log \"Injected credentials for upgrade\"\n } else {\n Write-Log \"Warning: credential injection failed, continuing\"\n }\n }\n Remove-Item -Path $credsTmp -Recurse -Force -ErrorAction SilentlyContinue\n }\n\n if ($script:AGENTTEAMS_USE_EMBEDDED -eq \"1\") {\n # ============================================================\n # New architecture: embedded controller + auto-created manager\n # (controller container hosts Higress / Tuwunel / MinIO / Element Web /\n # controller binary, then spawns the lightweight manager container)\n # ============================================================\n\n # Internal port: 8080 (Higress gateway inside the controller container).\n $internalGwPort = 8080\n\n $matrixDomain = if ($config.MATRIX_DOMAIN) {\n $config.MATRIX_DOMAIN\n } else {\n \"matrix-local.agentteams.io:$($config.PORT_GATEWAY)\"\n }\n $aigwDomain = if ($config.AI_GATEWAY_DOMAIN) { $config.AI_GATEWAY_DOMAIN } else { \"aigw-local.agentteams.io\" }\n if ($aigwDomain -notmatch \":\") { $aigwDomain = \"${aigwDomain}:${internalGwPort}\" }\n $fsDomain = if ($config.FS_DOMAIN) { $config.FS_DOMAIN } else { \"fs-local.agentteams.io\" }\n if ($fsDomain -notmatch \":\") { $fsDomain = \"${fsDomain}:${internalGwPort}\" }\n\n $ctrlArgs = @(\n \"run\", \"-d\",\n \"--name\", \"agentteams-controller\",\n \"--network\", \"agentteams-net\",\n \"--network-alias\", \"matrix-local.agentteams.io\",\n \"--network-alias\", \"aigw-local.agentteams.io\",\n \"--network-alias\", \"fs-local.agentteams.io\",\n \"-e\", \"AGENTTEAMS_ADMIN_USER=$($config.ADMIN_USER)\",\n \"-e\", \"AGENTTEAMS_ADMIN_PASSWORD=$($config.ADMIN_PASSWORD)\",\n \"-e\", \"AGENTTEAMS_MANAGER_PASSWORD=$($config.MANAGER_PASSWORD)\",\n \"-e\", \"AGENTTEAMS_REGISTRATION_TOKEN=$($config.REGISTRATION_TOKEN)\",\n \"-e\", \"AGENTTEAMS_MINIO_USER=$($config.MINIO_USER)\",\n \"-e\", \"AGENTTEAMS_MINIO_PASSWORD=$($config.MINIO_PASSWORD)\",\n \"-e\", \"AGENTTEAMS_LLM_PROVIDER=$($config.LLM_PROVIDER)\",\n \"-e\", \"AGENTTEAMS_LLM_API_KEY=$($config.LLM_API_KEY)\",\n \"-e\", \"AGENTTEAMS_DEFAULT_MODEL=$($config.DEFAULT_MODEL)\",\n \"-e\", \"AGENTTEAMS_MANAGER_GATEWAY_KEY=$($config.MANAGER_GATEWAY_KEY)\",\n \"-e\", \"AGENTTEAMS_MANAGER_RUNTIME=$($config.MANAGER_RUNTIME)\",\n \"-e\", \"AGENTTEAMS_MANAGER_IMAGE=$managerImage\",\n \"-e\", \"AGENTTEAMS_DEFAULT_WORKER_RUNTIME=$($config.DEFAULT_WORKER_RUNTIME)\",\n \"-e\", \"AGENTTEAMS_WORKER_IMAGE=$($script:WORKER_IMAGE)\",\n \"-e\", \"AGENTTEAMS_COPAW_WORKER_IMAGE=$($script:COPAW_WORKER_IMAGE)\",\n \"-e\", \"AGENTTEAMS_QWENPAW_WORKER_IMAGE=$($script:QWENPAW_WORKER_IMAGE)\",\n \"-e\", \"AGENTTEAMS_HERMES_WORKER_IMAGE=$($script:HERMES_WORKER_IMAGE)\",\n \"-e\", \"AGENTTEAMS_MATRIX_DOMAIN=$matrixDomain\",\n \"-e\", \"AGENTTEAMS_ELEMENT_HOMESERVER_URL=http://127.0.0.1:$($config.PORT_GATEWAY)\",\n \"-e\", \"AGENTTEAMS_MATRIX_URL=http://127.0.0.1:6167\",\n \"-e\", \"AGENTTEAMS_MATRIX_E2EE=$($config.MATRIX_E2EE)\",\n \"-e\", \"AGENTTEAMS_MINIO_ENDPOINT=http://127.0.0.1:9000\",\n \"-e\", \"AGENTTEAMS_MINIO_BUCKET=agentteams-storage\",\n \"-e\", \"AGENTTEAMS_STORAGE_PREFIX=agentteams/agentteams-storage\",\n \"-e\", \"AGENTTEAMS_FS_ENDPOINT=http://127.0.0.1:9000\",\n \"-e\", \"AGENTTEAMS_AI_GATEWAY_URL=http://$aigwDomain\",\n \"-e\", \"AGENTTEAMS_CONTROLLER_URL=http://agentteams-controller:8090\",\n \"-e\", \"AGENTTEAMS_DOCKER_NETWORK=agentteams-net\",\n \"-e\", \"AGENTTEAMS_WORKSPACE_DIR=$($config.WORKSPACE_DIR)\",\n \"-e\", \"AGENTTEAMS_HOST_SHARE_DIR=$($config.HOST_SHARE_DIR)\",\n \"-e\", \"AGENTTEAMS_MANAGER_ENABLED=true\",\n \"-e\", \"AGENTTEAMS_PORT_MANAGER_CONSOLE=$($config.PORT_MANAGER_CONSOLE)\"\n )\n\n if ($script:AGENTTEAMS_TIMEZONE) {\n $ctrlArgs += @(\"-e\", \"TZ=$($script:AGENTTEAMS_TIMEZONE)\")\n }\n if ($env:AGENTTEAMS_YOLO -eq \"1\") {\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_YOLO=1\")\n }\n if ($env:AGENTTEAMS_MATRIX_DEBUG -eq \"1\") {\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_MATRIX_DEBUG=1\")\n }\n if ($config.GITHUB_TOKEN) {\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_GITHUB_TOKEN=$($config.GITHUB_TOKEN)\")\n }\n if ($config.EMBEDDING_MODEL) {\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_EMBEDDING_MODEL=$($config.EMBEDDING_MODEL)\")\n }\n\n # Optional: custom model capability overrides (context window, max\n # tokens, vision, reasoning). These are read by the Controller's\n # config.go LoadConfig to annotate custom models not in the built-in\n # presets table.\n if ($config.MODEL_CONTEXT_WINDOW) {\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_MODEL_CONTEXT_WINDOW=$($config.MODEL_CONTEXT_WINDOW)\")\n }\n if ($config.MODEL_MAX_TOKENS) {\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_MODEL_MAX_TOKENS=$($config.MODEL_MAX_TOKENS)\")\n }\n if ($config.MODEL_VISION) {\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_MODEL_VISION=$($config.MODEL_VISION)\")\n }\n if ($config.MODEL_REASONING) {\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_MODEL_REASONING=$($config.MODEL_REASONING)\")\n }\n\n if ($config.OPENAI_BASE_URL) {\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_OPENAI_BASE_URL=$($config.OPENAI_BASE_URL)\")\n }\n if ($env:AGENTTEAMS_AI_STREAM_IDLE_TIMEOUT_SECONDS) {\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_AI_STREAM_IDLE_TIMEOUT_SECONDS=$($env:AGENTTEAMS_AI_STREAM_IDLE_TIMEOUT_SECONDS)\")\n }\n if ($script:AGENTTEAMS_LANGUAGE) {\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_LANGUAGE=$($script:AGENTTEAMS_LANGUAGE)\")\n }\n\n # Optional: CMS/ARMS observability. In embedded mode the controller\n # spawns the Manager and Workers, so it must receive these settings.\n $cmsTracesEnabled = if ($env:AGENTTEAMS_CMS_TRACES_ENABLED) { $env:AGENTTEAMS_CMS_TRACES_ENABLED } else { \"false\" }\n $cmsServiceName = if ($env:AGENTTEAMS_CMS_SERVICE_NAME) { $env:AGENTTEAMS_CMS_SERVICE_NAME } else { \"agentteams-manager\" }\n $cmsMetricsEnabled = if ($env:AGENTTEAMS_CMS_METRICS_ENABLED) { $env:AGENTTEAMS_CMS_METRICS_ENABLED } else { \"false\" }\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_CMS_TRACES_ENABLED=$cmsTracesEnabled\")\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_CMS_SERVICE_NAME=$cmsServiceName\")\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_CMS_METRICS_ENABLED=$cmsMetricsEnabled\")\n if ($env:AGENTTEAMS_CMS_ENDPOINT) {\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_CMS_ENDPOINT=$($env:AGENTTEAMS_CMS_ENDPOINT)\")\n }\n if ($env:AGENTTEAMS_CMS_LICENSE_KEY) {\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_CMS_LICENSE_KEY=$($env:AGENTTEAMS_CMS_LICENSE_KEY)\")\n }\n if ($env:AGENTTEAMS_CMS_PROJECT) {\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_CMS_PROJECT=$($env:AGENTTEAMS_CMS_PROJECT)\")\n }\n if ($env:AGENTTEAMS_CMS_WORKSPACE) {\n $ctrlArgs += @(\"-e\", \"AGENTTEAMS_CMS_WORKSPACE=$($env:AGENTTEAMS_CMS_WORKSPACE)\")\n }\n\n # Mount the docker socket so the controller can spawn manager + workers.\n $ctrlArgs += @(\"-v\", \"//var/run/docker.sock:/var/run/docker.sock\")\n $ctrlArgs += @(\"--security-opt\", \"label=disable\")\n\n # Persistent data + workspace mounts (manager workspace is bind-mounted under\n # /root/agentteams-fs/agents/manager so the controller can hand it to the spawned\n # manager container).\n $ctrlArgs += @(\"-v\", \"$($config.DATA_DIR):/data\")\n $wsDockerPath = ConvertTo-DockerPath -Path $config.WORKSPACE_DIR\n $ctrlArgs += @(\"-v\", \"${wsDockerPath}:/root/agentteams-fs/agents/manager\")\n Write-Log (Get-Msg \"host_share.sharing\" -f $config.HOST_SHARE_DIR)\n\n # Externally exposed ports — only the gateway / Higress console / Element Web,\n # since the manager console is now spawned inside its own container by the\n # controller (port mapping is handled there).\n $ctrlArgs += @(\"-p\", \"${portPrefix}$($config.PORT_GATEWAY):8080\")\n $ctrlArgs += @(\"-p\", \"${portPrefix}$($config.PORT_CONSOLE):8001\")\n $ctrlArgs += @(\"-p\", \"${portPrefix}$($config.PORT_ELEMENT_WEB):8088\")\n\n $ctrlArgs += @(\"--restart\", \"unless-stopped\")\n $ctrlArgs += $script:EMBEDDED_IMAGE\n\n Write-Log (Get-Msg \"install.starting_manager\")\n & docker $ctrlArgs\n Write-Log \"Embedded controller started: agentteams-controller\"\n\n # Wait for infra inside the controller container.\n function Wait-EmbeddedUrl {\n param([string]$Url, [string]$Container, [int]$MaxWait, [string]$Description)\n $elapsed = 0\n Write-Log \"Waiting for $Description...\"\n while ($elapsed -lt $MaxWait) {\n docker exec $Container curl -sf $Url *>$null\n if ($LASTEXITCODE -eq 0) {\n Write-Log \"$Description is ready (${elapsed}s)\"\n return $true\n }\n Start-Sleep -Seconds 2\n $elapsed += 2\n }\n Write-Host \"$($script:ESC)[31m[AgentTeams ERROR]$($script:ESC)[0m $Description not ready after ${MaxWait}s\" -ForegroundColor Red\n return $false\n }\n\n if (-not (Wait-EmbeddedUrl \"http://127.0.0.1:6167/_tuwunel/server_version\" \"agentteams-controller\" 120 \"Tuwunel (Matrix)\")) { Exit-Script 1 }\n if (-not (Wait-EmbeddedUrl \"http://127.0.0.1:9000/minio/health/live\" \"agentteams-controller\" 60 \"MinIO\")) { Exit-Script 1 }\n if (-not (Wait-EmbeddedUrl \"http://127.0.0.1:8080/status\" \"agentteams-controller\" 120 \"Higress Gateway\")) { Exit-Script 1 }\n\n # Wait for the controller to spawn the Manager Agent container.\n Write-Log \"Waiting for Manager Agent container...\"\n $mgrWait = 0\n $mgrMax = 300\n while ($mgrWait -lt $mgrMax) {\n $found = docker ps --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-manager$\"\n if ($found) {\n Write-Log \"Manager Agent container detected (${mgrWait}s)\"\n break\n }\n Start-Sleep -Seconds 3\n $mgrWait += 3\n }\n if ($mgrWait -ge $mgrMax) {\n Write-Host \"$($script:ESC)[31m[AgentTeams ERROR]$($script:ESC)[0m Manager Agent container not created after ${mgrMax}s\" -ForegroundColor Red\n Write-Log \"Controller logs:\"\n docker exec agentteams-controller tail -30 /var/log/agentteams/agentteams-controller-error.log 2>$null\n Exit-Script 1\n }\n\n # Wait for Manager Agent container to reach `running` state.\n Write-Log \"Waiting for Manager Agent to start...\"\n $agentWait = 0\n while ($agentWait -lt 120) {\n $state = docker inspect --format \"{{.State.Status}}\" agentteams-manager 2>$null\n if ($state -eq \"running\") {\n Write-Log \"Manager Agent is running\"\n break\n }\n Start-Sleep -Seconds 2\n $agentWait += 2\n }\n\n if ($env:AGENTTEAMS_YOLO -eq \"1\") {\n docker exec agentteams-manager touch /root/manager-workspace/yolo-mode 2>$null\n }\n\n # Wait for the Manager Agent's runtime + Matrix to be reachable. In embedded\n # mode Tuwunel lives in `agentteams-controller`, so the Matrix probe must target\n # the controller (the manager only exposes the agent runtime).\n Wait-ManagerReady -Container \"agentteams-manager\"\n Wait-MatrixReady -Container \"agentteams-controller\"\n\n # Wait for the controller to actually deliver the first-boot welcome\n # message. Gated by the controller on (a) Manager joining the DM room\n # and (b) Higress WASM key-auth propagating to /v1/chat/completions for\n # the Manager's gateway key — typically ~45-90s on a fresh install.\n # We poll Manager CR status.welcomeSent via the in-container AgentTeams\n # CLI, exec'd inside agentteams-controller (the source-of-truth container\n # — its bundled CLI binary is always in lockstep with the controller\n # binary serving the HTTP API, since they're the same `go build`\n # output. The agentteams-manager container's CLI may lag the controller\n # across image upgrades and silently drop the welcomeSent field,\n # leaving this loop hung). The controller mints a long-lived admin\n # SA token at startup and writes it to\n # AGENTTEAMS_AUTH_TOKEN_FILE=/var/run/agentteams/cli-token (set as a Dockerfile\n # ENV default), so a bare `docker exec agentteams-controller agt …`\n # auto-discovers both the endpoint and the token. The brief window\n # after container start before bootstrapAdminCLIToken completes is\n # absorbed by the loop's silent retry.\n $hasAgentTeamsCli = $false\n try {\n docker exec agentteams-controller sh -c 'command -v agt' *> $null\n if ($LASTEXITCODE -eq 0) { $hasAgentTeamsCli = $true }\n } catch {}\n\n if ($hasAgentTeamsCli) {\n Write-Log (Get-Msg \"install.welcome_msg.waiting\")\n $welcomeMax = if ($env:AGENTTEAMS_WELCOME_TIMEOUT) { [int]$env:AGENTTEAMS_WELCOME_TIMEOUT } else { 300 }\n $welcomeWait = 0\n $welcomeDone = $false\n while ($welcomeWait -lt $welcomeMax) {\n $wjson = \"\"\n try {\n $wjson = docker exec agentteams-controller `\n agt get managers default -o json 2>$null\n } catch {}\n if ($wjson -and ($wjson -replace '\\s', '') -match '\"welcomeSent\":true') {\n Write-Log (Get-Msg \"install.welcome_msg.confirmed\" $welcomeWait)\n $welcomeDone = $true\n break\n }\n Start-Sleep -Seconds 3\n $welcomeWait += 3\n }\n if (-not $welcomeDone) {\n # Non-fatal: install is still good. Keep going to the success\n # banner so the admin can use Element Web to nudge Manager into\n # onboarding manually (one DM message is enough).\n Write-Log (Get-Msg \"install.welcome_msg.timeout\" $welcomeMax)\n Write-Log (Get-Msg \"install.welcome_msg.timeout_hint\")\n Write-Log (Get-Msg \"install.welcome_msg.timeout_inspect\")\n }\n } else {\n Write-Log (Get-Msg \"install.welcome_msg.poll_unavailable\")\n }\n } else {\n # Run container (legacy path)\n Write-Log (Get-Msg \"install.starting_manager\")\n & docker $dockerArgs\n\n Wait-ManagerReady -Container \"agentteams-manager\"\n Wait-MatrixReady -Container \"agentteams-manager\"\n }\n\n # Create OpenAI-compatible provider if needed\n if ($config.LLM_PROVIDER -eq \"openai-compat\") {\n New-OpenAICompatProvider -BaseUrl $config.OPENAI_BASE_URL -ApiKey $config.LLM_API_KEY -ConsolePort ([int]$config.PORT_CONSOLE)\n }\n\n # Print success message\n Write-Log \"\"\n Write-Log (Get-Msg \"success.title\")\n Write-Log \"\"\n Write-Log (Get-Msg \"success.domains_configured\")\n Write-Log \" $($config.MATRIX_DOMAIN.Split(':')[0]) $($config.MATRIX_CLIENT_DOMAIN) $($config.AI_GATEWAY_DOMAIN) $($config.FS_DOMAIN) $($config.CONSOLE_DOMAIN)\"\n Write-Log \"\"\n\n $lanIP = Get-LanIP\n\n Write-Host \"$($script:ESC)[33m===============================================================$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.open_url')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[1;36m http://127.0.0.1:$($config.PORT_ELEMENT_WEB)/#/login$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.login_with')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.username' -f $config.ADMIN_USER)$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.password' -f $config.ADMIN_PASSWORD)$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.after_login')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.tell_it')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.manager_auto')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m --------------------------------------------------------------- $($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.mobile_title')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $($script:ESC)[0m\"\n if ($lanIP) {\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.mobile_step1')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.mobile_step2' -f \"http://${lanIP}:$($config.PORT_GATEWAY)\")$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.mobile_step3')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.mobile_username' -f $config.ADMIN_USER)$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.mobile_password' -f $config.ADMIN_PASSWORD)$($script:ESC)[0m\"\n } else {\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.mobile_step1')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.mobile_step2_noip' -f $config.PORT_GATEWAY)$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.mobile_noip_hint')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.mobile_step3')$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.mobile_username' -f $config.ADMIN_USER)$($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m $(Get-Msg 'success.mobile_password' -f $config.ADMIN_PASSWORD)$($script:ESC)[0m\"\n }\n Write-Host \"$($script:ESC)[33m $($script:ESC)[0m\"\n Write-Host \"$($script:ESC)[33m===============================================================$($script:ESC)[0m\"\n\n Write-Log \"\"\n Write-Log (Get-Msg \"success.other_consoles\")\n Write-Log (Get-Msg \"success.higress_console\" -f $config.PORT_CONSOLE, $config.ADMIN_USER, $config.ADMIN_PASSWORD)\n if ($script:AGENTTEAMS_USE_EMBEDDED -ne \"1\") {\n # In embedded mode the manager runs in its own auto-spawned container with\n # its own console-port mapping handled by the controller, so don't print a\n # host-side URL/credentials hint here.\n Write-Log (Get-Msg \"success.manager_console\" -f $config.PORT_MANAGER_CONSOLE)\n Write-Log (Get-Msg \"success.manager_console_gateway\" -f $config.ADMIN_USER, $config.ADMIN_PASSWORD)\n }\n Write-Log \"\"\n Write-Log (Get-Msg \"success.switch_llm.title\")\n Write-Log (Get-Msg \"success.switch_llm.hint\")\n Write-Log (Get-Msg \"success.switch_llm.docs\")\n Write-Log (Get-Msg \"success.switch_llm.url\")\n Write-Log \"\"\n Write-Log (Get-Msg \"success.tip\")\n Write-Log \"\"\n if ($config.LOCAL_ONLY -ne \"1\") {\n Write-Host (Get-Msg \"port.local_only.https_hint\") -ForegroundColor Yellow\n Write-Log \"\"\n }\n Write-Log (Get-Msg \"success.config_file\" -f $script:AGENTTEAMS_ENV_FILE)\n\n Write-Log (Get-Msg \"success.data_volume\" -f $config.DATA_DIR)\n\n Write-Log (Get-Msg \"success.workspace\" -f $config.WORKSPACE_DIR)\n}\n\n# ============================================================\n# Worker Installation\n# ============================================================\n\nfunction Install-Worker {\n param(\n [string]$Name,\n [string]$Fs,\n [string]$FsKey,\n [string]$FsSecret,\n [switch]$Reset,\n [switch]$FindSkills,\n [string]$SkillsApiUrl\n )\n\n # Validate required parameters\n if (-not $Name) {\n Write-Error (Get-Msg \"error.name_required\")\n }\n if (-not $Fs) {\n Write-Error (Get-Msg \"error.fs_required\")\n }\n if (-not $FsKey) {\n Write-Error (Get-Msg \"error.fs_key_required\")\n }\n if (-not $FsSecret) {\n Write-Error (Get-Msg \"error.fs_secret_required\")\n }\n\n $containerName = \"agentteams-worker-$Name\"\n\n # Handle reset\n if ($Reset) {\n Write-Log (Get-Msg \"worker.resetting\" -f $Name)\n docker stop $containerName *>$null\n docker rm $containerName *>$null\n }\n\n # Check for existing container\n $existing = docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^$containerName$\"\n if ($existing) {\n Write-Error (Get-Msg \"worker.exists\" -f $containerName)\n }\n\n # Detect timezone and registry\n $timezone = Get-AgentTeamsTimeZone\n $registry = Get-Registry -Timezone $timezone\n $workerImage = if ($env:AGENTTEAMS_INSTALL_WORKER_IMAGE) {\n $env:AGENTTEAMS_INSTALL_WORKER_IMAGE\n } else {\n \"$registry/agentteams/agentteams-worker:$($script:AGENTTEAMS_VERSION)\"\n }\n\n Write-Log (Get-Msg \"worker.starting\" -f $Name)\n\n $dockerArgs = @(\n \"run\", \"-d\",\n \"--name\", $containerName,\n \"-e\", \"HOME=/root/agentteams-fs/agents/$Name\",\n \"-w\", \"/root/agentteams-fs/agents/$Name\",\n \"-e\", \"AGENTTEAMS_WORKER_NAME=$Name\",\n \"-e\", \"AGENTTEAMS_FS_ENDPOINT=$Fs\",\n \"-e\", \"AGENTTEAMS_FS_ACCESS_KEY=$FsKey\",\n \"-e\", \"AGENTTEAMS_FS_SECRET_KEY=$FsSecret\"\n )\n\n if (-not $SkillsApiUrl) {\n if ($env:AGENTTEAMS_SKILLS_API_URL) {\n $SkillsApiUrl = $env:AGENTTEAMS_SKILLS_API_URL\n } else {\n $SkillsApiUrl = \"nacos://market.agentteams.io:80/public\"\n }\n }\n\n if ($FindSkills -and $SkillsApiUrl) {\n $dockerArgs += @(\"-e\", \"SKILLS_API_URL=$SkillsApiUrl\")\n Write-Log (Get-Msg \"worker.skills_url\" -f $SkillsApiUrl)\n }\n if ($env:AGENTTEAMS_NACOS_USERNAME) {\n $dockerArgs += @(\"-e\", \"AGENTTEAMS_NACOS_USERNAME=$($env:AGENTTEAMS_NACOS_USERNAME)\")\n }\n if ($env:AGENTTEAMS_NACOS_PASSWORD) {\n $dockerArgs += @(\"-e\", \"AGENTTEAMS_NACOS_PASSWORD=$($env:AGENTTEAMS_NACOS_PASSWORD)\")\n }\n if ($env:AGENTTEAMS_NACOS_TOKEN) {\n $dockerArgs += @(\"-e\", \"AGENTTEAMS_NACOS_TOKEN=$($env:AGENTTEAMS_NACOS_TOKEN)\")\n }\n\n $dockerArgs += @(\"--restart\", \"unless-stopped\", $workerImage)\n\n & docker $dockerArgs\n\n Write-Log \"\"\n Write-Log (Get-Msg \"worker.started\" -f $Name)\n Write-Log (Get-Msg \"worker.container\" -f $containerName)\n Write-Log (Get-Msg \"worker.view_logs\" -f $containerName)\n}\n\n# ============================================================\n# Uninstall\n# ============================================================\n\nfunction Uninstall-AgentTeams {\n Write-Log (Get-Msg \"uninstall.title\")\n\n # Stop and remove manager\n $manager = docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-manager$\"\n if ($manager) {\n Write-Log (Get-Msg \"uninstall.stopping_manager\")\n docker stop agentteams-manager *>$null\n docker rm agentteams-manager *>$null\n }\n\n # Stop and remove workers\n $workers = docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-worker-\"\n if ($workers) {\n Write-Log (Get-Msg \"uninstall.stopping_workers\")\n $workers | ForEach-Object {\n docker stop $_ *>$null\n docker rm $_ *>$null\n Write-Log (Get-Msg \"uninstall.removed\" -f $_)\n }\n }\n\n # Stop and remove docker-proxy (legacy <= v1.0.x; current arch uses\n # agentteams-controller for the same role)\n $proxy = docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-docker-proxy$\"\n if ($proxy) {\n Write-Log (Get-Msg \"uninstall.removing_proxy\")\n docker stop agentteams-docker-proxy *>$null\n docker rm agentteams-docker-proxy *>$null\n }\n\n # Stop and remove the embedded controller container. MUST happen\n # before the `docker volume rm agentteams-data` step below -- in embedded\n # mode agentteams-controller mounts agentteams-data at /data (Tuwunel DB,\n # MinIO state, Higress state, all the room messages), and `volume rm`\n # against an in-use volume fails silently. Skipping this used to\n # leave room/message history behind across \"uninstall + reinstall\"\n # cycles. See PR #692.\n $controller = docker ps -a --format \"{{.Names}}\" 2>$null | Select-String \"^agentteams-controller$\"\n if ($controller) {\n Write-Log (Get-Msg \"uninstall.stopping_controller\")\n docker stop agentteams-controller *>$null\n docker rm agentteams-controller *>$null\n }\n\n # Remove Docker volume (read custom name from env file if available)\n $dataVolume = \"agentteams-data\"\n if (Test-Path $script:AGENTTEAMS_ENV_FILE) {\n $envContent = Get-Content $script:AGENTTEAMS_ENV_FILE -ErrorAction SilentlyContinue\n $dataLine = $envContent | Select-String \"^AGENTTEAMS_DATA_DIR=\"\n if ($dataLine) {\n $parsed = ($dataLine -split \"=\", 2)[1]\n if ($parsed) { $dataVolume = $parsed }\n }\n }\n $volume = docker volume ls -q 2>$null | Select-String \"^${dataVolume}$\"\n if ($volume) {\n Write-Log (Get-Msg \"uninstall.removing_volume\")\n docker volume rm $dataVolume *>$null\n }\n\n # Remove Docker network\n $network = docker network ls --format \"{{.Name}}\" 2>$null | Select-String \"^agentteams-net$\"\n if ($network) {\n Write-Log (Get-Msg \"uninstall.removing_network\")\n docker network rm agentteams-net *>$null\n }\n\n # Remove workspace directory\n $workspaceDir = \"$env:USERPROFILE\\agentteams-manager\"\n if (Test-Path $script:AGENTTEAMS_ENV_FILE) {\n $envContent = Get-Content $script:AGENTTEAMS_ENV_FILE -ErrorAction SilentlyContinue\n $wsLine = $envContent | Select-String \"^AGENTTEAMS_WORKSPACE_DIR=\"\n if ($wsLine) {\n $workspaceDir = ($wsLine -split \"=\", 2)[1]\n }\n }\n if ($workspaceDir -and (Test-Path $workspaceDir)) {\n Write-Log (Get-Msg \"uninstall.removing_workspace\" -f $workspaceDir)\n Remove-Item -Recurse -Force $workspaceDir -ErrorAction SilentlyContinue\n }\n\n # Remove env file\n if (Test-Path $script:AGENTTEAMS_ENV_FILE) {\n Write-Log (Get-Msg \"uninstall.removing_env\" -f $script:AGENTTEAMS_ENV_FILE)\n Remove-Item -Force $script:AGENTTEAMS_ENV_FILE\n }\n\n # Remove install log (stop transcript first to release the file)\n if (Test-Path $script:AGENTTEAMS_LOG_FILE) {\n Write-Log (Get-Msg \"uninstall.removing_log\" -f $script:AGENTTEAMS_LOG_FILE)\n try { Stop-Transcript *>$null } catch {}\n Remove-Item -Force $script:AGENTTEAMS_LOG_FILE -ErrorAction SilentlyContinue\n }\n\n Write-Log \"\"\n Write-Log (Get-Msg \"uninstall.done\")\n}\n\n# ============================================================\n# Main Entry Point\n# ============================================================\n\ntry {\n switch ($Command) {\n \"manager\" {\n Install-Manager\n }\n \"worker\" {\n Install-Worker -Name $Name -Fs $Fs -FsKey $FsKey -FsSecret $FsSecret -Reset:$Reset -FindSkills:$FindSkills -SkillsApiUrl $SkillsApiUrl\n }\n \"uninstall\" {\n Uninstall-AgentTeams\n }\n }\n} catch {\n Exit-Script 1\n}\n\n# Stop transcript logging\ntry {\n Stop-Transcript -ErrorAction SilentlyContinue | Out-Null\n} catch {\n # Ignore errors when stopping transcript\n}\n"}}, "manager/scripts/init/start-manager-agent.sh": {"124f06d13fd6bb4c2054128dcff26bcd9f9c8dbf": {"t": 1785746542.4746842, "n": 64921, "h": "9e69523f67d18268", "c": "#!/bin/bash\n# start-manager-agent.sh - Initialize and start the Manager Agent\n# Supports local (supervisord), cloud (SAE), and K8s (Helm) deployments.\n# In local mode this is the last supervisord component to start (priority 800).\n# In cloud/k8s mode (AGENTTEAMS_RUNTIME=aliyun|k8s) this is the container entrypoint.\n#\n# Runtime selection:\n# AGENTTEAMS_MANAGER_RUNTIME=qwenpaw - QwenPaw 2.0 workspace mode (default)\n# AGENTTEAMS_MANAGER_RUNTIME=copaw - Legacy alias, auto-routed to QwenPaw\n# AGENTTEAMS_MANAGER_RUNTIME=openclaw - OpenClaw gateway mode\n\nsource /opt/agentteams/scripts/lib/agentteams-env.sh\n\n# ============================================================\n# Runtime selection\n# ============================================================\nMANAGER_RUNTIME=\"${AGENTTEAMS_MANAGER_RUNTIME:-qwenpaw}\"\ncase \"${MANAGER_RUNTIME}\" in\n copaw|qwenpaw)\n log \"Manager runtime: QwenPaw (Python workspace, runtime=${MANAGER_RUNTIME})\"\n ;;\n *)\n log \"Manager runtime: OpenClaw (Node.js gateway)\"\n MANAGER_RUNTIME=\"openclaw\"\n ;;\nesac\n\n# ============================================================\n# Set timezone from TZ env var\n# ============================================================\nif [ -n \"${TZ}\" ] && [ -f \"/usr/share/zoneinfo/${TZ}\" ]; then\n ln -sf \"/usr/share/zoneinfo/${TZ}\" /etc/localtime\n echo \"${TZ}\" > /etc/timezone\n log \"Timezone set to ${TZ}\"\nfi\n\nexport MATRIX_DOMAIN=\"${AGENTTEAMS_MATRIX_DOMAIN:-matrix-local.agentteams.io:8080}\"\nAI_GATEWAY_DOMAIN=\"${AGENTTEAMS_AI_GATEWAY_DOMAIN:-aigw-local.agentteams.io}\"\n\n# ============================================================\n# YOLO mode promotion\n# ============================================================\n# In embedded mode the controller does not propagate AGENTTEAMS_YOLO to the\n# manager container, but installer / test scripts touch a marker file at\n# `${WORKSPACE}/yolo-mode` instead. Promote that marker to the env var so the\n# agent's documented YOLO check (`AGENTTEAMS_YOLO=1`) reliably detects it without\n# depending on filesystem lookups during a turn.\nif [ -z \"${AGENTTEAMS_YOLO:-}\" ] && [ -f /root/manager-workspace/yolo-mode ]; then\n export AGENTTEAMS_YOLO=1\n log \"YOLO mode marker detected at /root/manager-workspace/yolo-mode; AGENTTEAMS_YOLO=1 exported\"\nfi\n\n# ============================================================\n# Cloud/K8s mode: validate required environment variables + initial credentials\n# ============================================================\nif [ \"${AGENTTEAMS_RUNTIME}\" = \"aliyun\" ] || [ \"${AGENTTEAMS_RUNTIME}\" = \"k8s\" ]; then\n : \"${AGENTTEAMS_MATRIX_URL:?AGENTTEAMS_MATRIX_URL is required}\"\n : \"${AGENTTEAMS_MATRIX_DOMAIN:?AGENTTEAMS_MATRIX_DOMAIN is required}\"\n : \"${AGENTTEAMS_AI_GATEWAY_URL:?AGENTTEAMS_AI_GATEWAY_URL is required}\"\n if [ \"${AGENTTEAMS_RUNTIME}\" = \"aliyun\" ]; then\n : \"${AGENTTEAMS_MANAGER_GATEWAY_KEY:?AGENTTEAMS_MANAGER_GATEWAY_KEY is required}\"\n : \"${AGENTTEAMS_MANAGER_PASSWORD:?AGENTTEAMS_MANAGER_PASSWORD is required (cloud containers are stateless, password must be injected)}\"\n fi\n if [ \"${AGENTTEAMS_RUNTIME}\" = \"k8s\" ]; then\n # K8s mode: controller handles initialization (admin registration, Higress setup).\n # Manager only needs credentials injected by the ManagerReconciler.\n : \"${AGENTTEAMS_MANAGER_GATEWAY_KEY:?AGENTTEAMS_MANAGER_GATEWAY_KEY is required (injected by controller)}\"\n # AGENTTEAMS_MANAGER_PASSWORD is optional: not needed in AppService mode\n # (token obtained via AS login), only required in legacy password mode.\n else\n # Cloud (aliyun) mode: Manager still does its own initialization\n : \"${AGENTTEAMS_REGISTRATION_TOKEN:?AGENTTEAMS_REGISTRATION_TOKEN is required}\"\n : \"${AGENTTEAMS_ADMIN_USER:?AGENTTEAMS_ADMIN_USER is required}\"\n : \"${AGENTTEAMS_ADMIN_PASSWORD:?AGENTTEAMS_ADMIN_PASSWORD is required}\"\n fi\n log \"${AGENTTEAMS_RUNTIME} mode: validating environment... OK\"\n log \" Matrix: ${AGENTTEAMS_MATRIX_URL}, AI Gateway: ${AGENTTEAMS_AI_GATEWAY_URL}, Storage: ${AGENTTEAMS_FS_BUCKET}\"\n if [ \"${AGENTTEAMS_RUNTIME}\" = \"aliyun\" ]; then\n ensure_mc_credentials || { log \"FATAL: Initial STS credential fetch failed\"; exit 1; }\n fi\nfi\n\n# ============================================================\n# Local mode: host symlinks, /etc/hosts, wait for local services\n# ============================================================\nif [ \"${AGENTTEAMS_RUNTIME}\" != \"aliyun\" ] && [ \"${AGENTTEAMS_RUNTIME}\" != \"k8s\" ]; then\n # Create symlink for host directory access\n if [ -d \"/host-share\" ]; then\n ORIGINAL_HOST_HOME=\"${HOST_ORIGINAL_HOME:-$HOME}\"\n if [ ! -e \"${ORIGINAL_HOST_HOME}\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/root\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/data\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/host-share\" ]; then\n mkdir -p \"$(dirname \"${ORIGINAL_HOST_HOME}\")\"\n ln -sfn /host-share \"${ORIGINAL_HOST_HOME}\"\n log \"Created symlink: ${ORIGINAL_HOST_HOME} -> /host-share\"\n else\n ln -sfn /host-share /root/host-home\n log \"Created fallback symlink: /root/host-home -> /host-share\"\n fi\n fi\n\n # Add local domains to /etc/hosts\n HOSTS_DOMAINS=\"${MATRIX_DOMAIN%%:*} ${AGENTTEAMS_MATRIX_CLIENT_DOMAIN:-matrix-client-local.agentteams.io} ${AI_GATEWAY_DOMAIN} ${AGENTTEAMS_FS_DOMAIN:-fs-local.agentteams.io}\"\n if ! grep -q \"${AI_GATEWAY_DOMAIN}\" /etc/hosts 2>/dev/null; then\n echo \"127.0.0.1 ${HOSTS_DOMAINS}\" >> /etc/hosts\n log \"Added local domains to /etc/hosts\"\n fi\n\n # Wait for local infrastructure\n waitForService \"Higress Gateway\" \"127.0.0.1\" 8080 180\n waitForService \"Higress Console\" \"127.0.0.1\" 8001 180\n waitForService \"Tuwunel\" \"127.0.0.1\" 6167 120\n waitForHTTP \"Tuwunel Matrix API\" \"${AGENTTEAMS_MATRIX_URL}/_tuwunel/server_version\" 120\n waitForService \"MinIO\" \"127.0.0.1\" 9000 120\nelse\n # Cloud/K8s mode: wait for external Tuwunel\n log \"Waiting for Tuwunel Matrix server at ${AGENTTEAMS_MATRIX_URL}...\"\n _retry=0\n while [ \"${_retry}\" -lt 30 ]; do\n if curl -sf \"${AGENTTEAMS_MATRIX_URL}/_matrix/client/versions\" > /dev/null 2>&1; then\n log \"Tuwunel is ready\"\n break\n fi\n _retry=$((_retry + 1))\n log \" Waiting for Tuwunel (attempt ${_retry}/30)...\"\n sleep 5\n done\n if [ \"${_retry}\" -ge 30 ]; then\n log \"ERROR: Tuwunel not reachable at ${AGENTTEAMS_MATRIX_URL}\"\n exit 1\n fi\nfi\n\n# ============================================================\n# Auto-generate secrets if not provided via environment\n# Persisted to /data so they survive container restart\n# ============================================================\nSECRETS_FILE=\"/data/agentteams-secrets.env\"\nif [ -f \"${SECRETS_FILE}\" ]; then\n source \"${SECRETS_FILE}\"\n log \"Loaded persisted secrets from ${SECRETS_FILE}\"\nfi\n\nif [ -z \"${AGENTTEAMS_MANAGER_GATEWAY_KEY}\" ]; then\n export AGENTTEAMS_MANAGER_GATEWAY_KEY=\"$(generateKey 32)\"\n log \"Auto-generated AGENTTEAMS_MANAGER_GATEWAY_KEY\"\nfi\nif [ -z \"${AGENTTEAMS_MANAGER_PASSWORD}\" ]; then\n export AGENTTEAMS_MANAGER_PASSWORD=\"$(generateKey 16)\"\n log \"Auto-generated AGENTTEAMS_MANAGER_PASSWORD\"\nfi\n\n# Persist secrets so they survive supervisord restart\nmkdir -p /data\ncat > \"${SECRETS_FILE}\" <<EOF\nexport AGENTTEAMS_MANAGER_GATEWAY_KEY=\"${AGENTTEAMS_MANAGER_GATEWAY_KEY}\"\nexport AGENTTEAMS_MANAGER_PASSWORD=\"${AGENTTEAMS_MANAGER_PASSWORD}\"\nEOF\nchmod 600 \"${SECRETS_FILE}\"\n\n# Cloud mode: pull workspace from OSS before initialization\nif [ \"${AGENTTEAMS_RUNTIME}\" = \"aliyun\" ]; then\n AGENTTEAMS_FS=\"/root/agentteams-fs\"\n mkdir -p \"${AGENTTEAMS_FS}/shared\" \"${AGENTTEAMS_FS}/agents\"\n log \"Pulling workspace from OSS...\"\n ensure_mc_credentials\n mc mirror \"${AGENTTEAMS_STORAGE_PREFIX}/manager/\" /root/manager-workspace/ --overwrite 2>/dev/null || true\n mc mirror \"${AGENTTEAMS_STORAGE_PREFIX}/shared/\" \"${AGENTTEAMS_FS}/shared/\" --overwrite 2>/dev/null || true\n mc mirror \"${AGENTTEAMS_STORAGE_PREFIX}/agents/\" \"${AGENTTEAMS_FS}/agents/\" --overwrite 2>/dev/null || true\n # Keep the canonical filesystem link in the Manager workspace.\n ln -sfn \"${AGENTTEAMS_FS}\" /root/manager-workspace/agentteams-fs\n ln -sfn \"${AGENTTEAMS_FS}\" /root/manager-workspace/agentteams-fs\nfi\n\n# K8s mode: sync workspace from cluster-internal MinIO\nif [ \"${AGENTTEAMS_RUNTIME}\" = \"k8s\" ]; then\n AGENTTEAMS_FS=\"/root/agentteams-fs\"\n mkdir -p \"${AGENTTEAMS_FS}/shared\" \"${AGENTTEAMS_FS}/agents\" \"${AGENTTEAMS_FS}/agentteams-config\"\n log \"Configuring mc alias for cluster MinIO...\"\n mc alias set agentteams \"${AGENTTEAMS_FS_ENDPOINT}\" \"${AGENTTEAMS_FS_ACCESS_KEY}\" \"${AGENTTEAMS_FS_SECRET_KEY}\"\n log \"Syncing workspace from MinIO...\"\n mc mirror \"${AGENTTEAMS_STORAGE_PREFIX}/manager/\" /root/manager-workspace/ --overwrite 2>/dev/null || true\n mc mirror \"${AGENTTEAMS_STORAGE_PREFIX}/\" \"${AGENTTEAMS_FS}/\" --overwrite 2>/dev/null || true\n ln -sfn \"${AGENTTEAMS_FS}\" /root/manager-workspace/agentteams-fs\n ln -sfn \"${AGENTTEAMS_FS}\" /root/manager-workspace/agentteams-fs\n touch \"${AGENTTEAMS_FS}/.initialized\"\nfi\n\n# ============================================================\n# Initialize / upgrade Manager workspace\n# First boot: full init via upgrade-builtins.sh\n# Subsequent boots: compare image version; upgrade only if changed\n# ============================================================\nmkdir -p /root/manager-workspace\n\nIMAGE_VERSION=$(cat /opt/agentteams/agent/.builtin-version 2>/dev/null || echo \"unknown\")\nINSTALLED_VERSION=$(cat /root/manager-workspace/.builtin-version 2>/dev/null || echo \"\")\n\nif [ ! -f /root/manager-workspace/.initialized ]; then\n log \"First boot: initializing manager workspace...\"\n bash /opt/agentteams/scripts/init/upgrade-builtins.sh\n touch /root/manager-workspace/.initialized\n log \"Manager workspace initialized (version: ${IMAGE_VERSION})\"\nelif [ \"${IMAGE_VERSION}\" != \"${INSTALLED_VERSION}\" ] || [ \"${IMAGE_VERSION}\" = \"latest\" ]; then\n log \"Upgrade detected: ${INSTALLED_VERSION} -> ${IMAGE_VERSION}${IMAGE_VERSION:+ (latest: always upgrade)}\"\n bash /opt/agentteams/scripts/init/upgrade-builtins.sh\n log \"Manager workspace upgraded to version: ${IMAGE_VERSION}\"\nelse\n log \"Workspace up to date (version: ${IMAGE_VERSION})\"\nfi\n\n# Local mode: wait for mc mirror initialization (shared + worker data in /root/agentteams-fs/)\nif [ \"${AGENTTEAMS_RUNTIME}\" != \"aliyun\" ] && [ \"${AGENTTEAMS_RUNTIME}\" != \"k8s\" ]; then\n log \"Waiting for MinIO storage initialization...\"\n _minio_wait=0\n while [ ! -f /root/agentteams-fs/.initialized ]; do\n sleep 2\n _minio_wait=$(( _minio_wait + 1 ))\n if [ \"${_minio_wait}\" -ge 60 ]; then\n log \"ERROR: MinIO storage initialization timed out after 120s\"\n exit 1\n fi\n done\n log \"MinIO storage initialized\"\nfi\n\n# ============================================================\n# Obtain Manager Matrix access token.\n#\n# Priority:\n# 1. Pre-injected token (AGENTTEAMS_MANAGER_MATRIX_TOKEN) — set by the\n# controller in both AppService mode (where no password exists) and\n# legacy mode (to avoid a redundant login round-trip).\n# 2. Password-based login — fallback for containers started without\n# controller involvement (manual docker run, legacy installs).\n# ============================================================\nif [ -n \"${AGENTTEAMS_MANAGER_MATRIX_TOKEN:-}\" ]; then\n MANAGER_TOKEN=\"${AGENTTEAMS_MANAGER_MATRIX_TOKEN}\"\n log \"Manager Matrix token pre-injected by controller (token prefix: ${MANAGER_TOKEN:0:10}...)\"\nelif [ \"${AGENTTEAMS_RUNTIME}\" = \"k8s\" ]; then\n log \"K8s mode: obtaining Manager Matrix token via password login...\"\n _LOGIN_RESPONSE=$(curl -s -X POST ${AGENTTEAMS_MATRIX_URL}/_matrix/client/v3/login \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"type\": \"m.login.password\",\n \"identifier\": {\"type\": \"m.id.user\", \"user\": \"manager\"},\n \"password\": \"'\"${AGENTTEAMS_MANAGER_PASSWORD}\"'\"\n }' 2>&1)\n MANAGER_TOKEN=$(echo \"${_LOGIN_RESPONSE}\" | jq -r '.access_token' 2>/dev/null)\n if [ -z \"${MANAGER_TOKEN}\" ] || [ \"${MANAGER_TOKEN}\" = \"null\" ]; then\n log \"ERROR: Failed to obtain Manager Matrix token\"\n log \"ERROR: Login response was: ${_LOGIN_RESPONSE}\"\n exit 1\n fi\n log \"Manager Matrix token obtained (token prefix: ${MANAGER_TOKEN:0:10}...)\"\nelse\nlog \"Registering human admin Matrix account...\"\ncurl -sf -X POST ${AGENTTEAMS_MATRIX_URL}/_matrix/client/v3/register \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"username\": \"'\"${AGENTTEAMS_ADMIN_USER}\"'\",\n \"password\": \"'\"${AGENTTEAMS_ADMIN_PASSWORD}\"'\",\n \"auth\": {\n \"type\": \"m.login.registration_token\",\n \"token\": \"'\"${AGENTTEAMS_REGISTRATION_TOKEN}\"'\"\n }\n }' > /dev/null 2>&1 || log \"Admin account may already exist\"\n\nlog \"Registering Manager Agent Matrix account...\"\ncurl -sf -X POST ${AGENTTEAMS_MATRIX_URL}/_matrix/client/v3/register \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"username\": \"manager\",\n \"password\": \"'\"${AGENTTEAMS_MANAGER_PASSWORD}\"'\",\n \"auth\": {\n \"type\": \"m.login.registration_token\",\n \"token\": \"'\"${AGENTTEAMS_REGISTRATION_TOKEN}\"'\"\n }\n }' > /dev/null 2>&1 || log \"Manager account may already exist\"\n\n# Get Manager Agent's Matrix access token\nlog \"Obtaining Manager Matrix access token...\"\n_LOGIN_RESPONSE=$(curl -s -X POST ${AGENTTEAMS_MATRIX_URL}/_matrix/client/v3/login \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"type\": \"m.login.password\",\n \"identifier\": {\"type\": \"m.id.user\", \"user\": \"manager\"},\n \"password\": \"'\"${AGENTTEAMS_MANAGER_PASSWORD}\"'\"\n }' 2>&1)\n_LOGIN_EXIT=$?\nlog \"Matrix login HTTP exit code: ${_LOGIN_EXIT}\"\nlog \"Matrix login response: ${_LOGIN_RESPONSE}\"\n\nMANAGER_TOKEN=$(echo \"${_LOGIN_RESPONSE}\" | jq -r '.access_token' 2>/dev/null)\n\nif [ -z \"${MANAGER_TOKEN}\" ] || [ \"${MANAGER_TOKEN}\" = \"null\" ]; then\n log \"ERROR: Failed to obtain Manager Matrix token (exit=${_LOGIN_EXIT})\"\n log \"ERROR: Login response was: ${_LOGIN_RESPONSE}\"\n exit 1\nfi\nlog \"Manager Matrix token obtained (token prefix: ${MANAGER_TOKEN:0:10}...)\"\nfi\n\n# ============================================================\n# Higress Console initialization\n# Docker mode: full setup-higress.sh (internal Higress at localhost:8001)\n# K8s mode: skip — controller Initializer handles Higress setup\n# Cloud (aliyun) mode: skip entirely (Higress managed externally)\n# ============================================================\n_HIGRESS_CONSOLE_URL=\"\"\n_HIGRESS_USER=\"${AGENTTEAMS_ADMIN_USER}\"\n_HIGRESS_PASS=\"${AGENTTEAMS_ADMIN_PASSWORD}\"\nif [ \"${AGENTTEAMS_RUNTIME}\" = \"k8s\" ]; then\n log \"K8s mode: skipping Higress initialization (handled by controller)\"\nelif [ \"${AGENTTEAMS_RUNTIME}\" != \"aliyun\" ]; then\n _HIGRESS_CONSOLE_URL=\"http://127.0.0.1:8001\"\nfi\n\nif [ -n \"${_HIGRESS_CONSOLE_URL}\" ]; then\n COOKIE_FILE=\"/tmp/higress-session-cookie\"\n\n log \"Waiting for Higress Console (${_HIGRESS_CONSOLE_URL}) to be fully ready and initializing admin...\"\n INIT_DONE=false\n for i in $(seq 1 90); do\n INIT_RESULT=$(curl -s -X POST \"${_HIGRESS_CONSOLE_URL}/system/init\" \\\n -H 'Content-Type: application/json' \\\n -d '{\"adminUser\":{\"name\":\"'\"${_HIGRESS_USER}\"'\",\"password\":\"'\"${_HIGRESS_PASS}\"'\",\"displayName\":\"'\"${_HIGRESS_USER}\"'\"}}' 2>/dev/null) || true\n if echo \"${INIT_RESULT}\" | grep -qE '\"success\":true|already.?init' 2>/dev/null; then\n INIT_DONE=true\n break\n fi\n if echo \"${INIT_RESULT}\" | grep -q '\"name\"' 2>/dev/null; then\n INIT_DONE=true\n break\n fi\n sleep 2\n done\n\n if [ \"${INIT_DONE}\" != \"true\" ]; then\n log \"ERROR: Higress Console did not become ready within 180s\"\n exit 1\n fi\n log \"Higress Console init done\"\n\n log \"Logging into Higress Console...\"\n LOGIN_OK=false\n for i in $(seq 1 10); do\n HTTP_CODE=$(curl -s -o /dev/null -w '%{http_code}' -X POST \"${_HIGRESS_CONSOLE_URL}/session/login\" \\\n -H 'Content-Type: application/json' \\\n -c \"${COOKIE_FILE}\" \\\n -d '{\"username\":\"'\"${_HIGRESS_USER}\"'\",\"password\":\"'\"${_HIGRESS_PASS}\"'\"}' 2>/dev/null) || true\n if { [ \"${HTTP_CODE}\" = \"200\" ] || [ \"${HTTP_CODE}\" = \"201\" ]; } && [ -f \"${COOKIE_FILE}\" ] && [ -s \"${COOKIE_FILE}\" ]; then\n LOGIN_OK=true\n break\n fi\n log \"Login attempt $i (HTTP ${HTTP_CODE}), retrying in 3s...\"\n sleep 3\n done\n\n if [ \"${LOGIN_OK}\" != \"true\" ]; then\n log \"ERROR: Could not login to Higress Console after retries\"\n exit 1\n fi\n log \"Higress Console login successful\"\n\n VERIFY_CODE=$(curl -s -o /dev/null -w '%{http_code}' \"${_HIGRESS_CONSOLE_URL}/v1/consumers\" -b \"${COOKIE_FILE}\" 2>/dev/null) || true\n if [ \"${VERIFY_CODE}\" = \"200\" ]; then\n log \"Console session verified (cookie valid)\"\n else\n log \"WARNING: Console session may be invalid (verify returned HTTP ${VERIFY_CODE})\"\n rm -f \"${COOKIE_FILE}\"\n for i in $(seq 1 5); do\n curl -s -o /dev/null -w '%{http_code}' -X POST \"${_HIGRESS_CONSOLE_URL}/session/login\" \\\n -H 'Content-Type: application/json' \\\n -c \"${COOKIE_FILE}\" \\\n -d '{\"username\":\"'\"${_HIGRESS_USER}\"'\",\"password\":\"'\"${_HIGRESS_PASS}\"'\"}' 2>/dev/null\n VERIFY2=$(curl -s -o /dev/null -w '%{http_code}' \"${_HIGRESS_CONSOLE_URL}/v1/consumers\" -b \"${COOKIE_FILE}\" 2>/dev/null) || true\n if [ \"${VERIFY2}\" = \"200\" ]; then\n log \"Re-login successful, session verified\"\n break\n fi\n sleep 2\n done\n fi\n\n export HIGRESS_COOKIE_FILE=\"${COOKIE_FILE}\"\n export HIGRESS_CONSOLE_URL=\"${_HIGRESS_CONSOLE_URL}\"\n\n if [ \"${AGENTTEAMS_RUNTIME}\" = \"k8s\" ]; then\n # K8s mode: lightweight Higress config — only what's needed for LLM access\n source /opt/agentteams/scripts/lib/base.sh\n _k8s_higress_api() {\n local method=\"$1\" path=\"$2\" desc=\"$3\"; shift 3; local body=\"$*\"\n local tmpfile; tmpfile=$(mktemp)\n local http_code\n http_code=$(curl -s -o \"${tmpfile}\" -w '%{http_code}' -X \"${method}\" \"${_HIGRESS_CONSOLE_URL}${path}\" \\\n -b \"${COOKIE_FILE}\" -H 'Content-Type: application/json' -d \"${body}\" 2>/dev/null) || true\n local response; response=$(cat \"${tmpfile}\" 2>/dev/null); rm -f \"${tmpfile}\"\n if echo \"${response}\" | grep -q '\"success\":true' 2>/dev/null; then\n log \"${desc} ... OK\"\n elif [ \"${http_code}\" = \"409\" ]; then\n log \"${desc} ... already exists, skipping\"\n elif [ \"${http_code}\" = \"200\" ] || [ \"${http_code}\" = \"201\" ] || [ \"${http_code}\" = \"204\" ]; then\n log \"${desc} ... OK (HTTP ${http_code})\"\n else\n log \"WARNING: ${desc} ... (HTTP ${http_code}): ${response}\"\n fi\n }\n\n # 1. Service Sources (DNS type → K8s Service FQDN)\n # Extract host:port from URLs for Higress service source registration\n _TUWUNEL_HOST=$(echo \"${AGENTTEAMS_MATRIX_URL}\" | sed 's|^http[s]*://||')\n _TUWUNEL_DOMAIN=$(echo \"${_TUWUNEL_HOST}\" | cut -d: -f1)\n _TUWUNEL_PORT=$(echo \"${_TUWUNEL_HOST}\" | cut -d: -f2)\n _k8s_higress_api POST /v1/service-sources \"Registering Tuwunel service source\" \\\n '{\"type\":\"dns\",\"name\":\"tuwunel\",\"domain\":\"'\"${_TUWUNEL_DOMAIN}\"'\",\"port\":'\"${_TUWUNEL_PORT}\"'}'\n\n if [ -n \"${AGENTTEAMS_ELEMENT_WEB_URL:-}\" ]; then\n _ELEMENT_HOST=$(echo \"${AGENTTEAMS_ELEMENT_WEB_URL}\" | sed 's|^http[s]*://||')\n _ELEMENT_DOMAIN=$(echo \"${_ELEMENT_HOST}\" | cut -d: -f1)\n _ELEMENT_PORT=$(echo \"${_ELEMENT_HOST}\" | cut -d: -f2)\n _k8s_higress_api POST /v1/service-sources \"Registering Element Web service source\" \\\n '{\"type\":\"dns\",\"name\":\"element-web\",\"domain\":\"'\"${_ELEMENT_DOMAIN}\"'\",\"port\":'\"${_ELEMENT_PORT}\"'}'\n fi\n\n # 2. Manager Consumer (key-auth)\n _k8s_higress_api POST /v1/consumers \"Creating Manager consumer\" \\\n '{\"name\":\"manager\",\"credentials\":[{\"type\":\"key-auth\",\"source\":\"BEARER\",\"values\":[\"'\"${AGENTTEAMS_MANAGER_GATEWAY_KEY}\"'\"]}]}'\n\n # 3. LLM Provider\n _LLM_PROVIDER=\"${AGENTTEAMS_LLM_PROVIDER:-qwen}\"\n _LLM_API_URL=\"${AGENTTEAMS_LLM_API_URL:-}\"\n if [ -z \"${_LLM_API_URL}\" ]; then\n case \"${_LLM_PROVIDER}\" in\n qwen) _LLM_API_URL=\"https://dashscope.aliyuncs.com/compatible-mode/v1\" ;;\n esac\n fi\n # 4. LLM Provider type-specific config\n case \"${_LLM_PROVIDER}\" in\n qwen)\n _k8s_higress_api POST /v1/ai/providers \"Creating LLM provider (qwen)\" \\\n '{\"type\":\"qwen\",\"name\":\"qwen\",\"tokens\":[\"'\"${AGENTTEAMS_LLM_API_KEY}\"'\"],\"protocol\":\"openai/v1\",\"tokenFailoverConfig\":{\"enabled\":false},\"rawConfigs\":{\"qwenEnableSearch\":false,\"qwenEnableCompatible\":true,\"qwenFileIds\":[],\"agentteamsMode\":true}}'\n ;;\n *)\n _BODY='{\"name\":\"'\"${_LLM_PROVIDER}\"'\",\"type\":\"openai\",\"tokens\":[\"'\"${AGENTTEAMS_LLM_API_KEY}\"'\"],\"modelMapping\":{},\"protocol\":\"openai/v1\",\"rawConfigs\":{\"agentteamsMode\":true}}'\n _k8s_higress_api POST /v1/ai/providers \"Creating LLM provider (${_LLM_PROVIDER})\" \"${_BODY}\"\n ;;\n esac\n\n # 5. AI Route (bind provider + consumer auth, /v1 prefix to avoid clash with Element Web catch-all)\n _k8s_higress_api POST /v1/ai/routes \"Creating AI Gateway route\" \\\n '{\"name\":\"default-ai-route\",\"domains\":[],\"pathPredicate\":{\"matchType\":\"PRE\",\"matchValue\":\"/v1\",\"caseSensitive\":false},\"upstreams\":[{\"provider\":\"'\"${_LLM_PROVIDER}\"'\",\"weight\":100,\"modelMapping\":{}}],\"authConfig\":{\"enabled\":true,\"allowedCredentialTypes\":[\"key-auth\"],\"allowedConsumers\":[\"manager\"]}}'\n\n # 6. Matrix Homeserver Route (/_matrix/* → Tuwunel, no auth)\n _k8s_higress_api POST /v1/routes \"Creating Matrix Homeserver route\" \\\n '{\"name\":\"matrix-homeserver\",\"domains\":[],\"path\":{\"matchType\":\"PRE\",\"matchValue\":\"/_matrix\"},\"services\":[{\"name\":\"tuwunel.dns\",\"port\":'\"${_TUWUNEL_PORT}\"',\"weight\":100}]}'\n\n # 7. Element Web Route (/ catch-all → Element Web, no auth)\n if [ -n \"${AGENTTEAMS_ELEMENT_WEB_URL:-}\" ]; then\n _k8s_higress_api POST /v1/routes \"Creating Element Web route\" \\\n '{\"name\":\"element-web\",\"domains\":[],\"path\":{\"matchType\":\"PRE\",\"matchValue\":\"/\"},\"services\":[{\"name\":\"element-web.dns\",\"port\":'\"${_ELEMENT_PORT}\"',\"weight\":100}]}'\n fi\n\n # 8. Remove Higress default landing page (Exact match on / takes precedence over Element Web catch-all)\n _k8s_higress_api DELETE /v1/routes/default \"Removing Higress default landing route\"\n\n log \"K8s Higress lightweight setup complete\"\n\n # Wait for AI plugin activation (~45s for first config)\n log \"Waiting for AI Gateway plugin activation (45s)...\"\n sleep 45\n else\n # Docker mode: full setup with all routes, domains, MCP servers\n /opt/agentteams/scripts/init/setup-higress.sh\n fi\nfi\n\n# ============================================================\n# Create admin DM room, persist to state.json, send welcome message\n# K8s mode: skip — controller ProvisionManager creates the Admin DM\n# room (Step 4 in service/provisioner.go) AND reconcileManagerWelcome\n# delivers the first-boot onboarding prompt once OpenClaw inside this\n# container has joined the room. In k8s mode the manager intentionally\n# does NOT have the admin password (only AGENTTEAMS_ADMIN_USER), so it\n# could not log in as admin to send the welcome itself anyway. The\n# Manager Agent discovers its admin DM room on first heartbeat via\n# state.json / `manage-state.sh --action set-admin-dm` (see HEARTBEAT.md\n# Step 1) — it does not need it to be pre-injected by this script.\n# Runs in both local and cloud modes (idempotent)\n# ============================================================\nif [ \"${AGENTTEAMS_RUNTIME}\" = \"k8s\" ]; then\n log \"K8s mode: skipping admin DM room creation and welcome message (both handled by agentteams-controller)\"\nelse\nMANAGER_FULL_ID=\"@manager:${MATRIX_DOMAIN}\"\nADMIN_FULL_ID=\"@${AGENTTEAMS_ADMIN_USER}:${MATRIX_DOMAIN}\"\n\nlog \"Logging in as admin to create DM room...\"\n_ADMIN_LOGIN=$(curl -sf -X POST \"${AGENTTEAMS_MATRIX_URL}/_matrix/client/v3/login\" \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"type\": \"m.login.password\",\n \"identifier\": {\"type\": \"m.id.user\", \"user\": \"'\"${AGENTTEAMS_ADMIN_USER}\"'\"},\n \"password\": \"'\"${AGENTTEAMS_ADMIN_PASSWORD}\"'\"\n }' 2>&1) || true\n\nADMIN_MATRIX_TOKEN=$(echo \"${_ADMIN_LOGIN}\" | jq -r '.access_token // empty' 2>/dev/null)\nif [ -z \"${ADMIN_MATRIX_TOKEN}\" ]; then\n log \"WARNING: Failed to login as admin, skipping DM room creation\"\nelse\n # Search for existing DM room with Manager (idempotent)\n DM_ROOM_ID=\"\"\n _JOINED_ROOMS=$(curl -sf \"${AGENTTEAMS_MATRIX_URL}/_matrix/client/v3/joined_rooms\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" 2>/dev/null \\\n | jq -r '.joined_rooms[]' 2>/dev/null) || true\n for _rid in ${_JOINED_ROOMS}; do\n _members=$(curl -sf \"${AGENTTEAMS_MATRIX_URL}/_matrix/client/v3/rooms/${_rid}/members\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" 2>/dev/null \\\n | jq -r '.chunk[].state_key' 2>/dev/null) || continue\n _count=$(echo \"${_members}\" | wc -l | xargs)\n if [ \"${_count}\" = \"2\" ] && echo \"${_members}\" | grep -q \"@manager:\"; then\n DM_ROOM_ID=\"${_rid}\"\n break\n fi\n done\n\n if [ -n \"${DM_ROOM_ID}\" ]; then\n log \"Existing DM room found: ${DM_ROOM_ID}\"\n else\n log \"Creating DM room with Manager...\"\n _RAW=$(curl -s -w '\\nHTTP_CODE:%{http_code}' -X POST \"${AGENTTEAMS_MATRIX_URL}/_matrix/client/v3/createRoom\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d \"{\\\"is_direct\\\":true,\\\"invite\\\":[\\\"${MANAGER_FULL_ID}\\\"],\\\"preset\\\":\\\"trusted_private_chat\\\"}\" 2>&1) || true\n _HTTP_CODE=$(echo \"${_RAW}\" | tail -1 | sed 's/HTTP_CODE://')\n _CREATE_RESP=$(echo \"${_RAW}\" | sed '$d')\n DM_ROOM_ID=$(echo \"${_CREATE_RESP}\" | jq -r '.room_id // empty' 2>/dev/null)\n if [ -n \"${DM_ROOM_ID}\" ]; then\n log \"DM room created: ${DM_ROOM_ID}\"\n else\n log \"WARNING: Failed to create DM room (HTTP ${_HTTP_CODE}): ${_CREATE_RESP}\"\n fi\n fi\n\n # Persist admin DM room ID to state.json\n if [ -n \"${DM_ROOM_ID}\" ]; then\n STATE_SCRIPT=\"/opt/agentteams/agent/skills/task-management/scripts/manage-state.sh\"\n if [ -f \"${STATE_SCRIPT}\" ]; then\n bash \"${STATE_SCRIPT}\" --action init 2>/dev/null || true\n bash \"${STATE_SCRIPT}\" --action set-admin-dm --room-id \"${DM_ROOM_ID}\" 2>/dev/null || true\n log \"Admin DM room persisted to state.json: ${DM_ROOM_ID}\"\n fi\n fi\n\n # Schedule welcome message in background (only on first boot)\n if [ -n \"${DM_ROOM_ID}\" ] && [ ! -f \"/root/manager-workspace/soul-configured\" ]; then\n log \"Scheduling welcome message (background, waiting for OpenClaw to start)...\"\n (\n _AGENTTEAMS_LANGUAGE=\"${AGENTTEAMS_LANGUAGE:-zh}\"\n _AGENTTEAMS_TIMEZONE=\"${TZ:-Asia/Shanghai}\"\n _wait=0\n _ready=false\n while [ \"${_wait}\" -lt 300 ]; do\n if curl -sf http://127.0.0.1:18799/ > /dev/null 2>&1; then\n _ready=true\n break\n fi\n sleep 3\n _wait=$((_wait + 3))\n done\n if [ \"${_ready}\" != \"true\" ]; then\n echo \"[manager] WARNING: OpenClaw gateway not ready within 300s, skipping welcome message\"\n exit 0\n fi\n # Ensure Manager has joined the DM room before sending the welcome\n # message. Without this, there is a race between OpenClaw's Matrix\n # auto-join and the message send — the message may land before Manager\n # joins, so OpenClaw's /sync never picks it up.\n _join_ok=false\n for _join_attempt in 1 2 3; do\n if curl -sf -X POST \"${AGENTTEAMS_MATRIX_URL}/_matrix/client/v3/rooms/${DM_ROOM_ID}/join\" \\\n -H \"Authorization: Bearer ${MANAGER_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d '{}' > /dev/null 2>&1; then\n echo \"[manager] Manager joined DM room before welcome message\"\n _join_ok=true\n break\n fi\n sleep 2\n done\n if [ \"${_join_ok}\" != \"true\" ]; then\n echo \"[manager] WARNING: Manager join request failed after 3 attempts (may already be joined)\"\n fi\n _welcome_msg=\"This is an automated message from the AgentTeams setup. This is a fresh installation.\n\n--- Installation Context ---\nUser Language: ${_AGENTTEAMS_LANGUAGE} (zh = Chinese, en = English)\nUser Timezone: ${_AGENTTEAMS_TIMEZONE} (IANA timezone identifier)\n---\n\nYou are an AI agent that manages a team of worker agents. Your identity and personality have not been configured yet — the human admin is about to meet you for the first time.\n\nPlease begin the onboarding conversation:\n\n1. Greet the admin warmly and briefly describe what you can do (coordinate workers, manage tasks, run multi-agent projects)\n2. The user has selected \\\"${_AGENTTEAMS_LANGUAGE}\\\" as their preferred language during installation. Use this language for your greeting and all subsequent communication.\n3. The user's timezone is ${_AGENTTEAMS_TIMEZONE}. Based on this timezone, you may infer their likely region and suggest additional language options.\n4. Ask them: a) What would they like to call you? b) Communication style preference? c) Any behavior guidelines? d) Confirm default language\n5. After they reply, write their preferences to ~/SOUL.md\n6. Confirm what you wrote, and ask if they would like to adjust anything\n7. Once confirmed, run: touch ~/soul-configured\n\nThe human admin will start chatting shortly.\"\n _txn_id=\"welcome-$(date +%s)\"\n _payload=$(jq -nc --arg body \"${_welcome_msg}\" '{\"msgtype\":\"m.text\",\"body\":$body}')\n _raw=$(curl -s -w '\\nHTTP_CODE:%{http_code}' -X PUT \"${AGENTTEAMS_MATRIX_URL}/_matrix/client/v3/rooms/${DM_ROOM_ID}/send/m.room.message/${_txn_id}\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d \"${_payload}\" 2>&1) || true\n _http_code=$(echo \"${_raw}\" | tail -1 | sed 's/HTTP_CODE://')\n _send_resp=$(echo \"${_raw}\" | sed '$d')\n if echo \"${_send_resp}\" | jq -e '.event_id' > /dev/null 2>&1; then\n echo \"[manager] Welcome message sent to DM room\"\n else\n echo \"[manager] WARNING: Failed to send welcome message (HTTP ${_http_code}): ${_send_resp}\"\n fi\n ) &\n log \"Welcome message background process started (PID: $!)\"\n fi\nfi\nfi # end K8s mode skip for admin DM room\n\n# ============================================================\n# Generate Manager Agent openclaw.json from template\n# ============================================================\nlog \"Generating Manager openclaw.json...\"\nexport MANAGER_MATRIX_TOKEN=\"${MANAGER_TOKEN}\"\nexport MANAGER_GATEWAY_KEY=\"${AGENTTEAMS_MANAGER_GATEWAY_KEY}\"\n# Resolve model parameters based on model name\nMODEL_NAME=\"${AGENTTEAMS_DEFAULT_MODEL:-qwen3.6-plus}\"\ncase \"${MODEL_NAME}\" in\n gpt-5.3-codex|gpt-5-mini|gpt-5-nano)\n export MODEL_CONTEXT_WINDOW=400000 MODEL_MAX_TOKENS=128000 ;;\n claude-opus-4-6)\n export MODEL_CONTEXT_WINDOW=1000000 MODEL_MAX_TOKENS=128000 ;;\n claude-sonnet-4-6)\n export MODEL_CONTEXT_WINDOW=1000000 MODEL_MAX_TOKENS=64000 ;;\n claude-haiku-4-5)\n export MODEL_CONTEXT_WINDOW=200000 MODEL_MAX_TOKENS=64000 ;;\n qwen3.6-plus|qwen3.5-plus)\n export MODEL_CONTEXT_WINDOW=200000 MODEL_MAX_TOKENS=64000 ;;\n deepseek-chat|deepseek-reasoner|kimi-k2.5)\n export MODEL_CONTEXT_WINDOW=256000 MODEL_MAX_TOKENS=128000 ;;\n glm-5|MiniMax-M2.7|MiniMax-M2.7-highspeed|MiniMax-M2.5)\n export MODEL_CONTEXT_WINDOW=200000 MODEL_MAX_TOKENS=128000 ;;\n *)\n export MODEL_CONTEXT_WINDOW=150000 MODEL_MAX_TOKENS=128000 ;;\nesac\nexport MODEL_REASONING=true\n\n# Override with user-supplied custom model parameters from env (set during install)\n[ -n \"${AGENTTEAMS_MODEL_CONTEXT_WINDOW:-}\" ] && export MODEL_CONTEXT_WINDOW=\"${AGENTTEAMS_MODEL_CONTEXT_WINDOW}\"\n[ -n \"${AGENTTEAMS_MODEL_MAX_TOKENS:-}\" ] && export MODEL_MAX_TOKENS=\"${AGENTTEAMS_MODEL_MAX_TOKENS}\"\n[ -n \"${AGENTTEAMS_MODEL_REASONING:-}\" ] && export MODEL_REASONING=\"${AGENTTEAMS_MODEL_REASONING}\"\n\n# E2EE: convert AGENTTEAMS_MATRIX_E2EE to JSON boolean for template substitution\nif [ \"${AGENTTEAMS_MATRIX_E2EE:-0}\" = \"1\" ] || [ \"${AGENTTEAMS_MATRIX_E2EE:-}\" = \"true\" ]; then\n export MATRIX_E2EE_ENABLED=true\nelse\n export MATRIX_E2EE_ENABLED=false\nfi\nlog \"Matrix E2EE: ${MATRIX_E2EE_ENABLED}\"\n\n# Resolve input modalities: only vision-capable models get \"image\"\ncase \"${MODEL_NAME}\" in\n gpt-5.4|gpt-5.3-codex|gpt-5-mini|gpt-5-nano|claude-opus-4-6|claude-sonnet-4-6|claude-haiku-4-5|qwen3.6-plus|qwen3.5-plus|kimi-k2.5)\n export MODEL_INPUT='[\"text\", \"image\"]' ;;\n *)\n export MODEL_INPUT='[\"text\"]' ;;\nesac\n# Override with user-supplied vision setting from env\nif [ \"${AGENTTEAMS_MODEL_VISION:-}\" = \"true\" ]; then\n export MODEL_INPUT='[\"text\", \"image\"]'\nelif [ \"${AGENTTEAMS_MODEL_VISION:-}\" = \"false\" ]; then\n export MODEL_INPUT='[\"text\"]'\nfi\n\nlog \"Model: ${MODEL_NAME} (context=${MODEL_CONTEXT_WINDOW}, maxTokens=${MODEL_MAX_TOKENS}, reasoning=${MODEL_REASONING}, input=${MODEL_INPUT})\"\n\nif [ -f /root/manager-workspace/openclaw.json ]; then\n log \"Manager openclaw.json already exists, updating dynamic fields only (preserving user customizations)...\"\n # Merge known models into existing config (add missing, preserve user-added)\n # Use known-models.json (valid JSON) instead of template (contains ${VAR} placeholders)\n KNOWN_MODELS=$(cat /opt/agentteams/configs/known-models.json 2>/dev/null || echo '[]')\n jq --arg token \"${MANAGER_TOKEN}\" \\\n --arg key \"${AGENTTEAMS_MANAGER_GATEWAY_KEY}\" \\\n --arg model \"${MODEL_NAME}\" \\\n --arg emb_model \"${AGENTTEAMS_EMBEDDING_MODEL}\" \\\n --arg aigw_domain \"${AI_GATEWAY_DOMAIN}\" \\\n --arg matrix_user_id \"@manager:${MATRIX_DOMAIN}\" \\\n --argjson e2ee \"${MATRIX_E2EE_ENABLED}\" \\\n --argjson known_models \"${KNOWN_MODELS}\" \\\n --argjson ctx \"${MODEL_CONTEXT_WINDOW}\" \\\n --argjson max \"${MODEL_MAX_TOKENS}\" \\\n --argjson reasoning \"${MODEL_REASONING}\" \\\n --argjson input \"${MODEL_INPUT}\" \\\n '\n # Merge known models: add any model id not already present\n .models.providers[\"agentteams-gateway\"].models as $existing\n | ($existing | map(.id)) as $existing_ids\n | ($known_models | map(select(.id as $id | $existing_ids | index($id) | not))) as $new\n | .models.providers[\"agentteams-gateway\"].models = ($existing + $new)\n # Ensure the user-chosen default model is in the list (custom model support)\n | if (.models.providers[\"agentteams-gateway\"].models | map(.id) | index($model) | not) then\n .models.providers[\"agentteams-gateway\"].models += [{\"id\": $model, \"name\": $model, \"reasoning\": $reasoning, \"contextWindow\": $ctx, \"maxTokens\": $max, \"input\": $input}]\n else . end\n # Rebuild model aliases from the full models list\n | (.models.providers[\"agentteams-gateway\"].models | map({ (\"agentteams-gateway/\" + .id): { \"alias\": .id } }) | add // {}) as $aliases\n | .agents.defaults.models = ((.agents.defaults.models // {}) + $aliases)\n | .channels.matrix.accessToken = $token | .channels.matrix.userId = $matrix_user_id | .models.providers[\"agentteams-gateway\"].apiKey = $key\n | ((.hooks.token // \"\") as $ht | if $ht == $key or $ht == ($key + \"-hooks\" | @base64) then del(.hooks) else . end)\n | .agents.defaults.model.primary = (\"agentteams-gateway/\" + $model)\n | .commands.restart = true\n | .gateway.port = 18799\n | .gateway.bind = \"lan\"\n | .gateway.controlUi = ((.gateway.controlUi // {}) + {\"dangerouslyDisableDeviceAuth\": true, \"allowInsecureAuth\": true, \"allowedOrigins\": [\"*\"]})\n | .channels.matrix.encryption = $e2ee\n | .channels.matrix.network = ((.channels.matrix.network // {}) + {\"dangerouslyAllowPrivateNetwork\": true})\n | .channels.matrix.autoJoin = \"always\"\n # OpenClaw YOLO defaults: host exec without approval prompts (see openclaw docs tools/exec-approvals)\n | .tools = (.tools // {})\n | .tools.exec = ((.tools.exec // {}) + {\"host\":\"gateway\",\"security\":\"full\",\"ask\":\"off\"})\n | .tools.elevated = (.tools.elevated // {})\n | .tools.elevated.enabled = true\n | .tools.elevated.allowFrom |= ((. // {}) | .matrix = [\"*\"])\n | .agents.defaults.elevatedDefault = \"full\"\n # Ensure memorySearch config exists (embedding model for memory) — skip if embedding model is empty\n | if $emb_model != \"\" then .agents.defaults.memorySearch //= {\"provider\":\"openai\",\"model\":$emb_model,\"remote\":{\"baseUrl\":(\"http://\" + $aigw_domain + \":8080/v1\"),\"apiKey\":$key}} else . end\n ' \\\n /root/manager-workspace/openclaw.json > /tmp/openclaw.json.tmp && \\\n mv /tmp/openclaw.json.tmp /root/manager-workspace/openclaw.json\n # Disable openclaw's observe-recovery mechanism which compares config against\n # a lastKnownGood baseline in config-health.json. When meta is missing from the\n # current file but present in the baseline, observe-recovery restores from .bak,\n # undoing user customizations (plugins, channels, etc).\n # Clearing config-health.json removes the baseline so observe-recovery won't\n # interfere, while preserving .bak as a backup.\n rm -f /root/manager-workspace/.openclaw/logs/config-health.json\n # Verify the token was written correctly\n _written_token=$(jq -r '.channels.matrix.accessToken' /root/manager-workspace/openclaw.json 2>/dev/null)\n if [ -z \"${_written_token}\" ] || [ \"${_written_token}\" = \"null\" ]; then\n log \"ERROR: Matrix token was not written correctly to openclaw.json (got: ${_written_token})\"\n else\n log \"Matrix token written to openclaw.json (prefix: ${_written_token:0:10}...)\"\n fi\nelse\n log \"Manager openclaw.json not found, generating from template...\"\n envsubst < /opt/agentteams/configs/manager-openclaw.json.tmpl > /root/manager-workspace/openclaw.json\n # Post-envsubst injection: memorySearch + custom model (single jq pass when possible)\n if ! jq -e --arg model \"${MODEL_NAME}\" '.models.providers[\"agentteams-gateway\"].models | map(.id) | index($model)' /root/manager-workspace/openclaw.json > /dev/null 2>&1; then\n log \"Custom model '${MODEL_NAME}' not in built-in list, injecting into config...\"\n jq --arg emb_model \"${AGENTTEAMS_EMBEDDING_MODEL}\" \\\n --arg aigw_domain \"${AI_GATEWAY_DOMAIN}\" \\\n --arg key \"${AGENTTEAMS_MANAGER_GATEWAY_KEY}\" \\\n --arg model \"${MODEL_NAME}\" \\\n --argjson ctx \"${MODEL_CONTEXT_WINDOW}\" \\\n --argjson max \"${MODEL_MAX_TOKENS}\" \\\n --argjson reasoning \"${MODEL_REASONING}\" \\\n --argjson input \"${MODEL_INPUT}\" \\\n '\n (if $emb_model != \"\" then .agents.defaults.memorySearch = {\"provider\":\"openai\",\"model\":$emb_model,\"remote\":{\"baseUrl\":(\"http://\" + $aigw_domain + \":8080/v1\"),\"apiKey\":$key}} else . end)\n | .models.providers[\"agentteams-gateway\"].models += [{\"id\": $model, \"name\": $model, \"reasoning\": $reasoning, \"contextWindow\": $ctx, \"maxTokens\": $max, \"input\": $input}]\n | .agents.defaults.models += {(\"agentteams-gateway/\" + $model): {\"alias\": $model}}\n ' /root/manager-workspace/openclaw.json > /tmp/openclaw.json.tmp && \\\n mv /tmp/openclaw.json.tmp /root/manager-workspace/openclaw.json\n elif [ -n \"${AGENTTEAMS_EMBEDDING_MODEL}\" ]; then\n jq --arg emb_model \"${AGENTTEAMS_EMBEDDING_MODEL}\" \\\n --arg aigw_domain \"${AI_GATEWAY_DOMAIN}\" \\\n --arg key \"${AGENTTEAMS_MANAGER_GATEWAY_KEY}\" \\\n '.agents.defaults.memorySearch = {\"provider\":\"openai\",\"model\":$emb_model,\"remote\":{\"baseUrl\":(\"http://\" + $aigw_domain + \":8080/v1\"),\"apiKey\":$key}}' \\\n /root/manager-workspace/openclaw.json > /tmp/openclaw.json.tmp && \\\n mv /tmp/openclaw.json.tmp /root/manager-workspace/openclaw.json\n fi\n _written_token=$(jq -r '.channels.matrix.accessToken' /root/manager-workspace/openclaw.json 2>/dev/null)\n log \"Matrix token written from template (prefix: ${_written_token:0:10}...)\"\nfi\n\n# Cloud/K8s mode: overlay cloud-specific settings onto generated config\nif [ \"${AGENTTEAMS_RUNTIME}\" = \"aliyun\" ] || [ \"${AGENTTEAMS_RUNTIME}\" = \"k8s\" ]; then\n log \"Applying cloud/k8s overlay to openclaw.json...\"\n jq --arg homeserver \"${AGENTTEAMS_MATRIX_URL}\" \\\n --arg gateway \"${AGENTTEAMS_AI_GATEWAY_URL}/v1\" \\\n --arg key \"${AGENTTEAMS_MANAGER_GATEWAY_KEY}\" \\\n '.channels.matrix.homeserver = $homeserver\n | .models.providers[\"agentteams-gateway\"].baseUrl = $gateway\n | .models.providers[\"agentteams-gateway\"].apiKey = $key\n | ((.hooks.token // \"\") as $ht | if $ht == $key or $ht == ($key + \"-hooks\" | @base64) then del(.hooks) else . end)\n | .commands.restart = false\n | if .agents.defaults.memorySearch then .agents.defaults.memorySearch.remote.baseUrl = $gateway | .agents.defaults.memorySearch.remote.apiKey = $key else . end' \\\n /root/manager-workspace/openclaw.json > /tmp/openclaw-cloud.json && \\\n mv /tmp/openclaw-cloud.json /root/manager-workspace/openclaw.json\n log \"Cloud/K8s overlay applied\"\nfi\n\n# ============================================================\n# Optional: enable openclaw-cms-plugin observability\n# Config is applied at runtime so secrets stay out of image layers.\n# ============================================================\nCMS_TRACES_ENABLED=\"$(echo \"${AGENTTEAMS_CMS_TRACES_ENABLED:-false}\" | tr '[:upper:]' '[:lower:]')\"\nif [ \"${CMS_TRACES_ENABLED}\" = \"true\" ]; then\n CMS_PLUGIN_NAME=\"openclaw-cms-plugin\"\n CMS_PLUGIN_DIR=\"${OPENCLAW_CMS_PLUGIN_DIR:-/opt/openclaw/extensions/openclaw-cms-plugin}\"\n CMS_PLUGIN_MANIFEST=\"${CMS_PLUGIN_DIR}/openclaw.plugin.json\"\n DIAG_PLUGIN_NAME=\"diagnostics-otel\"\n DIAG_PLUGIN_DIR=\"/opt/openclaw/extensions/diagnostics-otel\"\n CMS_LICENSE_KEY=\"${AGENTTEAMS_CMS_LICENSE_KEY:-}\"\n CMS_PROJECT=\"${AGENTTEAMS_CMS_PROJECT:-}\"\n CMS_METRICS_ENABLED=\"${AGENTTEAMS_CMS_METRICS_ENABLED:-false}\"\n\n if [ ! -f \"${CMS_PLUGIN_MANIFEST}\" ]; then\n log \"WARNING: ${CMS_PLUGIN_NAME} manifest not found at ${CMS_PLUGIN_MANIFEST}, skipping plugin config\"\n else\n _missing=0\n [ -z \"${AGENTTEAMS_CMS_ENDPOINT:-}\" ] && log \"WARNING: AGENTTEAMS_CMS_ENDPOINT is required when AGENTTEAMS_CMS_TRACES_ENABLED=true\" && _missing=1\n [ -z \"${CMS_LICENSE_KEY:-}\" ] && log \"WARNING: AGENTTEAMS_CMS_LICENSE_KEY is required when AGENTTEAMS_CMS_TRACES_ENABLED=true\" && _missing=1\n [ -z \"${AGENTTEAMS_CMS_WORKSPACE:-}\" ] && log \"WARNING: AGENTTEAMS_CMS_WORKSPACE is required when AGENTTEAMS_CMS_TRACES_ENABLED=true\" && _missing=1\n\n if [ \"${_missing}\" = \"0\" ]; then\n CMS_SERVICE_NAME=\"${AGENTTEAMS_CMS_SERVICE_NAME:-agentteams-manager}\"\n CMS_ENABLE_METRICS=\"${CMS_METRICS_ENABLED}\"\n DIAG_AVAILABLE=\"0\"\n _metrics_lc=\"$(echo \"${CMS_ENABLE_METRICS}\" | tr '[:upper:]' '[:lower:]')\"\n if [ \"${_metrics_lc}\" = \"true\" ]; then\n if [ -f \"${DIAG_PLUGIN_DIR}/package.json\" ]; then\n DIAG_AVAILABLE=\"1\"\n if [ ! -d \"${DIAG_PLUGIN_DIR}/node_modules\" ]; then\n log \"diagnostics-otel dependencies missing, installing...\"\n if (cd \"${DIAG_PLUGIN_DIR}\" && npm install --omit=dev --ignore-scripts >/tmp/agentteams-diag-install.log 2>&1); then\n log \"diagnostics-otel dependencies installed\"\n else\n log \"WARNING: diagnostics-otel npm install failed, metrics plugin may not load\"\n fi\n else\n log \"diagnostics-otel dependencies already present\"\n fi\n else\n log \"WARNING: diagnostics-otel package.json not found at ${DIAG_PLUGIN_DIR}, metrics plugin may not load\"\n fi\n fi\n\n log \"Applying ${CMS_PLUGIN_NAME} config to openclaw.json...\"\n jq --arg pluginName \"${CMS_PLUGIN_NAME}\" \\\n --arg pluginDir \"${CMS_PLUGIN_DIR}\" \\\n --arg endpoint \"${AGENTTEAMS_CMS_ENDPOINT}\" \\\n --arg licenseKey \"${CMS_LICENSE_KEY}\" \\\n --arg armsProject \"${CMS_PROJECT}\" \\\n --arg cmsWorkspace \"${AGENTTEAMS_CMS_WORKSPACE}\" \\\n --arg serviceName \"${CMS_SERVICE_NAME}\" \\\n --arg diagPluginName \"${DIAG_PLUGIN_NAME}\" \\\n --arg diagPluginDir \"${DIAG_PLUGIN_DIR}\" \\\n --arg metricsRaw \"${CMS_ENABLE_METRICS}\" \\\n --arg diagAvailableRaw \"${DIAG_AVAILABLE}\" \\\n '\n .plugins = (.plugins // {})\n | .plugins.load = (.plugins.load // {})\n | .plugins.entries = (.plugins.entries // {})\n | if (.plugins.allow | type) != \"array\" then .plugins.allow = [] else . end\n | if (.plugins.allow | index($pluginName)) == null then .plugins.allow += [$pluginName] else . end\n | if (.plugins.load.paths | type) != \"array\" then .plugins.load.paths = [] else . end\n | if (.plugins.load.paths | index($pluginDir)) == null then .plugins.load.paths += [$pluginDir] else . end\n | .plugins.entries[$pluginName] = {\n \"enabled\": true,\n \"config\": {\n \"endpoint\": $endpoint,\n \"headers\": {\n \"x-arms-license-key\": $licenseKey,\n \"x-arms-project\": $armsProject,\n \"x-cms-workspace\": $cmsWorkspace\n },\n \"serviceName\": $serviceName\n }\n }\n\n # diagnostics-otel metrics (optional)\n | ($metricsRaw | ascii_downcase) as $m\n | ($diagAvailableRaw == \"1\") as $diagAvailable\n | (($m == \"true\") and $diagAvailable) as $metricsEnabled\n | if $metricsEnabled then\n (if (.plugins.allow | index($diagPluginName)) == null then .plugins.allow += [$diagPluginName] else . end)\n | (if (.plugins.load.paths | index($diagPluginDir)) == null then .plugins.load.paths += [$diagPluginDir] else . end)\n | .plugins.entries[$diagPluginName].enabled = true\n | .diagnostics = (.diagnostics // {})\n | .diagnostics.otel = (.diagnostics.otel // {})\n | .diagnostics.enabled = true\n | .diagnostics.otel.enabled = true\n | .diagnostics.otel.endpoint = $endpoint\n | .diagnostics.otel.protocol = (.diagnostics.otel.protocol // \"http/protobuf\")\n | .diagnostics.otel.headers = {\n \"x-arms-license-key\": $licenseKey,\n \"x-arms-project\": $armsProject,\n \"x-cms-workspace\": $cmsWorkspace\n }\n | .diagnostics.otel.serviceName = $serviceName\n | .diagnostics.otel.metrics = true\n | .diagnostics.otel.traces = (.diagnostics.otel.traces // false)\n | .diagnostics.otel.logs = (.diagnostics.otel.logs // false)\n else\n .\n end\n ' /root/manager-workspace/openclaw.json > /tmp/openclaw-cms.json && \\\n mv /tmp/openclaw-cms.json /root/manager-workspace/openclaw.json\n log \"${CMS_PLUGIN_NAME} config applied (metrics=${CMS_ENABLE_METRICS}, service=${CMS_SERVICE_NAME})\"\n else\n log \"Skipping ${CMS_PLUGIN_NAME} config due to missing required env vars\"\n fi\n fi\nfi\n\n# ============================================================\n# Detect container runtime (for Worker creation)\n# ============================================================\nsource /opt/agentteams/scripts/lib/container-api.sh\nif container_api_available; then\n log \"Container runtime socket detected at ${CONTAINER_SOCKET} — direct Worker creation enabled\"\n export AGENTTEAMS_CONTAINER_RUNTIME=\"socket\"\nelif [ \"${AGENTTEAMS_RUNTIME}\" = \"aliyun\" ] || [ \"${AGENTTEAMS_RUNTIME}\" = \"k8s\" ]; then\n log \"Cloud/K8s mode — Workers created via controller API\"\n export AGENTTEAMS_CONTAINER_RUNTIME=\"cloud\"\nelse\n log \"No container runtime found — Worker creation will output install commands\"\n export AGENTTEAMS_CONTAINER_RUNTIME=\"none\"\nfi\n\n# ============================================================\n# Recreate Worker containers as needed after Manager restart.\n# Workers are on agentteams-net; Docker DNS resolves *-local.agentteams.io via\n# the Manager's network aliases, so IP changes don't require worker recreation.\n# Only recreate stopped/missing workers.\n# ============================================================\nif container_api_available; then\n _workers_json=$(agt get workers -o json 2>/dev/null || echo '{\"workers\":[]}')\n for _worker_name in $(echo \"${_workers_json}\" | jq -r '.workers[].name'); do\n [ -z \"${_worker_name}\" ] && continue\n\n _status=$(container_status_worker \"${_worker_name}\")\n if [ \"${_status}\" = \"running\" ]; then\n log \"Worker running: ${_worker_name}, skipping\"\n continue\n fi\n # Container missing or stopped — recreate.\n log \"Worker container ${_status}: ${_worker_name}, recreating...\"\n _creds_file=\"/data/worker-creds/${_worker_name}.env\"\n if [ -f \"${_creds_file}\" ]; then\n source \"${_creds_file}\"\n _runtime=$(echo \"${_workers_json}\" | jq -r --arg w \"${_worker_name}\" '.workers[] | select(.name == $w) | .runtime // \"openclaw\"')\n _recreated=false\n for _attempt in 1 2 3; do\n _env_map=\"\"\n _create_body=\"\"\n _env_map=$(jq -cn \\\n --arg name \"${_worker_name}\" \\\n --arg fak \"${_worker_name}\" \\\n --arg fsk \"${WORKER_MINIO_PASSWORD:-}\" \\\n --arg fs_domain \"${AGENTTEAMS_FS_DOMAIN:-fs-local.agentteams.io}\" \\\n --arg controller_url \"${AGENTTEAMS_CONTROLLER_URL:-}\" \\\n '{\n \"AGENTTEAMS_WORKER_NAME\": $name,\n \"AGENTTEAMS_FS_ENDPOINT\": (\"http://\" + ($fs_domain | split(\":\")[0]) + \":9000\"),\n \"AGENTTEAMS_FS_ACCESS_KEY\": $fak,\n \"AGENTTEAMS_FS_SECRET_KEY\": $fsk\n }\n | if $controller_url != \"\" then . + {\"AGENTTEAMS_CONTROLLER_URL\": $controller_url} else . end')\n _create_body=$(jq -cn --arg name \"${_worker_name}\" --arg runtime \"${_runtime}\" --argjson env \"${_env_map}\" '{name: $name, runtime: $runtime, env: $env}')\n worker_backend_create \"${_create_body}\" > /dev/null 2>&1 && _recreated=true && break\n log \" Attempt ${_attempt}/3 failed for ${_worker_name}, retrying in $((5 * _attempt))s...\"\n sleep $((5 * _attempt))\n done\n if [ \"${_recreated}\" = true ]; then\n log \" Recreated ${_runtime} worker: ${_worker_name}\"\n else\n log \" ERROR: Failed to recreate ${_worker_name} after 3 attempts\"\n fi\n else\n log \" WARNING: No credentials found for ${_worker_name} (${_creds_file} missing), skipping\"\n fi\n done\nfi\n\n# ============================================================\n# Notify workers of builtin updates if upgrade happened\n# Builtin files (AGENTS.md, skills) are already synced by upgrade-builtins.sh\n#\n# Cooldown: skip notification if the last successful notify was within\n# NOTIFY_COOLDOWN_SECS (default 3600s / 1 hour). This prevents repeated\n# notifications when the Manager crash-loops and re-runs upgrade-builtins\n# on every restart (e.g. IMAGE_VERSION=latest always triggers upgrade).\n# ============================================================\nNOTIFY_COOLDOWN_SECS=\"${AGENTTEAMS_NOTIFY_COOLDOWN_SECS:-3600}\"\nNOTIFY_TS_FILE=\"/root/manager-workspace/.last-worker-notify-ts\"\n\nif [ -f /root/manager-workspace/.upgrade-pending-worker-notify ]; then\n _now=$(date +%s)\n _last_notify=$(cat \"${NOTIFY_TS_FILE}\" 2>/dev/null || echo \"0\")\n _elapsed=$(( _now - _last_notify ))\n\n if [ \"${_elapsed}\" -lt \"${NOTIFY_COOLDOWN_SECS}\" ]; then\n log \"Skipping worker builtin notification (last notify ${_elapsed}s ago, cooldown ${NOTIFY_COOLDOWN_SECS}s)\"\n rm -f /root/manager-workspace/.upgrade-pending-worker-notify\n else\n log \"Notifying workers about builtin updates...\"\n _notify_ok=false\n _workers_json=$(agt get workers -o json 2>/dev/null || echo '{\"workers\":[]}')\n for _worker_name in $(echo \"${_workers_json}\" | jq -r '.workers[].name'); do\n [ -z \"${_worker_name}\" ] && continue\n _room_id=$(echo \"${_workers_json}\" | jq -r --arg w \"${_worker_name}\" '.workers[] | select(.name == $w) | .roomID // empty')\n if [ -n \"${_room_id}\" ]; then\n _worker_id=\"@${_worker_name}:${MATRIX_DOMAIN}\"\n _txn_id=\"upgrade-$(date +%s%N)\"\n _msg=\"@${_worker_name}:${MATRIX_DOMAIN} Manager upgraded builtin files (AGENTS.md, skills). Please use your file-sync skill to sync the latest config.\"\n _raw=$(curl -s -w '\\nHTTP_CODE:%{http_code}' -X PUT \\\n \"${AGENTTEAMS_MATRIX_URL}/_matrix/client/v3/rooms/${_room_id}/send/m.room.message/${_txn_id}\" \\\n -H \"Authorization: Bearer ${MANAGER_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d \"{\\\"msgtype\\\":\\\"m.text\\\",\\\"body\\\":\\\"${_msg}\\\",\\\"m.mentions\\\":{\\\"user_ids\\\":[\\\"${_worker_id}\\\"]}}\" \\\n 2>&1) || true\n _http_code=$(echo \"${_raw}\" | tail -1 | sed 's/HTTP_CODE://')\n _notify_resp=$(echo \"${_raw}\" | sed '$d')\n if echo \"${_notify_resp}\" | jq -e '.event_id' > /dev/null 2>&1; then\n log \" Notified ${_worker_name}\"; _notify_ok=true\n else\n log \" WARNING: Failed to notify ${_worker_name} (HTTP ${_http_code}): ${_notify_resp}\"\n fi\n fi\n done\n # Record timestamp only if at least one notification succeeded\n if [ \"${_notify_ok}\" = true ]; then\n echo \"${_now}\" > \"${NOTIFY_TS_FILE}\"\n fi\n rm -f /root/manager-workspace/.upgrade-pending-worker-notify\n fi\nfi\n\n# ============================================================\n# Start Manager Agent\n# ============================================================\nlog \"Starting Manager Agent (${MANAGER_RUNTIME})...\"\n\n# HOME is already set to /root/manager-workspace via docker run -e HOME=...\ncd \"${HOME}\"\n\n# Ensure host credential symlinks exist under HOME\nif [ -d \"/host-share\" ]; then\n [ -f \"/host-share/.gitconfig\" ] && ln -sf \"/host-share/.gitconfig\" \"${HOME}/.gitconfig\"\nfi\n\nlog \"HOME=${HOME} (manager-workspace, host-mounted)\"\n\n# ── Render agent doc templates ────────────────────────────────────────────\n# Replace ${VAR} placeholders with actual values so the AI agent reads\n# plain text and never needs to resolve environment variables.\nexport MANAGER_MATRIX_TOKEN MANAGER_TOKEN HIGRESS_COOKIE_FILE\nRENDER=/opt/agentteams/scripts/lib/render-skills.sh\nlog \"Rendering agent doc templates...\"\n# Manager-owned docs (workspace)\nbash \"$RENDER\" /root/manager-workspace/skills\nbash \"$RENDER\" /root/manager-workspace/skills-alpha\nbash \"$RENDER\" /root/manager-workspace AGENTS.md TOOLS.md HEARTBEAT.md SOUL.md\n# Worker templates (workspace + image) — rendered before push to MinIO\n# so Workers (including remote pip-install) receive plain text\nbash \"$RENDER\" /root/manager-workspace/worker-skills\nbash \"$RENDER\" /root/manager-workspace/worker-agent\nbash \"$RENDER\" /root/manager-workspace/copaw-worker-agent\nbash \"$RENDER\" /root/manager-workspace/hermes-worker-agent\nbash \"$RENDER\" /opt/agentteams/agent/worker-skills\nbash \"$RENDER\" /opt/agentteams/agent/worker-agent\nbash \"$RENDER\" /opt/agentteams/agent/copaw-worker-agent\nbash \"$RENDER\" /opt/agentteams/agent/hermes-worker-agent\nlog \"Agent doc templates rendered\"\n\n# Cloud mode: start background file sync (workspace ↔ OSS) and initial push\nif [ \"${AGENTTEAMS_RUNTIME}\" = \"aliyun\" ]; then\n log \"Syncing initial workspace to OSS...\"\n ensure_mc_credentials\n mc mirror /root/manager-workspace/ \"${AGENTTEAMS_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" 2>/dev/null || true\n\n # Local → OSS: change-triggered sync\n (\n while true; do\n CHANGED=$(find /root/manager-workspace/ -type f -newermt \"15 seconds ago\" 2>/dev/null | head -1)\n if [ -n \"${CHANGED}\" ]; then\n ensure_mc_credentials 2>/dev/null || true\n mc mirror /root/manager-workspace/ \"${AGENTTEAMS_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" --exclude \".npm/**\" \\\n --exclude \".local/**\" --exclude \".mc/**\" 2>/dev/null || true\n fi\n sleep 10\n done\n ) &\n log \"Local→OSS sync started (PID: $!)\"\n\n # OSS → Local: periodic pull (shared data, agent configs, manager openclaw.json)\n (\n while true; do\n sleep 60\n ensure_mc_credentials 2>/dev/null || true\n mc mirror \"${AGENTTEAMS_STORAGE_PREFIX}/shared/\" /root/agentteams-fs/shared/ --overwrite --newer-than \"1m\" 2>/dev/null || true\n mc mirror \"${AGENTTEAMS_STORAGE_PREFIX}/agents/\" /root/agentteams-fs/agents/ --overwrite --newer-than \"1m\" 2>/dev/null || true\n mc cp \"${AGENTTEAMS_STORAGE_PREFIX}/manager/openclaw.json\" /root/manager-workspace/openclaw.json 2>/dev/null || true\n done\n ) &\n log \"OSS→Local sync started (every 60s, PID: $!)\"\nfi\n\n# K8s mode: start background file sync (workspace ↔ MinIO)\nif [ \"${AGENTTEAMS_RUNTIME}\" = \"k8s\" ]; then\n log \"Syncing initial workspace to MinIO...\"\n mc mirror /root/manager-workspace/ \"${AGENTTEAMS_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" 2>/dev/null || true\n\n # Local → MinIO: change-triggered sync\n (\n while true; do\n CHANGED=$(find /root/manager-workspace/ -type f -newermt \"15 seconds ago\" 2>/dev/null | head -1)\n if [ -n \"${CHANGED}\" ]; then\n mc mirror /root/manager-workspace/ \"${AGENTTEAMS_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" --exclude \".npm/**\" \\\n --exclude \".local/**\" --exclude \".mc/**\" 2>/dev/null || true\n fi\n sleep 10\n done\n ) &\n log \"Local→MinIO sync started (PID: $!)\"\n\n # MinIO → Local: periodic pull (shared data, agent configs, manager openclaw.json)\n (\n while true; do\n sleep 60\n mc mirror \"${AGENTTEAMS_STORAGE_PREFIX}/shared/\" /root/agentteams-fs/shared/ --overwrite --newer-than \"1m\" 2>/dev/null || true\n mc mirror \"${AGENTTEAMS_STORAGE_PREFIX}/agents/\" /root/agentteams-fs/agents/ --overwrite --newer-than \"1m\" 2>/dev/null || true\n mc mirror \"${AGENTTEAMS_STORAGE_PREFIX}/agentteams-config/\" /root/agentteams-fs/agentteams-config/ --overwrite --newer-than \"1m\" 2>/dev/null || true\n mc cp \"${AGENTTEAMS_STORAGE_PREFIX}/manager/openclaw.json\" /root/manager-workspace/openclaw.json 2>/dev/null || true\n done\n ) &\n log \"MinIO→Local sync started (every 60s, PID: $!)\"\nfi\n\n# ============================================================\n# Auto-generate Manager mcporter config for pre-configured MCP servers\n# If AGENTTEAMS_GITHUB_TOKEN was set at install time, setup-higress.sh already\n# configured GitHub MCP on Higress. Run setup-mcp-server.sh now so that\n# config/mcporter.json exists when the Agent starts — no need to ask user for PAT.\n# ============================================================\nif [ -n \"${AGENTTEAMS_GITHUB_TOKEN}\" ] && [ \"${AGENTTEAMS_RUNTIME}\" != \"aliyun\" ] && [ \"${AGENTTEAMS_RUNTIME}\" != \"k8s\" ]; then\n if [ ! -f \"${HOME}/config/mcporter.json\" ]; then\n log \"Auto-generating Manager mcporter config for GitHub MCP (AGENTTEAMS_GITHUB_TOKEN set)...\"\n bash /opt/agentteams/agent/skills/mcp-server-management/scripts/setup-mcp-server.sh \\\n github \"${AGENTTEAMS_GITHUB_TOKEN}\" 2>&1 | while IFS= read -r line; do log \" [setup-mcp] ${line}\"; done || \\\n log \"WARNING: setup-mcp-server.sh failed — Agent may need to configure GitHub MCP manually\"\n else\n log \"Manager mcporter config already exists, skipping auto-generate\"\n fi\nfi\n\n# ============================================================\n# Runtime-specific startup\n# ============================================================\nif [ \"${MANAGER_RUNTIME}\" = \"copaw\" ] || [ \"${MANAGER_RUNTIME}\" = \"qwenpaw\" ]; then\n # Delegate to QwenPaw startup script\n exec /opt/agentteams/scripts/init/start-qwenpaw-manager.sh\nelse\n # ── OpenClaw Runtime ─────────────────────────────────────────────────────\n log \"Starting OpenClaw Manager...\"\n\n export OPENCLAW_CONFIG_PATH=\"/root/manager-workspace/openclaw.json\"\n\n # Symlink to default OpenClaw config path so CLI commands find the config\n mkdir -p \"${HOME}/.openclaw\"\n ln -sf \"/root/manager-workspace/openclaw.json\" \"${HOME}/.openclaw/openclaw.json\"\n\n # Clean orphaned session write locks (e.g. from SIGKILL or crash before exit handlers)\n # Prevents \"session file locked (timeout 10000ms)\" when PID was reused\n find \"${HOME}/.openclaw/agents\" -name \"*.jsonl.lock\" -delete 2>/dev/null || true\n log \"Cleaned up any orphaned session write locks\"\n\n # Clean Matrix crypto storage (SQLite WAL may be corrupted after unclean shutdown)\n # Crypto state is re-negotiated on startup; losing it only means re-establishing E2EE sessions\n rm -rf \"${HOME}/.openclaw/matrix\" 2>/dev/null || true\n log \"Cleaned Matrix crypto storage (will re-establish E2EE sessions)\"\n\n # Launch OpenClaw\n # Disable full-process respawn so the CLI uses its internal restart loop.\n # Without this, config reload spawns a detached child and exits, then\n # supervisord restarts the CLI — resulting in two gateway processes.\n export OPENCLAW_NO_RESPAWN=1\n\n # Optional matrix-plugin trace logging — when AGENTTEAMS_MATRIX_DEBUG=1 is set\n # in the manager environment (propagated by install / supervisord), turn on\n # OPENCLAW_MATRIX_DEBUG so the matrix plugin emits structured INFO-level\n # lifecycle traces (sync.state transitions, room.invite/join, message\n # handler arrival + filter outcomes). Useful for diagnosing \"worker never\n # joined the room\" / \"manager never replied\" hangs without rebuilding the\n # image.\n if [ \"${AGENTTEAMS_MATRIX_DEBUG:-}\" = \"1\" ] && [ -z \"${OPENCLAW_MATRIX_DEBUG:-}\" ]; then\n export OPENCLAW_MATRIX_DEBUG=1\n log \"AGENTTEAMS_MATRIX_DEBUG=1 detected; OPENCLAW_MATRIX_DEBUG=1 exported for matrix plugin tracing\"\n fi\n\n exec openclaw gateway run --verbose --force\nfi\n"}, "37c31b77d4e88ca87a1270c61a1e6f659e8023e1": {"t": 1785764731.881837, "n": 64818, "h": "854fc78a9c23ede8", "c": "#!/bin/bash\n# start-manager-agent.sh - Initialize and start the Manager Agent\n# Supports local (supervisord), cloud (SAE), and K8s (Helm) deployments.\n# In local mode this is the last supervisord component to start (priority 800).\n# In cloud/k8s mode (AGENTTEAMS_RUNTIME=aliyun|k8s) this is the container entrypoint.\n#\n# Runtime selection:\n# AGENTTEAMS_MANAGER_RUNTIME=openclaw (default) - OpenClaw gateway mode\n# AGENTTEAMS_MANAGER_RUNTIME=copaw - CoPaw workspace mode\n# (hermes runtime is supported for Workers only; Managers run openclaw or copaw.)\n\nsource /opt/agentteams/scripts/lib/agentteams-env.sh\n\n# ============================================================\n# Runtime selection\n# ============================================================\nMANAGER_RUNTIME=\"${AGENTTEAMS_MANAGER_RUNTIME:-openclaw}\"\ncase \"${MANAGER_RUNTIME}\" in\n copaw)\n log \"Manager runtime: CoPaw (Python workspace)\"\n ;;\n *)\n log \"Manager runtime: OpenClaw (Node.js gateway)\"\n MANAGER_RUNTIME=\"openclaw\"\n ;;\nesac\n\n# ============================================================\n# Set timezone from TZ env var\n# ============================================================\nif [ -n \"${TZ}\" ] && [ -f \"/usr/share/zoneinfo/${TZ}\" ]; then\n ln -sf \"/usr/share/zoneinfo/${TZ}\" /etc/localtime\n echo \"${TZ}\" > /etc/timezone\n log \"Timezone set to ${TZ}\"\nfi\n\nexport MATRIX_DOMAIN=\"${AGENTTEAMS_MATRIX_DOMAIN:-matrix-local.agentteams.io:8080}\"\nAI_GATEWAY_DOMAIN=\"${AGENTTEAMS_AI_GATEWAY_DOMAIN:-aigw-local.agentteams.io}\"\n\n# ============================================================\n# YOLO mode promotion\n# ============================================================\n# In embedded mode the controller does not propagate AGENTTEAMS_YOLO to the\n# manager container, but installer / test scripts touch a marker file at\n# `${WORKSPACE}/yolo-mode` instead. Promote that marker to the env var so the\n# agent's documented YOLO check (`AGENTTEAMS_YOLO=1`) reliably detects it without\n# depending on filesystem lookups during a turn.\nif [ -z \"${AGENTTEAMS_YOLO:-}\" ] && [ -f /root/manager-workspace/yolo-mode ]; then\n export AGENTTEAMS_YOLO=1\n log \"YOLO mode marker detected at /root/manager-workspace/yolo-mode; AGENTTEAMS_YOLO=1 exported\"\nfi\n\n# ============================================================\n# Cloud/K8s mode: validate required environment variables + initial credentials\n# ============================================================\nif [ \"${AGENTTEAMS_RUNTIME}\" = \"aliyun\" ] || [ \"${AGENTTEAMS_RUNTIME}\" = \"k8s\" ]; then\n : \"${AGENTTEAMS_MATRIX_URL:?AGENTTEAMS_MATRIX_URL is required}\"\n : \"${AGENTTEAMS_MATRIX_DOMAIN:?AGENTTEAMS_MATRIX_DOMAIN is required}\"\n : \"${AGENTTEAMS_AI_GATEWAY_URL:?AGENTTEAMS_AI_GATEWAY_URL is required}\"\n if [ \"${AGENTTEAMS_RUNTIME}\" = \"aliyun\" ]; then\n : \"${AGENTTEAMS_MANAGER_GATEWAY_KEY:?AGENTTEAMS_MANAGER_GATEWAY_KEY is required}\"\n : \"${AGENTTEAMS_MANAGER_PASSWORD:?AGENTTEAMS_MANAGER_PASSWORD is required (cloud containers are stateless, password must be injected)}\"\n fi\n if [ \"${AGENTTEAMS_RUNTIME}\" = \"k8s\" ]; then\n # K8s mode: controller handles initialization (admin registration, Higress setup).\n # Manager only needs credentials injected by the ManagerReconciler.\n : \"${AGENTTEAMS_MANAGER_GATEWAY_KEY:?AGENTTEAMS_MANAGER_GATEWAY_KEY is required (injected by controller)}\"\n # AGENTTEAMS_MANAGER_PASSWORD is optional: not needed in AppService mode\n # (token obtained via AS login), only required in legacy password mode.\n else\n # Cloud (aliyun) mode: Manager still does its own initialization\n : \"${AGENTTEAMS_REGISTRATION_TOKEN:?AGENTTEAMS_REGISTRATION_TOKEN is required}\"\n : \"${AGENTTEAMS_ADMIN_USER:?AGENTTEAMS_ADMIN_USER is required}\"\n : \"${AGENTTEAMS_ADMIN_PASSWORD:?AGENTTEAMS_ADMIN_PASSWORD is required}\"\n fi\n log \"${AGENTTEAMS_RUNTIME} mode: validating environment... OK\"\n log \" Matrix: ${AGENTTEAMS_MATRIX_URL}, AI Gateway: ${AGENTTEAMS_AI_GATEWAY_URL}, Storage: ${AGENTTEAMS_FS_BUCKET}\"\n if [ \"${AGENTTEAMS_RUNTIME}\" = \"aliyun\" ]; then\n ensure_mc_credentials || { log \"FATAL: Initial STS credential fetch failed\"; exit 1; }\n fi\nfi\n\n# ============================================================\n# Local mode: host symlinks, /etc/hosts, wait for local services\n# ============================================================\nif [ \"${AGENTTEAMS_RUNTIME}\" != \"aliyun\" ] && [ \"${AGENTTEAMS_RUNTIME}\" != \"k8s\" ]; then\n # Create symlink for host directory access\n if [ -d \"/host-share\" ]; then\n ORIGINAL_HOST_HOME=\"${HOST_ORIGINAL_HOME:-$HOME}\"\n if [ ! -e \"${ORIGINAL_HOST_HOME}\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/root\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/data\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/host-share\" ]; then\n mkdir -p \"$(dirname \"${ORIGINAL_HOST_HOME}\")\"\n ln -sfn /host-share \"${ORIGINAL_HOST_HOME}\"\n log \"Created symlink: ${ORIGINAL_HOST_HOME} -> /host-share\"\n else\n ln -sfn /host-share /root/host-home\n log \"Created fallback symlink: /root/host-home -> /host-share\"\n fi\n fi\n\n # Add local domains to /etc/hosts\n HOSTS_DOMAINS=\"${MATRIX_DOMAIN%%:*} ${AGENTTEAMS_MATRIX_CLIENT_DOMAIN:-matrix-client-local.agentteams.io} ${AI_GATEWAY_DOMAIN} ${AGENTTEAMS_FS_DOMAIN:-fs-local.agentteams.io}\"\n if ! grep -q \"${AI_GATEWAY_DOMAIN}\" /etc/hosts 2>/dev/null; then\n echo \"127.0.0.1 ${HOSTS_DOMAINS}\" >> /etc/hosts\n log \"Added local domains to /etc/hosts\"\n fi\n\n # Wait for local infrastructure\n waitForService \"Higress Gateway\" \"127.0.0.1\" 8080 180\n waitForService \"Higress Console\" \"127.0.0.1\" 8001 180\n waitForService \"Tuwunel\" \"127.0.0.1\" 6167 120\n waitForHTTP \"Tuwunel Matrix API\" \"${AGENTTEAMS_MATRIX_URL}/_tuwunel/server_version\" 120\n waitForService \"MinIO\" \"127.0.0.1\" 9000 120\nelse\n # Cloud/K8s mode: wait for external Tuwunel\n log \"Waiting for Tuwunel Matrix server at ${AGENTTEAMS_MATRIX_URL}...\"\n _retry=0\n while [ \"${_retry}\" -lt 30 ]; do\n if curl -sf \"${AGENTTEAMS_MATRIX_URL}/_matrix/client/versions\" > /dev/null 2>&1; then\n log \"Tuwunel is ready\"\n break\n fi\n _retry=$((_retry + 1))\n log \" Waiting for Tuwunel (attempt ${_retry}/30)...\"\n sleep 5\n done\n if [ \"${_retry}\" -ge 30 ]; then\n log \"ERROR: Tuwunel not reachable at ${AGENTTEAMS_MATRIX_URL}\"\n exit 1\n fi\nfi\n\n# ============================================================\n# Auto-generate secrets if not provided via environment\n# Persisted to /data so they survive container restart\n# ============================================================\nSECRETS_FILE=\"/data/agentteams-secrets.env\"\nif [ -f \"${SECRETS_FILE}\" ]; then\n source \"${SECRETS_FILE}\"\n log \"Loaded persisted secrets from ${SECRETS_FILE}\"\nfi\n\nif [ -z \"${AGENTTEAMS_MANAGER_GATEWAY_KEY}\" ]; then\n export AGENTTEAMS_MANAGER_GATEWAY_KEY=\"$(generateKey 32)\"\n log \"Auto-generated AGENTTEAMS_MANAGER_GATEWAY_KEY\"\nfi\nif [ -z \"${AGENTTEAMS_MANAGER_PASSWORD}\" ]; then\n export AGENTTEAMS_MANAGER_PASSWORD=\"$(generateKey 16)\"\n log \"Auto-generated AGENTTEAMS_MANAGER_PASSWORD\"\nfi\n\n# Persist secrets so they survive supervisord restart\nmkdir -p /data\ncat > \"${SECRETS_FILE}\" <<EOF\nexport AGENTTEAMS_MANAGER_GATEWAY_KEY=\"${AGENTTEAMS_MANAGER_GATEWAY_KEY}\"\nexport AGENTTEAMS_MANAGER_PASSWORD=\"${AGENTTEAMS_MANAGER_PASSWORD}\"\nEOF\nchmod 600 \"${SECRETS_FILE}\"\n\n# Cloud mode: pull workspace from OSS before initialization\nif [ \"${AGENTTEAMS_RUNTIME}\" = \"aliyun\" ]; then\n AGENTTEAMS_FS=\"/root/agentteams-fs\"\n mkdir -p \"${AGENTTEAMS_FS}/shared\" \"${AGENTTEAMS_FS}/agents\"\n log \"Pulling workspace from OSS...\"\n ensure_mc_credentials\n mc mirror \"${AGENTTEAMS_STORAGE_PREFIX}/manager/\" /root/manager-workspace/ --overwrite 2>/dev/null || true\n mc mirror \"${AGENTTEAMS_STORAGE_PREFIX}/shared/\" \"${AGENTTEAMS_FS}/shared/\" --overwrite 2>/dev/null || true\n mc mirror \"${AGENTTEAMS_STORAGE_PREFIX}/agents/\" \"${AGENTTEAMS_FS}/agents/\" --overwrite 2>/dev/null || true\n # Keep the canonical filesystem link in the Manager workspace.\n ln -sfn \"${AGENTTEAMS_FS}\" /root/manager-workspace/agentteams-fs\n ln -sfn \"${AGENTTEAMS_FS}\" /root/manager-workspace/agentteams-fs\nfi\n\n# K8s mode: sync workspace from cluster-internal MinIO\nif [ \"${AGENTTEAMS_RUNTIME}\" = \"k8s\" ]; then\n AGENTTEAMS_FS=\"/root/agentteams-fs\"\n mkdir -p \"${AGENTTEAMS_FS}/shared\" \"${AGENTTEAMS_FS}/agents\" \"${AGENTTEAMS_FS}/agentteams-config\"\n log \"Configuring mc alias for cluster MinIO...\"\n mc alias set agentteams \"${AGENTTEAMS_FS_ENDPOINT}\" \"${AGENTTEAMS_FS_ACCESS_KEY}\" \"${AGENTTEAMS_FS_SECRET_KEY}\"\n log \"Syncing workspace from MinIO...\"\n mc mirror \"${AGENTTEAMS_STORAGE_PREFIX}/manager/\" /root/manager-workspace/ --overwrite 2>/dev/null || true\n mc mirror \"${AGENTTEAMS_STORAGE_PREFIX}/\" \"${AGENTTEAMS_FS}/\" --overwrite 2>/dev/null || true\n ln -sfn \"${AGENTTEAMS_FS}\" /root/manager-workspace/agentteams-fs\n ln -sfn \"${AGENTTEAMS_FS}\" /root/manager-workspace/agentteams-fs\n touch \"${AGENTTEAMS_FS}/.initialized\"\nfi\n\n# ============================================================\n# Initialize / upgrade Manager workspace\n# First boot: full init via upgrade-builtins.sh\n# Subsequent boots: compare image version; upgrade only if changed\n# ============================================================\nmkdir -p /root/manager-workspace\n\nIMAGE_VERSION=$(cat /opt/agentteams/agent/.builtin-version 2>/dev/null || echo \"unknown\")\nINSTALLED_VERSION=$(cat /root/manager-workspace/.builtin-version 2>/dev/null || echo \"\")\n\nif [ ! -f /root/manager-workspace/.initialized ]; then\n log \"First boot: initializing manager workspace...\"\n bash /opt/agentteams/scripts/init/upgrade-builtins.sh\n touch /root/manager-workspace/.initialized\n log \"Manager workspace initialized (version: ${IMAGE_VERSION})\"\nelif [ \"${IMAGE_VERSION}\" != \"${INSTALLED_VERSION}\" ] || [ \"${IMAGE_VERSION}\" = \"latest\" ]; then\n log \"Upgrade detected: ${INSTALLED_VERSION} -> ${IMAGE_VERSION}${IMAGE_VERSION:+ (latest: always upgrade)}\"\n bash /opt/agentteams/scripts/init/upgrade-builtins.sh\n log \"Manager workspace upgraded to version: ${IMAGE_VERSION}\"\nelse\n log \"Workspace up to date (version: ${IMAGE_VERSION})\"\nfi\n\n# Local mode: wait for mc mirror initialization (shared + worker data in /root/agentteams-fs/)\nif [ \"${AGENTTEAMS_RUNTIME}\" != \"aliyun\" ] && [ \"${AGENTTEAMS_RUNTIME}\" != \"k8s\" ]; then\n log \"Waiting for MinIO storage initialization...\"\n _minio_wait=0\n while [ ! -f /root/agentteams-fs/.initialized ]; do\n sleep 2\n _minio_wait=$(( _minio_wait + 1 ))\n if [ \"${_minio_wait}\" -ge 60 ]; then\n log \"ERROR: MinIO storage initialization timed out after 120s\"\n exit 1\n fi\n done\n log \"MinIO storage initialized\"\nfi\n\n# ============================================================\n# Obtain Manager Matrix access token.\n#\n# Priority:\n# 1. Pre-injected token (AGENTTEAMS_MANAGER_MATRIX_TOKEN) — set by the\n# controller in both AppService mode (where no password exists) and\n# legacy mode (to avoid a redundant login round-trip).\n# 2. Password-based login — fallback for containers started without\n# controller involvement (manual docker run, legacy installs).\n# ============================================================\nif [ -n \"${AGENTTEAMS_MANAGER_MATRIX_TOKEN:-}\" ]; then\n MANAGER_TOKEN=\"${AGENTTEAMS_MANAGER_MATRIX_TOKEN}\"\n log \"Manager Matrix token pre-injected by controller (token prefix: ${MANAGER_TOKEN:0:10}...)\"\nelif [ \"${AGENTTEAMS_RUNTIME}\" = \"k8s\" ]; then\n log \"K8s mode: obtaining Manager Matrix token via password login...\"\n _LOGIN_RESPONSE=$(curl -s -X POST ${AGENTTEAMS_MATRIX_URL}/_matrix/client/v3/login \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"type\": \"m.login.password\",\n \"identifier\": {\"type\": \"m.id.user\", \"user\": \"manager\"},\n \"password\": \"'\"${AGENTTEAMS_MANAGER_PASSWORD}\"'\"\n }' 2>&1)\n MANAGER_TOKEN=$(echo \"${_LOGIN_RESPONSE}\" | jq -r '.access_token' 2>/dev/null)\n if [ -z \"${MANAGER_TOKEN}\" ] || [ \"${MANAGER_TOKEN}\" = \"null\" ]; then\n log \"ERROR: Failed to obtain Manager Matrix token\"\n log \"ERROR: Login response was: ${_LOGIN_RESPONSE}\"\n exit 1\n fi\n log \"Manager Matrix token obtained (token prefix: ${MANAGER_TOKEN:0:10}...)\"\nelse\nlog \"Registering human admin Matrix account...\"\ncurl -sf -X POST ${AGENTTEAMS_MATRIX_URL}/_matrix/client/v3/register \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"username\": \"'\"${AGENTTEAMS_ADMIN_USER}\"'\",\n \"password\": \"'\"${AGENTTEAMS_ADMIN_PASSWORD}\"'\",\n \"auth\": {\n \"type\": \"m.login.registration_token\",\n \"token\": \"'\"${AGENTTEAMS_REGISTRATION_TOKEN}\"'\"\n }\n }' > /dev/null 2>&1 || log \"Admin account may already exist\"\n\nlog \"Registering Manager Agent Matrix account...\"\ncurl -sf -X POST ${AGENTTEAMS_MATRIX_URL}/_matrix/client/v3/register \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"username\": \"manager\",\n \"password\": \"'\"${AGENTTEAMS_MANAGER_PASSWORD}\"'\",\n \"auth\": {\n \"type\": \"m.login.registration_token\",\n \"token\": \"'\"${AGENTTEAMS_REGISTRATION_TOKEN}\"'\"\n }\n }' > /dev/null 2>&1 || log \"Manager account may already exist\"\n\n# Get Manager Agent's Matrix access token\nlog \"Obtaining Manager Matrix access token...\"\n_LOGIN_RESPONSE=$(curl -s -X POST ${AGENTTEAMS_MATRIX_URL}/_matrix/client/v3/login \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"type\": \"m.login.password\",\n \"identifier\": {\"type\": \"m.id.user\", \"user\": \"manager\"},\n \"password\": \"'\"${AGENTTEAMS_MANAGER_PASSWORD}\"'\"\n }' 2>&1)\n_LOGIN_EXIT=$?\nlog \"Matrix login HTTP exit code: ${_LOGIN_EXIT}\"\nlog \"Matrix login response: ${_LOGIN_RESPONSE}\"\n\nMANAGER_TOKEN=$(echo \"${_LOGIN_RESPONSE}\" | jq -r '.access_token' 2>/dev/null)\n\nif [ -z \"${MANAGER_TOKEN}\" ] || [ \"${MANAGER_TOKEN}\" = \"null\" ]; then\n log \"ERROR: Failed to obtain Manager Matrix token (exit=${_LOGIN_EXIT})\"\n log \"ERROR: Login response was: ${_LOGIN_RESPONSE}\"\n exit 1\nfi\nlog \"Manager Matrix token obtained (token prefix: ${MANAGER_TOKEN:0:10}...)\"\nfi\n\n# ============================================================\n# Higress Console initialization\n# Docker mode: full setup-higress.sh (internal Higress at localhost:8001)\n# K8s mode: skip — controller Initializer handles Higress setup\n# Cloud (aliyun) mode: skip entirely (Higress managed externally)\n# ============================================================\n_HIGRESS_CONSOLE_URL=\"\"\n_HIGRESS_USER=\"${AGENTTEAMS_ADMIN_USER}\"\n_HIGRESS_PASS=\"${AGENTTEAMS_ADMIN_PASSWORD}\"\nif [ \"${AGENTTEAMS_RUNTIME}\" = \"k8s\" ]; then\n log \"K8s mode: skipping Higress initialization (handled by controller)\"\nelif [ \"${AGENTTEAMS_RUNTIME}\" != \"aliyun\" ]; then\n _HIGRESS_CONSOLE_URL=\"http://127.0.0.1:8001\"\nfi\n\nif [ -n \"${_HIGRESS_CONSOLE_URL}\" ]; then\n COOKIE_FILE=\"/tmp/higress-session-cookie\"\n\n log \"Waiting for Higress Console (${_HIGRESS_CONSOLE_URL}) to be fully ready and initializing admin...\"\n INIT_DONE=false\n for i in $(seq 1 90); do\n INIT_RESULT=$(curl -s -X POST \"${_HIGRESS_CONSOLE_URL}/system/init\" \\\n -H 'Content-Type: application/json' \\\n -d '{\"adminUser\":{\"name\":\"'\"${_HIGRESS_USER}\"'\",\"password\":\"'\"${_HIGRESS_PASS}\"'\",\"displayName\":\"'\"${_HIGRESS_USER}\"'\"}}' 2>/dev/null) || true\n if echo \"${INIT_RESULT}\" | grep -qE '\"success\":true|already.?init' 2>/dev/null; then\n INIT_DONE=true\n break\n fi\n if echo \"${INIT_RESULT}\" | grep -q '\"name\"' 2>/dev/null; then\n INIT_DONE=true\n break\n fi\n sleep 2\n done\n\n if [ \"${INIT_DONE}\" != \"true\" ]; then\n log \"ERROR: Higress Console did not become ready within 180s\"\n exit 1\n fi\n log \"Higress Console init done\"\n\n log \"Logging into Higress Console...\"\n LOGIN_OK=false\n for i in $(seq 1 10); do\n HTTP_CODE=$(curl -s -o /dev/null -w '%{http_code}' -X POST \"${_HIGRESS_CONSOLE_URL}/session/login\" \\\n -H 'Content-Type: application/json' \\\n -c \"${COOKIE_FILE}\" \\\n -d '{\"username\":\"'\"${_HIGRESS_USER}\"'\",\"password\":\"'\"${_HIGRESS_PASS}\"'\"}' 2>/dev/null) || true\n if { [ \"${HTTP_CODE}\" = \"200\" ] || [ \"${HTTP_CODE}\" = \"201\" ]; } && [ -f \"${COOKIE_FILE}\" ] && [ -s \"${COOKIE_FILE}\" ]; then\n LOGIN_OK=true\n break\n fi\n log \"Login attempt $i (HTTP ${HTTP_CODE}), retrying in 3s...\"\n sleep 3\n done\n\n if [ \"${LOGIN_OK}\" != \"true\" ]; then\n log \"ERROR: Could not login to Higress Console after retries\"\n exit 1\n fi\n log \"Higress Console login successful\"\n\n VERIFY_CODE=$(curl -s -o /dev/null -w '%{http_code}' \"${_HIGRESS_CONSOLE_URL}/v1/consumers\" -b \"${COOKIE_FILE}\" 2>/dev/null) || true\n if [ \"${VERIFY_CODE}\" = \"200\" ]; then\n log \"Console session verified (cookie valid)\"\n else\n log \"WARNING: Console session may be invalid (verify returned HTTP ${VERIFY_CODE})\"\n rm -f \"${COOKIE_FILE}\"\n for i in $(seq 1 5); do\n curl -s -o /dev/null -w '%{http_code}' -X POST \"${_HIGRESS_CONSOLE_URL}/session/login\" \\\n -H 'Content-Type: application/json' \\\n -c \"${COOKIE_FILE}\" \\\n -d '{\"username\":\"'\"${_HIGRESS_USER}\"'\",\"password\":\"'\"${_HIGRESS_PASS}\"'\"}' 2>/dev/null\n VERIFY2=$(curl -s -o /dev/null -w '%{http_code}' \"${_HIGRESS_CONSOLE_URL}/v1/consumers\" -b \"${COOKIE_FILE}\" 2>/dev/null) || true\n if [ \"${VERIFY2}\" = \"200\" ]; then\n log \"Re-login successful, session verified\"\n break\n fi\n sleep 2\n done\n fi\n\n export HIGRESS_COOKIE_FILE=\"${COOKIE_FILE}\"\n export HIGRESS_CONSOLE_URL=\"${_HIGRESS_CONSOLE_URL}\"\n\n if [ \"${AGENTTEAMS_RUNTIME}\" = \"k8s\" ]; then\n # K8s mode: lightweight Higress config — only what's needed for LLM access\n source /opt/agentteams/scripts/lib/base.sh\n _k8s_higress_api() {\n local method=\"$1\" path=\"$2\" desc=\"$3\"; shift 3; local body=\"$*\"\n local tmpfile; tmpfile=$(mktemp)\n local http_code\n http_code=$(curl -s -o \"${tmpfile}\" -w '%{http_code}' -X \"${method}\" \"${_HIGRESS_CONSOLE_URL}${path}\" \\\n -b \"${COOKIE_FILE}\" -H 'Content-Type: application/json' -d \"${body}\" 2>/dev/null) || true\n local response; response=$(cat \"${tmpfile}\" 2>/dev/null); rm -f \"${tmpfile}\"\n if echo \"${response}\" | grep -q '\"success\":true' 2>/dev/null; then\n log \"${desc} ... OK\"\n elif [ \"${http_code}\" = \"409\" ]; then\n log \"${desc} ... already exists, skipping\"\n elif [ \"${http_code}\" = \"200\" ] || [ \"${http_code}\" = \"201\" ] || [ \"${http_code}\" = \"204\" ]; then\n log \"${desc} ... OK (HTTP ${http_code})\"\n else\n log \"WARNING: ${desc} ... (HTTP ${http_code}): ${response}\"\n fi\n }\n\n # 1. Service Sources (DNS type → K8s Service FQDN)\n # Extract host:port from URLs for Higress service source registration\n _TUWUNEL_HOST=$(echo \"${AGENTTEAMS_MATRIX_URL}\" | sed 's|^http[s]*://||')\n _TUWUNEL_DOMAIN=$(echo \"${_TUWUNEL_HOST}\" | cut -d: -f1)\n _TUWUNEL_PORT=$(echo \"${_TUWUNEL_HOST}\" | cut -d: -f2)\n _k8s_higress_api POST /v1/service-sources \"Registering Tuwunel service source\" \\\n '{\"type\":\"dns\",\"name\":\"tuwunel\",\"domain\":\"'\"${_TUWUNEL_DOMAIN}\"'\",\"port\":'\"${_TUWUNEL_PORT}\"'}'\n\n if [ -n \"${AGENTTEAMS_ELEMENT_WEB_URL:-}\" ]; then\n _ELEMENT_HOST=$(echo \"${AGENTTEAMS_ELEMENT_WEB_URL}\" | sed 's|^http[s]*://||')\n _ELEMENT_DOMAIN=$(echo \"${_ELEMENT_HOST}\" | cut -d: -f1)\n _ELEMENT_PORT=$(echo \"${_ELEMENT_HOST}\" | cut -d: -f2)\n _k8s_higress_api POST /v1/service-sources \"Registering Element Web service source\" \\\n '{\"type\":\"dns\",\"name\":\"element-web\",\"domain\":\"'\"${_ELEMENT_DOMAIN}\"'\",\"port\":'\"${_ELEMENT_PORT}\"'}'\n fi\n\n # 2. Manager Consumer (key-auth)\n _k8s_higress_api POST /v1/consumers \"Creating Manager consumer\" \\\n '{\"name\":\"manager\",\"credentials\":[{\"type\":\"key-auth\",\"source\":\"BEARER\",\"values\":[\"'\"${AGENTTEAMS_MANAGER_GATEWAY_KEY}\"'\"]}]}'\n\n # 3. LLM Provider\n _LLM_PROVIDER=\"${AGENTTEAMS_LLM_PROVIDER:-qwen}\"\n _LLM_API_URL=\"${AGENTTEAMS_LLM_API_URL:-}\"\n if [ -z \"${_LLM_API_URL}\" ]; then\n case \"${_LLM_PROVIDER}\" in\n qwen) _LLM_API_URL=\"https://dashscope.aliyuncs.com/compatible-mode/v1\" ;;\n esac\n fi\n # 4. LLM Provider type-specific config\n case \"${_LLM_PROVIDER}\" in\n qwen)\n _k8s_higress_api POST /v1/ai/providers \"Creating LLM provider (qwen)\" \\\n '{\"type\":\"qwen\",\"name\":\"qwen\",\"tokens\":[\"'\"${AGENTTEAMS_LLM_API_KEY}\"'\"],\"protocol\":\"openai/v1\",\"tokenFailoverConfig\":{\"enabled\":false},\"rawConfigs\":{\"qwenEnableSearch\":false,\"qwenEnableCompatible\":true,\"qwenFileIds\":[],\"agentteamsMode\":true}}'\n ;;\n *)\n _BODY='{\"name\":\"'\"${_LLM_PROVIDER}\"'\",\"type\":\"openai\",\"tokens\":[\"'\"${AGENTTEAMS_LLM_API_KEY}\"'\"],\"modelMapping\":{},\"protocol\":\"openai/v1\",\"rawConfigs\":{\"agentteamsMode\":true}}'\n _k8s_higress_api POST /v1/ai/providers \"Creating LLM provider (${_LLM_PROVIDER})\" \"${_BODY}\"\n ;;\n esac\n\n # 5. AI Route (bind provider + consumer auth, /v1 prefix to avoid clash with Element Web catch-all)\n _k8s_higress_api POST /v1/ai/routes \"Creating AI Gateway route\" \\\n '{\"name\":\"default-ai-route\",\"domains\":[],\"pathPredicate\":{\"matchType\":\"PRE\",\"matchValue\":\"/v1\",\"caseSensitive\":false},\"upstreams\":[{\"provider\":\"'\"${_LLM_PROVIDER}\"'\",\"weight\":100,\"modelMapping\":{}}],\"authConfig\":{\"enabled\":true,\"allowedCredentialTypes\":[\"key-auth\"],\"allowedConsumers\":[\"manager\"]}}'\n\n # 6. Matrix Homeserver Route (/_matrix/* → Tuwunel, no auth)\n _k8s_higress_api POST /v1/routes \"Creating Matrix Homeserver route\" \\\n '{\"name\":\"matrix-homeserver\",\"domains\":[],\"path\":{\"matchType\":\"PRE\",\"matchValue\":\"/_matrix\"},\"services\":[{\"name\":\"tuwunel.dns\",\"port\":'\"${_TUWUNEL_PORT}\"',\"weight\":100}]}'\n\n # 7. Element Web Route (/ catch-all → Element Web, no auth)\n if [ -n \"${AGENTTEAMS_ELEMENT_WEB_URL:-}\" ]; then\n _k8s_higress_api POST /v1/routes \"Creating Element Web route\" \\\n '{\"name\":\"element-web\",\"domains\":[],\"path\":{\"matchType\":\"PRE\",\"matchValue\":\"/\"},\"services\":[{\"name\":\"element-web.dns\",\"port\":'\"${_ELEMENT_PORT}\"',\"weight\":100}]}'\n fi\n\n # 8. Remove Higress default landing page (Exact match on / takes precedence over Element Web catch-all)\n _k8s_higress_api DELETE /v1/routes/default \"Removing Higress default landing route\"\n\n log \"K8s Higress lightweight setup complete\"\n\n # Wait for AI plugin activation (~45s for first config)\n log \"Waiting for AI Gateway plugin activation (45s)...\"\n sleep 45\n else\n # Docker mode: full setup with all routes, domains, MCP servers\n /opt/agentteams/scripts/init/setup-higress.sh\n fi\nfi\n\n# ============================================================\n# Create admin DM room, persist to state.json, send welcome message\n# K8s mode: skip — controller ProvisionManager creates the Admin DM\n# room (Step 4 in service/provisioner.go) AND reconcileManagerWelcome\n# delivers the first-boot onboarding prompt once OpenClaw inside this\n# container has joined the room. In k8s mode the manager intentionally\n# does NOT have the admin password (only AGENTTEAMS_ADMIN_USER), so it\n# could not log in as admin to send the welcome itself anyway. The\n# Manager Agent discovers its admin DM room on first heartbeat via\n# state.json / `manage-state.sh --action set-admin-dm` (see HEARTBEAT.md\n# Step 1) — it does not need it to be pre-injected by this script.\n# Runs in both local and cloud modes (idempotent)\n# ============================================================\nif [ \"${AGENTTEAMS_RUNTIME}\" = \"k8s\" ]; then\n log \"K8s mode: skipping admin DM room creation and welcome message (both handled by agentteams-controller)\"\nelse\nMANAGER_FULL_ID=\"@manager:${MATRIX_DOMAIN}\"\nADMIN_FULL_ID=\"@${AGENTTEAMS_ADMIN_USER}:${MATRIX_DOMAIN}\"\n\nlog \"Logging in as admin to create DM room...\"\n_ADMIN_LOGIN=$(curl -sf -X POST \"${AGENTTEAMS_MATRIX_URL}/_matrix/client/v3/login\" \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"type\": \"m.login.password\",\n \"identifier\": {\"type\": \"m.id.user\", \"user\": \"'\"${AGENTTEAMS_ADMIN_USER}\"'\"},\n \"password\": \"'\"${AGENTTEAMS_ADMIN_PASSWORD}\"'\"\n }' 2>&1) || true\n\nADMIN_MATRIX_TOKEN=$(echo \"${_ADMIN_LOGIN}\" | jq -r '.access_token // empty' 2>/dev/null)\nif [ -z \"${ADMIN_MATRIX_TOKEN}\" ]; then\n log \"WARNING: Failed to login as admin, skipping DM room creation\"\nelse\n # Search for existing DM room with Manager (idempotent)\n DM_ROOM_ID=\"\"\n _JOINED_ROOMS=$(curl -sf \"${AGENTTEAMS_MATRIX_URL}/_matrix/client/v3/joined_rooms\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" 2>/dev/null \\\n | jq -r '.joined_rooms[]' 2>/dev/null) || true\n for _rid in ${_JOINED_ROOMS}; do\n _members=$(curl -sf \"${AGENTTEAMS_MATRIX_URL}/_matrix/client/v3/rooms/${_rid}/members\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" 2>/dev/null \\\n | jq -r '.chunk[].state_key' 2>/dev/null) || continue\n _count=$(echo \"${_members}\" | wc -l | xargs)\n if [ \"${_count}\" = \"2\" ] && echo \"${_members}\" | grep -q \"@manager:\"; then\n DM_ROOM_ID=\"${_rid}\"\n break\n fi\n done\n\n if [ -n \"${DM_ROOM_ID}\" ]; then\n log \"Existing DM room found: ${DM_ROOM_ID}\"\n else\n log \"Creating DM room with Manager...\"\n _RAW=$(curl -s -w '\\nHTTP_CODE:%{http_code}' -X POST \"${AGENTTEAMS_MATRIX_URL}/_matrix/client/v3/createRoom\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d \"{\\\"is_direct\\\":true,\\\"invite\\\":[\\\"${MANAGER_FULL_ID}\\\"],\\\"preset\\\":\\\"trusted_private_chat\\\"}\" 2>&1) || true\n _HTTP_CODE=$(echo \"${_RAW}\" | tail -1 | sed 's/HTTP_CODE://')\n _CREATE_RESP=$(echo \"${_RAW}\" | sed '$d')\n DM_ROOM_ID=$(echo \"${_CREATE_RESP}\" | jq -r '.room_id // empty' 2>/dev/null)\n if [ -n \"${DM_ROOM_ID}\" ]; then\n log \"DM room created: ${DM_ROOM_ID}\"\n else\n log \"WARNING: Failed to create DM room (HTTP ${_HTTP_CODE}): ${_CREATE_RESP}\"\n fi\n fi\n\n # Persist admin DM room ID to state.json\n if [ -n \"${DM_ROOM_ID}\" ]; then\n STATE_SCRIPT=\"/opt/agentteams/agent/skills/task-management/scripts/manage-state.sh\"\n if [ -f \"${STATE_SCRIPT}\" ]; then\n bash \"${STATE_SCRIPT}\" --action init 2>/dev/null || true\n bash \"${STATE_SCRIPT}\" --action set-admin-dm --room-id \"${DM_ROOM_ID}\" 2>/dev/null || true\n log \"Admin DM room persisted to state.json: ${DM_ROOM_ID}\"\n fi\n fi\n\n # Schedule welcome message in background (only on first boot)\n if [ -n \"${DM_ROOM_ID}\" ] && [ ! -f \"/root/manager-workspace/soul-configured\" ]; then\n log \"Scheduling welcome message (background, waiting for OpenClaw to start)...\"\n (\n _AGENTTEAMS_LANGUAGE=\"${AGENTTEAMS_LANGUAGE:-zh}\"\n _AGENTTEAMS_TIMEZONE=\"${TZ:-Asia/Shanghai}\"\n _wait=0\n _ready=false\n while [ \"${_wait}\" -lt 300 ]; do\n if curl -sf http://127.0.0.1:18799/ > /dev/null 2>&1; then\n _ready=true\n break\n fi\n sleep 3\n _wait=$((_wait + 3))\n done\n if [ \"${_ready}\" != \"true\" ]; then\n echo \"[manager] WARNING: OpenClaw gateway not ready within 300s, skipping welcome message\"\n exit 0\n fi\n # Ensure Manager has joined the DM room before sending the welcome\n # message. Without this, there is a race between OpenClaw's Matrix\n # auto-join and the message send — the message may land before Manager\n # joins, so OpenClaw's /sync never picks it up.\n _join_ok=false\n for _join_attempt in 1 2 3; do\n if curl -sf -X POST \"${AGENTTEAMS_MATRIX_URL}/_matrix/client/v3/rooms/${DM_ROOM_ID}/join\" \\\n -H \"Authorization: Bearer ${MANAGER_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d '{}' > /dev/null 2>&1; then\n echo \"[manager] Manager joined DM room before welcome message\"\n _join_ok=true\n break\n fi\n sleep 2\n done\n if [ \"${_join_ok}\" != \"true\" ]; then\n echo \"[manager] WARNING: Manager join request failed after 3 attempts (may already be joined)\"\n fi\n _welcome_msg=\"This is an automated message from the AgentTeams setup. This is a fresh installation.\n\n--- Installation Context ---\nUser Language: ${_AGENTTEAMS_LANGUAGE} (zh = Chinese, en = English)\nUser Timezone: ${_AGENTTEAMS_TIMEZONE} (IANA timezone identifier)\n---\n\nYou are an AI agent that manages a team of worker agents. Your identity and personality have not been configured yet — the human admin is about to meet you for the first time.\n\nPlease begin the onboarding conversation:\n\n1. Greet the admin warmly and briefly describe what you can do (coordinate workers, manage tasks, run multi-agent projects)\n2. The user has selected \\\"${_AGENTTEAMS_LANGUAGE}\\\" as their preferred language during installation. Use this language for your greeting and all subsequent communication.\n3. The user's timezone is ${_AGENTTEAMS_TIMEZONE}. Based on this timezone, you may infer their likely region and suggest additional language options.\n4. Ask them: a) What would they like to call you? b) Communication style preference? c) Any behavior guidelines? d) Confirm default language\n5. After they reply, write their preferences to ~/SOUL.md\n6. Confirm what you wrote, and ask if they would like to adjust anything\n7. Once confirmed, run: touch ~/soul-configured\n\nThe human admin will start chatting shortly.\"\n _txn_id=\"welcome-$(date +%s)\"\n _payload=$(jq -nc --arg body \"${_welcome_msg}\" '{\"msgtype\":\"m.text\",\"body\":$body}')\n _raw=$(curl -s -w '\\nHTTP_CODE:%{http_code}' -X PUT \"${AGENTTEAMS_MATRIX_URL}/_matrix/client/v3/rooms/${DM_ROOM_ID}/send/m.room.message/${_txn_id}\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d \"${_payload}\" 2>&1) || true\n _http_code=$(echo \"${_raw}\" | tail -1 | sed 's/HTTP_CODE://')\n _send_resp=$(echo \"${_raw}\" | sed '$d')\n if echo \"${_send_resp}\" | jq -e '.event_id' > /dev/null 2>&1; then\n echo \"[manager] Welcome message sent to DM room\"\n else\n echo \"[manager] WARNING: Failed to send welcome message (HTTP ${_http_code}): ${_send_resp}\"\n fi\n ) &\n log \"Welcome message background process started (PID: $!)\"\n fi\nfi\nfi # end K8s mode skip for admin DM room\n\n# ============================================================\n# Generate Manager Agent openclaw.json from template\n# ============================================================\nlog \"Generating Manager openclaw.json...\"\nexport MANAGER_MATRIX_TOKEN=\"${MANAGER_TOKEN}\"\nexport MANAGER_GATEWAY_KEY=\"${AGENTTEAMS_MANAGER_GATEWAY_KEY}\"\n# Resolve model parameters based on model name\nMODEL_NAME=\"${AGENTTEAMS_DEFAULT_MODEL:-qwen3.6-plus}\"\ncase \"${MODEL_NAME}\" in\n gpt-5.3-codex|gpt-5-mini|gpt-5-nano)\n export MODEL_CONTEXT_WINDOW=400000 MODEL_MAX_TOKENS=128000 ;;\n claude-opus-4-6)\n export MODEL_CONTEXT_WINDOW=1000000 MODEL_MAX_TOKENS=128000 ;;\n claude-sonnet-4-6)\n export MODEL_CONTEXT_WINDOW=1000000 MODEL_MAX_TOKENS=64000 ;;\n claude-haiku-4-5)\n export MODEL_CONTEXT_WINDOW=200000 MODEL_MAX_TOKENS=64000 ;;\n qwen3.6-plus|qwen3.5-plus)\n export MODEL_CONTEXT_WINDOW=200000 MODEL_MAX_TOKENS=64000 ;;\n deepseek-chat|deepseek-reasoner|kimi-k2.5)\n export MODEL_CONTEXT_WINDOW=256000 MODEL_MAX_TOKENS=128000 ;;\n glm-5|MiniMax-M2.7|MiniMax-M2.7-highspeed|MiniMax-M2.5)\n export MODEL_CONTEXT_WINDOW=200000 MODEL_MAX_TOKENS=128000 ;;\n *)\n export MODEL_CONTEXT_WINDOW=150000 MODEL_MAX_TOKENS=128000 ;;\nesac\nexport MODEL_REASONING=true\n\n# Override with user-supplied custom model parameters from env (set during install)\n[ -n \"${AGENTTEAMS_MODEL_CONTEXT_WINDOW:-}\" ] && export MODEL_CONTEXT_WINDOW=\"${AGENTTEAMS_MODEL_CONTEXT_WINDOW}\"\n[ -n \"${AGENTTEAMS_MODEL_MAX_TOKENS:-}\" ] && export MODEL_MAX_TOKENS=\"${AGENTTEAMS_MODEL_MAX_TOKENS}\"\n[ -n \"${AGENTTEAMS_MODEL_REASONING:-}\" ] && export MODEL_REASONING=\"${AGENTTEAMS_MODEL_REASONING}\"\n\n# E2EE: convert AGENTTEAMS_MATRIX_E2EE to JSON boolean for template substitution\nif [ \"${AGENTTEAMS_MATRIX_E2EE:-0}\" = \"1\" ] || [ \"${AGENTTEAMS_MATRIX_E2EE:-}\" = \"true\" ]; then\n export MATRIX_E2EE_ENABLED=true\nelse\n export MATRIX_E2EE_ENABLED=false\nfi\nlog \"Matrix E2EE: ${MATRIX_E2EE_ENABLED}\"\n\n# Resolve input modalities: only vision-capable models get \"image\"\ncase \"${MODEL_NAME}\" in\n gpt-5.4|gpt-5.3-codex|gpt-5-mini|gpt-5-nano|claude-opus-4-6|claude-sonnet-4-6|claude-haiku-4-5|qwen3.6-plus|qwen3.5-plus|kimi-k2.5)\n export MODEL_INPUT='[\"text\", \"image\"]' ;;\n *)\n export MODEL_INPUT='[\"text\"]' ;;\nesac\n# Override with user-supplied vision setting from env\nif [ \"${AGENTTEAMS_MODEL_VISION:-}\" = \"true\" ]; then\n export MODEL_INPUT='[\"text\", \"image\"]'\nelif [ \"${AGENTTEAMS_MODEL_VISION:-}\" = \"false\" ]; then\n export MODEL_INPUT='[\"text\"]'\nfi\n\nlog \"Model: ${MODEL_NAME} (context=${MODEL_CONTEXT_WINDOW}, maxTokens=${MODEL_MAX_TOKENS}, reasoning=${MODEL_REASONING}, input=${MODEL_INPUT})\"\n\nif [ -f /root/manager-workspace/openclaw.json ]; then\n log \"Manager openclaw.json already exists, updating dynamic fields only (preserving user customizations)...\"\n # Merge known models into existing config (add missing, preserve user-added)\n # Use known-models.json (valid JSON) instead of template (contains ${VAR} placeholders)\n KNOWN_MODELS=$(cat /opt/agentteams/configs/known-models.json 2>/dev/null || echo '[]')\n jq --arg token \"${MANAGER_TOKEN}\" \\\n --arg key \"${AGENTTEAMS_MANAGER_GATEWAY_KEY}\" \\\n --arg model \"${MODEL_NAME}\" \\\n --arg emb_model \"${AGENTTEAMS_EMBEDDING_MODEL}\" \\\n --arg aigw_domain \"${AI_GATEWAY_DOMAIN}\" \\\n --arg matrix_user_id \"@manager:${MATRIX_DOMAIN}\" \\\n --argjson e2ee \"${MATRIX_E2EE_ENABLED}\" \\\n --argjson known_models \"${KNOWN_MODELS}\" \\\n --argjson ctx \"${MODEL_CONTEXT_WINDOW}\" \\\n --argjson max \"${MODEL_MAX_TOKENS}\" \\\n --argjson reasoning \"${MODEL_REASONING}\" \\\n --argjson input \"${MODEL_INPUT}\" \\\n '\n # Merge known models: add any model id not already present\n .models.providers[\"agentteams-gateway\"].models as $existing\n | ($existing | map(.id)) as $existing_ids\n | ($known_models | map(select(.id as $id | $existing_ids | index($id) | not))) as $new\n | .models.providers[\"agentteams-gateway\"].models = ($existing + $new)\n # Ensure the user-chosen default model is in the list (custom model support)\n | if (.models.providers[\"agentteams-gateway\"].models | map(.id) | index($model) | not) then\n .models.providers[\"agentteams-gateway\"].models += [{\"id\": $model, \"name\": $model, \"reasoning\": $reasoning, \"contextWindow\": $ctx, \"maxTokens\": $max, \"input\": $input}]\n else . end\n # Rebuild model aliases from the full models list\n | (.models.providers[\"agentteams-gateway\"].models | map({ (\"agentteams-gateway/\" + .id): { \"alias\": .id } }) | add // {}) as $aliases\n | .agents.defaults.models = ((.agents.defaults.models // {}) + $aliases)\n | .channels.matrix.accessToken = $token | .channels.matrix.userId = $matrix_user_id | .models.providers[\"agentteams-gateway\"].apiKey = $key\n | ((.hooks.token // \"\") as $ht | if $ht == $key or $ht == ($key + \"-hooks\" | @base64) then del(.hooks) else . end)\n | .agents.defaults.model.primary = (\"agentteams-gateway/\" + $model)\n | .commands.restart = true\n | .gateway.port = 18799\n | .gateway.bind = \"lan\"\n | .gateway.controlUi = ((.gateway.controlUi // {}) + {\"dangerouslyDisableDeviceAuth\": true, \"allowInsecureAuth\": true, \"allowedOrigins\": [\"*\"]})\n | .channels.matrix.encryption = $e2ee\n | .channels.matrix.network = ((.channels.matrix.network // {}) + {\"dangerouslyAllowPrivateNetwork\": true})\n | .channels.matrix.autoJoin = \"always\"\n # OpenClaw YOLO defaults: host exec without approval prompts (see openclaw docs tools/exec-approvals)\n | .tools = (.tools // {})\n | .tools.exec = ((.tools.exec // {}) + {\"host\":\"gateway\",\"security\":\"full\",\"ask\":\"off\"})\n | .tools.elevated = (.tools.elevated // {})\n | .tools.elevated.enabled = true\n | .tools.elevated.allowFrom |= ((. // {}) | .matrix = [\"*\"])\n | .agents.defaults.elevatedDefault = \"full\"\n # Ensure memorySearch config exists (embedding model for memory) — skip if embedding model is empty\n | if $emb_model != \"\" then .agents.defaults.memorySearch //= {\"provider\":\"openai\",\"model\":$emb_model,\"remote\":{\"baseUrl\":(\"http://\" + $aigw_domain + \":8080/v1\"),\"apiKey\":$key}} else . end\n ' \\\n /root/manager-workspace/openclaw.json > /tmp/openclaw.json.tmp && \\\n mv /tmp/openclaw.json.tmp /root/manager-workspace/openclaw.json\n # Disable openclaw's observe-recovery mechanism which compares config against\n # a lastKnownGood baseline in config-health.json. When meta is missing from the\n # current file but present in the baseline, observe-recovery restores from .bak,\n # undoing user customizations (plugins, channels, etc).\n # Clearing config-health.json removes the baseline so observe-recovery won't\n # interfere, while preserving .bak as a backup.\n rm -f /root/manager-workspace/.openclaw/logs/config-health.json\n # Verify the token was written correctly\n _written_token=$(jq -r '.channels.matrix.accessToken' /root/manager-workspace/openclaw.json 2>/dev/null)\n if [ -z \"${_written_token}\" ] || [ \"${_written_token}\" = \"null\" ]; then\n log \"ERROR: Matrix token was not written correctly to openclaw.json (got: ${_written_token})\"\n else\n log \"Matrix token written to openclaw.json (prefix: ${_written_token:0:10}...)\"\n fi\nelse\n log \"Manager openclaw.json not found, generating from template...\"\n envsubst < /opt/agentteams/configs/manager-openclaw.json.tmpl > /root/manager-workspace/openclaw.json\n # Post-envsubst injection: memorySearch + custom model (single jq pass when possible)\n if ! jq -e --arg model \"${MODEL_NAME}\" '.models.providers[\"agentteams-gateway\"].models | map(.id) | index($model)' /root/manager-workspace/openclaw.json > /dev/null 2>&1; then\n log \"Custom model '${MODEL_NAME}' not in built-in list, injecting into config...\"\n jq --arg emb_model \"${AGENTTEAMS_EMBEDDING_MODEL}\" \\\n --arg aigw_domain \"${AI_GATEWAY_DOMAIN}\" \\\n --arg key \"${AGENTTEAMS_MANAGER_GATEWAY_KEY}\" \\\n --arg model \"${MODEL_NAME}\" \\\n --argjson ctx \"${MODEL_CONTEXT_WINDOW}\" \\\n --argjson max \"${MODEL_MAX_TOKENS}\" \\\n --argjson reasoning \"${MODEL_REASONING}\" \\\n --argjson input \"${MODEL_INPUT}\" \\\n '\n (if $emb_model != \"\" then .agents.defaults.memorySearch = {\"provider\":\"openai\",\"model\":$emb_model,\"remote\":{\"baseUrl\":(\"http://\" + $aigw_domain + \":8080/v1\"),\"apiKey\":$key}} else . end)\n | .models.providers[\"agentteams-gateway\"].models += [{\"id\": $model, \"name\": $model, \"reasoning\": $reasoning, \"contextWindow\": $ctx, \"maxTokens\": $max, \"input\": $input}]\n | .agents.defaults.models += {(\"agentteams-gateway/\" + $model): {\"alias\": $model}}\n ' /root/manager-workspace/openclaw.json > /tmp/openclaw.json.tmp && \\\n mv /tmp/openclaw.json.tmp /root/manager-workspace/openclaw.json\n elif [ -n \"${AGENTTEAMS_EMBEDDING_MODEL}\" ]; then\n jq --arg emb_model \"${AGENTTEAMS_EMBEDDING_MODEL}\" \\\n --arg aigw_domain \"${AI_GATEWAY_DOMAIN}\" \\\n --arg key \"${AGENTTEAMS_MANAGER_GATEWAY_KEY}\" \\\n '.agents.defaults.memorySearch = {\"provider\":\"openai\",\"model\":$emb_model,\"remote\":{\"baseUrl\":(\"http://\" + $aigw_domain + \":8080/v1\"),\"apiKey\":$key}}' \\\n /root/manager-workspace/openclaw.json > /tmp/openclaw.json.tmp && \\\n mv /tmp/openclaw.json.tmp /root/manager-workspace/openclaw.json\n fi\n _written_token=$(jq -r '.channels.matrix.accessToken' /root/manager-workspace/openclaw.json 2>/dev/null)\n log \"Matrix token written from template (prefix: ${_written_token:0:10}...)\"\nfi\n\n# Cloud/K8s mode: overlay cloud-specific settings onto generated config\nif [ \"${AGENTTEAMS_RUNTIME}\" = \"aliyun\" ] || [ \"${AGENTTEAMS_RUNTIME}\" = \"k8s\" ]; then\n log \"Applying cloud/k8s overlay to openclaw.json...\"\n jq --arg homeserver \"${AGENTTEAMS_MATRIX_URL}\" \\\n --arg gateway \"${AGENTTEAMS_AI_GATEWAY_URL}/v1\" \\\n --arg key \"${AGENTTEAMS_MANAGER_GATEWAY_KEY}\" \\\n '.channels.matrix.homeserver = $homeserver\n | .models.providers[\"agentteams-gateway\"].baseUrl = $gateway\n | .models.providers[\"agentteams-gateway\"].apiKey = $key\n | ((.hooks.token // \"\") as $ht | if $ht == $key or $ht == ($key + \"-hooks\" | @base64) then del(.hooks) else . end)\n | .commands.restart = false\n | if .agents.defaults.memorySearch then .agents.defaults.memorySearch.remote.baseUrl = $gateway | .agents.defaults.memorySearch.remote.apiKey = $key else . end' \\\n /root/manager-workspace/openclaw.json > /tmp/openclaw-cloud.json && \\\n mv /tmp/openclaw-cloud.json /root/manager-workspace/openclaw.json\n log \"Cloud/K8s overlay applied\"\nfi\n\n# ============================================================\n# Optional: enable openclaw-cms-plugin observability\n# Config is applied at runtime so secrets stay out of image layers.\n# ============================================================\nCMS_TRACES_ENABLED=\"$(echo \"${AGENTTEAMS_CMS_TRACES_ENABLED:-false}\" | tr '[:upper:]' '[:lower:]')\"\nif [ \"${CMS_TRACES_ENABLED}\" = \"true\" ]; then\n CMS_PLUGIN_NAME=\"openclaw-cms-plugin\"\n CMS_PLUGIN_DIR=\"${OPENCLAW_CMS_PLUGIN_DIR:-/opt/openclaw/extensions/openclaw-cms-plugin}\"\n CMS_PLUGIN_MANIFEST=\"${CMS_PLUGIN_DIR}/openclaw.plugin.json\"\n DIAG_PLUGIN_NAME=\"diagnostics-otel\"\n DIAG_PLUGIN_DIR=\"/opt/openclaw/extensions/diagnostics-otel\"\n CMS_LICENSE_KEY=\"${AGENTTEAMS_CMS_LICENSE_KEY:-}\"\n CMS_PROJECT=\"${AGENTTEAMS_CMS_PROJECT:-}\"\n CMS_METRICS_ENABLED=\"${AGENTTEAMS_CMS_METRICS_ENABLED:-false}\"\n\n if [ ! -f \"${CMS_PLUGIN_MANIFEST}\" ]; then\n log \"WARNING: ${CMS_PLUGIN_NAME} manifest not found at ${CMS_PLUGIN_MANIFEST}, skipping plugin config\"\n else\n _missing=0\n [ -z \"${AGENTTEAMS_CMS_ENDPOINT:-}\" ] && log \"WARNING: AGENTTEAMS_CMS_ENDPOINT is required when AGENTTEAMS_CMS_TRACES_ENABLED=true\" && _missing=1\n [ -z \"${CMS_LICENSE_KEY:-}\" ] && log \"WARNING: AGENTTEAMS_CMS_LICENSE_KEY is required when AGENTTEAMS_CMS_TRACES_ENABLED=true\" && _missing=1\n [ -z \"${AGENTTEAMS_CMS_WORKSPACE:-}\" ] && log \"WARNING: AGENTTEAMS_CMS_WORKSPACE is required when AGENTTEAMS_CMS_TRACES_ENABLED=true\" && _missing=1\n\n if [ \"${_missing}\" = \"0\" ]; then\n CMS_SERVICE_NAME=\"${AGENTTEAMS_CMS_SERVICE_NAME:-agentteams-manager}\"\n CMS_ENABLE_METRICS=\"${CMS_METRICS_ENABLED}\"\n DIAG_AVAILABLE=\"0\"\n _metrics_lc=\"$(echo \"${CMS_ENABLE_METRICS}\" | tr '[:upper:]' '[:lower:]')\"\n if [ \"${_metrics_lc}\" = \"true\" ]; then\n if [ -f \"${DIAG_PLUGIN_DIR}/package.json\" ]; then\n DIAG_AVAILABLE=\"1\"\n if [ ! -d \"${DIAG_PLUGIN_DIR}/node_modules\" ]; then\n log \"diagnostics-otel dependencies missing, installing...\"\n if (cd \"${DIAG_PLUGIN_DIR}\" && npm install --omit=dev --ignore-scripts >/tmp/agentteams-diag-install.log 2>&1); then\n log \"diagnostics-otel dependencies installed\"\n else\n log \"WARNING: diagnostics-otel npm install failed, metrics plugin may not load\"\n fi\n else\n log \"diagnostics-otel dependencies already present\"\n fi\n else\n log \"WARNING: diagnostics-otel package.json not found at ${DIAG_PLUGIN_DIR}, metrics plugin may not load\"\n fi\n fi\n\n log \"Applying ${CMS_PLUGIN_NAME} config to openclaw.json...\"\n jq --arg pluginName \"${CMS_PLUGIN_NAME}\" \\\n --arg pluginDir \"${CMS_PLUGIN_DIR}\" \\\n --arg endpoint \"${AGENTTEAMS_CMS_ENDPOINT}\" \\\n --arg licenseKey \"${CMS_LICENSE_KEY}\" \\\n --arg armsProject \"${CMS_PROJECT}\" \\\n --arg cmsWorkspace \"${AGENTTEAMS_CMS_WORKSPACE}\" \\\n --arg serviceName \"${CMS_SERVICE_NAME}\" \\\n --arg diagPluginName \"${DIAG_PLUGIN_NAME}\" \\\n --arg diagPluginDir \"${DIAG_PLUGIN_DIR}\" \\\n --arg metricsRaw \"${CMS_ENABLE_METRICS}\" \\\n --arg diagAvailableRaw \"${DIAG_AVAILABLE}\" \\\n '\n .plugins = (.plugins // {})\n | .plugins.load = (.plugins.load // {})\n | .plugins.entries = (.plugins.entries // {})\n | if (.plugins.allow | type) != \"array\" then .plugins.allow = [] else . end\n | if (.plugins.allow | index($pluginName)) == null then .plugins.allow += [$pluginName] else . end\n | if (.plugins.load.paths | type) != \"array\" then .plugins.load.paths = [] else . end\n | if (.plugins.load.paths | index($pluginDir)) == null then .plugins.load.paths += [$pluginDir] else . end\n | .plugins.entries[$pluginName] = {\n \"enabled\": true,\n \"config\": {\n \"endpoint\": $endpoint,\n \"headers\": {\n \"x-arms-license-key\": $licenseKey,\n \"x-arms-project\": $armsProject,\n \"x-cms-workspace\": $cmsWorkspace\n },\n \"serviceName\": $serviceName\n }\n }\n\n # diagnostics-otel metrics (optional)\n | ($metricsRaw | ascii_downcase) as $m\n | ($diagAvailableRaw == \"1\") as $diagAvailable\n | (($m == \"true\") and $diagAvailable) as $metricsEnabled\n | if $metricsEnabled then\n (if (.plugins.allow | index($diagPluginName)) == null then .plugins.allow += [$diagPluginName] else . end)\n | (if (.plugins.load.paths | index($diagPluginDir)) == null then .plugins.load.paths += [$diagPluginDir] else . end)\n | .plugins.entries[$diagPluginName].enabled = true\n | .diagnostics = (.diagnostics // {})\n | .diagnostics.otel = (.diagnostics.otel // {})\n | .diagnostics.enabled = true\n | .diagnostics.otel.enabled = true\n | .diagnostics.otel.endpoint = $endpoint\n | .diagnostics.otel.protocol = (.diagnostics.otel.protocol // \"http/protobuf\")\n | .diagnostics.otel.headers = {\n \"x-arms-license-key\": $licenseKey,\n \"x-arms-project\": $armsProject,\n \"x-cms-workspace\": $cmsWorkspace\n }\n | .diagnostics.otel.serviceName = $serviceName\n | .diagnostics.otel.metrics = true\n | .diagnostics.otel.traces = (.diagnostics.otel.traces // false)\n | .diagnostics.otel.logs = (.diagnostics.otel.logs // false)\n else\n .\n end\n ' /root/manager-workspace/openclaw.json > /tmp/openclaw-cms.json && \\\n mv /tmp/openclaw-cms.json /root/manager-workspace/openclaw.json\n log \"${CMS_PLUGIN_NAME} config applied (metrics=${CMS_ENABLE_METRICS}, service=${CMS_SERVICE_NAME})\"\n else\n log \"Skipping ${CMS_PLUGIN_NAME} config due to missing required env vars\"\n fi\n fi\nfi\n\n# ============================================================\n# Detect container runtime (for Worker creation)\n# ============================================================\nsource /opt/agentteams/scripts/lib/container-api.sh\nif container_api_available; then\n log \"Container runtime socket detected at ${CONTAINER_SOCKET} — direct Worker creation enabled\"\n export AGENTTEAMS_CONTAINER_RUNTIME=\"socket\"\nelif [ \"${AGENTTEAMS_RUNTIME}\" = \"aliyun\" ] || [ \"${AGENTTEAMS_RUNTIME}\" = \"k8s\" ]; then\n log \"Cloud/K8s mode — Workers created via controller API\"\n export AGENTTEAMS_CONTAINER_RUNTIME=\"cloud\"\nelse\n log \"No container runtime found — Worker creation will output install commands\"\n export AGENTTEAMS_CONTAINER_RUNTIME=\"none\"\nfi\n\n# ============================================================\n# Recreate Worker containers as needed after Manager restart.\n# Workers are on agentteams-net; Docker DNS resolves *-local.agentteams.io via\n# the Manager's network aliases, so IP changes don't require worker recreation.\n# Only recreate stopped/missing workers.\n# ============================================================\nif container_api_available; then\n _workers_json=$(agt get workers -o json 2>/dev/null || echo '{\"workers\":[]}')\n for _worker_name in $(echo \"${_workers_json}\" | jq -r '.workers[].name'); do\n [ -z \"${_worker_name}\" ] && continue\n\n _status=$(container_status_worker \"${_worker_name}\")\n if [ \"${_status}\" = \"running\" ]; then\n log \"Worker running: ${_worker_name}, skipping\"\n continue\n fi\n # Container missing or stopped — recreate.\n log \"Worker container ${_status}: ${_worker_name}, recreating...\"\n _creds_file=\"/data/worker-creds/${_worker_name}.env\"\n if [ -f \"${_creds_file}\" ]; then\n source \"${_creds_file}\"\n _runtime=$(echo \"${_workers_json}\" | jq -r --arg w \"${_worker_name}\" '.workers[] | select(.name == $w) | .runtime // \"openclaw\"')\n _recreated=false\n for _attempt in 1 2 3; do\n _env_map=\"\"\n _create_body=\"\"\n _env_map=$(jq -cn \\\n --arg name \"${_worker_name}\" \\\n --arg fak \"${_worker_name}\" \\\n --arg fsk \"${WORKER_MINIO_PASSWORD:-}\" \\\n --arg fs_domain \"${AGENTTEAMS_FS_DOMAIN:-fs-local.agentteams.io}\" \\\n --arg controller_url \"${AGENTTEAMS_CONTROLLER_URL:-}\" \\\n '{\n \"AGENTTEAMS_WORKER_NAME\": $name,\n \"AGENTTEAMS_FS_ENDPOINT\": (\"http://\" + ($fs_domain | split(\":\")[0]) + \":9000\"),\n \"AGENTTEAMS_FS_ACCESS_KEY\": $fak,\n \"AGENTTEAMS_FS_SECRET_KEY\": $fsk\n }\n | if $controller_url != \"\" then . + {\"AGENTTEAMS_CONTROLLER_URL\": $controller_url} else . end')\n _create_body=$(jq -cn --arg name \"${_worker_name}\" --arg runtime \"${_runtime}\" --argjson env \"${_env_map}\" '{name: $name, runtime: $runtime, env: $env}')\n worker_backend_create \"${_create_body}\" > /dev/null 2>&1 && _recreated=true && break\n log \" Attempt ${_attempt}/3 failed for ${_worker_name}, retrying in $((5 * _attempt))s...\"\n sleep $((5 * _attempt))\n done\n if [ \"${_recreated}\" = true ]; then\n log \" Recreated ${_runtime} worker: ${_worker_name}\"\n else\n log \" ERROR: Failed to recreate ${_worker_name} after 3 attempts\"\n fi\n else\n log \" WARNING: No credentials found for ${_worker_name} (${_creds_file} missing), skipping\"\n fi\n done\nfi\n\n# ============================================================\n# Notify workers of builtin updates if upgrade happened\n# Builtin files (AGENTS.md, skills) are already synced by upgrade-builtins.sh\n#\n# Cooldown: skip notification if the last successful notify was within\n# NOTIFY_COOLDOWN_SECS (default 3600s / 1 hour). This prevents repeated\n# notifications when the Manager crash-loops and re-runs upgrade-builtins\n# on every restart (e.g. IMAGE_VERSION=latest always triggers upgrade).\n# ============================================================\nNOTIFY_COOLDOWN_SECS=\"${AGENTTEAMS_NOTIFY_COOLDOWN_SECS:-3600}\"\nNOTIFY_TS_FILE=\"/root/manager-workspace/.last-worker-notify-ts\"\n\nif [ -f /root/manager-workspace/.upgrade-pending-worker-notify ]; then\n _now=$(date +%s)\n _last_notify=$(cat \"${NOTIFY_TS_FILE}\" 2>/dev/null || echo \"0\")\n _elapsed=$(( _now - _last_notify ))\n\n if [ \"${_elapsed}\" -lt \"${NOTIFY_COOLDOWN_SECS}\" ]; then\n log \"Skipping worker builtin notification (last notify ${_elapsed}s ago, cooldown ${NOTIFY_COOLDOWN_SECS}s)\"\n rm -f /root/manager-workspace/.upgrade-pending-worker-notify\n else\n log \"Notifying workers about builtin updates...\"\n _notify_ok=false\n _workers_json=$(agt get workers -o json 2>/dev/null || echo '{\"workers\":[]}')\n for _worker_name in $(echo \"${_workers_json}\" | jq -r '.workers[].name'); do\n [ -z \"${_worker_name}\" ] && continue\n _room_id=$(echo \"${_workers_json}\" | jq -r --arg w \"${_worker_name}\" '.workers[] | select(.name == $w) | .roomID // empty')\n if [ -n \"${_room_id}\" ]; then\n _worker_id=\"@${_worker_name}:${MATRIX_DOMAIN}\"\n _txn_id=\"upgrade-$(date +%s%N)\"\n _msg=\"@${_worker_name}:${MATRIX_DOMAIN} Manager upgraded builtin files (AGENTS.md, skills). Please use your file-sync skill to sync the latest config.\"\n _raw=$(curl -s -w '\\nHTTP_CODE:%{http_code}' -X PUT \\\n \"${AGENTTEAMS_MATRIX_URL}/_matrix/client/v3/rooms/${_room_id}/send/m.room.message/${_txn_id}\" \\\n -H \"Authorization: Bearer ${MANAGER_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d \"{\\\"msgtype\\\":\\\"m.text\\\",\\\"body\\\":\\\"${_msg}\\\",\\\"m.mentions\\\":{\\\"user_ids\\\":[\\\"${_worker_id}\\\"]}}\" \\\n 2>&1) || true\n _http_code=$(echo \"${_raw}\" | tail -1 | sed 's/HTTP_CODE://')\n _notify_resp=$(echo \"${_raw}\" | sed '$d')\n if echo \"${_notify_resp}\" | jq -e '.event_id' > /dev/null 2>&1; then\n log \" Notified ${_worker_name}\"; _notify_ok=true\n else\n log \" WARNING: Failed to notify ${_worker_name} (HTTP ${_http_code}): ${_notify_resp}\"\n fi\n fi\n done\n # Record timestamp only if at least one notification succeeded\n if [ \"${_notify_ok}\" = true ]; then\n echo \"${_now}\" > \"${NOTIFY_TS_FILE}\"\n fi\n rm -f /root/manager-workspace/.upgrade-pending-worker-notify\n fi\nfi\n\n# ============================================================\n# Start Manager Agent\n# ============================================================\nlog \"Starting Manager Agent (${MANAGER_RUNTIME})...\"\n\n# HOME is already set to /root/manager-workspace via docker run -e HOME=...\ncd \"${HOME}\"\n\n# Ensure host credential symlinks exist under HOME\nif [ -d \"/host-share\" ]; then\n [ -f \"/host-share/.gitconfig\" ] && ln -sf \"/host-share/.gitconfig\" \"${HOME}/.gitconfig\"\nfi\n\nlog \"HOME=${HOME} (manager-workspace, host-mounted)\"\n\n# ── Render agent doc templates ────────────────────────────────────────────\n# Replace ${VAR} placeholders with actual values so the AI agent reads\n# plain text and never needs to resolve environment variables.\nexport MANAGER_MATRIX_TOKEN MANAGER_TOKEN HIGRESS_COOKIE_FILE\nRENDER=/opt/agentteams/scripts/lib/render-skills.sh\nlog \"Rendering agent doc templates...\"\n# Manager-owned docs (workspace)\nbash \"$RENDER\" /root/manager-workspace/skills\nbash \"$RENDER\" /root/manager-workspace/skills-alpha\nbash \"$RENDER\" /root/manager-workspace AGENTS.md TOOLS.md HEARTBEAT.md SOUL.md\n# Worker templates (workspace + image) — rendered before push to MinIO\n# so Workers (including remote pip-install) receive plain text\nbash \"$RENDER\" /root/manager-workspace/worker-skills\nbash \"$RENDER\" /root/manager-workspace/worker-agent\nbash \"$RENDER\" /root/manager-workspace/copaw-worker-agent\nbash \"$RENDER\" /root/manager-workspace/hermes-worker-agent\nbash \"$RENDER\" /opt/agentteams/agent/worker-skills\nbash \"$RENDER\" /opt/agentteams/agent/worker-agent\nbash \"$RENDER\" /opt/agentteams/agent/copaw-worker-agent\nbash \"$RENDER\" /opt/agentteams/agent/hermes-worker-agent\nlog \"Agent doc templates rendered\"\n\n# Cloud mode: start background file sync (workspace ↔ OSS) and initial push\nif [ \"${AGENTTEAMS_RUNTIME}\" = \"aliyun\" ]; then\n log \"Syncing initial workspace to OSS...\"\n ensure_mc_credentials\n mc mirror /root/manager-workspace/ \"${AGENTTEAMS_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" 2>/dev/null || true\n\n # Local → OSS: change-triggered sync\n (\n while true; do\n CHANGED=$(find /root/manager-workspace/ -type f -newermt \"15 seconds ago\" 2>/dev/null | head -1)\n if [ -n \"${CHANGED}\" ]; then\n ensure_mc_credentials 2>/dev/null || true\n mc mirror /root/manager-workspace/ \"${AGENTTEAMS_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" --exclude \".npm/**\" \\\n --exclude \".local/**\" --exclude \".mc/**\" 2>/dev/null || true\n fi\n sleep 10\n done\n ) &\n log \"Local→OSS sync started (PID: $!)\"\n\n # OSS → Local: periodic pull (shared data, agent configs, manager openclaw.json)\n (\n while true; do\n sleep 60\n ensure_mc_credentials 2>/dev/null || true\n mc mirror \"${AGENTTEAMS_STORAGE_PREFIX}/shared/\" /root/agentteams-fs/shared/ --overwrite --newer-than \"1m\" 2>/dev/null || true\n mc mirror \"${AGENTTEAMS_STORAGE_PREFIX}/agents/\" /root/agentteams-fs/agents/ --overwrite --newer-than \"1m\" 2>/dev/null || true\n mc cp \"${AGENTTEAMS_STORAGE_PREFIX}/manager/openclaw.json\" /root/manager-workspace/openclaw.json 2>/dev/null || true\n done\n ) &\n log \"OSS→Local sync started (every 60s, PID: $!)\"\nfi\n\n# K8s mode: start background file sync (workspace ↔ MinIO)\nif [ \"${AGENTTEAMS_RUNTIME}\" = \"k8s\" ]; then\n log \"Syncing initial workspace to MinIO...\"\n mc mirror /root/manager-workspace/ \"${AGENTTEAMS_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" 2>/dev/null || true\n\n # Local → MinIO: change-triggered sync\n (\n while true; do\n CHANGED=$(find /root/manager-workspace/ -type f -newermt \"15 seconds ago\" 2>/dev/null | head -1)\n if [ -n \"${CHANGED}\" ]; then\n mc mirror /root/manager-workspace/ \"${AGENTTEAMS_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" --exclude \".npm/**\" \\\n --exclude \".local/**\" --exclude \".mc/**\" 2>/dev/null || true\n fi\n sleep 10\n done\n ) &\n log \"Local→MinIO sync started (PID: $!)\"\n\n # MinIO → Local: periodic pull (shared data, agent configs, manager openclaw.json)\n (\n while true; do\n sleep 60\n mc mirror \"${AGENTTEAMS_STORAGE_PREFIX}/shared/\" /root/agentteams-fs/shared/ --overwrite --newer-than \"1m\" 2>/dev/null || true\n mc mirror \"${AGENTTEAMS_STORAGE_PREFIX}/agents/\" /root/agentteams-fs/agents/ --overwrite --newer-than \"1m\" 2>/dev/null || true\n mc mirror \"${AGENTTEAMS_STORAGE_PREFIX}/agentteams-config/\" /root/agentteams-fs/agentteams-config/ --overwrite --newer-than \"1m\" 2>/dev/null || true\n mc cp \"${AGENTTEAMS_STORAGE_PREFIX}/manager/openclaw.json\" /root/manager-workspace/openclaw.json 2>/dev/null || true\n done\n ) &\n log \"MinIO→Local sync started (every 60s, PID: $!)\"\nfi\n\n# ============================================================\n# Auto-generate Manager mcporter config for pre-configured MCP servers\n# If AGENTTEAMS_GITHUB_TOKEN was set at install time, setup-higress.sh already\n# configured GitHub MCP on Higress. Run setup-mcp-server.sh now so that\n# config/mcporter.json exists when the Agent starts — no need to ask user for PAT.\n# ============================================================\nif [ -n \"${AGENTTEAMS_GITHUB_TOKEN}\" ] && [ \"${AGENTTEAMS_RUNTIME}\" != \"aliyun\" ] && [ \"${AGENTTEAMS_RUNTIME}\" != \"k8s\" ]; then\n if [ ! -f \"${HOME}/config/mcporter.json\" ]; then\n log \"Auto-generating Manager mcporter config for GitHub MCP (AGENTTEAMS_GITHUB_TOKEN set)...\"\n bash /opt/agentteams/agent/skills/mcp-server-management/scripts/setup-mcp-server.sh \\\n github \"${AGENTTEAMS_GITHUB_TOKEN}\" 2>&1 | while IFS= read -r line; do log \" [setup-mcp] ${line}\"; done || \\\n log \"WARNING: setup-mcp-server.sh failed — Agent may need to configure GitHub MCP manually\"\n else\n log \"Manager mcporter config already exists, skipping auto-generate\"\n fi\nfi\n\n# ============================================================\n# Runtime-specific startup\n# ============================================================\nif [ \"${MANAGER_RUNTIME}\" = \"copaw\" ]; then\n # Delegate to CoPaw startup script\n exec /opt/agentteams/scripts/init/start-copaw-manager.sh\nelse\n # ── OpenClaw Runtime ─────────────────────────────────────────────────────\n log \"Starting OpenClaw Manager...\"\n\n export OPENCLAW_CONFIG_PATH=\"/root/manager-workspace/openclaw.json\"\n\n # Symlink to default OpenClaw config path so CLI commands find the config\n mkdir -p \"${HOME}/.openclaw\"\n ln -sf \"/root/manager-workspace/openclaw.json\" \"${HOME}/.openclaw/openclaw.json\"\n\n # Clean orphaned session write locks (e.g. from SIGKILL or crash before exit handlers)\n # Prevents \"session file locked (timeout 10000ms)\" when PID was reused\n find \"${HOME}/.openclaw/agents\" -name \"*.jsonl.lock\" -delete 2>/dev/null || true\n log \"Cleaned up any orphaned session write locks\"\n\n # Clean Matrix crypto storage (SQLite WAL may be corrupted after unclean shutdown)\n # Crypto state is re-negotiated on startup; losing it only means re-establishing E2EE sessions\n rm -rf \"${HOME}/.openclaw/matrix\" 2>/dev/null || true\n log \"Cleaned Matrix crypto storage (will re-establish E2EE sessions)\"\n\n # Launch OpenClaw\n # Disable full-process respawn so the CLI uses its internal restart loop.\n # Without this, config reload spawns a detached child and exits, then\n # supervisord restarts the CLI — resulting in two gateway processes.\n export OPENCLAW_NO_RESPAWN=1\n\n # Optional matrix-plugin trace logging — when AGENTTEAMS_MATRIX_DEBUG=1 is set\n # in the manager environment (propagated by install / supervisord), turn on\n # OPENCLAW_MATRIX_DEBUG so the matrix plugin emits structured INFO-level\n # lifecycle traces (sync.state transitions, room.invite/join, message\n # handler arrival + filter outcomes). Useful for diagnosing \"worker never\n # joined the room\" / \"manager never replied\" hangs without rebuilding the\n # image.\n if [ \"${AGENTTEAMS_MATRIX_DEBUG:-}\" = \"1\" ] && [ -z \"${OPENCLAW_MATRIX_DEBUG:-}\" ]; then\n export OPENCLAW_MATRIX_DEBUG=1\n log \"AGENTTEAMS_MATRIX_DEBUG=1 detected; OPENCLAW_MATRIX_DEBUG=1 exported for matrix plugin tracing\"\n fi\n\n exec openclaw gateway run --verbose --force\nfi\n"}, "687b6d94ba04e894f5982150293c188158189d3e": {"t": 1785764733.5642896, "n": 69802, "h": "96ae921f7d6224fd", "c": "#!/bin/bash\n# start-manager-agent.sh - Initialize and start the Manager Agent\n# Supports local (supervisord), cloud (SAE), and K8s (Helm) deployments.\n# In local mode this is the last supervisord component to start (priority 800).\n# In cloud/k8s mode (AGENTTEAMS_RUNTIME=aliyun|k8s) this is the container entrypoint.\n#\n# Runtime selection:\n# AGENTTEAMS_MANAGER_RUNTIME=openclaw (default) - OpenClaw gateway mode\n# AGENTTEAMS_MANAGER_RUNTIME=copaw - CoPaw workspace mode\n# (hermes runtime is supported for Workers only; Managers run openclaw or copaw.)\n\nsource /opt/agentteams/scripts/lib/agentteams-env.sh\n\n# ============================================================\n# Runtime selection\n# ============================================================\nMANAGER_RUNTIME=\"${AGENTTEAMS_MANAGER_RUNTIME:-openclaw}\"\ncase \"${MANAGER_RUNTIME}\" in\n copaw)\n log \"Manager runtime: CoPaw (Python workspace)\"\n ;;\n *)\n log \"Manager runtime: OpenClaw (Node.js gateway)\"\n MANAGER_RUNTIME=\"openclaw\"\n ;;\nesac\n\n# ============================================================\n# Set timezone from TZ env var\n# ============================================================\nif [ -n \"${TZ}\" ] && [ -f \"/usr/share/zoneinfo/${TZ}\" ]; then\n ln -sf \"/usr/share/zoneinfo/${TZ}\" /etc/localtime\n echo \"${TZ}\" > /etc/timezone\n log \"Timezone set to ${TZ}\"\nfi\n\nexport MATRIX_DOMAIN=\"${AGENTTEAMS_MATRIX_DOMAIN:-matrix-local.agentteams.io:8080}\"\nAI_GATEWAY_DOMAIN=\"${AGENTTEAMS_AI_GATEWAY_DOMAIN:-aigw-local.agentteams.io}\"\n\n# ============================================================\n# YOLO mode promotion\n# ============================================================\n# In embedded mode the controller does not propagate AGENTTEAMS_YOLO to the\n# manager container, but installer / test scripts touch a marker file at\n# `${WORKSPACE}/yolo-mode` instead. Promote that marker to the env var so the\n# agent's documented YOLO check (`AGENTTEAMS_YOLO=1`) reliably detects it without\n# depending on filesystem lookups during a turn.\nif [ -z \"${AGENTTEAMS_YOLO:-}\" ] && [ -f /root/manager-workspace/yolo-mode ]; then\n export AGENTTEAMS_YOLO=1\n log \"YOLO mode marker detected at /root/manager-workspace/yolo-mode; AGENTTEAMS_YOLO=1 exported\"\nfi\n\n# ============================================================\n# Cloud/K8s mode: validate required environment variables + initial credentials\n# ============================================================\nif [ \"${AGENTTEAMS_RUNTIME}\" = \"aliyun\" ] || [ \"${AGENTTEAMS_RUNTIME}\" = \"k8s\" ]; then\n : \"${AGENTTEAMS_MATRIX_URL:?AGENTTEAMS_MATRIX_URL is required}\"\n : \"${AGENTTEAMS_MATRIX_DOMAIN:?AGENTTEAMS_MATRIX_DOMAIN is required}\"\n : \"${AGENTTEAMS_AI_GATEWAY_URL:?AGENTTEAMS_AI_GATEWAY_URL is required}\"\n if [ \"${AGENTTEAMS_RUNTIME}\" = \"aliyun\" ]; then\n : \"${AGENTTEAMS_MANAGER_GATEWAY_KEY:?AGENTTEAMS_MANAGER_GATEWAY_KEY is required}\"\n : \"${AGENTTEAMS_MANAGER_PASSWORD:?AGENTTEAMS_MANAGER_PASSWORD is required (cloud containers are stateless, password must be injected)}\"\n fi\n if [ \"${AGENTTEAMS_RUNTIME}\" = \"k8s\" ]; then\n # K8s mode: controller handles initialization (admin registration, Higress setup).\n # Manager only needs credentials injected by the ManagerReconciler.\n : \"${AGENTTEAMS_MANAGER_GATEWAY_KEY:?AGENTTEAMS_MANAGER_GATEWAY_KEY is required (injected by controller)}\"\n # AGENTTEAMS_MANAGER_PASSWORD is optional: not needed in AppService mode\n # (token obtained via AS login), only required in legacy password mode.\n else\n # Cloud (aliyun) mode: Manager still does its own initialization\n : \"${AGENTTEAMS_REGISTRATION_TOKEN:?AGENTTEAMS_REGISTRATION_TOKEN is required}\"\n : \"${AGENTTEAMS_ADMIN_USER:?AGENTTEAMS_ADMIN_USER is required}\"\n : \"${AGENTTEAMS_ADMIN_PASSWORD:?AGENTTEAMS_ADMIN_PASSWORD is required}\"\n fi\n log \"${AGENTTEAMS_RUNTIME} mode: validating environment... OK\"\n log \" Matrix: ${AGENTTEAMS_MATRIX_URL}, AI Gateway: ${AGENTTEAMS_AI_GATEWAY_URL}, Storage: ${AGENTTEAMS_FS_BUCKET}\"\n if [ \"${AGENTTEAMS_RUNTIME}\" = \"aliyun\" ]; then\n ensure_mc_credentials || { log \"FATAL: Initial STS credential fetch failed\"; exit 1; }\n fi\nfi\n\n# ============================================================\n# Local mode: host symlinks, /etc/hosts, wait for local services\n# ============================================================\nif [ \"${AGENTTEAMS_RUNTIME}\" != \"aliyun\" ] && [ \"${AGENTTEAMS_RUNTIME}\" != \"k8s\" ]; then\n # Create symlink for host directory access\n if [ -d \"/host-share\" ]; then\n ORIGINAL_HOST_HOME=\"${HOST_ORIGINAL_HOME:-$HOME}\"\n if [ ! -e \"${ORIGINAL_HOST_HOME}\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/root\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/data\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/host-share\" ]; then\n mkdir -p \"$(dirname \"${ORIGINAL_HOST_HOME}\")\"\n ln -sfn /host-share \"${ORIGINAL_HOST_HOME}\"\n log \"Created symlink: ${ORIGINAL_HOST_HOME} -> /host-share\"\n else\n ln -sfn /host-share /root/host-home\n log \"Created fallback symlink: /root/host-home -> /host-share\"\n fi\n fi\n\n # Add local domains to /etc/hosts\n HOSTS_DOMAINS=\"${MATRIX_DOMAIN%%:*} ${AGENTTEAMS_MATRIX_CLIENT_DOMAIN:-matrix-client-local.agentteams.io} ${AI_GATEWAY_DOMAIN} ${AGENTTEAMS_FS_DOMAIN:-fs-local.agentteams.io}\"\n if ! grep -q \"${AI_GATEWAY_DOMAIN}\" /etc/hosts 2>/dev/null; then\n echo \"127.0.0.1 ${HOSTS_DOMAINS}\" >> /etc/hosts\n log \"Added local domains to /etc/hosts\"\n fi\n\n # Wait for local infrastructure\n waitForService \"Higress Gateway\" \"127.0.0.1\" 8080 180\n waitForService \"Higress Console\" \"127.0.0.1\" 8001 180\n waitForService \"Tuwunel\" \"127.0.0.1\" 6167 120\n waitForHTTP \"Tuwunel Matrix API\" \"${AGENTTEAMS_MATRIX_URL}/_tuwunel/server_version\" 120\n waitForService \"MinIO\" \"127.0.0.1\" 9000 120\nelse\n # Cloud/K8s mode: wait for external Tuwunel\n log \"Waiting for Tuwunel Matrix server at ${AGENTTEAMS_MATRIX_URL}...\"\n _retry=0\n while [ \"${_retry}\" -lt 30 ]; do\n if curl -sf \"${AGENTTEAMS_MATRIX_URL}/_matrix/client/versions\" > /dev/null 2>&1; then\n log \"Tuwunel is ready\"\n break\n fi\n _retry=$((_retry + 1))\n log \" Waiting for Tuwunel (attempt ${_retry}/30)...\"\n sleep 5\n done\n if [ \"${_retry}\" -ge 30 ]; then\n log \"ERROR: Tuwunel not reachable at ${AGENTTEAMS_MATRIX_URL}\"\n exit 1\n fi\nfi\n\n# ============================================================\n# Auto-generate secrets if not provided via environment\n# Persisted to /data so they survive container restart\n# ============================================================\nSECRETS_FILE=\"/data/agentteams-secrets.env\"\nif [ -f \"${SECRETS_FILE}\" ]; then\n source \"${SECRETS_FILE}\"\n log \"Loaded persisted secrets from ${SECRETS_FILE}\"\nfi\n\nif [ -z \"${AGENTTEAMS_MANAGER_GATEWAY_KEY}\" ]; then\n export AGENTTEAMS_MANAGER_GATEWAY_KEY=\"$(generateKey 32)\"\n log \"Auto-generated AGENTTEAMS_MANAGER_GATEWAY_KEY\"\nfi\nif [ -z \"${AGENTTEAMS_MANAGER_PASSWORD}\" ]; then\n export AGENTTEAMS_MANAGER_PASSWORD=\"$(generateKey 16)\"\n log \"Auto-generated AGENTTEAMS_MANAGER_PASSWORD\"\nfi\n\n# Persist secrets so they survive supervisord restart\nmkdir -p /data\ncat > \"${SECRETS_FILE}\" <<EOF\nexport AGENTTEAMS_MANAGER_GATEWAY_KEY=\"${AGENTTEAMS_MANAGER_GATEWAY_KEY}\"\nexport AGENTTEAMS_MANAGER_PASSWORD=\"${AGENTTEAMS_MANAGER_PASSWORD}\"\nEOF\nchmod 600 \"${SECRETS_FILE}\"\n\n# Cloud mode: pull workspace from OSS before initialization\nif [ \"${AGENTTEAMS_RUNTIME}\" = \"aliyun\" ]; then\n AGENTTEAMS_FS=\"/root/agentteams-fs\"\n mkdir -p \"${AGENTTEAMS_FS}/shared\" \"${AGENTTEAMS_FS}/agents\"\n log \"Pulling workspace from OSS...\"\n ensure_mc_credentials\n mc mirror \"${AGENTTEAMS_STORAGE_PREFIX}/manager/\" /root/manager-workspace/ --overwrite 2>/dev/null || true\n mc mirror \"${AGENTTEAMS_STORAGE_PREFIX}/shared/\" \"${AGENTTEAMS_FS}/shared/\" --overwrite 2>/dev/null || true\n mc mirror \"${AGENTTEAMS_STORAGE_PREFIX}/agents/\" \"${AGENTTEAMS_FS}/agents/\" --overwrite 2>/dev/null || true\n # Keep the canonical filesystem link in the Manager workspace.\n ln -sfn \"${AGENTTEAMS_FS}\" /root/manager-workspace/agentteams-fs\n ln -sfn \"${AGENTTEAMS_FS}\" /root/manager-workspace/agentteams-fs\nfi\n\n# K8s mode: sync workspace from cluster-internal MinIO\nif [ \"${AGENTTEAMS_RUNTIME}\" = \"k8s\" ]; then\n AGENTTEAMS_FS=\"/root/agentteams-fs\"\n mkdir -p \"${AGENTTEAMS_FS}/shared\" \"${AGENTTEAMS_FS}/agents\" \"${AGENTTEAMS_FS}/agentteams-config\"\n log \"Configuring mc alias for cluster MinIO...\"\n mc alias set agentteams \"${AGENTTEAMS_FS_ENDPOINT}\" \"${AGENTTEAMS_FS_ACCESS_KEY}\" \"${AGENTTEAMS_FS_SECRET_KEY}\"\n log \"Syncing workspace from MinIO...\"\n mc mirror \"${AGENTTEAMS_STORAGE_PREFIX}/manager/\" /root/manager-workspace/ --overwrite 2>/dev/null || true\n mc mirror \"${AGENTTEAMS_STORAGE_PREFIX}/\" \"${AGENTTEAMS_FS}/\" --overwrite 2>/dev/null || true\n ln -sfn \"${AGENTTEAMS_FS}\" /root/manager-workspace/agentteams-fs\n ln -sfn \"${AGENTTEAMS_FS}\" /root/manager-workspace/agentteams-fs\n touch \"${AGENTTEAMS_FS}/.initialized\"\nfi\n\n# ============================================================\n# Initialize / upgrade Manager workspace\n# First boot: full init via upgrade-builtins.sh\n# Subsequent boots: compare image version; upgrade only if changed\n# ============================================================\nmkdir -p /root/manager-workspace\n\nIMAGE_VERSION=$(cat /opt/agentteams/agent/.builtin-version 2>/dev/null || echo \"unknown\")\nINSTALLED_VERSION=$(cat /root/manager-workspace/.builtin-version 2>/dev/null || echo \"\")\n\nif [ ! -f /root/manager-workspace/.initialized ]; then\n log \"First boot: initializing manager workspace...\"\n bash /opt/agentteams/scripts/init/upgrade-builtins.sh\n touch /root/manager-workspace/.initialized\n log \"Manager workspace initialized (version: ${IMAGE_VERSION})\"\nelif [ \"${IMAGE_VERSION}\" != \"${INSTALLED_VERSION}\" ] || [ \"${IMAGE_VERSION}\" = \"latest\" ]; then\n log \"Upgrade detected: ${INSTALLED_VERSION} -> ${IMAGE_VERSION}${IMAGE_VERSION:+ (latest: always upgrade)}\"\n bash /opt/agentteams/scripts/init/upgrade-builtins.sh\n log \"Manager workspace upgraded to version: ${IMAGE_VERSION}\"\nelse\n log \"Workspace up to date (version: ${IMAGE_VERSION})\"\nfi\n\n# Local mode: wait for mc mirror initialization (shared + worker data in /root/agentteams-fs/)\nif [ \"${AGENTTEAMS_RUNTIME}\" != \"aliyun\" ] && [ \"${AGENTTEAMS_RUNTIME}\" != \"k8s\" ]; then\n log \"Waiting for MinIO storage initialization...\"\n _minio_wait=0\n while [ ! -f /root/agentteams-fs/.initialized ]; do\n sleep 2\n _minio_wait=$(( _minio_wait + 1 ))\n if [ \"${_minio_wait}\" -ge 60 ]; then\n log \"ERROR: MinIO storage initialization timed out after 120s\"\n exit 1\n fi\n done\n log \"MinIO storage initialized\"\nfi\n\n# ============================================================\n# Obtain Manager Matrix access token.\n#\n# Priority:\n# 1. Pre-injected token (AGENTTEAMS_MANAGER_MATRIX_TOKEN) — set by the\n# controller in both AppService mode (where no password exists) and\n# legacy mode (to avoid a redundant login round-trip).\n# 2. Password-based login — fallback for containers started without\n# controller involvement (manual docker run, legacy installs).\n# ============================================================\nif [ -n \"${AGENTTEAMS_MANAGER_MATRIX_TOKEN:-}\" ]; then\n MANAGER_TOKEN=\"${AGENTTEAMS_MANAGER_MATRIX_TOKEN}\"\n log \"Manager Matrix token pre-injected by controller (token prefix: ${MANAGER_TOKEN:0:10}...)\"\nelif [ \"${AGENTTEAMS_RUNTIME}\" = \"k8s\" ]; then\n log \"K8s mode: obtaining Manager Matrix token via password login...\"\n _LOGIN_RESPONSE=$(curl -s -X POST ${AGENTTEAMS_MATRIX_URL}/_matrix/client/v3/login \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"type\": \"m.login.password\",\n \"identifier\": {\"type\": \"m.id.user\", \"user\": \"manager\"},\n \"password\": \"'\"${AGENTTEAMS_MANAGER_PASSWORD}\"'\"\n }' 2>&1)\n MANAGER_TOKEN=$(echo \"${_LOGIN_RESPONSE}\" | jq -r '.access_token' 2>/dev/null)\n if [ -z \"${MANAGER_TOKEN}\" ] || [ \"${MANAGER_TOKEN}\" = \"null\" ]; then\n log \"ERROR: Failed to obtain Manager Matrix token\"\n log \"ERROR: Login response was: ${_LOGIN_RESPONSE}\"\n exit 1\n fi\n log \"Manager Matrix token obtained (token prefix: ${MANAGER_TOKEN:0:10}...)\"\nelse\nlog \"Registering human admin Matrix account...\"\ncurl -sf -X POST ${AGENTTEAMS_MATRIX_URL}/_matrix/client/v3/register \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"username\": \"'\"${AGENTTEAMS_ADMIN_USER}\"'\",\n \"password\": \"'\"${AGENTTEAMS_ADMIN_PASSWORD}\"'\",\n \"auth\": {\n \"type\": \"m.login.registration_token\",\n \"token\": \"'\"${AGENTTEAMS_REGISTRATION_TOKEN}\"'\"\n }\n }' > /dev/null 2>&1 || log \"Admin account may already exist\"\n\nlog \"Registering Manager Agent Matrix account...\"\ncurl -sf -X POST ${AGENTTEAMS_MATRIX_URL}/_matrix/client/v3/register \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"username\": \"manager\",\n \"password\": \"'\"${AGENTTEAMS_MANAGER_PASSWORD}\"'\",\n \"auth\": {\n \"type\": \"m.login.registration_token\",\n \"token\": \"'\"${AGENTTEAMS_REGISTRATION_TOKEN}\"'\"\n }\n }' > /dev/null 2>&1 || log \"Manager account may already exist\"\n\n# Get Manager Agent's Matrix access token\nlog \"Obtaining Manager Matrix access token...\"\n_LOGIN_RESPONSE=$(curl -s -X POST ${AGENTTEAMS_MATRIX_URL}/_matrix/client/v3/login \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"type\": \"m.login.password\",\n \"identifier\": {\"type\": \"m.id.user\", \"user\": \"manager\"},\n \"password\": \"'\"${AGENTTEAMS_MANAGER_PASSWORD}\"'\"\n }' 2>&1)\n_LOGIN_EXIT=$?\nlog \"Matrix login HTTP exit code: ${_LOGIN_EXIT}\"\nlog \"Matrix login response: ${_LOGIN_RESPONSE}\"\n\nMANAGER_TOKEN=$(echo \"${_LOGIN_RESPONSE}\" | jq -r '.access_token' 2>/dev/null)\n\nif [ -z \"${MANAGER_TOKEN}\" ] || [ \"${MANAGER_TOKEN}\" = \"null\" ]; then\n log \"ERROR: Failed to obtain Manager Matrix token (exit=${_LOGIN_EXIT})\"\n log \"ERROR: Login response was: ${_LOGIN_RESPONSE}\"\n exit 1\nfi\nlog \"Manager Matrix token obtained (token prefix: ${MANAGER_TOKEN:0:10}...)\"\nfi\n\n# ============================================================\n# Higress Console initialization\n# Docker mode: full setup-higress.sh (internal Higress at localhost:8001)\n# K8s mode: skip — controller Initializer handles Higress setup\n# Cloud (aliyun) mode: skip entirely (Higress managed externally)\n# ============================================================\n_HIGRESS_CONSOLE_URL=\"\"\n_HIGRESS_USER=\"${AGENTTEAMS_ADMIN_USER}\"\n_HIGRESS_PASS=\"${AGENTTEAMS_ADMIN_PASSWORD}\"\nif [ \"${AGENTTEAMS_RUNTIME}\" = \"k8s\" ]; then\n log \"K8s mode: skipping Higress initialization (handled by controller)\"\nelif [ \"${AGENTTEAMS_RUNTIME}\" != \"aliyun\" ]; then\n _HIGRESS_CONSOLE_URL=\"http://127.0.0.1:8001\"\nfi\n\nif [ -n \"${_HIGRESS_CONSOLE_URL}\" ]; then\n COOKIE_FILE=\"/tmp/higress-session-cookie\"\n\n log \"Waiting for Higress Console (${_HIGRESS_CONSOLE_URL}) to be fully ready and initializing admin...\"\n INIT_DONE=false\n for i in $(seq 1 90); do\n INIT_RESULT=$(curl -s -X POST \"${_HIGRESS_CONSOLE_URL}/system/init\" \\\n -H 'Content-Type: application/json' \\\n -d '{\"adminUser\":{\"name\":\"'\"${_HIGRESS_USER}\"'\",\"password\":\"'\"${_HIGRESS_PASS}\"'\",\"displayName\":\"'\"${_HIGRESS_USER}\"'\"}}' 2>/dev/null) || true\n if echo \"${INIT_RESULT}\" | grep -qE '\"success\":true|already.?init' 2>/dev/null; then\n INIT_DONE=true\n break\n fi\n if echo \"${INIT_RESULT}\" | grep -q '\"name\"' 2>/dev/null; then\n INIT_DONE=true\n break\n fi\n sleep 2\n done\n\n if [ \"${INIT_DONE}\" != \"true\" ]; then\n log \"ERROR: Higress Console did not become ready within 180s\"\n exit 1\n fi\n log \"Higress Console init done\"\n\n log \"Logging into Higress Console...\"\n LOGIN_OK=false\n for i in $(seq 1 10); do\n HTTP_CODE=$(curl -s -o /dev/null -w '%{http_code}' -X POST \"${_HIGRESS_CONSOLE_URL}/session/login\" \\\n -H 'Content-Type: application/json' \\\n -c \"${COOKIE_FILE}\" \\\n -d '{\"username\":\"'\"${_HIGRESS_USER}\"'\",\"password\":\"'\"${_HIGRESS_PASS}\"'\"}' 2>/dev/null) || true\n if { [ \"${HTTP_CODE}\" = \"200\" ] || [ \"${HTTP_CODE}\" = \"201\" ]; } && [ -f \"${COOKIE_FILE}\" ] && [ -s \"${COOKIE_FILE}\" ]; then\n LOGIN_OK=true\n break\n fi\n log \"Login attempt $i (HTTP ${HTTP_CODE}), retrying in 3s...\"\n sleep 3\n done\n\n if [ \"${LOGIN_OK}\" != \"true\" ]; then\n log \"ERROR: Could not login to Higress Console after retries\"\n exit 1\n fi\n log \"Higress Console login successful\"\n\n VERIFY_CODE=$(curl -s -o /dev/null -w '%{http_code}' \"${_HIGRESS_CONSOLE_URL}/v1/consumers\" -b \"${COOKIE_FILE}\" 2>/dev/null) || true\n if [ \"${VERIFY_CODE}\" = \"200\" ]; then\n log \"Console session verified (cookie valid)\"\n else\n log \"WARNING: Console session may be invalid (verify returned HTTP ${VERIFY_CODE})\"\n rm -f \"${COOKIE_FILE}\"\n for i in $(seq 1 5); do\n curl -s -o /dev/null -w '%{http_code}' -X POST \"${_HIGRESS_CONSOLE_URL}/session/login\" \\\n -H 'Content-Type: application/json' \\\n -c \"${COOKIE_FILE}\" \\\n -d '{\"username\":\"'\"${_HIGRESS_USER}\"'\",\"password\":\"'\"${_HIGRESS_PASS}\"'\"}' 2>/dev/null\n VERIFY2=$(curl -s -o /dev/null -w '%{http_code}' \"${_HIGRESS_CONSOLE_URL}/v1/consumers\" -b \"${COOKIE_FILE}\" 2>/dev/null) || true\n if [ \"${VERIFY2}\" = \"200\" ]; then\n log \"Re-login successful, session verified\"\n break\n fi\n sleep 2\n done\n fi\n\n export HIGRESS_COOKIE_FILE=\"${COOKIE_FILE}\"\n export HIGRESS_CONSOLE_URL=\"${_HIGRESS_CONSOLE_URL}\"\n\n if [ \"${AGENTTEAMS_RUNTIME}\" = \"k8s\" ]; then\n # K8s mode: lightweight Higress config — only what's needed for LLM access\n source /opt/agentteams/scripts/lib/base.sh\n _k8s_higress_api() {\n local method=\"$1\" path=\"$2\" desc=\"$3\"; shift 3; local body=\"$*\"\n local tmpfile; tmpfile=$(mktemp)\n local http_code\n http_code=$(curl -s -o \"${tmpfile}\" -w '%{http_code}' -X \"${method}\" \"${_HIGRESS_CONSOLE_URL}${path}\" \\\n -b \"${COOKIE_FILE}\" -H 'Content-Type: application/json' -d \"${body}\" 2>/dev/null) || true\n local response; response=$(cat \"${tmpfile}\" 2>/dev/null); rm -f \"${tmpfile}\"\n if echo \"${response}\" | grep -q '\"success\":true' 2>/dev/null; then\n log \"${desc} ... OK\"\n elif [ \"${http_code}\" = \"409\" ]; then\n log \"${desc} ... already exists, skipping\"\n elif [ \"${http_code}\" = \"200\" ] || [ \"${http_code}\" = \"201\" ] || [ \"${http_code}\" = \"204\" ]; then\n log \"${desc} ... OK (HTTP ${http_code})\"\n else\n log \"WARNING: ${desc} ... (HTTP ${http_code}): ${response}\"\n fi\n }\n\n # 1. Service Sources (DNS type → K8s Service FQDN)\n # Extract host:port from URLs for Higress service source registration\n _TUWUNEL_HOST=$(echo \"${AGENTTEAMS_MATRIX_URL}\" | sed 's|^http[s]*://||')\n _TUWUNEL_DOMAIN=$(echo \"${_TUWUNEL_HOST}\" | cut -d: -f1)\n _TUWUNEL_PORT=$(echo \"${_TUWUNEL_HOST}\" | cut -d: -f2)\n _k8s_higress_api POST /v1/service-sources \"Registering Tuwunel service source\" \\\n '{\"type\":\"dns\",\"name\":\"tuwunel\",\"domain\":\"'\"${_TUWUNEL_DOMAIN}\"'\",\"port\":'\"${_TUWUNEL_PORT}\"'}'\n\n if [ -n \"${AGENTTEAMS_ELEMENT_WEB_URL:-}\" ]; then\n _ELEMENT_HOST=$(echo \"${AGENTTEAMS_ELEMENT_WEB_URL}\" | sed 's|^http[s]*://||')\n _ELEMENT_DOMAIN=$(echo \"${_ELEMENT_HOST}\" | cut -d: -f1)\n _ELEMENT_PORT=$(echo \"${_ELEMENT_HOST}\" | cut -d: -f2)\n _k8s_higress_api POST /v1/service-sources \"Registering Element Web service source\" \\\n '{\"type\":\"dns\",\"name\":\"element-web\",\"domain\":\"'\"${_ELEMENT_DOMAIN}\"'\",\"port\":'\"${_ELEMENT_PORT}\"'}'\n fi\n\n # 2. Manager Consumer (key-auth)\n _k8s_higress_api POST /v1/consumers \"Creating Manager consumer\" \\\n '{\"name\":\"manager\",\"credentials\":[{\"type\":\"key-auth\",\"source\":\"BEARER\",\"values\":[\"'\"${AGENTTEAMS_MANAGER_GATEWAY_KEY}\"'\"]}]}'\n\n # 3. LLM Provider\n _LLM_PROVIDER=\"${AGENTTEAMS_LLM_PROVIDER:-qwen}\"\n _LLM_API_URL=\"${AGENTTEAMS_LLM_API_URL:-}\"\n if [ -z \"${_LLM_API_URL}\" ]; then\n case \"${_LLM_PROVIDER}\" in\n qwen) _LLM_API_URL=\"https://dashscope.aliyuncs.com/compatible-mode/v1\" ;;\n esac\n fi\n # 4. LLM Provider type-specific config\n case \"${_LLM_PROVIDER}\" in\n qwen)\n _k8s_higress_api POST /v1/ai/providers \"Creating LLM provider (qwen)\" \\\n '{\"type\":\"qwen\",\"name\":\"qwen\",\"tokens\":[\"'\"${AGENTTEAMS_LLM_API_KEY}\"'\"],\"protocol\":\"openai/v1\",\"tokenFailoverConfig\":{\"enabled\":false},\"rawConfigs\":{\"qwenEnableSearch\":false,\"qwenEnableCompatible\":true,\"qwenFileIds\":[],\"agentteamsMode\":true}}'\n ;;\n *)\n _BODY='{\"name\":\"'\"${_LLM_PROVIDER}\"'\",\"type\":\"openai\",\"tokens\":[\"'\"${AGENTTEAMS_LLM_API_KEY}\"'\"],\"modelMapping\":{},\"protocol\":\"openai/v1\",\"rawConfigs\":{\"agentteamsMode\":true}}'\n _k8s_higress_api POST /v1/ai/providers \"Creating LLM provider (${_LLM_PROVIDER})\" \"${_BODY}\"\n ;;\n esac\n\n # 5. AI Route (bind provider + consumer auth, /v1 prefix to avoid clash with Element Web catch-all)\n _k8s_higress_api POST /v1/ai/routes \"Creating AI Gateway route\" \\\n '{\"name\":\"default-ai-route\",\"domains\":[],\"pathPredicate\":{\"matchType\":\"PRE\",\"matchValue\":\"/v1\",\"caseSensitive\":false},\"upstreams\":[{\"provider\":\"'\"${_LLM_PROVIDER}\"'\",\"weight\":100,\"modelMapping\":{}}],\"authConfig\":{\"enabled\":true,\"allowedCredentialTypes\":[\"key-auth\"],\"allowedConsumers\":[\"manager\"]}}'\n\n # 6. Matrix Homeserver Route (/_matrix/* → Tuwunel, no auth)\n _k8s_higress_api POST /v1/routes \"Creating Matrix Homeserver route\" \\\n '{\"name\":\"matrix-homeserver\",\"domains\":[],\"path\":{\"matchType\":\"PRE\",\"matchValue\":\"/_matrix\"},\"services\":[{\"name\":\"tuwunel.dns\",\"port\":'\"${_TUWUNEL_PORT}\"',\"weight\":100}]}'\n\n # 7. Element Web Route (/ catch-all → Element Web, no auth)\n if [ -n \"${AGENTTEAMS_ELEMENT_WEB_URL:-}\" ]; then\n _k8s_higress_api POST /v1/routes \"Creating Element Web route\" \\\n '{\"name\":\"element-web\",\"domains\":[],\"path\":{\"matchType\":\"PRE\",\"matchValue\":\"/\"},\"services\":[{\"name\":\"element-web.dns\",\"port\":'\"${_ELEMENT_PORT}\"',\"weight\":100}]}'\n fi\n\n # 8. Remove Higress default landing page (Exact match on / takes precedence over Element Web catch-all)\n _k8s_higress_api DELETE /v1/routes/default \"Removing Higress default landing route\"\n\n log \"K8s Higress lightweight setup complete\"\n\n # Wait for AI plugin activation (~45s for first config)\n log \"Waiting for AI Gateway plugin activation (45s)...\"\n sleep 45\n else\n # Docker mode: full setup with all routes, domains, MCP servers\n /opt/agentteams/scripts/init/setup-higress.sh\n fi\nfi\n\n# ============================================================\n# Create admin DM room, persist to state.json, send welcome message\n# K8s mode: skip — controller ProvisionManager creates the Admin DM\n# room (Step 4 in service/provisioner.go) AND reconcileManagerWelcome\n# delivers the first-boot onboarding prompt once OpenClaw inside this\n# container has joined the room. In k8s mode the manager intentionally\n# does NOT have the admin password (only AGENTTEAMS_ADMIN_USER), so it\n# could not log in as admin to send the welcome itself anyway. The\n# Manager Agent discovers its admin DM room on first heartbeat via\n# state.json / `manage-state.sh --action set-admin-dm` (see HEARTBEAT.md\n# Step 1) — it does not need it to be pre-injected by this script.\n# Runs in both local and cloud modes (idempotent)\n# ============================================================\nif [ \"${AGENTTEAMS_RUNTIME}\" = \"k8s\" ]; then\n log \"K8s mode: skipping admin DM room creation and welcome message (both handled by agentteams-controller)\"\nelse\nMANAGER_FULL_ID=\"@manager:${MATRIX_DOMAIN}\"\nADMIN_FULL_ID=\"@${AGENTTEAMS_ADMIN_USER}:${MATRIX_DOMAIN}\"\n\nlog \"Logging in as admin to create DM room...\"\n_ADMIN_LOGIN=$(curl -sf -X POST \"${AGENTTEAMS_MATRIX_URL}/_matrix/client/v3/login\" \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"type\": \"m.login.password\",\n \"identifier\": {\"type\": \"m.id.user\", \"user\": \"'\"${AGENTTEAMS_ADMIN_USER}\"'\"},\n \"password\": \"'\"${AGENTTEAMS_ADMIN_PASSWORD}\"'\"\n }' 2>&1) || true\n\nADMIN_MATRIX_TOKEN=$(echo \"${_ADMIN_LOGIN}\" | jq -r '.access_token // empty' 2>/dev/null)\nif [ -z \"${ADMIN_MATRIX_TOKEN}\" ]; then\n log \"WARNING: Failed to login as admin, skipping DM room creation\"\nelse\n # Search for existing DM room with Manager (idempotent)\n DM_ROOM_ID=\"\"\n _JOINED_ROOMS=$(curl -sf \"${AGENTTEAMS_MATRIX_URL}/_matrix/client/v3/joined_rooms\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" 2>/dev/null \\\n | jq -r '.joined_rooms[]' 2>/dev/null) || true\n for _rid in ${_JOINED_ROOMS}; do\n _members=$(curl -sf \"${AGENTTEAMS_MATRIX_URL}/_matrix/client/v3/rooms/${_rid}/members\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" 2>/dev/null \\\n | jq -r '.chunk[].state_key' 2>/dev/null) || continue\n _count=$(echo \"${_members}\" | wc -l | xargs)\n if [ \"${_count}\" = \"2\" ] && echo \"${_members}\" | grep -q \"@manager:\"; then\n DM_ROOM_ID=\"${_rid}\"\n break\n fi\n done\n\n if [ -n \"${DM_ROOM_ID}\" ]; then\n log \"Existing DM room found: ${DM_ROOM_ID}\"\n else\n log \"Creating DM room with Manager...\"\n _RAW=$(curl -s -w '\\nHTTP_CODE:%{http_code}' -X POST \"${AGENTTEAMS_MATRIX_URL}/_matrix/client/v3/createRoom\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d \"{\\\"is_direct\\\":true,\\\"invite\\\":[\\\"${MANAGER_FULL_ID}\\\"],\\\"preset\\\":\\\"trusted_private_chat\\\"}\" 2>&1) || true\n _HTTP_CODE=$(echo \"${_RAW}\" | tail -1 | sed 's/HTTP_CODE://')\n _CREATE_RESP=$(echo \"${_RAW}\" | sed '$d')\n DM_ROOM_ID=$(echo \"${_CREATE_RESP}\" | jq -r '.room_id // empty' 2>/dev/null)\n if [ -n \"${DM_ROOM_ID}\" ]; then\n log \"DM room created: ${DM_ROOM_ID}\"\n else\n log \"WARNING: Failed to create DM room (HTTP ${_HTTP_CODE}): ${_CREATE_RESP}\"\n fi\n fi\n\n # Persist admin DM room ID to state.json\n if [ -n \"${DM_ROOM_ID}\" ]; then\n STATE_SCRIPT=\"/opt/agentteams/agent/skills/task-management/scripts/manage-state.sh\"\n if [ -f \"${STATE_SCRIPT}\" ]; then\n bash \"${STATE_SCRIPT}\" --action init 2>/dev/null || true\n bash \"${STATE_SCRIPT}\" --action set-admin-dm --room-id \"${DM_ROOM_ID}\" 2>/dev/null || true\n log \"Admin DM room persisted to state.json: ${DM_ROOM_ID}\"\n fi\n fi\n\n # Schedule welcome message in background (only on first boot)\n if [ -n \"${DM_ROOM_ID}\" ] && [ ! -f \"/root/manager-workspace/soul-configured\" ]; then\n log \"Scheduling welcome message (background, waiting for OpenClaw to start)...\"\n (\n _AGENTTEAMS_LANGUAGE=\"${AGENTTEAMS_LANGUAGE:-zh}\"\n _AGENTTEAMS_TIMEZONE=\"${TZ:-Asia/Shanghai}\"\n _wait=0\n _ready=false\n while [ \"${_wait}\" -lt 300 ]; do\n if curl -sf http://127.0.0.1:18799/ > /dev/null 2>&1; then\n _ready=true\n break\n fi\n sleep 3\n _wait=$((_wait + 3))\n done\n if [ \"${_ready}\" != \"true\" ]; then\n echo \"[manager] WARNING: OpenClaw gateway not ready within 300s, skipping welcome message\"\n exit 0\n fi\n # Ensure Manager has joined the DM room before sending the welcome\n # message. Without this, there is a race between OpenClaw's Matrix\n # auto-join and the message send — the message may land before Manager\n # joins, so OpenClaw's /sync never picks it up.\n _join_ok=false\n for _join_attempt in 1 2 3; do\n if curl -sf -X POST \"${AGENTTEAMS_MATRIX_URL}/_matrix/client/v3/rooms/${DM_ROOM_ID}/join\" \\\n -H \"Authorization: Bearer ${MANAGER_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d '{}' > /dev/null 2>&1; then\n echo \"[manager] Manager joined DM room before welcome message\"\n _join_ok=true\n break\n fi\n sleep 2\n done\n if [ \"${_join_ok}\" != \"true\" ]; then\n echo \"[manager] WARNING: Manager join request failed after 3 attempts (may already be joined)\"\n fi\n _welcome_msg=\"This is an automated message from the AgentTeams setup. This is a fresh installation.\n\n--- Installation Context ---\nUser Language: ${_AGENTTEAMS_LANGUAGE} (zh = Chinese, en = English)\nUser Timezone: ${_AGENTTEAMS_TIMEZONE} (IANA timezone identifier)\n---\n\nYou are an AI agent that manages a team of worker agents. Your identity and personality have not been configured yet — the human admin is about to meet you for the first time.\n\nPlease begin the onboarding conversation:\n\n1. Greet the admin warmly and briefly describe what you can do (coordinate workers, manage tasks, run multi-agent projects)\n2. The user has selected \\\"${_AGENTTEAMS_LANGUAGE}\\\" as their preferred language during installation. Use this language for your greeting and all subsequent communication.\n3. The user's timezone is ${_AGENTTEAMS_TIMEZONE}. Based on this timezone, you may infer their likely region and suggest additional language options.\n4. Ask them: a) What would they like to call you? b) Communication style preference? c) Any behavior guidelines? d) Confirm default language\n5. After they reply, write their preferences to ~/SOUL.md\n6. Confirm what you wrote, and ask if they would like to adjust anything\n7. Once confirmed, run: touch ~/soul-configured\n\nThe human admin will start chatting shortly.\"\n _txn_id=\"welcome-$(date +%s)\"\n _payload=$(jq -nc --arg body \"${_welcome_msg}\" '{\"msgtype\":\"m.text\",\"body\":$body}')\n _raw=$(curl -s -w '\\nHTTP_CODE:%{http_code}' -X PUT \"${AGENTTEAMS_MATRIX_URL}/_matrix/client/v3/rooms/${DM_ROOM_ID}/send/m.room.message/${_txn_id}\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d \"${_payload}\" 2>&1) || true\n _http_code=$(echo \"${_raw}\" | tail -1 | sed 's/HTTP_CODE://')\n _send_resp=$(echo \"${_raw}\" | sed '$d')\n if echo \"${_send_resp}\" | jq -e '.event_id' > /dev/null 2>&1; then\n echo \"[manager] Welcome message sent to DM room\"\n else\n echo \"[manager] WARNING: Failed to send welcome message (HTTP ${_http_code}): ${_send_resp}\"\n fi\n ) &\n log \"Welcome message background process started (PID: $!)\"\n fi\nfi\nfi # end K8s mode skip for admin DM room\n\n# ============================================================\n# Generate Manager Agent openclaw.json from template\n# ============================================================\nlog \"Generating Manager openclaw.json...\"\nexport MANAGER_MATRIX_TOKEN=\"${MANAGER_TOKEN}\"\nexport MANAGER_GATEWAY_KEY=\"${AGENTTEAMS_MANAGER_GATEWAY_KEY}\"\n# Resolve model parameters based on model name\nMODEL_NAME=\"${AGENTTEAMS_DEFAULT_MODEL:-qwen3.6-plus}\"\ncase \"${MODEL_NAME}\" in\n gpt-5.3-codex|gpt-5-mini|gpt-5-nano)\n export MODEL_CONTEXT_WINDOW=400000 MODEL_MAX_TOKENS=128000 ;;\n claude-opus-4-6)\n export MODEL_CONTEXT_WINDOW=1000000 MODEL_MAX_TOKENS=128000 ;;\n claude-sonnet-4-6)\n export MODEL_CONTEXT_WINDOW=1000000 MODEL_MAX_TOKENS=64000 ;;\n claude-haiku-4-5)\n export MODEL_CONTEXT_WINDOW=200000 MODEL_MAX_TOKENS=64000 ;;\n qwen3.6-plus|qwen3.5-plus)\n export MODEL_CONTEXT_WINDOW=200000 MODEL_MAX_TOKENS=64000 ;;\n deepseek-chat|deepseek-reasoner|kimi-k2.5)\n export MODEL_CONTEXT_WINDOW=256000 MODEL_MAX_TOKENS=128000 ;;\n glm-5|MiniMax-M2.7|MiniMax-M2.7-highspeed|MiniMax-M2.5)\n export MODEL_CONTEXT_WINDOW=200000 MODEL_MAX_TOKENS=128000 ;;\n *)\n export MODEL_CONTEXT_WINDOW=150000 MODEL_MAX_TOKENS=128000 ;;\nesac\nexport MODEL_REASONING=true\n\n# Override with user-supplied custom model parameters from env (set during install)\n[ -n \"${AGENTTEAMS_MODEL_CONTEXT_WINDOW:-}\" ] && export MODEL_CONTEXT_WINDOW=\"${AGENTTEAMS_MODEL_CONTEXT_WINDOW}\"\n[ -n \"${AGENTTEAMS_MODEL_MAX_TOKENS:-}\" ] && export MODEL_MAX_TOKENS=\"${AGENTTEAMS_MODEL_MAX_TOKENS}\"\n[ -n \"${AGENTTEAMS_MODEL_REASONING:-}\" ] && export MODEL_REASONING=\"${AGENTTEAMS_MODEL_REASONING}\"\n\n# E2EE: convert AGENTTEAMS_MATRIX_E2EE to JSON boolean for template substitution\nif [ \"${AGENTTEAMS_MATRIX_E2EE:-0}\" = \"1\" ] || [ \"${AGENTTEAMS_MATRIX_E2EE:-}\" = \"true\" ]; then\n export MATRIX_E2EE_ENABLED=true\nelse\n export MATRIX_E2EE_ENABLED=false\nfi\nlog \"Matrix E2EE: ${MATRIX_E2EE_ENABLED}\"\n\n# Resolve input modalities: only vision-capable models get \"image\"\ncase \"${MODEL_NAME}\" in\n gpt-5.4|gpt-5.3-codex|gpt-5-mini|gpt-5-nano|claude-opus-4-6|claude-sonnet-4-6|claude-haiku-4-5|qwen3.6-plus|qwen3.5-plus|kimi-k2.5)\n export MODEL_INPUT='[\"text\", \"image\"]' ;;\n *)\n export MODEL_INPUT='[\"text\"]' ;;\nesac\n# Override with user-supplied vision setting from env\nif [ \"${AGENTTEAMS_MODEL_VISION:-}\" = \"true\" ]; then\n export MODEL_INPUT='[\"text\", \"image\"]'\nelif [ \"${AGENTTEAMS_MODEL_VISION:-}\" = \"false\" ]; then\n export MODEL_INPUT='[\"text\"]'\nfi\n\nlog \"Model: ${MODEL_NAME} (context=${MODEL_CONTEXT_WINDOW}, maxTokens=${MODEL_MAX_TOKENS}, reasoning=${MODEL_REASONING}, input=${MODEL_INPUT})\"\n\nif [ -f /root/manager-workspace/openclaw.json ]; then\n log \"Manager openclaw.json already exists, updating dynamic fields only (preserving user customizations)...\"\n # Merge known models into existing config (add missing, preserve user-added)\n # Use known-models.json (valid JSON) instead of template (contains ${VAR} placeholders)\n KNOWN_MODELS=$(cat /opt/agentteams/configs/known-models.json 2>/dev/null || echo '[]')\n jq --arg token \"${MANAGER_TOKEN}\" \\\n --arg key \"${AGENTTEAMS_MANAGER_GATEWAY_KEY}\" \\\n --arg model \"${MODEL_NAME}\" \\\n --arg emb_model \"${AGENTTEAMS_EMBEDDING_MODEL}\" \\\n --arg aigw_domain \"${AI_GATEWAY_DOMAIN}\" \\\n --arg matrix_user_id \"@manager:${MATRIX_DOMAIN}\" \\\n --argjson e2ee \"${MATRIX_E2EE_ENABLED}\" \\\n --argjson known_models \"${KNOWN_MODELS}\" \\\n --argjson ctx \"${MODEL_CONTEXT_WINDOW}\" \\\n --argjson max \"${MODEL_MAX_TOKENS}\" \\\n --argjson reasoning \"${MODEL_REASONING}\" \\\n --argjson input \"${MODEL_INPUT}\" \\\n '\n # Merge known models: add any model id not already present\n .models.providers[\"agentteams-gateway\"].models as $existing\n | ($existing | map(.id)) as $existing_ids\n | ($known_models | map(select(.id as $id | $existing_ids | index($id) | not))) as $new\n | .models.providers[\"agentteams-gateway\"].models = ($existing + $new)\n # Ensure the user-chosen default model is in the list (custom model support)\n | if (.models.providers[\"agentteams-gateway\"].models | map(.id) | index($model) | not) then\n .models.providers[\"agentteams-gateway\"].models += [{\"id\": $model, \"name\": $model, \"reasoning\": $reasoning, \"contextWindow\": $ctx, \"maxTokens\": $max, \"input\": $input}]\n else . end\n # Rebuild model aliases from the full models list\n | (.models.providers[\"agentteams-gateway\"].models | map({ (\"agentteams-gateway/\" + .id): { \"alias\": .id } }) | add // {}) as $aliases\n | .agents.defaults.models = ((.agents.defaults.models // {}) + $aliases)\n | .channels.matrix.accessToken = $token | .channels.matrix.userId = $matrix_user_id | .models.providers[\"agentteams-gateway\"].apiKey = $key\n | ((.hooks.token // \"\") as $ht | if $ht == $key or $ht == ($key + \"-hooks\" | @base64) then del(.hooks) else . end)\n | .agents.defaults.model.primary = (\"agentteams-gateway/\" + $model)\n | .commands.restart = true\n | .gateway.port = 18799\n | .gateway.bind = \"lan\"\n | .gateway.controlUi = ((.gateway.controlUi // {}) + {\"dangerouslyDisableDeviceAuth\": true, \"allowInsecureAuth\": true, \"allowedOrigins\": [\"*\"]})\n | .channels.matrix.encryption = $e2ee\n | .channels.matrix.network = ((.channels.matrix.network // {}) + {\"dangerouslyAllowPrivateNetwork\": true})\n | .channels.matrix.autoJoin = \"always\"\n # OpenClaw YOLO defaults: host exec without approval prompts (see openclaw docs tools/exec-approvals)\n | .tools = (.tools // {})\n | .tools.exec = ((.tools.exec // {}) + {\"host\":\"gateway\",\"security\":\"full\",\"ask\":\"off\"})\n | .tools.elevated = (.tools.elevated // {})\n | .tools.elevated.enabled = true\n | .tools.elevated.allowFrom |= ((. // {}) | .matrix = [\"*\"])\n | .agents.defaults.elevatedDefault = \"full\"\n # Ensure memorySearch config exists (embedding model for memory) — skip if embedding model is empty\n | if $emb_model != \"\" then .agents.defaults.memorySearch //= {\"provider\":\"openai\",\"model\":$emb_model,\"remote\":{\"baseUrl\":(\"http://\" + $aigw_domain + \":8080/v1\"),\"apiKey\":$key}} else . end\n ' \\\n /root/manager-workspace/openclaw.json > /tmp/openclaw.json.tmp && \\\n mv /tmp/openclaw.json.tmp /root/manager-workspace/openclaw.json\n # Disable openclaw's observe-recovery mechanism which compares config against\n # a lastKnownGood baseline in config-health.json. When meta is missing from the\n # current file but present in the baseline, observe-recovery restores from .bak,\n # undoing user customizations (plugins, channels, etc).\n # Clearing config-health.json removes the baseline so observe-recovery won't\n # interfere, while preserving .bak as a backup.\n rm -f /root/manager-workspace/.openclaw/logs/config-health.json\n # Verify the token was written correctly\n _written_token=$(jq -r '.channels.matrix.accessToken' /root/manager-workspace/openclaw.json 2>/dev/null)\n if [ -z \"${_written_token}\" ] || [ \"${_written_token}\" = \"null\" ]; then\n log \"ERROR: Matrix token was not written correctly to openclaw.json (got: ${_written_token})\"\n else\n log \"Matrix token written to openclaw.json (prefix: ${_written_token:0:10}...)\"\n fi\nelse\n log \"Manager openclaw.json not found, generating from template...\"\n envsubst < /opt/agentteams/configs/manager-openclaw.json.tmpl > /root/manager-workspace/openclaw.json\n # Post-envsubst injection: memorySearch + custom model (single jq pass when possible)\n if ! jq -e --arg model \"${MODEL_NAME}\" '.models.providers[\"agentteams-gateway\"].models | map(.id) | index($model)' /root/manager-workspace/openclaw.json > /dev/null 2>&1; then\n log \"Custom model '${MODEL_NAME}' not in built-in list, injecting into config...\"\n jq --arg emb_model \"${AGENTTEAMS_EMBEDDING_MODEL}\" \\\n --arg aigw_domain \"${AI_GATEWAY_DOMAIN}\" \\\n --arg key \"${AGENTTEAMS_MANAGER_GATEWAY_KEY}\" \\\n --arg model \"${MODEL_NAME}\" \\\n --argjson ctx \"${MODEL_CONTEXT_WINDOW}\" \\\n --argjson max \"${MODEL_MAX_TOKENS}\" \\\n --argjson reasoning \"${MODEL_REASONING}\" \\\n --argjson input \"${MODEL_INPUT}\" \\\n '\n (if $emb_model != \"\" then .agents.defaults.memorySearch = {\"provider\":\"openai\",\"model\":$emb_model,\"remote\":{\"baseUrl\":(\"http://\" + $aigw_domain + \":8080/v1\"),\"apiKey\":$key}} else . end)\n | .models.providers[\"agentteams-gateway\"].models += [{\"id\": $model, \"name\": $model, \"reasoning\": $reasoning, \"contextWindow\": $ctx, \"maxTokens\": $max, \"input\": $input}]\n | .agents.defaults.models += {(\"agentteams-gateway/\" + $model): {\"alias\": $model}}\n ' /root/manager-workspace/openclaw.json > /tmp/openclaw.json.tmp && \\\n mv /tmp/openclaw.json.tmp /root/manager-workspace/openclaw.json\n elif [ -n \"${AGENTTEAMS_EMBEDDING_MODEL}\" ]; then\n jq --arg emb_model \"${AGENTTEAMS_EMBEDDING_MODEL}\" \\\n --arg aigw_domain \"${AI_GATEWAY_DOMAIN}\" \\\n --arg key \"${AGENTTEAMS_MANAGER_GATEWAY_KEY}\" \\\n '.agents.defaults.memorySearch = {\"provider\":\"openai\",\"model\":$emb_model,\"remote\":{\"baseUrl\":(\"http://\" + $aigw_domain + \":8080/v1\"),\"apiKey\":$key}}' \\\n /root/manager-workspace/openclaw.json > /tmp/openclaw.json.tmp && \\\n mv /tmp/openclaw.json.tmp /root/manager-workspace/openclaw.json\n fi\n _written_token=$(jq -r '.channels.matrix.accessToken' /root/manager-workspace/openclaw.json 2>/dev/null)\n log \"Matrix token written from template (prefix: ${_written_token:0:10}...)\"\nfi\n\n# Cloud/K8s mode: overlay cloud-specific settings onto generated config\nif [ \"${AGENTTEAMS_RUNTIME}\" = \"aliyun\" ] || [ \"${AGENTTEAMS_RUNTIME}\" = \"k8s\" ]; then\n log \"Applying cloud/k8s overlay to openclaw.json...\"\n jq --arg homeserver \"${AGENTTEAMS_MATRIX_URL}\" \\\n --arg gateway \"${AGENTTEAMS_AI_GATEWAY_URL}/v1\" \\\n --arg key \"${AGENTTEAMS_MANAGER_GATEWAY_KEY}\" \\\n '.channels.matrix.homeserver = $homeserver\n | .models.providers[\"agentteams-gateway\"].baseUrl = $gateway\n | .models.providers[\"agentteams-gateway\"].apiKey = $key\n | ((.hooks.token // \"\") as $ht | if $ht == $key or $ht == ($key + \"-hooks\" | @base64) then del(.hooks) else . end)\n | .commands.restart = false\n | if .agents.defaults.memorySearch then .agents.defaults.memorySearch.remote.baseUrl = $gateway | .agents.defaults.memorySearch.remote.apiKey = $key else . end' \\\n /root/manager-workspace/openclaw.json > /tmp/openclaw-cloud.json && \\\n mv /tmp/openclaw-cloud.json /root/manager-workspace/openclaw.json\n log \"Cloud/K8s overlay applied\"\nfi\n\n# ============================================================\n# Optional: enable openclaw-cms-plugin observability\n# Config is applied at runtime so secrets stay out of image layers.\n# ============================================================\nCMS_TRACES_ENABLED=\"$(echo \"${AGENTTEAMS_CMS_TRACES_ENABLED:-false}\" | tr '[:upper:]' '[:lower:]')\"\nif [ \"${CMS_TRACES_ENABLED}\" = \"true\" ]; then\n CMS_PLUGIN_NAME=\"openclaw-cms-plugin\"\n CMS_PLUGIN_DIR=\"${OPENCLAW_CMS_PLUGIN_DIR:-/opt/openclaw/extensions/openclaw-cms-plugin}\"\n CMS_PLUGIN_MANIFEST=\"${CMS_PLUGIN_DIR}/openclaw.plugin.json\"\n DIAG_PLUGIN_NAME=\"diagnostics-otel\"\n DIAG_PLUGIN_DIR=\"/opt/openclaw/extensions/diagnostics-otel\"\n CMS_LICENSE_KEY=\"${AGENTTEAMS_CMS_LICENSE_KEY:-}\"\n CMS_PROJECT=\"${AGENTTEAMS_CMS_PROJECT:-}\"\n CMS_METRICS_ENABLED=\"${AGENTTEAMS_CMS_METRICS_ENABLED:-false}\"\n\n if [ ! -f \"${CMS_PLUGIN_MANIFEST}\" ]; then\n log \"WARNING: ${CMS_PLUGIN_NAME} manifest not found at ${CMS_PLUGIN_MANIFEST}, skipping plugin config\"\n else\n _missing=0\n [ -z \"${AGENTTEAMS_CMS_ENDPOINT:-}\" ] && log \"WARNING: AGENTTEAMS_CMS_ENDPOINT is required when AGENTTEAMS_CMS_TRACES_ENABLED=true\" && _missing=1\n [ -z \"${CMS_LICENSE_KEY:-}\" ] && log \"WARNING: AGENTTEAMS_CMS_LICENSE_KEY is required when AGENTTEAMS_CMS_TRACES_ENABLED=true\" && _missing=1\n [ -z \"${AGENTTEAMS_CMS_WORKSPACE:-}\" ] && log \"WARNING: AGENTTEAMS_CMS_WORKSPACE is required when AGENTTEAMS_CMS_TRACES_ENABLED=true\" && _missing=1\n\n if [ \"${_missing}\" = \"0\" ]; then\n CMS_SERVICE_NAME=\"${AGENTTEAMS_CMS_SERVICE_NAME:-agentteams-manager}\"\n CMS_ENABLE_METRICS=\"${CMS_METRICS_ENABLED}\"\n DIAG_AVAILABLE=\"0\"\n _metrics_lc=\"$(echo \"${CMS_ENABLE_METRICS}\" | tr '[:upper:]' '[:lower:]')\"\n if [ \"${_metrics_lc}\" = \"true\" ]; then\n if [ -f \"${DIAG_PLUGIN_DIR}/package.json\" ]; then\n DIAG_AVAILABLE=\"1\"\n if [ ! -d \"${DIAG_PLUGIN_DIR}/node_modules\" ]; then\n log \"diagnostics-otel dependencies missing, installing...\"\n if (cd \"${DIAG_PLUGIN_DIR}\" && npm install --omit=dev --ignore-scripts >/tmp/agentteams-diag-install.log 2>&1); then\n log \"diagnostics-otel dependencies installed\"\n else\n log \"WARNING: diagnostics-otel npm install failed, metrics plugin may not load\"\n fi\n else\n log \"diagnostics-otel dependencies already present\"\n fi\n else\n log \"WARNING: diagnostics-otel package.json not found at ${DIAG_PLUGIN_DIR}, metrics plugin may not load\"\n fi\n fi\n\n log \"Applying ${CMS_PLUGIN_NAME} config to openclaw.json...\"\n jq --arg pluginName \"${CMS_PLUGIN_NAME}\" \\\n --arg pluginDir \"${CMS_PLUGIN_DIR}\" \\\n --arg endpoint \"${AGENTTEAMS_CMS_ENDPOINT}\" \\\n --arg licenseKey \"${CMS_LICENSE_KEY}\" \\\n --arg armsProject \"${CMS_PROJECT}\" \\\n --arg cmsWorkspace \"${AGENTTEAMS_CMS_WORKSPACE}\" \\\n --arg serviceName \"${CMS_SERVICE_NAME}\" \\\n --arg diagPluginName \"${DIAG_PLUGIN_NAME}\" \\\n --arg diagPluginDir \"${DIAG_PLUGIN_DIR}\" \\\n --arg metricsRaw \"${CMS_ENABLE_METRICS}\" \\\n --arg diagAvailableRaw \"${DIAG_AVAILABLE}\" \\\n '\n .plugins = (.plugins // {})\n | .plugins.load = (.plugins.load // {})\n | .plugins.entries = (.plugins.entries // {})\n | if (.plugins.allow | type) != \"array\" then .plugins.allow = [] else . end\n | if (.plugins.allow | index($pluginName)) == null then .plugins.allow += [$pluginName] else . end\n | if (.plugins.load.paths | type) != \"array\" then .plugins.load.paths = [] else . end\n | if (.plugins.load.paths | index($pluginDir)) == null then .plugins.load.paths += [$pluginDir] else . end\n | .plugins.entries[$pluginName] = {\n \"enabled\": true,\n \"config\": {\n \"endpoint\": $endpoint,\n \"headers\": {\n \"x-arms-license-key\": $licenseKey,\n \"x-arms-project\": $armsProject,\n \"x-cms-workspace\": $cmsWorkspace\n },\n \"serviceName\": $serviceName\n }\n }\n\n # diagnostics-otel metrics (optional)\n | ($metricsRaw | ascii_downcase) as $m\n | ($diagAvailableRaw == \"1\") as $diagAvailable\n | (($m == \"true\") and $diagAvailable) as $metricsEnabled\n | if $metricsEnabled then\n (if (.plugins.allow | index($diagPluginName)) == null then .plugins.allow += [$diagPluginName] else . end)\n | (if (.plugins.load.paths | index($diagPluginDir)) == null then .plugins.load.paths += [$diagPluginDir] else . end)\n | .plugins.entries[$diagPluginName].enabled = true\n | .diagnostics = (.diagnostics // {})\n | .diagnostics.otel = (.diagnostics.otel // {})\n | .diagnostics.enabled = true\n | .diagnostics.otel.enabled = true\n | .diagnostics.otel.endpoint = $endpoint\n | .diagnostics.otel.protocol = (.diagnostics.otel.protocol // \"http/protobuf\")\n | .diagnostics.otel.headers = {\n \"x-arms-license-key\": $licenseKey,\n \"x-arms-project\": $armsProject,\n \"x-cms-workspace\": $cmsWorkspace\n }\n | .diagnostics.otel.serviceName = $serviceName\n | .diagnostics.otel.metrics = true\n | .diagnostics.otel.traces = (.diagnostics.otel.traces // false)\n | .diagnostics.otel.logs = (.diagnostics.otel.logs // false)\n else\n .\n end\n ' /root/manager-workspace/openclaw.json > /tmp/openclaw-cms.json && \\\n mv /tmp/openclaw-cms.json /root/manager-workspace/openclaw.json\n log \"${CMS_PLUGIN_NAME} config applied (metrics=${CMS_ENABLE_METRICS}, service=${CMS_SERVICE_NAME})\"\n else\n log \"Skipping ${CMS_PLUGIN_NAME} config due to missing required env vars\"\n fi\n fi\nfi\n\n# ============================================================\n# Detect container runtime (for Worker creation)\n# ============================================================\nsource /opt/agentteams/scripts/lib/container-api.sh\nif container_api_available; then\n log \"Container runtime socket detected at ${CONTAINER_SOCKET} — direct Worker creation enabled\"\n export AGENTTEAMS_CONTAINER_RUNTIME=\"socket\"\nelif [ \"${AGENTTEAMS_RUNTIME}\" = \"aliyun\" ] || [ \"${AGENTTEAMS_RUNTIME}\" = \"k8s\" ]; then\n log \"Cloud/K8s mode — Workers created via controller API\"\n export AGENTTEAMS_CONTAINER_RUNTIME=\"cloud\"\nelse\n log \"No container runtime found — Worker creation will output install commands\"\n export AGENTTEAMS_CONTAINER_RUNTIME=\"none\"\nfi\n\n# ============================================================\n# Upgrade Worker openclaw.json: merge known models + E2EE flag into existing configs\n# Existing workers in MinIO may have old single-model configs or missing encryption field.\n# Merge template models so they can hot-switch without restart.\n# ============================================================\nREGISTRY_FILE=\"/root/manager-workspace/workers-registry.json\"\nif [ -f \"${REGISTRY_FILE}\" ]; then\n # Use known-models.json (valid JSON) instead of template (contains ${VAR} placeholders)\n KNOWN_MODELS_FILE=\"/opt/agentteams/configs/known-models.json\"\n if [ -f \"${KNOWN_MODELS_FILE}\" ]; then\n _KNOWN_MODELS=$(cat \"${KNOWN_MODELS_FILE}\")\n for _wname in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_wname}\" ] && continue\n _minio_path=\"${AGENTTEAMS_STORAGE_PREFIX}/agents/${_wname}/openclaw.json\"\n _tmp_in=\"/tmp/openclaw-${_wname}-models-upgrade-in.json\"\n if mc cp \"${_minio_path}\" \"${_tmp_in}\" 2>/dev/null; then\n _tmp_out=\"/tmp/openclaw-${_wname}-models-upgrade-out.json\"\n # Idempotent merge: add missing known models, rebuild aliases, set e2ee.\n # Always runs — jq deduplicates by model id, so re-runs are safe.\n jq --argjson known_models \"${_KNOWN_MODELS}\" \\\n --argjson e2ee \"${MATRIX_E2EE_ENABLED}\" '\n .models.providers[\"agentteams-gateway\"].models as $existing\n | ($existing | map(.id)) as $existing_ids\n | ($known_models | map(select(.id as $id | $existing_ids | index($id) | not))) as $new\n | .models.providers[\"agentteams-gateway\"].models = ($existing + $new)\n | (.models.providers[\"agentteams-gateway\"].models | map({ (\"agentteams-gateway/\" + .id): { \"alias\": .id } }) | add // {}) as $aliases\n | .agents.defaults.models = ((.agents.defaults.models // {}) + $aliases)\n | .channels.matrix.encryption = $e2ee\n | .channels.matrix.autoJoin = \"always\"\n | .tools = (.tools // {})\n | .tools.exec = ((.tools.exec // {}) + {\"host\":\"gateway\",\"security\":\"full\",\"ask\":\"off\"})\n | .tools.elevated = (.tools.elevated // {})\n | .tools.elevated.enabled = true\n | .tools.elevated.allowFrom |= ((. // {}) | .matrix = [\"*\"])\n | .agents.defaults.elevatedDefault = \"full\"\n ' \"${_tmp_in}\" > \"${_tmp_out}\" 2>/dev/null\n if ! diff -q \"${_tmp_in}\" \"${_tmp_out}\" > /dev/null 2>&1; then\n if mc cp \"${_tmp_out}\" \"${_minio_path}\" 2>/dev/null; then\n _new_count=$(jq '.models.providers[\"agentteams-gateway\"].models | length' \"${_tmp_out}\" 2>/dev/null)\n log \"Worker ${_wname}: upgraded openclaw.json (models: ${_new_count}, e2ee: ${MATRIX_E2EE_ENABLED})\"\n fi\n fi\n rm -f \"${_tmp_in}\" \"${_tmp_out}\"\n fi\n done\n fi\nfi\n\n# ============================================================\n# Ensure Worker Matrix password files exist in MinIO (E2EE fix)\n# Workers need to re-login on restart to get a fresh device_id.\n# Older workers created before this fix won't have the password file.\n# ============================================================\nif [ -f \"${REGISTRY_FILE}\" ]; then\n for _wname in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_wname}\" ] && continue\n _creds_file=\"/data/worker-creds/${_wname}.env\"\n if [ -f \"${_creds_file}\" ]; then\n # Check if password file already exists in MinIO\n if ! mc stat \"${AGENTTEAMS_STORAGE_PREFIX}/agents/${_wname}/credentials/matrix/password\" > /dev/null 2>&1; then\n source \"${_creds_file}\"\n if [ -n \"${WORKER_PASSWORD}\" ]; then\n _tmp_pw=\"/tmp/matrix-pw-${_wname}\"\n echo -n \"${WORKER_PASSWORD}\" > \"${_tmp_pw}\"\n mc cp \"${_tmp_pw}\" \"${AGENTTEAMS_STORAGE_PREFIX}/agents/${_wname}/credentials/matrix/password\" 2>/dev/null \\\n && log \"Worker ${_wname}: wrote Matrix password to MinIO (E2EE re-login fix)\" \\\n || log \"Worker ${_wname}: WARNING: failed to write Matrix password to MinIO\"\n rm -f \"${_tmp_pw}\"\n fi\n fi\n fi\n done\nfi\n\n# ============================================================\n# Recreate Worker containers as needed after Manager restart.\n# Workers are on agentteams-net; Docker DNS resolves *-local.agentteams.io via\n# the Manager's network aliases, so IP changes don't require worker recreation.\n# Only recreate stopped/missing workers.\n# ============================================================\nif container_api_available; then\n REGISTRY_FILE=\"/root/manager-workspace/workers-registry.json\"\n if [ -f \"${REGISTRY_FILE}\" ]; then\n for _worker_name in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_worker_name}\" ] && continue\n\n # Skip remote workers — they are not Manager-managed containers.\n _deployment=$(jq -r --arg w \"${_worker_name}\" '.workers[$w].deployment // \"local\"' \"${REGISTRY_FILE}\" 2>/dev/null)\n if [ \"${_deployment}\" = \"remote\" ]; then\n log \"Worker ${_worker_name} is remote, skipping container recreate\"\n continue\n fi\n\n _status=$(container_status_worker \"${_worker_name}\")\n if [ \"${_status}\" = \"running\" ]; then\n log \"Worker running: ${_worker_name}, skipping\"\n continue\n fi\n # Container missing or stopped — recreate.\n log \"Worker container ${_status}: ${_worker_name}, recreating...\"\n _creds_file=\"/data/worker-creds/${_worker_name}.env\"\n if [ -f \"${_creds_file}\" ]; then\n source \"${_creds_file}\"\n _runtime=$(jq -r --arg w \"${_worker_name}\" '.workers[$w].runtime // \"openclaw\"' \"${REGISTRY_FILE}\" 2>/dev/null)\n _recreated=false\n for _attempt in 1 2 3; do\n local _env_map _create_body\n _env_map=$(jq -cn \\\n --arg name \"${_worker_name}\" \\\n --arg fak \"${_worker_name}\" \\\n --arg fsk \"${WORKER_MINIO_PASSWORD:-}\" \\\n --arg fs_domain \"${AGENTTEAMS_FS_DOMAIN:-fs-local.agentteams.io}\" \\\n --arg controller_url \"${AGENTTEAMS_CONTROLLER_URL:-}\" \\\n '{\n \"AGENTTEAMS_WORKER_NAME\": $name,\n \"AGENTTEAMS_FS_ENDPOINT\": (\"http://\" + ($fs_domain | split(\":\")[0]) + \":9000\"),\n \"AGENTTEAMS_FS_ACCESS_KEY\": $fak,\n \"AGENTTEAMS_FS_SECRET_KEY\": $fsk\n }\n | if $controller_url != \"\" then . + {\"AGENTTEAMS_CONTROLLER_URL\": $controller_url} else . end')\n _create_body=$(jq -cn --arg name \"${_worker_name}\" --arg runtime \"${_runtime}\" --argjson env \"${_env_map}\" '{name: $name, runtime: $runtime, env: $env}')\n worker_backend_create \"${_create_body}\" > /dev/null 2>&1 && _recreated=true && break\n log \" Attempt ${_attempt}/3 failed for ${_worker_name}, retrying in $((5 * _attempt))s...\"\n sleep $((5 * _attempt))\n done\n if [ \"${_recreated}\" = true ]; then\n log \" Recreated ${_runtime} worker: ${_worker_name}\"\n else\n log \" ERROR: Failed to recreate ${_worker_name} after 3 attempts\"\n fi\n else\n log \" WARNING: No credentials found for ${_worker_name} (${_creds_file} missing), skipping\"\n fi\n done\n fi\nfi\n\n# ============================================================\n# Notify workers of builtin updates if upgrade happened\n# Builtin files (AGENTS.md, skills) are already synced by upgrade-builtins.sh\n#\n# Cooldown: skip notification if the last successful notify was within\n# NOTIFY_COOLDOWN_SECS (default 3600s / 1 hour). This prevents repeated\n# notifications when the Manager crash-loops and re-runs upgrade-builtins\n# on every restart (e.g. IMAGE_VERSION=latest always triggers upgrade).\n# ============================================================\nNOTIFY_COOLDOWN_SECS=\"${AGENTTEAMS_NOTIFY_COOLDOWN_SECS:-3600}\"\nNOTIFY_TS_FILE=\"/root/manager-workspace/.last-worker-notify-ts\"\n\nif [ -f /root/manager-workspace/.upgrade-pending-worker-notify ]; then\n _now=$(date +%s)\n _last_notify=$(cat \"${NOTIFY_TS_FILE}\" 2>/dev/null || echo \"0\")\n _elapsed=$(( _now - _last_notify ))\n\n if [ \"${_elapsed}\" -lt \"${NOTIFY_COOLDOWN_SECS}\" ]; then\n log \"Skipping worker builtin notification (last notify ${_elapsed}s ago, cooldown ${NOTIFY_COOLDOWN_SECS}s)\"\n rm -f /root/manager-workspace/.upgrade-pending-worker-notify\n else\n log \"Notifying workers about builtin updates...\"\n REGISTRY_FILE=\"/root/manager-workspace/workers-registry.json\"\n _notify_ok=false\n if [ -f \"${REGISTRY_FILE}\" ]; then\n for _worker_name in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_worker_name}\" ] && continue\n _room_id=$(jq -r --arg w \"${_worker_name}\" '.workers[$w].room_id // empty' \"${REGISTRY_FILE}\" 2>/dev/null)\n if [ -n \"${_room_id}\" ]; then\n _worker_id=\"@${_worker_name}:${MATRIX_DOMAIN}\"\n _txn_id=\"upgrade-$(date +%s%N)\"\n _msg=\"@${_worker_name}:${MATRIX_DOMAIN} Manager upgraded builtin files (AGENTS.md, skills). Please use your file-sync skill to sync the latest config.\"\n _raw=$(curl -s -w '\\nHTTP_CODE:%{http_code}' -X PUT \\\n \"${AGENTTEAMS_MATRIX_URL}/_matrix/client/v3/rooms/${_room_id}/send/m.room.message/${_txn_id}\" \\\n -H \"Authorization: Bearer ${MANAGER_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d \"{\\\"msgtype\\\":\\\"m.text\\\",\\\"body\\\":\\\"${_msg}\\\",\\\"m.mentions\\\":{\\\"user_ids\\\":[\\\"${_worker_id}\\\"]}}\" \\\n 2>&1) || true\n _http_code=$(echo \"${_raw}\" | tail -1 | sed 's/HTTP_CODE://')\n _notify_resp=$(echo \"${_raw}\" | sed '$d')\n if echo \"${_notify_resp}\" | jq -e '.event_id' > /dev/null 2>&1; then\n log \" Notified ${_worker_name}\"; _notify_ok=true\n else\n log \" WARNING: Failed to notify ${_worker_name} (HTTP ${_http_code}): ${_notify_resp}\"\n fi\n fi\n done\n fi\n # Record timestamp only if at least one notification succeeded\n if [ \"${_notify_ok}\" = true ]; then\n echo \"${_now}\" > \"${NOTIFY_TS_FILE}\"\n fi\n rm -f /root/manager-workspace/.upgrade-pending-worker-notify\n fi\nfi\n\n# ============================================================\n# Start Manager Agent\n# ============================================================\nlog \"Starting Manager Agent (${MANAGER_RUNTIME})...\"\n\n# HOME is already set to /root/manager-workspace via docker run -e HOME=...\ncd \"${HOME}\"\n\n# Ensure host credential symlinks exist under HOME\nif [ -d \"/host-share\" ]; then\n [ -f \"/host-share/.gitconfig\" ] && ln -sf \"/host-share/.gitconfig\" \"${HOME}/.gitconfig\"\nfi\n\nlog \"HOME=${HOME} (manager-workspace, host-mounted)\"\n\n# ── Render agent doc templates ────────────────────────────────────────────\n# Replace ${VAR} placeholders with actual values so the AI agent reads\n# plain text and never needs to resolve environment variables.\nexport MANAGER_MATRIX_TOKEN MANAGER_TOKEN HIGRESS_COOKIE_FILE\nRENDER=/opt/agentteams/scripts/lib/render-skills.sh\nlog \"Rendering agent doc templates...\"\n# Manager-owned docs (workspace)\nbash \"$RENDER\" /root/manager-workspace/skills\nbash \"$RENDER\" /root/manager-workspace/skills-alpha\nbash \"$RENDER\" /root/manager-workspace AGENTS.md TOOLS.md HEARTBEAT.md SOUL.md\n# Worker templates (workspace + image) — rendered before push to MinIO\n# so Workers (including remote pip-install) receive plain text\nbash \"$RENDER\" /root/manager-workspace/worker-skills\nbash \"$RENDER\" /root/manager-workspace/worker-agent\nbash \"$RENDER\" /root/manager-workspace/copaw-worker-agent\nbash \"$RENDER\" /root/manager-workspace/hermes-worker-agent\nbash \"$RENDER\" /opt/agentteams/agent/worker-skills\nbash \"$RENDER\" /opt/agentteams/agent/worker-agent\nbash \"$RENDER\" /opt/agentteams/agent/copaw-worker-agent\nbash \"$RENDER\" /opt/agentteams/agent/hermes-worker-agent\nlog \"Agent doc templates rendered\"\n\n# Cloud mode: start background file sync (workspace ↔ OSS) and initial push\nif [ \"${AGENTTEAMS_RUNTIME}\" = \"aliyun\" ]; then\n log \"Syncing initial workspace to OSS...\"\n ensure_mc_credentials\n mc mirror /root/manager-workspace/ \"${AGENTTEAMS_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" 2>/dev/null || true\n\n # Local → OSS: change-triggered sync\n (\n while true; do\n CHANGED=$(find /root/manager-workspace/ -type f -newermt \"15 seconds ago\" 2>/dev/null | head -1)\n if [ -n \"${CHANGED}\" ]; then\n ensure_mc_credentials 2>/dev/null || true\n mc mirror /root/manager-workspace/ \"${AGENTTEAMS_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" --exclude \".npm/**\" \\\n --exclude \".local/**\" --exclude \".mc/**\" 2>/dev/null || true\n fi\n sleep 10\n done\n ) &\n log \"Local→OSS sync started (PID: $!)\"\n\n # OSS → Local: periodic pull (shared data, agent configs, manager openclaw.json)\n (\n while true; do\n sleep 60\n ensure_mc_credentials 2>/dev/null || true\n mc mirror \"${AGENTTEAMS_STORAGE_PREFIX}/shared/\" /root/agentteams-fs/shared/ --overwrite --newer-than \"1m\" 2>/dev/null || true\n mc mirror \"${AGENTTEAMS_STORAGE_PREFIX}/agents/\" /root/agentteams-fs/agents/ --overwrite --newer-than \"1m\" 2>/dev/null || true\n mc cp \"${AGENTTEAMS_STORAGE_PREFIX}/manager/openclaw.json\" /root/manager-workspace/openclaw.json 2>/dev/null || true\n done\n ) &\n log \"OSS→Local sync started (every 60s, PID: $!)\"\nfi\n\n# K8s mode: start background file sync (workspace ↔ MinIO)\nif [ \"${AGENTTEAMS_RUNTIME}\" = \"k8s\" ]; then\n log \"Syncing initial workspace to MinIO...\"\n mc mirror /root/manager-workspace/ \"${AGENTTEAMS_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" 2>/dev/null || true\n\n # Local → MinIO: change-triggered sync\n (\n while true; do\n CHANGED=$(find /root/manager-workspace/ -type f -newermt \"15 seconds ago\" 2>/dev/null | head -1)\n if [ -n \"${CHANGED}\" ]; then\n mc mirror /root/manager-workspace/ \"${AGENTTEAMS_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" --exclude \".npm/**\" \\\n --exclude \".local/**\" --exclude \".mc/**\" 2>/dev/null || true\n fi\n sleep 10\n done\n ) &\n log \"Local→MinIO sync started (PID: $!)\"\n\n # MinIO → Local: periodic pull (shared data, agent configs, manager openclaw.json)\n (\n while true; do\n sleep 60\n mc mirror \"${AGENTTEAMS_STORAGE_PREFIX}/shared/\" /root/agentteams-fs/shared/ --overwrite --newer-than \"1m\" 2>/dev/null || true\n mc mirror \"${AGENTTEAMS_STORAGE_PREFIX}/agents/\" /root/agentteams-fs/agents/ --overwrite --newer-than \"1m\" 2>/dev/null || true\n mc mirror \"${AGENTTEAMS_STORAGE_PREFIX}/agentteams-config/\" /root/agentteams-fs/agentteams-config/ --overwrite --newer-than \"1m\" 2>/dev/null || true\n mc cp \"${AGENTTEAMS_STORAGE_PREFIX}/manager/openclaw.json\" /root/manager-workspace/openclaw.json 2>/dev/null || true\n done\n ) &\n log \"MinIO→Local sync started (every 60s, PID: $!)\"\nfi\n\n# ============================================================\n# Auto-generate Manager mcporter config for pre-configured MCP servers\n# If AGENTTEAMS_GITHUB_TOKEN was set at install time, setup-higress.sh already\n# configured GitHub MCP on Higress. Run setup-mcp-server.sh now so that\n# config/mcporter.json exists when the Agent starts — no need to ask user for PAT.\n# ============================================================\nif [ -n \"${AGENTTEAMS_GITHUB_TOKEN}\" ] && [ \"${AGENTTEAMS_RUNTIME}\" != \"aliyun\" ] && [ \"${AGENTTEAMS_RUNTIME}\" != \"k8s\" ]; then\n if [ ! -f \"${HOME}/config/mcporter.json\" ]; then\n log \"Auto-generating Manager mcporter config for GitHub MCP (AGENTTEAMS_GITHUB_TOKEN set)...\"\n bash /opt/agentteams/agent/skills/mcp-server-management/scripts/setup-mcp-server.sh \\\n github \"${AGENTTEAMS_GITHUB_TOKEN}\" 2>&1 | while IFS= read -r line; do log \" [setup-mcp] ${line}\"; done || \\\n log \"WARNING: setup-mcp-server.sh failed — Agent may need to configure GitHub MCP manually\"\n else\n log \"Manager mcporter config already exists, skipping auto-generate\"\n fi\nfi\n\n# ============================================================\n# Runtime-specific startup\n# ============================================================\nif [ \"${MANAGER_RUNTIME}\" = \"copaw\" ]; then\n # Delegate to CoPaw startup script\n exec /opt/agentteams/scripts/init/start-copaw-manager.sh\nelse\n # ── OpenClaw Runtime ─────────────────────────────────────────────────────\n log \"Starting OpenClaw Manager...\"\n\n export OPENCLAW_CONFIG_PATH=\"/root/manager-workspace/openclaw.json\"\n\n # Symlink to default OpenClaw config path so CLI commands find the config\n mkdir -p \"${HOME}/.openclaw\"\n ln -sf \"/root/manager-workspace/openclaw.json\" \"${HOME}/.openclaw/openclaw.json\"\n\n # Clean orphaned session write locks (e.g. from SIGKILL or crash before exit handlers)\n # Prevents \"session file locked (timeout 10000ms)\" when PID was reused\n find \"${HOME}/.openclaw/agents\" -name \"*.jsonl.lock\" -delete 2>/dev/null || true\n log \"Cleaned up any orphaned session write locks\"\n\n # Clean Matrix crypto storage (SQLite WAL may be corrupted after unclean shutdown)\n # Crypto state is re-negotiated on startup; losing it only means re-establishing E2EE sessions\n rm -rf \"${HOME}/.openclaw/matrix\" 2>/dev/null || true\n log \"Cleaned Matrix crypto storage (will re-establish E2EE sessions)\"\n\n # Launch OpenClaw\n # Disable full-process respawn so the CLI uses its internal restart loop.\n # Without this, config reload spawns a detached child and exits, then\n # supervisord restarts the CLI — resulting in two gateway processes.\n export OPENCLAW_NO_RESPAWN=1\n\n # Optional matrix-plugin trace logging — when AGENTTEAMS_MATRIX_DEBUG=1 is set\n # in the manager environment (propagated by install / supervisord), turn on\n # OPENCLAW_MATRIX_DEBUG so the matrix plugin emits structured INFO-level\n # lifecycle traces (sync.state transitions, room.invite/join, message\n # handler arrival + filter outcomes). Useful for diagnosing \"worker never\n # joined the room\" / \"manager never replied\" hangs without rebuilding the\n # image.\n if [ \"${AGENTTEAMS_MATRIX_DEBUG:-}\" = \"1\" ] && [ -z \"${OPENCLAW_MATRIX_DEBUG:-}\" ]; then\n export OPENCLAW_MATRIX_DEBUG=1\n log \"AGENTTEAMS_MATRIX_DEBUG=1 detected; OPENCLAW_MATRIX_DEBUG=1 exported for matrix plugin tracing\"\n fi\n\n exec openclaw gateway run --verbose --force\nfi\n"}, "e6fa64c0cb7b14600c6abbf9b4a25f47bf544c1f": {"t": 1785764735.3185432, "n": 69782, "h": "6cc0eb54d8a462f6", "c": "#!/bin/bash\n# start-manager-agent.sh - Initialize and start the Manager Agent\n# Supports local (supervisord), cloud (SAE), and K8s (Helm) deployments.\n# In local mode this is the last supervisord component to start (priority 800).\n# In cloud/k8s mode (AGENTTEAMS_RUNTIME=aliyun|k8s) this is the container entrypoint.\n#\n# Runtime selection:\n# AGENTTEAMS_MANAGER_RUNTIME=openclaw (default) - OpenClaw gateway mode\n# AGENTTEAMS_MANAGER_RUNTIME=copaw - CoPaw workspace mode\n# (hermes runtime is supported for Workers only; Managers run openclaw or copaw.)\n\nsource /opt/agentteams/scripts/lib/agentteams-env.sh\n\n# ============================================================\n# Runtime selection\n# ============================================================\nMANAGER_RUNTIME=\"${AGENTTEAMS_MANAGER_RUNTIME:-openclaw}\"\ncase \"${MANAGER_RUNTIME}\" in\n copaw)\n log \"Manager runtime: CoPaw (Python workspace)\"\n ;;\n *)\n log \"Manager runtime: OpenClaw (Node.js gateway)\"\n MANAGER_RUNTIME=\"openclaw\"\n ;;\nesac\n\n# ============================================================\n# Set timezone from TZ env var\n# ============================================================\nif [ -n \"${TZ}\" ] && [ -f \"/usr/share/zoneinfo/${TZ}\" ]; then\n ln -sf \"/usr/share/zoneinfo/${TZ}\" /etc/localtime\n echo \"${TZ}\" > /etc/timezone\n log \"Timezone set to ${TZ}\"\nfi\n\nexport MATRIX_DOMAIN=\"${AGENTTEAMS_MATRIX_DOMAIN:-matrix-local.agentteams.io:8080}\"\nAI_GATEWAY_DOMAIN=\"${AGENTTEAMS_AI_GATEWAY_DOMAIN:-aigw-local.agentteams.io}\"\n\n# ============================================================\n# YOLO mode promotion\n# ============================================================\n# In embedded mode the controller does not propagate AGENTTEAMS_YOLO to the\n# manager container, but installer / test scripts touch a marker file at\n# `${WORKSPACE}/yolo-mode` instead. Promote that marker to the env var so the\n# agent's documented YOLO check (`AGENTTEAMS_YOLO=1`) reliably detects it without\n# depending on filesystem lookups during a turn.\nif [ -z \"${AGENTTEAMS_YOLO:-}\" ] && [ -f /root/manager-workspace/yolo-mode ]; then\n export AGENTTEAMS_YOLO=1\n log \"YOLO mode marker detected at /root/manager-workspace/yolo-mode; AGENTTEAMS_YOLO=1 exported\"\nfi\n\n# ============================================================\n# Cloud/K8s mode: validate required environment variables + initial credentials\n# ============================================================\nif [ \"${AGENTTEAMS_RUNTIME}\" = \"aliyun\" ] || [ \"${AGENTTEAMS_RUNTIME}\" = \"k8s\" ]; then\n : \"${AGENTTEAMS_MATRIX_URL:?AGENTTEAMS_MATRIX_URL is required}\"\n : \"${AGENTTEAMS_MATRIX_DOMAIN:?AGENTTEAMS_MATRIX_DOMAIN is required}\"\n : \"${AGENTTEAMS_AI_GATEWAY_URL:?AGENTTEAMS_AI_GATEWAY_URL is required}\"\n if [ \"${AGENTTEAMS_RUNTIME}\" = \"aliyun\" ]; then\n : \"${AGENTTEAMS_MANAGER_GATEWAY_KEY:?AGENTTEAMS_MANAGER_GATEWAY_KEY is required}\"\n : \"${AGENTTEAMS_MANAGER_PASSWORD:?AGENTTEAMS_MANAGER_PASSWORD is required (cloud containers are stateless, password must be injected)}\"\n fi\n if [ \"${AGENTTEAMS_RUNTIME}\" = \"k8s\" ]; then\n # K8s mode: controller handles initialization (admin registration, Higress setup).\n # Manager only needs credentials injected by the ManagerReconciler.\n : \"${AGENTTEAMS_MANAGER_GATEWAY_KEY:?AGENTTEAMS_MANAGER_GATEWAY_KEY is required (injected by controller)}\"\n # AGENTTEAMS_MANAGER_PASSWORD is optional: not needed in AppService mode\n # (token obtained via AS login), only required in legacy password mode.\n else\n # Cloud (aliyun) mode: Manager still does its own initialization\n : \"${AGENTTEAMS_REGISTRATION_TOKEN:?AGENTTEAMS_REGISTRATION_TOKEN is required}\"\n : \"${AGENTTEAMS_ADMIN_USER:?AGENTTEAMS_ADMIN_USER is required}\"\n : \"${AGENTTEAMS_ADMIN_PASSWORD:?AGENTTEAMS_ADMIN_PASSWORD is required}\"\n fi\n log \"${AGENTTEAMS_RUNTIME} mode: validating environment... OK\"\n log \" Matrix: ${AGENTTEAMS_MATRIX_URL}, AI Gateway: ${AGENTTEAMS_AI_GATEWAY_URL}, Storage: ${AGENTTEAMS_FS_BUCKET}\"\n if [ \"${AGENTTEAMS_RUNTIME}\" = \"aliyun\" ]; then\n ensure_mc_credentials || { log \"FATAL: Initial STS credential fetch failed\"; exit 1; }\n fi\nfi\n\n# ============================================================\n# Local mode: host symlinks, /etc/hosts, wait for local services\n# ============================================================\nif [ \"${AGENTTEAMS_RUNTIME}\" != \"aliyun\" ] && [ \"${AGENTTEAMS_RUNTIME}\" != \"k8s\" ]; then\n # Create symlink for host directory access\n if [ -d \"/host-share\" ]; then\n ORIGINAL_HOST_HOME=\"${HOST_ORIGINAL_HOME:-$HOME}\"\n if [ ! -e \"${ORIGINAL_HOST_HOME}\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/root\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/data\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/host-share\" ]; then\n mkdir -p \"$(dirname \"${ORIGINAL_HOST_HOME}\")\"\n ln -sfn /host-share \"${ORIGINAL_HOST_HOME}\"\n log \"Created symlink: ${ORIGINAL_HOST_HOME} -> /host-share\"\n else\n ln -sfn /host-share /root/host-home\n log \"Created fallback symlink: /root/host-home -> /host-share\"\n fi\n fi\n\n # Add local domains to /etc/hosts\n HOSTS_DOMAINS=\"${MATRIX_DOMAIN%%:*} ${AGENTTEAMS_MATRIX_CLIENT_DOMAIN:-matrix-client-local.agentteams.io} ${AI_GATEWAY_DOMAIN} ${AGENTTEAMS_FS_DOMAIN:-fs-local.agentteams.io}\"\n if ! grep -q \"${AI_GATEWAY_DOMAIN}\" /etc/hosts 2>/dev/null; then\n echo \"127.0.0.1 ${HOSTS_DOMAINS}\" >> /etc/hosts\n log \"Added local domains to /etc/hosts\"\n fi\n\n # Wait for local infrastructure\n waitForService \"Higress Gateway\" \"127.0.0.1\" 8080 180\n waitForService \"Higress Console\" \"127.0.0.1\" 8001 180\n waitForService \"Tuwunel\" \"127.0.0.1\" 6167 120\n waitForHTTP \"Tuwunel Matrix API\" \"${AGENTTEAMS_MATRIX_URL}/_tuwunel/server_version\" 120\n waitForService \"MinIO\" \"127.0.0.1\" 9000 120\nelse\n # Cloud/K8s mode: wait for external Tuwunel\n log \"Waiting for Tuwunel Matrix server at ${AGENTTEAMS_MATRIX_URL}...\"\n _retry=0\n while [ \"${_retry}\" -lt 30 ]; do\n if curl -sf \"${AGENTTEAMS_MATRIX_URL}/_matrix/client/versions\" > /dev/null 2>&1; then\n log \"Tuwunel is ready\"\n break\n fi\n _retry=$((_retry + 1))\n log \" Waiting for Tuwunel (attempt ${_retry}/30)...\"\n sleep 5\n done\n if [ \"${_retry}\" -ge 30 ]; then\n log \"ERROR: Tuwunel not reachable at ${AGENTTEAMS_MATRIX_URL}\"\n exit 1\n fi\nfi\n\n# ============================================================\n# Auto-generate secrets if not provided via environment\n# Persisted to /data so they survive container restart\n# ============================================================\nSECRETS_FILE=\"/data/agentteams-secrets.env\"\nif [ -f \"${SECRETS_FILE}\" ]; then\n source \"${SECRETS_FILE}\"\n log \"Loaded persisted secrets from ${SECRETS_FILE}\"\nfi\n\nif [ -z \"${AGENTTEAMS_MANAGER_GATEWAY_KEY}\" ]; then\n export AGENTTEAMS_MANAGER_GATEWAY_KEY=\"$(generateKey 32)\"\n log \"Auto-generated AGENTTEAMS_MANAGER_GATEWAY_KEY\"\nfi\nif [ -z \"${AGENTTEAMS_MANAGER_PASSWORD}\" ]; then\n export AGENTTEAMS_MANAGER_PASSWORD=\"$(generateKey 16)\"\n log \"Auto-generated AGENTTEAMS_MANAGER_PASSWORD\"\nfi\n\n# Persist secrets so they survive supervisord restart\nmkdir -p /data\ncat > \"${SECRETS_FILE}\" <<EOF\nexport AGENTTEAMS_MANAGER_GATEWAY_KEY=\"${AGENTTEAMS_MANAGER_GATEWAY_KEY}\"\nexport AGENTTEAMS_MANAGER_PASSWORD=\"${AGENTTEAMS_MANAGER_PASSWORD}\"\nEOF\nchmod 600 \"${SECRETS_FILE}\"\n\n# Cloud mode: pull workspace from OSS before initialization\nif [ \"${AGENTTEAMS_RUNTIME}\" = \"aliyun\" ]; then\n AGENTTEAMS_FS=\"/root/agentteams-fs\"\n mkdir -p \"${AGENTTEAMS_FS}/shared\" \"${AGENTTEAMS_FS}/agents\"\n log \"Pulling workspace from OSS...\"\n ensure_mc_credentials\n mc mirror \"${AGENTTEAMS_STORAGE_PREFIX}/manager/\" /root/manager-workspace/ --overwrite 2>/dev/null || true\n mc mirror \"${AGENTTEAMS_STORAGE_PREFIX}/shared/\" \"${AGENTTEAMS_FS}/shared/\" --overwrite 2>/dev/null || true\n mc mirror \"${AGENTTEAMS_STORAGE_PREFIX}/agents/\" \"${AGENTTEAMS_FS}/agents/\" --overwrite 2>/dev/null || true\n # Keep the canonical filesystem link in the Manager workspace.\n ln -sfn \"${AGENTTEAMS_FS}\" /root/manager-workspace/agentteams-fs\n ln -sfn \"${AGENTTEAMS_FS}\" /root/manager-workspace/hiclaw-fs\nfi\n\n# K8s mode: sync workspace from cluster-internal MinIO\nif [ \"${AGENTTEAMS_RUNTIME}\" = \"k8s\" ]; then\n AGENTTEAMS_FS=\"/root/agentteams-fs\"\n mkdir -p \"${AGENTTEAMS_FS}/shared\" \"${AGENTTEAMS_FS}/agents\" \"${AGENTTEAMS_FS}/agentteams-config\"\n log \"Configuring mc alias for cluster MinIO...\"\n mc alias set agentteams \"${AGENTTEAMS_FS_ENDPOINT}\" \"${AGENTTEAMS_FS_ACCESS_KEY}\" \"${AGENTTEAMS_FS_SECRET_KEY}\"\n log \"Syncing workspace from MinIO...\"\n mc mirror \"${AGENTTEAMS_STORAGE_PREFIX}/manager/\" /root/manager-workspace/ --overwrite 2>/dev/null || true\n mc mirror \"${AGENTTEAMS_STORAGE_PREFIX}/\" \"${AGENTTEAMS_FS}/\" --overwrite 2>/dev/null || true\n ln -sfn \"${AGENTTEAMS_FS}\" /root/manager-workspace/agentteams-fs\n ln -sfn \"${AGENTTEAMS_FS}\" /root/manager-workspace/hiclaw-fs\n touch \"${AGENTTEAMS_FS}/.initialized\"\nfi\n\n# ============================================================\n# Initialize / upgrade Manager workspace\n# First boot: full init via upgrade-builtins.sh\n# Subsequent boots: compare image version; upgrade only if changed\n# ============================================================\nmkdir -p /root/manager-workspace\n\nIMAGE_VERSION=$(cat /opt/agentteams/agent/.builtin-version 2>/dev/null || echo \"unknown\")\nINSTALLED_VERSION=$(cat /root/manager-workspace/.builtin-version 2>/dev/null || echo \"\")\n\nif [ ! -f /root/manager-workspace/.initialized ]; then\n log \"First boot: initializing manager workspace...\"\n bash /opt/agentteams/scripts/init/upgrade-builtins.sh\n touch /root/manager-workspace/.initialized\n log \"Manager workspace initialized (version: ${IMAGE_VERSION})\"\nelif [ \"${IMAGE_VERSION}\" != \"${INSTALLED_VERSION}\" ] || [ \"${IMAGE_VERSION}\" = \"latest\" ]; then\n log \"Upgrade detected: ${INSTALLED_VERSION} -> ${IMAGE_VERSION}${IMAGE_VERSION:+ (latest: always upgrade)}\"\n bash /opt/agentteams/scripts/init/upgrade-builtins.sh\n log \"Manager workspace upgraded to version: ${IMAGE_VERSION}\"\nelse\n log \"Workspace up to date (version: ${IMAGE_VERSION})\"\nfi\n\n# Local mode: wait for mc mirror initialization (shared + worker data in /root/agentteams-fs/)\nif [ \"${AGENTTEAMS_RUNTIME}\" != \"aliyun\" ] && [ \"${AGENTTEAMS_RUNTIME}\" != \"k8s\" ]; then\n log \"Waiting for MinIO storage initialization...\"\n _minio_wait=0\n while [ ! -f /root/agentteams-fs/.initialized ]; do\n sleep 2\n _minio_wait=$(( _minio_wait + 1 ))\n if [ \"${_minio_wait}\" -ge 60 ]; then\n log \"ERROR: MinIO storage initialization timed out after 120s\"\n exit 1\n fi\n done\n log \"MinIO storage initialized\"\nfi\n\n# ============================================================\n# Obtain Manager Matrix access token.\n#\n# Priority:\n# 1. Pre-injected token (AGENTTEAMS_MANAGER_MATRIX_TOKEN) — set by the\n# controller in both AppService mode (where no password exists) and\n# legacy mode (to avoid a redundant login round-trip).\n# 2. Password-based login — fallback for containers started without\n# controller involvement (manual docker run, legacy installs).\n# ============================================================\nif [ -n \"${AGENTTEAMS_MANAGER_MATRIX_TOKEN:-}\" ]; then\n MANAGER_TOKEN=\"${AGENTTEAMS_MANAGER_MATRIX_TOKEN}\"\n log \"Manager Matrix token pre-injected by controller (token prefix: ${MANAGER_TOKEN:0:10}...)\"\nelif [ \"${AGENTTEAMS_RUNTIME}\" = \"k8s\" ]; then\n log \"K8s mode: obtaining Manager Matrix token via password login...\"\n _LOGIN_RESPONSE=$(curl -s -X POST ${AGENTTEAMS_MATRIX_URL}/_matrix/client/v3/login \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"type\": \"m.login.password\",\n \"identifier\": {\"type\": \"m.id.user\", \"user\": \"manager\"},\n \"password\": \"'\"${AGENTTEAMS_MANAGER_PASSWORD}\"'\"\n }' 2>&1)\n MANAGER_TOKEN=$(echo \"${_LOGIN_RESPONSE}\" | jq -r '.access_token' 2>/dev/null)\n if [ -z \"${MANAGER_TOKEN}\" ] || [ \"${MANAGER_TOKEN}\" = \"null\" ]; then\n log \"ERROR: Failed to obtain Manager Matrix token\"\n log \"ERROR: Login response was: ${_LOGIN_RESPONSE}\"\n exit 1\n fi\n log \"Manager Matrix token obtained (token prefix: ${MANAGER_TOKEN:0:10}...)\"\nelse\nlog \"Registering human admin Matrix account...\"\ncurl -sf -X POST ${AGENTTEAMS_MATRIX_URL}/_matrix/client/v3/register \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"username\": \"'\"${AGENTTEAMS_ADMIN_USER}\"'\",\n \"password\": \"'\"${AGENTTEAMS_ADMIN_PASSWORD}\"'\",\n \"auth\": {\n \"type\": \"m.login.registration_token\",\n \"token\": \"'\"${AGENTTEAMS_REGISTRATION_TOKEN}\"'\"\n }\n }' > /dev/null 2>&1 || log \"Admin account may already exist\"\n\nlog \"Registering Manager Agent Matrix account...\"\ncurl -sf -X POST ${AGENTTEAMS_MATRIX_URL}/_matrix/client/v3/register \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"username\": \"manager\",\n \"password\": \"'\"${AGENTTEAMS_MANAGER_PASSWORD}\"'\",\n \"auth\": {\n \"type\": \"m.login.registration_token\",\n \"token\": \"'\"${AGENTTEAMS_REGISTRATION_TOKEN}\"'\"\n }\n }' > /dev/null 2>&1 || log \"Manager account may already exist\"\n\n# Get Manager Agent's Matrix access token\nlog \"Obtaining Manager Matrix access token...\"\n_LOGIN_RESPONSE=$(curl -s -X POST ${AGENTTEAMS_MATRIX_URL}/_matrix/client/v3/login \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"type\": \"m.login.password\",\n \"identifier\": {\"type\": \"m.id.user\", \"user\": \"manager\"},\n \"password\": \"'\"${AGENTTEAMS_MANAGER_PASSWORD}\"'\"\n }' 2>&1)\n_LOGIN_EXIT=$?\nlog \"Matrix login HTTP exit code: ${_LOGIN_EXIT}\"\nlog \"Matrix login response: ${_LOGIN_RESPONSE}\"\n\nMANAGER_TOKEN=$(echo \"${_LOGIN_RESPONSE}\" | jq -r '.access_token' 2>/dev/null)\n\nif [ -z \"${MANAGER_TOKEN}\" ] || [ \"${MANAGER_TOKEN}\" = \"null\" ]; then\n log \"ERROR: Failed to obtain Manager Matrix token (exit=${_LOGIN_EXIT})\"\n log \"ERROR: Login response was: ${_LOGIN_RESPONSE}\"\n exit 1\nfi\nlog \"Manager Matrix token obtained (token prefix: ${MANAGER_TOKEN:0:10}...)\"\nfi\n\n# ============================================================\n# Higress Console initialization\n# Docker mode: full setup-higress.sh (internal Higress at localhost:8001)\n# K8s mode: skip — controller Initializer handles Higress setup\n# Cloud (aliyun) mode: skip entirely (Higress managed externally)\n# ============================================================\n_HIGRESS_CONSOLE_URL=\"\"\n_HIGRESS_USER=\"${AGENTTEAMS_ADMIN_USER}\"\n_HIGRESS_PASS=\"${AGENTTEAMS_ADMIN_PASSWORD}\"\nif [ \"${AGENTTEAMS_RUNTIME}\" = \"k8s\" ]; then\n log \"K8s mode: skipping Higress initialization (handled by controller)\"\nelif [ \"${AGENTTEAMS_RUNTIME}\" != \"aliyun\" ]; then\n _HIGRESS_CONSOLE_URL=\"http://127.0.0.1:8001\"\nfi\n\nif [ -n \"${_HIGRESS_CONSOLE_URL}\" ]; then\n COOKIE_FILE=\"/tmp/higress-session-cookie\"\n\n log \"Waiting for Higress Console (${_HIGRESS_CONSOLE_URL}) to be fully ready and initializing admin...\"\n INIT_DONE=false\n for i in $(seq 1 90); do\n INIT_RESULT=$(curl -s -X POST \"${_HIGRESS_CONSOLE_URL}/system/init\" \\\n -H 'Content-Type: application/json' \\\n -d '{\"adminUser\":{\"name\":\"'\"${_HIGRESS_USER}\"'\",\"password\":\"'\"${_HIGRESS_PASS}\"'\",\"displayName\":\"'\"${_HIGRESS_USER}\"'\"}}' 2>/dev/null) || true\n if echo \"${INIT_RESULT}\" | grep -qE '\"success\":true|already.?init' 2>/dev/null; then\n INIT_DONE=true\n break\n fi\n if echo \"${INIT_RESULT}\" | grep -q '\"name\"' 2>/dev/null; then\n INIT_DONE=true\n break\n fi\n sleep 2\n done\n\n if [ \"${INIT_DONE}\" != \"true\" ]; then\n log \"ERROR: Higress Console did not become ready within 180s\"\n exit 1\n fi\n log \"Higress Console init done\"\n\n log \"Logging into Higress Console...\"\n LOGIN_OK=false\n for i in $(seq 1 10); do\n HTTP_CODE=$(curl -s -o /dev/null -w '%{http_code}' -X POST \"${_HIGRESS_CONSOLE_URL}/session/login\" \\\n -H 'Content-Type: application/json' \\\n -c \"${COOKIE_FILE}\" \\\n -d '{\"username\":\"'\"${_HIGRESS_USER}\"'\",\"password\":\"'\"${_HIGRESS_PASS}\"'\"}' 2>/dev/null) || true\n if { [ \"${HTTP_CODE}\" = \"200\" ] || [ \"${HTTP_CODE}\" = \"201\" ]; } && [ -f \"${COOKIE_FILE}\" ] && [ -s \"${COOKIE_FILE}\" ]; then\n LOGIN_OK=true\n break\n fi\n log \"Login attempt $i (HTTP ${HTTP_CODE}), retrying in 3s...\"\n sleep 3\n done\n\n if [ \"${LOGIN_OK}\" != \"true\" ]; then\n log \"ERROR: Could not login to Higress Console after retries\"\n exit 1\n fi\n log \"Higress Console login successful\"\n\n VERIFY_CODE=$(curl -s -o /dev/null -w '%{http_code}' \"${_HIGRESS_CONSOLE_URL}/v1/consumers\" -b \"${COOKIE_FILE}\" 2>/dev/null) || true\n if [ \"${VERIFY_CODE}\" = \"200\" ]; then\n log \"Console session verified (cookie valid)\"\n else\n log \"WARNING: Console session may be invalid (verify returned HTTP ${VERIFY_CODE})\"\n rm -f \"${COOKIE_FILE}\"\n for i in $(seq 1 5); do\n curl -s -o /dev/null -w '%{http_code}' -X POST \"${_HIGRESS_CONSOLE_URL}/session/login\" \\\n -H 'Content-Type: application/json' \\\n -c \"${COOKIE_FILE}\" \\\n -d '{\"username\":\"'\"${_HIGRESS_USER}\"'\",\"password\":\"'\"${_HIGRESS_PASS}\"'\"}' 2>/dev/null\n VERIFY2=$(curl -s -o /dev/null -w '%{http_code}' \"${_HIGRESS_CONSOLE_URL}/v1/consumers\" -b \"${COOKIE_FILE}\" 2>/dev/null) || true\n if [ \"${VERIFY2}\" = \"200\" ]; then\n log \"Re-login successful, session verified\"\n break\n fi\n sleep 2\n done\n fi\n\n export HIGRESS_COOKIE_FILE=\"${COOKIE_FILE}\"\n export HIGRESS_CONSOLE_URL=\"${_HIGRESS_CONSOLE_URL}\"\n\n if [ \"${AGENTTEAMS_RUNTIME}\" = \"k8s\" ]; then\n # K8s mode: lightweight Higress config — only what's needed for LLM access\n source /opt/agentteams/scripts/lib/base.sh\n _k8s_higress_api() {\n local method=\"$1\" path=\"$2\" desc=\"$3\"; shift 3; local body=\"$*\"\n local tmpfile; tmpfile=$(mktemp)\n local http_code\n http_code=$(curl -s -o \"${tmpfile}\" -w '%{http_code}' -X \"${method}\" \"${_HIGRESS_CONSOLE_URL}${path}\" \\\n -b \"${COOKIE_FILE}\" -H 'Content-Type: application/json' -d \"${body}\" 2>/dev/null) || true\n local response; response=$(cat \"${tmpfile}\" 2>/dev/null); rm -f \"${tmpfile}\"\n if echo \"${response}\" | grep -q '\"success\":true' 2>/dev/null; then\n log \"${desc} ... OK\"\n elif [ \"${http_code}\" = \"409\" ]; then\n log \"${desc} ... already exists, skipping\"\n elif [ \"${http_code}\" = \"200\" ] || [ \"${http_code}\" = \"201\" ] || [ \"${http_code}\" = \"204\" ]; then\n log \"${desc} ... OK (HTTP ${http_code})\"\n else\n log \"WARNING: ${desc} ... (HTTP ${http_code}): ${response}\"\n fi\n }\n\n # 1. Service Sources (DNS type → K8s Service FQDN)\n # Extract host:port from URLs for Higress service source registration\n _TUWUNEL_HOST=$(echo \"${AGENTTEAMS_MATRIX_URL}\" | sed 's|^http[s]*://||')\n _TUWUNEL_DOMAIN=$(echo \"${_TUWUNEL_HOST}\" | cut -d: -f1)\n _TUWUNEL_PORT=$(echo \"${_TUWUNEL_HOST}\" | cut -d: -f2)\n _k8s_higress_api POST /v1/service-sources \"Registering Tuwunel service source\" \\\n '{\"type\":\"dns\",\"name\":\"tuwunel\",\"domain\":\"'\"${_TUWUNEL_DOMAIN}\"'\",\"port\":'\"${_TUWUNEL_PORT}\"'}'\n\n if [ -n \"${AGENTTEAMS_ELEMENT_WEB_URL:-}\" ]; then\n _ELEMENT_HOST=$(echo \"${AGENTTEAMS_ELEMENT_WEB_URL}\" | sed 's|^http[s]*://||')\n _ELEMENT_DOMAIN=$(echo \"${_ELEMENT_HOST}\" | cut -d: -f1)\n _ELEMENT_PORT=$(echo \"${_ELEMENT_HOST}\" | cut -d: -f2)\n _k8s_higress_api POST /v1/service-sources \"Registering Element Web service source\" \\\n '{\"type\":\"dns\",\"name\":\"element-web\",\"domain\":\"'\"${_ELEMENT_DOMAIN}\"'\",\"port\":'\"${_ELEMENT_PORT}\"'}'\n fi\n\n # 2. Manager Consumer (key-auth)\n _k8s_higress_api POST /v1/consumers \"Creating Manager consumer\" \\\n '{\"name\":\"manager\",\"credentials\":[{\"type\":\"key-auth\",\"source\":\"BEARER\",\"values\":[\"'\"${AGENTTEAMS_MANAGER_GATEWAY_KEY}\"'\"]}]}'\n\n # 3. LLM Provider\n _LLM_PROVIDER=\"${AGENTTEAMS_LLM_PROVIDER:-qwen}\"\n _LLM_API_URL=\"${AGENTTEAMS_LLM_API_URL:-}\"\n if [ -z \"${_LLM_API_URL}\" ]; then\n case \"${_LLM_PROVIDER}\" in\n qwen) _LLM_API_URL=\"https://dashscope.aliyuncs.com/compatible-mode/v1\" ;;\n esac\n fi\n # 4. LLM Provider type-specific config\n case \"${_LLM_PROVIDER}\" in\n qwen)\n _k8s_higress_api POST /v1/ai/providers \"Creating LLM provider (qwen)\" \\\n '{\"type\":\"qwen\",\"name\":\"qwen\",\"tokens\":[\"'\"${AGENTTEAMS_LLM_API_KEY}\"'\"],\"protocol\":\"openai/v1\",\"tokenFailoverConfig\":{\"enabled\":false},\"rawConfigs\":{\"qwenEnableSearch\":false,\"qwenEnableCompatible\":true,\"qwenFileIds\":[],\"hiclawMode\":true}}'\n ;;\n *)\n _BODY='{\"name\":\"'\"${_LLM_PROVIDER}\"'\",\"type\":\"openai\",\"tokens\":[\"'\"${AGENTTEAMS_LLM_API_KEY}\"'\"],\"modelMapping\":{},\"protocol\":\"openai/v1\",\"rawConfigs\":{\"hiclawMode\":true}}'\n _k8s_higress_api POST /v1/ai/providers \"Creating LLM provider (${_LLM_PROVIDER})\" \"${_BODY}\"\n ;;\n esac\n\n # 5. AI Route (bind provider + consumer auth, /v1 prefix to avoid clash with Element Web catch-all)\n _k8s_higress_api POST /v1/ai/routes \"Creating AI Gateway route\" \\\n '{\"name\":\"default-ai-route\",\"domains\":[],\"pathPredicate\":{\"matchType\":\"PRE\",\"matchValue\":\"/v1\",\"caseSensitive\":false},\"upstreams\":[{\"provider\":\"'\"${_LLM_PROVIDER}\"'\",\"weight\":100,\"modelMapping\":{}}],\"authConfig\":{\"enabled\":true,\"allowedCredentialTypes\":[\"key-auth\"],\"allowedConsumers\":[\"manager\"]}}'\n\n # 6. Matrix Homeserver Route (/_matrix/* → Tuwunel, no auth)\n _k8s_higress_api POST /v1/routes \"Creating Matrix Homeserver route\" \\\n '{\"name\":\"matrix-homeserver\",\"domains\":[],\"path\":{\"matchType\":\"PRE\",\"matchValue\":\"/_matrix\"},\"services\":[{\"name\":\"tuwunel.dns\",\"port\":'\"${_TUWUNEL_PORT}\"',\"weight\":100}]}'\n\n # 7. Element Web Route (/ catch-all → Element Web, no auth)\n if [ -n \"${AGENTTEAMS_ELEMENT_WEB_URL:-}\" ]; then\n _k8s_higress_api POST /v1/routes \"Creating Element Web route\" \\\n '{\"name\":\"element-web\",\"domains\":[],\"path\":{\"matchType\":\"PRE\",\"matchValue\":\"/\"},\"services\":[{\"name\":\"element-web.dns\",\"port\":'\"${_ELEMENT_PORT}\"',\"weight\":100}]}'\n fi\n\n # 8. Remove Higress default landing page (Exact match on / takes precedence over Element Web catch-all)\n _k8s_higress_api DELETE /v1/routes/default \"Removing Higress default landing route\"\n\n log \"K8s Higress lightweight setup complete\"\n\n # Wait for AI plugin activation (~45s for first config)\n log \"Waiting for AI Gateway plugin activation (45s)...\"\n sleep 45\n else\n # Docker mode: full setup with all routes, domains, MCP servers\n /opt/agentteams/scripts/init/setup-higress.sh\n fi\nfi\n\n# ============================================================\n# Create admin DM room, persist to state.json, send welcome message\n# K8s mode: skip — controller ProvisionManager creates the Admin DM\n# room (Step 4 in service/provisioner.go) AND reconcileManagerWelcome\n# delivers the first-boot onboarding prompt once OpenClaw inside this\n# container has joined the room. In k8s mode the manager intentionally\n# does NOT have the admin password (only AGENTTEAMS_ADMIN_USER), so it\n# could not log in as admin to send the welcome itself anyway. The\n# Manager Agent discovers its admin DM room on first heartbeat via\n# state.json / `manage-state.sh --action set-admin-dm` (see HEARTBEAT.md\n# Step 1) — it does not need it to be pre-injected by this script.\n# Runs in both local and cloud modes (idempotent)\n# ============================================================\nif [ \"${AGENTTEAMS_RUNTIME}\" = \"k8s\" ]; then\n log \"K8s mode: skipping admin DM room creation and welcome message (both handled by agentteams-controller)\"\nelse\nMANAGER_FULL_ID=\"@manager:${MATRIX_DOMAIN}\"\nADMIN_FULL_ID=\"@${AGENTTEAMS_ADMIN_USER}:${MATRIX_DOMAIN}\"\n\nlog \"Logging in as admin to create DM room...\"\n_ADMIN_LOGIN=$(curl -sf -X POST \"${AGENTTEAMS_MATRIX_URL}/_matrix/client/v3/login\" \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"type\": \"m.login.password\",\n \"identifier\": {\"type\": \"m.id.user\", \"user\": \"'\"${AGENTTEAMS_ADMIN_USER}\"'\"},\n \"password\": \"'\"${AGENTTEAMS_ADMIN_PASSWORD}\"'\"\n }' 2>&1) || true\n\nADMIN_MATRIX_TOKEN=$(echo \"${_ADMIN_LOGIN}\" | jq -r '.access_token // empty' 2>/dev/null)\nif [ -z \"${ADMIN_MATRIX_TOKEN}\" ]; then\n log \"WARNING: Failed to login as admin, skipping DM room creation\"\nelse\n # Search for existing DM room with Manager (idempotent)\n DM_ROOM_ID=\"\"\n _JOINED_ROOMS=$(curl -sf \"${AGENTTEAMS_MATRIX_URL}/_matrix/client/v3/joined_rooms\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" 2>/dev/null \\\n | jq -r '.joined_rooms[]' 2>/dev/null) || true\n for _rid in ${_JOINED_ROOMS}; do\n _members=$(curl -sf \"${AGENTTEAMS_MATRIX_URL}/_matrix/client/v3/rooms/${_rid}/members\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" 2>/dev/null \\\n | jq -r '.chunk[].state_key' 2>/dev/null) || continue\n _count=$(echo \"${_members}\" | wc -l | xargs)\n if [ \"${_count}\" = \"2\" ] && echo \"${_members}\" | grep -q \"@manager:\"; then\n DM_ROOM_ID=\"${_rid}\"\n break\n fi\n done\n\n if [ -n \"${DM_ROOM_ID}\" ]; then\n log \"Existing DM room found: ${DM_ROOM_ID}\"\n else\n log \"Creating DM room with Manager...\"\n _RAW=$(curl -s -w '\\nHTTP_CODE:%{http_code}' -X POST \"${AGENTTEAMS_MATRIX_URL}/_matrix/client/v3/createRoom\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d \"{\\\"is_direct\\\":true,\\\"invite\\\":[\\\"${MANAGER_FULL_ID}\\\"],\\\"preset\\\":\\\"trusted_private_chat\\\"}\" 2>&1) || true\n _HTTP_CODE=$(echo \"${_RAW}\" | tail -1 | sed 's/HTTP_CODE://')\n _CREATE_RESP=$(echo \"${_RAW}\" | sed '$d')\n DM_ROOM_ID=$(echo \"${_CREATE_RESP}\" | jq -r '.room_id // empty' 2>/dev/null)\n if [ -n \"${DM_ROOM_ID}\" ]; then\n log \"DM room created: ${DM_ROOM_ID}\"\n else\n log \"WARNING: Failed to create DM room (HTTP ${_HTTP_CODE}): ${_CREATE_RESP}\"\n fi\n fi\n\n # Persist admin DM room ID to state.json\n if [ -n \"${DM_ROOM_ID}\" ]; then\n STATE_SCRIPT=\"/opt/agentteams/agent/skills/task-management/scripts/manage-state.sh\"\n if [ -f \"${STATE_SCRIPT}\" ]; then\n bash \"${STATE_SCRIPT}\" --action init 2>/dev/null || true\n bash \"${STATE_SCRIPT}\" --action set-admin-dm --room-id \"${DM_ROOM_ID}\" 2>/dev/null || true\n log \"Admin DM room persisted to state.json: ${DM_ROOM_ID}\"\n fi\n fi\n\n # Schedule welcome message in background (only on first boot)\n if [ -n \"${DM_ROOM_ID}\" ] && [ ! -f \"/root/manager-workspace/soul-configured\" ]; then\n log \"Scheduling welcome message (background, waiting for OpenClaw to start)...\"\n (\n _AGENTTEAMS_LANGUAGE=\"${AGENTTEAMS_LANGUAGE:-zh}\"\n _AGENTTEAMS_TIMEZONE=\"${TZ:-Asia/Shanghai}\"\n _wait=0\n _ready=false\n while [ \"${_wait}\" -lt 300 ]; do\n if curl -sf http://127.0.0.1:18799/ > /dev/null 2>&1; then\n _ready=true\n break\n fi\n sleep 3\n _wait=$((_wait + 3))\n done\n if [ \"${_ready}\" != \"true\" ]; then\n echo \"[manager] WARNING: OpenClaw gateway not ready within 300s, skipping welcome message\"\n exit 0\n fi\n # Ensure Manager has joined the DM room before sending the welcome\n # message. Without this, there is a race between OpenClaw's Matrix\n # auto-join and the message send — the message may land before Manager\n # joins, so OpenClaw's /sync never picks it up.\n _join_ok=false\n for _join_attempt in 1 2 3; do\n if curl -sf -X POST \"${AGENTTEAMS_MATRIX_URL}/_matrix/client/v3/rooms/${DM_ROOM_ID}/join\" \\\n -H \"Authorization: Bearer ${MANAGER_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d '{}' > /dev/null 2>&1; then\n echo \"[manager] Manager joined DM room before welcome message\"\n _join_ok=true\n break\n fi\n sleep 2\n done\n if [ \"${_join_ok}\" != \"true\" ]; then\n echo \"[manager] WARNING: Manager join request failed after 3 attempts (may already be joined)\"\n fi\n _welcome_msg=\"This is an automated message from the AgentTeams setup. This is a fresh installation.\n\n--- Installation Context ---\nUser Language: ${_AGENTTEAMS_LANGUAGE} (zh = Chinese, en = English)\nUser Timezone: ${_AGENTTEAMS_TIMEZONE} (IANA timezone identifier)\n---\n\nYou are an AI agent that manages a team of worker agents. Your identity and personality have not been configured yet — the human admin is about to meet you for the first time.\n\nPlease begin the onboarding conversation:\n\n1. Greet the admin warmly and briefly describe what you can do (coordinate workers, manage tasks, run multi-agent projects)\n2. The user has selected \\\"${_AGENTTEAMS_LANGUAGE}\\\" as their preferred language during installation. Use this language for your greeting and all subsequent communication.\n3. The user's timezone is ${_AGENTTEAMS_TIMEZONE}. Based on this timezone, you may infer their likely region and suggest additional language options.\n4. Ask them: a) What would they like to call you? b) Communication style preference? c) Any behavior guidelines? d) Confirm default language\n5. After they reply, write their preferences to ~/SOUL.md\n6. Confirm what you wrote, and ask if they would like to adjust anything\n7. Once confirmed, run: touch ~/soul-configured\n\nThe human admin will start chatting shortly.\"\n _txn_id=\"welcome-$(date +%s)\"\n _payload=$(jq -nc --arg body \"${_welcome_msg}\" '{\"msgtype\":\"m.text\",\"body\":$body}')\n _raw=$(curl -s -w '\\nHTTP_CODE:%{http_code}' -X PUT \"${AGENTTEAMS_MATRIX_URL}/_matrix/client/v3/rooms/${DM_ROOM_ID}/send/m.room.message/${_txn_id}\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d \"${_payload}\" 2>&1) || true\n _http_code=$(echo \"${_raw}\" | tail -1 | sed 's/HTTP_CODE://')\n _send_resp=$(echo \"${_raw}\" | sed '$d')\n if echo \"${_send_resp}\" | jq -e '.event_id' > /dev/null 2>&1; then\n echo \"[manager] Welcome message sent to DM room\"\n else\n echo \"[manager] WARNING: Failed to send welcome message (HTTP ${_http_code}): ${_send_resp}\"\n fi\n ) &\n log \"Welcome message background process started (PID: $!)\"\n fi\nfi\nfi # end K8s mode skip for admin DM room\n\n# ============================================================\n# Generate Manager Agent openclaw.json from template\n# ============================================================\nlog \"Generating Manager openclaw.json...\"\nexport MANAGER_MATRIX_TOKEN=\"${MANAGER_TOKEN}\"\nexport MANAGER_GATEWAY_KEY=\"${AGENTTEAMS_MANAGER_GATEWAY_KEY}\"\n# Resolve model parameters based on model name\nMODEL_NAME=\"${AGENTTEAMS_DEFAULT_MODEL:-qwen3.6-plus}\"\ncase \"${MODEL_NAME}\" in\n gpt-5.3-codex|gpt-5-mini|gpt-5-nano)\n export MODEL_CONTEXT_WINDOW=400000 MODEL_MAX_TOKENS=128000 ;;\n claude-opus-4-6)\n export MODEL_CONTEXT_WINDOW=1000000 MODEL_MAX_TOKENS=128000 ;;\n claude-sonnet-4-6)\n export MODEL_CONTEXT_WINDOW=1000000 MODEL_MAX_TOKENS=64000 ;;\n claude-haiku-4-5)\n export MODEL_CONTEXT_WINDOW=200000 MODEL_MAX_TOKENS=64000 ;;\n qwen3.6-plus|qwen3.5-plus)\n export MODEL_CONTEXT_WINDOW=200000 MODEL_MAX_TOKENS=64000 ;;\n deepseek-chat|deepseek-reasoner|kimi-k2.5)\n export MODEL_CONTEXT_WINDOW=256000 MODEL_MAX_TOKENS=128000 ;;\n glm-5|MiniMax-M2.7|MiniMax-M2.7-highspeed|MiniMax-M2.5)\n export MODEL_CONTEXT_WINDOW=200000 MODEL_MAX_TOKENS=128000 ;;\n *)\n export MODEL_CONTEXT_WINDOW=150000 MODEL_MAX_TOKENS=128000 ;;\nesac\nexport MODEL_REASONING=true\n\n# Override with user-supplied custom model parameters from env (set during install)\n[ -n \"${AGENTTEAMS_MODEL_CONTEXT_WINDOW:-}\" ] && export MODEL_CONTEXT_WINDOW=\"${AGENTTEAMS_MODEL_CONTEXT_WINDOW}\"\n[ -n \"${AGENTTEAMS_MODEL_MAX_TOKENS:-}\" ] && export MODEL_MAX_TOKENS=\"${AGENTTEAMS_MODEL_MAX_TOKENS}\"\n[ -n \"${AGENTTEAMS_MODEL_REASONING:-}\" ] && export MODEL_REASONING=\"${AGENTTEAMS_MODEL_REASONING}\"\n\n# E2EE: convert AGENTTEAMS_MATRIX_E2EE to JSON boolean for template substitution\nif [ \"${AGENTTEAMS_MATRIX_E2EE:-0}\" = \"1\" ] || [ \"${AGENTTEAMS_MATRIX_E2EE:-}\" = \"true\" ]; then\n export MATRIX_E2EE_ENABLED=true\nelse\n export MATRIX_E2EE_ENABLED=false\nfi\nlog \"Matrix E2EE: ${MATRIX_E2EE_ENABLED}\"\n\n# Resolve input modalities: only vision-capable models get \"image\"\ncase \"${MODEL_NAME}\" in\n gpt-5.4|gpt-5.3-codex|gpt-5-mini|gpt-5-nano|claude-opus-4-6|claude-sonnet-4-6|claude-haiku-4-5|qwen3.6-plus|qwen3.5-plus|kimi-k2.5)\n export MODEL_INPUT='[\"text\", \"image\"]' ;;\n *)\n export MODEL_INPUT='[\"text\"]' ;;\nesac\n# Override with user-supplied vision setting from env\nif [ \"${AGENTTEAMS_MODEL_VISION:-}\" = \"true\" ]; then\n export MODEL_INPUT='[\"text\", \"image\"]'\nelif [ \"${AGENTTEAMS_MODEL_VISION:-}\" = \"false\" ]; then\n export MODEL_INPUT='[\"text\"]'\nfi\n\nlog \"Model: ${MODEL_NAME} (context=${MODEL_CONTEXT_WINDOW}, maxTokens=${MODEL_MAX_TOKENS}, reasoning=${MODEL_REASONING}, input=${MODEL_INPUT})\"\n\nif [ -f /root/manager-workspace/openclaw.json ]; then\n log \"Manager openclaw.json already exists, updating dynamic fields only (preserving user customizations)...\"\n # Merge known models into existing config (add missing, preserve user-added)\n # Use known-models.json (valid JSON) instead of template (contains ${VAR} placeholders)\n KNOWN_MODELS=$(cat /opt/agentteams/configs/known-models.json 2>/dev/null || echo '[]')\n jq --arg token \"${MANAGER_TOKEN}\" \\\n --arg key \"${AGENTTEAMS_MANAGER_GATEWAY_KEY}\" \\\n --arg model \"${MODEL_NAME}\" \\\n --arg emb_model \"${AGENTTEAMS_EMBEDDING_MODEL}\" \\\n --arg aigw_domain \"${AI_GATEWAY_DOMAIN}\" \\\n --arg matrix_user_id \"@manager:${MATRIX_DOMAIN}\" \\\n --argjson e2ee \"${MATRIX_E2EE_ENABLED}\" \\\n --argjson known_models \"${KNOWN_MODELS}\" \\\n --argjson ctx \"${MODEL_CONTEXT_WINDOW}\" \\\n --argjson max \"${MODEL_MAX_TOKENS}\" \\\n --argjson reasoning \"${MODEL_REASONING}\" \\\n --argjson input \"${MODEL_INPUT}\" \\\n '\n # Merge known models: add any model id not already present\n .models.providers[\"agentteams-gateway\"].models as $existing\n | ($existing | map(.id)) as $existing_ids\n | ($known_models | map(select(.id as $id | $existing_ids | index($id) | not))) as $new\n | .models.providers[\"agentteams-gateway\"].models = ($existing + $new)\n # Ensure the user-chosen default model is in the list (custom model support)\n | if (.models.providers[\"agentteams-gateway\"].models | map(.id) | index($model) | not) then\n .models.providers[\"agentteams-gateway\"].models += [{\"id\": $model, \"name\": $model, \"reasoning\": $reasoning, \"contextWindow\": $ctx, \"maxTokens\": $max, \"input\": $input}]\n else . end\n # Rebuild model aliases from the full models list\n | (.models.providers[\"agentteams-gateway\"].models | map({ (\"agentteams-gateway/\" + .id): { \"alias\": .id } }) | add // {}) as $aliases\n | .agents.defaults.models = ((.agents.defaults.models // {}) + $aliases)\n | .channels.matrix.accessToken = $token | .channels.matrix.userId = $matrix_user_id | .models.providers[\"agentteams-gateway\"].apiKey = $key\n | ((.hooks.token // \"\") as $ht | if $ht == $key or $ht == ($key + \"-hooks\" | @base64) then del(.hooks) else . end)\n | .agents.defaults.model.primary = (\"agentteams-gateway/\" + $model)\n | .commands.restart = true\n | .gateway.port = 18799\n | .gateway.bind = \"lan\"\n | .gateway.controlUi = ((.gateway.controlUi // {}) + {\"dangerouslyDisableDeviceAuth\": true, \"allowInsecureAuth\": true, \"allowedOrigins\": [\"*\"]})\n | .channels.matrix.encryption = $e2ee\n | .channels.matrix.network = ((.channels.matrix.network // {}) + {\"dangerouslyAllowPrivateNetwork\": true})\n | .channels.matrix.autoJoin = \"always\"\n # OpenClaw YOLO defaults: host exec without approval prompts (see openclaw docs tools/exec-approvals)\n | .tools = (.tools // {})\n | .tools.exec = ((.tools.exec // {}) + {\"host\":\"gateway\",\"security\":\"full\",\"ask\":\"off\"})\n | .tools.elevated = (.tools.elevated // {})\n | .tools.elevated.enabled = true\n | .tools.elevated.allowFrom |= ((. // {}) | .matrix = [\"*\"])\n | .agents.defaults.elevatedDefault = \"full\"\n # Ensure memorySearch config exists (embedding model for memory) — skip if embedding model is empty\n | if $emb_model != \"\" then .agents.defaults.memorySearch //= {\"provider\":\"openai\",\"model\":$emb_model,\"remote\":{\"baseUrl\":(\"http://\" + $aigw_domain + \":8080/v1\"),\"apiKey\":$key}} else . end\n ' \\\n /root/manager-workspace/openclaw.json > /tmp/openclaw.json.tmp && \\\n mv /tmp/openclaw.json.tmp /root/manager-workspace/openclaw.json\n # Disable openclaw's observe-recovery mechanism which compares config against\n # a lastKnownGood baseline in config-health.json. When meta is missing from the\n # current file but present in the baseline, observe-recovery restores from .bak,\n # undoing user customizations (plugins, channels, etc).\n # Clearing config-health.json removes the baseline so observe-recovery won't\n # interfere, while preserving .bak as a backup.\n rm -f /root/manager-workspace/.openclaw/logs/config-health.json\n # Verify the token was written correctly\n _written_token=$(jq -r '.channels.matrix.accessToken' /root/manager-workspace/openclaw.json 2>/dev/null)\n if [ -z \"${_written_token}\" ] || [ \"${_written_token}\" = \"null\" ]; then\n log \"ERROR: Matrix token was not written correctly to openclaw.json (got: ${_written_token})\"\n else\n log \"Matrix token written to openclaw.json (prefix: ${_written_token:0:10}...)\"\n fi\nelse\n log \"Manager openclaw.json not found, generating from template...\"\n envsubst < /opt/agentteams/configs/manager-openclaw.json.tmpl > /root/manager-workspace/openclaw.json\n # Post-envsubst injection: memorySearch + custom model (single jq pass when possible)\n if ! jq -e --arg model \"${MODEL_NAME}\" '.models.providers[\"agentteams-gateway\"].models | map(.id) | index($model)' /root/manager-workspace/openclaw.json > /dev/null 2>&1; then\n log \"Custom model '${MODEL_NAME}' not in built-in list, injecting into config...\"\n jq --arg emb_model \"${AGENTTEAMS_EMBEDDING_MODEL}\" \\\n --arg aigw_domain \"${AI_GATEWAY_DOMAIN}\" \\\n --arg key \"${AGENTTEAMS_MANAGER_GATEWAY_KEY}\" \\\n --arg model \"${MODEL_NAME}\" \\\n --argjson ctx \"${MODEL_CONTEXT_WINDOW}\" \\\n --argjson max \"${MODEL_MAX_TOKENS}\" \\\n --argjson reasoning \"${MODEL_REASONING}\" \\\n --argjson input \"${MODEL_INPUT}\" \\\n '\n (if $emb_model != \"\" then .agents.defaults.memorySearch = {\"provider\":\"openai\",\"model\":$emb_model,\"remote\":{\"baseUrl\":(\"http://\" + $aigw_domain + \":8080/v1\"),\"apiKey\":$key}} else . end)\n | .models.providers[\"agentteams-gateway\"].models += [{\"id\": $model, \"name\": $model, \"reasoning\": $reasoning, \"contextWindow\": $ctx, \"maxTokens\": $max, \"input\": $input}]\n | .agents.defaults.models += {(\"agentteams-gateway/\" + $model): {\"alias\": $model}}\n ' /root/manager-workspace/openclaw.json > /tmp/openclaw.json.tmp && \\\n mv /tmp/openclaw.json.tmp /root/manager-workspace/openclaw.json\n elif [ -n \"${AGENTTEAMS_EMBEDDING_MODEL}\" ]; then\n jq --arg emb_model \"${AGENTTEAMS_EMBEDDING_MODEL}\" \\\n --arg aigw_domain \"${AI_GATEWAY_DOMAIN}\" \\\n --arg key \"${AGENTTEAMS_MANAGER_GATEWAY_KEY}\" \\\n '.agents.defaults.memorySearch = {\"provider\":\"openai\",\"model\":$emb_model,\"remote\":{\"baseUrl\":(\"http://\" + $aigw_domain + \":8080/v1\"),\"apiKey\":$key}}' \\\n /root/manager-workspace/openclaw.json > /tmp/openclaw.json.tmp && \\\n mv /tmp/openclaw.json.tmp /root/manager-workspace/openclaw.json\n fi\n _written_token=$(jq -r '.channels.matrix.accessToken' /root/manager-workspace/openclaw.json 2>/dev/null)\n log \"Matrix token written from template (prefix: ${_written_token:0:10}...)\"\nfi\n\n# Cloud/K8s mode: overlay cloud-specific settings onto generated config\nif [ \"${AGENTTEAMS_RUNTIME}\" = \"aliyun\" ] || [ \"${AGENTTEAMS_RUNTIME}\" = \"k8s\" ]; then\n log \"Applying cloud/k8s overlay to openclaw.json...\"\n jq --arg homeserver \"${AGENTTEAMS_MATRIX_URL}\" \\\n --arg gateway \"${AGENTTEAMS_AI_GATEWAY_URL}/v1\" \\\n --arg key \"${AGENTTEAMS_MANAGER_GATEWAY_KEY}\" \\\n '.channels.matrix.homeserver = $homeserver\n | .models.providers[\"agentteams-gateway\"].baseUrl = $gateway\n | .models.providers[\"agentteams-gateway\"].apiKey = $key\n | ((.hooks.token // \"\") as $ht | if $ht == $key or $ht == ($key + \"-hooks\" | @base64) then del(.hooks) else . end)\n | .commands.restart = false\n | if .agents.defaults.memorySearch then .agents.defaults.memorySearch.remote.baseUrl = $gateway | .agents.defaults.memorySearch.remote.apiKey = $key else . end' \\\n /root/manager-workspace/openclaw.json > /tmp/openclaw-cloud.json && \\\n mv /tmp/openclaw-cloud.json /root/manager-workspace/openclaw.json\n log \"Cloud/K8s overlay applied\"\nfi\n\n# ============================================================\n# Optional: enable openclaw-cms-plugin observability\n# Config is applied at runtime so secrets stay out of image layers.\n# ============================================================\nCMS_TRACES_ENABLED=\"$(echo \"${AGENTTEAMS_CMS_TRACES_ENABLED:-false}\" | tr '[:upper:]' '[:lower:]')\"\nif [ \"${CMS_TRACES_ENABLED}\" = \"true\" ]; then\n CMS_PLUGIN_NAME=\"openclaw-cms-plugin\"\n CMS_PLUGIN_DIR=\"${OPENCLAW_CMS_PLUGIN_DIR:-/opt/openclaw/extensions/openclaw-cms-plugin}\"\n CMS_PLUGIN_MANIFEST=\"${CMS_PLUGIN_DIR}/openclaw.plugin.json\"\n DIAG_PLUGIN_NAME=\"diagnostics-otel\"\n DIAG_PLUGIN_DIR=\"/opt/openclaw/extensions/diagnostics-otel\"\n CMS_LICENSE_KEY=\"${AGENTTEAMS_CMS_LICENSE_KEY:-}\"\n CMS_PROJECT=\"${AGENTTEAMS_CMS_PROJECT:-}\"\n CMS_METRICS_ENABLED=\"${AGENTTEAMS_CMS_METRICS_ENABLED:-false}\"\n\n if [ ! -f \"${CMS_PLUGIN_MANIFEST}\" ]; then\n log \"WARNING: ${CMS_PLUGIN_NAME} manifest not found at ${CMS_PLUGIN_MANIFEST}, skipping plugin config\"\n else\n _missing=0\n [ -z \"${AGENTTEAMS_CMS_ENDPOINT:-}\" ] && log \"WARNING: AGENTTEAMS_CMS_ENDPOINT is required when AGENTTEAMS_CMS_TRACES_ENABLED=true\" && _missing=1\n [ -z \"${CMS_LICENSE_KEY:-}\" ] && log \"WARNING: AGENTTEAMS_CMS_LICENSE_KEY is required when AGENTTEAMS_CMS_TRACES_ENABLED=true\" && _missing=1\n [ -z \"${AGENTTEAMS_CMS_WORKSPACE:-}\" ] && log \"WARNING: AGENTTEAMS_CMS_WORKSPACE is required when AGENTTEAMS_CMS_TRACES_ENABLED=true\" && _missing=1\n\n if [ \"${_missing}\" = \"0\" ]; then\n CMS_SERVICE_NAME=\"${AGENTTEAMS_CMS_SERVICE_NAME:-agentteams-manager}\"\n CMS_ENABLE_METRICS=\"${CMS_METRICS_ENABLED}\"\n DIAG_AVAILABLE=\"0\"\n _metrics_lc=\"$(echo \"${CMS_ENABLE_METRICS}\" | tr '[:upper:]' '[:lower:]')\"\n if [ \"${_metrics_lc}\" = \"true\" ]; then\n if [ -f \"${DIAG_PLUGIN_DIR}/package.json\" ]; then\n DIAG_AVAILABLE=\"1\"\n if [ ! -d \"${DIAG_PLUGIN_DIR}/node_modules\" ]; then\n log \"diagnostics-otel dependencies missing, installing...\"\n if (cd \"${DIAG_PLUGIN_DIR}\" && npm install --omit=dev --ignore-scripts >/tmp/hiclaw-diag-install.log 2>&1); then\n log \"diagnostics-otel dependencies installed\"\n else\n log \"WARNING: diagnostics-otel npm install failed, metrics plugin may not load\"\n fi\n else\n log \"diagnostics-otel dependencies already present\"\n fi\n else\n log \"WARNING: diagnostics-otel package.json not found at ${DIAG_PLUGIN_DIR}, metrics plugin may not load\"\n fi\n fi\n\n log \"Applying ${CMS_PLUGIN_NAME} config to openclaw.json...\"\n jq --arg pluginName \"${CMS_PLUGIN_NAME}\" \\\n --arg pluginDir \"${CMS_PLUGIN_DIR}\" \\\n --arg endpoint \"${AGENTTEAMS_CMS_ENDPOINT}\" \\\n --arg licenseKey \"${CMS_LICENSE_KEY}\" \\\n --arg armsProject \"${CMS_PROJECT}\" \\\n --arg cmsWorkspace \"${AGENTTEAMS_CMS_WORKSPACE}\" \\\n --arg serviceName \"${CMS_SERVICE_NAME}\" \\\n --arg diagPluginName \"${DIAG_PLUGIN_NAME}\" \\\n --arg diagPluginDir \"${DIAG_PLUGIN_DIR}\" \\\n --arg metricsRaw \"${CMS_ENABLE_METRICS}\" \\\n --arg diagAvailableRaw \"${DIAG_AVAILABLE}\" \\\n '\n .plugins = (.plugins // {})\n | .plugins.load = (.plugins.load // {})\n | .plugins.entries = (.plugins.entries // {})\n | if (.plugins.allow | type) != \"array\" then .plugins.allow = [] else . end\n | if (.plugins.allow | index($pluginName)) == null then .plugins.allow += [$pluginName] else . end\n | if (.plugins.load.paths | type) != \"array\" then .plugins.load.paths = [] else . end\n | if (.plugins.load.paths | index($pluginDir)) == null then .plugins.load.paths += [$pluginDir] else . end\n | .plugins.entries[$pluginName] = {\n \"enabled\": true,\n \"config\": {\n \"endpoint\": $endpoint,\n \"headers\": {\n \"x-arms-license-key\": $licenseKey,\n \"x-arms-project\": $armsProject,\n \"x-cms-workspace\": $cmsWorkspace\n },\n \"serviceName\": $serviceName\n }\n }\n\n # diagnostics-otel metrics (optional)\n | ($metricsRaw | ascii_downcase) as $m\n | ($diagAvailableRaw == \"1\") as $diagAvailable\n | (($m == \"true\") and $diagAvailable) as $metricsEnabled\n | if $metricsEnabled then\n (if (.plugins.allow | index($diagPluginName)) == null then .plugins.allow += [$diagPluginName] else . end)\n | (if (.plugins.load.paths | index($diagPluginDir)) == null then .plugins.load.paths += [$diagPluginDir] else . end)\n | .plugins.entries[$diagPluginName].enabled = true\n | .diagnostics = (.diagnostics // {})\n | .diagnostics.otel = (.diagnostics.otel // {})\n | .diagnostics.enabled = true\n | .diagnostics.otel.enabled = true\n | .diagnostics.otel.endpoint = $endpoint\n | .diagnostics.otel.protocol = (.diagnostics.otel.protocol // \"http/protobuf\")\n | .diagnostics.otel.headers = {\n \"x-arms-license-key\": $licenseKey,\n \"x-arms-project\": $armsProject,\n \"x-cms-workspace\": $cmsWorkspace\n }\n | .diagnostics.otel.serviceName = $serviceName\n | .diagnostics.otel.metrics = true\n | .diagnostics.otel.traces = (.diagnostics.otel.traces // false)\n | .diagnostics.otel.logs = (.diagnostics.otel.logs // false)\n else\n .\n end\n ' /root/manager-workspace/openclaw.json > /tmp/openclaw-cms.json && \\\n mv /tmp/openclaw-cms.json /root/manager-workspace/openclaw.json\n log \"${CMS_PLUGIN_NAME} config applied (metrics=${CMS_ENABLE_METRICS}, service=${CMS_SERVICE_NAME})\"\n else\n log \"Skipping ${CMS_PLUGIN_NAME} config due to missing required env vars\"\n fi\n fi\nfi\n\n# ============================================================\n# Detect container runtime (for Worker creation)\n# ============================================================\nsource /opt/agentteams/scripts/lib/container-api.sh\nif container_api_available; then\n log \"Container runtime socket detected at ${CONTAINER_SOCKET} — direct Worker creation enabled\"\n export AGENTTEAMS_CONTAINER_RUNTIME=\"socket\"\nelif [ \"${AGENTTEAMS_RUNTIME}\" = \"aliyun\" ] || [ \"${AGENTTEAMS_RUNTIME}\" = \"k8s\" ]; then\n log \"Cloud/K8s mode — Workers created via controller API\"\n export AGENTTEAMS_CONTAINER_RUNTIME=\"cloud\"\nelse\n log \"No container runtime found — Worker creation will output install commands\"\n export AGENTTEAMS_CONTAINER_RUNTIME=\"none\"\nfi\n\n# ============================================================\n# Upgrade Worker openclaw.json: merge known models + E2EE flag into existing configs\n# Existing workers in MinIO may have old single-model configs or missing encryption field.\n# Merge template models so they can hot-switch without restart.\n# ============================================================\nREGISTRY_FILE=\"/root/manager-workspace/workers-registry.json\"\nif [ -f \"${REGISTRY_FILE}\" ]; then\n # Use known-models.json (valid JSON) instead of template (contains ${VAR} placeholders)\n KNOWN_MODELS_FILE=\"/opt/agentteams/configs/known-models.json\"\n if [ -f \"${KNOWN_MODELS_FILE}\" ]; then\n _KNOWN_MODELS=$(cat \"${KNOWN_MODELS_FILE}\")\n for _wname in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_wname}\" ] && continue\n _minio_path=\"${AGENTTEAMS_STORAGE_PREFIX}/agents/${_wname}/openclaw.json\"\n _tmp_in=\"/tmp/openclaw-${_wname}-models-upgrade-in.json\"\n if mc cp \"${_minio_path}\" \"${_tmp_in}\" 2>/dev/null; then\n _tmp_out=\"/tmp/openclaw-${_wname}-models-upgrade-out.json\"\n # Idempotent merge: add missing known models, rebuild aliases, set e2ee.\n # Always runs — jq deduplicates by model id, so re-runs are safe.\n jq --argjson known_models \"${_KNOWN_MODELS}\" \\\n --argjson e2ee \"${MATRIX_E2EE_ENABLED}\" '\n .models.providers[\"agentteams-gateway\"].models as $existing\n | ($existing | map(.id)) as $existing_ids\n | ($known_models | map(select(.id as $id | $existing_ids | index($id) | not))) as $new\n | .models.providers[\"agentteams-gateway\"].models = ($existing + $new)\n | (.models.providers[\"agentteams-gateway\"].models | map({ (\"agentteams-gateway/\" + .id): { \"alias\": .id } }) | add // {}) as $aliases\n | .agents.defaults.models = ((.agents.defaults.models // {}) + $aliases)\n | .channels.matrix.encryption = $e2ee\n | .channels.matrix.autoJoin = \"always\"\n | .tools = (.tools // {})\n | .tools.exec = ((.tools.exec // {}) + {\"host\":\"gateway\",\"security\":\"full\",\"ask\":\"off\"})\n | .tools.elevated = (.tools.elevated // {})\n | .tools.elevated.enabled = true\n | .tools.elevated.allowFrom |= ((. // {}) | .matrix = [\"*\"])\n | .agents.defaults.elevatedDefault = \"full\"\n ' \"${_tmp_in}\" > \"${_tmp_out}\" 2>/dev/null\n if ! diff -q \"${_tmp_in}\" \"${_tmp_out}\" > /dev/null 2>&1; then\n if mc cp \"${_tmp_out}\" \"${_minio_path}\" 2>/dev/null; then\n _new_count=$(jq '.models.providers[\"agentteams-gateway\"].models | length' \"${_tmp_out}\" 2>/dev/null)\n log \"Worker ${_wname}: upgraded openclaw.json (models: ${_new_count}, e2ee: ${MATRIX_E2EE_ENABLED})\"\n fi\n fi\n rm -f \"${_tmp_in}\" \"${_tmp_out}\"\n fi\n done\n fi\nfi\n\n# ============================================================\n# Ensure Worker Matrix password files exist in MinIO (E2EE fix)\n# Workers need to re-login on restart to get a fresh device_id.\n# Older workers created before this fix won't have the password file.\n# ============================================================\nif [ -f \"${REGISTRY_FILE}\" ]; then\n for _wname in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_wname}\" ] && continue\n _creds_file=\"/data/worker-creds/${_wname}.env\"\n if [ -f \"${_creds_file}\" ]; then\n # Check if password file already exists in MinIO\n if ! mc stat \"${AGENTTEAMS_STORAGE_PREFIX}/agents/${_wname}/credentials/matrix/password\" > /dev/null 2>&1; then\n source \"${_creds_file}\"\n if [ -n \"${WORKER_PASSWORD}\" ]; then\n _tmp_pw=\"/tmp/matrix-pw-${_wname}\"\n echo -n \"${WORKER_PASSWORD}\" > \"${_tmp_pw}\"\n mc cp \"${_tmp_pw}\" \"${AGENTTEAMS_STORAGE_PREFIX}/agents/${_wname}/credentials/matrix/password\" 2>/dev/null \\\n && log \"Worker ${_wname}: wrote Matrix password to MinIO (E2EE re-login fix)\" \\\n || log \"Worker ${_wname}: WARNING: failed to write Matrix password to MinIO\"\n rm -f \"${_tmp_pw}\"\n fi\n fi\n fi\n done\nfi\n\n# ============================================================\n# Recreate Worker containers as needed after Manager restart.\n# Workers are on agentteams-net; Docker DNS resolves *-local.agentteams.io via\n# the Manager's network aliases, so IP changes don't require worker recreation.\n# Only recreate stopped/missing workers.\n# ============================================================\nif container_api_available; then\n REGISTRY_FILE=\"/root/manager-workspace/workers-registry.json\"\n if [ -f \"${REGISTRY_FILE}\" ]; then\n for _worker_name in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_worker_name}\" ] && continue\n\n # Skip remote workers — they are not Manager-managed containers.\n _deployment=$(jq -r --arg w \"${_worker_name}\" '.workers[$w].deployment // \"local\"' \"${REGISTRY_FILE}\" 2>/dev/null)\n if [ \"${_deployment}\" = \"remote\" ]; then\n log \"Worker ${_worker_name} is remote, skipping container recreate\"\n continue\n fi\n\n _status=$(container_status_worker \"${_worker_name}\")\n if [ \"${_status}\" = \"running\" ]; then\n log \"Worker running: ${_worker_name}, skipping\"\n continue\n fi\n # Container missing or stopped — recreate.\n log \"Worker container ${_status}: ${_worker_name}, recreating...\"\n _creds_file=\"/data/worker-creds/${_worker_name}.env\"\n if [ -f \"${_creds_file}\" ]; then\n source \"${_creds_file}\"\n _runtime=$(jq -r --arg w \"${_worker_name}\" '.workers[$w].runtime // \"openclaw\"' \"${REGISTRY_FILE}\" 2>/dev/null)\n _recreated=false\n for _attempt in 1 2 3; do\n local _env_map _create_body\n _env_map=$(jq -cn \\\n --arg name \"${_worker_name}\" \\\n --arg fak \"${_worker_name}\" \\\n --arg fsk \"${WORKER_MINIO_PASSWORD:-}\" \\\n --arg fs_domain \"${AGENTTEAMS_FS_DOMAIN:-fs-local.agentteams.io}\" \\\n --arg controller_url \"${AGENTTEAMS_CONTROLLER_URL:-}\" \\\n '{\n \"AGENTTEAMS_WORKER_NAME\": $name,\n \"AGENTTEAMS_FS_ENDPOINT\": (\"http://\" + ($fs_domain | split(\":\")[0]) + \":9000\"),\n \"AGENTTEAMS_FS_ACCESS_KEY\": $fak,\n \"AGENTTEAMS_FS_SECRET_KEY\": $fsk\n }\n | if $controller_url != \"\" then . + {\"AGENTTEAMS_CONTROLLER_URL\": $controller_url} else . end')\n _create_body=$(jq -cn --arg name \"${_worker_name}\" --arg runtime \"${_runtime}\" --argjson env \"${_env_map}\" '{name: $name, runtime: $runtime, env: $env}')\n worker_backend_create \"${_create_body}\" > /dev/null 2>&1 && _recreated=true && break\n log \" Attempt ${_attempt}/3 failed for ${_worker_name}, retrying in $((5 * _attempt))s...\"\n sleep $((5 * _attempt))\n done\n if [ \"${_recreated}\" = true ]; then\n log \" Recreated ${_runtime} worker: ${_worker_name}\"\n else\n log \" ERROR: Failed to recreate ${_worker_name} after 3 attempts\"\n fi\n else\n log \" WARNING: No credentials found for ${_worker_name} (${_creds_file} missing), skipping\"\n fi\n done\n fi\nfi\n\n# ============================================================\n# Notify workers of builtin updates if upgrade happened\n# Builtin files (AGENTS.md, skills) are already synced by upgrade-builtins.sh\n#\n# Cooldown: skip notification if the last successful notify was within\n# NOTIFY_COOLDOWN_SECS (default 3600s / 1 hour). This prevents repeated\n# notifications when the Manager crash-loops and re-runs upgrade-builtins\n# on every restart (e.g. IMAGE_VERSION=latest always triggers upgrade).\n# ============================================================\nNOTIFY_COOLDOWN_SECS=\"${AGENTTEAMS_NOTIFY_COOLDOWN_SECS:-3600}\"\nNOTIFY_TS_FILE=\"/root/manager-workspace/.last-worker-notify-ts\"\n\nif [ -f /root/manager-workspace/.upgrade-pending-worker-notify ]; then\n _now=$(date +%s)\n _last_notify=$(cat \"${NOTIFY_TS_FILE}\" 2>/dev/null || echo \"0\")\n _elapsed=$(( _now - _last_notify ))\n\n if [ \"${_elapsed}\" -lt \"${NOTIFY_COOLDOWN_SECS}\" ]; then\n log \"Skipping worker builtin notification (last notify ${_elapsed}s ago, cooldown ${NOTIFY_COOLDOWN_SECS}s)\"\n rm -f /root/manager-workspace/.upgrade-pending-worker-notify\n else\n log \"Notifying workers about builtin updates...\"\n REGISTRY_FILE=\"/root/manager-workspace/workers-registry.json\"\n _notify_ok=false\n if [ -f \"${REGISTRY_FILE}\" ]; then\n for _worker_name in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_worker_name}\" ] && continue\n _room_id=$(jq -r --arg w \"${_worker_name}\" '.workers[$w].room_id // empty' \"${REGISTRY_FILE}\" 2>/dev/null)\n if [ -n \"${_room_id}\" ]; then\n _worker_id=\"@${_worker_name}:${MATRIX_DOMAIN}\"\n _txn_id=\"upgrade-$(date +%s%N)\"\n _msg=\"@${_worker_name}:${MATRIX_DOMAIN} Manager upgraded builtin files (AGENTS.md, skills). Please use your file-sync skill to sync the latest config.\"\n _raw=$(curl -s -w '\\nHTTP_CODE:%{http_code}' -X PUT \\\n \"${AGENTTEAMS_MATRIX_URL}/_matrix/client/v3/rooms/${_room_id}/send/m.room.message/${_txn_id}\" \\\n -H \"Authorization: Bearer ${MANAGER_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d \"{\\\"msgtype\\\":\\\"m.text\\\",\\\"body\\\":\\\"${_msg}\\\",\\\"m.mentions\\\":{\\\"user_ids\\\":[\\\"${_worker_id}\\\"]}}\" \\\n 2>&1) || true\n _http_code=$(echo \"${_raw}\" | tail -1 | sed 's/HTTP_CODE://')\n _notify_resp=$(echo \"${_raw}\" | sed '$d')\n if echo \"${_notify_resp}\" | jq -e '.event_id' > /dev/null 2>&1; then\n log \" Notified ${_worker_name}\"; _notify_ok=true\n else\n log \" WARNING: Failed to notify ${_worker_name} (HTTP ${_http_code}): ${_notify_resp}\"\n fi\n fi\n done\n fi\n # Record timestamp only if at least one notification succeeded\n if [ \"${_notify_ok}\" = true ]; then\n echo \"${_now}\" > \"${NOTIFY_TS_FILE}\"\n fi\n rm -f /root/manager-workspace/.upgrade-pending-worker-notify\n fi\nfi\n\n# ============================================================\n# Start Manager Agent\n# ============================================================\nlog \"Starting Manager Agent (${MANAGER_RUNTIME})...\"\n\n# HOME is already set to /root/manager-workspace via docker run -e HOME=...\ncd \"${HOME}\"\n\n# Ensure host credential symlinks exist under HOME\nif [ -d \"/host-share\" ]; then\n [ -f \"/host-share/.gitconfig\" ] && ln -sf \"/host-share/.gitconfig\" \"${HOME}/.gitconfig\"\nfi\n\nlog \"HOME=${HOME} (manager-workspace, host-mounted)\"\n\n# ── Render agent doc templates ────────────────────────────────────────────\n# Replace ${VAR} placeholders with actual values so the AI agent reads\n# plain text and never needs to resolve environment variables.\nexport MANAGER_MATRIX_TOKEN MANAGER_TOKEN HIGRESS_COOKIE_FILE\nRENDER=/opt/agentteams/scripts/lib/render-skills.sh\nlog \"Rendering agent doc templates...\"\n# Manager-owned docs (workspace)\nbash \"$RENDER\" /root/manager-workspace/skills\nbash \"$RENDER\" /root/manager-workspace/skills-alpha\nbash \"$RENDER\" /root/manager-workspace AGENTS.md TOOLS.md HEARTBEAT.md SOUL.md\n# Worker templates (workspace + image) — rendered before push to MinIO\n# so Workers (including remote pip-install) receive plain text\nbash \"$RENDER\" /root/manager-workspace/worker-skills\nbash \"$RENDER\" /root/manager-workspace/worker-agent\nbash \"$RENDER\" /root/manager-workspace/copaw-worker-agent\nbash \"$RENDER\" /root/manager-workspace/hermes-worker-agent\nbash \"$RENDER\" /opt/agentteams/agent/worker-skills\nbash \"$RENDER\" /opt/agentteams/agent/worker-agent\nbash \"$RENDER\" /opt/agentteams/agent/copaw-worker-agent\nbash \"$RENDER\" /opt/agentteams/agent/hermes-worker-agent\nlog \"Agent doc templates rendered\"\n\n# Cloud mode: start background file sync (workspace ↔ OSS) and initial push\nif [ \"${AGENTTEAMS_RUNTIME}\" = \"aliyun\" ]; then\n log \"Syncing initial workspace to OSS...\"\n ensure_mc_credentials\n mc mirror /root/manager-workspace/ \"${AGENTTEAMS_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" 2>/dev/null || true\n\n # Local → OSS: change-triggered sync\n (\n while true; do\n CHANGED=$(find /root/manager-workspace/ -type f -newermt \"15 seconds ago\" 2>/dev/null | head -1)\n if [ -n \"${CHANGED}\" ]; then\n ensure_mc_credentials 2>/dev/null || true\n mc mirror /root/manager-workspace/ \"${AGENTTEAMS_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" --exclude \".npm/**\" \\\n --exclude \".local/**\" --exclude \".mc/**\" 2>/dev/null || true\n fi\n sleep 10\n done\n ) &\n log \"Local→OSS sync started (PID: $!)\"\n\n # OSS → Local: periodic pull (shared data, agent configs, manager openclaw.json)\n (\n while true; do\n sleep 60\n ensure_mc_credentials 2>/dev/null || true\n mc mirror \"${AGENTTEAMS_STORAGE_PREFIX}/shared/\" /root/agentteams-fs/shared/ --overwrite --newer-than \"1m\" 2>/dev/null || true\n mc mirror \"${AGENTTEAMS_STORAGE_PREFIX}/agents/\" /root/agentteams-fs/agents/ --overwrite --newer-than \"1m\" 2>/dev/null || true\n mc cp \"${AGENTTEAMS_STORAGE_PREFIX}/manager/openclaw.json\" /root/manager-workspace/openclaw.json 2>/dev/null || true\n done\n ) &\n log \"OSS→Local sync started (every 60s, PID: $!)\"\nfi\n\n# K8s mode: start background file sync (workspace ↔ MinIO)\nif [ \"${AGENTTEAMS_RUNTIME}\" = \"k8s\" ]; then\n log \"Syncing initial workspace to MinIO...\"\n mc mirror /root/manager-workspace/ \"${AGENTTEAMS_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" 2>/dev/null || true\n\n # Local → MinIO: change-triggered sync\n (\n while true; do\n CHANGED=$(find /root/manager-workspace/ -type f -newermt \"15 seconds ago\" 2>/dev/null | head -1)\n if [ -n \"${CHANGED}\" ]; then\n mc mirror /root/manager-workspace/ \"${AGENTTEAMS_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" --exclude \".npm/**\" \\\n --exclude \".local/**\" --exclude \".mc/**\" 2>/dev/null || true\n fi\n sleep 10\n done\n ) &\n log \"Local→MinIO sync started (PID: $!)\"\n\n # MinIO → Local: periodic pull (shared data, agent configs, manager openclaw.json)\n (\n while true; do\n sleep 60\n mc mirror \"${AGENTTEAMS_STORAGE_PREFIX}/shared/\" /root/agentteams-fs/shared/ --overwrite --newer-than \"1m\" 2>/dev/null || true\n mc mirror \"${AGENTTEAMS_STORAGE_PREFIX}/agents/\" /root/agentteams-fs/agents/ --overwrite --newer-than \"1m\" 2>/dev/null || true\n mc mirror \"${AGENTTEAMS_STORAGE_PREFIX}/agentteams-config/\" /root/agentteams-fs/agentteams-config/ --overwrite --newer-than \"1m\" 2>/dev/null || true\n mc cp \"${AGENTTEAMS_STORAGE_PREFIX}/manager/openclaw.json\" /root/manager-workspace/openclaw.json 2>/dev/null || true\n done\n ) &\n log \"MinIO→Local sync started (every 60s, PID: $!)\"\nfi\n\n# ============================================================\n# Auto-generate Manager mcporter config for pre-configured MCP servers\n# If AGENTTEAMS_GITHUB_TOKEN was set at install time, setup-higress.sh already\n# configured GitHub MCP on Higress. Run setup-mcp-server.sh now so that\n# config/mcporter.json exists when the Agent starts — no need to ask user for PAT.\n# ============================================================\nif [ -n \"${AGENTTEAMS_GITHUB_TOKEN}\" ] && [ \"${AGENTTEAMS_RUNTIME}\" != \"aliyun\" ] && [ \"${AGENTTEAMS_RUNTIME}\" != \"k8s\" ]; then\n if [ ! -f \"${HOME}/config/mcporter.json\" ]; then\n log \"Auto-generating Manager mcporter config for GitHub MCP (AGENTTEAMS_GITHUB_TOKEN set)...\"\n bash /opt/agentteams/agent/skills/mcp-server-management/scripts/setup-mcp-server.sh \\\n github \"${AGENTTEAMS_GITHUB_TOKEN}\" 2>&1 | while IFS= read -r line; do log \" [setup-mcp] ${line}\"; done || \\\n log \"WARNING: setup-mcp-server.sh failed — Agent may need to configure GitHub MCP manually\"\n else\n log \"Manager mcporter config already exists, skipping auto-generate\"\n fi\nfi\n\n# ============================================================\n# Runtime-specific startup\n# ============================================================\nif [ \"${MANAGER_RUNTIME}\" = \"copaw\" ]; then\n # Delegate to CoPaw startup script\n exec /opt/agentteams/scripts/init/start-copaw-manager.sh\nelse\n # ── OpenClaw Runtime ─────────────────────────────────────────────────────\n log \"Starting OpenClaw Manager...\"\n\n export OPENCLAW_CONFIG_PATH=\"/root/manager-workspace/openclaw.json\"\n\n # Symlink to default OpenClaw config path so CLI commands find the config\n mkdir -p \"${HOME}/.openclaw\"\n ln -sf \"/root/manager-workspace/openclaw.json\" \"${HOME}/.openclaw/openclaw.json\"\n\n # Clean orphaned session write locks (e.g. from SIGKILL or crash before exit handlers)\n # Prevents \"session file locked (timeout 10000ms)\" when PID was reused\n find \"${HOME}/.openclaw/agents\" -name \"*.jsonl.lock\" -delete 2>/dev/null || true\n log \"Cleaned up any orphaned session write locks\"\n\n # Clean Matrix crypto storage (SQLite WAL may be corrupted after unclean shutdown)\n # Crypto state is re-negotiated on startup; losing it only means re-establishing E2EE sessions\n rm -rf \"${HOME}/.openclaw/matrix\" 2>/dev/null || true\n log \"Cleaned Matrix crypto storage (will re-establish E2EE sessions)\"\n\n # Launch OpenClaw\n # Disable full-process respawn so the CLI uses its internal restart loop.\n # Without this, config reload spawns a detached child and exits, then\n # supervisord restarts the CLI — resulting in two gateway processes.\n export OPENCLAW_NO_RESPAWN=1\n\n # Optional matrix-plugin trace logging — when AGENTTEAMS_MATRIX_DEBUG=1 is set\n # in the manager environment (propagated by install / supervisord), turn on\n # OPENCLAW_MATRIX_DEBUG so the matrix plugin emits structured INFO-level\n # lifecycle traces (sync.state transitions, room.invite/join, message\n # handler arrival + filter outcomes). Useful for diagnosing \"worker never\n # joined the room\" / \"manager never replied\" hangs without rebuilding the\n # image.\n if [ \"${AGENTTEAMS_MATRIX_DEBUG:-}\" = \"1\" ] && [ -z \"${OPENCLAW_MATRIX_DEBUG:-}\" ]; then\n export OPENCLAW_MATRIX_DEBUG=1\n log \"AGENTTEAMS_MATRIX_DEBUG=1 detected; OPENCLAW_MATRIX_DEBUG=1 exported for matrix plugin tracing\"\n fi\n\n exec openclaw gateway run --verbose --force\nfi\n"}, "a7b707efcbb28cf09f68af8d77387e94c34cfc37": {"t": 1785764737.0409908, "n": 69517, "h": "d871c90e45600aa7", "c": "#!/bin/bash\n# start-manager-agent.sh - Initialize and start the Manager Agent\n# Supports local (supervisord), cloud (SAE), and K8s (Helm) deployments.\n# In local mode this is the last supervisord component to start (priority 800).\n# In cloud/k8s mode (AGENTTEAMS_RUNTIME=aliyun|k8s) this is the container entrypoint.\n#\n# Runtime selection:\n# AGENTTEAMS_MANAGER_RUNTIME=openclaw (default) - OpenClaw gateway mode\n# AGENTTEAMS_MANAGER_RUNTIME=copaw - CoPaw workspace mode\n# (hermes runtime is supported for Workers only; Managers run openclaw or copaw.)\n\nsource /opt/hiclaw/scripts/lib/hiclaw-env.sh\n\n# ============================================================\n# Runtime selection\n# ============================================================\nMANAGER_RUNTIME=\"${AGENTTEAMS_MANAGER_RUNTIME:-openclaw}\"\ncase \"${MANAGER_RUNTIME}\" in\n copaw)\n log \"Manager runtime: CoPaw (Python workspace)\"\n ;;\n *)\n log \"Manager runtime: OpenClaw (Node.js gateway)\"\n MANAGER_RUNTIME=\"openclaw\"\n ;;\nesac\n\n# ============================================================\n# Set timezone from TZ env var\n# ============================================================\nif [ -n \"${TZ}\" ] && [ -f \"/usr/share/zoneinfo/${TZ}\" ]; then\n ln -sf \"/usr/share/zoneinfo/${TZ}\" /etc/localtime\n echo \"${TZ}\" > /etc/timezone\n log \"Timezone set to ${TZ}\"\nfi\n\nexport MATRIX_DOMAIN=\"${AGENTTEAMS_MATRIX_DOMAIN:-matrix-local.agentteams.io:8080}\"\nAI_GATEWAY_DOMAIN=\"${AGENTTEAMS_AI_GATEWAY_DOMAIN:-aigw-local.agentteams.io}\"\n\n# ============================================================\n# YOLO mode promotion\n# ============================================================\n# In embedded mode the controller does not propagate AGENTTEAMS_YOLO to the\n# manager container, but installer / test scripts touch a marker file at\n# `${WORKSPACE}/yolo-mode` instead. Promote that marker to the env var so the\n# agent's documented YOLO check (`AGENTTEAMS_YOLO=1`) reliably detects it without\n# depending on filesystem lookups during a turn.\nif [ -z \"${AGENTTEAMS_YOLO:-}\" ] && [ -f /root/manager-workspace/yolo-mode ]; then\n export AGENTTEAMS_YOLO=1\n log \"YOLO mode marker detected at /root/manager-workspace/yolo-mode; AGENTTEAMS_YOLO=1 exported\"\nfi\n\n# ============================================================\n# Cloud/K8s mode: validate required environment variables + initial credentials\n# ============================================================\nif [ \"${AGENTTEAMS_RUNTIME}\" = \"aliyun\" ] || [ \"${AGENTTEAMS_RUNTIME}\" = \"k8s\" ]; then\n : \"${AGENTTEAMS_MATRIX_URL:?AGENTTEAMS_MATRIX_URL is required}\"\n : \"${AGENTTEAMS_MATRIX_DOMAIN:?AGENTTEAMS_MATRIX_DOMAIN is required}\"\n : \"${AGENTTEAMS_AI_GATEWAY_URL:?AGENTTEAMS_AI_GATEWAY_URL is required}\"\n if [ \"${AGENTTEAMS_RUNTIME}\" = \"aliyun\" ]; then\n : \"${AGENTTEAMS_MANAGER_GATEWAY_KEY:?AGENTTEAMS_MANAGER_GATEWAY_KEY is required}\"\n : \"${AGENTTEAMS_MANAGER_PASSWORD:?AGENTTEAMS_MANAGER_PASSWORD is required (cloud containers are stateless, password must be injected)}\"\n fi\n if [ \"${AGENTTEAMS_RUNTIME}\" = \"k8s\" ]; then\n # K8s mode: controller handles initialization (admin registration, Higress setup).\n # Manager only needs credentials injected by the ManagerReconciler.\n : \"${AGENTTEAMS_MANAGER_GATEWAY_KEY:?AGENTTEAMS_MANAGER_GATEWAY_KEY is required (injected by controller)}\"\n # AGENTTEAMS_MANAGER_PASSWORD is optional: not needed in AppService mode\n # (token obtained via AS login), only required in legacy password mode.\n else\n # Cloud (aliyun) mode: Manager still does its own initialization\n : \"${AGENTTEAMS_REGISTRATION_TOKEN:?AGENTTEAMS_REGISTRATION_TOKEN is required}\"\n : \"${AGENTTEAMS_ADMIN_USER:?AGENTTEAMS_ADMIN_USER is required}\"\n : \"${AGENTTEAMS_ADMIN_PASSWORD:?AGENTTEAMS_ADMIN_PASSWORD is required}\"\n fi\n log \"${AGENTTEAMS_RUNTIME} mode: validating environment... OK\"\n log \" Matrix: ${AGENTTEAMS_MATRIX_URL}, AI Gateway: ${AGENTTEAMS_AI_GATEWAY_URL}, Storage: ${AGENTTEAMS_FS_BUCKET}\"\n if [ \"${AGENTTEAMS_RUNTIME}\" = \"aliyun\" ]; then\n ensure_mc_credentials || { log \"FATAL: Initial STS credential fetch failed\"; exit 1; }\n fi\nfi\n\n# ============================================================\n# Local mode: host symlinks, /etc/hosts, wait for local services\n# ============================================================\nif [ \"${AGENTTEAMS_RUNTIME}\" != \"aliyun\" ] && [ \"${AGENTTEAMS_RUNTIME}\" != \"k8s\" ]; then\n # Create symlink for host directory access\n if [ -d \"/host-share\" ]; then\n ORIGINAL_HOST_HOME=\"${HOST_ORIGINAL_HOME:-$HOME}\"\n if [ ! -e \"${ORIGINAL_HOST_HOME}\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/root\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/data\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/host-share\" ]; then\n mkdir -p \"$(dirname \"${ORIGINAL_HOST_HOME}\")\"\n ln -sfn /host-share \"${ORIGINAL_HOST_HOME}\"\n log \"Created symlink: ${ORIGINAL_HOST_HOME} -> /host-share\"\n else\n ln -sfn /host-share /root/host-home\n log \"Created fallback symlink: /root/host-home -> /host-share\"\n fi\n fi\n\n # Add local domains to /etc/hosts\n HOSTS_DOMAINS=\"${MATRIX_DOMAIN%%:*} ${AGENTTEAMS_MATRIX_CLIENT_DOMAIN:-matrix-client-local.agentteams.io} ${AI_GATEWAY_DOMAIN} ${AGENTTEAMS_FS_DOMAIN:-fs-local.agentteams.io}\"\n if ! grep -q \"${AI_GATEWAY_DOMAIN}\" /etc/hosts 2>/dev/null; then\n echo \"127.0.0.1 ${HOSTS_DOMAINS}\" >> /etc/hosts\n log \"Added local domains to /etc/hosts\"\n fi\n\n # Wait for local infrastructure\n waitForService \"Higress Gateway\" \"127.0.0.1\" 8080 180\n waitForService \"Higress Console\" \"127.0.0.1\" 8001 180\n waitForService \"Tuwunel\" \"127.0.0.1\" 6167 120\n waitForHTTP \"Tuwunel Matrix API\" \"${AGENTTEAMS_MATRIX_URL}/_tuwunel/server_version\" 120\n waitForService \"MinIO\" \"127.0.0.1\" 9000 120\nelse\n # Cloud/K8s mode: wait for external Tuwunel\n log \"Waiting for Tuwunel Matrix server at ${AGENTTEAMS_MATRIX_URL}...\"\n _retry=0\n while [ \"${_retry}\" -lt 30 ]; do\n if curl -sf \"${AGENTTEAMS_MATRIX_URL}/_matrix/client/versions\" > /dev/null 2>&1; then\n log \"Tuwunel is ready\"\n break\n fi\n _retry=$((_retry + 1))\n log \" Waiting for Tuwunel (attempt ${_retry}/30)...\"\n sleep 5\n done\n if [ \"${_retry}\" -ge 30 ]; then\n log \"ERROR: Tuwunel not reachable at ${AGENTTEAMS_MATRIX_URL}\"\n exit 1\n fi\nfi\n\n# ============================================================\n# Auto-generate secrets if not provided via environment\n# Persisted to /data so they survive container restart\n# ============================================================\nSECRETS_FILE=\"/data/hiclaw-secrets.env\"\nif [ -f \"${SECRETS_FILE}\" ]; then\n source \"${SECRETS_FILE}\"\n log \"Loaded persisted secrets from ${SECRETS_FILE}\"\nfi\n\nif [ -z \"${AGENTTEAMS_MANAGER_GATEWAY_KEY}\" ]; then\n export AGENTTEAMS_MANAGER_GATEWAY_KEY=\"$(generateKey 32)\"\n log \"Auto-generated AGENTTEAMS_MANAGER_GATEWAY_KEY\"\nfi\nif [ -z \"${AGENTTEAMS_MANAGER_PASSWORD}\" ]; then\n export AGENTTEAMS_MANAGER_PASSWORD=\"$(generateKey 16)\"\n log \"Auto-generated AGENTTEAMS_MANAGER_PASSWORD\"\nfi\n\n# Persist secrets so they survive supervisord restart\nmkdir -p /data\ncat > \"${SECRETS_FILE}\" <<EOF\nexport AGENTTEAMS_MANAGER_GATEWAY_KEY=\"${AGENTTEAMS_MANAGER_GATEWAY_KEY}\"\nexport AGENTTEAMS_MANAGER_PASSWORD=\"${AGENTTEAMS_MANAGER_PASSWORD}\"\nEOF\nchmod 600 \"${SECRETS_FILE}\"\n\n# Cloud mode: pull workspace from OSS before initialization\nif [ \"${AGENTTEAMS_RUNTIME}\" = \"aliyun\" ]; then\n AGENTTEAMS_FS=\"/root/hiclaw-fs\"\n mkdir -p \"${AGENTTEAMS_FS}/shared\" \"${AGENTTEAMS_FS}/agents\"\n log \"Pulling workspace from OSS...\"\n ensure_mc_credentials\n mc mirror \"${AGENTTEAMS_STORAGE_PREFIX}/manager/\" /root/manager-workspace/ --overwrite 2>/dev/null || true\n mc mirror \"${AGENTTEAMS_STORAGE_PREFIX}/shared/\" \"${AGENTTEAMS_FS}/shared/\" --overwrite 2>/dev/null || true\n mc mirror \"${AGENTTEAMS_STORAGE_PREFIX}/agents/\" \"${AGENTTEAMS_FS}/agents/\" --overwrite 2>/dev/null || true\n # Symlink hiclaw-fs into workspace for agent access\n ln -sfn \"${AGENTTEAMS_FS}\" /root/manager-workspace/hiclaw-fs\nfi\n\n# K8s mode: sync workspace from cluster-internal MinIO\nif [ \"${AGENTTEAMS_RUNTIME}\" = \"k8s\" ]; then\n AGENTTEAMS_FS=\"/root/hiclaw-fs\"\n mkdir -p \"${AGENTTEAMS_FS}/shared\" \"${AGENTTEAMS_FS}/agents\" \"${AGENTTEAMS_FS}/agentteams-config\"\n log \"Configuring mc alias for cluster MinIO...\"\n mc alias set agentteams \"${AGENTTEAMS_FS_ENDPOINT}\" \"${AGENTTEAMS_FS_ACCESS_KEY}\" \"${AGENTTEAMS_FS_SECRET_KEY}\"\n log \"Syncing workspace from MinIO...\"\n mc mirror \"${AGENTTEAMS_STORAGE_PREFIX}/manager/\" /root/manager-workspace/ --overwrite 2>/dev/null || true\n mc mirror \"${AGENTTEAMS_STORAGE_PREFIX}/\" \"${AGENTTEAMS_FS}/\" --overwrite 2>/dev/null || true\n ln -sfn \"${AGENTTEAMS_FS}\" /root/manager-workspace/hiclaw-fs\n touch \"${AGENTTEAMS_FS}/.initialized\"\nfi\n\n# ============================================================\n# Initialize / upgrade Manager workspace\n# First boot: full init via upgrade-builtins.sh\n# Subsequent boots: compare image version; upgrade only if changed\n# ============================================================\nmkdir -p /root/manager-workspace\n\nIMAGE_VERSION=$(cat /opt/hiclaw/agent/.builtin-version 2>/dev/null || echo \"unknown\")\nINSTALLED_VERSION=$(cat /root/manager-workspace/.builtin-version 2>/dev/null || echo \"\")\n\nif [ ! -f /root/manager-workspace/.initialized ]; then\n log \"First boot: initializing manager workspace...\"\n bash /opt/hiclaw/scripts/init/upgrade-builtins.sh\n touch /root/manager-workspace/.initialized\n log \"Manager workspace initialized (version: ${IMAGE_VERSION})\"\nelif [ \"${IMAGE_VERSION}\" != \"${INSTALLED_VERSION}\" ] || [ \"${IMAGE_VERSION}\" = \"latest\" ]; then\n log \"Upgrade detected: ${INSTALLED_VERSION} -> ${IMAGE_VERSION}${IMAGE_VERSION:+ (latest: always upgrade)}\"\n bash /opt/hiclaw/scripts/init/upgrade-builtins.sh\n log \"Manager workspace upgraded to version: ${IMAGE_VERSION}\"\nelse\n log \"Workspace up to date (version: ${IMAGE_VERSION})\"\nfi\n\n# Local mode: wait for mc mirror initialization (shared + worker data in /root/hiclaw-fs/)\nif [ \"${AGENTTEAMS_RUNTIME}\" != \"aliyun\" ] && [ \"${AGENTTEAMS_RUNTIME}\" != \"k8s\" ]; then\n log \"Waiting for MinIO storage initialization...\"\n _minio_wait=0\n while [ ! -f /root/hiclaw-fs/.initialized ]; do\n sleep 2\n _minio_wait=$(( _minio_wait + 1 ))\n if [ \"${_minio_wait}\" -ge 60 ]; then\n log \"ERROR: MinIO storage initialization timed out after 120s\"\n exit 1\n fi\n done\n log \"MinIO storage initialized\"\nfi\n\n# ============================================================\n# Obtain Manager Matrix access token.\n#\n# Priority:\n# 1. Pre-injected token (AGENTTEAMS_MANAGER_MATRIX_TOKEN) — set by the\n# controller in both AppService mode (where no password exists) and\n# legacy mode (to avoid a redundant login round-trip).\n# 2. Password-based login — fallback for containers started without\n# controller involvement (manual docker run, legacy installs).\n# ============================================================\nif [ -n \"${AGENTTEAMS_MANAGER_MATRIX_TOKEN:-}\" ]; then\n MANAGER_TOKEN=\"${AGENTTEAMS_MANAGER_MATRIX_TOKEN}\"\n log \"Manager Matrix token pre-injected by controller (token prefix: ${MANAGER_TOKEN:0:10}...)\"\nelif [ \"${AGENTTEAMS_RUNTIME}\" = \"k8s\" ]; then\n log \"K8s mode: obtaining Manager Matrix token via password login...\"\n _LOGIN_RESPONSE=$(curl -s -X POST ${AGENTTEAMS_MATRIX_URL}/_matrix/client/v3/login \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"type\": \"m.login.password\",\n \"identifier\": {\"type\": \"m.id.user\", \"user\": \"manager\"},\n \"password\": \"'\"${AGENTTEAMS_MANAGER_PASSWORD}\"'\"\n }' 2>&1)\n MANAGER_TOKEN=$(echo \"${_LOGIN_RESPONSE}\" | jq -r '.access_token' 2>/dev/null)\n if [ -z \"${MANAGER_TOKEN}\" ] || [ \"${MANAGER_TOKEN}\" = \"null\" ]; then\n log \"ERROR: Failed to obtain Manager Matrix token\"\n log \"ERROR: Login response was: ${_LOGIN_RESPONSE}\"\n exit 1\n fi\n log \"Manager Matrix token obtained (token prefix: ${MANAGER_TOKEN:0:10}...)\"\nelse\nlog \"Registering human admin Matrix account...\"\ncurl -sf -X POST ${AGENTTEAMS_MATRIX_URL}/_matrix/client/v3/register \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"username\": \"'\"${AGENTTEAMS_ADMIN_USER}\"'\",\n \"password\": \"'\"${AGENTTEAMS_ADMIN_PASSWORD}\"'\",\n \"auth\": {\n \"type\": \"m.login.registration_token\",\n \"token\": \"'\"${AGENTTEAMS_REGISTRATION_TOKEN}\"'\"\n }\n }' > /dev/null 2>&1 || log \"Admin account may already exist\"\n\nlog \"Registering Manager Agent Matrix account...\"\ncurl -sf -X POST ${AGENTTEAMS_MATRIX_URL}/_matrix/client/v3/register \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"username\": \"manager\",\n \"password\": \"'\"${AGENTTEAMS_MANAGER_PASSWORD}\"'\",\n \"auth\": {\n \"type\": \"m.login.registration_token\",\n \"token\": \"'\"${AGENTTEAMS_REGISTRATION_TOKEN}\"'\"\n }\n }' > /dev/null 2>&1 || log \"Manager account may already exist\"\n\n# Get Manager Agent's Matrix access token\nlog \"Obtaining Manager Matrix access token...\"\n_LOGIN_RESPONSE=$(curl -s -X POST ${AGENTTEAMS_MATRIX_URL}/_matrix/client/v3/login \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"type\": \"m.login.password\",\n \"identifier\": {\"type\": \"m.id.user\", \"user\": \"manager\"},\n \"password\": \"'\"${AGENTTEAMS_MANAGER_PASSWORD}\"'\"\n }' 2>&1)\n_LOGIN_EXIT=$?\nlog \"Matrix login HTTP exit code: ${_LOGIN_EXIT}\"\nlog \"Matrix login response: ${_LOGIN_RESPONSE}\"\n\nMANAGER_TOKEN=$(echo \"${_LOGIN_RESPONSE}\" | jq -r '.access_token' 2>/dev/null)\n\nif [ -z \"${MANAGER_TOKEN}\" ] || [ \"${MANAGER_TOKEN}\" = \"null\" ]; then\n log \"ERROR: Failed to obtain Manager Matrix token (exit=${_LOGIN_EXIT})\"\n log \"ERROR: Login response was: ${_LOGIN_RESPONSE}\"\n exit 1\nfi\nlog \"Manager Matrix token obtained (token prefix: ${MANAGER_TOKEN:0:10}...)\"\nfi\n\n# ============================================================\n# Higress Console initialization\n# Docker mode: full setup-higress.sh (internal Higress at localhost:8001)\n# K8s mode: skip — controller Initializer handles Higress setup\n# Cloud (aliyun) mode: skip entirely (Higress managed externally)\n# ============================================================\n_HIGRESS_CONSOLE_URL=\"\"\n_HIGRESS_USER=\"${AGENTTEAMS_ADMIN_USER}\"\n_HIGRESS_PASS=\"${AGENTTEAMS_ADMIN_PASSWORD}\"\nif [ \"${AGENTTEAMS_RUNTIME}\" = \"k8s\" ]; then\n log \"K8s mode: skipping Higress initialization (handled by controller)\"\nelif [ \"${AGENTTEAMS_RUNTIME}\" != \"aliyun\" ]; then\n _HIGRESS_CONSOLE_URL=\"http://127.0.0.1:8001\"\nfi\n\nif [ -n \"${_HIGRESS_CONSOLE_URL}\" ]; then\n COOKIE_FILE=\"/tmp/higress-session-cookie\"\n\n log \"Waiting for Higress Console (${_HIGRESS_CONSOLE_URL}) to be fully ready and initializing admin...\"\n INIT_DONE=false\n for i in $(seq 1 90); do\n INIT_RESULT=$(curl -s -X POST \"${_HIGRESS_CONSOLE_URL}/system/init\" \\\n -H 'Content-Type: application/json' \\\n -d '{\"adminUser\":{\"name\":\"'\"${_HIGRESS_USER}\"'\",\"password\":\"'\"${_HIGRESS_PASS}\"'\",\"displayName\":\"'\"${_HIGRESS_USER}\"'\"}}' 2>/dev/null) || true\n if echo \"${INIT_RESULT}\" | grep -qE '\"success\":true|already.?init' 2>/dev/null; then\n INIT_DONE=true\n break\n fi\n if echo \"${INIT_RESULT}\" | grep -q '\"name\"' 2>/dev/null; then\n INIT_DONE=true\n break\n fi\n sleep 2\n done\n\n if [ \"${INIT_DONE}\" != \"true\" ]; then\n log \"ERROR: Higress Console did not become ready within 180s\"\n exit 1\n fi\n log \"Higress Console init done\"\n\n log \"Logging into Higress Console...\"\n LOGIN_OK=false\n for i in $(seq 1 10); do\n HTTP_CODE=$(curl -s -o /dev/null -w '%{http_code}' -X POST \"${_HIGRESS_CONSOLE_URL}/session/login\" \\\n -H 'Content-Type: application/json' \\\n -c \"${COOKIE_FILE}\" \\\n -d '{\"username\":\"'\"${_HIGRESS_USER}\"'\",\"password\":\"'\"${_HIGRESS_PASS}\"'\"}' 2>/dev/null) || true\n if { [ \"${HTTP_CODE}\" = \"200\" ] || [ \"${HTTP_CODE}\" = \"201\" ]; } && [ -f \"${COOKIE_FILE}\" ] && [ -s \"${COOKIE_FILE}\" ]; then\n LOGIN_OK=true\n break\n fi\n log \"Login attempt $i (HTTP ${HTTP_CODE}), retrying in 3s...\"\n sleep 3\n done\n\n if [ \"${LOGIN_OK}\" != \"true\" ]; then\n log \"ERROR: Could not login to Higress Console after retries\"\n exit 1\n fi\n log \"Higress Console login successful\"\n\n VERIFY_CODE=$(curl -s -o /dev/null -w '%{http_code}' \"${_HIGRESS_CONSOLE_URL}/v1/consumers\" -b \"${COOKIE_FILE}\" 2>/dev/null) || true\n if [ \"${VERIFY_CODE}\" = \"200\" ]; then\n log \"Console session verified (cookie valid)\"\n else\n log \"WARNING: Console session may be invalid (verify returned HTTP ${VERIFY_CODE})\"\n rm -f \"${COOKIE_FILE}\"\n for i in $(seq 1 5); do\n curl -s -o /dev/null -w '%{http_code}' -X POST \"${_HIGRESS_CONSOLE_URL}/session/login\" \\\n -H 'Content-Type: application/json' \\\n -c \"${COOKIE_FILE}\" \\\n -d '{\"username\":\"'\"${_HIGRESS_USER}\"'\",\"password\":\"'\"${_HIGRESS_PASS}\"'\"}' 2>/dev/null\n VERIFY2=$(curl -s -o /dev/null -w '%{http_code}' \"${_HIGRESS_CONSOLE_URL}/v1/consumers\" -b \"${COOKIE_FILE}\" 2>/dev/null) || true\n if [ \"${VERIFY2}\" = \"200\" ]; then\n log \"Re-login successful, session verified\"\n break\n fi\n sleep 2\n done\n fi\n\n export HIGRESS_COOKIE_FILE=\"${COOKIE_FILE}\"\n export HIGRESS_CONSOLE_URL=\"${_HIGRESS_CONSOLE_URL}\"\n\n if [ \"${AGENTTEAMS_RUNTIME}\" = \"k8s\" ]; then\n # K8s mode: lightweight Higress config — only what's needed for LLM access\n source /opt/hiclaw/scripts/lib/base.sh\n _k8s_higress_api() {\n local method=\"$1\" path=\"$2\" desc=\"$3\"; shift 3; local body=\"$*\"\n local tmpfile; tmpfile=$(mktemp)\n local http_code\n http_code=$(curl -s -o \"${tmpfile}\" -w '%{http_code}' -X \"${method}\" \"${_HIGRESS_CONSOLE_URL}${path}\" \\\n -b \"${COOKIE_FILE}\" -H 'Content-Type: application/json' -d \"${body}\" 2>/dev/null) || true\n local response; response=$(cat \"${tmpfile}\" 2>/dev/null); rm -f \"${tmpfile}\"\n if echo \"${response}\" | grep -q '\"success\":true' 2>/dev/null; then\n log \"${desc} ... OK\"\n elif [ \"${http_code}\" = \"409\" ]; then\n log \"${desc} ... already exists, skipping\"\n elif [ \"${http_code}\" = \"200\" ] || [ \"${http_code}\" = \"201\" ] || [ \"${http_code}\" = \"204\" ]; then\n log \"${desc} ... OK (HTTP ${http_code})\"\n else\n log \"WARNING: ${desc} ... (HTTP ${http_code}): ${response}\"\n fi\n }\n\n # 1. Service Sources (DNS type → K8s Service FQDN)\n # Extract host:port from URLs for Higress service source registration\n _TUWUNEL_HOST=$(echo \"${AGENTTEAMS_MATRIX_URL}\" | sed 's|^http[s]*://||')\n _TUWUNEL_DOMAIN=$(echo \"${_TUWUNEL_HOST}\" | cut -d: -f1)\n _TUWUNEL_PORT=$(echo \"${_TUWUNEL_HOST}\" | cut -d: -f2)\n _k8s_higress_api POST /v1/service-sources \"Registering Tuwunel service source\" \\\n '{\"type\":\"dns\",\"name\":\"tuwunel\",\"domain\":\"'\"${_TUWUNEL_DOMAIN}\"'\",\"port\":'\"${_TUWUNEL_PORT}\"'}'\n\n if [ -n \"${AGENTTEAMS_ELEMENT_WEB_URL:-}\" ]; then\n _ELEMENT_HOST=$(echo \"${AGENTTEAMS_ELEMENT_WEB_URL}\" | sed 's|^http[s]*://||')\n _ELEMENT_DOMAIN=$(echo \"${_ELEMENT_HOST}\" | cut -d: -f1)\n _ELEMENT_PORT=$(echo \"${_ELEMENT_HOST}\" | cut -d: -f2)\n _k8s_higress_api POST /v1/service-sources \"Registering Element Web service source\" \\\n '{\"type\":\"dns\",\"name\":\"element-web\",\"domain\":\"'\"${_ELEMENT_DOMAIN}\"'\",\"port\":'\"${_ELEMENT_PORT}\"'}'\n fi\n\n # 2. Manager Consumer (key-auth)\n _k8s_higress_api POST /v1/consumers \"Creating Manager consumer\" \\\n '{\"name\":\"manager\",\"credentials\":[{\"type\":\"key-auth\",\"source\":\"BEARER\",\"values\":[\"'\"${AGENTTEAMS_MANAGER_GATEWAY_KEY}\"'\"]}]}'\n\n # 3. LLM Provider\n _LLM_PROVIDER=\"${AGENTTEAMS_LLM_PROVIDER:-qwen}\"\n _LLM_API_URL=\"${AGENTTEAMS_LLM_API_URL:-}\"\n if [ -z \"${_LLM_API_URL}\" ]; then\n case \"${_LLM_PROVIDER}\" in\n qwen) _LLM_API_URL=\"https://dashscope.aliyuncs.com/compatible-mode/v1\" ;;\n esac\n fi\n # 4. LLM Provider type-specific config\n case \"${_LLM_PROVIDER}\" in\n qwen)\n _k8s_higress_api POST /v1/ai/providers \"Creating LLM provider (qwen)\" \\\n '{\"type\":\"qwen\",\"name\":\"qwen\",\"tokens\":[\"'\"${AGENTTEAMS_LLM_API_KEY}\"'\"],\"protocol\":\"openai/v1\",\"tokenFailoverConfig\":{\"enabled\":false},\"rawConfigs\":{\"qwenEnableSearch\":false,\"qwenEnableCompatible\":true,\"qwenFileIds\":[],\"hiclawMode\":true}}'\n ;;\n *)\n _BODY='{\"name\":\"'\"${_LLM_PROVIDER}\"'\",\"type\":\"openai\",\"tokens\":[\"'\"${AGENTTEAMS_LLM_API_KEY}\"'\"],\"modelMapping\":{},\"protocol\":\"openai/v1\",\"rawConfigs\":{\"hiclawMode\":true}}'\n _k8s_higress_api POST /v1/ai/providers \"Creating LLM provider (${_LLM_PROVIDER})\" \"${_BODY}\"\n ;;\n esac\n\n # 5. AI Route (bind provider + consumer auth, /v1 prefix to avoid clash with Element Web catch-all)\n _k8s_higress_api POST /v1/ai/routes \"Creating AI Gateway route\" \\\n '{\"name\":\"default-ai-route\",\"domains\":[],\"pathPredicate\":{\"matchType\":\"PRE\",\"matchValue\":\"/v1\",\"caseSensitive\":false},\"upstreams\":[{\"provider\":\"'\"${_LLM_PROVIDER}\"'\",\"weight\":100,\"modelMapping\":{}}],\"authConfig\":{\"enabled\":true,\"allowedCredentialTypes\":[\"key-auth\"],\"allowedConsumers\":[\"manager\"]}}'\n\n # 6. Matrix Homeserver Route (/_matrix/* → Tuwunel, no auth)\n _k8s_higress_api POST /v1/routes \"Creating Matrix Homeserver route\" \\\n '{\"name\":\"matrix-homeserver\",\"domains\":[],\"path\":{\"matchType\":\"PRE\",\"matchValue\":\"/_matrix\"},\"services\":[{\"name\":\"tuwunel.dns\",\"port\":'\"${_TUWUNEL_PORT}\"',\"weight\":100}]}'\n\n # 7. Element Web Route (/ catch-all → Element Web, no auth)\n if [ -n \"${AGENTTEAMS_ELEMENT_WEB_URL:-}\" ]; then\n _k8s_higress_api POST /v1/routes \"Creating Element Web route\" \\\n '{\"name\":\"element-web\",\"domains\":[],\"path\":{\"matchType\":\"PRE\",\"matchValue\":\"/\"},\"services\":[{\"name\":\"element-web.dns\",\"port\":'\"${_ELEMENT_PORT}\"',\"weight\":100}]}'\n fi\n\n # 8. Remove Higress default landing page (Exact match on / takes precedence over Element Web catch-all)\n _k8s_higress_api DELETE /v1/routes/default \"Removing Higress default landing route\"\n\n log \"K8s Higress lightweight setup complete\"\n\n # Wait for AI plugin activation (~45s for first config)\n log \"Waiting for AI Gateway plugin activation (45s)...\"\n sleep 45\n else\n # Docker mode: full setup with all routes, domains, MCP servers\n /opt/hiclaw/scripts/init/setup-higress.sh\n fi\nfi\n\n# ============================================================\n# Create admin DM room, persist to state.json, send welcome message\n# K8s mode: skip — controller ProvisionManager creates the Admin DM\n# room (Step 4 in service/provisioner.go) AND reconcileManagerWelcome\n# delivers the first-boot onboarding prompt once OpenClaw inside this\n# container has joined the room. In k8s mode the manager intentionally\n# does NOT have the admin password (only AGENTTEAMS_ADMIN_USER), so it\n# could not log in as admin to send the welcome itself anyway. The\n# Manager Agent discovers its admin DM room on first heartbeat via\n# state.json / `manage-state.sh --action set-admin-dm` (see HEARTBEAT.md\n# Step 1) — it does not need it to be pre-injected by this script.\n# Runs in both local and cloud modes (idempotent)\n# ============================================================\nif [ \"${AGENTTEAMS_RUNTIME}\" = \"k8s\" ]; then\n log \"K8s mode: skipping admin DM room creation and welcome message (both handled by agentteams-controller)\"\nelse\nMANAGER_FULL_ID=\"@manager:${MATRIX_DOMAIN}\"\nADMIN_FULL_ID=\"@${AGENTTEAMS_ADMIN_USER}:${MATRIX_DOMAIN}\"\n\nlog \"Logging in as admin to create DM room...\"\n_ADMIN_LOGIN=$(curl -sf -X POST \"${AGENTTEAMS_MATRIX_URL}/_matrix/client/v3/login\" \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"type\": \"m.login.password\",\n \"identifier\": {\"type\": \"m.id.user\", \"user\": \"'\"${AGENTTEAMS_ADMIN_USER}\"'\"},\n \"password\": \"'\"${AGENTTEAMS_ADMIN_PASSWORD}\"'\"\n }' 2>&1) || true\n\nADMIN_MATRIX_TOKEN=$(echo \"${_ADMIN_LOGIN}\" | jq -r '.access_token // empty' 2>/dev/null)\nif [ -z \"${ADMIN_MATRIX_TOKEN}\" ]; then\n log \"WARNING: Failed to login as admin, skipping DM room creation\"\nelse\n # Search for existing DM room with Manager (idempotent)\n DM_ROOM_ID=\"\"\n _JOINED_ROOMS=$(curl -sf \"${AGENTTEAMS_MATRIX_URL}/_matrix/client/v3/joined_rooms\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" 2>/dev/null \\\n | jq -r '.joined_rooms[]' 2>/dev/null) || true\n for _rid in ${_JOINED_ROOMS}; do\n _members=$(curl -sf \"${AGENTTEAMS_MATRIX_URL}/_matrix/client/v3/rooms/${_rid}/members\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" 2>/dev/null \\\n | jq -r '.chunk[].state_key' 2>/dev/null) || continue\n _count=$(echo \"${_members}\" | wc -l | xargs)\n if [ \"${_count}\" = \"2\" ] && echo \"${_members}\" | grep -q \"@manager:\"; then\n DM_ROOM_ID=\"${_rid}\"\n break\n fi\n done\n\n if [ -n \"${DM_ROOM_ID}\" ]; then\n log \"Existing DM room found: ${DM_ROOM_ID}\"\n else\n log \"Creating DM room with Manager...\"\n _RAW=$(curl -s -w '\\nHTTP_CODE:%{http_code}' -X POST \"${AGENTTEAMS_MATRIX_URL}/_matrix/client/v3/createRoom\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d \"{\\\"is_direct\\\":true,\\\"invite\\\":[\\\"${MANAGER_FULL_ID}\\\"],\\\"preset\\\":\\\"trusted_private_chat\\\"}\" 2>&1) || true\n _HTTP_CODE=$(echo \"${_RAW}\" | tail -1 | sed 's/HTTP_CODE://')\n _CREATE_RESP=$(echo \"${_RAW}\" | sed '$d')\n DM_ROOM_ID=$(echo \"${_CREATE_RESP}\" | jq -r '.room_id // empty' 2>/dev/null)\n if [ -n \"${DM_ROOM_ID}\" ]; then\n log \"DM room created: ${DM_ROOM_ID}\"\n else\n log \"WARNING: Failed to create DM room (HTTP ${_HTTP_CODE}): ${_CREATE_RESP}\"\n fi\n fi\n\n # Persist admin DM room ID to state.json\n if [ -n \"${DM_ROOM_ID}\" ]; then\n STATE_SCRIPT=\"/opt/hiclaw/agent/skills/task-management/scripts/manage-state.sh\"\n if [ -f \"${STATE_SCRIPT}\" ]; then\n bash \"${STATE_SCRIPT}\" --action init 2>/dev/null || true\n bash \"${STATE_SCRIPT}\" --action set-admin-dm --room-id \"${DM_ROOM_ID}\" 2>/dev/null || true\n log \"Admin DM room persisted to state.json: ${DM_ROOM_ID}\"\n fi\n fi\n\n # Schedule welcome message in background (only on first boot)\n if [ -n \"${DM_ROOM_ID}\" ] && [ ! -f \"/root/manager-workspace/soul-configured\" ]; then\n log \"Scheduling welcome message (background, waiting for OpenClaw to start)...\"\n (\n _AGENTTEAMS_LANGUAGE=\"${AGENTTEAMS_LANGUAGE:-zh}\"\n _AGENTTEAMS_TIMEZONE=\"${TZ:-Asia/Shanghai}\"\n _wait=0\n _ready=false\n while [ \"${_wait}\" -lt 300 ]; do\n if curl -sf http://127.0.0.1:18799/ > /dev/null 2>&1; then\n _ready=true\n break\n fi\n sleep 3\n _wait=$((_wait + 3))\n done\n if [ \"${_ready}\" != \"true\" ]; then\n echo \"[manager] WARNING: OpenClaw gateway not ready within 300s, skipping welcome message\"\n exit 0\n fi\n # Ensure Manager has joined the DM room before sending the welcome\n # message. Without this, there is a race between OpenClaw's Matrix\n # auto-join and the message send — the message may land before Manager\n # joins, so OpenClaw's /sync never picks it up.\n _join_ok=false\n for _join_attempt in 1 2 3; do\n if curl -sf -X POST \"${AGENTTEAMS_MATRIX_URL}/_matrix/client/v3/rooms/${DM_ROOM_ID}/join\" \\\n -H \"Authorization: Bearer ${MANAGER_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d '{}' > /dev/null 2>&1; then\n echo \"[manager] Manager joined DM room before welcome message\"\n _join_ok=true\n break\n fi\n sleep 2\n done\n if [ \"${_join_ok}\" != \"true\" ]; then\n echo \"[manager] WARNING: Manager join request failed after 3 attempts (may already be joined)\"\n fi\n _welcome_msg=\"This is an automated message from the AgentTeams setup. This is a fresh installation.\n\n--- Installation Context ---\nUser Language: ${_AGENTTEAMS_LANGUAGE} (zh = Chinese, en = English)\nUser Timezone: ${_AGENTTEAMS_TIMEZONE} (IANA timezone identifier)\n---\n\nYou are an AI agent that manages a team of worker agents. Your identity and personality have not been configured yet — the human admin is about to meet you for the first time.\n\nPlease begin the onboarding conversation:\n\n1. Greet the admin warmly and briefly describe what you can do (coordinate workers, manage tasks, run multi-agent projects)\n2. The user has selected \\\"${_AGENTTEAMS_LANGUAGE}\\\" as their preferred language during installation. Use this language for your greeting and all subsequent communication.\n3. The user's timezone is ${_AGENTTEAMS_TIMEZONE}. Based on this timezone, you may infer their likely region and suggest additional language options.\n4. Ask them: a) What would they like to call you? b) Communication style preference? c) Any behavior guidelines? d) Confirm default language\n5. After they reply, write their preferences to ~/SOUL.md\n6. Confirm what you wrote, and ask if they would like to adjust anything\n7. Once confirmed, run: touch ~/soul-configured\n\nThe human admin will start chatting shortly.\"\n _txn_id=\"welcome-$(date +%s)\"\n _payload=$(jq -nc --arg body \"${_welcome_msg}\" '{\"msgtype\":\"m.text\",\"body\":$body}')\n _raw=$(curl -s -w '\\nHTTP_CODE:%{http_code}' -X PUT \"${AGENTTEAMS_MATRIX_URL}/_matrix/client/v3/rooms/${DM_ROOM_ID}/send/m.room.message/${_txn_id}\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d \"${_payload}\" 2>&1) || true\n _http_code=$(echo \"${_raw}\" | tail -1 | sed 's/HTTP_CODE://')\n _send_resp=$(echo \"${_raw}\" | sed '$d')\n if echo \"${_send_resp}\" | jq -e '.event_id' > /dev/null 2>&1; then\n echo \"[manager] Welcome message sent to DM room\"\n else\n echo \"[manager] WARNING: Failed to send welcome message (HTTP ${_http_code}): ${_send_resp}\"\n fi\n ) &\n log \"Welcome message background process started (PID: $!)\"\n fi\nfi\nfi # end K8s mode skip for admin DM room\n\n# ============================================================\n# Generate Manager Agent openclaw.json from template\n# ============================================================\nlog \"Generating Manager openclaw.json...\"\nexport MANAGER_MATRIX_TOKEN=\"${MANAGER_TOKEN}\"\nexport MANAGER_GATEWAY_KEY=\"${AGENTTEAMS_MANAGER_GATEWAY_KEY}\"\n# Resolve model parameters based on model name\nMODEL_NAME=\"${AGENTTEAMS_DEFAULT_MODEL:-qwen3.6-plus}\"\ncase \"${MODEL_NAME}\" in\n gpt-5.3-codex|gpt-5-mini|gpt-5-nano)\n export MODEL_CONTEXT_WINDOW=400000 MODEL_MAX_TOKENS=128000 ;;\n claude-opus-4-6)\n export MODEL_CONTEXT_WINDOW=1000000 MODEL_MAX_TOKENS=128000 ;;\n claude-sonnet-4-6)\n export MODEL_CONTEXT_WINDOW=1000000 MODEL_MAX_TOKENS=64000 ;;\n claude-haiku-4-5)\n export MODEL_CONTEXT_WINDOW=200000 MODEL_MAX_TOKENS=64000 ;;\n qwen3.6-plus|qwen3.5-plus)\n export MODEL_CONTEXT_WINDOW=200000 MODEL_MAX_TOKENS=64000 ;;\n deepseek-chat|deepseek-reasoner|kimi-k2.5)\n export MODEL_CONTEXT_WINDOW=256000 MODEL_MAX_TOKENS=128000 ;;\n glm-5|MiniMax-M2.7|MiniMax-M2.7-highspeed|MiniMax-M2.5)\n export MODEL_CONTEXT_WINDOW=200000 MODEL_MAX_TOKENS=128000 ;;\n *)\n export MODEL_CONTEXT_WINDOW=150000 MODEL_MAX_TOKENS=128000 ;;\nesac\nexport MODEL_REASONING=true\n\n# Override with user-supplied custom model parameters from env (set during install)\n[ -n \"${AGENTTEAMS_MODEL_CONTEXT_WINDOW:-}\" ] && export MODEL_CONTEXT_WINDOW=\"${AGENTTEAMS_MODEL_CONTEXT_WINDOW}\"\n[ -n \"${AGENTTEAMS_MODEL_MAX_TOKENS:-}\" ] && export MODEL_MAX_TOKENS=\"${AGENTTEAMS_MODEL_MAX_TOKENS}\"\n[ -n \"${AGENTTEAMS_MODEL_REASONING:-}\" ] && export MODEL_REASONING=\"${AGENTTEAMS_MODEL_REASONING}\"\n\n# E2EE: convert AGENTTEAMS_MATRIX_E2EE to JSON boolean for template substitution\nif [ \"${AGENTTEAMS_MATRIX_E2EE:-0}\" = \"1\" ] || [ \"${AGENTTEAMS_MATRIX_E2EE:-}\" = \"true\" ]; then\n export MATRIX_E2EE_ENABLED=true\nelse\n export MATRIX_E2EE_ENABLED=false\nfi\nlog \"Matrix E2EE: ${MATRIX_E2EE_ENABLED}\"\n\n# Resolve input modalities: only vision-capable models get \"image\"\ncase \"${MODEL_NAME}\" in\n gpt-5.4|gpt-5.3-codex|gpt-5-mini|gpt-5-nano|claude-opus-4-6|claude-sonnet-4-6|claude-haiku-4-5|qwen3.6-plus|qwen3.5-plus|kimi-k2.5)\n export MODEL_INPUT='[\"text\", \"image\"]' ;;\n *)\n export MODEL_INPUT='[\"text\"]' ;;\nesac\n# Override with user-supplied vision setting from env\nif [ \"${AGENTTEAMS_MODEL_VISION:-}\" = \"true\" ]; then\n export MODEL_INPUT='[\"text\", \"image\"]'\nelif [ \"${AGENTTEAMS_MODEL_VISION:-}\" = \"false\" ]; then\n export MODEL_INPUT='[\"text\"]'\nfi\n\nlog \"Model: ${MODEL_NAME} (context=${MODEL_CONTEXT_WINDOW}, maxTokens=${MODEL_MAX_TOKENS}, reasoning=${MODEL_REASONING}, input=${MODEL_INPUT})\"\n\nif [ -f /root/manager-workspace/openclaw.json ]; then\n log \"Manager openclaw.json already exists, updating dynamic fields only (preserving user customizations)...\"\n # Merge known models into existing config (add missing, preserve user-added)\n # Use known-models.json (valid JSON) instead of template (contains ${VAR} placeholders)\n KNOWN_MODELS=$(cat /opt/hiclaw/configs/known-models.json 2>/dev/null || echo '[]')\n jq --arg token \"${MANAGER_TOKEN}\" \\\n --arg key \"${AGENTTEAMS_MANAGER_GATEWAY_KEY}\" \\\n --arg model \"${MODEL_NAME}\" \\\n --arg emb_model \"${AGENTTEAMS_EMBEDDING_MODEL}\" \\\n --arg aigw_domain \"${AI_GATEWAY_DOMAIN}\" \\\n --arg matrix_user_id \"@manager:${MATRIX_DOMAIN}\" \\\n --argjson e2ee \"${MATRIX_E2EE_ENABLED}\" \\\n --argjson known_models \"${KNOWN_MODELS}\" \\\n --argjson ctx \"${MODEL_CONTEXT_WINDOW}\" \\\n --argjson max \"${MODEL_MAX_TOKENS}\" \\\n --argjson reasoning \"${MODEL_REASONING}\" \\\n --argjson input \"${MODEL_INPUT}\" \\\n '\n # Merge known models: add any model id not already present\n .models.providers[\"agentteams-gateway\"].models as $existing\n | ($existing | map(.id)) as $existing_ids\n | ($known_models | map(select(.id as $id | $existing_ids | index($id) | not))) as $new\n | .models.providers[\"agentteams-gateway\"].models = ($existing + $new)\n # Ensure the user-chosen default model is in the list (custom model support)\n | if (.models.providers[\"agentteams-gateway\"].models | map(.id) | index($model) | not) then\n .models.providers[\"agentteams-gateway\"].models += [{\"id\": $model, \"name\": $model, \"reasoning\": $reasoning, \"contextWindow\": $ctx, \"maxTokens\": $max, \"input\": $input}]\n else . end\n # Rebuild model aliases from the full models list\n | (.models.providers[\"agentteams-gateway\"].models | map({ (\"agentteams-gateway/\" + .id): { \"alias\": .id } }) | add // {}) as $aliases\n | .agents.defaults.models = ((.agents.defaults.models // {}) + $aliases)\n | .channels.matrix.accessToken = $token | .channels.matrix.userId = $matrix_user_id | .models.providers[\"agentteams-gateway\"].apiKey = $key\n | ((.hooks.token // \"\") as $ht | if $ht == $key or $ht == ($key + \"-hooks\" | @base64) then del(.hooks) else . end)\n | .agents.defaults.model.primary = (\"agentteams-gateway/\" + $model)\n | .commands.restart = true\n | .gateway.port = 18799\n | .gateway.bind = \"lan\"\n | .gateway.controlUi = ((.gateway.controlUi // {}) + {\"dangerouslyDisableDeviceAuth\": true, \"allowInsecureAuth\": true, \"allowedOrigins\": [\"*\"]})\n | .channels.matrix.encryption = $e2ee\n | .channels.matrix.network = ((.channels.matrix.network // {}) + {\"dangerouslyAllowPrivateNetwork\": true})\n | .channels.matrix.autoJoin = \"always\"\n # OpenClaw YOLO defaults: host exec without approval prompts (see openclaw docs tools/exec-approvals)\n | .tools = (.tools // {})\n | .tools.exec = ((.tools.exec // {}) + {\"host\":\"gateway\",\"security\":\"full\",\"ask\":\"off\"})\n | .tools.elevated = (.tools.elevated // {})\n | .tools.elevated.enabled = true\n | .tools.elevated.allowFrom |= ((. // {}) | .matrix = [\"*\"])\n | .agents.defaults.elevatedDefault = \"full\"\n # Ensure memorySearch config exists (embedding model for memory) — skip if embedding model is empty\n | if $emb_model != \"\" then .agents.defaults.memorySearch //= {\"provider\":\"openai\",\"model\":$emb_model,\"remote\":{\"baseUrl\":(\"http://\" + $aigw_domain + \":8080/v1\"),\"apiKey\":$key}} else . end\n ' \\\n /root/manager-workspace/openclaw.json > /tmp/openclaw.json.tmp && \\\n mv /tmp/openclaw.json.tmp /root/manager-workspace/openclaw.json\n # Disable openclaw's observe-recovery mechanism which compares config against\n # a lastKnownGood baseline in config-health.json. When meta is missing from the\n # current file but present in the baseline, observe-recovery restores from .bak,\n # undoing user customizations (plugins, channels, etc).\n # Clearing config-health.json removes the baseline so observe-recovery won't\n # interfere, while preserving .bak as a backup.\n rm -f /root/manager-workspace/.openclaw/logs/config-health.json\n # Verify the token was written correctly\n _written_token=$(jq -r '.channels.matrix.accessToken' /root/manager-workspace/openclaw.json 2>/dev/null)\n if [ -z \"${_written_token}\" ] || [ \"${_written_token}\" = \"null\" ]; then\n log \"ERROR: Matrix token was not written correctly to openclaw.json (got: ${_written_token})\"\n else\n log \"Matrix token written to openclaw.json (prefix: ${_written_token:0:10}...)\"\n fi\nelse\n log \"Manager openclaw.json not found, generating from template...\"\n envsubst < /opt/hiclaw/configs/manager-openclaw.json.tmpl > /root/manager-workspace/openclaw.json\n # Post-envsubst injection: memorySearch + custom model (single jq pass when possible)\n if ! jq -e --arg model \"${MODEL_NAME}\" '.models.providers[\"agentteams-gateway\"].models | map(.id) | index($model)' /root/manager-workspace/openclaw.json > /dev/null 2>&1; then\n log \"Custom model '${MODEL_NAME}' not in built-in list, injecting into config...\"\n jq --arg emb_model \"${AGENTTEAMS_EMBEDDING_MODEL}\" \\\n --arg aigw_domain \"${AI_GATEWAY_DOMAIN}\" \\\n --arg key \"${AGENTTEAMS_MANAGER_GATEWAY_KEY}\" \\\n --arg model \"${MODEL_NAME}\" \\\n --argjson ctx \"${MODEL_CONTEXT_WINDOW}\" \\\n --argjson max \"${MODEL_MAX_TOKENS}\" \\\n --argjson reasoning \"${MODEL_REASONING}\" \\\n --argjson input \"${MODEL_INPUT}\" \\\n '\n (if $emb_model != \"\" then .agents.defaults.memorySearch = {\"provider\":\"openai\",\"model\":$emb_model,\"remote\":{\"baseUrl\":(\"http://\" + $aigw_domain + \":8080/v1\"),\"apiKey\":$key}} else . end)\n | .models.providers[\"agentteams-gateway\"].models += [{\"id\": $model, \"name\": $model, \"reasoning\": $reasoning, \"contextWindow\": $ctx, \"maxTokens\": $max, \"input\": $input}]\n | .agents.defaults.models += {(\"agentteams-gateway/\" + $model): {\"alias\": $model}}\n ' /root/manager-workspace/openclaw.json > /tmp/openclaw.json.tmp && \\\n mv /tmp/openclaw.json.tmp /root/manager-workspace/openclaw.json\n elif [ -n \"${AGENTTEAMS_EMBEDDING_MODEL}\" ]; then\n jq --arg emb_model \"${AGENTTEAMS_EMBEDDING_MODEL}\" \\\n --arg aigw_domain \"${AI_GATEWAY_DOMAIN}\" \\\n --arg key \"${AGENTTEAMS_MANAGER_GATEWAY_KEY}\" \\\n '.agents.defaults.memorySearch = {\"provider\":\"openai\",\"model\":$emb_model,\"remote\":{\"baseUrl\":(\"http://\" + $aigw_domain + \":8080/v1\"),\"apiKey\":$key}}' \\\n /root/manager-workspace/openclaw.json > /tmp/openclaw.json.tmp && \\\n mv /tmp/openclaw.json.tmp /root/manager-workspace/openclaw.json\n fi\n _written_token=$(jq -r '.channels.matrix.accessToken' /root/manager-workspace/openclaw.json 2>/dev/null)\n log \"Matrix token written from template (prefix: ${_written_token:0:10}...)\"\nfi\n\n# Cloud/K8s mode: overlay cloud-specific settings onto generated config\nif [ \"${AGENTTEAMS_RUNTIME}\" = \"aliyun\" ] || [ \"${AGENTTEAMS_RUNTIME}\" = \"k8s\" ]; then\n log \"Applying cloud/k8s overlay to openclaw.json...\"\n jq --arg homeserver \"${AGENTTEAMS_MATRIX_URL}\" \\\n --arg gateway \"${AGENTTEAMS_AI_GATEWAY_URL}/v1\" \\\n --arg key \"${AGENTTEAMS_MANAGER_GATEWAY_KEY}\" \\\n '.channels.matrix.homeserver = $homeserver\n | .models.providers[\"agentteams-gateway\"].baseUrl = $gateway\n | .models.providers[\"agentteams-gateway\"].apiKey = $key\n | ((.hooks.token // \"\") as $ht | if $ht == $key or $ht == ($key + \"-hooks\" | @base64) then del(.hooks) else . end)\n | .commands.restart = false\n | if .agents.defaults.memorySearch then .agents.defaults.memorySearch.remote.baseUrl = $gateway | .agents.defaults.memorySearch.remote.apiKey = $key else . end' \\\n /root/manager-workspace/openclaw.json > /tmp/openclaw-cloud.json && \\\n mv /tmp/openclaw-cloud.json /root/manager-workspace/openclaw.json\n log \"Cloud/K8s overlay applied\"\nfi\n\n# ============================================================\n# Optional: enable openclaw-cms-plugin observability\n# Config is applied at runtime so secrets stay out of image layers.\n# ============================================================\nCMS_TRACES_ENABLED=\"$(echo \"${AGENTTEAMS_CMS_TRACES_ENABLED:-false}\" | tr '[:upper:]' '[:lower:]')\"\nif [ \"${CMS_TRACES_ENABLED}\" = \"true\" ]; then\n CMS_PLUGIN_NAME=\"openclaw-cms-plugin\"\n CMS_PLUGIN_DIR=\"${OPENCLAW_CMS_PLUGIN_DIR:-/opt/openclaw/extensions/openclaw-cms-plugin}\"\n CMS_PLUGIN_MANIFEST=\"${CMS_PLUGIN_DIR}/openclaw.plugin.json\"\n DIAG_PLUGIN_NAME=\"diagnostics-otel\"\n DIAG_PLUGIN_DIR=\"/opt/openclaw/extensions/diagnostics-otel\"\n CMS_LICENSE_KEY=\"${AGENTTEAMS_CMS_LICENSE_KEY:-}\"\n CMS_PROJECT=\"${AGENTTEAMS_CMS_PROJECT:-}\"\n CMS_METRICS_ENABLED=\"${AGENTTEAMS_CMS_METRICS_ENABLED:-false}\"\n\n if [ ! -f \"${CMS_PLUGIN_MANIFEST}\" ]; then\n log \"WARNING: ${CMS_PLUGIN_NAME} manifest not found at ${CMS_PLUGIN_MANIFEST}, skipping plugin config\"\n else\n _missing=0\n [ -z \"${AGENTTEAMS_CMS_ENDPOINT:-}\" ] && log \"WARNING: AGENTTEAMS_CMS_ENDPOINT is required when AGENTTEAMS_CMS_TRACES_ENABLED=true\" && _missing=1\n [ -z \"${CMS_LICENSE_KEY:-}\" ] && log \"WARNING: AGENTTEAMS_CMS_LICENSE_KEY is required when AGENTTEAMS_CMS_TRACES_ENABLED=true\" && _missing=1\n [ -z \"${AGENTTEAMS_CMS_WORKSPACE:-}\" ] && log \"WARNING: AGENTTEAMS_CMS_WORKSPACE is required when AGENTTEAMS_CMS_TRACES_ENABLED=true\" && _missing=1\n\n if [ \"${_missing}\" = \"0\" ]; then\n CMS_SERVICE_NAME=\"${AGENTTEAMS_CMS_SERVICE_NAME:-agentteams-manager}\"\n CMS_ENABLE_METRICS=\"${CMS_METRICS_ENABLED}\"\n DIAG_AVAILABLE=\"0\"\n _metrics_lc=\"$(echo \"${CMS_ENABLE_METRICS}\" | tr '[:upper:]' '[:lower:]')\"\n if [ \"${_metrics_lc}\" = \"true\" ]; then\n if [ -f \"${DIAG_PLUGIN_DIR}/package.json\" ]; then\n DIAG_AVAILABLE=\"1\"\n if [ ! -d \"${DIAG_PLUGIN_DIR}/node_modules\" ]; then\n log \"diagnostics-otel dependencies missing, installing...\"\n if (cd \"${DIAG_PLUGIN_DIR}\" && npm install --omit=dev --ignore-scripts >/tmp/hiclaw-diag-install.log 2>&1); then\n log \"diagnostics-otel dependencies installed\"\n else\n log \"WARNING: diagnostics-otel npm install failed, metrics plugin may not load\"\n fi\n else\n log \"diagnostics-otel dependencies already present\"\n fi\n else\n log \"WARNING: diagnostics-otel package.json not found at ${DIAG_PLUGIN_DIR}, metrics plugin may not load\"\n fi\n fi\n\n log \"Applying ${CMS_PLUGIN_NAME} config to openclaw.json...\"\n jq --arg pluginName \"${CMS_PLUGIN_NAME}\" \\\n --arg pluginDir \"${CMS_PLUGIN_DIR}\" \\\n --arg endpoint \"${AGENTTEAMS_CMS_ENDPOINT}\" \\\n --arg licenseKey \"${CMS_LICENSE_KEY}\" \\\n --arg armsProject \"${CMS_PROJECT}\" \\\n --arg cmsWorkspace \"${AGENTTEAMS_CMS_WORKSPACE}\" \\\n --arg serviceName \"${CMS_SERVICE_NAME}\" \\\n --arg diagPluginName \"${DIAG_PLUGIN_NAME}\" \\\n --arg diagPluginDir \"${DIAG_PLUGIN_DIR}\" \\\n --arg metricsRaw \"${CMS_ENABLE_METRICS}\" \\\n --arg diagAvailableRaw \"${DIAG_AVAILABLE}\" \\\n '\n .plugins = (.plugins // {})\n | .plugins.load = (.plugins.load // {})\n | .plugins.entries = (.plugins.entries // {})\n | if (.plugins.allow | type) != \"array\" then .plugins.allow = [] else . end\n | if (.plugins.allow | index($pluginName)) == null then .plugins.allow += [$pluginName] else . end\n | if (.plugins.load.paths | type) != \"array\" then .plugins.load.paths = [] else . end\n | if (.plugins.load.paths | index($pluginDir)) == null then .plugins.load.paths += [$pluginDir] else . end\n | .plugins.entries[$pluginName] = {\n \"enabled\": true,\n \"config\": {\n \"endpoint\": $endpoint,\n \"headers\": {\n \"x-arms-license-key\": $licenseKey,\n \"x-arms-project\": $armsProject,\n \"x-cms-workspace\": $cmsWorkspace\n },\n \"serviceName\": $serviceName\n }\n }\n\n # diagnostics-otel metrics (optional)\n | ($metricsRaw | ascii_downcase) as $m\n | ($diagAvailableRaw == \"1\") as $diagAvailable\n | (($m == \"true\") and $diagAvailable) as $metricsEnabled\n | if $metricsEnabled then\n (if (.plugins.allow | index($diagPluginName)) == null then .plugins.allow += [$diagPluginName] else . end)\n | (if (.plugins.load.paths | index($diagPluginDir)) == null then .plugins.load.paths += [$diagPluginDir] else . end)\n | .plugins.entries[$diagPluginName].enabled = true\n | .diagnostics = (.diagnostics // {})\n | .diagnostics.otel = (.diagnostics.otel // {})\n | .diagnostics.enabled = true\n | .diagnostics.otel.enabled = true\n | .diagnostics.otel.endpoint = $endpoint\n | .diagnostics.otel.protocol = (.diagnostics.otel.protocol // \"http/protobuf\")\n | .diagnostics.otel.headers = {\n \"x-arms-license-key\": $licenseKey,\n \"x-arms-project\": $armsProject,\n \"x-cms-workspace\": $cmsWorkspace\n }\n | .diagnostics.otel.serviceName = $serviceName\n | .diagnostics.otel.metrics = true\n | .diagnostics.otel.traces = (.diagnostics.otel.traces // false)\n | .diagnostics.otel.logs = (.diagnostics.otel.logs // false)\n else\n .\n end\n ' /root/manager-workspace/openclaw.json > /tmp/openclaw-cms.json && \\\n mv /tmp/openclaw-cms.json /root/manager-workspace/openclaw.json\n log \"${CMS_PLUGIN_NAME} config applied (metrics=${CMS_ENABLE_METRICS}, service=${CMS_SERVICE_NAME})\"\n else\n log \"Skipping ${CMS_PLUGIN_NAME} config due to missing required env vars\"\n fi\n fi\nfi\n\n# ============================================================\n# Detect container runtime (for Worker creation)\n# ============================================================\nsource /opt/hiclaw/scripts/lib/container-api.sh\nif container_api_available; then\n log \"Container runtime socket detected at ${CONTAINER_SOCKET} — direct Worker creation enabled\"\n export AGENTTEAMS_CONTAINER_RUNTIME=\"socket\"\nelif [ \"${AGENTTEAMS_RUNTIME}\" = \"aliyun\" ] || [ \"${AGENTTEAMS_RUNTIME}\" = \"k8s\" ]; then\n log \"Cloud/K8s mode — Workers created via controller API\"\n export AGENTTEAMS_CONTAINER_RUNTIME=\"cloud\"\nelse\n log \"No container runtime found — Worker creation will output install commands\"\n export AGENTTEAMS_CONTAINER_RUNTIME=\"none\"\nfi\n\n# ============================================================\n# Upgrade Worker openclaw.json: merge known models + E2EE flag into existing configs\n# Existing workers in MinIO may have old single-model configs or missing encryption field.\n# Merge template models so they can hot-switch without restart.\n# ============================================================\nREGISTRY_FILE=\"/root/manager-workspace/workers-registry.json\"\nif [ -f \"${REGISTRY_FILE}\" ]; then\n # Use known-models.json (valid JSON) instead of template (contains ${VAR} placeholders)\n KNOWN_MODELS_FILE=\"/opt/hiclaw/configs/known-models.json\"\n if [ -f \"${KNOWN_MODELS_FILE}\" ]; then\n _KNOWN_MODELS=$(cat \"${KNOWN_MODELS_FILE}\")\n for _wname in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_wname}\" ] && continue\n _minio_path=\"${AGENTTEAMS_STORAGE_PREFIX}/agents/${_wname}/openclaw.json\"\n _tmp_in=\"/tmp/openclaw-${_wname}-models-upgrade-in.json\"\n if mc cp \"${_minio_path}\" \"${_tmp_in}\" 2>/dev/null; then\n _tmp_out=\"/tmp/openclaw-${_wname}-models-upgrade-out.json\"\n # Idempotent merge: add missing known models, rebuild aliases, set e2ee.\n # Always runs — jq deduplicates by model id, so re-runs are safe.\n jq --argjson known_models \"${_KNOWN_MODELS}\" \\\n --argjson e2ee \"${MATRIX_E2EE_ENABLED}\" '\n .models.providers[\"agentteams-gateway\"].models as $existing\n | ($existing | map(.id)) as $existing_ids\n | ($known_models | map(select(.id as $id | $existing_ids | index($id) | not))) as $new\n | .models.providers[\"agentteams-gateway\"].models = ($existing + $new)\n | (.models.providers[\"agentteams-gateway\"].models | map({ (\"agentteams-gateway/\" + .id): { \"alias\": .id } }) | add // {}) as $aliases\n | .agents.defaults.models = ((.agents.defaults.models // {}) + $aliases)\n | .channels.matrix.encryption = $e2ee\n | .channels.matrix.autoJoin = \"always\"\n | .tools = (.tools // {})\n | .tools.exec = ((.tools.exec // {}) + {\"host\":\"gateway\",\"security\":\"full\",\"ask\":\"off\"})\n | .tools.elevated = (.tools.elevated // {})\n | .tools.elevated.enabled = true\n | .tools.elevated.allowFrom |= ((. // {}) | .matrix = [\"*\"])\n | .agents.defaults.elevatedDefault = \"full\"\n ' \"${_tmp_in}\" > \"${_tmp_out}\" 2>/dev/null\n if ! diff -q \"${_tmp_in}\" \"${_tmp_out}\" > /dev/null 2>&1; then\n if mc cp \"${_tmp_out}\" \"${_minio_path}\" 2>/dev/null; then\n _new_count=$(jq '.models.providers[\"agentteams-gateway\"].models | length' \"${_tmp_out}\" 2>/dev/null)\n log \"Worker ${_wname}: upgraded openclaw.json (models: ${_new_count}, e2ee: ${MATRIX_E2EE_ENABLED})\"\n fi\n fi\n rm -f \"${_tmp_in}\" \"${_tmp_out}\"\n fi\n done\n fi\nfi\n\n# ============================================================\n# Ensure Worker Matrix password files exist in MinIO (E2EE fix)\n# Workers need to re-login on restart to get a fresh device_id.\n# Older workers created before this fix won't have the password file.\n# ============================================================\nif [ -f \"${REGISTRY_FILE}\" ]; then\n for _wname in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_wname}\" ] && continue\n _creds_file=\"/data/worker-creds/${_wname}.env\"\n if [ -f \"${_creds_file}\" ]; then\n # Check if password file already exists in MinIO\n if ! mc stat \"${AGENTTEAMS_STORAGE_PREFIX}/agents/${_wname}/credentials/matrix/password\" > /dev/null 2>&1; then\n source \"${_creds_file}\"\n if [ -n \"${WORKER_PASSWORD}\" ]; then\n _tmp_pw=\"/tmp/matrix-pw-${_wname}\"\n echo -n \"${WORKER_PASSWORD}\" > \"${_tmp_pw}\"\n mc cp \"${_tmp_pw}\" \"${AGENTTEAMS_STORAGE_PREFIX}/agents/${_wname}/credentials/matrix/password\" 2>/dev/null \\\n && log \"Worker ${_wname}: wrote Matrix password to MinIO (E2EE re-login fix)\" \\\n || log \"Worker ${_wname}: WARNING: failed to write Matrix password to MinIO\"\n rm -f \"${_tmp_pw}\"\n fi\n fi\n fi\n done\nfi\n\n# ============================================================\n# Recreate Worker containers as needed after Manager restart.\n# Workers are on agentteams-net; Docker DNS resolves *-local.agentteams.io via\n# the Manager's network aliases, so IP changes don't require worker recreation.\n# Only recreate stopped/missing workers.\n# ============================================================\nif container_api_available; then\n REGISTRY_FILE=\"/root/manager-workspace/workers-registry.json\"\n if [ -f \"${REGISTRY_FILE}\" ]; then\n for _worker_name in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_worker_name}\" ] && continue\n\n # Skip remote workers — they are not Manager-managed containers.\n _deployment=$(jq -r --arg w \"${_worker_name}\" '.workers[$w].deployment // \"local\"' \"${REGISTRY_FILE}\" 2>/dev/null)\n if [ \"${_deployment}\" = \"remote\" ]; then\n log \"Worker ${_worker_name} is remote, skipping container recreate\"\n continue\n fi\n\n _status=$(container_status_worker \"${_worker_name}\")\n if [ \"${_status}\" = \"running\" ]; then\n log \"Worker running: ${_worker_name}, skipping\"\n continue\n fi\n # Container missing or stopped — recreate.\n log \"Worker container ${_status}: ${_worker_name}, recreating...\"\n _creds_file=\"/data/worker-creds/${_worker_name}.env\"\n if [ -f \"${_creds_file}\" ]; then\n source \"${_creds_file}\"\n _runtime=$(jq -r --arg w \"${_worker_name}\" '.workers[$w].runtime // \"openclaw\"' \"${REGISTRY_FILE}\" 2>/dev/null)\n _recreated=false\n for _attempt in 1 2 3; do\n local _env_map _create_body\n _env_map=$(jq -cn \\\n --arg name \"${_worker_name}\" \\\n --arg fak \"${_worker_name}\" \\\n --arg fsk \"${WORKER_MINIO_PASSWORD:-}\" \\\n --arg fs_domain \"${AGENTTEAMS_FS_DOMAIN:-fs-local.agentteams.io}\" \\\n --arg controller_url \"${AGENTTEAMS_CONTROLLER_URL:-}\" \\\n '{\n \"AGENTTEAMS_WORKER_NAME\": $name,\n \"AGENTTEAMS_FS_ENDPOINT\": (\"http://\" + ($fs_domain | split(\":\")[0]) + \":9000\"),\n \"AGENTTEAMS_FS_ACCESS_KEY\": $fak,\n \"AGENTTEAMS_FS_SECRET_KEY\": $fsk\n }\n | if $controller_url != \"\" then . + {\"AGENTTEAMS_CONTROLLER_URL\": $controller_url} else . end')\n _create_body=$(jq -cn --arg name \"${_worker_name}\" --arg runtime \"${_runtime}\" --argjson env \"${_env_map}\" '{name: $name, runtime: $runtime, env: $env}')\n worker_backend_create \"${_create_body}\" > /dev/null 2>&1 && _recreated=true && break\n log \" Attempt ${_attempt}/3 failed for ${_worker_name}, retrying in $((5 * _attempt))s...\"\n sleep $((5 * _attempt))\n done\n if [ \"${_recreated}\" = true ]; then\n log \" Recreated ${_runtime} worker: ${_worker_name}\"\n else\n log \" ERROR: Failed to recreate ${_worker_name} after 3 attempts\"\n fi\n else\n log \" WARNING: No credentials found for ${_worker_name} (${_creds_file} missing), skipping\"\n fi\n done\n fi\nfi\n\n# ============================================================\n# Notify workers of builtin updates if upgrade happened\n# Builtin files (AGENTS.md, skills) are already synced by upgrade-builtins.sh\n#\n# Cooldown: skip notification if the last successful notify was within\n# NOTIFY_COOLDOWN_SECS (default 3600s / 1 hour). This prevents repeated\n# notifications when the Manager crash-loops and re-runs upgrade-builtins\n# on every restart (e.g. IMAGE_VERSION=latest always triggers upgrade).\n# ============================================================\nNOTIFY_COOLDOWN_SECS=\"${AGENTTEAMS_NOTIFY_COOLDOWN_SECS:-3600}\"\nNOTIFY_TS_FILE=\"/root/manager-workspace/.last-worker-notify-ts\"\n\nif [ -f /root/manager-workspace/.upgrade-pending-worker-notify ]; then\n _now=$(date +%s)\n _last_notify=$(cat \"${NOTIFY_TS_FILE}\" 2>/dev/null || echo \"0\")\n _elapsed=$(( _now - _last_notify ))\n\n if [ \"${_elapsed}\" -lt \"${NOTIFY_COOLDOWN_SECS}\" ]; then\n log \"Skipping worker builtin notification (last notify ${_elapsed}s ago, cooldown ${NOTIFY_COOLDOWN_SECS}s)\"\n rm -f /root/manager-workspace/.upgrade-pending-worker-notify\n else\n log \"Notifying workers about builtin updates...\"\n REGISTRY_FILE=\"/root/manager-workspace/workers-registry.json\"\n _notify_ok=false\n if [ -f \"${REGISTRY_FILE}\" ]; then\n for _worker_name in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_worker_name}\" ] && continue\n _room_id=$(jq -r --arg w \"${_worker_name}\" '.workers[$w].room_id // empty' \"${REGISTRY_FILE}\" 2>/dev/null)\n if [ -n \"${_room_id}\" ]; then\n _worker_id=\"@${_worker_name}:${MATRIX_DOMAIN}\"\n _txn_id=\"upgrade-$(date +%s%N)\"\n _msg=\"@${_worker_name}:${MATRIX_DOMAIN} Manager upgraded builtin files (AGENTS.md, skills). Please use your file-sync skill to sync the latest config.\"\n _raw=$(curl -s -w '\\nHTTP_CODE:%{http_code}' -X PUT \\\n \"${AGENTTEAMS_MATRIX_URL}/_matrix/client/v3/rooms/${_room_id}/send/m.room.message/${_txn_id}\" \\\n -H \"Authorization: Bearer ${MANAGER_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d \"{\\\"msgtype\\\":\\\"m.text\\\",\\\"body\\\":\\\"${_msg}\\\",\\\"m.mentions\\\":{\\\"user_ids\\\":[\\\"${_worker_id}\\\"]}}\" \\\n 2>&1) || true\n _http_code=$(echo \"${_raw}\" | tail -1 | sed 's/HTTP_CODE://')\n _notify_resp=$(echo \"${_raw}\" | sed '$d')\n if echo \"${_notify_resp}\" | jq -e '.event_id' > /dev/null 2>&1; then\n log \" Notified ${_worker_name}\"; _notify_ok=true\n else\n log \" WARNING: Failed to notify ${_worker_name} (HTTP ${_http_code}): ${_notify_resp}\"\n fi\n fi\n done\n fi\n # Record timestamp only if at least one notification succeeded\n if [ \"${_notify_ok}\" = true ]; then\n echo \"${_now}\" > \"${NOTIFY_TS_FILE}\"\n fi\n rm -f /root/manager-workspace/.upgrade-pending-worker-notify\n fi\nfi\n\n# ============================================================\n# Start Manager Agent\n# ============================================================\nlog \"Starting Manager Agent (${MANAGER_RUNTIME})...\"\n\n# HOME is already set to /root/manager-workspace via docker run -e HOME=...\ncd \"${HOME}\"\n\n# Ensure host credential symlinks exist under HOME\nif [ -d \"/host-share\" ]; then\n [ -f \"/host-share/.gitconfig\" ] && ln -sf \"/host-share/.gitconfig\" \"${HOME}/.gitconfig\"\nfi\n\nlog \"HOME=${HOME} (manager-workspace, host-mounted)\"\n\n# ── Render agent doc templates ────────────────────────────────────────────\n# Replace ${VAR} placeholders with actual values so the AI agent reads\n# plain text and never needs to resolve environment variables.\nexport MANAGER_MATRIX_TOKEN MANAGER_TOKEN HIGRESS_COOKIE_FILE\nRENDER=/opt/hiclaw/scripts/lib/render-skills.sh\nlog \"Rendering agent doc templates...\"\n# Manager-owned docs (workspace)\nbash \"$RENDER\" /root/manager-workspace/skills\nbash \"$RENDER\" /root/manager-workspace/skills-alpha\nbash \"$RENDER\" /root/manager-workspace AGENTS.md TOOLS.md HEARTBEAT.md SOUL.md\n# Worker templates (workspace + image) — rendered before push to MinIO\n# so Workers (including remote pip-install) receive plain text\nbash \"$RENDER\" /root/manager-workspace/worker-skills\nbash \"$RENDER\" /root/manager-workspace/worker-agent\nbash \"$RENDER\" /root/manager-workspace/copaw-worker-agent\nbash \"$RENDER\" /root/manager-workspace/hermes-worker-agent\nbash \"$RENDER\" /opt/hiclaw/agent/worker-skills\nbash \"$RENDER\" /opt/hiclaw/agent/worker-agent\nbash \"$RENDER\" /opt/hiclaw/agent/copaw-worker-agent\nbash \"$RENDER\" /opt/hiclaw/agent/hermes-worker-agent\nlog \"Agent doc templates rendered\"\n\n# Cloud mode: start background file sync (workspace ↔ OSS) and initial push\nif [ \"${AGENTTEAMS_RUNTIME}\" = \"aliyun\" ]; then\n log \"Syncing initial workspace to OSS...\"\n ensure_mc_credentials\n mc mirror /root/manager-workspace/ \"${AGENTTEAMS_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" 2>/dev/null || true\n\n # Local → OSS: change-triggered sync\n (\n while true; do\n CHANGED=$(find /root/manager-workspace/ -type f -newermt \"15 seconds ago\" 2>/dev/null | head -1)\n if [ -n \"${CHANGED}\" ]; then\n ensure_mc_credentials 2>/dev/null || true\n mc mirror /root/manager-workspace/ \"${AGENTTEAMS_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" --exclude \".npm/**\" \\\n --exclude \".local/**\" --exclude \".mc/**\" 2>/dev/null || true\n fi\n sleep 10\n done\n ) &\n log \"Local→OSS sync started (PID: $!)\"\n\n # OSS → Local: periodic pull (shared data, agent configs, manager openclaw.json)\n (\n while true; do\n sleep 60\n ensure_mc_credentials 2>/dev/null || true\n mc mirror \"${AGENTTEAMS_STORAGE_PREFIX}/shared/\" /root/hiclaw-fs/shared/ --overwrite --newer-than \"1m\" 2>/dev/null || true\n mc mirror \"${AGENTTEAMS_STORAGE_PREFIX}/agents/\" /root/hiclaw-fs/agents/ --overwrite --newer-than \"1m\" 2>/dev/null || true\n mc cp \"${AGENTTEAMS_STORAGE_PREFIX}/manager/openclaw.json\" /root/manager-workspace/openclaw.json 2>/dev/null || true\n done\n ) &\n log \"OSS→Local sync started (every 60s, PID: $!)\"\nfi\n\n# K8s mode: start background file sync (workspace ↔ MinIO)\nif [ \"${AGENTTEAMS_RUNTIME}\" = \"k8s\" ]; then\n log \"Syncing initial workspace to MinIO...\"\n mc mirror /root/manager-workspace/ \"${AGENTTEAMS_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" 2>/dev/null || true\n\n # Local → MinIO: change-triggered sync\n (\n while true; do\n CHANGED=$(find /root/manager-workspace/ -type f -newermt \"15 seconds ago\" 2>/dev/null | head -1)\n if [ -n \"${CHANGED}\" ]; then\n mc mirror /root/manager-workspace/ \"${AGENTTEAMS_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" --exclude \".npm/**\" \\\n --exclude \".local/**\" --exclude \".mc/**\" 2>/dev/null || true\n fi\n sleep 10\n done\n ) &\n log \"Local→MinIO sync started (PID: $!)\"\n\n # MinIO → Local: periodic pull (shared data, agent configs, manager openclaw.json)\n (\n while true; do\n sleep 60\n mc mirror \"${AGENTTEAMS_STORAGE_PREFIX}/shared/\" /root/hiclaw-fs/shared/ --overwrite --newer-than \"1m\" 2>/dev/null || true\n mc mirror \"${AGENTTEAMS_STORAGE_PREFIX}/agents/\" /root/hiclaw-fs/agents/ --overwrite --newer-than \"1m\" 2>/dev/null || true\n mc mirror \"${AGENTTEAMS_STORAGE_PREFIX}/agentteams-config/\" /root/hiclaw-fs/agentteams-config/ --overwrite --newer-than \"1m\" 2>/dev/null || true\n mc cp \"${AGENTTEAMS_STORAGE_PREFIX}/manager/openclaw.json\" /root/manager-workspace/openclaw.json 2>/dev/null || true\n done\n ) &\n log \"MinIO→Local sync started (every 60s, PID: $!)\"\nfi\n\n# ============================================================\n# Auto-generate Manager mcporter config for pre-configured MCP servers\n# If AGENTTEAMS_GITHUB_TOKEN was set at install time, setup-higress.sh already\n# configured GitHub MCP on Higress. Run setup-mcp-server.sh now so that\n# config/mcporter.json exists when the Agent starts — no need to ask user for PAT.\n# ============================================================\nif [ -n \"${AGENTTEAMS_GITHUB_TOKEN}\" ] && [ \"${AGENTTEAMS_RUNTIME}\" != \"aliyun\" ] && [ \"${AGENTTEAMS_RUNTIME}\" != \"k8s\" ]; then\n if [ ! -f \"${HOME}/config/mcporter.json\" ]; then\n log \"Auto-generating Manager mcporter config for GitHub MCP (AGENTTEAMS_GITHUB_TOKEN set)...\"\n bash /opt/hiclaw/agent/skills/mcp-server-management/scripts/setup-mcp-server.sh \\\n github \"${AGENTTEAMS_GITHUB_TOKEN}\" 2>&1 | while IFS= read -r line; do log \" [setup-mcp] ${line}\"; done || \\\n log \"WARNING: setup-mcp-server.sh failed — Agent may need to configure GitHub MCP manually\"\n else\n log \"Manager mcporter config already exists, skipping auto-generate\"\n fi\nfi\n\n# ============================================================\n# Runtime-specific startup\n# ============================================================\nif [ \"${MANAGER_RUNTIME}\" = \"copaw\" ]; then\n # Delegate to CoPaw startup script\n exec /opt/hiclaw/scripts/init/start-copaw-manager.sh\nelse\n # ── OpenClaw Runtime ─────────────────────────────────────────────────────\n log \"Starting OpenClaw Manager...\"\n\n export OPENCLAW_CONFIG_PATH=\"/root/manager-workspace/openclaw.json\"\n\n # Symlink to default OpenClaw config path so CLI commands find the config\n mkdir -p \"${HOME}/.openclaw\"\n ln -sf \"/root/manager-workspace/openclaw.json\" \"${HOME}/.openclaw/openclaw.json\"\n\n # Clean orphaned session write locks (e.g. from SIGKILL or crash before exit handlers)\n # Prevents \"session file locked (timeout 10000ms)\" when PID was reused\n find \"${HOME}/.openclaw/agents\" -name \"*.jsonl.lock\" -delete 2>/dev/null || true\n log \"Cleaned up any orphaned session write locks\"\n\n # Clean Matrix crypto storage (SQLite WAL may be corrupted after unclean shutdown)\n # Crypto state is re-negotiated on startup; losing it only means re-establishing E2EE sessions\n rm -rf \"${HOME}/.openclaw/matrix\" 2>/dev/null || true\n log \"Cleaned Matrix crypto storage (will re-establish E2EE sessions)\"\n\n # Launch OpenClaw\n # Disable full-process respawn so the CLI uses its internal restart loop.\n # Without this, config reload spawns a detached child and exits, then\n # supervisord restarts the CLI — resulting in two gateway processes.\n export OPENCLAW_NO_RESPAWN=1\n\n # Optional matrix-plugin trace logging — when AGENTTEAMS_MATRIX_DEBUG=1 is set\n # in the manager environment (propagated by install / supervisord), turn on\n # OPENCLAW_MATRIX_DEBUG so the matrix plugin emits structured INFO-level\n # lifecycle traces (sync.state transitions, room.invite/join, message\n # handler arrival + filter outcomes). Useful for diagnosing \"worker never\n # joined the room\" / \"manager never replied\" hangs without rebuilding the\n # image.\n if [ \"${AGENTTEAMS_MATRIX_DEBUG:-}\" = \"1\" ] && [ -z \"${OPENCLAW_MATRIX_DEBUG:-}\" ]; then\n export OPENCLAW_MATRIX_DEBUG=1\n log \"AGENTTEAMS_MATRIX_DEBUG=1 detected; OPENCLAW_MATRIX_DEBUG=1 exported for matrix plugin tracing\"\n fi\n\n exec openclaw gateway run --verbose --force\nfi\n"}, "0d1e603e9e72f020013dc21706986fe51d6c5f24": {"t": 1785764738.594509, "n": 68557, "h": "6feafd21ea91603c", "c": "#!/bin/bash\n# start-manager-agent.sh - Initialize and start the Manager Agent\n# Supports local (supervisord), cloud (SAE), and K8s (Helm) deployments.\n# In local mode this is the last supervisord component to start (priority 800).\n# In cloud/k8s mode (HICLAW_RUNTIME=aliyun|k8s) this is the container entrypoint.\n#\n# Runtime selection:\n# HICLAW_MANAGER_RUNTIME=openclaw (default) - OpenClaw gateway mode\n# HICLAW_MANAGER_RUNTIME=copaw - CoPaw workspace mode\n# (hermes runtime is supported for Workers only; Managers run openclaw or copaw.)\n\nsource /opt/hiclaw/scripts/lib/hiclaw-env.sh\n\n# ============================================================\n# Runtime selection\n# ============================================================\nMANAGER_RUNTIME=\"${HICLAW_MANAGER_RUNTIME:-openclaw}\"\ncase \"${MANAGER_RUNTIME}\" in\n copaw)\n log \"Manager runtime: CoPaw (Python workspace)\"\n ;;\n *)\n log \"Manager runtime: OpenClaw (Node.js gateway)\"\n MANAGER_RUNTIME=\"openclaw\"\n ;;\nesac\n\n# ============================================================\n# Set timezone from TZ env var\n# ============================================================\nif [ -n \"${TZ}\" ] && [ -f \"/usr/share/zoneinfo/${TZ}\" ]; then\n ln -sf \"/usr/share/zoneinfo/${TZ}\" /etc/localtime\n echo \"${TZ}\" > /etc/timezone\n log \"Timezone set to ${TZ}\"\nfi\n\nexport MATRIX_DOMAIN=\"${HICLAW_MATRIX_DOMAIN:-matrix-local.hiclaw.io:8080}\"\nAI_GATEWAY_DOMAIN=\"${HICLAW_AI_GATEWAY_DOMAIN:-aigw-local.hiclaw.io}\"\n\n# ============================================================\n# YOLO mode promotion\n# ============================================================\n# In embedded mode the controller does not propagate HICLAW_YOLO to the\n# manager container, but installer / test scripts touch a marker file at\n# `${WORKSPACE}/yolo-mode` instead. Promote that marker to the env var so the\n# agent's documented YOLO check (`HICLAW_YOLO=1`) reliably detects it without\n# depending on filesystem lookups during a turn.\nif [ -z \"${HICLAW_YOLO:-}\" ] && [ -f /root/manager-workspace/yolo-mode ]; then\n export HICLAW_YOLO=1\n log \"YOLO mode marker detected at /root/manager-workspace/yolo-mode; HICLAW_YOLO=1 exported\"\nfi\n\n# ============================================================\n# Cloud/K8s mode: validate required environment variables + initial credentials\n# ============================================================\nif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ] || [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n : \"${HICLAW_MATRIX_URL:?HICLAW_MATRIX_URL is required}\"\n : \"${HICLAW_MATRIX_DOMAIN:?HICLAW_MATRIX_DOMAIN is required}\"\n : \"${HICLAW_AI_GATEWAY_URL:?HICLAW_AI_GATEWAY_URL is required}\"\n if [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n : \"${HICLAW_MANAGER_GATEWAY_KEY:?HICLAW_MANAGER_GATEWAY_KEY is required}\"\n : \"${HICLAW_MANAGER_PASSWORD:?HICLAW_MANAGER_PASSWORD is required (cloud containers are stateless, password must be injected)}\"\n fi\n if [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n # K8s mode: controller handles initialization (admin registration, Higress setup).\n # Manager only needs credentials injected by the ManagerReconciler.\n : \"${HICLAW_MANAGER_GATEWAY_KEY:?HICLAW_MANAGER_GATEWAY_KEY is required (injected by controller)}\"\n # HICLAW_MANAGER_PASSWORD is optional: not needed in AppService mode\n # (token obtained via AS login), only required in legacy password mode.\n else\n # Cloud (aliyun) mode: Manager still does its own initialization\n : \"${HICLAW_REGISTRATION_TOKEN:?HICLAW_REGISTRATION_TOKEN is required}\"\n : \"${HICLAW_ADMIN_USER:?HICLAW_ADMIN_USER is required}\"\n : \"${HICLAW_ADMIN_PASSWORD:?HICLAW_ADMIN_PASSWORD is required}\"\n fi\n log \"${HICLAW_RUNTIME} mode: validating environment... OK\"\n log \" Matrix: ${HICLAW_MATRIX_URL}, AI Gateway: ${HICLAW_AI_GATEWAY_URL}, Storage: ${HICLAW_FS_BUCKET}\"\n if [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n ensure_mc_credentials || { log \"FATAL: Initial STS credential fetch failed\"; exit 1; }\n fi\nfi\n\n# ============================================================\n# Local mode: host symlinks, /etc/hosts, wait for local services\n# ============================================================\nif [ \"${HICLAW_RUNTIME}\" != \"aliyun\" ] && [ \"${HICLAW_RUNTIME}\" != \"k8s\" ]; then\n # Create symlink for host directory access\n if [ -d \"/host-share\" ]; then\n ORIGINAL_HOST_HOME=\"${HOST_ORIGINAL_HOME:-$HOME}\"\n if [ ! -e \"${ORIGINAL_HOST_HOME}\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/root\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/data\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/host-share\" ]; then\n mkdir -p \"$(dirname \"${ORIGINAL_HOST_HOME}\")\"\n ln -sfn /host-share \"${ORIGINAL_HOST_HOME}\"\n log \"Created symlink: ${ORIGINAL_HOST_HOME} -> /host-share\"\n else\n ln -sfn /host-share /root/host-home\n log \"Created fallback symlink: /root/host-home -> /host-share\"\n fi\n fi\n\n # Add local domains to /etc/hosts\n HOSTS_DOMAINS=\"${MATRIX_DOMAIN%%:*} ${HICLAW_MATRIX_CLIENT_DOMAIN:-matrix-client-local.hiclaw.io} ${AI_GATEWAY_DOMAIN} ${HICLAW_FS_DOMAIN:-fs-local.hiclaw.io}\"\n if ! grep -q \"${AI_GATEWAY_DOMAIN}\" /etc/hosts 2>/dev/null; then\n echo \"127.0.0.1 ${HOSTS_DOMAINS}\" >> /etc/hosts\n log \"Added local domains to /etc/hosts\"\n fi\n\n # Wait for local infrastructure\n waitForService \"Higress Gateway\" \"127.0.0.1\" 8080 180\n waitForService \"Higress Console\" \"127.0.0.1\" 8001 180\n waitForService \"Tuwunel\" \"127.0.0.1\" 6167 120\n waitForHTTP \"Tuwunel Matrix API\" \"${HICLAW_MATRIX_URL}/_tuwunel/server_version\" 120\n waitForService \"MinIO\" \"127.0.0.1\" 9000 120\nelse\n # Cloud/K8s mode: wait for external Tuwunel\n log \"Waiting for Tuwunel Matrix server at ${HICLAW_MATRIX_URL}...\"\n _retry=0\n while [ \"${_retry}\" -lt 30 ]; do\n if curl -sf \"${HICLAW_MATRIX_URL}/_matrix/client/versions\" > /dev/null 2>&1; then\n log \"Tuwunel is ready\"\n break\n fi\n _retry=$((_retry + 1))\n log \" Waiting for Tuwunel (attempt ${_retry}/30)...\"\n sleep 5\n done\n if [ \"${_retry}\" -ge 30 ]; then\n log \"ERROR: Tuwunel not reachable at ${HICLAW_MATRIX_URL}\"\n exit 1\n fi\nfi\n\n# ============================================================\n# Auto-generate secrets if not provided via environment\n# Persisted to /data so they survive container restart\n# ============================================================\nSECRETS_FILE=\"/data/hiclaw-secrets.env\"\nif [ -f \"${SECRETS_FILE}\" ]; then\n source \"${SECRETS_FILE}\"\n log \"Loaded persisted secrets from ${SECRETS_FILE}\"\nfi\n\nif [ -z \"${HICLAW_MANAGER_GATEWAY_KEY}\" ]; then\n export HICLAW_MANAGER_GATEWAY_KEY=\"$(generateKey 32)\"\n log \"Auto-generated HICLAW_MANAGER_GATEWAY_KEY\"\nfi\nif [ -z \"${HICLAW_MANAGER_PASSWORD}\" ]; then\n export HICLAW_MANAGER_PASSWORD=\"$(generateKey 16)\"\n log \"Auto-generated HICLAW_MANAGER_PASSWORD\"\nfi\n\n# Persist secrets so they survive supervisord restart\nmkdir -p /data\ncat > \"${SECRETS_FILE}\" <<EOF\nexport HICLAW_MANAGER_GATEWAY_KEY=\"${HICLAW_MANAGER_GATEWAY_KEY}\"\nexport HICLAW_MANAGER_PASSWORD=\"${HICLAW_MANAGER_PASSWORD}\"\nEOF\nchmod 600 \"${SECRETS_FILE}\"\n\n# Cloud mode: pull workspace from OSS before initialization\nif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n HICLAW_FS=\"/root/hiclaw-fs\"\n mkdir -p \"${HICLAW_FS}/shared\" \"${HICLAW_FS}/agents\"\n log \"Pulling workspace from OSS...\"\n ensure_mc_credentials\n mc mirror \"${HICLAW_STORAGE_PREFIX}/manager/\" /root/manager-workspace/ --overwrite 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/shared/\" \"${HICLAW_FS}/shared/\" --overwrite 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/agents/\" \"${HICLAW_FS}/agents/\" --overwrite 2>/dev/null || true\n # Symlink hiclaw-fs into workspace for agent access\n ln -sfn \"${HICLAW_FS}\" /root/manager-workspace/hiclaw-fs\nfi\n\n# K8s mode: sync workspace from cluster-internal MinIO\nif [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n HICLAW_FS=\"/root/hiclaw-fs\"\n mkdir -p \"${HICLAW_FS}/shared\" \"${HICLAW_FS}/agents\" \"${HICLAW_FS}/hiclaw-config\"\n log \"Configuring mc alias for cluster MinIO...\"\n mc alias set hiclaw \"${HICLAW_FS_ENDPOINT}\" \"${HICLAW_FS_ACCESS_KEY}\" \"${HICLAW_FS_SECRET_KEY}\"\n log \"Syncing workspace from MinIO...\"\n mc mirror \"${HICLAW_STORAGE_PREFIX}/manager/\" /root/manager-workspace/ --overwrite 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/\" \"${HICLAW_FS}/\" --overwrite 2>/dev/null || true\n ln -sfn \"${HICLAW_FS}\" /root/manager-workspace/hiclaw-fs\n touch \"${HICLAW_FS}/.initialized\"\nfi\n\n# ============================================================\n# Initialize / upgrade Manager workspace\n# First boot: full init via upgrade-builtins.sh\n# Subsequent boots: compare image version; upgrade only if changed\n# ============================================================\nmkdir -p /root/manager-workspace\n\nIMAGE_VERSION=$(cat /opt/hiclaw/agent/.builtin-version 2>/dev/null || echo \"unknown\")\nINSTALLED_VERSION=$(cat /root/manager-workspace/.builtin-version 2>/dev/null || echo \"\")\n\nif [ ! -f /root/manager-workspace/.initialized ]; then\n log \"First boot: initializing manager workspace...\"\n bash /opt/hiclaw/scripts/init/upgrade-builtins.sh\n touch /root/manager-workspace/.initialized\n log \"Manager workspace initialized (version: ${IMAGE_VERSION})\"\nelif [ \"${IMAGE_VERSION}\" != \"${INSTALLED_VERSION}\" ] || [ \"${IMAGE_VERSION}\" = \"latest\" ]; then\n log \"Upgrade detected: ${INSTALLED_VERSION} -> ${IMAGE_VERSION}${IMAGE_VERSION:+ (latest: always upgrade)}\"\n bash /opt/hiclaw/scripts/init/upgrade-builtins.sh\n log \"Manager workspace upgraded to version: ${IMAGE_VERSION}\"\nelse\n log \"Workspace up to date (version: ${IMAGE_VERSION})\"\nfi\n\n# Local mode: wait for mc mirror initialization (shared + worker data in /root/hiclaw-fs/)\nif [ \"${HICLAW_RUNTIME}\" != \"aliyun\" ] && [ \"${HICLAW_RUNTIME}\" != \"k8s\" ]; then\n log \"Waiting for MinIO storage initialization...\"\n _minio_wait=0\n while [ ! -f /root/hiclaw-fs/.initialized ]; do\n sleep 2\n _minio_wait=$(( _minio_wait + 1 ))\n if [ \"${_minio_wait}\" -ge 60 ]; then\n log \"ERROR: MinIO storage initialization timed out after 120s\"\n exit 1\n fi\n done\n log \"MinIO storage initialized\"\nfi\n\n# ============================================================\n# Obtain Manager Matrix access token.\n#\n# Priority:\n# 1. Pre-injected token (HICLAW_MANAGER_MATRIX_TOKEN) — set by the\n# controller in both AppService mode (where no password exists) and\n# legacy mode (to avoid a redundant login round-trip).\n# 2. Password-based login — fallback for containers started without\n# controller involvement (manual docker run, legacy installs).\n# ============================================================\nif [ -n \"${HICLAW_MANAGER_MATRIX_TOKEN:-}\" ]; then\n MANAGER_TOKEN=\"${HICLAW_MANAGER_MATRIX_TOKEN}\"\n log \"Manager Matrix token pre-injected by controller (token prefix: ${MANAGER_TOKEN:0:10}...)\"\nelif [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n log \"K8s mode: obtaining Manager Matrix token via password login...\"\n _LOGIN_RESPONSE=$(curl -s -X POST ${HICLAW_MATRIX_URL}/_matrix/client/v3/login \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"type\": \"m.login.password\",\n \"identifier\": {\"type\": \"m.id.user\", \"user\": \"manager\"},\n \"password\": \"'\"${HICLAW_MANAGER_PASSWORD}\"'\"\n }' 2>&1)\n MANAGER_TOKEN=$(echo \"${_LOGIN_RESPONSE}\" | jq -r '.access_token' 2>/dev/null)\n if [ -z \"${MANAGER_TOKEN}\" ] || [ \"${MANAGER_TOKEN}\" = \"null\" ]; then\n log \"ERROR: Failed to obtain Manager Matrix token\"\n log \"ERROR: Login response was: ${_LOGIN_RESPONSE}\"\n exit 1\n fi\n log \"Manager Matrix token obtained (token prefix: ${MANAGER_TOKEN:0:10}...)\"\nelse\nlog \"Registering human admin Matrix account...\"\ncurl -sf -X POST ${HICLAW_MATRIX_URL}/_matrix/client/v3/register \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"username\": \"'\"${HICLAW_ADMIN_USER}\"'\",\n \"password\": \"'\"${HICLAW_ADMIN_PASSWORD}\"'\",\n \"auth\": {\n \"type\": \"m.login.registration_token\",\n \"token\": \"'\"${HICLAW_REGISTRATION_TOKEN}\"'\"\n }\n }' > /dev/null 2>&1 || log \"Admin account may already exist\"\n\nlog \"Registering Manager Agent Matrix account...\"\ncurl -sf -X POST ${HICLAW_MATRIX_URL}/_matrix/client/v3/register \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"username\": \"manager\",\n \"password\": \"'\"${HICLAW_MANAGER_PASSWORD}\"'\",\n \"auth\": {\n \"type\": \"m.login.registration_token\",\n \"token\": \"'\"${HICLAW_REGISTRATION_TOKEN}\"'\"\n }\n }' > /dev/null 2>&1 || log \"Manager account may already exist\"\n\n# Get Manager Agent's Matrix access token\nlog \"Obtaining Manager Matrix access token...\"\n_LOGIN_RESPONSE=$(curl -s -X POST ${HICLAW_MATRIX_URL}/_matrix/client/v3/login \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"type\": \"m.login.password\",\n \"identifier\": {\"type\": \"m.id.user\", \"user\": \"manager\"},\n \"password\": \"'\"${HICLAW_MANAGER_PASSWORD}\"'\"\n }' 2>&1)\n_LOGIN_EXIT=$?\nlog \"Matrix login HTTP exit code: ${_LOGIN_EXIT}\"\nlog \"Matrix login response: ${_LOGIN_RESPONSE}\"\n\nMANAGER_TOKEN=$(echo \"${_LOGIN_RESPONSE}\" | jq -r '.access_token' 2>/dev/null)\n\nif [ -z \"${MANAGER_TOKEN}\" ] || [ \"${MANAGER_TOKEN}\" = \"null\" ]; then\n log \"ERROR: Failed to obtain Manager Matrix token (exit=${_LOGIN_EXIT})\"\n log \"ERROR: Login response was: ${_LOGIN_RESPONSE}\"\n exit 1\nfi\nlog \"Manager Matrix token obtained (token prefix: ${MANAGER_TOKEN:0:10}...)\"\nfi\n\n# ============================================================\n# Higress Console initialization\n# Docker mode: full setup-higress.sh (internal Higress at localhost:8001)\n# K8s mode: skip — controller Initializer handles Higress setup\n# Cloud (aliyun) mode: skip entirely (Higress managed externally)\n# ============================================================\n_HIGRESS_CONSOLE_URL=\"\"\n_HIGRESS_USER=\"${HICLAW_ADMIN_USER}\"\n_HIGRESS_PASS=\"${HICLAW_ADMIN_PASSWORD}\"\nif [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n log \"K8s mode: skipping Higress initialization (handled by controller)\"\nelif [ \"${HICLAW_RUNTIME}\" != \"aliyun\" ]; then\n _HIGRESS_CONSOLE_URL=\"http://127.0.0.1:8001\"\nfi\n\nif [ -n \"${_HIGRESS_CONSOLE_URL}\" ]; then\n COOKIE_FILE=\"/tmp/higress-session-cookie\"\n\n log \"Waiting for Higress Console (${_HIGRESS_CONSOLE_URL}) to be fully ready and initializing admin...\"\n INIT_DONE=false\n for i in $(seq 1 90); do\n INIT_RESULT=$(curl -s -X POST \"${_HIGRESS_CONSOLE_URL}/system/init\" \\\n -H 'Content-Type: application/json' \\\n -d '{\"adminUser\":{\"name\":\"'\"${_HIGRESS_USER}\"'\",\"password\":\"'\"${_HIGRESS_PASS}\"'\",\"displayName\":\"'\"${_HIGRESS_USER}\"'\"}}' 2>/dev/null) || true\n if echo \"${INIT_RESULT}\" | grep -qE '\"success\":true|already.?init' 2>/dev/null; then\n INIT_DONE=true\n break\n fi\n if echo \"${INIT_RESULT}\" | grep -q '\"name\"' 2>/dev/null; then\n INIT_DONE=true\n break\n fi\n sleep 2\n done\n\n if [ \"${INIT_DONE}\" != \"true\" ]; then\n log \"ERROR: Higress Console did not become ready within 180s\"\n exit 1\n fi\n log \"Higress Console init done\"\n\n log \"Logging into Higress Console...\"\n LOGIN_OK=false\n for i in $(seq 1 10); do\n HTTP_CODE=$(curl -s -o /dev/null -w '%{http_code}' -X POST \"${_HIGRESS_CONSOLE_URL}/session/login\" \\\n -H 'Content-Type: application/json' \\\n -c \"${COOKIE_FILE}\" \\\n -d '{\"username\":\"'\"${_HIGRESS_USER}\"'\",\"password\":\"'\"${_HIGRESS_PASS}\"'\"}' 2>/dev/null) || true\n if { [ \"${HTTP_CODE}\" = \"200\" ] || [ \"${HTTP_CODE}\" = \"201\" ]; } && [ -f \"${COOKIE_FILE}\" ] && [ -s \"${COOKIE_FILE}\" ]; then\n LOGIN_OK=true\n break\n fi\n log \"Login attempt $i (HTTP ${HTTP_CODE}), retrying in 3s...\"\n sleep 3\n done\n\n if [ \"${LOGIN_OK}\" != \"true\" ]; then\n log \"ERROR: Could not login to Higress Console after retries\"\n exit 1\n fi\n log \"Higress Console login successful\"\n\n VERIFY_CODE=$(curl -s -o /dev/null -w '%{http_code}' \"${_HIGRESS_CONSOLE_URL}/v1/consumers\" -b \"${COOKIE_FILE}\" 2>/dev/null) || true\n if [ \"${VERIFY_CODE}\" = \"200\" ]; then\n log \"Console session verified (cookie valid)\"\n else\n log \"WARNING: Console session may be invalid (verify returned HTTP ${VERIFY_CODE})\"\n rm -f \"${COOKIE_FILE}\"\n for i in $(seq 1 5); do\n curl -s -o /dev/null -w '%{http_code}' -X POST \"${_HIGRESS_CONSOLE_URL}/session/login\" \\\n -H 'Content-Type: application/json' \\\n -c \"${COOKIE_FILE}\" \\\n -d '{\"username\":\"'\"${_HIGRESS_USER}\"'\",\"password\":\"'\"${_HIGRESS_PASS}\"'\"}' 2>/dev/null\n VERIFY2=$(curl -s -o /dev/null -w '%{http_code}' \"${_HIGRESS_CONSOLE_URL}/v1/consumers\" -b \"${COOKIE_FILE}\" 2>/dev/null) || true\n if [ \"${VERIFY2}\" = \"200\" ]; then\n log \"Re-login successful, session verified\"\n break\n fi\n sleep 2\n done\n fi\n\n export HIGRESS_COOKIE_FILE=\"${COOKIE_FILE}\"\n export HIGRESS_CONSOLE_URL=\"${_HIGRESS_CONSOLE_URL}\"\n\n if [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n # K8s mode: lightweight Higress config — only what's needed for LLM access\n source /opt/hiclaw/scripts/lib/base.sh\n _k8s_higress_api() {\n local method=\"$1\" path=\"$2\" desc=\"$3\"; shift 3; local body=\"$*\"\n local tmpfile; tmpfile=$(mktemp)\n local http_code\n http_code=$(curl -s -o \"${tmpfile}\" -w '%{http_code}' -X \"${method}\" \"${_HIGRESS_CONSOLE_URL}${path}\" \\\n -b \"${COOKIE_FILE}\" -H 'Content-Type: application/json' -d \"${body}\" 2>/dev/null) || true\n local response; response=$(cat \"${tmpfile}\" 2>/dev/null); rm -f \"${tmpfile}\"\n if echo \"${response}\" | grep -q '\"success\":true' 2>/dev/null; then\n log \"${desc} ... OK\"\n elif [ \"${http_code}\" = \"409\" ]; then\n log \"${desc} ... already exists, skipping\"\n elif [ \"${http_code}\" = \"200\" ] || [ \"${http_code}\" = \"201\" ] || [ \"${http_code}\" = \"204\" ]; then\n log \"${desc} ... OK (HTTP ${http_code})\"\n else\n log \"WARNING: ${desc} ... (HTTP ${http_code}): ${response}\"\n fi\n }\n\n # 1. Service Sources (DNS type → K8s Service FQDN)\n # Extract host:port from URLs for Higress service source registration\n _TUWUNEL_HOST=$(echo \"${HICLAW_MATRIX_URL}\" | sed 's|^http[s]*://||')\n _TUWUNEL_DOMAIN=$(echo \"${_TUWUNEL_HOST}\" | cut -d: -f1)\n _TUWUNEL_PORT=$(echo \"${_TUWUNEL_HOST}\" | cut -d: -f2)\n _k8s_higress_api POST /v1/service-sources \"Registering Tuwunel service source\" \\\n '{\"type\":\"dns\",\"name\":\"tuwunel\",\"domain\":\"'\"${_TUWUNEL_DOMAIN}\"'\",\"port\":'\"${_TUWUNEL_PORT}\"'}'\n\n if [ -n \"${HICLAW_ELEMENT_WEB_URL:-}\" ]; then\n _ELEMENT_HOST=$(echo \"${HICLAW_ELEMENT_WEB_URL}\" | sed 's|^http[s]*://||')\n _ELEMENT_DOMAIN=$(echo \"${_ELEMENT_HOST}\" | cut -d: -f1)\n _ELEMENT_PORT=$(echo \"${_ELEMENT_HOST}\" | cut -d: -f2)\n _k8s_higress_api POST /v1/service-sources \"Registering Element Web service source\" \\\n '{\"type\":\"dns\",\"name\":\"element-web\",\"domain\":\"'\"${_ELEMENT_DOMAIN}\"'\",\"port\":'\"${_ELEMENT_PORT}\"'}'\n fi\n\n # 2. Manager Consumer (key-auth)\n _k8s_higress_api POST /v1/consumers \"Creating Manager consumer\" \\\n '{\"name\":\"manager\",\"credentials\":[{\"type\":\"key-auth\",\"source\":\"BEARER\",\"values\":[\"'\"${HICLAW_MANAGER_GATEWAY_KEY}\"'\"]}]}'\n\n # 3. LLM Provider\n _LLM_PROVIDER=\"${HICLAW_LLM_PROVIDER:-qwen}\"\n _LLM_API_URL=\"${HICLAW_LLM_API_URL:-}\"\n if [ -z \"${_LLM_API_URL}\" ]; then\n case \"${_LLM_PROVIDER}\" in\n qwen) _LLM_API_URL=\"https://dashscope.aliyuncs.com/compatible-mode/v1\" ;;\n esac\n fi\n # 4. LLM Provider type-specific config\n case \"${_LLM_PROVIDER}\" in\n qwen)\n _k8s_higress_api POST /v1/ai/providers \"Creating LLM provider (qwen)\" \\\n '{\"type\":\"qwen\",\"name\":\"qwen\",\"tokens\":[\"'\"${HICLAW_LLM_API_KEY}\"'\"],\"protocol\":\"openai/v1\",\"tokenFailoverConfig\":{\"enabled\":false},\"rawConfigs\":{\"qwenEnableSearch\":false,\"qwenEnableCompatible\":true,\"qwenFileIds\":[],\"hiclawMode\":true}}'\n ;;\n *)\n _BODY='{\"name\":\"'\"${_LLM_PROVIDER}\"'\",\"type\":\"openai\",\"tokens\":[\"'\"${HICLAW_LLM_API_KEY}\"'\"],\"modelMapping\":{},\"protocol\":\"openai/v1\",\"rawConfigs\":{\"hiclawMode\":true}}'\n _k8s_higress_api POST /v1/ai/providers \"Creating LLM provider (${_LLM_PROVIDER})\" \"${_BODY}\"\n ;;\n esac\n\n # 5. AI Route (bind provider + consumer auth, /v1 prefix to avoid clash with Element Web catch-all)\n _k8s_higress_api POST /v1/ai/routes \"Creating AI Gateway route\" \\\n '{\"name\":\"default-ai-route\",\"domains\":[],\"pathPredicate\":{\"matchType\":\"PRE\",\"matchValue\":\"/v1\",\"caseSensitive\":false},\"upstreams\":[{\"provider\":\"'\"${_LLM_PROVIDER}\"'\",\"weight\":100,\"modelMapping\":{}}],\"authConfig\":{\"enabled\":true,\"allowedCredentialTypes\":[\"key-auth\"],\"allowedConsumers\":[\"manager\"]}}'\n\n # 6. Matrix Homeserver Route (/_matrix/* → Tuwunel, no auth)\n _k8s_higress_api POST /v1/routes \"Creating Matrix Homeserver route\" \\\n '{\"name\":\"matrix-homeserver\",\"domains\":[],\"path\":{\"matchType\":\"PRE\",\"matchValue\":\"/_matrix\"},\"services\":[{\"name\":\"tuwunel.dns\",\"port\":'\"${_TUWUNEL_PORT}\"',\"weight\":100}]}'\n\n # 7. Element Web Route (/ catch-all → Element Web, no auth)\n if [ -n \"${HICLAW_ELEMENT_WEB_URL:-}\" ]; then\n _k8s_higress_api POST /v1/routes \"Creating Element Web route\" \\\n '{\"name\":\"element-web\",\"domains\":[],\"path\":{\"matchType\":\"PRE\",\"matchValue\":\"/\"},\"services\":[{\"name\":\"element-web.dns\",\"port\":'\"${_ELEMENT_PORT}\"',\"weight\":100}]}'\n fi\n\n # 8. Remove Higress default landing page (Exact match on / takes precedence over Element Web catch-all)\n _k8s_higress_api DELETE /v1/routes/default \"Removing Higress default landing route\"\n\n log \"K8s Higress lightweight setup complete\"\n\n # Wait for AI plugin activation (~45s for first config)\n log \"Waiting for AI Gateway plugin activation (45s)...\"\n sleep 45\n else\n # Docker mode: full setup with all routes, domains, MCP servers\n /opt/hiclaw/scripts/init/setup-higress.sh\n fi\nfi\n\n# ============================================================\n# Create admin DM room, persist to state.json, send welcome message\n# K8s mode: skip — controller ProvisionManager creates the Admin DM\n# room (Step 4 in service/provisioner.go) AND reconcileManagerWelcome\n# delivers the first-boot onboarding prompt once OpenClaw inside this\n# container has joined the room. In k8s mode the manager intentionally\n# does NOT have the admin password (only HICLAW_ADMIN_USER), so it\n# could not log in as admin to send the welcome itself anyway. The\n# Manager Agent discovers its admin DM room on first heartbeat via\n# state.json / `manage-state.sh --action set-admin-dm` (see HEARTBEAT.md\n# Step 1) — it does not need it to be pre-injected by this script.\n# Runs in both local and cloud modes (idempotent)\n# ============================================================\nif [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n log \"K8s mode: skipping admin DM room creation and welcome message (both handled by hiclaw-controller)\"\nelse\nMANAGER_FULL_ID=\"@manager:${MATRIX_DOMAIN}\"\nADMIN_FULL_ID=\"@${HICLAW_ADMIN_USER}:${MATRIX_DOMAIN}\"\n\nlog \"Logging in as admin to create DM room...\"\n_ADMIN_LOGIN=$(curl -sf -X POST \"${HICLAW_MATRIX_URL}/_matrix/client/v3/login\" \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"type\": \"m.login.password\",\n \"identifier\": {\"type\": \"m.id.user\", \"user\": \"'\"${HICLAW_ADMIN_USER}\"'\"},\n \"password\": \"'\"${HICLAW_ADMIN_PASSWORD}\"'\"\n }' 2>&1) || true\n\nADMIN_MATRIX_TOKEN=$(echo \"${_ADMIN_LOGIN}\" | jq -r '.access_token // empty' 2>/dev/null)\nif [ -z \"${ADMIN_MATRIX_TOKEN}\" ]; then\n log \"WARNING: Failed to login as admin, skipping DM room creation\"\nelse\n # Search for existing DM room with Manager (idempotent)\n DM_ROOM_ID=\"\"\n _JOINED_ROOMS=$(curl -sf \"${HICLAW_MATRIX_URL}/_matrix/client/v3/joined_rooms\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" 2>/dev/null \\\n | jq -r '.joined_rooms[]' 2>/dev/null) || true\n for _rid in ${_JOINED_ROOMS}; do\n _members=$(curl -sf \"${HICLAW_MATRIX_URL}/_matrix/client/v3/rooms/${_rid}/members\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" 2>/dev/null \\\n | jq -r '.chunk[].state_key' 2>/dev/null) || continue\n _count=$(echo \"${_members}\" | wc -l | xargs)\n if [ \"${_count}\" = \"2\" ] && echo \"${_members}\" | grep -q \"@manager:\"; then\n DM_ROOM_ID=\"${_rid}\"\n break\n fi\n done\n\n if [ -n \"${DM_ROOM_ID}\" ]; then\n log \"Existing DM room found: ${DM_ROOM_ID}\"\n else\n log \"Creating DM room with Manager...\"\n _RAW=$(curl -s -w '\\nHTTP_CODE:%{http_code}' -X POST \"${HICLAW_MATRIX_URL}/_matrix/client/v3/createRoom\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d \"{\\\"is_direct\\\":true,\\\"invite\\\":[\\\"${MANAGER_FULL_ID}\\\"],\\\"preset\\\":\\\"trusted_private_chat\\\"}\" 2>&1) || true\n _HTTP_CODE=$(echo \"${_RAW}\" | tail -1 | sed 's/HTTP_CODE://')\n _CREATE_RESP=$(echo \"${_RAW}\" | sed '$d')\n DM_ROOM_ID=$(echo \"${_CREATE_RESP}\" | jq -r '.room_id // empty' 2>/dev/null)\n if [ -n \"${DM_ROOM_ID}\" ]; then\n log \"DM room created: ${DM_ROOM_ID}\"\n else\n log \"WARNING: Failed to create DM room (HTTP ${_HTTP_CODE}): ${_CREATE_RESP}\"\n fi\n fi\n\n # Persist admin DM room ID to state.json\n if [ -n \"${DM_ROOM_ID}\" ]; then\n STATE_SCRIPT=\"/opt/hiclaw/agent/skills/task-management/scripts/manage-state.sh\"\n if [ -f \"${STATE_SCRIPT}\" ]; then\n bash \"${STATE_SCRIPT}\" --action init 2>/dev/null || true\n bash \"${STATE_SCRIPT}\" --action set-admin-dm --room-id \"${DM_ROOM_ID}\" 2>/dev/null || true\n log \"Admin DM room persisted to state.json: ${DM_ROOM_ID}\"\n fi\n fi\n\n # Schedule welcome message in background (only on first boot)\n if [ -n \"${DM_ROOM_ID}\" ] && [ ! -f \"/root/manager-workspace/soul-configured\" ]; then\n log \"Scheduling welcome message (background, waiting for OpenClaw to start)...\"\n (\n _HICLAW_LANGUAGE=\"${HICLAW_LANGUAGE:-zh}\"\n _HICLAW_TIMEZONE=\"${TZ:-Asia/Shanghai}\"\n _wait=0\n _ready=false\n while [ \"${_wait}\" -lt 300 ]; do\n if curl -sf http://127.0.0.1:18799/ > /dev/null 2>&1; then\n _ready=true\n break\n fi\n sleep 3\n _wait=$((_wait + 3))\n done\n if [ \"${_ready}\" != \"true\" ]; then\n echo \"[manager] WARNING: OpenClaw gateway not ready within 300s, skipping welcome message\"\n exit 0\n fi\n # Ensure Manager has joined the DM room before sending the welcome\n # message. Without this, there is a race between OpenClaw's Matrix\n # auto-join and the message send — the message may land before Manager\n # joins, so OpenClaw's /sync never picks it up.\n _join_ok=false\n for _join_attempt in 1 2 3; do\n if curl -sf -X POST \"${HICLAW_MATRIX_URL}/_matrix/client/v3/rooms/${DM_ROOM_ID}/join\" \\\n -H \"Authorization: Bearer ${MANAGER_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d '{}' > /dev/null 2>&1; then\n echo \"[manager] Manager joined DM room before welcome message\"\n _join_ok=true\n break\n fi\n sleep 2\n done\n if [ \"${_join_ok}\" != \"true\" ]; then\n echo \"[manager] WARNING: Manager join request failed after 3 attempts (may already be joined)\"\n fi\n _welcome_msg=\"This is an automated message from the HiClaw setup. This is a fresh installation.\n\n--- Installation Context ---\nUser Language: ${_HICLAW_LANGUAGE} (zh = Chinese, en = English)\nUser Timezone: ${_HICLAW_TIMEZONE} (IANA timezone identifier)\n---\n\nYou are an AI agent that manages a team of worker agents. Your identity and personality have not been configured yet — the human admin is about to meet you for the first time.\n\nPlease begin the onboarding conversation:\n\n1. Greet the admin warmly and briefly describe what you can do (coordinate workers, manage tasks, run multi-agent projects)\n2. The user has selected \\\"${_HICLAW_LANGUAGE}\\\" as their preferred language during installation. Use this language for your greeting and all subsequent communication.\n3. The user's timezone is ${_HICLAW_TIMEZONE}. Based on this timezone, you may infer their likely region and suggest additional language options.\n4. Ask them: a) What would they like to call you? b) Communication style preference? c) Any behavior guidelines? d) Confirm default language\n5. After they reply, write their preferences to ~/SOUL.md\n6. Confirm what you wrote, and ask if they would like to adjust anything\n7. Once confirmed, run: touch ~/soul-configured\n\nThe human admin will start chatting shortly.\"\n _txn_id=\"welcome-$(date +%s)\"\n _payload=$(jq -nc --arg body \"${_welcome_msg}\" '{\"msgtype\":\"m.text\",\"body\":$body}')\n _raw=$(curl -s -w '\\nHTTP_CODE:%{http_code}' -X PUT \"${HICLAW_MATRIX_URL}/_matrix/client/v3/rooms/${DM_ROOM_ID}/send/m.room.message/${_txn_id}\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d \"${_payload}\" 2>&1) || true\n _http_code=$(echo \"${_raw}\" | tail -1 | sed 's/HTTP_CODE://')\n _send_resp=$(echo \"${_raw}\" | sed '$d')\n if echo \"${_send_resp}\" | jq -e '.event_id' > /dev/null 2>&1; then\n echo \"[manager] Welcome message sent to DM room\"\n else\n echo \"[manager] WARNING: Failed to send welcome message (HTTP ${_http_code}): ${_send_resp}\"\n fi\n ) &\n log \"Welcome message background process started (PID: $!)\"\n fi\nfi\nfi # end K8s mode skip for admin DM room\n\n# ============================================================\n# Generate Manager Agent openclaw.json from template\n# ============================================================\nlog \"Generating Manager openclaw.json...\"\nexport MANAGER_MATRIX_TOKEN=\"${MANAGER_TOKEN}\"\nexport MANAGER_GATEWAY_KEY=\"${HICLAW_MANAGER_GATEWAY_KEY}\"\n# Resolve model parameters based on model name\nMODEL_NAME=\"${HICLAW_DEFAULT_MODEL:-qwen3.6-plus}\"\ncase \"${MODEL_NAME}\" in\n gpt-5.3-codex|gpt-5-mini|gpt-5-nano)\n export MODEL_CONTEXT_WINDOW=400000 MODEL_MAX_TOKENS=128000 ;;\n claude-opus-4-6)\n export MODEL_CONTEXT_WINDOW=1000000 MODEL_MAX_TOKENS=128000 ;;\n claude-sonnet-4-6)\n export MODEL_CONTEXT_WINDOW=1000000 MODEL_MAX_TOKENS=64000 ;;\n claude-haiku-4-5)\n export MODEL_CONTEXT_WINDOW=200000 MODEL_MAX_TOKENS=64000 ;;\n qwen3.6-plus|qwen3.5-plus)\n export MODEL_CONTEXT_WINDOW=200000 MODEL_MAX_TOKENS=64000 ;;\n deepseek-chat|deepseek-reasoner|kimi-k2.5)\n export MODEL_CONTEXT_WINDOW=256000 MODEL_MAX_TOKENS=128000 ;;\n glm-5|MiniMax-M2.7|MiniMax-M2.7-highspeed|MiniMax-M2.5)\n export MODEL_CONTEXT_WINDOW=200000 MODEL_MAX_TOKENS=128000 ;;\n *)\n export MODEL_CONTEXT_WINDOW=150000 MODEL_MAX_TOKENS=128000 ;;\nesac\nexport MODEL_REASONING=true\n\n# Override with user-supplied custom model parameters from env (set during install)\n[ -n \"${HICLAW_MODEL_CONTEXT_WINDOW:-}\" ] && export MODEL_CONTEXT_WINDOW=\"${HICLAW_MODEL_CONTEXT_WINDOW}\"\n[ -n \"${HICLAW_MODEL_MAX_TOKENS:-}\" ] && export MODEL_MAX_TOKENS=\"${HICLAW_MODEL_MAX_TOKENS}\"\n[ -n \"${HICLAW_MODEL_REASONING:-}\" ] && export MODEL_REASONING=\"${HICLAW_MODEL_REASONING}\"\n\n# E2EE: convert HICLAW_MATRIX_E2EE to JSON boolean for template substitution\nif [ \"${HICLAW_MATRIX_E2EE:-0}\" = \"1\" ] || [ \"${HICLAW_MATRIX_E2EE:-}\" = \"true\" ]; then\n export MATRIX_E2EE_ENABLED=true\nelse\n export MATRIX_E2EE_ENABLED=false\nfi\nlog \"Matrix E2EE: ${MATRIX_E2EE_ENABLED}\"\n\n# Resolve input modalities: only vision-capable models get \"image\"\ncase \"${MODEL_NAME}\" in\n gpt-5.4|gpt-5.3-codex|gpt-5-mini|gpt-5-nano|claude-opus-4-6|claude-sonnet-4-6|claude-haiku-4-5|qwen3.6-plus|qwen3.5-plus|kimi-k2.5)\n export MODEL_INPUT='[\"text\", \"image\"]' ;;\n *)\n export MODEL_INPUT='[\"text\"]' ;;\nesac\n# Override with user-supplied vision setting from env\nif [ \"${HICLAW_MODEL_VISION:-}\" = \"true\" ]; then\n export MODEL_INPUT='[\"text\", \"image\"]'\nelif [ \"${HICLAW_MODEL_VISION:-}\" = \"false\" ]; then\n export MODEL_INPUT='[\"text\"]'\nfi\n\nlog \"Model: ${MODEL_NAME} (context=${MODEL_CONTEXT_WINDOW}, maxTokens=${MODEL_MAX_TOKENS}, reasoning=${MODEL_REASONING}, input=${MODEL_INPUT})\"\n\nif [ -f /root/manager-workspace/openclaw.json ]; then\n log \"Manager openclaw.json already exists, updating dynamic fields only (preserving user customizations)...\"\n # Merge known models into existing config (add missing, preserve user-added)\n # Use known-models.json (valid JSON) instead of template (contains ${VAR} placeholders)\n KNOWN_MODELS=$(cat /opt/hiclaw/configs/known-models.json 2>/dev/null || echo '[]')\n jq --arg token \"${MANAGER_TOKEN}\" \\\n --arg key \"${HICLAW_MANAGER_GATEWAY_KEY}\" \\\n --arg model \"${MODEL_NAME}\" \\\n --arg emb_model \"${HICLAW_EMBEDDING_MODEL}\" \\\n --arg aigw_domain \"${AI_GATEWAY_DOMAIN}\" \\\n --arg matrix_user_id \"@manager:${MATRIX_DOMAIN}\" \\\n --argjson e2ee \"${MATRIX_E2EE_ENABLED}\" \\\n --argjson known_models \"${KNOWN_MODELS}\" \\\n --argjson ctx \"${MODEL_CONTEXT_WINDOW}\" \\\n --argjson max \"${MODEL_MAX_TOKENS}\" \\\n --argjson reasoning \"${MODEL_REASONING}\" \\\n --argjson input \"${MODEL_INPUT}\" \\\n '\n # Merge known models: add any model id not already present\n .models.providers[\"hiclaw-gateway\"].models as $existing\n | ($existing | map(.id)) as $existing_ids\n | ($known_models | map(select(.id as $id | $existing_ids | index($id) | not))) as $new\n | .models.providers[\"hiclaw-gateway\"].models = ($existing + $new)\n # Ensure the user-chosen default model is in the list (custom model support)\n | if (.models.providers[\"hiclaw-gateway\"].models | map(.id) | index($model) | not) then\n .models.providers[\"hiclaw-gateway\"].models += [{\"id\": $model, \"name\": $model, \"reasoning\": $reasoning, \"contextWindow\": $ctx, \"maxTokens\": $max, \"input\": $input}]\n else . end\n # Rebuild model aliases from the full models list\n | (.models.providers[\"hiclaw-gateway\"].models | map({ (\"hiclaw-gateway/\" + .id): { \"alias\": .id } }) | add // {}) as $aliases\n | .agents.defaults.models = ((.agents.defaults.models // {}) + $aliases)\n | .channels.matrix.accessToken = $token | .channels.matrix.userId = $matrix_user_id | .models.providers[\"hiclaw-gateway\"].apiKey = $key\n | ((.hooks.token // \"\") as $ht | if $ht == $key or $ht == ($key + \"-hooks\" | @base64) then del(.hooks) else . end)\n | .agents.defaults.model.primary = (\"hiclaw-gateway/\" + $model)\n | .commands.restart = true\n | .gateway.port = 18799\n | .gateway.bind = \"lan\"\n | .gateway.controlUi = ((.gateway.controlUi // {}) + {\"dangerouslyDisableDeviceAuth\": true, \"allowInsecureAuth\": true, \"allowedOrigins\": [\"*\"]})\n | .channels.matrix.encryption = $e2ee\n | .channels.matrix.network = ((.channels.matrix.network // {}) + {\"dangerouslyAllowPrivateNetwork\": true})\n | .channels.matrix.autoJoin = \"always\"\n # OpenClaw YOLO defaults: host exec without approval prompts (see openclaw docs tools/exec-approvals)\n | .tools = (.tools // {})\n | .tools.exec = ((.tools.exec // {}) + {\"host\":\"gateway\",\"security\":\"full\",\"ask\":\"off\"})\n | .tools.elevated = (.tools.elevated // {})\n | .tools.elevated.enabled = true\n | .tools.elevated.allowFrom |= ((. // {}) | .matrix = [\"*\"])\n | .agents.defaults.elevatedDefault = \"full\"\n # Ensure memorySearch config exists (embedding model for memory) — skip if embedding model is empty\n | if $emb_model != \"\" then .agents.defaults.memorySearch //= {\"provider\":\"openai\",\"model\":$emb_model,\"remote\":{\"baseUrl\":(\"http://\" + $aigw_domain + \":8080/v1\"),\"apiKey\":$key}} else . end\n ' \\\n /root/manager-workspace/openclaw.json > /tmp/openclaw.json.tmp && \\\n mv /tmp/openclaw.json.tmp /root/manager-workspace/openclaw.json\n # Disable openclaw's observe-recovery mechanism which compares config against\n # a lastKnownGood baseline in config-health.json. When meta is missing from the\n # current file but present in the baseline, observe-recovery restores from .bak,\n # undoing user customizations (plugins, channels, etc).\n # Clearing config-health.json removes the baseline so observe-recovery won't\n # interfere, while preserving .bak as a backup.\n rm -f /root/manager-workspace/.openclaw/logs/config-health.json\n # Verify the token was written correctly\n _written_token=$(jq -r '.channels.matrix.accessToken' /root/manager-workspace/openclaw.json 2>/dev/null)\n if [ -z \"${_written_token}\" ] || [ \"${_written_token}\" = \"null\" ]; then\n log \"ERROR: Matrix token was not written correctly to openclaw.json (got: ${_written_token})\"\n else\n log \"Matrix token written to openclaw.json (prefix: ${_written_token:0:10}...)\"\n fi\nelse\n log \"Manager openclaw.json not found, generating from template...\"\n envsubst < /opt/hiclaw/configs/manager-openclaw.json.tmpl > /root/manager-workspace/openclaw.json\n # Post-envsubst injection: memorySearch + custom model (single jq pass when possible)\n if ! jq -e --arg model \"${MODEL_NAME}\" '.models.providers[\"hiclaw-gateway\"].models | map(.id) | index($model)' /root/manager-workspace/openclaw.json > /dev/null 2>&1; then\n log \"Custom model '${MODEL_NAME}' not in built-in list, injecting into config...\"\n jq --arg emb_model \"${HICLAW_EMBEDDING_MODEL}\" \\\n --arg aigw_domain \"${AI_GATEWAY_DOMAIN}\" \\\n --arg key \"${HICLAW_MANAGER_GATEWAY_KEY}\" \\\n --arg model \"${MODEL_NAME}\" \\\n --argjson ctx \"${MODEL_CONTEXT_WINDOW}\" \\\n --argjson max \"${MODEL_MAX_TOKENS}\" \\\n --argjson reasoning \"${MODEL_REASONING}\" \\\n --argjson input \"${MODEL_INPUT}\" \\\n '\n (if $emb_model != \"\" then .agents.defaults.memorySearch = {\"provider\":\"openai\",\"model\":$emb_model,\"remote\":{\"baseUrl\":(\"http://\" + $aigw_domain + \":8080/v1\"),\"apiKey\":$key}} else . end)\n | .models.providers[\"hiclaw-gateway\"].models += [{\"id\": $model, \"name\": $model, \"reasoning\": $reasoning, \"contextWindow\": $ctx, \"maxTokens\": $max, \"input\": $input}]\n | .agents.defaults.models += {(\"hiclaw-gateway/\" + $model): {\"alias\": $model}}\n ' /root/manager-workspace/openclaw.json > /tmp/openclaw.json.tmp && \\\n mv /tmp/openclaw.json.tmp /root/manager-workspace/openclaw.json\n elif [ -n \"${HICLAW_EMBEDDING_MODEL}\" ]; then\n jq --arg emb_model \"${HICLAW_EMBEDDING_MODEL}\" \\\n --arg aigw_domain \"${AI_GATEWAY_DOMAIN}\" \\\n --arg key \"${HICLAW_MANAGER_GATEWAY_KEY}\" \\\n '.agents.defaults.memorySearch = {\"provider\":\"openai\",\"model\":$emb_model,\"remote\":{\"baseUrl\":(\"http://\" + $aigw_domain + \":8080/v1\"),\"apiKey\":$key}}' \\\n /root/manager-workspace/openclaw.json > /tmp/openclaw.json.tmp && \\\n mv /tmp/openclaw.json.tmp /root/manager-workspace/openclaw.json\n fi\n _written_token=$(jq -r '.channels.matrix.accessToken' /root/manager-workspace/openclaw.json 2>/dev/null)\n log \"Matrix token written from template (prefix: ${_written_token:0:10}...)\"\nfi\n\n# Cloud/K8s mode: overlay cloud-specific settings onto generated config\nif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ] || [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n log \"Applying cloud/k8s overlay to openclaw.json...\"\n jq --arg homeserver \"${HICLAW_MATRIX_URL}\" \\\n --arg gateway \"${HICLAW_AI_GATEWAY_URL}/v1\" \\\n --arg key \"${HICLAW_MANAGER_GATEWAY_KEY}\" \\\n '.channels.matrix.homeserver = $homeserver\n | .models.providers[\"hiclaw-gateway\"].baseUrl = $gateway\n | .models.providers[\"hiclaw-gateway\"].apiKey = $key\n | ((.hooks.token // \"\") as $ht | if $ht == $key or $ht == ($key + \"-hooks\" | @base64) then del(.hooks) else . end)\n | .commands.restart = false\n | if .agents.defaults.memorySearch then .agents.defaults.memorySearch.remote.baseUrl = $gateway | .agents.defaults.memorySearch.remote.apiKey = $key else . end' \\\n /root/manager-workspace/openclaw.json > /tmp/openclaw-cloud.json && \\\n mv /tmp/openclaw-cloud.json /root/manager-workspace/openclaw.json\n log \"Cloud/K8s overlay applied\"\nfi\n\n# ============================================================\n# Optional: enable openclaw-cms-plugin observability\n# Config is applied at runtime so secrets stay out of image layers.\n# ============================================================\nCMS_TRACES_ENABLED=\"$(echo \"${HICLAW_CMS_TRACES_ENABLED:-false}\" | tr '[:upper:]' '[:lower:]')\"\nif [ \"${CMS_TRACES_ENABLED}\" = \"true\" ]; then\n CMS_PLUGIN_NAME=\"openclaw-cms-plugin\"\n CMS_PLUGIN_DIR=\"${OPENCLAW_CMS_PLUGIN_DIR:-/opt/openclaw/extensions/openclaw-cms-plugin}\"\n CMS_PLUGIN_MANIFEST=\"${CMS_PLUGIN_DIR}/openclaw.plugin.json\"\n DIAG_PLUGIN_NAME=\"diagnostics-otel\"\n DIAG_PLUGIN_DIR=\"/opt/openclaw/extensions/diagnostics-otel\"\n CMS_LICENSE_KEY=\"${HICLAW_CMS_LICENSE_KEY:-}\"\n CMS_PROJECT=\"${HICLAW_CMS_PROJECT:-}\"\n CMS_METRICS_ENABLED=\"${HICLAW_CMS_METRICS_ENABLED:-false}\"\n\n if [ ! -f \"${CMS_PLUGIN_MANIFEST}\" ]; then\n log \"WARNING: ${CMS_PLUGIN_NAME} manifest not found at ${CMS_PLUGIN_MANIFEST}, skipping plugin config\"\n else\n _missing=0\n [ -z \"${HICLAW_CMS_ENDPOINT:-}\" ] && log \"WARNING: HICLAW_CMS_ENDPOINT is required when HICLAW_CMS_TRACES_ENABLED=true\" && _missing=1\n [ -z \"${CMS_LICENSE_KEY:-}\" ] && log \"WARNING: HICLAW_CMS_LICENSE_KEY is required when HICLAW_CMS_TRACES_ENABLED=true\" && _missing=1\n [ -z \"${HICLAW_CMS_WORKSPACE:-}\" ] && log \"WARNING: HICLAW_CMS_WORKSPACE is required when HICLAW_CMS_TRACES_ENABLED=true\" && _missing=1\n\n if [ \"${_missing}\" = \"0\" ]; then\n CMS_SERVICE_NAME=\"${HICLAW_CMS_SERVICE_NAME:-hiclaw-manager}\"\n CMS_ENABLE_METRICS=\"${CMS_METRICS_ENABLED}\"\n DIAG_AVAILABLE=\"0\"\n _metrics_lc=\"$(echo \"${CMS_ENABLE_METRICS}\" | tr '[:upper:]' '[:lower:]')\"\n if [ \"${_metrics_lc}\" = \"true\" ]; then\n if [ -f \"${DIAG_PLUGIN_DIR}/package.json\" ]; then\n DIAG_AVAILABLE=\"1\"\n if [ ! -d \"${DIAG_PLUGIN_DIR}/node_modules\" ]; then\n log \"diagnostics-otel dependencies missing, installing...\"\n if (cd \"${DIAG_PLUGIN_DIR}\" && npm install --omit=dev --ignore-scripts >/tmp/hiclaw-diag-install.log 2>&1); then\n log \"diagnostics-otel dependencies installed\"\n else\n log \"WARNING: diagnostics-otel npm install failed, metrics plugin may not load\"\n fi\n else\n log \"diagnostics-otel dependencies already present\"\n fi\n else\n log \"WARNING: diagnostics-otel package.json not found at ${DIAG_PLUGIN_DIR}, metrics plugin may not load\"\n fi\n fi\n\n log \"Applying ${CMS_PLUGIN_NAME} config to openclaw.json...\"\n jq --arg pluginName \"${CMS_PLUGIN_NAME}\" \\\n --arg pluginDir \"${CMS_PLUGIN_DIR}\" \\\n --arg endpoint \"${HICLAW_CMS_ENDPOINT}\" \\\n --arg licenseKey \"${CMS_LICENSE_KEY}\" \\\n --arg armsProject \"${CMS_PROJECT}\" \\\n --arg cmsWorkspace \"${HICLAW_CMS_WORKSPACE}\" \\\n --arg serviceName \"${CMS_SERVICE_NAME}\" \\\n --arg diagPluginName \"${DIAG_PLUGIN_NAME}\" \\\n --arg diagPluginDir \"${DIAG_PLUGIN_DIR}\" \\\n --arg metricsRaw \"${CMS_ENABLE_METRICS}\" \\\n --arg diagAvailableRaw \"${DIAG_AVAILABLE}\" \\\n '\n .plugins = (.plugins // {})\n | .plugins.load = (.plugins.load // {})\n | .plugins.entries = (.plugins.entries // {})\n | if (.plugins.allow | type) != \"array\" then .plugins.allow = [] else . end\n | if (.plugins.allow | index($pluginName)) == null then .plugins.allow += [$pluginName] else . end\n | if (.plugins.load.paths | type) != \"array\" then .plugins.load.paths = [] else . end\n | if (.plugins.load.paths | index($pluginDir)) == null then .plugins.load.paths += [$pluginDir] else . end\n | .plugins.entries[$pluginName] = {\n \"enabled\": true,\n \"config\": {\n \"endpoint\": $endpoint,\n \"headers\": {\n \"x-arms-license-key\": $licenseKey,\n \"x-arms-project\": $armsProject,\n \"x-cms-workspace\": $cmsWorkspace\n },\n \"serviceName\": $serviceName\n }\n }\n\n # diagnostics-otel metrics (optional)\n | ($metricsRaw | ascii_downcase) as $m\n | ($diagAvailableRaw == \"1\") as $diagAvailable\n | (($m == \"true\") and $diagAvailable) as $metricsEnabled\n | if $metricsEnabled then\n (if (.plugins.allow | index($diagPluginName)) == null then .plugins.allow += [$diagPluginName] else . end)\n | (if (.plugins.load.paths | index($diagPluginDir)) == null then .plugins.load.paths += [$diagPluginDir] else . end)\n | .plugins.entries[$diagPluginName].enabled = true\n | .diagnostics = (.diagnostics // {})\n | .diagnostics.otel = (.diagnostics.otel // {})\n | .diagnostics.enabled = true\n | .diagnostics.otel.enabled = true\n | .diagnostics.otel.endpoint = $endpoint\n | .diagnostics.otel.protocol = (.diagnostics.otel.protocol // \"http/protobuf\")\n | .diagnostics.otel.headers = {\n \"x-arms-license-key\": $licenseKey,\n \"x-arms-project\": $armsProject,\n \"x-cms-workspace\": $cmsWorkspace\n }\n | .diagnostics.otel.serviceName = $serviceName\n | .diagnostics.otel.metrics = true\n | .diagnostics.otel.traces = (.diagnostics.otel.traces // false)\n | .diagnostics.otel.logs = (.diagnostics.otel.logs // false)\n else\n .\n end\n ' /root/manager-workspace/openclaw.json > /tmp/openclaw-cms.json && \\\n mv /tmp/openclaw-cms.json /root/manager-workspace/openclaw.json\n log \"${CMS_PLUGIN_NAME} config applied (metrics=${CMS_ENABLE_METRICS}, service=${CMS_SERVICE_NAME})\"\n else\n log \"Skipping ${CMS_PLUGIN_NAME} config due to missing required env vars\"\n fi\n fi\nfi\n\n# ============================================================\n# Detect container runtime (for Worker creation)\n# ============================================================\nsource /opt/hiclaw/scripts/lib/container-api.sh\nif container_api_available; then\n log \"Container runtime socket detected at ${CONTAINER_SOCKET} — direct Worker creation enabled\"\n export HICLAW_CONTAINER_RUNTIME=\"socket\"\nelif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ] || [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n log \"Cloud/K8s mode — Workers created via controller API\"\n export HICLAW_CONTAINER_RUNTIME=\"cloud\"\nelse\n log \"No container runtime found — Worker creation will output install commands\"\n export HICLAW_CONTAINER_RUNTIME=\"none\"\nfi\n\n# ============================================================\n# Upgrade Worker openclaw.json: merge known models + E2EE flag into existing configs\n# Existing workers in MinIO may have old single-model configs or missing encryption field.\n# Merge template models so they can hot-switch without restart.\n# ============================================================\nREGISTRY_FILE=\"/root/manager-workspace/workers-registry.json\"\nif [ -f \"${REGISTRY_FILE}\" ]; then\n # Use known-models.json (valid JSON) instead of template (contains ${VAR} placeholders)\n KNOWN_MODELS_FILE=\"/opt/hiclaw/configs/known-models.json\"\n if [ -f \"${KNOWN_MODELS_FILE}\" ]; then\n _KNOWN_MODELS=$(cat \"${KNOWN_MODELS_FILE}\")\n for _wname in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_wname}\" ] && continue\n _minio_path=\"${HICLAW_STORAGE_PREFIX}/agents/${_wname}/openclaw.json\"\n _tmp_in=\"/tmp/openclaw-${_wname}-models-upgrade-in.json\"\n if mc cp \"${_minio_path}\" \"${_tmp_in}\" 2>/dev/null; then\n _tmp_out=\"/tmp/openclaw-${_wname}-models-upgrade-out.json\"\n # Idempotent merge: add missing known models, rebuild aliases, set e2ee.\n # Always runs — jq deduplicates by model id, so re-runs are safe.\n jq --argjson known_models \"${_KNOWN_MODELS}\" \\\n --argjson e2ee \"${MATRIX_E2EE_ENABLED}\" '\n .models.providers[\"hiclaw-gateway\"].models as $existing\n | ($existing | map(.id)) as $existing_ids\n | ($known_models | map(select(.id as $id | $existing_ids | index($id) | not))) as $new\n | .models.providers[\"hiclaw-gateway\"].models = ($existing + $new)\n | (.models.providers[\"hiclaw-gateway\"].models | map({ (\"hiclaw-gateway/\" + .id): { \"alias\": .id } }) | add // {}) as $aliases\n | .agents.defaults.models = ((.agents.defaults.models // {}) + $aliases)\n | .channels.matrix.encryption = $e2ee\n | .channels.matrix.autoJoin = \"always\"\n | .tools = (.tools // {})\n | .tools.exec = ((.tools.exec // {}) + {\"host\":\"gateway\",\"security\":\"full\",\"ask\":\"off\"})\n | .tools.elevated = (.tools.elevated // {})\n | .tools.elevated.enabled = true\n | .tools.elevated.allowFrom |= ((. // {}) | .matrix = [\"*\"])\n | .agents.defaults.elevatedDefault = \"full\"\n ' \"${_tmp_in}\" > \"${_tmp_out}\" 2>/dev/null\n if ! diff -q \"${_tmp_in}\" \"${_tmp_out}\" > /dev/null 2>&1; then\n if mc cp \"${_tmp_out}\" \"${_minio_path}\" 2>/dev/null; then\n _new_count=$(jq '.models.providers[\"hiclaw-gateway\"].models | length' \"${_tmp_out}\" 2>/dev/null)\n log \"Worker ${_wname}: upgraded openclaw.json (models: ${_new_count}, e2ee: ${MATRIX_E2EE_ENABLED})\"\n fi\n fi\n rm -f \"${_tmp_in}\" \"${_tmp_out}\"\n fi\n done\n fi\nfi\n\n# ============================================================\n# Ensure Worker Matrix password files exist in MinIO (E2EE fix)\n# Workers need to re-login on restart to get a fresh device_id.\n# Older workers created before this fix won't have the password file.\n# ============================================================\nif [ -f \"${REGISTRY_FILE}\" ]; then\n for _wname in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_wname}\" ] && continue\n _creds_file=\"/data/worker-creds/${_wname}.env\"\n if [ -f \"${_creds_file}\" ]; then\n # Check if password file already exists in MinIO\n if ! mc stat \"${HICLAW_STORAGE_PREFIX}/agents/${_wname}/credentials/matrix/password\" > /dev/null 2>&1; then\n source \"${_creds_file}\"\n if [ -n \"${WORKER_PASSWORD}\" ]; then\n _tmp_pw=\"/tmp/matrix-pw-${_wname}\"\n echo -n \"${WORKER_PASSWORD}\" > \"${_tmp_pw}\"\n mc cp \"${_tmp_pw}\" \"${HICLAW_STORAGE_PREFIX}/agents/${_wname}/credentials/matrix/password\" 2>/dev/null \\\n && log \"Worker ${_wname}: wrote Matrix password to MinIO (E2EE re-login fix)\" \\\n || log \"Worker ${_wname}: WARNING: failed to write Matrix password to MinIO\"\n rm -f \"${_tmp_pw}\"\n fi\n fi\n fi\n done\nfi\n\n# ============================================================\n# Recreate Worker containers as needed after Manager restart.\n# Workers are on hiclaw-net; Docker DNS resolves *-local.hiclaw.io via\n# the Manager's network aliases, so IP changes don't require worker recreation.\n# Only recreate stopped/missing workers.\n# ============================================================\nif container_api_available; then\n REGISTRY_FILE=\"/root/manager-workspace/workers-registry.json\"\n if [ -f \"${REGISTRY_FILE}\" ]; then\n for _worker_name in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_worker_name}\" ] && continue\n\n # Skip remote workers — they are not Manager-managed containers.\n _deployment=$(jq -r --arg w \"${_worker_name}\" '.workers[$w].deployment // \"local\"' \"${REGISTRY_FILE}\" 2>/dev/null)\n if [ \"${_deployment}\" = \"remote\" ]; then\n log \"Worker ${_worker_name} is remote, skipping container recreate\"\n continue\n fi\n\n _status=$(container_status_worker \"${_worker_name}\")\n if [ \"${_status}\" = \"running\" ]; then\n log \"Worker running: ${_worker_name}, skipping\"\n continue\n fi\n # Container missing or stopped — recreate.\n log \"Worker container ${_status}: ${_worker_name}, recreating...\"\n _creds_file=\"/data/worker-creds/${_worker_name}.env\"\n if [ -f \"${_creds_file}\" ]; then\n source \"${_creds_file}\"\n _runtime=$(jq -r --arg w \"${_worker_name}\" '.workers[$w].runtime // \"openclaw\"' \"${REGISTRY_FILE}\" 2>/dev/null)\n _recreated=false\n for _attempt in 1 2 3; do\n local _env_map _create_body\n _env_map=$(jq -cn \\\n --arg name \"${_worker_name}\" \\\n --arg fak \"${_worker_name}\" \\\n --arg fsk \"${WORKER_MINIO_PASSWORD:-}\" \\\n --arg fs_domain \"${HICLAW_FS_DOMAIN:-fs-local.hiclaw.io}\" \\\n --arg controller_url \"${HICLAW_CONTROLLER_URL:-}\" \\\n '{\n \"HICLAW_WORKER_NAME\": $name,\n \"HICLAW_FS_ENDPOINT\": (\"http://\" + ($fs_domain | split(\":\")[0]) + \":9000\"),\n \"HICLAW_FS_ACCESS_KEY\": $fak,\n \"HICLAW_FS_SECRET_KEY\": $fsk\n }\n | if $controller_url != \"\" then . + {\"HICLAW_CONTROLLER_URL\": $controller_url} else . end')\n _create_body=$(jq -cn --arg name \"${_worker_name}\" --arg runtime \"${_runtime}\" --argjson env \"${_env_map}\" '{name: $name, runtime: $runtime, env: $env}')\n worker_backend_create \"${_create_body}\" > /dev/null 2>&1 && _recreated=true && break\n log \" Attempt ${_attempt}/3 failed for ${_worker_name}, retrying in $((5 * _attempt))s...\"\n sleep $((5 * _attempt))\n done\n if [ \"${_recreated}\" = true ]; then\n log \" Recreated ${_runtime} worker: ${_worker_name}\"\n else\n log \" ERROR: Failed to recreate ${_worker_name} after 3 attempts\"\n fi\n else\n log \" WARNING: No credentials found for ${_worker_name} (${_creds_file} missing), skipping\"\n fi\n done\n fi\nfi\n\n# ============================================================\n# Notify workers of builtin updates if upgrade happened\n# Builtin files (AGENTS.md, skills) are already synced by upgrade-builtins.sh\n#\n# Cooldown: skip notification if the last successful notify was within\n# NOTIFY_COOLDOWN_SECS (default 3600s / 1 hour). This prevents repeated\n# notifications when the Manager crash-loops and re-runs upgrade-builtins\n# on every restart (e.g. IMAGE_VERSION=latest always triggers upgrade).\n# ============================================================\nNOTIFY_COOLDOWN_SECS=\"${HICLAW_NOTIFY_COOLDOWN_SECS:-3600}\"\nNOTIFY_TS_FILE=\"/root/manager-workspace/.last-worker-notify-ts\"\n\nif [ -f /root/manager-workspace/.upgrade-pending-worker-notify ]; then\n _now=$(date +%s)\n _last_notify=$(cat \"${NOTIFY_TS_FILE}\" 2>/dev/null || echo \"0\")\n _elapsed=$(( _now - _last_notify ))\n\n if [ \"${_elapsed}\" -lt \"${NOTIFY_COOLDOWN_SECS}\" ]; then\n log \"Skipping worker builtin notification (last notify ${_elapsed}s ago, cooldown ${NOTIFY_COOLDOWN_SECS}s)\"\n rm -f /root/manager-workspace/.upgrade-pending-worker-notify\n else\n log \"Notifying workers about builtin updates...\"\n REGISTRY_FILE=\"/root/manager-workspace/workers-registry.json\"\n _notify_ok=false\n if [ -f \"${REGISTRY_FILE}\" ]; then\n for _worker_name in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_worker_name}\" ] && continue\n _room_id=$(jq -r --arg w \"${_worker_name}\" '.workers[$w].room_id // empty' \"${REGISTRY_FILE}\" 2>/dev/null)\n if [ -n \"${_room_id}\" ]; then\n _worker_id=\"@${_worker_name}:${MATRIX_DOMAIN}\"\n _txn_id=\"upgrade-$(date +%s%N)\"\n _msg=\"@${_worker_name}:${MATRIX_DOMAIN} Manager upgraded builtin files (AGENTS.md, skills). Please use your file-sync skill to sync the latest config.\"\n _raw=$(curl -s -w '\\nHTTP_CODE:%{http_code}' -X PUT \\\n \"${HICLAW_MATRIX_URL}/_matrix/client/v3/rooms/${_room_id}/send/m.room.message/${_txn_id}\" \\\n -H \"Authorization: Bearer ${MANAGER_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d \"{\\\"msgtype\\\":\\\"m.text\\\",\\\"body\\\":\\\"${_msg}\\\",\\\"m.mentions\\\":{\\\"user_ids\\\":[\\\"${_worker_id}\\\"]}}\" \\\n 2>&1) || true\n _http_code=$(echo \"${_raw}\" | tail -1 | sed 's/HTTP_CODE://')\n _notify_resp=$(echo \"${_raw}\" | sed '$d')\n if echo \"${_notify_resp}\" | jq -e '.event_id' > /dev/null 2>&1; then\n log \" Notified ${_worker_name}\"; _notify_ok=true\n else\n log \" WARNING: Failed to notify ${_worker_name} (HTTP ${_http_code}): ${_notify_resp}\"\n fi\n fi\n done\n fi\n # Record timestamp only if at least one notification succeeded\n if [ \"${_notify_ok}\" = true ]; then\n echo \"${_now}\" > \"${NOTIFY_TS_FILE}\"\n fi\n rm -f /root/manager-workspace/.upgrade-pending-worker-notify\n fi\nfi\n\n# ============================================================\n# Start Manager Agent\n# ============================================================\nlog \"Starting Manager Agent (${MANAGER_RUNTIME})...\"\n\n# HOME is already set to /root/manager-workspace via docker run -e HOME=...\ncd \"${HOME}\"\n\n# Ensure host credential symlinks exist under HOME\nif [ -d \"/host-share\" ]; then\n [ -f \"/host-share/.gitconfig\" ] && ln -sf \"/host-share/.gitconfig\" \"${HOME}/.gitconfig\"\nfi\n\nlog \"HOME=${HOME} (manager-workspace, host-mounted)\"\n\n# ── Render agent doc templates ────────────────────────────────────────────\n# Replace ${VAR} placeholders with actual values so the AI agent reads\n# plain text and never needs to resolve environment variables.\nexport MANAGER_MATRIX_TOKEN MANAGER_TOKEN HIGRESS_COOKIE_FILE\nRENDER=/opt/hiclaw/scripts/lib/render-skills.sh\nlog \"Rendering agent doc templates...\"\n# Manager-owned docs (workspace)\nbash \"$RENDER\" /root/manager-workspace/skills\nbash \"$RENDER\" /root/manager-workspace/skills-alpha\nbash \"$RENDER\" /root/manager-workspace AGENTS.md TOOLS.md HEARTBEAT.md SOUL.md\n# Worker templates (workspace + image) — rendered before push to MinIO\n# so Workers (including remote pip-install) receive plain text\nbash \"$RENDER\" /root/manager-workspace/worker-skills\nbash \"$RENDER\" /root/manager-workspace/worker-agent\nbash \"$RENDER\" /root/manager-workspace/copaw-worker-agent\nbash \"$RENDER\" /root/manager-workspace/hermes-worker-agent\nbash \"$RENDER\" /opt/hiclaw/agent/worker-skills\nbash \"$RENDER\" /opt/hiclaw/agent/worker-agent\nbash \"$RENDER\" /opt/hiclaw/agent/copaw-worker-agent\nbash \"$RENDER\" /opt/hiclaw/agent/hermes-worker-agent\nlog \"Agent doc templates rendered\"\n\n# Cloud mode: start background file sync (workspace ↔ OSS) and initial push\nif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n log \"Syncing initial workspace to OSS...\"\n ensure_mc_credentials\n mc mirror /root/manager-workspace/ \"${HICLAW_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" 2>/dev/null || true\n\n # Local → OSS: change-triggered sync\n (\n while true; do\n CHANGED=$(find /root/manager-workspace/ -type f -newermt \"15 seconds ago\" 2>/dev/null | head -1)\n if [ -n \"${CHANGED}\" ]; then\n ensure_mc_credentials 2>/dev/null || true\n mc mirror /root/manager-workspace/ \"${HICLAW_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" --exclude \".npm/**\" \\\n --exclude \".local/**\" --exclude \".mc/**\" 2>/dev/null || true\n fi\n sleep 10\n done\n ) &\n log \"Local→OSS sync started (PID: $!)\"\n\n # OSS → Local: periodic pull (shared data, agent configs, manager openclaw.json)\n (\n while true; do\n sleep 60\n ensure_mc_credentials 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/shared/\" /root/hiclaw-fs/shared/ --overwrite --newer-than \"1m\" 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/agents/\" /root/hiclaw-fs/agents/ --overwrite --newer-than \"1m\" 2>/dev/null || true\n mc cp \"${HICLAW_STORAGE_PREFIX}/manager/openclaw.json\" /root/manager-workspace/openclaw.json 2>/dev/null || true\n done\n ) &\n log \"OSS→Local sync started (every 60s, PID: $!)\"\nfi\n\n# K8s mode: start background file sync (workspace ↔ MinIO)\nif [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n log \"Syncing initial workspace to MinIO...\"\n mc mirror /root/manager-workspace/ \"${HICLAW_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" 2>/dev/null || true\n\n # Local → MinIO: change-triggered sync\n (\n while true; do\n CHANGED=$(find /root/manager-workspace/ -type f -newermt \"15 seconds ago\" 2>/dev/null | head -1)\n if [ -n \"${CHANGED}\" ]; then\n mc mirror /root/manager-workspace/ \"${HICLAW_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" --exclude \".npm/**\" \\\n --exclude \".local/**\" --exclude \".mc/**\" 2>/dev/null || true\n fi\n sleep 10\n done\n ) &\n log \"Local→MinIO sync started (PID: $!)\"\n\n # MinIO → Local: periodic pull (shared data, agent configs, manager openclaw.json)\n (\n while true; do\n sleep 60\n mc mirror \"${HICLAW_STORAGE_PREFIX}/shared/\" /root/hiclaw-fs/shared/ --overwrite --newer-than \"1m\" 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/agents/\" /root/hiclaw-fs/agents/ --overwrite --newer-than \"1m\" 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/hiclaw-config/\" /root/hiclaw-fs/hiclaw-config/ --overwrite --newer-than \"1m\" 2>/dev/null || true\n mc cp \"${HICLAW_STORAGE_PREFIX}/manager/openclaw.json\" /root/manager-workspace/openclaw.json 2>/dev/null || true\n done\n ) &\n log \"MinIO→Local sync started (every 60s, PID: $!)\"\nfi\n\n# ============================================================\n# Auto-generate Manager mcporter config for pre-configured MCP servers\n# If HICLAW_GITHUB_TOKEN was set at install time, setup-higress.sh already\n# configured GitHub MCP on Higress. Run setup-mcp-server.sh now so that\n# config/mcporter.json exists when the Agent starts — no need to ask user for PAT.\n# ============================================================\nif [ -n \"${HICLAW_GITHUB_TOKEN}\" ] && [ \"${HICLAW_RUNTIME}\" != \"aliyun\" ] && [ \"${HICLAW_RUNTIME}\" != \"k8s\" ]; then\n if [ ! -f \"${HOME}/config/mcporter.json\" ]; then\n log \"Auto-generating Manager mcporter config for GitHub MCP (HICLAW_GITHUB_TOKEN set)...\"\n bash /opt/hiclaw/agent/skills/mcp-server-management/scripts/setup-mcp-server.sh \\\n github \"${HICLAW_GITHUB_TOKEN}\" 2>&1 | while IFS= read -r line; do log \" [setup-mcp] ${line}\"; done || \\\n log \"WARNING: setup-mcp-server.sh failed — Agent may need to configure GitHub MCP manually\"\n else\n log \"Manager mcporter config already exists, skipping auto-generate\"\n fi\nfi\n\n# ============================================================\n# Runtime-specific startup\n# ============================================================\nif [ \"${MANAGER_RUNTIME}\" = \"copaw\" ]; then\n # Delegate to CoPaw startup script\n exec /opt/hiclaw/scripts/init/start-copaw-manager.sh\nelse\n # ── OpenClaw Runtime ─────────────────────────────────────────────────────\n log \"Starting OpenClaw Manager...\"\n\n export OPENCLAW_CONFIG_PATH=\"/root/manager-workspace/openclaw.json\"\n\n # Symlink to default OpenClaw config path so CLI commands find the config\n mkdir -p \"${HOME}/.openclaw\"\n ln -sf \"/root/manager-workspace/openclaw.json\" \"${HOME}/.openclaw/openclaw.json\"\n\n # Clean orphaned session write locks (e.g. from SIGKILL or crash before exit handlers)\n # Prevents \"session file locked (timeout 10000ms)\" when PID was reused\n find \"${HOME}/.openclaw/agents\" -name \"*.jsonl.lock\" -delete 2>/dev/null || true\n log \"Cleaned up any orphaned session write locks\"\n\n # Clean Matrix crypto storage (SQLite WAL may be corrupted after unclean shutdown)\n # Crypto state is re-negotiated on startup; losing it only means re-establishing E2EE sessions\n rm -rf \"${HOME}/.openclaw/matrix\" 2>/dev/null || true\n log \"Cleaned Matrix crypto storage (will re-establish E2EE sessions)\"\n\n # Launch OpenClaw\n # Disable full-process respawn so the CLI uses its internal restart loop.\n # Without this, config reload spawns a detached child and exits, then\n # supervisord restarts the CLI — resulting in two gateway processes.\n export OPENCLAW_NO_RESPAWN=1\n\n # Optional matrix-plugin trace logging — when HICLAW_MATRIX_DEBUG=1 is set\n # in the manager environment (propagated by install / supervisord), turn on\n # OPENCLAW_MATRIX_DEBUG so the matrix plugin emits structured INFO-level\n # lifecycle traces (sync.state transitions, room.invite/join, message\n # handler arrival + filter outcomes). Useful for diagnosing \"worker never\n # joined the room\" / \"manager never replied\" hangs without rebuilding the\n # image.\n if [ \"${HICLAW_MATRIX_DEBUG:-}\" = \"1\" ] && [ -z \"${OPENCLAW_MATRIX_DEBUG:-}\" ]; then\n export OPENCLAW_MATRIX_DEBUG=1\n log \"HICLAW_MATRIX_DEBUG=1 detected; OPENCLAW_MATRIX_DEBUG=1 exported for matrix plugin tracing\"\n fi\n\n exec openclaw gateway run --verbose --force\nfi\n"}, "94bde15caa75a1b27a19a8988fdf1c1261e9971c": {"t": 1785764740.490468, "n": 68181, "h": "e2cc6bc5d06e30dd", "c": "#!/bin/bash\n# start-manager-agent.sh - Initialize and start the Manager Agent\n# Supports local (supervisord), cloud (SAE), and K8s (Helm) deployments.\n# In local mode this is the last supervisord component to start (priority 800).\n# In cloud/k8s mode (HICLAW_RUNTIME=aliyun|k8s) this is the container entrypoint.\n#\n# Runtime selection:\n# HICLAW_MANAGER_RUNTIME=openclaw (default) - OpenClaw gateway mode\n# HICLAW_MANAGER_RUNTIME=copaw - CoPaw workspace mode\n# (hermes runtime is supported for Workers only; Managers run openclaw or copaw.)\n\nsource /opt/hiclaw/scripts/lib/hiclaw-env.sh\n\n# ============================================================\n# Runtime selection\n# ============================================================\nMANAGER_RUNTIME=\"${HICLAW_MANAGER_RUNTIME:-openclaw}\"\ncase \"${MANAGER_RUNTIME}\" in\n copaw)\n log \"Manager runtime: CoPaw (Python workspace)\"\n ;;\n *)\n log \"Manager runtime: OpenClaw (Node.js gateway)\"\n MANAGER_RUNTIME=\"openclaw\"\n ;;\nesac\n\n# ============================================================\n# Set timezone from TZ env var\n# ============================================================\nif [ -n \"${TZ}\" ] && [ -f \"/usr/share/zoneinfo/${TZ}\" ]; then\n ln -sf \"/usr/share/zoneinfo/${TZ}\" /etc/localtime\n echo \"${TZ}\" > /etc/timezone\n log \"Timezone set to ${TZ}\"\nfi\n\nexport MATRIX_DOMAIN=\"${HICLAW_MATRIX_DOMAIN:-matrix-local.hiclaw.io:8080}\"\nAI_GATEWAY_DOMAIN=\"${HICLAW_AI_GATEWAY_DOMAIN:-aigw-local.hiclaw.io}\"\n\n# ============================================================\n# YOLO mode promotion\n# ============================================================\n# In embedded mode the controller does not propagate HICLAW_YOLO to the\n# manager container, but installer / test scripts touch a marker file at\n# `${WORKSPACE}/yolo-mode` instead. Promote that marker to the env var so the\n# agent's documented YOLO check (`HICLAW_YOLO=1`) reliably detects it without\n# depending on filesystem lookups during a turn.\nif [ -z \"${HICLAW_YOLO:-}\" ] && [ -f /root/manager-workspace/yolo-mode ]; then\n export HICLAW_YOLO=1\n log \"YOLO mode marker detected at /root/manager-workspace/yolo-mode; HICLAW_YOLO=1 exported\"\nfi\n\n# ============================================================\n# Cloud/K8s mode: validate required environment variables + initial credentials\n# ============================================================\nif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ] || [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n : \"${HICLAW_MATRIX_URL:?HICLAW_MATRIX_URL is required}\"\n : \"${HICLAW_MATRIX_DOMAIN:?HICLAW_MATRIX_DOMAIN is required}\"\n : \"${HICLAW_AI_GATEWAY_URL:?HICLAW_AI_GATEWAY_URL is required}\"\n if [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n : \"${HICLAW_MANAGER_GATEWAY_KEY:?HICLAW_MANAGER_GATEWAY_KEY is required}\"\n : \"${HICLAW_MANAGER_PASSWORD:?HICLAW_MANAGER_PASSWORD is required (cloud containers are stateless, password must be injected)}\"\n fi\n if [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n # K8s mode: controller handles initialization (admin registration, Higress setup).\n # Manager only needs credentials injected by the ManagerReconciler.\n : \"${HICLAW_MANAGER_GATEWAY_KEY:?HICLAW_MANAGER_GATEWAY_KEY is required (injected by controller)}\"\n : \"${HICLAW_MANAGER_PASSWORD:?HICLAW_MANAGER_PASSWORD is required (injected by controller)}\"\n else\n # Cloud (aliyun) mode: Manager still does its own initialization\n : \"${HICLAW_REGISTRATION_TOKEN:?HICLAW_REGISTRATION_TOKEN is required}\"\n : \"${HICLAW_ADMIN_USER:?HICLAW_ADMIN_USER is required}\"\n : \"${HICLAW_ADMIN_PASSWORD:?HICLAW_ADMIN_PASSWORD is required}\"\n fi\n log \"${HICLAW_RUNTIME} mode: validating environment... OK\"\n log \" Matrix: ${HICLAW_MATRIX_URL}, AI Gateway: ${HICLAW_AI_GATEWAY_URL}, Storage: ${HICLAW_FS_BUCKET}\"\n if [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n ensure_mc_credentials || { log \"FATAL: Initial STS credential fetch failed\"; exit 1; }\n fi\nfi\n\n# ============================================================\n# Local mode: host symlinks, /etc/hosts, wait for local services\n# ============================================================\nif [ \"${HICLAW_RUNTIME}\" != \"aliyun\" ] && [ \"${HICLAW_RUNTIME}\" != \"k8s\" ]; then\n # Create symlink for host directory access\n if [ -d \"/host-share\" ]; then\n ORIGINAL_HOST_HOME=\"${HOST_ORIGINAL_HOME:-$HOME}\"\n if [ ! -e \"${ORIGINAL_HOST_HOME}\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/root\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/data\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/host-share\" ]; then\n mkdir -p \"$(dirname \"${ORIGINAL_HOST_HOME}\")\"\n ln -sfn /host-share \"${ORIGINAL_HOST_HOME}\"\n log \"Created symlink: ${ORIGINAL_HOST_HOME} -> /host-share\"\n else\n ln -sfn /host-share /root/host-home\n log \"Created fallback symlink: /root/host-home -> /host-share\"\n fi\n fi\n\n # Add local domains to /etc/hosts\n HOSTS_DOMAINS=\"${MATRIX_DOMAIN%%:*} ${HICLAW_MATRIX_CLIENT_DOMAIN:-matrix-client-local.hiclaw.io} ${AI_GATEWAY_DOMAIN} ${HICLAW_FS_DOMAIN:-fs-local.hiclaw.io}\"\n if ! grep -q \"${AI_GATEWAY_DOMAIN}\" /etc/hosts 2>/dev/null; then\n echo \"127.0.0.1 ${HOSTS_DOMAINS}\" >> /etc/hosts\n log \"Added local domains to /etc/hosts\"\n fi\n\n # Wait for local infrastructure\n waitForService \"Higress Gateway\" \"127.0.0.1\" 8080 180\n waitForService \"Higress Console\" \"127.0.0.1\" 8001 180\n waitForService \"Tuwunel\" \"127.0.0.1\" 6167 120\n waitForHTTP \"Tuwunel Matrix API\" \"${HICLAW_MATRIX_URL}/_tuwunel/server_version\" 120\n waitForService \"MinIO\" \"127.0.0.1\" 9000 120\nelse\n # Cloud/K8s mode: wait for external Tuwunel\n log \"Waiting for Tuwunel Matrix server at ${HICLAW_MATRIX_URL}...\"\n _retry=0\n while [ \"${_retry}\" -lt 30 ]; do\n if curl -sf \"${HICLAW_MATRIX_URL}/_matrix/client/versions\" > /dev/null 2>&1; then\n log \"Tuwunel is ready\"\n break\n fi\n _retry=$((_retry + 1))\n log \" Waiting for Tuwunel (attempt ${_retry}/30)...\"\n sleep 5\n done\n if [ \"${_retry}\" -ge 30 ]; then\n log \"ERROR: Tuwunel not reachable at ${HICLAW_MATRIX_URL}\"\n exit 1\n fi\nfi\n\n# ============================================================\n# Auto-generate secrets if not provided via environment\n# Persisted to /data so they survive container restart\n# ============================================================\nSECRETS_FILE=\"/data/hiclaw-secrets.env\"\nif [ -f \"${SECRETS_FILE}\" ]; then\n source \"${SECRETS_FILE}\"\n log \"Loaded persisted secrets from ${SECRETS_FILE}\"\nfi\n\nif [ -z \"${HICLAW_MANAGER_GATEWAY_KEY}\" ]; then\n export HICLAW_MANAGER_GATEWAY_KEY=\"$(generateKey 32)\"\n log \"Auto-generated HICLAW_MANAGER_GATEWAY_KEY\"\nfi\nif [ -z \"${HICLAW_MANAGER_PASSWORD}\" ]; then\n export HICLAW_MANAGER_PASSWORD=\"$(generateKey 16)\"\n log \"Auto-generated HICLAW_MANAGER_PASSWORD\"\nfi\n\n# Persist secrets so they survive supervisord restart\nmkdir -p /data\ncat > \"${SECRETS_FILE}\" <<EOF\nexport HICLAW_MANAGER_GATEWAY_KEY=\"${HICLAW_MANAGER_GATEWAY_KEY}\"\nexport HICLAW_MANAGER_PASSWORD=\"${HICLAW_MANAGER_PASSWORD}\"\nEOF\nchmod 600 \"${SECRETS_FILE}\"\n\n# Cloud mode: pull workspace from OSS before initialization\nif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n HICLAW_FS=\"/root/hiclaw-fs\"\n mkdir -p \"${HICLAW_FS}/shared\" \"${HICLAW_FS}/agents\"\n log \"Pulling workspace from OSS...\"\n ensure_mc_credentials\n mc mirror \"${HICLAW_STORAGE_PREFIX}/manager/\" /root/manager-workspace/ --overwrite 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/shared/\" \"${HICLAW_FS}/shared/\" --overwrite 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/agents/\" \"${HICLAW_FS}/agents/\" --overwrite 2>/dev/null || true\n # Symlink hiclaw-fs into workspace for agent access\n ln -sfn \"${HICLAW_FS}\" /root/manager-workspace/hiclaw-fs\nfi\n\n# K8s mode: sync workspace from cluster-internal MinIO\nif [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n HICLAW_FS=\"/root/hiclaw-fs\"\n mkdir -p \"${HICLAW_FS}/shared\" \"${HICLAW_FS}/agents\" \"${HICLAW_FS}/hiclaw-config\"\n log \"Configuring mc alias for cluster MinIO...\"\n mc alias set hiclaw \"${HICLAW_FS_ENDPOINT}\" \"${HICLAW_FS_ACCESS_KEY}\" \"${HICLAW_FS_SECRET_KEY}\"\n log \"Syncing workspace from MinIO...\"\n mc mirror \"${HICLAW_STORAGE_PREFIX}/manager/\" /root/manager-workspace/ --overwrite 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/\" \"${HICLAW_FS}/\" --overwrite 2>/dev/null || true\n ln -sfn \"${HICLAW_FS}\" /root/manager-workspace/hiclaw-fs\n touch \"${HICLAW_FS}/.initialized\"\nfi\n\n# ============================================================\n# Initialize / upgrade Manager workspace\n# First boot: full init via upgrade-builtins.sh\n# Subsequent boots: compare image version; upgrade only if changed\n# ============================================================\nmkdir -p /root/manager-workspace\n\nIMAGE_VERSION=$(cat /opt/hiclaw/agent/.builtin-version 2>/dev/null || echo \"unknown\")\nINSTALLED_VERSION=$(cat /root/manager-workspace/.builtin-version 2>/dev/null || echo \"\")\n\nif [ ! -f /root/manager-workspace/.initialized ]; then\n log \"First boot: initializing manager workspace...\"\n bash /opt/hiclaw/scripts/init/upgrade-builtins.sh\n touch /root/manager-workspace/.initialized\n log \"Manager workspace initialized (version: ${IMAGE_VERSION})\"\nelif [ \"${IMAGE_VERSION}\" != \"${INSTALLED_VERSION}\" ] || [ \"${IMAGE_VERSION}\" = \"latest\" ]; then\n log \"Upgrade detected: ${INSTALLED_VERSION} -> ${IMAGE_VERSION}${IMAGE_VERSION:+ (latest: always upgrade)}\"\n bash /opt/hiclaw/scripts/init/upgrade-builtins.sh\n log \"Manager workspace upgraded to version: ${IMAGE_VERSION}\"\nelse\n log \"Workspace up to date (version: ${IMAGE_VERSION})\"\nfi\n\n# Local mode: wait for mc mirror initialization (shared + worker data in /root/hiclaw-fs/)\nif [ \"${HICLAW_RUNTIME}\" != \"aliyun\" ] && [ \"${HICLAW_RUNTIME}\" != \"k8s\" ]; then\n log \"Waiting for MinIO storage initialization...\"\n _minio_wait=0\n while [ ! -f /root/hiclaw-fs/.initialized ]; do\n sleep 2\n _minio_wait=$(( _minio_wait + 1 ))\n if [ \"${_minio_wait}\" -ge 60 ]; then\n log \"ERROR: MinIO storage initialization timed out after 120s\"\n exit 1\n fi\n done\n log \"MinIO storage initialized\"\nfi\n\n# ============================================================\n# Register Matrix users via Registration API (single-step, no UIAA)\n# K8s mode: skip — controller Initializer + ManagerReconciler already did this\n# ============================================================\nif [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n log \"K8s mode: skipping Matrix registration (handled by controller)\"\n # Controller injects HICLAW_MANAGER_PASSWORD via env; login to get token\n log \"Obtaining Manager Matrix access token...\"\n _LOGIN_RESPONSE=$(curl -s -X POST ${HICLAW_MATRIX_URL}/_matrix/client/v3/login \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"type\": \"m.login.password\",\n \"identifier\": {\"type\": \"m.id.user\", \"user\": \"manager\"},\n \"password\": \"'\"${HICLAW_MANAGER_PASSWORD}\"'\"\n }' 2>&1)\n MANAGER_TOKEN=$(echo \"${_LOGIN_RESPONSE}\" | jq -r '.access_token' 2>/dev/null)\n if [ -z \"${MANAGER_TOKEN}\" ] || [ \"${MANAGER_TOKEN}\" = \"null\" ]; then\n log \"ERROR: Failed to obtain Manager Matrix token\"\n log \"ERROR: Login response was: ${_LOGIN_RESPONSE}\"\n exit 1\n fi\n log \"Manager Matrix token obtained (token prefix: ${MANAGER_TOKEN:0:10}...)\"\nelse\nlog \"Registering human admin Matrix account...\"\ncurl -sf -X POST ${HICLAW_MATRIX_URL}/_matrix/client/v3/register \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"username\": \"'\"${HICLAW_ADMIN_USER}\"'\",\n \"password\": \"'\"${HICLAW_ADMIN_PASSWORD}\"'\",\n \"auth\": {\n \"type\": \"m.login.registration_token\",\n \"token\": \"'\"${HICLAW_REGISTRATION_TOKEN}\"'\"\n }\n }' > /dev/null 2>&1 || log \"Admin account may already exist\"\n\nlog \"Registering Manager Agent Matrix account...\"\ncurl -sf -X POST ${HICLAW_MATRIX_URL}/_matrix/client/v3/register \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"username\": \"manager\",\n \"password\": \"'\"${HICLAW_MANAGER_PASSWORD}\"'\",\n \"auth\": {\n \"type\": \"m.login.registration_token\",\n \"token\": \"'\"${HICLAW_REGISTRATION_TOKEN}\"'\"\n }\n }' > /dev/null 2>&1 || log \"Manager account may already exist\"\n\n# Get Manager Agent's Matrix access token\nlog \"Obtaining Manager Matrix access token...\"\n_LOGIN_RESPONSE=$(curl -s -X POST ${HICLAW_MATRIX_URL}/_matrix/client/v3/login \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"type\": \"m.login.password\",\n \"identifier\": {\"type\": \"m.id.user\", \"user\": \"manager\"},\n \"password\": \"'\"${HICLAW_MANAGER_PASSWORD}\"'\"\n }' 2>&1)\n_LOGIN_EXIT=$?\nlog \"Matrix login HTTP exit code: ${_LOGIN_EXIT}\"\nlog \"Matrix login response: ${_LOGIN_RESPONSE}\"\n\nMANAGER_TOKEN=$(echo \"${_LOGIN_RESPONSE}\" | jq -r '.access_token' 2>/dev/null)\n\nif [ -z \"${MANAGER_TOKEN}\" ] || [ \"${MANAGER_TOKEN}\" = \"null\" ]; then\n log \"ERROR: Failed to obtain Manager Matrix token (exit=${_LOGIN_EXIT})\"\n log \"ERROR: Login response was: ${_LOGIN_RESPONSE}\"\n exit 1\nfi\nlog \"Manager Matrix token obtained (token prefix: ${MANAGER_TOKEN:0:10}...)\"\nfi\n\n# ============================================================\n# Higress Console initialization\n# Docker mode: full setup-higress.sh (internal Higress at localhost:8001)\n# K8s mode: skip — controller Initializer handles Higress setup\n# Cloud (aliyun) mode: skip entirely (Higress managed externally)\n# ============================================================\n_HIGRESS_CONSOLE_URL=\"\"\n_HIGRESS_USER=\"${HICLAW_ADMIN_USER}\"\n_HIGRESS_PASS=\"${HICLAW_ADMIN_PASSWORD}\"\nif [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n log \"K8s mode: skipping Higress initialization (handled by controller)\"\nelif [ \"${HICLAW_RUNTIME}\" != \"aliyun\" ]; then\n _HIGRESS_CONSOLE_URL=\"http://127.0.0.1:8001\"\nfi\n\nif [ -n \"${_HIGRESS_CONSOLE_URL}\" ]; then\n COOKIE_FILE=\"/tmp/higress-session-cookie\"\n\n log \"Waiting for Higress Console (${_HIGRESS_CONSOLE_URL}) to be fully ready and initializing admin...\"\n INIT_DONE=false\n for i in $(seq 1 90); do\n INIT_RESULT=$(curl -s -X POST \"${_HIGRESS_CONSOLE_URL}/system/init\" \\\n -H 'Content-Type: application/json' \\\n -d '{\"adminUser\":{\"name\":\"'\"${_HIGRESS_USER}\"'\",\"password\":\"'\"${_HIGRESS_PASS}\"'\",\"displayName\":\"'\"${_HIGRESS_USER}\"'\"}}' 2>/dev/null) || true\n if echo \"${INIT_RESULT}\" | grep -qE '\"success\":true|already.?init' 2>/dev/null; then\n INIT_DONE=true\n break\n fi\n if echo \"${INIT_RESULT}\" | grep -q '\"name\"' 2>/dev/null; then\n INIT_DONE=true\n break\n fi\n sleep 2\n done\n\n if [ \"${INIT_DONE}\" != \"true\" ]; then\n log \"ERROR: Higress Console did not become ready within 180s\"\n exit 1\n fi\n log \"Higress Console init done\"\n\n log \"Logging into Higress Console...\"\n LOGIN_OK=false\n for i in $(seq 1 10); do\n HTTP_CODE=$(curl -s -o /dev/null -w '%{http_code}' -X POST \"${_HIGRESS_CONSOLE_URL}/session/login\" \\\n -H 'Content-Type: application/json' \\\n -c \"${COOKIE_FILE}\" \\\n -d '{\"username\":\"'\"${_HIGRESS_USER}\"'\",\"password\":\"'\"${_HIGRESS_PASS}\"'\"}' 2>/dev/null) || true\n if { [ \"${HTTP_CODE}\" = \"200\" ] || [ \"${HTTP_CODE}\" = \"201\" ]; } && [ -f \"${COOKIE_FILE}\" ] && [ -s \"${COOKIE_FILE}\" ]; then\n LOGIN_OK=true\n break\n fi\n log \"Login attempt $i (HTTP ${HTTP_CODE}), retrying in 3s...\"\n sleep 3\n done\n\n if [ \"${LOGIN_OK}\" != \"true\" ]; then\n log \"ERROR: Could not login to Higress Console after retries\"\n exit 1\n fi\n log \"Higress Console login successful\"\n\n VERIFY_CODE=$(curl -s -o /dev/null -w '%{http_code}' \"${_HIGRESS_CONSOLE_URL}/v1/consumers\" -b \"${COOKIE_FILE}\" 2>/dev/null) || true\n if [ \"${VERIFY_CODE}\" = \"200\" ]; then\n log \"Console session verified (cookie valid)\"\n else\n log \"WARNING: Console session may be invalid (verify returned HTTP ${VERIFY_CODE})\"\n rm -f \"${COOKIE_FILE}\"\n for i in $(seq 1 5); do\n curl -s -o /dev/null -w '%{http_code}' -X POST \"${_HIGRESS_CONSOLE_URL}/session/login\" \\\n -H 'Content-Type: application/json' \\\n -c \"${COOKIE_FILE}\" \\\n -d '{\"username\":\"'\"${_HIGRESS_USER}\"'\",\"password\":\"'\"${_HIGRESS_PASS}\"'\"}' 2>/dev/null\n VERIFY2=$(curl -s -o /dev/null -w '%{http_code}' \"${_HIGRESS_CONSOLE_URL}/v1/consumers\" -b \"${COOKIE_FILE}\" 2>/dev/null) || true\n if [ \"${VERIFY2}\" = \"200\" ]; then\n log \"Re-login successful, session verified\"\n break\n fi\n sleep 2\n done\n fi\n\n export HIGRESS_COOKIE_FILE=\"${COOKIE_FILE}\"\n export HIGRESS_CONSOLE_URL=\"${_HIGRESS_CONSOLE_URL}\"\n\n if [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n # K8s mode: lightweight Higress config — only what's needed for LLM access\n source /opt/hiclaw/scripts/lib/base.sh\n _k8s_higress_api() {\n local method=\"$1\" path=\"$2\" desc=\"$3\"; shift 3; local body=\"$*\"\n local tmpfile; tmpfile=$(mktemp)\n local http_code\n http_code=$(curl -s -o \"${tmpfile}\" -w '%{http_code}' -X \"${method}\" \"${_HIGRESS_CONSOLE_URL}${path}\" \\\n -b \"${COOKIE_FILE}\" -H 'Content-Type: application/json' -d \"${body}\" 2>/dev/null) || true\n local response; response=$(cat \"${tmpfile}\" 2>/dev/null); rm -f \"${tmpfile}\"\n if echo \"${response}\" | grep -q '\"success\":true' 2>/dev/null; then\n log \"${desc} ... OK\"\n elif [ \"${http_code}\" = \"409\" ]; then\n log \"${desc} ... already exists, skipping\"\n elif [ \"${http_code}\" = \"200\" ] || [ \"${http_code}\" = \"201\" ] || [ \"${http_code}\" = \"204\" ]; then\n log \"${desc} ... OK (HTTP ${http_code})\"\n else\n log \"WARNING: ${desc} ... (HTTP ${http_code}): ${response}\"\n fi\n }\n\n # 1. Service Sources (DNS type → K8s Service FQDN)\n # Extract host:port from URLs for Higress service source registration\n _TUWUNEL_HOST=$(echo \"${HICLAW_MATRIX_URL}\" | sed 's|^http[s]*://||')\n _TUWUNEL_DOMAIN=$(echo \"${_TUWUNEL_HOST}\" | cut -d: -f1)\n _TUWUNEL_PORT=$(echo \"${_TUWUNEL_HOST}\" | cut -d: -f2)\n _k8s_higress_api POST /v1/service-sources \"Registering Tuwunel service source\" \\\n '{\"type\":\"dns\",\"name\":\"tuwunel\",\"domain\":\"'\"${_TUWUNEL_DOMAIN}\"'\",\"port\":'\"${_TUWUNEL_PORT}\"'}'\n\n if [ -n \"${HICLAW_ELEMENT_WEB_URL:-}\" ]; then\n _ELEMENT_HOST=$(echo \"${HICLAW_ELEMENT_WEB_URL}\" | sed 's|^http[s]*://||')\n _ELEMENT_DOMAIN=$(echo \"${_ELEMENT_HOST}\" | cut -d: -f1)\n _ELEMENT_PORT=$(echo \"${_ELEMENT_HOST}\" | cut -d: -f2)\n _k8s_higress_api POST /v1/service-sources \"Registering Element Web service source\" \\\n '{\"type\":\"dns\",\"name\":\"element-web\",\"domain\":\"'\"${_ELEMENT_DOMAIN}\"'\",\"port\":'\"${_ELEMENT_PORT}\"'}'\n fi\n\n # 2. Manager Consumer (key-auth)\n _k8s_higress_api POST /v1/consumers \"Creating Manager consumer\" \\\n '{\"name\":\"manager\",\"credentials\":[{\"type\":\"key-auth\",\"source\":\"BEARER\",\"values\":[\"'\"${HICLAW_MANAGER_GATEWAY_KEY}\"'\"]}]}'\n\n # 3. LLM Provider\n _LLM_PROVIDER=\"${HICLAW_LLM_PROVIDER:-qwen}\"\n _LLM_API_URL=\"${HICLAW_LLM_API_URL:-}\"\n if [ -z \"${_LLM_API_URL}\" ]; then\n case \"${_LLM_PROVIDER}\" in\n qwen) _LLM_API_URL=\"https://dashscope.aliyuncs.com/compatible-mode/v1\" ;;\n esac\n fi\n # 4. LLM Provider type-specific config\n case \"${_LLM_PROVIDER}\" in\n qwen)\n _k8s_higress_api POST /v1/ai/providers \"Creating LLM provider (qwen)\" \\\n '{\"type\":\"qwen\",\"name\":\"qwen\",\"tokens\":[\"'\"${HICLAW_LLM_API_KEY}\"'\"],\"protocol\":\"openai/v1\",\"tokenFailoverConfig\":{\"enabled\":false},\"rawConfigs\":{\"qwenEnableSearch\":false,\"qwenEnableCompatible\":true,\"qwenFileIds\":[],\"hiclawMode\":true}}'\n ;;\n *)\n _BODY='{\"name\":\"'\"${_LLM_PROVIDER}\"'\",\"type\":\"openai\",\"tokens\":[\"'\"${HICLAW_LLM_API_KEY}\"'\"],\"modelMapping\":{},\"protocol\":\"openai/v1\",\"rawConfigs\":{\"hiclawMode\":true}}'\n _k8s_higress_api POST /v1/ai/providers \"Creating LLM provider (${_LLM_PROVIDER})\" \"${_BODY}\"\n ;;\n esac\n\n # 5. AI Route (bind provider + consumer auth, /v1 prefix to avoid clash with Element Web catch-all)\n _k8s_higress_api POST /v1/ai/routes \"Creating AI Gateway route\" \\\n '{\"name\":\"default-ai-route\",\"domains\":[],\"pathPredicate\":{\"matchType\":\"PRE\",\"matchValue\":\"/v1\",\"caseSensitive\":false},\"upstreams\":[{\"provider\":\"'\"${_LLM_PROVIDER}\"'\",\"weight\":100,\"modelMapping\":{}}],\"authConfig\":{\"enabled\":true,\"allowedCredentialTypes\":[\"key-auth\"],\"allowedConsumers\":[\"manager\"]}}'\n\n # 6. Matrix Homeserver Route (/_matrix/* → Tuwunel, no auth)\n _k8s_higress_api POST /v1/routes \"Creating Matrix Homeserver route\" \\\n '{\"name\":\"matrix-homeserver\",\"domains\":[],\"path\":{\"matchType\":\"PRE\",\"matchValue\":\"/_matrix\"},\"services\":[{\"name\":\"tuwunel.dns\",\"port\":'\"${_TUWUNEL_PORT}\"',\"weight\":100}]}'\n\n # 7. Element Web Route (/ catch-all → Element Web, no auth)\n if [ -n \"${HICLAW_ELEMENT_WEB_URL:-}\" ]; then\n _k8s_higress_api POST /v1/routes \"Creating Element Web route\" \\\n '{\"name\":\"element-web\",\"domains\":[],\"path\":{\"matchType\":\"PRE\",\"matchValue\":\"/\"},\"services\":[{\"name\":\"element-web.dns\",\"port\":'\"${_ELEMENT_PORT}\"',\"weight\":100}]}'\n fi\n\n # 8. Remove Higress default landing page (Exact match on / takes precedence over Element Web catch-all)\n _k8s_higress_api DELETE /v1/routes/default \"Removing Higress default landing route\"\n\n log \"K8s Higress lightweight setup complete\"\n\n # Wait for AI plugin activation (~45s for first config)\n log \"Waiting for AI Gateway plugin activation (45s)...\"\n sleep 45\n else\n # Docker mode: full setup with all routes, domains, MCP servers\n /opt/hiclaw/scripts/init/setup-higress.sh\n fi\nfi\n\n# ============================================================\n# Create admin DM room, persist to state.json, send welcome message\n# K8s mode: skip — controller ProvisionManager creates the Admin DM\n# room (Step 4 in service/provisioner.go) AND reconcileManagerWelcome\n# delivers the first-boot onboarding prompt once OpenClaw inside this\n# container has joined the room. In k8s mode the manager intentionally\n# does NOT have the admin password (only HICLAW_ADMIN_USER), so it\n# could not log in as admin to send the welcome itself anyway. The\n# Manager Agent discovers its admin DM room on first heartbeat via\n# state.json / `manage-state.sh --action set-admin-dm` (see HEARTBEAT.md\n# Step 1) — it does not need it to be pre-injected by this script.\n# Runs in both local and cloud modes (idempotent)\n# ============================================================\nif [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n log \"K8s mode: skipping admin DM room creation and welcome message (both handled by hiclaw-controller)\"\nelse\nMANAGER_FULL_ID=\"@manager:${MATRIX_DOMAIN}\"\nADMIN_FULL_ID=\"@${HICLAW_ADMIN_USER}:${MATRIX_DOMAIN}\"\n\nlog \"Logging in as admin to create DM room...\"\n_ADMIN_LOGIN=$(curl -sf -X POST \"${HICLAW_MATRIX_URL}/_matrix/client/v3/login\" \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"type\": \"m.login.password\",\n \"identifier\": {\"type\": \"m.id.user\", \"user\": \"'\"${HICLAW_ADMIN_USER}\"'\"},\n \"password\": \"'\"${HICLAW_ADMIN_PASSWORD}\"'\"\n }' 2>&1) || true\n\nADMIN_MATRIX_TOKEN=$(echo \"${_ADMIN_LOGIN}\" | jq -r '.access_token // empty' 2>/dev/null)\nif [ -z \"${ADMIN_MATRIX_TOKEN}\" ]; then\n log \"WARNING: Failed to login as admin, skipping DM room creation\"\nelse\n # Search for existing DM room with Manager (idempotent)\n DM_ROOM_ID=\"\"\n _JOINED_ROOMS=$(curl -sf \"${HICLAW_MATRIX_URL}/_matrix/client/v3/joined_rooms\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" 2>/dev/null \\\n | jq -r '.joined_rooms[]' 2>/dev/null) || true\n for _rid in ${_JOINED_ROOMS}; do\n _members=$(curl -sf \"${HICLAW_MATRIX_URL}/_matrix/client/v3/rooms/${_rid}/members\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" 2>/dev/null \\\n | jq -r '.chunk[].state_key' 2>/dev/null) || continue\n _count=$(echo \"${_members}\" | wc -l | xargs)\n if [ \"${_count}\" = \"2\" ] && echo \"${_members}\" | grep -q \"@manager:\"; then\n DM_ROOM_ID=\"${_rid}\"\n break\n fi\n done\n\n if [ -n \"${DM_ROOM_ID}\" ]; then\n log \"Existing DM room found: ${DM_ROOM_ID}\"\n else\n log \"Creating DM room with Manager...\"\n _RAW=$(curl -s -w '\\nHTTP_CODE:%{http_code}' -X POST \"${HICLAW_MATRIX_URL}/_matrix/client/v3/createRoom\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d \"{\\\"is_direct\\\":true,\\\"invite\\\":[\\\"${MANAGER_FULL_ID}\\\"],\\\"preset\\\":\\\"trusted_private_chat\\\"}\" 2>&1) || true\n _HTTP_CODE=$(echo \"${_RAW}\" | tail -1 | sed 's/HTTP_CODE://')\n _CREATE_RESP=$(echo \"${_RAW}\" | sed '$d')\n DM_ROOM_ID=$(echo \"${_CREATE_RESP}\" | jq -r '.room_id // empty' 2>/dev/null)\n if [ -n \"${DM_ROOM_ID}\" ]; then\n log \"DM room created: ${DM_ROOM_ID}\"\n else\n log \"WARNING: Failed to create DM room (HTTP ${_HTTP_CODE}): ${_CREATE_RESP}\"\n fi\n fi\n\n # Persist admin DM room ID to state.json\n if [ -n \"${DM_ROOM_ID}\" ]; then\n STATE_SCRIPT=\"/opt/hiclaw/agent/skills/task-management/scripts/manage-state.sh\"\n if [ -f \"${STATE_SCRIPT}\" ]; then\n bash \"${STATE_SCRIPT}\" --action init 2>/dev/null || true\n bash \"${STATE_SCRIPT}\" --action set-admin-dm --room-id \"${DM_ROOM_ID}\" 2>/dev/null || true\n log \"Admin DM room persisted to state.json: ${DM_ROOM_ID}\"\n fi\n fi\n\n # Schedule welcome message in background (only on first boot)\n if [ -n \"${DM_ROOM_ID}\" ] && [ ! -f \"/root/manager-workspace/soul-configured\" ]; then\n log \"Scheduling welcome message (background, waiting for OpenClaw to start)...\"\n (\n _HICLAW_LANGUAGE=\"${HICLAW_LANGUAGE:-zh}\"\n _HICLAW_TIMEZONE=\"${TZ:-Asia/Shanghai}\"\n _wait=0\n _ready=false\n while [ \"${_wait}\" -lt 300 ]; do\n if curl -sf http://127.0.0.1:18799/ > /dev/null 2>&1; then\n _ready=true\n break\n fi\n sleep 3\n _wait=$((_wait + 3))\n done\n if [ \"${_ready}\" != \"true\" ]; then\n echo \"[manager] WARNING: OpenClaw gateway not ready within 300s, skipping welcome message\"\n exit 0\n fi\n # Ensure Manager has joined the DM room before sending the welcome\n # message. Without this, there is a race between OpenClaw's Matrix\n # auto-join and the message send — the message may land before Manager\n # joins, so OpenClaw's /sync never picks it up.\n _join_ok=false\n for _join_attempt in 1 2 3; do\n if curl -sf -X POST \"${HICLAW_MATRIX_URL}/_matrix/client/v3/rooms/${DM_ROOM_ID}/join\" \\\n -H \"Authorization: Bearer ${MANAGER_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d '{}' > /dev/null 2>&1; then\n echo \"[manager] Manager joined DM room before welcome message\"\n _join_ok=true\n break\n fi\n sleep 2\n done\n if [ \"${_join_ok}\" != \"true\" ]; then\n echo \"[manager] WARNING: Manager join request failed after 3 attempts (may already be joined)\"\n fi\n _welcome_msg=\"This is an automated message from the HiClaw setup. This is a fresh installation.\n\n--- Installation Context ---\nUser Language: ${_HICLAW_LANGUAGE} (zh = Chinese, en = English)\nUser Timezone: ${_HICLAW_TIMEZONE} (IANA timezone identifier)\n---\n\nYou are an AI agent that manages a team of worker agents. Your identity and personality have not been configured yet — the human admin is about to meet you for the first time.\n\nPlease begin the onboarding conversation:\n\n1. Greet the admin warmly and briefly describe what you can do (coordinate workers, manage tasks, run multi-agent projects)\n2. The user has selected \\\"${_HICLAW_LANGUAGE}\\\" as their preferred language during installation. Use this language for your greeting and all subsequent communication.\n3. The user's timezone is ${_HICLAW_TIMEZONE}. Based on this timezone, you may infer their likely region and suggest additional language options.\n4. Ask them: a) What would they like to call you? b) Communication style preference? c) Any behavior guidelines? d) Confirm default language\n5. After they reply, write their preferences to ~/SOUL.md\n6. Confirm what you wrote, and ask if they would like to adjust anything\n7. Once confirmed, run: touch ~/soul-configured\n\nThe human admin will start chatting shortly.\"\n _txn_id=\"welcome-$(date +%s)\"\n _payload=$(jq -nc --arg body \"${_welcome_msg}\" '{\"msgtype\":\"m.text\",\"body\":$body}')\n _raw=$(curl -s -w '\\nHTTP_CODE:%{http_code}' -X PUT \"${HICLAW_MATRIX_URL}/_matrix/client/v3/rooms/${DM_ROOM_ID}/send/m.room.message/${_txn_id}\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d \"${_payload}\" 2>&1) || true\n _http_code=$(echo \"${_raw}\" | tail -1 | sed 's/HTTP_CODE://')\n _send_resp=$(echo \"${_raw}\" | sed '$d')\n if echo \"${_send_resp}\" | jq -e '.event_id' > /dev/null 2>&1; then\n echo \"[manager] Welcome message sent to DM room\"\n else\n echo \"[manager] WARNING: Failed to send welcome message (HTTP ${_http_code}): ${_send_resp}\"\n fi\n ) &\n log \"Welcome message background process started (PID: $!)\"\n fi\nfi\nfi # end K8s mode skip for admin DM room\n\n# ============================================================\n# Generate Manager Agent openclaw.json from template\n# ============================================================\nlog \"Generating Manager openclaw.json...\"\nexport MANAGER_MATRIX_TOKEN=\"${MANAGER_TOKEN}\"\nexport MANAGER_GATEWAY_KEY=\"${HICLAW_MANAGER_GATEWAY_KEY}\"\n# Resolve model parameters based on model name\nMODEL_NAME=\"${HICLAW_DEFAULT_MODEL:-qwen3.6-plus}\"\ncase \"${MODEL_NAME}\" in\n gpt-5.3-codex|gpt-5-mini|gpt-5-nano)\n export MODEL_CONTEXT_WINDOW=400000 MODEL_MAX_TOKENS=128000 ;;\n claude-opus-4-6)\n export MODEL_CONTEXT_WINDOW=1000000 MODEL_MAX_TOKENS=128000 ;;\n claude-sonnet-4-6)\n export MODEL_CONTEXT_WINDOW=1000000 MODEL_MAX_TOKENS=64000 ;;\n claude-haiku-4-5)\n export MODEL_CONTEXT_WINDOW=200000 MODEL_MAX_TOKENS=64000 ;;\n qwen3.6-plus|qwen3.5-plus)\n export MODEL_CONTEXT_WINDOW=200000 MODEL_MAX_TOKENS=64000 ;;\n deepseek-chat|deepseek-reasoner|kimi-k2.5)\n export MODEL_CONTEXT_WINDOW=256000 MODEL_MAX_TOKENS=128000 ;;\n glm-5|MiniMax-M2.7|MiniMax-M2.7-highspeed|MiniMax-M2.5)\n export MODEL_CONTEXT_WINDOW=200000 MODEL_MAX_TOKENS=128000 ;;\n *)\n export MODEL_CONTEXT_WINDOW=150000 MODEL_MAX_TOKENS=128000 ;;\nesac\nexport MODEL_REASONING=true\n\n# Override with user-supplied custom model parameters from env (set during install)\n[ -n \"${HICLAW_MODEL_CONTEXT_WINDOW:-}\" ] && export MODEL_CONTEXT_WINDOW=\"${HICLAW_MODEL_CONTEXT_WINDOW}\"\n[ -n \"${HICLAW_MODEL_MAX_TOKENS:-}\" ] && export MODEL_MAX_TOKENS=\"${HICLAW_MODEL_MAX_TOKENS}\"\n[ -n \"${HICLAW_MODEL_REASONING:-}\" ] && export MODEL_REASONING=\"${HICLAW_MODEL_REASONING}\"\n\n# E2EE: convert HICLAW_MATRIX_E2EE to JSON boolean for template substitution\nif [ \"${HICLAW_MATRIX_E2EE:-0}\" = \"1\" ] || [ \"${HICLAW_MATRIX_E2EE:-}\" = \"true\" ]; then\n export MATRIX_E2EE_ENABLED=true\nelse\n export MATRIX_E2EE_ENABLED=false\nfi\nlog \"Matrix E2EE: ${MATRIX_E2EE_ENABLED}\"\n\n# Resolve input modalities: only vision-capable models get \"image\"\ncase \"${MODEL_NAME}\" in\n gpt-5.4|gpt-5.3-codex|gpt-5-mini|gpt-5-nano|claude-opus-4-6|claude-sonnet-4-6|claude-haiku-4-5|qwen3.6-plus|qwen3.5-plus|kimi-k2.5)\n export MODEL_INPUT='[\"text\", \"image\"]' ;;\n *)\n export MODEL_INPUT='[\"text\"]' ;;\nesac\n# Override with user-supplied vision setting from env\nif [ \"${HICLAW_MODEL_VISION:-}\" = \"true\" ]; then\n export MODEL_INPUT='[\"text\", \"image\"]'\nelif [ \"${HICLAW_MODEL_VISION:-}\" = \"false\" ]; then\n export MODEL_INPUT='[\"text\"]'\nfi\n\nlog \"Model: ${MODEL_NAME} (context=${MODEL_CONTEXT_WINDOW}, maxTokens=${MODEL_MAX_TOKENS}, reasoning=${MODEL_REASONING}, input=${MODEL_INPUT})\"\n\nif [ -f /root/manager-workspace/openclaw.json ]; then\n log \"Manager openclaw.json already exists, updating dynamic fields only (preserving user customizations)...\"\n # Merge known models into existing config (add missing, preserve user-added)\n # Use known-models.json (valid JSON) instead of template (contains ${VAR} placeholders)\n KNOWN_MODELS=$(cat /opt/hiclaw/configs/known-models.json 2>/dev/null || echo '[]')\n jq --arg token \"${MANAGER_TOKEN}\" \\\n --arg key \"${HICLAW_MANAGER_GATEWAY_KEY}\" \\\n --arg model \"${MODEL_NAME}\" \\\n --arg emb_model \"${HICLAW_EMBEDDING_MODEL}\" \\\n --arg aigw_domain \"${AI_GATEWAY_DOMAIN}\" \\\n --arg matrix_user_id \"@manager:${MATRIX_DOMAIN}\" \\\n --argjson e2ee \"${MATRIX_E2EE_ENABLED}\" \\\n --argjson known_models \"${KNOWN_MODELS}\" \\\n --argjson ctx \"${MODEL_CONTEXT_WINDOW}\" \\\n --argjson max \"${MODEL_MAX_TOKENS}\" \\\n --argjson reasoning \"${MODEL_REASONING}\" \\\n --argjson input \"${MODEL_INPUT}\" \\\n '\n # Merge known models: add any model id not already present\n .models.providers[\"hiclaw-gateway\"].models as $existing\n | ($existing | map(.id)) as $existing_ids\n | ($known_models | map(select(.id as $id | $existing_ids | index($id) | not))) as $new\n | .models.providers[\"hiclaw-gateway\"].models = ($existing + $new)\n # Ensure the user-chosen default model is in the list (custom model support)\n | if (.models.providers[\"hiclaw-gateway\"].models | map(.id) | index($model) | not) then\n .models.providers[\"hiclaw-gateway\"].models += [{\"id\": $model, \"name\": $model, \"reasoning\": $reasoning, \"contextWindow\": $ctx, \"maxTokens\": $max, \"input\": $input}]\n else . end\n # Rebuild model aliases from the full models list\n | (.models.providers[\"hiclaw-gateway\"].models | map({ (\"hiclaw-gateway/\" + .id): { \"alias\": .id } }) | add // {}) as $aliases\n | .agents.defaults.models = ((.agents.defaults.models // {}) + $aliases)\n | .channels.matrix.accessToken = $token | .channels.matrix.userId = $matrix_user_id | .models.providers[\"hiclaw-gateway\"].apiKey = $key\n | ((.hooks.token // \"\") as $ht | if $ht == $key or $ht == ($key + \"-hooks\" | @base64) then del(.hooks) else . end)\n | .agents.defaults.model.primary = (\"hiclaw-gateway/\" + $model)\n | .commands.restart = true\n | .gateway.port = 18799\n | .gateway.bind = \"lan\"\n | .gateway.controlUi = ((.gateway.controlUi // {}) + {\"dangerouslyDisableDeviceAuth\": true, \"allowInsecureAuth\": true, \"allowedOrigins\": [\"*\"]})\n | .channels.matrix.encryption = $e2ee\n | .channels.matrix.network = ((.channels.matrix.network // {}) + {\"dangerouslyAllowPrivateNetwork\": true})\n | .channels.matrix.autoJoin = \"always\"\n # OpenClaw YOLO defaults: host exec without approval prompts (see openclaw docs tools/exec-approvals)\n | .tools = (.tools // {})\n | .tools.exec = ((.tools.exec // {}) + {\"host\":\"gateway\",\"security\":\"full\",\"ask\":\"off\"})\n | .tools.elevated = (.tools.elevated // {})\n | .tools.elevated.enabled = true\n | .tools.elevated.allowFrom |= ((. // {}) | .matrix = [\"*\"])\n | .agents.defaults.elevatedDefault = \"full\"\n # Ensure memorySearch config exists (embedding model for memory) — skip if embedding model is empty\n | if $emb_model != \"\" then .agents.defaults.memorySearch //= {\"provider\":\"openai\",\"model\":$emb_model,\"remote\":{\"baseUrl\":(\"http://\" + $aigw_domain + \":8080/v1\"),\"apiKey\":$key}} else . end\n ' \\\n /root/manager-workspace/openclaw.json > /tmp/openclaw.json.tmp && \\\n mv /tmp/openclaw.json.tmp /root/manager-workspace/openclaw.json\n # Disable openclaw's observe-recovery mechanism which compares config against\n # a lastKnownGood baseline in config-health.json. When meta is missing from the\n # current file but present in the baseline, observe-recovery restores from .bak,\n # undoing user customizations (plugins, channels, etc).\n # Clearing config-health.json removes the baseline so observe-recovery won't\n # interfere, while preserving .bak as a backup.\n rm -f /root/manager-workspace/.openclaw/logs/config-health.json\n # Verify the token was written correctly\n _written_token=$(jq -r '.channels.matrix.accessToken' /root/manager-workspace/openclaw.json 2>/dev/null)\n if [ -z \"${_written_token}\" ] || [ \"${_written_token}\" = \"null\" ]; then\n log \"ERROR: Matrix token was not written correctly to openclaw.json (got: ${_written_token})\"\n else\n log \"Matrix token written to openclaw.json (prefix: ${_written_token:0:10}...)\"\n fi\nelse\n log \"Manager openclaw.json not found, generating from template...\"\n envsubst < /opt/hiclaw/configs/manager-openclaw.json.tmpl > /root/manager-workspace/openclaw.json\n # Post-envsubst injection: memorySearch + custom model (single jq pass when possible)\n if ! jq -e --arg model \"${MODEL_NAME}\" '.models.providers[\"hiclaw-gateway\"].models | map(.id) | index($model)' /root/manager-workspace/openclaw.json > /dev/null 2>&1; then\n log \"Custom model '${MODEL_NAME}' not in built-in list, injecting into config...\"\n jq --arg emb_model \"${HICLAW_EMBEDDING_MODEL}\" \\\n --arg aigw_domain \"${AI_GATEWAY_DOMAIN}\" \\\n --arg key \"${HICLAW_MANAGER_GATEWAY_KEY}\" \\\n --arg model \"${MODEL_NAME}\" \\\n --argjson ctx \"${MODEL_CONTEXT_WINDOW}\" \\\n --argjson max \"${MODEL_MAX_TOKENS}\" \\\n --argjson reasoning \"${MODEL_REASONING}\" \\\n --argjson input \"${MODEL_INPUT}\" \\\n '\n (if $emb_model != \"\" then .agents.defaults.memorySearch = {\"provider\":\"openai\",\"model\":$emb_model,\"remote\":{\"baseUrl\":(\"http://\" + $aigw_domain + \":8080/v1\"),\"apiKey\":$key}} else . end)\n | .models.providers[\"hiclaw-gateway\"].models += [{\"id\": $model, \"name\": $model, \"reasoning\": $reasoning, \"contextWindow\": $ctx, \"maxTokens\": $max, \"input\": $input}]\n | .agents.defaults.models += {(\"hiclaw-gateway/\" + $model): {\"alias\": $model}}\n ' /root/manager-workspace/openclaw.json > /tmp/openclaw.json.tmp && \\\n mv /tmp/openclaw.json.tmp /root/manager-workspace/openclaw.json\n elif [ -n \"${HICLAW_EMBEDDING_MODEL}\" ]; then\n jq --arg emb_model \"${HICLAW_EMBEDDING_MODEL}\" \\\n --arg aigw_domain \"${AI_GATEWAY_DOMAIN}\" \\\n --arg key \"${HICLAW_MANAGER_GATEWAY_KEY}\" \\\n '.agents.defaults.memorySearch = {\"provider\":\"openai\",\"model\":$emb_model,\"remote\":{\"baseUrl\":(\"http://\" + $aigw_domain + \":8080/v1\"),\"apiKey\":$key}}' \\\n /root/manager-workspace/openclaw.json > /tmp/openclaw.json.tmp && \\\n mv /tmp/openclaw.json.tmp /root/manager-workspace/openclaw.json\n fi\n _written_token=$(jq -r '.channels.matrix.accessToken' /root/manager-workspace/openclaw.json 2>/dev/null)\n log \"Matrix token written from template (prefix: ${_written_token:0:10}...)\"\nfi\n\n# Cloud/K8s mode: overlay cloud-specific settings onto generated config\nif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ] || [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n log \"Applying cloud/k8s overlay to openclaw.json...\"\n jq --arg homeserver \"${HICLAW_MATRIX_URL}\" \\\n --arg gateway \"${HICLAW_AI_GATEWAY_URL}/v1\" \\\n --arg key \"${HICLAW_MANAGER_GATEWAY_KEY}\" \\\n '.channels.matrix.homeserver = $homeserver\n | .models.providers[\"hiclaw-gateway\"].baseUrl = $gateway\n | .models.providers[\"hiclaw-gateway\"].apiKey = $key\n | ((.hooks.token // \"\") as $ht | if $ht == $key or $ht == ($key + \"-hooks\" | @base64) then del(.hooks) else . end)\n | .commands.restart = false\n | if .agents.defaults.memorySearch then .agents.defaults.memorySearch.remote.baseUrl = $gateway | .agents.defaults.memorySearch.remote.apiKey = $key else . end' \\\n /root/manager-workspace/openclaw.json > /tmp/openclaw-cloud.json && \\\n mv /tmp/openclaw-cloud.json /root/manager-workspace/openclaw.json\n log \"Cloud/K8s overlay applied\"\nfi\n\n# ============================================================\n# Optional: enable openclaw-cms-plugin observability\n# Config is applied at runtime so secrets stay out of image layers.\n# ============================================================\nCMS_TRACES_ENABLED=\"$(echo \"${HICLAW_CMS_TRACES_ENABLED:-false}\" | tr '[:upper:]' '[:lower:]')\"\nif [ \"${CMS_TRACES_ENABLED}\" = \"true\" ]; then\n CMS_PLUGIN_NAME=\"openclaw-cms-plugin\"\n CMS_PLUGIN_DIR=\"${OPENCLAW_CMS_PLUGIN_DIR:-/opt/openclaw/extensions/openclaw-cms-plugin}\"\n CMS_PLUGIN_MANIFEST=\"${CMS_PLUGIN_DIR}/openclaw.plugin.json\"\n DIAG_PLUGIN_NAME=\"diagnostics-otel\"\n DIAG_PLUGIN_DIR=\"/opt/openclaw/extensions/diagnostics-otel\"\n CMS_LICENSE_KEY=\"${HICLAW_CMS_LICENSE_KEY:-}\"\n CMS_PROJECT=\"${HICLAW_CMS_PROJECT:-}\"\n CMS_METRICS_ENABLED=\"${HICLAW_CMS_METRICS_ENABLED:-false}\"\n\n if [ ! -f \"${CMS_PLUGIN_MANIFEST}\" ]; then\n log \"WARNING: ${CMS_PLUGIN_NAME} manifest not found at ${CMS_PLUGIN_MANIFEST}, skipping plugin config\"\n else\n _missing=0\n [ -z \"${HICLAW_CMS_ENDPOINT:-}\" ] && log \"WARNING: HICLAW_CMS_ENDPOINT is required when HICLAW_CMS_TRACES_ENABLED=true\" && _missing=1\n [ -z \"${CMS_LICENSE_KEY:-}\" ] && log \"WARNING: HICLAW_CMS_LICENSE_KEY is required when HICLAW_CMS_TRACES_ENABLED=true\" && _missing=1\n [ -z \"${HICLAW_CMS_WORKSPACE:-}\" ] && log \"WARNING: HICLAW_CMS_WORKSPACE is required when HICLAW_CMS_TRACES_ENABLED=true\" && _missing=1\n\n if [ \"${_missing}\" = \"0\" ]; then\n CMS_SERVICE_NAME=\"${HICLAW_CMS_SERVICE_NAME:-hiclaw-manager}\"\n CMS_ENABLE_METRICS=\"${CMS_METRICS_ENABLED}\"\n DIAG_AVAILABLE=\"0\"\n _metrics_lc=\"$(echo \"${CMS_ENABLE_METRICS}\" | tr '[:upper:]' '[:lower:]')\"\n if [ \"${_metrics_lc}\" = \"true\" ]; then\n if [ -f \"${DIAG_PLUGIN_DIR}/package.json\" ]; then\n DIAG_AVAILABLE=\"1\"\n if [ ! -d \"${DIAG_PLUGIN_DIR}/node_modules\" ]; then\n log \"diagnostics-otel dependencies missing, installing...\"\n if (cd \"${DIAG_PLUGIN_DIR}\" && npm install --omit=dev --ignore-scripts >/tmp/hiclaw-diag-install.log 2>&1); then\n log \"diagnostics-otel dependencies installed\"\n else\n log \"WARNING: diagnostics-otel npm install failed, metrics plugin may not load\"\n fi\n else\n log \"diagnostics-otel dependencies already present\"\n fi\n else\n log \"WARNING: diagnostics-otel package.json not found at ${DIAG_PLUGIN_DIR}, metrics plugin may not load\"\n fi\n fi\n\n log \"Applying ${CMS_PLUGIN_NAME} config to openclaw.json...\"\n jq --arg pluginName \"${CMS_PLUGIN_NAME}\" \\\n --arg pluginDir \"${CMS_PLUGIN_DIR}\" \\\n --arg endpoint \"${HICLAW_CMS_ENDPOINT}\" \\\n --arg licenseKey \"${CMS_LICENSE_KEY}\" \\\n --arg armsProject \"${CMS_PROJECT}\" \\\n --arg cmsWorkspace \"${HICLAW_CMS_WORKSPACE}\" \\\n --arg serviceName \"${CMS_SERVICE_NAME}\" \\\n --arg diagPluginName \"${DIAG_PLUGIN_NAME}\" \\\n --arg diagPluginDir \"${DIAG_PLUGIN_DIR}\" \\\n --arg metricsRaw \"${CMS_ENABLE_METRICS}\" \\\n --arg diagAvailableRaw \"${DIAG_AVAILABLE}\" \\\n '\n .plugins = (.plugins // {})\n | .plugins.load = (.plugins.load // {})\n | .plugins.entries = (.plugins.entries // {})\n | if (.plugins.allow | type) != \"array\" then .plugins.allow = [] else . end\n | if (.plugins.allow | index($pluginName)) == null then .plugins.allow += [$pluginName] else . end\n | if (.plugins.load.paths | type) != \"array\" then .plugins.load.paths = [] else . end\n | if (.plugins.load.paths | index($pluginDir)) == null then .plugins.load.paths += [$pluginDir] else . end\n | .plugins.entries[$pluginName] = {\n \"enabled\": true,\n \"config\": {\n \"endpoint\": $endpoint,\n \"headers\": {\n \"x-arms-license-key\": $licenseKey,\n \"x-arms-project\": $armsProject,\n \"x-cms-workspace\": $cmsWorkspace\n },\n \"serviceName\": $serviceName\n }\n }\n\n # diagnostics-otel metrics (optional)\n | ($metricsRaw | ascii_downcase) as $m\n | ($diagAvailableRaw == \"1\") as $diagAvailable\n | (($m == \"true\") and $diagAvailable) as $metricsEnabled\n | if $metricsEnabled then\n (if (.plugins.allow | index($diagPluginName)) == null then .plugins.allow += [$diagPluginName] else . end)\n | (if (.plugins.load.paths | index($diagPluginDir)) == null then .plugins.load.paths += [$diagPluginDir] else . end)\n | .plugins.entries[$diagPluginName].enabled = true\n | .diagnostics = (.diagnostics // {})\n | .diagnostics.otel = (.diagnostics.otel // {})\n | .diagnostics.enabled = true\n | .diagnostics.otel.enabled = true\n | .diagnostics.otel.endpoint = $endpoint\n | .diagnostics.otel.protocol = (.diagnostics.otel.protocol // \"http/protobuf\")\n | .diagnostics.otel.headers = {\n \"x-arms-license-key\": $licenseKey,\n \"x-arms-project\": $armsProject,\n \"x-cms-workspace\": $cmsWorkspace\n }\n | .diagnostics.otel.serviceName = $serviceName\n | .diagnostics.otel.metrics = true\n | .diagnostics.otel.traces = (.diagnostics.otel.traces // false)\n | .diagnostics.otel.logs = (.diagnostics.otel.logs // false)\n else\n .\n end\n ' /root/manager-workspace/openclaw.json > /tmp/openclaw-cms.json && \\\n mv /tmp/openclaw-cms.json /root/manager-workspace/openclaw.json\n log \"${CMS_PLUGIN_NAME} config applied (metrics=${CMS_ENABLE_METRICS}, service=${CMS_SERVICE_NAME})\"\n else\n log \"Skipping ${CMS_PLUGIN_NAME} config due to missing required env vars\"\n fi\n fi\nfi\n\n# ============================================================\n# Detect container runtime (for Worker creation)\n# ============================================================\nsource /opt/hiclaw/scripts/lib/container-api.sh\nif container_api_available; then\n log \"Container runtime socket detected at ${CONTAINER_SOCKET} — direct Worker creation enabled\"\n export HICLAW_CONTAINER_RUNTIME=\"socket\"\nelif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ] || [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n log \"Cloud/K8s mode — Workers created via controller API\"\n export HICLAW_CONTAINER_RUNTIME=\"cloud\"\nelse\n log \"No container runtime found — Worker creation will output install commands\"\n export HICLAW_CONTAINER_RUNTIME=\"none\"\nfi\n\n# ============================================================\n# Upgrade Worker openclaw.json: merge known models + E2EE flag into existing configs\n# Existing workers in MinIO may have old single-model configs or missing encryption field.\n# Merge template models so they can hot-switch without restart.\n# ============================================================\nREGISTRY_FILE=\"/root/manager-workspace/workers-registry.json\"\nif [ -f \"${REGISTRY_FILE}\" ]; then\n # Use known-models.json (valid JSON) instead of template (contains ${VAR} placeholders)\n KNOWN_MODELS_FILE=\"/opt/hiclaw/configs/known-models.json\"\n if [ -f \"${KNOWN_MODELS_FILE}\" ]; then\n _KNOWN_MODELS=$(cat \"${KNOWN_MODELS_FILE}\")\n for _wname in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_wname}\" ] && continue\n _minio_path=\"${HICLAW_STORAGE_PREFIX}/agents/${_wname}/openclaw.json\"\n _tmp_in=\"/tmp/openclaw-${_wname}-models-upgrade-in.json\"\n if mc cp \"${_minio_path}\" \"${_tmp_in}\" 2>/dev/null; then\n _tmp_out=\"/tmp/openclaw-${_wname}-models-upgrade-out.json\"\n # Idempotent merge: add missing known models, rebuild aliases, set e2ee.\n # Always runs — jq deduplicates by model id, so re-runs are safe.\n jq --argjson known_models \"${_KNOWN_MODELS}\" \\\n --argjson e2ee \"${MATRIX_E2EE_ENABLED}\" '\n .models.providers[\"hiclaw-gateway\"].models as $existing\n | ($existing | map(.id)) as $existing_ids\n | ($known_models | map(select(.id as $id | $existing_ids | index($id) | not))) as $new\n | .models.providers[\"hiclaw-gateway\"].models = ($existing + $new)\n | (.models.providers[\"hiclaw-gateway\"].models | map({ (\"hiclaw-gateway/\" + .id): { \"alias\": .id } }) | add // {}) as $aliases\n | .agents.defaults.models = ((.agents.defaults.models // {}) + $aliases)\n | .channels.matrix.encryption = $e2ee\n | .channels.matrix.autoJoin = \"always\"\n | .tools = (.tools // {})\n | .tools.exec = ((.tools.exec // {}) + {\"host\":\"gateway\",\"security\":\"full\",\"ask\":\"off\"})\n | .tools.elevated = (.tools.elevated // {})\n | .tools.elevated.enabled = true\n | .tools.elevated.allowFrom |= ((. // {}) | .matrix = [\"*\"])\n | .agents.defaults.elevatedDefault = \"full\"\n ' \"${_tmp_in}\" > \"${_tmp_out}\" 2>/dev/null\n if ! diff -q \"${_tmp_in}\" \"${_tmp_out}\" > /dev/null 2>&1; then\n if mc cp \"${_tmp_out}\" \"${_minio_path}\" 2>/dev/null; then\n _new_count=$(jq '.models.providers[\"hiclaw-gateway\"].models | length' \"${_tmp_out}\" 2>/dev/null)\n log \"Worker ${_wname}: upgraded openclaw.json (models: ${_new_count}, e2ee: ${MATRIX_E2EE_ENABLED})\"\n fi\n fi\n rm -f \"${_tmp_in}\" \"${_tmp_out}\"\n fi\n done\n fi\nfi\n\n# ============================================================\n# Ensure Worker Matrix password files exist in MinIO (E2EE fix)\n# Workers need to re-login on restart to get a fresh device_id.\n# Older workers created before this fix won't have the password file.\n# ============================================================\nif [ -f \"${REGISTRY_FILE}\" ]; then\n for _wname in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_wname}\" ] && continue\n _creds_file=\"/data/worker-creds/${_wname}.env\"\n if [ -f \"${_creds_file}\" ]; then\n # Check if password file already exists in MinIO\n if ! mc stat \"${HICLAW_STORAGE_PREFIX}/agents/${_wname}/credentials/matrix/password\" > /dev/null 2>&1; then\n source \"${_creds_file}\"\n if [ -n \"${WORKER_PASSWORD}\" ]; then\n _tmp_pw=\"/tmp/matrix-pw-${_wname}\"\n echo -n \"${WORKER_PASSWORD}\" > \"${_tmp_pw}\"\n mc cp \"${_tmp_pw}\" \"${HICLAW_STORAGE_PREFIX}/agents/${_wname}/credentials/matrix/password\" 2>/dev/null \\\n && log \"Worker ${_wname}: wrote Matrix password to MinIO (E2EE re-login fix)\" \\\n || log \"Worker ${_wname}: WARNING: failed to write Matrix password to MinIO\"\n rm -f \"${_tmp_pw}\"\n fi\n fi\n fi\n done\nfi\n\n# ============================================================\n# Recreate Worker containers as needed after Manager restart.\n# Workers are on hiclaw-net; Docker DNS resolves *-local.hiclaw.io via\n# the Manager's network aliases, so IP changes don't require worker recreation.\n# Only recreate stopped/missing workers.\n# ============================================================\nif container_api_available; then\n REGISTRY_FILE=\"/root/manager-workspace/workers-registry.json\"\n if [ -f \"${REGISTRY_FILE}\" ]; then\n for _worker_name in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_worker_name}\" ] && continue\n\n # Skip remote workers — they are not Manager-managed containers.\n _deployment=$(jq -r --arg w \"${_worker_name}\" '.workers[$w].deployment // \"local\"' \"${REGISTRY_FILE}\" 2>/dev/null)\n if [ \"${_deployment}\" = \"remote\" ]; then\n log \"Worker ${_worker_name} is remote, skipping container recreate\"\n continue\n fi\n\n _status=$(container_status_worker \"${_worker_name}\")\n if [ \"${_status}\" = \"running\" ]; then\n log \"Worker running: ${_worker_name}, skipping\"\n continue\n fi\n # Container missing or stopped — recreate.\n log \"Worker container ${_status}: ${_worker_name}, recreating...\"\n _creds_file=\"/data/worker-creds/${_worker_name}.env\"\n if [ -f \"${_creds_file}\" ]; then\n source \"${_creds_file}\"\n _runtime=$(jq -r --arg w \"${_worker_name}\" '.workers[$w].runtime // \"openclaw\"' \"${REGISTRY_FILE}\" 2>/dev/null)\n _recreated=false\n for _attempt in 1 2 3; do\n local _env_map _create_body\n _env_map=$(jq -cn \\\n --arg name \"${_worker_name}\" \\\n --arg fak \"${_worker_name}\" \\\n --arg fsk \"${WORKER_MINIO_PASSWORD:-}\" \\\n --arg fs_domain \"${HICLAW_FS_DOMAIN:-fs-local.hiclaw.io}\" \\\n --arg controller_url \"${HICLAW_CONTROLLER_URL:-}\" \\\n '{\n \"HICLAW_WORKER_NAME\": $name,\n \"HICLAW_FS_ENDPOINT\": (\"http://\" + ($fs_domain | split(\":\")[0]) + \":9000\"),\n \"HICLAW_FS_ACCESS_KEY\": $fak,\n \"HICLAW_FS_SECRET_KEY\": $fsk\n }\n | if $controller_url != \"\" then . + {\"HICLAW_CONTROLLER_URL\": $controller_url} else . end')\n _create_body=$(jq -cn --arg name \"${_worker_name}\" --arg runtime \"${_runtime}\" --argjson env \"${_env_map}\" '{name: $name, runtime: $runtime, env: $env}')\n worker_backend_create \"${_create_body}\" > /dev/null 2>&1 && _recreated=true && break\n log \" Attempt ${_attempt}/3 failed for ${_worker_name}, retrying in $((5 * _attempt))s...\"\n sleep $((5 * _attempt))\n done\n if [ \"${_recreated}\" = true ]; then\n log \" Recreated ${_runtime} worker: ${_worker_name}\"\n else\n log \" ERROR: Failed to recreate ${_worker_name} after 3 attempts\"\n fi\n else\n log \" WARNING: No credentials found for ${_worker_name} (${_creds_file} missing), skipping\"\n fi\n done\n fi\nfi\n\n# ============================================================\n# Notify workers of builtin updates if upgrade happened\n# Builtin files (AGENTS.md, skills) are already synced by upgrade-builtins.sh\n#\n# Cooldown: skip notification if the last successful notify was within\n# NOTIFY_COOLDOWN_SECS (default 3600s / 1 hour). This prevents repeated\n# notifications when the Manager crash-loops and re-runs upgrade-builtins\n# on every restart (e.g. IMAGE_VERSION=latest always triggers upgrade).\n# ============================================================\nNOTIFY_COOLDOWN_SECS=\"${HICLAW_NOTIFY_COOLDOWN_SECS:-3600}\"\nNOTIFY_TS_FILE=\"/root/manager-workspace/.last-worker-notify-ts\"\n\nif [ -f /root/manager-workspace/.upgrade-pending-worker-notify ]; then\n _now=$(date +%s)\n _last_notify=$(cat \"${NOTIFY_TS_FILE}\" 2>/dev/null || echo \"0\")\n _elapsed=$(( _now - _last_notify ))\n\n if [ \"${_elapsed}\" -lt \"${NOTIFY_COOLDOWN_SECS}\" ]; then\n log \"Skipping worker builtin notification (last notify ${_elapsed}s ago, cooldown ${NOTIFY_COOLDOWN_SECS}s)\"\n rm -f /root/manager-workspace/.upgrade-pending-worker-notify\n else\n log \"Notifying workers about builtin updates...\"\n REGISTRY_FILE=\"/root/manager-workspace/workers-registry.json\"\n _notify_ok=false\n if [ -f \"${REGISTRY_FILE}\" ]; then\n for _worker_name in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_worker_name}\" ] && continue\n _room_id=$(jq -r --arg w \"${_worker_name}\" '.workers[$w].room_id // empty' \"${REGISTRY_FILE}\" 2>/dev/null)\n if [ -n \"${_room_id}\" ]; then\n _worker_id=\"@${_worker_name}:${MATRIX_DOMAIN}\"\n _txn_id=\"upgrade-$(date +%s%N)\"\n _msg=\"@${_worker_name}:${MATRIX_DOMAIN} Manager upgraded builtin files (AGENTS.md, skills). Please use your file-sync skill to sync the latest config.\"\n _raw=$(curl -s -w '\\nHTTP_CODE:%{http_code}' -X PUT \\\n \"${HICLAW_MATRIX_URL}/_matrix/client/v3/rooms/${_room_id}/send/m.room.message/${_txn_id}\" \\\n -H \"Authorization: Bearer ${MANAGER_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d \"{\\\"msgtype\\\":\\\"m.text\\\",\\\"body\\\":\\\"${_msg}\\\",\\\"m.mentions\\\":{\\\"user_ids\\\":[\\\"${_worker_id}\\\"]}}\" \\\n 2>&1) || true\n _http_code=$(echo \"${_raw}\" | tail -1 | sed 's/HTTP_CODE://')\n _notify_resp=$(echo \"${_raw}\" | sed '$d')\n if echo \"${_notify_resp}\" | jq -e '.event_id' > /dev/null 2>&1; then\n log \" Notified ${_worker_name}\"; _notify_ok=true\n else\n log \" WARNING: Failed to notify ${_worker_name} (HTTP ${_http_code}): ${_notify_resp}\"\n fi\n fi\n done\n fi\n # Record timestamp only if at least one notification succeeded\n if [ \"${_notify_ok}\" = true ]; then\n echo \"${_now}\" > \"${NOTIFY_TS_FILE}\"\n fi\n rm -f /root/manager-workspace/.upgrade-pending-worker-notify\n fi\nfi\n\n# ============================================================\n# Start Manager Agent\n# ============================================================\nlog \"Starting Manager Agent (${MANAGER_RUNTIME})...\"\n\n# HOME is already set to /root/manager-workspace via docker run -e HOME=...\ncd \"${HOME}\"\n\n# Ensure host credential symlinks exist under HOME\nif [ -d \"/host-share\" ]; then\n [ -f \"/host-share/.gitconfig\" ] && ln -sf \"/host-share/.gitconfig\" \"${HOME}/.gitconfig\"\nfi\n\nlog \"HOME=${HOME} (manager-workspace, host-mounted)\"\n\n# ── Render agent doc templates ────────────────────────────────────────────\n# Replace ${VAR} placeholders with actual values so the AI agent reads\n# plain text and never needs to resolve environment variables.\nexport MANAGER_MATRIX_TOKEN MANAGER_TOKEN HIGRESS_COOKIE_FILE\nRENDER=/opt/hiclaw/scripts/lib/render-skills.sh\nlog \"Rendering agent doc templates...\"\n# Manager-owned docs (workspace)\nbash \"$RENDER\" /root/manager-workspace/skills\nbash \"$RENDER\" /root/manager-workspace/skills-alpha\nbash \"$RENDER\" /root/manager-workspace AGENTS.md TOOLS.md HEARTBEAT.md SOUL.md\n# Worker templates (workspace + image) — rendered before push to MinIO\n# so Workers (including remote pip-install) receive plain text\nbash \"$RENDER\" /root/manager-workspace/worker-skills\nbash \"$RENDER\" /root/manager-workspace/worker-agent\nbash \"$RENDER\" /root/manager-workspace/copaw-worker-agent\nbash \"$RENDER\" /root/manager-workspace/hermes-worker-agent\nbash \"$RENDER\" /opt/hiclaw/agent/worker-skills\nbash \"$RENDER\" /opt/hiclaw/agent/worker-agent\nbash \"$RENDER\" /opt/hiclaw/agent/copaw-worker-agent\nbash \"$RENDER\" /opt/hiclaw/agent/hermes-worker-agent\nlog \"Agent doc templates rendered\"\n\n# Cloud mode: start background file sync (workspace ↔ OSS) and initial push\nif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n log \"Syncing initial workspace to OSS...\"\n ensure_mc_credentials\n mc mirror /root/manager-workspace/ \"${HICLAW_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" 2>/dev/null || true\n\n # Local → OSS: change-triggered sync\n (\n while true; do\n CHANGED=$(find /root/manager-workspace/ -type f -newermt \"15 seconds ago\" 2>/dev/null | head -1)\n if [ -n \"${CHANGED}\" ]; then\n ensure_mc_credentials 2>/dev/null || true\n mc mirror /root/manager-workspace/ \"${HICLAW_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" --exclude \".npm/**\" \\\n --exclude \".local/**\" --exclude \".mc/**\" 2>/dev/null || true\n fi\n sleep 10\n done\n ) &\n log \"Local→OSS sync started (PID: $!)\"\n\n # OSS → Local: periodic pull (shared data, agent configs, manager openclaw.json)\n (\n while true; do\n sleep 60\n ensure_mc_credentials 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/shared/\" /root/hiclaw-fs/shared/ --overwrite --newer-than \"1m\" 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/agents/\" /root/hiclaw-fs/agents/ --overwrite --newer-than \"1m\" 2>/dev/null || true\n mc cp \"${HICLAW_STORAGE_PREFIX}/manager/openclaw.json\" /root/manager-workspace/openclaw.json 2>/dev/null || true\n done\n ) &\n log \"OSS→Local sync started (every 60s, PID: $!)\"\nfi\n\n# K8s mode: start background file sync (workspace ↔ MinIO)\nif [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n log \"Syncing initial workspace to MinIO...\"\n mc mirror /root/manager-workspace/ \"${HICLAW_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" 2>/dev/null || true\n\n # Local → MinIO: change-triggered sync\n (\n while true; do\n CHANGED=$(find /root/manager-workspace/ -type f -newermt \"15 seconds ago\" 2>/dev/null | head -1)\n if [ -n \"${CHANGED}\" ]; then\n mc mirror /root/manager-workspace/ \"${HICLAW_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" --exclude \".npm/**\" \\\n --exclude \".local/**\" --exclude \".mc/**\" 2>/dev/null || true\n fi\n sleep 10\n done\n ) &\n log \"Local→MinIO sync started (PID: $!)\"\n\n # MinIO → Local: periodic pull (shared data, agent configs, manager openclaw.json)\n (\n while true; do\n sleep 60\n mc mirror \"${HICLAW_STORAGE_PREFIX}/shared/\" /root/hiclaw-fs/shared/ --overwrite --newer-than \"1m\" 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/agents/\" /root/hiclaw-fs/agents/ --overwrite --newer-than \"1m\" 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/hiclaw-config/\" /root/hiclaw-fs/hiclaw-config/ --overwrite --newer-than \"1m\" 2>/dev/null || true\n mc cp \"${HICLAW_STORAGE_PREFIX}/manager/openclaw.json\" /root/manager-workspace/openclaw.json 2>/dev/null || true\n done\n ) &\n log \"MinIO→Local sync started (every 60s, PID: $!)\"\nfi\n\n# ============================================================\n# Auto-generate Manager mcporter config for pre-configured MCP servers\n# If HICLAW_GITHUB_TOKEN was set at install time, setup-higress.sh already\n# configured GitHub MCP on Higress. Run setup-mcp-server.sh now so that\n# config/mcporter.json exists when the Agent starts — no need to ask user for PAT.\n# ============================================================\nif [ -n \"${HICLAW_GITHUB_TOKEN}\" ] && [ \"${HICLAW_RUNTIME}\" != \"aliyun\" ] && [ \"${HICLAW_RUNTIME}\" != \"k8s\" ]; then\n if [ ! -f \"${HOME}/config/mcporter.json\" ]; then\n log \"Auto-generating Manager mcporter config for GitHub MCP (HICLAW_GITHUB_TOKEN set)...\"\n bash /opt/hiclaw/agent/skills/mcp-server-management/scripts/setup-mcp-server.sh \\\n github \"${HICLAW_GITHUB_TOKEN}\" 2>&1 | while IFS= read -r line; do log \" [setup-mcp] ${line}\"; done || \\\n log \"WARNING: setup-mcp-server.sh failed — Agent may need to configure GitHub MCP manually\"\n else\n log \"Manager mcporter config already exists, skipping auto-generate\"\n fi\nfi\n\n# ============================================================\n# Runtime-specific startup\n# ============================================================\nif [ \"${MANAGER_RUNTIME}\" = \"copaw\" ]; then\n # Delegate to CoPaw startup script\n exec /opt/hiclaw/scripts/init/start-copaw-manager.sh\nelse\n # ── OpenClaw Runtime ─────────────────────────────────────────────────────\n log \"Starting OpenClaw Manager...\"\n\n export OPENCLAW_CONFIG_PATH=\"/root/manager-workspace/openclaw.json\"\n\n # Symlink to default OpenClaw config path so CLI commands find the config\n mkdir -p \"${HOME}/.openclaw\"\n ln -sf \"/root/manager-workspace/openclaw.json\" \"${HOME}/.openclaw/openclaw.json\"\n\n # Clean orphaned session write locks (e.g. from SIGKILL or crash before exit handlers)\n # Prevents \"session file locked (timeout 10000ms)\" when PID was reused\n find \"${HOME}/.openclaw/agents\" -name \"*.jsonl.lock\" -delete 2>/dev/null || true\n log \"Cleaned up any orphaned session write locks\"\n\n # Clean Matrix crypto storage (SQLite WAL may be corrupted after unclean shutdown)\n # Crypto state is re-negotiated on startup; losing it only means re-establishing E2EE sessions\n rm -rf \"${HOME}/.openclaw/matrix\" 2>/dev/null || true\n log \"Cleaned Matrix crypto storage (will re-establish E2EE sessions)\"\n\n # Launch OpenClaw\n # Disable full-process respawn so the CLI uses its internal restart loop.\n # Without this, config reload spawns a detached child and exits, then\n # supervisord restarts the CLI — resulting in two gateway processes.\n export OPENCLAW_NO_RESPAWN=1\n\n # Optional matrix-plugin trace logging — when HICLAW_MATRIX_DEBUG=1 is set\n # in the manager environment (propagated by install / supervisord), turn on\n # OPENCLAW_MATRIX_DEBUG so the matrix plugin emits structured INFO-level\n # lifecycle traces (sync.state transitions, room.invite/join, message\n # handler arrival + filter outcomes). Useful for diagnosing \"worker never\n # joined the room\" / \"manager never replied\" hangs without rebuilding the\n # image.\n if [ \"${HICLAW_MATRIX_DEBUG:-}\" = \"1\" ] && [ -z \"${OPENCLAW_MATRIX_DEBUG:-}\" ]; then\n export OPENCLAW_MATRIX_DEBUG=1\n log \"HICLAW_MATRIX_DEBUG=1 detected; OPENCLAW_MATRIX_DEBUG=1 exported for matrix plugin tracing\"\n fi\n\n exec openclaw gateway run --verbose --force\nfi\n"}, "50f26c2954cb45c08c90430d2d07de91e9af0557": {"t": 1785764742.5470898, "n": 67886, "h": "83f6350337d5d021", "c": "#!/bin/bash\n# start-manager-agent.sh - Initialize and start the Manager Agent\n# Supports local (supervisord), cloud (SAE), and K8s (Helm) deployments.\n# In local mode this is the last supervisord component to start (priority 800).\n# In cloud/k8s mode (HICLAW_RUNTIME=aliyun|k8s) this is the container entrypoint.\n#\n# Runtime selection:\n# HICLAW_MANAGER_RUNTIME=openclaw (default) - OpenClaw gateway mode\n# HICLAW_MANAGER_RUNTIME=copaw - CoPaw workspace mode\n# (hermes runtime is supported for Workers only; Managers run openclaw or copaw.)\n\nsource /opt/hiclaw/scripts/lib/hiclaw-env.sh\n\n# ============================================================\n# Runtime selection\n# ============================================================\nMANAGER_RUNTIME=\"${HICLAW_MANAGER_RUNTIME:-openclaw}\"\ncase \"${MANAGER_RUNTIME}\" in\n copaw)\n log \"Manager runtime: CoPaw (Python workspace)\"\n ;;\n *)\n log \"Manager runtime: OpenClaw (Node.js gateway)\"\n MANAGER_RUNTIME=\"openclaw\"\n ;;\nesac\n\n# ============================================================\n# Set timezone from TZ env var\n# ============================================================\nif [ -n \"${TZ}\" ] && [ -f \"/usr/share/zoneinfo/${TZ}\" ]; then\n ln -sf \"/usr/share/zoneinfo/${TZ}\" /etc/localtime\n echo \"${TZ}\" > /etc/timezone\n log \"Timezone set to ${TZ}\"\nfi\n\nexport MATRIX_DOMAIN=\"${HICLAW_MATRIX_DOMAIN:-matrix-local.hiclaw.io:8080}\"\nAI_GATEWAY_DOMAIN=\"${HICLAW_AI_GATEWAY_DOMAIN:-aigw-local.hiclaw.io}\"\n\n# ============================================================\n# YOLO mode promotion\n# ============================================================\n# In embedded mode the controller does not propagate HICLAW_YOLO to the\n# manager container, but installer / test scripts touch a marker file at\n# `${WORKSPACE}/yolo-mode` instead. Promote that marker to the env var so the\n# agent's documented YOLO check (`HICLAW_YOLO=1`) reliably detects it without\n# depending on filesystem lookups during a turn.\nif [ -z \"${HICLAW_YOLO:-}\" ] && [ -f /root/manager-workspace/yolo-mode ]; then\n export HICLAW_YOLO=1\n log \"YOLO mode marker detected at /root/manager-workspace/yolo-mode; HICLAW_YOLO=1 exported\"\nfi\n\n# ============================================================\n# Cloud/K8s mode: validate required environment variables + initial credentials\n# ============================================================\nif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ] || [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n : \"${HICLAW_MATRIX_URL:?HICLAW_MATRIX_URL is required}\"\n : \"${HICLAW_MATRIX_DOMAIN:?HICLAW_MATRIX_DOMAIN is required}\"\n : \"${HICLAW_AI_GATEWAY_URL:?HICLAW_AI_GATEWAY_URL is required}\"\n if [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n : \"${HICLAW_MANAGER_GATEWAY_KEY:?HICLAW_MANAGER_GATEWAY_KEY is required}\"\n : \"${HICLAW_MANAGER_PASSWORD:?HICLAW_MANAGER_PASSWORD is required (cloud containers are stateless, password must be injected)}\"\n fi\n if [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n # K8s mode: controller handles initialization (admin registration, Higress setup).\n # Manager only needs credentials injected by the ManagerReconciler.\n : \"${HICLAW_MANAGER_GATEWAY_KEY:?HICLAW_MANAGER_GATEWAY_KEY is required (injected by controller)}\"\n : \"${HICLAW_MANAGER_PASSWORD:?HICLAW_MANAGER_PASSWORD is required (injected by controller)}\"\n else\n # Cloud (aliyun) mode: Manager still does its own initialization\n : \"${HICLAW_REGISTRATION_TOKEN:?HICLAW_REGISTRATION_TOKEN is required}\"\n : \"${HICLAW_ADMIN_USER:?HICLAW_ADMIN_USER is required}\"\n : \"${HICLAW_ADMIN_PASSWORD:?HICLAW_ADMIN_PASSWORD is required}\"\n fi\n log \"${HICLAW_RUNTIME} mode: validating environment... OK\"\n log \" Matrix: ${HICLAW_MATRIX_URL}, AI Gateway: ${HICLAW_AI_GATEWAY_URL}, Storage: ${HICLAW_FS_BUCKET}\"\n if [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n ensure_mc_credentials || { log \"FATAL: Initial STS credential fetch failed\"; exit 1; }\n fi\nfi\n\n# ============================================================\n# Local mode: host symlinks, /etc/hosts, wait for local services\n# ============================================================\nif [ \"${HICLAW_RUNTIME}\" != \"aliyun\" ] && [ \"${HICLAW_RUNTIME}\" != \"k8s\" ]; then\n # Create symlink for host directory access\n if [ -d \"/host-share\" ]; then\n ORIGINAL_HOST_HOME=\"${HOST_ORIGINAL_HOME:-$HOME}\"\n if [ ! -e \"${ORIGINAL_HOST_HOME}\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/root\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/data\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/host-share\" ]; then\n mkdir -p \"$(dirname \"${ORIGINAL_HOST_HOME}\")\"\n ln -sfn /host-share \"${ORIGINAL_HOST_HOME}\"\n log \"Created symlink: ${ORIGINAL_HOST_HOME} -> /host-share\"\n else\n ln -sfn /host-share /root/host-home\n log \"Created fallback symlink: /root/host-home -> /host-share\"\n fi\n fi\n\n # Add local domains to /etc/hosts\n HOSTS_DOMAINS=\"${MATRIX_DOMAIN%%:*} ${HICLAW_MATRIX_CLIENT_DOMAIN:-matrix-client-local.hiclaw.io} ${AI_GATEWAY_DOMAIN} ${HICLAW_FS_DOMAIN:-fs-local.hiclaw.io}\"\n if ! grep -q \"${AI_GATEWAY_DOMAIN}\" /etc/hosts 2>/dev/null; then\n echo \"127.0.0.1 ${HOSTS_DOMAINS}\" >> /etc/hosts\n log \"Added local domains to /etc/hosts\"\n fi\n\n # Wait for local infrastructure\n waitForService \"Higress Gateway\" \"127.0.0.1\" 8080 180\n waitForService \"Higress Console\" \"127.0.0.1\" 8001 180\n waitForService \"Tuwunel\" \"127.0.0.1\" 6167 120\n waitForHTTP \"Tuwunel Matrix API\" \"${HICLAW_MATRIX_URL}/_tuwunel/server_version\" 120\n waitForService \"MinIO\" \"127.0.0.1\" 9000 120\nelse\n # Cloud/K8s mode: wait for external Tuwunel\n log \"Waiting for Tuwunel Matrix server at ${HICLAW_MATRIX_URL}...\"\n _retry=0\n while [ \"${_retry}\" -lt 30 ]; do\n if curl -sf \"${HICLAW_MATRIX_URL}/_matrix/client/versions\" > /dev/null 2>&1; then\n log \"Tuwunel is ready\"\n break\n fi\n _retry=$((_retry + 1))\n log \" Waiting for Tuwunel (attempt ${_retry}/30)...\"\n sleep 5\n done\n if [ \"${_retry}\" -ge 30 ]; then\n log \"ERROR: Tuwunel not reachable at ${HICLAW_MATRIX_URL}\"\n exit 1\n fi\nfi\n\n# ============================================================\n# Auto-generate secrets if not provided via environment\n# Persisted to /data so they survive container restart\n# ============================================================\nSECRETS_FILE=\"/data/hiclaw-secrets.env\"\nif [ -f \"${SECRETS_FILE}\" ]; then\n source \"${SECRETS_FILE}\"\n log \"Loaded persisted secrets from ${SECRETS_FILE}\"\nfi\n\nif [ -z \"${HICLAW_MANAGER_GATEWAY_KEY}\" ]; then\n export HICLAW_MANAGER_GATEWAY_KEY=\"$(generateKey 32)\"\n log \"Auto-generated HICLAW_MANAGER_GATEWAY_KEY\"\nfi\nif [ -z \"${HICLAW_MANAGER_PASSWORD}\" ]; then\n export HICLAW_MANAGER_PASSWORD=\"$(generateKey 16)\"\n log \"Auto-generated HICLAW_MANAGER_PASSWORD\"\nfi\n\n# Persist secrets so they survive supervisord restart\nmkdir -p /data\ncat > \"${SECRETS_FILE}\" <<EOF\nexport HICLAW_MANAGER_GATEWAY_KEY=\"${HICLAW_MANAGER_GATEWAY_KEY}\"\nexport HICLAW_MANAGER_PASSWORD=\"${HICLAW_MANAGER_PASSWORD}\"\nEOF\nchmod 600 \"${SECRETS_FILE}\"\n\n# Cloud mode: pull workspace from OSS before initialization\nif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n HICLAW_FS=\"/root/hiclaw-fs\"\n mkdir -p \"${HICLAW_FS}/shared\" \"${HICLAW_FS}/agents\"\n log \"Pulling workspace from OSS...\"\n ensure_mc_credentials\n mc mirror \"${HICLAW_STORAGE_PREFIX}/manager/\" /root/manager-workspace/ --overwrite 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/shared/\" \"${HICLAW_FS}/shared/\" --overwrite 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/agents/\" \"${HICLAW_FS}/agents/\" --overwrite 2>/dev/null || true\n # Symlink hiclaw-fs into workspace for agent access\n ln -sfn \"${HICLAW_FS}\" /root/manager-workspace/hiclaw-fs\nfi\n\n# K8s mode: sync workspace from cluster-internal MinIO\nif [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n HICLAW_FS=\"/root/hiclaw-fs\"\n mkdir -p \"${HICLAW_FS}/shared\" \"${HICLAW_FS}/agents\" \"${HICLAW_FS}/hiclaw-config\"\n log \"Configuring mc alias for cluster MinIO...\"\n mc alias set hiclaw \"${HICLAW_FS_ENDPOINT}\" \"${HICLAW_FS_ACCESS_KEY}\" \"${HICLAW_FS_SECRET_KEY}\"\n log \"Syncing workspace from MinIO...\"\n mc mirror \"${HICLAW_STORAGE_PREFIX}/manager/\" /root/manager-workspace/ --overwrite 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/\" \"${HICLAW_FS}/\" --overwrite 2>/dev/null || true\n ln -sfn \"${HICLAW_FS}\" /root/manager-workspace/hiclaw-fs\n touch \"${HICLAW_FS}/.initialized\"\nfi\n\n# ============================================================\n# Initialize / upgrade Manager workspace\n# First boot: full init via upgrade-builtins.sh\n# Subsequent boots: compare image version; upgrade only if changed\n# ============================================================\nmkdir -p /root/manager-workspace\n\nIMAGE_VERSION=$(cat /opt/hiclaw/agent/.builtin-version 2>/dev/null || echo \"unknown\")\nINSTALLED_VERSION=$(cat /root/manager-workspace/.builtin-version 2>/dev/null || echo \"\")\n\nif [ ! -f /root/manager-workspace/.initialized ]; then\n log \"First boot: initializing manager workspace...\"\n bash /opt/hiclaw/scripts/init/upgrade-builtins.sh\n touch /root/manager-workspace/.initialized\n log \"Manager workspace initialized (version: ${IMAGE_VERSION})\"\nelif [ \"${IMAGE_VERSION}\" != \"${INSTALLED_VERSION}\" ] || [ \"${IMAGE_VERSION}\" = \"latest\" ]; then\n log \"Upgrade detected: ${INSTALLED_VERSION} -> ${IMAGE_VERSION}${IMAGE_VERSION:+ (latest: always upgrade)}\"\n bash /opt/hiclaw/scripts/init/upgrade-builtins.sh\n log \"Manager workspace upgraded to version: ${IMAGE_VERSION}\"\nelse\n log \"Workspace up to date (version: ${IMAGE_VERSION})\"\nfi\n\n# Local mode: wait for mc mirror initialization (shared + worker data in /root/hiclaw-fs/)\nif [ \"${HICLAW_RUNTIME}\" != \"aliyun\" ] && [ \"${HICLAW_RUNTIME}\" != \"k8s\" ]; then\n log \"Waiting for MinIO storage initialization...\"\n _minio_wait=0\n while [ ! -f /root/hiclaw-fs/.initialized ]; do\n sleep 2\n _minio_wait=$(( _minio_wait + 1 ))\n if [ \"${_minio_wait}\" -ge 60 ]; then\n log \"ERROR: MinIO storage initialization timed out after 120s\"\n exit 1\n fi\n done\n log \"MinIO storage initialized\"\nfi\n\n# ============================================================\n# Register Matrix users via Registration API (single-step, no UIAA)\n# K8s mode: skip — controller Initializer + ManagerReconciler already did this\n# ============================================================\nif [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n log \"K8s mode: skipping Matrix registration (handled by controller)\"\n # Controller injects HICLAW_MANAGER_PASSWORD via env; login to get token\n log \"Obtaining Manager Matrix access token...\"\n _LOGIN_RESPONSE=$(curl -s -X POST ${HICLAW_MATRIX_URL}/_matrix/client/v3/login \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"type\": \"m.login.password\",\n \"identifier\": {\"type\": \"m.id.user\", \"user\": \"manager\"},\n \"password\": \"'\"${HICLAW_MANAGER_PASSWORD}\"'\"\n }' 2>&1)\n MANAGER_TOKEN=$(echo \"${_LOGIN_RESPONSE}\" | jq -r '.access_token' 2>/dev/null)\n if [ -z \"${MANAGER_TOKEN}\" ] || [ \"${MANAGER_TOKEN}\" = \"null\" ]; then\n log \"ERROR: Failed to obtain Manager Matrix token\"\n log \"ERROR: Login response was: ${_LOGIN_RESPONSE}\"\n exit 1\n fi\n log \"Manager Matrix token obtained (token prefix: ${MANAGER_TOKEN:0:10}...)\"\nelse\nlog \"Registering human admin Matrix account...\"\ncurl -sf -X POST ${HICLAW_MATRIX_URL}/_matrix/client/v3/register \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"username\": \"'\"${HICLAW_ADMIN_USER}\"'\",\n \"password\": \"'\"${HICLAW_ADMIN_PASSWORD}\"'\",\n \"auth\": {\n \"type\": \"m.login.registration_token\",\n \"token\": \"'\"${HICLAW_REGISTRATION_TOKEN}\"'\"\n }\n }' > /dev/null 2>&1 || log \"Admin account may already exist\"\n\nlog \"Registering Manager Agent Matrix account...\"\ncurl -sf -X POST ${HICLAW_MATRIX_URL}/_matrix/client/v3/register \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"username\": \"manager\",\n \"password\": \"'\"${HICLAW_MANAGER_PASSWORD}\"'\",\n \"auth\": {\n \"type\": \"m.login.registration_token\",\n \"token\": \"'\"${HICLAW_REGISTRATION_TOKEN}\"'\"\n }\n }' > /dev/null 2>&1 || log \"Manager account may already exist\"\n\n# Get Manager Agent's Matrix access token\nlog \"Obtaining Manager Matrix access token...\"\n_LOGIN_RESPONSE=$(curl -s -X POST ${HICLAW_MATRIX_URL}/_matrix/client/v3/login \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"type\": \"m.login.password\",\n \"identifier\": {\"type\": \"m.id.user\", \"user\": \"manager\"},\n \"password\": \"'\"${HICLAW_MANAGER_PASSWORD}\"'\"\n }' 2>&1)\n_LOGIN_EXIT=$?\nlog \"Matrix login HTTP exit code: ${_LOGIN_EXIT}\"\nlog \"Matrix login response: ${_LOGIN_RESPONSE}\"\n\nMANAGER_TOKEN=$(echo \"${_LOGIN_RESPONSE}\" | jq -r '.access_token' 2>/dev/null)\n\nif [ -z \"${MANAGER_TOKEN}\" ] || [ \"${MANAGER_TOKEN}\" = \"null\" ]; then\n log \"ERROR: Failed to obtain Manager Matrix token (exit=${_LOGIN_EXIT})\"\n log \"ERROR: Login response was: ${_LOGIN_RESPONSE}\"\n exit 1\nfi\nlog \"Manager Matrix token obtained (token prefix: ${MANAGER_TOKEN:0:10}...)\"\nfi\n\n# ============================================================\n# Higress Console initialization\n# Docker mode: full setup-higress.sh (internal Higress at localhost:8001)\n# K8s mode: skip — controller Initializer handles Higress setup\n# Cloud (aliyun) mode: skip entirely (Higress managed externally)\n# ============================================================\n_HIGRESS_CONSOLE_URL=\"\"\n_HIGRESS_USER=\"${HICLAW_ADMIN_USER}\"\n_HIGRESS_PASS=\"${HICLAW_ADMIN_PASSWORD}\"\nif [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n log \"K8s mode: skipping Higress initialization (handled by controller)\"\nelif [ \"${HICLAW_RUNTIME}\" != \"aliyun\" ]; then\n _HIGRESS_CONSOLE_URL=\"http://127.0.0.1:8001\"\nfi\n\nif [ -n \"${_HIGRESS_CONSOLE_URL}\" ]; then\n COOKIE_FILE=\"/tmp/higress-session-cookie\"\n\n log \"Waiting for Higress Console (${_HIGRESS_CONSOLE_URL}) to be fully ready and initializing admin...\"\n INIT_DONE=false\n for i in $(seq 1 90); do\n INIT_RESULT=$(curl -s -X POST \"${_HIGRESS_CONSOLE_URL}/system/init\" \\\n -H 'Content-Type: application/json' \\\n -d '{\"adminUser\":{\"name\":\"'\"${_HIGRESS_USER}\"'\",\"password\":\"'\"${_HIGRESS_PASS}\"'\",\"displayName\":\"'\"${_HIGRESS_USER}\"'\"}}' 2>/dev/null) || true\n if echo \"${INIT_RESULT}\" | grep -qE '\"success\":true|already.?init' 2>/dev/null; then\n INIT_DONE=true\n break\n fi\n if echo \"${INIT_RESULT}\" | grep -q '\"name\"' 2>/dev/null; then\n INIT_DONE=true\n break\n fi\n sleep 2\n done\n\n if [ \"${INIT_DONE}\" != \"true\" ]; then\n log \"ERROR: Higress Console did not become ready within 180s\"\n exit 1\n fi\n log \"Higress Console init done\"\n\n log \"Logging into Higress Console...\"\n LOGIN_OK=false\n for i in $(seq 1 10); do\n HTTP_CODE=$(curl -s -o /dev/null -w '%{http_code}' -X POST \"${_HIGRESS_CONSOLE_URL}/session/login\" \\\n -H 'Content-Type: application/json' \\\n -c \"${COOKIE_FILE}\" \\\n -d '{\"username\":\"'\"${_HIGRESS_USER}\"'\",\"password\":\"'\"${_HIGRESS_PASS}\"'\"}' 2>/dev/null) || true\n if { [ \"${HTTP_CODE}\" = \"200\" ] || [ \"${HTTP_CODE}\" = \"201\" ]; } && [ -f \"${COOKIE_FILE}\" ] && [ -s \"${COOKIE_FILE}\" ]; then\n LOGIN_OK=true\n break\n fi\n log \"Login attempt $i (HTTP ${HTTP_CODE}), retrying in 3s...\"\n sleep 3\n done\n\n if [ \"${LOGIN_OK}\" != \"true\" ]; then\n log \"ERROR: Could not login to Higress Console after retries\"\n exit 1\n fi\n log \"Higress Console login successful\"\n\n VERIFY_CODE=$(curl -s -o /dev/null -w '%{http_code}' \"${_HIGRESS_CONSOLE_URL}/v1/consumers\" -b \"${COOKIE_FILE}\" 2>/dev/null) || true\n if [ \"${VERIFY_CODE}\" = \"200\" ]; then\n log \"Console session verified (cookie valid)\"\n else\n log \"WARNING: Console session may be invalid (verify returned HTTP ${VERIFY_CODE})\"\n rm -f \"${COOKIE_FILE}\"\n for i in $(seq 1 5); do\n curl -s -o /dev/null -w '%{http_code}' -X POST \"${_HIGRESS_CONSOLE_URL}/session/login\" \\\n -H 'Content-Type: application/json' \\\n -c \"${COOKIE_FILE}\" \\\n -d '{\"username\":\"'\"${_HIGRESS_USER}\"'\",\"password\":\"'\"${_HIGRESS_PASS}\"'\"}' 2>/dev/null\n VERIFY2=$(curl -s -o /dev/null -w '%{http_code}' \"${_HIGRESS_CONSOLE_URL}/v1/consumers\" -b \"${COOKIE_FILE}\" 2>/dev/null) || true\n if [ \"${VERIFY2}\" = \"200\" ]; then\n log \"Re-login successful, session verified\"\n break\n fi\n sleep 2\n done\n fi\n\n export HIGRESS_COOKIE_FILE=\"${COOKIE_FILE}\"\n export HIGRESS_CONSOLE_URL=\"${_HIGRESS_CONSOLE_URL}\"\n\n if [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n # K8s mode: lightweight Higress config — only what's needed for LLM access\n source /opt/hiclaw/scripts/lib/base.sh\n _k8s_higress_api() {\n local method=\"$1\" path=\"$2\" desc=\"$3\"; shift 3; local body=\"$*\"\n local tmpfile; tmpfile=$(mktemp)\n local http_code\n http_code=$(curl -s -o \"${tmpfile}\" -w '%{http_code}' -X \"${method}\" \"${_HIGRESS_CONSOLE_URL}${path}\" \\\n -b \"${COOKIE_FILE}\" -H 'Content-Type: application/json' -d \"${body}\" 2>/dev/null) || true\n local response; response=$(cat \"${tmpfile}\" 2>/dev/null); rm -f \"${tmpfile}\"\n if echo \"${response}\" | grep -q '\"success\":true' 2>/dev/null; then\n log \"${desc} ... OK\"\n elif [ \"${http_code}\" = \"409\" ]; then\n log \"${desc} ... already exists, skipping\"\n elif [ \"${http_code}\" = \"200\" ] || [ \"${http_code}\" = \"201\" ] || [ \"${http_code}\" = \"204\" ]; then\n log \"${desc} ... OK (HTTP ${http_code})\"\n else\n log \"WARNING: ${desc} ... (HTTP ${http_code}): ${response}\"\n fi\n }\n\n # 1. Service Sources (DNS type → K8s Service FQDN)\n # Extract host:port from URLs for Higress service source registration\n _TUWUNEL_HOST=$(echo \"${HICLAW_MATRIX_URL}\" | sed 's|^http[s]*://||')\n _TUWUNEL_DOMAIN=$(echo \"${_TUWUNEL_HOST}\" | cut -d: -f1)\n _TUWUNEL_PORT=$(echo \"${_TUWUNEL_HOST}\" | cut -d: -f2)\n _k8s_higress_api POST /v1/service-sources \"Registering Tuwunel service source\" \\\n '{\"type\":\"dns\",\"name\":\"tuwunel\",\"domain\":\"'\"${_TUWUNEL_DOMAIN}\"'\",\"port\":'\"${_TUWUNEL_PORT}\"'}'\n\n if [ -n \"${HICLAW_ELEMENT_WEB_URL:-}\" ]; then\n _ELEMENT_HOST=$(echo \"${HICLAW_ELEMENT_WEB_URL}\" | sed 's|^http[s]*://||')\n _ELEMENT_DOMAIN=$(echo \"${_ELEMENT_HOST}\" | cut -d: -f1)\n _ELEMENT_PORT=$(echo \"${_ELEMENT_HOST}\" | cut -d: -f2)\n _k8s_higress_api POST /v1/service-sources \"Registering Element Web service source\" \\\n '{\"type\":\"dns\",\"name\":\"element-web\",\"domain\":\"'\"${_ELEMENT_DOMAIN}\"'\",\"port\":'\"${_ELEMENT_PORT}\"'}'\n fi\n\n # 2. Manager Consumer (key-auth)\n _k8s_higress_api POST /v1/consumers \"Creating Manager consumer\" \\\n '{\"name\":\"manager\",\"credentials\":[{\"type\":\"key-auth\",\"source\":\"BEARER\",\"values\":[\"'\"${HICLAW_MANAGER_GATEWAY_KEY}\"'\"]}]}'\n\n # 3. LLM Provider\n _LLM_PROVIDER=\"${HICLAW_LLM_PROVIDER:-qwen}\"\n _LLM_API_URL=\"${HICLAW_LLM_API_URL:-}\"\n if [ -z \"${_LLM_API_URL}\" ]; then\n case \"${_LLM_PROVIDER}\" in\n qwen) _LLM_API_URL=\"https://dashscope.aliyuncs.com/compatible-mode/v1\" ;;\n esac\n fi\n # 4. LLM Provider type-specific config\n case \"${_LLM_PROVIDER}\" in\n qwen)\n _k8s_higress_api POST /v1/ai/providers \"Creating LLM provider (qwen)\" \\\n '{\"type\":\"qwen\",\"name\":\"qwen\",\"tokens\":[\"'\"${HICLAW_LLM_API_KEY}\"'\"],\"protocol\":\"openai/v1\",\"tokenFailoverConfig\":{\"enabled\":false},\"rawConfigs\":{\"qwenEnableSearch\":false,\"qwenEnableCompatible\":true,\"qwenFileIds\":[],\"hiclawMode\":true}}'\n ;;\n *)\n _BODY='{\"name\":\"'\"${_LLM_PROVIDER}\"'\",\"type\":\"openai\",\"tokens\":[\"'\"${HICLAW_LLM_API_KEY}\"'\"],\"modelMapping\":{},\"protocol\":\"openai/v1\",\"rawConfigs\":{\"hiclawMode\":true}}'\n _k8s_higress_api POST /v1/ai/providers \"Creating LLM provider (${_LLM_PROVIDER})\" \"${_BODY}\"\n ;;\n esac\n\n # 5. AI Route (bind provider + consumer auth, /v1 prefix to avoid clash with Element Web catch-all)\n _k8s_higress_api POST /v1/ai/routes \"Creating AI Gateway route\" \\\n '{\"name\":\"default-ai-route\",\"domains\":[],\"pathPredicate\":{\"matchType\":\"PRE\",\"matchValue\":\"/v1\",\"caseSensitive\":false},\"upstreams\":[{\"provider\":\"'\"${_LLM_PROVIDER}\"'\",\"weight\":100,\"modelMapping\":{}}],\"authConfig\":{\"enabled\":true,\"allowedCredentialTypes\":[\"key-auth\"],\"allowedConsumers\":[\"manager\"]}}'\n\n # 6. Matrix Homeserver Route (/_matrix/* → Tuwunel, no auth)\n _k8s_higress_api POST /v1/routes \"Creating Matrix Homeserver route\" \\\n '{\"name\":\"matrix-homeserver\",\"domains\":[],\"path\":{\"matchType\":\"PRE\",\"matchValue\":\"/_matrix\"},\"services\":[{\"name\":\"tuwunel.dns\",\"port\":'\"${_TUWUNEL_PORT}\"',\"weight\":100}]}'\n\n # 7. Element Web Route (/ catch-all → Element Web, no auth)\n if [ -n \"${HICLAW_ELEMENT_WEB_URL:-}\" ]; then\n _k8s_higress_api POST /v1/routes \"Creating Element Web route\" \\\n '{\"name\":\"element-web\",\"domains\":[],\"path\":{\"matchType\":\"PRE\",\"matchValue\":\"/\"},\"services\":[{\"name\":\"element-web.dns\",\"port\":'\"${_ELEMENT_PORT}\"',\"weight\":100}]}'\n fi\n\n # 8. Remove Higress default landing page (Exact match on / takes precedence over Element Web catch-all)\n _k8s_higress_api DELETE /v1/routes/default \"Removing Higress default landing route\"\n\n log \"K8s Higress lightweight setup complete\"\n\n # Wait for AI plugin activation (~45s for first config)\n log \"Waiting for AI Gateway plugin activation (45s)...\"\n sleep 45\n else\n # Docker mode: full setup with all routes, domains, MCP servers\n /opt/hiclaw/scripts/init/setup-higress.sh\n fi\nfi\n\n# ============================================================\n# Create admin DM room, persist to state.json, send welcome message\n# K8s mode: skip — controller ProvisionManager creates the Admin DM\n# room (Step 4 in service/provisioner.go) AND reconcileManagerWelcome\n# delivers the first-boot onboarding prompt once OpenClaw inside this\n# container has joined the room. In k8s mode the manager intentionally\n# does NOT have the admin password (only HICLAW_ADMIN_USER), so it\n# could not log in as admin to send the welcome itself anyway. The\n# Manager Agent discovers its admin DM room on first heartbeat via\n# state.json / `manage-state.sh --action set-admin-dm` (see HEARTBEAT.md\n# Step 1) — it does not need it to be pre-injected by this script.\n# Runs in both local and cloud modes (idempotent)\n# ============================================================\nif [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n log \"K8s mode: skipping admin DM room creation and welcome message (both handled by hiclaw-controller)\"\nelse\nMANAGER_FULL_ID=\"@manager:${MATRIX_DOMAIN}\"\nADMIN_FULL_ID=\"@${HICLAW_ADMIN_USER}:${MATRIX_DOMAIN}\"\n\nlog \"Logging in as admin to create DM room...\"\n_ADMIN_LOGIN=$(curl -sf -X POST \"${HICLAW_MATRIX_URL}/_matrix/client/v3/login\" \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"type\": \"m.login.password\",\n \"identifier\": {\"type\": \"m.id.user\", \"user\": \"'\"${HICLAW_ADMIN_USER}\"'\"},\n \"password\": \"'\"${HICLAW_ADMIN_PASSWORD}\"'\"\n }' 2>&1) || true\n\nADMIN_MATRIX_TOKEN=$(echo \"${_ADMIN_LOGIN}\" | jq -r '.access_token // empty' 2>/dev/null)\nif [ -z \"${ADMIN_MATRIX_TOKEN}\" ]; then\n log \"WARNING: Failed to login as admin, skipping DM room creation\"\nelse\n # Search for existing DM room with Manager (idempotent)\n DM_ROOM_ID=\"\"\n _JOINED_ROOMS=$(curl -sf \"${HICLAW_MATRIX_URL}/_matrix/client/v3/joined_rooms\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" 2>/dev/null \\\n | jq -r '.joined_rooms[]' 2>/dev/null) || true\n for _rid in ${_JOINED_ROOMS}; do\n _members=$(curl -sf \"${HICLAW_MATRIX_URL}/_matrix/client/v3/rooms/${_rid}/members\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" 2>/dev/null \\\n | jq -r '.chunk[].state_key' 2>/dev/null) || continue\n _count=$(echo \"${_members}\" | wc -l | xargs)\n if [ \"${_count}\" = \"2\" ] && echo \"${_members}\" | grep -q \"@manager:\"; then\n DM_ROOM_ID=\"${_rid}\"\n break\n fi\n done\n\n if [ -n \"${DM_ROOM_ID}\" ]; then\n log \"Existing DM room found: ${DM_ROOM_ID}\"\n else\n log \"Creating DM room with Manager...\"\n _RAW=$(curl -s -w '\\nHTTP_CODE:%{http_code}' -X POST \"${HICLAW_MATRIX_URL}/_matrix/client/v3/createRoom\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d \"{\\\"is_direct\\\":true,\\\"invite\\\":[\\\"${MANAGER_FULL_ID}\\\"],\\\"preset\\\":\\\"trusted_private_chat\\\"}\" 2>&1) || true\n _HTTP_CODE=$(echo \"${_RAW}\" | tail -1 | sed 's/HTTP_CODE://')\n _CREATE_RESP=$(echo \"${_RAW}\" | sed '$d')\n DM_ROOM_ID=$(echo \"${_CREATE_RESP}\" | jq -r '.room_id // empty' 2>/dev/null)\n if [ -n \"${DM_ROOM_ID}\" ]; then\n log \"DM room created: ${DM_ROOM_ID}\"\n else\n log \"WARNING: Failed to create DM room (HTTP ${_HTTP_CODE}): ${_CREATE_RESP}\"\n fi\n fi\n\n # Persist admin DM room ID to state.json\n if [ -n \"${DM_ROOM_ID}\" ]; then\n STATE_SCRIPT=\"/opt/hiclaw/agent/skills/task-management/scripts/manage-state.sh\"\n if [ -f \"${STATE_SCRIPT}\" ]; then\n bash \"${STATE_SCRIPT}\" --action init 2>/dev/null || true\n bash \"${STATE_SCRIPT}\" --action set-admin-dm --room-id \"${DM_ROOM_ID}\" 2>/dev/null || true\n log \"Admin DM room persisted to state.json: ${DM_ROOM_ID}\"\n fi\n fi\n\n # Schedule welcome message in background (only on first boot)\n if [ -n \"${DM_ROOM_ID}\" ] && [ ! -f \"/root/manager-workspace/soul-configured\" ]; then\n log \"Scheduling welcome message (background, waiting for OpenClaw to start)...\"\n (\n _HICLAW_LANGUAGE=\"${HICLAW_LANGUAGE:-zh}\"\n _HICLAW_TIMEZONE=\"${TZ:-Asia/Shanghai}\"\n _wait=0\n _ready=false\n while [ \"${_wait}\" -lt 300 ]; do\n if curl -sf http://127.0.0.1:18799/ > /dev/null 2>&1; then\n _ready=true\n break\n fi\n sleep 3\n _wait=$((_wait + 3))\n done\n if [ \"${_ready}\" != \"true\" ]; then\n echo \"[manager] WARNING: OpenClaw gateway not ready within 300s, skipping welcome message\"\n exit 0\n fi\n # Ensure Manager has joined the DM room before sending the welcome\n # message. Without this, there is a race between OpenClaw's Matrix\n # auto-join and the message send — the message may land before Manager\n # joins, so OpenClaw's /sync never picks it up.\n _join_ok=false\n for _join_attempt in 1 2 3; do\n if curl -sf -X POST \"${HICLAW_MATRIX_URL}/_matrix/client/v3/rooms/${DM_ROOM_ID}/join\" \\\n -H \"Authorization: Bearer ${MANAGER_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d '{}' > /dev/null 2>&1; then\n echo \"[manager] Manager joined DM room before welcome message\"\n _join_ok=true\n break\n fi\n sleep 2\n done\n if [ \"${_join_ok}\" != \"true\" ]; then\n echo \"[manager] WARNING: Manager join request failed after 3 attempts (may already be joined)\"\n fi\n _welcome_msg=\"This is an automated message from the HiClaw setup. This is a fresh installation.\n\n--- Installation Context ---\nUser Language: ${_HICLAW_LANGUAGE} (zh = Chinese, en = English)\nUser Timezone: ${_HICLAW_TIMEZONE} (IANA timezone identifier)\n---\n\nYou are an AI agent that manages a team of worker agents. Your identity and personality have not been configured yet — the human admin is about to meet you for the first time.\n\nPlease begin the onboarding conversation:\n\n1. Greet the admin warmly and briefly describe what you can do (coordinate workers, manage tasks, run multi-agent projects)\n2. The user has selected \\\"${_HICLAW_LANGUAGE}\\\" as their preferred language during installation. Use this language for your greeting and all subsequent communication.\n3. The user's timezone is ${_HICLAW_TIMEZONE}. Based on this timezone, you may infer their likely region and suggest additional language options.\n4. Ask them: a) What would they like to call you? b) Communication style preference? c) Any behavior guidelines? d) Confirm default language\n5. After they reply, write their preferences to ~/SOUL.md\n6. Confirm what you wrote, and ask if they would like to adjust anything\n7. Once confirmed, run: touch ~/soul-configured\n\nThe human admin will start chatting shortly.\"\n _txn_id=\"welcome-$(date +%s)\"\n _payload=$(jq -nc --arg body \"${_welcome_msg}\" '{\"msgtype\":\"m.text\",\"body\":$body}')\n _raw=$(curl -s -w '\\nHTTP_CODE:%{http_code}' -X PUT \"${HICLAW_MATRIX_URL}/_matrix/client/v3/rooms/${DM_ROOM_ID}/send/m.room.message/${_txn_id}\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d \"${_payload}\" 2>&1) || true\n _http_code=$(echo \"${_raw}\" | tail -1 | sed 's/HTTP_CODE://')\n _send_resp=$(echo \"${_raw}\" | sed '$d')\n if echo \"${_send_resp}\" | jq -e '.event_id' > /dev/null 2>&1; then\n echo \"[manager] Welcome message sent to DM room\"\n else\n echo \"[manager] WARNING: Failed to send welcome message (HTTP ${_http_code}): ${_send_resp}\"\n fi\n ) &\n log \"Welcome message background process started (PID: $!)\"\n fi\nfi\nfi # end K8s mode skip for admin DM room\n\n# ============================================================\n# Generate Manager Agent openclaw.json from template\n# ============================================================\nlog \"Generating Manager openclaw.json...\"\nexport MANAGER_MATRIX_TOKEN=\"${MANAGER_TOKEN}\"\nexport MANAGER_GATEWAY_KEY=\"${HICLAW_MANAGER_GATEWAY_KEY}\"\n# Resolve model parameters based on model name\nMODEL_NAME=\"${HICLAW_DEFAULT_MODEL:-qwen3.6-plus}\"\ncase \"${MODEL_NAME}\" in\n gpt-5.3-codex|gpt-5-mini|gpt-5-nano)\n export MODEL_CONTEXT_WINDOW=400000 MODEL_MAX_TOKENS=128000 ;;\n claude-opus-4-6)\n export MODEL_CONTEXT_WINDOW=1000000 MODEL_MAX_TOKENS=128000 ;;\n claude-sonnet-4-6)\n export MODEL_CONTEXT_WINDOW=1000000 MODEL_MAX_TOKENS=64000 ;;\n claude-haiku-4-5)\n export MODEL_CONTEXT_WINDOW=200000 MODEL_MAX_TOKENS=64000 ;;\n qwen3.6-plus|qwen3.5-plus)\n export MODEL_CONTEXT_WINDOW=200000 MODEL_MAX_TOKENS=64000 ;;\n deepseek-chat|deepseek-reasoner|kimi-k2.5)\n export MODEL_CONTEXT_WINDOW=256000 MODEL_MAX_TOKENS=128000 ;;\n glm-5|MiniMax-M2.7|MiniMax-M2.7-highspeed|MiniMax-M2.5)\n export MODEL_CONTEXT_WINDOW=200000 MODEL_MAX_TOKENS=128000 ;;\n *)\n export MODEL_CONTEXT_WINDOW=150000 MODEL_MAX_TOKENS=128000 ;;\nesac\nexport MODEL_REASONING=true\n\n# Override with user-supplied custom model parameters from env (set during install)\n[ -n \"${HICLAW_MODEL_CONTEXT_WINDOW:-}\" ] && export MODEL_CONTEXT_WINDOW=\"${HICLAW_MODEL_CONTEXT_WINDOW}\"\n[ -n \"${HICLAW_MODEL_MAX_TOKENS:-}\" ] && export MODEL_MAX_TOKENS=\"${HICLAW_MODEL_MAX_TOKENS}\"\n[ -n \"${HICLAW_MODEL_REASONING:-}\" ] && export MODEL_REASONING=\"${HICLAW_MODEL_REASONING}\"\n\n# E2EE: convert HICLAW_MATRIX_E2EE to JSON boolean for template substitution\nif [ \"${HICLAW_MATRIX_E2EE:-0}\" = \"1\" ] || [ \"${HICLAW_MATRIX_E2EE:-}\" = \"true\" ]; then\n export MATRIX_E2EE_ENABLED=true\nelse\n export MATRIX_E2EE_ENABLED=false\nfi\nlog \"Matrix E2EE: ${MATRIX_E2EE_ENABLED}\"\n\n# Resolve input modalities: only vision-capable models get \"image\"\ncase \"${MODEL_NAME}\" in\n gpt-5.4|gpt-5.3-codex|gpt-5-mini|gpt-5-nano|claude-opus-4-6|claude-sonnet-4-6|claude-haiku-4-5|qwen3.6-plus|qwen3.5-plus|kimi-k2.5)\n export MODEL_INPUT='[\"text\", \"image\"]' ;;\n *)\n export MODEL_INPUT='[\"text\"]' ;;\nesac\n# Override with user-supplied vision setting from env\nif [ \"${HICLAW_MODEL_VISION:-}\" = \"true\" ]; then\n export MODEL_INPUT='[\"text\", \"image\"]'\nelif [ \"${HICLAW_MODEL_VISION:-}\" = \"false\" ]; then\n export MODEL_INPUT='[\"text\"]'\nfi\n\nlog \"Model: ${MODEL_NAME} (context=${MODEL_CONTEXT_WINDOW}, maxTokens=${MODEL_MAX_TOKENS}, reasoning=${MODEL_REASONING}, input=${MODEL_INPUT})\"\n\nif [ -f /root/manager-workspace/openclaw.json ]; then\n log \"Manager openclaw.json already exists, updating dynamic fields only (preserving user customizations)...\"\n # Merge known models into existing config (add missing, preserve user-added)\n # Use known-models.json (valid JSON) instead of template (contains ${VAR} placeholders)\n KNOWN_MODELS=$(cat /opt/hiclaw/configs/known-models.json 2>/dev/null || echo '[]')\n jq --arg token \"${MANAGER_TOKEN}\" \\\n --arg key \"${HICLAW_MANAGER_GATEWAY_KEY}\" \\\n --arg model \"${MODEL_NAME}\" \\\n --arg emb_model \"${HICLAW_EMBEDDING_MODEL}\" \\\n --arg aigw_domain \"${AI_GATEWAY_DOMAIN}\" \\\n --arg matrix_user_id \"@manager:${MATRIX_DOMAIN}\" \\\n --argjson e2ee \"${MATRIX_E2EE_ENABLED}\" \\\n --argjson known_models \"${KNOWN_MODELS}\" \\\n --argjson ctx \"${MODEL_CONTEXT_WINDOW}\" \\\n --argjson max \"${MODEL_MAX_TOKENS}\" \\\n --argjson reasoning \"${MODEL_REASONING}\" \\\n --argjson input \"${MODEL_INPUT}\" \\\n '\n # Merge known models: add any model id not already present\n .models.providers[\"hiclaw-gateway\"].models as $existing\n | ($existing | map(.id)) as $existing_ids\n | ($known_models | map(select(.id as $id | $existing_ids | index($id) | not))) as $new\n | .models.providers[\"hiclaw-gateway\"].models = ($existing + $new)\n # Ensure the user-chosen default model is in the list (custom model support)\n | if (.models.providers[\"hiclaw-gateway\"].models | map(.id) | index($model) | not) then\n .models.providers[\"hiclaw-gateway\"].models += [{\"id\": $model, \"name\": $model, \"reasoning\": $reasoning, \"contextWindow\": $ctx, \"maxTokens\": $max, \"input\": $input}]\n else . end\n # Rebuild model aliases from the full models list\n | (.models.providers[\"hiclaw-gateway\"].models | map({ (\"hiclaw-gateway/\" + .id): { \"alias\": .id } }) | add // {}) as $aliases\n | .agents.defaults.models = ((.agents.defaults.models // {}) + $aliases)\n | .channels.matrix.accessToken = $token | .channels.matrix.userId = $matrix_user_id | .models.providers[\"hiclaw-gateway\"].apiKey = $key\n | ((.hooks.token // \"\") as $ht | if $ht == $key or $ht == ($key + \"-hooks\" | @base64) then del(.hooks) else . end)\n | .agents.defaults.model.primary = (\"hiclaw-gateway/\" + $model)\n | .commands.restart = true\n | .gateway.port = 18799\n | .gateway.bind = \"lan\"\n | .gateway.controlUi = ((.gateway.controlUi // {}) + {\"dangerouslyDisableDeviceAuth\": true, \"allowInsecureAuth\": true, \"allowedOrigins\": [\"*\"]})\n | .channels.matrix.encryption = $e2ee\n | .channels.matrix.network = ((.channels.matrix.network // {}) + {\"dangerouslyAllowPrivateNetwork\": true})\n | .channels.matrix.autoJoin = \"always\"\n # OpenClaw YOLO defaults: host exec without approval prompts (see openclaw docs tools/exec-approvals)\n | .tools = (.tools // {})\n | .tools.exec = ((.tools.exec // {}) + {\"host\":\"gateway\",\"security\":\"full\",\"ask\":\"off\"})\n | .tools.elevated = (.tools.elevated // {})\n | .tools.elevated.enabled = true\n | .tools.elevated.allowFrom |= ((. // {}) | .matrix = [\"*\"])\n | .agents.defaults.elevatedDefault = \"full\"\n # Ensure memorySearch config exists (embedding model for memory) — skip if embedding model is empty\n | if $emb_model != \"\" then .agents.defaults.memorySearch //= {\"provider\":\"openai\",\"model\":$emb_model,\"remote\":{\"baseUrl\":(\"http://\" + $aigw_domain + \":8080/v1\"),\"apiKey\":$key}} else . end\n ' \\\n /root/manager-workspace/openclaw.json > /tmp/openclaw.json.tmp && \\\n mv /tmp/openclaw.json.tmp /root/manager-workspace/openclaw.json\n # Disable openclaw's observe-recovery mechanism which compares config against\n # a lastKnownGood baseline in config-health.json. When meta is missing from the\n # current file but present in the baseline, observe-recovery restores from .bak,\n # undoing user customizations (plugins, channels, etc).\n # Clearing config-health.json removes the baseline so observe-recovery won't\n # interfere, while preserving .bak as a backup.\n rm -f /root/manager-workspace/.openclaw/logs/config-health.json\n # Verify the token was written correctly\n _written_token=$(jq -r '.channels.matrix.accessToken' /root/manager-workspace/openclaw.json 2>/dev/null)\n if [ -z \"${_written_token}\" ] || [ \"${_written_token}\" = \"null\" ]; then\n log \"ERROR: Matrix token was not written correctly to openclaw.json (got: ${_written_token})\"\n else\n log \"Matrix token written to openclaw.json (prefix: ${_written_token:0:10}...)\"\n fi\nelse\n log \"Manager openclaw.json not found, generating from template...\"\n envsubst < /opt/hiclaw/configs/manager-openclaw.json.tmpl > /root/manager-workspace/openclaw.json\n # Post-envsubst injection: memorySearch + custom model (single jq pass when possible)\n if ! jq -e --arg model \"${MODEL_NAME}\" '.models.providers[\"hiclaw-gateway\"].models | map(.id) | index($model)' /root/manager-workspace/openclaw.json > /dev/null 2>&1; then\n log \"Custom model '${MODEL_NAME}' not in built-in list, injecting into config...\"\n jq --arg emb_model \"${HICLAW_EMBEDDING_MODEL}\" \\\n --arg aigw_domain \"${AI_GATEWAY_DOMAIN}\" \\\n --arg key \"${HICLAW_MANAGER_GATEWAY_KEY}\" \\\n --arg model \"${MODEL_NAME}\" \\\n --argjson ctx \"${MODEL_CONTEXT_WINDOW}\" \\\n --argjson max \"${MODEL_MAX_TOKENS}\" \\\n --argjson reasoning \"${MODEL_REASONING}\" \\\n --argjson input \"${MODEL_INPUT}\" \\\n '\n (if $emb_model != \"\" then .agents.defaults.memorySearch = {\"provider\":\"openai\",\"model\":$emb_model,\"remote\":{\"baseUrl\":(\"http://\" + $aigw_domain + \":8080/v1\"),\"apiKey\":$key}} else . end)\n | .models.providers[\"hiclaw-gateway\"].models += [{\"id\": $model, \"name\": $model, \"reasoning\": $reasoning, \"contextWindow\": $ctx, \"maxTokens\": $max, \"input\": $input}]\n | .agents.defaults.models += {(\"hiclaw-gateway/\" + $model): {\"alias\": $model}}\n ' /root/manager-workspace/openclaw.json > /tmp/openclaw.json.tmp && \\\n mv /tmp/openclaw.json.tmp /root/manager-workspace/openclaw.json\n elif [ -n \"${HICLAW_EMBEDDING_MODEL}\" ]; then\n jq --arg emb_model \"${HICLAW_EMBEDDING_MODEL}\" \\\n --arg aigw_domain \"${AI_GATEWAY_DOMAIN}\" \\\n --arg key \"${HICLAW_MANAGER_GATEWAY_KEY}\" \\\n '.agents.defaults.memorySearch = {\"provider\":\"openai\",\"model\":$emb_model,\"remote\":{\"baseUrl\":(\"http://\" + $aigw_domain + \":8080/v1\"),\"apiKey\":$key}}' \\\n /root/manager-workspace/openclaw.json > /tmp/openclaw.json.tmp && \\\n mv /tmp/openclaw.json.tmp /root/manager-workspace/openclaw.json\n fi\n _written_token=$(jq -r '.channels.matrix.accessToken' /root/manager-workspace/openclaw.json 2>/dev/null)\n log \"Matrix token written from template (prefix: ${_written_token:0:10}...)\"\nfi\n\n# Cloud/K8s mode: overlay cloud-specific settings onto generated config\nif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ] || [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n log \"Applying cloud/k8s overlay to openclaw.json...\"\n jq --arg homeserver \"${HICLAW_MATRIX_URL}\" \\\n --arg gateway \"${HICLAW_AI_GATEWAY_URL}/v1\" \\\n --arg key \"${HICLAW_MANAGER_GATEWAY_KEY}\" \\\n '.channels.matrix.homeserver = $homeserver\n | .models.providers[\"hiclaw-gateway\"].baseUrl = $gateway\n | .models.providers[\"hiclaw-gateway\"].apiKey = $key\n | ((.hooks.token // \"\") as $ht | if $ht == $key or $ht == ($key + \"-hooks\" | @base64) then del(.hooks) else . end)\n | .commands.restart = false\n | if .agents.defaults.memorySearch then .agents.defaults.memorySearch.remote.baseUrl = $gateway | .agents.defaults.memorySearch.remote.apiKey = $key else . end' \\\n /root/manager-workspace/openclaw.json > /tmp/openclaw-cloud.json && \\\n mv /tmp/openclaw-cloud.json /root/manager-workspace/openclaw.json\n log \"Cloud/K8s overlay applied\"\nfi\n\n# ============================================================\n# Optional: enable openclaw-cms-plugin observability\n# Config is applied at runtime so secrets stay out of image layers.\n# ============================================================\nCMS_TRACES_ENABLED=\"$(echo \"${HICLAW_CMS_TRACES_ENABLED:-false}\" | tr '[:upper:]' '[:lower:]')\"\nif [ \"${CMS_TRACES_ENABLED}\" = \"true\" ]; then\n CMS_PLUGIN_NAME=\"openclaw-cms-plugin\"\n CMS_PLUGIN_DIR=\"${OPENCLAW_CMS_PLUGIN_DIR:-/opt/openclaw/extensions/openclaw-cms-plugin}\"\n CMS_PLUGIN_MANIFEST=\"${CMS_PLUGIN_DIR}/openclaw.plugin.json\"\n DIAG_PLUGIN_NAME=\"diagnostics-otel\"\n DIAG_PLUGIN_DIR=\"/opt/openclaw/extensions/diagnostics-otel\"\n CMS_LICENSE_KEY=\"${HICLAW_CMS_LICENSE_KEY:-}\"\n CMS_PROJECT=\"${HICLAW_CMS_PROJECT:-}\"\n CMS_METRICS_ENABLED=\"${HICLAW_CMS_METRICS_ENABLED:-false}\"\n\n if [ ! -f \"${CMS_PLUGIN_MANIFEST}\" ]; then\n log \"WARNING: ${CMS_PLUGIN_NAME} manifest not found at ${CMS_PLUGIN_MANIFEST}, skipping plugin config\"\n else\n _missing=0\n [ -z \"${HICLAW_CMS_ENDPOINT:-}\" ] && log \"WARNING: HICLAW_CMS_ENDPOINT is required when HICLAW_CMS_TRACES_ENABLED=true\" && _missing=1\n [ -z \"${CMS_LICENSE_KEY:-}\" ] && log \"WARNING: HICLAW_CMS_LICENSE_KEY is required when HICLAW_CMS_TRACES_ENABLED=true\" && _missing=1\n [ -z \"${HICLAW_CMS_WORKSPACE:-}\" ] && log \"WARNING: HICLAW_CMS_WORKSPACE is required when HICLAW_CMS_TRACES_ENABLED=true\" && _missing=1\n\n if [ \"${_missing}\" = \"0\" ]; then\n CMS_SERVICE_NAME=\"${HICLAW_CMS_SERVICE_NAME:-hiclaw-manager}\"\n CMS_ENABLE_METRICS=\"${CMS_METRICS_ENABLED}\"\n DIAG_AVAILABLE=\"0\"\n _metrics_lc=\"$(echo \"${CMS_ENABLE_METRICS}\" | tr '[:upper:]' '[:lower:]')\"\n if [ \"${_metrics_lc}\" = \"true\" ]; then\n if [ -f \"${DIAG_PLUGIN_DIR}/package.json\" ]; then\n DIAG_AVAILABLE=\"1\"\n if [ ! -d \"${DIAG_PLUGIN_DIR}/node_modules\" ]; then\n log \"diagnostics-otel dependencies missing, installing...\"\n if (cd \"${DIAG_PLUGIN_DIR}\" && npm install --omit=dev --ignore-scripts >/tmp/hiclaw-diag-install.log 2>&1); then\n log \"diagnostics-otel dependencies installed\"\n else\n log \"WARNING: diagnostics-otel npm install failed, metrics plugin may not load\"\n fi\n else\n log \"diagnostics-otel dependencies already present\"\n fi\n else\n log \"WARNING: diagnostics-otel package.json not found at ${DIAG_PLUGIN_DIR}, metrics plugin may not load\"\n fi\n fi\n\n log \"Applying ${CMS_PLUGIN_NAME} config to openclaw.json...\"\n jq --arg pluginName \"${CMS_PLUGIN_NAME}\" \\\n --arg pluginDir \"${CMS_PLUGIN_DIR}\" \\\n --arg endpoint \"${HICLAW_CMS_ENDPOINT}\" \\\n --arg licenseKey \"${CMS_LICENSE_KEY}\" \\\n --arg armsProject \"${CMS_PROJECT}\" \\\n --arg cmsWorkspace \"${HICLAW_CMS_WORKSPACE}\" \\\n --arg serviceName \"${CMS_SERVICE_NAME}\" \\\n --arg diagPluginName \"${DIAG_PLUGIN_NAME}\" \\\n --arg diagPluginDir \"${DIAG_PLUGIN_DIR}\" \\\n --arg metricsRaw \"${CMS_ENABLE_METRICS}\" \\\n --arg diagAvailableRaw \"${DIAG_AVAILABLE}\" \\\n '\n .plugins = (.plugins // {})\n | .plugins.load = (.plugins.load // {})\n | .plugins.entries = (.plugins.entries // {})\n | if (.plugins.allow | type) != \"array\" then .plugins.allow = [] else . end\n | if (.plugins.allow | index($pluginName)) == null then .plugins.allow += [$pluginName] else . end\n | if (.plugins.load.paths | type) != \"array\" then .plugins.load.paths = [] else . end\n | if (.plugins.load.paths | index($pluginDir)) == null then .plugins.load.paths += [$pluginDir] else . end\n | .plugins.entries[$pluginName] = {\n \"enabled\": true,\n \"config\": {\n \"endpoint\": $endpoint,\n \"headers\": {\n \"x-arms-license-key\": $licenseKey,\n \"x-arms-project\": $armsProject,\n \"x-cms-workspace\": $cmsWorkspace\n },\n \"serviceName\": $serviceName\n }\n }\n\n # diagnostics-otel metrics (optional)\n | ($metricsRaw | ascii_downcase) as $m\n | ($diagAvailableRaw == \"1\") as $diagAvailable\n | (($m == \"true\") and $diagAvailable) as $metricsEnabled\n | if $metricsEnabled then\n (if (.plugins.allow | index($diagPluginName)) == null then .plugins.allow += [$diagPluginName] else . end)\n | (if (.plugins.load.paths | index($diagPluginDir)) == null then .plugins.load.paths += [$diagPluginDir] else . end)\n | .plugins.entries[$diagPluginName].enabled = true\n | .diagnostics = (.diagnostics // {})\n | .diagnostics.otel = (.diagnostics.otel // {})\n | .diagnostics.enabled = true\n | .diagnostics.otel.enabled = true\n | .diagnostics.otel.endpoint = $endpoint\n | .diagnostics.otel.protocol = (.diagnostics.otel.protocol // \"http/protobuf\")\n | .diagnostics.otel.headers = {\n \"x-arms-license-key\": $licenseKey,\n \"x-arms-project\": $armsProject,\n \"x-cms-workspace\": $cmsWorkspace\n }\n | .diagnostics.otel.serviceName = $serviceName\n | .diagnostics.otel.metrics = true\n | .diagnostics.otel.traces = (.diagnostics.otel.traces // false)\n | .diagnostics.otel.logs = (.diagnostics.otel.logs // false)\n else\n .\n end\n ' /root/manager-workspace/openclaw.json > /tmp/openclaw-cms.json && \\\n mv /tmp/openclaw-cms.json /root/manager-workspace/openclaw.json\n log \"${CMS_PLUGIN_NAME} config applied (metrics=${CMS_ENABLE_METRICS}, service=${CMS_SERVICE_NAME})\"\n else\n log \"Skipping ${CMS_PLUGIN_NAME} config due to missing required env vars\"\n fi\n fi\nfi\n\n# ============================================================\n# Detect container runtime (for Worker creation)\n# ============================================================\nsource /opt/hiclaw/scripts/lib/container-api.sh\nif container_api_available; then\n log \"Container runtime socket detected at ${CONTAINER_SOCKET} — direct Worker creation enabled\"\n export HICLAW_CONTAINER_RUNTIME=\"socket\"\nelif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ] || [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n log \"Cloud/K8s mode — Workers created via controller API\"\n export HICLAW_CONTAINER_RUNTIME=\"cloud\"\nelse\n log \"No container runtime found — Worker creation will output install commands\"\n export HICLAW_CONTAINER_RUNTIME=\"none\"\nfi\n\n# ============================================================\n# Upgrade Worker openclaw.json: merge known models + E2EE flag into existing configs\n# Existing workers in MinIO may have old single-model configs or missing encryption field.\n# Merge template models so they can hot-switch without restart.\n# ============================================================\nREGISTRY_FILE=\"/root/manager-workspace/workers-registry.json\"\nif [ -f \"${REGISTRY_FILE}\" ]; then\n # Use known-models.json (valid JSON) instead of template (contains ${VAR} placeholders)\n KNOWN_MODELS_FILE=\"/opt/hiclaw/configs/known-models.json\"\n if [ -f \"${KNOWN_MODELS_FILE}\" ]; then\n _KNOWN_MODELS=$(cat \"${KNOWN_MODELS_FILE}\")\n for _wname in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_wname}\" ] && continue\n _minio_path=\"${HICLAW_STORAGE_PREFIX}/agents/${_wname}/openclaw.json\"\n _tmp_in=\"/tmp/openclaw-${_wname}-models-upgrade-in.json\"\n if mc cp \"${_minio_path}\" \"${_tmp_in}\" 2>/dev/null; then\n _tmp_out=\"/tmp/openclaw-${_wname}-models-upgrade-out.json\"\n # Idempotent merge: add missing known models, rebuild aliases, set e2ee.\n # Always runs — jq deduplicates by model id, so re-runs are safe.\n jq --argjson known_models \"${_KNOWN_MODELS}\" \\\n --argjson e2ee \"${MATRIX_E2EE_ENABLED}\" '\n .models.providers[\"hiclaw-gateway\"].models as $existing\n | ($existing | map(.id)) as $existing_ids\n | ($known_models | map(select(.id as $id | $existing_ids | index($id) | not))) as $new\n | .models.providers[\"hiclaw-gateway\"].models = ($existing + $new)\n | (.models.providers[\"hiclaw-gateway\"].models | map({ (\"hiclaw-gateway/\" + .id): { \"alias\": .id } }) | add // {}) as $aliases\n | .agents.defaults.models = ((.agents.defaults.models // {}) + $aliases)\n | .channels.matrix.encryption = $e2ee\n | .channels.matrix.autoJoin = \"always\"\n | .tools = (.tools // {})\n | .tools.exec = ((.tools.exec // {}) + {\"host\":\"gateway\",\"security\":\"full\",\"ask\":\"off\"})\n | .tools.elevated = (.tools.elevated // {})\n | .tools.elevated.enabled = true\n | .tools.elevated.allowFrom |= ((. // {}) | .matrix = [\"*\"])\n | .agents.defaults.elevatedDefault = \"full\"\n ' \"${_tmp_in}\" > \"${_tmp_out}\" 2>/dev/null\n if ! diff -q \"${_tmp_in}\" \"${_tmp_out}\" > /dev/null 2>&1; then\n if mc cp \"${_tmp_out}\" \"${_minio_path}\" 2>/dev/null; then\n _new_count=$(jq '.models.providers[\"hiclaw-gateway\"].models | length' \"${_tmp_out}\" 2>/dev/null)\n log \"Worker ${_wname}: upgraded openclaw.json (models: ${_new_count}, e2ee: ${MATRIX_E2EE_ENABLED})\"\n fi\n fi\n rm -f \"${_tmp_in}\" \"${_tmp_out}\"\n fi\n done\n fi\nfi\n\n# ============================================================\n# Ensure Worker Matrix password files exist in MinIO (E2EE fix)\n# Workers need to re-login on restart to get a fresh device_id.\n# Older workers created before this fix won't have the password file.\n# ============================================================\nif [ -f \"${REGISTRY_FILE}\" ]; then\n for _wname in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_wname}\" ] && continue\n _creds_file=\"/data/worker-creds/${_wname}.env\"\n if [ -f \"${_creds_file}\" ]; then\n # Check if password file already exists in MinIO\n if ! mc stat \"${HICLAW_STORAGE_PREFIX}/agents/${_wname}/credentials/matrix/password\" > /dev/null 2>&1; then\n source \"${_creds_file}\"\n if [ -n \"${WORKER_PASSWORD}\" ]; then\n _tmp_pw=\"/tmp/matrix-pw-${_wname}\"\n echo -n \"${WORKER_PASSWORD}\" > \"${_tmp_pw}\"\n mc cp \"${_tmp_pw}\" \"${HICLAW_STORAGE_PREFIX}/agents/${_wname}/credentials/matrix/password\" 2>/dev/null \\\n && log \"Worker ${_wname}: wrote Matrix password to MinIO (E2EE re-login fix)\" \\\n || log \"Worker ${_wname}: WARNING: failed to write Matrix password to MinIO\"\n rm -f \"${_tmp_pw}\"\n fi\n fi\n fi\n done\nfi\n\n# ============================================================\n# Recreate Worker containers as needed after Manager restart.\n# Workers are on hiclaw-net; Docker DNS resolves *-local.hiclaw.io via\n# the Manager's network aliases, so IP changes don't require worker recreation.\n# Only recreate stopped/missing workers.\n# ============================================================\nif container_api_available; then\n REGISTRY_FILE=\"/root/manager-workspace/workers-registry.json\"\n if [ -f \"${REGISTRY_FILE}\" ]; then\n for _worker_name in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_worker_name}\" ] && continue\n\n # Skip remote workers — they are not Manager-managed containers.\n _deployment=$(jq -r --arg w \"${_worker_name}\" '.workers[$w].deployment // \"local\"' \"${REGISTRY_FILE}\" 2>/dev/null)\n if [ \"${_deployment}\" = \"remote\" ]; then\n log \"Worker ${_worker_name} is remote, skipping container recreate\"\n continue\n fi\n\n _status=$(container_status_worker \"${_worker_name}\")\n if [ \"${_status}\" = \"running\" ]; then\n log \"Worker running: ${_worker_name}, skipping\"\n continue\n fi\n # Container missing or stopped — recreate.\n log \"Worker container ${_status}: ${_worker_name}, recreating...\"\n _creds_file=\"/data/worker-creds/${_worker_name}.env\"\n if [ -f \"${_creds_file}\" ]; then\n source \"${_creds_file}\"\n _runtime=$(jq -r --arg w \"${_worker_name}\" '.workers[$w].runtime // \"openclaw\"' \"${REGISTRY_FILE}\" 2>/dev/null)\n _recreated=false\n for _attempt in 1 2 3; do\n local _env_map _create_body\n _env_map=$(jq -cn \\\n --arg name \"${_worker_name}\" \\\n --arg fak \"${_worker_name}\" \\\n --arg fsk \"${WORKER_MINIO_PASSWORD:-}\" \\\n --arg fs_domain \"${HICLAW_FS_DOMAIN:-fs-local.hiclaw.io}\" \\\n --arg controller_url \"${HICLAW_CONTROLLER_URL:-}\" \\\n '{\n \"HICLAW_WORKER_NAME\": $name,\n \"HICLAW_FS_ENDPOINT\": (\"http://\" + ($fs_domain | split(\":\")[0]) + \":9000\"),\n \"HICLAW_FS_ACCESS_KEY\": $fak,\n \"HICLAW_FS_SECRET_KEY\": $fsk\n }\n | if $controller_url != \"\" then . + {\"HICLAW_CONTROLLER_URL\": $controller_url} else . end')\n _create_body=$(jq -cn --arg name \"${_worker_name}\" --arg runtime \"${_runtime}\" --argjson env \"${_env_map}\" '{name: $name, runtime: $runtime, env: $env}')\n worker_backend_create \"${_create_body}\" > /dev/null 2>&1 && _recreated=true && break\n log \" Attempt ${_attempt}/3 failed for ${_worker_name}, retrying in $((5 * _attempt))s...\"\n sleep $((5 * _attempt))\n done\n if [ \"${_recreated}\" = true ]; then\n log \" Recreated ${_runtime} worker: ${_worker_name}\"\n else\n log \" ERROR: Failed to recreate ${_worker_name} after 3 attempts\"\n fi\n else\n log \" WARNING: No credentials found for ${_worker_name} (${_creds_file} missing), skipping\"\n fi\n done\n fi\nfi\n\n# ============================================================\n# Notify workers of builtin updates if upgrade happened\n# Builtin files (AGENTS.md, skills) are already synced by upgrade-builtins.sh\n#\n# Cooldown: skip notification if the last successful notify was within\n# NOTIFY_COOLDOWN_SECS (default 3600s / 1 hour). This prevents repeated\n# notifications when the Manager crash-loops and re-runs upgrade-builtins\n# on every restart (e.g. IMAGE_VERSION=latest always triggers upgrade).\n# ============================================================\nNOTIFY_COOLDOWN_SECS=\"${HICLAW_NOTIFY_COOLDOWN_SECS:-3600}\"\nNOTIFY_TS_FILE=\"/root/manager-workspace/.last-worker-notify-ts\"\n\nif [ -f /root/manager-workspace/.upgrade-pending-worker-notify ]; then\n _now=$(date +%s)\n _last_notify=$(cat \"${NOTIFY_TS_FILE}\" 2>/dev/null || echo \"0\")\n _elapsed=$(( _now - _last_notify ))\n\n if [ \"${_elapsed}\" -lt \"${NOTIFY_COOLDOWN_SECS}\" ]; then\n log \"Skipping worker builtin notification (last notify ${_elapsed}s ago, cooldown ${NOTIFY_COOLDOWN_SECS}s)\"\n rm -f /root/manager-workspace/.upgrade-pending-worker-notify\n else\n log \"Notifying workers about builtin updates...\"\n REGISTRY_FILE=\"/root/manager-workspace/workers-registry.json\"\n _notify_ok=false\n if [ -f \"${REGISTRY_FILE}\" ]; then\n for _worker_name in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_worker_name}\" ] && continue\n _room_id=$(jq -r --arg w \"${_worker_name}\" '.workers[$w].room_id // empty' \"${REGISTRY_FILE}\" 2>/dev/null)\n if [ -n \"${_room_id}\" ]; then\n _worker_id=\"@${_worker_name}:${MATRIX_DOMAIN}\"\n _txn_id=\"upgrade-$(date +%s%N)\"\n _msg=\"@${_worker_name}:${MATRIX_DOMAIN} Manager upgraded builtin files (AGENTS.md, skills). Please use your file-sync skill to sync the latest config.\"\n _raw=$(curl -s -w '\\nHTTP_CODE:%{http_code}' -X PUT \\\n \"${HICLAW_MATRIX_URL}/_matrix/client/v3/rooms/${_room_id}/send/m.room.message/${_txn_id}\" \\\n -H \"Authorization: Bearer ${MANAGER_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d \"{\\\"msgtype\\\":\\\"m.text\\\",\\\"body\\\":\\\"${_msg}\\\",\\\"m.mentions\\\":{\\\"user_ids\\\":[\\\"${_worker_id}\\\"]}}\" \\\n 2>&1) || true\n _http_code=$(echo \"${_raw}\" | tail -1 | sed 's/HTTP_CODE://')\n _notify_resp=$(echo \"${_raw}\" | sed '$d')\n if echo \"${_notify_resp}\" | jq -e '.event_id' > /dev/null 2>&1; then\n log \" Notified ${_worker_name}\"; _notify_ok=true\n else\n log \" WARNING: Failed to notify ${_worker_name} (HTTP ${_http_code}): ${_notify_resp}\"\n fi\n fi\n done\n fi\n # Record timestamp only if at least one notification succeeded\n if [ \"${_notify_ok}\" = true ]; then\n echo \"${_now}\" > \"${NOTIFY_TS_FILE}\"\n fi\n rm -f /root/manager-workspace/.upgrade-pending-worker-notify\n fi\nfi\n\n# ============================================================\n# Start Manager Agent\n# ============================================================\nlog \"Starting Manager Agent (${MANAGER_RUNTIME})...\"\n\n# HOME is already set to /root/manager-workspace via docker run -e HOME=...\ncd \"${HOME}\"\n\n# Ensure host credential symlinks exist under HOME\nif [ -d \"/host-share\" ]; then\n [ -f \"/host-share/.gitconfig\" ] && ln -sf \"/host-share/.gitconfig\" \"${HOME}/.gitconfig\"\nfi\n\nlog \"HOME=${HOME} (manager-workspace, host-mounted)\"\n\n# ── Render agent doc templates ────────────────────────────────────────────\n# Replace ${VAR} placeholders with actual values so the AI agent reads\n# plain text and never needs to resolve environment variables.\nexport MANAGER_MATRIX_TOKEN MANAGER_TOKEN HIGRESS_COOKIE_FILE\nRENDER=/opt/hiclaw/scripts/lib/render-skills.sh\nlog \"Rendering agent doc templates...\"\n# Manager-owned docs (workspace)\nbash \"$RENDER\" /root/manager-workspace/skills\nbash \"$RENDER\" /root/manager-workspace/skills-alpha\nbash \"$RENDER\" /root/manager-workspace AGENTS.md TOOLS.md HEARTBEAT.md SOUL.md\n# Worker templates (workspace + image) — rendered before push to MinIO\n# so Workers (including remote pip-install) receive plain text\nbash \"$RENDER\" /root/manager-workspace/worker-skills\nbash \"$RENDER\" /root/manager-workspace/worker-agent\nbash \"$RENDER\" /root/manager-workspace/copaw-worker-agent\nbash \"$RENDER\" /root/manager-workspace/hermes-worker-agent\nbash \"$RENDER\" /opt/hiclaw/agent/worker-skills\nbash \"$RENDER\" /opt/hiclaw/agent/worker-agent\nbash \"$RENDER\" /opt/hiclaw/agent/copaw-worker-agent\nbash \"$RENDER\" /opt/hiclaw/agent/hermes-worker-agent\nlog \"Agent doc templates rendered\"\n\n# Cloud mode: start background file sync (workspace ↔ OSS) and initial push\nif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n log \"Syncing initial workspace to OSS...\"\n ensure_mc_credentials\n mc mirror /root/manager-workspace/ \"${HICLAW_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" 2>/dev/null || true\n\n # Local → OSS: change-triggered sync\n (\n while true; do\n CHANGED=$(find /root/manager-workspace/ -type f -newermt \"15 seconds ago\" 2>/dev/null | head -1)\n if [ -n \"${CHANGED}\" ]; then\n ensure_mc_credentials 2>/dev/null || true\n mc mirror /root/manager-workspace/ \"${HICLAW_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" --exclude \".npm/**\" \\\n --exclude \".local/**\" --exclude \".mc/**\" 2>/dev/null || true\n fi\n sleep 10\n done\n ) &\n log \"Local→OSS sync started (PID: $!)\"\n\n # OSS → Local: periodic pull (shared data, agent configs)\n (\n while true; do\n sleep 300\n ensure_mc_credentials 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/shared/\" /root/hiclaw-fs/shared/ --overwrite --newer-than \"5m\" 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/agents/\" /root/hiclaw-fs/agents/ --overwrite --newer-than \"5m\" 2>/dev/null || true\n done\n ) &\n log \"OSS→Local sync started (every 5m, PID: $!)\"\nfi\n\n# K8s mode: start background file sync (workspace ↔ MinIO)\nif [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n log \"Syncing initial workspace to MinIO...\"\n mc mirror /root/manager-workspace/ \"${HICLAW_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" 2>/dev/null || true\n\n # Local → MinIO: change-triggered sync\n (\n while true; do\n CHANGED=$(find /root/manager-workspace/ -type f -newermt \"15 seconds ago\" 2>/dev/null | head -1)\n if [ -n \"${CHANGED}\" ]; then\n mc mirror /root/manager-workspace/ \"${HICLAW_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" --exclude \".npm/**\" \\\n --exclude \".local/**\" --exclude \".mc/**\" 2>/dev/null || true\n fi\n sleep 10\n done\n ) &\n log \"Local→MinIO sync started (PID: $!)\"\n\n # MinIO → Local: periodic pull (shared data, agent configs)\n (\n while true; do\n sleep 300\n mc mirror \"${HICLAW_STORAGE_PREFIX}/shared/\" /root/hiclaw-fs/shared/ --overwrite --newer-than \"5m\" 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/agents/\" /root/hiclaw-fs/agents/ --overwrite --newer-than \"5m\" 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/hiclaw-config/\" /root/hiclaw-fs/hiclaw-config/ --overwrite --newer-than \"15s\" 2>/dev/null || true\n done\n ) &\n log \"MinIO→Local sync started (every 5m, PID: $!)\"\nfi\n\n# ============================================================\n# Auto-generate Manager mcporter config for pre-configured MCP servers\n# If HICLAW_GITHUB_TOKEN was set at install time, setup-higress.sh already\n# configured GitHub MCP on Higress. Run setup-mcp-server.sh now so that\n# config/mcporter.json exists when the Agent starts — no need to ask user for PAT.\n# ============================================================\nif [ -n \"${HICLAW_GITHUB_TOKEN}\" ] && [ \"${HICLAW_RUNTIME}\" != \"aliyun\" ] && [ \"${HICLAW_RUNTIME}\" != \"k8s\" ]; then\n if [ ! -f \"${HOME}/config/mcporter.json\" ]; then\n log \"Auto-generating Manager mcporter config for GitHub MCP (HICLAW_GITHUB_TOKEN set)...\"\n bash /opt/hiclaw/agent/skills/mcp-server-management/scripts/setup-mcp-server.sh \\\n github \"${HICLAW_GITHUB_TOKEN}\" 2>&1 | while IFS= read -r line; do log \" [setup-mcp] ${line}\"; done || \\\n log \"WARNING: setup-mcp-server.sh failed — Agent may need to configure GitHub MCP manually\"\n else\n log \"Manager mcporter config already exists, skipping auto-generate\"\n fi\nfi\n\n# ============================================================\n# Runtime-specific startup\n# ============================================================\nif [ \"${MANAGER_RUNTIME}\" = \"copaw\" ]; then\n # Delegate to CoPaw startup script\n exec /opt/hiclaw/scripts/init/start-copaw-manager.sh\nelse\n # ── OpenClaw Runtime ─────────────────────────────────────────────────────\n log \"Starting OpenClaw Manager...\"\n\n export OPENCLAW_CONFIG_PATH=\"/root/manager-workspace/openclaw.json\"\n\n # Symlink to default OpenClaw config path so CLI commands find the config\n mkdir -p \"${HOME}/.openclaw\"\n ln -sf \"/root/manager-workspace/openclaw.json\" \"${HOME}/.openclaw/openclaw.json\"\n\n # Clean orphaned session write locks (e.g. from SIGKILL or crash before exit handlers)\n # Prevents \"session file locked (timeout 10000ms)\" when PID was reused\n find \"${HOME}/.openclaw/agents\" -name \"*.jsonl.lock\" -delete 2>/dev/null || true\n log \"Cleaned up any orphaned session write locks\"\n\n # Clean Matrix crypto storage (SQLite WAL may be corrupted after unclean shutdown)\n # Crypto state is re-negotiated on startup; losing it only means re-establishing E2EE sessions\n rm -rf \"${HOME}/.openclaw/matrix\" 2>/dev/null || true\n log \"Cleaned Matrix crypto storage (will re-establish E2EE sessions)\"\n\n # Launch OpenClaw\n # Disable full-process respawn so the CLI uses its internal restart loop.\n # Without this, config reload spawns a detached child and exits, then\n # supervisord restarts the CLI — resulting in two gateway processes.\n export OPENCLAW_NO_RESPAWN=1\n\n # Optional matrix-plugin trace logging — when HICLAW_MATRIX_DEBUG=1 is set\n # in the manager environment (propagated by install / supervisord), turn on\n # OPENCLAW_MATRIX_DEBUG so the matrix plugin emits structured INFO-level\n # lifecycle traces (sync.state transitions, room.invite/join, message\n # handler arrival + filter outcomes). Useful for diagnosing \"worker never\n # joined the room\" / \"manager never replied\" hangs without rebuilding the\n # image.\n if [ \"${HICLAW_MATRIX_DEBUG:-}\" = \"1\" ] && [ -z \"${OPENCLAW_MATRIX_DEBUG:-}\" ]; then\n export OPENCLAW_MATRIX_DEBUG=1\n log \"HICLAW_MATRIX_DEBUG=1 detected; OPENCLAW_MATRIX_DEBUG=1 exported for matrix plugin tracing\"\n fi\n\n exec openclaw gateway run --verbose --force\nfi\n"}, "2eec58748b4872d512a0b85935719386b2700090": {"t": 1785764744.2789733, "n": 68201, "h": "d07039aa0ae9605e", "c": "#!/bin/bash\n# start-manager-agent.sh - Initialize and start the Manager Agent\n# Supports local (supervisord), cloud (SAE), and K8s (Helm) deployments.\n# In local mode this is the last supervisord component to start (priority 800).\n# In cloud/k8s mode (HICLAW_RUNTIME=aliyun|k8s) this is the container entrypoint.\n#\n# Runtime selection:\n# HICLAW_MANAGER_RUNTIME=openclaw (default) - OpenClaw gateway mode\n# HICLAW_MANAGER_RUNTIME=copaw - CoPaw workspace mode\n# (hermes runtime is supported for Workers only; Managers run openclaw or copaw.)\n\nsource /opt/hiclaw/scripts/lib/hiclaw-env.sh\n\n# ============================================================\n# Runtime selection\n# ============================================================\nMANAGER_RUNTIME=\"${HICLAW_MANAGER_RUNTIME:-openclaw}\"\ncase \"${MANAGER_RUNTIME}\" in\n copaw)\n log \"Manager runtime: CoPaw (Python workspace)\"\n ;;\n *)\n log \"Manager runtime: OpenClaw (Node.js gateway)\"\n MANAGER_RUNTIME=\"openclaw\"\n ;;\nesac\n\n# ============================================================\n# Set timezone from TZ env var\n# ============================================================\nif [ -n \"${TZ}\" ] && [ -f \"/usr/share/zoneinfo/${TZ}\" ]; then\n ln -sf \"/usr/share/zoneinfo/${TZ}\" /etc/localtime\n echo \"${TZ}\" > /etc/timezone\n log \"Timezone set to ${TZ}\"\nfi\n\nexport MATRIX_DOMAIN=\"${HICLAW_MATRIX_DOMAIN:-matrix-local.hiclaw.io:8080}\"\nAI_GATEWAY_DOMAIN=\"${HICLAW_AI_GATEWAY_DOMAIN:-aigw-local.hiclaw.io}\"\n\n# ============================================================\n# YOLO mode promotion\n# ============================================================\n# In embedded mode the controller does not propagate HICLAW_YOLO to the\n# manager container, but installer / test scripts touch a marker file at\n# `${WORKSPACE}/yolo-mode` instead. Promote that marker to the env var so the\n# agent's documented YOLO check (`HICLAW_YOLO=1`) reliably detects it without\n# depending on filesystem lookups during a turn.\nif [ -z \"${HICLAW_YOLO:-}\" ] && [ -f /root/manager-workspace/yolo-mode ]; then\n export HICLAW_YOLO=1\n log \"YOLO mode marker detected at /root/manager-workspace/yolo-mode; HICLAW_YOLO=1 exported\"\nfi\n\n# ============================================================\n# Cloud/K8s mode: validate required environment variables + initial credentials\n# ============================================================\nif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ] || [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n : \"${HICLAW_MATRIX_URL:?HICLAW_MATRIX_URL is required}\"\n : \"${HICLAW_MATRIX_DOMAIN:?HICLAW_MATRIX_DOMAIN is required}\"\n : \"${HICLAW_AI_GATEWAY_URL:?HICLAW_AI_GATEWAY_URL is required}\"\n if [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n : \"${HICLAW_MANAGER_GATEWAY_KEY:?HICLAW_MANAGER_GATEWAY_KEY is required}\"\n : \"${HICLAW_MANAGER_PASSWORD:?HICLAW_MANAGER_PASSWORD is required (cloud containers are stateless, password must be injected)}\"\n fi\n if [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n # K8s mode: controller handles initialization (admin registration, Higress setup).\n # Manager only needs credentials injected by the ManagerReconciler.\n : \"${HICLAW_MANAGER_GATEWAY_KEY:?HICLAW_MANAGER_GATEWAY_KEY is required (injected by controller)}\"\n : \"${HICLAW_MANAGER_PASSWORD:?HICLAW_MANAGER_PASSWORD is required (injected by controller)}\"\n else\n # Cloud (aliyun) mode: Manager still does its own initialization\n : \"${HICLAW_REGISTRATION_TOKEN:?HICLAW_REGISTRATION_TOKEN is required}\"\n : \"${HICLAW_ADMIN_USER:?HICLAW_ADMIN_USER is required}\"\n : \"${HICLAW_ADMIN_PASSWORD:?HICLAW_ADMIN_PASSWORD is required}\"\n fi\n log \"${HICLAW_RUNTIME} mode: validating environment... OK\"\n log \" Matrix: ${HICLAW_MATRIX_URL}, AI Gateway: ${HICLAW_AI_GATEWAY_URL}, Storage: ${HICLAW_FS_BUCKET}\"\n if [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n ensure_mc_credentials || { log \"FATAL: Initial STS credential fetch failed\"; exit 1; }\n fi\nfi\n\n# ============================================================\n# Local mode: host symlinks, /etc/hosts, wait for local services\n# ============================================================\nif [ \"${HICLAW_RUNTIME}\" != \"aliyun\" ] && [ \"${HICLAW_RUNTIME}\" != \"k8s\" ]; then\n # Create symlink for host directory access\n if [ -d \"/host-share\" ]; then\n ORIGINAL_HOST_HOME=\"${HOST_ORIGINAL_HOME:-$HOME}\"\n if [ ! -e \"${ORIGINAL_HOST_HOME}\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/root\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/data\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/host-share\" ]; then\n mkdir -p \"$(dirname \"${ORIGINAL_HOST_HOME}\")\"\n ln -sfn /host-share \"${ORIGINAL_HOST_HOME}\"\n log \"Created symlink: ${ORIGINAL_HOST_HOME} -> /host-share\"\n else\n ln -sfn /host-share /root/host-home\n log \"Created fallback symlink: /root/host-home -> /host-share\"\n fi\n fi\n\n # Add local domains to /etc/hosts\n HOSTS_DOMAINS=\"${MATRIX_DOMAIN%%:*} ${HICLAW_MATRIX_CLIENT_DOMAIN:-matrix-client-local.hiclaw.io} ${AI_GATEWAY_DOMAIN} ${HICLAW_FS_DOMAIN:-fs-local.hiclaw.io}\"\n if ! grep -q \"${AI_GATEWAY_DOMAIN}\" /etc/hosts 2>/dev/null; then\n echo \"127.0.0.1 ${HOSTS_DOMAINS}\" >> /etc/hosts\n log \"Added local domains to /etc/hosts\"\n fi\n\n # Wait for local infrastructure\n waitForService \"Higress Gateway\" \"127.0.0.1\" 8080 180\n waitForService \"Higress Console\" \"127.0.0.1\" 8001 180\n waitForService \"Tuwunel\" \"127.0.0.1\" 6167 120\n waitForHTTP \"Tuwunel Matrix API\" \"${HICLAW_MATRIX_URL}/_tuwunel/server_version\" 120\n waitForService \"MinIO\" \"127.0.0.1\" 9000 120\nelse\n # Cloud/K8s mode: wait for external Tuwunel\n log \"Waiting for Tuwunel Matrix server at ${HICLAW_MATRIX_URL}...\"\n _retry=0\n while [ \"${_retry}\" -lt 30 ]; do\n if curl -sf \"${HICLAW_MATRIX_URL}/_matrix/client/versions\" > /dev/null 2>&1; then\n log \"Tuwunel is ready\"\n break\n fi\n _retry=$((_retry + 1))\n log \" Waiting for Tuwunel (attempt ${_retry}/30)...\"\n sleep 5\n done\n if [ \"${_retry}\" -ge 30 ]; then\n log \"ERROR: Tuwunel not reachable at ${HICLAW_MATRIX_URL}\"\n exit 1\n fi\nfi\n\n# ============================================================\n# Auto-generate secrets if not provided via environment\n# Persisted to /data so they survive container restart\n# ============================================================\nSECRETS_FILE=\"/data/hiclaw-secrets.env\"\nif [ -f \"${SECRETS_FILE}\" ]; then\n source \"${SECRETS_FILE}\"\n log \"Loaded persisted secrets from ${SECRETS_FILE}\"\nfi\n\nif [ -z \"${HICLAW_MANAGER_GATEWAY_KEY}\" ]; then\n export HICLAW_MANAGER_GATEWAY_KEY=\"$(generateKey 32)\"\n log \"Auto-generated HICLAW_MANAGER_GATEWAY_KEY\"\nfi\nif [ -z \"${HICLAW_MANAGER_PASSWORD}\" ]; then\n export HICLAW_MANAGER_PASSWORD=\"$(generateKey 16)\"\n log \"Auto-generated HICLAW_MANAGER_PASSWORD\"\nfi\n\n# Persist secrets so they survive supervisord restart\nmkdir -p /data\ncat > \"${SECRETS_FILE}\" <<EOF\nexport HICLAW_MANAGER_GATEWAY_KEY=\"${HICLAW_MANAGER_GATEWAY_KEY}\"\nexport HICLAW_MANAGER_PASSWORD=\"${HICLAW_MANAGER_PASSWORD}\"\nEOF\nchmod 600 \"${SECRETS_FILE}\"\n\n# Cloud mode: pull workspace from OSS before initialization\nif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n HICLAW_FS=\"/root/hiclaw-fs\"\n mkdir -p \"${HICLAW_FS}/shared\" \"${HICLAW_FS}/agents\"\n log \"Pulling workspace from OSS...\"\n ensure_mc_credentials\n mc mirror \"${HICLAW_STORAGE_PREFIX}/manager/\" /root/manager-workspace/ --overwrite 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/shared/\" \"${HICLAW_FS}/shared/\" --overwrite 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/agents/\" \"${HICLAW_FS}/agents/\" --overwrite 2>/dev/null || true\n # Symlink hiclaw-fs into workspace for agent access\n ln -sfn \"${HICLAW_FS}\" /root/manager-workspace/hiclaw-fs\nfi\n\n# K8s mode: sync workspace from cluster-internal MinIO\nif [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n HICLAW_FS=\"/root/hiclaw-fs\"\n mkdir -p \"${HICLAW_FS}/shared\" \"${HICLAW_FS}/agents\" \"${HICLAW_FS}/hiclaw-config\"\n log \"Configuring mc alias for cluster MinIO...\"\n mc alias set hiclaw \"${HICLAW_FS_ENDPOINT}\" \"${HICLAW_FS_ACCESS_KEY}\" \"${HICLAW_FS_SECRET_KEY}\"\n log \"Syncing workspace from MinIO...\"\n mc mirror \"${HICLAW_STORAGE_PREFIX}/manager/\" /root/manager-workspace/ --overwrite 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/\" \"${HICLAW_FS}/\" --overwrite 2>/dev/null || true\n ln -sfn \"${HICLAW_FS}\" /root/manager-workspace/hiclaw-fs\n touch \"${HICLAW_FS}/.initialized\"\nfi\n\n# ============================================================\n# Initialize / upgrade Manager workspace\n# First boot: full init via upgrade-builtins.sh\n# Subsequent boots: compare image version; upgrade only if changed\n# ============================================================\nmkdir -p /root/manager-workspace\n\nIMAGE_VERSION=$(cat /opt/hiclaw/agent/.builtin-version 2>/dev/null || echo \"unknown\")\nINSTALLED_VERSION=$(cat /root/manager-workspace/.builtin-version 2>/dev/null || echo \"\")\n\nif [ ! -f /root/manager-workspace/.initialized ]; then\n log \"First boot: initializing manager workspace...\"\n bash /opt/hiclaw/scripts/init/upgrade-builtins.sh\n touch /root/manager-workspace/.initialized\n log \"Manager workspace initialized (version: ${IMAGE_VERSION})\"\nelif [ \"${IMAGE_VERSION}\" != \"${INSTALLED_VERSION}\" ] || [ \"${IMAGE_VERSION}\" = \"latest\" ]; then\n log \"Upgrade detected: ${INSTALLED_VERSION} -> ${IMAGE_VERSION}${IMAGE_VERSION:+ (latest: always upgrade)}\"\n bash /opt/hiclaw/scripts/init/upgrade-builtins.sh\n log \"Manager workspace upgraded to version: ${IMAGE_VERSION}\"\nelse\n log \"Workspace up to date (version: ${IMAGE_VERSION})\"\nfi\n\n# Local mode: wait for mc mirror initialization (shared + worker data in /root/hiclaw-fs/)\nif [ \"${HICLAW_RUNTIME}\" != \"aliyun\" ] && [ \"${HICLAW_RUNTIME}\" != \"k8s\" ]; then\n log \"Waiting for MinIO storage initialization...\"\n _minio_wait=0\n while [ ! -f /root/hiclaw-fs/.initialized ]; do\n sleep 2\n _minio_wait=$(( _minio_wait + 1 ))\n if [ \"${_minio_wait}\" -ge 60 ]; then\n log \"ERROR: MinIO storage initialization timed out after 120s\"\n exit 1\n fi\n done\n log \"MinIO storage initialized\"\nfi\n\n# ============================================================\n# Register Matrix users via Registration API (single-step, no UIAA)\n# K8s mode: skip — controller Initializer + ManagerReconciler already did this\n# ============================================================\nif [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n log \"K8s mode: skipping Matrix registration (handled by controller)\"\n # Controller injects HICLAW_MANAGER_PASSWORD via env; login to get token\n log \"Obtaining Manager Matrix access token...\"\n _LOGIN_RESPONSE=$(curl -s -X POST ${HICLAW_MATRIX_URL}/_matrix/client/v3/login \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"type\": \"m.login.password\",\n \"identifier\": {\"type\": \"m.id.user\", \"user\": \"manager\"},\n \"password\": \"'\"${HICLAW_MANAGER_PASSWORD}\"'\"\n }' 2>&1)\n MANAGER_TOKEN=$(echo \"${_LOGIN_RESPONSE}\" | jq -r '.access_token' 2>/dev/null)\n if [ -z \"${MANAGER_TOKEN}\" ] || [ \"${MANAGER_TOKEN}\" = \"null\" ]; then\n log \"ERROR: Failed to obtain Manager Matrix token\"\n log \"ERROR: Login response was: ${_LOGIN_RESPONSE}\"\n exit 1\n fi\n log \"Manager Matrix token obtained (token prefix: ${MANAGER_TOKEN:0:10}...)\"\nelse\nlog \"Registering human admin Matrix account...\"\ncurl -sf -X POST ${HICLAW_MATRIX_URL}/_matrix/client/v3/register \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"username\": \"'\"${HICLAW_ADMIN_USER}\"'\",\n \"password\": \"'\"${HICLAW_ADMIN_PASSWORD}\"'\",\n \"auth\": {\n \"type\": \"m.login.registration_token\",\n \"token\": \"'\"${HICLAW_REGISTRATION_TOKEN}\"'\"\n }\n }' > /dev/null 2>&1 || log \"Admin account may already exist\"\n\nlog \"Registering Manager Agent Matrix account...\"\ncurl -sf -X POST ${HICLAW_MATRIX_URL}/_matrix/client/v3/register \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"username\": \"manager\",\n \"password\": \"'\"${HICLAW_MANAGER_PASSWORD}\"'\",\n \"auth\": {\n \"type\": \"m.login.registration_token\",\n \"token\": \"'\"${HICLAW_REGISTRATION_TOKEN}\"'\"\n }\n }' > /dev/null 2>&1 || log \"Manager account may already exist\"\n\n# Get Manager Agent's Matrix access token\nlog \"Obtaining Manager Matrix access token...\"\n_LOGIN_RESPONSE=$(curl -s -X POST ${HICLAW_MATRIX_URL}/_matrix/client/v3/login \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"type\": \"m.login.password\",\n \"identifier\": {\"type\": \"m.id.user\", \"user\": \"manager\"},\n \"password\": \"'\"${HICLAW_MANAGER_PASSWORD}\"'\"\n }' 2>&1)\n_LOGIN_EXIT=$?\nlog \"Matrix login HTTP exit code: ${_LOGIN_EXIT}\"\nlog \"Matrix login response: ${_LOGIN_RESPONSE}\"\n\nMANAGER_TOKEN=$(echo \"${_LOGIN_RESPONSE}\" | jq -r '.access_token' 2>/dev/null)\n\nif [ -z \"${MANAGER_TOKEN}\" ] || [ \"${MANAGER_TOKEN}\" = \"null\" ]; then\n log \"ERROR: Failed to obtain Manager Matrix token (exit=${_LOGIN_EXIT})\"\n log \"ERROR: Login response was: ${_LOGIN_RESPONSE}\"\n exit 1\nfi\nlog \"Manager Matrix token obtained (token prefix: ${MANAGER_TOKEN:0:10}...)\"\nfi\n\n# ============================================================\n# Higress Console initialization\n# Docker mode: full setup-higress.sh (internal Higress at localhost:8001)\n# K8s mode: skip — controller Initializer handles Higress setup\n# Cloud (aliyun) mode: skip entirely (Higress managed externally)\n# ============================================================\n_HIGRESS_CONSOLE_URL=\"\"\n_HIGRESS_USER=\"${HICLAW_ADMIN_USER}\"\n_HIGRESS_PASS=\"${HICLAW_ADMIN_PASSWORD}\"\nif [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n log \"K8s mode: skipping Higress initialization (handled by controller)\"\nelif [ \"${HICLAW_RUNTIME}\" != \"aliyun\" ]; then\n _HIGRESS_CONSOLE_URL=\"http://127.0.0.1:8001\"\nfi\n\nif [ -n \"${_HIGRESS_CONSOLE_URL}\" ]; then\n COOKIE_FILE=\"/tmp/higress-session-cookie\"\n\n log \"Waiting for Higress Console (${_HIGRESS_CONSOLE_URL}) to be fully ready and initializing admin...\"\n INIT_DONE=false\n for i in $(seq 1 90); do\n INIT_RESULT=$(curl -s -X POST \"${_HIGRESS_CONSOLE_URL}/system/init\" \\\n -H 'Content-Type: application/json' \\\n -d '{\"adminUser\":{\"name\":\"'\"${_HIGRESS_USER}\"'\",\"password\":\"'\"${_HIGRESS_PASS}\"'\",\"displayName\":\"'\"${_HIGRESS_USER}\"'\"}}' 2>/dev/null) || true\n if echo \"${INIT_RESULT}\" | grep -qE '\"success\":true|already.?init' 2>/dev/null; then\n INIT_DONE=true\n break\n fi\n if echo \"${INIT_RESULT}\" | grep -q '\"name\"' 2>/dev/null; then\n INIT_DONE=true\n break\n fi\n sleep 2\n done\n\n if [ \"${INIT_DONE}\" != \"true\" ]; then\n log \"ERROR: Higress Console did not become ready within 180s\"\n exit 1\n fi\n log \"Higress Console init done\"\n\n log \"Logging into Higress Console...\"\n LOGIN_OK=false\n for i in $(seq 1 10); do\n HTTP_CODE=$(curl -s -o /dev/null -w '%{http_code}' -X POST \"${_HIGRESS_CONSOLE_URL}/session/login\" \\\n -H 'Content-Type: application/json' \\\n -c \"${COOKIE_FILE}\" \\\n -d '{\"username\":\"'\"${_HIGRESS_USER}\"'\",\"password\":\"'\"${_HIGRESS_PASS}\"'\"}' 2>/dev/null) || true\n if { [ \"${HTTP_CODE}\" = \"200\" ] || [ \"${HTTP_CODE}\" = \"201\" ]; } && [ -f \"${COOKIE_FILE}\" ] && [ -s \"${COOKIE_FILE}\" ]; then\n LOGIN_OK=true\n break\n fi\n log \"Login attempt $i (HTTP ${HTTP_CODE}), retrying in 3s...\"\n sleep 3\n done\n\n if [ \"${LOGIN_OK}\" != \"true\" ]; then\n log \"ERROR: Could not login to Higress Console after retries\"\n exit 1\n fi\n log \"Higress Console login successful\"\n\n VERIFY_CODE=$(curl -s -o /dev/null -w '%{http_code}' \"${_HIGRESS_CONSOLE_URL}/v1/consumers\" -b \"${COOKIE_FILE}\" 2>/dev/null) || true\n if [ \"${VERIFY_CODE}\" = \"200\" ]; then\n log \"Console session verified (cookie valid)\"\n else\n log \"WARNING: Console session may be invalid (verify returned HTTP ${VERIFY_CODE})\"\n rm -f \"${COOKIE_FILE}\"\n for i in $(seq 1 5); do\n curl -s -o /dev/null -w '%{http_code}' -X POST \"${_HIGRESS_CONSOLE_URL}/session/login\" \\\n -H 'Content-Type: application/json' \\\n -c \"${COOKIE_FILE}\" \\\n -d '{\"username\":\"'\"${_HIGRESS_USER}\"'\",\"password\":\"'\"${_HIGRESS_PASS}\"'\"}' 2>/dev/null\n VERIFY2=$(curl -s -o /dev/null -w '%{http_code}' \"${_HIGRESS_CONSOLE_URL}/v1/consumers\" -b \"${COOKIE_FILE}\" 2>/dev/null) || true\n if [ \"${VERIFY2}\" = \"200\" ]; then\n log \"Re-login successful, session verified\"\n break\n fi\n sleep 2\n done\n fi\n\n export HIGRESS_COOKIE_FILE=\"${COOKIE_FILE}\"\n export HIGRESS_CONSOLE_URL=\"${_HIGRESS_CONSOLE_URL}\"\n\n if [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n # K8s mode: lightweight Higress config — only what's needed for LLM access\n source /opt/hiclaw/scripts/lib/base.sh\n _k8s_higress_api() {\n local method=\"$1\" path=\"$2\" desc=\"$3\"; shift 3; local body=\"$*\"\n local tmpfile; tmpfile=$(mktemp)\n local http_code\n http_code=$(curl -s -o \"${tmpfile}\" -w '%{http_code}' -X \"${method}\" \"${_HIGRESS_CONSOLE_URL}${path}\" \\\n -b \"${COOKIE_FILE}\" -H 'Content-Type: application/json' -d \"${body}\" 2>/dev/null) || true\n local response; response=$(cat \"${tmpfile}\" 2>/dev/null); rm -f \"${tmpfile}\"\n if echo \"${response}\" | grep -q '\"success\":true' 2>/dev/null; then\n log \"${desc} ... OK\"\n elif [ \"${http_code}\" = \"409\" ]; then\n log \"${desc} ... already exists, skipping\"\n elif [ \"${http_code}\" = \"200\" ] || [ \"${http_code}\" = \"201\" ] || [ \"${http_code}\" = \"204\" ]; then\n log \"${desc} ... OK (HTTP ${http_code})\"\n else\n log \"WARNING: ${desc} ... (HTTP ${http_code}): ${response}\"\n fi\n }\n\n # 1. Service Sources (DNS type → K8s Service FQDN)\n # Extract host:port from URLs for Higress service source registration\n _TUWUNEL_HOST=$(echo \"${HICLAW_MATRIX_URL}\" | sed 's|^http[s]*://||')\n _TUWUNEL_DOMAIN=$(echo \"${_TUWUNEL_HOST}\" | cut -d: -f1)\n _TUWUNEL_PORT=$(echo \"${_TUWUNEL_HOST}\" | cut -d: -f2)\n _k8s_higress_api POST /v1/service-sources \"Registering Tuwunel service source\" \\\n '{\"type\":\"dns\",\"name\":\"tuwunel\",\"domain\":\"'\"${_TUWUNEL_DOMAIN}\"'\",\"port\":'\"${_TUWUNEL_PORT}\"'}'\n\n if [ -n \"${HICLAW_ELEMENT_WEB_URL:-}\" ]; then\n _ELEMENT_HOST=$(echo \"${HICLAW_ELEMENT_WEB_URL}\" | sed 's|^http[s]*://||')\n _ELEMENT_DOMAIN=$(echo \"${_ELEMENT_HOST}\" | cut -d: -f1)\n _ELEMENT_PORT=$(echo \"${_ELEMENT_HOST}\" | cut -d: -f2)\n _k8s_higress_api POST /v1/service-sources \"Registering Element Web service source\" \\\n '{\"type\":\"dns\",\"name\":\"element-web\",\"domain\":\"'\"${_ELEMENT_DOMAIN}\"'\",\"port\":'\"${_ELEMENT_PORT}\"'}'\n fi\n\n # 2. Manager Consumer (key-auth)\n _k8s_higress_api POST /v1/consumers \"Creating Manager consumer\" \\\n '{\"name\":\"manager\",\"credentials\":[{\"type\":\"key-auth\",\"source\":\"BEARER\",\"values\":[\"'\"${HICLAW_MANAGER_GATEWAY_KEY}\"'\"]}]}'\n\n # 3. LLM Provider\n _LLM_PROVIDER=\"${HICLAW_LLM_PROVIDER:-qwen}\"\n _LLM_API_URL=\"${HICLAW_LLM_API_URL:-}\"\n if [ -z \"${_LLM_API_URL}\" ]; then\n case \"${_LLM_PROVIDER}\" in\n qwen) _LLM_API_URL=\"https://dashscope.aliyuncs.com/compatible-mode/v1\" ;;\n esac\n fi\n # 4. LLM Provider type-specific config\n case \"${_LLM_PROVIDER}\" in\n qwen)\n _k8s_higress_api POST /v1/ai/providers \"Creating LLM provider (qwen)\" \\\n '{\"type\":\"qwen\",\"name\":\"qwen\",\"tokens\":[\"'\"${HICLAW_LLM_API_KEY}\"'\"],\"protocol\":\"openai/v1\",\"tokenFailoverConfig\":{\"enabled\":false},\"rawConfigs\":{\"qwenEnableSearch\":false,\"qwenEnableCompatible\":true,\"qwenFileIds\":[],\"hiclawMode\":true}}'\n ;;\n *)\n _BODY='{\"name\":\"'\"${_LLM_PROVIDER}\"'\",\"type\":\"openai\",\"tokens\":[\"'\"${HICLAW_LLM_API_KEY}\"'\"],\"modelMapping\":{},\"protocol\":\"openai/v1\",\"rawConfigs\":{\"hiclawMode\":true}}'\n _k8s_higress_api POST /v1/ai/providers \"Creating LLM provider (${_LLM_PROVIDER})\" \"${_BODY}\"\n ;;\n esac\n\n # 5. AI Route (bind provider + consumer auth, /v1 prefix to avoid clash with Element Web catch-all)\n _k8s_higress_api POST /v1/ai/routes \"Creating AI Gateway route\" \\\n '{\"name\":\"default-ai-route\",\"domains\":[],\"pathPredicate\":{\"matchType\":\"PRE\",\"matchValue\":\"/v1\",\"caseSensitive\":false},\"upstreams\":[{\"provider\":\"'\"${_LLM_PROVIDER}\"'\",\"weight\":100,\"modelMapping\":{}}],\"authConfig\":{\"enabled\":true,\"allowedCredentialTypes\":[\"key-auth\"],\"allowedConsumers\":[\"manager\"]}}'\n\n # 6. Matrix Homeserver Route (/_matrix/* → Tuwunel, no auth)\n _k8s_higress_api POST /v1/routes \"Creating Matrix Homeserver route\" \\\n '{\"name\":\"matrix-homeserver\",\"domains\":[],\"path\":{\"matchType\":\"PRE\",\"matchValue\":\"/_matrix\"},\"services\":[{\"name\":\"tuwunel.dns\",\"port\":'\"${_TUWUNEL_PORT}\"',\"weight\":100}]}'\n\n # 7. Element Web Route (/ catch-all → Element Web, no auth)\n if [ -n \"${HICLAW_ELEMENT_WEB_URL:-}\" ]; then\n _k8s_higress_api POST /v1/routes \"Creating Element Web route\" \\\n '{\"name\":\"element-web\",\"domains\":[],\"path\":{\"matchType\":\"PRE\",\"matchValue\":\"/\"},\"services\":[{\"name\":\"element-web.dns\",\"port\":'\"${_ELEMENT_PORT}\"',\"weight\":100}]}'\n fi\n\n # 8. Remove Higress default landing page (Exact match on / takes precedence over Element Web catch-all)\n _k8s_higress_api DELETE /v1/routes/default \"Removing Higress default landing route\"\n\n log \"K8s Higress lightweight setup complete\"\n\n # Wait for AI plugin activation (~45s for first config)\n log \"Waiting for AI Gateway plugin activation (45s)...\"\n sleep 45\n else\n # Docker mode: full setup with all routes, domains, MCP servers\n /opt/hiclaw/scripts/init/setup-higress.sh\n fi\nfi\n\n# ============================================================\n# Create admin DM room, persist to state.json, send welcome message\n# K8s mode: skip — controller ProvisionManager creates the Admin DM\n# room (Step 4 in service/provisioner.go) AND reconcileManagerWelcome\n# delivers the first-boot onboarding prompt once OpenClaw inside this\n# container has joined the room. In k8s mode the manager intentionally\n# does NOT have the admin password (only HICLAW_ADMIN_USER), so it\n# could not log in as admin to send the welcome itself anyway. The\n# Manager Agent discovers its admin DM room on first heartbeat via\n# state.json / `manage-state.sh --action set-admin-dm` (see HEARTBEAT.md\n# Step 1) — it does not need it to be pre-injected by this script.\n# Runs in both local and cloud modes (idempotent)\n# ============================================================\nif [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n log \"K8s mode: skipping admin DM room creation and welcome message (both handled by hiclaw-controller)\"\nelse\nMANAGER_FULL_ID=\"@manager:${MATRIX_DOMAIN}\"\nADMIN_FULL_ID=\"@${HICLAW_ADMIN_USER}:${MATRIX_DOMAIN}\"\n\nlog \"Logging in as admin to create DM room...\"\n_ADMIN_LOGIN=$(curl -sf -X POST \"${HICLAW_MATRIX_URL}/_matrix/client/v3/login\" \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"type\": \"m.login.password\",\n \"identifier\": {\"type\": \"m.id.user\", \"user\": \"'\"${HICLAW_ADMIN_USER}\"'\"},\n \"password\": \"'\"${HICLAW_ADMIN_PASSWORD}\"'\"\n }' 2>&1) || true\n\nADMIN_MATRIX_TOKEN=$(echo \"${_ADMIN_LOGIN}\" | jq -r '.access_token // empty' 2>/dev/null)\nif [ -z \"${ADMIN_MATRIX_TOKEN}\" ]; then\n log \"WARNING: Failed to login as admin, skipping DM room creation\"\nelse\n # Search for existing DM room with Manager (idempotent)\n DM_ROOM_ID=\"\"\n _JOINED_ROOMS=$(curl -sf \"${HICLAW_MATRIX_URL}/_matrix/client/v3/joined_rooms\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" 2>/dev/null \\\n | jq -r '.joined_rooms[]' 2>/dev/null) || true\n for _rid in ${_JOINED_ROOMS}; do\n _members=$(curl -sf \"${HICLAW_MATRIX_URL}/_matrix/client/v3/rooms/${_rid}/members\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" 2>/dev/null \\\n | jq -r '.chunk[].state_key' 2>/dev/null) || continue\n _count=$(echo \"${_members}\" | wc -l | xargs)\n if [ \"${_count}\" = \"2\" ] && echo \"${_members}\" | grep -q \"@manager:\"; then\n DM_ROOM_ID=\"${_rid}\"\n break\n fi\n done\n\n if [ -n \"${DM_ROOM_ID}\" ]; then\n log \"Existing DM room found: ${DM_ROOM_ID}\"\n else\n log \"Creating DM room with Manager...\"\n _RAW=$(curl -s -w '\\nHTTP_CODE:%{http_code}' -X POST \"${HICLAW_MATRIX_URL}/_matrix/client/v3/createRoom\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d \"{\\\"is_direct\\\":true,\\\"invite\\\":[\\\"${MANAGER_FULL_ID}\\\"],\\\"preset\\\":\\\"trusted_private_chat\\\"}\" 2>&1) || true\n _HTTP_CODE=$(echo \"${_RAW}\" | tail -1 | sed 's/HTTP_CODE://')\n _CREATE_RESP=$(echo \"${_RAW}\" | sed '$d')\n DM_ROOM_ID=$(echo \"${_CREATE_RESP}\" | jq -r '.room_id // empty' 2>/dev/null)\n if [ -n \"${DM_ROOM_ID}\" ]; then\n log \"DM room created: ${DM_ROOM_ID}\"\n else\n log \"WARNING: Failed to create DM room (HTTP ${_HTTP_CODE}): ${_CREATE_RESP}\"\n fi\n fi\n\n # Persist admin DM room ID to state.json\n if [ -n \"${DM_ROOM_ID}\" ]; then\n STATE_SCRIPT=\"/opt/hiclaw/agent/skills/task-management/scripts/manage-state.sh\"\n if [ -f \"${STATE_SCRIPT}\" ]; then\n bash \"${STATE_SCRIPT}\" --action init 2>/dev/null || true\n bash \"${STATE_SCRIPT}\" --action set-admin-dm --room-id \"${DM_ROOM_ID}\" 2>/dev/null || true\n log \"Admin DM room persisted to state.json: ${DM_ROOM_ID}\"\n fi\n fi\n\n # Schedule welcome message in background (only on first boot)\n if [ -n \"${DM_ROOM_ID}\" ] && [ ! -f \"/root/manager-workspace/soul-configured\" ]; then\n log \"Scheduling welcome message (background, waiting for OpenClaw to start)...\"\n (\n _HICLAW_LANGUAGE=\"${HICLAW_LANGUAGE:-zh}\"\n _HICLAW_TIMEZONE=\"${TZ:-Asia/Shanghai}\"\n _wait=0\n _ready=false\n while [ \"${_wait}\" -lt 300 ]; do\n if curl -sf http://127.0.0.1:18799/ > /dev/null 2>&1; then\n _ready=true\n break\n fi\n sleep 3\n _wait=$((_wait + 3))\n done\n if [ \"${_ready}\" != \"true\" ]; then\n echo \"[manager] WARNING: OpenClaw gateway not ready within 300s, skipping welcome message\"\n exit 0\n fi\n # Ensure Manager has joined the DM room before sending the welcome\n # message. Without this, there is a race between OpenClaw's Matrix\n # auto-join and the message send — the message may land before Manager\n # joins, so OpenClaw's /sync never picks it up.\n _join_ok=false\n for _join_attempt in 1 2 3; do\n if curl -sf -X POST \"${HICLAW_MATRIX_URL}/_matrix/client/v3/rooms/${DM_ROOM_ID}/join\" \\\n -H \"Authorization: Bearer ${MANAGER_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d '{}' > /dev/null 2>&1; then\n echo \"[manager] Manager joined DM room before welcome message\"\n _join_ok=true\n break\n fi\n sleep 2\n done\n if [ \"${_join_ok}\" != \"true\" ]; then\n echo \"[manager] WARNING: Manager join request failed after 3 attempts (may already be joined)\"\n fi\n _welcome_msg=\"This is an automated message from the HiClaw setup. This is a fresh installation.\n\n--- Installation Context ---\nUser Language: ${_HICLAW_LANGUAGE} (zh = Chinese, en = English)\nUser Timezone: ${_HICLAW_TIMEZONE} (IANA timezone identifier)\n---\n\nYou are an AI agent that manages a team of worker agents. Your identity and personality have not been configured yet — the human admin is about to meet you for the first time.\n\nPlease begin the onboarding conversation:\n\n1. Greet the admin warmly and briefly describe what you can do (coordinate workers, manage tasks, run multi-agent projects)\n2. The user has selected \\\"${_HICLAW_LANGUAGE}\\\" as their preferred language during installation. Use this language for your greeting and all subsequent communication.\n3. The user's timezone is ${_HICLAW_TIMEZONE}. Based on this timezone, you may infer their likely region and suggest additional language options.\n4. Ask them: a) What would they like to call you? b) Communication style preference? c) Any behavior guidelines? d) Confirm default language\n5. After they reply, write their preferences to ~/SOUL.md\n6. Confirm what you wrote, and ask if they would like to adjust anything\n7. Once confirmed, run: touch ~/soul-configured\n\nThe human admin will start chatting shortly.\"\n _txn_id=\"welcome-$(date +%s)\"\n _payload=$(jq -nc --arg body \"${_welcome_msg}\" '{\"msgtype\":\"m.text\",\"body\":$body}')\n _raw=$(curl -s -w '\\nHTTP_CODE:%{http_code}' -X PUT \"${HICLAW_MATRIX_URL}/_matrix/client/v3/rooms/${DM_ROOM_ID}/send/m.room.message/${_txn_id}\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d \"${_payload}\" 2>&1) || true\n _http_code=$(echo \"${_raw}\" | tail -1 | sed 's/HTTP_CODE://')\n _send_resp=$(echo \"${_raw}\" | sed '$d')\n if echo \"${_send_resp}\" | jq -e '.event_id' > /dev/null 2>&1; then\n echo \"[manager] Welcome message sent to DM room\"\n else\n echo \"[manager] WARNING: Failed to send welcome message (HTTP ${_http_code}): ${_send_resp}\"\n fi\n ) &\n log \"Welcome message background process started (PID: $!)\"\n fi\nfi\nfi # end K8s mode skip for admin DM room\n\n# ============================================================\n# Generate Manager Agent openclaw.json from template\n# ============================================================\nlog \"Generating Manager openclaw.json...\"\nexport MANAGER_MATRIX_TOKEN=\"${MANAGER_TOKEN}\"\nexport MANAGER_GATEWAY_KEY=\"${HICLAW_MANAGER_GATEWAY_KEY}\"\n# Resolve model parameters based on model name\nMODEL_NAME=\"${HICLAW_DEFAULT_MODEL:-qwen3.6-plus}\"\ncase \"${MODEL_NAME}\" in\n gpt-5.3-codex|gpt-5-mini|gpt-5-nano)\n export MODEL_CONTEXT_WINDOW=400000 MODEL_MAX_TOKENS=128000 ;;\n claude-opus-4-6)\n export MODEL_CONTEXT_WINDOW=1000000 MODEL_MAX_TOKENS=128000 ;;\n claude-sonnet-4-6)\n export MODEL_CONTEXT_WINDOW=1000000 MODEL_MAX_TOKENS=64000 ;;\n claude-haiku-4-5)\n export MODEL_CONTEXT_WINDOW=200000 MODEL_MAX_TOKENS=64000 ;;\n qwen3.6-plus|qwen3.5-plus)\n export MODEL_CONTEXT_WINDOW=200000 MODEL_MAX_TOKENS=64000 ;;\n deepseek-chat|deepseek-reasoner|kimi-k2.5)\n export MODEL_CONTEXT_WINDOW=256000 MODEL_MAX_TOKENS=128000 ;;\n glm-5|MiniMax-M2.7|MiniMax-M2.7-highspeed|MiniMax-M2.5)\n export MODEL_CONTEXT_WINDOW=200000 MODEL_MAX_TOKENS=128000 ;;\n *)\n export MODEL_CONTEXT_WINDOW=150000 MODEL_MAX_TOKENS=128000 ;;\nesac\nexport MODEL_REASONING=true\n\n# Override with user-supplied custom model parameters from env (set during install)\n[ -n \"${HICLAW_MODEL_CONTEXT_WINDOW:-}\" ] && export MODEL_CONTEXT_WINDOW=\"${HICLAW_MODEL_CONTEXT_WINDOW}\"\n[ -n \"${HICLAW_MODEL_MAX_TOKENS:-}\" ] && export MODEL_MAX_TOKENS=\"${HICLAW_MODEL_MAX_TOKENS}\"\n[ -n \"${HICLAW_MODEL_REASONING:-}\" ] && export MODEL_REASONING=\"${HICLAW_MODEL_REASONING}\"\n\n# E2EE: convert HICLAW_MATRIX_E2EE to JSON boolean for template substitution\nif [ \"${HICLAW_MATRIX_E2EE:-0}\" = \"1\" ] || [ \"${HICLAW_MATRIX_E2EE:-}\" = \"true\" ]; then\n export MATRIX_E2EE_ENABLED=true\nelse\n export MATRIX_E2EE_ENABLED=false\nfi\nlog \"Matrix E2EE: ${MATRIX_E2EE_ENABLED}\"\n\n# Resolve input modalities: only vision-capable models get \"image\"\ncase \"${MODEL_NAME}\" in\n gpt-5.4|gpt-5.3-codex|gpt-5-mini|gpt-5-nano|claude-opus-4-6|claude-sonnet-4-6|claude-haiku-4-5|qwen3.6-plus|qwen3.5-plus|kimi-k2.5)\n export MODEL_INPUT='[\"text\", \"image\"]' ;;\n *)\n export MODEL_INPUT='[\"text\"]' ;;\nesac\n# Override with user-supplied vision setting from env\nif [ \"${HICLAW_MODEL_VISION:-}\" = \"true\" ]; then\n export MODEL_INPUT='[\"text\", \"image\"]'\nelif [ \"${HICLAW_MODEL_VISION:-}\" = \"false\" ]; then\n export MODEL_INPUT='[\"text\"]'\nfi\n\nlog \"Model: ${MODEL_NAME} (context=${MODEL_CONTEXT_WINDOW}, maxTokens=${MODEL_MAX_TOKENS}, reasoning=${MODEL_REASONING}, input=${MODEL_INPUT})\"\n\nif [ -f /root/manager-workspace/openclaw.json ]; then\n log \"Manager openclaw.json already exists, updating dynamic fields only (preserving user customizations)...\"\n # Merge known models into existing config (add missing, preserve user-added)\n # Use known-models.json (valid JSON) instead of template (contains ${VAR} placeholders)\n KNOWN_MODELS=$(cat /opt/hiclaw/configs/known-models.json 2>/dev/null || echo '[]')\n jq --arg token \"${MANAGER_TOKEN}\" \\\n --arg key \"${HICLAW_MANAGER_GATEWAY_KEY}\" \\\n --arg model \"${MODEL_NAME}\" \\\n --arg emb_model \"${HICLAW_EMBEDDING_MODEL}\" \\\n --arg aigw_domain \"${AI_GATEWAY_DOMAIN}\" \\\n --arg matrix_user_id \"@manager:${MATRIX_DOMAIN}\" \\\n --argjson e2ee \"${MATRIX_E2EE_ENABLED}\" \\\n --argjson known_models \"${KNOWN_MODELS}\" \\\n --argjson ctx \"${MODEL_CONTEXT_WINDOW}\" \\\n --argjson max \"${MODEL_MAX_TOKENS}\" \\\n --argjson reasoning \"${MODEL_REASONING}\" \\\n --argjson input \"${MODEL_INPUT}\" \\\n '\n # Merge known models: add any model id not already present\n .models.providers[\"hiclaw-gateway\"].models as $existing\n | ($existing | map(.id)) as $existing_ids\n | ($known_models | map(select(.id as $id | $existing_ids | index($id) | not))) as $new\n | .models.providers[\"hiclaw-gateway\"].models = ($existing + $new)\n # Ensure the user-chosen default model is in the list (custom model support)\n | if (.models.providers[\"hiclaw-gateway\"].models | map(.id) | index($model) | not) then\n .models.providers[\"hiclaw-gateway\"].models += [{\"id\": $model, \"name\": $model, \"reasoning\": $reasoning, \"contextWindow\": $ctx, \"maxTokens\": $max, \"input\": $input}]\n else . end\n # Rebuild model aliases from the full models list\n | (.models.providers[\"hiclaw-gateway\"].models | map({ (\"hiclaw-gateway/\" + .id): { \"alias\": .id } }) | add // {}) as $aliases\n | .agents.defaults.models = ((.agents.defaults.models // {}) + $aliases)\n | .channels.matrix.accessToken = $token | .channels.matrix.userId = $matrix_user_id | .models.providers[\"hiclaw-gateway\"].apiKey = $key\n | ((.hooks.token // \"\") as $ht | if $ht == $key or $ht == ($key + \"-hooks\" | @base64) then del(.hooks) else . end)\n | .agents.defaults.model.primary = (\"hiclaw-gateway/\" + $model)\n | .commands.restart = true\n | .gateway.port = 18799\n | .gateway.bind = \"lan\"\n | .gateway.controlUi = ((.gateway.controlUi // {}) + {\"dangerouslyDisableDeviceAuth\": true, \"allowInsecureAuth\": true, \"allowedOrigins\": [\"*\"]})\n | .channels.matrix.encryption = $e2ee\n | .channels.matrix.network = ((.channels.matrix.network // {}) + {\"dangerouslyAllowPrivateNetwork\": true})\n | .channels.matrix.autoJoin = \"always\"\n # OpenClaw YOLO defaults: host exec without approval prompts (see openclaw docs tools/exec-approvals)\n | .tools = (.tools // {})\n | .tools.exec = ((.tools.exec // {}) + {\"host\":\"gateway\",\"security\":\"full\",\"ask\":\"off\"})\n | .tools.elevated = (.tools.elevated // {})\n | .tools.elevated.enabled = true\n | .tools.elevated.allowFrom |= ((. // {}) | .matrix = [\"*\"])\n | .agents.defaults.elevatedDefault = \"full\"\n # Ensure memorySearch config exists (embedding model for memory) — skip if embedding model is empty\n | if $emb_model != \"\" then .agents.defaults.memorySearch //= {\"provider\":\"openai\",\"model\":$emb_model,\"remote\":{\"baseUrl\":(\"http://\" + $aigw_domain + \":8080/v1\"),\"apiKey\":$key}} else . end\n ' \\\n /root/manager-workspace/openclaw.json > /tmp/openclaw.json.tmp && \\\n mv /tmp/openclaw.json.tmp /root/manager-workspace/openclaw.json\n # Anti-tampering bypass: openclaw's config-observe-recovery clobbers the file\n # back to .bak whenever it sees \"missing-meta-vs-last-good\", which strips\n # external edits like channels.matrix.network.dangerouslyAllowPrivateNetwork.\n # Stamp meta and mirror to .bak so our edits survive.\n _oc_ver=$(openclaw --version 2>/dev/null | head -1 | awk '{print $NF}')\n [ -z \"${_oc_ver}\" ] && _oc_ver=\"hiclaw-bootstrap\"\n jq --arg ver \"${_oc_ver}\" --arg ts \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\" \\\n '.meta = ((.meta // {}) + {lastTouchedVersion: $ver, lastTouchedAt: $ts})' \\\n /root/manager-workspace/openclaw.json > /tmp/openclaw.meta.json && \\\n mv /tmp/openclaw.meta.json /root/manager-workspace/openclaw.json\n cp -f /root/manager-workspace/openclaw.json /root/manager-workspace/openclaw.json.bak\n # Verify the token was written correctly\n _written_token=$(jq -r '.channels.matrix.accessToken' /root/manager-workspace/openclaw.json 2>/dev/null)\n if [ -z \"${_written_token}\" ] || [ \"${_written_token}\" = \"null\" ]; then\n log \"ERROR: Matrix token was not written correctly to openclaw.json (got: ${_written_token})\"\n else\n log \"Matrix token written to openclaw.json (prefix: ${_written_token:0:10}...)\"\n fi\nelse\n log \"Manager openclaw.json not found, generating from template...\"\n envsubst < /opt/hiclaw/configs/manager-openclaw.json.tmpl > /root/manager-workspace/openclaw.json\n # Post-envsubst injection: memorySearch + custom model (single jq pass when possible)\n if ! jq -e --arg model \"${MODEL_NAME}\" '.models.providers[\"hiclaw-gateway\"].models | map(.id) | index($model)' /root/manager-workspace/openclaw.json > /dev/null 2>&1; then\n log \"Custom model '${MODEL_NAME}' not in built-in list, injecting into config...\"\n jq --arg emb_model \"${HICLAW_EMBEDDING_MODEL}\" \\\n --arg aigw_domain \"${AI_GATEWAY_DOMAIN}\" \\\n --arg key \"${HICLAW_MANAGER_GATEWAY_KEY}\" \\\n --arg model \"${MODEL_NAME}\" \\\n --argjson ctx \"${MODEL_CONTEXT_WINDOW}\" \\\n --argjson max \"${MODEL_MAX_TOKENS}\" \\\n --argjson reasoning \"${MODEL_REASONING}\" \\\n --argjson input \"${MODEL_INPUT}\" \\\n '\n (if $emb_model != \"\" then .agents.defaults.memorySearch = {\"provider\":\"openai\",\"model\":$emb_model,\"remote\":{\"baseUrl\":(\"http://\" + $aigw_domain + \":8080/v1\"),\"apiKey\":$key}} else . end)\n | .models.providers[\"hiclaw-gateway\"].models += [{\"id\": $model, \"name\": $model, \"reasoning\": $reasoning, \"contextWindow\": $ctx, \"maxTokens\": $max, \"input\": $input}]\n | .agents.defaults.models += {(\"hiclaw-gateway/\" + $model): {\"alias\": $model}}\n ' /root/manager-workspace/openclaw.json > /tmp/openclaw.json.tmp && \\\n mv /tmp/openclaw.json.tmp /root/manager-workspace/openclaw.json\n elif [ -n \"${HICLAW_EMBEDDING_MODEL}\" ]; then\n jq --arg emb_model \"${HICLAW_EMBEDDING_MODEL}\" \\\n --arg aigw_domain \"${AI_GATEWAY_DOMAIN}\" \\\n --arg key \"${HICLAW_MANAGER_GATEWAY_KEY}\" \\\n '.agents.defaults.memorySearch = {\"provider\":\"openai\",\"model\":$emb_model,\"remote\":{\"baseUrl\":(\"http://\" + $aigw_domain + \":8080/v1\"),\"apiKey\":$key}}' \\\n /root/manager-workspace/openclaw.json > /tmp/openclaw.json.tmp && \\\n mv /tmp/openclaw.json.tmp /root/manager-workspace/openclaw.json\n fi\n _written_token=$(jq -r '.channels.matrix.accessToken' /root/manager-workspace/openclaw.json 2>/dev/null)\n log \"Matrix token written from template (prefix: ${_written_token:0:10}...)\"\nfi\n\n# Cloud/K8s mode: overlay cloud-specific settings onto generated config\nif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ] || [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n log \"Applying cloud/k8s overlay to openclaw.json...\"\n jq --arg homeserver \"${HICLAW_MATRIX_URL}\" \\\n --arg gateway \"${HICLAW_AI_GATEWAY_URL}/v1\" \\\n --arg key \"${HICLAW_MANAGER_GATEWAY_KEY}\" \\\n '.channels.matrix.homeserver = $homeserver\n | .models.providers[\"hiclaw-gateway\"].baseUrl = $gateway\n | .models.providers[\"hiclaw-gateway\"].apiKey = $key\n | ((.hooks.token // \"\") as $ht | if $ht == $key or $ht == ($key + \"-hooks\" | @base64) then del(.hooks) else . end)\n | .commands.restart = false\n | if .agents.defaults.memorySearch then .agents.defaults.memorySearch.remote.baseUrl = $gateway | .agents.defaults.memorySearch.remote.apiKey = $key else . end' \\\n /root/manager-workspace/openclaw.json > /tmp/openclaw-cloud.json && \\\n mv /tmp/openclaw-cloud.json /root/manager-workspace/openclaw.json\n log \"Cloud/K8s overlay applied\"\nfi\n\n# ============================================================\n# Optional: enable openclaw-cms-plugin observability\n# Config is applied at runtime so secrets stay out of image layers.\n# ============================================================\nCMS_TRACES_ENABLED=\"$(echo \"${HICLAW_CMS_TRACES_ENABLED:-false}\" | tr '[:upper:]' '[:lower:]')\"\nif [ \"${CMS_TRACES_ENABLED}\" = \"true\" ]; then\n CMS_PLUGIN_NAME=\"openclaw-cms-plugin\"\n CMS_PLUGIN_DIR=\"${OPENCLAW_CMS_PLUGIN_DIR:-/opt/openclaw/extensions/openclaw-cms-plugin}\"\n CMS_PLUGIN_MANIFEST=\"${CMS_PLUGIN_DIR}/openclaw.plugin.json\"\n DIAG_PLUGIN_NAME=\"diagnostics-otel\"\n DIAG_PLUGIN_DIR=\"/opt/openclaw/extensions/diagnostics-otel\"\n CMS_LICENSE_KEY=\"${HICLAW_CMS_LICENSE_KEY:-}\"\n CMS_PROJECT=\"${HICLAW_CMS_PROJECT:-}\"\n CMS_METRICS_ENABLED=\"${HICLAW_CMS_METRICS_ENABLED:-false}\"\n\n if [ ! -f \"${CMS_PLUGIN_MANIFEST}\" ]; then\n log \"WARNING: ${CMS_PLUGIN_NAME} manifest not found at ${CMS_PLUGIN_MANIFEST}, skipping plugin config\"\n else\n _missing=0\n [ -z \"${HICLAW_CMS_ENDPOINT:-}\" ] && log \"WARNING: HICLAW_CMS_ENDPOINT is required when HICLAW_CMS_TRACES_ENABLED=true\" && _missing=1\n [ -z \"${CMS_LICENSE_KEY:-}\" ] && log \"WARNING: HICLAW_CMS_LICENSE_KEY is required when HICLAW_CMS_TRACES_ENABLED=true\" && _missing=1\n [ -z \"${HICLAW_CMS_WORKSPACE:-}\" ] && log \"WARNING: HICLAW_CMS_WORKSPACE is required when HICLAW_CMS_TRACES_ENABLED=true\" && _missing=1\n\n if [ \"${_missing}\" = \"0\" ]; then\n CMS_SERVICE_NAME=\"${HICLAW_CMS_SERVICE_NAME:-hiclaw-manager}\"\n CMS_ENABLE_METRICS=\"${CMS_METRICS_ENABLED}\"\n DIAG_AVAILABLE=\"0\"\n _metrics_lc=\"$(echo \"${CMS_ENABLE_METRICS}\" | tr '[:upper:]' '[:lower:]')\"\n if [ \"${_metrics_lc}\" = \"true\" ]; then\n if [ -f \"${DIAG_PLUGIN_DIR}/package.json\" ]; then\n DIAG_AVAILABLE=\"1\"\n if [ ! -d \"${DIAG_PLUGIN_DIR}/node_modules\" ]; then\n log \"diagnostics-otel dependencies missing, installing...\"\n if (cd \"${DIAG_PLUGIN_DIR}\" && npm install --omit=dev --ignore-scripts >/tmp/hiclaw-diag-install.log 2>&1); then\n log \"diagnostics-otel dependencies installed\"\n else\n log \"WARNING: diagnostics-otel npm install failed, metrics plugin may not load\"\n fi\n else\n log \"diagnostics-otel dependencies already present\"\n fi\n else\n log \"WARNING: diagnostics-otel package.json not found at ${DIAG_PLUGIN_DIR}, metrics plugin may not load\"\n fi\n fi\n\n log \"Applying ${CMS_PLUGIN_NAME} config to openclaw.json...\"\n jq --arg pluginName \"${CMS_PLUGIN_NAME}\" \\\n --arg pluginDir \"${CMS_PLUGIN_DIR}\" \\\n --arg endpoint \"${HICLAW_CMS_ENDPOINT}\" \\\n --arg licenseKey \"${CMS_LICENSE_KEY}\" \\\n --arg armsProject \"${CMS_PROJECT}\" \\\n --arg cmsWorkspace \"${HICLAW_CMS_WORKSPACE}\" \\\n --arg serviceName \"${CMS_SERVICE_NAME}\" \\\n --arg diagPluginName \"${DIAG_PLUGIN_NAME}\" \\\n --arg diagPluginDir \"${DIAG_PLUGIN_DIR}\" \\\n --arg metricsRaw \"${CMS_ENABLE_METRICS}\" \\\n --arg diagAvailableRaw \"${DIAG_AVAILABLE}\" \\\n '\n .plugins = (.plugins // {})\n | .plugins.load = (.plugins.load // {})\n | .plugins.entries = (.plugins.entries // {})\n | if (.plugins.allow | type) != \"array\" then .plugins.allow = [] else . end\n | if (.plugins.allow | index($pluginName)) == null then .plugins.allow += [$pluginName] else . end\n | if (.plugins.load.paths | type) != \"array\" then .plugins.load.paths = [] else . end\n | if (.plugins.load.paths | index($pluginDir)) == null then .plugins.load.paths += [$pluginDir] else . end\n | .plugins.entries[$pluginName] = {\n \"enabled\": true,\n \"config\": {\n \"endpoint\": $endpoint,\n \"headers\": {\n \"x-arms-license-key\": $licenseKey,\n \"x-arms-project\": $armsProject,\n \"x-cms-workspace\": $cmsWorkspace\n },\n \"serviceName\": $serviceName\n }\n }\n\n # diagnostics-otel metrics (optional)\n | ($metricsRaw | ascii_downcase) as $m\n | ($diagAvailableRaw == \"1\") as $diagAvailable\n | (($m == \"true\") and $diagAvailable) as $metricsEnabled\n | if $metricsEnabled then\n (if (.plugins.allow | index($diagPluginName)) == null then .plugins.allow += [$diagPluginName] else . end)\n | (if (.plugins.load.paths | index($diagPluginDir)) == null then .plugins.load.paths += [$diagPluginDir] else . end)\n | .plugins.entries[$diagPluginName].enabled = true\n | .diagnostics = (.diagnostics // {})\n | .diagnostics.otel = (.diagnostics.otel // {})\n | .diagnostics.enabled = true\n | .diagnostics.otel.enabled = true\n | .diagnostics.otel.endpoint = $endpoint\n | .diagnostics.otel.protocol = (.diagnostics.otel.protocol // \"http/protobuf\")\n | .diagnostics.otel.headers = {\n \"x-arms-license-key\": $licenseKey,\n \"x-arms-project\": $armsProject,\n \"x-cms-workspace\": $cmsWorkspace\n }\n | .diagnostics.otel.serviceName = $serviceName\n | .diagnostics.otel.metrics = true\n | .diagnostics.otel.traces = (.diagnostics.otel.traces // false)\n | .diagnostics.otel.logs = (.diagnostics.otel.logs // false)\n else\n .\n end\n ' /root/manager-workspace/openclaw.json > /tmp/openclaw-cms.json && \\\n mv /tmp/openclaw-cms.json /root/manager-workspace/openclaw.json\n log \"${CMS_PLUGIN_NAME} config applied (metrics=${CMS_ENABLE_METRICS}, service=${CMS_SERVICE_NAME})\"\n else\n log \"Skipping ${CMS_PLUGIN_NAME} config due to missing required env vars\"\n fi\n fi\nfi\n\n# ============================================================\n# Detect container runtime (for Worker creation)\n# ============================================================\nsource /opt/hiclaw/scripts/lib/container-api.sh\nif container_api_available; then\n log \"Container runtime socket detected at ${CONTAINER_SOCKET} — direct Worker creation enabled\"\n export HICLAW_CONTAINER_RUNTIME=\"socket\"\nelif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ] || [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n log \"Cloud/K8s mode — Workers created via controller API\"\n export HICLAW_CONTAINER_RUNTIME=\"cloud\"\nelse\n log \"No container runtime found — Worker creation will output install commands\"\n export HICLAW_CONTAINER_RUNTIME=\"none\"\nfi\n\n# ============================================================\n# Upgrade Worker openclaw.json: merge known models + E2EE flag into existing configs\n# Existing workers in MinIO may have old single-model configs or missing encryption field.\n# Merge template models so they can hot-switch without restart.\n# ============================================================\nREGISTRY_FILE=\"/root/manager-workspace/workers-registry.json\"\nif [ -f \"${REGISTRY_FILE}\" ]; then\n # Use known-models.json (valid JSON) instead of template (contains ${VAR} placeholders)\n KNOWN_MODELS_FILE=\"/opt/hiclaw/configs/known-models.json\"\n if [ -f \"${KNOWN_MODELS_FILE}\" ]; then\n _KNOWN_MODELS=$(cat \"${KNOWN_MODELS_FILE}\")\n for _wname in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_wname}\" ] && continue\n _minio_path=\"${HICLAW_STORAGE_PREFIX}/agents/${_wname}/openclaw.json\"\n _tmp_in=\"/tmp/openclaw-${_wname}-models-upgrade-in.json\"\n if mc cp \"${_minio_path}\" \"${_tmp_in}\" 2>/dev/null; then\n _tmp_out=\"/tmp/openclaw-${_wname}-models-upgrade-out.json\"\n # Idempotent merge: add missing known models, rebuild aliases, set e2ee.\n # Always runs — jq deduplicates by model id, so re-runs are safe.\n jq --argjson known_models \"${_KNOWN_MODELS}\" \\\n --argjson e2ee \"${MATRIX_E2EE_ENABLED}\" '\n .models.providers[\"hiclaw-gateway\"].models as $existing\n | ($existing | map(.id)) as $existing_ids\n | ($known_models | map(select(.id as $id | $existing_ids | index($id) | not))) as $new\n | .models.providers[\"hiclaw-gateway\"].models = ($existing + $new)\n | (.models.providers[\"hiclaw-gateway\"].models | map({ (\"hiclaw-gateway/\" + .id): { \"alias\": .id } }) | add // {}) as $aliases\n | .agents.defaults.models = ((.agents.defaults.models // {}) + $aliases)\n | .channels.matrix.encryption = $e2ee\n | .channels.matrix.autoJoin = \"always\"\n | .tools = (.tools // {})\n | .tools.exec = ((.tools.exec // {}) + {\"host\":\"gateway\",\"security\":\"full\",\"ask\":\"off\"})\n | .tools.elevated = (.tools.elevated // {})\n | .tools.elevated.enabled = true\n | .tools.elevated.allowFrom |= ((. // {}) | .matrix = [\"*\"])\n | .agents.defaults.elevatedDefault = \"full\"\n ' \"${_tmp_in}\" > \"${_tmp_out}\" 2>/dev/null\n if ! diff -q \"${_tmp_in}\" \"${_tmp_out}\" > /dev/null 2>&1; then\n if mc cp \"${_tmp_out}\" \"${_minio_path}\" 2>/dev/null; then\n _new_count=$(jq '.models.providers[\"hiclaw-gateway\"].models | length' \"${_tmp_out}\" 2>/dev/null)\n log \"Worker ${_wname}: upgraded openclaw.json (models: ${_new_count}, e2ee: ${MATRIX_E2EE_ENABLED})\"\n fi\n fi\n rm -f \"${_tmp_in}\" \"${_tmp_out}\"\n fi\n done\n fi\nfi\n\n# ============================================================\n# Ensure Worker Matrix password files exist in MinIO (E2EE fix)\n# Workers need to re-login on restart to get a fresh device_id.\n# Older workers created before this fix won't have the password file.\n# ============================================================\nif [ -f \"${REGISTRY_FILE}\" ]; then\n for _wname in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_wname}\" ] && continue\n _creds_file=\"/data/worker-creds/${_wname}.env\"\n if [ -f \"${_creds_file}\" ]; then\n # Check if password file already exists in MinIO\n if ! mc stat \"${HICLAW_STORAGE_PREFIX}/agents/${_wname}/credentials/matrix/password\" > /dev/null 2>&1; then\n source \"${_creds_file}\"\n if [ -n \"${WORKER_PASSWORD}\" ]; then\n _tmp_pw=\"/tmp/matrix-pw-${_wname}\"\n echo -n \"${WORKER_PASSWORD}\" > \"${_tmp_pw}\"\n mc cp \"${_tmp_pw}\" \"${HICLAW_STORAGE_PREFIX}/agents/${_wname}/credentials/matrix/password\" 2>/dev/null \\\n && log \"Worker ${_wname}: wrote Matrix password to MinIO (E2EE re-login fix)\" \\\n || log \"Worker ${_wname}: WARNING: failed to write Matrix password to MinIO\"\n rm -f \"${_tmp_pw}\"\n fi\n fi\n fi\n done\nfi\n\n# ============================================================\n# Recreate Worker containers as needed after Manager restart.\n# Workers are on hiclaw-net; Docker DNS resolves *-local.hiclaw.io via\n# the Manager's network aliases, so IP changes don't require worker recreation.\n# Only recreate stopped/missing workers.\n# ============================================================\nif container_api_available; then\n REGISTRY_FILE=\"/root/manager-workspace/workers-registry.json\"\n if [ -f \"${REGISTRY_FILE}\" ]; then\n for _worker_name in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_worker_name}\" ] && continue\n\n # Skip remote workers — they are not Manager-managed containers.\n _deployment=$(jq -r --arg w \"${_worker_name}\" '.workers[$w].deployment // \"local\"' \"${REGISTRY_FILE}\" 2>/dev/null)\n if [ \"${_deployment}\" = \"remote\" ]; then\n log \"Worker ${_worker_name} is remote, skipping container recreate\"\n continue\n fi\n\n _status=$(container_status_worker \"${_worker_name}\")\n if [ \"${_status}\" = \"running\" ]; then\n log \"Worker running: ${_worker_name}, skipping\"\n continue\n fi\n # Container missing or stopped — recreate.\n log \"Worker container ${_status}: ${_worker_name}, recreating...\"\n _creds_file=\"/data/worker-creds/${_worker_name}.env\"\n if [ -f \"${_creds_file}\" ]; then\n source \"${_creds_file}\"\n _runtime=$(jq -r --arg w \"${_worker_name}\" '.workers[$w].runtime // \"openclaw\"' \"${REGISTRY_FILE}\" 2>/dev/null)\n _recreated=false\n for _attempt in 1 2 3; do\n local _env_map _create_body\n _env_map=$(jq -cn \\\n --arg name \"${_worker_name}\" \\\n --arg fak \"${_worker_name}\" \\\n --arg fsk \"${WORKER_MINIO_PASSWORD:-}\" \\\n --arg fs_domain \"${HICLAW_FS_DOMAIN:-fs-local.hiclaw.io}\" \\\n --arg controller_url \"${HICLAW_CONTROLLER_URL:-}\" \\\n '{\n \"HICLAW_WORKER_NAME\": $name,\n \"HICLAW_FS_ENDPOINT\": (\"http://\" + ($fs_domain | split(\":\")[0]) + \":9000\"),\n \"HICLAW_FS_ACCESS_KEY\": $fak,\n \"HICLAW_FS_SECRET_KEY\": $fsk\n }\n | if $controller_url != \"\" then . + {\"HICLAW_CONTROLLER_URL\": $controller_url} else . end')\n _create_body=$(jq -cn --arg name \"${_worker_name}\" --arg runtime \"${_runtime}\" --argjson env \"${_env_map}\" '{name: $name, runtime: $runtime, env: $env}')\n worker_backend_create \"${_create_body}\" > /dev/null 2>&1 && _recreated=true && break\n log \" Attempt ${_attempt}/3 failed for ${_worker_name}, retrying in $((5 * _attempt))s...\"\n sleep $((5 * _attempt))\n done\n if [ \"${_recreated}\" = true ]; then\n log \" Recreated ${_runtime} worker: ${_worker_name}\"\n else\n log \" ERROR: Failed to recreate ${_worker_name} after 3 attempts\"\n fi\n else\n log \" WARNING: No credentials found for ${_worker_name} (${_creds_file} missing), skipping\"\n fi\n done\n fi\nfi\n\n# ============================================================\n# Notify workers of builtin updates if upgrade happened\n# Builtin files (AGENTS.md, skills) are already synced by upgrade-builtins.sh\n#\n# Cooldown: skip notification if the last successful notify was within\n# NOTIFY_COOLDOWN_SECS (default 3600s / 1 hour). This prevents repeated\n# notifications when the Manager crash-loops and re-runs upgrade-builtins\n# on every restart (e.g. IMAGE_VERSION=latest always triggers upgrade).\n# ============================================================\nNOTIFY_COOLDOWN_SECS=\"${HICLAW_NOTIFY_COOLDOWN_SECS:-3600}\"\nNOTIFY_TS_FILE=\"/root/manager-workspace/.last-worker-notify-ts\"\n\nif [ -f /root/manager-workspace/.upgrade-pending-worker-notify ]; then\n _now=$(date +%s)\n _last_notify=$(cat \"${NOTIFY_TS_FILE}\" 2>/dev/null || echo \"0\")\n _elapsed=$(( _now - _last_notify ))\n\n if [ \"${_elapsed}\" -lt \"${NOTIFY_COOLDOWN_SECS}\" ]; then\n log \"Skipping worker builtin notification (last notify ${_elapsed}s ago, cooldown ${NOTIFY_COOLDOWN_SECS}s)\"\n rm -f /root/manager-workspace/.upgrade-pending-worker-notify\n else\n log \"Notifying workers about builtin updates...\"\n REGISTRY_FILE=\"/root/manager-workspace/workers-registry.json\"\n _notify_ok=false\n if [ -f \"${REGISTRY_FILE}\" ]; then\n for _worker_name in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_worker_name}\" ] && continue\n _room_id=$(jq -r --arg w \"${_worker_name}\" '.workers[$w].room_id // empty' \"${REGISTRY_FILE}\" 2>/dev/null)\n if [ -n \"${_room_id}\" ]; then\n _worker_id=\"@${_worker_name}:${MATRIX_DOMAIN}\"\n _txn_id=\"upgrade-$(date +%s%N)\"\n _msg=\"@${_worker_name}:${MATRIX_DOMAIN} Manager upgraded builtin files (AGENTS.md, skills). Please use your file-sync skill to sync the latest config.\"\n _raw=$(curl -s -w '\\nHTTP_CODE:%{http_code}' -X PUT \\\n \"${HICLAW_MATRIX_URL}/_matrix/client/v3/rooms/${_room_id}/send/m.room.message/${_txn_id}\" \\\n -H \"Authorization: Bearer ${MANAGER_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d \"{\\\"msgtype\\\":\\\"m.text\\\",\\\"body\\\":\\\"${_msg}\\\",\\\"m.mentions\\\":{\\\"user_ids\\\":[\\\"${_worker_id}\\\"]}}\" \\\n 2>&1) || true\n _http_code=$(echo \"${_raw}\" | tail -1 | sed 's/HTTP_CODE://')\n _notify_resp=$(echo \"${_raw}\" | sed '$d')\n if echo \"${_notify_resp}\" | jq -e '.event_id' > /dev/null 2>&1; then\n log \" Notified ${_worker_name}\"; _notify_ok=true\n else\n log \" WARNING: Failed to notify ${_worker_name} (HTTP ${_http_code}): ${_notify_resp}\"\n fi\n fi\n done\n fi\n # Record timestamp only if at least one notification succeeded\n if [ \"${_notify_ok}\" = true ]; then\n echo \"${_now}\" > \"${NOTIFY_TS_FILE}\"\n fi\n rm -f /root/manager-workspace/.upgrade-pending-worker-notify\n fi\nfi\n\n# ============================================================\n# Start Manager Agent\n# ============================================================\nlog \"Starting Manager Agent (${MANAGER_RUNTIME})...\"\n\n# HOME is already set to /root/manager-workspace via docker run -e HOME=...\ncd \"${HOME}\"\n\n# Ensure host credential symlinks exist under HOME\nif [ -d \"/host-share\" ]; then\n [ -f \"/host-share/.gitconfig\" ] && ln -sf \"/host-share/.gitconfig\" \"${HOME}/.gitconfig\"\nfi\n\nlog \"HOME=${HOME} (manager-workspace, host-mounted)\"\n\n# ── Render agent doc templates ────────────────────────────────────────────\n# Replace ${VAR} placeholders with actual values so the AI agent reads\n# plain text and never needs to resolve environment variables.\nexport MANAGER_MATRIX_TOKEN MANAGER_TOKEN HIGRESS_COOKIE_FILE\nRENDER=/opt/hiclaw/scripts/lib/render-skills.sh\nlog \"Rendering agent doc templates...\"\n# Manager-owned docs (workspace)\nbash \"$RENDER\" /root/manager-workspace/skills\nbash \"$RENDER\" /root/manager-workspace/skills-alpha\nbash \"$RENDER\" /root/manager-workspace AGENTS.md TOOLS.md HEARTBEAT.md SOUL.md\n# Worker templates (workspace + image) — rendered before push to MinIO\n# so Workers (including remote pip-install) receive plain text\nbash \"$RENDER\" /root/manager-workspace/worker-skills\nbash \"$RENDER\" /root/manager-workspace/worker-agent\nbash \"$RENDER\" /root/manager-workspace/copaw-worker-agent\nbash \"$RENDER\" /root/manager-workspace/hermes-worker-agent\nbash \"$RENDER\" /opt/hiclaw/agent/worker-skills\nbash \"$RENDER\" /opt/hiclaw/agent/worker-agent\nbash \"$RENDER\" /opt/hiclaw/agent/copaw-worker-agent\nbash \"$RENDER\" /opt/hiclaw/agent/hermes-worker-agent\nlog \"Agent doc templates rendered\"\n\n# Cloud mode: start background file sync (workspace ↔ OSS) and initial push\nif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n log \"Syncing initial workspace to OSS...\"\n ensure_mc_credentials\n mc mirror /root/manager-workspace/ \"${HICLAW_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" 2>/dev/null || true\n\n # Local → OSS: change-triggered sync\n (\n while true; do\n CHANGED=$(find /root/manager-workspace/ -type f -newermt \"15 seconds ago\" 2>/dev/null | head -1)\n if [ -n \"${CHANGED}\" ]; then\n ensure_mc_credentials 2>/dev/null || true\n mc mirror /root/manager-workspace/ \"${HICLAW_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" --exclude \".npm/**\" \\\n --exclude \".local/**\" --exclude \".mc/**\" 2>/dev/null || true\n fi\n sleep 10\n done\n ) &\n log \"Local→OSS sync started (PID: $!)\"\n\n # OSS → Local: periodic pull (shared data, agent configs)\n (\n while true; do\n sleep 300\n ensure_mc_credentials 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/shared/\" /root/hiclaw-fs/shared/ --overwrite --newer-than \"5m\" 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/agents/\" /root/hiclaw-fs/agents/ --overwrite --newer-than \"5m\" 2>/dev/null || true\n done\n ) &\n log \"OSS→Local sync started (every 5m, PID: $!)\"\nfi\n\n# K8s mode: start background file sync (workspace ↔ MinIO)\nif [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n log \"Syncing initial workspace to MinIO...\"\n mc mirror /root/manager-workspace/ \"${HICLAW_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" 2>/dev/null || true\n\n # Local → MinIO: change-triggered sync\n (\n while true; do\n CHANGED=$(find /root/manager-workspace/ -type f -newermt \"15 seconds ago\" 2>/dev/null | head -1)\n if [ -n \"${CHANGED}\" ]; then\n mc mirror /root/manager-workspace/ \"${HICLAW_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" --exclude \".npm/**\" \\\n --exclude \".local/**\" --exclude \".mc/**\" 2>/dev/null || true\n fi\n sleep 10\n done\n ) &\n log \"Local→MinIO sync started (PID: $!)\"\n\n # MinIO → Local: periodic pull (shared data, agent configs)\n (\n while true; do\n sleep 300\n mc mirror \"${HICLAW_STORAGE_PREFIX}/shared/\" /root/hiclaw-fs/shared/ --overwrite --newer-than \"5m\" 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/agents/\" /root/hiclaw-fs/agents/ --overwrite --newer-than \"5m\" 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/hiclaw-config/\" /root/hiclaw-fs/hiclaw-config/ --overwrite --newer-than \"15s\" 2>/dev/null || true\n done\n ) &\n log \"MinIO→Local sync started (every 5m, PID: $!)\"\nfi\n\n# ============================================================\n# Auto-generate Manager mcporter config for pre-configured MCP servers\n# If HICLAW_GITHUB_TOKEN was set at install time, setup-higress.sh already\n# configured GitHub MCP on Higress. Run setup-mcp-server.sh now so that\n# config/mcporter.json exists when the Agent starts — no need to ask user for PAT.\n# ============================================================\nif [ -n \"${HICLAW_GITHUB_TOKEN}\" ] && [ \"${HICLAW_RUNTIME}\" != \"aliyun\" ] && [ \"${HICLAW_RUNTIME}\" != \"k8s\" ]; then\n if [ ! -f \"${HOME}/config/mcporter.json\" ]; then\n log \"Auto-generating Manager mcporter config for GitHub MCP (HICLAW_GITHUB_TOKEN set)...\"\n bash /opt/hiclaw/agent/skills/mcp-server-management/scripts/setup-mcp-server.sh \\\n github \"${HICLAW_GITHUB_TOKEN}\" 2>&1 | while IFS= read -r line; do log \" [setup-mcp] ${line}\"; done || \\\n log \"WARNING: setup-mcp-server.sh failed — Agent may need to configure GitHub MCP manually\"\n else\n log \"Manager mcporter config already exists, skipping auto-generate\"\n fi\nfi\n\n# ============================================================\n# Runtime-specific startup\n# ============================================================\nif [ \"${MANAGER_RUNTIME}\" = \"copaw\" ]; then\n # Delegate to CoPaw startup script\n exec /opt/hiclaw/scripts/init/start-copaw-manager.sh\nelse\n # ── OpenClaw Runtime ─────────────────────────────────────────────────────\n log \"Starting OpenClaw Manager...\"\n\n export OPENCLAW_CONFIG_PATH=\"/root/manager-workspace/openclaw.json\"\n\n # Symlink to default OpenClaw config path so CLI commands find the config\n mkdir -p \"${HOME}/.openclaw\"\n ln -sf \"/root/manager-workspace/openclaw.json\" \"${HOME}/.openclaw/openclaw.json\"\n\n # Clean orphaned session write locks (e.g. from SIGKILL or crash before exit handlers)\n # Prevents \"session file locked (timeout 10000ms)\" when PID was reused\n find \"${HOME}/.openclaw/agents\" -name \"*.jsonl.lock\" -delete 2>/dev/null || true\n log \"Cleaned up any orphaned session write locks\"\n\n # Clean Matrix crypto storage (SQLite WAL may be corrupted after unclean shutdown)\n # Crypto state is re-negotiated on startup; losing it only means re-establishing E2EE sessions\n rm -rf \"${HOME}/.openclaw/matrix\" 2>/dev/null || true\n log \"Cleaned Matrix crypto storage (will re-establish E2EE sessions)\"\n\n # Launch OpenClaw\n # Disable full-process respawn so the CLI uses its internal restart loop.\n # Without this, config reload spawns a detached child and exits, then\n # supervisord restarts the CLI — resulting in two gateway processes.\n export OPENCLAW_NO_RESPAWN=1\n\n # Optional matrix-plugin trace logging — when HICLAW_MATRIX_DEBUG=1 is set\n # in the manager environment (propagated by install / supervisord), turn on\n # OPENCLAW_MATRIX_DEBUG so the matrix plugin emits structured INFO-level\n # lifecycle traces (sync.state transitions, room.invite/join, message\n # handler arrival + filter outcomes). Useful for diagnosing \"worker never\n # joined the room\" / \"manager never replied\" hangs without rebuilding the\n # image.\n if [ \"${HICLAW_MATRIX_DEBUG:-}\" = \"1\" ] && [ -z \"${OPENCLAW_MATRIX_DEBUG:-}\" ]; then\n export OPENCLAW_MATRIX_DEBUG=1\n log \"HICLAW_MATRIX_DEBUG=1 detected; OPENCLAW_MATRIX_DEBUG=1 exported for matrix plugin tracing\"\n fi\n\n exec openclaw gateway run --verbose --force\nfi\n"}, "88869f9be3bdd615515d1ed83e4a21aee9bb1697": {"t": 1785764746.0867445, "n": 68175, "h": "a552c7de7ac10fea", "c": "#!/bin/bash\n# start-manager-agent.sh - Initialize and start the Manager Agent\n# Supports local (supervisord), cloud (SAE), and K8s (Helm) deployments.\n# In local mode this is the last supervisord component to start (priority 800).\n# In cloud/k8s mode (HICLAW_RUNTIME=aliyun|k8s) this is the container entrypoint.\n#\n# Runtime selection:\n# HICLAW_MANAGER_RUNTIME=openclaw (default) - OpenClaw gateway mode\n# HICLAW_MANAGER_RUNTIME=copaw - CoPaw workspace mode\n# (hermes runtime is supported for Workers only; Managers run openclaw or copaw.)\n\nsource /opt/hiclaw/scripts/lib/hiclaw-env.sh\n\n# ============================================================\n# Runtime selection\n# ============================================================\nMANAGER_RUNTIME=\"${HICLAW_MANAGER_RUNTIME:-openclaw}\"\ncase \"${MANAGER_RUNTIME}\" in\n copaw)\n log \"Manager runtime: CoPaw (Python workspace)\"\n ;;\n *)\n log \"Manager runtime: OpenClaw (Node.js gateway)\"\n MANAGER_RUNTIME=\"openclaw\"\n ;;\nesac\n\n# ============================================================\n# Set timezone from TZ env var\n# ============================================================\nif [ -n \"${TZ}\" ] && [ -f \"/usr/share/zoneinfo/${TZ}\" ]; then\n ln -sf \"/usr/share/zoneinfo/${TZ}\" /etc/localtime\n echo \"${TZ}\" > /etc/timezone\n log \"Timezone set to ${TZ}\"\nfi\n\nexport MATRIX_DOMAIN=\"${HICLAW_MATRIX_DOMAIN:-matrix-local.hiclaw.io:8080}\"\nAI_GATEWAY_DOMAIN=\"${HICLAW_AI_GATEWAY_DOMAIN:-aigw-local.hiclaw.io}\"\n\n# ============================================================\n# YOLO mode promotion\n# ============================================================\n# In embedded mode the controller does not propagate HICLAW_YOLO to the\n# manager container, but installer / test scripts touch a marker file at\n# `${WORKSPACE}/yolo-mode` instead. Promote that marker to the env var so the\n# agent's documented YOLO check (`HICLAW_YOLO=1`) reliably detects it without\n# depending on filesystem lookups during a turn.\nif [ -z \"${HICLAW_YOLO:-}\" ] && [ -f /root/manager-workspace/yolo-mode ]; then\n export HICLAW_YOLO=1\n log \"YOLO mode marker detected at /root/manager-workspace/yolo-mode; HICLAW_YOLO=1 exported\"\nfi\n\n# ============================================================\n# Cloud/K8s mode: validate required environment variables + initial credentials\n# ============================================================\nif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ] || [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n : \"${HICLAW_MATRIX_URL:?HICLAW_MATRIX_URL is required}\"\n : \"${HICLAW_MATRIX_DOMAIN:?HICLAW_MATRIX_DOMAIN is required}\"\n : \"${HICLAW_AI_GATEWAY_URL:?HICLAW_AI_GATEWAY_URL is required}\"\n if [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n : \"${HICLAW_MANAGER_GATEWAY_KEY:?HICLAW_MANAGER_GATEWAY_KEY is required}\"\n : \"${HICLAW_MANAGER_PASSWORD:?HICLAW_MANAGER_PASSWORD is required (cloud containers are stateless, password must be injected)}\"\n fi\n if [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n # K8s mode: controller handles initialization (admin registration, Higress setup).\n # Manager only needs credentials injected by the ManagerReconciler.\n : \"${HICLAW_MANAGER_GATEWAY_KEY:?HICLAW_MANAGER_GATEWAY_KEY is required (injected by controller)}\"\n : \"${HICLAW_MANAGER_PASSWORD:?HICLAW_MANAGER_PASSWORD is required (injected by controller)}\"\n else\n # Cloud (aliyun) mode: Manager still does its own initialization\n : \"${HICLAW_REGISTRATION_TOKEN:?HICLAW_REGISTRATION_TOKEN is required}\"\n : \"${HICLAW_ADMIN_USER:?HICLAW_ADMIN_USER is required}\"\n : \"${HICLAW_ADMIN_PASSWORD:?HICLAW_ADMIN_PASSWORD is required}\"\n fi\n log \"${HICLAW_RUNTIME} mode: validating environment... OK\"\n log \" Matrix: ${HICLAW_MATRIX_URL}, AI Gateway: ${HICLAW_AI_GATEWAY_URL}, Storage: ${HICLAW_FS_BUCKET}\"\n if [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n ensure_mc_credentials || { log \"FATAL: Initial STS credential fetch failed\"; exit 1; }\n fi\nfi\n\n# ============================================================\n# Local mode: host symlinks, /etc/hosts, wait for local services\n# ============================================================\nif [ \"${HICLAW_RUNTIME}\" != \"aliyun\" ] && [ \"${HICLAW_RUNTIME}\" != \"k8s\" ]; then\n # Create symlink for host directory access\n if [ -d \"/host-share\" ]; then\n ORIGINAL_HOST_HOME=\"${HOST_ORIGINAL_HOME:-$HOME}\"\n if [ ! -e \"${ORIGINAL_HOST_HOME}\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/root\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/data\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/host-share\" ]; then\n mkdir -p \"$(dirname \"${ORIGINAL_HOST_HOME}\")\"\n ln -sfn /host-share \"${ORIGINAL_HOST_HOME}\"\n log \"Created symlink: ${ORIGINAL_HOST_HOME} -> /host-share\"\n else\n ln -sfn /host-share /root/host-home\n log \"Created fallback symlink: /root/host-home -> /host-share\"\n fi\n fi\n\n # Add local domains to /etc/hosts\n HOSTS_DOMAINS=\"${MATRIX_DOMAIN%%:*} ${HICLAW_MATRIX_CLIENT_DOMAIN:-matrix-client-local.hiclaw.io} ${AI_GATEWAY_DOMAIN} ${HICLAW_FS_DOMAIN:-fs-local.hiclaw.io}\"\n if ! grep -q \"${AI_GATEWAY_DOMAIN}\" /etc/hosts 2>/dev/null; then\n echo \"127.0.0.1 ${HOSTS_DOMAINS}\" >> /etc/hosts\n log \"Added local domains to /etc/hosts\"\n fi\n\n # Wait for local infrastructure\n waitForService \"Higress Gateway\" \"127.0.0.1\" 8080 180\n waitForService \"Higress Console\" \"127.0.0.1\" 8001 180\n waitForService \"Tuwunel\" \"127.0.0.1\" 6167 120\n waitForHTTP \"Tuwunel Matrix API\" \"${HICLAW_MATRIX_URL}/_tuwunel/server_version\" 120\n waitForService \"MinIO\" \"127.0.0.1\" 9000 120\nelse\n # Cloud/K8s mode: wait for external Tuwunel\n log \"Waiting for Tuwunel Matrix server at ${HICLAW_MATRIX_URL}...\"\n _retry=0\n while [ \"${_retry}\" -lt 30 ]; do\n if curl -sf \"${HICLAW_MATRIX_URL}/_matrix/client/versions\" > /dev/null 2>&1; then\n log \"Tuwunel is ready\"\n break\n fi\n _retry=$((_retry + 1))\n log \" Waiting for Tuwunel (attempt ${_retry}/30)...\"\n sleep 5\n done\n if [ \"${_retry}\" -ge 30 ]; then\n log \"ERROR: Tuwunel not reachable at ${HICLAW_MATRIX_URL}\"\n exit 1\n fi\nfi\n\n# ============================================================\n# Auto-generate secrets if not provided via environment\n# Persisted to /data so they survive container restart\n# ============================================================\nSECRETS_FILE=\"/data/hiclaw-secrets.env\"\nif [ -f \"${SECRETS_FILE}\" ]; then\n source \"${SECRETS_FILE}\"\n log \"Loaded persisted secrets from ${SECRETS_FILE}\"\nfi\n\nif [ -z \"${HICLAW_MANAGER_GATEWAY_KEY}\" ]; then\n export HICLAW_MANAGER_GATEWAY_KEY=\"$(generateKey 32)\"\n log \"Auto-generated HICLAW_MANAGER_GATEWAY_KEY\"\nfi\nif [ -z \"${HICLAW_MANAGER_PASSWORD}\" ]; then\n export HICLAW_MANAGER_PASSWORD=\"$(generateKey 16)\"\n log \"Auto-generated HICLAW_MANAGER_PASSWORD\"\nfi\n\n# Persist secrets so they survive supervisord restart\nmkdir -p /data\ncat > \"${SECRETS_FILE}\" <<EOF\nexport HICLAW_MANAGER_GATEWAY_KEY=\"${HICLAW_MANAGER_GATEWAY_KEY}\"\nexport HICLAW_MANAGER_PASSWORD=\"${HICLAW_MANAGER_PASSWORD}\"\nEOF\nchmod 600 \"${SECRETS_FILE}\"\n\n# Cloud mode: pull workspace from OSS before initialization\nif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n HICLAW_FS=\"/root/hiclaw-fs\"\n mkdir -p \"${HICLAW_FS}/shared\" \"${HICLAW_FS}/agents\"\n log \"Pulling workspace from OSS...\"\n ensure_mc_credentials\n mc mirror \"${HICLAW_STORAGE_PREFIX}/manager/\" /root/manager-workspace/ --overwrite 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/shared/\" \"${HICLAW_FS}/shared/\" --overwrite 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/agents/\" \"${HICLAW_FS}/agents/\" --overwrite 2>/dev/null || true\n # Symlink hiclaw-fs into workspace for agent access\n ln -sfn \"${HICLAW_FS}\" /root/manager-workspace/hiclaw-fs\nfi\n\n# K8s mode: sync workspace from cluster-internal MinIO\nif [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n HICLAW_FS=\"/root/hiclaw-fs\"\n mkdir -p \"${HICLAW_FS}/shared\" \"${HICLAW_FS}/agents\" \"${HICLAW_FS}/hiclaw-config\"\n log \"Configuring mc alias for cluster MinIO...\"\n mc alias set hiclaw \"${HICLAW_FS_ENDPOINT}\" \"${HICLAW_FS_ACCESS_KEY}\" \"${HICLAW_FS_SECRET_KEY}\"\n log \"Syncing workspace from MinIO...\"\n mc mirror \"${HICLAW_STORAGE_PREFIX}/manager/\" /root/manager-workspace/ --overwrite 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/\" \"${HICLAW_FS}/\" --overwrite 2>/dev/null || true\n ln -sfn \"${HICLAW_FS}\" /root/manager-workspace/hiclaw-fs\n touch \"${HICLAW_FS}/.initialized\"\nfi\n\n# ============================================================\n# Initialize / upgrade Manager workspace\n# First boot: full init via upgrade-builtins.sh\n# Subsequent boots: compare image version; upgrade only if changed\n# ============================================================\nmkdir -p /root/manager-workspace\n\nIMAGE_VERSION=$(cat /opt/hiclaw/agent/.builtin-version 2>/dev/null || echo \"unknown\")\nINSTALLED_VERSION=$(cat /root/manager-workspace/.builtin-version 2>/dev/null || echo \"\")\n\nif [ ! -f /root/manager-workspace/.initialized ]; then\n log \"First boot: initializing manager workspace...\"\n bash /opt/hiclaw/scripts/init/upgrade-builtins.sh\n touch /root/manager-workspace/.initialized\n log \"Manager workspace initialized (version: ${IMAGE_VERSION})\"\nelif [ \"${IMAGE_VERSION}\" != \"${INSTALLED_VERSION}\" ] || [ \"${IMAGE_VERSION}\" = \"latest\" ]; then\n log \"Upgrade detected: ${INSTALLED_VERSION} -> ${IMAGE_VERSION}${IMAGE_VERSION:+ (latest: always upgrade)}\"\n bash /opt/hiclaw/scripts/init/upgrade-builtins.sh\n log \"Manager workspace upgraded to version: ${IMAGE_VERSION}\"\nelse\n log \"Workspace up to date (version: ${IMAGE_VERSION})\"\nfi\n\n# Local mode: wait for mc mirror initialization (shared + worker data in /root/hiclaw-fs/)\nif [ \"${HICLAW_RUNTIME}\" != \"aliyun\" ] && [ \"${HICLAW_RUNTIME}\" != \"k8s\" ]; then\n log \"Waiting for MinIO storage initialization...\"\n _minio_wait=0\n while [ ! -f /root/hiclaw-fs/.initialized ]; do\n sleep 2\n _minio_wait=$(( _minio_wait + 1 ))\n if [ \"${_minio_wait}\" -ge 60 ]; then\n log \"ERROR: MinIO storage initialization timed out after 120s\"\n exit 1\n fi\n done\n log \"MinIO storage initialized\"\nfi\n\n# ============================================================\n# Register Matrix users via Registration API (single-step, no UIAA)\n# K8s mode: skip — controller Initializer + ManagerReconciler already did this\n# ============================================================\nif [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n log \"K8s mode: skipping Matrix registration (handled by controller)\"\n # Controller injects HICLAW_MANAGER_PASSWORD via env; login to get token\n log \"Obtaining Manager Matrix access token...\"\n _LOGIN_RESPONSE=$(curl -s -X POST ${HICLAW_MATRIX_URL}/_matrix/client/v3/login \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"type\": \"m.login.password\",\n \"identifier\": {\"type\": \"m.id.user\", \"user\": \"manager\"},\n \"password\": \"'\"${HICLAW_MANAGER_PASSWORD}\"'\"\n }' 2>&1)\n MANAGER_TOKEN=$(echo \"${_LOGIN_RESPONSE}\" | jq -r '.access_token' 2>/dev/null)\n if [ -z \"${MANAGER_TOKEN}\" ] || [ \"${MANAGER_TOKEN}\" = \"null\" ]; then\n log \"ERROR: Failed to obtain Manager Matrix token\"\n log \"ERROR: Login response was: ${_LOGIN_RESPONSE}\"\n exit 1\n fi\n log \"Manager Matrix token obtained (token prefix: ${MANAGER_TOKEN:0:10}...)\"\nelse\nlog \"Registering human admin Matrix account...\"\ncurl -sf -X POST ${HICLAW_MATRIX_URL}/_matrix/client/v3/register \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"username\": \"'\"${HICLAW_ADMIN_USER}\"'\",\n \"password\": \"'\"${HICLAW_ADMIN_PASSWORD}\"'\",\n \"auth\": {\n \"type\": \"m.login.registration_token\",\n \"token\": \"'\"${HICLAW_REGISTRATION_TOKEN}\"'\"\n }\n }' > /dev/null 2>&1 || log \"Admin account may already exist\"\n\nlog \"Registering Manager Agent Matrix account...\"\ncurl -sf -X POST ${HICLAW_MATRIX_URL}/_matrix/client/v3/register \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"username\": \"manager\",\n \"password\": \"'\"${HICLAW_MANAGER_PASSWORD}\"'\",\n \"auth\": {\n \"type\": \"m.login.registration_token\",\n \"token\": \"'\"${HICLAW_REGISTRATION_TOKEN}\"'\"\n }\n }' > /dev/null 2>&1 || log \"Manager account may already exist\"\n\n# Get Manager Agent's Matrix access token\nlog \"Obtaining Manager Matrix access token...\"\n_LOGIN_RESPONSE=$(curl -s -X POST ${HICLAW_MATRIX_URL}/_matrix/client/v3/login \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"type\": \"m.login.password\",\n \"identifier\": {\"type\": \"m.id.user\", \"user\": \"manager\"},\n \"password\": \"'\"${HICLAW_MANAGER_PASSWORD}\"'\"\n }' 2>&1)\n_LOGIN_EXIT=$?\nlog \"Matrix login HTTP exit code: ${_LOGIN_EXIT}\"\nlog \"Matrix login response: ${_LOGIN_RESPONSE}\"\n\nMANAGER_TOKEN=$(echo \"${_LOGIN_RESPONSE}\" | jq -r '.access_token' 2>/dev/null)\n\nif [ -z \"${MANAGER_TOKEN}\" ] || [ \"${MANAGER_TOKEN}\" = \"null\" ]; then\n log \"ERROR: Failed to obtain Manager Matrix token (exit=${_LOGIN_EXIT})\"\n log \"ERROR: Login response was: ${_LOGIN_RESPONSE}\"\n exit 1\nfi\nlog \"Manager Matrix token obtained (token prefix: ${MANAGER_TOKEN:0:10}...)\"\nfi\n\n# ============================================================\n# Higress Console initialization\n# Docker mode: full setup-higress.sh (internal Higress at localhost:8001)\n# K8s mode: skip — controller Initializer handles Higress setup\n# Cloud (aliyun) mode: skip entirely (Higress managed externally)\n# ============================================================\n_HIGRESS_CONSOLE_URL=\"\"\n_HIGRESS_USER=\"${HICLAW_ADMIN_USER}\"\n_HIGRESS_PASS=\"${HICLAW_ADMIN_PASSWORD}\"\nif [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n log \"K8s mode: skipping Higress initialization (handled by controller)\"\nelif [ \"${HICLAW_RUNTIME}\" != \"aliyun\" ]; then\n _HIGRESS_CONSOLE_URL=\"http://127.0.0.1:8001\"\nfi\n\nif [ -n \"${_HIGRESS_CONSOLE_URL}\" ]; then\n COOKIE_FILE=\"/tmp/higress-session-cookie\"\n\n log \"Waiting for Higress Console (${_HIGRESS_CONSOLE_URL}) to be fully ready and initializing admin...\"\n INIT_DONE=false\n for i in $(seq 1 90); do\n INIT_RESULT=$(curl -s -X POST \"${_HIGRESS_CONSOLE_URL}/system/init\" \\\n -H 'Content-Type: application/json' \\\n -d '{\"adminUser\":{\"name\":\"'\"${_HIGRESS_USER}\"'\",\"password\":\"'\"${_HIGRESS_PASS}\"'\",\"displayName\":\"'\"${_HIGRESS_USER}\"'\"}}' 2>/dev/null) || true\n if echo \"${INIT_RESULT}\" | grep -qE '\"success\":true|already.?init' 2>/dev/null; then\n INIT_DONE=true\n break\n fi\n if echo \"${INIT_RESULT}\" | grep -q '\"name\"' 2>/dev/null; then\n INIT_DONE=true\n break\n fi\n sleep 2\n done\n\n if [ \"${INIT_DONE}\" != \"true\" ]; then\n log \"ERROR: Higress Console did not become ready within 180s\"\n exit 1\n fi\n log \"Higress Console init done\"\n\n log \"Logging into Higress Console...\"\n LOGIN_OK=false\n for i in $(seq 1 10); do\n HTTP_CODE=$(curl -s -o /dev/null -w '%{http_code}' -X POST \"${_HIGRESS_CONSOLE_URL}/session/login\" \\\n -H 'Content-Type: application/json' \\\n -c \"${COOKIE_FILE}\" \\\n -d '{\"username\":\"'\"${_HIGRESS_USER}\"'\",\"password\":\"'\"${_HIGRESS_PASS}\"'\"}' 2>/dev/null) || true\n if { [ \"${HTTP_CODE}\" = \"200\" ] || [ \"${HTTP_CODE}\" = \"201\" ]; } && [ -f \"${COOKIE_FILE}\" ] && [ -s \"${COOKIE_FILE}\" ]; then\n LOGIN_OK=true\n break\n fi\n log \"Login attempt $i (HTTP ${HTTP_CODE}), retrying in 3s...\"\n sleep 3\n done\n\n if [ \"${LOGIN_OK}\" != \"true\" ]; then\n log \"ERROR: Could not login to Higress Console after retries\"\n exit 1\n fi\n log \"Higress Console login successful\"\n\n VERIFY_CODE=$(curl -s -o /dev/null -w '%{http_code}' \"${_HIGRESS_CONSOLE_URL}/v1/consumers\" -b \"${COOKIE_FILE}\" 2>/dev/null) || true\n if [ \"${VERIFY_CODE}\" = \"200\" ]; then\n log \"Console session verified (cookie valid)\"\n else\n log \"WARNING: Console session may be invalid (verify returned HTTP ${VERIFY_CODE})\"\n rm -f \"${COOKIE_FILE}\"\n for i in $(seq 1 5); do\n curl -s -o /dev/null -w '%{http_code}' -X POST \"${_HIGRESS_CONSOLE_URL}/session/login\" \\\n -H 'Content-Type: application/json' \\\n -c \"${COOKIE_FILE}\" \\\n -d '{\"username\":\"'\"${_HIGRESS_USER}\"'\",\"password\":\"'\"${_HIGRESS_PASS}\"'\"}' 2>/dev/null\n VERIFY2=$(curl -s -o /dev/null -w '%{http_code}' \"${_HIGRESS_CONSOLE_URL}/v1/consumers\" -b \"${COOKIE_FILE}\" 2>/dev/null) || true\n if [ \"${VERIFY2}\" = \"200\" ]; then\n log \"Re-login successful, session verified\"\n break\n fi\n sleep 2\n done\n fi\n\n export HIGRESS_COOKIE_FILE=\"${COOKIE_FILE}\"\n export HIGRESS_CONSOLE_URL=\"${_HIGRESS_CONSOLE_URL}\"\n\n if [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n # K8s mode: lightweight Higress config — only what's needed for LLM access\n source /opt/hiclaw/scripts/lib/base.sh\n _k8s_higress_api() {\n local method=\"$1\" path=\"$2\" desc=\"$3\"; shift 3; local body=\"$*\"\n local tmpfile; tmpfile=$(mktemp)\n local http_code\n http_code=$(curl -s -o \"${tmpfile}\" -w '%{http_code}' -X \"${method}\" \"${_HIGRESS_CONSOLE_URL}${path}\" \\\n -b \"${COOKIE_FILE}\" -H 'Content-Type: application/json' -d \"${body}\" 2>/dev/null) || true\n local response; response=$(cat \"${tmpfile}\" 2>/dev/null); rm -f \"${tmpfile}\"\n if echo \"${response}\" | grep -q '\"success\":true' 2>/dev/null; then\n log \"${desc} ... OK\"\n elif [ \"${http_code}\" = \"409\" ]; then\n log \"${desc} ... already exists, skipping\"\n elif [ \"${http_code}\" = \"200\" ] || [ \"${http_code}\" = \"201\" ] || [ \"${http_code}\" = \"204\" ]; then\n log \"${desc} ... OK (HTTP ${http_code})\"\n else\n log \"WARNING: ${desc} ... (HTTP ${http_code}): ${response}\"\n fi\n }\n\n # 1. Service Sources (DNS type → K8s Service FQDN)\n # Extract host:port from URLs for Higress service source registration\n _TUWUNEL_HOST=$(echo \"${HICLAW_MATRIX_URL}\" | sed 's|^http[s]*://||')\n _TUWUNEL_DOMAIN=$(echo \"${_TUWUNEL_HOST}\" | cut -d: -f1)\n _TUWUNEL_PORT=$(echo \"${_TUWUNEL_HOST}\" | cut -d: -f2)\n _k8s_higress_api POST /v1/service-sources \"Registering Tuwunel service source\" \\\n '{\"type\":\"dns\",\"name\":\"tuwunel\",\"domain\":\"'\"${_TUWUNEL_DOMAIN}\"'\",\"port\":'\"${_TUWUNEL_PORT}\"'}'\n\n if [ -n \"${HICLAW_ELEMENT_WEB_URL:-}\" ]; then\n _ELEMENT_HOST=$(echo \"${HICLAW_ELEMENT_WEB_URL}\" | sed 's|^http[s]*://||')\n _ELEMENT_DOMAIN=$(echo \"${_ELEMENT_HOST}\" | cut -d: -f1)\n _ELEMENT_PORT=$(echo \"${_ELEMENT_HOST}\" | cut -d: -f2)\n _k8s_higress_api POST /v1/service-sources \"Registering Element Web service source\" \\\n '{\"type\":\"dns\",\"name\":\"element-web\",\"domain\":\"'\"${_ELEMENT_DOMAIN}\"'\",\"port\":'\"${_ELEMENT_PORT}\"'}'\n fi\n\n # 2. Manager Consumer (key-auth)\n _k8s_higress_api POST /v1/consumers \"Creating Manager consumer\" \\\n '{\"name\":\"manager\",\"credentials\":[{\"type\":\"key-auth\",\"source\":\"BEARER\",\"values\":[\"'\"${HICLAW_MANAGER_GATEWAY_KEY}\"'\"]}]}'\n\n # 3. LLM Provider\n _LLM_PROVIDER=\"${HICLAW_LLM_PROVIDER:-qwen}\"\n _LLM_API_URL=\"${HICLAW_LLM_API_URL:-}\"\n if [ -z \"${_LLM_API_URL}\" ]; then\n case \"${_LLM_PROVIDER}\" in\n qwen) _LLM_API_URL=\"https://dashscope.aliyuncs.com/compatible-mode/v1\" ;;\n esac\n fi\n # 4. LLM Provider type-specific config\n case \"${_LLM_PROVIDER}\" in\n qwen)\n _k8s_higress_api POST /v1/ai/providers \"Creating LLM provider (qwen)\" \\\n '{\"type\":\"qwen\",\"name\":\"qwen\",\"tokens\":[\"'\"${HICLAW_LLM_API_KEY}\"'\"],\"protocol\":\"openai/v1\",\"tokenFailoverConfig\":{\"enabled\":false},\"rawConfigs\":{\"qwenEnableSearch\":false,\"qwenEnableCompatible\":true,\"qwenFileIds\":[],\"hiclawMode\":true}}'\n ;;\n *)\n _BODY='{\"name\":\"'\"${_LLM_PROVIDER}\"'\",\"type\":\"openai\",\"tokens\":[\"'\"${HICLAW_LLM_API_KEY}\"'\"],\"modelMapping\":{},\"protocol\":\"openai/v1\",\"rawConfigs\":{\"hiclawMode\":true}}'\n _k8s_higress_api POST /v1/ai/providers \"Creating LLM provider (${_LLM_PROVIDER})\" \"${_BODY}\"\n ;;\n esac\n\n # 5. AI Route (bind provider + consumer auth, /v1 prefix to avoid clash with Element Web catch-all)\n _k8s_higress_api POST /v1/ai/routes \"Creating AI Gateway route\" \\\n '{\"name\":\"default-ai-route\",\"domains\":[],\"pathPredicate\":{\"matchType\":\"PRE\",\"matchValue\":\"/v1\",\"caseSensitive\":false},\"upstreams\":[{\"provider\":\"'\"${_LLM_PROVIDER}\"'\",\"weight\":100,\"modelMapping\":{}}],\"authConfig\":{\"enabled\":true,\"allowedCredentialTypes\":[\"key-auth\"],\"allowedConsumers\":[\"manager\"]}}'\n\n # 6. Matrix Homeserver Route (/_matrix/* → Tuwunel, no auth)\n _k8s_higress_api POST /v1/routes \"Creating Matrix Homeserver route\" \\\n '{\"name\":\"matrix-homeserver\",\"domains\":[],\"path\":{\"matchType\":\"PRE\",\"matchValue\":\"/_matrix\"},\"services\":[{\"name\":\"tuwunel.dns\",\"port\":'\"${_TUWUNEL_PORT}\"',\"weight\":100}]}'\n\n # 7. Element Web Route (/ catch-all → Element Web, no auth)\n if [ -n \"${HICLAW_ELEMENT_WEB_URL:-}\" ]; then\n _k8s_higress_api POST /v1/routes \"Creating Element Web route\" \\\n '{\"name\":\"element-web\",\"domains\":[],\"path\":{\"matchType\":\"PRE\",\"matchValue\":\"/\"},\"services\":[{\"name\":\"element-web.dns\",\"port\":'\"${_ELEMENT_PORT}\"',\"weight\":100}]}'\n fi\n\n # 8. Remove Higress default landing page (Exact match on / takes precedence over Element Web catch-all)\n _k8s_higress_api DELETE /v1/routes/default \"Removing Higress default landing route\"\n\n log \"K8s Higress lightweight setup complete\"\n\n # Wait for AI plugin activation (~45s for first config)\n log \"Waiting for AI Gateway plugin activation (45s)...\"\n sleep 45\n else\n # Docker mode: full setup with all routes, domains, MCP servers\n /opt/hiclaw/scripts/init/setup-higress.sh\n fi\nfi\n\n# ============================================================\n# Create admin DM room, persist to state.json, send welcome message\n# K8s mode: skip — controller ProvisionManager creates the Admin DM\n# room (Step 4 in service/provisioner.go) AND reconcileManagerWelcome\n# delivers the first-boot onboarding prompt once OpenClaw inside this\n# container has joined the room. In k8s mode the manager intentionally\n# does NOT have the admin password (only HICLAW_ADMIN_USER), so it\n# could not log in as admin to send the welcome itself anyway. The\n# Manager Agent discovers its admin DM room on first heartbeat via\n# state.json / `manage-state.sh --action set-admin-dm` (see HEARTBEAT.md\n# Step 1) — it does not need it to be pre-injected by this script.\n# Runs in both local and cloud modes (idempotent)\n# ============================================================\nif [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n log \"K8s mode: skipping admin DM room creation and welcome message (both handled by hiclaw-controller)\"\nelse\nMANAGER_FULL_ID=\"@manager:${MATRIX_DOMAIN}\"\nADMIN_FULL_ID=\"@${HICLAW_ADMIN_USER}:${MATRIX_DOMAIN}\"\n\nlog \"Logging in as admin to create DM room...\"\n_ADMIN_LOGIN=$(curl -sf -X POST \"${HICLAW_MATRIX_URL}/_matrix/client/v3/login\" \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"type\": \"m.login.password\",\n \"identifier\": {\"type\": \"m.id.user\", \"user\": \"'\"${HICLAW_ADMIN_USER}\"'\"},\n \"password\": \"'\"${HICLAW_ADMIN_PASSWORD}\"'\"\n }' 2>&1) || true\n\nADMIN_MATRIX_TOKEN=$(echo \"${_ADMIN_LOGIN}\" | jq -r '.access_token // empty' 2>/dev/null)\nif [ -z \"${ADMIN_MATRIX_TOKEN}\" ]; then\n log \"WARNING: Failed to login as admin, skipping DM room creation\"\nelse\n # Search for existing DM room with Manager (idempotent)\n DM_ROOM_ID=\"\"\n _JOINED_ROOMS=$(curl -sf \"${HICLAW_MATRIX_URL}/_matrix/client/v3/joined_rooms\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" 2>/dev/null \\\n | jq -r '.joined_rooms[]' 2>/dev/null) || true\n for _rid in ${_JOINED_ROOMS}; do\n _members=$(curl -sf \"${HICLAW_MATRIX_URL}/_matrix/client/v3/rooms/${_rid}/members\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" 2>/dev/null \\\n | jq -r '.chunk[].state_key' 2>/dev/null) || continue\n _count=$(echo \"${_members}\" | wc -l | xargs)\n if [ \"${_count}\" = \"2\" ] && echo \"${_members}\" | grep -q \"@manager:\"; then\n DM_ROOM_ID=\"${_rid}\"\n break\n fi\n done\n\n if [ -n \"${DM_ROOM_ID}\" ]; then\n log \"Existing DM room found: ${DM_ROOM_ID}\"\n else\n log \"Creating DM room with Manager...\"\n _RAW=$(curl -s -w '\\nHTTP_CODE:%{http_code}' -X POST \"${HICLAW_MATRIX_URL}/_matrix/client/v3/createRoom\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d \"{\\\"is_direct\\\":true,\\\"invite\\\":[\\\"${MANAGER_FULL_ID}\\\"],\\\"preset\\\":\\\"trusted_private_chat\\\"}\" 2>&1) || true\n _HTTP_CODE=$(echo \"${_RAW}\" | tail -1 | sed 's/HTTP_CODE://')\n _CREATE_RESP=$(echo \"${_RAW}\" | sed '$d')\n DM_ROOM_ID=$(echo \"${_CREATE_RESP}\" | jq -r '.room_id // empty' 2>/dev/null)\n if [ -n \"${DM_ROOM_ID}\" ]; then\n log \"DM room created: ${DM_ROOM_ID}\"\n else\n log \"WARNING: Failed to create DM room (HTTP ${_HTTP_CODE}): ${_CREATE_RESP}\"\n fi\n fi\n\n # Persist admin DM room ID to state.json\n if [ -n \"${DM_ROOM_ID}\" ]; then\n STATE_SCRIPT=\"/opt/hiclaw/agent/skills/task-management/scripts/manage-state.sh\"\n if [ -f \"${STATE_SCRIPT}\" ]; then\n bash \"${STATE_SCRIPT}\" --action init 2>/dev/null || true\n bash \"${STATE_SCRIPT}\" --action set-admin-dm --room-id \"${DM_ROOM_ID}\" 2>/dev/null || true\n log \"Admin DM room persisted to state.json: ${DM_ROOM_ID}\"\n fi\n fi\n\n # Schedule welcome message in background (only on first boot)\n if [ -n \"${DM_ROOM_ID}\" ] && [ ! -f \"/root/manager-workspace/soul-configured\" ]; then\n log \"Scheduling welcome message (background, waiting for OpenClaw to start)...\"\n (\n _HICLAW_LANGUAGE=\"${HICLAW_LANGUAGE:-zh}\"\n _HICLAW_TIMEZONE=\"${TZ:-Asia/Shanghai}\"\n _wait=0\n _ready=false\n while [ \"${_wait}\" -lt 300 ]; do\n if curl -sf http://127.0.0.1:18799/ > /dev/null 2>&1; then\n _ready=true\n break\n fi\n sleep 3\n _wait=$((_wait + 3))\n done\n if [ \"${_ready}\" != \"true\" ]; then\n echo \"[manager] WARNING: OpenClaw gateway not ready within 300s, skipping welcome message\"\n exit 0\n fi\n # Ensure Manager has joined the DM room before sending the welcome\n # message. Without this, there is a race between OpenClaw's Matrix\n # auto-join and the message send — the message may land before Manager\n # joins, so OpenClaw's /sync never picks it up.\n _join_ok=false\n for _join_attempt in 1 2 3; do\n if curl -sf -X POST \"${HICLAW_MATRIX_URL}/_matrix/client/v3/rooms/${DM_ROOM_ID}/join\" \\\n -H \"Authorization: Bearer ${MANAGER_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d '{}' > /dev/null 2>&1; then\n echo \"[manager] Manager joined DM room before welcome message\"\n _join_ok=true\n break\n fi\n sleep 2\n done\n if [ \"${_join_ok}\" != \"true\" ]; then\n echo \"[manager] WARNING: Manager join request failed after 3 attempts (may already be joined)\"\n fi\n _welcome_msg=\"This is an automated message from the HiClaw setup. This is a fresh installation.\n\n--- Installation Context ---\nUser Language: ${_HICLAW_LANGUAGE} (zh = Chinese, en = English)\nUser Timezone: ${_HICLAW_TIMEZONE} (IANA timezone identifier)\n---\n\nYou are an AI agent that manages a team of worker agents. Your identity and personality have not been configured yet — the human admin is about to meet you for the first time.\n\nPlease begin the onboarding conversation:\n\n1. Greet the admin warmly and briefly describe what you can do (coordinate workers, manage tasks, run multi-agent projects)\n2. The user has selected \\\"${_HICLAW_LANGUAGE}\\\" as their preferred language during installation. Use this language for your greeting and all subsequent communication.\n3. The user's timezone is ${_HICLAW_TIMEZONE}. Based on this timezone, you may infer their likely region and suggest additional language options.\n4. Ask them: a) What would they like to call you? b) Communication style preference? c) Any behavior guidelines? d) Confirm default language\n5. After they reply, write their preferences to ~/SOUL.md\n6. Confirm what you wrote, and ask if they would like to adjust anything\n7. Once confirmed, run: touch ~/soul-configured\n\nThe human admin will start chatting shortly.\"\n _txn_id=\"welcome-$(date +%s)\"\n _payload=$(jq -nc --arg body \"${_welcome_msg}\" '{\"msgtype\":\"m.text\",\"body\":$body}')\n _raw=$(curl -s -w '\\nHTTP_CODE:%{http_code}' -X PUT \"${HICLAW_MATRIX_URL}/_matrix/client/v3/rooms/${DM_ROOM_ID}/send/m.room.message/${_txn_id}\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d \"${_payload}\" 2>&1) || true\n _http_code=$(echo \"${_raw}\" | tail -1 | sed 's/HTTP_CODE://')\n _send_resp=$(echo \"${_raw}\" | sed '$d')\n if echo \"${_send_resp}\" | jq -e '.event_id' > /dev/null 2>&1; then\n echo \"[manager] Welcome message sent to DM room\"\n else\n echo \"[manager] WARNING: Failed to send welcome message (HTTP ${_http_code}): ${_send_resp}\"\n fi\n ) &\n log \"Welcome message background process started (PID: $!)\"\n fi\nfi\nfi # end K8s mode skip for admin DM room\n\n# ============================================================\n# Generate Manager Agent openclaw.json from template\n# ============================================================\nlog \"Generating Manager openclaw.json...\"\nexport MANAGER_MATRIX_TOKEN=\"${MANAGER_TOKEN}\"\nexport MANAGER_GATEWAY_KEY=\"${HICLAW_MANAGER_GATEWAY_KEY}\"\n# Resolve model parameters based on model name\nMODEL_NAME=\"${HICLAW_DEFAULT_MODEL:-qwen3.5-plus}\"\ncase \"${MODEL_NAME}\" in\n gpt-5.3-codex|gpt-5-mini|gpt-5-nano)\n export MODEL_CONTEXT_WINDOW=400000 MODEL_MAX_TOKENS=128000 ;;\n claude-opus-4-6)\n export MODEL_CONTEXT_WINDOW=1000000 MODEL_MAX_TOKENS=128000 ;;\n claude-sonnet-4-6)\n export MODEL_CONTEXT_WINDOW=1000000 MODEL_MAX_TOKENS=64000 ;;\n claude-haiku-4-5)\n export MODEL_CONTEXT_WINDOW=200000 MODEL_MAX_TOKENS=64000 ;;\n qwen3.5-plus)\n export MODEL_CONTEXT_WINDOW=200000 MODEL_MAX_TOKENS=64000 ;;\n deepseek-chat|deepseek-reasoner|kimi-k2.5)\n export MODEL_CONTEXT_WINDOW=256000 MODEL_MAX_TOKENS=128000 ;;\n glm-5|MiniMax-M2.7|MiniMax-M2.7-highspeed|MiniMax-M2.5)\n export MODEL_CONTEXT_WINDOW=200000 MODEL_MAX_TOKENS=128000 ;;\n *)\n export MODEL_CONTEXT_WINDOW=150000 MODEL_MAX_TOKENS=128000 ;;\nesac\nexport MODEL_REASONING=true\n\n# Override with user-supplied custom model parameters from env (set during install)\n[ -n \"${HICLAW_MODEL_CONTEXT_WINDOW:-}\" ] && export MODEL_CONTEXT_WINDOW=\"${HICLAW_MODEL_CONTEXT_WINDOW}\"\n[ -n \"${HICLAW_MODEL_MAX_TOKENS:-}\" ] && export MODEL_MAX_TOKENS=\"${HICLAW_MODEL_MAX_TOKENS}\"\n[ -n \"${HICLAW_MODEL_REASONING:-}\" ] && export MODEL_REASONING=\"${HICLAW_MODEL_REASONING}\"\n\n# E2EE: convert HICLAW_MATRIX_E2EE to JSON boolean for template substitution\nif [ \"${HICLAW_MATRIX_E2EE:-0}\" = \"1\" ] || [ \"${HICLAW_MATRIX_E2EE:-}\" = \"true\" ]; then\n export MATRIX_E2EE_ENABLED=true\nelse\n export MATRIX_E2EE_ENABLED=false\nfi\nlog \"Matrix E2EE: ${MATRIX_E2EE_ENABLED}\"\n\n# Resolve input modalities: only vision-capable models get \"image\"\ncase \"${MODEL_NAME}\" in\n gpt-5.4|gpt-5.3-codex|gpt-5-mini|gpt-5-nano|claude-opus-4-6|claude-sonnet-4-6|claude-haiku-4-5|qwen3.5-plus|kimi-k2.5)\n export MODEL_INPUT='[\"text\", \"image\"]' ;;\n *)\n export MODEL_INPUT='[\"text\"]' ;;\nesac\n# Override with user-supplied vision setting from env\nif [ \"${HICLAW_MODEL_VISION:-}\" = \"true\" ]; then\n export MODEL_INPUT='[\"text\", \"image\"]'\nelif [ \"${HICLAW_MODEL_VISION:-}\" = \"false\" ]; then\n export MODEL_INPUT='[\"text\"]'\nfi\n\nlog \"Model: ${MODEL_NAME} (context=${MODEL_CONTEXT_WINDOW}, maxTokens=${MODEL_MAX_TOKENS}, reasoning=${MODEL_REASONING}, input=${MODEL_INPUT})\"\n\nif [ -f /root/manager-workspace/openclaw.json ]; then\n log \"Manager openclaw.json already exists, updating dynamic fields only (preserving user customizations)...\"\n # Merge known models into existing config (add missing, preserve user-added)\n # Use known-models.json (valid JSON) instead of template (contains ${VAR} placeholders)\n KNOWN_MODELS=$(cat /opt/hiclaw/configs/known-models.json 2>/dev/null || echo '[]')\n jq --arg token \"${MANAGER_TOKEN}\" \\\n --arg key \"${HICLAW_MANAGER_GATEWAY_KEY}\" \\\n --arg model \"${MODEL_NAME}\" \\\n --arg emb_model \"${HICLAW_EMBEDDING_MODEL}\" \\\n --arg aigw_domain \"${AI_GATEWAY_DOMAIN}\" \\\n --arg matrix_user_id \"@manager:${MATRIX_DOMAIN}\" \\\n --argjson e2ee \"${MATRIX_E2EE_ENABLED}\" \\\n --argjson known_models \"${KNOWN_MODELS}\" \\\n --argjson ctx \"${MODEL_CONTEXT_WINDOW}\" \\\n --argjson max \"${MODEL_MAX_TOKENS}\" \\\n --argjson reasoning \"${MODEL_REASONING}\" \\\n --argjson input \"${MODEL_INPUT}\" \\\n '\n # Merge known models: add any model id not already present\n .models.providers[\"hiclaw-gateway\"].models as $existing\n | ($existing | map(.id)) as $existing_ids\n | ($known_models | map(select(.id as $id | $existing_ids | index($id) | not))) as $new\n | .models.providers[\"hiclaw-gateway\"].models = ($existing + $new)\n # Ensure the user-chosen default model is in the list (custom model support)\n | if (.models.providers[\"hiclaw-gateway\"].models | map(.id) | index($model) | not) then\n .models.providers[\"hiclaw-gateway\"].models += [{\"id\": $model, \"name\": $model, \"reasoning\": $reasoning, \"contextWindow\": $ctx, \"maxTokens\": $max, \"input\": $input}]\n else . end\n # Rebuild model aliases from the full models list\n | (.models.providers[\"hiclaw-gateway\"].models | map({ (\"hiclaw-gateway/\" + .id): { \"alias\": .id } }) | add // {}) as $aliases\n | .agents.defaults.models = ((.agents.defaults.models // {}) + $aliases)\n | .channels.matrix.accessToken = $token | .channels.matrix.userId = $matrix_user_id | .models.providers[\"hiclaw-gateway\"].apiKey = $key\n | ((.hooks.token // \"\") as $ht | if $ht == $key or $ht == ($key + \"-hooks\" | @base64) then del(.hooks) else . end)\n | .agents.defaults.model.primary = (\"hiclaw-gateway/\" + $model)\n | .commands.restart = true\n | .gateway.port = 18799\n | .gateway.bind = \"lan\"\n | .gateway.controlUi = ((.gateway.controlUi // {}) + {\"dangerouslyDisableDeviceAuth\": true, \"allowInsecureAuth\": true, \"allowedOrigins\": [\"*\"]})\n | .channels.matrix.encryption = $e2ee\n | .channels.matrix.network = ((.channels.matrix.network // {}) + {\"dangerouslyAllowPrivateNetwork\": true})\n | .channels.matrix.autoJoin = \"always\"\n # OpenClaw YOLO defaults: host exec without approval prompts (see openclaw docs tools/exec-approvals)\n | .tools = (.tools // {})\n | .tools.exec = ((.tools.exec // {}) + {\"host\":\"gateway\",\"security\":\"full\",\"ask\":\"off\"})\n | .tools.elevated = (.tools.elevated // {})\n | .tools.elevated.enabled = true\n | .tools.elevated.allowFrom |= ((. // {}) | .matrix = [\"*\"])\n | .agents.defaults.elevatedDefault = \"full\"\n # Ensure memorySearch config exists (embedding model for memory) — skip if embedding model is empty\n | if $emb_model != \"\" then .agents.defaults.memorySearch //= {\"provider\":\"openai\",\"model\":$emb_model,\"remote\":{\"baseUrl\":(\"http://\" + $aigw_domain + \":8080/v1\"),\"apiKey\":$key}} else . end\n ' \\\n /root/manager-workspace/openclaw.json > /tmp/openclaw.json.tmp && \\\n mv /tmp/openclaw.json.tmp /root/manager-workspace/openclaw.json\n # Anti-tampering bypass: openclaw's config-observe-recovery clobbers the file\n # back to .bak whenever it sees \"missing-meta-vs-last-good\", which strips\n # external edits like channels.matrix.network.dangerouslyAllowPrivateNetwork.\n # Stamp meta and mirror to .bak so our edits survive.\n _oc_ver=$(openclaw --version 2>/dev/null | head -1 | awk '{print $NF}')\n [ -z \"${_oc_ver}\" ] && _oc_ver=\"hiclaw-bootstrap\"\n jq --arg ver \"${_oc_ver}\" --arg ts \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\" \\\n '.meta = ((.meta // {}) + {lastTouchedVersion: $ver, lastTouchedAt: $ts})' \\\n /root/manager-workspace/openclaw.json > /tmp/openclaw.meta.json && \\\n mv /tmp/openclaw.meta.json /root/manager-workspace/openclaw.json\n cp -f /root/manager-workspace/openclaw.json /root/manager-workspace/openclaw.json.bak\n # Verify the token was written correctly\n _written_token=$(jq -r '.channels.matrix.accessToken' /root/manager-workspace/openclaw.json 2>/dev/null)\n if [ -z \"${_written_token}\" ] || [ \"${_written_token}\" = \"null\" ]; then\n log \"ERROR: Matrix token was not written correctly to openclaw.json (got: ${_written_token})\"\n else\n log \"Matrix token written to openclaw.json (prefix: ${_written_token:0:10}...)\"\n fi\nelse\n log \"Manager openclaw.json not found, generating from template...\"\n envsubst < /opt/hiclaw/configs/manager-openclaw.json.tmpl > /root/manager-workspace/openclaw.json\n # Post-envsubst injection: memorySearch + custom model (single jq pass when possible)\n if ! jq -e --arg model \"${MODEL_NAME}\" '.models.providers[\"hiclaw-gateway\"].models | map(.id) | index($model)' /root/manager-workspace/openclaw.json > /dev/null 2>&1; then\n log \"Custom model '${MODEL_NAME}' not in built-in list, injecting into config...\"\n jq --arg emb_model \"${HICLAW_EMBEDDING_MODEL}\" \\\n --arg aigw_domain \"${AI_GATEWAY_DOMAIN}\" \\\n --arg key \"${HICLAW_MANAGER_GATEWAY_KEY}\" \\\n --arg model \"${MODEL_NAME}\" \\\n --argjson ctx \"${MODEL_CONTEXT_WINDOW}\" \\\n --argjson max \"${MODEL_MAX_TOKENS}\" \\\n --argjson reasoning \"${MODEL_REASONING}\" \\\n --argjson input \"${MODEL_INPUT}\" \\\n '\n (if $emb_model != \"\" then .agents.defaults.memorySearch = {\"provider\":\"openai\",\"model\":$emb_model,\"remote\":{\"baseUrl\":(\"http://\" + $aigw_domain + \":8080/v1\"),\"apiKey\":$key}} else . end)\n | .models.providers[\"hiclaw-gateway\"].models += [{\"id\": $model, \"name\": $model, \"reasoning\": $reasoning, \"contextWindow\": $ctx, \"maxTokens\": $max, \"input\": $input}]\n | .agents.defaults.models += {(\"hiclaw-gateway/\" + $model): {\"alias\": $model}}\n ' /root/manager-workspace/openclaw.json > /tmp/openclaw.json.tmp && \\\n mv /tmp/openclaw.json.tmp /root/manager-workspace/openclaw.json\n elif [ -n \"${HICLAW_EMBEDDING_MODEL}\" ]; then\n jq --arg emb_model \"${HICLAW_EMBEDDING_MODEL}\" \\\n --arg aigw_domain \"${AI_GATEWAY_DOMAIN}\" \\\n --arg key \"${HICLAW_MANAGER_GATEWAY_KEY}\" \\\n '.agents.defaults.memorySearch = {\"provider\":\"openai\",\"model\":$emb_model,\"remote\":{\"baseUrl\":(\"http://\" + $aigw_domain + \":8080/v1\"),\"apiKey\":$key}}' \\\n /root/manager-workspace/openclaw.json > /tmp/openclaw.json.tmp && \\\n mv /tmp/openclaw.json.tmp /root/manager-workspace/openclaw.json\n fi\n _written_token=$(jq -r '.channels.matrix.accessToken' /root/manager-workspace/openclaw.json 2>/dev/null)\n log \"Matrix token written from template (prefix: ${_written_token:0:10}...)\"\nfi\n\n# Cloud/K8s mode: overlay cloud-specific settings onto generated config\nif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ] || [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n log \"Applying cloud/k8s overlay to openclaw.json...\"\n jq --arg homeserver \"${HICLAW_MATRIX_URL}\" \\\n --arg gateway \"${HICLAW_AI_GATEWAY_URL}/v1\" \\\n --arg key \"${HICLAW_MANAGER_GATEWAY_KEY}\" \\\n '.channels.matrix.homeserver = $homeserver\n | .models.providers[\"hiclaw-gateway\"].baseUrl = $gateway\n | .models.providers[\"hiclaw-gateway\"].apiKey = $key\n | ((.hooks.token // \"\") as $ht | if $ht == $key or $ht == ($key + \"-hooks\" | @base64) then del(.hooks) else . end)\n | .commands.restart = false\n | if .agents.defaults.memorySearch then .agents.defaults.memorySearch.remote.baseUrl = $gateway | .agents.defaults.memorySearch.remote.apiKey = $key else . end' \\\n /root/manager-workspace/openclaw.json > /tmp/openclaw-cloud.json && \\\n mv /tmp/openclaw-cloud.json /root/manager-workspace/openclaw.json\n log \"Cloud/K8s overlay applied\"\nfi\n\n# ============================================================\n# Optional: enable openclaw-cms-plugin observability\n# Config is applied at runtime so secrets stay out of image layers.\n# ============================================================\nCMS_TRACES_ENABLED=\"$(echo \"${HICLAW_CMS_TRACES_ENABLED:-false}\" | tr '[:upper:]' '[:lower:]')\"\nif [ \"${CMS_TRACES_ENABLED}\" = \"true\" ]; then\n CMS_PLUGIN_NAME=\"openclaw-cms-plugin\"\n CMS_PLUGIN_DIR=\"${OPENCLAW_CMS_PLUGIN_DIR:-/opt/openclaw/extensions/openclaw-cms-plugin}\"\n CMS_PLUGIN_MANIFEST=\"${CMS_PLUGIN_DIR}/openclaw.plugin.json\"\n DIAG_PLUGIN_NAME=\"diagnostics-otel\"\n DIAG_PLUGIN_DIR=\"/opt/openclaw/extensions/diagnostics-otel\"\n CMS_LICENSE_KEY=\"${HICLAW_CMS_LICENSE_KEY:-}\"\n CMS_PROJECT=\"${HICLAW_CMS_PROJECT:-}\"\n CMS_METRICS_ENABLED=\"${HICLAW_CMS_METRICS_ENABLED:-false}\"\n\n if [ ! -f \"${CMS_PLUGIN_MANIFEST}\" ]; then\n log \"WARNING: ${CMS_PLUGIN_NAME} manifest not found at ${CMS_PLUGIN_MANIFEST}, skipping plugin config\"\n else\n _missing=0\n [ -z \"${HICLAW_CMS_ENDPOINT:-}\" ] && log \"WARNING: HICLAW_CMS_ENDPOINT is required when HICLAW_CMS_TRACES_ENABLED=true\" && _missing=1\n [ -z \"${CMS_LICENSE_KEY:-}\" ] && log \"WARNING: HICLAW_CMS_LICENSE_KEY is required when HICLAW_CMS_TRACES_ENABLED=true\" && _missing=1\n [ -z \"${HICLAW_CMS_WORKSPACE:-}\" ] && log \"WARNING: HICLAW_CMS_WORKSPACE is required when HICLAW_CMS_TRACES_ENABLED=true\" && _missing=1\n\n if [ \"${_missing}\" = \"0\" ]; then\n CMS_SERVICE_NAME=\"${HICLAW_CMS_SERVICE_NAME:-hiclaw-manager}\"\n CMS_ENABLE_METRICS=\"${CMS_METRICS_ENABLED}\"\n DIAG_AVAILABLE=\"0\"\n _metrics_lc=\"$(echo \"${CMS_ENABLE_METRICS}\" | tr '[:upper:]' '[:lower:]')\"\n if [ \"${_metrics_lc}\" = \"true\" ]; then\n if [ -f \"${DIAG_PLUGIN_DIR}/package.json\" ]; then\n DIAG_AVAILABLE=\"1\"\n if [ ! -d \"${DIAG_PLUGIN_DIR}/node_modules\" ]; then\n log \"diagnostics-otel dependencies missing, installing...\"\n if (cd \"${DIAG_PLUGIN_DIR}\" && npm install --omit=dev --ignore-scripts >/tmp/hiclaw-diag-install.log 2>&1); then\n log \"diagnostics-otel dependencies installed\"\n else\n log \"WARNING: diagnostics-otel npm install failed, metrics plugin may not load\"\n fi\n else\n log \"diagnostics-otel dependencies already present\"\n fi\n else\n log \"WARNING: diagnostics-otel package.json not found at ${DIAG_PLUGIN_DIR}, metrics plugin may not load\"\n fi\n fi\n\n log \"Applying ${CMS_PLUGIN_NAME} config to openclaw.json...\"\n jq --arg pluginName \"${CMS_PLUGIN_NAME}\" \\\n --arg pluginDir \"${CMS_PLUGIN_DIR}\" \\\n --arg endpoint \"${HICLAW_CMS_ENDPOINT}\" \\\n --arg licenseKey \"${CMS_LICENSE_KEY}\" \\\n --arg armsProject \"${CMS_PROJECT}\" \\\n --arg cmsWorkspace \"${HICLAW_CMS_WORKSPACE}\" \\\n --arg serviceName \"${CMS_SERVICE_NAME}\" \\\n --arg diagPluginName \"${DIAG_PLUGIN_NAME}\" \\\n --arg diagPluginDir \"${DIAG_PLUGIN_DIR}\" \\\n --arg metricsRaw \"${CMS_ENABLE_METRICS}\" \\\n --arg diagAvailableRaw \"${DIAG_AVAILABLE}\" \\\n '\n .plugins = (.plugins // {})\n | .plugins.load = (.plugins.load // {})\n | .plugins.entries = (.plugins.entries // {})\n | if (.plugins.allow | type) != \"array\" then .plugins.allow = [] else . end\n | if (.plugins.allow | index($pluginName)) == null then .plugins.allow += [$pluginName] else . end\n | if (.plugins.load.paths | type) != \"array\" then .plugins.load.paths = [] else . end\n | if (.plugins.load.paths | index($pluginDir)) == null then .plugins.load.paths += [$pluginDir] else . end\n | .plugins.entries[$pluginName] = {\n \"enabled\": true,\n \"config\": {\n \"endpoint\": $endpoint,\n \"headers\": {\n \"x-arms-license-key\": $licenseKey,\n \"x-arms-project\": $armsProject,\n \"x-cms-workspace\": $cmsWorkspace\n },\n \"serviceName\": $serviceName\n }\n }\n\n # diagnostics-otel metrics (optional)\n | ($metricsRaw | ascii_downcase) as $m\n | ($diagAvailableRaw == \"1\") as $diagAvailable\n | (($m == \"true\") and $diagAvailable) as $metricsEnabled\n | if $metricsEnabled then\n (if (.plugins.allow | index($diagPluginName)) == null then .plugins.allow += [$diagPluginName] else . end)\n | (if (.plugins.load.paths | index($diagPluginDir)) == null then .plugins.load.paths += [$diagPluginDir] else . end)\n | .plugins.entries[$diagPluginName].enabled = true\n | .diagnostics = (.diagnostics // {})\n | .diagnostics.otel = (.diagnostics.otel // {})\n | .diagnostics.enabled = true\n | .diagnostics.otel.enabled = true\n | .diagnostics.otel.endpoint = $endpoint\n | .diagnostics.otel.protocol = (.diagnostics.otel.protocol // \"http/protobuf\")\n | .diagnostics.otel.headers = {\n \"x-arms-license-key\": $licenseKey,\n \"x-arms-project\": $armsProject,\n \"x-cms-workspace\": $cmsWorkspace\n }\n | .diagnostics.otel.serviceName = $serviceName\n | .diagnostics.otel.metrics = true\n | .diagnostics.otel.traces = (.diagnostics.otel.traces // false)\n | .diagnostics.otel.logs = (.diagnostics.otel.logs // false)\n else\n .\n end\n ' /root/manager-workspace/openclaw.json > /tmp/openclaw-cms.json && \\\n mv /tmp/openclaw-cms.json /root/manager-workspace/openclaw.json\n log \"${CMS_PLUGIN_NAME} config applied (metrics=${CMS_ENABLE_METRICS}, service=${CMS_SERVICE_NAME})\"\n else\n log \"Skipping ${CMS_PLUGIN_NAME} config due to missing required env vars\"\n fi\n fi\nfi\n\n# ============================================================\n# Detect container runtime (for Worker creation)\n# ============================================================\nsource /opt/hiclaw/scripts/lib/container-api.sh\nif container_api_available; then\n log \"Container runtime socket detected at ${CONTAINER_SOCKET} — direct Worker creation enabled\"\n export HICLAW_CONTAINER_RUNTIME=\"socket\"\nelif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ] || [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n log \"Cloud/K8s mode — Workers created via controller API\"\n export HICLAW_CONTAINER_RUNTIME=\"cloud\"\nelse\n log \"No container runtime found — Worker creation will output install commands\"\n export HICLAW_CONTAINER_RUNTIME=\"none\"\nfi\n\n# ============================================================\n# Upgrade Worker openclaw.json: merge known models + E2EE flag into existing configs\n# Existing workers in MinIO may have old single-model configs or missing encryption field.\n# Merge template models so they can hot-switch without restart.\n# ============================================================\nREGISTRY_FILE=\"/root/manager-workspace/workers-registry.json\"\nif [ -f \"${REGISTRY_FILE}\" ]; then\n # Use known-models.json (valid JSON) instead of template (contains ${VAR} placeholders)\n KNOWN_MODELS_FILE=\"/opt/hiclaw/configs/known-models.json\"\n if [ -f \"${KNOWN_MODELS_FILE}\" ]; then\n _KNOWN_MODELS=$(cat \"${KNOWN_MODELS_FILE}\")\n for _wname in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_wname}\" ] && continue\n _minio_path=\"${HICLAW_STORAGE_PREFIX}/agents/${_wname}/openclaw.json\"\n _tmp_in=\"/tmp/openclaw-${_wname}-models-upgrade-in.json\"\n if mc cp \"${_minio_path}\" \"${_tmp_in}\" 2>/dev/null; then\n _tmp_out=\"/tmp/openclaw-${_wname}-models-upgrade-out.json\"\n # Idempotent merge: add missing known models, rebuild aliases, set e2ee.\n # Always runs — jq deduplicates by model id, so re-runs are safe.\n jq --argjson known_models \"${_KNOWN_MODELS}\" \\\n --argjson e2ee \"${MATRIX_E2EE_ENABLED}\" '\n .models.providers[\"hiclaw-gateway\"].models as $existing\n | ($existing | map(.id)) as $existing_ids\n | ($known_models | map(select(.id as $id | $existing_ids | index($id) | not))) as $new\n | .models.providers[\"hiclaw-gateway\"].models = ($existing + $new)\n | (.models.providers[\"hiclaw-gateway\"].models | map({ (\"hiclaw-gateway/\" + .id): { \"alias\": .id } }) | add // {}) as $aliases\n | .agents.defaults.models = ((.agents.defaults.models // {}) + $aliases)\n | .channels.matrix.encryption = $e2ee\n | .channels.matrix.autoJoin = \"always\"\n | .tools = (.tools // {})\n | .tools.exec = ((.tools.exec // {}) + {\"host\":\"gateway\",\"security\":\"full\",\"ask\":\"off\"})\n | .tools.elevated = (.tools.elevated // {})\n | .tools.elevated.enabled = true\n | .tools.elevated.allowFrom |= ((. // {}) | .matrix = [\"*\"])\n | .agents.defaults.elevatedDefault = \"full\"\n ' \"${_tmp_in}\" > \"${_tmp_out}\" 2>/dev/null\n if ! diff -q \"${_tmp_in}\" \"${_tmp_out}\" > /dev/null 2>&1; then\n if mc cp \"${_tmp_out}\" \"${_minio_path}\" 2>/dev/null; then\n _new_count=$(jq '.models.providers[\"hiclaw-gateway\"].models | length' \"${_tmp_out}\" 2>/dev/null)\n log \"Worker ${_wname}: upgraded openclaw.json (models: ${_new_count}, e2ee: ${MATRIX_E2EE_ENABLED})\"\n fi\n fi\n rm -f \"${_tmp_in}\" \"${_tmp_out}\"\n fi\n done\n fi\nfi\n\n# ============================================================\n# Ensure Worker Matrix password files exist in MinIO (E2EE fix)\n# Workers need to re-login on restart to get a fresh device_id.\n# Older workers created before this fix won't have the password file.\n# ============================================================\nif [ -f \"${REGISTRY_FILE}\" ]; then\n for _wname in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_wname}\" ] && continue\n _creds_file=\"/data/worker-creds/${_wname}.env\"\n if [ -f \"${_creds_file}\" ]; then\n # Check if password file already exists in MinIO\n if ! mc stat \"${HICLAW_STORAGE_PREFIX}/agents/${_wname}/credentials/matrix/password\" > /dev/null 2>&1; then\n source \"${_creds_file}\"\n if [ -n \"${WORKER_PASSWORD}\" ]; then\n _tmp_pw=\"/tmp/matrix-pw-${_wname}\"\n echo -n \"${WORKER_PASSWORD}\" > \"${_tmp_pw}\"\n mc cp \"${_tmp_pw}\" \"${HICLAW_STORAGE_PREFIX}/agents/${_wname}/credentials/matrix/password\" 2>/dev/null \\\n && log \"Worker ${_wname}: wrote Matrix password to MinIO (E2EE re-login fix)\" \\\n || log \"Worker ${_wname}: WARNING: failed to write Matrix password to MinIO\"\n rm -f \"${_tmp_pw}\"\n fi\n fi\n fi\n done\nfi\n\n# ============================================================\n# Recreate Worker containers as needed after Manager restart.\n# Workers are on hiclaw-net; Docker DNS resolves *-local.hiclaw.io via\n# the Manager's network aliases, so IP changes don't require worker recreation.\n# Only recreate stopped/missing workers.\n# ============================================================\nif container_api_available; then\n REGISTRY_FILE=\"/root/manager-workspace/workers-registry.json\"\n if [ -f \"${REGISTRY_FILE}\" ]; then\n for _worker_name in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_worker_name}\" ] && continue\n\n # Skip remote workers — they are not Manager-managed containers.\n _deployment=$(jq -r --arg w \"${_worker_name}\" '.workers[$w].deployment // \"local\"' \"${REGISTRY_FILE}\" 2>/dev/null)\n if [ \"${_deployment}\" = \"remote\" ]; then\n log \"Worker ${_worker_name} is remote, skipping container recreate\"\n continue\n fi\n\n _status=$(container_status_worker \"${_worker_name}\")\n if [ \"${_status}\" = \"running\" ]; then\n log \"Worker running: ${_worker_name}, skipping\"\n continue\n fi\n # Container missing or stopped — recreate.\n log \"Worker container ${_status}: ${_worker_name}, recreating...\"\n _creds_file=\"/data/worker-creds/${_worker_name}.env\"\n if [ -f \"${_creds_file}\" ]; then\n source \"${_creds_file}\"\n _runtime=$(jq -r --arg w \"${_worker_name}\" '.workers[$w].runtime // \"openclaw\"' \"${REGISTRY_FILE}\" 2>/dev/null)\n _recreated=false\n for _attempt in 1 2 3; do\n local _env_map _create_body\n _env_map=$(jq -cn \\\n --arg name \"${_worker_name}\" \\\n --arg fak \"${_worker_name}\" \\\n --arg fsk \"${WORKER_MINIO_PASSWORD:-}\" \\\n --arg fs_domain \"${HICLAW_FS_DOMAIN:-fs-local.hiclaw.io}\" \\\n --arg controller_url \"${HICLAW_CONTROLLER_URL:-}\" \\\n '{\n \"HICLAW_WORKER_NAME\": $name,\n \"HICLAW_FS_ENDPOINT\": (\"http://\" + ($fs_domain | split(\":\")[0]) + \":9000\"),\n \"HICLAW_FS_ACCESS_KEY\": $fak,\n \"HICLAW_FS_SECRET_KEY\": $fsk\n }\n | if $controller_url != \"\" then . + {\"HICLAW_CONTROLLER_URL\": $controller_url} else . end')\n _create_body=$(jq -cn --arg name \"${_worker_name}\" --arg runtime \"${_runtime}\" --argjson env \"${_env_map}\" '{name: $name, runtime: $runtime, env: $env}')\n worker_backend_create \"${_create_body}\" > /dev/null 2>&1 && _recreated=true && break\n log \" Attempt ${_attempt}/3 failed for ${_worker_name}, retrying in $((5 * _attempt))s...\"\n sleep $((5 * _attempt))\n done\n if [ \"${_recreated}\" = true ]; then\n log \" Recreated ${_runtime} worker: ${_worker_name}\"\n else\n log \" ERROR: Failed to recreate ${_worker_name} after 3 attempts\"\n fi\n else\n log \" WARNING: No credentials found for ${_worker_name} (${_creds_file} missing), skipping\"\n fi\n done\n fi\nfi\n\n# ============================================================\n# Notify workers of builtin updates if upgrade happened\n# Builtin files (AGENTS.md, skills) are already synced by upgrade-builtins.sh\n#\n# Cooldown: skip notification if the last successful notify was within\n# NOTIFY_COOLDOWN_SECS (default 3600s / 1 hour). This prevents repeated\n# notifications when the Manager crash-loops and re-runs upgrade-builtins\n# on every restart (e.g. IMAGE_VERSION=latest always triggers upgrade).\n# ============================================================\nNOTIFY_COOLDOWN_SECS=\"${HICLAW_NOTIFY_COOLDOWN_SECS:-3600}\"\nNOTIFY_TS_FILE=\"/root/manager-workspace/.last-worker-notify-ts\"\n\nif [ -f /root/manager-workspace/.upgrade-pending-worker-notify ]; then\n _now=$(date +%s)\n _last_notify=$(cat \"${NOTIFY_TS_FILE}\" 2>/dev/null || echo \"0\")\n _elapsed=$(( _now - _last_notify ))\n\n if [ \"${_elapsed}\" -lt \"${NOTIFY_COOLDOWN_SECS}\" ]; then\n log \"Skipping worker builtin notification (last notify ${_elapsed}s ago, cooldown ${NOTIFY_COOLDOWN_SECS}s)\"\n rm -f /root/manager-workspace/.upgrade-pending-worker-notify\n else\n log \"Notifying workers about builtin updates...\"\n REGISTRY_FILE=\"/root/manager-workspace/workers-registry.json\"\n _notify_ok=false\n if [ -f \"${REGISTRY_FILE}\" ]; then\n for _worker_name in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_worker_name}\" ] && continue\n _room_id=$(jq -r --arg w \"${_worker_name}\" '.workers[$w].room_id // empty' \"${REGISTRY_FILE}\" 2>/dev/null)\n if [ -n \"${_room_id}\" ]; then\n _worker_id=\"@${_worker_name}:${MATRIX_DOMAIN}\"\n _txn_id=\"upgrade-$(date +%s%N)\"\n _msg=\"@${_worker_name}:${MATRIX_DOMAIN} Manager upgraded builtin files (AGENTS.md, skills). Please use your file-sync skill to sync the latest config.\"\n _raw=$(curl -s -w '\\nHTTP_CODE:%{http_code}' -X PUT \\\n \"${HICLAW_MATRIX_URL}/_matrix/client/v3/rooms/${_room_id}/send/m.room.message/${_txn_id}\" \\\n -H \"Authorization: Bearer ${MANAGER_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d \"{\\\"msgtype\\\":\\\"m.text\\\",\\\"body\\\":\\\"${_msg}\\\",\\\"m.mentions\\\":{\\\"user_ids\\\":[\\\"${_worker_id}\\\"]}}\" \\\n 2>&1) || true\n _http_code=$(echo \"${_raw}\" | tail -1 | sed 's/HTTP_CODE://')\n _notify_resp=$(echo \"${_raw}\" | sed '$d')\n if echo \"${_notify_resp}\" | jq -e '.event_id' > /dev/null 2>&1; then\n log \" Notified ${_worker_name}\"; _notify_ok=true\n else\n log \" WARNING: Failed to notify ${_worker_name} (HTTP ${_http_code}): ${_notify_resp}\"\n fi\n fi\n done\n fi\n # Record timestamp only if at least one notification succeeded\n if [ \"${_notify_ok}\" = true ]; then\n echo \"${_now}\" > \"${NOTIFY_TS_FILE}\"\n fi\n rm -f /root/manager-workspace/.upgrade-pending-worker-notify\n fi\nfi\n\n# ============================================================\n# Start Manager Agent\n# ============================================================\nlog \"Starting Manager Agent (${MANAGER_RUNTIME})...\"\n\n# HOME is already set to /root/manager-workspace via docker run -e HOME=...\ncd \"${HOME}\"\n\n# Ensure host credential symlinks exist under HOME\nif [ -d \"/host-share\" ]; then\n [ -f \"/host-share/.gitconfig\" ] && ln -sf \"/host-share/.gitconfig\" \"${HOME}/.gitconfig\"\nfi\n\nlog \"HOME=${HOME} (manager-workspace, host-mounted)\"\n\n# ── Render agent doc templates ────────────────────────────────────────────\n# Replace ${VAR} placeholders with actual values so the AI agent reads\n# plain text and never needs to resolve environment variables.\nexport MANAGER_MATRIX_TOKEN MANAGER_TOKEN HIGRESS_COOKIE_FILE\nRENDER=/opt/hiclaw/scripts/lib/render-skills.sh\nlog \"Rendering agent doc templates...\"\n# Manager-owned docs (workspace)\nbash \"$RENDER\" /root/manager-workspace/skills\nbash \"$RENDER\" /root/manager-workspace/skills-alpha\nbash \"$RENDER\" /root/manager-workspace AGENTS.md TOOLS.md HEARTBEAT.md SOUL.md\n# Worker templates (workspace + image) — rendered before push to MinIO\n# so Workers (including remote pip-install) receive plain text\nbash \"$RENDER\" /root/manager-workspace/worker-skills\nbash \"$RENDER\" /root/manager-workspace/worker-agent\nbash \"$RENDER\" /root/manager-workspace/copaw-worker-agent\nbash \"$RENDER\" /root/manager-workspace/hermes-worker-agent\nbash \"$RENDER\" /opt/hiclaw/agent/worker-skills\nbash \"$RENDER\" /opt/hiclaw/agent/worker-agent\nbash \"$RENDER\" /opt/hiclaw/agent/copaw-worker-agent\nbash \"$RENDER\" /opt/hiclaw/agent/hermes-worker-agent\nlog \"Agent doc templates rendered\"\n\n# Cloud mode: start background file sync (workspace ↔ OSS) and initial push\nif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n log \"Syncing initial workspace to OSS...\"\n ensure_mc_credentials\n mc mirror /root/manager-workspace/ \"${HICLAW_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" 2>/dev/null || true\n\n # Local → OSS: change-triggered sync\n (\n while true; do\n CHANGED=$(find /root/manager-workspace/ -type f -newermt \"15 seconds ago\" 2>/dev/null | head -1)\n if [ -n \"${CHANGED}\" ]; then\n ensure_mc_credentials 2>/dev/null || true\n mc mirror /root/manager-workspace/ \"${HICLAW_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" --exclude \".npm/**\" \\\n --exclude \".local/**\" --exclude \".mc/**\" 2>/dev/null || true\n fi\n sleep 10\n done\n ) &\n log \"Local→OSS sync started (PID: $!)\"\n\n # OSS → Local: periodic pull (shared data, agent configs)\n (\n while true; do\n sleep 300\n ensure_mc_credentials 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/shared/\" /root/hiclaw-fs/shared/ --overwrite --newer-than \"5m\" 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/agents/\" /root/hiclaw-fs/agents/ --overwrite --newer-than \"5m\" 2>/dev/null || true\n done\n ) &\n log \"OSS→Local sync started (every 5m, PID: $!)\"\nfi\n\n# K8s mode: start background file sync (workspace ↔ MinIO)\nif [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n log \"Syncing initial workspace to MinIO...\"\n mc mirror /root/manager-workspace/ \"${HICLAW_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" 2>/dev/null || true\n\n # Local → MinIO: change-triggered sync\n (\n while true; do\n CHANGED=$(find /root/manager-workspace/ -type f -newermt \"15 seconds ago\" 2>/dev/null | head -1)\n if [ -n \"${CHANGED}\" ]; then\n mc mirror /root/manager-workspace/ \"${HICLAW_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" --exclude \".npm/**\" \\\n --exclude \".local/**\" --exclude \".mc/**\" 2>/dev/null || true\n fi\n sleep 10\n done\n ) &\n log \"Local→MinIO sync started (PID: $!)\"\n\n # MinIO → Local: periodic pull (shared data, agent configs)\n (\n while true; do\n sleep 300\n mc mirror \"${HICLAW_STORAGE_PREFIX}/shared/\" /root/hiclaw-fs/shared/ --overwrite --newer-than \"5m\" 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/agents/\" /root/hiclaw-fs/agents/ --overwrite --newer-than \"5m\" 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/hiclaw-config/\" /root/hiclaw-fs/hiclaw-config/ --overwrite --newer-than \"15s\" 2>/dev/null || true\n done\n ) &\n log \"MinIO→Local sync started (every 5m, PID: $!)\"\nfi\n\n# ============================================================\n# Auto-generate Manager mcporter config for pre-configured MCP servers\n# If HICLAW_GITHUB_TOKEN was set at install time, setup-higress.sh already\n# configured GitHub MCP on Higress. Run setup-mcp-server.sh now so that\n# config/mcporter.json exists when the Agent starts — no need to ask user for PAT.\n# ============================================================\nif [ -n \"${HICLAW_GITHUB_TOKEN}\" ] && [ \"${HICLAW_RUNTIME}\" != \"aliyun\" ] && [ \"${HICLAW_RUNTIME}\" != \"k8s\" ]; then\n if [ ! -f \"${HOME}/config/mcporter.json\" ]; then\n log \"Auto-generating Manager mcporter config for GitHub MCP (HICLAW_GITHUB_TOKEN set)...\"\n bash /opt/hiclaw/agent/skills/mcp-server-management/scripts/setup-mcp-server.sh \\\n github \"${HICLAW_GITHUB_TOKEN}\" 2>&1 | while IFS= read -r line; do log \" [setup-mcp] ${line}\"; done || \\\n log \"WARNING: setup-mcp-server.sh failed — Agent may need to configure GitHub MCP manually\"\n else\n log \"Manager mcporter config already exists, skipping auto-generate\"\n fi\nfi\n\n# ============================================================\n# Runtime-specific startup\n# ============================================================\nif [ \"${MANAGER_RUNTIME}\" = \"copaw\" ]; then\n # Delegate to CoPaw startup script\n exec /opt/hiclaw/scripts/init/start-copaw-manager.sh\nelse\n # ── OpenClaw Runtime ─────────────────────────────────────────────────────\n log \"Starting OpenClaw Manager...\"\n\n export OPENCLAW_CONFIG_PATH=\"/root/manager-workspace/openclaw.json\"\n\n # Symlink to default OpenClaw config path so CLI commands find the config\n mkdir -p \"${HOME}/.openclaw\"\n ln -sf \"/root/manager-workspace/openclaw.json\" \"${HOME}/.openclaw/openclaw.json\"\n\n # Clean orphaned session write locks (e.g. from SIGKILL or crash before exit handlers)\n # Prevents \"session file locked (timeout 10000ms)\" when PID was reused\n find \"${HOME}/.openclaw/agents\" -name \"*.jsonl.lock\" -delete 2>/dev/null || true\n log \"Cleaned up any orphaned session write locks\"\n\n # Clean Matrix crypto storage (SQLite WAL may be corrupted after unclean shutdown)\n # Crypto state is re-negotiated on startup; losing it only means re-establishing E2EE sessions\n rm -rf \"${HOME}/.openclaw/matrix\" 2>/dev/null || true\n log \"Cleaned Matrix crypto storage (will re-establish E2EE sessions)\"\n\n # Launch OpenClaw\n # Disable full-process respawn so the CLI uses its internal restart loop.\n # Without this, config reload spawns a detached child and exits, then\n # supervisord restarts the CLI — resulting in two gateway processes.\n export OPENCLAW_NO_RESPAWN=1\n\n # Optional matrix-plugin trace logging — when HICLAW_MATRIX_DEBUG=1 is set\n # in the manager environment (propagated by install / supervisord), turn on\n # OPENCLAW_MATRIX_DEBUG so the matrix plugin emits structured INFO-level\n # lifecycle traces (sync.state transitions, room.invite/join, message\n # handler arrival + filter outcomes). Useful for diagnosing \"worker never\n # joined the room\" / \"manager never replied\" hangs without rebuilding the\n # image.\n if [ \"${HICLAW_MATRIX_DEBUG:-}\" = \"1\" ] && [ -z \"${OPENCLAW_MATRIX_DEBUG:-}\" ]; then\n export OPENCLAW_MATRIX_DEBUG=1\n log \"HICLAW_MATRIX_DEBUG=1 detected; OPENCLAW_MATRIX_DEBUG=1 exported for matrix plugin tracing\"\n fi\n\n exec openclaw gateway run --verbose --force\nfi\n"}, "2442d9767dd845238057dbd77cd1261831352342": {"t": 1785764747.8599548, "n": 68175, "h": "08b9896909fed34a", "c": "#!/bin/bash\n# start-manager-agent.sh - Initialize and start the Manager Agent\n# Supports local (supervisord), cloud (SAE), and K8s (Helm) deployments.\n# In local mode this is the last supervisord component to start (priority 800).\n# In cloud/k8s mode (HICLAW_RUNTIME=aliyun|k8s) this is the container entrypoint.\n#\n# Runtime selection:\n# HICLAW_MANAGER_RUNTIME=openclaw (default) - OpenClaw gateway mode\n# HICLAW_MANAGER_RUNTIME=copaw - CoPaw workspace mode\n# (hermes runtime is supported for Workers only; Managers run openclaw or copaw.)\n\nsource /opt/hiclaw/scripts/lib/hiclaw-env.sh\n\n# ============================================================\n# Runtime selection\n# ============================================================\nMANAGER_RUNTIME=\"${HICLAW_MANAGER_RUNTIME:-openclaw}\"\ncase \"${MANAGER_RUNTIME}\" in\n copaw)\n log \"Manager runtime: CoPaw (Python workspace)\"\n ;;\n *)\n log \"Manager runtime: OpenClaw (Node.js gateway)\"\n MANAGER_RUNTIME=\"openclaw\"\n ;;\nesac\n\n# ============================================================\n# Set timezone from TZ env var\n# ============================================================\nif [ -n \"${TZ}\" ] && [ -f \"/usr/share/zoneinfo/${TZ}\" ]; then\n ln -sf \"/usr/share/zoneinfo/${TZ}\" /etc/localtime\n echo \"${TZ}\" > /etc/timezone\n log \"Timezone set to ${TZ}\"\nfi\n\nexport MATRIX_DOMAIN=\"${HICLAW_MATRIX_DOMAIN:-matrix-local.hiclaw.io:8080}\"\nAI_GATEWAY_DOMAIN=\"${HICLAW_AI_GATEWAY_DOMAIN:-aigw-local.hiclaw.io}\"\n\n# ============================================================\n# YOLO mode promotion\n# ============================================================\n# In embedded mode the controller does not propagate HICLAW_YOLO to the\n# manager container, but installer / test scripts touch a marker file at\n# `${WORKSPACE}/yolo-mode` instead. Promote that marker to the env var so the\n# agent's documented YOLO check (`HICLAW_YOLO=1`) reliably detects it without\n# depending on filesystem lookups during a turn.\nif [ -z \"${HICLAW_YOLO:-}\" ] && [ -f /root/manager-workspace/yolo-mode ]; then\n export HICLAW_YOLO=1\n log \"YOLO mode marker detected at /root/manager-workspace/yolo-mode; HICLAW_YOLO=1 exported\"\nfi\n\n# ============================================================\n# Cloud/K8s mode: validate required environment variables + initial credentials\n# ============================================================\nif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ] || [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n : \"${HICLAW_MATRIX_URL:?HICLAW_MATRIX_URL is required}\"\n : \"${HICLAW_MATRIX_DOMAIN:?HICLAW_MATRIX_DOMAIN is required}\"\n : \"${HICLAW_AI_GATEWAY_URL:?HICLAW_AI_GATEWAY_URL is required}\"\n if [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n : \"${HICLAW_MANAGER_GATEWAY_KEY:?HICLAW_MANAGER_GATEWAY_KEY is required}\"\n : \"${HICLAW_MANAGER_PASSWORD:?HICLAW_MANAGER_PASSWORD is required (cloud containers are stateless, password must be injected)}\"\n fi\n if [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n # K8s mode: controller handles initialization (admin registration, Higress setup).\n # Manager only needs credentials injected by the ManagerReconciler.\n : \"${HICLAW_MANAGER_GATEWAY_KEY:?HICLAW_MANAGER_GATEWAY_KEY is required (injected by controller)}\"\n : \"${HICLAW_MANAGER_PASSWORD:?HICLAW_MANAGER_PASSWORD is required (injected by controller)}\"\n else\n # Cloud (aliyun) mode: Manager still does its own initialization\n : \"${HICLAW_REGISTRATION_TOKEN:?HICLAW_REGISTRATION_TOKEN is required}\"\n : \"${HICLAW_ADMIN_USER:?HICLAW_ADMIN_USER is required}\"\n : \"${HICLAW_ADMIN_PASSWORD:?HICLAW_ADMIN_PASSWORD is required}\"\n fi\n log \"${HICLAW_RUNTIME} mode: validating environment... OK\"\n log \" Matrix: ${HICLAW_MATRIX_URL}, AI Gateway: ${HICLAW_AI_GATEWAY_URL}, Storage: ${HICLAW_FS_BUCKET}\"\n if [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n ensure_mc_credentials || { log \"FATAL: Initial STS credential fetch failed\"; exit 1; }\n fi\nfi\n\n# ============================================================\n# Local mode: host symlinks, /etc/hosts, wait for local services\n# ============================================================\nif [ \"${HICLAW_RUNTIME}\" != \"aliyun\" ] && [ \"${HICLAW_RUNTIME}\" != \"k8s\" ]; then\n # Create symlink for host directory access\n if [ -d \"/host-share\" ]; then\n ORIGINAL_HOST_HOME=\"${HOST_ORIGINAL_HOME:-$HOME}\"\n if [ ! -e \"${ORIGINAL_HOST_HOME}\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/root\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/data\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/host-share\" ]; then\n mkdir -p \"$(dirname \"${ORIGINAL_HOST_HOME}\")\"\n ln -sfn /host-share \"${ORIGINAL_HOST_HOME}\"\n log \"Created symlink: ${ORIGINAL_HOST_HOME} -> /host-share\"\n else\n ln -sfn /host-share /root/host-home\n log \"Created fallback symlink: /root/host-home -> /host-share\"\n fi\n fi\n\n # Add local domains to /etc/hosts\n HOSTS_DOMAINS=\"${MATRIX_DOMAIN%%:*} ${HICLAW_MATRIX_CLIENT_DOMAIN:-matrix-client-local.hiclaw.io} ${AI_GATEWAY_DOMAIN} ${HICLAW_FS_DOMAIN:-fs-local.hiclaw.io}\"\n if ! grep -q \"${AI_GATEWAY_DOMAIN}\" /etc/hosts 2>/dev/null; then\n echo \"127.0.0.1 ${HOSTS_DOMAINS}\" >> /etc/hosts\n log \"Added local domains to /etc/hosts\"\n fi\n\n # Wait for local infrastructure\n waitForService \"Higress Gateway\" \"127.0.0.1\" 8080 180\n waitForService \"Higress Console\" \"127.0.0.1\" 8001 180\n waitForService \"Tuwunel\" \"127.0.0.1\" 6167 120\n waitForHTTP \"Tuwunel Matrix API\" \"${HICLAW_MATRIX_URL}/_tuwunel/server_version\" 120\n waitForService \"MinIO\" \"127.0.0.1\" 9000 120\nelse\n # Cloud/K8s mode: wait for external Tuwunel\n log \"Waiting for Tuwunel Matrix server at ${HICLAW_MATRIX_URL}...\"\n _retry=0\n while [ \"${_retry}\" -lt 30 ]; do\n if curl -sf \"${HICLAW_MATRIX_URL}/_matrix/client/versions\" > /dev/null 2>&1; then\n log \"Tuwunel is ready\"\n break\n fi\n _retry=$((_retry + 1))\n log \" Waiting for Tuwunel (attempt ${_retry}/30)...\"\n sleep 5\n done\n if [ \"${_retry}\" -ge 30 ]; then\n log \"ERROR: Tuwunel not reachable at ${HICLAW_MATRIX_URL}\"\n exit 1\n fi\nfi\n\n# ============================================================\n# Auto-generate secrets if not provided via environment\n# Persisted to /data so they survive container restart\n# ============================================================\nSECRETS_FILE=\"/data/hiclaw-secrets.env\"\nif [ -f \"${SECRETS_FILE}\" ]; then\n source \"${SECRETS_FILE}\"\n log \"Loaded persisted secrets from ${SECRETS_FILE}\"\nfi\n\nif [ -z \"${HICLAW_MANAGER_GATEWAY_KEY}\" ]; then\n export HICLAW_MANAGER_GATEWAY_KEY=\"$(generateKey 32)\"\n log \"Auto-generated HICLAW_MANAGER_GATEWAY_KEY\"\nfi\nif [ -z \"${HICLAW_MANAGER_PASSWORD}\" ]; then\n export HICLAW_MANAGER_PASSWORD=\"$(generateKey 16)\"\n log \"Auto-generated HICLAW_MANAGER_PASSWORD\"\nfi\n\n# Persist secrets so they survive supervisord restart\nmkdir -p /data\ncat > \"${SECRETS_FILE}\" <<EOF\nexport HICLAW_MANAGER_GATEWAY_KEY=\"${HICLAW_MANAGER_GATEWAY_KEY}\"\nexport HICLAW_MANAGER_PASSWORD=\"${HICLAW_MANAGER_PASSWORD}\"\nEOF\nchmod 600 \"${SECRETS_FILE}\"\n\n# Cloud mode: pull workspace from OSS before initialization\nif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n HICLAW_FS=\"/root/hiclaw-fs\"\n mkdir -p \"${HICLAW_FS}/shared\" \"${HICLAW_FS}/agents\"\n log \"Pulling workspace from OSS...\"\n ensure_mc_credentials\n mc mirror \"${HICLAW_STORAGE_PREFIX}/manager/\" /root/manager-workspace/ --overwrite 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/shared/\" \"${HICLAW_FS}/shared/\" --overwrite 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/agents/\" \"${HICLAW_FS}/agents/\" --overwrite 2>/dev/null || true\n # Symlink hiclaw-fs into workspace for agent access\n ln -sfn \"${HICLAW_FS}\" /root/manager-workspace/hiclaw-fs\nfi\n\n# K8s mode: sync workspace from cluster-internal MinIO\nif [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n HICLAW_FS=\"/root/hiclaw-fs\"\n mkdir -p \"${HICLAW_FS}/shared\" \"${HICLAW_FS}/agents\" \"${HICLAW_FS}/hiclaw-config\"\n log \"Configuring mc alias for cluster MinIO...\"\n mc alias set hiclaw \"${HICLAW_FS_ENDPOINT}\" \"${HICLAW_FS_ACCESS_KEY}\" \"${HICLAW_FS_SECRET_KEY}\"\n log \"Syncing workspace from MinIO...\"\n mc mirror \"${HICLAW_STORAGE_PREFIX}/manager/\" /root/manager-workspace/ --overwrite 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/\" \"${HICLAW_FS}/\" --overwrite 2>/dev/null || true\n ln -sfn \"${HICLAW_FS}\" /root/manager-workspace/hiclaw-fs\n touch \"${HICLAW_FS}/.initialized\"\nfi\n\n# ============================================================\n# Initialize / upgrade Manager workspace\n# First boot: full init via upgrade-builtins.sh\n# Subsequent boots: compare image version; upgrade only if changed\n# ============================================================\nmkdir -p /root/manager-workspace\n\nIMAGE_VERSION=$(cat /opt/hiclaw/agent/.builtin-version 2>/dev/null || echo \"unknown\")\nINSTALLED_VERSION=$(cat /root/manager-workspace/.builtin-version 2>/dev/null || echo \"\")\n\nif [ ! -f /root/manager-workspace/.initialized ]; then\n log \"First boot: initializing manager workspace...\"\n bash /opt/hiclaw/scripts/init/upgrade-builtins.sh\n touch /root/manager-workspace/.initialized\n log \"Manager workspace initialized (version: ${IMAGE_VERSION})\"\nelif [ \"${IMAGE_VERSION}\" != \"${INSTALLED_VERSION}\" ] || [ \"${IMAGE_VERSION}\" = \"latest\" ]; then\n log \"Upgrade detected: ${INSTALLED_VERSION} -> ${IMAGE_VERSION}${IMAGE_VERSION:+ (latest: always upgrade)}\"\n bash /opt/hiclaw/scripts/init/upgrade-builtins.sh\n log \"Manager workspace upgraded to version: ${IMAGE_VERSION}\"\nelse\n log \"Workspace up to date (version: ${IMAGE_VERSION})\"\nfi\n\n# Local mode: wait for mc mirror initialization (shared + worker data in /root/hiclaw-fs/)\nif [ \"${HICLAW_RUNTIME}\" != \"aliyun\" ] && [ \"${HICLAW_RUNTIME}\" != \"k8s\" ]; then\n log \"Waiting for MinIO storage initialization...\"\n _minio_wait=0\n while [ ! -f /root/hiclaw-fs/.initialized ]; do\n sleep 2\n _minio_wait=$(( _minio_wait + 1 ))\n if [ \"${_minio_wait}\" -ge 60 ]; then\n log \"ERROR: MinIO storage initialization timed out after 120s\"\n exit 1\n fi\n done\n log \"MinIO storage initialized\"\nfi\n\n# ============================================================\n# Register Matrix users via Registration API (single-step, no UIAA)\n# K8s mode: skip — controller Initializer + ManagerReconciler already did this\n# ============================================================\nif [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n log \"K8s mode: skipping Matrix registration (handled by controller)\"\n # Controller injects HICLAW_MANAGER_PASSWORD via env; login to get token\n log \"Obtaining Manager Matrix access token...\"\n _LOGIN_RESPONSE=$(curl -s -X POST ${HICLAW_MATRIX_URL}/_matrix/client/v3/login \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"type\": \"m.login.password\",\n \"identifier\": {\"type\": \"m.id.user\", \"user\": \"manager\"},\n \"password\": \"'\"${HICLAW_MANAGER_PASSWORD}\"'\"\n }' 2>&1)\n MANAGER_TOKEN=$(echo \"${_LOGIN_RESPONSE}\" | jq -r '.access_token' 2>/dev/null)\n if [ -z \"${MANAGER_TOKEN}\" ] || [ \"${MANAGER_TOKEN}\" = \"null\" ]; then\n log \"ERROR: Failed to obtain Manager Matrix token\"\n log \"ERROR: Login response was: ${_LOGIN_RESPONSE}\"\n exit 1\n fi\n log \"Manager Matrix token obtained (token prefix: ${MANAGER_TOKEN:0:10}...)\"\nelse\nlog \"Registering human admin Matrix account...\"\ncurl -sf -X POST ${HICLAW_MATRIX_URL}/_matrix/client/v3/register \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"username\": \"'\"${HICLAW_ADMIN_USER}\"'\",\n \"password\": \"'\"${HICLAW_ADMIN_PASSWORD}\"'\",\n \"auth\": {\n \"type\": \"m.login.registration_token\",\n \"token\": \"'\"${HICLAW_REGISTRATION_TOKEN}\"'\"\n }\n }' > /dev/null 2>&1 || log \"Admin account may already exist\"\n\nlog \"Registering Manager Agent Matrix account...\"\ncurl -sf -X POST ${HICLAW_MATRIX_URL}/_matrix/client/v3/register \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"username\": \"manager\",\n \"password\": \"'\"${HICLAW_MANAGER_PASSWORD}\"'\",\n \"auth\": {\n \"type\": \"m.login.registration_token\",\n \"token\": \"'\"${HICLAW_REGISTRATION_TOKEN}\"'\"\n }\n }' > /dev/null 2>&1 || log \"Manager account may already exist\"\n\n# Get Manager Agent's Matrix access token\nlog \"Obtaining Manager Matrix access token...\"\n_LOGIN_RESPONSE=$(curl -s -X POST ${HICLAW_MATRIX_URL}/_matrix/client/v3/login \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"type\": \"m.login.password\",\n \"identifier\": {\"type\": \"m.id.user\", \"user\": \"manager\"},\n \"password\": \"'\"${HICLAW_MANAGER_PASSWORD}\"'\"\n }' 2>&1)\n_LOGIN_EXIT=$?\nlog \"Matrix login HTTP exit code: ${_LOGIN_EXIT}\"\nlog \"Matrix login response: ${_LOGIN_RESPONSE}\"\n\nMANAGER_TOKEN=$(echo \"${_LOGIN_RESPONSE}\" | jq -r '.access_token' 2>/dev/null)\n\nif [ -z \"${MANAGER_TOKEN}\" ] || [ \"${MANAGER_TOKEN}\" = \"null\" ]; then\n log \"ERROR: Failed to obtain Manager Matrix token (exit=${_LOGIN_EXIT})\"\n log \"ERROR: Login response was: ${_LOGIN_RESPONSE}\"\n exit 1\nfi\nlog \"Manager Matrix token obtained (token prefix: ${MANAGER_TOKEN:0:10}...)\"\nfi\n\n# ============================================================\n# Higress Console initialization\n# Docker mode: full setup-higress.sh (internal Higress at localhost:8001)\n# K8s mode: skip — controller Initializer handles Higress setup\n# Cloud (aliyun) mode: skip entirely (Higress managed externally)\n# ============================================================\n_HIGRESS_CONSOLE_URL=\"\"\n_HIGRESS_USER=\"${HICLAW_ADMIN_USER}\"\n_HIGRESS_PASS=\"${HICLAW_ADMIN_PASSWORD}\"\nif [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n log \"K8s mode: skipping Higress initialization (handled by controller)\"\nelif [ \"${HICLAW_RUNTIME}\" != \"aliyun\" ]; then\n _HIGRESS_CONSOLE_URL=\"http://127.0.0.1:8001\"\nfi\n\nif [ -n \"${_HIGRESS_CONSOLE_URL}\" ]; then\n COOKIE_FILE=\"/tmp/higress-session-cookie\"\n\n log \"Waiting for Higress Console (${_HIGRESS_CONSOLE_URL}) to be fully ready and initializing admin...\"\n INIT_DONE=false\n for i in $(seq 1 90); do\n INIT_RESULT=$(curl -s -X POST \"${_HIGRESS_CONSOLE_URL}/system/init\" \\\n -H 'Content-Type: application/json' \\\n -d '{\"adminUser\":{\"name\":\"'\"${_HIGRESS_USER}\"'\",\"password\":\"'\"${_HIGRESS_PASS}\"'\",\"displayName\":\"'\"${_HIGRESS_USER}\"'\"}}' 2>/dev/null) || true\n if echo \"${INIT_RESULT}\" | grep -qE '\"success\":true|already.?init' 2>/dev/null; then\n INIT_DONE=true\n break\n fi\n if echo \"${INIT_RESULT}\" | grep -q '\"name\"' 2>/dev/null; then\n INIT_DONE=true\n break\n fi\n sleep 2\n done\n\n if [ \"${INIT_DONE}\" != \"true\" ]; then\n log \"ERROR: Higress Console did not become ready within 180s\"\n exit 1\n fi\n log \"Higress Console init done\"\n\n log \"Logging into Higress Console...\"\n LOGIN_OK=false\n for i in $(seq 1 10); do\n HTTP_CODE=$(curl -s -o /dev/null -w '%{http_code}' -X POST \"${_HIGRESS_CONSOLE_URL}/session/login\" \\\n -H 'Content-Type: application/json' \\\n -c \"${COOKIE_FILE}\" \\\n -d '{\"username\":\"'\"${_HIGRESS_USER}\"'\",\"password\":\"'\"${_HIGRESS_PASS}\"'\"}' 2>/dev/null) || true\n if { [ \"${HTTP_CODE}\" = \"200\" ] || [ \"${HTTP_CODE}\" = \"201\" ]; } && [ -f \"${COOKIE_FILE}\" ] && [ -s \"${COOKIE_FILE}\" ]; then\n LOGIN_OK=true\n break\n fi\n log \"Login attempt $i (HTTP ${HTTP_CODE}), retrying in 3s...\"\n sleep 3\n done\n\n if [ \"${LOGIN_OK}\" != \"true\" ]; then\n log \"ERROR: Could not login to Higress Console after retries\"\n exit 1\n fi\n log \"Higress Console login successful\"\n\n VERIFY_CODE=$(curl -s -o /dev/null -w '%{http_code}' \"${_HIGRESS_CONSOLE_URL}/v1/consumers\" -b \"${COOKIE_FILE}\" 2>/dev/null) || true\n if [ \"${VERIFY_CODE}\" = \"200\" ]; then\n log \"Console session verified (cookie valid)\"\n else\n log \"WARNING: Console session may be invalid (verify returned HTTP ${VERIFY_CODE})\"\n rm -f \"${COOKIE_FILE}\"\n for i in $(seq 1 5); do\n curl -s -o /dev/null -w '%{http_code}' -X POST \"${_HIGRESS_CONSOLE_URL}/session/login\" \\\n -H 'Content-Type: application/json' \\\n -c \"${COOKIE_FILE}\" \\\n -d '{\"username\":\"'\"${_HIGRESS_USER}\"'\",\"password\":\"'\"${_HIGRESS_PASS}\"'\"}' 2>/dev/null\n VERIFY2=$(curl -s -o /dev/null -w '%{http_code}' \"${_HIGRESS_CONSOLE_URL}/v1/consumers\" -b \"${COOKIE_FILE}\" 2>/dev/null) || true\n if [ \"${VERIFY2}\" = \"200\" ]; then\n log \"Re-login successful, session verified\"\n break\n fi\n sleep 2\n done\n fi\n\n export HIGRESS_COOKIE_FILE=\"${COOKIE_FILE}\"\n export HIGRESS_CONSOLE_URL=\"${_HIGRESS_CONSOLE_URL}\"\n\n if [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n # K8s mode: lightweight Higress config — only what's needed for LLM access\n source /opt/hiclaw/scripts/lib/base.sh\n _k8s_higress_api() {\n local method=\"$1\" path=\"$2\" desc=\"$3\"; shift 3; local body=\"$*\"\n local tmpfile; tmpfile=$(mktemp)\n local http_code\n http_code=$(curl -s -o \"${tmpfile}\" -w '%{http_code}' -X \"${method}\" \"${_HIGRESS_CONSOLE_URL}${path}\" \\\n -b \"${COOKIE_FILE}\" -H 'Content-Type: application/json' -d \"${body}\" 2>/dev/null) || true\n local response; response=$(cat \"${tmpfile}\" 2>/dev/null); rm -f \"${tmpfile}\"\n if echo \"${response}\" | grep -q '\"success\":true' 2>/dev/null; then\n log \"${desc} ... OK\"\n elif [ \"${http_code}\" = \"409\" ]; then\n log \"${desc} ... already exists, skipping\"\n elif [ \"${http_code}\" = \"200\" ] || [ \"${http_code}\" = \"201\" ] || [ \"${http_code}\" = \"204\" ]; then\n log \"${desc} ... OK (HTTP ${http_code})\"\n else\n log \"WARNING: ${desc} ... (HTTP ${http_code}): ${response}\"\n fi\n }\n\n # 1. Service Sources (DNS type → K8s Service FQDN)\n # Extract host:port from URLs for Higress service source registration\n _TUWUNEL_HOST=$(echo \"${HICLAW_MATRIX_URL}\" | sed 's|^http[s]*://||')\n _TUWUNEL_DOMAIN=$(echo \"${_TUWUNEL_HOST}\" | cut -d: -f1)\n _TUWUNEL_PORT=$(echo \"${_TUWUNEL_HOST}\" | cut -d: -f2)\n _k8s_higress_api POST /v1/service-sources \"Registering Tuwunel service source\" \\\n '{\"type\":\"dns\",\"name\":\"tuwunel\",\"domain\":\"'\"${_TUWUNEL_DOMAIN}\"'\",\"port\":'\"${_TUWUNEL_PORT}\"'}'\n\n if [ -n \"${HICLAW_ELEMENT_WEB_URL:-}\" ]; then\n _ELEMENT_HOST=$(echo \"${HICLAW_ELEMENT_WEB_URL}\" | sed 's|^http[s]*://||')\n _ELEMENT_DOMAIN=$(echo \"${_ELEMENT_HOST}\" | cut -d: -f1)\n _ELEMENT_PORT=$(echo \"${_ELEMENT_HOST}\" | cut -d: -f2)\n _k8s_higress_api POST /v1/service-sources \"Registering Element Web service source\" \\\n '{\"type\":\"dns\",\"name\":\"element-web\",\"domain\":\"'\"${_ELEMENT_DOMAIN}\"'\",\"port\":'\"${_ELEMENT_PORT}\"'}'\n fi\n\n # 2. Manager Consumer (key-auth)\n _k8s_higress_api POST /v1/consumers \"Creating Manager consumer\" \\\n '{\"name\":\"manager\",\"credentials\":[{\"type\":\"key-auth\",\"source\":\"BEARER\",\"values\":[\"'\"${HICLAW_MANAGER_GATEWAY_KEY}\"'\"]}]}'\n\n # 3. LLM Provider\n _LLM_PROVIDER=\"${HICLAW_LLM_PROVIDER:-qwen}\"\n _LLM_API_URL=\"${HICLAW_LLM_API_URL:-}\"\n if [ -z \"${_LLM_API_URL}\" ]; then\n case \"${_LLM_PROVIDER}\" in\n qwen) _LLM_API_URL=\"https://dashscope.aliyuncs.com/compatible-mode/v1\" ;;\n esac\n fi\n # 4. LLM Provider type-specific config\n case \"${_LLM_PROVIDER}\" in\n qwen)\n _k8s_higress_api POST /v1/ai/providers \"Creating LLM provider (qwen)\" \\\n '{\"type\":\"qwen\",\"name\":\"qwen\",\"tokens\":[\"'\"${HICLAW_LLM_API_KEY}\"'\"],\"protocol\":\"openai/v1\",\"tokenFailoverConfig\":{\"enabled\":false},\"rawConfigs\":{\"qwenEnableSearch\":false,\"qwenEnableCompatible\":true,\"qwenFileIds\":[],\"hiclawMode\":true}}'\n ;;\n *)\n _BODY='{\"name\":\"'\"${_LLM_PROVIDER}\"'\",\"type\":\"openai\",\"tokens\":[\"'\"${HICLAW_LLM_API_KEY}\"'\"],\"modelMapping\":{},\"protocol\":\"openai/v1\",\"rawConfigs\":{\"hiclawMode\":true}}'\n _k8s_higress_api POST /v1/ai/providers \"Creating LLM provider (${_LLM_PROVIDER})\" \"${_BODY}\"\n ;;\n esac\n\n # 5. AI Route (bind provider + consumer auth, /v1 prefix to avoid clash with Element Web catch-all)\n _k8s_higress_api POST /v1/ai/routes \"Creating AI Gateway route\" \\\n '{\"name\":\"default-ai-route\",\"domains\":[],\"pathPredicate\":{\"matchType\":\"PRE\",\"matchValue\":\"/v1\",\"caseSensitive\":false},\"upstreams\":[{\"provider\":\"'\"${_LLM_PROVIDER}\"'\",\"weight\":100,\"modelMapping\":{}}],\"authConfig\":{\"enabled\":true,\"allowedCredentialTypes\":[\"key-auth\"],\"allowedConsumers\":[\"manager\"]}}'\n\n # 6. Matrix Homeserver Route (/_matrix/* → Tuwunel, no auth)\n _k8s_higress_api POST /v1/routes \"Creating Matrix Homeserver route\" \\\n '{\"name\":\"matrix-homeserver\",\"domains\":[],\"path\":{\"matchType\":\"PRE\",\"matchValue\":\"/_matrix\"},\"services\":[{\"name\":\"tuwunel.dns\",\"port\":'\"${_TUWUNEL_PORT}\"',\"weight\":100}]}'\n\n # 7. Element Web Route (/ catch-all → Element Web, no auth)\n if [ -n \"${HICLAW_ELEMENT_WEB_URL:-}\" ]; then\n _k8s_higress_api POST /v1/routes \"Creating Element Web route\" \\\n '{\"name\":\"element-web\",\"domains\":[],\"path\":{\"matchType\":\"PRE\",\"matchValue\":\"/\"},\"services\":[{\"name\":\"element-web.dns\",\"port\":'\"${_ELEMENT_PORT}\"',\"weight\":100}]}'\n fi\n\n # 8. Remove Higress default landing page (Exact match on / takes precedence over Element Web catch-all)\n _k8s_higress_api DELETE /v1/routes/default \"Removing Higress default landing route\"\n\n log \"K8s Higress lightweight setup complete\"\n\n # Wait for AI plugin activation (~45s for first config)\n log \"Waiting for AI Gateway plugin activation (45s)...\"\n sleep 45\n else\n # Docker mode: full setup with all routes, domains, MCP servers\n /opt/hiclaw/scripts/init/setup-higress.sh\n fi\nfi\n\n# ============================================================\n# Create admin DM room, persist to state.json, send welcome message\n# K8s mode: skip — controller ProvisionManager creates the Admin DM\n# room (Step 4 in service/provisioner.go) AND reconcileManagerWelcome\n# delivers the first-boot onboarding prompt once OpenClaw inside this\n# container has joined the room. In k8s mode the manager intentionally\n# does NOT have the admin password (only HICLAW_ADMIN_USER), so it\n# could not log in as admin to send the welcome itself anyway. The\n# Manager Agent discovers its admin DM room on first heartbeat via\n# state.json / `manage-state.sh --action set-admin-dm` (see HEARTBEAT.md\n# Step 1) — it does not need it to be pre-injected by this script.\n# Runs in both local and cloud modes (idempotent)\n# ============================================================\nif [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n log \"K8s mode: skipping admin DM room creation and welcome message (both handled by hiclaw-controller)\"\nelse\nMANAGER_FULL_ID=\"@manager:${MATRIX_DOMAIN}\"\nADMIN_FULL_ID=\"@${HICLAW_ADMIN_USER}:${MATRIX_DOMAIN}\"\n\nlog \"Logging in as admin to create DM room...\"\n_ADMIN_LOGIN=$(curl -sf -X POST \"${HICLAW_MATRIX_URL}/_matrix/client/v3/login\" \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"type\": \"m.login.password\",\n \"identifier\": {\"type\": \"m.id.user\", \"user\": \"'\"${HICLAW_ADMIN_USER}\"'\"},\n \"password\": \"'\"${HICLAW_ADMIN_PASSWORD}\"'\"\n }' 2>&1) || true\n\nADMIN_MATRIX_TOKEN=$(echo \"${_ADMIN_LOGIN}\" | jq -r '.access_token // empty' 2>/dev/null)\nif [ -z \"${ADMIN_MATRIX_TOKEN}\" ]; then\n log \"WARNING: Failed to login as admin, skipping DM room creation\"\nelse\n # Search for existing DM room with Manager (idempotent)\n DM_ROOM_ID=\"\"\n _JOINED_ROOMS=$(curl -sf \"${HICLAW_MATRIX_URL}/_matrix/client/v3/joined_rooms\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" 2>/dev/null \\\n | jq -r '.joined_rooms[]' 2>/dev/null) || true\n for _rid in ${_JOINED_ROOMS}; do\n _members=$(curl -sf \"${HICLAW_MATRIX_URL}/_matrix/client/v3/rooms/${_rid}/members\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" 2>/dev/null \\\n | jq -r '.chunk[].state_key' 2>/dev/null) || continue\n _count=$(echo \"${_members}\" | wc -l | xargs)\n if [ \"${_count}\" = \"2\" ] && echo \"${_members}\" | grep -q \"@manager:\"; then\n DM_ROOM_ID=\"${_rid}\"\n break\n fi\n done\n\n if [ -n \"${DM_ROOM_ID}\" ]; then\n log \"Existing DM room found: ${DM_ROOM_ID}\"\n else\n log \"Creating DM room with Manager...\"\n _RAW=$(curl -s -w '\\nHTTP_CODE:%{http_code}' -X POST \"${HICLAW_MATRIX_URL}/_matrix/client/v3/createRoom\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d \"{\\\"is_direct\\\":true,\\\"invite\\\":[\\\"${MANAGER_FULL_ID}\\\"],\\\"preset\\\":\\\"trusted_private_chat\\\"}\" 2>&1) || true\n _HTTP_CODE=$(echo \"${_RAW}\" | tail -1 | sed 's/HTTP_CODE://')\n _CREATE_RESP=$(echo \"${_RAW}\" | sed '$d')\n DM_ROOM_ID=$(echo \"${_CREATE_RESP}\" | jq -r '.room_id // empty' 2>/dev/null)\n if [ -n \"${DM_ROOM_ID}\" ]; then\n log \"DM room created: ${DM_ROOM_ID}\"\n else\n log \"WARNING: Failed to create DM room (HTTP ${_HTTP_CODE}): ${_CREATE_RESP}\"\n fi\n fi\n\n # Persist admin DM room ID to state.json\n if [ -n \"${DM_ROOM_ID}\" ]; then\n STATE_SCRIPT=\"/opt/hiclaw/agent/skills/task-management/scripts/manage-state.sh\"\n if [ -f \"${STATE_SCRIPT}\" ]; then\n bash \"${STATE_SCRIPT}\" --action init 2>/dev/null || true\n bash \"${STATE_SCRIPT}\" --action set-admin-dm --room-id \"${DM_ROOM_ID}\" 2>/dev/null || true\n log \"Admin DM room persisted to state.json: ${DM_ROOM_ID}\"\n fi\n fi\n\n # Schedule welcome message in background (only on first boot)\n if [ -n \"${DM_ROOM_ID}\" ] && [ ! -f \"/root/manager-workspace/soul-configured\" ]; then\n log \"Scheduling welcome message (background, waiting for OpenClaw to start)...\"\n (\n _HICLAW_LANGUAGE=\"${HICLAW_LANGUAGE:-zh}\"\n _HICLAW_TIMEZONE=\"${TZ:-Asia/Shanghai}\"\n _wait=0\n _ready=false\n while [ \"${_wait}\" -lt 300 ]; do\n if curl -sf http://127.0.0.1:18799/ > /dev/null 2>&1; then\n _ready=true\n break\n fi\n sleep 3\n _wait=$((_wait + 3))\n done\n if [ \"${_ready}\" != \"true\" ]; then\n echo \"[manager] WARNING: OpenClaw gateway not ready within 300s, skipping welcome message\"\n exit 0\n fi\n # Ensure Manager has joined the DM room before sending the welcome\n # message. Without this, there is a race between OpenClaw's Matrix\n # auto-join and the message send — the message may land before Manager\n # joins, so OpenClaw's /sync never picks it up.\n _join_ok=false\n for _join_attempt in 1 2 3; do\n if curl -sf -X POST \"${HICLAW_MATRIX_URL}/_matrix/client/v3/rooms/${DM_ROOM_ID}/join\" \\\n -H \"Authorization: Bearer ${MANAGER_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d '{}' > /dev/null 2>&1; then\n echo \"[manager] Manager joined DM room before welcome message\"\n _join_ok=true\n break\n fi\n sleep 2\n done\n if [ \"${_join_ok}\" != \"true\" ]; then\n echo \"[manager] WARNING: Manager join request failed after 3 attempts (may already be joined)\"\n fi\n _welcome_msg=\"This is an automated message from the HiClaw setup. This is a fresh installation.\n\n--- Installation Context ---\nUser Language: ${_HICLAW_LANGUAGE} (zh = Chinese, en = English)\nUser Timezone: ${_HICLAW_TIMEZONE} (IANA timezone identifier)\n---\n\nYou are an AI agent that manages a team of worker agents. Your identity and personality have not been configured yet — the human admin is about to meet you for the first time.\n\nPlease begin the onboarding conversation:\n\n1. Greet the admin warmly and briefly describe what you can do (coordinate workers, manage tasks, run multi-agent projects)\n2. The user has selected \\\"${_HICLAW_LANGUAGE}\\\" as their preferred language during installation. Use this language for your greeting and all subsequent communication.\n3. The user's timezone is ${_HICLAW_TIMEZONE}. Based on this timezone, you may infer their likely region and suggest additional language options.\n4. Ask them: a) What would they like to call you? b) Communication style preference? c) Any behavior guidelines? d) Confirm default language\n5. After they reply, write their preferences to ~/SOUL.md\n6. Confirm what you wrote, and ask if they would like to adjust anything\n7. Once confirmed, run: touch ~/soul-configured\n\nThe human admin will start chatting shortly.\"\n _txn_id=\"welcome-$(date +%s)\"\n _payload=$(jq -nc --arg body \"${_welcome_msg}\" '{\"msgtype\":\"m.text\",\"body\":$body}')\n _raw=$(curl -s -w '\\nHTTP_CODE:%{http_code}' -X PUT \"${HICLAW_MATRIX_URL}/_matrix/client/v3/rooms/${DM_ROOM_ID}/send/m.room.message/${_txn_id}\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d \"${_payload}\" 2>&1) || true\n _http_code=$(echo \"${_raw}\" | tail -1 | sed 's/HTTP_CODE://')\n _send_resp=$(echo \"${_raw}\" | sed '$d')\n if echo \"${_send_resp}\" | jq -e '.event_id' > /dev/null 2>&1; then\n echo \"[manager] Welcome message sent to DM room\"\n else\n echo \"[manager] WARNING: Failed to send welcome message (HTTP ${_http_code}): ${_send_resp}\"\n fi\n ) &\n log \"Welcome message background process started (PID: $!)\"\n fi\nfi\nfi # end K8s mode skip for admin DM room\n\n# ============================================================\n# Generate Manager Agent openclaw.json from template\n# ============================================================\nlog \"Generating Manager openclaw.json...\"\nexport MANAGER_MATRIX_TOKEN=\"${MANAGER_TOKEN}\"\nexport MANAGER_GATEWAY_KEY=\"${HICLAW_MANAGER_GATEWAY_KEY}\"\n# Resolve model parameters based on model name\nMODEL_NAME=\"${HICLAW_DEFAULT_MODEL:-qwen3.5-plus}\"\ncase \"${MODEL_NAME}\" in\n gpt-5.3-codex|gpt-5-mini|gpt-5-nano)\n export MODEL_CONTEXT_WINDOW=400000 MODEL_MAX_TOKENS=128000 ;;\n claude-opus-4-6)\n export MODEL_CONTEXT_WINDOW=1000000 MODEL_MAX_TOKENS=128000 ;;\n claude-sonnet-4-6)\n export MODEL_CONTEXT_WINDOW=1000000 MODEL_MAX_TOKENS=64000 ;;\n claude-haiku-4-5)\n export MODEL_CONTEXT_WINDOW=200000 MODEL_MAX_TOKENS=64000 ;;\n qwen3.5-plus)\n export MODEL_CONTEXT_WINDOW=200000 MODEL_MAX_TOKENS=64000 ;;\n deepseek-chat|deepseek-reasoner|kimi-k2.5)\n export MODEL_CONTEXT_WINDOW=256000 MODEL_MAX_TOKENS=128000 ;;\n glm-5|MiniMax-M2.7|MiniMax-M2.7-highspeed|MiniMax-M2.5)\n export MODEL_CONTEXT_WINDOW=200000 MODEL_MAX_TOKENS=128000 ;;\n *)\n export MODEL_CONTEXT_WINDOW=150000 MODEL_MAX_TOKENS=128000 ;;\nesac\nexport MODEL_REASONING=true\n\n# Override with user-supplied custom model parameters from env (set during install)\n[ -n \"${HICLAW_MODEL_CONTEXT_WINDOW:-}\" ] && export MODEL_CONTEXT_WINDOW=\"${HICLAW_MODEL_CONTEXT_WINDOW}\"\n[ -n \"${HICLAW_MODEL_MAX_TOKENS:-}\" ] && export MODEL_MAX_TOKENS=\"${HICLAW_MODEL_MAX_TOKENS}\"\n[ -n \"${HICLAW_MODEL_REASONING:-}\" ] && export MODEL_REASONING=\"${HICLAW_MODEL_REASONING}\"\n\n# E2EE: convert HICLAW_MATRIX_E2EE to JSON boolean for template substitution\nif [ \"${HICLAW_MATRIX_E2EE:-0}\" = \"1\" ] || [ \"${HICLAW_MATRIX_E2EE:-}\" = \"true\" ]; then\n export MATRIX_E2EE_ENABLED=true\nelse\n export MATRIX_E2EE_ENABLED=false\nfi\nlog \"Matrix E2EE: ${MATRIX_E2EE_ENABLED}\"\n\n# Resolve input modalities: only vision-capable models get \"image\"\ncase \"${MODEL_NAME}\" in\n gpt-5.4|gpt-5.3-codex|gpt-5-mini|gpt-5-nano|claude-opus-4-6|claude-sonnet-4-6|claude-haiku-4-5|qwen3.5-plus|kimi-k2.5)\n export MODEL_INPUT='[\"text\", \"image\"]' ;;\n *)\n export MODEL_INPUT='[\"text\"]' ;;\nesac\n# Override with user-supplied vision setting from env\nif [ \"${HICLAW_MODEL_VISION:-}\" = \"true\" ]; then\n export MODEL_INPUT='[\"text\", \"image\"]'\nelif [ \"${HICLAW_MODEL_VISION:-}\" = \"false\" ]; then\n export MODEL_INPUT='[\"text\"]'\nfi\n\nlog \"Model: ${MODEL_NAME} (context=${MODEL_CONTEXT_WINDOW}, maxTokens=${MODEL_MAX_TOKENS}, reasoning=${MODEL_REASONING}, input=${MODEL_INPUT})\"\n\nif [ -f /root/manager-workspace/openclaw.json ]; then\n log \"Manager openclaw.json already exists, updating dynamic fields only (preserving user customizations)...\"\n # Merge known models into existing config (add missing, preserve user-added)\n # Use known-models.json (valid JSON) instead of template (contains ${VAR} placeholders)\n KNOWN_MODELS=$(cat /opt/hiclaw/configs/known-models.json 2>/dev/null || echo '[]')\n jq --arg token \"${MANAGER_TOKEN}\" \\\n --arg key \"${HICLAW_MANAGER_GATEWAY_KEY}\" \\\n --arg model \"${MODEL_NAME}\" \\\n --arg emb_model \"${HICLAW_EMBEDDING_MODEL}\" \\\n --arg aigw_domain \"${AI_GATEWAY_DOMAIN}\" \\\n --arg matrix_user_id \"@manager:${MATRIX_DOMAIN}\" \\\n --argjson e2ee \"${MATRIX_E2EE_ENABLED}\" \\\n --argjson known_models \"${KNOWN_MODELS}\" \\\n --argjson ctx \"${MODEL_CONTEXT_WINDOW}\" \\\n --argjson max \"${MODEL_MAX_TOKENS}\" \\\n --argjson reasoning \"${MODEL_REASONING}\" \\\n --argjson input \"${MODEL_INPUT}\" \\\n '\n # Merge known models: add any model id not already present\n .models.providers[\"hiclaw-gateway\"].models as $existing\n | ($existing | map(.id)) as $existing_ids\n | ($known_models | map(select(.id as $id | $existing_ids | index($id) | not))) as $new\n | .models.providers[\"hiclaw-gateway\"].models = ($existing + $new)\n # Ensure the user-chosen default model is in the list (custom model support)\n | if (.models.providers[\"hiclaw-gateway\"].models | map(.id) | index($model) | not) then\n .models.providers[\"hiclaw-gateway\"].models += [{\"id\": $model, \"name\": $model, \"reasoning\": $reasoning, \"contextWindow\": $ctx, \"maxTokens\": $max, \"input\": $input}]\n else . end\n # Rebuild model aliases from the full models list\n | (.models.providers[\"hiclaw-gateway\"].models | map({ (\"hiclaw-gateway/\" + .id): { \"alias\": .id } }) | add // {}) as $aliases\n | .agents.defaults.models = ((.agents.defaults.models // {}) + $aliases)\n | .channels.matrix.accessToken = $token | .channels.matrix.userId = $matrix_user_id | .models.providers[\"hiclaw-gateway\"].apiKey = $key\n | ((.hooks.token // \"\") as $ht | if $ht == $key or $ht == ($key + \"-hooks\" | @base64) then del(.hooks) else . end)\n | .agents.defaults.model.primary = (\"hiclaw-gateway/\" + $model)\n | .commands.restart = true\n | .gateway.port = 18799\n | .gateway.bind = \"lan\"\n | .gateway.controlUi = ((.gateway.controlUi // {}) + {\"dangerouslyDisableDeviceAuth\": true, \"allowInsecureAuth\": true, \"allowedOrigins\": [\"*\"]})\n | .channels.matrix.encryption = $e2ee\n | .channels.matrix.network = ((.channels.matrix.network // {}) + {\"dangerouslyAllowPrivateNetwork\": true})\n | .channels.matrix.autoJoin = \"always\"\n # OpenClaw YOLO defaults: host exec without approval prompts (see openclaw docs tools/exec-approvals)\n | .tools = (.tools // {})\n | .tools.exec = ((.tools.exec // {}) + {\"host\":\"gateway\",\"security\":\"full\",\"ask\":\"off\"})\n | .tools.elevated = (.tools.elevated // {})\n | .tools.elevated.enabled = true\n | .tools.elevated.allowFrom |= ((. // {}) | .matrix = [\"*\"])\n | .agents.defaults.elevatedDefault = \"full\"\n # Ensure memorySearch config exists (embedding model for memory) — skip if embedding model is empty\n | if $emb_model != \"\" then .agents.defaults.memorySearch //= {\"provider\":\"openai\",\"model\":$emb_model,\"remote\":{\"baseUrl\":(\"http://\" + $aigw_domain + \":8080/v1\"),\"apiKey\":$key}} else . end\n ' \\\n /root/manager-workspace/openclaw.json > /tmp/openclaw.json.tmp && \\\n mv /tmp/openclaw.json.tmp /root/manager-workspace/openclaw.json\n # Anti-tampering bypass: openclaw's config-observe-recovery clobbers the file\n # back to .bak whenever it sees \"missing-meta-vs-last-good\", which strips\n # external edits like channels.matrix.network.dangerouslyAllowPrivateNetwork.\n # Stamp meta and mirror to .bak so our edits survive.\n _oc_ver=$(openclaw --version 2>/dev/null | head -1 | awk '{print $NF}')\n [ -z \"${_oc_ver}\" ] && _oc_ver=\"hiclaw-bootstrap\"\n jq --arg ver \"${_oc_ver}\" --arg ts \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\" \\\n '.meta = ((.meta // {}) + {lastTouchedVersion: $ver, lastTouchedAt: $ts})' \\\n /root/manager-workspace/openclaw.json > /tmp/openclaw.meta.json && \\\n mv /tmp/openclaw.meta.json /root/manager-workspace/openclaw.json\n cp -f /root/manager-workspace/openclaw.json /root/manager-workspace/openclaw.json.bak\n # Verify the token was written correctly\n _written_token=$(jq -r '.channels.matrix.accessToken' /root/manager-workspace/openclaw.json 2>/dev/null)\n if [ -z \"${_written_token}\" ] || [ \"${_written_token}\" = \"null\" ]; then\n log \"ERROR: Matrix token was not written correctly to openclaw.json (got: ${_written_token})\"\n else\n log \"Matrix token written to openclaw.json (prefix: ${_written_token:0:10}...)\"\n fi\nelse\n log \"Manager openclaw.json not found, generating from template...\"\n envsubst < /opt/hiclaw/configs/manager-openclaw.json.tmpl > /root/manager-workspace/openclaw.json\n # Post-envsubst injection: memorySearch + custom model (single jq pass when possible)\n if ! jq -e --arg model \"${MODEL_NAME}\" '.models.providers[\"hiclaw-gateway\"].models | map(.id) | index($model)' /root/manager-workspace/openclaw.json > /dev/null 2>&1; then\n log \"Custom model '${MODEL_NAME}' not in built-in list, injecting into config...\"\n jq --arg emb_model \"${HICLAW_EMBEDDING_MODEL}\" \\\n --arg aigw_domain \"${AI_GATEWAY_DOMAIN}\" \\\n --arg key \"${HICLAW_MANAGER_GATEWAY_KEY}\" \\\n --arg model \"${MODEL_NAME}\" \\\n --argjson ctx \"${MODEL_CONTEXT_WINDOW}\" \\\n --argjson max \"${MODEL_MAX_TOKENS}\" \\\n --argjson reasoning \"${MODEL_REASONING}\" \\\n --argjson input \"${MODEL_INPUT}\" \\\n '\n (if $emb_model != \"\" then .agents.defaults.memorySearch = {\"provider\":\"openai\",\"model\":$emb_model,\"remote\":{\"baseUrl\":(\"http://\" + $aigw_domain + \":8080/v1\"),\"apiKey\":$key}} else . end)\n | .models.providers[\"hiclaw-gateway\"].models += [{\"id\": $model, \"name\": $model, \"reasoning\": $reasoning, \"contextWindow\": $ctx, \"maxTokens\": $max, \"input\": $input}]\n | .agents.defaults.models += {(\"hiclaw-gateway/\" + $model): {\"alias\": $model}}\n ' /root/manager-workspace/openclaw.json > /tmp/openclaw.json.tmp && \\\n mv /tmp/openclaw.json.tmp /root/manager-workspace/openclaw.json\n elif [ -n \"${HICLAW_EMBEDDING_MODEL}\" ]; then\n jq --arg emb_model \"${HICLAW_EMBEDDING_MODEL}\" \\\n --arg aigw_domain \"${AI_GATEWAY_DOMAIN}\" \\\n --arg key \"${HICLAW_MANAGER_GATEWAY_KEY}\" \\\n '.agents.defaults.memorySearch = {\"provider\":\"openai\",\"model\":$emb_model,\"remote\":{\"baseUrl\":(\"http://\" + $aigw_domain + \":8080/v1\"),\"apiKey\":$key}}' \\\n /root/manager-workspace/openclaw.json > /tmp/openclaw.json.tmp && \\\n mv /tmp/openclaw.json.tmp /root/manager-workspace/openclaw.json\n fi\n _written_token=$(jq -r '.channels.matrix.accessToken' /root/manager-workspace/openclaw.json 2>/dev/null)\n log \"Matrix token written from template (prefix: ${_written_token:0:10}...)\"\nfi\n\n# Cloud/K8s mode: overlay cloud-specific settings onto generated config\nif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ] || [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n log \"Applying cloud/k8s overlay to openclaw.json...\"\n jq --arg homeserver \"${HICLAW_MATRIX_URL}\" \\\n --arg gateway \"${HICLAW_AI_GATEWAY_URL}/v1\" \\\n --arg key \"${HICLAW_MANAGER_GATEWAY_KEY}\" \\\n '.channels.matrix.homeserver = $homeserver\n | .models.providers[\"hiclaw-gateway\"].baseUrl = $gateway\n | .models.providers[\"hiclaw-gateway\"].apiKey = $key\n | ((.hooks.token // \"\") as $ht | if $ht == $key or $ht == ($key + \"-hooks\" | @base64) then del(.hooks) else . end)\n | .commands.restart = false\n | if .agents.defaults.memorySearch then .agents.defaults.memorySearch.remote.baseUrl = $gateway | .agents.defaults.memorySearch.remote.apiKey = $key else . end' \\\n /root/manager-workspace/openclaw.json > /tmp/openclaw-cloud.json && \\\n mv /tmp/openclaw-cloud.json /root/manager-workspace/openclaw.json\n log \"Cloud/K8s overlay applied\"\nfi\n\n# ============================================================\n# Optional: enable openclaw-cms-plugin observability\n# Config is applied at runtime so secrets stay out of image layers.\n# ============================================================\nCMS_TRACES_ENABLED=\"$(echo \"${HICLAW_CMS_TRACES_ENABLED:-false}\" | tr '[:upper:]' '[:lower:]')\"\nif [ \"${CMS_TRACES_ENABLED}\" = \"true\" ]; then\n CMS_PLUGIN_NAME=\"openclaw-cms-plugin\"\n CMS_PLUGIN_DIR=\"${OPENCLAW_CMS_PLUGIN_DIR:-/opt/openclaw/extensions/openclaw-cms-plugin}\"\n CMS_PLUGIN_MANIFEST=\"${CMS_PLUGIN_DIR}/openclaw.plugin.json\"\n DIAG_PLUGIN_NAME=\"diagnostics-otel\"\n DIAG_PLUGIN_DIR=\"/opt/openclaw/extensions/diagnostics-otel\"\n CMS_LICENSE_KEY=\"${HICLAW_CMS_LICENSE_KEY:-}\"\n CMS_PROJECT=\"${HICLAW_CMS_PROJECT:-}\"\n CMS_METRICS_ENABLED=\"${HICLAW_CMS_METRICS_ENABLED:-false}\"\n\n if [ ! -f \"${CMS_PLUGIN_MANIFEST}\" ]; then\n log \"WARNING: ${CMS_PLUGIN_NAME} manifest not found at ${CMS_PLUGIN_MANIFEST}, skipping plugin config\"\n else\n _missing=0\n [ -z \"${HICLAW_CMS_ENDPOINT:-}\" ] && log \"WARNING: HICLAW_CMS_ENDPOINT is required when HICLAW_CMS_TRACES_ENABLED=true\" && _missing=1\n [ -z \"${CMS_LICENSE_KEY:-}\" ] && log \"WARNING: HICLAW_CMS_LICENSE_KEY is required when HICLAW_CMS_TRACES_ENABLED=true\" && _missing=1\n [ -z \"${HICLAW_CMS_WORKSPACE:-}\" ] && log \"WARNING: HICLAW_CMS_WORKSPACE is required when HICLAW_CMS_TRACES_ENABLED=true\" && _missing=1\n\n if [ \"${_missing}\" = \"0\" ]; then\n CMS_SERVICE_NAME=\"${HICLAW_CMS_SERVICE_NAME:-hiclaw-manager}\"\n CMS_ENABLE_METRICS=\"${CMS_METRICS_ENABLED}\"\n DIAG_AVAILABLE=\"0\"\n _metrics_lc=\"$(echo \"${CMS_ENABLE_METRICS}\" | tr '[:upper:]' '[:lower:]')\"\n if [ \"${_metrics_lc}\" = \"true\" ]; then\n if [ -f \"${DIAG_PLUGIN_DIR}/package.json\" ]; then\n DIAG_AVAILABLE=\"1\"\n if [ ! -d \"${DIAG_PLUGIN_DIR}/node_modules\" ]; then\n log \"diagnostics-otel dependencies missing, installing...\"\n if (cd \"${DIAG_PLUGIN_DIR}\" && npm install --omit=dev --ignore-scripts >/tmp/hiclaw-diag-install.log 2>&1); then\n log \"diagnostics-otel dependencies installed\"\n else\n log \"WARNING: diagnostics-otel npm install failed, metrics plugin may not load\"\n fi\n else\n log \"diagnostics-otel dependencies already present\"\n fi\n else\n log \"WARNING: diagnostics-otel package.json not found at ${DIAG_PLUGIN_DIR}, metrics plugin may not load\"\n fi\n fi\n\n log \"Applying ${CMS_PLUGIN_NAME} config to openclaw.json...\"\n jq --arg pluginName \"${CMS_PLUGIN_NAME}\" \\\n --arg pluginDir \"${CMS_PLUGIN_DIR}\" \\\n --arg endpoint \"${HICLAW_CMS_ENDPOINT}\" \\\n --arg licenseKey \"${CMS_LICENSE_KEY}\" \\\n --arg armsProject \"${CMS_PROJECT}\" \\\n --arg cmsWorkspace \"${HICLAW_CMS_WORKSPACE}\" \\\n --arg serviceName \"${CMS_SERVICE_NAME}\" \\\n --arg diagPluginName \"${DIAG_PLUGIN_NAME}\" \\\n --arg diagPluginDir \"${DIAG_PLUGIN_DIR}\" \\\n --arg metricsRaw \"${CMS_ENABLE_METRICS}\" \\\n --arg diagAvailableRaw \"${DIAG_AVAILABLE}\" \\\n '\n .plugins = (.plugins // {})\n | .plugins.load = (.plugins.load // {})\n | .plugins.entries = (.plugins.entries // {})\n | if (.plugins.allow | type) != \"array\" then .plugins.allow = [] else . end\n | if (.plugins.allow | index($pluginName)) == null then .plugins.allow += [$pluginName] else . end\n | if (.plugins.load.paths | type) != \"array\" then .plugins.load.paths = [] else . end\n | if (.plugins.load.paths | index($pluginDir)) == null then .plugins.load.paths += [$pluginDir] else . end\n | .plugins.entries[$pluginName] = {\n \"enabled\": true,\n \"config\": {\n \"endpoint\": $endpoint,\n \"headers\": {\n \"x-arms-license-key\": $licenseKey,\n \"x-arms-project\": $armsProject,\n \"x-cms-workspace\": $cmsWorkspace\n },\n \"serviceName\": $serviceName\n }\n }\n\n # diagnostics-otel metrics (optional)\n | ($metricsRaw | ascii_downcase) as $m\n | ($diagAvailableRaw == \"1\") as $diagAvailable\n | (($m == \"true\") and $diagAvailable) as $metricsEnabled\n | if $metricsEnabled then\n (if (.plugins.allow | index($diagPluginName)) == null then .plugins.allow += [$diagPluginName] else . end)\n | (if (.plugins.load.paths | index($diagPluginDir)) == null then .plugins.load.paths += [$diagPluginDir] else . end)\n | .plugins.entries[$diagPluginName].enabled = true\n | .diagnostics = (.diagnostics // {})\n | .diagnostics.otel = (.diagnostics.otel // {})\n | .diagnostics.enabled = true\n | .diagnostics.otel.enabled = true\n | .diagnostics.otel.endpoint = $endpoint\n | .diagnostics.otel.protocol = (.diagnostics.otel.protocol // \"http/protobuf\")\n | .diagnostics.otel.headers = {\n \"x-arms-license-key\": $licenseKey,\n \"x-arms-project\": $armsProject,\n \"x-cms-workspace\": $cmsWorkspace\n }\n | .diagnostics.otel.serviceName = $serviceName\n | .diagnostics.otel.metrics = true\n | .diagnostics.otel.traces = (.diagnostics.otel.traces // false)\n | .diagnostics.otel.logs = (.diagnostics.otel.logs // false)\n else\n .\n end\n ' /root/manager-workspace/openclaw.json > /tmp/openclaw-cms.json && \\\n mv /tmp/openclaw-cms.json /root/manager-workspace/openclaw.json\n log \"${CMS_PLUGIN_NAME} config applied (metrics=${CMS_ENABLE_METRICS}, service=${CMS_SERVICE_NAME})\"\n else\n log \"Skipping ${CMS_PLUGIN_NAME} config due to missing required env vars\"\n fi\n fi\nfi\n\n# ============================================================\n# Detect container runtime (for Worker creation)\n# ============================================================\nsource /opt/hiclaw/scripts/lib/container-api.sh\nif container_api_available; then\n log \"Container runtime socket detected at ${CONTAINER_SOCKET} — direct Worker creation enabled\"\n export HICLAW_CONTAINER_RUNTIME=\"socket\"\nelif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ] || [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n log \"Cloud/K8s mode — Workers created via controller API\"\n export HICLAW_CONTAINER_RUNTIME=\"cloud\"\nelse\n log \"No container runtime found — Worker creation will output install commands\"\n export HICLAW_CONTAINER_RUNTIME=\"none\"\nfi\n\n# ============================================================\n# Upgrade Worker openclaw.json: merge known models + E2EE flag into existing configs\n# Existing workers in MinIO may have old single-model configs or missing encryption field.\n# Merge template models so they can hot-switch without restart.\n# ============================================================\nREGISTRY_FILE=\"/root/manager-workspace/workers-registry.json\"\nif [ -f \"${REGISTRY_FILE}\" ]; then\n # Use known-models.json (valid JSON) instead of template (contains ${VAR} placeholders)\n KNOWN_MODELS_FILE=\"/opt/hiclaw/configs/known-models.json\"\n if [ -f \"${KNOWN_MODELS_FILE}\" ]; then\n _KNOWN_MODELS=$(cat \"${KNOWN_MODELS_FILE}\")\n for _wname in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_wname}\" ] && continue\n _minio_path=\"${HICLAW_STORAGE_PREFIX}/agents/${_wname}/openclaw.json\"\n _tmp_in=\"/tmp/openclaw-${_wname}-models-upgrade-in.json\"\n if mc cp \"${_minio_path}\" \"${_tmp_in}\" 2>/dev/null; then\n _tmp_out=\"/tmp/openclaw-${_wname}-models-upgrade-out.json\"\n # Idempotent merge: add missing known models, rebuild aliases, set e2ee.\n # Always runs — jq deduplicates by model id, so re-runs are safe.\n jq --argjson known_models \"${_KNOWN_MODELS}\" \\\n --argjson e2ee \"${MATRIX_E2EE_ENABLED}\" '\n .models.providers[\"hiclaw-gateway\"].models as $existing\n | ($existing | map(.id)) as $existing_ids\n | ($known_models | map(select(.id as $id | $existing_ids | index($id) | not))) as $new\n | .models.providers[\"hiclaw-gateway\"].models = ($existing + $new)\n | (.models.providers[\"hiclaw-gateway\"].models | map({ (\"hiclaw-gateway/\" + .id): { \"alias\": .id } }) | add // {}) as $aliases\n | .agents.defaults.models = ((.agents.defaults.models // {}) + $aliases)\n | .channels.matrix.encryption = $e2ee\n | .channels.matrix.autoJoin = \"always\"\n | .tools = (.tools // {})\n | .tools.exec = ((.tools.exec // {}) + {\"host\":\"gateway\",\"security\":\"full\",\"ask\":\"off\"})\n | .tools.elevated = (.tools.elevated // {})\n | .tools.elevated.enabled = true\n | .tools.elevated.allowFrom |= ((. // {}) | .matrix = [\"*\"])\n | .agents.defaults.elevatedDefault = \"full\"\n ' \"${_tmp_in}\" > \"${_tmp_out}\" 2>/dev/null\n if ! diff -q \"${_tmp_in}\" \"${_tmp_out}\" > /dev/null 2>&1; then\n if mc cp \"${_tmp_out}\" \"${_minio_path}\" 2>/dev/null; then\n _new_count=$(jq '.models.providers[\"hiclaw-gateway\"].models | length' \"${_tmp_out}\" 2>/dev/null)\n log \"Worker ${_wname}: upgraded openclaw.json (models: ${_new_count}, e2ee: ${MATRIX_E2EE_ENABLED})\"\n fi\n fi\n rm -f \"${_tmp_in}\" \"${_tmp_out}\"\n fi\n done\n fi\nfi\n\n# ============================================================\n# Ensure Worker Matrix password files exist in MinIO (E2EE fix)\n# Workers need to re-login on restart to get a fresh device_id.\n# Older workers created before this fix won't have the password file.\n# ============================================================\nif [ -f \"${REGISTRY_FILE}\" ]; then\n for _wname in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_wname}\" ] && continue\n _creds_file=\"/data/worker-creds/${_wname}.env\"\n if [ -f \"${_creds_file}\" ]; then\n # Check if password file already exists in MinIO\n if ! mc stat \"${HICLAW_STORAGE_PREFIX}/agents/${_wname}/credentials/matrix/password\" > /dev/null 2>&1; then\n source \"${_creds_file}\"\n if [ -n \"${WORKER_PASSWORD}\" ]; then\n _tmp_pw=\"/tmp/matrix-pw-${_wname}\"\n echo -n \"${WORKER_PASSWORD}\" > \"${_tmp_pw}\"\n mc cp \"${_tmp_pw}\" \"${HICLAW_STORAGE_PREFIX}/agents/${_wname}/credentials/matrix/password\" 2>/dev/null \\\n && log \"Worker ${_wname}: wrote Matrix password to MinIO (E2EE re-login fix)\" \\\n || log \"Worker ${_wname}: WARNING: failed to write Matrix password to MinIO\"\n rm -f \"${_tmp_pw}\"\n fi\n fi\n fi\n done\nfi\n\n# ============================================================\n# Recreate Worker containers as needed after Manager restart.\n# Workers are on hiclaw-net; Docker DNS resolves *-local.hiclaw.io via\n# the Manager's network aliases, so IP changes don't require worker recreation.\n# Only recreate stopped/missing workers.\n# ============================================================\nif container_api_available; then\n REGISTRY_FILE=\"/root/manager-workspace/workers-registry.json\"\n if [ -f \"${REGISTRY_FILE}\" ]; then\n for _worker_name in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_worker_name}\" ] && continue\n\n # Skip remote workers — they are not Manager-managed containers.\n _deployment=$(jq -r --arg w \"${_worker_name}\" '.workers[$w].deployment // \"local\"' \"${REGISTRY_FILE}\" 2>/dev/null)\n if [ \"${_deployment}\" = \"remote\" ]; then\n log \"Worker ${_worker_name} is remote, skipping container recreate\"\n continue\n fi\n\n _status=$(container_status_worker \"${_worker_name}\")\n if [ \"${_status}\" = \"running\" ]; then\n log \"Worker running: ${_worker_name}, skipping\"\n continue\n fi\n # Container missing or stopped — recreate.\n log \"Worker container ${_status}: ${_worker_name}, recreating...\"\n _creds_file=\"/data/worker-creds/${_worker_name}.env\"\n if [ -f \"${_creds_file}\" ]; then\n source \"${_creds_file}\"\n _runtime=$(jq -r --arg w \"${_worker_name}\" '.workers[$w].runtime // \"openclaw\"' \"${REGISTRY_FILE}\" 2>/dev/null)\n _recreated=false\n for _attempt in 1 2 3; do\n local _env_map _create_body\n _env_map=$(jq -cn \\\n --arg name \"${_worker_name}\" \\\n --arg fak \"${_worker_name}\" \\\n --arg fsk \"${WORKER_MINIO_PASSWORD:-}\" \\\n --arg fs_domain \"${HICLAW_FS_DOMAIN:-fs-local.hiclaw.io}\" \\\n --arg controller_url \"${HICLAW_CONTROLLER_URL:-}\" \\\n '{\n \"HICLAW_WORKER_NAME\": $name,\n \"HICLAW_FS_ENDPOINT\": (\"http://\" + ($fs_domain | split(\":\")[0]) + \":8080\"),\n \"HICLAW_FS_ACCESS_KEY\": $fak,\n \"HICLAW_FS_SECRET_KEY\": $fsk\n }\n | if $controller_url != \"\" then . + {\"HICLAW_CONTROLLER_URL\": $controller_url} else . end')\n _create_body=$(jq -cn --arg name \"${_worker_name}\" --arg runtime \"${_runtime}\" --argjson env \"${_env_map}\" '{name: $name, runtime: $runtime, env: $env}')\n worker_backend_create \"${_create_body}\" > /dev/null 2>&1 && _recreated=true && break\n log \" Attempt ${_attempt}/3 failed for ${_worker_name}, retrying in $((5 * _attempt))s...\"\n sleep $((5 * _attempt))\n done\n if [ \"${_recreated}\" = true ]; then\n log \" Recreated ${_runtime} worker: ${_worker_name}\"\n else\n log \" ERROR: Failed to recreate ${_worker_name} after 3 attempts\"\n fi\n else\n log \" WARNING: No credentials found for ${_worker_name} (${_creds_file} missing), skipping\"\n fi\n done\n fi\nfi\n\n# ============================================================\n# Notify workers of builtin updates if upgrade happened\n# Builtin files (AGENTS.md, skills) are already synced by upgrade-builtins.sh\n#\n# Cooldown: skip notification if the last successful notify was within\n# NOTIFY_COOLDOWN_SECS (default 3600s / 1 hour). This prevents repeated\n# notifications when the Manager crash-loops and re-runs upgrade-builtins\n# on every restart (e.g. IMAGE_VERSION=latest always triggers upgrade).\n# ============================================================\nNOTIFY_COOLDOWN_SECS=\"${HICLAW_NOTIFY_COOLDOWN_SECS:-3600}\"\nNOTIFY_TS_FILE=\"/root/manager-workspace/.last-worker-notify-ts\"\n\nif [ -f /root/manager-workspace/.upgrade-pending-worker-notify ]; then\n _now=$(date +%s)\n _last_notify=$(cat \"${NOTIFY_TS_FILE}\" 2>/dev/null || echo \"0\")\n _elapsed=$(( _now - _last_notify ))\n\n if [ \"${_elapsed}\" -lt \"${NOTIFY_COOLDOWN_SECS}\" ]; then\n log \"Skipping worker builtin notification (last notify ${_elapsed}s ago, cooldown ${NOTIFY_COOLDOWN_SECS}s)\"\n rm -f /root/manager-workspace/.upgrade-pending-worker-notify\n else\n log \"Notifying workers about builtin updates...\"\n REGISTRY_FILE=\"/root/manager-workspace/workers-registry.json\"\n _notify_ok=false\n if [ -f \"${REGISTRY_FILE}\" ]; then\n for _worker_name in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_worker_name}\" ] && continue\n _room_id=$(jq -r --arg w \"${_worker_name}\" '.workers[$w].room_id // empty' \"${REGISTRY_FILE}\" 2>/dev/null)\n if [ -n \"${_room_id}\" ]; then\n _worker_id=\"@${_worker_name}:${MATRIX_DOMAIN}\"\n _txn_id=\"upgrade-$(date +%s%N)\"\n _msg=\"@${_worker_name}:${MATRIX_DOMAIN} Manager upgraded builtin files (AGENTS.md, skills). Please use your file-sync skill to sync the latest config.\"\n _raw=$(curl -s -w '\\nHTTP_CODE:%{http_code}' -X PUT \\\n \"${HICLAW_MATRIX_URL}/_matrix/client/v3/rooms/${_room_id}/send/m.room.message/${_txn_id}\" \\\n -H \"Authorization: Bearer ${MANAGER_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d \"{\\\"msgtype\\\":\\\"m.text\\\",\\\"body\\\":\\\"${_msg}\\\",\\\"m.mentions\\\":{\\\"user_ids\\\":[\\\"${_worker_id}\\\"]}}\" \\\n 2>&1) || true\n _http_code=$(echo \"${_raw}\" | tail -1 | sed 's/HTTP_CODE://')\n _notify_resp=$(echo \"${_raw}\" | sed '$d')\n if echo \"${_notify_resp}\" | jq -e '.event_id' > /dev/null 2>&1; then\n log \" Notified ${_worker_name}\"; _notify_ok=true\n else\n log \" WARNING: Failed to notify ${_worker_name} (HTTP ${_http_code}): ${_notify_resp}\"\n fi\n fi\n done\n fi\n # Record timestamp only if at least one notification succeeded\n if [ \"${_notify_ok}\" = true ]; then\n echo \"${_now}\" > \"${NOTIFY_TS_FILE}\"\n fi\n rm -f /root/manager-workspace/.upgrade-pending-worker-notify\n fi\nfi\n\n# ============================================================\n# Start Manager Agent\n# ============================================================\nlog \"Starting Manager Agent (${MANAGER_RUNTIME})...\"\n\n# HOME is already set to /root/manager-workspace via docker run -e HOME=...\ncd \"${HOME}\"\n\n# Ensure host credential symlinks exist under HOME\nif [ -d \"/host-share\" ]; then\n [ -f \"/host-share/.gitconfig\" ] && ln -sf \"/host-share/.gitconfig\" \"${HOME}/.gitconfig\"\nfi\n\nlog \"HOME=${HOME} (manager-workspace, host-mounted)\"\n\n# ── Render agent doc templates ────────────────────────────────────────────\n# Replace ${VAR} placeholders with actual values so the AI agent reads\n# plain text and never needs to resolve environment variables.\nexport MANAGER_MATRIX_TOKEN MANAGER_TOKEN HIGRESS_COOKIE_FILE\nRENDER=/opt/hiclaw/scripts/lib/render-skills.sh\nlog \"Rendering agent doc templates...\"\n# Manager-owned docs (workspace)\nbash \"$RENDER\" /root/manager-workspace/skills\nbash \"$RENDER\" /root/manager-workspace/skills-alpha\nbash \"$RENDER\" /root/manager-workspace AGENTS.md TOOLS.md HEARTBEAT.md SOUL.md\n# Worker templates (workspace + image) — rendered before push to MinIO\n# so Workers (including remote pip-install) receive plain text\nbash \"$RENDER\" /root/manager-workspace/worker-skills\nbash \"$RENDER\" /root/manager-workspace/worker-agent\nbash \"$RENDER\" /root/manager-workspace/copaw-worker-agent\nbash \"$RENDER\" /root/manager-workspace/hermes-worker-agent\nbash \"$RENDER\" /opt/hiclaw/agent/worker-skills\nbash \"$RENDER\" /opt/hiclaw/agent/worker-agent\nbash \"$RENDER\" /opt/hiclaw/agent/copaw-worker-agent\nbash \"$RENDER\" /opt/hiclaw/agent/hermes-worker-agent\nlog \"Agent doc templates rendered\"\n\n# Cloud mode: start background file sync (workspace ↔ OSS) and initial push\nif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n log \"Syncing initial workspace to OSS...\"\n ensure_mc_credentials\n mc mirror /root/manager-workspace/ \"${HICLAW_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" 2>/dev/null || true\n\n # Local → OSS: change-triggered sync\n (\n while true; do\n CHANGED=$(find /root/manager-workspace/ -type f -newermt \"15 seconds ago\" 2>/dev/null | head -1)\n if [ -n \"${CHANGED}\" ]; then\n ensure_mc_credentials 2>/dev/null || true\n mc mirror /root/manager-workspace/ \"${HICLAW_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" --exclude \".npm/**\" \\\n --exclude \".local/**\" --exclude \".mc/**\" 2>/dev/null || true\n fi\n sleep 10\n done\n ) &\n log \"Local→OSS sync started (PID: $!)\"\n\n # OSS → Local: periodic pull (shared data, agent configs)\n (\n while true; do\n sleep 300\n ensure_mc_credentials 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/shared/\" /root/hiclaw-fs/shared/ --overwrite --newer-than \"5m\" 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/agents/\" /root/hiclaw-fs/agents/ --overwrite --newer-than \"5m\" 2>/dev/null || true\n done\n ) &\n log \"OSS→Local sync started (every 5m, PID: $!)\"\nfi\n\n# K8s mode: start background file sync (workspace ↔ MinIO)\nif [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n log \"Syncing initial workspace to MinIO...\"\n mc mirror /root/manager-workspace/ \"${HICLAW_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" 2>/dev/null || true\n\n # Local → MinIO: change-triggered sync\n (\n while true; do\n CHANGED=$(find /root/manager-workspace/ -type f -newermt \"15 seconds ago\" 2>/dev/null | head -1)\n if [ -n \"${CHANGED}\" ]; then\n mc mirror /root/manager-workspace/ \"${HICLAW_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" --exclude \".npm/**\" \\\n --exclude \".local/**\" --exclude \".mc/**\" 2>/dev/null || true\n fi\n sleep 10\n done\n ) &\n log \"Local→MinIO sync started (PID: $!)\"\n\n # MinIO → Local: periodic pull (shared data, agent configs)\n (\n while true; do\n sleep 300\n mc mirror \"${HICLAW_STORAGE_PREFIX}/shared/\" /root/hiclaw-fs/shared/ --overwrite --newer-than \"5m\" 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/agents/\" /root/hiclaw-fs/agents/ --overwrite --newer-than \"5m\" 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/hiclaw-config/\" /root/hiclaw-fs/hiclaw-config/ --overwrite --newer-than \"15s\" 2>/dev/null || true\n done\n ) &\n log \"MinIO→Local sync started (every 5m, PID: $!)\"\nfi\n\n# ============================================================\n# Auto-generate Manager mcporter config for pre-configured MCP servers\n# If HICLAW_GITHUB_TOKEN was set at install time, setup-higress.sh already\n# configured GitHub MCP on Higress. Run setup-mcp-server.sh now so that\n# config/mcporter.json exists when the Agent starts — no need to ask user for PAT.\n# ============================================================\nif [ -n \"${HICLAW_GITHUB_TOKEN}\" ] && [ \"${HICLAW_RUNTIME}\" != \"aliyun\" ] && [ \"${HICLAW_RUNTIME}\" != \"k8s\" ]; then\n if [ ! -f \"${HOME}/config/mcporter.json\" ]; then\n log \"Auto-generating Manager mcporter config for GitHub MCP (HICLAW_GITHUB_TOKEN set)...\"\n bash /opt/hiclaw/agent/skills/mcp-server-management/scripts/setup-mcp-server.sh \\\n github \"${HICLAW_GITHUB_TOKEN}\" 2>&1 | while IFS= read -r line; do log \" [setup-mcp] ${line}\"; done || \\\n log \"WARNING: setup-mcp-server.sh failed — Agent may need to configure GitHub MCP manually\"\n else\n log \"Manager mcporter config already exists, skipping auto-generate\"\n fi\nfi\n\n# ============================================================\n# Runtime-specific startup\n# ============================================================\nif [ \"${MANAGER_RUNTIME}\" = \"copaw\" ]; then\n # Delegate to CoPaw startup script\n exec /opt/hiclaw/scripts/init/start-copaw-manager.sh\nelse\n # ── OpenClaw Runtime ─────────────────────────────────────────────────────\n log \"Starting OpenClaw Manager...\"\n\n export OPENCLAW_CONFIG_PATH=\"/root/manager-workspace/openclaw.json\"\n\n # Symlink to default OpenClaw config path so CLI commands find the config\n mkdir -p \"${HOME}/.openclaw\"\n ln -sf \"/root/manager-workspace/openclaw.json\" \"${HOME}/.openclaw/openclaw.json\"\n\n # Clean orphaned session write locks (e.g. from SIGKILL or crash before exit handlers)\n # Prevents \"session file locked (timeout 10000ms)\" when PID was reused\n find \"${HOME}/.openclaw/agents\" -name \"*.jsonl.lock\" -delete 2>/dev/null || true\n log \"Cleaned up any orphaned session write locks\"\n\n # Clean Matrix crypto storage (SQLite WAL may be corrupted after unclean shutdown)\n # Crypto state is re-negotiated on startup; losing it only means re-establishing E2EE sessions\n rm -rf \"${HOME}/.openclaw/matrix\" 2>/dev/null || true\n log \"Cleaned Matrix crypto storage (will re-establish E2EE sessions)\"\n\n # Launch OpenClaw\n # Disable full-process respawn so the CLI uses its internal restart loop.\n # Without this, config reload spawns a detached child and exits, then\n # supervisord restarts the CLI — resulting in two gateway processes.\n export OPENCLAW_NO_RESPAWN=1\n\n # Optional matrix-plugin trace logging — when HICLAW_MATRIX_DEBUG=1 is set\n # in the manager environment (propagated by install / supervisord), turn on\n # OPENCLAW_MATRIX_DEBUG so the matrix plugin emits structured INFO-level\n # lifecycle traces (sync.state transitions, room.invite/join, message\n # handler arrival + filter outcomes). Useful for diagnosing \"worker never\n # joined the room\" / \"manager never replied\" hangs without rebuilding the\n # image.\n if [ \"${HICLAW_MATRIX_DEBUG:-}\" = \"1\" ] && [ -z \"${OPENCLAW_MATRIX_DEBUG:-}\" ]; then\n export OPENCLAW_MATRIX_DEBUG=1\n log \"HICLAW_MATRIX_DEBUG=1 detected; OPENCLAW_MATRIX_DEBUG=1 exported for matrix plugin tracing\"\n fi\n\n exec openclaw gateway run --verbose --force\nfi\n"}, "92ec7c2f6e5322e792764d41e58b09783995083c": {"t": 1785764749.4747381, "n": 67303, "h": "fddcf9399b4aca8c", "c": "#!/bin/bash\n# start-manager-agent.sh - Initialize and start the Manager Agent\n# Supports local (supervisord), cloud (SAE), and K8s (Helm) deployments.\n# In local mode this is the last supervisord component to start (priority 800).\n# In cloud/k8s mode (HICLAW_RUNTIME=aliyun|k8s) this is the container entrypoint.\n#\n# Runtime selection:\n# HICLAW_MANAGER_RUNTIME=openclaw (default) - OpenClaw gateway mode\n# HICLAW_MANAGER_RUNTIME=copaw - CoPaw workspace mode\n# (hermes runtime is supported for Workers only; Managers run openclaw or copaw.)\n\nsource /opt/hiclaw/scripts/lib/hiclaw-env.sh\n\n# ============================================================\n# Runtime selection\n# ============================================================\nMANAGER_RUNTIME=\"${HICLAW_MANAGER_RUNTIME:-openclaw}\"\ncase \"${MANAGER_RUNTIME}\" in\n copaw)\n log \"Manager runtime: CoPaw (Python workspace)\"\n ;;\n *)\n log \"Manager runtime: OpenClaw (Node.js gateway)\"\n MANAGER_RUNTIME=\"openclaw\"\n ;;\nesac\n\n# ============================================================\n# Set timezone from TZ env var\n# ============================================================\nif [ -n \"${TZ}\" ] && [ -f \"/usr/share/zoneinfo/${TZ}\" ]; then\n ln -sf \"/usr/share/zoneinfo/${TZ}\" /etc/localtime\n echo \"${TZ}\" > /etc/timezone\n log \"Timezone set to ${TZ}\"\nfi\n\nexport MATRIX_DOMAIN=\"${HICLAW_MATRIX_DOMAIN:-matrix-local.hiclaw.io:8080}\"\nAI_GATEWAY_DOMAIN=\"${HICLAW_AI_GATEWAY_DOMAIN:-aigw-local.hiclaw.io}\"\n\n# ============================================================\n# YOLO mode promotion\n# ============================================================\n# In embedded mode the controller does not propagate HICLAW_YOLO to the\n# manager container, but installer / test scripts touch a marker file at\n# `${WORKSPACE}/yolo-mode` instead. Promote that marker to the env var so the\n# agent's documented YOLO check (`HICLAW_YOLO=1`) reliably detects it without\n# depending on filesystem lookups during a turn.\nif [ -z \"${HICLAW_YOLO:-}\" ] && [ -f /root/manager-workspace/yolo-mode ]; then\n export HICLAW_YOLO=1\n log \"YOLO mode marker detected at /root/manager-workspace/yolo-mode; HICLAW_YOLO=1 exported\"\nfi\n\n# ============================================================\n# Cloud/K8s mode: validate required environment variables + initial credentials\n# ============================================================\nif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ] || [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n : \"${HICLAW_MATRIX_URL:?HICLAW_MATRIX_URL is required}\"\n : \"${HICLAW_MATRIX_DOMAIN:?HICLAW_MATRIX_DOMAIN is required}\"\n : \"${HICLAW_AI_GATEWAY_URL:?HICLAW_AI_GATEWAY_URL is required}\"\n if [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n : \"${HICLAW_MANAGER_GATEWAY_KEY:?HICLAW_MANAGER_GATEWAY_KEY is required}\"\n : \"${HICLAW_MANAGER_PASSWORD:?HICLAW_MANAGER_PASSWORD is required (cloud containers are stateless, password must be injected)}\"\n fi\n if [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n # K8s mode: controller handles initialization (admin registration, Higress setup).\n # Manager only needs credentials injected by the ManagerReconciler.\n : \"${HICLAW_MANAGER_GATEWAY_KEY:?HICLAW_MANAGER_GATEWAY_KEY is required (injected by controller)}\"\n : \"${HICLAW_MANAGER_PASSWORD:?HICLAW_MANAGER_PASSWORD is required (injected by controller)}\"\n else\n # Cloud (aliyun) mode: Manager still does its own initialization\n : \"${HICLAW_REGISTRATION_TOKEN:?HICLAW_REGISTRATION_TOKEN is required}\"\n : \"${HICLAW_ADMIN_USER:?HICLAW_ADMIN_USER is required}\"\n : \"${HICLAW_ADMIN_PASSWORD:?HICLAW_ADMIN_PASSWORD is required}\"\n fi\n log \"${HICLAW_RUNTIME} mode: validating environment... OK\"\n log \" Matrix: ${HICLAW_MATRIX_URL}, AI Gateway: ${HICLAW_AI_GATEWAY_URL}, Storage: ${HICLAW_FS_BUCKET}\"\n if [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n ensure_mc_credentials || { log \"FATAL: Initial STS credential fetch failed\"; exit 1; }\n fi\nfi\n\n# ============================================================\n# Local mode: host symlinks, /etc/hosts, wait for local services\n# ============================================================\nif [ \"${HICLAW_RUNTIME}\" != \"aliyun\" ] && [ \"${HICLAW_RUNTIME}\" != \"k8s\" ]; then\n # Create symlink for host directory access\n if [ -d \"/host-share\" ]; then\n ORIGINAL_HOST_HOME=\"${HOST_ORIGINAL_HOME:-$HOME}\"\n if [ ! -e \"${ORIGINAL_HOST_HOME}\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/root\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/data\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/host-share\" ]; then\n mkdir -p \"$(dirname \"${ORIGINAL_HOST_HOME}\")\"\n ln -sfn /host-share \"${ORIGINAL_HOST_HOME}\"\n log \"Created symlink: ${ORIGINAL_HOST_HOME} -> /host-share\"\n else\n ln -sfn /host-share /root/host-home\n log \"Created fallback symlink: /root/host-home -> /host-share\"\n fi\n fi\n\n # Add local domains to /etc/hosts\n HOSTS_DOMAINS=\"${MATRIX_DOMAIN%%:*} ${HICLAW_MATRIX_CLIENT_DOMAIN:-matrix-client-local.hiclaw.io} ${AI_GATEWAY_DOMAIN} ${HICLAW_FS_DOMAIN:-fs-local.hiclaw.io}\"\n if ! grep -q \"${AI_GATEWAY_DOMAIN}\" /etc/hosts 2>/dev/null; then\n echo \"127.0.0.1 ${HOSTS_DOMAINS}\" >> /etc/hosts\n log \"Added local domains to /etc/hosts\"\n fi\n\n # Wait for local infrastructure\n waitForService \"Higress Gateway\" \"127.0.0.1\" 8080 180\n waitForService \"Higress Console\" \"127.0.0.1\" 8001 180\n waitForService \"Tuwunel\" \"127.0.0.1\" 6167 120\n waitForHTTP \"Tuwunel Matrix API\" \"${HICLAW_MATRIX_URL}/_tuwunel/server_version\" 120\n waitForService \"MinIO\" \"127.0.0.1\" 9000 120\nelse\n # Cloud/K8s mode: wait for external Tuwunel\n log \"Waiting for Tuwunel Matrix server at ${HICLAW_MATRIX_URL}...\"\n _retry=0\n while [ \"${_retry}\" -lt 30 ]; do\n if curl -sf \"${HICLAW_MATRIX_URL}/_matrix/client/versions\" > /dev/null 2>&1; then\n log \"Tuwunel is ready\"\n break\n fi\n _retry=$((_retry + 1))\n log \" Waiting for Tuwunel (attempt ${_retry}/30)...\"\n sleep 5\n done\n if [ \"${_retry}\" -ge 30 ]; then\n log \"ERROR: Tuwunel not reachable at ${HICLAW_MATRIX_URL}\"\n exit 1\n fi\nfi\n\n# ============================================================\n# Auto-generate secrets if not provided via environment\n# Persisted to /data so they survive container restart\n# ============================================================\nSECRETS_FILE=\"/data/hiclaw-secrets.env\"\nif [ -f \"${SECRETS_FILE}\" ]; then\n source \"${SECRETS_FILE}\"\n log \"Loaded persisted secrets from ${SECRETS_FILE}\"\nfi\n\nif [ -z \"${HICLAW_MANAGER_GATEWAY_KEY}\" ]; then\n export HICLAW_MANAGER_GATEWAY_KEY=\"$(generateKey 32)\"\n log \"Auto-generated HICLAW_MANAGER_GATEWAY_KEY\"\nfi\nif [ -z \"${HICLAW_MANAGER_PASSWORD}\" ]; then\n export HICLAW_MANAGER_PASSWORD=\"$(generateKey 16)\"\n log \"Auto-generated HICLAW_MANAGER_PASSWORD\"\nfi\n\n# Persist secrets so they survive supervisord restart\nmkdir -p /data\ncat > \"${SECRETS_FILE}\" <<EOF\nexport HICLAW_MANAGER_GATEWAY_KEY=\"${HICLAW_MANAGER_GATEWAY_KEY}\"\nexport HICLAW_MANAGER_PASSWORD=\"${HICLAW_MANAGER_PASSWORD}\"\nEOF\nchmod 600 \"${SECRETS_FILE}\"\n\n# Cloud mode: pull workspace from OSS before initialization\nif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n HICLAW_FS=\"/root/hiclaw-fs\"\n mkdir -p \"${HICLAW_FS}/shared\" \"${HICLAW_FS}/agents\"\n log \"Pulling workspace from OSS...\"\n ensure_mc_credentials\n mc mirror \"${HICLAW_STORAGE_PREFIX}/manager/\" /root/manager-workspace/ --overwrite 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/shared/\" \"${HICLAW_FS}/shared/\" --overwrite 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/agents/\" \"${HICLAW_FS}/agents/\" --overwrite 2>/dev/null || true\n # Symlink hiclaw-fs into workspace for agent access\n ln -sfn \"${HICLAW_FS}\" /root/manager-workspace/hiclaw-fs\nfi\n\n# K8s mode: sync workspace from cluster-internal MinIO\nif [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n HICLAW_FS=\"/root/hiclaw-fs\"\n mkdir -p \"${HICLAW_FS}/shared\" \"${HICLAW_FS}/agents\" \"${HICLAW_FS}/hiclaw-config\"\n log \"Configuring mc alias for cluster MinIO...\"\n mc alias set hiclaw \"${HICLAW_FS_ENDPOINT}\" \"${HICLAW_FS_ACCESS_KEY}\" \"${HICLAW_FS_SECRET_KEY}\"\n log \"Syncing workspace from MinIO...\"\n mc mirror \"${HICLAW_STORAGE_PREFIX}/manager/\" /root/manager-workspace/ --overwrite 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/\" \"${HICLAW_FS}/\" --overwrite 2>/dev/null || true\n ln -sfn \"${HICLAW_FS}\" /root/manager-workspace/hiclaw-fs\n touch \"${HICLAW_FS}/.initialized\"\nfi\n\n# ============================================================\n# Initialize / upgrade Manager workspace\n# First boot: full init via upgrade-builtins.sh\n# Subsequent boots: compare image version; upgrade only if changed\n# ============================================================\nmkdir -p /root/manager-workspace\n\nIMAGE_VERSION=$(cat /opt/hiclaw/agent/.builtin-version 2>/dev/null || echo \"unknown\")\nINSTALLED_VERSION=$(cat /root/manager-workspace/.builtin-version 2>/dev/null || echo \"\")\n\nif [ ! -f /root/manager-workspace/.initialized ]; then\n log \"First boot: initializing manager workspace...\"\n bash /opt/hiclaw/scripts/init/upgrade-builtins.sh\n touch /root/manager-workspace/.initialized\n log \"Manager workspace initialized (version: ${IMAGE_VERSION})\"\nelif [ \"${IMAGE_VERSION}\" != \"${INSTALLED_VERSION}\" ] || [ \"${IMAGE_VERSION}\" = \"latest\" ]; then\n log \"Upgrade detected: ${INSTALLED_VERSION} -> ${IMAGE_VERSION}${IMAGE_VERSION:+ (latest: always upgrade)}\"\n bash /opt/hiclaw/scripts/init/upgrade-builtins.sh\n log \"Manager workspace upgraded to version: ${IMAGE_VERSION}\"\nelse\n log \"Workspace up to date (version: ${IMAGE_VERSION})\"\nfi\n\n# Local mode: wait for mc mirror initialization (shared + worker data in /root/hiclaw-fs/)\nif [ \"${HICLAW_RUNTIME}\" != \"aliyun\" ] && [ \"${HICLAW_RUNTIME}\" != \"k8s\" ]; then\n log \"Waiting for MinIO storage initialization...\"\n _minio_wait=0\n while [ ! -f /root/hiclaw-fs/.initialized ]; do\n sleep 2\n _minio_wait=$(( _minio_wait + 1 ))\n if [ \"${_minio_wait}\" -ge 60 ]; then\n log \"ERROR: MinIO storage initialization timed out after 120s\"\n exit 1\n fi\n done\n log \"MinIO storage initialized\"\nfi\n\n# ============================================================\n# Register Matrix users via Registration API (single-step, no UIAA)\n# K8s mode: skip — controller Initializer + ManagerReconciler already did this\n# ============================================================\nif [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n log \"K8s mode: skipping Matrix registration (handled by controller)\"\n # Controller injects HICLAW_MANAGER_PASSWORD via env; login to get token\n log \"Obtaining Manager Matrix access token...\"\n _LOGIN_RESPONSE=$(curl -s -X POST ${HICLAW_MATRIX_URL}/_matrix/client/v3/login \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"type\": \"m.login.password\",\n \"identifier\": {\"type\": \"m.id.user\", \"user\": \"manager\"},\n \"password\": \"'\"${HICLAW_MANAGER_PASSWORD}\"'\"\n }' 2>&1)\n MANAGER_TOKEN=$(echo \"${_LOGIN_RESPONSE}\" | jq -r '.access_token' 2>/dev/null)\n if [ -z \"${MANAGER_TOKEN}\" ] || [ \"${MANAGER_TOKEN}\" = \"null\" ]; then\n log \"ERROR: Failed to obtain Manager Matrix token\"\n log \"ERROR: Login response was: ${_LOGIN_RESPONSE}\"\n exit 1\n fi\n log \"Manager Matrix token obtained (token prefix: ${MANAGER_TOKEN:0:10}...)\"\nelse\nlog \"Registering human admin Matrix account...\"\ncurl -sf -X POST ${HICLAW_MATRIX_URL}/_matrix/client/v3/register \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"username\": \"'\"${HICLAW_ADMIN_USER}\"'\",\n \"password\": \"'\"${HICLAW_ADMIN_PASSWORD}\"'\",\n \"auth\": {\n \"type\": \"m.login.registration_token\",\n \"token\": \"'\"${HICLAW_REGISTRATION_TOKEN}\"'\"\n }\n }' > /dev/null 2>&1 || log \"Admin account may already exist\"\n\nlog \"Registering Manager Agent Matrix account...\"\ncurl -sf -X POST ${HICLAW_MATRIX_URL}/_matrix/client/v3/register \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"username\": \"manager\",\n \"password\": \"'\"${HICLAW_MANAGER_PASSWORD}\"'\",\n \"auth\": {\n \"type\": \"m.login.registration_token\",\n \"token\": \"'\"${HICLAW_REGISTRATION_TOKEN}\"'\"\n }\n }' > /dev/null 2>&1 || log \"Manager account may already exist\"\n\n# Get Manager Agent's Matrix access token\nlog \"Obtaining Manager Matrix access token...\"\n_LOGIN_RESPONSE=$(curl -s -X POST ${HICLAW_MATRIX_URL}/_matrix/client/v3/login \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"type\": \"m.login.password\",\n \"identifier\": {\"type\": \"m.id.user\", \"user\": \"manager\"},\n \"password\": \"'\"${HICLAW_MANAGER_PASSWORD}\"'\"\n }' 2>&1)\n_LOGIN_EXIT=$?\nlog \"Matrix login HTTP exit code: ${_LOGIN_EXIT}\"\nlog \"Matrix login response: ${_LOGIN_RESPONSE}\"\n\nMANAGER_TOKEN=$(echo \"${_LOGIN_RESPONSE}\" | jq -r '.access_token' 2>/dev/null)\n\nif [ -z \"${MANAGER_TOKEN}\" ] || [ \"${MANAGER_TOKEN}\" = \"null\" ]; then\n log \"ERROR: Failed to obtain Manager Matrix token (exit=${_LOGIN_EXIT})\"\n log \"ERROR: Login response was: ${_LOGIN_RESPONSE}\"\n exit 1\nfi\nlog \"Manager Matrix token obtained (token prefix: ${MANAGER_TOKEN:0:10}...)\"\nfi\n\n# ============================================================\n# Higress Console initialization\n# Docker mode: full setup-higress.sh (internal Higress at localhost:8001)\n# K8s mode: skip — controller Initializer handles Higress setup\n# Cloud (aliyun) mode: skip entirely (Higress managed externally)\n# ============================================================\n_HIGRESS_CONSOLE_URL=\"\"\n_HIGRESS_USER=\"${HICLAW_ADMIN_USER}\"\n_HIGRESS_PASS=\"${HICLAW_ADMIN_PASSWORD}\"\nif [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n log \"K8s mode: skipping Higress initialization (handled by controller)\"\nelif [ \"${HICLAW_RUNTIME}\" != \"aliyun\" ]; then\n _HIGRESS_CONSOLE_URL=\"http://127.0.0.1:8001\"\nfi\n\nif [ -n \"${_HIGRESS_CONSOLE_URL}\" ]; then\n COOKIE_FILE=\"/tmp/higress-session-cookie\"\n\n log \"Waiting for Higress Console (${_HIGRESS_CONSOLE_URL}) to be fully ready and initializing admin...\"\n INIT_DONE=false\n for i in $(seq 1 90); do\n INIT_RESULT=$(curl -s -X POST \"${_HIGRESS_CONSOLE_URL}/system/init\" \\\n -H 'Content-Type: application/json' \\\n -d '{\"adminUser\":{\"name\":\"'\"${_HIGRESS_USER}\"'\",\"password\":\"'\"${_HIGRESS_PASS}\"'\",\"displayName\":\"'\"${_HIGRESS_USER}\"'\"}}' 2>/dev/null) || true\n if echo \"${INIT_RESULT}\" | grep -qE '\"success\":true|already.?init' 2>/dev/null; then\n INIT_DONE=true\n break\n fi\n if echo \"${INIT_RESULT}\" | grep -q '\"name\"' 2>/dev/null; then\n INIT_DONE=true\n break\n fi\n sleep 2\n done\n\n if [ \"${INIT_DONE}\" != \"true\" ]; then\n log \"ERROR: Higress Console did not become ready within 180s\"\n exit 1\n fi\n log \"Higress Console init done\"\n\n log \"Logging into Higress Console...\"\n LOGIN_OK=false\n for i in $(seq 1 10); do\n HTTP_CODE=$(curl -s -o /dev/null -w '%{http_code}' -X POST \"${_HIGRESS_CONSOLE_URL}/session/login\" \\\n -H 'Content-Type: application/json' \\\n -c \"${COOKIE_FILE}\" \\\n -d '{\"username\":\"'\"${_HIGRESS_USER}\"'\",\"password\":\"'\"${_HIGRESS_PASS}\"'\"}' 2>/dev/null) || true\n if { [ \"${HTTP_CODE}\" = \"200\" ] || [ \"${HTTP_CODE}\" = \"201\" ]; } && [ -f \"${COOKIE_FILE}\" ] && [ -s \"${COOKIE_FILE}\" ]; then\n LOGIN_OK=true\n break\n fi\n log \"Login attempt $i (HTTP ${HTTP_CODE}), retrying in 3s...\"\n sleep 3\n done\n\n if [ \"${LOGIN_OK}\" != \"true\" ]; then\n log \"ERROR: Could not login to Higress Console after retries\"\n exit 1\n fi\n log \"Higress Console login successful\"\n\n VERIFY_CODE=$(curl -s -o /dev/null -w '%{http_code}' \"${_HIGRESS_CONSOLE_URL}/v1/consumers\" -b \"${COOKIE_FILE}\" 2>/dev/null) || true\n if [ \"${VERIFY_CODE}\" = \"200\" ]; then\n log \"Console session verified (cookie valid)\"\n else\n log \"WARNING: Console session may be invalid (verify returned HTTP ${VERIFY_CODE})\"\n rm -f \"${COOKIE_FILE}\"\n for i in $(seq 1 5); do\n curl -s -o /dev/null -w '%{http_code}' -X POST \"${_HIGRESS_CONSOLE_URL}/session/login\" \\\n -H 'Content-Type: application/json' \\\n -c \"${COOKIE_FILE}\" \\\n -d '{\"username\":\"'\"${_HIGRESS_USER}\"'\",\"password\":\"'\"${_HIGRESS_PASS}\"'\"}' 2>/dev/null\n VERIFY2=$(curl -s -o /dev/null -w '%{http_code}' \"${_HIGRESS_CONSOLE_URL}/v1/consumers\" -b \"${COOKIE_FILE}\" 2>/dev/null) || true\n if [ \"${VERIFY2}\" = \"200\" ]; then\n log \"Re-login successful, session verified\"\n break\n fi\n sleep 2\n done\n fi\n\n export HIGRESS_COOKIE_FILE=\"${COOKIE_FILE}\"\n export HIGRESS_CONSOLE_URL=\"${_HIGRESS_CONSOLE_URL}\"\n\n if [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n # K8s mode: lightweight Higress config — only what's needed for LLM access\n source /opt/hiclaw/scripts/lib/base.sh\n _k8s_higress_api() {\n local method=\"$1\" path=\"$2\" desc=\"$3\"; shift 3; local body=\"$*\"\n local tmpfile; tmpfile=$(mktemp)\n local http_code\n http_code=$(curl -s -o \"${tmpfile}\" -w '%{http_code}' -X \"${method}\" \"${_HIGRESS_CONSOLE_URL}${path}\" \\\n -b \"${COOKIE_FILE}\" -H 'Content-Type: application/json' -d \"${body}\" 2>/dev/null) || true\n local response; response=$(cat \"${tmpfile}\" 2>/dev/null); rm -f \"${tmpfile}\"\n if echo \"${response}\" | grep -q '\"success\":true' 2>/dev/null; then\n log \"${desc} ... OK\"\n elif [ \"${http_code}\" = \"409\" ]; then\n log \"${desc} ... already exists, skipping\"\n elif [ \"${http_code}\" = \"200\" ] || [ \"${http_code}\" = \"201\" ] || [ \"${http_code}\" = \"204\" ]; then\n log \"${desc} ... OK (HTTP ${http_code})\"\n else\n log \"WARNING: ${desc} ... (HTTP ${http_code}): ${response}\"\n fi\n }\n\n # 1. Service Sources (DNS type → K8s Service FQDN)\n # Extract host:port from URLs for Higress service source registration\n _TUWUNEL_HOST=$(echo \"${HICLAW_MATRIX_URL}\" | sed 's|^http[s]*://||')\n _TUWUNEL_DOMAIN=$(echo \"${_TUWUNEL_HOST}\" | cut -d: -f1)\n _TUWUNEL_PORT=$(echo \"${_TUWUNEL_HOST}\" | cut -d: -f2)\n _k8s_higress_api POST /v1/service-sources \"Registering Tuwunel service source\" \\\n '{\"type\":\"dns\",\"name\":\"tuwunel\",\"domain\":\"'\"${_TUWUNEL_DOMAIN}\"'\",\"port\":'\"${_TUWUNEL_PORT}\"'}'\n\n if [ -n \"${HICLAW_ELEMENT_WEB_URL:-}\" ]; then\n _ELEMENT_HOST=$(echo \"${HICLAW_ELEMENT_WEB_URL}\" | sed 's|^http[s]*://||')\n _ELEMENT_DOMAIN=$(echo \"${_ELEMENT_HOST}\" | cut -d: -f1)\n _ELEMENT_PORT=$(echo \"${_ELEMENT_HOST}\" | cut -d: -f2)\n _k8s_higress_api POST /v1/service-sources \"Registering Element Web service source\" \\\n '{\"type\":\"dns\",\"name\":\"element-web\",\"domain\":\"'\"${_ELEMENT_DOMAIN}\"'\",\"port\":'\"${_ELEMENT_PORT}\"'}'\n fi\n\n # 2. Manager Consumer (key-auth)\n _k8s_higress_api POST /v1/consumers \"Creating Manager consumer\" \\\n '{\"name\":\"manager\",\"credentials\":[{\"type\":\"key-auth\",\"source\":\"BEARER\",\"values\":[\"'\"${HICLAW_MANAGER_GATEWAY_KEY}\"'\"]}]}'\n\n # 3. LLM Provider\n _LLM_PROVIDER=\"${HICLAW_LLM_PROVIDER:-qwen}\"\n _LLM_API_URL=\"${HICLAW_LLM_API_URL:-}\"\n if [ -z \"${_LLM_API_URL}\" ]; then\n case \"${_LLM_PROVIDER}\" in\n qwen) _LLM_API_URL=\"https://dashscope.aliyuncs.com/compatible-mode/v1\" ;;\n esac\n fi\n # 4. LLM Provider type-specific config\n case \"${_LLM_PROVIDER}\" in\n qwen)\n _k8s_higress_api POST /v1/ai/providers \"Creating LLM provider (qwen)\" \\\n '{\"type\":\"qwen\",\"name\":\"qwen\",\"tokens\":[\"'\"${HICLAW_LLM_API_KEY}\"'\"],\"protocol\":\"openai/v1\",\"tokenFailoverConfig\":{\"enabled\":false},\"rawConfigs\":{\"qwenEnableSearch\":false,\"qwenEnableCompatible\":true,\"qwenFileIds\":[],\"hiclawMode\":true}}'\n ;;\n *)\n _BODY='{\"name\":\"'\"${_LLM_PROVIDER}\"'\",\"type\":\"openai\",\"tokens\":[\"'\"${HICLAW_LLM_API_KEY}\"'\"],\"modelMapping\":{},\"protocol\":\"openai/v1\",\"rawConfigs\":{\"hiclawMode\":true}}'\n _k8s_higress_api POST /v1/ai/providers \"Creating LLM provider (${_LLM_PROVIDER})\" \"${_BODY}\"\n ;;\n esac\n\n # 5. AI Route (bind provider + consumer auth, /v1 prefix to avoid clash with Element Web catch-all)\n _k8s_higress_api POST /v1/ai/routes \"Creating AI Gateway route\" \\\n '{\"name\":\"default-ai-route\",\"domains\":[],\"pathPredicate\":{\"matchType\":\"PRE\",\"matchValue\":\"/v1\",\"caseSensitive\":false},\"upstreams\":[{\"provider\":\"'\"${_LLM_PROVIDER}\"'\",\"weight\":100,\"modelMapping\":{}}],\"authConfig\":{\"enabled\":true,\"allowedCredentialTypes\":[\"key-auth\"],\"allowedConsumers\":[\"manager\"]}}'\n\n # 6. Matrix Homeserver Route (/_matrix/* → Tuwunel, no auth)\n _k8s_higress_api POST /v1/routes \"Creating Matrix Homeserver route\" \\\n '{\"name\":\"matrix-homeserver\",\"domains\":[],\"path\":{\"matchType\":\"PRE\",\"matchValue\":\"/_matrix\"},\"services\":[{\"name\":\"tuwunel.dns\",\"port\":'\"${_TUWUNEL_PORT}\"',\"weight\":100}]}'\n\n # 7. Element Web Route (/ catch-all → Element Web, no auth)\n if [ -n \"${HICLAW_ELEMENT_WEB_URL:-}\" ]; then\n _k8s_higress_api POST /v1/routes \"Creating Element Web route\" \\\n '{\"name\":\"element-web\",\"domains\":[],\"path\":{\"matchType\":\"PRE\",\"matchValue\":\"/\"},\"services\":[{\"name\":\"element-web.dns\",\"port\":'\"${_ELEMENT_PORT}\"',\"weight\":100}]}'\n fi\n\n # 8. Remove Higress default landing page (Exact match on / takes precedence over Element Web catch-all)\n _k8s_higress_api DELETE /v1/routes/default \"Removing Higress default landing route\"\n\n log \"K8s Higress lightweight setup complete\"\n\n # Wait for AI plugin activation (~45s for first config)\n log \"Waiting for AI Gateway plugin activation (45s)...\"\n sleep 45\n else\n # Docker mode: full setup with all routes, domains, MCP servers\n /opt/hiclaw/scripts/init/setup-higress.sh\n fi\nfi\n\n# ============================================================\n# Create admin DM room, persist to state.json, send welcome message\n# K8s mode: skip — controller ProvisionManager creates the Admin DM\n# room (Step 4 in service/provisioner.go) AND reconcileManagerWelcome\n# delivers the first-boot onboarding prompt once OpenClaw inside this\n# container has joined the room. In k8s mode the manager intentionally\n# does NOT have the admin password (only HICLAW_ADMIN_USER), so it\n# could not log in as admin to send the welcome itself anyway. The\n# Manager Agent discovers its admin DM room on first heartbeat via\n# state.json / `manage-state.sh --action set-admin-dm` (see HEARTBEAT.md\n# Step 1) — it does not need it to be pre-injected by this script.\n# Runs in both local and cloud modes (idempotent)\n# ============================================================\nif [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n log \"K8s mode: skipping admin DM room creation and welcome message (both handled by hiclaw-controller)\"\nelse\nMANAGER_FULL_ID=\"@manager:${MATRIX_DOMAIN}\"\nADMIN_FULL_ID=\"@${HICLAW_ADMIN_USER}:${MATRIX_DOMAIN}\"\n\nlog \"Logging in as admin to create DM room...\"\n_ADMIN_LOGIN=$(curl -sf -X POST \"${HICLAW_MATRIX_URL}/_matrix/client/v3/login\" \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"type\": \"m.login.password\",\n \"identifier\": {\"type\": \"m.id.user\", \"user\": \"'\"${HICLAW_ADMIN_USER}\"'\"},\n \"password\": \"'\"${HICLAW_ADMIN_PASSWORD}\"'\"\n }' 2>&1) || true\n\nADMIN_MATRIX_TOKEN=$(echo \"${_ADMIN_LOGIN}\" | jq -r '.access_token // empty' 2>/dev/null)\nif [ -z \"${ADMIN_MATRIX_TOKEN}\" ]; then\n log \"WARNING: Failed to login as admin, skipping DM room creation\"\nelse\n # Search for existing DM room with Manager (idempotent)\n DM_ROOM_ID=\"\"\n _JOINED_ROOMS=$(curl -sf \"${HICLAW_MATRIX_URL}/_matrix/client/v3/joined_rooms\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" 2>/dev/null \\\n | jq -r '.joined_rooms[]' 2>/dev/null) || true\n for _rid in ${_JOINED_ROOMS}; do\n _members=$(curl -sf \"${HICLAW_MATRIX_URL}/_matrix/client/v3/rooms/${_rid}/members\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" 2>/dev/null \\\n | jq -r '.chunk[].state_key' 2>/dev/null) || continue\n _count=$(echo \"${_members}\" | wc -l | xargs)\n if [ \"${_count}\" = \"2\" ] && echo \"${_members}\" | grep -q \"@manager:\"; then\n DM_ROOM_ID=\"${_rid}\"\n break\n fi\n done\n\n if [ -n \"${DM_ROOM_ID}\" ]; then\n log \"Existing DM room found: ${DM_ROOM_ID}\"\n else\n log \"Creating DM room with Manager...\"\n _RAW=$(curl -s -w '\\nHTTP_CODE:%{http_code}' -X POST \"${HICLAW_MATRIX_URL}/_matrix/client/v3/createRoom\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d \"{\\\"is_direct\\\":true,\\\"invite\\\":[\\\"${MANAGER_FULL_ID}\\\"],\\\"preset\\\":\\\"trusted_private_chat\\\"}\" 2>&1) || true\n _HTTP_CODE=$(echo \"${_RAW}\" | tail -1 | sed 's/HTTP_CODE://')\n _CREATE_RESP=$(echo \"${_RAW}\" | sed '$d')\n DM_ROOM_ID=$(echo \"${_CREATE_RESP}\" | jq -r '.room_id // empty' 2>/dev/null)\n if [ -n \"${DM_ROOM_ID}\" ]; then\n log \"DM room created: ${DM_ROOM_ID}\"\n else\n log \"WARNING: Failed to create DM room (HTTP ${_HTTP_CODE}): ${_CREATE_RESP}\"\n fi\n fi\n\n # Persist admin DM room ID to state.json\n if [ -n \"${DM_ROOM_ID}\" ]; then\n STATE_SCRIPT=\"/opt/hiclaw/agent/skills/task-management/scripts/manage-state.sh\"\n if [ -f \"${STATE_SCRIPT}\" ]; then\n bash \"${STATE_SCRIPT}\" --action init 2>/dev/null || true\n bash \"${STATE_SCRIPT}\" --action set-admin-dm --room-id \"${DM_ROOM_ID}\" 2>/dev/null || true\n log \"Admin DM room persisted to state.json: ${DM_ROOM_ID}\"\n fi\n fi\n\n # Schedule welcome message in background (only on first boot)\n if [ -n \"${DM_ROOM_ID}\" ] && [ ! -f \"/root/manager-workspace/soul-configured\" ]; then\n log \"Scheduling welcome message (background, waiting for OpenClaw to start)...\"\n (\n _HICLAW_LANGUAGE=\"${HICLAW_LANGUAGE:-zh}\"\n _HICLAW_TIMEZONE=\"${TZ:-Asia/Shanghai}\"\n _wait=0\n _ready=false\n while [ \"${_wait}\" -lt 300 ]; do\n if curl -sf http://127.0.0.1:18799/ > /dev/null 2>&1; then\n _ready=true\n break\n fi\n sleep 3\n _wait=$((_wait + 3))\n done\n if [ \"${_ready}\" != \"true\" ]; then\n echo \"[manager] WARNING: OpenClaw gateway not ready within 300s, skipping welcome message\"\n exit 0\n fi\n # Ensure Manager has joined the DM room before sending the welcome\n # message. Without this, there is a race between OpenClaw's Matrix\n # auto-join and the message send — the message may land before Manager\n # joins, so OpenClaw's /sync never picks it up.\n _join_ok=false\n for _join_attempt in 1 2 3; do\n if curl -sf -X POST \"${HICLAW_MATRIX_URL}/_matrix/client/v3/rooms/${DM_ROOM_ID}/join\" \\\n -H \"Authorization: Bearer ${MANAGER_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d '{}' > /dev/null 2>&1; then\n echo \"[manager] Manager joined DM room before welcome message\"\n _join_ok=true\n break\n fi\n sleep 2\n done\n if [ \"${_join_ok}\" != \"true\" ]; then\n echo \"[manager] WARNING: Manager join request failed after 3 attempts (may already be joined)\"\n fi\n _welcome_msg=\"This is an automated message from the HiClaw setup. This is a fresh installation.\n\n--- Installation Context ---\nUser Language: ${_HICLAW_LANGUAGE} (zh = Chinese, en = English)\nUser Timezone: ${_HICLAW_TIMEZONE} (IANA timezone identifier)\n---\n\nYou are an AI agent that manages a team of worker agents. Your identity and personality have not been configured yet — the human admin is about to meet you for the first time.\n\nPlease begin the onboarding conversation:\n\n1. Greet the admin warmly and briefly describe what you can do (coordinate workers, manage tasks, run multi-agent projects)\n2. The user has selected \\\"${_HICLAW_LANGUAGE}\\\" as their preferred language during installation. Use this language for your greeting and all subsequent communication.\n3. The user's timezone is ${_HICLAW_TIMEZONE}. Based on this timezone, you may infer their likely region and suggest additional language options.\n4. Ask them: a) What would they like to call you? b) Communication style preference? c) Any behavior guidelines? d) Confirm default language\n5. After they reply, write their preferences to ~/SOUL.md\n6. Confirm what you wrote, and ask if they would like to adjust anything\n7. Once confirmed, run: touch ~/soul-configured\n\nThe human admin will start chatting shortly.\"\n _txn_id=\"welcome-$(date +%s)\"\n _payload=$(jq -nc --arg body \"${_welcome_msg}\" '{\"msgtype\":\"m.text\",\"body\":$body}')\n _raw=$(curl -s -w '\\nHTTP_CODE:%{http_code}' -X PUT \"${HICLAW_MATRIX_URL}/_matrix/client/v3/rooms/${DM_ROOM_ID}/send/m.room.message/${_txn_id}\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d \"${_payload}\" 2>&1) || true\n _http_code=$(echo \"${_raw}\" | tail -1 | sed 's/HTTP_CODE://')\n _send_resp=$(echo \"${_raw}\" | sed '$d')\n if echo \"${_send_resp}\" | jq -e '.event_id' > /dev/null 2>&1; then\n echo \"[manager] Welcome message sent to DM room\"\n else\n echo \"[manager] WARNING: Failed to send welcome message (HTTP ${_http_code}): ${_send_resp}\"\n fi\n ) &\n log \"Welcome message background process started (PID: $!)\"\n fi\nfi\nfi # end K8s mode skip for admin DM room\n\n# ============================================================\n# Generate Manager Agent openclaw.json from template\n# ============================================================\nlog \"Generating Manager openclaw.json...\"\nexport MANAGER_MATRIX_TOKEN=\"${MANAGER_TOKEN}\"\nexport MANAGER_GATEWAY_KEY=\"${HICLAW_MANAGER_GATEWAY_KEY}\"\n# Resolve model parameters based on model name\nMODEL_NAME=\"${HICLAW_DEFAULT_MODEL:-qwen3.5-plus}\"\ncase \"${MODEL_NAME}\" in\n gpt-5.3-codex|gpt-5-mini|gpt-5-nano)\n export MODEL_CONTEXT_WINDOW=400000 MODEL_MAX_TOKENS=128000 ;;\n claude-opus-4-6)\n export MODEL_CONTEXT_WINDOW=1000000 MODEL_MAX_TOKENS=128000 ;;\n claude-sonnet-4-6)\n export MODEL_CONTEXT_WINDOW=1000000 MODEL_MAX_TOKENS=64000 ;;\n claude-haiku-4-5)\n export MODEL_CONTEXT_WINDOW=200000 MODEL_MAX_TOKENS=64000 ;;\n qwen3.5-plus)\n export MODEL_CONTEXT_WINDOW=200000 MODEL_MAX_TOKENS=64000 ;;\n deepseek-chat|deepseek-reasoner|kimi-k2.5)\n export MODEL_CONTEXT_WINDOW=256000 MODEL_MAX_TOKENS=128000 ;;\n glm-5|MiniMax-M2.7|MiniMax-M2.7-highspeed|MiniMax-M2.5)\n export MODEL_CONTEXT_WINDOW=200000 MODEL_MAX_TOKENS=128000 ;;\n *)\n export MODEL_CONTEXT_WINDOW=150000 MODEL_MAX_TOKENS=128000 ;;\nesac\nexport MODEL_REASONING=true\n\n# Override with user-supplied custom model parameters from env (set during install)\n[ -n \"${HICLAW_MODEL_CONTEXT_WINDOW:-}\" ] && export MODEL_CONTEXT_WINDOW=\"${HICLAW_MODEL_CONTEXT_WINDOW}\"\n[ -n \"${HICLAW_MODEL_MAX_TOKENS:-}\" ] && export MODEL_MAX_TOKENS=\"${HICLAW_MODEL_MAX_TOKENS}\"\n[ -n \"${HICLAW_MODEL_REASONING:-}\" ] && export MODEL_REASONING=\"${HICLAW_MODEL_REASONING}\"\n\n# E2EE: convert HICLAW_MATRIX_E2EE to JSON boolean for template substitution\nif [ \"${HICLAW_MATRIX_E2EE:-0}\" = \"1\" ] || [ \"${HICLAW_MATRIX_E2EE:-}\" = \"true\" ]; then\n export MATRIX_E2EE_ENABLED=true\nelse\n export MATRIX_E2EE_ENABLED=false\nfi\nlog \"Matrix E2EE: ${MATRIX_E2EE_ENABLED}\"\n\n# Resolve input modalities: only vision-capable models get \"image\"\ncase \"${MODEL_NAME}\" in\n gpt-5.4|gpt-5.3-codex|gpt-5-mini|gpt-5-nano|claude-opus-4-6|claude-sonnet-4-6|claude-haiku-4-5|qwen3.5-plus|kimi-k2.5)\n export MODEL_INPUT='[\"text\", \"image\"]' ;;\n *)\n export MODEL_INPUT='[\"text\"]' ;;\nesac\n# Override with user-supplied vision setting from env\nif [ \"${HICLAW_MODEL_VISION:-}\" = \"true\" ]; then\n export MODEL_INPUT='[\"text\", \"image\"]'\nelif [ \"${HICLAW_MODEL_VISION:-}\" = \"false\" ]; then\n export MODEL_INPUT='[\"text\"]'\nfi\n\nlog \"Model: ${MODEL_NAME} (context=${MODEL_CONTEXT_WINDOW}, maxTokens=${MODEL_MAX_TOKENS}, reasoning=${MODEL_REASONING}, input=${MODEL_INPUT})\"\n\nif [ -f /root/manager-workspace/openclaw.json ]; then\n log \"Manager openclaw.json already exists, updating dynamic fields only (preserving user customizations)...\"\n # Merge known models into existing config (add missing, preserve user-added)\n # Use known-models.json (valid JSON) instead of template (contains ${VAR} placeholders)\n KNOWN_MODELS=$(cat /opt/hiclaw/configs/known-models.json 2>/dev/null || echo '[]')\n jq --arg token \"${MANAGER_TOKEN}\" \\\n --arg key \"${HICLAW_MANAGER_GATEWAY_KEY}\" \\\n --arg model \"${MODEL_NAME}\" \\\n --arg emb_model \"${HICLAW_EMBEDDING_MODEL}\" \\\n --arg aigw_domain \"${AI_GATEWAY_DOMAIN}\" \\\n --arg matrix_user_id \"@manager:${MATRIX_DOMAIN}\" \\\n --argjson e2ee \"${MATRIX_E2EE_ENABLED}\" \\\n --argjson known_models \"${KNOWN_MODELS}\" \\\n --argjson ctx \"${MODEL_CONTEXT_WINDOW}\" \\\n --argjson max \"${MODEL_MAX_TOKENS}\" \\\n --argjson reasoning \"${MODEL_REASONING}\" \\\n --argjson input \"${MODEL_INPUT}\" \\\n '\n # Merge known models: add any model id not already present\n .models.providers[\"hiclaw-gateway\"].models as $existing\n | ($existing | map(.id)) as $existing_ids\n | ($known_models | map(select(.id as $id | $existing_ids | index($id) | not))) as $new\n | .models.providers[\"hiclaw-gateway\"].models = ($existing + $new)\n # Ensure the user-chosen default model is in the list (custom model support)\n | if (.models.providers[\"hiclaw-gateway\"].models | map(.id) | index($model) | not) then\n .models.providers[\"hiclaw-gateway\"].models += [{\"id\": $model, \"name\": $model, \"reasoning\": $reasoning, \"contextWindow\": $ctx, \"maxTokens\": $max, \"input\": $input}]\n else . end\n # Rebuild model aliases from the full models list\n | (.models.providers[\"hiclaw-gateway\"].models | map({ (\"hiclaw-gateway/\" + .id): { \"alias\": .id } }) | add // {}) as $aliases\n | .agents.defaults.models = ((.agents.defaults.models // {}) + $aliases)\n | .channels.matrix.accessToken = $token | .channels.matrix.userId = $matrix_user_id | .models.providers[\"hiclaw-gateway\"].apiKey = $key\n | ((.hooks.token // \"\") as $ht | if $ht == $key or $ht == ($key + \"-hooks\" | @base64) then del(.hooks) else . end)\n | .agents.defaults.model.primary = (\"hiclaw-gateway/\" + $model)\n | .commands.restart = true\n | .gateway.port = 18799\n | .gateway.bind = \"lan\"\n | .gateway.controlUi = ((.gateway.controlUi // {}) + {\"dangerouslyDisableDeviceAuth\": true, \"allowInsecureAuth\": true, \"allowedOrigins\": [\"*\"]})\n | .channels.matrix.encryption = $e2ee\n | .channels.matrix.network = ((.channels.matrix.network // {}) + {\"dangerouslyAllowPrivateNetwork\": true})\n | .channels.matrix.autoJoin = \"always\"\n # Ensure memorySearch config exists (embedding model for memory) — skip if embedding model is empty\n | if $emb_model != \"\" then .agents.defaults.memorySearch //= {\"provider\":\"openai\",\"model\":$emb_model,\"remote\":{\"baseUrl\":(\"http://\" + $aigw_domain + \":8080/v1\"),\"apiKey\":$key}} else . end\n ' \\\n /root/manager-workspace/openclaw.json > /tmp/openclaw.json.tmp && \\\n mv /tmp/openclaw.json.tmp /root/manager-workspace/openclaw.json\n # Anti-tampering bypass: openclaw's config-observe-recovery clobbers the file\n # back to .bak whenever it sees \"missing-meta-vs-last-good\", which strips\n # external edits like channels.matrix.network.dangerouslyAllowPrivateNetwork.\n # Stamp meta and mirror to .bak so our edits survive.\n _oc_ver=$(openclaw --version 2>/dev/null | head -1 | awk '{print $NF}')\n [ -z \"${_oc_ver}\" ] && _oc_ver=\"hiclaw-bootstrap\"\n jq --arg ver \"${_oc_ver}\" --arg ts \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\" \\\n '.meta = ((.meta // {}) + {lastTouchedVersion: $ver, lastTouchedAt: $ts})' \\\n /root/manager-workspace/openclaw.json > /tmp/openclaw.meta.json && \\\n mv /tmp/openclaw.meta.json /root/manager-workspace/openclaw.json\n cp -f /root/manager-workspace/openclaw.json /root/manager-workspace/openclaw.json.bak\n # Verify the token was written correctly\n _written_token=$(jq -r '.channels.matrix.accessToken' /root/manager-workspace/openclaw.json 2>/dev/null)\n if [ -z \"${_written_token}\" ] || [ \"${_written_token}\" = \"null\" ]; then\n log \"ERROR: Matrix token was not written correctly to openclaw.json (got: ${_written_token})\"\n else\n log \"Matrix token written to openclaw.json (prefix: ${_written_token:0:10}...)\"\n fi\nelse\n log \"Manager openclaw.json not found, generating from template...\"\n envsubst < /opt/hiclaw/configs/manager-openclaw.json.tmpl > /root/manager-workspace/openclaw.json\n # Post-envsubst injection: memorySearch + custom model (single jq pass when possible)\n if ! jq -e --arg model \"${MODEL_NAME}\" '.models.providers[\"hiclaw-gateway\"].models | map(.id) | index($model)' /root/manager-workspace/openclaw.json > /dev/null 2>&1; then\n log \"Custom model '${MODEL_NAME}' not in built-in list, injecting into config...\"\n jq --arg emb_model \"${HICLAW_EMBEDDING_MODEL}\" \\\n --arg aigw_domain \"${AI_GATEWAY_DOMAIN}\" \\\n --arg key \"${HICLAW_MANAGER_GATEWAY_KEY}\" \\\n --arg model \"${MODEL_NAME}\" \\\n --argjson ctx \"${MODEL_CONTEXT_WINDOW}\" \\\n --argjson max \"${MODEL_MAX_TOKENS}\" \\\n --argjson reasoning \"${MODEL_REASONING}\" \\\n --argjson input \"${MODEL_INPUT}\" \\\n '\n (if $emb_model != \"\" then .agents.defaults.memorySearch = {\"provider\":\"openai\",\"model\":$emb_model,\"remote\":{\"baseUrl\":(\"http://\" + $aigw_domain + \":8080/v1\"),\"apiKey\":$key}} else . end)\n | .models.providers[\"hiclaw-gateway\"].models += [{\"id\": $model, \"name\": $model, \"reasoning\": $reasoning, \"contextWindow\": $ctx, \"maxTokens\": $max, \"input\": $input}]\n | .agents.defaults.models += {(\"hiclaw-gateway/\" + $model): {\"alias\": $model}}\n ' /root/manager-workspace/openclaw.json > /tmp/openclaw.json.tmp && \\\n mv /tmp/openclaw.json.tmp /root/manager-workspace/openclaw.json\n elif [ -n \"${HICLAW_EMBEDDING_MODEL}\" ]; then\n jq --arg emb_model \"${HICLAW_EMBEDDING_MODEL}\" \\\n --arg aigw_domain \"${AI_GATEWAY_DOMAIN}\" \\\n --arg key \"${HICLAW_MANAGER_GATEWAY_KEY}\" \\\n '.agents.defaults.memorySearch = {\"provider\":\"openai\",\"model\":$emb_model,\"remote\":{\"baseUrl\":(\"http://\" + $aigw_domain + \":8080/v1\"),\"apiKey\":$key}}' \\\n /root/manager-workspace/openclaw.json > /tmp/openclaw.json.tmp && \\\n mv /tmp/openclaw.json.tmp /root/manager-workspace/openclaw.json\n fi\n _written_token=$(jq -r '.channels.matrix.accessToken' /root/manager-workspace/openclaw.json 2>/dev/null)\n log \"Matrix token written from template (prefix: ${_written_token:0:10}...)\"\nfi\n\n# Cloud/K8s mode: overlay cloud-specific settings onto generated config\nif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ] || [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n log \"Applying cloud/k8s overlay to openclaw.json...\"\n jq --arg homeserver \"${HICLAW_MATRIX_URL}\" \\\n --arg gateway \"${HICLAW_AI_GATEWAY_URL}/v1\" \\\n --arg key \"${HICLAW_MANAGER_GATEWAY_KEY}\" \\\n '.channels.matrix.homeserver = $homeserver\n | .models.providers[\"hiclaw-gateway\"].baseUrl = $gateway\n | .models.providers[\"hiclaw-gateway\"].apiKey = $key\n | ((.hooks.token // \"\") as $ht | if $ht == $key or $ht == ($key + \"-hooks\" | @base64) then del(.hooks) else . end)\n | .commands.restart = false\n | if .agents.defaults.memorySearch then .agents.defaults.memorySearch.remote.baseUrl = $gateway | .agents.defaults.memorySearch.remote.apiKey = $key else . end' \\\n /root/manager-workspace/openclaw.json > /tmp/openclaw-cloud.json && \\\n mv /tmp/openclaw-cloud.json /root/manager-workspace/openclaw.json\n log \"Cloud/K8s overlay applied\"\nfi\n\n# ============================================================\n# Optional: enable openclaw-cms-plugin observability\n# Config is applied at runtime so secrets stay out of image layers.\n# ============================================================\nCMS_TRACES_ENABLED=\"$(echo \"${HICLAW_CMS_TRACES_ENABLED:-false}\" | tr '[:upper:]' '[:lower:]')\"\nif [ \"${CMS_TRACES_ENABLED}\" = \"true\" ]; then\n CMS_PLUGIN_NAME=\"openclaw-cms-plugin\"\n CMS_PLUGIN_DIR=\"${OPENCLAW_CMS_PLUGIN_DIR:-/opt/openclaw/extensions/openclaw-cms-plugin}\"\n CMS_PLUGIN_MANIFEST=\"${CMS_PLUGIN_DIR}/openclaw.plugin.json\"\n DIAG_PLUGIN_NAME=\"diagnostics-otel\"\n DIAG_PLUGIN_DIR=\"/opt/openclaw/extensions/diagnostics-otel\"\n CMS_LICENSE_KEY=\"${HICLAW_CMS_LICENSE_KEY:-}\"\n CMS_PROJECT=\"${HICLAW_CMS_PROJECT:-}\"\n CMS_METRICS_ENABLED=\"${HICLAW_CMS_METRICS_ENABLED:-false}\"\n\n if [ ! -f \"${CMS_PLUGIN_MANIFEST}\" ]; then\n log \"WARNING: ${CMS_PLUGIN_NAME} manifest not found at ${CMS_PLUGIN_MANIFEST}, skipping plugin config\"\n else\n _missing=0\n [ -z \"${HICLAW_CMS_ENDPOINT:-}\" ] && log \"WARNING: HICLAW_CMS_ENDPOINT is required when HICLAW_CMS_TRACES_ENABLED=true\" && _missing=1\n [ -z \"${CMS_LICENSE_KEY:-}\" ] && log \"WARNING: HICLAW_CMS_LICENSE_KEY is required when HICLAW_CMS_TRACES_ENABLED=true\" && _missing=1\n [ -z \"${HICLAW_CMS_WORKSPACE:-}\" ] && log \"WARNING: HICLAW_CMS_WORKSPACE is required when HICLAW_CMS_TRACES_ENABLED=true\" && _missing=1\n\n if [ \"${_missing}\" = \"0\" ]; then\n CMS_SERVICE_NAME=\"${HICLAW_CMS_SERVICE_NAME:-hiclaw-manager}\"\n CMS_ENABLE_METRICS=\"${CMS_METRICS_ENABLED}\"\n DIAG_AVAILABLE=\"0\"\n _metrics_lc=\"$(echo \"${CMS_ENABLE_METRICS}\" | tr '[:upper:]' '[:lower:]')\"\n if [ \"${_metrics_lc}\" = \"true\" ]; then\n if [ -f \"${DIAG_PLUGIN_DIR}/package.json\" ]; then\n DIAG_AVAILABLE=\"1\"\n if [ ! -d \"${DIAG_PLUGIN_DIR}/node_modules\" ]; then\n log \"diagnostics-otel dependencies missing, installing...\"\n if (cd \"${DIAG_PLUGIN_DIR}\" && npm install --omit=dev --ignore-scripts >/tmp/hiclaw-diag-install.log 2>&1); then\n log \"diagnostics-otel dependencies installed\"\n else\n log \"WARNING: diagnostics-otel npm install failed, metrics plugin may not load\"\n fi\n else\n log \"diagnostics-otel dependencies already present\"\n fi\n else\n log \"WARNING: diagnostics-otel package.json not found at ${DIAG_PLUGIN_DIR}, metrics plugin may not load\"\n fi\n fi\n\n log \"Applying ${CMS_PLUGIN_NAME} config to openclaw.json...\"\n jq --arg pluginName \"${CMS_PLUGIN_NAME}\" \\\n --arg pluginDir \"${CMS_PLUGIN_DIR}\" \\\n --arg endpoint \"${HICLAW_CMS_ENDPOINT}\" \\\n --arg licenseKey \"${CMS_LICENSE_KEY}\" \\\n --arg armsProject \"${CMS_PROJECT}\" \\\n --arg cmsWorkspace \"${HICLAW_CMS_WORKSPACE}\" \\\n --arg serviceName \"${CMS_SERVICE_NAME}\" \\\n --arg diagPluginName \"${DIAG_PLUGIN_NAME}\" \\\n --arg diagPluginDir \"${DIAG_PLUGIN_DIR}\" \\\n --arg metricsRaw \"${CMS_ENABLE_METRICS}\" \\\n --arg diagAvailableRaw \"${DIAG_AVAILABLE}\" \\\n '\n .plugins = (.plugins // {})\n | .plugins.load = (.plugins.load // {})\n | .plugins.entries = (.plugins.entries // {})\n | if (.plugins.allow | type) != \"array\" then .plugins.allow = [] else . end\n | if (.plugins.allow | index($pluginName)) == null then .plugins.allow += [$pluginName] else . end\n | if (.plugins.load.paths | type) != \"array\" then .plugins.load.paths = [] else . end\n | if (.plugins.load.paths | index($pluginDir)) == null then .plugins.load.paths += [$pluginDir] else . end\n | .plugins.entries[$pluginName] = {\n \"enabled\": true,\n \"config\": {\n \"endpoint\": $endpoint,\n \"headers\": {\n \"x-arms-license-key\": $licenseKey,\n \"x-arms-project\": $armsProject,\n \"x-cms-workspace\": $cmsWorkspace\n },\n \"serviceName\": $serviceName\n }\n }\n\n # diagnostics-otel metrics (optional)\n | ($metricsRaw | ascii_downcase) as $m\n | ($diagAvailableRaw == \"1\") as $diagAvailable\n | (($m == \"true\") and $diagAvailable) as $metricsEnabled\n | if $metricsEnabled then\n (if (.plugins.allow | index($diagPluginName)) == null then .plugins.allow += [$diagPluginName] else . end)\n | (if (.plugins.load.paths | index($diagPluginDir)) == null then .plugins.load.paths += [$diagPluginDir] else . end)\n | .plugins.entries[$diagPluginName].enabled = true\n | .diagnostics = (.diagnostics // {})\n | .diagnostics.otel = (.diagnostics.otel // {})\n | .diagnostics.enabled = true\n | .diagnostics.otel.enabled = true\n | .diagnostics.otel.endpoint = $endpoint\n | .diagnostics.otel.protocol = (.diagnostics.otel.protocol // \"http/protobuf\")\n | .diagnostics.otel.headers = {\n \"x-arms-license-key\": $licenseKey,\n \"x-arms-project\": $armsProject,\n \"x-cms-workspace\": $cmsWorkspace\n }\n | .diagnostics.otel.serviceName = $serviceName\n | .diagnostics.otel.metrics = true\n | .diagnostics.otel.traces = (.diagnostics.otel.traces // false)\n | .diagnostics.otel.logs = (.diagnostics.otel.logs // false)\n else\n .\n end\n ' /root/manager-workspace/openclaw.json > /tmp/openclaw-cms.json && \\\n mv /tmp/openclaw-cms.json /root/manager-workspace/openclaw.json\n log \"${CMS_PLUGIN_NAME} config applied (metrics=${CMS_ENABLE_METRICS}, service=${CMS_SERVICE_NAME})\"\n else\n log \"Skipping ${CMS_PLUGIN_NAME} config due to missing required env vars\"\n fi\n fi\nfi\n\n# ============================================================\n# Detect container runtime (for Worker creation)\n# ============================================================\nsource /opt/hiclaw/scripts/lib/container-api.sh\nif container_api_available; then\n log \"Container runtime socket detected at ${CONTAINER_SOCKET} — direct Worker creation enabled\"\n export HICLAW_CONTAINER_RUNTIME=\"socket\"\nelif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ] || [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n log \"Cloud/K8s mode — Workers created via controller API\"\n export HICLAW_CONTAINER_RUNTIME=\"cloud\"\nelse\n log \"No container runtime found — Worker creation will output install commands\"\n export HICLAW_CONTAINER_RUNTIME=\"none\"\nfi\n\n# ============================================================\n# Upgrade Worker openclaw.json: merge known models + E2EE flag into existing configs\n# Existing workers in MinIO may have old single-model configs or missing encryption field.\n# Merge template models so they can hot-switch without restart.\n# ============================================================\nREGISTRY_FILE=\"/root/manager-workspace/workers-registry.json\"\nif [ -f \"${REGISTRY_FILE}\" ]; then\n # Use known-models.json (valid JSON) instead of template (contains ${VAR} placeholders)\n KNOWN_MODELS_FILE=\"/opt/hiclaw/configs/known-models.json\"\n if [ -f \"${KNOWN_MODELS_FILE}\" ]; then\n _KNOWN_MODELS=$(cat \"${KNOWN_MODELS_FILE}\")\n for _wname in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_wname}\" ] && continue\n _minio_path=\"${HICLAW_STORAGE_PREFIX}/agents/${_wname}/openclaw.json\"\n _tmp_in=\"/tmp/openclaw-${_wname}-models-upgrade-in.json\"\n if mc cp \"${_minio_path}\" \"${_tmp_in}\" 2>/dev/null; then\n _tmp_out=\"/tmp/openclaw-${_wname}-models-upgrade-out.json\"\n # Idempotent merge: add missing known models, rebuild aliases, set e2ee.\n # Always runs — jq deduplicates by model id, so re-runs are safe.\n jq --argjson known_models \"${_KNOWN_MODELS}\" \\\n --argjson e2ee \"${MATRIX_E2EE_ENABLED}\" '\n .models.providers[\"hiclaw-gateway\"].models as $existing\n | ($existing | map(.id)) as $existing_ids\n | ($known_models | map(select(.id as $id | $existing_ids | index($id) | not))) as $new\n | .models.providers[\"hiclaw-gateway\"].models = ($existing + $new)\n | (.models.providers[\"hiclaw-gateway\"].models | map({ (\"hiclaw-gateway/\" + .id): { \"alias\": .id } }) | add // {}) as $aliases\n | .agents.defaults.models = ((.agents.defaults.models // {}) + $aliases)\n | .channels.matrix.encryption = $e2ee\n | .channels.matrix.autoJoin = \"always\"\n ' \"${_tmp_in}\" > \"${_tmp_out}\" 2>/dev/null\n if ! diff -q \"${_tmp_in}\" \"${_tmp_out}\" > /dev/null 2>&1; then\n if mc cp \"${_tmp_out}\" \"${_minio_path}\" 2>/dev/null; then\n _new_count=$(jq '.models.providers[\"hiclaw-gateway\"].models | length' \"${_tmp_out}\" 2>/dev/null)\n log \"Worker ${_wname}: upgraded openclaw.json (models: ${_new_count}, e2ee: ${MATRIX_E2EE_ENABLED})\"\n fi\n fi\n rm -f \"${_tmp_in}\" \"${_tmp_out}\"\n fi\n done\n fi\nfi\n\n# ============================================================\n# Ensure Worker Matrix password files exist in MinIO (E2EE fix)\n# Workers need to re-login on restart to get a fresh device_id.\n# Older workers created before this fix won't have the password file.\n# ============================================================\nif [ -f \"${REGISTRY_FILE}\" ]; then\n for _wname in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_wname}\" ] && continue\n _creds_file=\"/data/worker-creds/${_wname}.env\"\n if [ -f \"${_creds_file}\" ]; then\n # Check if password file already exists in MinIO\n if ! mc stat \"${HICLAW_STORAGE_PREFIX}/agents/${_wname}/credentials/matrix/password\" > /dev/null 2>&1; then\n source \"${_creds_file}\"\n if [ -n \"${WORKER_PASSWORD}\" ]; then\n _tmp_pw=\"/tmp/matrix-pw-${_wname}\"\n echo -n \"${WORKER_PASSWORD}\" > \"${_tmp_pw}\"\n mc cp \"${_tmp_pw}\" \"${HICLAW_STORAGE_PREFIX}/agents/${_wname}/credentials/matrix/password\" 2>/dev/null \\\n && log \"Worker ${_wname}: wrote Matrix password to MinIO (E2EE re-login fix)\" \\\n || log \"Worker ${_wname}: WARNING: failed to write Matrix password to MinIO\"\n rm -f \"${_tmp_pw}\"\n fi\n fi\n fi\n done\nfi\n\n# ============================================================\n# Recreate Worker containers as needed after Manager restart.\n# Workers are on hiclaw-net; Docker DNS resolves *-local.hiclaw.io via\n# the Manager's network aliases, so IP changes don't require worker recreation.\n# Only recreate stopped/missing workers.\n# ============================================================\nif container_api_available; then\n REGISTRY_FILE=\"/root/manager-workspace/workers-registry.json\"\n if [ -f \"${REGISTRY_FILE}\" ]; then\n for _worker_name in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_worker_name}\" ] && continue\n\n # Skip remote workers — they are not Manager-managed containers.\n _deployment=$(jq -r --arg w \"${_worker_name}\" '.workers[$w].deployment // \"local\"' \"${REGISTRY_FILE}\" 2>/dev/null)\n if [ \"${_deployment}\" = \"remote\" ]; then\n log \"Worker ${_worker_name} is remote, skipping container recreate\"\n continue\n fi\n\n _status=$(container_status_worker \"${_worker_name}\")\n if [ \"${_status}\" = \"running\" ]; then\n log \"Worker running: ${_worker_name}, skipping\"\n continue\n fi\n # Container missing or stopped — recreate.\n log \"Worker container ${_status}: ${_worker_name}, recreating...\"\n _creds_file=\"/data/worker-creds/${_worker_name}.env\"\n if [ -f \"${_creds_file}\" ]; then\n source \"${_creds_file}\"\n _runtime=$(jq -r --arg w \"${_worker_name}\" '.workers[$w].runtime // \"openclaw\"' \"${REGISTRY_FILE}\" 2>/dev/null)\n _recreated=false\n for _attempt in 1 2 3; do\n local _env_map _create_body\n _env_map=$(jq -cn \\\n --arg name \"${_worker_name}\" \\\n --arg fak \"${_worker_name}\" \\\n --arg fsk \"${WORKER_MINIO_PASSWORD:-}\" \\\n --arg fs_domain \"${HICLAW_FS_DOMAIN:-fs-local.hiclaw.io}\" \\\n --arg controller_url \"${HICLAW_CONTROLLER_URL:-}\" \\\n '{\n \"HICLAW_WORKER_NAME\": $name,\n \"HICLAW_FS_ENDPOINT\": (\"http://\" + ($fs_domain | split(\":\")[0]) + \":8080\"),\n \"HICLAW_FS_ACCESS_KEY\": $fak,\n \"HICLAW_FS_SECRET_KEY\": $fsk\n }\n | if $controller_url != \"\" then . + {\"HICLAW_CONTROLLER_URL\": $controller_url} else . end')\n _create_body=$(jq -cn --arg name \"${_worker_name}\" --arg runtime \"${_runtime}\" --argjson env \"${_env_map}\" '{name: $name, runtime: $runtime, env: $env}')\n worker_backend_create \"${_create_body}\" > /dev/null 2>&1 && _recreated=true && break\n log \" Attempt ${_attempt}/3 failed for ${_worker_name}, retrying in $((5 * _attempt))s...\"\n sleep $((5 * _attempt))\n done\n if [ \"${_recreated}\" = true ]; then\n log \" Recreated ${_runtime} worker: ${_worker_name}\"\n else\n log \" ERROR: Failed to recreate ${_worker_name} after 3 attempts\"\n fi\n else\n log \" WARNING: No credentials found for ${_worker_name} (${_creds_file} missing), skipping\"\n fi\n done\n fi\nfi\n\n# ============================================================\n# Notify workers of builtin updates if upgrade happened\n# Builtin files (AGENTS.md, skills) are already synced by upgrade-builtins.sh\n#\n# Cooldown: skip notification if the last successful notify was within\n# NOTIFY_COOLDOWN_SECS (default 3600s / 1 hour). This prevents repeated\n# notifications when the Manager crash-loops and re-runs upgrade-builtins\n# on every restart (e.g. IMAGE_VERSION=latest always triggers upgrade).\n# ============================================================\nNOTIFY_COOLDOWN_SECS=\"${HICLAW_NOTIFY_COOLDOWN_SECS:-3600}\"\nNOTIFY_TS_FILE=\"/root/manager-workspace/.last-worker-notify-ts\"\n\nif [ -f /root/manager-workspace/.upgrade-pending-worker-notify ]; then\n _now=$(date +%s)\n _last_notify=$(cat \"${NOTIFY_TS_FILE}\" 2>/dev/null || echo \"0\")\n _elapsed=$(( _now - _last_notify ))\n\n if [ \"${_elapsed}\" -lt \"${NOTIFY_COOLDOWN_SECS}\" ]; then\n log \"Skipping worker builtin notification (last notify ${_elapsed}s ago, cooldown ${NOTIFY_COOLDOWN_SECS}s)\"\n rm -f /root/manager-workspace/.upgrade-pending-worker-notify\n else\n log \"Notifying workers about builtin updates...\"\n REGISTRY_FILE=\"/root/manager-workspace/workers-registry.json\"\n _notify_ok=false\n if [ -f \"${REGISTRY_FILE}\" ]; then\n for _worker_name in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_worker_name}\" ] && continue\n _room_id=$(jq -r --arg w \"${_worker_name}\" '.workers[$w].room_id // empty' \"${REGISTRY_FILE}\" 2>/dev/null)\n if [ -n \"${_room_id}\" ]; then\n _worker_id=\"@${_worker_name}:${MATRIX_DOMAIN}\"\n _txn_id=\"upgrade-$(date +%s%N)\"\n _msg=\"@${_worker_name}:${MATRIX_DOMAIN} Manager upgraded builtin files (AGENTS.md, skills). Please use your file-sync skill to sync the latest config.\"\n _raw=$(curl -s -w '\\nHTTP_CODE:%{http_code}' -X PUT \\\n \"${HICLAW_MATRIX_URL}/_matrix/client/v3/rooms/${_room_id}/send/m.room.message/${_txn_id}\" \\\n -H \"Authorization: Bearer ${MANAGER_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d \"{\\\"msgtype\\\":\\\"m.text\\\",\\\"body\\\":\\\"${_msg}\\\",\\\"m.mentions\\\":{\\\"user_ids\\\":[\\\"${_worker_id}\\\"]}}\" \\\n 2>&1) || true\n _http_code=$(echo \"${_raw}\" | tail -1 | sed 's/HTTP_CODE://')\n _notify_resp=$(echo \"${_raw}\" | sed '$d')\n if echo \"${_notify_resp}\" | jq -e '.event_id' > /dev/null 2>&1; then\n log \" Notified ${_worker_name}\"; _notify_ok=true\n else\n log \" WARNING: Failed to notify ${_worker_name} (HTTP ${_http_code}): ${_notify_resp}\"\n fi\n fi\n done\n fi\n # Record timestamp only if at least one notification succeeded\n if [ \"${_notify_ok}\" = true ]; then\n echo \"${_now}\" > \"${NOTIFY_TS_FILE}\"\n fi\n rm -f /root/manager-workspace/.upgrade-pending-worker-notify\n fi\nfi\n\n# ============================================================\n# Start Manager Agent\n# ============================================================\nlog \"Starting Manager Agent (${MANAGER_RUNTIME})...\"\n\n# HOME is already set to /root/manager-workspace via docker run -e HOME=...\ncd \"${HOME}\"\n\n# Ensure host credential symlinks exist under HOME\nif [ -d \"/host-share\" ]; then\n [ -f \"/host-share/.gitconfig\" ] && ln -sf \"/host-share/.gitconfig\" \"${HOME}/.gitconfig\"\nfi\n\nlog \"HOME=${HOME} (manager-workspace, host-mounted)\"\n\n# ── Render agent doc templates ────────────────────────────────────────────\n# Replace ${VAR} placeholders with actual values so the AI agent reads\n# plain text and never needs to resolve environment variables.\nexport MANAGER_MATRIX_TOKEN MANAGER_TOKEN HIGRESS_COOKIE_FILE\nRENDER=/opt/hiclaw/scripts/lib/render-skills.sh\nlog \"Rendering agent doc templates...\"\n# Manager-owned docs (workspace)\nbash \"$RENDER\" /root/manager-workspace/skills\nbash \"$RENDER\" /root/manager-workspace/skills-alpha\nbash \"$RENDER\" /root/manager-workspace AGENTS.md TOOLS.md HEARTBEAT.md SOUL.md\n# Worker templates (workspace + image) — rendered before push to MinIO\n# so Workers (including remote pip-install) receive plain text\nbash \"$RENDER\" /root/manager-workspace/worker-skills\nbash \"$RENDER\" /root/manager-workspace/worker-agent\nbash \"$RENDER\" /root/manager-workspace/copaw-worker-agent\nbash \"$RENDER\" /root/manager-workspace/hermes-worker-agent\nbash \"$RENDER\" /opt/hiclaw/agent/worker-skills\nbash \"$RENDER\" /opt/hiclaw/agent/worker-agent\nbash \"$RENDER\" /opt/hiclaw/agent/copaw-worker-agent\nbash \"$RENDER\" /opt/hiclaw/agent/hermes-worker-agent\nlog \"Agent doc templates rendered\"\n\n# Cloud mode: start background file sync (workspace ↔ OSS) and initial push\nif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n log \"Syncing initial workspace to OSS...\"\n ensure_mc_credentials\n mc mirror /root/manager-workspace/ \"${HICLAW_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" 2>/dev/null || true\n\n # Local → OSS: change-triggered sync\n (\n while true; do\n CHANGED=$(find /root/manager-workspace/ -type f -newermt \"15 seconds ago\" 2>/dev/null | head -1)\n if [ -n \"${CHANGED}\" ]; then\n ensure_mc_credentials 2>/dev/null || true\n mc mirror /root/manager-workspace/ \"${HICLAW_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" --exclude \".npm/**\" \\\n --exclude \".local/**\" --exclude \".mc/**\" 2>/dev/null || true\n fi\n sleep 10\n done\n ) &\n log \"Local→OSS sync started (PID: $!)\"\n\n # OSS → Local: periodic pull (shared data, agent configs)\n (\n while true; do\n sleep 300\n ensure_mc_credentials 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/shared/\" /root/hiclaw-fs/shared/ --overwrite --newer-than \"5m\" 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/agents/\" /root/hiclaw-fs/agents/ --overwrite --newer-than \"5m\" 2>/dev/null || true\n done\n ) &\n log \"OSS→Local sync started (every 5m, PID: $!)\"\nfi\n\n# K8s mode: start background file sync (workspace ↔ MinIO)\nif [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n log \"Syncing initial workspace to MinIO...\"\n mc mirror /root/manager-workspace/ \"${HICLAW_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" 2>/dev/null || true\n\n # Local → MinIO: change-triggered sync\n (\n while true; do\n CHANGED=$(find /root/manager-workspace/ -type f -newermt \"15 seconds ago\" 2>/dev/null | head -1)\n if [ -n \"${CHANGED}\" ]; then\n mc mirror /root/manager-workspace/ \"${HICLAW_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" --exclude \".npm/**\" \\\n --exclude \".local/**\" --exclude \".mc/**\" 2>/dev/null || true\n fi\n sleep 10\n done\n ) &\n log \"Local→MinIO sync started (PID: $!)\"\n\n # MinIO → Local: periodic pull (shared data, agent configs)\n (\n while true; do\n sleep 300\n mc mirror \"${HICLAW_STORAGE_PREFIX}/shared/\" /root/hiclaw-fs/shared/ --overwrite --newer-than \"5m\" 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/agents/\" /root/hiclaw-fs/agents/ --overwrite --newer-than \"5m\" 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/hiclaw-config/\" /root/hiclaw-fs/hiclaw-config/ --overwrite --newer-than \"15s\" 2>/dev/null || true\n done\n ) &\n log \"MinIO→Local sync started (every 5m, PID: $!)\"\nfi\n\n# ============================================================\n# Auto-generate Manager mcporter config for pre-configured MCP servers\n# If HICLAW_GITHUB_TOKEN was set at install time, setup-higress.sh already\n# configured GitHub MCP on Higress. Run setup-mcp-server.sh now so that\n# config/mcporter.json exists when the Agent starts — no need to ask user for PAT.\n# ============================================================\nif [ -n \"${HICLAW_GITHUB_TOKEN}\" ] && [ \"${HICLAW_RUNTIME}\" != \"aliyun\" ] && [ \"${HICLAW_RUNTIME}\" != \"k8s\" ]; then\n if [ ! -f \"${HOME}/config/mcporter.json\" ]; then\n log \"Auto-generating Manager mcporter config for GitHub MCP (HICLAW_GITHUB_TOKEN set)...\"\n bash /opt/hiclaw/agent/skills/mcp-server-management/scripts/setup-mcp-server.sh \\\n github \"${HICLAW_GITHUB_TOKEN}\" 2>&1 | while IFS= read -r line; do log \" [setup-mcp] ${line}\"; done || \\\n log \"WARNING: setup-mcp-server.sh failed — Agent may need to configure GitHub MCP manually\"\n else\n log \"Manager mcporter config already exists, skipping auto-generate\"\n fi\nfi\n\n# ============================================================\n# Runtime-specific startup\n# ============================================================\nif [ \"${MANAGER_RUNTIME}\" = \"copaw\" ]; then\n # Delegate to CoPaw startup script\n exec /opt/hiclaw/scripts/init/start-copaw-manager.sh\nelse\n # ── OpenClaw Runtime ─────────────────────────────────────────────────────\n log \"Starting OpenClaw Manager...\"\n\n export OPENCLAW_CONFIG_PATH=\"/root/manager-workspace/openclaw.json\"\n\n # Symlink to default OpenClaw config path so CLI commands find the config\n mkdir -p \"${HOME}/.openclaw\"\n ln -sf \"/root/manager-workspace/openclaw.json\" \"${HOME}/.openclaw/openclaw.json\"\n\n # Clean orphaned session write locks (e.g. from SIGKILL or crash before exit handlers)\n # Prevents \"session file locked (timeout 10000ms)\" when PID was reused\n find \"${HOME}/.openclaw/agents\" -name \"*.jsonl.lock\" -delete 2>/dev/null || true\n log \"Cleaned up any orphaned session write locks\"\n\n # Clean Matrix crypto storage (SQLite WAL may be corrupted after unclean shutdown)\n # Crypto state is re-negotiated on startup; losing it only means re-establishing E2EE sessions\n rm -rf \"${HOME}/.openclaw/matrix\" 2>/dev/null || true\n log \"Cleaned Matrix crypto storage (will re-establish E2EE sessions)\"\n\n # Launch OpenClaw\n # Disable full-process respawn so the CLI uses its internal restart loop.\n # Without this, config reload spawns a detached child and exits, then\n # supervisord restarts the CLI — resulting in two gateway processes.\n export OPENCLAW_NO_RESPAWN=1\n\n # Optional matrix-plugin trace logging — when HICLAW_MATRIX_DEBUG=1 is set\n # in the manager environment (propagated by install / supervisord), turn on\n # OPENCLAW_MATRIX_DEBUG so the matrix plugin emits structured INFO-level\n # lifecycle traces (sync.state transitions, room.invite/join, message\n # handler arrival + filter outcomes). Useful for diagnosing \"worker never\n # joined the room\" / \"manager never replied\" hangs without rebuilding the\n # image.\n if [ \"${HICLAW_MATRIX_DEBUG:-}\" = \"1\" ] && [ -z \"${OPENCLAW_MATRIX_DEBUG:-}\" ]; then\n export OPENCLAW_MATRIX_DEBUG=1\n log \"HICLAW_MATRIX_DEBUG=1 detected; OPENCLAW_MATRIX_DEBUG=1 exported for matrix plugin tracing\"\n fi\n\n exec openclaw gateway run --verbose --force\nfi\n"}, "e8573c5a5f6d56e168cfb297c7ccf6b0b6666d36": {"t": 1785764751.135123, "n": 66732, "h": "3e52772b28aea144", "c": "#!/bin/bash\n# start-manager-agent.sh - Initialize and start the Manager Agent\n# Supports local (supervisord), cloud (SAE), and K8s (Helm) deployments.\n# In local mode this is the last supervisord component to start (priority 800).\n# In cloud/k8s mode (HICLAW_RUNTIME=aliyun|k8s) this is the container entrypoint.\n#\n# Runtime selection:\n# HICLAW_MANAGER_RUNTIME=openclaw (default) - OpenClaw gateway mode\n# HICLAW_MANAGER_RUNTIME=copaw - CoPaw workspace mode\n# (hermes runtime is supported for Workers only; Managers run openclaw or copaw.)\n\nsource /opt/hiclaw/scripts/lib/hiclaw-env.sh\n\n# ============================================================\n# Runtime selection\n# ============================================================\nMANAGER_RUNTIME=\"${HICLAW_MANAGER_RUNTIME:-openclaw}\"\ncase \"${MANAGER_RUNTIME}\" in\n copaw)\n log \"Manager runtime: CoPaw (Python workspace)\"\n ;;\n *)\n log \"Manager runtime: OpenClaw (Node.js gateway)\"\n MANAGER_RUNTIME=\"openclaw\"\n ;;\nesac\n\n# ============================================================\n# Set timezone from TZ env var\n# ============================================================\nif [ -n \"${TZ}\" ] && [ -f \"/usr/share/zoneinfo/${TZ}\" ]; then\n ln -sf \"/usr/share/zoneinfo/${TZ}\" /etc/localtime\n echo \"${TZ}\" > /etc/timezone\n log \"Timezone set to ${TZ}\"\nfi\n\nexport MATRIX_DOMAIN=\"${HICLAW_MATRIX_DOMAIN:-matrix-local.hiclaw.io:8080}\"\nAI_GATEWAY_DOMAIN=\"${HICLAW_AI_GATEWAY_DOMAIN:-aigw-local.hiclaw.io}\"\n\n# ============================================================\n# YOLO mode promotion\n# ============================================================\n# In embedded mode the controller does not propagate HICLAW_YOLO to the\n# manager container, but installer / test scripts touch a marker file at\n# `${WORKSPACE}/yolo-mode` instead. Promote that marker to the env var so the\n# agent's documented YOLO check (`HICLAW_YOLO=1`) reliably detects it without\n# depending on filesystem lookups during a turn.\nif [ -z \"${HICLAW_YOLO:-}\" ] && [ -f /root/manager-workspace/yolo-mode ]; then\n export HICLAW_YOLO=1\n log \"YOLO mode marker detected at /root/manager-workspace/yolo-mode; HICLAW_YOLO=1 exported\"\nfi\n\n# ============================================================\n# Cloud/K8s mode: validate required environment variables + initial credentials\n# ============================================================\nif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ] || [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n : \"${HICLAW_MATRIX_URL:?HICLAW_MATRIX_URL is required}\"\n : \"${HICLAW_MATRIX_DOMAIN:?HICLAW_MATRIX_DOMAIN is required}\"\n : \"${HICLAW_AI_GATEWAY_URL:?HICLAW_AI_GATEWAY_URL is required}\"\n if [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n : \"${HICLAW_MANAGER_GATEWAY_KEY:?HICLAW_MANAGER_GATEWAY_KEY is required}\"\n : \"${HICLAW_MANAGER_PASSWORD:?HICLAW_MANAGER_PASSWORD is required (cloud containers are stateless, password must be injected)}\"\n fi\n if [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n # K8s mode: controller handles initialization (admin registration, Higress setup).\n # Manager only needs credentials injected by the ManagerReconciler.\n : \"${HICLAW_MANAGER_GATEWAY_KEY:?HICLAW_MANAGER_GATEWAY_KEY is required (injected by controller)}\"\n : \"${HICLAW_MANAGER_PASSWORD:?HICLAW_MANAGER_PASSWORD is required (injected by controller)}\"\n else\n # Cloud (aliyun) mode: Manager still does its own initialization\n : \"${HICLAW_REGISTRATION_TOKEN:?HICLAW_REGISTRATION_TOKEN is required}\"\n : \"${HICLAW_ADMIN_USER:?HICLAW_ADMIN_USER is required}\"\n : \"${HICLAW_ADMIN_PASSWORD:?HICLAW_ADMIN_PASSWORD is required}\"\n fi\n log \"${HICLAW_RUNTIME} mode: validating environment... OK\"\n log \" Matrix: ${HICLAW_MATRIX_URL}, AI Gateway: ${HICLAW_AI_GATEWAY_URL}, Storage: ${HICLAW_FS_BUCKET}\"\n if [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n ensure_mc_credentials || { log \"FATAL: Initial STS credential fetch failed\"; exit 1; }\n fi\nfi\n\n# ============================================================\n# Local mode: host symlinks, /etc/hosts, wait for local services\n# ============================================================\nif [ \"${HICLAW_RUNTIME}\" != \"aliyun\" ] && [ \"${HICLAW_RUNTIME}\" != \"k8s\" ]; then\n # Create symlink for host directory access\n if [ -d \"/host-share\" ]; then\n ORIGINAL_HOST_HOME=\"${HOST_ORIGINAL_HOME:-$HOME}\"\n if [ ! -e \"${ORIGINAL_HOST_HOME}\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/root\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/data\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/host-share\" ]; then\n mkdir -p \"$(dirname \"${ORIGINAL_HOST_HOME}\")\"\n ln -sfn /host-share \"${ORIGINAL_HOST_HOME}\"\n log \"Created symlink: ${ORIGINAL_HOST_HOME} -> /host-share\"\n else\n ln -sfn /host-share /root/host-home\n log \"Created fallback symlink: /root/host-home -> /host-share\"\n fi\n fi\n\n # Add local domains to /etc/hosts\n HOSTS_DOMAINS=\"${MATRIX_DOMAIN%%:*} ${HICLAW_MATRIX_CLIENT_DOMAIN:-matrix-client-local.hiclaw.io} ${AI_GATEWAY_DOMAIN} ${HICLAW_FS_DOMAIN:-fs-local.hiclaw.io}\"\n if ! grep -q \"${AI_GATEWAY_DOMAIN}\" /etc/hosts 2>/dev/null; then\n echo \"127.0.0.1 ${HOSTS_DOMAINS}\" >> /etc/hosts\n log \"Added local domains to /etc/hosts\"\n fi\n\n # Wait for local infrastructure\n waitForService \"Higress Gateway\" \"127.0.0.1\" 8080 180\n waitForService \"Higress Console\" \"127.0.0.1\" 8001 180\n waitForService \"Tuwunel\" \"127.0.0.1\" 6167 120\n waitForHTTP \"Tuwunel Matrix API\" \"${HICLAW_MATRIX_URL}/_tuwunel/server_version\" 120\n waitForService \"MinIO\" \"127.0.0.1\" 9000 120\nelse\n # Cloud/K8s mode: wait for external Tuwunel\n log \"Waiting for Tuwunel Matrix server at ${HICLAW_MATRIX_URL}...\"\n _retry=0\n while [ \"${_retry}\" -lt 30 ]; do\n if curl -sf \"${HICLAW_MATRIX_URL}/_matrix/client/versions\" > /dev/null 2>&1; then\n log \"Tuwunel is ready\"\n break\n fi\n _retry=$((_retry + 1))\n log \" Waiting for Tuwunel (attempt ${_retry}/30)...\"\n sleep 5\n done\n if [ \"${_retry}\" -ge 30 ]; then\n log \"ERROR: Tuwunel not reachable at ${HICLAW_MATRIX_URL}\"\n exit 1\n fi\nfi\n\n# ============================================================\n# Auto-generate secrets if not provided via environment\n# Persisted to /data so they survive container restart\n# ============================================================\nSECRETS_FILE=\"/data/hiclaw-secrets.env\"\nif [ -f \"${SECRETS_FILE}\" ]; then\n source \"${SECRETS_FILE}\"\n log \"Loaded persisted secrets from ${SECRETS_FILE}\"\nfi\n\nif [ -z \"${HICLAW_MANAGER_GATEWAY_KEY}\" ]; then\n export HICLAW_MANAGER_GATEWAY_KEY=\"$(generateKey 32)\"\n log \"Auto-generated HICLAW_MANAGER_GATEWAY_KEY\"\nfi\nif [ -z \"${HICLAW_MANAGER_PASSWORD}\" ]; then\n export HICLAW_MANAGER_PASSWORD=\"$(generateKey 16)\"\n log \"Auto-generated HICLAW_MANAGER_PASSWORD\"\nfi\n\n# Persist secrets so they survive supervisord restart\nmkdir -p /data\ncat > \"${SECRETS_FILE}\" <<EOF\nexport HICLAW_MANAGER_GATEWAY_KEY=\"${HICLAW_MANAGER_GATEWAY_KEY}\"\nexport HICLAW_MANAGER_PASSWORD=\"${HICLAW_MANAGER_PASSWORD}\"\nEOF\nchmod 600 \"${SECRETS_FILE}\"\n\n# Cloud mode: pull workspace from OSS before initialization\nif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n HICLAW_FS=\"/root/hiclaw-fs\"\n mkdir -p \"${HICLAW_FS}/shared\" \"${HICLAW_FS}/agents\"\n log \"Pulling workspace from OSS...\"\n ensure_mc_credentials\n mc mirror \"${HICLAW_STORAGE_PREFIX}/manager/\" /root/manager-workspace/ --overwrite 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/shared/\" \"${HICLAW_FS}/shared/\" --overwrite 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/agents/\" \"${HICLAW_FS}/agents/\" --overwrite 2>/dev/null || true\n # Symlink hiclaw-fs into workspace for agent access\n ln -sfn \"${HICLAW_FS}\" /root/manager-workspace/hiclaw-fs\nfi\n\n# K8s mode: sync workspace from cluster-internal MinIO\nif [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n HICLAW_FS=\"/root/hiclaw-fs\"\n mkdir -p \"${HICLAW_FS}/shared\" \"${HICLAW_FS}/agents\" \"${HICLAW_FS}/hiclaw-config\"\n log \"Configuring mc alias for cluster MinIO...\"\n mc alias set hiclaw \"${HICLAW_FS_ENDPOINT}\" \"${HICLAW_FS_ACCESS_KEY}\" \"${HICLAW_FS_SECRET_KEY}\"\n log \"Syncing workspace from MinIO...\"\n mc mirror \"${HICLAW_STORAGE_PREFIX}/manager/\" /root/manager-workspace/ --overwrite 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/\" \"${HICLAW_FS}/\" --overwrite 2>/dev/null || true\n ln -sfn \"${HICLAW_FS}\" /root/manager-workspace/hiclaw-fs\n touch \"${HICLAW_FS}/.initialized\"\nfi\n\n# ============================================================\n# Initialize / upgrade Manager workspace\n# First boot: full init via upgrade-builtins.sh\n# Subsequent boots: compare image version; upgrade only if changed\n# ============================================================\nmkdir -p /root/manager-workspace\n\nIMAGE_VERSION=$(cat /opt/hiclaw/agent/.builtin-version 2>/dev/null || echo \"unknown\")\nINSTALLED_VERSION=$(cat /root/manager-workspace/.builtin-version 2>/dev/null || echo \"\")\n\nif [ ! -f /root/manager-workspace/.initialized ]; then\n log \"First boot: initializing manager workspace...\"\n bash /opt/hiclaw/scripts/init/upgrade-builtins.sh\n touch /root/manager-workspace/.initialized\n log \"Manager workspace initialized (version: ${IMAGE_VERSION})\"\nelif [ \"${IMAGE_VERSION}\" != \"${INSTALLED_VERSION}\" ] || [ \"${IMAGE_VERSION}\" = \"latest\" ]; then\n log \"Upgrade detected: ${INSTALLED_VERSION} -> ${IMAGE_VERSION}${IMAGE_VERSION:+ (latest: always upgrade)}\"\n bash /opt/hiclaw/scripts/init/upgrade-builtins.sh\n log \"Manager workspace upgraded to version: ${IMAGE_VERSION}\"\nelse\n log \"Workspace up to date (version: ${IMAGE_VERSION})\"\nfi\n\n# Local mode: wait for mc mirror initialization (shared + worker data in /root/hiclaw-fs/)\nif [ \"${HICLAW_RUNTIME}\" != \"aliyun\" ] && [ \"${HICLAW_RUNTIME}\" != \"k8s\" ]; then\n log \"Waiting for MinIO storage initialization...\"\n _minio_wait=0\n while [ ! -f /root/hiclaw-fs/.initialized ]; do\n sleep 2\n _minio_wait=$(( _minio_wait + 1 ))\n if [ \"${_minio_wait}\" -ge 60 ]; then\n log \"ERROR: MinIO storage initialization timed out after 120s\"\n exit 1\n fi\n done\n log \"MinIO storage initialized\"\nfi\n\n# ============================================================\n# Register Matrix users via Registration API (single-step, no UIAA)\n# K8s mode: skip — controller Initializer + ManagerReconciler already did this\n# ============================================================\nif [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n log \"K8s mode: skipping Matrix registration (handled by controller)\"\n # Controller injects HICLAW_MANAGER_PASSWORD via env; login to get token\n log \"Obtaining Manager Matrix access token...\"\n _LOGIN_RESPONSE=$(curl -s -X POST ${HICLAW_MATRIX_URL}/_matrix/client/v3/login \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"type\": \"m.login.password\",\n \"identifier\": {\"type\": \"m.id.user\", \"user\": \"manager\"},\n \"password\": \"'\"${HICLAW_MANAGER_PASSWORD}\"'\"\n }' 2>&1)\n MANAGER_TOKEN=$(echo \"${_LOGIN_RESPONSE}\" | jq -r '.access_token' 2>/dev/null)\n if [ -z \"${MANAGER_TOKEN}\" ] || [ \"${MANAGER_TOKEN}\" = \"null\" ]; then\n log \"ERROR: Failed to obtain Manager Matrix token\"\n log \"ERROR: Login response was: ${_LOGIN_RESPONSE}\"\n exit 1\n fi\n log \"Manager Matrix token obtained (token prefix: ${MANAGER_TOKEN:0:10}...)\"\nelse\nlog \"Registering human admin Matrix account...\"\ncurl -sf -X POST ${HICLAW_MATRIX_URL}/_matrix/client/v3/register \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"username\": \"'\"${HICLAW_ADMIN_USER}\"'\",\n \"password\": \"'\"${HICLAW_ADMIN_PASSWORD}\"'\",\n \"auth\": {\n \"type\": \"m.login.registration_token\",\n \"token\": \"'\"${HICLAW_REGISTRATION_TOKEN}\"'\"\n }\n }' > /dev/null 2>&1 || log \"Admin account may already exist\"\n\nlog \"Registering Manager Agent Matrix account...\"\ncurl -sf -X POST ${HICLAW_MATRIX_URL}/_matrix/client/v3/register \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"username\": \"manager\",\n \"password\": \"'\"${HICLAW_MANAGER_PASSWORD}\"'\",\n \"auth\": {\n \"type\": \"m.login.registration_token\",\n \"token\": \"'\"${HICLAW_REGISTRATION_TOKEN}\"'\"\n }\n }' > /dev/null 2>&1 || log \"Manager account may already exist\"\n\n# Get Manager Agent's Matrix access token\nlog \"Obtaining Manager Matrix access token...\"\n_LOGIN_RESPONSE=$(curl -s -X POST ${HICLAW_MATRIX_URL}/_matrix/client/v3/login \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"type\": \"m.login.password\",\n \"identifier\": {\"type\": \"m.id.user\", \"user\": \"manager\"},\n \"password\": \"'\"${HICLAW_MANAGER_PASSWORD}\"'\"\n }' 2>&1)\n_LOGIN_EXIT=$?\nlog \"Matrix login HTTP exit code: ${_LOGIN_EXIT}\"\nlog \"Matrix login response: ${_LOGIN_RESPONSE}\"\n\nMANAGER_TOKEN=$(echo \"${_LOGIN_RESPONSE}\" | jq -r '.access_token' 2>/dev/null)\n\nif [ -z \"${MANAGER_TOKEN}\" ] || [ \"${MANAGER_TOKEN}\" = \"null\" ]; then\n log \"ERROR: Failed to obtain Manager Matrix token (exit=${_LOGIN_EXIT})\"\n log \"ERROR: Login response was: ${_LOGIN_RESPONSE}\"\n exit 1\nfi\nlog \"Manager Matrix token obtained (token prefix: ${MANAGER_TOKEN:0:10}...)\"\nfi\n\n# ============================================================\n# Higress Console initialization\n# Docker mode: full setup-higress.sh (internal Higress at localhost:8001)\n# K8s mode: skip — controller Initializer handles Higress setup\n# Cloud (aliyun) mode: skip entirely (Higress managed externally)\n# ============================================================\n_HIGRESS_CONSOLE_URL=\"\"\n_HIGRESS_USER=\"${HICLAW_ADMIN_USER}\"\n_HIGRESS_PASS=\"${HICLAW_ADMIN_PASSWORD}\"\nif [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n log \"K8s mode: skipping Higress initialization (handled by controller)\"\nelif [ \"${HICLAW_RUNTIME}\" != \"aliyun\" ]; then\n _HIGRESS_CONSOLE_URL=\"http://127.0.0.1:8001\"\nfi\n\nif [ -n \"${_HIGRESS_CONSOLE_URL}\" ]; then\n COOKIE_FILE=\"/tmp/higress-session-cookie\"\n\n log \"Waiting for Higress Console (${_HIGRESS_CONSOLE_URL}) to be fully ready and initializing admin...\"\n INIT_DONE=false\n for i in $(seq 1 90); do\n INIT_RESULT=$(curl -s -X POST \"${_HIGRESS_CONSOLE_URL}/system/init\" \\\n -H 'Content-Type: application/json' \\\n -d '{\"adminUser\":{\"name\":\"'\"${_HIGRESS_USER}\"'\",\"password\":\"'\"${_HIGRESS_PASS}\"'\",\"displayName\":\"'\"${_HIGRESS_USER}\"'\"}}' 2>/dev/null) || true\n if echo \"${INIT_RESULT}\" | grep -qE '\"success\":true|already.?init' 2>/dev/null; then\n INIT_DONE=true\n break\n fi\n if echo \"${INIT_RESULT}\" | grep -q '\"name\"' 2>/dev/null; then\n INIT_DONE=true\n break\n fi\n sleep 2\n done\n\n if [ \"${INIT_DONE}\" != \"true\" ]; then\n log \"ERROR: Higress Console did not become ready within 180s\"\n exit 1\n fi\n log \"Higress Console init done\"\n\n log \"Logging into Higress Console...\"\n LOGIN_OK=false\n for i in $(seq 1 10); do\n HTTP_CODE=$(curl -s -o /dev/null -w '%{http_code}' -X POST \"${_HIGRESS_CONSOLE_URL}/session/login\" \\\n -H 'Content-Type: application/json' \\\n -c \"${COOKIE_FILE}\" \\\n -d '{\"username\":\"'\"${_HIGRESS_USER}\"'\",\"password\":\"'\"${_HIGRESS_PASS}\"'\"}' 2>/dev/null) || true\n if { [ \"${HTTP_CODE}\" = \"200\" ] || [ \"${HTTP_CODE}\" = \"201\" ]; } && [ -f \"${COOKIE_FILE}\" ] && [ -s \"${COOKIE_FILE}\" ]; then\n LOGIN_OK=true\n break\n fi\n log \"Login attempt $i (HTTP ${HTTP_CODE}), retrying in 3s...\"\n sleep 3\n done\n\n if [ \"${LOGIN_OK}\" != \"true\" ]; then\n log \"ERROR: Could not login to Higress Console after retries\"\n exit 1\n fi\n log \"Higress Console login successful\"\n\n VERIFY_CODE=$(curl -s -o /dev/null -w '%{http_code}' \"${_HIGRESS_CONSOLE_URL}/v1/consumers\" -b \"${COOKIE_FILE}\" 2>/dev/null) || true\n if [ \"${VERIFY_CODE}\" = \"200\" ]; then\n log \"Console session verified (cookie valid)\"\n else\n log \"WARNING: Console session may be invalid (verify returned HTTP ${VERIFY_CODE})\"\n rm -f \"${COOKIE_FILE}\"\n for i in $(seq 1 5); do\n curl -s -o /dev/null -w '%{http_code}' -X POST \"${_HIGRESS_CONSOLE_URL}/session/login\" \\\n -H 'Content-Type: application/json' \\\n -c \"${COOKIE_FILE}\" \\\n -d '{\"username\":\"'\"${_HIGRESS_USER}\"'\",\"password\":\"'\"${_HIGRESS_PASS}\"'\"}' 2>/dev/null\n VERIFY2=$(curl -s -o /dev/null -w '%{http_code}' \"${_HIGRESS_CONSOLE_URL}/v1/consumers\" -b \"${COOKIE_FILE}\" 2>/dev/null) || true\n if [ \"${VERIFY2}\" = \"200\" ]; then\n log \"Re-login successful, session verified\"\n break\n fi\n sleep 2\n done\n fi\n\n export HIGRESS_COOKIE_FILE=\"${COOKIE_FILE}\"\n export HIGRESS_CONSOLE_URL=\"${_HIGRESS_CONSOLE_URL}\"\n\n if [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n # K8s mode: lightweight Higress config — only what's needed for LLM access\n source /opt/hiclaw/scripts/lib/base.sh\n _k8s_higress_api() {\n local method=\"$1\" path=\"$2\" desc=\"$3\"; shift 3; local body=\"$*\"\n local tmpfile; tmpfile=$(mktemp)\n local http_code\n http_code=$(curl -s -o \"${tmpfile}\" -w '%{http_code}' -X \"${method}\" \"${_HIGRESS_CONSOLE_URL}${path}\" \\\n -b \"${COOKIE_FILE}\" -H 'Content-Type: application/json' -d \"${body}\" 2>/dev/null) || true\n local response; response=$(cat \"${tmpfile}\" 2>/dev/null); rm -f \"${tmpfile}\"\n if echo \"${response}\" | grep -q '\"success\":true' 2>/dev/null; then\n log \"${desc} ... OK\"\n elif [ \"${http_code}\" = \"409\" ]; then\n log \"${desc} ... already exists, skipping\"\n elif [ \"${http_code}\" = \"200\" ] || [ \"${http_code}\" = \"201\" ] || [ \"${http_code}\" = \"204\" ]; then\n log \"${desc} ... OK (HTTP ${http_code})\"\n else\n log \"WARNING: ${desc} ... (HTTP ${http_code}): ${response}\"\n fi\n }\n\n # 1. Service Sources (DNS type → K8s Service FQDN)\n # Extract host:port from URLs for Higress service source registration\n _TUWUNEL_HOST=$(echo \"${HICLAW_MATRIX_URL}\" | sed 's|^http[s]*://||')\n _TUWUNEL_DOMAIN=$(echo \"${_TUWUNEL_HOST}\" | cut -d: -f1)\n _TUWUNEL_PORT=$(echo \"${_TUWUNEL_HOST}\" | cut -d: -f2)\n _k8s_higress_api POST /v1/service-sources \"Registering Tuwunel service source\" \\\n '{\"type\":\"dns\",\"name\":\"tuwunel\",\"domain\":\"'\"${_TUWUNEL_DOMAIN}\"'\",\"port\":'\"${_TUWUNEL_PORT}\"'}'\n\n if [ -n \"${HICLAW_ELEMENT_WEB_URL:-}\" ]; then\n _ELEMENT_HOST=$(echo \"${HICLAW_ELEMENT_WEB_URL}\" | sed 's|^http[s]*://||')\n _ELEMENT_DOMAIN=$(echo \"${_ELEMENT_HOST}\" | cut -d: -f1)\n _ELEMENT_PORT=$(echo \"${_ELEMENT_HOST}\" | cut -d: -f2)\n _k8s_higress_api POST /v1/service-sources \"Registering Element Web service source\" \\\n '{\"type\":\"dns\",\"name\":\"element-web\",\"domain\":\"'\"${_ELEMENT_DOMAIN}\"'\",\"port\":'\"${_ELEMENT_PORT}\"'}'\n fi\n\n # 2. Manager Consumer (key-auth)\n _k8s_higress_api POST /v1/consumers \"Creating Manager consumer\" \\\n '{\"name\":\"manager\",\"credentials\":[{\"type\":\"key-auth\",\"source\":\"BEARER\",\"values\":[\"'\"${HICLAW_MANAGER_GATEWAY_KEY}\"'\"]}]}'\n\n # 3. LLM Provider\n _LLM_PROVIDER=\"${HICLAW_LLM_PROVIDER:-qwen}\"\n _LLM_API_URL=\"${HICLAW_LLM_API_URL:-}\"\n if [ -z \"${_LLM_API_URL}\" ]; then\n case \"${_LLM_PROVIDER}\" in\n qwen) _LLM_API_URL=\"https://dashscope.aliyuncs.com/compatible-mode/v1\" ;;\n esac\n fi\n # 4. LLM Provider type-specific config\n case \"${_LLM_PROVIDER}\" in\n qwen)\n _k8s_higress_api POST /v1/ai/providers \"Creating LLM provider (qwen)\" \\\n '{\"type\":\"qwen\",\"name\":\"qwen\",\"tokens\":[\"'\"${HICLAW_LLM_API_KEY}\"'\"],\"protocol\":\"openai/v1\",\"tokenFailoverConfig\":{\"enabled\":false},\"rawConfigs\":{\"qwenEnableSearch\":false,\"qwenEnableCompatible\":true,\"qwenFileIds\":[],\"hiclawMode\":true}}'\n ;;\n *)\n _BODY='{\"name\":\"'\"${_LLM_PROVIDER}\"'\",\"type\":\"openai\",\"tokens\":[\"'\"${HICLAW_LLM_API_KEY}\"'\"],\"modelMapping\":{},\"protocol\":\"openai/v1\",\"rawConfigs\":{\"hiclawMode\":true}}'\n _k8s_higress_api POST /v1/ai/providers \"Creating LLM provider (${_LLM_PROVIDER})\" \"${_BODY}\"\n ;;\n esac\n\n # 5. AI Route (bind provider + consumer auth, /v1 prefix to avoid clash with Element Web catch-all)\n _k8s_higress_api POST /v1/ai/routes \"Creating AI Gateway route\" \\\n '{\"name\":\"default-ai-route\",\"domains\":[],\"pathPredicate\":{\"matchType\":\"PRE\",\"matchValue\":\"/v1\",\"caseSensitive\":false},\"upstreams\":[{\"provider\":\"'\"${_LLM_PROVIDER}\"'\",\"weight\":100,\"modelMapping\":{}}],\"authConfig\":{\"enabled\":true,\"allowedCredentialTypes\":[\"key-auth\"],\"allowedConsumers\":[\"manager\"]}}'\n\n # 6. Matrix Homeserver Route (/_matrix/* → Tuwunel, no auth)\n _k8s_higress_api POST /v1/routes \"Creating Matrix Homeserver route\" \\\n '{\"name\":\"matrix-homeserver\",\"domains\":[],\"path\":{\"matchType\":\"PRE\",\"matchValue\":\"/_matrix\"},\"services\":[{\"name\":\"tuwunel.dns\",\"port\":'\"${_TUWUNEL_PORT}\"',\"weight\":100}]}'\n\n # 7. Element Web Route (/ catch-all → Element Web, no auth)\n if [ -n \"${HICLAW_ELEMENT_WEB_URL:-}\" ]; then\n _k8s_higress_api POST /v1/routes \"Creating Element Web route\" \\\n '{\"name\":\"element-web\",\"domains\":[],\"path\":{\"matchType\":\"PRE\",\"matchValue\":\"/\"},\"services\":[{\"name\":\"element-web.dns\",\"port\":'\"${_ELEMENT_PORT}\"',\"weight\":100}]}'\n fi\n\n # 8. Remove Higress default landing page (Exact match on / takes precedence over Element Web catch-all)\n _k8s_higress_api DELETE /v1/routes/default \"Removing Higress default landing route\"\n\n log \"K8s Higress lightweight setup complete\"\n\n # Wait for AI plugin activation (~45s for first config)\n log \"Waiting for AI Gateway plugin activation (45s)...\"\n sleep 45\n else\n # Docker mode: full setup with all routes, domains, MCP servers\n /opt/hiclaw/scripts/init/setup-higress.sh\n fi\nfi\n\n# ============================================================\n# Create admin DM room, persist to state.json, send welcome message\n# K8s mode: skip — controller ProvisionManager already creates the Admin DM room\n# Runs in both local and cloud modes (idempotent)\n# ============================================================\nif [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n log \"K8s mode: skipping admin DM room creation (handled by controller)\"\nelse\nMANAGER_FULL_ID=\"@manager:${MATRIX_DOMAIN}\"\nADMIN_FULL_ID=\"@${HICLAW_ADMIN_USER}:${MATRIX_DOMAIN}\"\n\nlog \"Logging in as admin to create DM room...\"\n_ADMIN_LOGIN=$(curl -sf -X POST \"${HICLAW_MATRIX_URL}/_matrix/client/v3/login\" \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"type\": \"m.login.password\",\n \"identifier\": {\"type\": \"m.id.user\", \"user\": \"'\"${HICLAW_ADMIN_USER}\"'\"},\n \"password\": \"'\"${HICLAW_ADMIN_PASSWORD}\"'\"\n }' 2>&1) || true\n\nADMIN_MATRIX_TOKEN=$(echo \"${_ADMIN_LOGIN}\" | jq -r '.access_token // empty' 2>/dev/null)\nif [ -z \"${ADMIN_MATRIX_TOKEN}\" ]; then\n log \"WARNING: Failed to login as admin, skipping DM room creation\"\nelse\n # Search for existing DM room with Manager (idempotent)\n DM_ROOM_ID=\"\"\n _JOINED_ROOMS=$(curl -sf \"${HICLAW_MATRIX_URL}/_matrix/client/v3/joined_rooms\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" 2>/dev/null \\\n | jq -r '.joined_rooms[]' 2>/dev/null) || true\n for _rid in ${_JOINED_ROOMS}; do\n _members=$(curl -sf \"${HICLAW_MATRIX_URL}/_matrix/client/v3/rooms/${_rid}/members\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" 2>/dev/null \\\n | jq -r '.chunk[].state_key' 2>/dev/null) || continue\n _count=$(echo \"${_members}\" | wc -l | xargs)\n if [ \"${_count}\" = \"2\" ] && echo \"${_members}\" | grep -q \"@manager:\"; then\n DM_ROOM_ID=\"${_rid}\"\n break\n fi\n done\n\n if [ -n \"${DM_ROOM_ID}\" ]; then\n log \"Existing DM room found: ${DM_ROOM_ID}\"\n else\n log \"Creating DM room with Manager...\"\n _RAW=$(curl -s -w '\\nHTTP_CODE:%{http_code}' -X POST \"${HICLAW_MATRIX_URL}/_matrix/client/v3/createRoom\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d \"{\\\"is_direct\\\":true,\\\"invite\\\":[\\\"${MANAGER_FULL_ID}\\\"],\\\"preset\\\":\\\"trusted_private_chat\\\"}\" 2>&1) || true\n _HTTP_CODE=$(echo \"${_RAW}\" | tail -1 | sed 's/HTTP_CODE://')\n _CREATE_RESP=$(echo \"${_RAW}\" | sed '$d')\n DM_ROOM_ID=$(echo \"${_CREATE_RESP}\" | jq -r '.room_id // empty' 2>/dev/null)\n if [ -n \"${DM_ROOM_ID}\" ]; then\n log \"DM room created: ${DM_ROOM_ID}\"\n else\n log \"WARNING: Failed to create DM room (HTTP ${_HTTP_CODE}): ${_CREATE_RESP}\"\n fi\n fi\n\n # Persist admin DM room ID to state.json\n if [ -n \"${DM_ROOM_ID}\" ]; then\n STATE_SCRIPT=\"/opt/hiclaw/agent/skills/task-management/scripts/manage-state.sh\"\n if [ -f \"${STATE_SCRIPT}\" ]; then\n bash \"${STATE_SCRIPT}\" --action init 2>/dev/null || true\n bash \"${STATE_SCRIPT}\" --action set-admin-dm --room-id \"${DM_ROOM_ID}\" 2>/dev/null || true\n log \"Admin DM room persisted to state.json: ${DM_ROOM_ID}\"\n fi\n fi\n\n # Schedule welcome message in background (only on first boot)\n if [ -n \"${DM_ROOM_ID}\" ] && [ ! -f \"/root/manager-workspace/soul-configured\" ]; then\n log \"Scheduling welcome message (background, waiting for OpenClaw to start)...\"\n (\n _HICLAW_LANGUAGE=\"${HICLAW_LANGUAGE:-zh}\"\n _HICLAW_TIMEZONE=\"${TZ:-Asia/Shanghai}\"\n _wait=0\n _ready=false\n while [ \"${_wait}\" -lt 300 ]; do\n if curl -sf http://127.0.0.1:18799/ > /dev/null 2>&1; then\n _ready=true\n break\n fi\n sleep 3\n _wait=$((_wait + 3))\n done\n if [ \"${_ready}\" != \"true\" ]; then\n echo \"[manager] WARNING: OpenClaw gateway not ready within 300s, skipping welcome message\"\n exit 0\n fi\n # Ensure Manager has joined the DM room before sending the welcome\n # message. Without this, there is a race between OpenClaw's Matrix\n # auto-join and the message send — the message may land before Manager\n # joins, so OpenClaw's /sync never picks it up.\n _join_ok=false\n for _join_attempt in 1 2 3; do\n if curl -sf -X POST \"${HICLAW_MATRIX_URL}/_matrix/client/v3/rooms/${DM_ROOM_ID}/join\" \\\n -H \"Authorization: Bearer ${MANAGER_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d '{}' > /dev/null 2>&1; then\n echo \"[manager] Manager joined DM room before welcome message\"\n _join_ok=true\n break\n fi\n sleep 2\n done\n if [ \"${_join_ok}\" != \"true\" ]; then\n echo \"[manager] WARNING: Manager join request failed after 3 attempts (may already be joined)\"\n fi\n _welcome_msg=\"This is an automated message from the HiClaw setup. This is a fresh installation.\n\n--- Installation Context ---\nUser Language: ${_HICLAW_LANGUAGE} (zh = Chinese, en = English)\nUser Timezone: ${_HICLAW_TIMEZONE} (IANA timezone identifier)\n---\n\nYou are an AI agent that manages a team of worker agents. Your identity and personality have not been configured yet — the human admin is about to meet you for the first time.\n\nPlease begin the onboarding conversation:\n\n1. Greet the admin warmly and briefly describe what you can do (coordinate workers, manage tasks, run multi-agent projects)\n2. The user has selected \\\"${_HICLAW_LANGUAGE}\\\" as their preferred language during installation. Use this language for your greeting and all subsequent communication.\n3. The user's timezone is ${_HICLAW_TIMEZONE}. Based on this timezone, you may infer their likely region and suggest additional language options.\n4. Ask them: a) What would they like to call you? b) Communication style preference? c) Any behavior guidelines? d) Confirm default language\n5. After they reply, write their preferences to ~/SOUL.md\n6. Confirm what you wrote, and ask if they would like to adjust anything\n7. Once confirmed, run: touch ~/soul-configured\n\nThe human admin will start chatting shortly.\"\n _txn_id=\"welcome-$(date +%s)\"\n _payload=$(jq -nc --arg body \"${_welcome_msg}\" '{\"msgtype\":\"m.text\",\"body\":$body}')\n _raw=$(curl -s -w '\\nHTTP_CODE:%{http_code}' -X PUT \"${HICLAW_MATRIX_URL}/_matrix/client/v3/rooms/${DM_ROOM_ID}/send/m.room.message/${_txn_id}\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d \"${_payload}\" 2>&1) || true\n _http_code=$(echo \"${_raw}\" | tail -1 | sed 's/HTTP_CODE://')\n _send_resp=$(echo \"${_raw}\" | sed '$d')\n if echo \"${_send_resp}\" | jq -e '.event_id' > /dev/null 2>&1; then\n echo \"[manager] Welcome message sent to DM room\"\n else\n echo \"[manager] WARNING: Failed to send welcome message (HTTP ${_http_code}): ${_send_resp}\"\n fi\n ) &\n log \"Welcome message background process started (PID: $!)\"\n fi\nfi\nfi # end K8s mode skip for admin DM room\n\n# ============================================================\n# Generate Manager Agent openclaw.json from template\n# ============================================================\nlog \"Generating Manager openclaw.json...\"\nexport MANAGER_MATRIX_TOKEN=\"${MANAGER_TOKEN}\"\nexport MANAGER_GATEWAY_KEY=\"${HICLAW_MANAGER_GATEWAY_KEY}\"\n# Resolve model parameters based on model name\nMODEL_NAME=\"${HICLAW_DEFAULT_MODEL:-qwen3.5-plus}\"\ncase \"${MODEL_NAME}\" in\n gpt-5.3-codex|gpt-5-mini|gpt-5-nano)\n export MODEL_CONTEXT_WINDOW=400000 MODEL_MAX_TOKENS=128000 ;;\n claude-opus-4-6)\n export MODEL_CONTEXT_WINDOW=1000000 MODEL_MAX_TOKENS=128000 ;;\n claude-sonnet-4-6)\n export MODEL_CONTEXT_WINDOW=1000000 MODEL_MAX_TOKENS=64000 ;;\n claude-haiku-4-5)\n export MODEL_CONTEXT_WINDOW=200000 MODEL_MAX_TOKENS=64000 ;;\n qwen3.5-plus)\n export MODEL_CONTEXT_WINDOW=200000 MODEL_MAX_TOKENS=64000 ;;\n deepseek-chat|deepseek-reasoner|kimi-k2.5)\n export MODEL_CONTEXT_WINDOW=256000 MODEL_MAX_TOKENS=128000 ;;\n glm-5|MiniMax-M2.7|MiniMax-M2.7-highspeed|MiniMax-M2.5)\n export MODEL_CONTEXT_WINDOW=200000 MODEL_MAX_TOKENS=128000 ;;\n *)\n export MODEL_CONTEXT_WINDOW=150000 MODEL_MAX_TOKENS=128000 ;;\nesac\nexport MODEL_REASONING=true\n\n# Override with user-supplied custom model parameters from env (set during install)\n[ -n \"${HICLAW_MODEL_CONTEXT_WINDOW:-}\" ] && export MODEL_CONTEXT_WINDOW=\"${HICLAW_MODEL_CONTEXT_WINDOW}\"\n[ -n \"${HICLAW_MODEL_MAX_TOKENS:-}\" ] && export MODEL_MAX_TOKENS=\"${HICLAW_MODEL_MAX_TOKENS}\"\n[ -n \"${HICLAW_MODEL_REASONING:-}\" ] && export MODEL_REASONING=\"${HICLAW_MODEL_REASONING}\"\n\n# E2EE: convert HICLAW_MATRIX_E2EE to JSON boolean for template substitution\nif [ \"${HICLAW_MATRIX_E2EE:-0}\" = \"1\" ] || [ \"${HICLAW_MATRIX_E2EE:-}\" = \"true\" ]; then\n export MATRIX_E2EE_ENABLED=true\nelse\n export MATRIX_E2EE_ENABLED=false\nfi\nlog \"Matrix E2EE: ${MATRIX_E2EE_ENABLED}\"\n\n# Resolve input modalities: only vision-capable models get \"image\"\ncase \"${MODEL_NAME}\" in\n gpt-5.4|gpt-5.3-codex|gpt-5-mini|gpt-5-nano|claude-opus-4-6|claude-sonnet-4-6|claude-haiku-4-5|qwen3.5-plus|kimi-k2.5)\n export MODEL_INPUT='[\"text\", \"image\"]' ;;\n *)\n export MODEL_INPUT='[\"text\"]' ;;\nesac\n# Override with user-supplied vision setting from env\nif [ \"${HICLAW_MODEL_VISION:-}\" = \"true\" ]; then\n export MODEL_INPUT='[\"text\", \"image\"]'\nelif [ \"${HICLAW_MODEL_VISION:-}\" = \"false\" ]; then\n export MODEL_INPUT='[\"text\"]'\nfi\n\nlog \"Model: ${MODEL_NAME} (context=${MODEL_CONTEXT_WINDOW}, maxTokens=${MODEL_MAX_TOKENS}, reasoning=${MODEL_REASONING}, input=${MODEL_INPUT})\"\n\nif [ -f /root/manager-workspace/openclaw.json ]; then\n log \"Manager openclaw.json already exists, updating dynamic fields only (preserving user customizations)...\"\n # Merge known models into existing config (add missing, preserve user-added)\n # Use known-models.json (valid JSON) instead of template (contains ${VAR} placeholders)\n KNOWN_MODELS=$(cat /opt/hiclaw/configs/known-models.json 2>/dev/null || echo '[]')\n jq --arg token \"${MANAGER_TOKEN}\" \\\n --arg key \"${HICLAW_MANAGER_GATEWAY_KEY}\" \\\n --arg model \"${MODEL_NAME}\" \\\n --arg emb_model \"${HICLAW_EMBEDDING_MODEL}\" \\\n --arg aigw_domain \"${AI_GATEWAY_DOMAIN}\" \\\n --arg matrix_user_id \"@manager:${MATRIX_DOMAIN}\" \\\n --argjson e2ee \"${MATRIX_E2EE_ENABLED}\" \\\n --argjson known_models \"${KNOWN_MODELS}\" \\\n --argjson ctx \"${MODEL_CONTEXT_WINDOW}\" \\\n --argjson max \"${MODEL_MAX_TOKENS}\" \\\n --argjson reasoning \"${MODEL_REASONING}\" \\\n --argjson input \"${MODEL_INPUT}\" \\\n '\n # Merge known models: add any model id not already present\n .models.providers[\"hiclaw-gateway\"].models as $existing\n | ($existing | map(.id)) as $existing_ids\n | ($known_models | map(select(.id as $id | $existing_ids | index($id) | not))) as $new\n | .models.providers[\"hiclaw-gateway\"].models = ($existing + $new)\n # Ensure the user-chosen default model is in the list (custom model support)\n | if (.models.providers[\"hiclaw-gateway\"].models | map(.id) | index($model) | not) then\n .models.providers[\"hiclaw-gateway\"].models += [{\"id\": $model, \"name\": $model, \"reasoning\": $reasoning, \"contextWindow\": $ctx, \"maxTokens\": $max, \"input\": $input}]\n else . end\n # Rebuild model aliases from the full models list\n | (.models.providers[\"hiclaw-gateway\"].models | map({ (\"hiclaw-gateway/\" + .id): { \"alias\": .id } }) | add // {}) as $aliases\n | .agents.defaults.models = ((.agents.defaults.models // {}) + $aliases)\n | .channels.matrix.accessToken = $token | .channels.matrix.userId = $matrix_user_id | .models.providers[\"hiclaw-gateway\"].apiKey = $key\n | ((.hooks.token // \"\") as $ht | if $ht == $key or $ht == ($key + \"-hooks\" | @base64) then del(.hooks) else . end)\n | .agents.defaults.model.primary = (\"hiclaw-gateway/\" + $model)\n | .commands.restart = true\n | .gateway.port = 18799\n | .gateway.bind = \"lan\"\n | .gateway.controlUi = ((.gateway.controlUi // {}) + {\"dangerouslyDisableDeviceAuth\": true, \"allowInsecureAuth\": true, \"allowedOrigins\": [\"*\"]})\n | .channels.matrix.encryption = $e2ee\n | .channels.matrix.network = ((.channels.matrix.network // {}) + {\"dangerouslyAllowPrivateNetwork\": true})\n | .channels.matrix.autoJoin = \"always\"\n # Ensure memorySearch config exists (embedding model for memory) — skip if embedding model is empty\n | if $emb_model != \"\" then .agents.defaults.memorySearch //= {\"provider\":\"openai\",\"model\":$emb_model,\"remote\":{\"baseUrl\":(\"http://\" + $aigw_domain + \":8080/v1\"),\"apiKey\":$key}} else . end\n ' \\\n /root/manager-workspace/openclaw.json > /tmp/openclaw.json.tmp && \\\n mv /tmp/openclaw.json.tmp /root/manager-workspace/openclaw.json\n # Anti-tampering bypass: openclaw's config-observe-recovery clobbers the file\n # back to .bak whenever it sees \"missing-meta-vs-last-good\", which strips\n # external edits like channels.matrix.network.dangerouslyAllowPrivateNetwork.\n # Stamp meta and mirror to .bak so our edits survive.\n _oc_ver=$(openclaw --version 2>/dev/null | head -1 | awk '{print $NF}')\n [ -z \"${_oc_ver}\" ] && _oc_ver=\"hiclaw-bootstrap\"\n jq --arg ver \"${_oc_ver}\" --arg ts \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\" \\\n '.meta = ((.meta // {}) + {lastTouchedVersion: $ver, lastTouchedAt: $ts})' \\\n /root/manager-workspace/openclaw.json > /tmp/openclaw.meta.json && \\\n mv /tmp/openclaw.meta.json /root/manager-workspace/openclaw.json\n cp -f /root/manager-workspace/openclaw.json /root/manager-workspace/openclaw.json.bak\n # Verify the token was written correctly\n _written_token=$(jq -r '.channels.matrix.accessToken' /root/manager-workspace/openclaw.json 2>/dev/null)\n if [ -z \"${_written_token}\" ] || [ \"${_written_token}\" = \"null\" ]; then\n log \"ERROR: Matrix token was not written correctly to openclaw.json (got: ${_written_token})\"\n else\n log \"Matrix token written to openclaw.json (prefix: ${_written_token:0:10}...)\"\n fi\nelse\n log \"Manager openclaw.json not found, generating from template...\"\n envsubst < /opt/hiclaw/configs/manager-openclaw.json.tmpl > /root/manager-workspace/openclaw.json\n # Post-envsubst injection: memorySearch + custom model (single jq pass when possible)\n if ! jq -e --arg model \"${MODEL_NAME}\" '.models.providers[\"hiclaw-gateway\"].models | map(.id) | index($model)' /root/manager-workspace/openclaw.json > /dev/null 2>&1; then\n log \"Custom model '${MODEL_NAME}' not in built-in list, injecting into config...\"\n jq --arg emb_model \"${HICLAW_EMBEDDING_MODEL}\" \\\n --arg aigw_domain \"${AI_GATEWAY_DOMAIN}\" \\\n --arg key \"${HICLAW_MANAGER_GATEWAY_KEY}\" \\\n --arg model \"${MODEL_NAME}\" \\\n --argjson ctx \"${MODEL_CONTEXT_WINDOW}\" \\\n --argjson max \"${MODEL_MAX_TOKENS}\" \\\n --argjson reasoning \"${MODEL_REASONING}\" \\\n --argjson input \"${MODEL_INPUT}\" \\\n '\n (if $emb_model != \"\" then .agents.defaults.memorySearch = {\"provider\":\"openai\",\"model\":$emb_model,\"remote\":{\"baseUrl\":(\"http://\" + $aigw_domain + \":8080/v1\"),\"apiKey\":$key}} else . end)\n | .models.providers[\"hiclaw-gateway\"].models += [{\"id\": $model, \"name\": $model, \"reasoning\": $reasoning, \"contextWindow\": $ctx, \"maxTokens\": $max, \"input\": $input}]\n | .agents.defaults.models += {(\"hiclaw-gateway/\" + $model): {\"alias\": $model}}\n ' /root/manager-workspace/openclaw.json > /tmp/openclaw.json.tmp && \\\n mv /tmp/openclaw.json.tmp /root/manager-workspace/openclaw.json\n elif [ -n \"${HICLAW_EMBEDDING_MODEL}\" ]; then\n jq --arg emb_model \"${HICLAW_EMBEDDING_MODEL}\" \\\n --arg aigw_domain \"${AI_GATEWAY_DOMAIN}\" \\\n --arg key \"${HICLAW_MANAGER_GATEWAY_KEY}\" \\\n '.agents.defaults.memorySearch = {\"provider\":\"openai\",\"model\":$emb_model,\"remote\":{\"baseUrl\":(\"http://\" + $aigw_domain + \":8080/v1\"),\"apiKey\":$key}}' \\\n /root/manager-workspace/openclaw.json > /tmp/openclaw.json.tmp && \\\n mv /tmp/openclaw.json.tmp /root/manager-workspace/openclaw.json\n fi\n _written_token=$(jq -r '.channels.matrix.accessToken' /root/manager-workspace/openclaw.json 2>/dev/null)\n log \"Matrix token written from template (prefix: ${_written_token:0:10}...)\"\nfi\n\n# Cloud/K8s mode: overlay cloud-specific settings onto generated config\nif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ] || [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n log \"Applying cloud/k8s overlay to openclaw.json...\"\n jq --arg homeserver \"${HICLAW_MATRIX_URL}\" \\\n --arg gateway \"${HICLAW_AI_GATEWAY_URL}/v1\" \\\n --arg key \"${HICLAW_MANAGER_GATEWAY_KEY}\" \\\n '.channels.matrix.homeserver = $homeserver\n | .models.providers[\"hiclaw-gateway\"].baseUrl = $gateway\n | .models.providers[\"hiclaw-gateway\"].apiKey = $key\n | ((.hooks.token // \"\") as $ht | if $ht == $key or $ht == ($key + \"-hooks\" | @base64) then del(.hooks) else . end)\n | .commands.restart = false\n | if .agents.defaults.memorySearch then .agents.defaults.memorySearch.remote.baseUrl = $gateway | .agents.defaults.memorySearch.remote.apiKey = $key else . end' \\\n /root/manager-workspace/openclaw.json > /tmp/openclaw-cloud.json && \\\n mv /tmp/openclaw-cloud.json /root/manager-workspace/openclaw.json\n log \"Cloud/K8s overlay applied\"\nfi\n\n# ============================================================\n# Optional: enable openclaw-cms-plugin observability\n# Config is applied at runtime so secrets stay out of image layers.\n# ============================================================\nCMS_TRACES_ENABLED=\"$(echo \"${HICLAW_CMS_TRACES_ENABLED:-false}\" | tr '[:upper:]' '[:lower:]')\"\nif [ \"${CMS_TRACES_ENABLED}\" = \"true\" ]; then\n CMS_PLUGIN_NAME=\"openclaw-cms-plugin\"\n CMS_PLUGIN_DIR=\"${OPENCLAW_CMS_PLUGIN_DIR:-/opt/openclaw/extensions/openclaw-cms-plugin}\"\n CMS_PLUGIN_MANIFEST=\"${CMS_PLUGIN_DIR}/openclaw.plugin.json\"\n DIAG_PLUGIN_NAME=\"diagnostics-otel\"\n DIAG_PLUGIN_DIR=\"/opt/openclaw/extensions/diagnostics-otel\"\n CMS_LICENSE_KEY=\"${HICLAW_CMS_LICENSE_KEY:-}\"\n CMS_PROJECT=\"${HICLAW_CMS_PROJECT:-}\"\n CMS_METRICS_ENABLED=\"${HICLAW_CMS_METRICS_ENABLED:-false}\"\n\n if [ ! -f \"${CMS_PLUGIN_MANIFEST}\" ]; then\n log \"WARNING: ${CMS_PLUGIN_NAME} manifest not found at ${CMS_PLUGIN_MANIFEST}, skipping plugin config\"\n else\n _missing=0\n [ -z \"${HICLAW_CMS_ENDPOINT:-}\" ] && log \"WARNING: HICLAW_CMS_ENDPOINT is required when HICLAW_CMS_TRACES_ENABLED=true\" && _missing=1\n [ -z \"${CMS_LICENSE_KEY:-}\" ] && log \"WARNING: HICLAW_CMS_LICENSE_KEY is required when HICLAW_CMS_TRACES_ENABLED=true\" && _missing=1\n [ -z \"${HICLAW_CMS_WORKSPACE:-}\" ] && log \"WARNING: HICLAW_CMS_WORKSPACE is required when HICLAW_CMS_TRACES_ENABLED=true\" && _missing=1\n\n if [ \"${_missing}\" = \"0\" ]; then\n CMS_SERVICE_NAME=\"${HICLAW_CMS_SERVICE_NAME:-hiclaw-manager}\"\n CMS_ENABLE_METRICS=\"${CMS_METRICS_ENABLED}\"\n DIAG_AVAILABLE=\"0\"\n _metrics_lc=\"$(echo \"${CMS_ENABLE_METRICS}\" | tr '[:upper:]' '[:lower:]')\"\n if [ \"${_metrics_lc}\" = \"true\" ]; then\n if [ -f \"${DIAG_PLUGIN_DIR}/package.json\" ]; then\n DIAG_AVAILABLE=\"1\"\n if [ ! -d \"${DIAG_PLUGIN_DIR}/node_modules\" ]; then\n log \"diagnostics-otel dependencies missing, installing...\"\n if (cd \"${DIAG_PLUGIN_DIR}\" && npm install --omit=dev --ignore-scripts >/tmp/hiclaw-diag-install.log 2>&1); then\n log \"diagnostics-otel dependencies installed\"\n else\n log \"WARNING: diagnostics-otel npm install failed, metrics plugin may not load\"\n fi\n else\n log \"diagnostics-otel dependencies already present\"\n fi\n else\n log \"WARNING: diagnostics-otel package.json not found at ${DIAG_PLUGIN_DIR}, metrics plugin may not load\"\n fi\n fi\n\n log \"Applying ${CMS_PLUGIN_NAME} config to openclaw.json...\"\n jq --arg pluginName \"${CMS_PLUGIN_NAME}\" \\\n --arg pluginDir \"${CMS_PLUGIN_DIR}\" \\\n --arg endpoint \"${HICLAW_CMS_ENDPOINT}\" \\\n --arg licenseKey \"${CMS_LICENSE_KEY}\" \\\n --arg armsProject \"${CMS_PROJECT}\" \\\n --arg cmsWorkspace \"${HICLAW_CMS_WORKSPACE}\" \\\n --arg serviceName \"${CMS_SERVICE_NAME}\" \\\n --arg diagPluginName \"${DIAG_PLUGIN_NAME}\" \\\n --arg diagPluginDir \"${DIAG_PLUGIN_DIR}\" \\\n --arg metricsRaw \"${CMS_ENABLE_METRICS}\" \\\n --arg diagAvailableRaw \"${DIAG_AVAILABLE}\" \\\n '\n .plugins = (.plugins // {})\n | .plugins.load = (.plugins.load // {})\n | .plugins.entries = (.plugins.entries // {})\n | if (.plugins.allow | type) != \"array\" then .plugins.allow = [] else . end\n | if (.plugins.allow | index($pluginName)) == null then .plugins.allow += [$pluginName] else . end\n | if (.plugins.load.paths | type) != \"array\" then .plugins.load.paths = [] else . end\n | if (.plugins.load.paths | index($pluginDir)) == null then .plugins.load.paths += [$pluginDir] else . end\n | .plugins.entries[$pluginName] = {\n \"enabled\": true,\n \"config\": {\n \"endpoint\": $endpoint,\n \"headers\": {\n \"x-arms-license-key\": $licenseKey,\n \"x-arms-project\": $armsProject,\n \"x-cms-workspace\": $cmsWorkspace\n },\n \"serviceName\": $serviceName\n }\n }\n\n # diagnostics-otel metrics (optional)\n | ($metricsRaw | ascii_downcase) as $m\n | ($diagAvailableRaw == \"1\") as $diagAvailable\n | (($m == \"true\") and $diagAvailable) as $metricsEnabled\n | if $metricsEnabled then\n (if (.plugins.allow | index($diagPluginName)) == null then .plugins.allow += [$diagPluginName] else . end)\n | (if (.plugins.load.paths | index($diagPluginDir)) == null then .plugins.load.paths += [$diagPluginDir] else . end)\n | .plugins.entries[$diagPluginName].enabled = true\n | .diagnostics = (.diagnostics // {})\n | .diagnostics.otel = (.diagnostics.otel // {})\n | .diagnostics.enabled = true\n | .diagnostics.otel.enabled = true\n | .diagnostics.otel.endpoint = $endpoint\n | .diagnostics.otel.protocol = (.diagnostics.otel.protocol // \"http/protobuf\")\n | .diagnostics.otel.headers = {\n \"x-arms-license-key\": $licenseKey,\n \"x-arms-project\": $armsProject,\n \"x-cms-workspace\": $cmsWorkspace\n }\n | .diagnostics.otel.serviceName = $serviceName\n | .diagnostics.otel.metrics = true\n | .diagnostics.otel.traces = (.diagnostics.otel.traces // false)\n | .diagnostics.otel.logs = (.diagnostics.otel.logs // false)\n else\n .\n end\n ' /root/manager-workspace/openclaw.json > /tmp/openclaw-cms.json && \\\n mv /tmp/openclaw-cms.json /root/manager-workspace/openclaw.json\n log \"${CMS_PLUGIN_NAME} config applied (metrics=${CMS_ENABLE_METRICS}, service=${CMS_SERVICE_NAME})\"\n else\n log \"Skipping ${CMS_PLUGIN_NAME} config due to missing required env vars\"\n fi\n fi\nfi\n\n# ============================================================\n# Detect container runtime (for Worker creation)\n# ============================================================\nsource /opt/hiclaw/scripts/lib/container-api.sh\nif container_api_available; then\n log \"Container runtime socket detected at ${CONTAINER_SOCKET} — direct Worker creation enabled\"\n export HICLAW_CONTAINER_RUNTIME=\"socket\"\nelif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ] || [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n log \"Cloud/K8s mode — Workers created via controller API\"\n export HICLAW_CONTAINER_RUNTIME=\"cloud\"\nelse\n log \"No container runtime found — Worker creation will output install commands\"\n export HICLAW_CONTAINER_RUNTIME=\"none\"\nfi\n\n# ============================================================\n# Upgrade Worker openclaw.json: merge known models + E2EE flag into existing configs\n# Existing workers in MinIO may have old single-model configs or missing encryption field.\n# Merge template models so they can hot-switch without restart.\n# ============================================================\nREGISTRY_FILE=\"/root/manager-workspace/workers-registry.json\"\nif [ -f \"${REGISTRY_FILE}\" ]; then\n # Use known-models.json (valid JSON) instead of template (contains ${VAR} placeholders)\n KNOWN_MODELS_FILE=\"/opt/hiclaw/configs/known-models.json\"\n if [ -f \"${KNOWN_MODELS_FILE}\" ]; then\n _KNOWN_MODELS=$(cat \"${KNOWN_MODELS_FILE}\")\n for _wname in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_wname}\" ] && continue\n _minio_path=\"${HICLAW_STORAGE_PREFIX}/agents/${_wname}/openclaw.json\"\n _tmp_in=\"/tmp/openclaw-${_wname}-models-upgrade-in.json\"\n if mc cp \"${_minio_path}\" \"${_tmp_in}\" 2>/dev/null; then\n _tmp_out=\"/tmp/openclaw-${_wname}-models-upgrade-out.json\"\n # Idempotent merge: add missing known models, rebuild aliases, set e2ee.\n # Always runs — jq deduplicates by model id, so re-runs are safe.\n jq --argjson known_models \"${_KNOWN_MODELS}\" \\\n --argjson e2ee \"${MATRIX_E2EE_ENABLED}\" '\n .models.providers[\"hiclaw-gateway\"].models as $existing\n | ($existing | map(.id)) as $existing_ids\n | ($known_models | map(select(.id as $id | $existing_ids | index($id) | not))) as $new\n | .models.providers[\"hiclaw-gateway\"].models = ($existing + $new)\n | (.models.providers[\"hiclaw-gateway\"].models | map({ (\"hiclaw-gateway/\" + .id): { \"alias\": .id } }) | add // {}) as $aliases\n | .agents.defaults.models = ((.agents.defaults.models // {}) + $aliases)\n | .channels.matrix.encryption = $e2ee\n | .channels.matrix.autoJoin = \"always\"\n ' \"${_tmp_in}\" > \"${_tmp_out}\" 2>/dev/null\n if ! diff -q \"${_tmp_in}\" \"${_tmp_out}\" > /dev/null 2>&1; then\n if mc cp \"${_tmp_out}\" \"${_minio_path}\" 2>/dev/null; then\n _new_count=$(jq '.models.providers[\"hiclaw-gateway\"].models | length' \"${_tmp_out}\" 2>/dev/null)\n log \"Worker ${_wname}: upgraded openclaw.json (models: ${_new_count}, e2ee: ${MATRIX_E2EE_ENABLED})\"\n fi\n fi\n rm -f \"${_tmp_in}\" \"${_tmp_out}\"\n fi\n done\n fi\nfi\n\n# ============================================================\n# Ensure Worker Matrix password files exist in MinIO (E2EE fix)\n# Workers need to re-login on restart to get a fresh device_id.\n# Older workers created before this fix won't have the password file.\n# ============================================================\nif [ -f \"${REGISTRY_FILE}\" ]; then\n for _wname in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_wname}\" ] && continue\n _creds_file=\"/data/worker-creds/${_wname}.env\"\n if [ -f \"${_creds_file}\" ]; then\n # Check if password file already exists in MinIO\n if ! mc stat \"${HICLAW_STORAGE_PREFIX}/agents/${_wname}/credentials/matrix/password\" > /dev/null 2>&1; then\n source \"${_creds_file}\"\n if [ -n \"${WORKER_PASSWORD}\" ]; then\n _tmp_pw=\"/tmp/matrix-pw-${_wname}\"\n echo -n \"${WORKER_PASSWORD}\" > \"${_tmp_pw}\"\n mc cp \"${_tmp_pw}\" \"${HICLAW_STORAGE_PREFIX}/agents/${_wname}/credentials/matrix/password\" 2>/dev/null \\\n && log \"Worker ${_wname}: wrote Matrix password to MinIO (E2EE re-login fix)\" \\\n || log \"Worker ${_wname}: WARNING: failed to write Matrix password to MinIO\"\n rm -f \"${_tmp_pw}\"\n fi\n fi\n fi\n done\nfi\n\n# ============================================================\n# Recreate Worker containers as needed after Manager restart.\n# Workers are on hiclaw-net; Docker DNS resolves *-local.hiclaw.io via\n# the Manager's network aliases, so IP changes don't require worker recreation.\n# Only recreate stopped/missing workers.\n# ============================================================\nif container_api_available; then\n REGISTRY_FILE=\"/root/manager-workspace/workers-registry.json\"\n if [ -f \"${REGISTRY_FILE}\" ]; then\n for _worker_name in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_worker_name}\" ] && continue\n\n # Skip remote workers — they are not Manager-managed containers.\n _deployment=$(jq -r --arg w \"${_worker_name}\" '.workers[$w].deployment // \"local\"' \"${REGISTRY_FILE}\" 2>/dev/null)\n if [ \"${_deployment}\" = \"remote\" ]; then\n log \"Worker ${_worker_name} is remote, skipping container recreate\"\n continue\n fi\n\n _status=$(container_status_worker \"${_worker_name}\")\n if [ \"${_status}\" = \"running\" ]; then\n log \"Worker running: ${_worker_name}, skipping\"\n continue\n fi\n # Container missing or stopped — recreate.\n log \"Worker container ${_status}: ${_worker_name}, recreating...\"\n _creds_file=\"/data/worker-creds/${_worker_name}.env\"\n if [ -f \"${_creds_file}\" ]; then\n source \"${_creds_file}\"\n _runtime=$(jq -r --arg w \"${_worker_name}\" '.workers[$w].runtime // \"openclaw\"' \"${REGISTRY_FILE}\" 2>/dev/null)\n _recreated=false\n for _attempt in 1 2 3; do\n local _env_map _create_body\n _env_map=$(jq -cn \\\n --arg name \"${_worker_name}\" \\\n --arg fak \"${_worker_name}\" \\\n --arg fsk \"${WORKER_MINIO_PASSWORD:-}\" \\\n --arg fs_domain \"${HICLAW_FS_DOMAIN:-fs-local.hiclaw.io}\" \\\n --arg controller_url \"${HICLAW_CONTROLLER_URL:-}\" \\\n '{\n \"HICLAW_WORKER_NAME\": $name,\n \"HICLAW_FS_ENDPOINT\": (\"http://\" + ($fs_domain | split(\":\")[0]) + \":8080\"),\n \"HICLAW_FS_ACCESS_KEY\": $fak,\n \"HICLAW_FS_SECRET_KEY\": $fsk\n }\n | if $controller_url != \"\" then . + {\"HICLAW_CONTROLLER_URL\": $controller_url} else . end')\n _create_body=$(jq -cn --arg name \"${_worker_name}\" --arg runtime \"${_runtime}\" --argjson env \"${_env_map}\" '{name: $name, runtime: $runtime, env: $env}')\n worker_backend_create \"${_create_body}\" > /dev/null 2>&1 && _recreated=true && break\n log \" Attempt ${_attempt}/3 failed for ${_worker_name}, retrying in $((5 * _attempt))s...\"\n sleep $((5 * _attempt))\n done\n if [ \"${_recreated}\" = true ]; then\n log \" Recreated ${_runtime} worker: ${_worker_name}\"\n else\n log \" ERROR: Failed to recreate ${_worker_name} after 3 attempts\"\n fi\n else\n log \" WARNING: No credentials found for ${_worker_name} (${_creds_file} missing), skipping\"\n fi\n done\n fi\nfi\n\n# ============================================================\n# Notify workers of builtin updates if upgrade happened\n# Builtin files (AGENTS.md, skills) are already synced by upgrade-builtins.sh\n#\n# Cooldown: skip notification if the last successful notify was within\n# NOTIFY_COOLDOWN_SECS (default 3600s / 1 hour). This prevents repeated\n# notifications when the Manager crash-loops and re-runs upgrade-builtins\n# on every restart (e.g. IMAGE_VERSION=latest always triggers upgrade).\n# ============================================================\nNOTIFY_COOLDOWN_SECS=\"${HICLAW_NOTIFY_COOLDOWN_SECS:-3600}\"\nNOTIFY_TS_FILE=\"/root/manager-workspace/.last-worker-notify-ts\"\n\nif [ -f /root/manager-workspace/.upgrade-pending-worker-notify ]; then\n _now=$(date +%s)\n _last_notify=$(cat \"${NOTIFY_TS_FILE}\" 2>/dev/null || echo \"0\")\n _elapsed=$(( _now - _last_notify ))\n\n if [ \"${_elapsed}\" -lt \"${NOTIFY_COOLDOWN_SECS}\" ]; then\n log \"Skipping worker builtin notification (last notify ${_elapsed}s ago, cooldown ${NOTIFY_COOLDOWN_SECS}s)\"\n rm -f /root/manager-workspace/.upgrade-pending-worker-notify\n else\n log \"Notifying workers about builtin updates...\"\n REGISTRY_FILE=\"/root/manager-workspace/workers-registry.json\"\n _notify_ok=false\n if [ -f \"${REGISTRY_FILE}\" ]; then\n for _worker_name in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_worker_name}\" ] && continue\n _room_id=$(jq -r --arg w \"${_worker_name}\" '.workers[$w].room_id // empty' \"${REGISTRY_FILE}\" 2>/dev/null)\n if [ -n \"${_room_id}\" ]; then\n _worker_id=\"@${_worker_name}:${MATRIX_DOMAIN}\"\n _txn_id=\"upgrade-$(date +%s%N)\"\n _msg=\"@${_worker_name}:${MATRIX_DOMAIN} Manager upgraded builtin files (AGENTS.md, skills). Please use your file-sync skill to sync the latest config.\"\n _raw=$(curl -s -w '\\nHTTP_CODE:%{http_code}' -X PUT \\\n \"${HICLAW_MATRIX_URL}/_matrix/client/v3/rooms/${_room_id}/send/m.room.message/${_txn_id}\" \\\n -H \"Authorization: Bearer ${MANAGER_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d \"{\\\"msgtype\\\":\\\"m.text\\\",\\\"body\\\":\\\"${_msg}\\\",\\\"m.mentions\\\":{\\\"user_ids\\\":[\\\"${_worker_id}\\\"]}}\" \\\n 2>&1) || true\n _http_code=$(echo \"${_raw}\" | tail -1 | sed 's/HTTP_CODE://')\n _notify_resp=$(echo \"${_raw}\" | sed '$d')\n if echo \"${_notify_resp}\" | jq -e '.event_id' > /dev/null 2>&1; then\n log \" Notified ${_worker_name}\"; _notify_ok=true\n else\n log \" WARNING: Failed to notify ${_worker_name} (HTTP ${_http_code}): ${_notify_resp}\"\n fi\n fi\n done\n fi\n # Record timestamp only if at least one notification succeeded\n if [ \"${_notify_ok}\" = true ]; then\n echo \"${_now}\" > \"${NOTIFY_TS_FILE}\"\n fi\n rm -f /root/manager-workspace/.upgrade-pending-worker-notify\n fi\nfi\n\n# ============================================================\n# Start Manager Agent\n# ============================================================\nlog \"Starting Manager Agent (${MANAGER_RUNTIME})...\"\n\n# HOME is already set to /root/manager-workspace via docker run -e HOME=...\ncd \"${HOME}\"\n\n# Ensure host credential symlinks exist under HOME\nif [ -d \"/host-share\" ]; then\n [ -f \"/host-share/.gitconfig\" ] && ln -sf \"/host-share/.gitconfig\" \"${HOME}/.gitconfig\"\nfi\n\nlog \"HOME=${HOME} (manager-workspace, host-mounted)\"\n\n# ── Render agent doc templates ────────────────────────────────────────────\n# Replace ${VAR} placeholders with actual values so the AI agent reads\n# plain text and never needs to resolve environment variables.\nexport MANAGER_MATRIX_TOKEN MANAGER_TOKEN HIGRESS_COOKIE_FILE\nRENDER=/opt/hiclaw/scripts/lib/render-skills.sh\nlog \"Rendering agent doc templates...\"\n# Manager-owned docs (workspace)\nbash \"$RENDER\" /root/manager-workspace/skills\nbash \"$RENDER\" /root/manager-workspace/skills-alpha\nbash \"$RENDER\" /root/manager-workspace AGENTS.md TOOLS.md HEARTBEAT.md SOUL.md\n# Worker templates (workspace + image) — rendered before push to MinIO\n# so Workers (including remote pip-install) receive plain text\nbash \"$RENDER\" /root/manager-workspace/worker-skills\nbash \"$RENDER\" /root/manager-workspace/worker-agent\nbash \"$RENDER\" /root/manager-workspace/copaw-worker-agent\nbash \"$RENDER\" /root/manager-workspace/hermes-worker-agent\nbash \"$RENDER\" /opt/hiclaw/agent/worker-skills\nbash \"$RENDER\" /opt/hiclaw/agent/worker-agent\nbash \"$RENDER\" /opt/hiclaw/agent/copaw-worker-agent\nbash \"$RENDER\" /opt/hiclaw/agent/hermes-worker-agent\nlog \"Agent doc templates rendered\"\n\n# Cloud mode: start background file sync (workspace ↔ OSS) and initial push\nif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n log \"Syncing initial workspace to OSS...\"\n ensure_mc_credentials\n mc mirror /root/manager-workspace/ \"${HICLAW_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" 2>/dev/null || true\n\n # Local → OSS: change-triggered sync\n (\n while true; do\n CHANGED=$(find /root/manager-workspace/ -type f -newermt \"15 seconds ago\" 2>/dev/null | head -1)\n if [ -n \"${CHANGED}\" ]; then\n ensure_mc_credentials 2>/dev/null || true\n mc mirror /root/manager-workspace/ \"${HICLAW_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" --exclude \".npm/**\" \\\n --exclude \".local/**\" --exclude \".mc/**\" 2>/dev/null || true\n fi\n sleep 10\n done\n ) &\n log \"Local→OSS sync started (PID: $!)\"\n\n # OSS → Local: periodic pull (shared data, agent configs)\n (\n while true; do\n sleep 300\n ensure_mc_credentials 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/shared/\" /root/hiclaw-fs/shared/ --overwrite --newer-than \"5m\" 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/agents/\" /root/hiclaw-fs/agents/ --overwrite --newer-than \"5m\" 2>/dev/null || true\n done\n ) &\n log \"OSS→Local sync started (every 5m, PID: $!)\"\nfi\n\n# K8s mode: start background file sync (workspace ↔ MinIO)\nif [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n log \"Syncing initial workspace to MinIO...\"\n mc mirror /root/manager-workspace/ \"${HICLAW_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" 2>/dev/null || true\n\n # Local → MinIO: change-triggered sync\n (\n while true; do\n CHANGED=$(find /root/manager-workspace/ -type f -newermt \"15 seconds ago\" 2>/dev/null | head -1)\n if [ -n \"${CHANGED}\" ]; then\n mc mirror /root/manager-workspace/ \"${HICLAW_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" --exclude \".npm/**\" \\\n --exclude \".local/**\" --exclude \".mc/**\" 2>/dev/null || true\n fi\n sleep 10\n done\n ) &\n log \"Local→MinIO sync started (PID: $!)\"\n\n # MinIO → Local: periodic pull (shared data, agent configs)\n (\n while true; do\n sleep 300\n mc mirror \"${HICLAW_STORAGE_PREFIX}/shared/\" /root/hiclaw-fs/shared/ --overwrite --newer-than \"5m\" 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/agents/\" /root/hiclaw-fs/agents/ --overwrite --newer-than \"5m\" 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/hiclaw-config/\" /root/hiclaw-fs/hiclaw-config/ --overwrite --newer-than \"15s\" 2>/dev/null || true\n done\n ) &\n log \"MinIO→Local sync started (every 5m, PID: $!)\"\nfi\n\n# ============================================================\n# Auto-generate Manager mcporter config for pre-configured MCP servers\n# If HICLAW_GITHUB_TOKEN was set at install time, setup-higress.sh already\n# configured GitHub MCP on Higress. Run setup-mcp-server.sh now so that\n# config/mcporter.json exists when the Agent starts — no need to ask user for PAT.\n# ============================================================\nif [ -n \"${HICLAW_GITHUB_TOKEN}\" ] && [ \"${HICLAW_RUNTIME}\" != \"aliyun\" ] && [ \"${HICLAW_RUNTIME}\" != \"k8s\" ]; then\n if [ ! -f \"${HOME}/config/mcporter.json\" ]; then\n log \"Auto-generating Manager mcporter config for GitHub MCP (HICLAW_GITHUB_TOKEN set)...\"\n bash /opt/hiclaw/agent/skills/mcp-server-management/scripts/setup-mcp-server.sh \\\n github \"${HICLAW_GITHUB_TOKEN}\" 2>&1 | while IFS= read -r line; do log \" [setup-mcp] ${line}\"; done || \\\n log \"WARNING: setup-mcp-server.sh failed — Agent may need to configure GitHub MCP manually\"\n else\n log \"Manager mcporter config already exists, skipping auto-generate\"\n fi\nfi\n\n# ============================================================\n# Runtime-specific startup\n# ============================================================\nif [ \"${MANAGER_RUNTIME}\" = \"copaw\" ]; then\n # Delegate to CoPaw startup script\n exec /opt/hiclaw/scripts/init/start-copaw-manager.sh\nelse\n # ── OpenClaw Runtime ─────────────────────────────────────────────────────\n log \"Starting OpenClaw Manager...\"\n\n export OPENCLAW_CONFIG_PATH=\"/root/manager-workspace/openclaw.json\"\n\n # Symlink to default OpenClaw config path so CLI commands find the config\n mkdir -p \"${HOME}/.openclaw\"\n ln -sf \"/root/manager-workspace/openclaw.json\" \"${HOME}/.openclaw/openclaw.json\"\n\n # Clean orphaned session write locks (e.g. from SIGKILL or crash before exit handlers)\n # Prevents \"session file locked (timeout 10000ms)\" when PID was reused\n find \"${HOME}/.openclaw/agents\" -name \"*.jsonl.lock\" -delete 2>/dev/null || true\n log \"Cleaned up any orphaned session write locks\"\n\n # Clean Matrix crypto storage (SQLite WAL may be corrupted after unclean shutdown)\n # Crypto state is re-negotiated on startup; losing it only means re-establishing E2EE sessions\n rm -rf \"${HOME}/.openclaw/matrix\" 2>/dev/null || true\n log \"Cleaned Matrix crypto storage (will re-establish E2EE sessions)\"\n\n # Launch OpenClaw\n # Disable full-process respawn so the CLI uses its internal restart loop.\n # Without this, config reload spawns a detached child and exits, then\n # supervisord restarts the CLI — resulting in two gateway processes.\n export OPENCLAW_NO_RESPAWN=1\n\n # Optional matrix-plugin trace logging — when HICLAW_MATRIX_DEBUG=1 is set\n # in the manager environment (propagated by install / supervisord), turn on\n # OPENCLAW_MATRIX_DEBUG so the matrix plugin emits structured INFO-level\n # lifecycle traces (sync.state transitions, room.invite/join, message\n # handler arrival + filter outcomes). Useful for diagnosing \"worker never\n # joined the room\" / \"manager never replied\" hangs without rebuilding the\n # image.\n if [ \"${HICLAW_MATRIX_DEBUG:-}\" = \"1\" ] && [ -z \"${OPENCLAW_MATRIX_DEBUG:-}\" ]; then\n export OPENCLAW_MATRIX_DEBUG=1\n log \"HICLAW_MATRIX_DEBUG=1 detected; OPENCLAW_MATRIX_DEBUG=1 exported for matrix plugin tracing\"\n fi\n\n exec openclaw gateway run --verbose --force\nfi\n"}, "3366dd85143eb61fdc78732f2481a30614147b97": {"t": 1785764752.7122796, "n": 66538, "h": "87b41bdd670c01cc", "c": "#!/bin/bash\n# start-manager-agent.sh - Initialize and start the Manager Agent\n# Supports local (supervisord), cloud (SAE), and K8s (Helm) deployments.\n# In local mode this is the last supervisord component to start (priority 800).\n# In cloud/k8s mode (HICLAW_RUNTIME=aliyun|k8s) this is the container entrypoint.\n#\n# Runtime selection:\n# HICLAW_MANAGER_RUNTIME=openclaw (default) - OpenClaw gateway mode\n# HICLAW_MANAGER_RUNTIME=copaw - CoPaw workspace mode\n\nsource /opt/hiclaw/scripts/lib/hiclaw-env.sh\n\n# ============================================================\n# Runtime selection\n# ============================================================\nMANAGER_RUNTIME=\"${HICLAW_MANAGER_RUNTIME:-openclaw}\"\ncase \"${MANAGER_RUNTIME}\" in\n copaw)\n log \"Manager runtime: CoPaw (Python workspace)\"\n ;;\n *)\n log \"Manager runtime: OpenClaw (Node.js gateway)\"\n MANAGER_RUNTIME=\"openclaw\"\n ;;\nesac\n\n# ============================================================\n# Set timezone from TZ env var\n# ============================================================\nif [ -n \"${TZ}\" ] && [ -f \"/usr/share/zoneinfo/${TZ}\" ]; then\n ln -sf \"/usr/share/zoneinfo/${TZ}\" /etc/localtime\n echo \"${TZ}\" > /etc/timezone\n log \"Timezone set to ${TZ}\"\nfi\n\nexport MATRIX_DOMAIN=\"${HICLAW_MATRIX_DOMAIN:-matrix-local.hiclaw.io:8080}\"\nAI_GATEWAY_DOMAIN=\"${HICLAW_AI_GATEWAY_DOMAIN:-aigw-local.hiclaw.io}\"\n\n# ============================================================\n# YOLO mode promotion\n# ============================================================\n# In embedded mode the controller does not propagate HICLAW_YOLO to the\n# manager container, but installer / test scripts touch a marker file at\n# `${WORKSPACE}/yolo-mode` instead. Promote that marker to the env var so the\n# agent's documented YOLO check (`HICLAW_YOLO=1`) reliably detects it without\n# depending on filesystem lookups during a turn.\nif [ -z \"${HICLAW_YOLO:-}\" ] && [ -f /root/manager-workspace/yolo-mode ]; then\n export HICLAW_YOLO=1\n log \"YOLO mode marker detected at /root/manager-workspace/yolo-mode; HICLAW_YOLO=1 exported\"\nfi\n\n# ============================================================\n# Cloud/K8s mode: validate required environment variables + initial credentials\n# ============================================================\nif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ] || [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n : \"${HICLAW_MATRIX_URL:?HICLAW_MATRIX_URL is required}\"\n : \"${HICLAW_MATRIX_DOMAIN:?HICLAW_MATRIX_DOMAIN is required}\"\n : \"${HICLAW_AI_GATEWAY_URL:?HICLAW_AI_GATEWAY_URL is required}\"\n if [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n : \"${HICLAW_MANAGER_GATEWAY_KEY:?HICLAW_MANAGER_GATEWAY_KEY is required}\"\n : \"${HICLAW_MANAGER_PASSWORD:?HICLAW_MANAGER_PASSWORD is required (cloud containers are stateless, password must be injected)}\"\n fi\n if [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n # K8s mode: controller handles initialization (admin registration, Higress setup).\n # Manager only needs credentials injected by the ManagerReconciler.\n : \"${HICLAW_MANAGER_GATEWAY_KEY:?HICLAW_MANAGER_GATEWAY_KEY is required (injected by controller)}\"\n : \"${HICLAW_MANAGER_PASSWORD:?HICLAW_MANAGER_PASSWORD is required (injected by controller)}\"\n else\n # Cloud (aliyun) mode: Manager still does its own initialization\n : \"${HICLAW_REGISTRATION_TOKEN:?HICLAW_REGISTRATION_TOKEN is required}\"\n : \"${HICLAW_ADMIN_USER:?HICLAW_ADMIN_USER is required}\"\n : \"${HICLAW_ADMIN_PASSWORD:?HICLAW_ADMIN_PASSWORD is required}\"\n fi\n log \"${HICLAW_RUNTIME} mode: validating environment... OK\"\n log \" Matrix: ${HICLAW_MATRIX_URL}, AI Gateway: ${HICLAW_AI_GATEWAY_URL}, Storage: ${HICLAW_FS_BUCKET}\"\n if [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n ensure_mc_credentials || { log \"FATAL: Initial STS credential fetch failed\"; exit 1; }\n fi\nfi\n\n# ============================================================\n# Local mode: host symlinks, /etc/hosts, wait for local services\n# ============================================================\nif [ \"${HICLAW_RUNTIME}\" != \"aliyun\" ] && [ \"${HICLAW_RUNTIME}\" != \"k8s\" ]; then\n # Create symlink for host directory access\n if [ -d \"/host-share\" ]; then\n ORIGINAL_HOST_HOME=\"${HOST_ORIGINAL_HOME:-$HOME}\"\n if [ ! -e \"${ORIGINAL_HOST_HOME}\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/root\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/data\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/host-share\" ]; then\n mkdir -p \"$(dirname \"${ORIGINAL_HOST_HOME}\")\"\n ln -sfn /host-share \"${ORIGINAL_HOST_HOME}\"\n log \"Created symlink: ${ORIGINAL_HOST_HOME} -> /host-share\"\n else\n ln -sfn /host-share /root/host-home\n log \"Created fallback symlink: /root/host-home -> /host-share\"\n fi\n fi\n\n # Add local domains to /etc/hosts\n HOSTS_DOMAINS=\"${MATRIX_DOMAIN%%:*} ${HICLAW_MATRIX_CLIENT_DOMAIN:-matrix-client-local.hiclaw.io} ${AI_GATEWAY_DOMAIN} ${HICLAW_FS_DOMAIN:-fs-local.hiclaw.io}\"\n if ! grep -q \"${AI_GATEWAY_DOMAIN}\" /etc/hosts 2>/dev/null; then\n echo \"127.0.0.1 ${HOSTS_DOMAINS}\" >> /etc/hosts\n log \"Added local domains to /etc/hosts\"\n fi\n\n # Wait for local infrastructure\n waitForService \"Higress Gateway\" \"127.0.0.1\" 8080 180\n waitForService \"Higress Console\" \"127.0.0.1\" 8001 180\n waitForService \"Tuwunel\" \"127.0.0.1\" 6167 120\n waitForHTTP \"Tuwunel Matrix API\" \"${HICLAW_MATRIX_URL}/_tuwunel/server_version\" 120\n waitForService \"MinIO\" \"127.0.0.1\" 9000 120\nelse\n # Cloud/K8s mode: wait for external Tuwunel\n log \"Waiting for Tuwunel Matrix server at ${HICLAW_MATRIX_URL}...\"\n _retry=0\n while [ \"${_retry}\" -lt 30 ]; do\n if curl -sf \"${HICLAW_MATRIX_URL}/_matrix/client/versions\" > /dev/null 2>&1; then\n log \"Tuwunel is ready\"\n break\n fi\n _retry=$((_retry + 1))\n log \" Waiting for Tuwunel (attempt ${_retry}/30)...\"\n sleep 5\n done\n if [ \"${_retry}\" -ge 30 ]; then\n log \"ERROR: Tuwunel not reachable at ${HICLAW_MATRIX_URL}\"\n exit 1\n fi\nfi\n\n# ============================================================\n# Auto-generate secrets if not provided via environment\n# Persisted to /data so they survive container restart\n# ============================================================\nSECRETS_FILE=\"/data/hiclaw-secrets.env\"\nif [ -f \"${SECRETS_FILE}\" ]; then\n source \"${SECRETS_FILE}\"\n log \"Loaded persisted secrets from ${SECRETS_FILE}\"\nfi\n\nif [ -z \"${HICLAW_MANAGER_GATEWAY_KEY}\" ]; then\n export HICLAW_MANAGER_GATEWAY_KEY=\"$(generateKey 32)\"\n log \"Auto-generated HICLAW_MANAGER_GATEWAY_KEY\"\nfi\nif [ -z \"${HICLAW_MANAGER_PASSWORD}\" ]; then\n export HICLAW_MANAGER_PASSWORD=\"$(generateKey 16)\"\n log \"Auto-generated HICLAW_MANAGER_PASSWORD\"\nfi\n\n# Persist secrets so they survive supervisord restart\nmkdir -p /data\ncat > \"${SECRETS_FILE}\" <<EOF\nexport HICLAW_MANAGER_GATEWAY_KEY=\"${HICLAW_MANAGER_GATEWAY_KEY}\"\nexport HICLAW_MANAGER_PASSWORD=\"${HICLAW_MANAGER_PASSWORD}\"\nEOF\nchmod 600 \"${SECRETS_FILE}\"\n\n# Cloud mode: pull workspace from OSS before initialization\nif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n HICLAW_FS=\"/root/hiclaw-fs\"\n mkdir -p \"${HICLAW_FS}/shared\" \"${HICLAW_FS}/agents\"\n log \"Pulling workspace from OSS...\"\n ensure_mc_credentials\n mc mirror \"${HICLAW_STORAGE_PREFIX}/manager/\" /root/manager-workspace/ --overwrite 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/shared/\" \"${HICLAW_FS}/shared/\" --overwrite 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/agents/\" \"${HICLAW_FS}/agents/\" --overwrite 2>/dev/null || true\n # Symlink hiclaw-fs into workspace for agent access\n ln -sfn \"${HICLAW_FS}\" /root/manager-workspace/hiclaw-fs\nfi\n\n# K8s mode: sync workspace from cluster-internal MinIO\nif [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n HICLAW_FS=\"/root/hiclaw-fs\"\n mkdir -p \"${HICLAW_FS}/shared\" \"${HICLAW_FS}/agents\" \"${HICLAW_FS}/hiclaw-config\"\n log \"Configuring mc alias for cluster MinIO...\"\n mc alias set hiclaw \"${HICLAW_FS_ENDPOINT}\" \"${HICLAW_FS_ACCESS_KEY}\" \"${HICLAW_FS_SECRET_KEY}\"\n log \"Syncing workspace from MinIO...\"\n mc mirror \"${HICLAW_STORAGE_PREFIX}/manager/\" /root/manager-workspace/ --overwrite 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/\" \"${HICLAW_FS}/\" --overwrite 2>/dev/null || true\n ln -sfn \"${HICLAW_FS}\" /root/manager-workspace/hiclaw-fs\n touch \"${HICLAW_FS}/.initialized\"\nfi\n\n# ============================================================\n# Initialize / upgrade Manager workspace\n# First boot: full init via upgrade-builtins.sh\n# Subsequent boots: compare image version; upgrade only if changed\n# ============================================================\nmkdir -p /root/manager-workspace\n\nIMAGE_VERSION=$(cat /opt/hiclaw/agent/.builtin-version 2>/dev/null || echo \"unknown\")\nINSTALLED_VERSION=$(cat /root/manager-workspace/.builtin-version 2>/dev/null || echo \"\")\n\nif [ ! -f /root/manager-workspace/.initialized ]; then\n log \"First boot: initializing manager workspace...\"\n bash /opt/hiclaw/scripts/init/upgrade-builtins.sh\n touch /root/manager-workspace/.initialized\n log \"Manager workspace initialized (version: ${IMAGE_VERSION})\"\nelif [ \"${IMAGE_VERSION}\" != \"${INSTALLED_VERSION}\" ] || [ \"${IMAGE_VERSION}\" = \"latest\" ]; then\n log \"Upgrade detected: ${INSTALLED_VERSION} -> ${IMAGE_VERSION}${IMAGE_VERSION:+ (latest: always upgrade)}\"\n bash /opt/hiclaw/scripts/init/upgrade-builtins.sh\n log \"Manager workspace upgraded to version: ${IMAGE_VERSION}\"\nelse\n log \"Workspace up to date (version: ${IMAGE_VERSION})\"\nfi\n\n# Local mode: wait for mc mirror initialization (shared + worker data in /root/hiclaw-fs/)\nif [ \"${HICLAW_RUNTIME}\" != \"aliyun\" ] && [ \"${HICLAW_RUNTIME}\" != \"k8s\" ]; then\n log \"Waiting for MinIO storage initialization...\"\n _minio_wait=0\n while [ ! -f /root/hiclaw-fs/.initialized ]; do\n sleep 2\n _minio_wait=$(( _minio_wait + 1 ))\n if [ \"${_minio_wait}\" -ge 60 ]; then\n log \"ERROR: MinIO storage initialization timed out after 120s\"\n exit 1\n fi\n done\n log \"MinIO storage initialized\"\nfi\n\n# ============================================================\n# Register Matrix users via Registration API (single-step, no UIAA)\n# K8s mode: skip — controller Initializer + ManagerReconciler already did this\n# ============================================================\nif [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n log \"K8s mode: skipping Matrix registration (handled by controller)\"\n # Controller injects HICLAW_MANAGER_PASSWORD via env; login to get token\n log \"Obtaining Manager Matrix access token...\"\n _LOGIN_RESPONSE=$(curl -s -X POST ${HICLAW_MATRIX_URL}/_matrix/client/v3/login \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"type\": \"m.login.password\",\n \"identifier\": {\"type\": \"m.id.user\", \"user\": \"manager\"},\n \"password\": \"'\"${HICLAW_MANAGER_PASSWORD}\"'\"\n }' 2>&1)\n MANAGER_TOKEN=$(echo \"${_LOGIN_RESPONSE}\" | jq -r '.access_token' 2>/dev/null)\n if [ -z \"${MANAGER_TOKEN}\" ] || [ \"${MANAGER_TOKEN}\" = \"null\" ]; then\n log \"ERROR: Failed to obtain Manager Matrix token\"\n log \"ERROR: Login response was: ${_LOGIN_RESPONSE}\"\n exit 1\n fi\n log \"Manager Matrix token obtained (token prefix: ${MANAGER_TOKEN:0:10}...)\"\nelse\nlog \"Registering human admin Matrix account...\"\ncurl -sf -X POST ${HICLAW_MATRIX_URL}/_matrix/client/v3/register \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"username\": \"'\"${HICLAW_ADMIN_USER}\"'\",\n \"password\": \"'\"${HICLAW_ADMIN_PASSWORD}\"'\",\n \"auth\": {\n \"type\": \"m.login.registration_token\",\n \"token\": \"'\"${HICLAW_REGISTRATION_TOKEN}\"'\"\n }\n }' > /dev/null 2>&1 || log \"Admin account may already exist\"\n\nlog \"Registering Manager Agent Matrix account...\"\ncurl -sf -X POST ${HICLAW_MATRIX_URL}/_matrix/client/v3/register \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"username\": \"manager\",\n \"password\": \"'\"${HICLAW_MANAGER_PASSWORD}\"'\",\n \"auth\": {\n \"type\": \"m.login.registration_token\",\n \"token\": \"'\"${HICLAW_REGISTRATION_TOKEN}\"'\"\n }\n }' > /dev/null 2>&1 || log \"Manager account may already exist\"\n\n# Get Manager Agent's Matrix access token\nlog \"Obtaining Manager Matrix access token...\"\n_LOGIN_RESPONSE=$(curl -s -X POST ${HICLAW_MATRIX_URL}/_matrix/client/v3/login \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"type\": \"m.login.password\",\n \"identifier\": {\"type\": \"m.id.user\", \"user\": \"manager\"},\n \"password\": \"'\"${HICLAW_MANAGER_PASSWORD}\"'\"\n }' 2>&1)\n_LOGIN_EXIT=$?\nlog \"Matrix login HTTP exit code: ${_LOGIN_EXIT}\"\nlog \"Matrix login response: ${_LOGIN_RESPONSE}\"\n\nMANAGER_TOKEN=$(echo \"${_LOGIN_RESPONSE}\" | jq -r '.access_token' 2>/dev/null)\n\nif [ -z \"${MANAGER_TOKEN}\" ] || [ \"${MANAGER_TOKEN}\" = \"null\" ]; then\n log \"ERROR: Failed to obtain Manager Matrix token (exit=${_LOGIN_EXIT})\"\n log \"ERROR: Login response was: ${_LOGIN_RESPONSE}\"\n exit 1\nfi\nlog \"Manager Matrix token obtained (token prefix: ${MANAGER_TOKEN:0:10}...)\"\nfi\n\n# ============================================================\n# Higress Console initialization\n# Docker mode: full setup-higress.sh (internal Higress at localhost:8001)\n# K8s mode: skip — controller Initializer handles Higress setup\n# Cloud (aliyun) mode: skip entirely (Higress managed externally)\n# ============================================================\n_HIGRESS_CONSOLE_URL=\"\"\n_HIGRESS_USER=\"${HICLAW_ADMIN_USER}\"\n_HIGRESS_PASS=\"${HICLAW_ADMIN_PASSWORD}\"\nif [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n log \"K8s mode: skipping Higress initialization (handled by controller)\"\nelif [ \"${HICLAW_RUNTIME}\" != \"aliyun\" ]; then\n _HIGRESS_CONSOLE_URL=\"http://127.0.0.1:8001\"\nfi\n\nif [ -n \"${_HIGRESS_CONSOLE_URL}\" ]; then\n COOKIE_FILE=\"/tmp/higress-session-cookie\"\n\n log \"Waiting for Higress Console (${_HIGRESS_CONSOLE_URL}) to be fully ready and initializing admin...\"\n INIT_DONE=false\n for i in $(seq 1 90); do\n INIT_RESULT=$(curl -s -X POST \"${_HIGRESS_CONSOLE_URL}/system/init\" \\\n -H 'Content-Type: application/json' \\\n -d '{\"adminUser\":{\"name\":\"'\"${_HIGRESS_USER}\"'\",\"password\":\"'\"${_HIGRESS_PASS}\"'\",\"displayName\":\"'\"${_HIGRESS_USER}\"'\"}}' 2>/dev/null) || true\n if echo \"${INIT_RESULT}\" | grep -qE '\"success\":true|already.?init' 2>/dev/null; then\n INIT_DONE=true\n break\n fi\n if echo \"${INIT_RESULT}\" | grep -q '\"name\"' 2>/dev/null; then\n INIT_DONE=true\n break\n fi\n sleep 2\n done\n\n if [ \"${INIT_DONE}\" != \"true\" ]; then\n log \"ERROR: Higress Console did not become ready within 180s\"\n exit 1\n fi\n log \"Higress Console init done\"\n\n log \"Logging into Higress Console...\"\n LOGIN_OK=false\n for i in $(seq 1 10); do\n HTTP_CODE=$(curl -s -o /dev/null -w '%{http_code}' -X POST \"${_HIGRESS_CONSOLE_URL}/session/login\" \\\n -H 'Content-Type: application/json' \\\n -c \"${COOKIE_FILE}\" \\\n -d '{\"username\":\"'\"${_HIGRESS_USER}\"'\",\"password\":\"'\"${_HIGRESS_PASS}\"'\"}' 2>/dev/null) || true\n if { [ \"${HTTP_CODE}\" = \"200\" ] || [ \"${HTTP_CODE}\" = \"201\" ]; } && [ -f \"${COOKIE_FILE}\" ] && [ -s \"${COOKIE_FILE}\" ]; then\n LOGIN_OK=true\n break\n fi\n log \"Login attempt $i (HTTP ${HTTP_CODE}), retrying in 3s...\"\n sleep 3\n done\n\n if [ \"${LOGIN_OK}\" != \"true\" ]; then\n log \"ERROR: Could not login to Higress Console after retries\"\n exit 1\n fi\n log \"Higress Console login successful\"\n\n VERIFY_CODE=$(curl -s -o /dev/null -w '%{http_code}' \"${_HIGRESS_CONSOLE_URL}/v1/consumers\" -b \"${COOKIE_FILE}\" 2>/dev/null) || true\n if [ \"${VERIFY_CODE}\" = \"200\" ]; then\n log \"Console session verified (cookie valid)\"\n else\n log \"WARNING: Console session may be invalid (verify returned HTTP ${VERIFY_CODE})\"\n rm -f \"${COOKIE_FILE}\"\n for i in $(seq 1 5); do\n curl -s -o /dev/null -w '%{http_code}' -X POST \"${_HIGRESS_CONSOLE_URL}/session/login\" \\\n -H 'Content-Type: application/json' \\\n -c \"${COOKIE_FILE}\" \\\n -d '{\"username\":\"'\"${_HIGRESS_USER}\"'\",\"password\":\"'\"${_HIGRESS_PASS}\"'\"}' 2>/dev/null\n VERIFY2=$(curl -s -o /dev/null -w '%{http_code}' \"${_HIGRESS_CONSOLE_URL}/v1/consumers\" -b \"${COOKIE_FILE}\" 2>/dev/null) || true\n if [ \"${VERIFY2}\" = \"200\" ]; then\n log \"Re-login successful, session verified\"\n break\n fi\n sleep 2\n done\n fi\n\n export HIGRESS_COOKIE_FILE=\"${COOKIE_FILE}\"\n export HIGRESS_CONSOLE_URL=\"${_HIGRESS_CONSOLE_URL}\"\n\n if [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n # K8s mode: lightweight Higress config — only what's needed for LLM access\n source /opt/hiclaw/scripts/lib/base.sh\n _k8s_higress_api() {\n local method=\"$1\" path=\"$2\" desc=\"$3\"; shift 3; local body=\"$*\"\n local tmpfile; tmpfile=$(mktemp)\n local http_code\n http_code=$(curl -s -o \"${tmpfile}\" -w '%{http_code}' -X \"${method}\" \"${_HIGRESS_CONSOLE_URL}${path}\" \\\n -b \"${COOKIE_FILE}\" -H 'Content-Type: application/json' -d \"${body}\" 2>/dev/null) || true\n local response; response=$(cat \"${tmpfile}\" 2>/dev/null); rm -f \"${tmpfile}\"\n if echo \"${response}\" | grep -q '\"success\":true' 2>/dev/null; then\n log \"${desc} ... OK\"\n elif [ \"${http_code}\" = \"409\" ]; then\n log \"${desc} ... already exists, skipping\"\n elif [ \"${http_code}\" = \"200\" ] || [ \"${http_code}\" = \"201\" ] || [ \"${http_code}\" = \"204\" ]; then\n log \"${desc} ... OK (HTTP ${http_code})\"\n else\n log \"WARNING: ${desc} ... (HTTP ${http_code}): ${response}\"\n fi\n }\n\n # 1. Service Sources (DNS type → K8s Service FQDN)\n # Extract host:port from URLs for Higress service source registration\n _TUWUNEL_HOST=$(echo \"${HICLAW_MATRIX_URL}\" | sed 's|^http[s]*://||')\n _TUWUNEL_DOMAIN=$(echo \"${_TUWUNEL_HOST}\" | cut -d: -f1)\n _TUWUNEL_PORT=$(echo \"${_TUWUNEL_HOST}\" | cut -d: -f2)\n _k8s_higress_api POST /v1/service-sources \"Registering Tuwunel service source\" \\\n '{\"type\":\"dns\",\"name\":\"tuwunel\",\"domain\":\"'\"${_TUWUNEL_DOMAIN}\"'\",\"port\":'\"${_TUWUNEL_PORT}\"'}'\n\n if [ -n \"${HICLAW_ELEMENT_WEB_URL:-}\" ]; then\n _ELEMENT_HOST=$(echo \"${HICLAW_ELEMENT_WEB_URL}\" | sed 's|^http[s]*://||')\n _ELEMENT_DOMAIN=$(echo \"${_ELEMENT_HOST}\" | cut -d: -f1)\n _ELEMENT_PORT=$(echo \"${_ELEMENT_HOST}\" | cut -d: -f2)\n _k8s_higress_api POST /v1/service-sources \"Registering Element Web service source\" \\\n '{\"type\":\"dns\",\"name\":\"element-web\",\"domain\":\"'\"${_ELEMENT_DOMAIN}\"'\",\"port\":'\"${_ELEMENT_PORT}\"'}'\n fi\n\n # 2. Manager Consumer (key-auth)\n _k8s_higress_api POST /v1/consumers \"Creating Manager consumer\" \\\n '{\"name\":\"manager\",\"credentials\":[{\"type\":\"key-auth\",\"source\":\"BEARER\",\"values\":[\"'\"${HICLAW_MANAGER_GATEWAY_KEY}\"'\"]}]}'\n\n # 3. LLM Provider\n _LLM_PROVIDER=\"${HICLAW_LLM_PROVIDER:-qwen}\"\n _LLM_API_URL=\"${HICLAW_LLM_API_URL:-}\"\n if [ -z \"${_LLM_API_URL}\" ]; then\n case \"${_LLM_PROVIDER}\" in\n qwen) _LLM_API_URL=\"https://dashscope.aliyuncs.com/compatible-mode/v1\" ;;\n esac\n fi\n # 4. LLM Provider type-specific config\n case \"${_LLM_PROVIDER}\" in\n qwen)\n _k8s_higress_api POST /v1/ai/providers \"Creating LLM provider (qwen)\" \\\n '{\"type\":\"qwen\",\"name\":\"qwen\",\"tokens\":[\"'\"${HICLAW_LLM_API_KEY}\"'\"],\"protocol\":\"openai/v1\",\"tokenFailoverConfig\":{\"enabled\":false},\"rawConfigs\":{\"qwenEnableSearch\":false,\"qwenEnableCompatible\":true,\"qwenFileIds\":[],\"hiclawMode\":true}}'\n ;;\n *)\n _BODY='{\"name\":\"'\"${_LLM_PROVIDER}\"'\",\"type\":\"openai\",\"tokens\":[\"'\"${HICLAW_LLM_API_KEY}\"'\"],\"modelMapping\":{},\"protocol\":\"openai/v1\",\"rawConfigs\":{\"hiclawMode\":true}}'\n _k8s_higress_api POST /v1/ai/providers \"Creating LLM provider (${_LLM_PROVIDER})\" \"${_BODY}\"\n ;;\n esac\n\n # 5. AI Route (bind provider + consumer auth, /v1 prefix to avoid clash with Element Web catch-all)\n _k8s_higress_api POST /v1/ai/routes \"Creating AI Gateway route\" \\\n '{\"name\":\"default-ai-route\",\"domains\":[],\"pathPredicate\":{\"matchType\":\"PRE\",\"matchValue\":\"/v1\",\"caseSensitive\":false},\"upstreams\":[{\"provider\":\"'\"${_LLM_PROVIDER}\"'\",\"weight\":100,\"modelMapping\":{}}],\"authConfig\":{\"enabled\":true,\"allowedCredentialTypes\":[\"key-auth\"],\"allowedConsumers\":[\"manager\"]}}'\n\n # 6. Matrix Homeserver Route (/_matrix/* → Tuwunel, no auth)\n _k8s_higress_api POST /v1/routes \"Creating Matrix Homeserver route\" \\\n '{\"name\":\"matrix-homeserver\",\"domains\":[],\"path\":{\"matchType\":\"PRE\",\"matchValue\":\"/_matrix\"},\"services\":[{\"name\":\"tuwunel.dns\",\"port\":'\"${_TUWUNEL_PORT}\"',\"weight\":100}]}'\n\n # 7. Element Web Route (/ catch-all → Element Web, no auth)\n if [ -n \"${HICLAW_ELEMENT_WEB_URL:-}\" ]; then\n _k8s_higress_api POST /v1/routes \"Creating Element Web route\" \\\n '{\"name\":\"element-web\",\"domains\":[],\"path\":{\"matchType\":\"PRE\",\"matchValue\":\"/\"},\"services\":[{\"name\":\"element-web.dns\",\"port\":'\"${_ELEMENT_PORT}\"',\"weight\":100}]}'\n fi\n\n # 8. Remove Higress default landing page (Exact match on / takes precedence over Element Web catch-all)\n _k8s_higress_api DELETE /v1/routes/default \"Removing Higress default landing route\"\n\n log \"K8s Higress lightweight setup complete\"\n\n # Wait for AI plugin activation (~45s for first config)\n log \"Waiting for AI Gateway plugin activation (45s)...\"\n sleep 45\n else\n # Docker mode: full setup with all routes, domains, MCP servers\n /opt/hiclaw/scripts/init/setup-higress.sh\n fi\nfi\n\n# ============================================================\n# Create admin DM room, persist to state.json, send welcome message\n# K8s mode: skip — controller ProvisionManager already creates the Admin DM room\n# Runs in both local and cloud modes (idempotent)\n# ============================================================\nif [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n log \"K8s mode: skipping admin DM room creation (handled by controller)\"\nelse\nMANAGER_FULL_ID=\"@manager:${MATRIX_DOMAIN}\"\nADMIN_FULL_ID=\"@${HICLAW_ADMIN_USER}:${MATRIX_DOMAIN}\"\n\nlog \"Logging in as admin to create DM room...\"\n_ADMIN_LOGIN=$(curl -sf -X POST \"${HICLAW_MATRIX_URL}/_matrix/client/v3/login\" \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"type\": \"m.login.password\",\n \"identifier\": {\"type\": \"m.id.user\", \"user\": \"'\"${HICLAW_ADMIN_USER}\"'\"},\n \"password\": \"'\"${HICLAW_ADMIN_PASSWORD}\"'\"\n }' 2>&1) || true\n\nADMIN_MATRIX_TOKEN=$(echo \"${_ADMIN_LOGIN}\" | jq -r '.access_token // empty' 2>/dev/null)\nif [ -z \"${ADMIN_MATRIX_TOKEN}\" ]; then\n log \"WARNING: Failed to login as admin, skipping DM room creation\"\nelse\n # Search for existing DM room with Manager (idempotent)\n DM_ROOM_ID=\"\"\n _JOINED_ROOMS=$(curl -sf \"${HICLAW_MATRIX_URL}/_matrix/client/v3/joined_rooms\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" 2>/dev/null \\\n | jq -r '.joined_rooms[]' 2>/dev/null) || true\n for _rid in ${_JOINED_ROOMS}; do\n _members=$(curl -sf \"${HICLAW_MATRIX_URL}/_matrix/client/v3/rooms/${_rid}/members\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" 2>/dev/null \\\n | jq -r '.chunk[].state_key' 2>/dev/null) || continue\n _count=$(echo \"${_members}\" | wc -l | xargs)\n if [ \"${_count}\" = \"2\" ] && echo \"${_members}\" | grep -q \"@manager:\"; then\n DM_ROOM_ID=\"${_rid}\"\n break\n fi\n done\n\n if [ -n \"${DM_ROOM_ID}\" ]; then\n log \"Existing DM room found: ${DM_ROOM_ID}\"\n else\n log \"Creating DM room with Manager...\"\n _RAW=$(curl -s -w '\\nHTTP_CODE:%{http_code}' -X POST \"${HICLAW_MATRIX_URL}/_matrix/client/v3/createRoom\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d \"{\\\"is_direct\\\":true,\\\"invite\\\":[\\\"${MANAGER_FULL_ID}\\\"],\\\"preset\\\":\\\"trusted_private_chat\\\"}\" 2>&1) || true\n _HTTP_CODE=$(echo \"${_RAW}\" | tail -1 | sed 's/HTTP_CODE://')\n _CREATE_RESP=$(echo \"${_RAW}\" | sed '$d')\n DM_ROOM_ID=$(echo \"${_CREATE_RESP}\" | jq -r '.room_id // empty' 2>/dev/null)\n if [ -n \"${DM_ROOM_ID}\" ]; then\n log \"DM room created: ${DM_ROOM_ID}\"\n else\n log \"WARNING: Failed to create DM room (HTTP ${_HTTP_CODE}): ${_CREATE_RESP}\"\n fi\n fi\n\n # Persist admin DM room ID to state.json\n if [ -n \"${DM_ROOM_ID}\" ]; then\n STATE_SCRIPT=\"/opt/hiclaw/agent/skills/task-management/scripts/manage-state.sh\"\n if [ -f \"${STATE_SCRIPT}\" ]; then\n bash \"${STATE_SCRIPT}\" --action init 2>/dev/null || true\n bash \"${STATE_SCRIPT}\" --action set-admin-dm --room-id \"${DM_ROOM_ID}\" 2>/dev/null || true\n log \"Admin DM room persisted to state.json: ${DM_ROOM_ID}\"\n fi\n fi\n\n # Schedule welcome message in background (only on first boot)\n if [ -n \"${DM_ROOM_ID}\" ] && [ ! -f \"/root/manager-workspace/soul-configured\" ]; then\n log \"Scheduling welcome message (background, waiting for OpenClaw to start)...\"\n (\n _HICLAW_LANGUAGE=\"${HICLAW_LANGUAGE:-zh}\"\n _HICLAW_TIMEZONE=\"${TZ:-Asia/Shanghai}\"\n _wait=0\n _ready=false\n while [ \"${_wait}\" -lt 300 ]; do\n if curl -sf http://127.0.0.1:18799/ > /dev/null 2>&1; then\n _ready=true\n break\n fi\n sleep 3\n _wait=$((_wait + 3))\n done\n if [ \"${_ready}\" != \"true\" ]; then\n echo \"[manager] WARNING: OpenClaw gateway not ready within 300s, skipping welcome message\"\n exit 0\n fi\n # Ensure Manager has joined the DM room before sending the welcome\n # message. Without this, there is a race between OpenClaw's Matrix\n # auto-join and the message send — the message may land before Manager\n # joins, so OpenClaw's /sync never picks it up.\n _join_ok=false\n for _join_attempt in 1 2 3; do\n if curl -sf -X POST \"${HICLAW_MATRIX_URL}/_matrix/client/v3/rooms/${DM_ROOM_ID}/join\" \\\n -H \"Authorization: Bearer ${MANAGER_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d '{}' > /dev/null 2>&1; then\n echo \"[manager] Manager joined DM room before welcome message\"\n _join_ok=true\n break\n fi\n sleep 2\n done\n if [ \"${_join_ok}\" != \"true\" ]; then\n echo \"[manager] WARNING: Manager join request failed after 3 attempts (may already be joined)\"\n fi\n _welcome_msg=\"This is an automated message from the HiClaw setup. This is a fresh installation.\n\n--- Installation Context ---\nUser Language: ${_HICLAW_LANGUAGE} (zh = Chinese, en = English)\nUser Timezone: ${_HICLAW_TIMEZONE} (IANA timezone identifier)\n---\n\nYou are an AI agent that manages a team of worker agents. Your identity and personality have not been configured yet — the human admin is about to meet you for the first time.\n\nPlease begin the onboarding conversation:\n\n1. Greet the admin warmly and briefly describe what you can do (coordinate workers, manage tasks, run multi-agent projects)\n2. The user has selected \\\"${_HICLAW_LANGUAGE}\\\" as their preferred language during installation. Use this language for your greeting and all subsequent communication.\n3. The user's timezone is ${_HICLAW_TIMEZONE}. Based on this timezone, you may infer their likely region and suggest additional language options.\n4. Ask them: a) What would they like to call you? b) Communication style preference? c) Any behavior guidelines? d) Confirm default language\n5. After they reply, write their preferences to ~/SOUL.md\n6. Confirm what you wrote, and ask if they would like to adjust anything\n7. Once confirmed, run: touch ~/soul-configured\n\nThe human admin will start chatting shortly.\"\n _txn_id=\"welcome-$(date +%s)\"\n _payload=$(jq -nc --arg body \"${_welcome_msg}\" '{\"msgtype\":\"m.text\",\"body\":$body}')\n _raw=$(curl -s -w '\\nHTTP_CODE:%{http_code}' -X PUT \"${HICLAW_MATRIX_URL}/_matrix/client/v3/rooms/${DM_ROOM_ID}/send/m.room.message/${_txn_id}\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d \"${_payload}\" 2>&1) || true\n _http_code=$(echo \"${_raw}\" | tail -1 | sed 's/HTTP_CODE://')\n _send_resp=$(echo \"${_raw}\" | sed '$d')\n if echo \"${_send_resp}\" | jq -e '.event_id' > /dev/null 2>&1; then\n echo \"[manager] Welcome message sent to DM room\"\n else\n echo \"[manager] WARNING: Failed to send welcome message (HTTP ${_http_code}): ${_send_resp}\"\n fi\n ) &\n log \"Welcome message background process started (PID: $!)\"\n fi\nfi\nfi # end K8s mode skip for admin DM room\n\n# ============================================================\n# Generate Manager Agent openclaw.json from template\n# ============================================================\nlog \"Generating Manager openclaw.json...\"\nexport MANAGER_MATRIX_TOKEN=\"${MANAGER_TOKEN}\"\nexport MANAGER_GATEWAY_KEY=\"${HICLAW_MANAGER_GATEWAY_KEY}\"\n# Resolve model parameters based on model name\nMODEL_NAME=\"${HICLAW_DEFAULT_MODEL:-qwen3.5-plus}\"\ncase \"${MODEL_NAME}\" in\n gpt-5.3-codex|gpt-5-mini|gpt-5-nano)\n export MODEL_CONTEXT_WINDOW=400000 MODEL_MAX_TOKENS=128000 ;;\n claude-opus-4-6)\n export MODEL_CONTEXT_WINDOW=1000000 MODEL_MAX_TOKENS=128000 ;;\n claude-sonnet-4-6)\n export MODEL_CONTEXT_WINDOW=1000000 MODEL_MAX_TOKENS=64000 ;;\n claude-haiku-4-5)\n export MODEL_CONTEXT_WINDOW=200000 MODEL_MAX_TOKENS=64000 ;;\n qwen3.5-plus)\n export MODEL_CONTEXT_WINDOW=200000 MODEL_MAX_TOKENS=64000 ;;\n deepseek-chat|deepseek-reasoner|kimi-k2.5)\n export MODEL_CONTEXT_WINDOW=256000 MODEL_MAX_TOKENS=128000 ;;\n glm-5|MiniMax-M2.7|MiniMax-M2.7-highspeed|MiniMax-M2.5)\n export MODEL_CONTEXT_WINDOW=200000 MODEL_MAX_TOKENS=128000 ;;\n *)\n export MODEL_CONTEXT_WINDOW=150000 MODEL_MAX_TOKENS=128000 ;;\nesac\nexport MODEL_REASONING=true\n\n# Override with user-supplied custom model parameters from env (set during install)\n[ -n \"${HICLAW_MODEL_CONTEXT_WINDOW:-}\" ] && export MODEL_CONTEXT_WINDOW=\"${HICLAW_MODEL_CONTEXT_WINDOW}\"\n[ -n \"${HICLAW_MODEL_MAX_TOKENS:-}\" ] && export MODEL_MAX_TOKENS=\"${HICLAW_MODEL_MAX_TOKENS}\"\n[ -n \"${HICLAW_MODEL_REASONING:-}\" ] && export MODEL_REASONING=\"${HICLAW_MODEL_REASONING}\"\n\n# E2EE: convert HICLAW_MATRIX_E2EE to JSON boolean for template substitution\nif [ \"${HICLAW_MATRIX_E2EE:-0}\" = \"1\" ] || [ \"${HICLAW_MATRIX_E2EE:-}\" = \"true\" ]; then\n export MATRIX_E2EE_ENABLED=true\nelse\n export MATRIX_E2EE_ENABLED=false\nfi\nlog \"Matrix E2EE: ${MATRIX_E2EE_ENABLED}\"\n\n# Resolve input modalities: only vision-capable models get \"image\"\ncase \"${MODEL_NAME}\" in\n gpt-5.4|gpt-5.3-codex|gpt-5-mini|gpt-5-nano|claude-opus-4-6|claude-sonnet-4-6|claude-haiku-4-5|qwen3.5-plus|kimi-k2.5)\n export MODEL_INPUT='[\"text\", \"image\"]' ;;\n *)\n export MODEL_INPUT='[\"text\"]' ;;\nesac\n# Override with user-supplied vision setting from env\nif [ \"${HICLAW_MODEL_VISION:-}\" = \"true\" ]; then\n export MODEL_INPUT='[\"text\", \"image\"]'\nelif [ \"${HICLAW_MODEL_VISION:-}\" = \"false\" ]; then\n export MODEL_INPUT='[\"text\"]'\nfi\n\nlog \"Model: ${MODEL_NAME} (context=${MODEL_CONTEXT_WINDOW}, maxTokens=${MODEL_MAX_TOKENS}, reasoning=${MODEL_REASONING}, input=${MODEL_INPUT})\"\n\nif [ -f /root/manager-workspace/openclaw.json ]; then\n log \"Manager openclaw.json already exists, updating dynamic fields only (preserving user customizations)...\"\n # Merge known models into existing config (add missing, preserve user-added)\n # Use known-models.json (valid JSON) instead of template (contains ${VAR} placeholders)\n KNOWN_MODELS=$(cat /opt/hiclaw/configs/known-models.json 2>/dev/null || echo '[]')\n jq --arg token \"${MANAGER_TOKEN}\" \\\n --arg key \"${HICLAW_MANAGER_GATEWAY_KEY}\" \\\n --arg model \"${MODEL_NAME}\" \\\n --arg emb_model \"${HICLAW_EMBEDDING_MODEL}\" \\\n --arg aigw_domain \"${AI_GATEWAY_DOMAIN}\" \\\n --arg matrix_user_id \"@manager:${MATRIX_DOMAIN}\" \\\n --argjson e2ee \"${MATRIX_E2EE_ENABLED}\" \\\n --argjson known_models \"${KNOWN_MODELS}\" \\\n --argjson ctx \"${MODEL_CONTEXT_WINDOW}\" \\\n --argjson max \"${MODEL_MAX_TOKENS}\" \\\n --argjson reasoning \"${MODEL_REASONING}\" \\\n --argjson input \"${MODEL_INPUT}\" \\\n '\n # Merge known models: add any model id not already present\n .models.providers[\"hiclaw-gateway\"].models as $existing\n | ($existing | map(.id)) as $existing_ids\n | ($known_models | map(select(.id as $id | $existing_ids | index($id) | not))) as $new\n | .models.providers[\"hiclaw-gateway\"].models = ($existing + $new)\n # Ensure the user-chosen default model is in the list (custom model support)\n | if (.models.providers[\"hiclaw-gateway\"].models | map(.id) | index($model) | not) then\n .models.providers[\"hiclaw-gateway\"].models += [{\"id\": $model, \"name\": $model, \"reasoning\": $reasoning, \"contextWindow\": $ctx, \"maxTokens\": $max, \"input\": $input}]\n else . end\n # Rebuild model aliases from the full models list\n | (.models.providers[\"hiclaw-gateway\"].models | map({ (\"hiclaw-gateway/\" + .id): { \"alias\": .id } }) | add // {}) as $aliases\n | .agents.defaults.models = ((.agents.defaults.models // {}) + $aliases)\n | .channels.matrix.accessToken = $token | .channels.matrix.userId = $matrix_user_id | .models.providers[\"hiclaw-gateway\"].apiKey = $key\n | ((.hooks.token // \"\") as $ht | if $ht == $key or $ht == ($key + \"-hooks\" | @base64) then del(.hooks) else . end)\n | .agents.defaults.model.primary = (\"hiclaw-gateway/\" + $model)\n | .commands.restart = true\n | .gateway.port = 18799\n | .gateway.bind = \"lan\"\n | .gateway.controlUi = ((.gateway.controlUi // {}) + {\"dangerouslyDisableDeviceAuth\": true, \"allowInsecureAuth\": true, \"allowedOrigins\": [\"*\"]})\n | .channels.matrix.encryption = $e2ee\n | .channels.matrix.network = ((.channels.matrix.network // {}) + {\"dangerouslyAllowPrivateNetwork\": true})\n | .channels.matrix.autoJoin = \"always\"\n # Ensure memorySearch config exists (embedding model for memory) — skip if embedding model is empty\n | if $emb_model != \"\" then .agents.defaults.memorySearch //= {\"provider\":\"openai\",\"model\":$emb_model,\"remote\":{\"baseUrl\":(\"http://\" + $aigw_domain + \":8080/v1\"),\"apiKey\":$key}} else . end\n ' \\\n /root/manager-workspace/openclaw.json > /tmp/openclaw.json.tmp && \\\n mv /tmp/openclaw.json.tmp /root/manager-workspace/openclaw.json\n # Anti-tampering bypass: openclaw's config-observe-recovery clobbers the file\n # back to .bak whenever it sees \"missing-meta-vs-last-good\", which strips\n # external edits like channels.matrix.network.dangerouslyAllowPrivateNetwork.\n # Stamp meta and mirror to .bak so our edits survive.\n _oc_ver=$(openclaw --version 2>/dev/null | head -1 | awk '{print $NF}')\n [ -z \"${_oc_ver}\" ] && _oc_ver=\"hiclaw-bootstrap\"\n jq --arg ver \"${_oc_ver}\" --arg ts \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\" \\\n '.meta = ((.meta // {}) + {lastTouchedVersion: $ver, lastTouchedAt: $ts})' \\\n /root/manager-workspace/openclaw.json > /tmp/openclaw.meta.json && \\\n mv /tmp/openclaw.meta.json /root/manager-workspace/openclaw.json\n cp -f /root/manager-workspace/openclaw.json /root/manager-workspace/openclaw.json.bak\n # Verify the token was written correctly\n _written_token=$(jq -r '.channels.matrix.accessToken' /root/manager-workspace/openclaw.json 2>/dev/null)\n if [ -z \"${_written_token}\" ] || [ \"${_written_token}\" = \"null\" ]; then\n log \"ERROR: Matrix token was not written correctly to openclaw.json (got: ${_written_token})\"\n else\n log \"Matrix token written to openclaw.json (prefix: ${_written_token:0:10}...)\"\n fi\nelse\n log \"Manager openclaw.json not found, generating from template...\"\n envsubst < /opt/hiclaw/configs/manager-openclaw.json.tmpl > /root/manager-workspace/openclaw.json\n # Post-envsubst injection: memorySearch + custom model (single jq pass when possible)\n if ! jq -e --arg model \"${MODEL_NAME}\" '.models.providers[\"hiclaw-gateway\"].models | map(.id) | index($model)' /root/manager-workspace/openclaw.json > /dev/null 2>&1; then\n log \"Custom model '${MODEL_NAME}' not in built-in list, injecting into config...\"\n jq --arg emb_model \"${HICLAW_EMBEDDING_MODEL}\" \\\n --arg aigw_domain \"${AI_GATEWAY_DOMAIN}\" \\\n --arg key \"${HICLAW_MANAGER_GATEWAY_KEY}\" \\\n --arg model \"${MODEL_NAME}\" \\\n --argjson ctx \"${MODEL_CONTEXT_WINDOW}\" \\\n --argjson max \"${MODEL_MAX_TOKENS}\" \\\n --argjson reasoning \"${MODEL_REASONING}\" \\\n --argjson input \"${MODEL_INPUT}\" \\\n '\n (if $emb_model != \"\" then .agents.defaults.memorySearch = {\"provider\":\"openai\",\"model\":$emb_model,\"remote\":{\"baseUrl\":(\"http://\" + $aigw_domain + \":8080/v1\"),\"apiKey\":$key}} else . end)\n | .models.providers[\"hiclaw-gateway\"].models += [{\"id\": $model, \"name\": $model, \"reasoning\": $reasoning, \"contextWindow\": $ctx, \"maxTokens\": $max, \"input\": $input}]\n | .agents.defaults.models += {(\"hiclaw-gateway/\" + $model): {\"alias\": $model}}\n ' /root/manager-workspace/openclaw.json > /tmp/openclaw.json.tmp && \\\n mv /tmp/openclaw.json.tmp /root/manager-workspace/openclaw.json\n elif [ -n \"${HICLAW_EMBEDDING_MODEL}\" ]; then\n jq --arg emb_model \"${HICLAW_EMBEDDING_MODEL}\" \\\n --arg aigw_domain \"${AI_GATEWAY_DOMAIN}\" \\\n --arg key \"${HICLAW_MANAGER_GATEWAY_KEY}\" \\\n '.agents.defaults.memorySearch = {\"provider\":\"openai\",\"model\":$emb_model,\"remote\":{\"baseUrl\":(\"http://\" + $aigw_domain + \":8080/v1\"),\"apiKey\":$key}}' \\\n /root/manager-workspace/openclaw.json > /tmp/openclaw.json.tmp && \\\n mv /tmp/openclaw.json.tmp /root/manager-workspace/openclaw.json\n fi\n _written_token=$(jq -r '.channels.matrix.accessToken' /root/manager-workspace/openclaw.json 2>/dev/null)\n log \"Matrix token written from template (prefix: ${_written_token:0:10}...)\"\nfi\n\n# Cloud/K8s mode: overlay cloud-specific settings onto generated config\nif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ] || [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n log \"Applying cloud/k8s overlay to openclaw.json...\"\n jq --arg homeserver \"${HICLAW_MATRIX_URL}\" \\\n --arg gateway \"${HICLAW_AI_GATEWAY_URL}/v1\" \\\n --arg key \"${HICLAW_MANAGER_GATEWAY_KEY}\" \\\n '.channels.matrix.homeserver = $homeserver\n | .models.providers[\"hiclaw-gateway\"].baseUrl = $gateway\n | .models.providers[\"hiclaw-gateway\"].apiKey = $key\n | ((.hooks.token // \"\") as $ht | if $ht == $key or $ht == ($key + \"-hooks\" | @base64) then del(.hooks) else . end)\n | .commands.restart = false\n | if .agents.defaults.memorySearch then .agents.defaults.memorySearch.remote.baseUrl = $gateway | .agents.defaults.memorySearch.remote.apiKey = $key else . end' \\\n /root/manager-workspace/openclaw.json > /tmp/openclaw-cloud.json && \\\n mv /tmp/openclaw-cloud.json /root/manager-workspace/openclaw.json\n log \"Cloud/K8s overlay applied\"\nfi\n\n# ============================================================\n# Optional: enable openclaw-cms-plugin observability\n# Config is applied at runtime so secrets stay out of image layers.\n# ============================================================\nCMS_TRACES_ENABLED=\"$(echo \"${HICLAW_CMS_TRACES_ENABLED:-false}\" | tr '[:upper:]' '[:lower:]')\"\nif [ \"${CMS_TRACES_ENABLED}\" = \"true\" ]; then\n CMS_PLUGIN_NAME=\"openclaw-cms-plugin\"\n CMS_PLUGIN_DIR=\"${OPENCLAW_CMS_PLUGIN_DIR:-/opt/openclaw/extensions/openclaw-cms-plugin}\"\n CMS_PLUGIN_MANIFEST=\"${CMS_PLUGIN_DIR}/openclaw.plugin.json\"\n DIAG_PLUGIN_NAME=\"diagnostics-otel\"\n DIAG_PLUGIN_DIR=\"/opt/openclaw/extensions/diagnostics-otel\"\n CMS_LICENSE_KEY=\"${HICLAW_CMS_LICENSE_KEY:-}\"\n CMS_PROJECT=\"${HICLAW_CMS_PROJECT:-}\"\n CMS_METRICS_ENABLED=\"${HICLAW_CMS_METRICS_ENABLED:-false}\"\n\n if [ ! -f \"${CMS_PLUGIN_MANIFEST}\" ]; then\n log \"WARNING: ${CMS_PLUGIN_NAME} manifest not found at ${CMS_PLUGIN_MANIFEST}, skipping plugin config\"\n else\n _missing=0\n [ -z \"${HICLAW_CMS_ENDPOINT:-}\" ] && log \"WARNING: HICLAW_CMS_ENDPOINT is required when HICLAW_CMS_TRACES_ENABLED=true\" && _missing=1\n [ -z \"${CMS_LICENSE_KEY:-}\" ] && log \"WARNING: HICLAW_CMS_LICENSE_KEY is required when HICLAW_CMS_TRACES_ENABLED=true\" && _missing=1\n [ -z \"${HICLAW_CMS_WORKSPACE:-}\" ] && log \"WARNING: HICLAW_CMS_WORKSPACE is required when HICLAW_CMS_TRACES_ENABLED=true\" && _missing=1\n\n if [ \"${_missing}\" = \"0\" ]; then\n CMS_SERVICE_NAME=\"${HICLAW_CMS_SERVICE_NAME:-hiclaw-manager}\"\n CMS_ENABLE_METRICS=\"${CMS_METRICS_ENABLED}\"\n DIAG_AVAILABLE=\"0\"\n _metrics_lc=\"$(echo \"${CMS_ENABLE_METRICS}\" | tr '[:upper:]' '[:lower:]')\"\n if [ \"${_metrics_lc}\" = \"true\" ]; then\n if [ -f \"${DIAG_PLUGIN_DIR}/package.json\" ]; then\n DIAG_AVAILABLE=\"1\"\n if [ ! -d \"${DIAG_PLUGIN_DIR}/node_modules\" ]; then\n log \"diagnostics-otel dependencies missing, installing...\"\n if (cd \"${DIAG_PLUGIN_DIR}\" && npm install --omit=dev --ignore-scripts >/tmp/hiclaw-diag-install.log 2>&1); then\n log \"diagnostics-otel dependencies installed\"\n else\n log \"WARNING: diagnostics-otel npm install failed, metrics plugin may not load\"\n fi\n else\n log \"diagnostics-otel dependencies already present\"\n fi\n else\n log \"WARNING: diagnostics-otel package.json not found at ${DIAG_PLUGIN_DIR}, metrics plugin may not load\"\n fi\n fi\n\n log \"Applying ${CMS_PLUGIN_NAME} config to openclaw.json...\"\n jq --arg pluginName \"${CMS_PLUGIN_NAME}\" \\\n --arg pluginDir \"${CMS_PLUGIN_DIR}\" \\\n --arg endpoint \"${HICLAW_CMS_ENDPOINT}\" \\\n --arg licenseKey \"${CMS_LICENSE_KEY}\" \\\n --arg armsProject \"${CMS_PROJECT}\" \\\n --arg cmsWorkspace \"${HICLAW_CMS_WORKSPACE}\" \\\n --arg serviceName \"${CMS_SERVICE_NAME}\" \\\n --arg diagPluginName \"${DIAG_PLUGIN_NAME}\" \\\n --arg diagPluginDir \"${DIAG_PLUGIN_DIR}\" \\\n --arg metricsRaw \"${CMS_ENABLE_METRICS}\" \\\n --arg diagAvailableRaw \"${DIAG_AVAILABLE}\" \\\n '\n .plugins = (.plugins // {})\n | .plugins.load = (.plugins.load // {})\n | .plugins.entries = (.plugins.entries // {})\n | if (.plugins.allow | type) != \"array\" then .plugins.allow = [] else . end\n | if (.plugins.allow | index($pluginName)) == null then .plugins.allow += [$pluginName] else . end\n | if (.plugins.load.paths | type) != \"array\" then .plugins.load.paths = [] else . end\n | if (.plugins.load.paths | index($pluginDir)) == null then .plugins.load.paths += [$pluginDir] else . end\n | .plugins.entries[$pluginName] = {\n \"enabled\": true,\n \"config\": {\n \"endpoint\": $endpoint,\n \"headers\": {\n \"x-arms-license-key\": $licenseKey,\n \"x-arms-project\": $armsProject,\n \"x-cms-workspace\": $cmsWorkspace\n },\n \"serviceName\": $serviceName\n }\n }\n\n # diagnostics-otel metrics (optional)\n | ($metricsRaw | ascii_downcase) as $m\n | ($diagAvailableRaw == \"1\") as $diagAvailable\n | (($m == \"true\") and $diagAvailable) as $metricsEnabled\n | if $metricsEnabled then\n (if (.plugins.allow | index($diagPluginName)) == null then .plugins.allow += [$diagPluginName] else . end)\n | (if (.plugins.load.paths | index($diagPluginDir)) == null then .plugins.load.paths += [$diagPluginDir] else . end)\n | .plugins.entries[$diagPluginName].enabled = true\n | .diagnostics = (.diagnostics // {})\n | .diagnostics.otel = (.diagnostics.otel // {})\n | .diagnostics.enabled = true\n | .diagnostics.otel.enabled = true\n | .diagnostics.otel.endpoint = $endpoint\n | .diagnostics.otel.protocol = (.diagnostics.otel.protocol // \"http/protobuf\")\n | .diagnostics.otel.headers = {\n \"x-arms-license-key\": $licenseKey,\n \"x-arms-project\": $armsProject,\n \"x-cms-workspace\": $cmsWorkspace\n }\n | .diagnostics.otel.serviceName = $serviceName\n | .diagnostics.otel.metrics = true\n | .diagnostics.otel.traces = (.diagnostics.otel.traces // false)\n | .diagnostics.otel.logs = (.diagnostics.otel.logs // false)\n else\n .\n end\n ' /root/manager-workspace/openclaw.json > /tmp/openclaw-cms.json && \\\n mv /tmp/openclaw-cms.json /root/manager-workspace/openclaw.json\n log \"${CMS_PLUGIN_NAME} config applied (metrics=${CMS_ENABLE_METRICS}, service=${CMS_SERVICE_NAME})\"\n else\n log \"Skipping ${CMS_PLUGIN_NAME} config due to missing required env vars\"\n fi\n fi\nfi\n\n# ============================================================\n# Detect container runtime (for Worker creation)\n# ============================================================\nsource /opt/hiclaw/scripts/lib/container-api.sh\nif container_api_available; then\n log \"Container runtime socket detected at ${CONTAINER_SOCKET} — direct Worker creation enabled\"\n export HICLAW_CONTAINER_RUNTIME=\"socket\"\nelif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ] || [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n log \"Cloud/K8s mode — Workers created via controller API\"\n export HICLAW_CONTAINER_RUNTIME=\"cloud\"\nelse\n log \"No container runtime found — Worker creation will output install commands\"\n export HICLAW_CONTAINER_RUNTIME=\"none\"\nfi\n\n# ============================================================\n# Upgrade Worker openclaw.json: merge known models + E2EE flag into existing configs\n# Existing workers in MinIO may have old single-model configs or missing encryption field.\n# Merge template models so they can hot-switch without restart.\n# ============================================================\nREGISTRY_FILE=\"/root/manager-workspace/workers-registry.json\"\nif [ -f \"${REGISTRY_FILE}\" ]; then\n # Use known-models.json (valid JSON) instead of template (contains ${VAR} placeholders)\n KNOWN_MODELS_FILE=\"/opt/hiclaw/configs/known-models.json\"\n if [ -f \"${KNOWN_MODELS_FILE}\" ]; then\n _KNOWN_MODELS=$(cat \"${KNOWN_MODELS_FILE}\")\n for _wname in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_wname}\" ] && continue\n _minio_path=\"${HICLAW_STORAGE_PREFIX}/agents/${_wname}/openclaw.json\"\n _tmp_in=\"/tmp/openclaw-${_wname}-models-upgrade-in.json\"\n if mc cp \"${_minio_path}\" \"${_tmp_in}\" 2>/dev/null; then\n _tmp_out=\"/tmp/openclaw-${_wname}-models-upgrade-out.json\"\n # Idempotent merge: add missing known models, rebuild aliases, set e2ee.\n # Always runs — jq deduplicates by model id, so re-runs are safe.\n jq --argjson known_models \"${_KNOWN_MODELS}\" \\\n --argjson e2ee \"${MATRIX_E2EE_ENABLED}\" '\n .models.providers[\"hiclaw-gateway\"].models as $existing\n | ($existing | map(.id)) as $existing_ids\n | ($known_models | map(select(.id as $id | $existing_ids | index($id) | not))) as $new\n | .models.providers[\"hiclaw-gateway\"].models = ($existing + $new)\n | (.models.providers[\"hiclaw-gateway\"].models | map({ (\"hiclaw-gateway/\" + .id): { \"alias\": .id } }) | add // {}) as $aliases\n | .agents.defaults.models = ((.agents.defaults.models // {}) + $aliases)\n | .channels.matrix.encryption = $e2ee\n | .channels.matrix.autoJoin = \"always\"\n ' \"${_tmp_in}\" > \"${_tmp_out}\" 2>/dev/null\n if ! diff -q \"${_tmp_in}\" \"${_tmp_out}\" > /dev/null 2>&1; then\n if mc cp \"${_tmp_out}\" \"${_minio_path}\" 2>/dev/null; then\n _new_count=$(jq '.models.providers[\"hiclaw-gateway\"].models | length' \"${_tmp_out}\" 2>/dev/null)\n log \"Worker ${_wname}: upgraded openclaw.json (models: ${_new_count}, e2ee: ${MATRIX_E2EE_ENABLED})\"\n fi\n fi\n rm -f \"${_tmp_in}\" \"${_tmp_out}\"\n fi\n done\n fi\nfi\n\n# ============================================================\n# Ensure Worker Matrix password files exist in MinIO (E2EE fix)\n# Workers need to re-login on restart to get a fresh device_id.\n# Older workers created before this fix won't have the password file.\n# ============================================================\nif [ -f \"${REGISTRY_FILE}\" ]; then\n for _wname in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_wname}\" ] && continue\n _creds_file=\"/data/worker-creds/${_wname}.env\"\n if [ -f \"${_creds_file}\" ]; then\n # Check if password file already exists in MinIO\n if ! mc stat \"${HICLAW_STORAGE_PREFIX}/agents/${_wname}/credentials/matrix/password\" > /dev/null 2>&1; then\n source \"${_creds_file}\"\n if [ -n \"${WORKER_PASSWORD}\" ]; then\n _tmp_pw=\"/tmp/matrix-pw-${_wname}\"\n echo -n \"${WORKER_PASSWORD}\" > \"${_tmp_pw}\"\n mc cp \"${_tmp_pw}\" \"${HICLAW_STORAGE_PREFIX}/agents/${_wname}/credentials/matrix/password\" 2>/dev/null \\\n && log \"Worker ${_wname}: wrote Matrix password to MinIO (E2EE re-login fix)\" \\\n || log \"Worker ${_wname}: WARNING: failed to write Matrix password to MinIO\"\n rm -f \"${_tmp_pw}\"\n fi\n fi\n fi\n done\nfi\n\n# ============================================================\n# Recreate Worker containers as needed after Manager restart.\n# Workers are on hiclaw-net; Docker DNS resolves *-local.hiclaw.io via\n# the Manager's network aliases, so IP changes don't require worker recreation.\n# Only recreate stopped/missing workers.\n# ============================================================\nif container_api_available; then\n REGISTRY_FILE=\"/root/manager-workspace/workers-registry.json\"\n if [ -f \"${REGISTRY_FILE}\" ]; then\n for _worker_name in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_worker_name}\" ] && continue\n\n # Skip remote workers — they are not Manager-managed containers.\n _deployment=$(jq -r --arg w \"${_worker_name}\" '.workers[$w].deployment // \"local\"' \"${REGISTRY_FILE}\" 2>/dev/null)\n if [ \"${_deployment}\" = \"remote\" ]; then\n log \"Worker ${_worker_name} is remote, skipping container recreate\"\n continue\n fi\n\n _status=$(container_status_worker \"${_worker_name}\")\n if [ \"${_status}\" = \"running\" ]; then\n log \"Worker running: ${_worker_name}, skipping\"\n continue\n fi\n # Container missing or stopped — recreate.\n log \"Worker container ${_status}: ${_worker_name}, recreating...\"\n _creds_file=\"/data/worker-creds/${_worker_name}.env\"\n if [ -f \"${_creds_file}\" ]; then\n source \"${_creds_file}\"\n _runtime=$(jq -r --arg w \"${_worker_name}\" '.workers[$w].runtime // \"openclaw\"' \"${REGISTRY_FILE}\" 2>/dev/null)\n _recreated=false\n for _attempt in 1 2 3; do\n local _env_map _create_body\n _env_map=$(jq -cn \\\n --arg name \"${_worker_name}\" \\\n --arg fak \"${_worker_name}\" \\\n --arg fsk \"${WORKER_MINIO_PASSWORD:-}\" \\\n --arg fs_domain \"${HICLAW_FS_DOMAIN:-fs-local.hiclaw.io}\" \\\n --arg controller_url \"${HICLAW_CONTROLLER_URL:-}\" \\\n '{\n \"HICLAW_WORKER_NAME\": $name,\n \"HICLAW_FS_ENDPOINT\": (\"http://\" + ($fs_domain | split(\":\")[0]) + \":8080\"),\n \"HICLAW_FS_ACCESS_KEY\": $fak,\n \"HICLAW_FS_SECRET_KEY\": $fsk\n }\n | if $controller_url != \"\" then . + {\"HICLAW_CONTROLLER_URL\": $controller_url} else . end')\n _create_body=$(jq -cn --arg name \"${_worker_name}\" --arg runtime \"${_runtime}\" --argjson env \"${_env_map}\" '{name: $name, runtime: $runtime, env: $env}')\n worker_backend_create \"${_create_body}\" > /dev/null 2>&1 && _recreated=true && break\n log \" Attempt ${_attempt}/3 failed for ${_worker_name}, retrying in $((5 * _attempt))s...\"\n sleep $((5 * _attempt))\n done\n if [ \"${_recreated}\" = true ]; then\n log \" Recreated ${_runtime} worker: ${_worker_name}\"\n else\n log \" ERROR: Failed to recreate ${_worker_name} after 3 attempts\"\n fi\n else\n log \" WARNING: No credentials found for ${_worker_name} (${_creds_file} missing), skipping\"\n fi\n done\n fi\nfi\n\n# ============================================================\n# Notify workers of builtin updates if upgrade happened\n# Builtin files (AGENTS.md, skills) are already synced by upgrade-builtins.sh\n#\n# Cooldown: skip notification if the last successful notify was within\n# NOTIFY_COOLDOWN_SECS (default 3600s / 1 hour). This prevents repeated\n# notifications when the Manager crash-loops and re-runs upgrade-builtins\n# on every restart (e.g. IMAGE_VERSION=latest always triggers upgrade).\n# ============================================================\nNOTIFY_COOLDOWN_SECS=\"${HICLAW_NOTIFY_COOLDOWN_SECS:-3600}\"\nNOTIFY_TS_FILE=\"/root/manager-workspace/.last-worker-notify-ts\"\n\nif [ -f /root/manager-workspace/.upgrade-pending-worker-notify ]; then\n _now=$(date +%s)\n _last_notify=$(cat \"${NOTIFY_TS_FILE}\" 2>/dev/null || echo \"0\")\n _elapsed=$(( _now - _last_notify ))\n\n if [ \"${_elapsed}\" -lt \"${NOTIFY_COOLDOWN_SECS}\" ]; then\n log \"Skipping worker builtin notification (last notify ${_elapsed}s ago, cooldown ${NOTIFY_COOLDOWN_SECS}s)\"\n rm -f /root/manager-workspace/.upgrade-pending-worker-notify\n else\n log \"Notifying workers about builtin updates...\"\n REGISTRY_FILE=\"/root/manager-workspace/workers-registry.json\"\n _notify_ok=false\n if [ -f \"${REGISTRY_FILE}\" ]; then\n for _worker_name in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_worker_name}\" ] && continue\n _room_id=$(jq -r --arg w \"${_worker_name}\" '.workers[$w].room_id // empty' \"${REGISTRY_FILE}\" 2>/dev/null)\n if [ -n \"${_room_id}\" ]; then\n _worker_id=\"@${_worker_name}:${MATRIX_DOMAIN}\"\n _txn_id=\"upgrade-$(date +%s%N)\"\n _msg=\"@${_worker_name}:${MATRIX_DOMAIN} Manager upgraded builtin files (AGENTS.md, skills). Please use your file-sync skill to sync the latest config.\"\n _raw=$(curl -s -w '\\nHTTP_CODE:%{http_code}' -X PUT \\\n \"${HICLAW_MATRIX_URL}/_matrix/client/v3/rooms/${_room_id}/send/m.room.message/${_txn_id}\" \\\n -H \"Authorization: Bearer ${MANAGER_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d \"{\\\"msgtype\\\":\\\"m.text\\\",\\\"body\\\":\\\"${_msg}\\\",\\\"m.mentions\\\":{\\\"user_ids\\\":[\\\"${_worker_id}\\\"]}}\" \\\n 2>&1) || true\n _http_code=$(echo \"${_raw}\" | tail -1 | sed 's/HTTP_CODE://')\n _notify_resp=$(echo \"${_raw}\" | sed '$d')\n if echo \"${_notify_resp}\" | jq -e '.event_id' > /dev/null 2>&1; then\n log \" Notified ${_worker_name}\"; _notify_ok=true\n else\n log \" WARNING: Failed to notify ${_worker_name} (HTTP ${_http_code}): ${_notify_resp}\"\n fi\n fi\n done\n fi\n # Record timestamp only if at least one notification succeeded\n if [ \"${_notify_ok}\" = true ]; then\n echo \"${_now}\" > \"${NOTIFY_TS_FILE}\"\n fi\n rm -f /root/manager-workspace/.upgrade-pending-worker-notify\n fi\nfi\n\n# ============================================================\n# Start Manager Agent\n# ============================================================\nlog \"Starting Manager Agent (${MANAGER_RUNTIME})...\"\n\n# HOME is already set to /root/manager-workspace via docker run -e HOME=...\ncd \"${HOME}\"\n\n# Ensure host credential symlinks exist under HOME\nif [ -d \"/host-share\" ]; then\n [ -f \"/host-share/.gitconfig\" ] && ln -sf \"/host-share/.gitconfig\" \"${HOME}/.gitconfig\"\nfi\n\nlog \"HOME=${HOME} (manager-workspace, host-mounted)\"\n\n# ── Render agent doc templates ────────────────────────────────────────────\n# Replace ${VAR} placeholders with actual values so the AI agent reads\n# plain text and never needs to resolve environment variables.\nexport MANAGER_MATRIX_TOKEN MANAGER_TOKEN HIGRESS_COOKIE_FILE\nRENDER=/opt/hiclaw/scripts/lib/render-skills.sh\nlog \"Rendering agent doc templates...\"\n# Manager-owned docs (workspace)\nbash \"$RENDER\" /root/manager-workspace/skills\nbash \"$RENDER\" /root/manager-workspace/skills-alpha\nbash \"$RENDER\" /root/manager-workspace AGENTS.md TOOLS.md HEARTBEAT.md SOUL.md\n# Worker templates (workspace + image) — rendered before push to MinIO\n# so Workers (including remote pip-install) receive plain text\nbash \"$RENDER\" /root/manager-workspace/worker-skills\nbash \"$RENDER\" /root/manager-workspace/worker-agent\nbash \"$RENDER\" /root/manager-workspace/copaw-worker-agent\nbash \"$RENDER\" /opt/hiclaw/agent/worker-skills\nbash \"$RENDER\" /opt/hiclaw/agent/worker-agent\nbash \"$RENDER\" /opt/hiclaw/agent/copaw-worker-agent\nlog \"Agent doc templates rendered\"\n\n# Cloud mode: start background file sync (workspace ↔ OSS) and initial push\nif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n log \"Syncing initial workspace to OSS...\"\n ensure_mc_credentials\n mc mirror /root/manager-workspace/ \"${HICLAW_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" 2>/dev/null || true\n\n # Local → OSS: change-triggered sync\n (\n while true; do\n CHANGED=$(find /root/manager-workspace/ -type f -newermt \"15 seconds ago\" 2>/dev/null | head -1)\n if [ -n \"${CHANGED}\" ]; then\n ensure_mc_credentials 2>/dev/null || true\n mc mirror /root/manager-workspace/ \"${HICLAW_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" --exclude \".npm/**\" \\\n --exclude \".local/**\" --exclude \".mc/**\" 2>/dev/null || true\n fi\n sleep 10\n done\n ) &\n log \"Local→OSS sync started (PID: $!)\"\n\n # OSS → Local: periodic pull (shared data, agent configs)\n (\n while true; do\n sleep 300\n ensure_mc_credentials 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/shared/\" /root/hiclaw-fs/shared/ --overwrite --newer-than \"5m\" 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/agents/\" /root/hiclaw-fs/agents/ --overwrite --newer-than \"5m\" 2>/dev/null || true\n done\n ) &\n log \"OSS→Local sync started (every 5m, PID: $!)\"\nfi\n\n# K8s mode: start background file sync (workspace ↔ MinIO)\nif [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n log \"Syncing initial workspace to MinIO...\"\n mc mirror /root/manager-workspace/ \"${HICLAW_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" 2>/dev/null || true\n\n # Local → MinIO: change-triggered sync\n (\n while true; do\n CHANGED=$(find /root/manager-workspace/ -type f -newermt \"15 seconds ago\" 2>/dev/null | head -1)\n if [ -n \"${CHANGED}\" ]; then\n mc mirror /root/manager-workspace/ \"${HICLAW_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" --exclude \".npm/**\" \\\n --exclude \".local/**\" --exclude \".mc/**\" 2>/dev/null || true\n fi\n sleep 10\n done\n ) &\n log \"Local→MinIO sync started (PID: $!)\"\n\n # MinIO → Local: periodic pull (shared data, agent configs)\n (\n while true; do\n sleep 300\n mc mirror \"${HICLAW_STORAGE_PREFIX}/shared/\" /root/hiclaw-fs/shared/ --overwrite --newer-than \"5m\" 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/agents/\" /root/hiclaw-fs/agents/ --overwrite --newer-than \"5m\" 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/hiclaw-config/\" /root/hiclaw-fs/hiclaw-config/ --overwrite --newer-than \"15s\" 2>/dev/null || true\n done\n ) &\n log \"MinIO→Local sync started (every 5m, PID: $!)\"\nfi\n\n# ============================================================\n# Auto-generate Manager mcporter config for pre-configured MCP servers\n# If HICLAW_GITHUB_TOKEN was set at install time, setup-higress.sh already\n# configured GitHub MCP on Higress. Run setup-mcp-server.sh now so that\n# config/mcporter.json exists when the Agent starts — no need to ask user for PAT.\n# ============================================================\nif [ -n \"${HICLAW_GITHUB_TOKEN}\" ] && [ \"${HICLAW_RUNTIME}\" != \"aliyun\" ] && [ \"${HICLAW_RUNTIME}\" != \"k8s\" ]; then\n if [ ! -f \"${HOME}/config/mcporter.json\" ]; then\n log \"Auto-generating Manager mcporter config for GitHub MCP (HICLAW_GITHUB_TOKEN set)...\"\n bash /opt/hiclaw/agent/skills/mcp-server-management/scripts/setup-mcp-server.sh \\\n github \"${HICLAW_GITHUB_TOKEN}\" 2>&1 | while IFS= read -r line; do log \" [setup-mcp] ${line}\"; done || \\\n log \"WARNING: setup-mcp-server.sh failed — Agent may need to configure GitHub MCP manually\"\n else\n log \"Manager mcporter config already exists, skipping auto-generate\"\n fi\nfi\n\n# ============================================================\n# Runtime-specific startup\n# ============================================================\nif [ \"${MANAGER_RUNTIME}\" = \"copaw\" ]; then\n # Delegate to CoPaw startup script\n exec /opt/hiclaw/scripts/init/start-copaw-manager.sh\nelse\n # ── OpenClaw Runtime ─────────────────────────────────────────────────────\n log \"Starting OpenClaw Manager...\"\n\n export OPENCLAW_CONFIG_PATH=\"/root/manager-workspace/openclaw.json\"\n\n # Symlink to default OpenClaw config path so CLI commands find the config\n mkdir -p \"${HOME}/.openclaw\"\n ln -sf \"/root/manager-workspace/openclaw.json\" \"${HOME}/.openclaw/openclaw.json\"\n\n # Clean orphaned session write locks (e.g. from SIGKILL or crash before exit handlers)\n # Prevents \"session file locked (timeout 10000ms)\" when PID was reused\n find \"${HOME}/.openclaw/agents\" -name \"*.jsonl.lock\" -delete 2>/dev/null || true\n log \"Cleaned up any orphaned session write locks\"\n\n # Clean Matrix crypto storage (SQLite WAL may be corrupted after unclean shutdown)\n # Crypto state is re-negotiated on startup; losing it only means re-establishing E2EE sessions\n rm -rf \"${HOME}/.openclaw/matrix\" 2>/dev/null || true\n log \"Cleaned Matrix crypto storage (will re-establish E2EE sessions)\"\n\n # Launch OpenClaw\n # Disable full-process respawn so the CLI uses its internal restart loop.\n # Without this, config reload spawns a detached child and exits, then\n # supervisord restarts the CLI — resulting in two gateway processes.\n export OPENCLAW_NO_RESPAWN=1\n\n # Optional matrix-plugin trace logging — when HICLAW_MATRIX_DEBUG=1 is set\n # in the manager environment (propagated by install / supervisord), turn on\n # OPENCLAW_MATRIX_DEBUG so the matrix plugin emits structured INFO-level\n # lifecycle traces (sync.state transitions, room.invite/join, message\n # handler arrival + filter outcomes). Useful for diagnosing \"worker never\n # joined the room\" / \"manager never replied\" hangs without rebuilding the\n # image.\n if [ \"${HICLAW_MATRIX_DEBUG:-}\" = \"1\" ] && [ -z \"${OPENCLAW_MATRIX_DEBUG:-}\" ]; then\n export OPENCLAW_MATRIX_DEBUG=1\n log \"HICLAW_MATRIX_DEBUG=1 detected; OPENCLAW_MATRIX_DEBUG=1 exported for matrix plugin tracing\"\n fi\n\n exec openclaw gateway run --verbose --force\nfi\n"}, "03313f450adcccc909f52c1cd9fe9d7cef4123f4": {"t": 1785764754.3053322, "n": 63812, "h": "72a57fad0530abb6", "c": "#!/bin/bash\n# start-manager-agent.sh - Initialize and start the Manager Agent\n# Supports local (supervisord), cloud (SAE), and K8s (Helm) deployments.\n# In local mode this is the last supervisord component to start (priority 800).\n# In cloud/k8s mode (HICLAW_RUNTIME=aliyun|k8s) this is the container entrypoint.\n#\n# Runtime selection:\n# HICLAW_MANAGER_RUNTIME=openclaw (default) - OpenClaw gateway mode\n# HICLAW_MANAGER_RUNTIME=copaw - CoPaw workspace mode\n\nsource /opt/hiclaw/scripts/lib/hiclaw-env.sh\n\n# ============================================================\n# Runtime selection\n# ============================================================\nMANAGER_RUNTIME=\"${HICLAW_MANAGER_RUNTIME:-openclaw}\"\ncase \"${MANAGER_RUNTIME}\" in\n copaw)\n log \"Manager runtime: CoPaw (Python workspace)\"\n ;;\n *)\n log \"Manager runtime: OpenClaw (Node.js gateway)\"\n MANAGER_RUNTIME=\"openclaw\"\n ;;\nesac\n\n# ============================================================\n# Set timezone from TZ env var\n# ============================================================\nif [ -n \"${TZ}\" ] && [ -f \"/usr/share/zoneinfo/${TZ}\" ]; then\n ln -sf \"/usr/share/zoneinfo/${TZ}\" /etc/localtime\n echo \"${TZ}\" > /etc/timezone\n log \"Timezone set to ${TZ}\"\nfi\n\nexport MATRIX_DOMAIN=\"${HICLAW_MATRIX_DOMAIN:-matrix-local.hiclaw.io:8080}\"\nAI_GATEWAY_DOMAIN=\"${HICLAW_AI_GATEWAY_DOMAIN:-aigw-local.hiclaw.io}\"\n\n# ============================================================\n# Cloud/K8s mode: validate required environment variables + initial credentials\n# ============================================================\nif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ] || [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n : \"${HICLAW_MATRIX_URL:?HICLAW_MATRIX_URL is required}\"\n : \"${HICLAW_MATRIX_DOMAIN:?HICLAW_MATRIX_DOMAIN is required}\"\n : \"${HICLAW_AI_GATEWAY_URL:?HICLAW_AI_GATEWAY_URL is required}\"\n if [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n : \"${HICLAW_MANAGER_GATEWAY_KEY:?HICLAW_MANAGER_GATEWAY_KEY is required}\"\n : \"${HICLAW_MANAGER_PASSWORD:?HICLAW_MANAGER_PASSWORD is required (cloud containers are stateless, password must be injected)}\"\n fi\n if [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n # K8s mode: controller handles initialization (admin registration, Higress setup).\n # Manager only needs credentials injected by the ManagerReconciler.\n : \"${HICLAW_MANAGER_GATEWAY_KEY:?HICLAW_MANAGER_GATEWAY_KEY is required (injected by controller)}\"\n : \"${HICLAW_MANAGER_PASSWORD:?HICLAW_MANAGER_PASSWORD is required (injected by controller)}\"\n else\n # Cloud (aliyun) mode: Manager still does its own initialization\n : \"${HICLAW_REGISTRATION_TOKEN:?HICLAW_REGISTRATION_TOKEN is required}\"\n : \"${HICLAW_ADMIN_USER:?HICLAW_ADMIN_USER is required}\"\n : \"${HICLAW_ADMIN_PASSWORD:?HICLAW_ADMIN_PASSWORD is required}\"\n fi\n log \"${HICLAW_RUNTIME} mode: validating environment... OK\"\n log \" Matrix: ${HICLAW_MATRIX_URL}, AI Gateway: ${HICLAW_AI_GATEWAY_URL}, Storage: ${HICLAW_FS_BUCKET}\"\n if [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n ensure_mc_credentials || { log \"FATAL: Initial STS credential fetch failed\"; exit 1; }\n fi\nfi\n\n# ============================================================\n# Local mode: host symlinks, /etc/hosts, wait for local services\n# ============================================================\nif [ \"${HICLAW_RUNTIME}\" != \"aliyun\" ] && [ \"${HICLAW_RUNTIME}\" != \"k8s\" ]; then\n # Create symlink for host directory access\n if [ -d \"/host-share\" ]; then\n ORIGINAL_HOST_HOME=\"${HOST_ORIGINAL_HOME:-$HOME}\"\n if [ ! -e \"${ORIGINAL_HOST_HOME}\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/root\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/data\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/host-share\" ]; then\n mkdir -p \"$(dirname \"${ORIGINAL_HOST_HOME}\")\"\n ln -sfn /host-share \"${ORIGINAL_HOST_HOME}\"\n log \"Created symlink: ${ORIGINAL_HOST_HOME} -> /host-share\"\n else\n ln -sfn /host-share /root/host-home\n log \"Created fallback symlink: /root/host-home -> /host-share\"\n fi\n fi\n\n # Add local domains to /etc/hosts\n HOSTS_DOMAINS=\"${MATRIX_DOMAIN%%:*} ${HICLAW_MATRIX_CLIENT_DOMAIN:-matrix-client-local.hiclaw.io} ${AI_GATEWAY_DOMAIN} ${HICLAW_FS_DOMAIN:-fs-local.hiclaw.io}\"\n if ! grep -q \"${AI_GATEWAY_DOMAIN}\" /etc/hosts 2>/dev/null; then\n echo \"127.0.0.1 ${HOSTS_DOMAINS}\" >> /etc/hosts\n log \"Added local domains to /etc/hosts\"\n fi\n\n # Wait for local infrastructure\n waitForService \"Higress Gateway\" \"127.0.0.1\" 8080 180\n waitForService \"Higress Console\" \"127.0.0.1\" 8001 180\n waitForService \"Tuwunel\" \"127.0.0.1\" 6167 120\n waitForHTTP \"Tuwunel Matrix API\" \"${HICLAW_MATRIX_URL}/_tuwunel/server_version\" 120\n waitForService \"MinIO\" \"127.0.0.1\" 9000 120\nelse\n # Cloud/K8s mode: wait for external Tuwunel\n log \"Waiting for Tuwunel Matrix server at ${HICLAW_MATRIX_URL}...\"\n _retry=0\n while [ \"${_retry}\" -lt 30 ]; do\n if curl -sf \"${HICLAW_MATRIX_URL}/_matrix/client/versions\" > /dev/null 2>&1; then\n log \"Tuwunel is ready\"\n break\n fi\n _retry=$((_retry + 1))\n log \" Waiting for Tuwunel (attempt ${_retry}/30)...\"\n sleep 5\n done\n if [ \"${_retry}\" -ge 30 ]; then\n log \"ERROR: Tuwunel not reachable at ${HICLAW_MATRIX_URL}\"\n exit 1\n fi\nfi\n\n# ============================================================\n# Auto-generate secrets if not provided via environment\n# Persisted to /data so they survive container restart\n# ============================================================\nSECRETS_FILE=\"/data/hiclaw-secrets.env\"\nif [ -f \"${SECRETS_FILE}\" ]; then\n source \"${SECRETS_FILE}\"\n log \"Loaded persisted secrets from ${SECRETS_FILE}\"\nfi\n\nif [ -z \"${HICLAW_MANAGER_GATEWAY_KEY}\" ]; then\n export HICLAW_MANAGER_GATEWAY_KEY=\"$(generateKey 32)\"\n log \"Auto-generated HICLAW_MANAGER_GATEWAY_KEY\"\nfi\nif [ -z \"${HICLAW_MANAGER_PASSWORD}\" ]; then\n export HICLAW_MANAGER_PASSWORD=\"$(generateKey 16)\"\n log \"Auto-generated HICLAW_MANAGER_PASSWORD\"\nfi\n\n# Persist secrets so they survive supervisord restart\nmkdir -p /data\ncat > \"${SECRETS_FILE}\" <<EOF\nexport HICLAW_MANAGER_GATEWAY_KEY=\"${HICLAW_MANAGER_GATEWAY_KEY}\"\nexport HICLAW_MANAGER_PASSWORD=\"${HICLAW_MANAGER_PASSWORD}\"\nEOF\nchmod 600 \"${SECRETS_FILE}\"\n\n# Cloud mode: pull workspace from OSS before initialization\nif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n HICLAW_FS=\"/root/hiclaw-fs\"\n mkdir -p \"${HICLAW_FS}/shared\" \"${HICLAW_FS}/agents\"\n log \"Pulling workspace from OSS...\"\n ensure_mc_credentials\n mc mirror \"${HICLAW_STORAGE_PREFIX}/manager/\" /root/manager-workspace/ --overwrite 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/shared/\" \"${HICLAW_FS}/shared/\" --overwrite 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/agents/\" \"${HICLAW_FS}/agents/\" --overwrite 2>/dev/null || true\n # Symlink hiclaw-fs into workspace for agent access\n ln -sfn \"${HICLAW_FS}\" /root/manager-workspace/hiclaw-fs\nfi\n\n# K8s mode: sync workspace from cluster-internal MinIO\nif [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n HICLAW_FS=\"/root/hiclaw-fs\"\n mkdir -p \"${HICLAW_FS}/shared\" \"${HICLAW_FS}/agents\" \"${HICLAW_FS}/hiclaw-config\"\n log \"Configuring mc alias for cluster MinIO...\"\n mc alias set hiclaw \"${HICLAW_FS_ENDPOINT}\" \"${HICLAW_FS_ACCESS_KEY}\" \"${HICLAW_FS_SECRET_KEY}\"\n log \"Syncing workspace from MinIO...\"\n mc mirror \"${HICLAW_STORAGE_PREFIX}/manager/\" /root/manager-workspace/ --overwrite 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/\" \"${HICLAW_FS}/\" --overwrite 2>/dev/null || true\n ln -sfn \"${HICLAW_FS}\" /root/manager-workspace/hiclaw-fs\n touch \"${HICLAW_FS}/.initialized\"\nfi\n\n# ============================================================\n# Initialize / upgrade Manager workspace\n# First boot: full init via upgrade-builtins.sh\n# Subsequent boots: compare image version; upgrade only if changed\n# ============================================================\nmkdir -p /root/manager-workspace\n\nIMAGE_VERSION=$(cat /opt/hiclaw/agent/.builtin-version 2>/dev/null || echo \"unknown\")\nINSTALLED_VERSION=$(cat /root/manager-workspace/.builtin-version 2>/dev/null || echo \"\")\n\nif [ ! -f /root/manager-workspace/.initialized ]; then\n log \"First boot: initializing manager workspace...\"\n bash /opt/hiclaw/scripts/init/upgrade-builtins.sh\n touch /root/manager-workspace/.initialized\n log \"Manager workspace initialized (version: ${IMAGE_VERSION})\"\nelif [ \"${IMAGE_VERSION}\" != \"${INSTALLED_VERSION}\" ] || [ \"${IMAGE_VERSION}\" = \"latest\" ]; then\n log \"Upgrade detected: ${INSTALLED_VERSION} -> ${IMAGE_VERSION}${IMAGE_VERSION:+ (latest: always upgrade)}\"\n bash /opt/hiclaw/scripts/init/upgrade-builtins.sh\n log \"Manager workspace upgraded to version: ${IMAGE_VERSION}\"\nelse\n log \"Workspace up to date (version: ${IMAGE_VERSION})\"\nfi\n\n# Local mode: wait for mc mirror initialization (shared + worker data in /root/hiclaw-fs/)\nif [ \"${HICLAW_RUNTIME}\" != \"aliyun\" ] && [ \"${HICLAW_RUNTIME}\" != \"k8s\" ]; then\n log \"Waiting for MinIO storage initialization...\"\n _minio_wait=0\n while [ ! -f /root/hiclaw-fs/.initialized ]; do\n sleep 2\n _minio_wait=$(( _minio_wait + 1 ))\n if [ \"${_minio_wait}\" -ge 60 ]; then\n log \"ERROR: MinIO storage initialization timed out after 120s\"\n exit 1\n fi\n done\n log \"MinIO storage initialized\"\nfi\n\n# ============================================================\n# Register Matrix users via Registration API (single-step, no UIAA)\n# K8s mode: skip — controller Initializer + ManagerReconciler already did this\n# ============================================================\nif [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n log \"K8s mode: skipping Matrix registration (handled by controller)\"\n # Controller injects HICLAW_MANAGER_PASSWORD via env; login to get token\n log \"Obtaining Manager Matrix access token...\"\n _LOGIN_RESPONSE=$(curl -s -X POST ${HICLAW_MATRIX_URL}/_matrix/client/v3/login \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"type\": \"m.login.password\",\n \"identifier\": {\"type\": \"m.id.user\", \"user\": \"manager\"},\n \"password\": \"'\"${HICLAW_MANAGER_PASSWORD}\"'\"\n }' 2>&1)\n MANAGER_TOKEN=$(echo \"${_LOGIN_RESPONSE}\" | jq -r '.access_token' 2>/dev/null)\n if [ -z \"${MANAGER_TOKEN}\" ] || [ \"${MANAGER_TOKEN}\" = \"null\" ]; then\n log \"ERROR: Failed to obtain Manager Matrix token\"\n log \"ERROR: Login response was: ${_LOGIN_RESPONSE}\"\n exit 1\n fi\n log \"Manager Matrix token obtained (token prefix: ${MANAGER_TOKEN:0:10}...)\"\nelse\nlog \"Registering human admin Matrix account...\"\ncurl -sf -X POST ${HICLAW_MATRIX_URL}/_matrix/client/v3/register \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"username\": \"'\"${HICLAW_ADMIN_USER}\"'\",\n \"password\": \"'\"${HICLAW_ADMIN_PASSWORD}\"'\",\n \"auth\": {\n \"type\": \"m.login.registration_token\",\n \"token\": \"'\"${HICLAW_REGISTRATION_TOKEN}\"'\"\n }\n }' > /dev/null 2>&1 || log \"Admin account may already exist\"\n\nlog \"Registering Manager Agent Matrix account...\"\ncurl -sf -X POST ${HICLAW_MATRIX_URL}/_matrix/client/v3/register \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"username\": \"manager\",\n \"password\": \"'\"${HICLAW_MANAGER_PASSWORD}\"'\",\n \"auth\": {\n \"type\": \"m.login.registration_token\",\n \"token\": \"'\"${HICLAW_REGISTRATION_TOKEN}\"'\"\n }\n }' > /dev/null 2>&1 || log \"Manager account may already exist\"\n\n# Get Manager Agent's Matrix access token\nlog \"Obtaining Manager Matrix access token...\"\n_LOGIN_RESPONSE=$(curl -s -X POST ${HICLAW_MATRIX_URL}/_matrix/client/v3/login \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"type\": \"m.login.password\",\n \"identifier\": {\"type\": \"m.id.user\", \"user\": \"manager\"},\n \"password\": \"'\"${HICLAW_MANAGER_PASSWORD}\"'\"\n }' 2>&1)\n_LOGIN_EXIT=$?\nlog \"Matrix login HTTP exit code: ${_LOGIN_EXIT}\"\nlog \"Matrix login response: ${_LOGIN_RESPONSE}\"\n\nMANAGER_TOKEN=$(echo \"${_LOGIN_RESPONSE}\" | jq -r '.access_token' 2>/dev/null)\n\nif [ -z \"${MANAGER_TOKEN}\" ] || [ \"${MANAGER_TOKEN}\" = \"null\" ]; then\n log \"ERROR: Failed to obtain Manager Matrix token (exit=${_LOGIN_EXIT})\"\n log \"ERROR: Login response was: ${_LOGIN_RESPONSE}\"\n exit 1\nfi\nlog \"Manager Matrix token obtained (token prefix: ${MANAGER_TOKEN:0:10}...)\"\nfi\n\n# ============================================================\n# Higress Console initialization\n# Docker mode: full setup-higress.sh (internal Higress at localhost:8001)\n# K8s mode: skip — controller Initializer handles Higress setup\n# Cloud (aliyun) mode: skip entirely (Higress managed externally)\n# ============================================================\n_HIGRESS_CONSOLE_URL=\"\"\n_HIGRESS_USER=\"${HICLAW_ADMIN_USER}\"\n_HIGRESS_PASS=\"${HICLAW_ADMIN_PASSWORD}\"\nif [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n log \"K8s mode: skipping Higress initialization (handled by controller)\"\nelif [ \"${HICLAW_RUNTIME}\" != \"aliyun\" ]; then\n _HIGRESS_CONSOLE_URL=\"http://127.0.0.1:8001\"\nfi\n\nif [ -n \"${_HIGRESS_CONSOLE_URL}\" ]; then\n COOKIE_FILE=\"/tmp/higress-session-cookie\"\n\n log \"Waiting for Higress Console (${_HIGRESS_CONSOLE_URL}) to be fully ready and initializing admin...\"\n INIT_DONE=false\n for i in $(seq 1 90); do\n INIT_RESULT=$(curl -s -X POST \"${_HIGRESS_CONSOLE_URL}/system/init\" \\\n -H 'Content-Type: application/json' \\\n -d '{\"adminUser\":{\"name\":\"'\"${_HIGRESS_USER}\"'\",\"password\":\"'\"${_HIGRESS_PASS}\"'\",\"displayName\":\"'\"${_HIGRESS_USER}\"'\"}}' 2>/dev/null) || true\n if echo \"${INIT_RESULT}\" | grep -qE '\"success\":true|already.?init' 2>/dev/null; then\n INIT_DONE=true\n break\n fi\n if echo \"${INIT_RESULT}\" | grep -q '\"name\"' 2>/dev/null; then\n INIT_DONE=true\n break\n fi\n sleep 2\n done\n\n if [ \"${INIT_DONE}\" != \"true\" ]; then\n log \"ERROR: Higress Console did not become ready within 180s\"\n exit 1\n fi\n log \"Higress Console init done\"\n\n log \"Logging into Higress Console...\"\n LOGIN_OK=false\n for i in $(seq 1 10); do\n HTTP_CODE=$(curl -s -o /dev/null -w '%{http_code}' -X POST \"${_HIGRESS_CONSOLE_URL}/session/login\" \\\n -H 'Content-Type: application/json' \\\n -c \"${COOKIE_FILE}\" \\\n -d '{\"username\":\"'\"${_HIGRESS_USER}\"'\",\"password\":\"'\"${_HIGRESS_PASS}\"'\"}' 2>/dev/null) || true\n if { [ \"${HTTP_CODE}\" = \"200\" ] || [ \"${HTTP_CODE}\" = \"201\" ]; } && [ -f \"${COOKIE_FILE}\" ] && [ -s \"${COOKIE_FILE}\" ]; then\n LOGIN_OK=true\n break\n fi\n log \"Login attempt $i (HTTP ${HTTP_CODE}), retrying in 3s...\"\n sleep 3\n done\n\n if [ \"${LOGIN_OK}\" != \"true\" ]; then\n log \"ERROR: Could not login to Higress Console after retries\"\n exit 1\n fi\n log \"Higress Console login successful\"\n\n VERIFY_CODE=$(curl -s -o /dev/null -w '%{http_code}' \"${_HIGRESS_CONSOLE_URL}/v1/consumers\" -b \"${COOKIE_FILE}\" 2>/dev/null) || true\n if [ \"${VERIFY_CODE}\" = \"200\" ]; then\n log \"Console session verified (cookie valid)\"\n else\n log \"WARNING: Console session may be invalid (verify returned HTTP ${VERIFY_CODE})\"\n rm -f \"${COOKIE_FILE}\"\n for i in $(seq 1 5); do\n curl -s -o /dev/null -w '%{http_code}' -X POST \"${_HIGRESS_CONSOLE_URL}/session/login\" \\\n -H 'Content-Type: application/json' \\\n -c \"${COOKIE_FILE}\" \\\n -d '{\"username\":\"'\"${_HIGRESS_USER}\"'\",\"password\":\"'\"${_HIGRESS_PASS}\"'\"}' 2>/dev/null\n VERIFY2=$(curl -s -o /dev/null -w '%{http_code}' \"${_HIGRESS_CONSOLE_URL}/v1/consumers\" -b \"${COOKIE_FILE}\" 2>/dev/null) || true\n if [ \"${VERIFY2}\" = \"200\" ]; then\n log \"Re-login successful, session verified\"\n break\n fi\n sleep 2\n done\n fi\n\n export HIGRESS_COOKIE_FILE=\"${COOKIE_FILE}\"\n export HIGRESS_CONSOLE_URL=\"${_HIGRESS_CONSOLE_URL}\"\n\n if [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n # K8s mode: lightweight Higress config — only what's needed for LLM access\n source /opt/hiclaw/scripts/lib/base.sh\n _k8s_higress_api() {\n local method=\"$1\" path=\"$2\" desc=\"$3\"; shift 3; local body=\"$*\"\n local tmpfile; tmpfile=$(mktemp)\n local http_code\n http_code=$(curl -s -o \"${tmpfile}\" -w '%{http_code}' -X \"${method}\" \"${_HIGRESS_CONSOLE_URL}${path}\" \\\n -b \"${COOKIE_FILE}\" -H 'Content-Type: application/json' -d \"${body}\" 2>/dev/null) || true\n local response; response=$(cat \"${tmpfile}\" 2>/dev/null); rm -f \"${tmpfile}\"\n if echo \"${response}\" | grep -q '\"success\":true' 2>/dev/null; then\n log \"${desc} ... OK\"\n elif [ \"${http_code}\" = \"409\" ]; then\n log \"${desc} ... already exists, skipping\"\n elif [ \"${http_code}\" = \"200\" ] || [ \"${http_code}\" = \"201\" ] || [ \"${http_code}\" = \"204\" ]; then\n log \"${desc} ... OK (HTTP ${http_code})\"\n else\n log \"WARNING: ${desc} ... (HTTP ${http_code}): ${response}\"\n fi\n }\n\n # 1. Service Sources (DNS type → K8s Service FQDN)\n # Extract host:port from URLs for Higress service source registration\n _TUWUNEL_HOST=$(echo \"${HICLAW_MATRIX_URL}\" | sed 's|^http[s]*://||')\n _TUWUNEL_DOMAIN=$(echo \"${_TUWUNEL_HOST}\" | cut -d: -f1)\n _TUWUNEL_PORT=$(echo \"${_TUWUNEL_HOST}\" | cut -d: -f2)\n _k8s_higress_api POST /v1/service-sources \"Registering Tuwunel service source\" \\\n '{\"type\":\"dns\",\"name\":\"tuwunel\",\"domain\":\"'\"${_TUWUNEL_DOMAIN}\"'\",\"port\":'\"${_TUWUNEL_PORT}\"'}'\n\n if [ -n \"${HICLAW_ELEMENT_WEB_URL:-}\" ]; then\n _ELEMENT_HOST=$(echo \"${HICLAW_ELEMENT_WEB_URL}\" | sed 's|^http[s]*://||')\n _ELEMENT_DOMAIN=$(echo \"${_ELEMENT_HOST}\" | cut -d: -f1)\n _ELEMENT_PORT=$(echo \"${_ELEMENT_HOST}\" | cut -d: -f2)\n _k8s_higress_api POST /v1/service-sources \"Registering Element Web service source\" \\\n '{\"type\":\"dns\",\"name\":\"element-web\",\"domain\":\"'\"${_ELEMENT_DOMAIN}\"'\",\"port\":'\"${_ELEMENT_PORT}\"'}'\n fi\n\n # 2. Manager Consumer (key-auth)\n _k8s_higress_api POST /v1/consumers \"Creating Manager consumer\" \\\n '{\"name\":\"manager\",\"credentials\":[{\"type\":\"key-auth\",\"source\":\"BEARER\",\"values\":[\"'\"${HICLAW_MANAGER_GATEWAY_KEY}\"'\"]}]}'\n\n # 3. LLM Provider\n _LLM_PROVIDER=\"${HICLAW_LLM_PROVIDER:-qwen}\"\n _LLM_API_URL=\"${HICLAW_LLM_API_URL:-}\"\n if [ -z \"${_LLM_API_URL}\" ]; then\n case \"${_LLM_PROVIDER}\" in\n qwen) _LLM_API_URL=\"https://dashscope.aliyuncs.com/compatible-mode/v1\" ;;\n esac\n fi\n # 4. LLM Provider type-specific config\n case \"${_LLM_PROVIDER}\" in\n qwen)\n _k8s_higress_api POST /v1/ai/providers \"Creating LLM provider (qwen)\" \\\n '{\"type\":\"qwen\",\"name\":\"qwen\",\"tokens\":[\"'\"${HICLAW_LLM_API_KEY}\"'\"],\"protocol\":\"openai/v1\",\"tokenFailoverConfig\":{\"enabled\":false},\"rawConfigs\":{\"qwenEnableSearch\":false,\"qwenEnableCompatible\":true,\"qwenFileIds\":[],\"hiclawMode\":true}}'\n ;;\n *)\n _BODY='{\"name\":\"'\"${_LLM_PROVIDER}\"'\",\"type\":\"openai\",\"tokens\":[\"'\"${HICLAW_LLM_API_KEY}\"'\"],\"modelMapping\":{},\"protocol\":\"openai/v1\",\"rawConfigs\":{\"hiclawMode\":true}}'\n _k8s_higress_api POST /v1/ai/providers \"Creating LLM provider (${_LLM_PROVIDER})\" \"${_BODY}\"\n ;;\n esac\n\n # 5. AI Route (bind provider + consumer auth, /v1 prefix to avoid clash with Element Web catch-all)\n _k8s_higress_api POST /v1/ai/routes \"Creating AI Gateway route\" \\\n '{\"name\":\"default-ai-route\",\"domains\":[],\"pathPredicate\":{\"matchType\":\"PRE\",\"matchValue\":\"/v1\",\"caseSensitive\":false},\"upstreams\":[{\"provider\":\"'\"${_LLM_PROVIDER}\"'\",\"weight\":100,\"modelMapping\":{}}],\"authConfig\":{\"enabled\":true,\"allowedCredentialTypes\":[\"key-auth\"],\"allowedConsumers\":[\"manager\"]}}'\n\n # 6. Matrix Homeserver Route (/_matrix/* → Tuwunel, no auth)\n _k8s_higress_api POST /v1/routes \"Creating Matrix Homeserver route\" \\\n '{\"name\":\"matrix-homeserver\",\"domains\":[],\"path\":{\"matchType\":\"PRE\",\"matchValue\":\"/_matrix\"},\"services\":[{\"name\":\"tuwunel.dns\",\"port\":'\"${_TUWUNEL_PORT}\"',\"weight\":100}]}'\n\n # 7. Element Web Route (/ catch-all → Element Web, no auth)\n if [ -n \"${HICLAW_ELEMENT_WEB_URL:-}\" ]; then\n _k8s_higress_api POST /v1/routes \"Creating Element Web route\" \\\n '{\"name\":\"element-web\",\"domains\":[],\"path\":{\"matchType\":\"PRE\",\"matchValue\":\"/\"},\"services\":[{\"name\":\"element-web.dns\",\"port\":'\"${_ELEMENT_PORT}\"',\"weight\":100}]}'\n fi\n\n # 8. Remove Higress default landing page (Exact match on / takes precedence over Element Web catch-all)\n _k8s_higress_api DELETE /v1/routes/default \"Removing Higress default landing route\"\n\n log \"K8s Higress lightweight setup complete\"\n\n # Wait for AI plugin activation (~45s for first config)\n log \"Waiting for AI Gateway plugin activation (45s)...\"\n sleep 45\n else\n # Docker mode: full setup with all routes, domains, MCP servers\n /opt/hiclaw/scripts/init/setup-higress.sh\n fi\nfi\n\n# ============================================================\n# Create admin DM room, persist to state.json, send welcome message\n# K8s mode: skip — controller ProvisionManager already creates the Admin DM room\n# Runs in both local and cloud modes (idempotent)\n# ============================================================\nif [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n log \"K8s mode: skipping admin DM room creation (handled by controller)\"\nelse\nMANAGER_FULL_ID=\"@manager:${MATRIX_DOMAIN}\"\nADMIN_FULL_ID=\"@${HICLAW_ADMIN_USER}:${MATRIX_DOMAIN}\"\n\nlog \"Logging in as admin to create DM room...\"\n_ADMIN_LOGIN=$(curl -sf -X POST \"${HICLAW_MATRIX_URL}/_matrix/client/v3/login\" \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"type\": \"m.login.password\",\n \"identifier\": {\"type\": \"m.id.user\", \"user\": \"'\"${HICLAW_ADMIN_USER}\"'\"},\n \"password\": \"'\"${HICLAW_ADMIN_PASSWORD}\"'\"\n }' 2>&1) || true\n\nADMIN_MATRIX_TOKEN=$(echo \"${_ADMIN_LOGIN}\" | jq -r '.access_token // empty' 2>/dev/null)\nif [ -z \"${ADMIN_MATRIX_TOKEN}\" ]; then\n log \"WARNING: Failed to login as admin, skipping DM room creation\"\nelse\n # Search for existing DM room with Manager (idempotent)\n DM_ROOM_ID=\"\"\n _JOINED_ROOMS=$(curl -sf \"${HICLAW_MATRIX_URL}/_matrix/client/v3/joined_rooms\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" 2>/dev/null \\\n | jq -r '.joined_rooms[]' 2>/dev/null) || true\n for _rid in ${_JOINED_ROOMS}; do\n _members=$(curl -sf \"${HICLAW_MATRIX_URL}/_matrix/client/v3/rooms/${_rid}/members\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" 2>/dev/null \\\n | jq -r '.chunk[].state_key' 2>/dev/null) || continue\n _count=$(echo \"${_members}\" | wc -l | xargs)\n if [ \"${_count}\" = \"2\" ] && echo \"${_members}\" | grep -q \"@manager:\"; then\n DM_ROOM_ID=\"${_rid}\"\n break\n fi\n done\n\n if [ -n \"${DM_ROOM_ID}\" ]; then\n log \"Existing DM room found: ${DM_ROOM_ID}\"\n else\n log \"Creating DM room with Manager...\"\n _RAW=$(curl -s -w '\\nHTTP_CODE:%{http_code}' -X POST \"${HICLAW_MATRIX_URL}/_matrix/client/v3/createRoom\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d \"{\\\"is_direct\\\":true,\\\"invite\\\":[\\\"${MANAGER_FULL_ID}\\\"],\\\"preset\\\":\\\"trusted_private_chat\\\"}\" 2>&1) || true\n _HTTP_CODE=$(echo \"${_RAW}\" | tail -1 | sed 's/HTTP_CODE://')\n _CREATE_RESP=$(echo \"${_RAW}\" | sed '$d')\n DM_ROOM_ID=$(echo \"${_CREATE_RESP}\" | jq -r '.room_id // empty' 2>/dev/null)\n if [ -n \"${DM_ROOM_ID}\" ]; then\n log \"DM room created: ${DM_ROOM_ID}\"\n else\n log \"WARNING: Failed to create DM room (HTTP ${_HTTP_CODE}): ${_CREATE_RESP}\"\n fi\n fi\n\n # Persist admin DM room ID to state.json\n if [ -n \"${DM_ROOM_ID}\" ]; then\n STATE_SCRIPT=\"/opt/hiclaw/agent/skills/task-management/scripts/manage-state.sh\"\n if [ -f \"${STATE_SCRIPT}\" ]; then\n bash \"${STATE_SCRIPT}\" --action init 2>/dev/null || true\n bash \"${STATE_SCRIPT}\" --action set-admin-dm --room-id \"${DM_ROOM_ID}\" 2>/dev/null || true\n log \"Admin DM room persisted to state.json: ${DM_ROOM_ID}\"\n fi\n fi\n\n # Schedule welcome message in background (only on first boot)\n if [ -n \"${DM_ROOM_ID}\" ] && [ ! -f \"/root/manager-workspace/soul-configured\" ]; then\n log \"Scheduling welcome message (background, waiting for OpenClaw to start)...\"\n (\n _HICLAW_LANGUAGE=\"${HICLAW_LANGUAGE:-zh}\"\n _HICLAW_TIMEZONE=\"${TZ:-Asia/Shanghai}\"\n _wait=0\n _ready=false\n while [ \"${_wait}\" -lt 300 ]; do\n if curl -sf http://127.0.0.1:18799/ > /dev/null 2>&1; then\n _ready=true\n break\n fi\n sleep 3\n _wait=$((_wait + 3))\n done\n if [ \"${_ready}\" != \"true\" ]; then\n echo \"[manager] WARNING: OpenClaw gateway not ready within 300s, skipping welcome message\"\n exit 0\n fi\n # Ensure Manager has joined the DM room before sending the welcome\n # message. Without this, there is a race between OpenClaw's Matrix\n # auto-join and the message send — the message may land before Manager\n # joins, so OpenClaw's /sync never picks it up.\n _join_ok=false\n for _join_attempt in 1 2 3; do\n if curl -sf -X POST \"${HICLAW_MATRIX_URL}/_matrix/client/v3/rooms/${DM_ROOM_ID}/join\" \\\n -H \"Authorization: Bearer ${MANAGER_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d '{}' > /dev/null 2>&1; then\n echo \"[manager] Manager joined DM room before welcome message\"\n _join_ok=true\n break\n fi\n sleep 2\n done\n if [ \"${_join_ok}\" != \"true\" ]; then\n echo \"[manager] WARNING: Manager join request failed after 3 attempts (may already be joined)\"\n fi\n _welcome_msg=\"This is an automated message from the HiClaw setup. This is a fresh installation.\n\n--- Installation Context ---\nUser Language: ${_HICLAW_LANGUAGE} (zh = Chinese, en = English)\nUser Timezone: ${_HICLAW_TIMEZONE} (IANA timezone identifier)\n---\n\nYou are an AI agent that manages a team of worker agents. Your identity and personality have not been configured yet — the human admin is about to meet you for the first time.\n\nPlease begin the onboarding conversation:\n\n1. Greet the admin warmly and briefly describe what you can do (coordinate workers, manage tasks, run multi-agent projects)\n2. The user has selected \\\"${_HICLAW_LANGUAGE}\\\" as their preferred language during installation. Use this language for your greeting and all subsequent communication.\n3. The user's timezone is ${_HICLAW_TIMEZONE}. Based on this timezone, you may infer their likely region and suggest additional language options.\n4. Ask them: a) What would they like to call you? b) Communication style preference? c) Any behavior guidelines? d) Confirm default language\n5. After they reply, write their preferences to ~/SOUL.md\n6. Confirm what you wrote, and ask if they would like to adjust anything\n7. Once confirmed, run: touch ~/soul-configured\n\nThe human admin will start chatting shortly.\"\n _txn_id=\"welcome-$(date +%s)\"\n _payload=$(jq -nc --arg body \"${_welcome_msg}\" '{\"msgtype\":\"m.text\",\"body\":$body}')\n _raw=$(curl -s -w '\\nHTTP_CODE:%{http_code}' -X PUT \"${HICLAW_MATRIX_URL}/_matrix/client/v3/rooms/${DM_ROOM_ID}/send/m.room.message/${_txn_id}\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d \"${_payload}\" 2>&1) || true\n _http_code=$(echo \"${_raw}\" | tail -1 | sed 's/HTTP_CODE://')\n _send_resp=$(echo \"${_raw}\" | sed '$d')\n if echo \"${_send_resp}\" | jq -e '.event_id' > /dev/null 2>&1; then\n echo \"[manager] Welcome message sent to DM room\"\n else\n echo \"[manager] WARNING: Failed to send welcome message (HTTP ${_http_code}): ${_send_resp}\"\n fi\n ) &\n log \"Welcome message background process started (PID: $!)\"\n fi\nfi\nfi # end K8s mode skip for admin DM room\n\n# ============================================================\n# Generate Manager Agent openclaw.json from template\n# ============================================================\nlog \"Generating Manager openclaw.json...\"\nexport MANAGER_MATRIX_TOKEN=\"${MANAGER_TOKEN}\"\nexport MANAGER_GATEWAY_KEY=\"${HICLAW_MANAGER_GATEWAY_KEY}\"\n# Resolve model parameters based on model name\nMODEL_NAME=\"${HICLAW_DEFAULT_MODEL:-qwen3.5-plus}\"\ncase \"${MODEL_NAME}\" in\n gpt-5.3-codex|gpt-5-mini|gpt-5-nano)\n export MODEL_CONTEXT_WINDOW=400000 MODEL_MAX_TOKENS=128000 ;;\n claude-opus-4-6)\n export MODEL_CONTEXT_WINDOW=1000000 MODEL_MAX_TOKENS=128000 ;;\n claude-sonnet-4-6)\n export MODEL_CONTEXT_WINDOW=1000000 MODEL_MAX_TOKENS=64000 ;;\n claude-haiku-4-5)\n export MODEL_CONTEXT_WINDOW=200000 MODEL_MAX_TOKENS=64000 ;;\n qwen3.5-plus)\n export MODEL_CONTEXT_WINDOW=200000 MODEL_MAX_TOKENS=64000 ;;\n deepseek-chat|deepseek-reasoner|kimi-k2.5)\n export MODEL_CONTEXT_WINDOW=256000 MODEL_MAX_TOKENS=128000 ;;\n glm-5|MiniMax-M2.7|MiniMax-M2.7-highspeed|MiniMax-M2.5)\n export MODEL_CONTEXT_WINDOW=200000 MODEL_MAX_TOKENS=128000 ;;\n *)\n export MODEL_CONTEXT_WINDOW=150000 MODEL_MAX_TOKENS=128000 ;;\nesac\nexport MODEL_REASONING=true\n\n# Override with user-supplied custom model parameters from env (set during install)\n[ -n \"${HICLAW_MODEL_CONTEXT_WINDOW:-}\" ] && export MODEL_CONTEXT_WINDOW=\"${HICLAW_MODEL_CONTEXT_WINDOW}\"\n[ -n \"${HICLAW_MODEL_MAX_TOKENS:-}\" ] && export MODEL_MAX_TOKENS=\"${HICLAW_MODEL_MAX_TOKENS}\"\n[ -n \"${HICLAW_MODEL_REASONING:-}\" ] && export MODEL_REASONING=\"${HICLAW_MODEL_REASONING}\"\n\n# E2EE: convert HICLAW_MATRIX_E2EE to JSON boolean for template substitution\nif [ \"${HICLAW_MATRIX_E2EE:-0}\" = \"1\" ] || [ \"${HICLAW_MATRIX_E2EE:-}\" = \"true\" ]; then\n export MATRIX_E2EE_ENABLED=true\nelse\n export MATRIX_E2EE_ENABLED=false\nfi\nlog \"Matrix E2EE: ${MATRIX_E2EE_ENABLED}\"\n\n# Resolve input modalities: only vision-capable models get \"image\"\ncase \"${MODEL_NAME}\" in\n gpt-5.4|gpt-5.3-codex|gpt-5-mini|gpt-5-nano|claude-opus-4-6|claude-sonnet-4-6|claude-haiku-4-5|qwen3.5-plus|kimi-k2.5)\n export MODEL_INPUT='[\"text\", \"image\"]' ;;\n *)\n export MODEL_INPUT='[\"text\"]' ;;\nesac\n# Override with user-supplied vision setting from env\nif [ \"${HICLAW_MODEL_VISION:-}\" = \"true\" ]; then\n export MODEL_INPUT='[\"text\", \"image\"]'\nelif [ \"${HICLAW_MODEL_VISION:-}\" = \"false\" ]; then\n export MODEL_INPUT='[\"text\"]'\nfi\n\nlog \"Model: ${MODEL_NAME} (context=${MODEL_CONTEXT_WINDOW}, maxTokens=${MODEL_MAX_TOKENS}, reasoning=${MODEL_REASONING}, input=${MODEL_INPUT})\"\n\nif [ -f /root/manager-workspace/openclaw.json ]; then\n log \"Manager openclaw.json already exists, updating dynamic fields only (preserving user customizations)...\"\n # Merge known models into existing config (add missing, preserve user-added)\n # Use known-models.json (valid JSON) instead of template (contains ${VAR} placeholders)\n KNOWN_MODELS=$(cat /opt/hiclaw/configs/known-models.json 2>/dev/null || echo '[]')\n jq --arg token \"${MANAGER_TOKEN}\" \\\n --arg key \"${HICLAW_MANAGER_GATEWAY_KEY}\" \\\n --arg model \"${MODEL_NAME}\" \\\n --arg emb_model \"${HICLAW_EMBEDDING_MODEL}\" \\\n --arg aigw_domain \"${AI_GATEWAY_DOMAIN}\" \\\n --argjson e2ee \"${MATRIX_E2EE_ENABLED}\" \\\n --argjson known_models \"${KNOWN_MODELS}\" \\\n --argjson ctx \"${MODEL_CONTEXT_WINDOW}\" \\\n --argjson max \"${MODEL_MAX_TOKENS}\" \\\n --argjson reasoning \"${MODEL_REASONING}\" \\\n --argjson input \"${MODEL_INPUT}\" \\\n '\n # Merge known models: add any model id not already present\n .models.providers[\"hiclaw-gateway\"].models as $existing\n | ($existing | map(.id)) as $existing_ids\n | ($known_models | map(select(.id as $id | $existing_ids | index($id) | not))) as $new\n | .models.providers[\"hiclaw-gateway\"].models = ($existing + $new)\n # Ensure the user-chosen default model is in the list (custom model support)\n | if (.models.providers[\"hiclaw-gateway\"].models | map(.id) | index($model) | not) then\n .models.providers[\"hiclaw-gateway\"].models += [{\"id\": $model, \"name\": $model, \"reasoning\": $reasoning, \"contextWindow\": $ctx, \"maxTokens\": $max, \"input\": $input}]\n else . end\n # Rebuild model aliases from the full models list\n | (.models.providers[\"hiclaw-gateway\"].models | map({ (\"hiclaw-gateway/\" + .id): { \"alias\": .id } }) | add // {}) as $aliases\n | .agents.defaults.models = ((.agents.defaults.models // {}) + $aliases)\n | .channels.matrix.accessToken = $token | .models.providers[\"hiclaw-gateway\"].apiKey = $key\n | ((.hooks.token // \"\") as $ht | if $ht == $key or $ht == ($key + \"-hooks\" | @base64) then del(.hooks) else . end)\n | .agents.defaults.model.primary = (\"hiclaw-gateway/\" + $model)\n | .commands.restart = true\n | .gateway.controlUi.dangerouslyDisableDeviceAuth = true\n | .channels.matrix.encryption = $e2ee\n # Ensure memorySearch config exists (embedding model for memory) — skip if embedding model is empty\n | if $emb_model != \"\" then .agents.defaults.memorySearch //= {\"provider\":\"openai\",\"model\":$emb_model,\"remote\":{\"baseUrl\":(\"http://\" + $aigw_domain + \":8080/v1\"),\"apiKey\":$key}} else . end\n ' \\\n /root/manager-workspace/openclaw.json > /tmp/openclaw.json.tmp && \\\n mv /tmp/openclaw.json.tmp /root/manager-workspace/openclaw.json\n # Verify the token was written correctly\n _written_token=$(jq -r '.channels.matrix.accessToken' /root/manager-workspace/openclaw.json 2>/dev/null)\n if [ -z \"${_written_token}\" ] || [ \"${_written_token}\" = \"null\" ]; then\n log \"ERROR: Matrix token was not written correctly to openclaw.json (got: ${_written_token})\"\n else\n log \"Matrix token written to openclaw.json (prefix: ${_written_token:0:10}...)\"\n fi\nelse\n log \"Manager openclaw.json not found, generating from template...\"\n envsubst < /opt/hiclaw/configs/manager-openclaw.json.tmpl > /root/manager-workspace/openclaw.json\n # Post-envsubst injection: memorySearch + custom model (single jq pass when possible)\n if ! jq -e --arg model \"${MODEL_NAME}\" '.models.providers[\"hiclaw-gateway\"].models | map(.id) | index($model)' /root/manager-workspace/openclaw.json > /dev/null 2>&1; then\n log \"Custom model '${MODEL_NAME}' not in built-in list, injecting into config...\"\n jq --arg emb_model \"${HICLAW_EMBEDDING_MODEL}\" \\\n --arg aigw_domain \"${AI_GATEWAY_DOMAIN}\" \\\n --arg key \"${HICLAW_MANAGER_GATEWAY_KEY}\" \\\n --arg model \"${MODEL_NAME}\" \\\n --argjson ctx \"${MODEL_CONTEXT_WINDOW}\" \\\n --argjson max \"${MODEL_MAX_TOKENS}\" \\\n --argjson reasoning \"${MODEL_REASONING}\" \\\n --argjson input \"${MODEL_INPUT}\" \\\n '\n (if $emb_model != \"\" then .agents.defaults.memorySearch = {\"provider\":\"openai\",\"model\":$emb_model,\"remote\":{\"baseUrl\":(\"http://\" + $aigw_domain + \":8080/v1\"),\"apiKey\":$key}} else . end)\n | .models.providers[\"hiclaw-gateway\"].models += [{\"id\": $model, \"name\": $model, \"reasoning\": $reasoning, \"contextWindow\": $ctx, \"maxTokens\": $max, \"input\": $input}]\n | .agents.defaults.models += {(\"hiclaw-gateway/\" + $model): {\"alias\": $model}}\n ' /root/manager-workspace/openclaw.json > /tmp/openclaw.json.tmp && \\\n mv /tmp/openclaw.json.tmp /root/manager-workspace/openclaw.json\n elif [ -n \"${HICLAW_EMBEDDING_MODEL}\" ]; then\n jq --arg emb_model \"${HICLAW_EMBEDDING_MODEL}\" \\\n --arg aigw_domain \"${AI_GATEWAY_DOMAIN}\" \\\n --arg key \"${HICLAW_MANAGER_GATEWAY_KEY}\" \\\n '.agents.defaults.memorySearch = {\"provider\":\"openai\",\"model\":$emb_model,\"remote\":{\"baseUrl\":(\"http://\" + $aigw_domain + \":8080/v1\"),\"apiKey\":$key}}' \\\n /root/manager-workspace/openclaw.json > /tmp/openclaw.json.tmp && \\\n mv /tmp/openclaw.json.tmp /root/manager-workspace/openclaw.json\n fi\n _written_token=$(jq -r '.channels.matrix.accessToken' /root/manager-workspace/openclaw.json 2>/dev/null)\n log \"Matrix token written from template (prefix: ${_written_token:0:10}...)\"\nfi\n\n# Cloud/K8s mode: overlay cloud-specific settings onto generated config\nif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ] || [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n log \"Applying cloud/k8s overlay to openclaw.json...\"\n jq --arg homeserver \"${HICLAW_MATRIX_URL}\" \\\n --arg gateway \"${HICLAW_AI_GATEWAY_URL}/v1\" \\\n --arg key \"${HICLAW_MANAGER_GATEWAY_KEY}\" \\\n '.channels.matrix.homeserver = $homeserver\n | .models.providers[\"hiclaw-gateway\"].baseUrl = $gateway\n | .models.providers[\"hiclaw-gateway\"].apiKey = $key\n | ((.hooks.token // \"\") as $ht | if $ht == $key or $ht == ($key + \"-hooks\" | @base64) then del(.hooks) else . end)\n | .commands.restart = false\n | if .agents.defaults.memorySearch then .agents.defaults.memorySearch.remote.baseUrl = $gateway | .agents.defaults.memorySearch.remote.apiKey = $key else . end' \\\n /root/manager-workspace/openclaw.json > /tmp/openclaw-cloud.json && \\\n mv /tmp/openclaw-cloud.json /root/manager-workspace/openclaw.json\n log \"Cloud/K8s overlay applied\"\nfi\n\n# ============================================================\n# Optional: enable openclaw-cms-plugin observability\n# Config is applied at runtime so secrets stay out of image layers.\n# ============================================================\nCMS_TRACES_ENABLED=\"$(echo \"${HICLAW_CMS_TRACES_ENABLED:-false}\" | tr '[:upper:]' '[:lower:]')\"\nif [ \"${CMS_TRACES_ENABLED}\" = \"true\" ]; then\n CMS_PLUGIN_NAME=\"openclaw-cms-plugin\"\n CMS_PLUGIN_DIR=\"${OPENCLAW_CMS_PLUGIN_DIR:-/opt/openclaw/extensions/openclaw-cms-plugin}\"\n CMS_PLUGIN_MANIFEST=\"${CMS_PLUGIN_DIR}/openclaw.plugin.json\"\n DIAG_PLUGIN_NAME=\"diagnostics-otel\"\n DIAG_PLUGIN_DIR=\"/opt/openclaw/extensions/diagnostics-otel\"\n CMS_LICENSE_KEY=\"${HICLAW_CMS_LICENSE_KEY:-}\"\n CMS_PROJECT=\"${HICLAW_CMS_PROJECT:-}\"\n CMS_METRICS_ENABLED=\"${HICLAW_CMS_METRICS_ENABLED:-false}\"\n\n if [ ! -f \"${CMS_PLUGIN_MANIFEST}\" ]; then\n log \"WARNING: ${CMS_PLUGIN_NAME} manifest not found at ${CMS_PLUGIN_MANIFEST}, skipping plugin config\"\n else\n _missing=0\n [ -z \"${HICLAW_CMS_ENDPOINT:-}\" ] && log \"WARNING: HICLAW_CMS_ENDPOINT is required when HICLAW_CMS_TRACES_ENABLED=true\" && _missing=1\n [ -z \"${CMS_LICENSE_KEY:-}\" ] && log \"WARNING: HICLAW_CMS_LICENSE_KEY is required when HICLAW_CMS_TRACES_ENABLED=true\" && _missing=1\n [ -z \"${HICLAW_CMS_WORKSPACE:-}\" ] && log \"WARNING: HICLAW_CMS_WORKSPACE is required when HICLAW_CMS_TRACES_ENABLED=true\" && _missing=1\n\n if [ \"${_missing}\" = \"0\" ]; then\n CMS_SERVICE_NAME=\"${HICLAW_CMS_SERVICE_NAME:-hiclaw-manager}\"\n CMS_ENABLE_METRICS=\"${CMS_METRICS_ENABLED}\"\n DIAG_AVAILABLE=\"0\"\n _metrics_lc=\"$(echo \"${CMS_ENABLE_METRICS}\" | tr '[:upper:]' '[:lower:]')\"\n if [ \"${_metrics_lc}\" = \"true\" ]; then\n if [ -f \"${DIAG_PLUGIN_DIR}/package.json\" ]; then\n DIAG_AVAILABLE=\"1\"\n if [ ! -d \"${DIAG_PLUGIN_DIR}/node_modules\" ]; then\n log \"diagnostics-otel dependencies missing, installing...\"\n if (cd \"${DIAG_PLUGIN_DIR}\" && npm install --omit=dev --ignore-scripts >/tmp/hiclaw-diag-install.log 2>&1); then\n log \"diagnostics-otel dependencies installed\"\n else\n log \"WARNING: diagnostics-otel npm install failed, metrics plugin may not load\"\n fi\n else\n log \"diagnostics-otel dependencies already present\"\n fi\n else\n log \"WARNING: diagnostics-otel package.json not found at ${DIAG_PLUGIN_DIR}, metrics plugin may not load\"\n fi\n fi\n\n log \"Applying ${CMS_PLUGIN_NAME} config to openclaw.json...\"\n jq --arg pluginName \"${CMS_PLUGIN_NAME}\" \\\n --arg pluginDir \"${CMS_PLUGIN_DIR}\" \\\n --arg endpoint \"${HICLAW_CMS_ENDPOINT}\" \\\n --arg licenseKey \"${CMS_LICENSE_KEY}\" \\\n --arg armsProject \"${CMS_PROJECT}\" \\\n --arg cmsWorkspace \"${HICLAW_CMS_WORKSPACE}\" \\\n --arg serviceName \"${CMS_SERVICE_NAME}\" \\\n --arg diagPluginName \"${DIAG_PLUGIN_NAME}\" \\\n --arg diagPluginDir \"${DIAG_PLUGIN_DIR}\" \\\n --arg metricsRaw \"${CMS_ENABLE_METRICS}\" \\\n --arg diagAvailableRaw \"${DIAG_AVAILABLE}\" \\\n '\n .plugins = (.plugins // {})\n | .plugins.load = (.plugins.load // {})\n | .plugins.entries = (.plugins.entries // {})\n | if (.plugins.allow | type) != \"array\" then .plugins.allow = [] else . end\n | if (.plugins.allow | index($pluginName)) == null then .plugins.allow += [$pluginName] else . end\n | if (.plugins.load.paths | type) != \"array\" then .plugins.load.paths = [] else . end\n | if (.plugins.load.paths | index($pluginDir)) == null then .plugins.load.paths += [$pluginDir] else . end\n | .plugins.entries[$pluginName] = {\n \"enabled\": true,\n \"config\": {\n \"endpoint\": $endpoint,\n \"headers\": {\n \"x-arms-license-key\": $licenseKey,\n \"x-arms-project\": $armsProject,\n \"x-cms-workspace\": $cmsWorkspace\n },\n \"serviceName\": $serviceName\n }\n }\n\n # diagnostics-otel metrics (optional)\n | ($metricsRaw | ascii_downcase) as $m\n | ($diagAvailableRaw == \"1\") as $diagAvailable\n | (($m == \"true\") and $diagAvailable) as $metricsEnabled\n | if $metricsEnabled then\n (if (.plugins.allow | index($diagPluginName)) == null then .plugins.allow += [$diagPluginName] else . end)\n | (if (.plugins.load.paths | index($diagPluginDir)) == null then .plugins.load.paths += [$diagPluginDir] else . end)\n | .plugins.entries[$diagPluginName].enabled = true\n | .diagnostics = (.diagnostics // {})\n | .diagnostics.otel = (.diagnostics.otel // {})\n | .diagnostics.enabled = true\n | .diagnostics.otel.enabled = true\n | .diagnostics.otel.endpoint = $endpoint\n | .diagnostics.otel.protocol = (.diagnostics.otel.protocol // \"http/protobuf\")\n | .diagnostics.otel.headers = {\n \"x-arms-license-key\": $licenseKey,\n \"x-arms-project\": $armsProject,\n \"x-cms-workspace\": $cmsWorkspace\n }\n | .diagnostics.otel.serviceName = $serviceName\n | .diagnostics.otel.metrics = true\n | .diagnostics.otel.traces = (.diagnostics.otel.traces // false)\n | .diagnostics.otel.logs = (.diagnostics.otel.logs // false)\n else\n .\n end\n ' /root/manager-workspace/openclaw.json > /tmp/openclaw-cms.json && \\\n mv /tmp/openclaw-cms.json /root/manager-workspace/openclaw.json\n log \"${CMS_PLUGIN_NAME} config applied (metrics=${CMS_ENABLE_METRICS}, service=${CMS_SERVICE_NAME})\"\n else\n log \"Skipping ${CMS_PLUGIN_NAME} config due to missing required env vars\"\n fi\n fi\nfi\n\n# ============================================================\n# Detect container runtime (for Worker creation)\n# ============================================================\nsource /opt/hiclaw/scripts/lib/container-api.sh\nif container_api_available; then\n log \"Container runtime socket detected at ${CONTAINER_SOCKET} — direct Worker creation enabled\"\n export HICLAW_CONTAINER_RUNTIME=\"socket\"\nelif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ] || [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n log \"Cloud/K8s mode — Workers created via controller API\"\n export HICLAW_CONTAINER_RUNTIME=\"cloud\"\nelse\n log \"No container runtime found — Worker creation will output install commands\"\n export HICLAW_CONTAINER_RUNTIME=\"none\"\nfi\n\n# ============================================================\n# Upgrade Worker openclaw.json: merge known models + E2EE flag into existing configs\n# Existing workers in MinIO may have old single-model configs or missing encryption field.\n# Merge template models so they can hot-switch without restart.\n# ============================================================\nREGISTRY_FILE=\"/root/manager-workspace/workers-registry.json\"\nif [ -f \"${REGISTRY_FILE}\" ]; then\n # Use known-models.json (valid JSON) instead of template (contains ${VAR} placeholders)\n KNOWN_MODELS_FILE=\"/opt/hiclaw/configs/known-models.json\"\n if [ -f \"${KNOWN_MODELS_FILE}\" ]; then\n _KNOWN_MODELS=$(cat \"${KNOWN_MODELS_FILE}\")\n for _wname in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_wname}\" ] && continue\n _minio_path=\"${HICLAW_STORAGE_PREFIX}/agents/${_wname}/openclaw.json\"\n _tmp_in=\"/tmp/openclaw-${_wname}-models-upgrade-in.json\"\n if mc cp \"${_minio_path}\" \"${_tmp_in}\" 2>/dev/null; then\n _tmp_out=\"/tmp/openclaw-${_wname}-models-upgrade-out.json\"\n # Idempotent merge: add missing known models, rebuild aliases, set e2ee.\n # Always runs — jq deduplicates by model id, so re-runs are safe.\n jq --argjson known_models \"${_KNOWN_MODELS}\" \\\n --argjson e2ee \"${MATRIX_E2EE_ENABLED}\" '\n .models.providers[\"hiclaw-gateway\"].models as $existing\n | ($existing | map(.id)) as $existing_ids\n | ($known_models | map(select(.id as $id | $existing_ids | index($id) | not))) as $new\n | .models.providers[\"hiclaw-gateway\"].models = ($existing + $new)\n | (.models.providers[\"hiclaw-gateway\"].models | map({ (\"hiclaw-gateway/\" + .id): { \"alias\": .id } }) | add // {}) as $aliases\n | .agents.defaults.models = ((.agents.defaults.models // {}) + $aliases)\n | .channels.matrix.encryption = $e2ee\n ' \"${_tmp_in}\" > \"${_tmp_out}\" 2>/dev/null\n if ! diff -q \"${_tmp_in}\" \"${_tmp_out}\" > /dev/null 2>&1; then\n if mc cp \"${_tmp_out}\" \"${_minio_path}\" 2>/dev/null; then\n _new_count=$(jq '.models.providers[\"hiclaw-gateway\"].models | length' \"${_tmp_out}\" 2>/dev/null)\n log \"Worker ${_wname}: upgraded openclaw.json (models: ${_new_count}, e2ee: ${MATRIX_E2EE_ENABLED})\"\n fi\n fi\n rm -f \"${_tmp_in}\" \"${_tmp_out}\"\n fi\n done\n fi\nfi\n\n# ============================================================\n# Ensure Worker Matrix password files exist in MinIO (E2EE fix)\n# Workers need to re-login on restart to get a fresh device_id.\n# Older workers created before this fix won't have the password file.\n# ============================================================\nif [ -f \"${REGISTRY_FILE}\" ]; then\n for _wname in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_wname}\" ] && continue\n _creds_file=\"/data/worker-creds/${_wname}.env\"\n if [ -f \"${_creds_file}\" ]; then\n # Check if password file already exists in MinIO\n if ! mc stat \"${HICLAW_STORAGE_PREFIX}/agents/${_wname}/credentials/matrix/password\" > /dev/null 2>&1; then\n source \"${_creds_file}\"\n if [ -n \"${WORKER_PASSWORD}\" ]; then\n _tmp_pw=\"/tmp/matrix-pw-${_wname}\"\n echo -n \"${WORKER_PASSWORD}\" > \"${_tmp_pw}\"\n mc cp \"${_tmp_pw}\" \"${HICLAW_STORAGE_PREFIX}/agents/${_wname}/credentials/matrix/password\" 2>/dev/null \\\n && log \"Worker ${_wname}: wrote Matrix password to MinIO (E2EE re-login fix)\" \\\n || log \"Worker ${_wname}: WARNING: failed to write Matrix password to MinIO\"\n rm -f \"${_tmp_pw}\"\n fi\n fi\n fi\n done\nfi\n\n# ============================================================\n# Recreate Worker containers as needed after Manager restart.\n# Workers are on hiclaw-net; Docker DNS resolves *-local.hiclaw.io via\n# the Manager's network aliases, so IP changes don't require worker recreation.\n# Only recreate stopped/missing workers.\n# ============================================================\nif container_api_available; then\n REGISTRY_FILE=\"/root/manager-workspace/workers-registry.json\"\n if [ -f \"${REGISTRY_FILE}\" ]; then\n for _worker_name in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_worker_name}\" ] && continue\n\n # Skip remote workers — they are not Manager-managed containers.\n _deployment=$(jq -r --arg w \"${_worker_name}\" '.workers[$w].deployment // \"local\"' \"${REGISTRY_FILE}\" 2>/dev/null)\n if [ \"${_deployment}\" = \"remote\" ]; then\n log \"Worker ${_worker_name} is remote, skipping container recreate\"\n continue\n fi\n\n _status=$(container_status_worker \"${_worker_name}\")\n if [ \"${_status}\" = \"running\" ]; then\n log \"Worker running: ${_worker_name}, skipping\"\n continue\n fi\n # Container missing or stopped — recreate.\n log \"Worker container ${_status}: ${_worker_name}, recreating...\"\n _creds_file=\"/data/worker-creds/${_worker_name}.env\"\n if [ -f \"${_creds_file}\" ]; then\n source \"${_creds_file}\"\n _runtime=$(jq -r --arg w \"${_worker_name}\" '.workers[$w].runtime // \"openclaw\"' \"${REGISTRY_FILE}\" 2>/dev/null)\n _recreated=false\n for _attempt in 1 2 3; do\n local _env_map _create_body\n _env_map=$(jq -cn \\\n --arg name \"${_worker_name}\" \\\n --arg fak \"${_worker_name}\" \\\n --arg fsk \"${WORKER_MINIO_PASSWORD:-}\" \\\n --arg fs_domain \"${HICLAW_FS_DOMAIN:-fs-local.hiclaw.io}\" \\\n --arg controller_url \"${HICLAW_CONTROLLER_URL:-}\" \\\n '{\n \"HICLAW_WORKER_NAME\": $name,\n \"HICLAW_FS_ENDPOINT\": (\"http://\" + ($fs_domain | split(\":\")[0]) + \":8080\"),\n \"HICLAW_FS_ACCESS_KEY\": $fak,\n \"HICLAW_FS_SECRET_KEY\": $fsk\n }\n | if $controller_url != \"\" then . + {\"HICLAW_CONTROLLER_URL\": $controller_url} else . end')\n _create_body=$(jq -cn --arg name \"${_worker_name}\" --arg runtime \"${_runtime}\" --argjson env \"${_env_map}\" '{name: $name, runtime: $runtime, env: $env}')\n worker_backend_create \"${_create_body}\" > /dev/null 2>&1 && _recreated=true && break\n log \" Attempt ${_attempt}/3 failed for ${_worker_name}, retrying in $((5 * _attempt))s...\"\n sleep $((5 * _attempt))\n done\n if [ \"${_recreated}\" = true ]; then\n log \" Recreated ${_runtime} worker: ${_worker_name}\"\n else\n log \" ERROR: Failed to recreate ${_worker_name} after 3 attempts\"\n fi\n else\n log \" WARNING: No credentials found for ${_worker_name} (${_creds_file} missing), skipping\"\n fi\n done\n fi\nfi\n\n# ============================================================\n# Notify workers of builtin updates if upgrade happened\n# Builtin files (AGENTS.md, skills) are already synced by upgrade-builtins.sh\n#\n# Cooldown: skip notification if the last successful notify was within\n# NOTIFY_COOLDOWN_SECS (default 3600s / 1 hour). This prevents repeated\n# notifications when the Manager crash-loops and re-runs upgrade-builtins\n# on every restart (e.g. IMAGE_VERSION=latest always triggers upgrade).\n# ============================================================\nNOTIFY_COOLDOWN_SECS=\"${HICLAW_NOTIFY_COOLDOWN_SECS:-3600}\"\nNOTIFY_TS_FILE=\"/root/manager-workspace/.last-worker-notify-ts\"\n\nif [ -f /root/manager-workspace/.upgrade-pending-worker-notify ]; then\n _now=$(date +%s)\n _last_notify=$(cat \"${NOTIFY_TS_FILE}\" 2>/dev/null || echo \"0\")\n _elapsed=$(( _now - _last_notify ))\n\n if [ \"${_elapsed}\" -lt \"${NOTIFY_COOLDOWN_SECS}\" ]; then\n log \"Skipping worker builtin notification (last notify ${_elapsed}s ago, cooldown ${NOTIFY_COOLDOWN_SECS}s)\"\n rm -f /root/manager-workspace/.upgrade-pending-worker-notify\n else\n log \"Notifying workers about builtin updates...\"\n REGISTRY_FILE=\"/root/manager-workspace/workers-registry.json\"\n _notify_ok=false\n if [ -f \"${REGISTRY_FILE}\" ]; then\n for _worker_name in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_worker_name}\" ] && continue\n _room_id=$(jq -r --arg w \"${_worker_name}\" '.workers[$w].room_id // empty' \"${REGISTRY_FILE}\" 2>/dev/null)\n if [ -n \"${_room_id}\" ]; then\n _worker_id=\"@${_worker_name}:${MATRIX_DOMAIN}\"\n _txn_id=\"upgrade-$(date +%s%N)\"\n _msg=\"@${_worker_name}:${MATRIX_DOMAIN} Manager upgraded builtin files (AGENTS.md, skills). Please use your file-sync skill to sync the latest config.\"\n _raw=$(curl -s -w '\\nHTTP_CODE:%{http_code}' -X PUT \\\n \"${HICLAW_MATRIX_URL}/_matrix/client/v3/rooms/${_room_id}/send/m.room.message/${_txn_id}\" \\\n -H \"Authorization: Bearer ${MANAGER_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d \"{\\\"msgtype\\\":\\\"m.text\\\",\\\"body\\\":\\\"${_msg}\\\",\\\"m.mentions\\\":{\\\"user_ids\\\":[\\\"${_worker_id}\\\"]}}\" \\\n 2>&1) || true\n _http_code=$(echo \"${_raw}\" | tail -1 | sed 's/HTTP_CODE://')\n _notify_resp=$(echo \"${_raw}\" | sed '$d')\n if echo \"${_notify_resp}\" | jq -e '.event_id' > /dev/null 2>&1; then\n log \" Notified ${_worker_name}\"; _notify_ok=true\n else\n log \" WARNING: Failed to notify ${_worker_name} (HTTP ${_http_code}): ${_notify_resp}\"\n fi\n fi\n done\n fi\n # Record timestamp only if at least one notification succeeded\n if [ \"${_notify_ok}\" = true ]; then\n echo \"${_now}\" > \"${NOTIFY_TS_FILE}\"\n fi\n rm -f /root/manager-workspace/.upgrade-pending-worker-notify\n fi\nfi\n\n# ============================================================\n# Start Manager Agent\n# ============================================================\nlog \"Starting Manager Agent (${MANAGER_RUNTIME})...\"\n\n# HOME is already set to /root/manager-workspace via docker run -e HOME=...\ncd \"${HOME}\"\n\n# Ensure host credential symlinks exist under HOME\nif [ -d \"/host-share\" ]; then\n [ -f \"/host-share/.gitconfig\" ] && ln -sf \"/host-share/.gitconfig\" \"${HOME}/.gitconfig\"\nfi\n\nlog \"HOME=${HOME} (manager-workspace, host-mounted)\"\n\n# ── Render agent doc templates ────────────────────────────────────────────\n# Replace ${VAR} placeholders with actual values so the AI agent reads\n# plain text and never needs to resolve environment variables.\nexport MANAGER_MATRIX_TOKEN MANAGER_TOKEN HIGRESS_COOKIE_FILE\nRENDER=/opt/hiclaw/scripts/lib/render-skills.sh\nlog \"Rendering agent doc templates...\"\n# Manager-owned docs (workspace)\nbash \"$RENDER\" /root/manager-workspace/skills\nbash \"$RENDER\" /root/manager-workspace/skills-alpha\nbash \"$RENDER\" /root/manager-workspace AGENTS.md TOOLS.md HEARTBEAT.md SOUL.md\n# Worker templates (workspace + image) — rendered before push to MinIO\n# so Workers (including remote pip-install) receive plain text\nbash \"$RENDER\" /root/manager-workspace/worker-skills\nbash \"$RENDER\" /root/manager-workspace/worker-agent\nbash \"$RENDER\" /root/manager-workspace/copaw-worker-agent\nbash \"$RENDER\" /opt/hiclaw/agent/worker-skills\nbash \"$RENDER\" /opt/hiclaw/agent/worker-agent\nbash \"$RENDER\" /opt/hiclaw/agent/copaw-worker-agent\nlog \"Agent doc templates rendered\"\n\n# Cloud mode: start background file sync (workspace ↔ OSS) and initial push\nif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n log \"Syncing initial workspace to OSS...\"\n ensure_mc_credentials\n mc mirror /root/manager-workspace/ \"${HICLAW_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" 2>/dev/null || true\n\n # Local → OSS: change-triggered sync\n (\n while true; do\n CHANGED=$(find /root/manager-workspace/ -type f -newermt \"15 seconds ago\" 2>/dev/null | head -1)\n if [ -n \"${CHANGED}\" ]; then\n ensure_mc_credentials 2>/dev/null || true\n mc mirror /root/manager-workspace/ \"${HICLAW_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" --exclude \".npm/**\" \\\n --exclude \".local/**\" --exclude \".mc/**\" 2>/dev/null || true\n fi\n sleep 10\n done\n ) &\n log \"Local→OSS sync started (PID: $!)\"\n\n # OSS → Local: periodic pull (shared data, agent configs)\n (\n while true; do\n sleep 300\n ensure_mc_credentials 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/shared/\" /root/hiclaw-fs/shared/ --overwrite --newer-than \"5m\" 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/agents/\" /root/hiclaw-fs/agents/ --overwrite --newer-than \"5m\" 2>/dev/null || true\n done\n ) &\n log \"OSS→Local sync started (every 5m, PID: $!)\"\nfi\n\n# K8s mode: start background file sync (workspace ↔ MinIO)\nif [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n log \"Syncing initial workspace to MinIO...\"\n mc mirror /root/manager-workspace/ \"${HICLAW_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" 2>/dev/null || true\n\n # Local → MinIO: change-triggered sync\n (\n while true; do\n CHANGED=$(find /root/manager-workspace/ -type f -newermt \"15 seconds ago\" 2>/dev/null | head -1)\n if [ -n \"${CHANGED}\" ]; then\n mc mirror /root/manager-workspace/ \"${HICLAW_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" --exclude \".npm/**\" \\\n --exclude \".local/**\" --exclude \".mc/**\" 2>/dev/null || true\n fi\n sleep 10\n done\n ) &\n log \"Local→MinIO sync started (PID: $!)\"\n\n # MinIO → Local: periodic pull (shared data, agent configs)\n (\n while true; do\n sleep 300\n mc mirror \"${HICLAW_STORAGE_PREFIX}/shared/\" /root/hiclaw-fs/shared/ --overwrite --newer-than \"5m\" 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/agents/\" /root/hiclaw-fs/agents/ --overwrite --newer-than \"5m\" 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/hiclaw-config/\" /root/hiclaw-fs/hiclaw-config/ --overwrite --newer-than \"15s\" 2>/dev/null || true\n done\n ) &\n log \"MinIO→Local sync started (every 5m, PID: $!)\"\nfi\n\n# ============================================================\n# Auto-generate Manager mcporter config for pre-configured MCP servers\n# If HICLAW_GITHUB_TOKEN was set at install time, setup-higress.sh already\n# configured GitHub MCP on Higress. Run setup-mcp-server.sh now so that\n# config/mcporter.json exists when the Agent starts — no need to ask user for PAT.\n# ============================================================\nif [ -n \"${HICLAW_GITHUB_TOKEN}\" ] && [ \"${HICLAW_RUNTIME}\" != \"aliyun\" ] && [ \"${HICLAW_RUNTIME}\" != \"k8s\" ]; then\n if [ ! -f \"${HOME}/config/mcporter.json\" ]; then\n log \"Auto-generating Manager mcporter config for GitHub MCP (HICLAW_GITHUB_TOKEN set)...\"\n bash /opt/hiclaw/agent/skills/mcp-server-management/scripts/setup-mcp-server.sh \\\n github \"${HICLAW_GITHUB_TOKEN}\" 2>&1 | while IFS= read -r line; do log \" [setup-mcp] ${line}\"; done || \\\n log \"WARNING: setup-mcp-server.sh failed — Agent may need to configure GitHub MCP manually\"\n else\n log \"Manager mcporter config already exists, skipping auto-generate\"\n fi\nfi\n\n# ============================================================\n# Runtime-specific startup\n# ============================================================\nif [ \"${MANAGER_RUNTIME}\" = \"copaw\" ]; then\n # Delegate to CoPaw startup script\n exec /opt/hiclaw/scripts/init/start-copaw-manager.sh\nelse\n # ── OpenClaw Runtime ─────────────────────────────────────────────────────\n log \"Starting OpenClaw Manager...\"\n\n export OPENCLAW_CONFIG_PATH=\"/root/manager-workspace/openclaw.json\"\n\n # Symlink to default OpenClaw config path so CLI commands find the config\n mkdir -p \"${HOME}/.openclaw\"\n ln -sf \"/root/manager-workspace/openclaw.json\" \"${HOME}/.openclaw/openclaw.json\"\n\n # Clean orphaned session write locks (e.g. from SIGKILL or crash before exit handlers)\n # Prevents \"session file locked (timeout 10000ms)\" when PID was reused\n find \"${HOME}/.openclaw/agents\" -name \"*.jsonl.lock\" -delete 2>/dev/null || true\n log \"Cleaned up any orphaned session write locks\"\n\n # Clean Matrix crypto storage (SQLite WAL may be corrupted after unclean shutdown)\n # Crypto state is re-negotiated on startup; losing it only means re-establishing E2EE sessions\n rm -rf \"${HOME}/.openclaw/matrix\" 2>/dev/null || true\n log \"Cleaned Matrix crypto storage (will re-establish E2EE sessions)\"\n\n # Launch OpenClaw\n # Disable full-process respawn so the CLI uses its internal restart loop.\n # Without this, config reload spawns a detached child and exits, then\n # supervisord restarts the CLI — resulting in two gateway processes.\n export OPENCLAW_NO_RESPAWN=1\n exec openclaw gateway run --verbose --force\nfi\n"}, "e30b736987e16f9b8f88212834fc98404ba41f86": {"t": 1785764757.1347268, "n": 63705, "h": "7376fbcd7c00f2f9", "c": "#!/bin/bash\n# start-manager-agent.sh - Initialize and start the Manager Agent\n# Supports local (supervisord), cloud (SAE), and K8s (Helm) deployments.\n# In local mode this is the last supervisord component to start (priority 800).\n# In cloud/k8s mode (HICLAW_RUNTIME=aliyun|k8s) this is the container entrypoint.\n#\n# Runtime selection:\n# HICLAW_MANAGER_RUNTIME=openclaw (default) - OpenClaw gateway mode\n# HICLAW_MANAGER_RUNTIME=copaw - CoPaw workspace mode\n\nsource /opt/hiclaw/scripts/lib/hiclaw-env.sh\n\n# ============================================================\n# Runtime selection\n# ============================================================\nMANAGER_RUNTIME=\"${HICLAW_MANAGER_RUNTIME:-openclaw}\"\ncase \"${MANAGER_RUNTIME}\" in\n copaw)\n log \"Manager runtime: CoPaw (Python workspace)\"\n ;;\n *)\n log \"Manager runtime: OpenClaw (Node.js gateway)\"\n MANAGER_RUNTIME=\"openclaw\"\n ;;\nesac\n\n# ============================================================\n# Set timezone from TZ env var\n# ============================================================\nif [ -n \"${TZ}\" ] && [ -f \"/usr/share/zoneinfo/${TZ}\" ]; then\n ln -sf \"/usr/share/zoneinfo/${TZ}\" /etc/localtime\n echo \"${TZ}\" > /etc/timezone\n log \"Timezone set to ${TZ}\"\nfi\n\nexport MATRIX_DOMAIN=\"${HICLAW_MATRIX_DOMAIN:-matrix-local.hiclaw.io:8080}\"\nAI_GATEWAY_DOMAIN=\"${HICLAW_AI_GATEWAY_DOMAIN:-aigw-local.hiclaw.io}\"\n\n# ============================================================\n# Cloud/K8s mode: validate required environment variables + initial credentials\n# ============================================================\nif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ] || [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n : \"${HICLAW_MATRIX_URL:?HICLAW_MATRIX_URL is required}\"\n : \"${HICLAW_MATRIX_DOMAIN:?HICLAW_MATRIX_DOMAIN is required}\"\n : \"${HICLAW_AI_GATEWAY_URL:?HICLAW_AI_GATEWAY_URL is required}\"\n if [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n : \"${HICLAW_MANAGER_GATEWAY_KEY:?HICLAW_MANAGER_GATEWAY_KEY is required}\"\n : \"${HICLAW_MANAGER_PASSWORD:?HICLAW_MANAGER_PASSWORD is required (cloud containers are stateless, password must be injected)}\"\n fi\n if [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n # K8s mode: controller handles initialization (admin registration, Higress setup).\n # Manager only needs credentials injected by the ManagerReconciler.\n : \"${HICLAW_MANAGER_GATEWAY_KEY:?HICLAW_MANAGER_GATEWAY_KEY is required (injected by controller)}\"\n : \"${HICLAW_MANAGER_PASSWORD:?HICLAW_MANAGER_PASSWORD is required (injected by controller)}\"\n else\n # Cloud (aliyun) mode: Manager still does its own initialization\n : \"${HICLAW_REGISTRATION_TOKEN:?HICLAW_REGISTRATION_TOKEN is required}\"\n : \"${HICLAW_ADMIN_USER:?HICLAW_ADMIN_USER is required}\"\n : \"${HICLAW_ADMIN_PASSWORD:?HICLAW_ADMIN_PASSWORD is required}\"\n fi\n log \"${HICLAW_RUNTIME} mode: validating environment... OK\"\n log \" Matrix: ${HICLAW_MATRIX_URL}, AI Gateway: ${HICLAW_AI_GATEWAY_URL}, Storage: ${HICLAW_FS_BUCKET}\"\n if [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n ensure_mc_credentials || { log \"FATAL: Initial STS credential fetch failed\"; exit 1; }\n fi\nfi\n\n# ============================================================\n# Local mode: host symlinks, /etc/hosts, wait for local services\n# ============================================================\nif [ \"${HICLAW_RUNTIME}\" != \"aliyun\" ] && [ \"${HICLAW_RUNTIME}\" != \"k8s\" ]; then\n # Create symlink for host directory access\n if [ -d \"/host-share\" ]; then\n ORIGINAL_HOST_HOME=\"${HOST_ORIGINAL_HOME:-$HOME}\"\n if [ ! -e \"${ORIGINAL_HOST_HOME}\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/root\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/data\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/host-share\" ]; then\n mkdir -p \"$(dirname \"${ORIGINAL_HOST_HOME}\")\"\n ln -sfn /host-share \"${ORIGINAL_HOST_HOME}\"\n log \"Created symlink: ${ORIGINAL_HOST_HOME} -> /host-share\"\n else\n ln -sfn /host-share /root/host-home\n log \"Created fallback symlink: /root/host-home -> /host-share\"\n fi\n fi\n\n # Add local domains to /etc/hosts\n HOSTS_DOMAINS=\"${MATRIX_DOMAIN%%:*} ${HICLAW_MATRIX_CLIENT_DOMAIN:-matrix-client-local.hiclaw.io} ${AI_GATEWAY_DOMAIN} ${HICLAW_FS_DOMAIN:-fs-local.hiclaw.io}\"\n if ! grep -q \"${AI_GATEWAY_DOMAIN}\" /etc/hosts 2>/dev/null; then\n echo \"127.0.0.1 ${HOSTS_DOMAINS}\" >> /etc/hosts\n log \"Added local domains to /etc/hosts\"\n fi\n\n # Wait for local infrastructure\n waitForService \"Higress Gateway\" \"127.0.0.1\" 8080 180\n waitForService \"Higress Console\" \"127.0.0.1\" 8001 180\n waitForService \"Tuwunel\" \"127.0.0.1\" 6167 120\n waitForHTTP \"Tuwunel Matrix API\" \"${HICLAW_MATRIX_URL}/_tuwunel/server_version\" 120\n waitForService \"MinIO\" \"127.0.0.1\" 9000 120\nelse\n # Cloud/K8s mode: wait for external Tuwunel\n log \"Waiting for Tuwunel Matrix server at ${HICLAW_MATRIX_URL}...\"\n _retry=0\n while [ \"${_retry}\" -lt 30 ]; do\n if curl -sf \"${HICLAW_MATRIX_URL}/_matrix/client/versions\" > /dev/null 2>&1; then\n log \"Tuwunel is ready\"\n break\n fi\n _retry=$((_retry + 1))\n log \" Waiting for Tuwunel (attempt ${_retry}/30)...\"\n sleep 5\n done\n if [ \"${_retry}\" -ge 30 ]; then\n log \"ERROR: Tuwunel not reachable at ${HICLAW_MATRIX_URL}\"\n exit 1\n fi\nfi\n\n# ============================================================\n# Auto-generate secrets if not provided via environment\n# Persisted to /data so they survive container restart\n# ============================================================\nSECRETS_FILE=\"/data/hiclaw-secrets.env\"\nif [ -f \"${SECRETS_FILE}\" ]; then\n source \"${SECRETS_FILE}\"\n log \"Loaded persisted secrets from ${SECRETS_FILE}\"\nfi\n\nif [ -z \"${HICLAW_MANAGER_GATEWAY_KEY}\" ]; then\n export HICLAW_MANAGER_GATEWAY_KEY=\"$(generateKey 32)\"\n log \"Auto-generated HICLAW_MANAGER_GATEWAY_KEY\"\nfi\nif [ -z \"${HICLAW_MANAGER_PASSWORD}\" ]; then\n export HICLAW_MANAGER_PASSWORD=\"$(generateKey 16)\"\n log \"Auto-generated HICLAW_MANAGER_PASSWORD\"\nfi\n\n# Persist secrets so they survive supervisord restart\nmkdir -p /data\ncat > \"${SECRETS_FILE}\" <<EOF\nexport HICLAW_MANAGER_GATEWAY_KEY=\"${HICLAW_MANAGER_GATEWAY_KEY}\"\nexport HICLAW_MANAGER_PASSWORD=\"${HICLAW_MANAGER_PASSWORD}\"\nEOF\nchmod 600 \"${SECRETS_FILE}\"\n\n# Cloud mode: pull workspace from OSS before initialization\nif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n HICLAW_FS=\"/root/hiclaw-fs\"\n mkdir -p \"${HICLAW_FS}/shared\" \"${HICLAW_FS}/agents\"\n log \"Pulling workspace from OSS...\"\n ensure_mc_credentials\n mc mirror \"${HICLAW_STORAGE_PREFIX}/manager/\" /root/manager-workspace/ --overwrite 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/shared/\" \"${HICLAW_FS}/shared/\" --overwrite 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/agents/\" \"${HICLAW_FS}/agents/\" --overwrite 2>/dev/null || true\n # Symlink hiclaw-fs into workspace for agent access\n ln -sfn \"${HICLAW_FS}\" /root/manager-workspace/hiclaw-fs\nfi\n\n# K8s mode: sync workspace from cluster-internal MinIO\nif [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n HICLAW_FS=\"/root/hiclaw-fs\"\n mkdir -p \"${HICLAW_FS}/shared\" \"${HICLAW_FS}/agents\" \"${HICLAW_FS}/hiclaw-config\"\n log \"Configuring mc alias for cluster MinIO...\"\n mc alias set hiclaw \"${HICLAW_FS_ENDPOINT}\" \"${HICLAW_FS_ACCESS_KEY}\" \"${HICLAW_FS_SECRET_KEY}\"\n log \"Syncing workspace from MinIO...\"\n mc mirror \"${HICLAW_STORAGE_PREFIX}/\" \"${HICLAW_FS}/\" --overwrite 2>/dev/null || true\n ln -sfn \"${HICLAW_FS}\" /root/manager-workspace/hiclaw-fs\n touch \"${HICLAW_FS}/.initialized\"\nfi\n\n# ============================================================\n# Initialize / upgrade Manager workspace\n# First boot: full init via upgrade-builtins.sh\n# Subsequent boots: compare image version; upgrade only if changed\n# ============================================================\nmkdir -p /root/manager-workspace\n\nIMAGE_VERSION=$(cat /opt/hiclaw/agent/.builtin-version 2>/dev/null || echo \"unknown\")\nINSTALLED_VERSION=$(cat /root/manager-workspace/.builtin-version 2>/dev/null || echo \"\")\n\nif [ ! -f /root/manager-workspace/.initialized ]; then\n log \"First boot: initializing manager workspace...\"\n bash /opt/hiclaw/scripts/init/upgrade-builtins.sh\n touch /root/manager-workspace/.initialized\n log \"Manager workspace initialized (version: ${IMAGE_VERSION})\"\nelif [ \"${IMAGE_VERSION}\" != \"${INSTALLED_VERSION}\" ] || [ \"${IMAGE_VERSION}\" = \"latest\" ]; then\n log \"Upgrade detected: ${INSTALLED_VERSION} -> ${IMAGE_VERSION}${IMAGE_VERSION:+ (latest: always upgrade)}\"\n bash /opt/hiclaw/scripts/init/upgrade-builtins.sh\n log \"Manager workspace upgraded to version: ${IMAGE_VERSION}\"\nelse\n log \"Workspace up to date (version: ${IMAGE_VERSION})\"\nfi\n\n# Local mode: wait for mc mirror initialization (shared + worker data in /root/hiclaw-fs/)\nif [ \"${HICLAW_RUNTIME}\" != \"aliyun\" ] && [ \"${HICLAW_RUNTIME}\" != \"k8s\" ]; then\n log \"Waiting for MinIO storage initialization...\"\n _minio_wait=0\n while [ ! -f /root/hiclaw-fs/.initialized ]; do\n sleep 2\n _minio_wait=$(( _minio_wait + 1 ))\n if [ \"${_minio_wait}\" -ge 60 ]; then\n log \"ERROR: MinIO storage initialization timed out after 120s\"\n exit 1\n fi\n done\n log \"MinIO storage initialized\"\nfi\n\n# ============================================================\n# Register Matrix users via Registration API (single-step, no UIAA)\n# K8s mode: skip — controller Initializer + ManagerReconciler already did this\n# ============================================================\nif [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n log \"K8s mode: skipping Matrix registration (handled by controller)\"\n # Controller injects HICLAW_MANAGER_PASSWORD via env; login to get token\n log \"Obtaining Manager Matrix access token...\"\n _LOGIN_RESPONSE=$(curl -s -X POST ${HICLAW_MATRIX_URL}/_matrix/client/v3/login \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"type\": \"m.login.password\",\n \"identifier\": {\"type\": \"m.id.user\", \"user\": \"manager\"},\n \"password\": \"'\"${HICLAW_MANAGER_PASSWORD}\"'\"\n }' 2>&1)\n MANAGER_TOKEN=$(echo \"${_LOGIN_RESPONSE}\" | jq -r '.access_token' 2>/dev/null)\n if [ -z \"${MANAGER_TOKEN}\" ] || [ \"${MANAGER_TOKEN}\" = \"null\" ]; then\n log \"ERROR: Failed to obtain Manager Matrix token\"\n log \"ERROR: Login response was: ${_LOGIN_RESPONSE}\"\n exit 1\n fi\n log \"Manager Matrix token obtained (token prefix: ${MANAGER_TOKEN:0:10}...)\"\nelse\nlog \"Registering human admin Matrix account...\"\ncurl -sf -X POST ${HICLAW_MATRIX_URL}/_matrix/client/v3/register \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"username\": \"'\"${HICLAW_ADMIN_USER}\"'\",\n \"password\": \"'\"${HICLAW_ADMIN_PASSWORD}\"'\",\n \"auth\": {\n \"type\": \"m.login.registration_token\",\n \"token\": \"'\"${HICLAW_REGISTRATION_TOKEN}\"'\"\n }\n }' > /dev/null 2>&1 || log \"Admin account may already exist\"\n\nlog \"Registering Manager Agent Matrix account...\"\ncurl -sf -X POST ${HICLAW_MATRIX_URL}/_matrix/client/v3/register \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"username\": \"manager\",\n \"password\": \"'\"${HICLAW_MANAGER_PASSWORD}\"'\",\n \"auth\": {\n \"type\": \"m.login.registration_token\",\n \"token\": \"'\"${HICLAW_REGISTRATION_TOKEN}\"'\"\n }\n }' > /dev/null 2>&1 || log \"Manager account may already exist\"\n\n# Get Manager Agent's Matrix access token\nlog \"Obtaining Manager Matrix access token...\"\n_LOGIN_RESPONSE=$(curl -s -X POST ${HICLAW_MATRIX_URL}/_matrix/client/v3/login \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"type\": \"m.login.password\",\n \"identifier\": {\"type\": \"m.id.user\", \"user\": \"manager\"},\n \"password\": \"'\"${HICLAW_MANAGER_PASSWORD}\"'\"\n }' 2>&1)\n_LOGIN_EXIT=$?\nlog \"Matrix login HTTP exit code: ${_LOGIN_EXIT}\"\nlog \"Matrix login response: ${_LOGIN_RESPONSE}\"\n\nMANAGER_TOKEN=$(echo \"${_LOGIN_RESPONSE}\" | jq -r '.access_token' 2>/dev/null)\n\nif [ -z \"${MANAGER_TOKEN}\" ] || [ \"${MANAGER_TOKEN}\" = \"null\" ]; then\n log \"ERROR: Failed to obtain Manager Matrix token (exit=${_LOGIN_EXIT})\"\n log \"ERROR: Login response was: ${_LOGIN_RESPONSE}\"\n exit 1\nfi\nlog \"Manager Matrix token obtained (token prefix: ${MANAGER_TOKEN:0:10}...)\"\nfi\n\n# ============================================================\n# Higress Console initialization\n# Docker mode: full setup-higress.sh (internal Higress at localhost:8001)\n# K8s mode: skip — controller Initializer handles Higress setup\n# Cloud (aliyun) mode: skip entirely (Higress managed externally)\n# ============================================================\n_HIGRESS_CONSOLE_URL=\"\"\n_HIGRESS_USER=\"${HICLAW_ADMIN_USER}\"\n_HIGRESS_PASS=\"${HICLAW_ADMIN_PASSWORD}\"\nif [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n log \"K8s mode: skipping Higress initialization (handled by controller)\"\nelif [ \"${HICLAW_RUNTIME}\" != \"aliyun\" ]; then\n _HIGRESS_CONSOLE_URL=\"http://127.0.0.1:8001\"\nfi\n\nif [ -n \"${_HIGRESS_CONSOLE_URL}\" ]; then\n COOKIE_FILE=\"/tmp/higress-session-cookie\"\n\n log \"Waiting for Higress Console (${_HIGRESS_CONSOLE_URL}) to be fully ready and initializing admin...\"\n INIT_DONE=false\n for i in $(seq 1 90); do\n INIT_RESULT=$(curl -s -X POST \"${_HIGRESS_CONSOLE_URL}/system/init\" \\\n -H 'Content-Type: application/json' \\\n -d '{\"adminUser\":{\"name\":\"'\"${_HIGRESS_USER}\"'\",\"password\":\"'\"${_HIGRESS_PASS}\"'\",\"displayName\":\"'\"${_HIGRESS_USER}\"'\"}}' 2>/dev/null) || true\n if echo \"${INIT_RESULT}\" | grep -qE '\"success\":true|already.?init' 2>/dev/null; then\n INIT_DONE=true\n break\n fi\n if echo \"${INIT_RESULT}\" | grep -q '\"name\"' 2>/dev/null; then\n INIT_DONE=true\n break\n fi\n sleep 2\n done\n\n if [ \"${INIT_DONE}\" != \"true\" ]; then\n log \"ERROR: Higress Console did not become ready within 180s\"\n exit 1\n fi\n log \"Higress Console init done\"\n\n log \"Logging into Higress Console...\"\n LOGIN_OK=false\n for i in $(seq 1 10); do\n HTTP_CODE=$(curl -s -o /dev/null -w '%{http_code}' -X POST \"${_HIGRESS_CONSOLE_URL}/session/login\" \\\n -H 'Content-Type: application/json' \\\n -c \"${COOKIE_FILE}\" \\\n -d '{\"username\":\"'\"${_HIGRESS_USER}\"'\",\"password\":\"'\"${_HIGRESS_PASS}\"'\"}' 2>/dev/null) || true\n if { [ \"${HTTP_CODE}\" = \"200\" ] || [ \"${HTTP_CODE}\" = \"201\" ]; } && [ -f \"${COOKIE_FILE}\" ] && [ -s \"${COOKIE_FILE}\" ]; then\n LOGIN_OK=true\n break\n fi\n log \"Login attempt $i (HTTP ${HTTP_CODE}), retrying in 3s...\"\n sleep 3\n done\n\n if [ \"${LOGIN_OK}\" != \"true\" ]; then\n log \"ERROR: Could not login to Higress Console after retries\"\n exit 1\n fi\n log \"Higress Console login successful\"\n\n VERIFY_CODE=$(curl -s -o /dev/null -w '%{http_code}' \"${_HIGRESS_CONSOLE_URL}/v1/consumers\" -b \"${COOKIE_FILE}\" 2>/dev/null) || true\n if [ \"${VERIFY_CODE}\" = \"200\" ]; then\n log \"Console session verified (cookie valid)\"\n else\n log \"WARNING: Console session may be invalid (verify returned HTTP ${VERIFY_CODE})\"\n rm -f \"${COOKIE_FILE}\"\n for i in $(seq 1 5); do\n curl -s -o /dev/null -w '%{http_code}' -X POST \"${_HIGRESS_CONSOLE_URL}/session/login\" \\\n -H 'Content-Type: application/json' \\\n -c \"${COOKIE_FILE}\" \\\n -d '{\"username\":\"'\"${_HIGRESS_USER}\"'\",\"password\":\"'\"${_HIGRESS_PASS}\"'\"}' 2>/dev/null\n VERIFY2=$(curl -s -o /dev/null -w '%{http_code}' \"${_HIGRESS_CONSOLE_URL}/v1/consumers\" -b \"${COOKIE_FILE}\" 2>/dev/null) || true\n if [ \"${VERIFY2}\" = \"200\" ]; then\n log \"Re-login successful, session verified\"\n break\n fi\n sleep 2\n done\n fi\n\n export HIGRESS_COOKIE_FILE=\"${COOKIE_FILE}\"\n export HIGRESS_CONSOLE_URL=\"${_HIGRESS_CONSOLE_URL}\"\n\n if [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n # K8s mode: lightweight Higress config — only what's needed for LLM access\n source /opt/hiclaw/scripts/lib/base.sh\n _k8s_higress_api() {\n local method=\"$1\" path=\"$2\" desc=\"$3\"; shift 3; local body=\"$*\"\n local tmpfile; tmpfile=$(mktemp)\n local http_code\n http_code=$(curl -s -o \"${tmpfile}\" -w '%{http_code}' -X \"${method}\" \"${_HIGRESS_CONSOLE_URL}${path}\" \\\n -b \"${COOKIE_FILE}\" -H 'Content-Type: application/json' -d \"${body}\" 2>/dev/null) || true\n local response; response=$(cat \"${tmpfile}\" 2>/dev/null); rm -f \"${tmpfile}\"\n if echo \"${response}\" | grep -q '\"success\":true' 2>/dev/null; then\n log \"${desc} ... OK\"\n elif [ \"${http_code}\" = \"409\" ]; then\n log \"${desc} ... already exists, skipping\"\n elif [ \"${http_code}\" = \"200\" ] || [ \"${http_code}\" = \"201\" ] || [ \"${http_code}\" = \"204\" ]; then\n log \"${desc} ... OK (HTTP ${http_code})\"\n else\n log \"WARNING: ${desc} ... (HTTP ${http_code}): ${response}\"\n fi\n }\n\n # 1. Service Sources (DNS type → K8s Service FQDN)\n # Extract host:port from URLs for Higress service source registration\n _TUWUNEL_HOST=$(echo \"${HICLAW_MATRIX_URL}\" | sed 's|^http[s]*://||')\n _TUWUNEL_DOMAIN=$(echo \"${_TUWUNEL_HOST}\" | cut -d: -f1)\n _TUWUNEL_PORT=$(echo \"${_TUWUNEL_HOST}\" | cut -d: -f2)\n _k8s_higress_api POST /v1/service-sources \"Registering Tuwunel service source\" \\\n '{\"type\":\"dns\",\"name\":\"tuwunel\",\"domain\":\"'\"${_TUWUNEL_DOMAIN}\"'\",\"port\":'\"${_TUWUNEL_PORT}\"'}'\n\n if [ -n \"${HICLAW_ELEMENT_WEB_URL:-}\" ]; then\n _ELEMENT_HOST=$(echo \"${HICLAW_ELEMENT_WEB_URL}\" | sed 's|^http[s]*://||')\n _ELEMENT_DOMAIN=$(echo \"${_ELEMENT_HOST}\" | cut -d: -f1)\n _ELEMENT_PORT=$(echo \"${_ELEMENT_HOST}\" | cut -d: -f2)\n _k8s_higress_api POST /v1/service-sources \"Registering Element Web service source\" \\\n '{\"type\":\"dns\",\"name\":\"element-web\",\"domain\":\"'\"${_ELEMENT_DOMAIN}\"'\",\"port\":'\"${_ELEMENT_PORT}\"'}'\n fi\n\n # 2. Manager Consumer (key-auth)\n _k8s_higress_api POST /v1/consumers \"Creating Manager consumer\" \\\n '{\"name\":\"manager\",\"credentials\":[{\"type\":\"key-auth\",\"source\":\"BEARER\",\"values\":[\"'\"${HICLAW_MANAGER_GATEWAY_KEY}\"'\"]}]}'\n\n # 3. LLM Provider\n _LLM_PROVIDER=\"${HICLAW_LLM_PROVIDER:-qwen}\"\n _LLM_API_URL=\"${HICLAW_LLM_API_URL:-}\"\n if [ -z \"${_LLM_API_URL}\" ]; then\n case \"${_LLM_PROVIDER}\" in\n qwen) _LLM_API_URL=\"https://dashscope.aliyuncs.com/compatible-mode/v1\" ;;\n esac\n fi\n # 4. LLM Provider type-specific config\n case \"${_LLM_PROVIDER}\" in\n qwen)\n _k8s_higress_api POST /v1/ai/providers \"Creating LLM provider (qwen)\" \\\n '{\"type\":\"qwen\",\"name\":\"qwen\",\"tokens\":[\"'\"${HICLAW_LLM_API_KEY}\"'\"],\"protocol\":\"openai/v1\",\"tokenFailoverConfig\":{\"enabled\":false},\"rawConfigs\":{\"qwenEnableSearch\":false,\"qwenEnableCompatible\":true,\"qwenFileIds\":[],\"hiclawMode\":true}}'\n ;;\n *)\n _BODY='{\"name\":\"'\"${_LLM_PROVIDER}\"'\",\"type\":\"openai\",\"tokens\":[\"'\"${HICLAW_LLM_API_KEY}\"'\"],\"modelMapping\":{},\"protocol\":\"openai/v1\",\"rawConfigs\":{\"hiclawMode\":true}}'\n _k8s_higress_api POST /v1/ai/providers \"Creating LLM provider (${_LLM_PROVIDER})\" \"${_BODY}\"\n ;;\n esac\n\n # 5. AI Route (bind provider + consumer auth, /v1 prefix to avoid clash with Element Web catch-all)\n _k8s_higress_api POST /v1/ai/routes \"Creating AI Gateway route\" \\\n '{\"name\":\"default-ai-route\",\"domains\":[],\"pathPredicate\":{\"matchType\":\"PRE\",\"matchValue\":\"/v1\",\"caseSensitive\":false},\"upstreams\":[{\"provider\":\"'\"${_LLM_PROVIDER}\"'\",\"weight\":100,\"modelMapping\":{}}],\"authConfig\":{\"enabled\":true,\"allowedCredentialTypes\":[\"key-auth\"],\"allowedConsumers\":[\"manager\"]}}'\n\n # 6. Matrix Homeserver Route (/_matrix/* → Tuwunel, no auth)\n _k8s_higress_api POST /v1/routes \"Creating Matrix Homeserver route\" \\\n '{\"name\":\"matrix-homeserver\",\"domains\":[],\"path\":{\"matchType\":\"PRE\",\"matchValue\":\"/_matrix\"},\"services\":[{\"name\":\"tuwunel.dns\",\"port\":'\"${_TUWUNEL_PORT}\"',\"weight\":100}]}'\n\n # 7. Element Web Route (/ catch-all → Element Web, no auth)\n if [ -n \"${HICLAW_ELEMENT_WEB_URL:-}\" ]; then\n _k8s_higress_api POST /v1/routes \"Creating Element Web route\" \\\n '{\"name\":\"element-web\",\"domains\":[],\"path\":{\"matchType\":\"PRE\",\"matchValue\":\"/\"},\"services\":[{\"name\":\"element-web.dns\",\"port\":'\"${_ELEMENT_PORT}\"',\"weight\":100}]}'\n fi\n\n # 8. Remove Higress default landing page (Exact match on / takes precedence over Element Web catch-all)\n _k8s_higress_api DELETE /v1/routes/default \"Removing Higress default landing route\"\n\n log \"K8s Higress lightweight setup complete\"\n\n # Wait for AI plugin activation (~45s for first config)\n log \"Waiting for AI Gateway plugin activation (45s)...\"\n sleep 45\n else\n # Docker mode: full setup with all routes, domains, MCP servers\n /opt/hiclaw/scripts/init/setup-higress.sh\n fi\nfi\n\n# ============================================================\n# Create admin DM room, persist to state.json, send welcome message\n# K8s mode: skip — controller ProvisionManager already creates the Admin DM room\n# Runs in both local and cloud modes (idempotent)\n# ============================================================\nif [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n log \"K8s mode: skipping admin DM room creation (handled by controller)\"\nelse\nMANAGER_FULL_ID=\"@manager:${MATRIX_DOMAIN}\"\nADMIN_FULL_ID=\"@${HICLAW_ADMIN_USER}:${MATRIX_DOMAIN}\"\n\nlog \"Logging in as admin to create DM room...\"\n_ADMIN_LOGIN=$(curl -sf -X POST \"${HICLAW_MATRIX_URL}/_matrix/client/v3/login\" \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"type\": \"m.login.password\",\n \"identifier\": {\"type\": \"m.id.user\", \"user\": \"'\"${HICLAW_ADMIN_USER}\"'\"},\n \"password\": \"'\"${HICLAW_ADMIN_PASSWORD}\"'\"\n }' 2>&1) || true\n\nADMIN_MATRIX_TOKEN=$(echo \"${_ADMIN_LOGIN}\" | jq -r '.access_token // empty' 2>/dev/null)\nif [ -z \"${ADMIN_MATRIX_TOKEN}\" ]; then\n log \"WARNING: Failed to login as admin, skipping DM room creation\"\nelse\n # Search for existing DM room with Manager (idempotent)\n DM_ROOM_ID=\"\"\n _JOINED_ROOMS=$(curl -sf \"${HICLAW_MATRIX_URL}/_matrix/client/v3/joined_rooms\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" 2>/dev/null \\\n | jq -r '.joined_rooms[]' 2>/dev/null) || true\n for _rid in ${_JOINED_ROOMS}; do\n _members=$(curl -sf \"${HICLAW_MATRIX_URL}/_matrix/client/v3/rooms/${_rid}/members\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" 2>/dev/null \\\n | jq -r '.chunk[].state_key' 2>/dev/null) || continue\n _count=$(echo \"${_members}\" | wc -l | xargs)\n if [ \"${_count}\" = \"2\" ] && echo \"${_members}\" | grep -q \"@manager:\"; then\n DM_ROOM_ID=\"${_rid}\"\n break\n fi\n done\n\n if [ -n \"${DM_ROOM_ID}\" ]; then\n log \"Existing DM room found: ${DM_ROOM_ID}\"\n else\n log \"Creating DM room with Manager...\"\n _RAW=$(curl -s -w '\\nHTTP_CODE:%{http_code}' -X POST \"${HICLAW_MATRIX_URL}/_matrix/client/v3/createRoom\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d \"{\\\"is_direct\\\":true,\\\"invite\\\":[\\\"${MANAGER_FULL_ID}\\\"],\\\"preset\\\":\\\"trusted_private_chat\\\"}\" 2>&1) || true\n _HTTP_CODE=$(echo \"${_RAW}\" | tail -1 | sed 's/HTTP_CODE://')\n _CREATE_RESP=$(echo \"${_RAW}\" | sed '$d')\n DM_ROOM_ID=$(echo \"${_CREATE_RESP}\" | jq -r '.room_id // empty' 2>/dev/null)\n if [ -n \"${DM_ROOM_ID}\" ]; then\n log \"DM room created: ${DM_ROOM_ID}\"\n else\n log \"WARNING: Failed to create DM room (HTTP ${_HTTP_CODE}): ${_CREATE_RESP}\"\n fi\n fi\n\n # Persist admin DM room ID to state.json\n if [ -n \"${DM_ROOM_ID}\" ]; then\n STATE_SCRIPT=\"/opt/hiclaw/agent/skills/task-management/scripts/manage-state.sh\"\n if [ -f \"${STATE_SCRIPT}\" ]; then\n bash \"${STATE_SCRIPT}\" --action init 2>/dev/null || true\n bash \"${STATE_SCRIPT}\" --action set-admin-dm --room-id \"${DM_ROOM_ID}\" 2>/dev/null || true\n log \"Admin DM room persisted to state.json: ${DM_ROOM_ID}\"\n fi\n fi\n\n # Schedule welcome message in background (only on first boot)\n if [ -n \"${DM_ROOM_ID}\" ] && [ ! -f \"/root/manager-workspace/soul-configured\" ]; then\n log \"Scheduling welcome message (background, waiting for OpenClaw to start)...\"\n (\n _HICLAW_LANGUAGE=\"${HICLAW_LANGUAGE:-zh}\"\n _HICLAW_TIMEZONE=\"${TZ:-Asia/Shanghai}\"\n _wait=0\n _ready=false\n while [ \"${_wait}\" -lt 300 ]; do\n if curl -sf http://127.0.0.1:18799/ > /dev/null 2>&1; then\n _ready=true\n break\n fi\n sleep 3\n _wait=$((_wait + 3))\n done\n if [ \"${_ready}\" != \"true\" ]; then\n echo \"[manager] WARNING: OpenClaw gateway not ready within 300s, skipping welcome message\"\n exit 0\n fi\n # Ensure Manager has joined the DM room before sending the welcome\n # message. Without this, there is a race between OpenClaw's Matrix\n # auto-join and the message send — the message may land before Manager\n # joins, so OpenClaw's /sync never picks it up.\n _join_ok=false\n for _join_attempt in 1 2 3; do\n if curl -sf -X POST \"${HICLAW_MATRIX_URL}/_matrix/client/v3/rooms/${DM_ROOM_ID}/join\" \\\n -H \"Authorization: Bearer ${MANAGER_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d '{}' > /dev/null 2>&1; then\n echo \"[manager] Manager joined DM room before welcome message\"\n _join_ok=true\n break\n fi\n sleep 2\n done\n if [ \"${_join_ok}\" != \"true\" ]; then\n echo \"[manager] WARNING: Manager join request failed after 3 attempts (may already be joined)\"\n fi\n _welcome_msg=\"This is an automated message from the HiClaw setup. This is a fresh installation.\n\n--- Installation Context ---\nUser Language: ${_HICLAW_LANGUAGE} (zh = Chinese, en = English)\nUser Timezone: ${_HICLAW_TIMEZONE} (IANA timezone identifier)\n---\n\nYou are an AI agent that manages a team of worker agents. Your identity and personality have not been configured yet — the human admin is about to meet you for the first time.\n\nPlease begin the onboarding conversation:\n\n1. Greet the admin warmly and briefly describe what you can do (coordinate workers, manage tasks, run multi-agent projects)\n2. The user has selected \\\"${_HICLAW_LANGUAGE}\\\" as their preferred language during installation. Use this language for your greeting and all subsequent communication.\n3. The user's timezone is ${_HICLAW_TIMEZONE}. Based on this timezone, you may infer their likely region and suggest additional language options.\n4. Ask them: a) What would they like to call you? b) Communication style preference? c) Any behavior guidelines? d) Confirm default language\n5. After they reply, write their preferences to ~/SOUL.md\n6. Confirm what you wrote, and ask if they would like to adjust anything\n7. Once confirmed, run: touch ~/soul-configured\n\nThe human admin will start chatting shortly.\"\n _txn_id=\"welcome-$(date +%s)\"\n _payload=$(jq -nc --arg body \"${_welcome_msg}\" '{\"msgtype\":\"m.text\",\"body\":$body}')\n _raw=$(curl -s -w '\\nHTTP_CODE:%{http_code}' -X PUT \"${HICLAW_MATRIX_URL}/_matrix/client/v3/rooms/${DM_ROOM_ID}/send/m.room.message/${_txn_id}\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d \"${_payload}\" 2>&1) || true\n _http_code=$(echo \"${_raw}\" | tail -1 | sed 's/HTTP_CODE://')\n _send_resp=$(echo \"${_raw}\" | sed '$d')\n if echo \"${_send_resp}\" | jq -e '.event_id' > /dev/null 2>&1; then\n echo \"[manager] Welcome message sent to DM room\"\n else\n echo \"[manager] WARNING: Failed to send welcome message (HTTP ${_http_code}): ${_send_resp}\"\n fi\n ) &\n log \"Welcome message background process started (PID: $!)\"\n fi\nfi\nfi # end K8s mode skip for admin DM room\n\n# ============================================================\n# Generate Manager Agent openclaw.json from template\n# ============================================================\nlog \"Generating Manager openclaw.json...\"\nexport MANAGER_MATRIX_TOKEN=\"${MANAGER_TOKEN}\"\nexport MANAGER_GATEWAY_KEY=\"${HICLAW_MANAGER_GATEWAY_KEY}\"\n# Resolve model parameters based on model name\nMODEL_NAME=\"${HICLAW_DEFAULT_MODEL:-qwen3.5-plus}\"\ncase \"${MODEL_NAME}\" in\n gpt-5.3-codex|gpt-5-mini|gpt-5-nano)\n export MODEL_CONTEXT_WINDOW=400000 MODEL_MAX_TOKENS=128000 ;;\n claude-opus-4-6)\n export MODEL_CONTEXT_WINDOW=1000000 MODEL_MAX_TOKENS=128000 ;;\n claude-sonnet-4-6)\n export MODEL_CONTEXT_WINDOW=1000000 MODEL_MAX_TOKENS=64000 ;;\n claude-haiku-4-5)\n export MODEL_CONTEXT_WINDOW=200000 MODEL_MAX_TOKENS=64000 ;;\n qwen3.5-plus)\n export MODEL_CONTEXT_WINDOW=200000 MODEL_MAX_TOKENS=64000 ;;\n deepseek-chat|deepseek-reasoner|kimi-k2.5)\n export MODEL_CONTEXT_WINDOW=256000 MODEL_MAX_TOKENS=128000 ;;\n glm-5|MiniMax-M2.7|MiniMax-M2.7-highspeed|MiniMax-M2.5)\n export MODEL_CONTEXT_WINDOW=200000 MODEL_MAX_TOKENS=128000 ;;\n *)\n export MODEL_CONTEXT_WINDOW=150000 MODEL_MAX_TOKENS=128000 ;;\nesac\nexport MODEL_REASONING=true\n\n# Override with user-supplied custom model parameters from env (set during install)\n[ -n \"${HICLAW_MODEL_CONTEXT_WINDOW:-}\" ] && export MODEL_CONTEXT_WINDOW=\"${HICLAW_MODEL_CONTEXT_WINDOW}\"\n[ -n \"${HICLAW_MODEL_MAX_TOKENS:-}\" ] && export MODEL_MAX_TOKENS=\"${HICLAW_MODEL_MAX_TOKENS}\"\n[ -n \"${HICLAW_MODEL_REASONING:-}\" ] && export MODEL_REASONING=\"${HICLAW_MODEL_REASONING}\"\n\n# E2EE: convert HICLAW_MATRIX_E2EE to JSON boolean for template substitution\nif [ \"${HICLAW_MATRIX_E2EE:-0}\" = \"1\" ] || [ \"${HICLAW_MATRIX_E2EE:-}\" = \"true\" ]; then\n export MATRIX_E2EE_ENABLED=true\nelse\n export MATRIX_E2EE_ENABLED=false\nfi\nlog \"Matrix E2EE: ${MATRIX_E2EE_ENABLED}\"\n\n# Resolve input modalities: only vision-capable models get \"image\"\ncase \"${MODEL_NAME}\" in\n gpt-5.4|gpt-5.3-codex|gpt-5-mini|gpt-5-nano|claude-opus-4-6|claude-sonnet-4-6|claude-haiku-4-5|qwen3.5-plus|kimi-k2.5)\n export MODEL_INPUT='[\"text\", \"image\"]' ;;\n *)\n export MODEL_INPUT='[\"text\"]' ;;\nesac\n# Override with user-supplied vision setting from env\nif [ \"${HICLAW_MODEL_VISION:-}\" = \"true\" ]; then\n export MODEL_INPUT='[\"text\", \"image\"]'\nelif [ \"${HICLAW_MODEL_VISION:-}\" = \"false\" ]; then\n export MODEL_INPUT='[\"text\"]'\nfi\n\nlog \"Model: ${MODEL_NAME} (context=${MODEL_CONTEXT_WINDOW}, maxTokens=${MODEL_MAX_TOKENS}, reasoning=${MODEL_REASONING}, input=${MODEL_INPUT})\"\n\nif [ -f /root/manager-workspace/openclaw.json ]; then\n log \"Manager openclaw.json already exists, updating dynamic fields only (preserving user customizations)...\"\n # Merge known models into existing config (add missing, preserve user-added)\n # Use known-models.json (valid JSON) instead of template (contains ${VAR} placeholders)\n KNOWN_MODELS=$(cat /opt/hiclaw/configs/known-models.json 2>/dev/null || echo '[]')\n jq --arg token \"${MANAGER_TOKEN}\" \\\n --arg key \"${HICLAW_MANAGER_GATEWAY_KEY}\" \\\n --arg model \"${MODEL_NAME}\" \\\n --arg emb_model \"${HICLAW_EMBEDDING_MODEL}\" \\\n --arg aigw_domain \"${AI_GATEWAY_DOMAIN}\" \\\n --argjson e2ee \"${MATRIX_E2EE_ENABLED}\" \\\n --argjson known_models \"${KNOWN_MODELS}\" \\\n --argjson ctx \"${MODEL_CONTEXT_WINDOW}\" \\\n --argjson max \"${MODEL_MAX_TOKENS}\" \\\n --argjson reasoning \"${MODEL_REASONING}\" \\\n --argjson input \"${MODEL_INPUT}\" \\\n '\n # Merge known models: add any model id not already present\n .models.providers[\"hiclaw-gateway\"].models as $existing\n | ($existing | map(.id)) as $existing_ids\n | ($known_models | map(select(.id as $id | $existing_ids | index($id) | not))) as $new\n | .models.providers[\"hiclaw-gateway\"].models = ($existing + $new)\n # Ensure the user-chosen default model is in the list (custom model support)\n | if (.models.providers[\"hiclaw-gateway\"].models | map(.id) | index($model) | not) then\n .models.providers[\"hiclaw-gateway\"].models += [{\"id\": $model, \"name\": $model, \"reasoning\": $reasoning, \"contextWindow\": $ctx, \"maxTokens\": $max, \"input\": $input}]\n else . end\n # Rebuild model aliases from the full models list\n | (.models.providers[\"hiclaw-gateway\"].models | map({ (\"hiclaw-gateway/\" + .id): { \"alias\": .id } }) | add // {}) as $aliases\n | .agents.defaults.models = ((.agents.defaults.models // {}) + $aliases)\n | .channels.matrix.accessToken = $token | .models.providers[\"hiclaw-gateway\"].apiKey = $key\n | ((.hooks.token // \"\") as $ht | if $ht == $key or $ht == ($key + \"-hooks\" | @base64) then del(.hooks) else . end)\n | .agents.defaults.model.primary = (\"hiclaw-gateway/\" + $model)\n | .commands.restart = true\n | .gateway.controlUi.dangerouslyDisableDeviceAuth = true\n | .channels.matrix.encryption = $e2ee\n # Ensure memorySearch config exists (embedding model for memory) — skip if embedding model is empty\n | if $emb_model != \"\" then .agents.defaults.memorySearch //= {\"provider\":\"openai\",\"model\":$emb_model,\"remote\":{\"baseUrl\":(\"http://\" + $aigw_domain + \":8080/v1\"),\"apiKey\":$key}} else . end\n ' \\\n /root/manager-workspace/openclaw.json > /tmp/openclaw.json.tmp && \\\n mv /tmp/openclaw.json.tmp /root/manager-workspace/openclaw.json\n # Verify the token was written correctly\n _written_token=$(jq -r '.channels.matrix.accessToken' /root/manager-workspace/openclaw.json 2>/dev/null)\n if [ -z \"${_written_token}\" ] || [ \"${_written_token}\" = \"null\" ]; then\n log \"ERROR: Matrix token was not written correctly to openclaw.json (got: ${_written_token})\"\n else\n log \"Matrix token written to openclaw.json (prefix: ${_written_token:0:10}...)\"\n fi\nelse\n log \"Manager openclaw.json not found, generating from template...\"\n envsubst < /opt/hiclaw/configs/manager-openclaw.json.tmpl > /root/manager-workspace/openclaw.json\n # Post-envsubst injection: memorySearch + custom model (single jq pass when possible)\n if ! jq -e --arg model \"${MODEL_NAME}\" '.models.providers[\"hiclaw-gateway\"].models | map(.id) | index($model)' /root/manager-workspace/openclaw.json > /dev/null 2>&1; then\n log \"Custom model '${MODEL_NAME}' not in built-in list, injecting into config...\"\n jq --arg emb_model \"${HICLAW_EMBEDDING_MODEL}\" \\\n --arg aigw_domain \"${AI_GATEWAY_DOMAIN}\" \\\n --arg key \"${HICLAW_MANAGER_GATEWAY_KEY}\" \\\n --arg model \"${MODEL_NAME}\" \\\n --argjson ctx \"${MODEL_CONTEXT_WINDOW}\" \\\n --argjson max \"${MODEL_MAX_TOKENS}\" \\\n --argjson reasoning \"${MODEL_REASONING}\" \\\n --argjson input \"${MODEL_INPUT}\" \\\n '\n (if $emb_model != \"\" then .agents.defaults.memorySearch = {\"provider\":\"openai\",\"model\":$emb_model,\"remote\":{\"baseUrl\":(\"http://\" + $aigw_domain + \":8080/v1\"),\"apiKey\":$key}} else . end)\n | .models.providers[\"hiclaw-gateway\"].models += [{\"id\": $model, \"name\": $model, \"reasoning\": $reasoning, \"contextWindow\": $ctx, \"maxTokens\": $max, \"input\": $input}]\n | .agents.defaults.models += {(\"hiclaw-gateway/\" + $model): {\"alias\": $model}}\n ' /root/manager-workspace/openclaw.json > /tmp/openclaw.json.tmp && \\\n mv /tmp/openclaw.json.tmp /root/manager-workspace/openclaw.json\n elif [ -n \"${HICLAW_EMBEDDING_MODEL}\" ]; then\n jq --arg emb_model \"${HICLAW_EMBEDDING_MODEL}\" \\\n --arg aigw_domain \"${AI_GATEWAY_DOMAIN}\" \\\n --arg key \"${HICLAW_MANAGER_GATEWAY_KEY}\" \\\n '.agents.defaults.memorySearch = {\"provider\":\"openai\",\"model\":$emb_model,\"remote\":{\"baseUrl\":(\"http://\" + $aigw_domain + \":8080/v1\"),\"apiKey\":$key}}' \\\n /root/manager-workspace/openclaw.json > /tmp/openclaw.json.tmp && \\\n mv /tmp/openclaw.json.tmp /root/manager-workspace/openclaw.json\n fi\n _written_token=$(jq -r '.channels.matrix.accessToken' /root/manager-workspace/openclaw.json 2>/dev/null)\n log \"Matrix token written from template (prefix: ${_written_token:0:10}...)\"\nfi\n\n# Cloud/K8s mode: overlay cloud-specific settings onto generated config\nif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ] || [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n log \"Applying cloud/k8s overlay to openclaw.json...\"\n jq --arg homeserver \"${HICLAW_MATRIX_URL}\" \\\n --arg gateway \"${HICLAW_AI_GATEWAY_URL}/v1\" \\\n --arg key \"${HICLAW_MANAGER_GATEWAY_KEY}\" \\\n '.channels.matrix.homeserver = $homeserver\n | .models.providers[\"hiclaw-gateway\"].baseUrl = $gateway\n | .models.providers[\"hiclaw-gateway\"].apiKey = $key\n | ((.hooks.token // \"\") as $ht | if $ht == $key or $ht == ($key + \"-hooks\" | @base64) then del(.hooks) else . end)\n | .commands.restart = false\n | if .agents.defaults.memorySearch then .agents.defaults.memorySearch.remote.baseUrl = $gateway | .agents.defaults.memorySearch.remote.apiKey = $key else . end' \\\n /root/manager-workspace/openclaw.json > /tmp/openclaw-cloud.json && \\\n mv /tmp/openclaw-cloud.json /root/manager-workspace/openclaw.json\n log \"Cloud/K8s overlay applied\"\nfi\n\n# ============================================================\n# Optional: enable openclaw-cms-plugin observability\n# Config is applied at runtime so secrets stay out of image layers.\n# ============================================================\nCMS_TRACES_ENABLED=\"$(echo \"${HICLAW_CMS_TRACES_ENABLED:-false}\" | tr '[:upper:]' '[:lower:]')\"\nif [ \"${CMS_TRACES_ENABLED}\" = \"true\" ]; then\n CMS_PLUGIN_NAME=\"openclaw-cms-plugin\"\n CMS_PLUGIN_DIR=\"${OPENCLAW_CMS_PLUGIN_DIR:-/opt/openclaw/extensions/openclaw-cms-plugin}\"\n CMS_PLUGIN_MANIFEST=\"${CMS_PLUGIN_DIR}/openclaw.plugin.json\"\n DIAG_PLUGIN_NAME=\"diagnostics-otel\"\n DIAG_PLUGIN_DIR=\"/opt/openclaw/extensions/diagnostics-otel\"\n CMS_LICENSE_KEY=\"${HICLAW_CMS_LICENSE_KEY:-}\"\n CMS_PROJECT=\"${HICLAW_CMS_PROJECT:-}\"\n CMS_METRICS_ENABLED=\"${HICLAW_CMS_METRICS_ENABLED:-false}\"\n\n if [ ! -f \"${CMS_PLUGIN_MANIFEST}\" ]; then\n log \"WARNING: ${CMS_PLUGIN_NAME} manifest not found at ${CMS_PLUGIN_MANIFEST}, skipping plugin config\"\n else\n _missing=0\n [ -z \"${HICLAW_CMS_ENDPOINT:-}\" ] && log \"WARNING: HICLAW_CMS_ENDPOINT is required when HICLAW_CMS_TRACES_ENABLED=true\" && _missing=1\n [ -z \"${CMS_LICENSE_KEY:-}\" ] && log \"WARNING: HICLAW_CMS_LICENSE_KEY is required when HICLAW_CMS_TRACES_ENABLED=true\" && _missing=1\n [ -z \"${HICLAW_CMS_WORKSPACE:-}\" ] && log \"WARNING: HICLAW_CMS_WORKSPACE is required when HICLAW_CMS_TRACES_ENABLED=true\" && _missing=1\n\n if [ \"${_missing}\" = \"0\" ]; then\n CMS_SERVICE_NAME=\"${HICLAW_CMS_SERVICE_NAME:-hiclaw-manager}\"\n CMS_ENABLE_METRICS=\"${CMS_METRICS_ENABLED}\"\n DIAG_AVAILABLE=\"0\"\n _metrics_lc=\"$(echo \"${CMS_ENABLE_METRICS}\" | tr '[:upper:]' '[:lower:]')\"\n if [ \"${_metrics_lc}\" = \"true\" ]; then\n if [ -f \"${DIAG_PLUGIN_DIR}/package.json\" ]; then\n DIAG_AVAILABLE=\"1\"\n if [ ! -d \"${DIAG_PLUGIN_DIR}/node_modules\" ]; then\n log \"diagnostics-otel dependencies missing, installing...\"\n if (cd \"${DIAG_PLUGIN_DIR}\" && npm install --omit=dev --ignore-scripts >/tmp/hiclaw-diag-install.log 2>&1); then\n log \"diagnostics-otel dependencies installed\"\n else\n log \"WARNING: diagnostics-otel npm install failed, metrics plugin may not load\"\n fi\n else\n log \"diagnostics-otel dependencies already present\"\n fi\n else\n log \"WARNING: diagnostics-otel package.json not found at ${DIAG_PLUGIN_DIR}, metrics plugin may not load\"\n fi\n fi\n\n log \"Applying ${CMS_PLUGIN_NAME} config to openclaw.json...\"\n jq --arg pluginName \"${CMS_PLUGIN_NAME}\" \\\n --arg pluginDir \"${CMS_PLUGIN_DIR}\" \\\n --arg endpoint \"${HICLAW_CMS_ENDPOINT}\" \\\n --arg licenseKey \"${CMS_LICENSE_KEY}\" \\\n --arg armsProject \"${CMS_PROJECT}\" \\\n --arg cmsWorkspace \"${HICLAW_CMS_WORKSPACE}\" \\\n --arg serviceName \"${CMS_SERVICE_NAME}\" \\\n --arg diagPluginName \"${DIAG_PLUGIN_NAME}\" \\\n --arg diagPluginDir \"${DIAG_PLUGIN_DIR}\" \\\n --arg metricsRaw \"${CMS_ENABLE_METRICS}\" \\\n --arg diagAvailableRaw \"${DIAG_AVAILABLE}\" \\\n '\n .plugins = (.plugins // {})\n | .plugins.load = (.plugins.load // {})\n | .plugins.entries = (.plugins.entries // {})\n | if (.plugins.allow | type) != \"array\" then .plugins.allow = [] else . end\n | if (.plugins.allow | index($pluginName)) == null then .plugins.allow += [$pluginName] else . end\n | if (.plugins.load.paths | type) != \"array\" then .plugins.load.paths = [] else . end\n | if (.plugins.load.paths | index($pluginDir)) == null then .plugins.load.paths += [$pluginDir] else . end\n | .plugins.entries[$pluginName] = {\n \"enabled\": true,\n \"config\": {\n \"endpoint\": $endpoint,\n \"headers\": {\n \"x-arms-license-key\": $licenseKey,\n \"x-arms-project\": $armsProject,\n \"x-cms-workspace\": $cmsWorkspace\n },\n \"serviceName\": $serviceName\n }\n }\n\n # diagnostics-otel metrics (optional)\n | ($metricsRaw | ascii_downcase) as $m\n | ($diagAvailableRaw == \"1\") as $diagAvailable\n | (($m == \"true\") and $diagAvailable) as $metricsEnabled\n | if $metricsEnabled then\n (if (.plugins.allow | index($diagPluginName)) == null then .plugins.allow += [$diagPluginName] else . end)\n | (if (.plugins.load.paths | index($diagPluginDir)) == null then .plugins.load.paths += [$diagPluginDir] else . end)\n | .plugins.entries[$diagPluginName].enabled = true\n | .diagnostics = (.diagnostics // {})\n | .diagnostics.otel = (.diagnostics.otel // {})\n | .diagnostics.enabled = true\n | .diagnostics.otel.enabled = true\n | .diagnostics.otel.endpoint = $endpoint\n | .diagnostics.otel.protocol = (.diagnostics.otel.protocol // \"http/protobuf\")\n | .diagnostics.otel.headers = {\n \"x-arms-license-key\": $licenseKey,\n \"x-arms-project\": $armsProject,\n \"x-cms-workspace\": $cmsWorkspace\n }\n | .diagnostics.otel.serviceName = $serviceName\n | .diagnostics.otel.metrics = true\n | .diagnostics.otel.traces = (.diagnostics.otel.traces // false)\n | .diagnostics.otel.logs = (.diagnostics.otel.logs // false)\n else\n .\n end\n ' /root/manager-workspace/openclaw.json > /tmp/openclaw-cms.json && \\\n mv /tmp/openclaw-cms.json /root/manager-workspace/openclaw.json\n log \"${CMS_PLUGIN_NAME} config applied (metrics=${CMS_ENABLE_METRICS}, service=${CMS_SERVICE_NAME})\"\n else\n log \"Skipping ${CMS_PLUGIN_NAME} config due to missing required env vars\"\n fi\n fi\nfi\n\n# ============================================================\n# Detect container runtime (for Worker creation)\n# ============================================================\nsource /opt/hiclaw/scripts/lib/container-api.sh\nif container_api_available; then\n log \"Container runtime socket detected at ${CONTAINER_SOCKET} — direct Worker creation enabled\"\n export HICLAW_CONTAINER_RUNTIME=\"socket\"\nelif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ] || [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n log \"Cloud/K8s mode — Workers created via controller API\"\n export HICLAW_CONTAINER_RUNTIME=\"cloud\"\nelse\n log \"No container runtime found — Worker creation will output install commands\"\n export HICLAW_CONTAINER_RUNTIME=\"none\"\nfi\n\n# ============================================================\n# Upgrade Worker openclaw.json: merge known models + E2EE flag into existing configs\n# Existing workers in MinIO may have old single-model configs or missing encryption field.\n# Merge template models so they can hot-switch without restart.\n# ============================================================\nREGISTRY_FILE=\"/root/manager-workspace/workers-registry.json\"\nif [ -f \"${REGISTRY_FILE}\" ]; then\n # Use known-models.json (valid JSON) instead of template (contains ${VAR} placeholders)\n KNOWN_MODELS_FILE=\"/opt/hiclaw/configs/known-models.json\"\n if [ -f \"${KNOWN_MODELS_FILE}\" ]; then\n _KNOWN_MODELS=$(cat \"${KNOWN_MODELS_FILE}\")\n for _wname in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_wname}\" ] && continue\n _minio_path=\"${HICLAW_STORAGE_PREFIX}/agents/${_wname}/openclaw.json\"\n _tmp_in=\"/tmp/openclaw-${_wname}-models-upgrade-in.json\"\n if mc cp \"${_minio_path}\" \"${_tmp_in}\" 2>/dev/null; then\n _tmp_out=\"/tmp/openclaw-${_wname}-models-upgrade-out.json\"\n # Idempotent merge: add missing known models, rebuild aliases, set e2ee.\n # Always runs — jq deduplicates by model id, so re-runs are safe.\n jq --argjson known_models \"${_KNOWN_MODELS}\" \\\n --argjson e2ee \"${MATRIX_E2EE_ENABLED}\" '\n .models.providers[\"hiclaw-gateway\"].models as $existing\n | ($existing | map(.id)) as $existing_ids\n | ($known_models | map(select(.id as $id | $existing_ids | index($id) | not))) as $new\n | .models.providers[\"hiclaw-gateway\"].models = ($existing + $new)\n | (.models.providers[\"hiclaw-gateway\"].models | map({ (\"hiclaw-gateway/\" + .id): { \"alias\": .id } }) | add // {}) as $aliases\n | .agents.defaults.models = ((.agents.defaults.models // {}) + $aliases)\n | .channels.matrix.encryption = $e2ee\n ' \"${_tmp_in}\" > \"${_tmp_out}\" 2>/dev/null\n if ! diff -q \"${_tmp_in}\" \"${_tmp_out}\" > /dev/null 2>&1; then\n if mc cp \"${_tmp_out}\" \"${_minio_path}\" 2>/dev/null; then\n _new_count=$(jq '.models.providers[\"hiclaw-gateway\"].models | length' \"${_tmp_out}\" 2>/dev/null)\n log \"Worker ${_wname}: upgraded openclaw.json (models: ${_new_count}, e2ee: ${MATRIX_E2EE_ENABLED})\"\n fi\n fi\n rm -f \"${_tmp_in}\" \"${_tmp_out}\"\n fi\n done\n fi\nfi\n\n# ============================================================\n# Ensure Worker Matrix password files exist in MinIO (E2EE fix)\n# Workers need to re-login on restart to get a fresh device_id.\n# Older workers created before this fix won't have the password file.\n# ============================================================\nif [ -f \"${REGISTRY_FILE}\" ]; then\n for _wname in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_wname}\" ] && continue\n _creds_file=\"/data/worker-creds/${_wname}.env\"\n if [ -f \"${_creds_file}\" ]; then\n # Check if password file already exists in MinIO\n if ! mc stat \"${HICLAW_STORAGE_PREFIX}/agents/${_wname}/credentials/matrix/password\" > /dev/null 2>&1; then\n source \"${_creds_file}\"\n if [ -n \"${WORKER_PASSWORD}\" ]; then\n _tmp_pw=\"/tmp/matrix-pw-${_wname}\"\n echo -n \"${WORKER_PASSWORD}\" > \"${_tmp_pw}\"\n mc cp \"${_tmp_pw}\" \"${HICLAW_STORAGE_PREFIX}/agents/${_wname}/credentials/matrix/password\" 2>/dev/null \\\n && log \"Worker ${_wname}: wrote Matrix password to MinIO (E2EE re-login fix)\" \\\n || log \"Worker ${_wname}: WARNING: failed to write Matrix password to MinIO\"\n rm -f \"${_tmp_pw}\"\n fi\n fi\n fi\n done\nfi\n\n# ============================================================\n# Recreate Worker containers as needed after Manager restart.\n# Workers are on hiclaw-net; Docker DNS resolves *-local.hiclaw.io via\n# the Manager's network aliases, so IP changes don't require worker recreation.\n# Only recreate stopped/missing workers.\n# ============================================================\nif container_api_available; then\n REGISTRY_FILE=\"/root/manager-workspace/workers-registry.json\"\n if [ -f \"${REGISTRY_FILE}\" ]; then\n for _worker_name in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_worker_name}\" ] && continue\n\n # Skip remote workers — they are not Manager-managed containers.\n _deployment=$(jq -r --arg w \"${_worker_name}\" '.workers[$w].deployment // \"local\"' \"${REGISTRY_FILE}\" 2>/dev/null)\n if [ \"${_deployment}\" = \"remote\" ]; then\n log \"Worker ${_worker_name} is remote, skipping container recreate\"\n continue\n fi\n\n _status=$(container_status_worker \"${_worker_name}\")\n if [ \"${_status}\" = \"running\" ]; then\n log \"Worker running: ${_worker_name}, skipping\"\n continue\n fi\n # Container missing or stopped — recreate.\n log \"Worker container ${_status}: ${_worker_name}, recreating...\"\n _creds_file=\"/data/worker-creds/${_worker_name}.env\"\n if [ -f \"${_creds_file}\" ]; then\n source \"${_creds_file}\"\n _runtime=$(jq -r --arg w \"${_worker_name}\" '.workers[$w].runtime // \"openclaw\"' \"${REGISTRY_FILE}\" 2>/dev/null)\n _recreated=false\n for _attempt in 1 2 3; do\n local _env_map _create_body\n _env_map=$(jq -cn \\\n --arg name \"${_worker_name}\" \\\n --arg fak \"${_worker_name}\" \\\n --arg fsk \"${WORKER_MINIO_PASSWORD:-}\" \\\n --arg fs_domain \"${HICLAW_FS_DOMAIN:-fs-local.hiclaw.io}\" \\\n --arg controller_url \"${HICLAW_CONTROLLER_URL:-}\" \\\n '{\n \"HICLAW_WORKER_NAME\": $name,\n \"HICLAW_FS_ENDPOINT\": (\"http://\" + ($fs_domain | split(\":\")[0]) + \":8080\"),\n \"HICLAW_FS_ACCESS_KEY\": $fak,\n \"HICLAW_FS_SECRET_KEY\": $fsk\n }\n | if $controller_url != \"\" then . + {\"HICLAW_CONTROLLER_URL\": $controller_url} else . end')\n _create_body=$(jq -cn --arg name \"${_worker_name}\" --arg runtime \"${_runtime}\" --argjson env \"${_env_map}\" '{name: $name, runtime: $runtime, env: $env}')\n worker_backend_create \"${_create_body}\" > /dev/null 2>&1 && _recreated=true && break\n log \" Attempt ${_attempt}/3 failed for ${_worker_name}, retrying in $((5 * _attempt))s...\"\n sleep $((5 * _attempt))\n done\n if [ \"${_recreated}\" = true ]; then\n log \" Recreated ${_runtime} worker: ${_worker_name}\"\n else\n log \" ERROR: Failed to recreate ${_worker_name} after 3 attempts\"\n fi\n else\n log \" WARNING: No credentials found for ${_worker_name} (${_creds_file} missing), skipping\"\n fi\n done\n fi\nfi\n\n# ============================================================\n# Notify workers of builtin updates if upgrade happened\n# Builtin files (AGENTS.md, skills) are already synced by upgrade-builtins.sh\n#\n# Cooldown: skip notification if the last successful notify was within\n# NOTIFY_COOLDOWN_SECS (default 3600s / 1 hour). This prevents repeated\n# notifications when the Manager crash-loops and re-runs upgrade-builtins\n# on every restart (e.g. IMAGE_VERSION=latest always triggers upgrade).\n# ============================================================\nNOTIFY_COOLDOWN_SECS=\"${HICLAW_NOTIFY_COOLDOWN_SECS:-3600}\"\nNOTIFY_TS_FILE=\"/root/manager-workspace/.last-worker-notify-ts\"\n\nif [ -f /root/manager-workspace/.upgrade-pending-worker-notify ]; then\n _now=$(date +%s)\n _last_notify=$(cat \"${NOTIFY_TS_FILE}\" 2>/dev/null || echo \"0\")\n _elapsed=$(( _now - _last_notify ))\n\n if [ \"${_elapsed}\" -lt \"${NOTIFY_COOLDOWN_SECS}\" ]; then\n log \"Skipping worker builtin notification (last notify ${_elapsed}s ago, cooldown ${NOTIFY_COOLDOWN_SECS}s)\"\n rm -f /root/manager-workspace/.upgrade-pending-worker-notify\n else\n log \"Notifying workers about builtin updates...\"\n REGISTRY_FILE=\"/root/manager-workspace/workers-registry.json\"\n _notify_ok=false\n if [ -f \"${REGISTRY_FILE}\" ]; then\n for _worker_name in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_worker_name}\" ] && continue\n _room_id=$(jq -r --arg w \"${_worker_name}\" '.workers[$w].room_id // empty' \"${REGISTRY_FILE}\" 2>/dev/null)\n if [ -n \"${_room_id}\" ]; then\n _worker_id=\"@${_worker_name}:${MATRIX_DOMAIN}\"\n _txn_id=\"upgrade-$(date +%s%N)\"\n _msg=\"@${_worker_name}:${MATRIX_DOMAIN} Manager upgraded builtin files (AGENTS.md, skills). Please use your file-sync skill to sync the latest config.\"\n _raw=$(curl -s -w '\\nHTTP_CODE:%{http_code}' -X PUT \\\n \"${HICLAW_MATRIX_URL}/_matrix/client/v3/rooms/${_room_id}/send/m.room.message/${_txn_id}\" \\\n -H \"Authorization: Bearer ${MANAGER_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d \"{\\\"msgtype\\\":\\\"m.text\\\",\\\"body\\\":\\\"${_msg}\\\",\\\"m.mentions\\\":{\\\"user_ids\\\":[\\\"${_worker_id}\\\"]}}\" \\\n 2>&1) || true\n _http_code=$(echo \"${_raw}\" | tail -1 | sed 's/HTTP_CODE://')\n _notify_resp=$(echo \"${_raw}\" | sed '$d')\n if echo \"${_notify_resp}\" | jq -e '.event_id' > /dev/null 2>&1; then\n log \" Notified ${_worker_name}\"; _notify_ok=true\n else\n log \" WARNING: Failed to notify ${_worker_name} (HTTP ${_http_code}): ${_notify_resp}\"\n fi\n fi\n done\n fi\n # Record timestamp only if at least one notification succeeded\n if [ \"${_notify_ok}\" = true ]; then\n echo \"${_now}\" > \"${NOTIFY_TS_FILE}\"\n fi\n rm -f /root/manager-workspace/.upgrade-pending-worker-notify\n fi\nfi\n\n# ============================================================\n# Start Manager Agent\n# ============================================================\nlog \"Starting Manager Agent (${MANAGER_RUNTIME})...\"\n\n# HOME is already set to /root/manager-workspace via docker run -e HOME=...\ncd \"${HOME}\"\n\n# Ensure host credential symlinks exist under HOME\nif [ -d \"/host-share\" ]; then\n [ -f \"/host-share/.gitconfig\" ] && ln -sf \"/host-share/.gitconfig\" \"${HOME}/.gitconfig\"\nfi\n\nlog \"HOME=${HOME} (manager-workspace, host-mounted)\"\n\n# ── Render agent doc templates ────────────────────────────────────────────\n# Replace ${VAR} placeholders with actual values so the AI agent reads\n# plain text and never needs to resolve environment variables.\nexport MANAGER_MATRIX_TOKEN MANAGER_TOKEN HIGRESS_COOKIE_FILE\nRENDER=/opt/hiclaw/scripts/lib/render-skills.sh\nlog \"Rendering agent doc templates...\"\n# Manager-owned docs (workspace)\nbash \"$RENDER\" /root/manager-workspace/skills\nbash \"$RENDER\" /root/manager-workspace/skills-alpha\nbash \"$RENDER\" /root/manager-workspace AGENTS.md TOOLS.md HEARTBEAT.md SOUL.md\n# Worker templates (workspace + image) — rendered before push to MinIO\n# so Workers (including remote pip-install) receive plain text\nbash \"$RENDER\" /root/manager-workspace/worker-skills\nbash \"$RENDER\" /root/manager-workspace/worker-agent\nbash \"$RENDER\" /root/manager-workspace/copaw-worker-agent\nbash \"$RENDER\" /opt/hiclaw/agent/worker-skills\nbash \"$RENDER\" /opt/hiclaw/agent/worker-agent\nbash \"$RENDER\" /opt/hiclaw/agent/copaw-worker-agent\nlog \"Agent doc templates rendered\"\n\n# Cloud mode: start background file sync (workspace ↔ OSS) and initial push\nif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n log \"Syncing initial workspace to OSS...\"\n ensure_mc_credentials\n mc mirror /root/manager-workspace/ \"${HICLAW_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" 2>/dev/null || true\n\n # Local → OSS: change-triggered sync\n (\n while true; do\n CHANGED=$(find /root/manager-workspace/ -type f -newermt \"15 seconds ago\" 2>/dev/null | head -1)\n if [ -n \"${CHANGED}\" ]; then\n ensure_mc_credentials 2>/dev/null || true\n mc mirror /root/manager-workspace/ \"${HICLAW_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" --exclude \".npm/**\" \\\n --exclude \".local/**\" --exclude \".mc/**\" 2>/dev/null || true\n fi\n sleep 10\n done\n ) &\n log \"Local→OSS sync started (PID: $!)\"\n\n # OSS → Local: periodic pull (shared data, agent configs)\n (\n while true; do\n sleep 300\n ensure_mc_credentials 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/shared/\" /root/hiclaw-fs/shared/ --overwrite --newer-than \"5m\" 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/agents/\" /root/hiclaw-fs/agents/ --overwrite --newer-than \"5m\" 2>/dev/null || true\n done\n ) &\n log \"OSS→Local sync started (every 5m, PID: $!)\"\nfi\n\n# K8s mode: start background file sync (workspace ↔ MinIO)\nif [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n log \"Syncing initial workspace to MinIO...\"\n mc mirror /root/manager-workspace/ \"${HICLAW_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" 2>/dev/null || true\n\n # Local → MinIO: change-triggered sync\n (\n while true; do\n CHANGED=$(find /root/manager-workspace/ -type f -newermt \"15 seconds ago\" 2>/dev/null | head -1)\n if [ -n \"${CHANGED}\" ]; then\n mc mirror /root/manager-workspace/ \"${HICLAW_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" --exclude \".npm/**\" \\\n --exclude \".local/**\" --exclude \".mc/**\" 2>/dev/null || true\n fi\n sleep 10\n done\n ) &\n log \"Local→MinIO sync started (PID: $!)\"\n\n # MinIO → Local: periodic pull (shared data, agent configs)\n (\n while true; do\n sleep 300\n mc mirror \"${HICLAW_STORAGE_PREFIX}/shared/\" /root/hiclaw-fs/shared/ --overwrite --newer-than \"5m\" 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/agents/\" /root/hiclaw-fs/agents/ --overwrite --newer-than \"5m\" 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/hiclaw-config/\" /root/hiclaw-fs/hiclaw-config/ --overwrite --newer-than \"15s\" 2>/dev/null || true\n done\n ) &\n log \"MinIO→Local sync started (every 5m, PID: $!)\"\nfi\n\n# ============================================================\n# Auto-generate Manager mcporter config for pre-configured MCP servers\n# If HICLAW_GITHUB_TOKEN was set at install time, setup-higress.sh already\n# configured GitHub MCP on Higress. Run setup-mcp-server.sh now so that\n# config/mcporter.json exists when the Agent starts — no need to ask user for PAT.\n# ============================================================\nif [ -n \"${HICLAW_GITHUB_TOKEN}\" ] && [ \"${HICLAW_RUNTIME}\" != \"aliyun\" ] && [ \"${HICLAW_RUNTIME}\" != \"k8s\" ]; then\n if [ ! -f \"${HOME}/config/mcporter.json\" ]; then\n log \"Auto-generating Manager mcporter config for GitHub MCP (HICLAW_GITHUB_TOKEN set)...\"\n bash /opt/hiclaw/agent/skills/mcp-server-management/scripts/setup-mcp-server.sh \\\n github \"${HICLAW_GITHUB_TOKEN}\" 2>&1 | while IFS= read -r line; do log \" [setup-mcp] ${line}\"; done || \\\n log \"WARNING: setup-mcp-server.sh failed — Agent may need to configure GitHub MCP manually\"\n else\n log \"Manager mcporter config already exists, skipping auto-generate\"\n fi\nfi\n\n# ============================================================\n# Runtime-specific startup\n# ============================================================\nif [ \"${MANAGER_RUNTIME}\" = \"copaw\" ]; then\n # Delegate to CoPaw startup script\n exec /opt/hiclaw/scripts/init/start-copaw-manager.sh\nelse\n # ── OpenClaw Runtime ─────────────────────────────────────────────────────\n log \"Starting OpenClaw Manager...\"\n\n export OPENCLAW_CONFIG_PATH=\"/root/manager-workspace/openclaw.json\"\n\n # Symlink to default OpenClaw config path so CLI commands find the config\n mkdir -p \"${HOME}/.openclaw\"\n ln -sf \"/root/manager-workspace/openclaw.json\" \"${HOME}/.openclaw/openclaw.json\"\n\n # Clean orphaned session write locks (e.g. from SIGKILL or crash before exit handlers)\n # Prevents \"session file locked (timeout 10000ms)\" when PID was reused\n find \"${HOME}/.openclaw/agents\" -name \"*.jsonl.lock\" -delete 2>/dev/null || true\n log \"Cleaned up any orphaned session write locks\"\n\n # Clean Matrix crypto storage (SQLite WAL may be corrupted after unclean shutdown)\n # Crypto state is re-negotiated on startup; losing it only means re-establishing E2EE sessions\n rm -rf \"${HOME}/.openclaw/matrix\" 2>/dev/null || true\n log \"Cleaned Matrix crypto storage (will re-establish E2EE sessions)\"\n\n # Launch OpenClaw\n # Disable full-process respawn so the CLI uses its internal restart loop.\n # Without this, config reload spawns a detached child and exits, then\n # supervisord restarts the CLI — resulting in two gateway processes.\n export OPENCLAW_NO_RESPAWN=1\n exec openclaw gateway run --verbose --force\nfi\n"}, "24079a4a196b34b80ac884f9fa5268e31bf1adf3": {"t": 1785764758.6277544, "n": 63769, "h": "7f62e43d1b6c67b5", "c": "#!/bin/bash\n# start-manager-agent.sh - Initialize and start the Manager Agent\n# Supports local (supervisord), cloud (SAE), and K8s (Helm) deployments.\n# In local mode this is the last supervisord component to start (priority 800).\n# In cloud/k8s mode (HICLAW_RUNTIME=aliyun|k8s) this is the container entrypoint.\n#\n# Runtime selection:\n# HICLAW_MANAGER_RUNTIME=openclaw (default) - OpenClaw gateway mode\n# HICLAW_MANAGER_RUNTIME=copaw - CoPaw workspace mode\n\nsource /opt/hiclaw/scripts/lib/hiclaw-env.sh\n\n# ============================================================\n# Runtime selection\n# ============================================================\nMANAGER_RUNTIME=\"${HICLAW_MANAGER_RUNTIME:-openclaw}\"\ncase \"${MANAGER_RUNTIME}\" in\n copaw)\n log \"Manager runtime: CoPaw (Python workspace)\"\n ;;\n *)\n log \"Manager runtime: OpenClaw (Node.js gateway)\"\n MANAGER_RUNTIME=\"openclaw\"\n ;;\nesac\n\n# ============================================================\n# Set timezone from TZ env var\n# ============================================================\nif [ -n \"${TZ}\" ] && [ -f \"/usr/share/zoneinfo/${TZ}\" ]; then\n ln -sf \"/usr/share/zoneinfo/${TZ}\" /etc/localtime\n echo \"${TZ}\" > /etc/timezone\n log \"Timezone set to ${TZ}\"\nfi\n\nexport MATRIX_DOMAIN=\"${HICLAW_MATRIX_DOMAIN:-matrix-local.hiclaw.io:8080}\"\nAI_GATEWAY_DOMAIN=\"${HICLAW_AI_GATEWAY_DOMAIN:-aigw-local.hiclaw.io}\"\n\n# ============================================================\n# Cloud/K8s mode: validate required environment variables + initial credentials\n# ============================================================\nif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ] || [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n : \"${HICLAW_MATRIX_URL:?HICLAW_MATRIX_URL is required}\"\n : \"${HICLAW_MATRIX_DOMAIN:?HICLAW_MATRIX_DOMAIN is required}\"\n : \"${HICLAW_AI_GATEWAY_URL:?HICLAW_AI_GATEWAY_URL is required}\"\n if [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n : \"${HICLAW_MANAGER_GATEWAY_KEY:?HICLAW_MANAGER_GATEWAY_KEY is required}\"\n : \"${HICLAW_MANAGER_PASSWORD:?HICLAW_MANAGER_PASSWORD is required (cloud containers are stateless, password must be injected)}\"\n fi\n if [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n # K8s mode: controller handles initialization (admin registration, Higress setup).\n # Manager only needs credentials injected by the ManagerReconciler.\n : \"${HICLAW_MANAGER_GATEWAY_KEY:?HICLAW_MANAGER_GATEWAY_KEY is required (injected by controller)}\"\n : \"${HICLAW_MANAGER_PASSWORD:?HICLAW_MANAGER_PASSWORD is required (injected by controller)}\"\n else\n # Cloud (aliyun) mode: Manager still does its own initialization\n : \"${HICLAW_REGISTRATION_TOKEN:?HICLAW_REGISTRATION_TOKEN is required}\"\n : \"${HICLAW_ADMIN_USER:?HICLAW_ADMIN_USER is required}\"\n : \"${HICLAW_ADMIN_PASSWORD:?HICLAW_ADMIN_PASSWORD is required}\"\n fi\n log \"${HICLAW_RUNTIME} mode: validating environment... OK\"\n log \" Matrix: ${HICLAW_MATRIX_URL}, AI Gateway: ${HICLAW_AI_GATEWAY_URL}, Storage: ${HICLAW_FS_BUCKET}\"\n if [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n ensure_mc_credentials || { log \"FATAL: Initial STS credential fetch failed\"; exit 1; }\n fi\nfi\n\n# ============================================================\n# Local mode: host symlinks, /etc/hosts, wait for local services\n# ============================================================\nif [ \"${HICLAW_RUNTIME}\" != \"aliyun\" ] && [ \"${HICLAW_RUNTIME}\" != \"k8s\" ]; then\n # Create symlink for host directory access\n if [ -d \"/host-share\" ]; then\n ORIGINAL_HOST_HOME=\"${HOST_ORIGINAL_HOME:-$HOME}\"\n if [ ! -e \"${ORIGINAL_HOST_HOME}\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/root\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/data\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/host-share\" ]; then\n mkdir -p \"$(dirname \"${ORIGINAL_HOST_HOME}\")\"\n ln -sfn /host-share \"${ORIGINAL_HOST_HOME}\"\n log \"Created symlink: ${ORIGINAL_HOST_HOME} -> /host-share\"\n else\n ln -sfn /host-share /root/host-home\n log \"Created fallback symlink: /root/host-home -> /host-share\"\n fi\n fi\n\n # Add local domains to /etc/hosts\n HOSTS_DOMAINS=\"${MATRIX_DOMAIN%%:*} ${HICLAW_MATRIX_CLIENT_DOMAIN:-matrix-client-local.hiclaw.io} ${AI_GATEWAY_DOMAIN} ${HICLAW_FS_DOMAIN:-fs-local.hiclaw.io}\"\n if ! grep -q \"${AI_GATEWAY_DOMAIN}\" /etc/hosts 2>/dev/null; then\n echo \"127.0.0.1 ${HOSTS_DOMAINS}\" >> /etc/hosts\n log \"Added local domains to /etc/hosts\"\n fi\n\n # Wait for local infrastructure\n waitForService \"Higress Gateway\" \"127.0.0.1\" 8080 180\n waitForService \"Higress Console\" \"127.0.0.1\" 8001 180\n waitForService \"Tuwunel\" \"127.0.0.1\" 6167 120\n waitForHTTP \"Tuwunel Matrix API\" \"${HICLAW_MATRIX_URL}/_tuwunel/server_version\" 120\n waitForService \"MinIO\" \"127.0.0.1\" 9000 120\nelse\n # Cloud/K8s mode: wait for external Tuwunel\n log \"Waiting for Tuwunel Matrix server at ${HICLAW_MATRIX_URL}...\"\n _retry=0\n while [ \"${_retry}\" -lt 30 ]; do\n if curl -sf \"${HICLAW_MATRIX_URL}/_matrix/client/versions\" > /dev/null 2>&1; then\n log \"Tuwunel is ready\"\n break\n fi\n _retry=$((_retry + 1))\n log \" Waiting for Tuwunel (attempt ${_retry}/30)...\"\n sleep 5\n done\n if [ \"${_retry}\" -ge 30 ]; then\n log \"ERROR: Tuwunel not reachable at ${HICLAW_MATRIX_URL}\"\n exit 1\n fi\nfi\n\n# ============================================================\n# Auto-generate secrets if not provided via environment\n# Persisted to /data so they survive container restart\n# ============================================================\nSECRETS_FILE=\"/data/hiclaw-secrets.env\"\nif [ -f \"${SECRETS_FILE}\" ]; then\n source \"${SECRETS_FILE}\"\n log \"Loaded persisted secrets from ${SECRETS_FILE}\"\nfi\n\nif [ -z \"${HICLAW_MANAGER_GATEWAY_KEY}\" ]; then\n export HICLAW_MANAGER_GATEWAY_KEY=\"$(generateKey 32)\"\n log \"Auto-generated HICLAW_MANAGER_GATEWAY_KEY\"\nfi\nif [ -z \"${HICLAW_MANAGER_PASSWORD}\" ]; then\n export HICLAW_MANAGER_PASSWORD=\"$(generateKey 16)\"\n log \"Auto-generated HICLAW_MANAGER_PASSWORD\"\nfi\n\n# Persist secrets so they survive supervisord restart\nmkdir -p /data\ncat > \"${SECRETS_FILE}\" <<EOF\nexport HICLAW_MANAGER_GATEWAY_KEY=\"${HICLAW_MANAGER_GATEWAY_KEY}\"\nexport HICLAW_MANAGER_PASSWORD=\"${HICLAW_MANAGER_PASSWORD}\"\nEOF\nchmod 600 \"${SECRETS_FILE}\"\n\n# Cloud mode: pull workspace from OSS before initialization\nif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n HICLAW_FS=\"/root/hiclaw-fs\"\n mkdir -p \"${HICLAW_FS}/shared\" \"${HICLAW_FS}/agents\"\n log \"Pulling workspace from OSS...\"\n ensure_mc_credentials\n mc mirror \"${HICLAW_STORAGE_PREFIX}/manager/\" /root/manager-workspace/ --overwrite 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/shared/\" \"${HICLAW_FS}/shared/\" --overwrite 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/agents/\" \"${HICLAW_FS}/agents/\" --overwrite 2>/dev/null || true\n # Symlink hiclaw-fs into workspace for agent access\n ln -sfn \"${HICLAW_FS}\" /root/manager-workspace/hiclaw-fs\nfi\n\n# K8s mode: sync workspace from cluster-internal MinIO\nif [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n HICLAW_FS=\"/root/hiclaw-fs\"\n mkdir -p \"${HICLAW_FS}/shared\" \"${HICLAW_FS}/agents\" \"${HICLAW_FS}/hiclaw-config\"\n log \"Configuring mc alias for cluster MinIO...\"\n mc alias set hiclaw \"${HICLAW_FS_ENDPOINT}\" \"${HICLAW_FS_ACCESS_KEY}\" \"${HICLAW_FS_SECRET_KEY}\"\n log \"Syncing workspace from MinIO...\"\n mc mirror \"${HICLAW_STORAGE_PREFIX}/\" \"${HICLAW_FS}/\" --overwrite 2>/dev/null || true\n ln -sfn \"${HICLAW_FS}\" /root/manager-workspace/hiclaw-fs\n touch \"${HICLAW_FS}/.initialized\"\nfi\n\n# ============================================================\n# Initialize / upgrade Manager workspace\n# First boot: full init via upgrade-builtins.sh\n# Subsequent boots: compare image version; upgrade only if changed\n# ============================================================\nmkdir -p /root/manager-workspace\n\nIMAGE_VERSION=$(cat /opt/hiclaw/agent/.builtin-version 2>/dev/null || echo \"unknown\")\nINSTALLED_VERSION=$(cat /root/manager-workspace/.builtin-version 2>/dev/null || echo \"\")\n\nif [ ! -f /root/manager-workspace/.initialized ]; then\n log \"First boot: initializing manager workspace...\"\n bash /opt/hiclaw/scripts/init/upgrade-builtins.sh\n touch /root/manager-workspace/.initialized\n log \"Manager workspace initialized (version: ${IMAGE_VERSION})\"\nelif [ \"${IMAGE_VERSION}\" != \"${INSTALLED_VERSION}\" ] || [ \"${IMAGE_VERSION}\" = \"latest\" ]; then\n log \"Upgrade detected: ${INSTALLED_VERSION} -> ${IMAGE_VERSION}${IMAGE_VERSION:+ (latest: always upgrade)}\"\n bash /opt/hiclaw/scripts/init/upgrade-builtins.sh\n log \"Manager workspace upgraded to version: ${IMAGE_VERSION}\"\nelse\n log \"Workspace up to date (version: ${IMAGE_VERSION})\"\nfi\n\n# Local mode: wait for mc mirror initialization (shared + worker data in /root/hiclaw-fs/)\nif [ \"${HICLAW_RUNTIME}\" != \"aliyun\" ] && [ \"${HICLAW_RUNTIME}\" != \"k8s\" ]; then\n log \"Waiting for MinIO storage initialization...\"\n _minio_wait=0\n while [ ! -f /root/hiclaw-fs/.initialized ]; do\n sleep 2\n _minio_wait=$(( _minio_wait + 1 ))\n if [ \"${_minio_wait}\" -ge 60 ]; then\n log \"ERROR: MinIO storage initialization timed out after 120s\"\n exit 1\n fi\n done\n log \"MinIO storage initialized\"\nfi\n\n# ============================================================\n# Register Matrix users via Registration API (single-step, no UIAA)\n# K8s mode: skip — controller Initializer + ManagerReconciler already did this\n# ============================================================\nif [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n log \"K8s mode: skipping Matrix registration (handled by controller)\"\n # Controller injects HICLAW_MANAGER_PASSWORD via env; login to get token\n log \"Obtaining Manager Matrix access token...\"\n _LOGIN_RESPONSE=$(curl -s -X POST ${HICLAW_MATRIX_URL}/_matrix/client/v3/login \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"type\": \"m.login.password\",\n \"identifier\": {\"type\": \"m.id.user\", \"user\": \"manager\"},\n \"password\": \"'\"${HICLAW_MANAGER_PASSWORD}\"'\"\n }' 2>&1)\n MANAGER_TOKEN=$(echo \"${_LOGIN_RESPONSE}\" | jq -r '.access_token' 2>/dev/null)\n if [ -z \"${MANAGER_TOKEN}\" ] || [ \"${MANAGER_TOKEN}\" = \"null\" ]; then\n log \"ERROR: Failed to obtain Manager Matrix token\"\n log \"ERROR: Login response was: ${_LOGIN_RESPONSE}\"\n exit 1\n fi\n log \"Manager Matrix token obtained (token prefix: ${MANAGER_TOKEN:0:10}...)\"\nelse\nlog \"Registering human admin Matrix account...\"\ncurl -sf -X POST ${HICLAW_MATRIX_URL}/_matrix/client/v3/register \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"username\": \"'\"${HICLAW_ADMIN_USER}\"'\",\n \"password\": \"'\"${HICLAW_ADMIN_PASSWORD}\"'\",\n \"auth\": {\n \"type\": \"m.login.registration_token\",\n \"token\": \"'\"${HICLAW_REGISTRATION_TOKEN}\"'\"\n }\n }' > /dev/null 2>&1 || log \"Admin account may already exist\"\n\nlog \"Registering Manager Agent Matrix account...\"\ncurl -sf -X POST ${HICLAW_MATRIX_URL}/_matrix/client/v3/register \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"username\": \"manager\",\n \"password\": \"'\"${HICLAW_MANAGER_PASSWORD}\"'\",\n \"auth\": {\n \"type\": \"m.login.registration_token\",\n \"token\": \"'\"${HICLAW_REGISTRATION_TOKEN}\"'\"\n }\n }' > /dev/null 2>&1 || log \"Manager account may already exist\"\n\n# Get Manager Agent's Matrix access token\nlog \"Obtaining Manager Matrix access token...\"\n_LOGIN_RESPONSE=$(curl -s -X POST ${HICLAW_MATRIX_URL}/_matrix/client/v3/login \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"type\": \"m.login.password\",\n \"identifier\": {\"type\": \"m.id.user\", \"user\": \"manager\"},\n \"password\": \"'\"${HICLAW_MANAGER_PASSWORD}\"'\"\n }' 2>&1)\n_LOGIN_EXIT=$?\nlog \"Matrix login HTTP exit code: ${_LOGIN_EXIT}\"\nlog \"Matrix login response: ${_LOGIN_RESPONSE}\"\n\nMANAGER_TOKEN=$(echo \"${_LOGIN_RESPONSE}\" | jq -r '.access_token' 2>/dev/null)\n\nif [ -z \"${MANAGER_TOKEN}\" ] || [ \"${MANAGER_TOKEN}\" = \"null\" ]; then\n log \"ERROR: Failed to obtain Manager Matrix token (exit=${_LOGIN_EXIT})\"\n log \"ERROR: Login response was: ${_LOGIN_RESPONSE}\"\n exit 1\nfi\nlog \"Manager Matrix token obtained (token prefix: ${MANAGER_TOKEN:0:10}...)\"\nfi\n\n# ============================================================\n# Higress Console initialization\n# Docker mode: full setup-higress.sh (internal Higress at localhost:8001)\n# K8s mode: skip — controller Initializer handles Higress setup\n# Cloud (aliyun) mode: skip entirely (Higress managed externally)\n# ============================================================\n_HIGRESS_CONSOLE_URL=\"\"\n_HIGRESS_USER=\"${HICLAW_HIGRESS_ADMIN_USER:-${HICLAW_ADMIN_USER}}\"\n_HIGRESS_PASS=\"${HICLAW_HIGRESS_ADMIN_PASSWORD:-${HICLAW_ADMIN_PASSWORD}}\"\nif [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n log \"K8s mode: skipping Higress initialization (handled by controller)\"\nelif [ \"${HICLAW_RUNTIME}\" != \"aliyun\" ]; then\n _HIGRESS_CONSOLE_URL=\"http://127.0.0.1:8001\"\nfi\n\nif [ -n \"${_HIGRESS_CONSOLE_URL}\" ]; then\n COOKIE_FILE=\"/tmp/higress-session-cookie\"\n\n log \"Waiting for Higress Console (${_HIGRESS_CONSOLE_URL}) to be fully ready and initializing admin...\"\n INIT_DONE=false\n for i in $(seq 1 90); do\n INIT_RESULT=$(curl -s -X POST \"${_HIGRESS_CONSOLE_URL}/system/init\" \\\n -H 'Content-Type: application/json' \\\n -d '{\"adminUser\":{\"name\":\"'\"${_HIGRESS_USER}\"'\",\"password\":\"'\"${_HIGRESS_PASS}\"'\",\"displayName\":\"'\"${_HIGRESS_USER}\"'\"}}' 2>/dev/null) || true\n if echo \"${INIT_RESULT}\" | grep -qE '\"success\":true|already.?init' 2>/dev/null; then\n INIT_DONE=true\n break\n fi\n if echo \"${INIT_RESULT}\" | grep -q '\"name\"' 2>/dev/null; then\n INIT_DONE=true\n break\n fi\n sleep 2\n done\n\n if [ \"${INIT_DONE}\" != \"true\" ]; then\n log \"ERROR: Higress Console did not become ready within 180s\"\n exit 1\n fi\n log \"Higress Console init done\"\n\n log \"Logging into Higress Console...\"\n LOGIN_OK=false\n for i in $(seq 1 10); do\n HTTP_CODE=$(curl -s -o /dev/null -w '%{http_code}' -X POST \"${_HIGRESS_CONSOLE_URL}/session/login\" \\\n -H 'Content-Type: application/json' \\\n -c \"${COOKIE_FILE}\" \\\n -d '{\"username\":\"'\"${_HIGRESS_USER}\"'\",\"password\":\"'\"${_HIGRESS_PASS}\"'\"}' 2>/dev/null) || true\n if { [ \"${HTTP_CODE}\" = \"200\" ] || [ \"${HTTP_CODE}\" = \"201\" ]; } && [ -f \"${COOKIE_FILE}\" ] && [ -s \"${COOKIE_FILE}\" ]; then\n LOGIN_OK=true\n break\n fi\n log \"Login attempt $i (HTTP ${HTTP_CODE}), retrying in 3s...\"\n sleep 3\n done\n\n if [ \"${LOGIN_OK}\" != \"true\" ]; then\n log \"ERROR: Could not login to Higress Console after retries\"\n exit 1\n fi\n log \"Higress Console login successful\"\n\n VERIFY_CODE=$(curl -s -o /dev/null -w '%{http_code}' \"${_HIGRESS_CONSOLE_URL}/v1/consumers\" -b \"${COOKIE_FILE}\" 2>/dev/null) || true\n if [ \"${VERIFY_CODE}\" = \"200\" ]; then\n log \"Console session verified (cookie valid)\"\n else\n log \"WARNING: Console session may be invalid (verify returned HTTP ${VERIFY_CODE})\"\n rm -f \"${COOKIE_FILE}\"\n for i in $(seq 1 5); do\n curl -s -o /dev/null -w '%{http_code}' -X POST \"${_HIGRESS_CONSOLE_URL}/session/login\" \\\n -H 'Content-Type: application/json' \\\n -c \"${COOKIE_FILE}\" \\\n -d '{\"username\":\"'\"${_HIGRESS_USER}\"'\",\"password\":\"'\"${_HIGRESS_PASS}\"'\"}' 2>/dev/null\n VERIFY2=$(curl -s -o /dev/null -w '%{http_code}' \"${_HIGRESS_CONSOLE_URL}/v1/consumers\" -b \"${COOKIE_FILE}\" 2>/dev/null) || true\n if [ \"${VERIFY2}\" = \"200\" ]; then\n log \"Re-login successful, session verified\"\n break\n fi\n sleep 2\n done\n fi\n\n export HIGRESS_COOKIE_FILE=\"${COOKIE_FILE}\"\n export HIGRESS_CONSOLE_URL=\"${_HIGRESS_CONSOLE_URL}\"\n\n if [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n # K8s mode: lightweight Higress config — only what's needed for LLM access\n source /opt/hiclaw/scripts/lib/base.sh\n _k8s_higress_api() {\n local method=\"$1\" path=\"$2\" desc=\"$3\"; shift 3; local body=\"$*\"\n local tmpfile; tmpfile=$(mktemp)\n local http_code\n http_code=$(curl -s -o \"${tmpfile}\" -w '%{http_code}' -X \"${method}\" \"${_HIGRESS_CONSOLE_URL}${path}\" \\\n -b \"${COOKIE_FILE}\" -H 'Content-Type: application/json' -d \"${body}\" 2>/dev/null) || true\n local response; response=$(cat \"${tmpfile}\" 2>/dev/null); rm -f \"${tmpfile}\"\n if echo \"${response}\" | grep -q '\"success\":true' 2>/dev/null; then\n log \"${desc} ... OK\"\n elif [ \"${http_code}\" = \"409\" ]; then\n log \"${desc} ... already exists, skipping\"\n elif [ \"${http_code}\" = \"200\" ] || [ \"${http_code}\" = \"201\" ] || [ \"${http_code}\" = \"204\" ]; then\n log \"${desc} ... OK (HTTP ${http_code})\"\n else\n log \"WARNING: ${desc} ... (HTTP ${http_code}): ${response}\"\n fi\n }\n\n # 1. Service Sources (DNS type → K8s Service FQDN)\n # Extract host:port from URLs for Higress service source registration\n _TUWUNEL_HOST=$(echo \"${HICLAW_MATRIX_URL}\" | sed 's|^http[s]*://||')\n _TUWUNEL_DOMAIN=$(echo \"${_TUWUNEL_HOST}\" | cut -d: -f1)\n _TUWUNEL_PORT=$(echo \"${_TUWUNEL_HOST}\" | cut -d: -f2)\n _k8s_higress_api POST /v1/service-sources \"Registering Tuwunel service source\" \\\n '{\"type\":\"dns\",\"name\":\"tuwunel\",\"domain\":\"'\"${_TUWUNEL_DOMAIN}\"'\",\"port\":'\"${_TUWUNEL_PORT}\"'}'\n\n if [ -n \"${HICLAW_ELEMENT_WEB_URL:-}\" ]; then\n _ELEMENT_HOST=$(echo \"${HICLAW_ELEMENT_WEB_URL}\" | sed 's|^http[s]*://||')\n _ELEMENT_DOMAIN=$(echo \"${_ELEMENT_HOST}\" | cut -d: -f1)\n _ELEMENT_PORT=$(echo \"${_ELEMENT_HOST}\" | cut -d: -f2)\n _k8s_higress_api POST /v1/service-sources \"Registering Element Web service source\" \\\n '{\"type\":\"dns\",\"name\":\"element-web\",\"domain\":\"'\"${_ELEMENT_DOMAIN}\"'\",\"port\":'\"${_ELEMENT_PORT}\"'}'\n fi\n\n # 2. Manager Consumer (key-auth)\n _k8s_higress_api POST /v1/consumers \"Creating Manager consumer\" \\\n '{\"name\":\"manager\",\"credentials\":[{\"type\":\"key-auth\",\"source\":\"BEARER\",\"values\":[\"'\"${HICLAW_MANAGER_GATEWAY_KEY}\"'\"]}]}'\n\n # 3. LLM Provider\n _LLM_PROVIDER=\"${HICLAW_LLM_PROVIDER:-qwen}\"\n _LLM_API_URL=\"${HICLAW_LLM_API_URL:-}\"\n if [ -z \"${_LLM_API_URL}\" ]; then\n case \"${_LLM_PROVIDER}\" in\n qwen) _LLM_API_URL=\"https://dashscope.aliyuncs.com/compatible-mode/v1\" ;;\n esac\n fi\n # 4. LLM Provider type-specific config\n case \"${_LLM_PROVIDER}\" in\n qwen)\n _k8s_higress_api POST /v1/ai/providers \"Creating LLM provider (qwen)\" \\\n '{\"type\":\"qwen\",\"name\":\"qwen\",\"tokens\":[\"'\"${HICLAW_LLM_API_KEY}\"'\"],\"protocol\":\"openai/v1\",\"tokenFailoverConfig\":{\"enabled\":false},\"rawConfigs\":{\"qwenEnableSearch\":false,\"qwenEnableCompatible\":true,\"qwenFileIds\":[],\"hiclawMode\":true}}'\n ;;\n *)\n _BODY='{\"name\":\"'\"${_LLM_PROVIDER}\"'\",\"type\":\"openai\",\"tokens\":[\"'\"${HICLAW_LLM_API_KEY}\"'\"],\"modelMapping\":{},\"protocol\":\"openai/v1\",\"rawConfigs\":{\"hiclawMode\":true}}'\n _k8s_higress_api POST /v1/ai/providers \"Creating LLM provider (${_LLM_PROVIDER})\" \"${_BODY}\"\n ;;\n esac\n\n # 5. AI Route (bind provider + consumer auth, /v1 prefix to avoid clash with Element Web catch-all)\n _k8s_higress_api POST /v1/ai/routes \"Creating AI Gateway route\" \\\n '{\"name\":\"default-ai-route\",\"domains\":[],\"pathPredicate\":{\"matchType\":\"PRE\",\"matchValue\":\"/v1\",\"caseSensitive\":false},\"upstreams\":[{\"provider\":\"'\"${_LLM_PROVIDER}\"'\",\"weight\":100,\"modelMapping\":{}}],\"authConfig\":{\"enabled\":true,\"allowedCredentialTypes\":[\"key-auth\"],\"allowedConsumers\":[\"manager\"]}}'\n\n # 6. Matrix Homeserver Route (/_matrix/* → Tuwunel, no auth)\n _k8s_higress_api POST /v1/routes \"Creating Matrix Homeserver route\" \\\n '{\"name\":\"matrix-homeserver\",\"domains\":[],\"path\":{\"matchType\":\"PRE\",\"matchValue\":\"/_matrix\"},\"services\":[{\"name\":\"tuwunel.dns\",\"port\":'\"${_TUWUNEL_PORT}\"',\"weight\":100}]}'\n\n # 7. Element Web Route (/ catch-all → Element Web, no auth)\n if [ -n \"${HICLAW_ELEMENT_WEB_URL:-}\" ]; then\n _k8s_higress_api POST /v1/routes \"Creating Element Web route\" \\\n '{\"name\":\"element-web\",\"domains\":[],\"path\":{\"matchType\":\"PRE\",\"matchValue\":\"/\"},\"services\":[{\"name\":\"element-web.dns\",\"port\":'\"${_ELEMENT_PORT}\"',\"weight\":100}]}'\n fi\n\n # 8. Remove Higress default landing page (Exact match on / takes precedence over Element Web catch-all)\n _k8s_higress_api DELETE /v1/routes/default \"Removing Higress default landing route\"\n\n log \"K8s Higress lightweight setup complete\"\n\n # Wait for AI plugin activation (~45s for first config)\n log \"Waiting for AI Gateway plugin activation (45s)...\"\n sleep 45\n else\n # Docker mode: full setup with all routes, domains, MCP servers\n /opt/hiclaw/scripts/init/setup-higress.sh\n fi\nfi\n\n# ============================================================\n# Create admin DM room, persist to state.json, send welcome message\n# K8s mode: skip — controller ProvisionManager already creates the Admin DM room\n# Runs in both local and cloud modes (idempotent)\n# ============================================================\nif [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n log \"K8s mode: skipping admin DM room creation (handled by controller)\"\nelse\nMANAGER_FULL_ID=\"@manager:${MATRIX_DOMAIN}\"\nADMIN_FULL_ID=\"@${HICLAW_ADMIN_USER}:${MATRIX_DOMAIN}\"\n\nlog \"Logging in as admin to create DM room...\"\n_ADMIN_LOGIN=$(curl -sf -X POST \"${HICLAW_MATRIX_URL}/_matrix/client/v3/login\" \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"type\": \"m.login.password\",\n \"identifier\": {\"type\": \"m.id.user\", \"user\": \"'\"${HICLAW_ADMIN_USER}\"'\"},\n \"password\": \"'\"${HICLAW_ADMIN_PASSWORD}\"'\"\n }' 2>&1) || true\n\nADMIN_MATRIX_TOKEN=$(echo \"${_ADMIN_LOGIN}\" | jq -r '.access_token // empty' 2>/dev/null)\nif [ -z \"${ADMIN_MATRIX_TOKEN}\" ]; then\n log \"WARNING: Failed to login as admin, skipping DM room creation\"\nelse\n # Search for existing DM room with Manager (idempotent)\n DM_ROOM_ID=\"\"\n _JOINED_ROOMS=$(curl -sf \"${HICLAW_MATRIX_URL}/_matrix/client/v3/joined_rooms\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" 2>/dev/null \\\n | jq -r '.joined_rooms[]' 2>/dev/null) || true\n for _rid in ${_JOINED_ROOMS}; do\n _members=$(curl -sf \"${HICLAW_MATRIX_URL}/_matrix/client/v3/rooms/${_rid}/members\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" 2>/dev/null \\\n | jq -r '.chunk[].state_key' 2>/dev/null) || continue\n _count=$(echo \"${_members}\" | wc -l | xargs)\n if [ \"${_count}\" = \"2\" ] && echo \"${_members}\" | grep -q \"@manager:\"; then\n DM_ROOM_ID=\"${_rid}\"\n break\n fi\n done\n\n if [ -n \"${DM_ROOM_ID}\" ]; then\n log \"Existing DM room found: ${DM_ROOM_ID}\"\n else\n log \"Creating DM room with Manager...\"\n _RAW=$(curl -s -w '\\nHTTP_CODE:%{http_code}' -X POST \"${HICLAW_MATRIX_URL}/_matrix/client/v3/createRoom\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d \"{\\\"is_direct\\\":true,\\\"invite\\\":[\\\"${MANAGER_FULL_ID}\\\"],\\\"preset\\\":\\\"trusted_private_chat\\\"}\" 2>&1) || true\n _HTTP_CODE=$(echo \"${_RAW}\" | tail -1 | sed 's/HTTP_CODE://')\n _CREATE_RESP=$(echo \"${_RAW}\" | sed '$d')\n DM_ROOM_ID=$(echo \"${_CREATE_RESP}\" | jq -r '.room_id // empty' 2>/dev/null)\n if [ -n \"${DM_ROOM_ID}\" ]; then\n log \"DM room created: ${DM_ROOM_ID}\"\n else\n log \"WARNING: Failed to create DM room (HTTP ${_HTTP_CODE}): ${_CREATE_RESP}\"\n fi\n fi\n\n # Persist admin DM room ID to state.json\n if [ -n \"${DM_ROOM_ID}\" ]; then\n STATE_SCRIPT=\"/opt/hiclaw/agent/skills/task-management/scripts/manage-state.sh\"\n if [ -f \"${STATE_SCRIPT}\" ]; then\n bash \"${STATE_SCRIPT}\" --action init 2>/dev/null || true\n bash \"${STATE_SCRIPT}\" --action set-admin-dm --room-id \"${DM_ROOM_ID}\" 2>/dev/null || true\n log \"Admin DM room persisted to state.json: ${DM_ROOM_ID}\"\n fi\n fi\n\n # Schedule welcome message in background (only on first boot)\n if [ -n \"${DM_ROOM_ID}\" ] && [ ! -f \"/root/manager-workspace/soul-configured\" ]; then\n log \"Scheduling welcome message (background, waiting for OpenClaw to start)...\"\n (\n _HICLAW_LANGUAGE=\"${HICLAW_LANGUAGE:-zh}\"\n _HICLAW_TIMEZONE=\"${TZ:-Asia/Shanghai}\"\n _wait=0\n _ready=false\n while [ \"${_wait}\" -lt 300 ]; do\n if curl -sf http://127.0.0.1:18799/ > /dev/null 2>&1; then\n _ready=true\n break\n fi\n sleep 3\n _wait=$((_wait + 3))\n done\n if [ \"${_ready}\" != \"true\" ]; then\n echo \"[manager] WARNING: OpenClaw gateway not ready within 300s, skipping welcome message\"\n exit 0\n fi\n # Ensure Manager has joined the DM room before sending the welcome\n # message. Without this, there is a race between OpenClaw's Matrix\n # auto-join and the message send — the message may land before Manager\n # joins, so OpenClaw's /sync never picks it up.\n _join_ok=false\n for _join_attempt in 1 2 3; do\n if curl -sf -X POST \"${HICLAW_MATRIX_URL}/_matrix/client/v3/rooms/${DM_ROOM_ID}/join\" \\\n -H \"Authorization: Bearer ${MANAGER_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d '{}' > /dev/null 2>&1; then\n echo \"[manager] Manager joined DM room before welcome message\"\n _join_ok=true\n break\n fi\n sleep 2\n done\n if [ \"${_join_ok}\" != \"true\" ]; then\n echo \"[manager] WARNING: Manager join request failed after 3 attempts (may already be joined)\"\n fi\n _welcome_msg=\"This is an automated message from the HiClaw setup. This is a fresh installation.\n\n--- Installation Context ---\nUser Language: ${_HICLAW_LANGUAGE} (zh = Chinese, en = English)\nUser Timezone: ${_HICLAW_TIMEZONE} (IANA timezone identifier)\n---\n\nYou are an AI agent that manages a team of worker agents. Your identity and personality have not been configured yet — the human admin is about to meet you for the first time.\n\nPlease begin the onboarding conversation:\n\n1. Greet the admin warmly and briefly describe what you can do (coordinate workers, manage tasks, run multi-agent projects)\n2. The user has selected \\\"${_HICLAW_LANGUAGE}\\\" as their preferred language during installation. Use this language for your greeting and all subsequent communication.\n3. The user's timezone is ${_HICLAW_TIMEZONE}. Based on this timezone, you may infer their likely region and suggest additional language options.\n4. Ask them: a) What would they like to call you? b) Communication style preference? c) Any behavior guidelines? d) Confirm default language\n5. After they reply, write their preferences to ~/SOUL.md\n6. Confirm what you wrote, and ask if they would like to adjust anything\n7. Once confirmed, run: touch ~/soul-configured\n\nThe human admin will start chatting shortly.\"\n _txn_id=\"welcome-$(date +%s)\"\n _payload=$(jq -nc --arg body \"${_welcome_msg}\" '{\"msgtype\":\"m.text\",\"body\":$body}')\n _raw=$(curl -s -w '\\nHTTP_CODE:%{http_code}' -X PUT \"${HICLAW_MATRIX_URL}/_matrix/client/v3/rooms/${DM_ROOM_ID}/send/m.room.message/${_txn_id}\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d \"${_payload}\" 2>&1) || true\n _http_code=$(echo \"${_raw}\" | tail -1 | sed 's/HTTP_CODE://')\n _send_resp=$(echo \"${_raw}\" | sed '$d')\n if echo \"${_send_resp}\" | jq -e '.event_id' > /dev/null 2>&1; then\n echo \"[manager] Welcome message sent to DM room\"\n else\n echo \"[manager] WARNING: Failed to send welcome message (HTTP ${_http_code}): ${_send_resp}\"\n fi\n ) &\n log \"Welcome message background process started (PID: $!)\"\n fi\nfi\nfi # end K8s mode skip for admin DM room\n\n# ============================================================\n# Generate Manager Agent openclaw.json from template\n# ============================================================\nlog \"Generating Manager openclaw.json...\"\nexport MANAGER_MATRIX_TOKEN=\"${MANAGER_TOKEN}\"\nexport MANAGER_GATEWAY_KEY=\"${HICLAW_MANAGER_GATEWAY_KEY}\"\n# Resolve model parameters based on model name\nMODEL_NAME=\"${HICLAW_DEFAULT_MODEL:-qwen3.5-plus}\"\ncase \"${MODEL_NAME}\" in\n gpt-5.3-codex|gpt-5-mini|gpt-5-nano)\n export MODEL_CONTEXT_WINDOW=400000 MODEL_MAX_TOKENS=128000 ;;\n claude-opus-4-6)\n export MODEL_CONTEXT_WINDOW=1000000 MODEL_MAX_TOKENS=128000 ;;\n claude-sonnet-4-6)\n export MODEL_CONTEXT_WINDOW=1000000 MODEL_MAX_TOKENS=64000 ;;\n claude-haiku-4-5)\n export MODEL_CONTEXT_WINDOW=200000 MODEL_MAX_TOKENS=64000 ;;\n qwen3.5-plus)\n export MODEL_CONTEXT_WINDOW=200000 MODEL_MAX_TOKENS=64000 ;;\n deepseek-chat|deepseek-reasoner|kimi-k2.5)\n export MODEL_CONTEXT_WINDOW=256000 MODEL_MAX_TOKENS=128000 ;;\n glm-5|MiniMax-M2.7|MiniMax-M2.7-highspeed|MiniMax-M2.5)\n export MODEL_CONTEXT_WINDOW=200000 MODEL_MAX_TOKENS=128000 ;;\n *)\n export MODEL_CONTEXT_WINDOW=150000 MODEL_MAX_TOKENS=128000 ;;\nesac\nexport MODEL_REASONING=true\n\n# Override with user-supplied custom model parameters from env (set during install)\n[ -n \"${HICLAW_MODEL_CONTEXT_WINDOW:-}\" ] && export MODEL_CONTEXT_WINDOW=\"${HICLAW_MODEL_CONTEXT_WINDOW}\"\n[ -n \"${HICLAW_MODEL_MAX_TOKENS:-}\" ] && export MODEL_MAX_TOKENS=\"${HICLAW_MODEL_MAX_TOKENS}\"\n[ -n \"${HICLAW_MODEL_REASONING:-}\" ] && export MODEL_REASONING=\"${HICLAW_MODEL_REASONING}\"\n\n# E2EE: convert HICLAW_MATRIX_E2EE to JSON boolean for template substitution\nif [ \"${HICLAW_MATRIX_E2EE:-0}\" = \"1\" ] || [ \"${HICLAW_MATRIX_E2EE:-}\" = \"true\" ]; then\n export MATRIX_E2EE_ENABLED=true\nelse\n export MATRIX_E2EE_ENABLED=false\nfi\nlog \"Matrix E2EE: ${MATRIX_E2EE_ENABLED}\"\n\n# Resolve input modalities: only vision-capable models get \"image\"\ncase \"${MODEL_NAME}\" in\n gpt-5.4|gpt-5.3-codex|gpt-5-mini|gpt-5-nano|claude-opus-4-6|claude-sonnet-4-6|claude-haiku-4-5|qwen3.5-plus|kimi-k2.5)\n export MODEL_INPUT='[\"text\", \"image\"]' ;;\n *)\n export MODEL_INPUT='[\"text\"]' ;;\nesac\n# Override with user-supplied vision setting from env\nif [ \"${HICLAW_MODEL_VISION:-}\" = \"true\" ]; then\n export MODEL_INPUT='[\"text\", \"image\"]'\nelif [ \"${HICLAW_MODEL_VISION:-}\" = \"false\" ]; then\n export MODEL_INPUT='[\"text\"]'\nfi\n\nlog \"Model: ${MODEL_NAME} (context=${MODEL_CONTEXT_WINDOW}, maxTokens=${MODEL_MAX_TOKENS}, reasoning=${MODEL_REASONING}, input=${MODEL_INPUT})\"\n\nif [ -f /root/manager-workspace/openclaw.json ]; then\n log \"Manager openclaw.json already exists, updating dynamic fields only (preserving user customizations)...\"\n # Merge known models into existing config (add missing, preserve user-added)\n # Use known-models.json (valid JSON) instead of template (contains ${VAR} placeholders)\n KNOWN_MODELS=$(cat /opt/hiclaw/configs/known-models.json 2>/dev/null || echo '[]')\n jq --arg token \"${MANAGER_TOKEN}\" \\\n --arg key \"${HICLAW_MANAGER_GATEWAY_KEY}\" \\\n --arg model \"${MODEL_NAME}\" \\\n --arg emb_model \"${HICLAW_EMBEDDING_MODEL}\" \\\n --arg aigw_domain \"${AI_GATEWAY_DOMAIN}\" \\\n --argjson e2ee \"${MATRIX_E2EE_ENABLED}\" \\\n --argjson known_models \"${KNOWN_MODELS}\" \\\n --argjson ctx \"${MODEL_CONTEXT_WINDOW}\" \\\n --argjson max \"${MODEL_MAX_TOKENS}\" \\\n --argjson reasoning \"${MODEL_REASONING}\" \\\n --argjson input \"${MODEL_INPUT}\" \\\n '\n # Merge known models: add any model id not already present\n .models.providers[\"hiclaw-gateway\"].models as $existing\n | ($existing | map(.id)) as $existing_ids\n | ($known_models | map(select(.id as $id | $existing_ids | index($id) | not))) as $new\n | .models.providers[\"hiclaw-gateway\"].models = ($existing + $new)\n # Ensure the user-chosen default model is in the list (custom model support)\n | if (.models.providers[\"hiclaw-gateway\"].models | map(.id) | index($model) | not) then\n .models.providers[\"hiclaw-gateway\"].models += [{\"id\": $model, \"name\": $model, \"reasoning\": $reasoning, \"contextWindow\": $ctx, \"maxTokens\": $max, \"input\": $input}]\n else . end\n # Rebuild model aliases from the full models list\n | (.models.providers[\"hiclaw-gateway\"].models | map({ (\"hiclaw-gateway/\" + .id): { \"alias\": .id } }) | add // {}) as $aliases\n | .agents.defaults.models = ((.agents.defaults.models // {}) + $aliases)\n | .channels.matrix.accessToken = $token | .models.providers[\"hiclaw-gateway\"].apiKey = $key\n | ((.hooks.token // \"\") as $ht | if $ht == $key or $ht == ($key + \"-hooks\" | @base64) then del(.hooks) else . end)\n | .agents.defaults.model.primary = (\"hiclaw-gateway/\" + $model)\n | .commands.restart = true\n | .gateway.controlUi.dangerouslyDisableDeviceAuth = true\n | .channels.matrix.encryption = $e2ee\n # Ensure memorySearch config exists (embedding model for memory) — skip if embedding model is empty\n | if $emb_model != \"\" then .agents.defaults.memorySearch //= {\"provider\":\"openai\",\"model\":$emb_model,\"remote\":{\"baseUrl\":(\"http://\" + $aigw_domain + \":8080/v1\"),\"apiKey\":$key}} else . end\n ' \\\n /root/manager-workspace/openclaw.json > /tmp/openclaw.json.tmp && \\\n mv /tmp/openclaw.json.tmp /root/manager-workspace/openclaw.json\n # Verify the token was written correctly\n _written_token=$(jq -r '.channels.matrix.accessToken' /root/manager-workspace/openclaw.json 2>/dev/null)\n if [ -z \"${_written_token}\" ] || [ \"${_written_token}\" = \"null\" ]; then\n log \"ERROR: Matrix token was not written correctly to openclaw.json (got: ${_written_token})\"\n else\n log \"Matrix token written to openclaw.json (prefix: ${_written_token:0:10}...)\"\n fi\nelse\n log \"Manager openclaw.json not found, generating from template...\"\n envsubst < /opt/hiclaw/configs/manager-openclaw.json.tmpl > /root/manager-workspace/openclaw.json\n # Post-envsubst injection: memorySearch + custom model (single jq pass when possible)\n if ! jq -e --arg model \"${MODEL_NAME}\" '.models.providers[\"hiclaw-gateway\"].models | map(.id) | index($model)' /root/manager-workspace/openclaw.json > /dev/null 2>&1; then\n log \"Custom model '${MODEL_NAME}' not in built-in list, injecting into config...\"\n jq --arg emb_model \"${HICLAW_EMBEDDING_MODEL}\" \\\n --arg aigw_domain \"${AI_GATEWAY_DOMAIN}\" \\\n --arg key \"${HICLAW_MANAGER_GATEWAY_KEY}\" \\\n --arg model \"${MODEL_NAME}\" \\\n --argjson ctx \"${MODEL_CONTEXT_WINDOW}\" \\\n --argjson max \"${MODEL_MAX_TOKENS}\" \\\n --argjson reasoning \"${MODEL_REASONING}\" \\\n --argjson input \"${MODEL_INPUT}\" \\\n '\n (if $emb_model != \"\" then .agents.defaults.memorySearch = {\"provider\":\"openai\",\"model\":$emb_model,\"remote\":{\"baseUrl\":(\"http://\" + $aigw_domain + \":8080/v1\"),\"apiKey\":$key}} else . end)\n | .models.providers[\"hiclaw-gateway\"].models += [{\"id\": $model, \"name\": $model, \"reasoning\": $reasoning, \"contextWindow\": $ctx, \"maxTokens\": $max, \"input\": $input}]\n | .agents.defaults.models += {(\"hiclaw-gateway/\" + $model): {\"alias\": $model}}\n ' /root/manager-workspace/openclaw.json > /tmp/openclaw.json.tmp && \\\n mv /tmp/openclaw.json.tmp /root/manager-workspace/openclaw.json\n elif [ -n \"${HICLAW_EMBEDDING_MODEL}\" ]; then\n jq --arg emb_model \"${HICLAW_EMBEDDING_MODEL}\" \\\n --arg aigw_domain \"${AI_GATEWAY_DOMAIN}\" \\\n --arg key \"${HICLAW_MANAGER_GATEWAY_KEY}\" \\\n '.agents.defaults.memorySearch = {\"provider\":\"openai\",\"model\":$emb_model,\"remote\":{\"baseUrl\":(\"http://\" + $aigw_domain + \":8080/v1\"),\"apiKey\":$key}}' \\\n /root/manager-workspace/openclaw.json > /tmp/openclaw.json.tmp && \\\n mv /tmp/openclaw.json.tmp /root/manager-workspace/openclaw.json\n fi\n _written_token=$(jq -r '.channels.matrix.accessToken' /root/manager-workspace/openclaw.json 2>/dev/null)\n log \"Matrix token written from template (prefix: ${_written_token:0:10}...)\"\nfi\n\n# Cloud/K8s mode: overlay cloud-specific settings onto generated config\nif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ] || [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n log \"Applying cloud/k8s overlay to openclaw.json...\"\n jq --arg homeserver \"${HICLAW_MATRIX_URL}\" \\\n --arg gateway \"${HICLAW_AI_GATEWAY_URL}/v1\" \\\n --arg key \"${HICLAW_MANAGER_GATEWAY_KEY}\" \\\n '.channels.matrix.homeserver = $homeserver\n | .models.providers[\"hiclaw-gateway\"].baseUrl = $gateway\n | .models.providers[\"hiclaw-gateway\"].apiKey = $key\n | ((.hooks.token // \"\") as $ht | if $ht == $key or $ht == ($key + \"-hooks\" | @base64) then del(.hooks) else . end)\n | .commands.restart = false\n | if .agents.defaults.memorySearch then .agents.defaults.memorySearch.remote.baseUrl = $gateway | .agents.defaults.memorySearch.remote.apiKey = $key else . end' \\\n /root/manager-workspace/openclaw.json > /tmp/openclaw-cloud.json && \\\n mv /tmp/openclaw-cloud.json /root/manager-workspace/openclaw.json\n log \"Cloud/K8s overlay applied\"\nfi\n\n# ============================================================\n# Optional: enable openclaw-cms-plugin observability\n# Config is applied at runtime so secrets stay out of image layers.\n# ============================================================\nCMS_TRACES_ENABLED=\"$(echo \"${HICLAW_CMS_TRACES_ENABLED:-false}\" | tr '[:upper:]' '[:lower:]')\"\nif [ \"${CMS_TRACES_ENABLED}\" = \"true\" ]; then\n CMS_PLUGIN_NAME=\"openclaw-cms-plugin\"\n CMS_PLUGIN_DIR=\"${OPENCLAW_CMS_PLUGIN_DIR:-/opt/openclaw/extensions/openclaw-cms-plugin}\"\n CMS_PLUGIN_MANIFEST=\"${CMS_PLUGIN_DIR}/openclaw.plugin.json\"\n DIAG_PLUGIN_NAME=\"diagnostics-otel\"\n DIAG_PLUGIN_DIR=\"/opt/openclaw/extensions/diagnostics-otel\"\n CMS_LICENSE_KEY=\"${HICLAW_CMS_LICENSE_KEY:-}\"\n CMS_PROJECT=\"${HICLAW_CMS_PROJECT:-}\"\n CMS_METRICS_ENABLED=\"${HICLAW_CMS_METRICS_ENABLED:-false}\"\n\n if [ ! -f \"${CMS_PLUGIN_MANIFEST}\" ]; then\n log \"WARNING: ${CMS_PLUGIN_NAME} manifest not found at ${CMS_PLUGIN_MANIFEST}, skipping plugin config\"\n else\n _missing=0\n [ -z \"${HICLAW_CMS_ENDPOINT:-}\" ] && log \"WARNING: HICLAW_CMS_ENDPOINT is required when HICLAW_CMS_TRACES_ENABLED=true\" && _missing=1\n [ -z \"${CMS_LICENSE_KEY:-}\" ] && log \"WARNING: HICLAW_CMS_LICENSE_KEY is required when HICLAW_CMS_TRACES_ENABLED=true\" && _missing=1\n [ -z \"${HICLAW_CMS_WORKSPACE:-}\" ] && log \"WARNING: HICLAW_CMS_WORKSPACE is required when HICLAW_CMS_TRACES_ENABLED=true\" && _missing=1\n\n if [ \"${_missing}\" = \"0\" ]; then\n CMS_SERVICE_NAME=\"${HICLAW_CMS_SERVICE_NAME:-hiclaw-manager}\"\n CMS_ENABLE_METRICS=\"${CMS_METRICS_ENABLED}\"\n DIAG_AVAILABLE=\"0\"\n _metrics_lc=\"$(echo \"${CMS_ENABLE_METRICS}\" | tr '[:upper:]' '[:lower:]')\"\n if [ \"${_metrics_lc}\" = \"true\" ]; then\n if [ -f \"${DIAG_PLUGIN_DIR}/package.json\" ]; then\n DIAG_AVAILABLE=\"1\"\n if [ ! -d \"${DIAG_PLUGIN_DIR}/node_modules\" ]; then\n log \"diagnostics-otel dependencies missing, installing...\"\n if (cd \"${DIAG_PLUGIN_DIR}\" && npm install --omit=dev --ignore-scripts >/tmp/hiclaw-diag-install.log 2>&1); then\n log \"diagnostics-otel dependencies installed\"\n else\n log \"WARNING: diagnostics-otel npm install failed, metrics plugin may not load\"\n fi\n else\n log \"diagnostics-otel dependencies already present\"\n fi\n else\n log \"WARNING: diagnostics-otel package.json not found at ${DIAG_PLUGIN_DIR}, metrics plugin may not load\"\n fi\n fi\n\n log \"Applying ${CMS_PLUGIN_NAME} config to openclaw.json...\"\n jq --arg pluginName \"${CMS_PLUGIN_NAME}\" \\\n --arg pluginDir \"${CMS_PLUGIN_DIR}\" \\\n --arg endpoint \"${HICLAW_CMS_ENDPOINT}\" \\\n --arg licenseKey \"${CMS_LICENSE_KEY}\" \\\n --arg armsProject \"${CMS_PROJECT}\" \\\n --arg cmsWorkspace \"${HICLAW_CMS_WORKSPACE}\" \\\n --arg serviceName \"${CMS_SERVICE_NAME}\" \\\n --arg diagPluginName \"${DIAG_PLUGIN_NAME}\" \\\n --arg diagPluginDir \"${DIAG_PLUGIN_DIR}\" \\\n --arg metricsRaw \"${CMS_ENABLE_METRICS}\" \\\n --arg diagAvailableRaw \"${DIAG_AVAILABLE}\" \\\n '\n .plugins = (.plugins // {})\n | .plugins.load = (.plugins.load // {})\n | .plugins.entries = (.plugins.entries // {})\n | if (.plugins.allow | type) != \"array\" then .plugins.allow = [] else . end\n | if (.plugins.allow | index($pluginName)) == null then .plugins.allow += [$pluginName] else . end\n | if (.plugins.load.paths | type) != \"array\" then .plugins.load.paths = [] else . end\n | if (.plugins.load.paths | index($pluginDir)) == null then .plugins.load.paths += [$pluginDir] else . end\n | .plugins.entries[$pluginName] = {\n \"enabled\": true,\n \"config\": {\n \"endpoint\": $endpoint,\n \"headers\": {\n \"x-arms-license-key\": $licenseKey,\n \"x-arms-project\": $armsProject,\n \"x-cms-workspace\": $cmsWorkspace\n },\n \"serviceName\": $serviceName\n }\n }\n\n # diagnostics-otel metrics (optional)\n | ($metricsRaw | ascii_downcase) as $m\n | ($diagAvailableRaw == \"1\") as $diagAvailable\n | (($m == \"true\") and $diagAvailable) as $metricsEnabled\n | if $metricsEnabled then\n (if (.plugins.allow | index($diagPluginName)) == null then .plugins.allow += [$diagPluginName] else . end)\n | (if (.plugins.load.paths | index($diagPluginDir)) == null then .plugins.load.paths += [$diagPluginDir] else . end)\n | .plugins.entries[$diagPluginName].enabled = true\n | .diagnostics = (.diagnostics // {})\n | .diagnostics.otel = (.diagnostics.otel // {})\n | .diagnostics.enabled = true\n | .diagnostics.otel.enabled = true\n | .diagnostics.otel.endpoint = $endpoint\n | .diagnostics.otel.protocol = (.diagnostics.otel.protocol // \"http/protobuf\")\n | .diagnostics.otel.headers = {\n \"x-arms-license-key\": $licenseKey,\n \"x-arms-project\": $armsProject,\n \"x-cms-workspace\": $cmsWorkspace\n }\n | .diagnostics.otel.serviceName = $serviceName\n | .diagnostics.otel.metrics = true\n | .diagnostics.otel.traces = (.diagnostics.otel.traces // false)\n | .diagnostics.otel.logs = (.diagnostics.otel.logs // false)\n else\n .\n end\n ' /root/manager-workspace/openclaw.json > /tmp/openclaw-cms.json && \\\n mv /tmp/openclaw-cms.json /root/manager-workspace/openclaw.json\n log \"${CMS_PLUGIN_NAME} config applied (metrics=${CMS_ENABLE_METRICS}, service=${CMS_SERVICE_NAME})\"\n else\n log \"Skipping ${CMS_PLUGIN_NAME} config due to missing required env vars\"\n fi\n fi\nfi\n\n# ============================================================\n# Detect container runtime (for Worker creation)\n# ============================================================\nsource /opt/hiclaw/scripts/lib/container-api.sh\nif container_api_available; then\n log \"Container runtime socket detected at ${CONTAINER_SOCKET} — direct Worker creation enabled\"\n export HICLAW_CONTAINER_RUNTIME=\"socket\"\nelif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ] || [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n log \"Cloud/K8s mode — Workers created via controller API\"\n export HICLAW_CONTAINER_RUNTIME=\"cloud\"\nelse\n log \"No container runtime found — Worker creation will output install commands\"\n export HICLAW_CONTAINER_RUNTIME=\"none\"\nfi\n\n# ============================================================\n# Upgrade Worker openclaw.json: merge known models + E2EE flag into existing configs\n# Existing workers in MinIO may have old single-model configs or missing encryption field.\n# Merge template models so they can hot-switch without restart.\n# ============================================================\nREGISTRY_FILE=\"/root/manager-workspace/workers-registry.json\"\nif [ -f \"${REGISTRY_FILE}\" ]; then\n # Use known-models.json (valid JSON) instead of template (contains ${VAR} placeholders)\n KNOWN_MODELS_FILE=\"/opt/hiclaw/configs/known-models.json\"\n if [ -f \"${KNOWN_MODELS_FILE}\" ]; then\n _KNOWN_MODELS=$(cat \"${KNOWN_MODELS_FILE}\")\n for _wname in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_wname}\" ] && continue\n _minio_path=\"${HICLAW_STORAGE_PREFIX}/agents/${_wname}/openclaw.json\"\n _tmp_in=\"/tmp/openclaw-${_wname}-models-upgrade-in.json\"\n if mc cp \"${_minio_path}\" \"${_tmp_in}\" 2>/dev/null; then\n _tmp_out=\"/tmp/openclaw-${_wname}-models-upgrade-out.json\"\n # Idempotent merge: add missing known models, rebuild aliases, set e2ee.\n # Always runs — jq deduplicates by model id, so re-runs are safe.\n jq --argjson known_models \"${_KNOWN_MODELS}\" \\\n --argjson e2ee \"${MATRIX_E2EE_ENABLED}\" '\n .models.providers[\"hiclaw-gateway\"].models as $existing\n | ($existing | map(.id)) as $existing_ids\n | ($known_models | map(select(.id as $id | $existing_ids | index($id) | not))) as $new\n | .models.providers[\"hiclaw-gateway\"].models = ($existing + $new)\n | (.models.providers[\"hiclaw-gateway\"].models | map({ (\"hiclaw-gateway/\" + .id): { \"alias\": .id } }) | add // {}) as $aliases\n | .agents.defaults.models = ((.agents.defaults.models // {}) + $aliases)\n | .channels.matrix.encryption = $e2ee\n ' \"${_tmp_in}\" > \"${_tmp_out}\" 2>/dev/null\n if ! diff -q \"${_tmp_in}\" \"${_tmp_out}\" > /dev/null 2>&1; then\n if mc cp \"${_tmp_out}\" \"${_minio_path}\" 2>/dev/null; then\n _new_count=$(jq '.models.providers[\"hiclaw-gateway\"].models | length' \"${_tmp_out}\" 2>/dev/null)\n log \"Worker ${_wname}: upgraded openclaw.json (models: ${_new_count}, e2ee: ${MATRIX_E2EE_ENABLED})\"\n fi\n fi\n rm -f \"${_tmp_in}\" \"${_tmp_out}\"\n fi\n done\n fi\nfi\n\n# ============================================================\n# Ensure Worker Matrix password files exist in MinIO (E2EE fix)\n# Workers need to re-login on restart to get a fresh device_id.\n# Older workers created before this fix won't have the password file.\n# ============================================================\nif [ -f \"${REGISTRY_FILE}\" ]; then\n for _wname in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_wname}\" ] && continue\n _creds_file=\"/data/worker-creds/${_wname}.env\"\n if [ -f \"${_creds_file}\" ]; then\n # Check if password file already exists in MinIO\n if ! mc stat \"${HICLAW_STORAGE_PREFIX}/agents/${_wname}/credentials/matrix/password\" > /dev/null 2>&1; then\n source \"${_creds_file}\"\n if [ -n \"${WORKER_PASSWORD}\" ]; then\n _tmp_pw=\"/tmp/matrix-pw-${_wname}\"\n echo -n \"${WORKER_PASSWORD}\" > \"${_tmp_pw}\"\n mc cp \"${_tmp_pw}\" \"${HICLAW_STORAGE_PREFIX}/agents/${_wname}/credentials/matrix/password\" 2>/dev/null \\\n && log \"Worker ${_wname}: wrote Matrix password to MinIO (E2EE re-login fix)\" \\\n || log \"Worker ${_wname}: WARNING: failed to write Matrix password to MinIO\"\n rm -f \"${_tmp_pw}\"\n fi\n fi\n fi\n done\nfi\n\n# ============================================================\n# Recreate Worker containers as needed after Manager restart.\n# Workers are on hiclaw-net; Docker DNS resolves *-local.hiclaw.io via\n# the Manager's network aliases, so IP changes don't require worker recreation.\n# Only recreate stopped/missing workers.\n# ============================================================\nif container_api_available; then\n REGISTRY_FILE=\"/root/manager-workspace/workers-registry.json\"\n if [ -f \"${REGISTRY_FILE}\" ]; then\n for _worker_name in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_worker_name}\" ] && continue\n\n # Skip remote workers — they are not Manager-managed containers.\n _deployment=$(jq -r --arg w \"${_worker_name}\" '.workers[$w].deployment // \"local\"' \"${REGISTRY_FILE}\" 2>/dev/null)\n if [ \"${_deployment}\" = \"remote\" ]; then\n log \"Worker ${_worker_name} is remote, skipping container recreate\"\n continue\n fi\n\n _status=$(container_status_worker \"${_worker_name}\")\n if [ \"${_status}\" = \"running\" ]; then\n log \"Worker running: ${_worker_name}, skipping\"\n continue\n fi\n # Container missing or stopped — recreate.\n log \"Worker container ${_status}: ${_worker_name}, recreating...\"\n _creds_file=\"/data/worker-creds/${_worker_name}.env\"\n if [ -f \"${_creds_file}\" ]; then\n source \"${_creds_file}\"\n _runtime=$(jq -r --arg w \"${_worker_name}\" '.workers[$w].runtime // \"openclaw\"' \"${REGISTRY_FILE}\" 2>/dev/null)\n _recreated=false\n for _attempt in 1 2 3; do\n local _env_map _create_body\n _env_map=$(jq -cn \\\n --arg name \"${_worker_name}\" \\\n --arg fak \"${_worker_name}\" \\\n --arg fsk \"${WORKER_MINIO_PASSWORD:-}\" \\\n --arg fs_domain \"${HICLAW_FS_DOMAIN:-fs-local.hiclaw.io}\" \\\n --arg controller_url \"${HICLAW_CONTROLLER_URL:-}\" \\\n '{\n \"HICLAW_WORKER_NAME\": $name,\n \"HICLAW_FS_ENDPOINT\": (\"http://\" + ($fs_domain | split(\":\")[0]) + \":8080\"),\n \"HICLAW_FS_ACCESS_KEY\": $fak,\n \"HICLAW_FS_SECRET_KEY\": $fsk\n }\n | if $controller_url != \"\" then . + {\"HICLAW_CONTROLLER_URL\": $controller_url} else . end')\n _create_body=$(jq -cn --arg name \"${_worker_name}\" --arg runtime \"${_runtime}\" --argjson env \"${_env_map}\" '{name: $name, runtime: $runtime, env: $env}')\n worker_backend_create \"${_create_body}\" > /dev/null 2>&1 && _recreated=true && break\n log \" Attempt ${_attempt}/3 failed for ${_worker_name}, retrying in $((5 * _attempt))s...\"\n sleep $((5 * _attempt))\n done\n if [ \"${_recreated}\" = true ]; then\n log \" Recreated ${_runtime} worker: ${_worker_name}\"\n else\n log \" ERROR: Failed to recreate ${_worker_name} after 3 attempts\"\n fi\n else\n log \" WARNING: No credentials found for ${_worker_name} (${_creds_file} missing), skipping\"\n fi\n done\n fi\nfi\n\n# ============================================================\n# Notify workers of builtin updates if upgrade happened\n# Builtin files (AGENTS.md, skills) are already synced by upgrade-builtins.sh\n#\n# Cooldown: skip notification if the last successful notify was within\n# NOTIFY_COOLDOWN_SECS (default 3600s / 1 hour). This prevents repeated\n# notifications when the Manager crash-loops and re-runs upgrade-builtins\n# on every restart (e.g. IMAGE_VERSION=latest always triggers upgrade).\n# ============================================================\nNOTIFY_COOLDOWN_SECS=\"${HICLAW_NOTIFY_COOLDOWN_SECS:-3600}\"\nNOTIFY_TS_FILE=\"/root/manager-workspace/.last-worker-notify-ts\"\n\nif [ -f /root/manager-workspace/.upgrade-pending-worker-notify ]; then\n _now=$(date +%s)\n _last_notify=$(cat \"${NOTIFY_TS_FILE}\" 2>/dev/null || echo \"0\")\n _elapsed=$(( _now - _last_notify ))\n\n if [ \"${_elapsed}\" -lt \"${NOTIFY_COOLDOWN_SECS}\" ]; then\n log \"Skipping worker builtin notification (last notify ${_elapsed}s ago, cooldown ${NOTIFY_COOLDOWN_SECS}s)\"\n rm -f /root/manager-workspace/.upgrade-pending-worker-notify\n else\n log \"Notifying workers about builtin updates...\"\n REGISTRY_FILE=\"/root/manager-workspace/workers-registry.json\"\n _notify_ok=false\n if [ -f \"${REGISTRY_FILE}\" ]; then\n for _worker_name in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_worker_name}\" ] && continue\n _room_id=$(jq -r --arg w \"${_worker_name}\" '.workers[$w].room_id // empty' \"${REGISTRY_FILE}\" 2>/dev/null)\n if [ -n \"${_room_id}\" ]; then\n _worker_id=\"@${_worker_name}:${MATRIX_DOMAIN}\"\n _txn_id=\"upgrade-$(date +%s%N)\"\n _msg=\"@${_worker_name}:${MATRIX_DOMAIN} Manager upgraded builtin files (AGENTS.md, skills). Please use your file-sync skill to sync the latest config.\"\n _raw=$(curl -s -w '\\nHTTP_CODE:%{http_code}' -X PUT \\\n \"${HICLAW_MATRIX_URL}/_matrix/client/v3/rooms/${_room_id}/send/m.room.message/${_txn_id}\" \\\n -H \"Authorization: Bearer ${MANAGER_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d \"{\\\"msgtype\\\":\\\"m.text\\\",\\\"body\\\":\\\"${_msg}\\\",\\\"m.mentions\\\":{\\\"user_ids\\\":[\\\"${_worker_id}\\\"]}}\" \\\n 2>&1) || true\n _http_code=$(echo \"${_raw}\" | tail -1 | sed 's/HTTP_CODE://')\n _notify_resp=$(echo \"${_raw}\" | sed '$d')\n if echo \"${_notify_resp}\" | jq -e '.event_id' > /dev/null 2>&1; then\n log \" Notified ${_worker_name}\"; _notify_ok=true\n else\n log \" WARNING: Failed to notify ${_worker_name} (HTTP ${_http_code}): ${_notify_resp}\"\n fi\n fi\n done\n fi\n # Record timestamp only if at least one notification succeeded\n if [ \"${_notify_ok}\" = true ]; then\n echo \"${_now}\" > \"${NOTIFY_TS_FILE}\"\n fi\n rm -f /root/manager-workspace/.upgrade-pending-worker-notify\n fi\nfi\n\n# ============================================================\n# Start Manager Agent\n# ============================================================\nlog \"Starting Manager Agent (${MANAGER_RUNTIME})...\"\n\n# HOME is already set to /root/manager-workspace via docker run -e HOME=...\ncd \"${HOME}\"\n\n# Ensure host credential symlinks exist under HOME\nif [ -d \"/host-share\" ]; then\n [ -f \"/host-share/.gitconfig\" ] && ln -sf \"/host-share/.gitconfig\" \"${HOME}/.gitconfig\"\nfi\n\nlog \"HOME=${HOME} (manager-workspace, host-mounted)\"\n\n# ── Render agent doc templates ────────────────────────────────────────────\n# Replace ${VAR} placeholders with actual values so the AI agent reads\n# plain text and never needs to resolve environment variables.\nexport MANAGER_MATRIX_TOKEN MANAGER_TOKEN HIGRESS_COOKIE_FILE\nRENDER=/opt/hiclaw/scripts/lib/render-skills.sh\nlog \"Rendering agent doc templates...\"\n# Manager-owned docs (workspace)\nbash \"$RENDER\" /root/manager-workspace/skills\nbash \"$RENDER\" /root/manager-workspace/skills-alpha\nbash \"$RENDER\" /root/manager-workspace AGENTS.md TOOLS.md HEARTBEAT.md SOUL.md\n# Worker templates (workspace + image) — rendered before push to MinIO\n# so Workers (including remote pip-install) receive plain text\nbash \"$RENDER\" /root/manager-workspace/worker-skills\nbash \"$RENDER\" /root/manager-workspace/worker-agent\nbash \"$RENDER\" /root/manager-workspace/copaw-worker-agent\nbash \"$RENDER\" /opt/hiclaw/agent/worker-skills\nbash \"$RENDER\" /opt/hiclaw/agent/worker-agent\nbash \"$RENDER\" /opt/hiclaw/agent/copaw-worker-agent\nlog \"Agent doc templates rendered\"\n\n# Cloud mode: start background file sync (workspace ↔ OSS) and initial push\nif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n log \"Syncing initial workspace to OSS...\"\n ensure_mc_credentials\n mc mirror /root/manager-workspace/ \"${HICLAW_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" 2>/dev/null || true\n\n # Local → OSS: change-triggered sync\n (\n while true; do\n CHANGED=$(find /root/manager-workspace/ -type f -newermt \"15 seconds ago\" 2>/dev/null | head -1)\n if [ -n \"${CHANGED}\" ]; then\n ensure_mc_credentials 2>/dev/null || true\n mc mirror /root/manager-workspace/ \"${HICLAW_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" --exclude \".npm/**\" \\\n --exclude \".local/**\" --exclude \".mc/**\" 2>/dev/null || true\n fi\n sleep 10\n done\n ) &\n log \"Local→OSS sync started (PID: $!)\"\n\n # OSS → Local: periodic pull (shared data, agent configs)\n (\n while true; do\n sleep 300\n ensure_mc_credentials 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/shared/\" /root/hiclaw-fs/shared/ --overwrite --newer-than \"5m\" 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/agents/\" /root/hiclaw-fs/agents/ --overwrite --newer-than \"5m\" 2>/dev/null || true\n done\n ) &\n log \"OSS→Local sync started (every 5m, PID: $!)\"\nfi\n\n# K8s mode: start background file sync (workspace ↔ MinIO)\nif [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n log \"Syncing initial workspace to MinIO...\"\n mc mirror /root/manager-workspace/ \"${HICLAW_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" 2>/dev/null || true\n\n # Local → MinIO: change-triggered sync\n (\n while true; do\n CHANGED=$(find /root/manager-workspace/ -type f -newermt \"15 seconds ago\" 2>/dev/null | head -1)\n if [ -n \"${CHANGED}\" ]; then\n mc mirror /root/manager-workspace/ \"${HICLAW_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" --exclude \".npm/**\" \\\n --exclude \".local/**\" --exclude \".mc/**\" 2>/dev/null || true\n fi\n sleep 10\n done\n ) &\n log \"Local→MinIO sync started (PID: $!)\"\n\n # MinIO → Local: periodic pull (shared data, agent configs)\n (\n while true; do\n sleep 300\n mc mirror \"${HICLAW_STORAGE_PREFIX}/shared/\" /root/hiclaw-fs/shared/ --overwrite --newer-than \"5m\" 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/agents/\" /root/hiclaw-fs/agents/ --overwrite --newer-than \"5m\" 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/hiclaw-config/\" /root/hiclaw-fs/hiclaw-config/ --overwrite --newer-than \"15s\" 2>/dev/null || true\n done\n ) &\n log \"MinIO→Local sync started (every 5m, PID: $!)\"\nfi\n\n# ============================================================\n# Auto-generate Manager mcporter config for pre-configured MCP servers\n# If HICLAW_GITHUB_TOKEN was set at install time, setup-higress.sh already\n# configured GitHub MCP on Higress. Run setup-mcp-server.sh now so that\n# config/mcporter.json exists when the Agent starts — no need to ask user for PAT.\n# ============================================================\nif [ -n \"${HICLAW_GITHUB_TOKEN}\" ] && [ \"${HICLAW_RUNTIME}\" != \"aliyun\" ] && [ \"${HICLAW_RUNTIME}\" != \"k8s\" ]; then\n if [ ! -f \"${HOME}/config/mcporter.json\" ]; then\n log \"Auto-generating Manager mcporter config for GitHub MCP (HICLAW_GITHUB_TOKEN set)...\"\n bash /opt/hiclaw/agent/skills/mcp-server-management/scripts/setup-mcp-server.sh \\\n github \"${HICLAW_GITHUB_TOKEN}\" 2>&1 | while IFS= read -r line; do log \" [setup-mcp] ${line}\"; done || \\\n log \"WARNING: setup-mcp-server.sh failed — Agent may need to configure GitHub MCP manually\"\n else\n log \"Manager mcporter config already exists, skipping auto-generate\"\n fi\nfi\n\n# ============================================================\n# Runtime-specific startup\n# ============================================================\nif [ \"${MANAGER_RUNTIME}\" = \"copaw\" ]; then\n # Delegate to CoPaw startup script\n exec /opt/hiclaw/scripts/init/start-copaw-manager.sh\nelse\n # ── OpenClaw Runtime ─────────────────────────────────────────────────────\n log \"Starting OpenClaw Manager...\"\n\n export OPENCLAW_CONFIG_PATH=\"/root/manager-workspace/openclaw.json\"\n\n # Symlink to default OpenClaw config path so CLI commands find the config\n mkdir -p \"${HOME}/.openclaw\"\n ln -sf \"/root/manager-workspace/openclaw.json\" \"${HOME}/.openclaw/openclaw.json\"\n\n # Clean orphaned session write locks (e.g. from SIGKILL or crash before exit handlers)\n # Prevents \"session file locked (timeout 10000ms)\" when PID was reused\n find \"${HOME}/.openclaw/agents\" -name \"*.jsonl.lock\" -delete 2>/dev/null || true\n log \"Cleaned up any orphaned session write locks\"\n\n # Clean Matrix crypto storage (SQLite WAL may be corrupted after unclean shutdown)\n # Crypto state is re-negotiated on startup; losing it only means re-establishing E2EE sessions\n rm -rf \"${HOME}/.openclaw/matrix\" 2>/dev/null || true\n log \"Cleaned Matrix crypto storage (will re-establish E2EE sessions)\"\n\n # Launch OpenClaw\n # Disable full-process respawn so the CLI uses its internal restart loop.\n # Without this, config reload spawns a detached child and exits, then\n # supervisord restarts the CLI — resulting in two gateway processes.\n export OPENCLAW_NO_RESPAWN=1\n exec openclaw gateway run --verbose --force\nfi\n"}, "d029ed2ea22f25f489c8f5881544d96e5f599b06": {"t": 1785764760.2903368, "n": 63837, "h": "6a97a2ada3150add", "c": "#!/bin/bash\n# start-manager-agent.sh - Initialize and start the Manager Agent\n# Supports local (supervisord), cloud (SAE), and K8s (Helm) deployments.\n# In local mode this is the last supervisord component to start (priority 800).\n# In cloud/k8s mode (HICLAW_RUNTIME=aliyun|k8s) this is the container entrypoint.\n#\n# Runtime selection:\n# HICLAW_MANAGER_RUNTIME=openclaw (default) - OpenClaw gateway mode\n# HICLAW_MANAGER_RUNTIME=copaw - CoPaw workspace mode\n\nsource /opt/hiclaw/scripts/lib/hiclaw-env.sh\n\n# ============================================================\n# Runtime selection\n# ============================================================\nMANAGER_RUNTIME=\"${HICLAW_MANAGER_RUNTIME:-openclaw}\"\ncase \"${MANAGER_RUNTIME}\" in\n copaw)\n log \"Manager runtime: CoPaw (Python workspace)\"\n ;;\n *)\n log \"Manager runtime: OpenClaw (Node.js gateway)\"\n MANAGER_RUNTIME=\"openclaw\"\n ;;\nesac\n\n# ============================================================\n# Set timezone from TZ env var\n# ============================================================\nif [ -n \"${TZ}\" ] && [ -f \"/usr/share/zoneinfo/${TZ}\" ]; then\n ln -sf \"/usr/share/zoneinfo/${TZ}\" /etc/localtime\n echo \"${TZ}\" > /etc/timezone\n log \"Timezone set to ${TZ}\"\nfi\n\nexport MATRIX_DOMAIN=\"${HICLAW_MATRIX_DOMAIN:-matrix-local.hiclaw.io:8080}\"\nAI_GATEWAY_DOMAIN=\"${HICLAW_AI_GATEWAY_DOMAIN:-aigw-local.hiclaw.io}\"\n\n# ============================================================\n# Cloud/K8s mode: validate required environment variables + initial credentials\n# ============================================================\nif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ] || [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n : \"${HICLAW_MATRIX_URL:?HICLAW_MATRIX_URL is required}\"\n : \"${HICLAW_MATRIX_DOMAIN:?HICLAW_MATRIX_DOMAIN is required}\"\n : \"${HICLAW_AI_GATEWAY_URL:?HICLAW_AI_GATEWAY_URL is required}\"\n if [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n : \"${HICLAW_MANAGER_GATEWAY_KEY:?HICLAW_MANAGER_GATEWAY_KEY is required}\"\n : \"${HICLAW_MANAGER_PASSWORD:?HICLAW_MANAGER_PASSWORD is required (cloud containers are stateless, password must be injected)}\"\n fi\n if [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n # K8s mode: controller handles initialization (admin registration, Higress setup).\n # Manager only needs credentials injected by the ManagerReconciler.\n : \"${HICLAW_MANAGER_GATEWAY_KEY:?HICLAW_MANAGER_GATEWAY_KEY is required (injected by controller)}\"\n : \"${HICLAW_MANAGER_PASSWORD:?HICLAW_MANAGER_PASSWORD is required (injected by controller)}\"\n else\n # Cloud (aliyun) mode: Manager still does its own initialization\n : \"${HICLAW_REGISTRATION_TOKEN:?HICLAW_REGISTRATION_TOKEN is required}\"\n : \"${HICLAW_ADMIN_USER:?HICLAW_ADMIN_USER is required}\"\n : \"${HICLAW_ADMIN_PASSWORD:?HICLAW_ADMIN_PASSWORD is required}\"\n fi\n log \"${HICLAW_RUNTIME} mode: validating environment... OK\"\n log \" Matrix: ${HICLAW_MATRIX_SERVER}, AI Gateway: ${HICLAW_AI_GATEWAY_URL}, Storage: ${HICLAW_STORAGE_BUCKET}\"\n if [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n ensure_mc_credentials || { log \"FATAL: Initial STS credential fetch failed\"; exit 1; }\n fi\nfi\n\n# ============================================================\n# Local mode: host symlinks, /etc/hosts, wait for local services\n# ============================================================\nif [ \"${HICLAW_RUNTIME}\" != \"aliyun\" ] && [ \"${HICLAW_RUNTIME}\" != \"k8s\" ]; then\n # Create symlink for host directory access\n if [ -d \"/host-share\" ]; then\n ORIGINAL_HOST_HOME=\"${HOST_ORIGINAL_HOME:-$HOME}\"\n if [ ! -e \"${ORIGINAL_HOST_HOME}\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/root\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/data\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/host-share\" ]; then\n mkdir -p \"$(dirname \"${ORIGINAL_HOST_HOME}\")\"\n ln -sfn /host-share \"${ORIGINAL_HOST_HOME}\"\n log \"Created symlink: ${ORIGINAL_HOST_HOME} -> /host-share\"\n else\n ln -sfn /host-share /root/host-home\n log \"Created fallback symlink: /root/host-home -> /host-share\"\n fi\n fi\n\n # Add local domains to /etc/hosts\n HOSTS_DOMAINS=\"${MATRIX_DOMAIN%%:*} ${HICLAW_MATRIX_CLIENT_DOMAIN:-matrix-client-local.hiclaw.io} ${AI_GATEWAY_DOMAIN} ${HICLAW_FS_DOMAIN:-fs-local.hiclaw.io}\"\n if ! grep -q \"${AI_GATEWAY_DOMAIN}\" /etc/hosts 2>/dev/null; then\n echo \"127.0.0.1 ${HOSTS_DOMAINS}\" >> /etc/hosts\n log \"Added local domains to /etc/hosts\"\n fi\n\n # Wait for local infrastructure\n waitForService \"Higress Gateway\" \"127.0.0.1\" 8080 180\n waitForService \"Higress Console\" \"127.0.0.1\" 8001 180\n waitForService \"Tuwunel\" \"127.0.0.1\" 6167 120\n waitForHTTP \"Tuwunel Matrix API\" \"${HICLAW_MATRIX_SERVER}/_tuwunel/server_version\" 120\n waitForService \"MinIO\" \"127.0.0.1\" 9000 120\nelse\n # Cloud/K8s mode: wait for external Tuwunel\n log \"Waiting for Tuwunel Matrix server at ${HICLAW_MATRIX_SERVER}...\"\n _retry=0\n while [ \"${_retry}\" -lt 30 ]; do\n if curl -sf \"${HICLAW_MATRIX_SERVER}/_matrix/client/versions\" > /dev/null 2>&1; then\n log \"Tuwunel is ready\"\n break\n fi\n _retry=$((_retry + 1))\n log \" Waiting for Tuwunel (attempt ${_retry}/30)...\"\n sleep 5\n done\n if [ \"${_retry}\" -ge 30 ]; then\n log \"ERROR: Tuwunel not reachable at ${HICLAW_MATRIX_SERVER}\"\n exit 1\n fi\nfi\n\n# ============================================================\n# Auto-generate secrets if not provided via environment\n# Persisted to /data so they survive container restart\n# ============================================================\nSECRETS_FILE=\"/data/hiclaw-secrets.env\"\nif [ -f \"${SECRETS_FILE}\" ]; then\n source \"${SECRETS_FILE}\"\n log \"Loaded persisted secrets from ${SECRETS_FILE}\"\nfi\n\nif [ -z \"${HICLAW_MANAGER_GATEWAY_KEY}\" ]; then\n export HICLAW_MANAGER_GATEWAY_KEY=\"$(generateKey 32)\"\n log \"Auto-generated HICLAW_MANAGER_GATEWAY_KEY\"\nfi\nif [ -z \"${HICLAW_MANAGER_PASSWORD}\" ]; then\n export HICLAW_MANAGER_PASSWORD=\"$(generateKey 16)\"\n log \"Auto-generated HICLAW_MANAGER_PASSWORD\"\nfi\n\n# Persist secrets so they survive supervisord restart\nmkdir -p /data\ncat > \"${SECRETS_FILE}\" <<EOF\nexport HICLAW_MANAGER_GATEWAY_KEY=\"${HICLAW_MANAGER_GATEWAY_KEY}\"\nexport HICLAW_MANAGER_PASSWORD=\"${HICLAW_MANAGER_PASSWORD}\"\nEOF\nchmod 600 \"${SECRETS_FILE}\"\n\n# Cloud mode: pull workspace from OSS before initialization\nif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n HICLAW_FS=\"/root/hiclaw-fs\"\n mkdir -p \"${HICLAW_FS}/shared\" \"${HICLAW_FS}/agents\"\n log \"Pulling workspace from OSS...\"\n ensure_mc_credentials\n mc mirror \"${HICLAW_STORAGE_PREFIX}/manager/\" /root/manager-workspace/ --overwrite 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/shared/\" \"${HICLAW_FS}/shared/\" --overwrite 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/agents/\" \"${HICLAW_FS}/agents/\" --overwrite 2>/dev/null || true\n # Symlink hiclaw-fs into workspace for agent access\n ln -sfn \"${HICLAW_FS}\" /root/manager-workspace/hiclaw-fs\nfi\n\n# K8s mode: sync workspace from cluster-internal MinIO\nif [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n HICLAW_FS=\"/root/hiclaw-fs\"\n mkdir -p \"${HICLAW_FS}/shared\" \"${HICLAW_FS}/agents\" \"${HICLAW_FS}/hiclaw-config\"\n log \"Configuring mc alias for cluster MinIO...\"\n mc alias set hiclaw \"${HICLAW_MINIO_ENDPOINT}\" \"${HICLAW_MINIO_ACCESS_KEY}\" \"${HICLAW_MINIO_SECRET_KEY}\"\n log \"Syncing workspace from MinIO...\"\n mc mirror \"${HICLAW_STORAGE_PREFIX}/\" \"${HICLAW_FS}/\" --overwrite 2>/dev/null || true\n ln -sfn \"${HICLAW_FS}\" /root/manager-workspace/hiclaw-fs\n touch \"${HICLAW_FS}/.initialized\"\nfi\n\n# ============================================================\n# Initialize / upgrade Manager workspace\n# First boot: full init via upgrade-builtins.sh\n# Subsequent boots: compare image version; upgrade only if changed\n# ============================================================\nmkdir -p /root/manager-workspace\n\nIMAGE_VERSION=$(cat /opt/hiclaw/agent/.builtin-version 2>/dev/null || echo \"unknown\")\nINSTALLED_VERSION=$(cat /root/manager-workspace/.builtin-version 2>/dev/null || echo \"\")\n\nif [ ! -f /root/manager-workspace/.initialized ]; then\n log \"First boot: initializing manager workspace...\"\n bash /opt/hiclaw/scripts/init/upgrade-builtins.sh\n touch /root/manager-workspace/.initialized\n log \"Manager workspace initialized (version: ${IMAGE_VERSION})\"\nelif [ \"${IMAGE_VERSION}\" != \"${INSTALLED_VERSION}\" ] || [ \"${IMAGE_VERSION}\" = \"latest\" ]; then\n log \"Upgrade detected: ${INSTALLED_VERSION} -> ${IMAGE_VERSION}${IMAGE_VERSION:+ (latest: always upgrade)}\"\n bash /opt/hiclaw/scripts/init/upgrade-builtins.sh\n log \"Manager workspace upgraded to version: ${IMAGE_VERSION}\"\nelse\n log \"Workspace up to date (version: ${IMAGE_VERSION})\"\nfi\n\n# Local mode: wait for mc mirror initialization (shared + worker data in /root/hiclaw-fs/)\nif [ \"${HICLAW_RUNTIME}\" != \"aliyun\" ] && [ \"${HICLAW_RUNTIME}\" != \"k8s\" ]; then\n log \"Waiting for MinIO storage initialization...\"\n _minio_wait=0\n while [ ! -f /root/hiclaw-fs/.initialized ]; do\n sleep 2\n _minio_wait=$(( _minio_wait + 1 ))\n if [ \"${_minio_wait}\" -ge 60 ]; then\n log \"ERROR: MinIO storage initialization timed out after 120s\"\n exit 1\n fi\n done\n log \"MinIO storage initialized\"\nfi\n\n# ============================================================\n# Register Matrix users via Registration API (single-step, no UIAA)\n# K8s mode: skip — controller Initializer + ManagerReconciler already did this\n# ============================================================\nif [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n log \"K8s mode: skipping Matrix registration (handled by controller)\"\n # Controller injects HICLAW_MANAGER_PASSWORD via env; login to get token\n log \"Obtaining Manager Matrix access token...\"\n _LOGIN_RESPONSE=$(curl -s -X POST ${HICLAW_MATRIX_SERVER}/_matrix/client/v3/login \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"type\": \"m.login.password\",\n \"identifier\": {\"type\": \"m.id.user\", \"user\": \"manager\"},\n \"password\": \"'\"${HICLAW_MANAGER_PASSWORD}\"'\"\n }' 2>&1)\n MANAGER_TOKEN=$(echo \"${_LOGIN_RESPONSE}\" | jq -r '.access_token' 2>/dev/null)\n if [ -z \"${MANAGER_TOKEN}\" ] || [ \"${MANAGER_TOKEN}\" = \"null\" ]; then\n log \"ERROR: Failed to obtain Manager Matrix token\"\n log \"ERROR: Login response was: ${_LOGIN_RESPONSE}\"\n exit 1\n fi\n log \"Manager Matrix token obtained (token prefix: ${MANAGER_TOKEN:0:10}...)\"\nelse\nlog \"Registering human admin Matrix account...\"\ncurl -sf -X POST ${HICLAW_MATRIX_SERVER}/_matrix/client/v3/register \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"username\": \"'\"${HICLAW_ADMIN_USER}\"'\",\n \"password\": \"'\"${HICLAW_ADMIN_PASSWORD}\"'\",\n \"auth\": {\n \"type\": \"m.login.registration_token\",\n \"token\": \"'\"${HICLAW_REGISTRATION_TOKEN}\"'\"\n }\n }' > /dev/null 2>&1 || log \"Admin account may already exist\"\n\nlog \"Registering Manager Agent Matrix account...\"\ncurl -sf -X POST ${HICLAW_MATRIX_SERVER}/_matrix/client/v3/register \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"username\": \"manager\",\n \"password\": \"'\"${HICLAW_MANAGER_PASSWORD}\"'\",\n \"auth\": {\n \"type\": \"m.login.registration_token\",\n \"token\": \"'\"${HICLAW_REGISTRATION_TOKEN}\"'\"\n }\n }' > /dev/null 2>&1 || log \"Manager account may already exist\"\n\n# Get Manager Agent's Matrix access token\nlog \"Obtaining Manager Matrix access token...\"\n_LOGIN_RESPONSE=$(curl -s -X POST ${HICLAW_MATRIX_SERVER}/_matrix/client/v3/login \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"type\": \"m.login.password\",\n \"identifier\": {\"type\": \"m.id.user\", \"user\": \"manager\"},\n \"password\": \"'\"${HICLAW_MANAGER_PASSWORD}\"'\"\n }' 2>&1)\n_LOGIN_EXIT=$?\nlog \"Matrix login HTTP exit code: ${_LOGIN_EXIT}\"\nlog \"Matrix login response: ${_LOGIN_RESPONSE}\"\n\nMANAGER_TOKEN=$(echo \"${_LOGIN_RESPONSE}\" | jq -r '.access_token' 2>/dev/null)\n\nif [ -z \"${MANAGER_TOKEN}\" ] || [ \"${MANAGER_TOKEN}\" = \"null\" ]; then\n log \"ERROR: Failed to obtain Manager Matrix token (exit=${_LOGIN_EXIT})\"\n log \"ERROR: Login response was: ${_LOGIN_RESPONSE}\"\n exit 1\nfi\nlog \"Manager Matrix token obtained (token prefix: ${MANAGER_TOKEN:0:10}...)\"\nfi\n\n# ============================================================\n# Higress Console initialization\n# Docker mode: full setup-higress.sh (internal Higress at localhost:8001)\n# K8s mode: skip — controller Initializer handles Higress setup\n# Cloud (aliyun) mode: skip entirely (Higress managed externally)\n# ============================================================\n_HIGRESS_CONSOLE_URL=\"\"\n_HIGRESS_USER=\"${HICLAW_HIGRESS_ADMIN_USER:-${HICLAW_ADMIN_USER}}\"\n_HIGRESS_PASS=\"${HICLAW_HIGRESS_ADMIN_PASSWORD:-${HICLAW_ADMIN_PASSWORD}}\"\nif [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n log \"K8s mode: skipping Higress initialization (handled by controller)\"\nelif [ \"${HICLAW_RUNTIME}\" != \"aliyun\" ]; then\n _HIGRESS_CONSOLE_URL=\"http://127.0.0.1:8001\"\nfi\n\nif [ -n \"${_HIGRESS_CONSOLE_URL}\" ]; then\n COOKIE_FILE=\"/tmp/higress-session-cookie\"\n\n log \"Waiting for Higress Console (${_HIGRESS_CONSOLE_URL}) to be fully ready and initializing admin...\"\n INIT_DONE=false\n for i in $(seq 1 90); do\n INIT_RESULT=$(curl -s -X POST \"${_HIGRESS_CONSOLE_URL}/system/init\" \\\n -H 'Content-Type: application/json' \\\n -d '{\"adminUser\":{\"name\":\"'\"${_HIGRESS_USER}\"'\",\"password\":\"'\"${_HIGRESS_PASS}\"'\",\"displayName\":\"'\"${_HIGRESS_USER}\"'\"}}' 2>/dev/null) || true\n if echo \"${INIT_RESULT}\" | grep -qE '\"success\":true|already.?init' 2>/dev/null; then\n INIT_DONE=true\n break\n fi\n if echo \"${INIT_RESULT}\" | grep -q '\"name\"' 2>/dev/null; then\n INIT_DONE=true\n break\n fi\n sleep 2\n done\n\n if [ \"${INIT_DONE}\" != \"true\" ]; then\n log \"ERROR: Higress Console did not become ready within 180s\"\n exit 1\n fi\n log \"Higress Console init done\"\n\n log \"Logging into Higress Console...\"\n LOGIN_OK=false\n for i in $(seq 1 10); do\n HTTP_CODE=$(curl -s -o /dev/null -w '%{http_code}' -X POST \"${_HIGRESS_CONSOLE_URL}/session/login\" \\\n -H 'Content-Type: application/json' \\\n -c \"${COOKIE_FILE}\" \\\n -d '{\"username\":\"'\"${_HIGRESS_USER}\"'\",\"password\":\"'\"${_HIGRESS_PASS}\"'\"}' 2>/dev/null) || true\n if { [ \"${HTTP_CODE}\" = \"200\" ] || [ \"${HTTP_CODE}\" = \"201\" ]; } && [ -f \"${COOKIE_FILE}\" ] && [ -s \"${COOKIE_FILE}\" ]; then\n LOGIN_OK=true\n break\n fi\n log \"Login attempt $i (HTTP ${HTTP_CODE}), retrying in 3s...\"\n sleep 3\n done\n\n if [ \"${LOGIN_OK}\" != \"true\" ]; then\n log \"ERROR: Could not login to Higress Console after retries\"\n exit 1\n fi\n log \"Higress Console login successful\"\n\n VERIFY_CODE=$(curl -s -o /dev/null -w '%{http_code}' \"${_HIGRESS_CONSOLE_URL}/v1/consumers\" -b \"${COOKIE_FILE}\" 2>/dev/null) || true\n if [ \"${VERIFY_CODE}\" = \"200\" ]; then\n log \"Console session verified (cookie valid)\"\n else\n log \"WARNING: Console session may be invalid (verify returned HTTP ${VERIFY_CODE})\"\n rm -f \"${COOKIE_FILE}\"\n for i in $(seq 1 5); do\n curl -s -o /dev/null -w '%{http_code}' -X POST \"${_HIGRESS_CONSOLE_URL}/session/login\" \\\n -H 'Content-Type: application/json' \\\n -c \"${COOKIE_FILE}\" \\\n -d '{\"username\":\"'\"${_HIGRESS_USER}\"'\",\"password\":\"'\"${_HIGRESS_PASS}\"'\"}' 2>/dev/null\n VERIFY2=$(curl -s -o /dev/null -w '%{http_code}' \"${_HIGRESS_CONSOLE_URL}/v1/consumers\" -b \"${COOKIE_FILE}\" 2>/dev/null) || true\n if [ \"${VERIFY2}\" = \"200\" ]; then\n log \"Re-login successful, session verified\"\n break\n fi\n sleep 2\n done\n fi\n\n export HIGRESS_COOKIE_FILE=\"${COOKIE_FILE}\"\n export HIGRESS_CONSOLE_URL=\"${_HIGRESS_CONSOLE_URL}\"\n\n if [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n # K8s mode: lightweight Higress config — only what's needed for LLM access\n source /opt/hiclaw/scripts/lib/base.sh\n _k8s_higress_api() {\n local method=\"$1\" path=\"$2\" desc=\"$3\"; shift 3; local body=\"$*\"\n local tmpfile; tmpfile=$(mktemp)\n local http_code\n http_code=$(curl -s -o \"${tmpfile}\" -w '%{http_code}' -X \"${method}\" \"${_HIGRESS_CONSOLE_URL}${path}\" \\\n -b \"${COOKIE_FILE}\" -H 'Content-Type: application/json' -d \"${body}\" 2>/dev/null) || true\n local response; response=$(cat \"${tmpfile}\" 2>/dev/null); rm -f \"${tmpfile}\"\n if echo \"${response}\" | grep -q '\"success\":true' 2>/dev/null; then\n log \"${desc} ... OK\"\n elif [ \"${http_code}\" = \"409\" ]; then\n log \"${desc} ... already exists, skipping\"\n elif [ \"${http_code}\" = \"200\" ] || [ \"${http_code}\" = \"201\" ] || [ \"${http_code}\" = \"204\" ]; then\n log \"${desc} ... OK (HTTP ${http_code})\"\n else\n log \"WARNING: ${desc} ... (HTTP ${http_code}): ${response}\"\n fi\n }\n\n # 1. Service Sources (DNS type → K8s Service FQDN)\n # Extract host:port from URLs for Higress service source registration\n _TUWUNEL_HOST=$(echo \"${HICLAW_MATRIX_SERVER}\" | sed 's|^http[s]*://||')\n _TUWUNEL_DOMAIN=$(echo \"${_TUWUNEL_HOST}\" | cut -d: -f1)\n _TUWUNEL_PORT=$(echo \"${_TUWUNEL_HOST}\" | cut -d: -f2)\n _k8s_higress_api POST /v1/service-sources \"Registering Tuwunel service source\" \\\n '{\"type\":\"dns\",\"name\":\"tuwunel\",\"domain\":\"'\"${_TUWUNEL_DOMAIN}\"'\",\"port\":'\"${_TUWUNEL_PORT}\"'}'\n\n if [ -n \"${HICLAW_ELEMENT_WEB_URL:-}\" ]; then\n _ELEMENT_HOST=$(echo \"${HICLAW_ELEMENT_WEB_URL}\" | sed 's|^http[s]*://||')\n _ELEMENT_DOMAIN=$(echo \"${_ELEMENT_HOST}\" | cut -d: -f1)\n _ELEMENT_PORT=$(echo \"${_ELEMENT_HOST}\" | cut -d: -f2)\n _k8s_higress_api POST /v1/service-sources \"Registering Element Web service source\" \\\n '{\"type\":\"dns\",\"name\":\"element-web\",\"domain\":\"'\"${_ELEMENT_DOMAIN}\"'\",\"port\":'\"${_ELEMENT_PORT}\"'}'\n fi\n\n # 2. Manager Consumer (key-auth)\n _k8s_higress_api POST /v1/consumers \"Creating Manager consumer\" \\\n '{\"name\":\"manager\",\"credentials\":[{\"type\":\"key-auth\",\"source\":\"BEARER\",\"values\":[\"'\"${HICLAW_MANAGER_GATEWAY_KEY}\"'\"]}]}'\n\n # 3. LLM Provider\n _LLM_PROVIDER=\"${HICLAW_LLM_PROVIDER:-qwen}\"\n _LLM_API_URL=\"${HICLAW_LLM_API_URL:-}\"\n if [ -z \"${_LLM_API_URL}\" ]; then\n case \"${_LLM_PROVIDER}\" in\n qwen) _LLM_API_URL=\"https://dashscope.aliyuncs.com/compatible-mode/v1\" ;;\n esac\n fi\n # 4. LLM Provider type-specific config\n case \"${_LLM_PROVIDER}\" in\n qwen)\n _k8s_higress_api POST /v1/ai/providers \"Creating LLM provider (qwen)\" \\\n '{\"type\":\"qwen\",\"name\":\"qwen\",\"tokens\":[\"'\"${HICLAW_LLM_API_KEY}\"'\"],\"protocol\":\"openai/v1\",\"tokenFailoverConfig\":{\"enabled\":false},\"rawConfigs\":{\"qwenEnableSearch\":false,\"qwenEnableCompatible\":true,\"qwenFileIds\":[],\"hiclawMode\":true}}'\n ;;\n *)\n _BODY='{\"name\":\"'\"${_LLM_PROVIDER}\"'\",\"type\":\"openai\",\"tokens\":[\"'\"${HICLAW_LLM_API_KEY}\"'\"],\"modelMapping\":{},\"protocol\":\"openai/v1\",\"rawConfigs\":{\"hiclawMode\":true}}'\n _k8s_higress_api POST /v1/ai/providers \"Creating LLM provider (${_LLM_PROVIDER})\" \"${_BODY}\"\n ;;\n esac\n\n # 5. AI Route (bind provider + consumer auth, /v1 prefix to avoid clash with Element Web catch-all)\n _k8s_higress_api POST /v1/ai/routes \"Creating AI Gateway route\" \\\n '{\"name\":\"default-ai-route\",\"domains\":[],\"pathPredicate\":{\"matchType\":\"PRE\",\"matchValue\":\"/v1\",\"caseSensitive\":false},\"upstreams\":[{\"provider\":\"'\"${_LLM_PROVIDER}\"'\",\"weight\":100,\"modelMapping\":{}}],\"authConfig\":{\"enabled\":true,\"allowedCredentialTypes\":[\"key-auth\"],\"allowedConsumers\":[\"manager\"]}}'\n\n # 6. Matrix Homeserver Route (/_matrix/* → Tuwunel, no auth)\n _k8s_higress_api POST /v1/routes \"Creating Matrix Homeserver route\" \\\n '{\"name\":\"matrix-homeserver\",\"domains\":[],\"path\":{\"matchType\":\"PRE\",\"matchValue\":\"/_matrix\"},\"services\":[{\"name\":\"tuwunel.dns\",\"port\":'\"${_TUWUNEL_PORT}\"',\"weight\":100}]}'\n\n # 7. Element Web Route (/ catch-all → Element Web, no auth)\n if [ -n \"${HICLAW_ELEMENT_WEB_URL:-}\" ]; then\n _k8s_higress_api POST /v1/routes \"Creating Element Web route\" \\\n '{\"name\":\"element-web\",\"domains\":[],\"path\":{\"matchType\":\"PRE\",\"matchValue\":\"/\"},\"services\":[{\"name\":\"element-web.dns\",\"port\":'\"${_ELEMENT_PORT}\"',\"weight\":100}]}'\n fi\n\n # 8. Remove Higress default landing page (Exact match on / takes precedence over Element Web catch-all)\n _k8s_higress_api DELETE /v1/routes/default \"Removing Higress default landing route\"\n\n log \"K8s Higress lightweight setup complete\"\n\n # Wait for AI plugin activation (~45s for first config)\n log \"Waiting for AI Gateway plugin activation (45s)...\"\n sleep 45\n else\n # Docker mode: full setup with all routes, domains, MCP servers\n /opt/hiclaw/scripts/init/setup-higress.sh\n fi\nfi\n\n# ============================================================\n# Create admin DM room, persist to state.json, send welcome message\n# K8s mode: skip — controller ProvisionManager already creates the Admin DM room\n# Runs in both local and cloud modes (idempotent)\n# ============================================================\nif [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n log \"K8s mode: skipping admin DM room creation (handled by controller)\"\nelse\nMANAGER_FULL_ID=\"@manager:${MATRIX_DOMAIN}\"\nADMIN_FULL_ID=\"@${HICLAW_ADMIN_USER}:${MATRIX_DOMAIN}\"\n\nlog \"Logging in as admin to create DM room...\"\n_ADMIN_LOGIN=$(curl -sf -X POST \"${HICLAW_MATRIX_SERVER}/_matrix/client/v3/login\" \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"type\": \"m.login.password\",\n \"identifier\": {\"type\": \"m.id.user\", \"user\": \"'\"${HICLAW_ADMIN_USER}\"'\"},\n \"password\": \"'\"${HICLAW_ADMIN_PASSWORD}\"'\"\n }' 2>&1) || true\n\nADMIN_MATRIX_TOKEN=$(echo \"${_ADMIN_LOGIN}\" | jq -r '.access_token // empty' 2>/dev/null)\nif [ -z \"${ADMIN_MATRIX_TOKEN}\" ]; then\n log \"WARNING: Failed to login as admin, skipping DM room creation\"\nelse\n # Search for existing DM room with Manager (idempotent)\n DM_ROOM_ID=\"\"\n _JOINED_ROOMS=$(curl -sf \"${HICLAW_MATRIX_SERVER}/_matrix/client/v3/joined_rooms\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" 2>/dev/null \\\n | jq -r '.joined_rooms[]' 2>/dev/null) || true\n for _rid in ${_JOINED_ROOMS}; do\n _members=$(curl -sf \"${HICLAW_MATRIX_SERVER}/_matrix/client/v3/rooms/${_rid}/members\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" 2>/dev/null \\\n | jq -r '.chunk[].state_key' 2>/dev/null) || continue\n _count=$(echo \"${_members}\" | wc -l | xargs)\n if [ \"${_count}\" = \"2\" ] && echo \"${_members}\" | grep -q \"@manager:\"; then\n DM_ROOM_ID=\"${_rid}\"\n break\n fi\n done\n\n if [ -n \"${DM_ROOM_ID}\" ]; then\n log \"Existing DM room found: ${DM_ROOM_ID}\"\n else\n log \"Creating DM room with Manager...\"\n _RAW=$(curl -s -w '\\nHTTP_CODE:%{http_code}' -X POST \"${HICLAW_MATRIX_SERVER}/_matrix/client/v3/createRoom\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d \"{\\\"is_direct\\\":true,\\\"invite\\\":[\\\"${MANAGER_FULL_ID}\\\"],\\\"preset\\\":\\\"trusted_private_chat\\\"}\" 2>&1) || true\n _HTTP_CODE=$(echo \"${_RAW}\" | tail -1 | sed 's/HTTP_CODE://')\n _CREATE_RESP=$(echo \"${_RAW}\" | sed '$d')\n DM_ROOM_ID=$(echo \"${_CREATE_RESP}\" | jq -r '.room_id // empty' 2>/dev/null)\n if [ -n \"${DM_ROOM_ID}\" ]; then\n log \"DM room created: ${DM_ROOM_ID}\"\n else\n log \"WARNING: Failed to create DM room (HTTP ${_HTTP_CODE}): ${_CREATE_RESP}\"\n fi\n fi\n\n # Persist admin DM room ID to state.json\n if [ -n \"${DM_ROOM_ID}\" ]; then\n STATE_SCRIPT=\"/opt/hiclaw/agent/skills/task-management/scripts/manage-state.sh\"\n if [ -f \"${STATE_SCRIPT}\" ]; then\n bash \"${STATE_SCRIPT}\" --action init 2>/dev/null || true\n bash \"${STATE_SCRIPT}\" --action set-admin-dm --room-id \"${DM_ROOM_ID}\" 2>/dev/null || true\n log \"Admin DM room persisted to state.json: ${DM_ROOM_ID}\"\n fi\n fi\n\n # Schedule welcome message in background (only on first boot)\n if [ -n \"${DM_ROOM_ID}\" ] && [ ! -f \"/root/manager-workspace/soul-configured\" ]; then\n log \"Scheduling welcome message (background, waiting for OpenClaw to start)...\"\n (\n _HICLAW_LANGUAGE=\"${HICLAW_LANGUAGE:-zh}\"\n _HICLAW_TIMEZONE=\"${TZ:-Asia/Shanghai}\"\n _wait=0\n _ready=false\n while [ \"${_wait}\" -lt 300 ]; do\n if curl -sf http://127.0.0.1:18799/ > /dev/null 2>&1; then\n _ready=true\n break\n fi\n sleep 3\n _wait=$((_wait + 3))\n done\n if [ \"${_ready}\" != \"true\" ]; then\n echo \"[manager] WARNING: OpenClaw gateway not ready within 300s, skipping welcome message\"\n exit 0\n fi\n # Ensure Manager has joined the DM room before sending the welcome\n # message. Without this, there is a race between OpenClaw's Matrix\n # auto-join and the message send — the message may land before Manager\n # joins, so OpenClaw's /sync never picks it up.\n _join_ok=false\n for _join_attempt in 1 2 3; do\n if curl -sf -X POST \"${HICLAW_MATRIX_SERVER}/_matrix/client/v3/rooms/${DM_ROOM_ID}/join\" \\\n -H \"Authorization: Bearer ${MANAGER_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d '{}' > /dev/null 2>&1; then\n echo \"[manager] Manager joined DM room before welcome message\"\n _join_ok=true\n break\n fi\n sleep 2\n done\n if [ \"${_join_ok}\" != \"true\" ]; then\n echo \"[manager] WARNING: Manager join request failed after 3 attempts (may already be joined)\"\n fi\n _welcome_msg=\"This is an automated message from the HiClaw setup. This is a fresh installation.\n\n--- Installation Context ---\nUser Language: ${_HICLAW_LANGUAGE} (zh = Chinese, en = English)\nUser Timezone: ${_HICLAW_TIMEZONE} (IANA timezone identifier)\n---\n\nYou are an AI agent that manages a team of worker agents. Your identity and personality have not been configured yet — the human admin is about to meet you for the first time.\n\nPlease begin the onboarding conversation:\n\n1. Greet the admin warmly and briefly describe what you can do (coordinate workers, manage tasks, run multi-agent projects)\n2. The user has selected \\\"${_HICLAW_LANGUAGE}\\\" as their preferred language during installation. Use this language for your greeting and all subsequent communication.\n3. The user's timezone is ${_HICLAW_TIMEZONE}. Based on this timezone, you may infer their likely region and suggest additional language options.\n4. Ask them: a) What would they like to call you? b) Communication style preference? c) Any behavior guidelines? d) Confirm default language\n5. After they reply, write their preferences to ~/SOUL.md\n6. Confirm what you wrote, and ask if they would like to adjust anything\n7. Once confirmed, run: touch ~/soul-configured\n\nThe human admin will start chatting shortly.\"\n _txn_id=\"welcome-$(date +%s)\"\n _payload=$(jq -nc --arg body \"${_welcome_msg}\" '{\"msgtype\":\"m.text\",\"body\":$body}')\n _raw=$(curl -s -w '\\nHTTP_CODE:%{http_code}' -X PUT \"${HICLAW_MATRIX_SERVER}/_matrix/client/v3/rooms/${DM_ROOM_ID}/send/m.room.message/${_txn_id}\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d \"${_payload}\" 2>&1) || true\n _http_code=$(echo \"${_raw}\" | tail -1 | sed 's/HTTP_CODE://')\n _send_resp=$(echo \"${_raw}\" | sed '$d')\n if echo \"${_send_resp}\" | jq -e '.event_id' > /dev/null 2>&1; then\n echo \"[manager] Welcome message sent to DM room\"\n else\n echo \"[manager] WARNING: Failed to send welcome message (HTTP ${_http_code}): ${_send_resp}\"\n fi\n ) &\n log \"Welcome message background process started (PID: $!)\"\n fi\nfi\nfi # end K8s mode skip for admin DM room\n\n# ============================================================\n# Generate Manager Agent openclaw.json from template\n# ============================================================\nlog \"Generating Manager openclaw.json...\"\nexport MANAGER_MATRIX_TOKEN=\"${MANAGER_TOKEN}\"\nexport MANAGER_GATEWAY_KEY=\"${HICLAW_MANAGER_GATEWAY_KEY}\"\n# Resolve model parameters based on model name\nMODEL_NAME=\"${HICLAW_DEFAULT_MODEL:-qwen3.5-plus}\"\ncase \"${MODEL_NAME}\" in\n gpt-5.3-codex|gpt-5-mini|gpt-5-nano)\n export MODEL_CONTEXT_WINDOW=400000 MODEL_MAX_TOKENS=128000 ;;\n claude-opus-4-6)\n export MODEL_CONTEXT_WINDOW=1000000 MODEL_MAX_TOKENS=128000 ;;\n claude-sonnet-4-6)\n export MODEL_CONTEXT_WINDOW=1000000 MODEL_MAX_TOKENS=64000 ;;\n claude-haiku-4-5)\n export MODEL_CONTEXT_WINDOW=200000 MODEL_MAX_TOKENS=64000 ;;\n qwen3.5-plus)\n export MODEL_CONTEXT_WINDOW=200000 MODEL_MAX_TOKENS=64000 ;;\n deepseek-chat|deepseek-reasoner|kimi-k2.5)\n export MODEL_CONTEXT_WINDOW=256000 MODEL_MAX_TOKENS=128000 ;;\n glm-5|MiniMax-M2.7|MiniMax-M2.7-highspeed|MiniMax-M2.5)\n export MODEL_CONTEXT_WINDOW=200000 MODEL_MAX_TOKENS=128000 ;;\n *)\n export MODEL_CONTEXT_WINDOW=150000 MODEL_MAX_TOKENS=128000 ;;\nesac\nexport MODEL_REASONING=true\n\n# Override with user-supplied custom model parameters from env (set during install)\n[ -n \"${HICLAW_MODEL_CONTEXT_WINDOW:-}\" ] && export MODEL_CONTEXT_WINDOW=\"${HICLAW_MODEL_CONTEXT_WINDOW}\"\n[ -n \"${HICLAW_MODEL_MAX_TOKENS:-}\" ] && export MODEL_MAX_TOKENS=\"${HICLAW_MODEL_MAX_TOKENS}\"\n[ -n \"${HICLAW_MODEL_REASONING:-}\" ] && export MODEL_REASONING=\"${HICLAW_MODEL_REASONING}\"\n\n# E2EE: convert HICLAW_MATRIX_E2EE to JSON boolean for template substitution\nif [ \"${HICLAW_MATRIX_E2EE:-0}\" = \"1\" ] || [ \"${HICLAW_MATRIX_E2EE:-}\" = \"true\" ]; then\n export MATRIX_E2EE_ENABLED=true\nelse\n export MATRIX_E2EE_ENABLED=false\nfi\nlog \"Matrix E2EE: ${MATRIX_E2EE_ENABLED}\"\n\n# Resolve input modalities: only vision-capable models get \"image\"\ncase \"${MODEL_NAME}\" in\n gpt-5.4|gpt-5.3-codex|gpt-5-mini|gpt-5-nano|claude-opus-4-6|claude-sonnet-4-6|claude-haiku-4-5|qwen3.5-plus|kimi-k2.5)\n export MODEL_INPUT='[\"text\", \"image\"]' ;;\n *)\n export MODEL_INPUT='[\"text\"]' ;;\nesac\n# Override with user-supplied vision setting from env\nif [ \"${HICLAW_MODEL_VISION:-}\" = \"true\" ]; then\n export MODEL_INPUT='[\"text\", \"image\"]'\nelif [ \"${HICLAW_MODEL_VISION:-}\" = \"false\" ]; then\n export MODEL_INPUT='[\"text\"]'\nfi\n\nlog \"Model: ${MODEL_NAME} (context=${MODEL_CONTEXT_WINDOW}, maxTokens=${MODEL_MAX_TOKENS}, reasoning=${MODEL_REASONING}, input=${MODEL_INPUT})\"\n\nif [ -f /root/manager-workspace/openclaw.json ]; then\n log \"Manager openclaw.json already exists, updating dynamic fields only (preserving user customizations)...\"\n # Merge known models into existing config (add missing, preserve user-added)\n # Use known-models.json (valid JSON) instead of template (contains ${VAR} placeholders)\n KNOWN_MODELS=$(cat /opt/hiclaw/configs/known-models.json 2>/dev/null || echo '[]')\n jq --arg token \"${MANAGER_TOKEN}\" \\\n --arg key \"${HICLAW_MANAGER_GATEWAY_KEY}\" \\\n --arg model \"${MODEL_NAME}\" \\\n --arg emb_model \"${HICLAW_EMBEDDING_MODEL}\" \\\n --arg aigw_domain \"${AI_GATEWAY_DOMAIN}\" \\\n --argjson e2ee \"${MATRIX_E2EE_ENABLED}\" \\\n --argjson known_models \"${KNOWN_MODELS}\" \\\n --argjson ctx \"${MODEL_CONTEXT_WINDOW}\" \\\n --argjson max \"${MODEL_MAX_TOKENS}\" \\\n --argjson reasoning \"${MODEL_REASONING}\" \\\n --argjson input \"${MODEL_INPUT}\" \\\n '\n # Merge known models: add any model id not already present\n .models.providers[\"hiclaw-gateway\"].models as $existing\n | ($existing | map(.id)) as $existing_ids\n | ($known_models | map(select(.id as $id | $existing_ids | index($id) | not))) as $new\n | .models.providers[\"hiclaw-gateway\"].models = ($existing + $new)\n # Ensure the user-chosen default model is in the list (custom model support)\n | if (.models.providers[\"hiclaw-gateway\"].models | map(.id) | index($model) | not) then\n .models.providers[\"hiclaw-gateway\"].models += [{\"id\": $model, \"name\": $model, \"reasoning\": $reasoning, \"contextWindow\": $ctx, \"maxTokens\": $max, \"input\": $input}]\n else . end\n # Rebuild model aliases from the full models list\n | (.models.providers[\"hiclaw-gateway\"].models | map({ (\"hiclaw-gateway/\" + .id): { \"alias\": .id } }) | add // {}) as $aliases\n | .agents.defaults.models = ((.agents.defaults.models // {}) + $aliases)\n | .channels.matrix.accessToken = $token | .models.providers[\"hiclaw-gateway\"].apiKey = $key\n | ((.hooks.token // \"\") as $ht | if $ht == $key or $ht == ($key + \"-hooks\" | @base64) then del(.hooks) else . end)\n | .agents.defaults.model.primary = (\"hiclaw-gateway/\" + $model)\n | .commands.restart = true\n | .gateway.controlUi.dangerouslyDisableDeviceAuth = true\n | .channels.matrix.encryption = $e2ee\n # Ensure memorySearch config exists (embedding model for memory) — skip if embedding model is empty\n | if $emb_model != \"\" then .agents.defaults.memorySearch //= {\"provider\":\"openai\",\"model\":$emb_model,\"remote\":{\"baseUrl\":(\"http://\" + $aigw_domain + \":8080/v1\"),\"apiKey\":$key}} else . end\n ' \\\n /root/manager-workspace/openclaw.json > /tmp/openclaw.json.tmp && \\\n mv /tmp/openclaw.json.tmp /root/manager-workspace/openclaw.json\n # Verify the token was written correctly\n _written_token=$(jq -r '.channels.matrix.accessToken' /root/manager-workspace/openclaw.json 2>/dev/null)\n if [ -z \"${_written_token}\" ] || [ \"${_written_token}\" = \"null\" ]; then\n log \"ERROR: Matrix token was not written correctly to openclaw.json (got: ${_written_token})\"\n else\n log \"Matrix token written to openclaw.json (prefix: ${_written_token:0:10}...)\"\n fi\nelse\n log \"Manager openclaw.json not found, generating from template...\"\n envsubst < /opt/hiclaw/configs/manager-openclaw.json.tmpl > /root/manager-workspace/openclaw.json\n # Post-envsubst injection: memorySearch + custom model (single jq pass when possible)\n if ! jq -e --arg model \"${MODEL_NAME}\" '.models.providers[\"hiclaw-gateway\"].models | map(.id) | index($model)' /root/manager-workspace/openclaw.json > /dev/null 2>&1; then\n log \"Custom model '${MODEL_NAME}' not in built-in list, injecting into config...\"\n jq --arg emb_model \"${HICLAW_EMBEDDING_MODEL}\" \\\n --arg aigw_domain \"${AI_GATEWAY_DOMAIN}\" \\\n --arg key \"${HICLAW_MANAGER_GATEWAY_KEY}\" \\\n --arg model \"${MODEL_NAME}\" \\\n --argjson ctx \"${MODEL_CONTEXT_WINDOW}\" \\\n --argjson max \"${MODEL_MAX_TOKENS}\" \\\n --argjson reasoning \"${MODEL_REASONING}\" \\\n --argjson input \"${MODEL_INPUT}\" \\\n '\n (if $emb_model != \"\" then .agents.defaults.memorySearch = {\"provider\":\"openai\",\"model\":$emb_model,\"remote\":{\"baseUrl\":(\"http://\" + $aigw_domain + \":8080/v1\"),\"apiKey\":$key}} else . end)\n | .models.providers[\"hiclaw-gateway\"].models += [{\"id\": $model, \"name\": $model, \"reasoning\": $reasoning, \"contextWindow\": $ctx, \"maxTokens\": $max, \"input\": $input}]\n | .agents.defaults.models += {(\"hiclaw-gateway/\" + $model): {\"alias\": $model}}\n ' /root/manager-workspace/openclaw.json > /tmp/openclaw.json.tmp && \\\n mv /tmp/openclaw.json.tmp /root/manager-workspace/openclaw.json\n elif [ -n \"${HICLAW_EMBEDDING_MODEL}\" ]; then\n jq --arg emb_model \"${HICLAW_EMBEDDING_MODEL}\" \\\n --arg aigw_domain \"${AI_GATEWAY_DOMAIN}\" \\\n --arg key \"${HICLAW_MANAGER_GATEWAY_KEY}\" \\\n '.agents.defaults.memorySearch = {\"provider\":\"openai\",\"model\":$emb_model,\"remote\":{\"baseUrl\":(\"http://\" + $aigw_domain + \":8080/v1\"),\"apiKey\":$key}}' \\\n /root/manager-workspace/openclaw.json > /tmp/openclaw.json.tmp && \\\n mv /tmp/openclaw.json.tmp /root/manager-workspace/openclaw.json\n fi\n _written_token=$(jq -r '.channels.matrix.accessToken' /root/manager-workspace/openclaw.json 2>/dev/null)\n log \"Matrix token written from template (prefix: ${_written_token:0:10}...)\"\nfi\n\n# Cloud/K8s mode: overlay cloud-specific settings onto generated config\nif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ] || [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n log \"Applying cloud/k8s overlay to openclaw.json...\"\n jq --arg homeserver \"${HICLAW_MATRIX_SERVER}\" \\\n --arg gateway \"${HICLAW_AI_GATEWAY_URL}/v1\" \\\n --arg key \"${HICLAW_MANAGER_GATEWAY_KEY}\" \\\n '.channels.matrix.homeserver = $homeserver\n | .models.providers[\"hiclaw-gateway\"].baseUrl = $gateway\n | .models.providers[\"hiclaw-gateway\"].apiKey = $key\n | ((.hooks.token // \"\") as $ht | if $ht == $key or $ht == ($key + \"-hooks\" | @base64) then del(.hooks) else . end)\n | .commands.restart = false\n | if .agents.defaults.memorySearch then .agents.defaults.memorySearch.remote.baseUrl = $gateway | .agents.defaults.memorySearch.remote.apiKey = $key else . end' \\\n /root/manager-workspace/openclaw.json > /tmp/openclaw-cloud.json && \\\n mv /tmp/openclaw-cloud.json /root/manager-workspace/openclaw.json\n log \"Cloud/K8s overlay applied\"\nfi\n\n# ============================================================\n# Optional: enable openclaw-cms-plugin observability\n# Config is applied at runtime so secrets stay out of image layers.\n# ============================================================\nCMS_TRACES_ENABLED=\"$(echo \"${HICLAW_CMS_TRACES_ENABLED:-false}\" | tr '[:upper:]' '[:lower:]')\"\nif [ \"${CMS_TRACES_ENABLED}\" = \"true\" ]; then\n CMS_PLUGIN_NAME=\"openclaw-cms-plugin\"\n CMS_PLUGIN_DIR=\"${OPENCLAW_CMS_PLUGIN_DIR:-/opt/openclaw/extensions/openclaw-cms-plugin}\"\n CMS_PLUGIN_MANIFEST=\"${CMS_PLUGIN_DIR}/openclaw.plugin.json\"\n DIAG_PLUGIN_NAME=\"diagnostics-otel\"\n DIAG_PLUGIN_DIR=\"/opt/openclaw/extensions/diagnostics-otel\"\n CMS_LICENSE_KEY=\"${HICLAW_CMS_LICENSE_KEY:-}\"\n CMS_PROJECT=\"${HICLAW_CMS_PROJECT:-}\"\n CMS_METRICS_ENABLED=\"${HICLAW_CMS_METRICS_ENABLED:-false}\"\n\n if [ ! -f \"${CMS_PLUGIN_MANIFEST}\" ]; then\n log \"WARNING: ${CMS_PLUGIN_NAME} manifest not found at ${CMS_PLUGIN_MANIFEST}, skipping plugin config\"\n else\n _missing=0\n [ -z \"${HICLAW_CMS_ENDPOINT:-}\" ] && log \"WARNING: HICLAW_CMS_ENDPOINT is required when HICLAW_CMS_TRACES_ENABLED=true\" && _missing=1\n [ -z \"${CMS_LICENSE_KEY:-}\" ] && log \"WARNING: HICLAW_CMS_LICENSE_KEY is required when HICLAW_CMS_TRACES_ENABLED=true\" && _missing=1\n [ -z \"${HICLAW_CMS_WORKSPACE:-}\" ] && log \"WARNING: HICLAW_CMS_WORKSPACE is required when HICLAW_CMS_TRACES_ENABLED=true\" && _missing=1\n\n if [ \"${_missing}\" = \"0\" ]; then\n CMS_SERVICE_NAME=\"${HICLAW_CMS_SERVICE_NAME:-hiclaw-manager}\"\n CMS_ENABLE_METRICS=\"${CMS_METRICS_ENABLED}\"\n DIAG_AVAILABLE=\"0\"\n _metrics_lc=\"$(echo \"${CMS_ENABLE_METRICS}\" | tr '[:upper:]' '[:lower:]')\"\n if [ \"${_metrics_lc}\" = \"true\" ]; then\n if [ -f \"${DIAG_PLUGIN_DIR}/package.json\" ]; then\n DIAG_AVAILABLE=\"1\"\n if [ ! -d \"${DIAG_PLUGIN_DIR}/node_modules\" ]; then\n log \"diagnostics-otel dependencies missing, installing...\"\n if (cd \"${DIAG_PLUGIN_DIR}\" && npm install --omit=dev --ignore-scripts >/tmp/hiclaw-diag-install.log 2>&1); then\n log \"diagnostics-otel dependencies installed\"\n else\n log \"WARNING: diagnostics-otel npm install failed, metrics plugin may not load\"\n fi\n else\n log \"diagnostics-otel dependencies already present\"\n fi\n else\n log \"WARNING: diagnostics-otel package.json not found at ${DIAG_PLUGIN_DIR}, metrics plugin may not load\"\n fi\n fi\n\n log \"Applying ${CMS_PLUGIN_NAME} config to openclaw.json...\"\n jq --arg pluginName \"${CMS_PLUGIN_NAME}\" \\\n --arg pluginDir \"${CMS_PLUGIN_DIR}\" \\\n --arg endpoint \"${HICLAW_CMS_ENDPOINT}\" \\\n --arg licenseKey \"${CMS_LICENSE_KEY}\" \\\n --arg armsProject \"${CMS_PROJECT}\" \\\n --arg cmsWorkspace \"${HICLAW_CMS_WORKSPACE}\" \\\n --arg serviceName \"${CMS_SERVICE_NAME}\" \\\n --arg diagPluginName \"${DIAG_PLUGIN_NAME}\" \\\n --arg diagPluginDir \"${DIAG_PLUGIN_DIR}\" \\\n --arg metricsRaw \"${CMS_ENABLE_METRICS}\" \\\n --arg diagAvailableRaw \"${DIAG_AVAILABLE}\" \\\n '\n .plugins = (.plugins // {})\n | .plugins.load = (.plugins.load // {})\n | .plugins.entries = (.plugins.entries // {})\n | if (.plugins.allow | type) != \"array\" then .plugins.allow = [] else . end\n | if (.plugins.allow | index($pluginName)) == null then .plugins.allow += [$pluginName] else . end\n | if (.plugins.load.paths | type) != \"array\" then .plugins.load.paths = [] else . end\n | if (.plugins.load.paths | index($pluginDir)) == null then .plugins.load.paths += [$pluginDir] else . end\n | .plugins.entries[$pluginName] = {\n \"enabled\": true,\n \"config\": {\n \"endpoint\": $endpoint,\n \"headers\": {\n \"x-arms-license-key\": $licenseKey,\n \"x-arms-project\": $armsProject,\n \"x-cms-workspace\": $cmsWorkspace\n },\n \"serviceName\": $serviceName\n }\n }\n\n # diagnostics-otel metrics (optional)\n | ($metricsRaw | ascii_downcase) as $m\n | ($diagAvailableRaw == \"1\") as $diagAvailable\n | (($m == \"true\") and $diagAvailable) as $metricsEnabled\n | if $metricsEnabled then\n (if (.plugins.allow | index($diagPluginName)) == null then .plugins.allow += [$diagPluginName] else . end)\n | (if (.plugins.load.paths | index($diagPluginDir)) == null then .plugins.load.paths += [$diagPluginDir] else . end)\n | .plugins.entries[$diagPluginName].enabled = true\n | .diagnostics = (.diagnostics // {})\n | .diagnostics.otel = (.diagnostics.otel // {})\n | .diagnostics.enabled = true\n | .diagnostics.otel.enabled = true\n | .diagnostics.otel.endpoint = $endpoint\n | .diagnostics.otel.protocol = (.diagnostics.otel.protocol // \"http/protobuf\")\n | .diagnostics.otel.headers = {\n \"x-arms-license-key\": $licenseKey,\n \"x-arms-project\": $armsProject,\n \"x-cms-workspace\": $cmsWorkspace\n }\n | .diagnostics.otel.serviceName = $serviceName\n | .diagnostics.otel.metrics = true\n | .diagnostics.otel.traces = (.diagnostics.otel.traces // false)\n | .diagnostics.otel.logs = (.diagnostics.otel.logs // false)\n else\n .\n end\n ' /root/manager-workspace/openclaw.json > /tmp/openclaw-cms.json && \\\n mv /tmp/openclaw-cms.json /root/manager-workspace/openclaw.json\n log \"${CMS_PLUGIN_NAME} config applied (metrics=${CMS_ENABLE_METRICS}, service=${CMS_SERVICE_NAME})\"\n else\n log \"Skipping ${CMS_PLUGIN_NAME} config due to missing required env vars\"\n fi\n fi\nfi\n\n# ============================================================\n# Detect container runtime (for Worker creation)\n# ============================================================\nsource /opt/hiclaw/scripts/lib/container-api.sh\nif container_api_available; then\n log \"Container runtime socket detected at ${CONTAINER_SOCKET} — direct Worker creation enabled\"\n export HICLAW_CONTAINER_RUNTIME=\"socket\"\nelif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ] || [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n log \"Cloud/K8s mode — Workers created via controller API\"\n export HICLAW_CONTAINER_RUNTIME=\"cloud\"\nelse\n log \"No container runtime found — Worker creation will output install commands\"\n export HICLAW_CONTAINER_RUNTIME=\"none\"\nfi\n\n# ============================================================\n# Upgrade Worker openclaw.json: merge known models + E2EE flag into existing configs\n# Existing workers in MinIO may have old single-model configs or missing encryption field.\n# Merge template models so they can hot-switch without restart.\n# ============================================================\nREGISTRY_FILE=\"/root/manager-workspace/workers-registry.json\"\nif [ -f \"${REGISTRY_FILE}\" ]; then\n # Use known-models.json (valid JSON) instead of template (contains ${VAR} placeholders)\n KNOWN_MODELS_FILE=\"/opt/hiclaw/configs/known-models.json\"\n if [ -f \"${KNOWN_MODELS_FILE}\" ]; then\n _KNOWN_MODELS=$(cat \"${KNOWN_MODELS_FILE}\")\n for _wname in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_wname}\" ] && continue\n _minio_path=\"${HICLAW_STORAGE_PREFIX}/agents/${_wname}/openclaw.json\"\n _tmp_in=\"/tmp/openclaw-${_wname}-models-upgrade-in.json\"\n if mc cp \"${_minio_path}\" \"${_tmp_in}\" 2>/dev/null; then\n _tmp_out=\"/tmp/openclaw-${_wname}-models-upgrade-out.json\"\n # Idempotent merge: add missing known models, rebuild aliases, set e2ee.\n # Always runs — jq deduplicates by model id, so re-runs are safe.\n jq --argjson known_models \"${_KNOWN_MODELS}\" \\\n --argjson e2ee \"${MATRIX_E2EE_ENABLED}\" '\n .models.providers[\"hiclaw-gateway\"].models as $existing\n | ($existing | map(.id)) as $existing_ids\n | ($known_models | map(select(.id as $id | $existing_ids | index($id) | not))) as $new\n | .models.providers[\"hiclaw-gateway\"].models = ($existing + $new)\n | (.models.providers[\"hiclaw-gateway\"].models | map({ (\"hiclaw-gateway/\" + .id): { \"alias\": .id } }) | add // {}) as $aliases\n | .agents.defaults.models = ((.agents.defaults.models // {}) + $aliases)\n | .channels.matrix.encryption = $e2ee\n ' \"${_tmp_in}\" > \"${_tmp_out}\" 2>/dev/null\n if ! diff -q \"${_tmp_in}\" \"${_tmp_out}\" > /dev/null 2>&1; then\n if mc cp \"${_tmp_out}\" \"${_minio_path}\" 2>/dev/null; then\n _new_count=$(jq '.models.providers[\"hiclaw-gateway\"].models | length' \"${_tmp_out}\" 2>/dev/null)\n log \"Worker ${_wname}: upgraded openclaw.json (models: ${_new_count}, e2ee: ${MATRIX_E2EE_ENABLED})\"\n fi\n fi\n rm -f \"${_tmp_in}\" \"${_tmp_out}\"\n fi\n done\n fi\nfi\n\n# ============================================================\n# Ensure Worker Matrix password files exist in MinIO (E2EE fix)\n# Workers need to re-login on restart to get a fresh device_id.\n# Older workers created before this fix won't have the password file.\n# ============================================================\nif [ -f \"${REGISTRY_FILE}\" ]; then\n for _wname in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_wname}\" ] && continue\n _creds_file=\"/data/worker-creds/${_wname}.env\"\n if [ -f \"${_creds_file}\" ]; then\n # Check if password file already exists in MinIO\n if ! mc stat \"${HICLAW_STORAGE_PREFIX}/agents/${_wname}/credentials/matrix/password\" > /dev/null 2>&1; then\n source \"${_creds_file}\"\n if [ -n \"${WORKER_PASSWORD}\" ]; then\n _tmp_pw=\"/tmp/matrix-pw-${_wname}\"\n echo -n \"${WORKER_PASSWORD}\" > \"${_tmp_pw}\"\n mc cp \"${_tmp_pw}\" \"${HICLAW_STORAGE_PREFIX}/agents/${_wname}/credentials/matrix/password\" 2>/dev/null \\\n && log \"Worker ${_wname}: wrote Matrix password to MinIO (E2EE re-login fix)\" \\\n || log \"Worker ${_wname}: WARNING: failed to write Matrix password to MinIO\"\n rm -f \"${_tmp_pw}\"\n fi\n fi\n fi\n done\nfi\n\n# ============================================================\n# Recreate Worker containers as needed after Manager restart.\n# Workers are on hiclaw-net; Docker DNS resolves *-local.hiclaw.io via\n# the Manager's network aliases, so IP changes don't require worker recreation.\n# Only recreate stopped/missing workers.\n# ============================================================\nif container_api_available; then\n REGISTRY_FILE=\"/root/manager-workspace/workers-registry.json\"\n if [ -f \"${REGISTRY_FILE}\" ]; then\n for _worker_name in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_worker_name}\" ] && continue\n\n # Skip remote workers — they are not Manager-managed containers.\n _deployment=$(jq -r --arg w \"${_worker_name}\" '.workers[$w].deployment // \"local\"' \"${REGISTRY_FILE}\" 2>/dev/null)\n if [ \"${_deployment}\" = \"remote\" ]; then\n log \"Worker ${_worker_name} is remote, skipping container recreate\"\n continue\n fi\n\n _status=$(container_status_worker \"${_worker_name}\")\n if [ \"${_status}\" = \"running\" ]; then\n log \"Worker running: ${_worker_name}, skipping\"\n continue\n fi\n # Container missing or stopped — recreate.\n log \"Worker container ${_status}: ${_worker_name}, recreating...\"\n _creds_file=\"/data/worker-creds/${_worker_name}.env\"\n if [ -f \"${_creds_file}\" ]; then\n source \"${_creds_file}\"\n _runtime=$(jq -r --arg w \"${_worker_name}\" '.workers[$w].runtime // \"openclaw\"' \"${REGISTRY_FILE}\" 2>/dev/null)\n _recreated=false\n for _attempt in 1 2 3; do\n local _env_map _create_body\n _env_map=$(jq -cn \\\n --arg name \"${_worker_name}\" \\\n --arg fak \"${_worker_name}\" \\\n --arg fsk \"${WORKER_MINIO_PASSWORD:-}\" \\\n --arg fs_domain \"${HICLAW_FS_DOMAIN:-fs-local.hiclaw.io}\" \\\n --arg controller_url \"${HICLAW_CONTROLLER_URL:-}\" \\\n '{\n \"HICLAW_WORKER_NAME\": $name,\n \"HICLAW_FS_ENDPOINT\": (\"http://\" + ($fs_domain | split(\":\")[0]) + \":8080\"),\n \"HICLAW_FS_ACCESS_KEY\": $fak,\n \"HICLAW_FS_SECRET_KEY\": $fsk\n }\n | if $controller_url != \"\" then . + {\"HICLAW_CONTROLLER_URL\": $controller_url} else . end')\n _create_body=$(jq -cn --arg name \"${_worker_name}\" --arg runtime \"${_runtime}\" --argjson env \"${_env_map}\" '{name: $name, runtime: $runtime, env: $env}')\n worker_backend_create \"${_create_body}\" > /dev/null 2>&1 && _recreated=true && break\n log \" Attempt ${_attempt}/3 failed for ${_worker_name}, retrying in $((5 * _attempt))s...\"\n sleep $((5 * _attempt))\n done\n if [ \"${_recreated}\" = true ]; then\n log \" Recreated ${_runtime} worker: ${_worker_name}\"\n else\n log \" ERROR: Failed to recreate ${_worker_name} after 3 attempts\"\n fi\n else\n log \" WARNING: No credentials found for ${_worker_name} (${_creds_file} missing), skipping\"\n fi\n done\n fi\nfi\n\n# ============================================================\n# Notify workers of builtin updates if upgrade happened\n# Builtin files (AGENTS.md, skills) are already synced by upgrade-builtins.sh\n#\n# Cooldown: skip notification if the last successful notify was within\n# NOTIFY_COOLDOWN_SECS (default 3600s / 1 hour). This prevents repeated\n# notifications when the Manager crash-loops and re-runs upgrade-builtins\n# on every restart (e.g. IMAGE_VERSION=latest always triggers upgrade).\n# ============================================================\nNOTIFY_COOLDOWN_SECS=\"${HICLAW_NOTIFY_COOLDOWN_SECS:-3600}\"\nNOTIFY_TS_FILE=\"/root/manager-workspace/.last-worker-notify-ts\"\n\nif [ -f /root/manager-workspace/.upgrade-pending-worker-notify ]; then\n _now=$(date +%s)\n _last_notify=$(cat \"${NOTIFY_TS_FILE}\" 2>/dev/null || echo \"0\")\n _elapsed=$(( _now - _last_notify ))\n\n if [ \"${_elapsed}\" -lt \"${NOTIFY_COOLDOWN_SECS}\" ]; then\n log \"Skipping worker builtin notification (last notify ${_elapsed}s ago, cooldown ${NOTIFY_COOLDOWN_SECS}s)\"\n rm -f /root/manager-workspace/.upgrade-pending-worker-notify\n else\n log \"Notifying workers about builtin updates...\"\n REGISTRY_FILE=\"/root/manager-workspace/workers-registry.json\"\n _notify_ok=false\n if [ -f \"${REGISTRY_FILE}\" ]; then\n for _worker_name in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_worker_name}\" ] && continue\n _room_id=$(jq -r --arg w \"${_worker_name}\" '.workers[$w].room_id // empty' \"${REGISTRY_FILE}\" 2>/dev/null)\n if [ -n \"${_room_id}\" ]; then\n _worker_id=\"@${_worker_name}:${MATRIX_DOMAIN}\"\n _txn_id=\"upgrade-$(date +%s%N)\"\n _msg=\"@${_worker_name}:${MATRIX_DOMAIN} Manager upgraded builtin files (AGENTS.md, skills). Please use your file-sync skill to sync the latest config.\"\n _raw=$(curl -s -w '\\nHTTP_CODE:%{http_code}' -X PUT \\\n \"${HICLAW_MATRIX_SERVER}/_matrix/client/v3/rooms/${_room_id}/send/m.room.message/${_txn_id}\" \\\n -H \"Authorization: Bearer ${MANAGER_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d \"{\\\"msgtype\\\":\\\"m.text\\\",\\\"body\\\":\\\"${_msg}\\\",\\\"m.mentions\\\":{\\\"user_ids\\\":[\\\"${_worker_id}\\\"]}}\" \\\n 2>&1) || true\n _http_code=$(echo \"${_raw}\" | tail -1 | sed 's/HTTP_CODE://')\n _notify_resp=$(echo \"${_raw}\" | sed '$d')\n if echo \"${_notify_resp}\" | jq -e '.event_id' > /dev/null 2>&1; then\n log \" Notified ${_worker_name}\"; _notify_ok=true\n else\n log \" WARNING: Failed to notify ${_worker_name} (HTTP ${_http_code}): ${_notify_resp}\"\n fi\n fi\n done\n fi\n # Record timestamp only if at least one notification succeeded\n if [ \"${_notify_ok}\" = true ]; then\n echo \"${_now}\" > \"${NOTIFY_TS_FILE}\"\n fi\n rm -f /root/manager-workspace/.upgrade-pending-worker-notify\n fi\nfi\n\n# ============================================================\n# Start Manager Agent\n# ============================================================\nlog \"Starting Manager Agent (${MANAGER_RUNTIME})...\"\n\n# HOME is already set to /root/manager-workspace via docker run -e HOME=...\ncd \"${HOME}\"\n\n# Ensure host credential symlinks exist under HOME\nif [ -d \"/host-share\" ]; then\n [ -f \"/host-share/.gitconfig\" ] && ln -sf \"/host-share/.gitconfig\" \"${HOME}/.gitconfig\"\nfi\n\nlog \"HOME=${HOME} (manager-workspace, host-mounted)\"\n\n# ── Render agent doc templates ────────────────────────────────────────────\n# Replace ${VAR} placeholders with actual values so the AI agent reads\n# plain text and never needs to resolve environment variables.\nexport MANAGER_MATRIX_TOKEN MANAGER_TOKEN HIGRESS_COOKIE_FILE\nRENDER=/opt/hiclaw/scripts/lib/render-skills.sh\nlog \"Rendering agent doc templates...\"\n# Manager-owned docs (workspace)\nbash \"$RENDER\" /root/manager-workspace/skills\nbash \"$RENDER\" /root/manager-workspace/skills-alpha\nbash \"$RENDER\" /root/manager-workspace AGENTS.md TOOLS.md HEARTBEAT.md SOUL.md\n# Worker templates (workspace + image) — rendered before push to MinIO\n# so Workers (including remote pip-install) receive plain text\nbash \"$RENDER\" /root/manager-workspace/worker-skills\nbash \"$RENDER\" /root/manager-workspace/worker-agent\nbash \"$RENDER\" /root/manager-workspace/copaw-worker-agent\nbash \"$RENDER\" /opt/hiclaw/agent/worker-skills\nbash \"$RENDER\" /opt/hiclaw/agent/worker-agent\nbash \"$RENDER\" /opt/hiclaw/agent/copaw-worker-agent\nlog \"Agent doc templates rendered\"\n\n# Cloud mode: start background file sync (workspace ↔ OSS) and initial push\nif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n log \"Syncing initial workspace to OSS...\"\n ensure_mc_credentials\n mc mirror /root/manager-workspace/ \"${HICLAW_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" 2>/dev/null || true\n\n # Local → OSS: change-triggered sync\n (\n while true; do\n CHANGED=$(find /root/manager-workspace/ -type f -newermt \"15 seconds ago\" 2>/dev/null | head -1)\n if [ -n \"${CHANGED}\" ]; then\n ensure_mc_credentials 2>/dev/null || true\n mc mirror /root/manager-workspace/ \"${HICLAW_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" --exclude \".npm/**\" \\\n --exclude \".local/**\" --exclude \".mc/**\" 2>/dev/null || true\n fi\n sleep 10\n done\n ) &\n log \"Local→OSS sync started (PID: $!)\"\n\n # OSS → Local: periodic pull (shared data, agent configs)\n (\n while true; do\n sleep 300\n ensure_mc_credentials 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/shared/\" /root/hiclaw-fs/shared/ --overwrite --newer-than \"5m\" 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/agents/\" /root/hiclaw-fs/agents/ --overwrite --newer-than \"5m\" 2>/dev/null || true\n done\n ) &\n log \"OSS→Local sync started (every 5m, PID: $!)\"\nfi\n\n# K8s mode: start background file sync (workspace ↔ MinIO)\nif [ \"${HICLAW_RUNTIME}\" = \"k8s\" ]; then\n log \"Syncing initial workspace to MinIO...\"\n mc mirror /root/manager-workspace/ \"${HICLAW_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" 2>/dev/null || true\n\n # Local → MinIO: change-triggered sync\n (\n while true; do\n CHANGED=$(find /root/manager-workspace/ -type f -newermt \"15 seconds ago\" 2>/dev/null | head -1)\n if [ -n \"${CHANGED}\" ]; then\n mc mirror /root/manager-workspace/ \"${HICLAW_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" --exclude \".npm/**\" \\\n --exclude \".local/**\" --exclude \".mc/**\" 2>/dev/null || true\n fi\n sleep 10\n done\n ) &\n log \"Local→MinIO sync started (PID: $!)\"\n\n # MinIO → Local: periodic pull (shared data, agent configs)\n (\n while true; do\n sleep 300\n mc mirror \"${HICLAW_STORAGE_PREFIX}/shared/\" /root/hiclaw-fs/shared/ --overwrite --newer-than \"5m\" 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/agents/\" /root/hiclaw-fs/agents/ --overwrite --newer-than \"5m\" 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/hiclaw-config/\" /root/hiclaw-fs/hiclaw-config/ --overwrite --newer-than \"15s\" 2>/dev/null || true\n done\n ) &\n log \"MinIO→Local sync started (every 5m, PID: $!)\"\nfi\n\n# ============================================================\n# Auto-generate Manager mcporter config for pre-configured MCP servers\n# If HICLAW_GITHUB_TOKEN was set at install time, setup-higress.sh already\n# configured GitHub MCP on Higress. Run setup-mcp-server.sh now so that\n# config/mcporter.json exists when the Agent starts — no need to ask user for PAT.\n# ============================================================\nif [ -n \"${HICLAW_GITHUB_TOKEN}\" ] && [ \"${HICLAW_RUNTIME}\" != \"aliyun\" ] && [ \"${HICLAW_RUNTIME}\" != \"k8s\" ]; then\n if [ ! -f \"${HOME}/config/mcporter.json\" ]; then\n log \"Auto-generating Manager mcporter config for GitHub MCP (HICLAW_GITHUB_TOKEN set)...\"\n bash /opt/hiclaw/agent/skills/mcp-server-management/scripts/setup-mcp-server.sh \\\n github \"${HICLAW_GITHUB_TOKEN}\" 2>&1 | while IFS= read -r line; do log \" [setup-mcp] ${line}\"; done || \\\n log \"WARNING: setup-mcp-server.sh failed — Agent may need to configure GitHub MCP manually\"\n else\n log \"Manager mcporter config already exists, skipping auto-generate\"\n fi\nfi\n\n# ============================================================\n# Runtime-specific startup\n# ============================================================\nif [ \"${MANAGER_RUNTIME}\" = \"copaw\" ]; then\n # Delegate to CoPaw startup script\n exec /opt/hiclaw/scripts/init/start-copaw-manager.sh\nelse\n # ── OpenClaw Runtime ─────────────────────────────────────────────────────\n log \"Starting OpenClaw Manager...\"\n\n export OPENCLAW_CONFIG_PATH=\"/root/manager-workspace/openclaw.json\"\n\n # Symlink to default OpenClaw config path so CLI commands find the config\n mkdir -p \"${HOME}/.openclaw\"\n ln -sf \"/root/manager-workspace/openclaw.json\" \"${HOME}/.openclaw/openclaw.json\"\n\n # Clean orphaned session write locks (e.g. from SIGKILL or crash before exit handlers)\n # Prevents \"session file locked (timeout 10000ms)\" when PID was reused\n find \"${HOME}/.openclaw/agents\" -name \"*.jsonl.lock\" -delete 2>/dev/null || true\n log \"Cleaned up any orphaned session write locks\"\n\n # Clean Matrix crypto storage (SQLite WAL may be corrupted after unclean shutdown)\n # Crypto state is re-negotiated on startup; losing it only means re-establishing E2EE sessions\n rm -rf \"${HOME}/.openclaw/matrix\" 2>/dev/null || true\n log \"Cleaned Matrix crypto storage (will re-establish E2EE sessions)\"\n\n # Launch OpenClaw\n # Disable full-process respawn so the CLI uses its internal restart loop.\n # Without this, config reload spawns a detached child and exits, then\n # supervisord restarts the CLI — resulting in two gateway processes.\n export OPENCLAW_NO_RESPAWN=1\n exec openclaw gateway run --verbose --force\nfi\n"}, "38b71d86d8ce2dba57b89a80f25d0cd3f05e4c8e": {"t": 1785764762.151131, "n": 52966, "h": "1439987a09bda345", "c": "#!/bin/bash\n# start-manager-agent.sh - Initialize and start the Manager Agent\n# Supports both local (supervisord) and cloud (SAE single-process) deployments.\n# In local mode this is the last supervisord component to start (priority 800).\n# In cloud mode (HICLAW_RUNTIME=aliyun) this is the container entrypoint.\n#\n# Runtime selection:\n# HICLAW_MANAGER_RUNTIME=openclaw (default) - OpenClaw gateway mode\n# HICLAW_MANAGER_RUNTIME=copaw - CoPaw workspace mode\n\nsource /opt/hiclaw/scripts/lib/hiclaw-env.sh\n\n# ============================================================\n# Runtime selection\n# ============================================================\nMANAGER_RUNTIME=\"${HICLAW_MANAGER_RUNTIME:-openclaw}\"\ncase \"${MANAGER_RUNTIME}\" in\n copaw)\n log \"Manager runtime: CoPaw (Python workspace)\"\n ;;\n *)\n log \"Manager runtime: OpenClaw (Node.js gateway)\"\n MANAGER_RUNTIME=\"openclaw\"\n ;;\nesac\n\n# ============================================================\n# Set timezone from TZ env var\n# ============================================================\nif [ -n \"${TZ}\" ] && [ -f \"/usr/share/zoneinfo/${TZ}\" ]; then\n ln -sf \"/usr/share/zoneinfo/${TZ}\" /etc/localtime\n echo \"${TZ}\" > /etc/timezone\n log \"Timezone set to ${TZ}\"\nfi\n\nexport MATRIX_DOMAIN=\"${HICLAW_MATRIX_DOMAIN:-matrix-local.hiclaw.io:8080}\"\nAI_GATEWAY_DOMAIN=\"${HICLAW_AI_GATEWAY_DOMAIN:-aigw-local.hiclaw.io}\"\n\n# ============================================================\n# Cloud mode: validate required environment variables + initial credentials\n# ============================================================\nif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n : \"${HICLAW_MATRIX_URL:?HICLAW_MATRIX_URL is required}\"\n : \"${HICLAW_MATRIX_DOMAIN:?HICLAW_MATRIX_DOMAIN is required}\"\n : \"${HICLAW_AI_GATEWAY_URL:?HICLAW_AI_GATEWAY_URL is required}\"\n : \"${HICLAW_MANAGER_GATEWAY_KEY:?HICLAW_MANAGER_GATEWAY_KEY is required}\"\n : \"${HICLAW_MANAGER_PASSWORD:?HICLAW_MANAGER_PASSWORD is required (cloud containers are stateless, password must be injected)}\"\n : \"${HICLAW_REGISTRATION_TOKEN:?HICLAW_REGISTRATION_TOKEN is required}\"\n : \"${HICLAW_ADMIN_USER:?HICLAW_ADMIN_USER is required}\"\n : \"${HICLAW_ADMIN_PASSWORD:?HICLAW_ADMIN_PASSWORD is required}\"\n log \"Cloud mode: validating environment... OK\"\n log \" Matrix: ${HICLAW_MATRIX_SERVER}, AI Gateway: ${HICLAW_AI_GATEWAY_URL}, OSS: ${HICLAW_STORAGE_BUCKET}\"\n ensure_mc_credentials || { log \"FATAL: Initial STS credential fetch failed\"; exit 1; }\nfi\n\n# ============================================================\n# Local mode: host symlinks, /etc/hosts, wait for local services\n# ============================================================\nif [ \"${HICLAW_RUNTIME}\" != \"aliyun\" ]; then\n # Create symlink for host directory access\n if [ -d \"/host-share\" ]; then\n ORIGINAL_HOST_HOME=\"${HOST_ORIGINAL_HOME:-$HOME}\"\n if [ ! -e \"${ORIGINAL_HOST_HOME}\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/root\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/data\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/host-share\" ]; then\n mkdir -p \"$(dirname \"${ORIGINAL_HOST_HOME}\")\"\n ln -sfn /host-share \"${ORIGINAL_HOST_HOME}\"\n log \"Created symlink: ${ORIGINAL_HOST_HOME} -> /host-share\"\n else\n ln -sfn /host-share /root/host-home\n log \"Created fallback symlink: /root/host-home -> /host-share\"\n fi\n fi\n\n # Add local domains to /etc/hosts\n HOSTS_DOMAINS=\"${MATRIX_DOMAIN%%:*} ${HICLAW_MATRIX_CLIENT_DOMAIN:-matrix-client-local.hiclaw.io} ${AI_GATEWAY_DOMAIN} ${HICLAW_FS_DOMAIN:-fs-local.hiclaw.io}\"\n if ! grep -q \"${AI_GATEWAY_DOMAIN}\" /etc/hosts 2>/dev/null; then\n echo \"127.0.0.1 ${HOSTS_DOMAINS}\" >> /etc/hosts\n log \"Added local domains to /etc/hosts\"\n fi\n\n # Wait for local infrastructure\n waitForService \"Higress Gateway\" \"127.0.0.1\" 8080 180\n waitForService \"Higress Console\" \"127.0.0.1\" 8001 180\n waitForService \"Tuwunel\" \"127.0.0.1\" 6167 120\n waitForHTTP \"Tuwunel Matrix API\" \"${HICLAW_MATRIX_SERVER}/_tuwunel/server_version\" 120\n waitForService \"MinIO\" \"127.0.0.1\" 9000 120\nelse\n # Cloud mode: wait for external Tuwunel\n log \"Waiting for Tuwunel Matrix server at ${HICLAW_MATRIX_SERVER}...\"\n _retry=0\n while [ \"${_retry}\" -lt 30 ]; do\n if curl -sf \"${HICLAW_MATRIX_SERVER}/_matrix/client/versions\" > /dev/null 2>&1; then\n log \"Tuwunel is ready\"\n break\n fi\n _retry=$((_retry + 1))\n log \" Waiting for Tuwunel (attempt ${_retry}/30)...\"\n sleep 5\n done\n if [ \"${_retry}\" -ge 30 ]; then\n log \"ERROR: Tuwunel not reachable at ${HICLAW_MATRIX_SERVER}\"\n exit 1\n fi\nfi\n\n# ============================================================\n# Auto-generate secrets if not provided via environment\n# Persisted to /data so they survive container restart\n# ============================================================\nSECRETS_FILE=\"/data/hiclaw-secrets.env\"\nif [ -f \"${SECRETS_FILE}\" ]; then\n source \"${SECRETS_FILE}\"\n log \"Loaded persisted secrets from ${SECRETS_FILE}\"\nfi\n\nif [ -z \"${HICLAW_MANAGER_GATEWAY_KEY}\" ]; then\n export HICLAW_MANAGER_GATEWAY_KEY=\"$(generateKey 32)\"\n log \"Auto-generated HICLAW_MANAGER_GATEWAY_KEY\"\nfi\nif [ -z \"${HICLAW_MANAGER_PASSWORD}\" ]; then\n export HICLAW_MANAGER_PASSWORD=\"$(generateKey 16)\"\n log \"Auto-generated HICLAW_MANAGER_PASSWORD\"\nfi\n\n# Persist secrets so they survive supervisord restart\nmkdir -p /data\ncat > \"${SECRETS_FILE}\" <<EOF\nexport HICLAW_MANAGER_GATEWAY_KEY=\"${HICLAW_MANAGER_GATEWAY_KEY}\"\nexport HICLAW_MANAGER_PASSWORD=\"${HICLAW_MANAGER_PASSWORD}\"\nEOF\nchmod 600 \"${SECRETS_FILE}\"\n\n# Cloud mode: pull workspace from OSS before initialization\nif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n HICLAW_FS=\"/root/hiclaw-fs\"\n mkdir -p \"${HICLAW_FS}/shared\" \"${HICLAW_FS}/agents\"\n log \"Pulling workspace from OSS...\"\n ensure_mc_credentials\n mc mirror \"${HICLAW_STORAGE_PREFIX}/manager/\" /root/manager-workspace/ --overwrite 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/shared/\" \"${HICLAW_FS}/shared/\" --overwrite 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/agents/\" \"${HICLAW_FS}/agents/\" --overwrite 2>/dev/null || true\n # Symlink hiclaw-fs into workspace for agent access\n ln -sfn \"${HICLAW_FS}\" /root/manager-workspace/hiclaw-fs\nfi\n\n# ============================================================\n# Initialize / upgrade Manager workspace\n# First boot: full init via upgrade-builtins.sh\n# Subsequent boots: compare image version; upgrade only if changed\n# ============================================================\nmkdir -p /root/manager-workspace\n\nIMAGE_VERSION=$(cat /opt/hiclaw/agent/.builtin-version 2>/dev/null || echo \"unknown\")\nINSTALLED_VERSION=$(cat /root/manager-workspace/.builtin-version 2>/dev/null || echo \"\")\n\nif [ ! -f /root/manager-workspace/.initialized ]; then\n log \"First boot: initializing manager workspace...\"\n bash /opt/hiclaw/scripts/init/upgrade-builtins.sh\n touch /root/manager-workspace/.initialized\n log \"Manager workspace initialized (version: ${IMAGE_VERSION})\"\nelif [ \"${IMAGE_VERSION}\" != \"${INSTALLED_VERSION}\" ] || [ \"${IMAGE_VERSION}\" = \"latest\" ]; then\n log \"Upgrade detected: ${INSTALLED_VERSION} -> ${IMAGE_VERSION}${IMAGE_VERSION:+ (latest: always upgrade)}\"\n bash /opt/hiclaw/scripts/init/upgrade-builtins.sh\n log \"Manager workspace upgraded to version: ${IMAGE_VERSION}\"\nelse\n log \"Workspace up to date (version: ${IMAGE_VERSION})\"\nfi\n\n# Local mode: wait for mc mirror initialization (shared + worker data in /root/hiclaw-fs/)\nif [ \"${HICLAW_RUNTIME}\" != \"aliyun\" ]; then\n log \"Waiting for MinIO storage initialization...\"\n _minio_wait=0\n while [ ! -f /root/hiclaw-fs/.initialized ]; do\n sleep 2\n _minio_wait=$(( _minio_wait + 1 ))\n if [ \"${_minio_wait}\" -ge 60 ]; then\n log \"ERROR: MinIO storage initialization timed out after 120s\"\n exit 1\n fi\n done\n log \"MinIO storage initialized\"\nfi\n\n# ============================================================\n# Register Matrix users via Registration API (single-step, no UIAA)\n# ============================================================\nlog \"Registering human admin Matrix account...\"\ncurl -sf -X POST ${HICLAW_MATRIX_SERVER}/_matrix/client/v3/register \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"username\": \"'\"${HICLAW_ADMIN_USER}\"'\",\n \"password\": \"'\"${HICLAW_ADMIN_PASSWORD}\"'\",\n \"auth\": {\n \"type\": \"m.login.registration_token\",\n \"token\": \"'\"${HICLAW_REGISTRATION_TOKEN}\"'\"\n }\n }' > /dev/null 2>&1 || log \"Admin account may already exist\"\n\nlog \"Registering Manager Agent Matrix account...\"\ncurl -sf -X POST ${HICLAW_MATRIX_SERVER}/_matrix/client/v3/register \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"username\": \"manager\",\n \"password\": \"'\"${HICLAW_MANAGER_PASSWORD}\"'\",\n \"auth\": {\n \"type\": \"m.login.registration_token\",\n \"token\": \"'\"${HICLAW_REGISTRATION_TOKEN}\"'\"\n }\n }' > /dev/null 2>&1 || log \"Manager account may already exist\"\n\n# Get Manager Agent's Matrix access token\nlog \"Obtaining Manager Matrix access token...\"\n_LOGIN_RESPONSE=$(curl -sf -X POST ${HICLAW_MATRIX_SERVER}/_matrix/client/v3/login \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"type\": \"m.login.password\",\n \"identifier\": {\"type\": \"m.id.user\", \"user\": \"manager\"},\n \"password\": \"'\"${HICLAW_MANAGER_PASSWORD}\"'\"\n }' 2>&1)\n_LOGIN_EXIT=$?\nlog \"Matrix login HTTP exit code: ${_LOGIN_EXIT}\"\nlog \"Matrix login response: ${_LOGIN_RESPONSE}\"\n\nMANAGER_TOKEN=$(echo \"${_LOGIN_RESPONSE}\" | jq -r '.access_token' 2>/dev/null)\n\nif [ -z \"${MANAGER_TOKEN}\" ] || [ \"${MANAGER_TOKEN}\" = \"null\" ]; then\n log \"ERROR: Failed to obtain Manager Matrix token (exit=${_LOGIN_EXIT})\"\n log \"ERROR: Login response was: ${_LOGIN_RESPONSE}\"\n exit 1\nfi\nlog \"Manager Matrix token obtained (token prefix: ${MANAGER_TOKEN:0:10}...)\"\n\n# ============================================================\n# Local mode: Initialize Higress Console + configure routes\n# Cloud mode: Create admin DM room + schedule welcome message\n# ============================================================\nif [ \"${HICLAW_RUNTIME}\" != \"aliyun\" ]; then\n COOKIE_FILE=\"/tmp/higress-session-cookie\"\n\n log \"Waiting for Higress Console to be fully ready and initializing admin...\"\n INIT_DONE=false\n for i in $(seq 1 90); do\n INIT_RESULT=$(curl -s -X POST http://127.0.0.1:8001/system/init \\\n -H 'Content-Type: application/json' \\\n -d '{\"adminUser\":{\"name\":\"'\"${HICLAW_ADMIN_USER}\"'\",\"password\":\"'\"${HICLAW_ADMIN_PASSWORD}\"'\",\"displayName\":\"'\"${HICLAW_ADMIN_USER}\"'\"}}' 2>/dev/null) || true\n if echo \"${INIT_RESULT}\" | grep -qE '\"success\":true|already.?init' 2>/dev/null; then\n INIT_DONE=true\n break\n fi\n if echo \"${INIT_RESULT}\" | grep -q '\"name\"' 2>/dev/null; then\n INIT_DONE=true\n break\n fi\n sleep 2\n done\n\n if [ \"${INIT_DONE}\" != \"true\" ]; then\n log \"ERROR: Higress Console did not become ready within 180s\"\n exit 1\n fi\n log \"Higress Console init done\"\n\n log \"Logging into Higress Console...\"\n LOGIN_OK=false\n for i in $(seq 1 10); do\n HTTP_CODE=$(curl -s -o /dev/null -w '%{http_code}' -X POST http://127.0.0.1:8001/session/login \\\n -H 'Content-Type: application/json' \\\n -c \"${COOKIE_FILE}\" \\\n -d '{\"username\":\"'\"${HICLAW_ADMIN_USER}\"'\",\"password\":\"'\"${HICLAW_ADMIN_PASSWORD}\"'\"}' 2>/dev/null) || true\n if { [ \"${HTTP_CODE}\" = \"200\" ] || [ \"${HTTP_CODE}\" = \"201\" ]; } && [ -f \"${COOKIE_FILE}\" ] && [ -s \"${COOKIE_FILE}\" ]; then\n LOGIN_OK=true\n break\n fi\n log \"Login attempt $i (HTTP ${HTTP_CODE}), retrying in 3s...\"\n sleep 3\n done\n\n if [ \"${LOGIN_OK}\" != \"true\" ]; then\n log \"ERROR: Could not login to Higress Console after retries\"\n exit 1\n fi\n log \"Higress Console login successful\"\n\n VERIFY_CODE=$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8001/v1/consumers -b \"${COOKIE_FILE}\" 2>/dev/null) || true\n if [ \"${VERIFY_CODE}\" = \"200\" ]; then\n log \"Console session verified (cookie valid)\"\n else\n log \"WARNING: Console session may be invalid (verify returned HTTP ${VERIFY_CODE})\"\n rm -f \"${COOKIE_FILE}\"\n for i in $(seq 1 5); do\n curl -s -o /dev/null -w '%{http_code}' -X POST http://127.0.0.1:8001/session/login \\\n -H 'Content-Type: application/json' \\\n -c \"${COOKIE_FILE}\" \\\n -d '{\"username\":\"'\"${HICLAW_ADMIN_USER}\"'\",\"password\":\"'\"${HICLAW_ADMIN_PASSWORD}\"'\"}' 2>/dev/null\n VERIFY2=$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8001/v1/consumers -b \"${COOKIE_FILE}\" 2>/dev/null) || true\n if [ \"${VERIFY2}\" = \"200\" ]; then\n log \"Re-login successful, session verified\"\n break\n fi\n sleep 2\n done\n fi\n\n export HIGRESS_COOKIE_FILE=\"${COOKIE_FILE}\"\n\n # Configure Higress routes, consumers, MCP servers\n /opt/hiclaw/scripts/init/setup-higress.sh\nfi\n\n# ============================================================\n# Create admin DM room, persist to state.json, send welcome message\n# Runs in both local and cloud modes (idempotent)\n# ============================================================\nMANAGER_FULL_ID=\"@manager:${MATRIX_DOMAIN}\"\nADMIN_FULL_ID=\"@${HICLAW_ADMIN_USER}:${MATRIX_DOMAIN}\"\n\nlog \"Logging in as admin to create DM room...\"\n_ADMIN_LOGIN=$(curl -sf -X POST \"${HICLAW_MATRIX_SERVER}/_matrix/client/v3/login\" \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"type\": \"m.login.password\",\n \"identifier\": {\"type\": \"m.id.user\", \"user\": \"'\"${HICLAW_ADMIN_USER}\"'\"},\n \"password\": \"'\"${HICLAW_ADMIN_PASSWORD}\"'\"\n }' 2>&1) || true\n\nADMIN_MATRIX_TOKEN=$(echo \"${_ADMIN_LOGIN}\" | jq -r '.access_token // empty' 2>/dev/null)\nif [ -z \"${ADMIN_MATRIX_TOKEN}\" ]; then\n log \"WARNING: Failed to login as admin, skipping DM room creation\"\nelse\n # Search for existing DM room with Manager (idempotent)\n DM_ROOM_ID=\"\"\n _JOINED_ROOMS=$(curl -sf \"${HICLAW_MATRIX_SERVER}/_matrix/client/v3/joined_rooms\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" 2>/dev/null \\\n | jq -r '.joined_rooms[]' 2>/dev/null) || true\n for _rid in ${_JOINED_ROOMS}; do\n _members=$(curl -sf \"${HICLAW_MATRIX_SERVER}/_matrix/client/v3/rooms/${_rid}/members\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" 2>/dev/null \\\n | jq -r '.chunk[].state_key' 2>/dev/null) || continue\n _count=$(echo \"${_members}\" | wc -l | xargs)\n if [ \"${_count}\" = \"2\" ] && echo \"${_members}\" | grep -q \"@manager:\"; then\n DM_ROOM_ID=\"${_rid}\"\n break\n fi\n done\n\n if [ -n \"${DM_ROOM_ID}\" ]; then\n log \"Existing DM room found: ${DM_ROOM_ID}\"\n else\n log \"Creating DM room with Manager...\"\n _RAW=$(curl -s -w '\\nHTTP_CODE:%{http_code}' -X POST \"${HICLAW_MATRIX_SERVER}/_matrix/client/v3/createRoom\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d \"{\\\"is_direct\\\":true,\\\"invite\\\":[\\\"${MANAGER_FULL_ID}\\\"],\\\"preset\\\":\\\"trusted_private_chat\\\"}\" 2>&1) || true\n _HTTP_CODE=$(echo \"${_RAW}\" | tail -1 | sed 's/HTTP_CODE://')\n _CREATE_RESP=$(echo \"${_RAW}\" | sed '$d')\n DM_ROOM_ID=$(echo \"${_CREATE_RESP}\" | jq -r '.room_id // empty' 2>/dev/null)\n if [ -n \"${DM_ROOM_ID}\" ]; then\n log \"DM room created: ${DM_ROOM_ID}\"\n else\n log \"WARNING: Failed to create DM room (HTTP ${_HTTP_CODE}): ${_CREATE_RESP}\"\n fi\n fi\n\n # Persist admin DM room ID to state.json\n if [ -n \"${DM_ROOM_ID}\" ]; then\n STATE_SCRIPT=\"/opt/hiclaw/agent/skills/task-management/scripts/manage-state.sh\"\n if [ -f \"${STATE_SCRIPT}\" ]; then\n bash \"${STATE_SCRIPT}\" --action init 2>/dev/null || true\n bash \"${STATE_SCRIPT}\" --action set-admin-dm --room-id \"${DM_ROOM_ID}\" 2>/dev/null || true\n log \"Admin DM room persisted to state.json: ${DM_ROOM_ID}\"\n fi\n fi\n\n # Schedule welcome message in background (only on first boot)\n if [ -n \"${DM_ROOM_ID}\" ] && [ ! -f \"/root/manager-workspace/soul-configured\" ]; then\n log \"Scheduling welcome message (background, waiting for OpenClaw to start)...\"\n (\n _HICLAW_LANGUAGE=\"${HICLAW_LANGUAGE:-zh}\"\n _HICLAW_TIMEZONE=\"${TZ:-Asia/Shanghai}\"\n _wait=0\n _ready=false\n while [ \"${_wait}\" -lt 300 ]; do\n if curl -sf http://127.0.0.1:18799/ > /dev/null 2>&1; then\n _ready=true\n break\n fi\n sleep 3\n _wait=$((_wait + 3))\n done\n if [ \"${_ready}\" != \"true\" ]; then\n echo \"[manager] WARNING: OpenClaw gateway not ready within 300s, skipping welcome message\"\n exit 0\n fi\n # Ensure Manager has joined the DM room before sending the welcome\n # message. Without this, there is a race between OpenClaw's Matrix\n # auto-join and the message send — the message may land before Manager\n # joins, so OpenClaw's /sync never picks it up.\n _join_ok=false\n for _join_attempt in 1 2 3; do\n if curl -sf -X POST \"${HICLAW_MATRIX_SERVER}/_matrix/client/v3/rooms/${DM_ROOM_ID}/join\" \\\n -H \"Authorization: Bearer ${MANAGER_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d '{}' > /dev/null 2>&1; then\n echo \"[manager] Manager joined DM room before welcome message\"\n _join_ok=true\n break\n fi\n sleep 2\n done\n if [ \"${_join_ok}\" != \"true\" ]; then\n echo \"[manager] WARNING: Manager join request failed after 3 attempts (may already be joined)\"\n fi\n _welcome_msg=\"This is an automated message from the HiClaw setup. This is a fresh installation.\n\n--- Installation Context ---\nUser Language: ${_HICLAW_LANGUAGE} (zh = Chinese, en = English)\nUser Timezone: ${_HICLAW_TIMEZONE} (IANA timezone identifier)\n---\n\nYou are an AI agent that manages a team of worker agents. Your identity and personality have not been configured yet — the human admin is about to meet you for the first time.\n\nPlease begin the onboarding conversation:\n\n1. Greet the admin warmly and briefly describe what you can do (coordinate workers, manage tasks, run multi-agent projects)\n2. The user has selected \\\"${_HICLAW_LANGUAGE}\\\" as their preferred language during installation. Use this language for your greeting and all subsequent communication.\n3. The user's timezone is ${_HICLAW_TIMEZONE}. Based on this timezone, you may infer their likely region and suggest additional language options.\n4. Ask them: a) What would they like to call you? b) Communication style preference? c) Any behavior guidelines? d) Confirm default language\n5. After they reply, write their preferences to ~/SOUL.md\n6. Confirm what you wrote, and ask if they would like to adjust anything\n7. Once confirmed, run: touch ~/soul-configured\n\nThe human admin will start chatting shortly.\"\n _txn_id=\"welcome-$(date +%s)\"\n _payload=$(jq -nc --arg body \"${_welcome_msg}\" '{\"msgtype\":\"m.text\",\"body\":$body}')\n _raw=$(curl -s -w '\\nHTTP_CODE:%{http_code}' -X PUT \"${HICLAW_MATRIX_SERVER}/_matrix/client/v3/rooms/${DM_ROOM_ID}/send/m.room.message/${_txn_id}\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d \"${_payload}\" 2>&1) || true\n _http_code=$(echo \"${_raw}\" | tail -1 | sed 's/HTTP_CODE://')\n _send_resp=$(echo \"${_raw}\" | sed '$d')\n if echo \"${_send_resp}\" | jq -e '.event_id' > /dev/null 2>&1; then\n echo \"[manager] Welcome message sent to DM room\"\n else\n echo \"[manager] WARNING: Failed to send welcome message (HTTP ${_http_code}): ${_send_resp}\"\n fi\n ) &\n log \"Welcome message background process started (PID: $!)\"\n fi\nfi\n\n# ============================================================\n# Generate Manager Agent openclaw.json from template\n# ============================================================\nlog \"Generating Manager openclaw.json...\"\nexport MANAGER_MATRIX_TOKEN=\"${MANAGER_TOKEN}\"\nexport MANAGER_GATEWAY_KEY=\"${HICLAW_MANAGER_GATEWAY_KEY}\"\n# Resolve model parameters based on model name\nMODEL_NAME=\"${HICLAW_DEFAULT_MODEL:-qwen3.5-plus}\"\ncase \"${MODEL_NAME}\" in\n gpt-5.3-codex|gpt-5-mini|gpt-5-nano)\n export MODEL_CONTEXT_WINDOW=400000 MODEL_MAX_TOKENS=128000 ;;\n claude-opus-4-6)\n export MODEL_CONTEXT_WINDOW=1000000 MODEL_MAX_TOKENS=128000 ;;\n claude-sonnet-4-6)\n export MODEL_CONTEXT_WINDOW=1000000 MODEL_MAX_TOKENS=64000 ;;\n claude-haiku-4-5)\n export MODEL_CONTEXT_WINDOW=200000 MODEL_MAX_TOKENS=64000 ;;\n qwen3.5-plus)\n export MODEL_CONTEXT_WINDOW=200000 MODEL_MAX_TOKENS=64000 ;;\n deepseek-chat|deepseek-reasoner|kimi-k2.5)\n export MODEL_CONTEXT_WINDOW=256000 MODEL_MAX_TOKENS=128000 ;;\n glm-5|MiniMax-M2.7|MiniMax-M2.7-highspeed|MiniMax-M2.5)\n export MODEL_CONTEXT_WINDOW=200000 MODEL_MAX_TOKENS=128000 ;;\n *)\n export MODEL_CONTEXT_WINDOW=150000 MODEL_MAX_TOKENS=128000 ;;\nesac\nexport MODEL_REASONING=true\n\n# Override with user-supplied custom model parameters from env (set during install)\n[ -n \"${HICLAW_MODEL_CONTEXT_WINDOW:-}\" ] && export MODEL_CONTEXT_WINDOW=\"${HICLAW_MODEL_CONTEXT_WINDOW}\"\n[ -n \"${HICLAW_MODEL_MAX_TOKENS:-}\" ] && export MODEL_MAX_TOKENS=\"${HICLAW_MODEL_MAX_TOKENS}\"\n[ -n \"${HICLAW_MODEL_REASONING:-}\" ] && export MODEL_REASONING=\"${HICLAW_MODEL_REASONING}\"\n\n# E2EE: convert HICLAW_MATRIX_E2EE to JSON boolean for template substitution\nif [ \"${HICLAW_MATRIX_E2EE:-0}\" = \"1\" ] || [ \"${HICLAW_MATRIX_E2EE:-}\" = \"true\" ]; then\n export MATRIX_E2EE_ENABLED=true\nelse\n export MATRIX_E2EE_ENABLED=false\nfi\nlog \"Matrix E2EE: ${MATRIX_E2EE_ENABLED}\"\n\n# Resolve input modalities: only vision-capable models get \"image\"\ncase \"${MODEL_NAME}\" in\n gpt-5.4|gpt-5.3-codex|gpt-5-mini|gpt-5-nano|claude-opus-4-6|claude-sonnet-4-6|claude-haiku-4-5|qwen3.5-plus|kimi-k2.5)\n export MODEL_INPUT='[\"text\", \"image\"]' ;;\n *)\n export MODEL_INPUT='[\"text\"]' ;;\nesac\n# Override with user-supplied vision setting from env\nif [ \"${HICLAW_MODEL_VISION:-}\" = \"true\" ]; then\n export MODEL_INPUT='[\"text\", \"image\"]'\nelif [ \"${HICLAW_MODEL_VISION:-}\" = \"false\" ]; then\n export MODEL_INPUT='[\"text\"]'\nfi\n\nlog \"Model: ${MODEL_NAME} (context=${MODEL_CONTEXT_WINDOW}, maxTokens=${MODEL_MAX_TOKENS}, reasoning=${MODEL_REASONING}, input=${MODEL_INPUT})\"\n\nif [ -f /root/manager-workspace/openclaw.json ]; then\n log \"Manager openclaw.json already exists, updating dynamic fields only (preserving user customizations)...\"\n # Merge known models into existing config (add missing, preserve user-added)\n # Use known-models.json (valid JSON) instead of template (contains ${VAR} placeholders)\n KNOWN_MODELS=$(cat /opt/hiclaw/configs/known-models.json 2>/dev/null || echo '[]')\n jq --arg token \"${MANAGER_TOKEN}\" \\\n --arg key \"${HICLAW_MANAGER_GATEWAY_KEY}\" \\\n --arg model \"${MODEL_NAME}\" \\\n --arg emb_model \"${HICLAW_EMBEDDING_MODEL}\" \\\n --arg aigw_domain \"${AI_GATEWAY_DOMAIN}\" \\\n --argjson e2ee \"${MATRIX_E2EE_ENABLED}\" \\\n --argjson known_models \"${KNOWN_MODELS}\" \\\n --argjson ctx \"${MODEL_CONTEXT_WINDOW}\" \\\n --argjson max \"${MODEL_MAX_TOKENS}\" \\\n --argjson reasoning \"${MODEL_REASONING}\" \\\n --argjson input \"${MODEL_INPUT}\" \\\n '\n # Merge known models: add any model id not already present\n .models.providers[\"hiclaw-gateway\"].models as $existing\n | ($existing | map(.id)) as $existing_ids\n | ($known_models | map(select(.id as $id | $existing_ids | index($id) | not))) as $new\n | .models.providers[\"hiclaw-gateway\"].models = ($existing + $new)\n # Ensure the user-chosen default model is in the list (custom model support)\n | if (.models.providers[\"hiclaw-gateway\"].models | map(.id) | index($model) | not) then\n .models.providers[\"hiclaw-gateway\"].models += [{\"id\": $model, \"name\": $model, \"reasoning\": $reasoning, \"contextWindow\": $ctx, \"maxTokens\": $max, \"input\": $input}]\n else . end\n # Rebuild model aliases from the full models list\n | (.models.providers[\"hiclaw-gateway\"].models | map({ (\"hiclaw-gateway/\" + .id): { \"alias\": .id } }) | add // {}) as $aliases\n | .agents.defaults.models = ((.agents.defaults.models // {}) + $aliases)\n | .channels.matrix.accessToken = $token | .models.providers[\"hiclaw-gateway\"].apiKey = $key\n | ((.hooks.token // \"\") as $ht | if $ht == $key or $ht == ($key + \"-hooks\" | @base64) then del(.hooks) else . end)\n | .agents.defaults.model.primary = (\"hiclaw-gateway/\" + $model)\n | .commands.restart = true\n | .gateway.controlUi.dangerouslyDisableDeviceAuth = true\n | .channels.matrix.encryption = $e2ee\n # Ensure memorySearch config exists (embedding model for memory) — skip if embedding model is empty\n | if $emb_model != \"\" then .agents.defaults.memorySearch //= {\"provider\":\"openai\",\"model\":$emb_model,\"remote\":{\"baseUrl\":(\"http://\" + $aigw_domain + \":8080/v1\"),\"apiKey\":$key}} else . end\n ' \\\n /root/manager-workspace/openclaw.json > /tmp/openclaw.json.tmp && \\\n mv /tmp/openclaw.json.tmp /root/manager-workspace/openclaw.json\n # Verify the token was written correctly\n _written_token=$(jq -r '.channels.matrix.accessToken' /root/manager-workspace/openclaw.json 2>/dev/null)\n if [ -z \"${_written_token}\" ] || [ \"${_written_token}\" = \"null\" ]; then\n log \"ERROR: Matrix token was not written correctly to openclaw.json (got: ${_written_token})\"\n else\n log \"Matrix token written to openclaw.json (prefix: ${_written_token:0:10}...)\"\n fi\nelse\n log \"Manager openclaw.json not found, generating from template...\"\n envsubst < /opt/hiclaw/configs/manager-openclaw.json.tmpl > /root/manager-workspace/openclaw.json\n # Post-envsubst injection: memorySearch + custom model (single jq pass when possible)\n if ! jq -e --arg model \"${MODEL_NAME}\" '.models.providers[\"hiclaw-gateway\"].models | map(.id) | index($model)' /root/manager-workspace/openclaw.json > /dev/null 2>&1; then\n log \"Custom model '${MODEL_NAME}' not in built-in list, injecting into config...\"\n jq --arg emb_model \"${HICLAW_EMBEDDING_MODEL}\" \\\n --arg aigw_domain \"${AI_GATEWAY_DOMAIN}\" \\\n --arg key \"${HICLAW_MANAGER_GATEWAY_KEY}\" \\\n --arg model \"${MODEL_NAME}\" \\\n --argjson ctx \"${MODEL_CONTEXT_WINDOW}\" \\\n --argjson max \"${MODEL_MAX_TOKENS}\" \\\n --argjson reasoning \"${MODEL_REASONING}\" \\\n --argjson input \"${MODEL_INPUT}\" \\\n '\n (if $emb_model != \"\" then .agents.defaults.memorySearch = {\"provider\":\"openai\",\"model\":$emb_model,\"remote\":{\"baseUrl\":(\"http://\" + $aigw_domain + \":8080/v1\"),\"apiKey\":$key}} else . end)\n | .models.providers[\"hiclaw-gateway\"].models += [{\"id\": $model, \"name\": $model, \"reasoning\": $reasoning, \"contextWindow\": $ctx, \"maxTokens\": $max, \"input\": $input}]\n | .agents.defaults.models += {(\"hiclaw-gateway/\" + $model): {\"alias\": $model}}\n ' /root/manager-workspace/openclaw.json > /tmp/openclaw.json.tmp && \\\n mv /tmp/openclaw.json.tmp /root/manager-workspace/openclaw.json\n elif [ -n \"${HICLAW_EMBEDDING_MODEL}\" ]; then\n jq --arg emb_model \"${HICLAW_EMBEDDING_MODEL}\" \\\n --arg aigw_domain \"${AI_GATEWAY_DOMAIN}\" \\\n --arg key \"${HICLAW_MANAGER_GATEWAY_KEY}\" \\\n '.agents.defaults.memorySearch = {\"provider\":\"openai\",\"model\":$emb_model,\"remote\":{\"baseUrl\":(\"http://\" + $aigw_domain + \":8080/v1\"),\"apiKey\":$key}}' \\\n /root/manager-workspace/openclaw.json > /tmp/openclaw.json.tmp && \\\n mv /tmp/openclaw.json.tmp /root/manager-workspace/openclaw.json\n fi\n _written_token=$(jq -r '.channels.matrix.accessToken' /root/manager-workspace/openclaw.json 2>/dev/null)\n log \"Matrix token written from template (prefix: ${_written_token:0:10}...)\"\nfi\n\n# Cloud mode: overlay cloud-specific settings onto generated config\nif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n log \"Applying cloud overlay to openclaw.json...\"\n jq --arg homeserver \"${HICLAW_MATRIX_SERVER}\" \\\n --arg gateway \"${HICLAW_AI_GATEWAY_URL}/v1\" \\\n --arg key \"${HICLAW_MANAGER_GATEWAY_KEY}\" \\\n '.channels.matrix.homeserver = $homeserver\n | .models.providers[\"hiclaw-gateway\"].baseUrl = $gateway\n | .models.providers[\"hiclaw-gateway\"].apiKey = $key\n | ((.hooks.token // \"\") as $ht | if $ht == $key or $ht == ($key + \"-hooks\" | @base64) then del(.hooks) else . end)\n | .commands.restart = false\n | if .agents.defaults.memorySearch then .agents.defaults.memorySearch.remote.baseUrl = $gateway | .agents.defaults.memorySearch.remote.apiKey = $key else . end' \\\n /root/manager-workspace/openclaw.json > /tmp/openclaw-cloud.json && \\\n mv /tmp/openclaw-cloud.json /root/manager-workspace/openclaw.json\n log \"Cloud overlay applied\"\nfi\n\n# ============================================================\n# Optional: enable openclaw-cms-plugin observability\n# Config is applied at runtime so secrets stay out of image layers.\n# ============================================================\nCMS_TRACES_ENABLED=\"$(echo \"${HICLAW_CMS_TRACES_ENABLED:-false}\" | tr '[:upper:]' '[:lower:]')\"\nif [ \"${CMS_TRACES_ENABLED}\" = \"true\" ]; then\n CMS_PLUGIN_NAME=\"openclaw-cms-plugin\"\n CMS_PLUGIN_DIR=\"${OPENCLAW_CMS_PLUGIN_DIR:-/opt/openclaw/extensions/openclaw-cms-plugin}\"\n CMS_PLUGIN_MANIFEST=\"${CMS_PLUGIN_DIR}/openclaw.plugin.json\"\n DIAG_PLUGIN_NAME=\"diagnostics-otel\"\n DIAG_PLUGIN_DIR=\"/opt/openclaw/extensions/diagnostics-otel\"\n CMS_LICENSE_KEY=\"${HICLAW_CMS_LICENSE_KEY:-}\"\n CMS_PROJECT=\"${HICLAW_CMS_PROJECT:-}\"\n CMS_METRICS_ENABLED=\"${HICLAW_CMS_METRICS_ENABLED:-false}\"\n\n if [ ! -f \"${CMS_PLUGIN_MANIFEST}\" ]; then\n log \"WARNING: ${CMS_PLUGIN_NAME} manifest not found at ${CMS_PLUGIN_MANIFEST}, skipping plugin config\"\n else\n _missing=0\n [ -z \"${HICLAW_CMS_ENDPOINT:-}\" ] && log \"WARNING: HICLAW_CMS_ENDPOINT is required when HICLAW_CMS_TRACES_ENABLED=true\" && _missing=1\n [ -z \"${CMS_LICENSE_KEY:-}\" ] && log \"WARNING: HICLAW_CMS_LICENSE_KEY is required when HICLAW_CMS_TRACES_ENABLED=true\" && _missing=1\n [ -z \"${HICLAW_CMS_WORKSPACE:-}\" ] && log \"WARNING: HICLAW_CMS_WORKSPACE is required when HICLAW_CMS_TRACES_ENABLED=true\" && _missing=1\n\n if [ \"${_missing}\" = \"0\" ]; then\n CMS_SERVICE_NAME=\"${HICLAW_CMS_SERVICE_NAME:-hiclaw-manager}\"\n CMS_ENABLE_METRICS=\"${CMS_METRICS_ENABLED}\"\n DIAG_AVAILABLE=\"0\"\n _metrics_lc=\"$(echo \"${CMS_ENABLE_METRICS}\" | tr '[:upper:]' '[:lower:]')\"\n if [ \"${_metrics_lc}\" = \"true\" ]; then\n if [ -f \"${DIAG_PLUGIN_DIR}/package.json\" ]; then\n DIAG_AVAILABLE=\"1\"\n if [ ! -d \"${DIAG_PLUGIN_DIR}/node_modules\" ]; then\n log \"diagnostics-otel dependencies missing, installing...\"\n if (cd \"${DIAG_PLUGIN_DIR}\" && npm install --omit=dev --ignore-scripts >/tmp/hiclaw-diag-install.log 2>&1); then\n log \"diagnostics-otel dependencies installed\"\n else\n log \"WARNING: diagnostics-otel npm install failed, metrics plugin may not load\"\n fi\n else\n log \"diagnostics-otel dependencies already present\"\n fi\n else\n log \"WARNING: diagnostics-otel package.json not found at ${DIAG_PLUGIN_DIR}, metrics plugin may not load\"\n fi\n fi\n\n log \"Applying ${CMS_PLUGIN_NAME} config to openclaw.json...\"\n jq --arg pluginName \"${CMS_PLUGIN_NAME}\" \\\n --arg pluginDir \"${CMS_PLUGIN_DIR}\" \\\n --arg endpoint \"${HICLAW_CMS_ENDPOINT}\" \\\n --arg licenseKey \"${CMS_LICENSE_KEY}\" \\\n --arg armsProject \"${CMS_PROJECT}\" \\\n --arg cmsWorkspace \"${HICLAW_CMS_WORKSPACE}\" \\\n --arg serviceName \"${CMS_SERVICE_NAME}\" \\\n --arg diagPluginName \"${DIAG_PLUGIN_NAME}\" \\\n --arg diagPluginDir \"${DIAG_PLUGIN_DIR}\" \\\n --arg metricsRaw \"${CMS_ENABLE_METRICS}\" \\\n --arg diagAvailableRaw \"${DIAG_AVAILABLE}\" \\\n '\n .plugins = (.plugins // {})\n | .plugins.load = (.plugins.load // {})\n | .plugins.entries = (.plugins.entries // {})\n | if (.plugins.allow | type) != \"array\" then .plugins.allow = [] else . end\n | if (.plugins.allow | index($pluginName)) == null then .plugins.allow += [$pluginName] else . end\n | if (.plugins.load.paths | type) != \"array\" then .plugins.load.paths = [] else . end\n | if (.plugins.load.paths | index($pluginDir)) == null then .plugins.load.paths += [$pluginDir] else . end\n | .plugins.entries[$pluginName] = {\n \"enabled\": true,\n \"config\": {\n \"endpoint\": $endpoint,\n \"headers\": {\n \"x-arms-license-key\": $licenseKey,\n \"x-arms-project\": $armsProject,\n \"x-cms-workspace\": $cmsWorkspace\n },\n \"serviceName\": $serviceName\n }\n }\n\n # diagnostics-otel metrics (optional)\n | ($metricsRaw | ascii_downcase) as $m\n | ($diagAvailableRaw == \"1\") as $diagAvailable\n | (($m == \"true\") and $diagAvailable) as $metricsEnabled\n | if $metricsEnabled then\n (if (.plugins.allow | index($diagPluginName)) == null then .plugins.allow += [$diagPluginName] else . end)\n | (if (.plugins.load.paths | index($diagPluginDir)) == null then .plugins.load.paths += [$diagPluginDir] else . end)\n | .plugins.entries[$diagPluginName].enabled = true\n | .diagnostics = (.diagnostics // {})\n | .diagnostics.otel = (.diagnostics.otel // {})\n | .diagnostics.enabled = true\n | .diagnostics.otel.enabled = true\n | .diagnostics.otel.endpoint = $endpoint\n | .diagnostics.otel.protocol = (.diagnostics.otel.protocol // \"http/protobuf\")\n | .diagnostics.otel.headers = {\n \"x-arms-license-key\": $licenseKey,\n \"x-arms-project\": $armsProject,\n \"x-cms-workspace\": $cmsWorkspace\n }\n | .diagnostics.otel.serviceName = $serviceName\n | .diagnostics.otel.metrics = true\n | .diagnostics.otel.traces = (.diagnostics.otel.traces // false)\n | .diagnostics.otel.logs = (.diagnostics.otel.logs // false)\n else\n .\n end\n ' /root/manager-workspace/openclaw.json > /tmp/openclaw-cms.json && \\\n mv /tmp/openclaw-cms.json /root/manager-workspace/openclaw.json\n log \"${CMS_PLUGIN_NAME} config applied (metrics=${CMS_ENABLE_METRICS}, service=${CMS_SERVICE_NAME})\"\n else\n log \"Skipping ${CMS_PLUGIN_NAME} config due to missing required env vars\"\n fi\n fi\nfi\n\n# ============================================================\n# Detect container runtime (for Worker creation)\n# ============================================================\nsource /opt/hiclaw/scripts/lib/container-api.sh\nif container_api_available; then\n log \"Container runtime socket detected at ${CONTAINER_SOCKET} — direct Worker creation enabled\"\n export HICLAW_CONTAINER_RUNTIME=\"socket\"\nelif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n log \"Cloud mode — Workers created via SAE API\"\n export HICLAW_CONTAINER_RUNTIME=\"cloud\"\nelse\n log \"No container runtime found — Worker creation will output install commands\"\n export HICLAW_CONTAINER_RUNTIME=\"none\"\nfi\n\n# ============================================================\n# Upgrade Worker openclaw.json: merge known models + E2EE flag into existing configs\n# Existing workers in MinIO may have old single-model configs or missing encryption field.\n# Merge template models so they can hot-switch without restart.\n# ============================================================\nREGISTRY_FILE=\"/root/manager-workspace/workers-registry.json\"\nif [ -f \"${REGISTRY_FILE}\" ]; then\n # Use known-models.json (valid JSON) instead of template (contains ${VAR} placeholders)\n KNOWN_MODELS_FILE=\"/opt/hiclaw/configs/known-models.json\"\n if [ -f \"${KNOWN_MODELS_FILE}\" ]; then\n _KNOWN_MODELS=$(cat \"${KNOWN_MODELS_FILE}\")\n for _wname in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_wname}\" ] && continue\n _minio_path=\"${HICLAW_STORAGE_PREFIX}/agents/${_wname}/openclaw.json\"\n _tmp_in=\"/tmp/openclaw-${_wname}-models-upgrade-in.json\"\n if mc cp \"${_minio_path}\" \"${_tmp_in}\" 2>/dev/null; then\n _tmp_out=\"/tmp/openclaw-${_wname}-models-upgrade-out.json\"\n # Idempotent merge: add missing known models, rebuild aliases, set e2ee.\n # Always runs — jq deduplicates by model id, so re-runs are safe.\n jq --argjson known_models \"${_KNOWN_MODELS}\" \\\n --argjson e2ee \"${MATRIX_E2EE_ENABLED}\" '\n .models.providers[\"hiclaw-gateway\"].models as $existing\n | ($existing | map(.id)) as $existing_ids\n | ($known_models | map(select(.id as $id | $existing_ids | index($id) | not))) as $new\n | .models.providers[\"hiclaw-gateway\"].models = ($existing + $new)\n | (.models.providers[\"hiclaw-gateway\"].models | map({ (\"hiclaw-gateway/\" + .id): { \"alias\": .id } }) | add // {}) as $aliases\n | .agents.defaults.models = ((.agents.defaults.models // {}) + $aliases)\n | .channels.matrix.encryption = $e2ee\n ' \"${_tmp_in}\" > \"${_tmp_out}\" 2>/dev/null\n if ! diff -q \"${_tmp_in}\" \"${_tmp_out}\" > /dev/null 2>&1; then\n if mc cp \"${_tmp_out}\" \"${_minio_path}\" 2>/dev/null; then\n _new_count=$(jq '.models.providers[\"hiclaw-gateway\"].models | length' \"${_tmp_out}\" 2>/dev/null)\n log \"Worker ${_wname}: upgraded openclaw.json (models: ${_new_count}, e2ee: ${MATRIX_E2EE_ENABLED})\"\n fi\n fi\n rm -f \"${_tmp_in}\" \"${_tmp_out}\"\n fi\n done\n fi\nfi\n\n# ============================================================\n# Ensure Worker Matrix password files exist in MinIO (E2EE fix)\n# Workers need to re-login on restart to get a fresh device_id.\n# Older workers created before this fix won't have the password file.\n# ============================================================\nif [ -f \"${REGISTRY_FILE}\" ]; then\n for _wname in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_wname}\" ] && continue\n _creds_file=\"/data/worker-creds/${_wname}.env\"\n if [ -f \"${_creds_file}\" ]; then\n # Check if password file already exists in MinIO\n if ! mc stat \"${HICLAW_STORAGE_PREFIX}/agents/${_wname}/credentials/matrix/password\" > /dev/null 2>&1; then\n source \"${_creds_file}\"\n if [ -n \"${WORKER_PASSWORD}\" ]; then\n _tmp_pw=\"/tmp/matrix-pw-${_wname}\"\n echo -n \"${WORKER_PASSWORD}\" > \"${_tmp_pw}\"\n mc cp \"${_tmp_pw}\" \"${HICLAW_STORAGE_PREFIX}/agents/${_wname}/credentials/matrix/password\" 2>/dev/null \\\n && log \"Worker ${_wname}: wrote Matrix password to MinIO (E2EE re-login fix)\" \\\n || log \"Worker ${_wname}: WARNING: failed to write Matrix password to MinIO\"\n rm -f \"${_tmp_pw}\"\n fi\n fi\n fi\n done\nfi\n\n# ============================================================\n# Recreate Worker containers as needed after Manager restart.\n# Workers are on hiclaw-net; Docker DNS resolves *-local.hiclaw.io via\n# the Manager's network aliases, so IP changes don't require worker recreation.\n# Only recreate stopped/missing workers.\n# ============================================================\nif container_api_available; then\n REGISTRY_FILE=\"/root/manager-workspace/workers-registry.json\"\n if [ -f \"${REGISTRY_FILE}\" ]; then\n for _worker_name in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_worker_name}\" ] && continue\n\n # Skip remote workers — they are not Manager-managed containers.\n _deployment=$(jq -r --arg w \"${_worker_name}\" '.workers[$w].deployment // \"local\"' \"${REGISTRY_FILE}\" 2>/dev/null)\n if [ \"${_deployment}\" = \"remote\" ]; then\n log \"Worker ${_worker_name} is remote, skipping container recreate\"\n continue\n fi\n\n _status=$(container_status_worker \"${_worker_name}\")\n if [ \"${_status}\" = \"running\" ]; then\n log \"Worker running: ${_worker_name}, skipping\"\n continue\n fi\n # Container missing or stopped — recreate.\n log \"Worker container ${_status}: ${_worker_name}, recreating...\"\n _creds_file=\"/data/worker-creds/${_worker_name}.env\"\n if [ -f \"${_creds_file}\" ]; then\n source \"${_creds_file}\"\n _runtime=$(jq -r --arg w \"${_worker_name}\" '.workers[$w].runtime // \"openclaw\"' \"${REGISTRY_FILE}\" 2>/dev/null)\n _recreated=false\n for _attempt in 1 2 3; do\n if [ \"${_runtime}\" = \"copaw\" ]; then\n container_create_copaw_worker \"${_worker_name}\" \"${_worker_name}\" \"${WORKER_MINIO_PASSWORD}\" 2>&1 && _recreated=true && break\n else\n container_create_worker \"${_worker_name}\" \"${_worker_name}\" \"${WORKER_MINIO_PASSWORD}\" 2>&1 && _recreated=true && break\n fi\n log \" Attempt ${_attempt}/3 failed for ${_worker_name}, retrying in $((5 * _attempt))s...\"\n sleep $((5 * _attempt))\n done\n if [ \"${_recreated}\" = true ]; then\n log \" Recreated ${_runtime} worker: ${_worker_name}\"\n else\n log \" ERROR: Failed to recreate ${_worker_name} after 3 attempts\"\n fi\n else\n log \" WARNING: No credentials found for ${_worker_name} (${_creds_file} missing), skipping\"\n fi\n done\n fi\nfi\n\n# ============================================================\n# Notify workers of builtin updates if upgrade happened\n# Builtin files (AGENTS.md, skills) are already synced by upgrade-builtins.sh\n#\n# Cooldown: skip notification if the last successful notify was within\n# NOTIFY_COOLDOWN_SECS (default 3600s / 1 hour). This prevents repeated\n# notifications when the Manager crash-loops and re-runs upgrade-builtins\n# on every restart (e.g. IMAGE_VERSION=latest always triggers upgrade).\n# ============================================================\nNOTIFY_COOLDOWN_SECS=\"${HICLAW_NOTIFY_COOLDOWN_SECS:-3600}\"\nNOTIFY_TS_FILE=\"/root/manager-workspace/.last-worker-notify-ts\"\n\nif [ -f /root/manager-workspace/.upgrade-pending-worker-notify ]; then\n _now=$(date +%s)\n _last_notify=$(cat \"${NOTIFY_TS_FILE}\" 2>/dev/null || echo \"0\")\n _elapsed=$(( _now - _last_notify ))\n\n if [ \"${_elapsed}\" -lt \"${NOTIFY_COOLDOWN_SECS}\" ]; then\n log \"Skipping worker builtin notification (last notify ${_elapsed}s ago, cooldown ${NOTIFY_COOLDOWN_SECS}s)\"\n rm -f /root/manager-workspace/.upgrade-pending-worker-notify\n else\n log \"Notifying workers about builtin updates...\"\n REGISTRY_FILE=\"/root/manager-workspace/workers-registry.json\"\n _notify_ok=false\n if [ -f \"${REGISTRY_FILE}\" ]; then\n for _worker_name in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_worker_name}\" ] && continue\n _room_id=$(jq -r --arg w \"${_worker_name}\" '.workers[$w].room_id // empty' \"${REGISTRY_FILE}\" 2>/dev/null)\n if [ -n \"${_room_id}\" ]; then\n _worker_id=\"@${_worker_name}:${MATRIX_DOMAIN}\"\n _txn_id=\"upgrade-$(date +%s%N)\"\n _msg=\"@${_worker_name}:${MATRIX_DOMAIN} Manager upgraded builtin files (AGENTS.md, skills). Please use your file-sync skill to sync the latest config.\"\n _raw=$(curl -s -w '\\nHTTP_CODE:%{http_code}' -X PUT \\\n \"${HICLAW_MATRIX_SERVER}/_matrix/client/v3/rooms/${_room_id}/send/m.room.message/${_txn_id}\" \\\n -H \"Authorization: Bearer ${MANAGER_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d \"{\\\"msgtype\\\":\\\"m.text\\\",\\\"body\\\":\\\"${_msg}\\\",\\\"m.mentions\\\":{\\\"user_ids\\\":[\\\"${_worker_id}\\\"]}}\" \\\n 2>&1) || true\n _http_code=$(echo \"${_raw}\" | tail -1 | sed 's/HTTP_CODE://')\n _notify_resp=$(echo \"${_raw}\" | sed '$d')\n if echo \"${_notify_resp}\" | jq -e '.event_id' > /dev/null 2>&1; then\n log \" Notified ${_worker_name}\"; _notify_ok=true\n else\n log \" WARNING: Failed to notify ${_worker_name} (HTTP ${_http_code}): ${_notify_resp}\"\n fi\n fi\n done\n fi\n # Record timestamp only if at least one notification succeeded\n if [ \"${_notify_ok}\" = true ]; then\n echo \"${_now}\" > \"${NOTIFY_TS_FILE}\"\n fi\n rm -f /root/manager-workspace/.upgrade-pending-worker-notify\n fi\nfi\n\n# ============================================================\n# Start Manager Agent\n# ============================================================\nlog \"Starting Manager Agent (${MANAGER_RUNTIME})...\"\n\n# HOME is already set to /root/manager-workspace via docker run -e HOME=...\ncd \"${HOME}\"\n\n# Ensure host credential symlinks exist under HOME\nif [ -d \"/host-share\" ]; then\n [ -f \"/host-share/.gitconfig\" ] && ln -sf \"/host-share/.gitconfig\" \"${HOME}/.gitconfig\"\nfi\n\nlog \"HOME=${HOME} (manager-workspace, host-mounted)\"\n\n# ── Render agent doc templates ────────────────────────────────────────────\n# Replace ${VAR} placeholders with actual values so the AI agent reads\n# plain text and never needs to resolve environment variables.\nexport MANAGER_MATRIX_TOKEN MANAGER_TOKEN HIGRESS_COOKIE_FILE\nRENDER=/opt/hiclaw/scripts/lib/render-skills.sh\nlog \"Rendering agent doc templates...\"\n# Manager-owned docs (workspace)\nbash \"$RENDER\" /root/manager-workspace/skills\nbash \"$RENDER\" /root/manager-workspace/skills-alpha\nbash \"$RENDER\" /root/manager-workspace AGENTS.md TOOLS.md HEARTBEAT.md SOUL.md\n# Worker templates (workspace + image) — rendered before push to MinIO\n# so Workers (including remote pip-install) receive plain text\nbash \"$RENDER\" /root/manager-workspace/worker-skills\nbash \"$RENDER\" /root/manager-workspace/worker-agent\nbash \"$RENDER\" /root/manager-workspace/copaw-worker-agent\nbash \"$RENDER\" /opt/hiclaw/agent/worker-skills\nbash \"$RENDER\" /opt/hiclaw/agent/worker-agent\nbash \"$RENDER\" /opt/hiclaw/agent/copaw-worker-agent\nlog \"Agent doc templates rendered\"\n\n# Cloud mode: start background file sync (workspace ↔ OSS) and initial push\nif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n log \"Syncing initial workspace to OSS...\"\n ensure_mc_credentials\n mc mirror /root/manager-workspace/ \"${HICLAW_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" 2>/dev/null || true\n\n # Local → OSS: change-triggered sync\n (\n while true; do\n CHANGED=$(find /root/manager-workspace/ -type f -newermt \"15 seconds ago\" 2>/dev/null | head -1)\n if [ -n \"${CHANGED}\" ]; then\n ensure_mc_credentials 2>/dev/null || true\n mc mirror /root/manager-workspace/ \"${HICLAW_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" --exclude \".npm/**\" \\\n --exclude \".local/**\" --exclude \".mc/**\" 2>/dev/null || true\n fi\n sleep 10\n done\n ) &\n log \"Local→OSS sync started (PID: $!)\"\n\n # OSS → Local: periodic pull (shared data, agent configs)\n (\n while true; do\n sleep 300\n ensure_mc_credentials 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/shared/\" /root/hiclaw-fs/shared/ --overwrite --newer-than \"5m\" 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/agents/\" /root/hiclaw-fs/agents/ --overwrite --newer-than \"5m\" 2>/dev/null || true\n done\n ) &\n log \"OSS→Local sync started (every 5m, PID: $!)\"\nfi\n\n# ============================================================\n# Auto-generate Manager mcporter config for pre-configured MCP servers\n# If HICLAW_GITHUB_TOKEN was set at install time, setup-higress.sh already\n# configured GitHub MCP on Higress. Run setup-mcp-server.sh now so that\n# config/mcporter.json exists when the Agent starts — no need to ask user for PAT.\n# ============================================================\nif [ -n \"${HICLAW_GITHUB_TOKEN}\" ] && [ \"${HICLAW_RUNTIME}\" != \"aliyun\" ]; then\n if [ ! -f \"${HOME}/config/mcporter.json\" ]; then\n log \"Auto-generating Manager mcporter config for GitHub MCP (HICLAW_GITHUB_TOKEN set)...\"\n bash /opt/hiclaw/agent/skills/mcp-server-management/scripts/setup-mcp-server.sh \\\n github \"${HICLAW_GITHUB_TOKEN}\" 2>&1 | while IFS= read -r line; do log \" [setup-mcp] ${line}\"; done || \\\n log \"WARNING: setup-mcp-server.sh failed — Agent may need to configure GitHub MCP manually\"\n else\n log \"Manager mcporter config already exists, skipping auto-generate\"\n fi\nfi\n\n# ============================================================\n# Runtime-specific startup\n# ============================================================\nif [ \"${MANAGER_RUNTIME}\" = \"copaw\" ]; then\n # Delegate to CoPaw startup script\n exec /opt/hiclaw/scripts/init/start-copaw-manager.sh\nelse\n # ── OpenClaw Runtime ─────────────────────────────────────────────────────\n log \"Starting OpenClaw Manager...\"\n\n export OPENCLAW_CONFIG_PATH=\"/root/manager-workspace/openclaw.json\"\n\n # Symlink to default OpenClaw config path so CLI commands find the config\n mkdir -p \"${HOME}/.openclaw\"\n ln -sf \"/root/manager-workspace/openclaw.json\" \"${HOME}/.openclaw/openclaw.json\"\n\n # Clean orphaned session write locks (e.g. from SIGKILL or crash before exit handlers)\n # Prevents \"session file locked (timeout 10000ms)\" when PID was reused\n find \"${HOME}/.openclaw/agents\" -name \"*.jsonl.lock\" -delete 2>/dev/null || true\n log \"Cleaned up any orphaned session write locks\"\n\n # Clean Matrix crypto storage (SQLite WAL may be corrupted after unclean shutdown)\n # Crypto state is re-negotiated on startup; losing it only means re-establishing E2EE sessions\n rm -rf \"${HOME}/.openclaw/matrix\" 2>/dev/null || true\n log \"Cleaned Matrix crypto storage (will re-establish E2EE sessions)\"\n\n # Launch OpenClaw\n # Disable full-process respawn so the CLI uses its internal restart loop.\n # Without this, config reload spawns a detached child and exits, then\n # supervisord restarts the CLI — resulting in two gateway processes.\n export OPENCLAW_NO_RESPAWN=1\n exec openclaw gateway run --verbose --force\nfi\n"}, "077538de0cffef54b1c5cfcde541f995361134d6": {"t": 1785764763.7943976, "n": 52708, "h": "c3b0478432008ea6", "c": "#!/bin/bash\n# start-manager-agent.sh - Initialize and start the Manager Agent\n# Supports both local (supervisord) and cloud (SAE single-process) deployments.\n# In local mode this is the last supervisord component to start (priority 800).\n# In cloud mode (HICLAW_RUNTIME=aliyun) this is the container entrypoint.\n#\n# Runtime selection:\n# HICLAW_MANAGER_RUNTIME=openclaw (default) - OpenClaw gateway mode\n# HICLAW_MANAGER_RUNTIME=copaw - CoPaw workspace mode\n\nsource /opt/hiclaw/scripts/lib/hiclaw-env.sh\n\n# ============================================================\n# Runtime selection\n# ============================================================\nMANAGER_RUNTIME=\"${HICLAW_MANAGER_RUNTIME:-openclaw}\"\ncase \"${MANAGER_RUNTIME}\" in\n copaw)\n log \"Manager runtime: CoPaw (Python workspace)\"\n ;;\n *)\n log \"Manager runtime: OpenClaw (Node.js gateway)\"\n MANAGER_RUNTIME=\"openclaw\"\n ;;\nesac\n\n# ============================================================\n# Set timezone from TZ env var\n# ============================================================\nif [ -n \"${TZ}\" ] && [ -f \"/usr/share/zoneinfo/${TZ}\" ]; then\n ln -sf \"/usr/share/zoneinfo/${TZ}\" /etc/localtime\n echo \"${TZ}\" > /etc/timezone\n log \"Timezone set to ${TZ}\"\nfi\n\nexport MATRIX_DOMAIN=\"${HICLAW_MATRIX_DOMAIN:-matrix-local.hiclaw.io:8080}\"\nAI_GATEWAY_DOMAIN=\"${HICLAW_AI_GATEWAY_DOMAIN:-aigw-local.hiclaw.io}\"\n\n# ============================================================\n# Cloud mode: validate required environment variables + initial credentials\n# ============================================================\nif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n : \"${HICLAW_MATRIX_URL:?HICLAW_MATRIX_URL is required}\"\n : \"${HICLAW_MATRIX_DOMAIN:?HICLAW_MATRIX_DOMAIN is required}\"\n : \"${HICLAW_AI_GATEWAY_URL:?HICLAW_AI_GATEWAY_URL is required}\"\n : \"${HICLAW_MANAGER_GATEWAY_KEY:?HICLAW_MANAGER_GATEWAY_KEY is required}\"\n : \"${HICLAW_MANAGER_PASSWORD:?HICLAW_MANAGER_PASSWORD is required (cloud containers are stateless, password must be injected)}\"\n : \"${HICLAW_REGISTRATION_TOKEN:?HICLAW_REGISTRATION_TOKEN is required}\"\n : \"${HICLAW_ADMIN_USER:?HICLAW_ADMIN_USER is required}\"\n : \"${HICLAW_ADMIN_PASSWORD:?HICLAW_ADMIN_PASSWORD is required}\"\n log \"Cloud mode: validating environment... OK\"\n log \" Matrix: ${HICLAW_MATRIX_SERVER}, AI Gateway: ${HICLAW_AI_GATEWAY_URL}, OSS: ${HICLAW_STORAGE_BUCKET}\"\n ensure_mc_credentials || { log \"FATAL: Initial STS credential fetch failed\"; exit 1; }\nfi\n\n# ============================================================\n# Local mode: host symlinks, /etc/hosts, wait for local services\n# ============================================================\nif [ \"${HICLAW_RUNTIME}\" != \"aliyun\" ]; then\n # Create symlink for host directory access\n if [ -d \"/host-share\" ]; then\n ORIGINAL_HOST_HOME=\"${HOST_ORIGINAL_HOME:-$HOME}\"\n if [ ! -e \"${ORIGINAL_HOST_HOME}\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/root\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/data\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/host-share\" ]; then\n mkdir -p \"$(dirname \"${ORIGINAL_HOST_HOME}\")\"\n ln -sfn /host-share \"${ORIGINAL_HOST_HOME}\"\n log \"Created symlink: ${ORIGINAL_HOST_HOME} -> /host-share\"\n else\n ln -sfn /host-share /root/host-home\n log \"Created fallback symlink: /root/host-home -> /host-share\"\n fi\n fi\n\n # Add local domains to /etc/hosts\n HOSTS_DOMAINS=\"${MATRIX_DOMAIN%%:*} ${HICLAW_MATRIX_CLIENT_DOMAIN:-matrix-client-local.hiclaw.io} ${AI_GATEWAY_DOMAIN} ${HICLAW_FS_DOMAIN:-fs-local.hiclaw.io}\"\n if ! grep -q \"${AI_GATEWAY_DOMAIN}\" /etc/hosts 2>/dev/null; then\n echo \"127.0.0.1 ${HOSTS_DOMAINS}\" >> /etc/hosts\n log \"Added local domains to /etc/hosts\"\n fi\n\n # Wait for local infrastructure\n waitForService \"Higress Gateway\" \"127.0.0.1\" 8080 180\n waitForService \"Higress Console\" \"127.0.0.1\" 8001 180\n waitForService \"Tuwunel\" \"127.0.0.1\" 6167 120\n waitForHTTP \"Tuwunel Matrix API\" \"${HICLAW_MATRIX_SERVER}/_tuwunel/server_version\" 120\n waitForService \"MinIO\" \"127.0.0.1\" 9000 120\nelse\n # Cloud mode: wait for external Tuwunel\n log \"Waiting for Tuwunel Matrix server at ${HICLAW_MATRIX_SERVER}...\"\n _retry=0\n while [ \"${_retry}\" -lt 30 ]; do\n if curl -sf \"${HICLAW_MATRIX_SERVER}/_matrix/client/versions\" > /dev/null 2>&1; then\n log \"Tuwunel is ready\"\n break\n fi\n _retry=$((_retry + 1))\n log \" Waiting for Tuwunel (attempt ${_retry}/30)...\"\n sleep 5\n done\n if [ \"${_retry}\" -ge 30 ]; then\n log \"ERROR: Tuwunel not reachable at ${HICLAW_MATRIX_SERVER}\"\n exit 1\n fi\nfi\n\n# ============================================================\n# Auto-generate secrets if not provided via environment\n# Persisted to /data so they survive container restart\n# ============================================================\nSECRETS_FILE=\"/data/hiclaw-secrets.env\"\nif [ -f \"${SECRETS_FILE}\" ]; then\n source \"${SECRETS_FILE}\"\n log \"Loaded persisted secrets from ${SECRETS_FILE}\"\nfi\n\nif [ -z \"${HICLAW_MANAGER_GATEWAY_KEY}\" ]; then\n export HICLAW_MANAGER_GATEWAY_KEY=\"$(generateKey 32)\"\n log \"Auto-generated HICLAW_MANAGER_GATEWAY_KEY\"\nfi\nif [ -z \"${HICLAW_MANAGER_PASSWORD}\" ]; then\n export HICLAW_MANAGER_PASSWORD=\"$(generateKey 16)\"\n log \"Auto-generated HICLAW_MANAGER_PASSWORD\"\nfi\n\n# Persist secrets so they survive supervisord restart\nmkdir -p /data\ncat > \"${SECRETS_FILE}\" <<EOF\nexport HICLAW_MANAGER_GATEWAY_KEY=\"${HICLAW_MANAGER_GATEWAY_KEY}\"\nexport HICLAW_MANAGER_PASSWORD=\"${HICLAW_MANAGER_PASSWORD}\"\nEOF\nchmod 600 \"${SECRETS_FILE}\"\n\n# Cloud mode: pull workspace from OSS before initialization\nif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n HICLAW_FS=\"/root/hiclaw-fs\"\n mkdir -p \"${HICLAW_FS}/shared\" \"${HICLAW_FS}/agents\"\n log \"Pulling workspace from OSS...\"\n ensure_mc_credentials\n mc mirror \"${HICLAW_STORAGE_PREFIX}/manager/\" /root/manager-workspace/ --overwrite 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/shared/\" \"${HICLAW_FS}/shared/\" --overwrite 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/agents/\" \"${HICLAW_FS}/agents/\" --overwrite 2>/dev/null || true\n # Symlink hiclaw-fs into workspace for agent access\n ln -sfn \"${HICLAW_FS}\" /root/manager-workspace/hiclaw-fs\nfi\n\n# ============================================================\n# Initialize / upgrade Manager workspace\n# First boot: full init via upgrade-builtins.sh\n# Subsequent boots: compare image version; upgrade only if changed\n# ============================================================\nmkdir -p /root/manager-workspace\n\nIMAGE_VERSION=$(cat /opt/hiclaw/agent/.builtin-version 2>/dev/null || echo \"unknown\")\nINSTALLED_VERSION=$(cat /root/manager-workspace/.builtin-version 2>/dev/null || echo \"\")\n\nif [ ! -f /root/manager-workspace/.initialized ]; then\n log \"First boot: initializing manager workspace...\"\n bash /opt/hiclaw/scripts/init/upgrade-builtins.sh\n touch /root/manager-workspace/.initialized\n log \"Manager workspace initialized (version: ${IMAGE_VERSION})\"\nelif [ \"${IMAGE_VERSION}\" != \"${INSTALLED_VERSION}\" ] || [ \"${IMAGE_VERSION}\" = \"latest\" ]; then\n log \"Upgrade detected: ${INSTALLED_VERSION} -> ${IMAGE_VERSION}${IMAGE_VERSION:+ (latest: always upgrade)}\"\n bash /opt/hiclaw/scripts/init/upgrade-builtins.sh\n log \"Manager workspace upgraded to version: ${IMAGE_VERSION}\"\nelse\n log \"Workspace up to date (version: ${IMAGE_VERSION})\"\nfi\n\n# Local mode: wait for mc mirror initialization (shared + worker data in /root/hiclaw-fs/)\nif [ \"${HICLAW_RUNTIME}\" != \"aliyun\" ]; then\n log \"Waiting for MinIO storage initialization...\"\n _minio_wait=0\n while [ ! -f /root/hiclaw-fs/.initialized ]; do\n sleep 2\n _minio_wait=$(( _minio_wait + 1 ))\n if [ \"${_minio_wait}\" -ge 60 ]; then\n log \"ERROR: MinIO storage initialization timed out after 120s\"\n exit 1\n fi\n done\n log \"MinIO storage initialized\"\nfi\n\n# ============================================================\n# Register Matrix users via Registration API (single-step, no UIAA)\n# ============================================================\nlog \"Registering human admin Matrix account...\"\ncurl -sf -X POST ${HICLAW_MATRIX_SERVER}/_matrix/client/v3/register \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"username\": \"'\"${HICLAW_ADMIN_USER}\"'\",\n \"password\": \"'\"${HICLAW_ADMIN_PASSWORD}\"'\",\n \"auth\": {\n \"type\": \"m.login.registration_token\",\n \"token\": \"'\"${HICLAW_REGISTRATION_TOKEN}\"'\"\n }\n }' > /dev/null 2>&1 || log \"Admin account may already exist\"\n\nlog \"Registering Manager Agent Matrix account...\"\ncurl -sf -X POST ${HICLAW_MATRIX_SERVER}/_matrix/client/v3/register \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"username\": \"manager\",\n \"password\": \"'\"${HICLAW_MANAGER_PASSWORD}\"'\",\n \"auth\": {\n \"type\": \"m.login.registration_token\",\n \"token\": \"'\"${HICLAW_REGISTRATION_TOKEN}\"'\"\n }\n }' > /dev/null 2>&1 || log \"Manager account may already exist\"\n\n# Get Manager Agent's Matrix access token\nlog \"Obtaining Manager Matrix access token...\"\n_LOGIN_RESPONSE=$(curl -sf -X POST ${HICLAW_MATRIX_SERVER}/_matrix/client/v3/login \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"type\": \"m.login.password\",\n \"identifier\": {\"type\": \"m.id.user\", \"user\": \"manager\"},\n \"password\": \"'\"${HICLAW_MANAGER_PASSWORD}\"'\"\n }' 2>&1)\n_LOGIN_EXIT=$?\nlog \"Matrix login HTTP exit code: ${_LOGIN_EXIT}\"\nlog \"Matrix login response: ${_LOGIN_RESPONSE}\"\n\nMANAGER_TOKEN=$(echo \"${_LOGIN_RESPONSE}\" | jq -r '.access_token' 2>/dev/null)\n\nif [ -z \"${MANAGER_TOKEN}\" ] || [ \"${MANAGER_TOKEN}\" = \"null\" ]; then\n log \"ERROR: Failed to obtain Manager Matrix token (exit=${_LOGIN_EXIT})\"\n log \"ERROR: Login response was: ${_LOGIN_RESPONSE}\"\n exit 1\nfi\nlog \"Manager Matrix token obtained (token prefix: ${MANAGER_TOKEN:0:10}...)\"\n\n# ============================================================\n# Local mode: Initialize Higress Console + configure routes\n# Cloud mode: Create admin DM room + schedule welcome message\n# ============================================================\nif [ \"${HICLAW_RUNTIME}\" != \"aliyun\" ]; then\n COOKIE_FILE=\"/tmp/higress-session-cookie\"\n\n log \"Waiting for Higress Console to be fully ready and initializing admin...\"\n INIT_DONE=false\n for i in $(seq 1 90); do\n INIT_RESULT=$(curl -s -X POST http://127.0.0.1:8001/system/init \\\n -H 'Content-Type: application/json' \\\n -d '{\"adminUser\":{\"name\":\"'\"${HICLAW_ADMIN_USER}\"'\",\"password\":\"'\"${HICLAW_ADMIN_PASSWORD}\"'\",\"displayName\":\"'\"${HICLAW_ADMIN_USER}\"'\"}}' 2>/dev/null) || true\n if echo \"${INIT_RESULT}\" | grep -qE '\"success\":true|already.?init' 2>/dev/null; then\n INIT_DONE=true\n break\n fi\n if echo \"${INIT_RESULT}\" | grep -q '\"name\"' 2>/dev/null; then\n INIT_DONE=true\n break\n fi\n sleep 2\n done\n\n if [ \"${INIT_DONE}\" != \"true\" ]; then\n log \"ERROR: Higress Console did not become ready within 180s\"\n exit 1\n fi\n log \"Higress Console init done\"\n\n log \"Logging into Higress Console...\"\n LOGIN_OK=false\n for i in $(seq 1 10); do\n HTTP_CODE=$(curl -s -o /dev/null -w '%{http_code}' -X POST http://127.0.0.1:8001/session/login \\\n -H 'Content-Type: application/json' \\\n -c \"${COOKIE_FILE}\" \\\n -d '{\"username\":\"'\"${HICLAW_ADMIN_USER}\"'\",\"password\":\"'\"${HICLAW_ADMIN_PASSWORD}\"'\"}' 2>/dev/null) || true\n if { [ \"${HTTP_CODE}\" = \"200\" ] || [ \"${HTTP_CODE}\" = \"201\" ]; } && [ -f \"${COOKIE_FILE}\" ] && [ -s \"${COOKIE_FILE}\" ]; then\n LOGIN_OK=true\n break\n fi\n log \"Login attempt $i (HTTP ${HTTP_CODE}), retrying in 3s...\"\n sleep 3\n done\n\n if [ \"${LOGIN_OK}\" != \"true\" ]; then\n log \"ERROR: Could not login to Higress Console after retries\"\n exit 1\n fi\n log \"Higress Console login successful\"\n\n VERIFY_CODE=$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8001/v1/consumers -b \"${COOKIE_FILE}\" 2>/dev/null) || true\n if [ \"${VERIFY_CODE}\" = \"200\" ]; then\n log \"Console session verified (cookie valid)\"\n else\n log \"WARNING: Console session may be invalid (verify returned HTTP ${VERIFY_CODE})\"\n rm -f \"${COOKIE_FILE}\"\n for i in $(seq 1 5); do\n curl -s -o /dev/null -w '%{http_code}' -X POST http://127.0.0.1:8001/session/login \\\n -H 'Content-Type: application/json' \\\n -c \"${COOKIE_FILE}\" \\\n -d '{\"username\":\"'\"${HICLAW_ADMIN_USER}\"'\",\"password\":\"'\"${HICLAW_ADMIN_PASSWORD}\"'\"}' 2>/dev/null\n VERIFY2=$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8001/v1/consumers -b \"${COOKIE_FILE}\" 2>/dev/null) || true\n if [ \"${VERIFY2}\" = \"200\" ]; then\n log \"Re-login successful, session verified\"\n break\n fi\n sleep 2\n done\n fi\n\n export HIGRESS_COOKIE_FILE=\"${COOKIE_FILE}\"\n\n # Configure Higress routes, consumers, MCP servers\n /opt/hiclaw/scripts/init/setup-higress.sh\nfi\n\n# ============================================================\n# Create admin DM room, persist to state.json, send welcome message\n# Runs in both local and cloud modes (idempotent)\n# ============================================================\nMANAGER_FULL_ID=\"@manager:${MATRIX_DOMAIN}\"\nADMIN_FULL_ID=\"@${HICLAW_ADMIN_USER}:${MATRIX_DOMAIN}\"\n\nlog \"Logging in as admin to create DM room...\"\n_ADMIN_LOGIN=$(curl -sf -X POST \"${HICLAW_MATRIX_SERVER}/_matrix/client/v3/login\" \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"type\": \"m.login.password\",\n \"identifier\": {\"type\": \"m.id.user\", \"user\": \"'\"${HICLAW_ADMIN_USER}\"'\"},\n \"password\": \"'\"${HICLAW_ADMIN_PASSWORD}\"'\"\n }' 2>&1) || true\n\nADMIN_MATRIX_TOKEN=$(echo \"${_ADMIN_LOGIN}\" | jq -r '.access_token // empty' 2>/dev/null)\nif [ -z \"${ADMIN_MATRIX_TOKEN}\" ]; then\n log \"WARNING: Failed to login as admin, skipping DM room creation\"\nelse\n # Search for existing DM room with Manager (idempotent)\n DM_ROOM_ID=\"\"\n _JOINED_ROOMS=$(curl -sf \"${HICLAW_MATRIX_SERVER}/_matrix/client/v3/joined_rooms\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" 2>/dev/null \\\n | jq -r '.joined_rooms[]' 2>/dev/null) || true\n for _rid in ${_JOINED_ROOMS}; do\n _members=$(curl -sf \"${HICLAW_MATRIX_SERVER}/_matrix/client/v3/rooms/${_rid}/members\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" 2>/dev/null \\\n | jq -r '.chunk[].state_key' 2>/dev/null) || continue\n _count=$(echo \"${_members}\" | wc -l | xargs)\n if [ \"${_count}\" = \"2\" ] && echo \"${_members}\" | grep -q \"@manager:\"; then\n DM_ROOM_ID=\"${_rid}\"\n break\n fi\n done\n\n if [ -n \"${DM_ROOM_ID}\" ]; then\n log \"Existing DM room found: ${DM_ROOM_ID}\"\n else\n log \"Creating DM room with Manager...\"\n _RAW=$(curl -s -w '\\nHTTP_CODE:%{http_code}' -X POST \"${HICLAW_MATRIX_SERVER}/_matrix/client/v3/createRoom\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d \"{\\\"is_direct\\\":true,\\\"invite\\\":[\\\"${MANAGER_FULL_ID}\\\"],\\\"preset\\\":\\\"trusted_private_chat\\\"}\" 2>&1) || true\n _HTTP_CODE=$(echo \"${_RAW}\" | tail -1 | sed 's/HTTP_CODE://')\n _CREATE_RESP=$(echo \"${_RAW}\" | sed '$d')\n DM_ROOM_ID=$(echo \"${_CREATE_RESP}\" | jq -r '.room_id // empty' 2>/dev/null)\n if [ -n \"${DM_ROOM_ID}\" ]; then\n log \"DM room created: ${DM_ROOM_ID}\"\n else\n log \"WARNING: Failed to create DM room (HTTP ${_HTTP_CODE}): ${_CREATE_RESP}\"\n fi\n fi\n\n # Persist admin DM room ID to state.json\n if [ -n \"${DM_ROOM_ID}\" ]; then\n STATE_SCRIPT=\"/opt/hiclaw/agent/skills/task-management/scripts/manage-state.sh\"\n if [ -f \"${STATE_SCRIPT}\" ]; then\n bash \"${STATE_SCRIPT}\" --action init 2>/dev/null || true\n bash \"${STATE_SCRIPT}\" --action set-admin-dm --room-id \"${DM_ROOM_ID}\" 2>/dev/null || true\n log \"Admin DM room persisted to state.json: ${DM_ROOM_ID}\"\n fi\n fi\n\n # Schedule welcome message in background (only on first boot)\n if [ -n \"${DM_ROOM_ID}\" ] && [ ! -f \"/root/manager-workspace/soul-configured\" ]; then\n log \"Scheduling welcome message (background, waiting for OpenClaw to start)...\"\n (\n _HICLAW_LANGUAGE=\"${HICLAW_LANGUAGE:-zh}\"\n _HICLAW_TIMEZONE=\"${TZ:-Asia/Shanghai}\"\n _wait=0\n _ready=false\n while [ \"${_wait}\" -lt 300 ]; do\n if curl -sf http://127.0.0.1:18799/ > /dev/null 2>&1; then\n _ready=true\n break\n fi\n sleep 3\n _wait=$((_wait + 3))\n done\n if [ \"${_ready}\" != \"true\" ]; then\n echo \"[manager] WARNING: OpenClaw gateway not ready within 300s, skipping welcome message\"\n exit 0\n fi\n # Ensure Manager has joined the DM room before sending the welcome\n # message. Without this, there is a race between OpenClaw's Matrix\n # auto-join and the message send — the message may land before Manager\n # joins, so OpenClaw's /sync never picks it up.\n _join_ok=false\n for _join_attempt in 1 2 3; do\n if curl -sf -X POST \"${HICLAW_MATRIX_SERVER}/_matrix/client/v3/rooms/${DM_ROOM_ID}/join\" \\\n -H \"Authorization: Bearer ${MANAGER_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d '{}' > /dev/null 2>&1; then\n echo \"[manager] Manager joined DM room before welcome message\"\n _join_ok=true\n break\n fi\n sleep 2\n done\n if [ \"${_join_ok}\" != \"true\" ]; then\n echo \"[manager] WARNING: Manager join request failed after 3 attempts (may already be joined)\"\n fi\n _welcome_msg=\"This is an automated message from the HiClaw setup. This is a fresh installation.\n\n--- Installation Context ---\nUser Language: ${_HICLAW_LANGUAGE} (zh = Chinese, en = English)\nUser Timezone: ${_HICLAW_TIMEZONE} (IANA timezone identifier)\n---\n\nYou are an AI agent that manages a team of worker agents. Your identity and personality have not been configured yet — the human admin is about to meet you for the first time.\n\nPlease begin the onboarding conversation:\n\n1. Greet the admin warmly and briefly describe what you can do (coordinate workers, manage tasks, run multi-agent projects)\n2. The user has selected \\\"${_HICLAW_LANGUAGE}\\\" as their preferred language during installation. Use this language for your greeting and all subsequent communication.\n3. The user's timezone is ${_HICLAW_TIMEZONE}. Based on this timezone, you may infer their likely region and suggest additional language options.\n4. Ask them: a) What would they like to call you? b) Communication style preference? c) Any behavior guidelines? d) Confirm default language\n5. After they reply, write their preferences to ~/SOUL.md\n6. Confirm what you wrote, and ask if they would like to adjust anything\n7. Once confirmed, run: touch ~/soul-configured\n\nThe human admin will start chatting shortly.\"\n _txn_id=\"welcome-$(date +%s)\"\n _payload=$(jq -nc --arg body \"${_welcome_msg}\" '{\"msgtype\":\"m.text\",\"body\":$body}')\n _raw=$(curl -s -w '\\nHTTP_CODE:%{http_code}' -X PUT \"${HICLAW_MATRIX_SERVER}/_matrix/client/v3/rooms/${DM_ROOM_ID}/send/m.room.message/${_txn_id}\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d \"${_payload}\" 2>&1) || true\n _http_code=$(echo \"${_raw}\" | tail -1 | sed 's/HTTP_CODE://')\n _send_resp=$(echo \"${_raw}\" | sed '$d')\n if echo \"${_send_resp}\" | jq -e '.event_id' > /dev/null 2>&1; then\n echo \"[manager] Welcome message sent to DM room\"\n else\n echo \"[manager] WARNING: Failed to send welcome message (HTTP ${_http_code}): ${_send_resp}\"\n fi\n ) &\n log \"Welcome message background process started (PID: $!)\"\n fi\nfi\n\n# ============================================================\n# Generate Manager Agent openclaw.json from template\n# ============================================================\nlog \"Generating Manager openclaw.json...\"\nexport MANAGER_MATRIX_TOKEN=\"${MANAGER_TOKEN}\"\nexport MANAGER_GATEWAY_KEY=\"${HICLAW_MANAGER_GATEWAY_KEY}\"\n# Resolve model parameters based on model name\nMODEL_NAME=\"${HICLAW_DEFAULT_MODEL:-qwen3.5-plus}\"\ncase \"${MODEL_NAME}\" in\n gpt-5.3-codex|gpt-5-mini|gpt-5-nano)\n export MODEL_CONTEXT_WINDOW=400000 MODEL_MAX_TOKENS=128000 ;;\n claude-opus-4-6)\n export MODEL_CONTEXT_WINDOW=1000000 MODEL_MAX_TOKENS=128000 ;;\n claude-sonnet-4-6)\n export MODEL_CONTEXT_WINDOW=1000000 MODEL_MAX_TOKENS=64000 ;;\n claude-haiku-4-5)\n export MODEL_CONTEXT_WINDOW=200000 MODEL_MAX_TOKENS=64000 ;;\n qwen3.5-plus)\n export MODEL_CONTEXT_WINDOW=200000 MODEL_MAX_TOKENS=64000 ;;\n deepseek-chat|deepseek-reasoner|kimi-k2.5)\n export MODEL_CONTEXT_WINDOW=256000 MODEL_MAX_TOKENS=128000 ;;\n glm-5|MiniMax-M2.7|MiniMax-M2.7-highspeed|MiniMax-M2.5)\n export MODEL_CONTEXT_WINDOW=200000 MODEL_MAX_TOKENS=128000 ;;\n *)\n export MODEL_CONTEXT_WINDOW=150000 MODEL_MAX_TOKENS=128000 ;;\nesac\nexport MODEL_REASONING=true\n\n# Override with user-supplied custom model parameters from env (set during install)\n[ -n \"${HICLAW_MODEL_CONTEXT_WINDOW:-}\" ] && export MODEL_CONTEXT_WINDOW=\"${HICLAW_MODEL_CONTEXT_WINDOW}\"\n[ -n \"${HICLAW_MODEL_MAX_TOKENS:-}\" ] && export MODEL_MAX_TOKENS=\"${HICLAW_MODEL_MAX_TOKENS}\"\n[ -n \"${HICLAW_MODEL_REASONING:-}\" ] && export MODEL_REASONING=\"${HICLAW_MODEL_REASONING}\"\n\n# E2EE: convert HICLAW_MATRIX_E2EE to JSON boolean for template substitution\nif [ \"${HICLAW_MATRIX_E2EE:-0}\" = \"1\" ] || [ \"${HICLAW_MATRIX_E2EE:-}\" = \"true\" ]; then\n export MATRIX_E2EE_ENABLED=true\nelse\n export MATRIX_E2EE_ENABLED=false\nfi\nlog \"Matrix E2EE: ${MATRIX_E2EE_ENABLED}\"\n\n# Resolve input modalities: only vision-capable models get \"image\"\ncase \"${MODEL_NAME}\" in\n gpt-5.4|gpt-5.3-codex|gpt-5-mini|gpt-5-nano|claude-opus-4-6|claude-sonnet-4-6|claude-haiku-4-5|qwen3.5-plus|kimi-k2.5)\n export MODEL_INPUT='[\"text\", \"image\"]' ;;\n *)\n export MODEL_INPUT='[\"text\"]' ;;\nesac\n# Override with user-supplied vision setting from env\nif [ \"${HICLAW_MODEL_VISION:-}\" = \"true\" ]; then\n export MODEL_INPUT='[\"text\", \"image\"]'\nelif [ \"${HICLAW_MODEL_VISION:-}\" = \"false\" ]; then\n export MODEL_INPUT='[\"text\"]'\nfi\n\nlog \"Model: ${MODEL_NAME} (context=${MODEL_CONTEXT_WINDOW}, maxTokens=${MODEL_MAX_TOKENS}, reasoning=${MODEL_REASONING}, input=${MODEL_INPUT})\"\n\nif [ -f /root/manager-workspace/openclaw.json ]; then\n log \"Manager openclaw.json already exists, updating dynamic fields only (preserving user customizations)...\"\n # Merge known models into existing config (add missing, preserve user-added)\n # Use known-models.json (valid JSON) instead of template (contains ${VAR} placeholders)\n KNOWN_MODELS=$(cat /opt/hiclaw/configs/known-models.json 2>/dev/null || echo '[]')\n jq --arg token \"${MANAGER_TOKEN}\" \\\n --arg key \"${HICLAW_MANAGER_GATEWAY_KEY}\" \\\n --arg model \"${MODEL_NAME}\" \\\n --arg emb_model \"${HICLAW_EMBEDDING_MODEL}\" \\\n --arg aigw_domain \"${AI_GATEWAY_DOMAIN}\" \\\n --argjson e2ee \"${MATRIX_E2EE_ENABLED}\" \\\n --argjson known_models \"${KNOWN_MODELS}\" \\\n --argjson ctx \"${MODEL_CONTEXT_WINDOW}\" \\\n --argjson max \"${MODEL_MAX_TOKENS}\" \\\n --argjson reasoning \"${MODEL_REASONING}\" \\\n --argjson input \"${MODEL_INPUT}\" \\\n '\n # Merge known models: add any model id not already present\n .models.providers[\"hiclaw-gateway\"].models as $existing\n | ($existing | map(.id)) as $existing_ids\n | ($known_models | map(select(.id as $id | $existing_ids | index($id) | not))) as $new\n | .models.providers[\"hiclaw-gateway\"].models = ($existing + $new)\n # Ensure the user-chosen default model is in the list (custom model support)\n | if (.models.providers[\"hiclaw-gateway\"].models | map(.id) | index($model) | not) then\n .models.providers[\"hiclaw-gateway\"].models += [{\"id\": $model, \"name\": $model, \"reasoning\": $reasoning, \"contextWindow\": $ctx, \"maxTokens\": $max, \"input\": $input}]\n else . end\n # Rebuild model aliases from the full models list\n | (.models.providers[\"hiclaw-gateway\"].models | map({ (\"hiclaw-gateway/\" + .id): { \"alias\": .id } }) | add // {}) as $aliases\n | .agents.defaults.models = ((.agents.defaults.models // {}) + $aliases)\n | .channels.matrix.accessToken = $token | .models.providers[\"hiclaw-gateway\"].apiKey = $key\n | ((.hooks.token // \"\") as $ht | if $ht == $key or $ht == ($key + \"-hooks\" | @base64) then del(.hooks) else . end)\n | .agents.defaults.model.primary = (\"hiclaw-gateway/\" + $model)\n | .commands.restart = true\n | .gateway.controlUi.dangerouslyDisableDeviceAuth = true\n | .channels.matrix.encryption = $e2ee\n # Ensure memorySearch config exists (embedding model for memory) — skip if embedding model is empty\n | if $emb_model != \"\" then .agents.defaults.memorySearch //= {\"provider\":\"openai\",\"model\":$emb_model,\"remote\":{\"baseUrl\":(\"http://\" + $aigw_domain + \":8080/v1\"),\"apiKey\":$key}} else . end\n ' \\\n /root/manager-workspace/openclaw.json > /tmp/openclaw.json.tmp && \\\n mv /tmp/openclaw.json.tmp /root/manager-workspace/openclaw.json\n # Verify the token was written correctly\n _written_token=$(jq -r '.channels.matrix.accessToken' /root/manager-workspace/openclaw.json 2>/dev/null)\n if [ -z \"${_written_token}\" ] || [ \"${_written_token}\" = \"null\" ]; then\n log \"ERROR: Matrix token was not written correctly to openclaw.json (got: ${_written_token})\"\n else\n log \"Matrix token written to openclaw.json (prefix: ${_written_token:0:10}...)\"\n fi\nelse\n log \"Manager openclaw.json not found, generating from template...\"\n envsubst < /opt/hiclaw/configs/manager-openclaw.json.tmpl > /root/manager-workspace/openclaw.json\n # Post-envsubst injection: memorySearch + custom model (single jq pass when possible)\n if ! jq -e --arg model \"${MODEL_NAME}\" '.models.providers[\"hiclaw-gateway\"].models | map(.id) | index($model)' /root/manager-workspace/openclaw.json > /dev/null 2>&1; then\n log \"Custom model '${MODEL_NAME}' not in built-in list, injecting into config...\"\n jq --arg emb_model \"${HICLAW_EMBEDDING_MODEL}\" \\\n --arg aigw_domain \"${AI_GATEWAY_DOMAIN}\" \\\n --arg key \"${HICLAW_MANAGER_GATEWAY_KEY}\" \\\n --arg model \"${MODEL_NAME}\" \\\n --argjson ctx \"${MODEL_CONTEXT_WINDOW}\" \\\n --argjson max \"${MODEL_MAX_TOKENS}\" \\\n --argjson reasoning \"${MODEL_REASONING}\" \\\n --argjson input \"${MODEL_INPUT}\" \\\n '\n (if $emb_model != \"\" then .agents.defaults.memorySearch = {\"provider\":\"openai\",\"model\":$emb_model,\"remote\":{\"baseUrl\":(\"http://\" + $aigw_domain + \":8080/v1\"),\"apiKey\":$key}} else . end)\n | .models.providers[\"hiclaw-gateway\"].models += [{\"id\": $model, \"name\": $model, \"reasoning\": $reasoning, \"contextWindow\": $ctx, \"maxTokens\": $max, \"input\": $input}]\n | .agents.defaults.models += {(\"hiclaw-gateway/\" + $model): {\"alias\": $model}}\n ' /root/manager-workspace/openclaw.json > /tmp/openclaw.json.tmp && \\\n mv /tmp/openclaw.json.tmp /root/manager-workspace/openclaw.json\n elif [ -n \"${HICLAW_EMBEDDING_MODEL}\" ]; then\n jq --arg emb_model \"${HICLAW_EMBEDDING_MODEL}\" \\\n --arg aigw_domain \"${AI_GATEWAY_DOMAIN}\" \\\n --arg key \"${HICLAW_MANAGER_GATEWAY_KEY}\" \\\n '.agents.defaults.memorySearch = {\"provider\":\"openai\",\"model\":$emb_model,\"remote\":{\"baseUrl\":(\"http://\" + $aigw_domain + \":8080/v1\"),\"apiKey\":$key}}' \\\n /root/manager-workspace/openclaw.json > /tmp/openclaw.json.tmp && \\\n mv /tmp/openclaw.json.tmp /root/manager-workspace/openclaw.json\n fi\n _written_token=$(jq -r '.channels.matrix.accessToken' /root/manager-workspace/openclaw.json 2>/dev/null)\n log \"Matrix token written from template (prefix: ${_written_token:0:10}...)\"\nfi\n\n# Cloud mode: overlay cloud-specific settings onto generated config\nif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n log \"Applying cloud overlay to openclaw.json...\"\n jq --arg homeserver \"${HICLAW_MATRIX_SERVER}\" \\\n --arg gateway \"${HICLAW_AI_GATEWAY_URL}/v1\" \\\n --arg key \"${HICLAW_MANAGER_GATEWAY_KEY}\" \\\n '.channels.matrix.homeserver = $homeserver\n | .models.providers[\"hiclaw-gateway\"].baseUrl = $gateway\n | .models.providers[\"hiclaw-gateway\"].apiKey = $key\n | ((.hooks.token // \"\") as $ht | if $ht == $key or $ht == ($key + \"-hooks\" | @base64) then del(.hooks) else . end)\n | .commands.restart = false\n | if .agents.defaults.memorySearch then .agents.defaults.memorySearch.remote.baseUrl = $gateway | .agents.defaults.memorySearch.remote.apiKey = $key else . end' \\\n /root/manager-workspace/openclaw.json > /tmp/openclaw-cloud.json && \\\n mv /tmp/openclaw-cloud.json /root/manager-workspace/openclaw.json\n log \"Cloud overlay applied\"\nfi\n\n# ============================================================\n# Optional: enable openclaw-cms-plugin observability\n# Config is applied at runtime so secrets stay out of image layers.\n# ============================================================\nCMS_TRACES_ENABLED=\"$(echo \"${HICLAW_CMS_TRACES_ENABLED:-false}\" | tr '[:upper:]' '[:lower:]')\"\nif [ \"${CMS_TRACES_ENABLED}\" = \"true\" ]; then\n CMS_PLUGIN_NAME=\"openclaw-cms-plugin\"\n CMS_PLUGIN_DIR=\"${OPENCLAW_CMS_PLUGIN_DIR:-/opt/openclaw/extensions/openclaw-cms-plugin}\"\n CMS_PLUGIN_MANIFEST=\"${CMS_PLUGIN_DIR}/openclaw.plugin.json\"\n DIAG_PLUGIN_NAME=\"diagnostics-otel\"\n DIAG_PLUGIN_DIR=\"/opt/openclaw/extensions/diagnostics-otel\"\n CMS_LICENSE_KEY=\"${HICLAW_CMS_LICENSE_KEY:-}\"\n CMS_PROJECT=\"${HICLAW_CMS_PROJECT:-}\"\n CMS_METRICS_ENABLED=\"${HICLAW_CMS_METRICS_ENABLED:-false}\"\n\n if [ ! -f \"${CMS_PLUGIN_MANIFEST}\" ]; then\n log \"WARNING: ${CMS_PLUGIN_NAME} manifest not found at ${CMS_PLUGIN_MANIFEST}, skipping plugin config\"\n else\n _missing=0\n [ -z \"${HICLAW_CMS_ENDPOINT:-}\" ] && log \"WARNING: HICLAW_CMS_ENDPOINT is required when HICLAW_CMS_TRACES_ENABLED=true\" && _missing=1\n [ -z \"${CMS_LICENSE_KEY:-}\" ] && log \"WARNING: HICLAW_CMS_LICENSE_KEY is required when HICLAW_CMS_TRACES_ENABLED=true\" && _missing=1\n [ -z \"${HICLAW_CMS_WORKSPACE:-}\" ] && log \"WARNING: HICLAW_CMS_WORKSPACE is required when HICLAW_CMS_TRACES_ENABLED=true\" && _missing=1\n\n if [ \"${_missing}\" = \"0\" ]; then\n CMS_SERVICE_NAME=\"${HICLAW_CMS_SERVICE_NAME:-hiclaw-manager}\"\n CMS_ENABLE_METRICS=\"${CMS_METRICS_ENABLED}\"\n DIAG_AVAILABLE=\"0\"\n _metrics_lc=\"$(echo \"${CMS_ENABLE_METRICS}\" | tr '[:upper:]' '[:lower:]')\"\n if [ \"${_metrics_lc}\" = \"true\" ]; then\n if [ -f \"${DIAG_PLUGIN_DIR}/package.json\" ]; then\n DIAG_AVAILABLE=\"1\"\n if [ ! -d \"${DIAG_PLUGIN_DIR}/node_modules\" ]; then\n log \"diagnostics-otel dependencies missing, installing...\"\n if (cd \"${DIAG_PLUGIN_DIR}\" && npm install --omit=dev --ignore-scripts >/tmp/hiclaw-diag-install.log 2>&1); then\n log \"diagnostics-otel dependencies installed\"\n else\n log \"WARNING: diagnostics-otel npm install failed, metrics plugin may not load\"\n fi\n else\n log \"diagnostics-otel dependencies already present\"\n fi\n else\n log \"WARNING: diagnostics-otel package.json not found at ${DIAG_PLUGIN_DIR}, metrics plugin may not load\"\n fi\n fi\n\n log \"Applying ${CMS_PLUGIN_NAME} config to openclaw.json...\"\n jq --arg pluginName \"${CMS_PLUGIN_NAME}\" \\\n --arg pluginDir \"${CMS_PLUGIN_DIR}\" \\\n --arg endpoint \"${HICLAW_CMS_ENDPOINT}\" \\\n --arg licenseKey \"${CMS_LICENSE_KEY}\" \\\n --arg armsProject \"${CMS_PROJECT}\" \\\n --arg cmsWorkspace \"${HICLAW_CMS_WORKSPACE}\" \\\n --arg serviceName \"${CMS_SERVICE_NAME}\" \\\n --arg diagPluginName \"${DIAG_PLUGIN_NAME}\" \\\n --arg diagPluginDir \"${DIAG_PLUGIN_DIR}\" \\\n --arg metricsRaw \"${CMS_ENABLE_METRICS}\" \\\n --arg diagAvailableRaw \"${DIAG_AVAILABLE}\" \\\n '\n .plugins = (.plugins // {})\n | .plugins.load = (.plugins.load // {})\n | .plugins.entries = (.plugins.entries // {})\n | if (.plugins.allow | type) != \"array\" then .plugins.allow = [] else . end\n | if (.plugins.allow | index($pluginName)) == null then .plugins.allow += [$pluginName] else . end\n | if (.plugins.load.paths | type) != \"array\" then .plugins.load.paths = [] else . end\n | if (.plugins.load.paths | index($pluginDir)) == null then .plugins.load.paths += [$pluginDir] else . end\n | .plugins.entries[$pluginName] = {\n \"enabled\": true,\n \"config\": {\n \"endpoint\": $endpoint,\n \"headers\": {\n \"x-arms-license-key\": $licenseKey,\n \"x-arms-project\": $armsProject,\n \"x-cms-workspace\": $cmsWorkspace\n },\n \"serviceName\": $serviceName\n }\n }\n\n # diagnostics-otel metrics (optional)\n | ($metricsRaw | ascii_downcase) as $m\n | ($diagAvailableRaw == \"1\") as $diagAvailable\n | (($m == \"true\") and $diagAvailable) as $metricsEnabled\n | if $metricsEnabled then\n (if (.plugins.allow | index($diagPluginName)) == null then .plugins.allow += [$diagPluginName] else . end)\n | (if (.plugins.load.paths | index($diagPluginDir)) == null then .plugins.load.paths += [$diagPluginDir] else . end)\n | .plugins.entries[$diagPluginName].enabled = true\n | .diagnostics = (.diagnostics // {})\n | .diagnostics.otel = (.diagnostics.otel // {})\n | .diagnostics.enabled = true\n | .diagnostics.otel.enabled = true\n | .diagnostics.otel.endpoint = $endpoint\n | .diagnostics.otel.protocol = (.diagnostics.otel.protocol // \"http/protobuf\")\n | .diagnostics.otel.headers = {\n \"x-arms-license-key\": $licenseKey,\n \"x-arms-project\": $armsProject,\n \"x-cms-workspace\": $cmsWorkspace\n }\n | .diagnostics.otel.serviceName = $serviceName\n | .diagnostics.otel.metrics = true\n | .diagnostics.otel.traces = (.diagnostics.otel.traces // false)\n | .diagnostics.otel.logs = (.diagnostics.otel.logs // false)\n else\n .\n end\n ' /root/manager-workspace/openclaw.json > /tmp/openclaw-cms.json && \\\n mv /tmp/openclaw-cms.json /root/manager-workspace/openclaw.json\n log \"${CMS_PLUGIN_NAME} config applied (metrics=${CMS_ENABLE_METRICS}, service=${CMS_SERVICE_NAME})\"\n else\n log \"Skipping ${CMS_PLUGIN_NAME} config due to missing required env vars\"\n fi\n fi\nfi\n\n# ============================================================\n# Detect container runtime (for Worker creation)\n# ============================================================\nsource /opt/hiclaw/scripts/lib/container-api.sh\nif container_api_available; then\n log \"Container runtime socket detected at ${CONTAINER_SOCKET} — direct Worker creation enabled\"\n export HICLAW_CONTAINER_RUNTIME=\"socket\"\nelif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n log \"Cloud mode — Workers created via SAE API\"\n export HICLAW_CONTAINER_RUNTIME=\"cloud\"\nelse\n log \"No container runtime found — Worker creation will output install commands\"\n export HICLAW_CONTAINER_RUNTIME=\"none\"\nfi\n\n# ============================================================\n# Upgrade Worker openclaw.json: merge known models + E2EE flag into existing configs\n# Existing workers in MinIO may have old single-model configs or missing encryption field.\n# Merge template models so they can hot-switch without restart.\n# ============================================================\nREGISTRY_FILE=\"/root/manager-workspace/workers-registry.json\"\nif [ -f \"${REGISTRY_FILE}\" ]; then\n # Use known-models.json (valid JSON) instead of template (contains ${VAR} placeholders)\n KNOWN_MODELS_FILE=\"/opt/hiclaw/configs/known-models.json\"\n if [ -f \"${KNOWN_MODELS_FILE}\" ]; then\n _KNOWN_MODELS=$(cat \"${KNOWN_MODELS_FILE}\")\n for _wname in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_wname}\" ] && continue\n _minio_path=\"${HICLAW_STORAGE_PREFIX}/agents/${_wname}/openclaw.json\"\n _tmp_in=\"/tmp/openclaw-${_wname}-models-upgrade-in.json\"\n if mc cp \"${_minio_path}\" \"${_tmp_in}\" 2>/dev/null; then\n _tmp_out=\"/tmp/openclaw-${_wname}-models-upgrade-out.json\"\n # Idempotent merge: add missing known models, rebuild aliases, set e2ee.\n # Always runs — jq deduplicates by model id, so re-runs are safe.\n jq --argjson known_models \"${_KNOWN_MODELS}\" \\\n --argjson e2ee \"${MATRIX_E2EE_ENABLED}\" '\n .models.providers[\"hiclaw-gateway\"].models as $existing\n | ($existing | map(.id)) as $existing_ids\n | ($known_models | map(select(.id as $id | $existing_ids | index($id) | not))) as $new\n | .models.providers[\"hiclaw-gateway\"].models = ($existing + $new)\n | (.models.providers[\"hiclaw-gateway\"].models | map({ (\"hiclaw-gateway/\" + .id): { \"alias\": .id } }) | add // {}) as $aliases\n | .agents.defaults.models = ((.agents.defaults.models // {}) + $aliases)\n | .channels.matrix.encryption = $e2ee\n ' \"${_tmp_in}\" > \"${_tmp_out}\" 2>/dev/null\n if ! diff -q \"${_tmp_in}\" \"${_tmp_out}\" > /dev/null 2>&1; then\n if mc cp \"${_tmp_out}\" \"${_minio_path}\" 2>/dev/null; then\n _new_count=$(jq '.models.providers[\"hiclaw-gateway\"].models | length' \"${_tmp_out}\" 2>/dev/null)\n log \"Worker ${_wname}: upgraded openclaw.json (models: ${_new_count}, e2ee: ${MATRIX_E2EE_ENABLED})\"\n fi\n fi\n rm -f \"${_tmp_in}\" \"${_tmp_out}\"\n fi\n done\n fi\nfi\n\n# ============================================================\n# Ensure Worker Matrix password files exist in MinIO (E2EE fix)\n# Workers need to re-login on restart to get a fresh device_id.\n# Older workers created before this fix won't have the password file.\n# ============================================================\nif [ -f \"${REGISTRY_FILE}\" ]; then\n for _wname in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_wname}\" ] && continue\n _creds_file=\"/data/worker-creds/${_wname}.env\"\n if [ -f \"${_creds_file}\" ]; then\n # Check if password file already exists in MinIO\n if ! mc stat \"${HICLAW_STORAGE_PREFIX}/agents/${_wname}/credentials/matrix/password\" > /dev/null 2>&1; then\n source \"${_creds_file}\"\n if [ -n \"${WORKER_PASSWORD}\" ]; then\n _tmp_pw=\"/tmp/matrix-pw-${_wname}\"\n echo -n \"${WORKER_PASSWORD}\" > \"${_tmp_pw}\"\n mc cp \"${_tmp_pw}\" \"${HICLAW_STORAGE_PREFIX}/agents/${_wname}/credentials/matrix/password\" 2>/dev/null \\\n && log \"Worker ${_wname}: wrote Matrix password to MinIO (E2EE re-login fix)\" \\\n || log \"Worker ${_wname}: WARNING: failed to write Matrix password to MinIO\"\n rm -f \"${_tmp_pw}\"\n fi\n fi\n fi\n done\nfi\n\n# ============================================================\n# Recreate Worker containers as needed after Manager restart.\n# Workers are on hiclaw-net; Docker DNS resolves *-local.hiclaw.io via\n# the Manager's network aliases, so IP changes don't require worker recreation.\n# Only recreate stopped/missing workers.\n# ============================================================\nif container_api_available; then\n REGISTRY_FILE=\"/root/manager-workspace/workers-registry.json\"\n if [ -f \"${REGISTRY_FILE}\" ]; then\n for _worker_name in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_worker_name}\" ] && continue\n\n # Skip remote workers — they are not Manager-managed containers.\n _deployment=$(jq -r --arg w \"${_worker_name}\" '.workers[$w].deployment // \"local\"' \"${REGISTRY_FILE}\" 2>/dev/null)\n if [ \"${_deployment}\" = \"remote\" ]; then\n log \"Worker ${_worker_name} is remote, skipping container recreate\"\n continue\n fi\n\n _status=$(container_status_worker \"${_worker_name}\")\n if [ \"${_status}\" = \"running\" ]; then\n log \"Worker running: ${_worker_name}, skipping\"\n continue\n fi\n # Container missing or stopped — recreate.\n log \"Worker container ${_status}: ${_worker_name}, recreating...\"\n _creds_file=\"/data/worker-creds/${_worker_name}.env\"\n if [ -f \"${_creds_file}\" ]; then\n source \"${_creds_file}\"\n _runtime=$(jq -r --arg w \"${_worker_name}\" '.workers[$w].runtime // \"openclaw\"' \"${REGISTRY_FILE}\" 2>/dev/null)\n _recreated=false\n for _attempt in 1 2 3; do\n if [ \"${_runtime}\" = \"copaw\" ]; then\n container_create_copaw_worker \"${_worker_name}\" \"${_worker_name}\" \"${WORKER_MINIO_PASSWORD}\" 2>&1 && _recreated=true && break\n else\n container_create_worker \"${_worker_name}\" \"${_worker_name}\" \"${WORKER_MINIO_PASSWORD}\" 2>&1 && _recreated=true && break\n fi\n log \" Attempt ${_attempt}/3 failed for ${_worker_name}, retrying in $((5 * _attempt))s...\"\n sleep $((5 * _attempt))\n done\n if [ \"${_recreated}\" = true ]; then\n log \" Recreated ${_runtime} worker: ${_worker_name}\"\n else\n log \" ERROR: Failed to recreate ${_worker_name} after 3 attempts\"\n fi\n else\n log \" WARNING: No credentials found for ${_worker_name} (${_creds_file} missing), skipping\"\n fi\n done\n fi\nfi\n\n# ============================================================\n# Notify workers of builtin updates if upgrade happened\n# Builtin files (AGENTS.md, skills) are already synced by upgrade-builtins.sh\n#\n# Cooldown: skip notification if the last successful notify was within\n# NOTIFY_COOLDOWN_SECS (default 3600s / 1 hour). This prevents repeated\n# notifications when the Manager crash-loops and re-runs upgrade-builtins\n# on every restart (e.g. IMAGE_VERSION=latest always triggers upgrade).\n# ============================================================\nNOTIFY_COOLDOWN_SECS=\"${HICLAW_NOTIFY_COOLDOWN_SECS:-3600}\"\nNOTIFY_TS_FILE=\"/root/manager-workspace/.last-worker-notify-ts\"\n\nif [ -f /root/manager-workspace/.upgrade-pending-worker-notify ]; then\n _now=$(date +%s)\n _last_notify=$(cat \"${NOTIFY_TS_FILE}\" 2>/dev/null || echo \"0\")\n _elapsed=$(( _now - _last_notify ))\n\n if [ \"${_elapsed}\" -lt \"${NOTIFY_COOLDOWN_SECS}\" ]; then\n log \"Skipping worker builtin notification (last notify ${_elapsed}s ago, cooldown ${NOTIFY_COOLDOWN_SECS}s)\"\n rm -f /root/manager-workspace/.upgrade-pending-worker-notify\n else\n log \"Notifying workers about builtin updates...\"\n REGISTRY_FILE=\"/root/manager-workspace/workers-registry.json\"\n _notify_ok=false\n if [ -f \"${REGISTRY_FILE}\" ]; then\n for _worker_name in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_worker_name}\" ] && continue\n _room_id=$(jq -r --arg w \"${_worker_name}\" '.workers[$w].room_id // empty' \"${REGISTRY_FILE}\" 2>/dev/null)\n if [ -n \"${_room_id}\" ]; then\n _worker_id=\"@${_worker_name}:${MATRIX_DOMAIN}\"\n _txn_id=\"upgrade-$(date +%s%N)\"\n _msg=\"@${_worker_name}:${MATRIX_DOMAIN} Manager upgraded builtin files (AGENTS.md, skills). Please use your file-sync skill to sync the latest config.\"\n _raw=$(curl -s -w '\\nHTTP_CODE:%{http_code}' -X PUT \\\n \"${HICLAW_MATRIX_SERVER}/_matrix/client/v3/rooms/${_room_id}/send/m.room.message/${_txn_id}\" \\\n -H \"Authorization: Bearer ${MANAGER_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d \"{\\\"msgtype\\\":\\\"m.text\\\",\\\"body\\\":\\\"${_msg}\\\",\\\"m.mentions\\\":{\\\"user_ids\\\":[\\\"${_worker_id}\\\"]}}\" \\\n 2>&1) || true\n _http_code=$(echo \"${_raw}\" | tail -1 | sed 's/HTTP_CODE://')\n _notify_resp=$(echo \"${_raw}\" | sed '$d')\n if echo \"${_notify_resp}\" | jq -e '.event_id' > /dev/null 2>&1; then\n log \" Notified ${_worker_name}\"; _notify_ok=true\n else\n log \" WARNING: Failed to notify ${_worker_name} (HTTP ${_http_code}): ${_notify_resp}\"\n fi\n fi\n done\n fi\n # Record timestamp only if at least one notification succeeded\n if [ \"${_notify_ok}\" = true ]; then\n echo \"${_now}\" > \"${NOTIFY_TS_FILE}\"\n fi\n rm -f /root/manager-workspace/.upgrade-pending-worker-notify\n fi\nfi\n\n# ============================================================\n# Start Manager Agent\n# ============================================================\nlog \"Starting Manager Agent (${MANAGER_RUNTIME})...\"\n\n# HOME is already set to /root/manager-workspace via docker run -e HOME=...\ncd \"${HOME}\"\n\n# Ensure host credential symlinks exist under HOME\nif [ -d \"/host-share\" ]; then\n [ -f \"/host-share/.gitconfig\" ] && ln -sf \"/host-share/.gitconfig\" \"${HOME}/.gitconfig\"\nfi\n\nlog \"HOME=${HOME} (manager-workspace, host-mounted)\"\n\n# ── Render agent doc templates ────────────────────────────────────────────\n# Replace ${VAR} placeholders with actual values so the AI agent reads\n# plain text and never needs to resolve environment variables.\nexport MANAGER_MATRIX_TOKEN MANAGER_TOKEN HIGRESS_COOKIE_FILE\nRENDER=/opt/hiclaw/scripts/lib/render-skills.sh\nlog \"Rendering agent doc templates...\"\n# Manager-owned docs (workspace)\nbash \"$RENDER\" /root/manager-workspace/skills\nbash \"$RENDER\" /root/manager-workspace/skills-alpha\nbash \"$RENDER\" /root/manager-workspace AGENTS.md TOOLS.md HEARTBEAT.md SOUL.md\n# Worker templates (workspace + image) — rendered before push to MinIO\n# so Workers (including remote pip-install) receive plain text\nbash \"$RENDER\" /root/manager-workspace/worker-skills\nbash \"$RENDER\" /root/manager-workspace/worker-agent\nbash \"$RENDER\" /root/manager-workspace/copaw-worker-agent\nbash \"$RENDER\" /opt/hiclaw/agent/worker-skills\nbash \"$RENDER\" /opt/hiclaw/agent/worker-agent\nbash \"$RENDER\" /opt/hiclaw/agent/copaw-worker-agent\nlog \"Agent doc templates rendered\"\n\n# Cloud mode: start background file sync (workspace ↔ OSS) and initial push\nif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n log \"Syncing initial workspace to OSS...\"\n ensure_mc_credentials\n mc mirror /root/manager-workspace/ \"${HICLAW_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" 2>/dev/null || true\n\n # Local → OSS: change-triggered sync\n (\n while true; do\n CHANGED=$(find /root/manager-workspace/ -type f -newermt \"15 seconds ago\" 2>/dev/null | head -1)\n if [ -n \"${CHANGED}\" ]; then\n ensure_mc_credentials 2>/dev/null || true\n mc mirror /root/manager-workspace/ \"${HICLAW_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" --exclude \".npm/**\" \\\n --exclude \".local/**\" --exclude \".mc/**\" 2>/dev/null || true\n fi\n sleep 10\n done\n ) &\n log \"Local→OSS sync started (PID: $!)\"\n\n # OSS → Local: periodic pull (shared data, agent configs)\n (\n while true; do\n sleep 300\n ensure_mc_credentials 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/shared/\" /root/hiclaw-fs/shared/ --overwrite --newer-than \"5m\" 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/agents/\" /root/hiclaw-fs/agents/ --overwrite --newer-than \"5m\" 2>/dev/null || true\n done\n ) &\n log \"OSS→Local sync started (every 5m, PID: $!)\"\nfi\n\n# ============================================================\n# Auto-generate Manager mcporter config for pre-configured MCP servers\n# If HICLAW_GITHUB_TOKEN was set at install time, setup-higress.sh already\n# configured GitHub MCP on Higress. Run setup-mcp-server.sh now so that\n# config/mcporter.json exists when the Agent starts — no need to ask user for PAT.\n# ============================================================\nif [ -n \"${HICLAW_GITHUB_TOKEN}\" ] && [ \"${HICLAW_RUNTIME}\" != \"aliyun\" ]; then\n if [ ! -f \"${HOME}/config/mcporter.json\" ]; then\n log \"Auto-generating Manager mcporter config for GitHub MCP (HICLAW_GITHUB_TOKEN set)...\"\n bash /opt/hiclaw/agent/skills/mcp-server-management/scripts/setup-mcp-server.sh \\\n github \"${HICLAW_GITHUB_TOKEN}\" 2>&1 | while IFS= read -r line; do log \" [setup-mcp] ${line}\"; done || \\\n log \"WARNING: setup-mcp-server.sh failed — Agent may need to configure GitHub MCP manually\"\n else\n log \"Manager mcporter config already exists, skipping auto-generate\"\n fi\nfi\n\n# ============================================================\n# Runtime-specific startup\n# ============================================================\nif [ \"${MANAGER_RUNTIME}\" = \"copaw\" ]; then\n # Delegate to CoPaw startup script\n exec /opt/hiclaw/scripts/init/start-copaw-manager.sh\nelse\n # ── OpenClaw Runtime ─────────────────────────────────────────────────────\n log \"Starting OpenClaw Manager...\"\n\n export OPENCLAW_CONFIG_PATH=\"/root/manager-workspace/openclaw.json\"\n\n # Symlink to default OpenClaw config path so CLI commands find the config\n mkdir -p \"${HOME}/.openclaw\"\n ln -sf \"/root/manager-workspace/openclaw.json\" \"${HOME}/.openclaw/openclaw.json\"\n\n # Clean orphaned session write locks (e.g. from SIGKILL or crash before exit handlers)\n # Prevents \"session file locked (timeout 10000ms)\" when PID was reused\n find \"${HOME}/.openclaw/agents\" -name \"*.jsonl.lock\" -delete 2>/dev/null || true\n log \"Cleaned up any orphaned session write locks\"\n\n # Clean Matrix crypto storage (SQLite WAL may be corrupted after unclean shutdown)\n # Crypto state is re-negotiated on startup; losing it only means re-establishing E2EE sessions\n rm -rf \"${HOME}/.openclaw/matrix\" 2>/dev/null || true\n log \"Cleaned Matrix crypto storage (will re-establish E2EE sessions)\"\n\n # Launch OpenClaw\n exec openclaw gateway run --verbose --force\nfi\n"}, "0042e5543bc9ef0a5c1c5277fb64505af366364c": {"t": 1785764765.2941647, "n": 50729, "h": "39746dfd46b59cab", "c": "#!/bin/bash\n# start-manager-agent.sh - Initialize and start the Manager Agent\n# Supports both local (supervisord) and cloud (SAE single-process) deployments.\n# In local mode this is the last supervisord component to start (priority 800).\n# In cloud mode (HICLAW_RUNTIME=aliyun) this is the container entrypoint.\n\nsource /opt/hiclaw/scripts/lib/hiclaw-env.sh\n\n# ============================================================\n# Set timezone from TZ env var\n# ============================================================\nif [ -n \"${TZ}\" ] && [ -f \"/usr/share/zoneinfo/${TZ}\" ]; then\n ln -sf \"/usr/share/zoneinfo/${TZ}\" /etc/localtime\n echo \"${TZ}\" > /etc/timezone\n log \"Timezone set to ${TZ}\"\nfi\n\nMATRIX_DOMAIN=\"${HICLAW_MATRIX_DOMAIN:-matrix-local.hiclaw.io:8080}\"\nAI_GATEWAY_DOMAIN=\"${HICLAW_AI_GATEWAY_DOMAIN:-aigw-local.hiclaw.io}\"\n\n# ============================================================\n# Cloud mode: validate required environment variables + initial credentials\n# ============================================================\nif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n : \"${HICLAW_MATRIX_URL:?HICLAW_MATRIX_URL is required}\"\n : \"${HICLAW_MATRIX_DOMAIN:?HICLAW_MATRIX_DOMAIN is required}\"\n : \"${HICLAW_AI_GATEWAY_URL:?HICLAW_AI_GATEWAY_URL is required}\"\n : \"${HICLAW_MANAGER_GATEWAY_KEY:?HICLAW_MANAGER_GATEWAY_KEY is required}\"\n : \"${HICLAW_MANAGER_PASSWORD:?HICLAW_MANAGER_PASSWORD is required (cloud containers are stateless, password must be injected)}\"\n : \"${HICLAW_REGISTRATION_TOKEN:?HICLAW_REGISTRATION_TOKEN is required}\"\n : \"${HICLAW_ADMIN_USER:?HICLAW_ADMIN_USER is required}\"\n : \"${HICLAW_ADMIN_PASSWORD:?HICLAW_ADMIN_PASSWORD is required}\"\n log \"Cloud mode: validating environment... OK\"\n log \" Matrix: ${HICLAW_MATRIX_SERVER}, AI Gateway: ${HICLAW_AI_GATEWAY_URL}, OSS: ${HICLAW_STORAGE_BUCKET}\"\n ensure_mc_credentials || { log \"FATAL: Initial STS credential fetch failed\"; exit 1; }\nfi\n\n# ============================================================\n# Local mode: host symlinks, /etc/hosts, wait for local services\n# ============================================================\nif [ \"${HICLAW_RUNTIME}\" != \"aliyun\" ]; then\n # Create symlink for host directory access\n if [ -d \"/host-share\" ]; then\n ORIGINAL_HOST_HOME=\"${HOST_ORIGINAL_HOME:-$HOME}\"\n if [ ! -e \"${ORIGINAL_HOST_HOME}\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/root\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/data\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/host-share\" ]; then\n mkdir -p \"$(dirname \"${ORIGINAL_HOST_HOME}\")\"\n ln -sfn /host-share \"${ORIGINAL_HOST_HOME}\"\n log \"Created symlink: ${ORIGINAL_HOST_HOME} -> /host-share\"\n else\n ln -sfn /host-share /root/host-home\n log \"Created fallback symlink: /root/host-home -> /host-share\"\n fi\n fi\n\n # Add local domains to /etc/hosts\n HOSTS_DOMAINS=\"${MATRIX_DOMAIN%%:*} ${HICLAW_MATRIX_CLIENT_DOMAIN:-matrix-client-local.hiclaw.io} ${AI_GATEWAY_DOMAIN} ${HICLAW_FS_DOMAIN:-fs-local.hiclaw.io}\"\n if ! grep -q \"${AI_GATEWAY_DOMAIN}\" /etc/hosts 2>/dev/null; then\n echo \"127.0.0.1 ${HOSTS_DOMAINS}\" >> /etc/hosts\n log \"Added local domains to /etc/hosts\"\n fi\n\n # Wait for local infrastructure\n waitForService \"Higress Gateway\" \"127.0.0.1\" 8080 180\n waitForService \"Higress Console\" \"127.0.0.1\" 8001 180\n waitForService \"Tuwunel\" \"127.0.0.1\" 6167 120\n waitForHTTP \"Tuwunel Matrix API\" \"${HICLAW_MATRIX_SERVER}/_tuwunel/server_version\" 120\n waitForService \"MinIO\" \"127.0.0.1\" 9000 120\nelse\n # Cloud mode: wait for external Tuwunel\n log \"Waiting for Tuwunel Matrix server at ${HICLAW_MATRIX_SERVER}...\"\n _retry=0\n while [ \"${_retry}\" -lt 30 ]; do\n if curl -sf \"${HICLAW_MATRIX_SERVER}/_matrix/client/versions\" > /dev/null 2>&1; then\n log \"Tuwunel is ready\"\n break\n fi\n _retry=$((_retry + 1))\n log \" Waiting for Tuwunel (attempt ${_retry}/30)...\"\n sleep 5\n done\n if [ \"${_retry}\" -ge 30 ]; then\n log \"ERROR: Tuwunel not reachable at ${HICLAW_MATRIX_SERVER}\"\n exit 1\n fi\nfi\n\n# ============================================================\n# Auto-generate secrets if not provided via environment\n# Persisted to /data so they survive container restart\n# ============================================================\nSECRETS_FILE=\"/data/hiclaw-secrets.env\"\nif [ -f \"${SECRETS_FILE}\" ]; then\n source \"${SECRETS_FILE}\"\n log \"Loaded persisted secrets from ${SECRETS_FILE}\"\nfi\n\nif [ -z \"${HICLAW_MANAGER_GATEWAY_KEY}\" ]; then\n export HICLAW_MANAGER_GATEWAY_KEY=\"$(generateKey 32)\"\n log \"Auto-generated HICLAW_MANAGER_GATEWAY_KEY\"\nfi\nif [ -z \"${HICLAW_MANAGER_PASSWORD}\" ]; then\n export HICLAW_MANAGER_PASSWORD=\"$(generateKey 16)\"\n log \"Auto-generated HICLAW_MANAGER_PASSWORD\"\nfi\n\n# Persist secrets so they survive supervisord restart\nmkdir -p /data\ncat > \"${SECRETS_FILE}\" <<EOF\nexport HICLAW_MANAGER_GATEWAY_KEY=\"${HICLAW_MANAGER_GATEWAY_KEY}\"\nexport HICLAW_MANAGER_PASSWORD=\"${HICLAW_MANAGER_PASSWORD}\"\nEOF\nchmod 600 \"${SECRETS_FILE}\"\n\n# Cloud mode: pull workspace from OSS before initialization\nif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n HICLAW_FS=\"/root/hiclaw-fs\"\n mkdir -p \"${HICLAW_FS}/shared\" \"${HICLAW_FS}/agents\"\n log \"Pulling workspace from OSS...\"\n ensure_mc_credentials\n mc mirror \"${HICLAW_STORAGE_PREFIX}/manager/\" /root/manager-workspace/ --overwrite 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/shared/\" \"${HICLAW_FS}/shared/\" --overwrite 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/agents/\" \"${HICLAW_FS}/agents/\" --overwrite 2>/dev/null || true\n # Symlink hiclaw-fs into workspace for agent access\n ln -sfn \"${HICLAW_FS}\" /root/manager-workspace/hiclaw-fs\nfi\n\n# ============================================================\n# Initialize / upgrade Manager workspace\n# First boot: full init via upgrade-builtins.sh\n# Subsequent boots: compare image version; upgrade only if changed\n# ============================================================\nmkdir -p /root/manager-workspace\n\nIMAGE_VERSION=$(cat /opt/hiclaw/agent/.builtin-version 2>/dev/null || echo \"unknown\")\nINSTALLED_VERSION=$(cat /root/manager-workspace/.builtin-version 2>/dev/null || echo \"\")\n\nif [ ! -f /root/manager-workspace/.initialized ]; then\n log \"First boot: initializing manager workspace...\"\n bash /opt/hiclaw/scripts/init/upgrade-builtins.sh\n touch /root/manager-workspace/.initialized\n log \"Manager workspace initialized (version: ${IMAGE_VERSION})\"\nelif [ \"${IMAGE_VERSION}\" != \"${INSTALLED_VERSION}\" ] || [ \"${IMAGE_VERSION}\" = \"latest\" ]; then\n log \"Upgrade detected: ${INSTALLED_VERSION} -> ${IMAGE_VERSION}${IMAGE_VERSION:+ (latest: always upgrade)}\"\n bash /opt/hiclaw/scripts/init/upgrade-builtins.sh\n log \"Manager workspace upgraded to version: ${IMAGE_VERSION}\"\nelse\n log \"Workspace up to date (version: ${IMAGE_VERSION})\"\nfi\n\n# Local mode: wait for mc mirror initialization (shared + worker data in /root/hiclaw-fs/)\nif [ \"${HICLAW_RUNTIME}\" != \"aliyun\" ]; then\n log \"Waiting for MinIO storage initialization...\"\n _minio_wait=0\n while [ ! -f /root/hiclaw-fs/.initialized ]; do\n sleep 2\n _minio_wait=$(( _minio_wait + 1 ))\n if [ \"${_minio_wait}\" -ge 60 ]; then\n log \"ERROR: MinIO storage initialization timed out after 120s\"\n exit 1\n fi\n done\n log \"MinIO storage initialized\"\nfi\n\n# ============================================================\n# Register Matrix users via Registration API (single-step, no UIAA)\n# ============================================================\nlog \"Registering human admin Matrix account...\"\ncurl -sf -X POST ${HICLAW_MATRIX_SERVER}/_matrix/client/v3/register \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"username\": \"'\"${HICLAW_ADMIN_USER}\"'\",\n \"password\": \"'\"${HICLAW_ADMIN_PASSWORD}\"'\",\n \"auth\": {\n \"type\": \"m.login.registration_token\",\n \"token\": \"'\"${HICLAW_REGISTRATION_TOKEN}\"'\"\n }\n }' > /dev/null 2>&1 || log \"Admin account may already exist\"\n\nlog \"Registering Manager Agent Matrix account...\"\ncurl -sf -X POST ${HICLAW_MATRIX_SERVER}/_matrix/client/v3/register \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"username\": \"manager\",\n \"password\": \"'\"${HICLAW_MANAGER_PASSWORD}\"'\",\n \"auth\": {\n \"type\": \"m.login.registration_token\",\n \"token\": \"'\"${HICLAW_REGISTRATION_TOKEN}\"'\"\n }\n }' > /dev/null 2>&1 || log \"Manager account may already exist\"\n\n# Get Manager Agent's Matrix access token\nlog \"Obtaining Manager Matrix access token...\"\n_LOGIN_RESPONSE=$(curl -sf -X POST ${HICLAW_MATRIX_SERVER}/_matrix/client/v3/login \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"type\": \"m.login.password\",\n \"identifier\": {\"type\": \"m.id.user\", \"user\": \"manager\"},\n \"password\": \"'\"${HICLAW_MANAGER_PASSWORD}\"'\"\n }' 2>&1)\n_LOGIN_EXIT=$?\nlog \"Matrix login HTTP exit code: ${_LOGIN_EXIT}\"\nlog \"Matrix login response: ${_LOGIN_RESPONSE}\"\n\nMANAGER_TOKEN=$(echo \"${_LOGIN_RESPONSE}\" | jq -r '.access_token' 2>/dev/null)\n\nif [ -z \"${MANAGER_TOKEN}\" ] || [ \"${MANAGER_TOKEN}\" = \"null\" ]; then\n log \"ERROR: Failed to obtain Manager Matrix token (exit=${_LOGIN_EXIT})\"\n log \"ERROR: Login response was: ${_LOGIN_RESPONSE}\"\n exit 1\nfi\nlog \"Manager Matrix token obtained (token prefix: ${MANAGER_TOKEN:0:10}...)\"\n\n# ============================================================\n# Local mode: Initialize Higress Console + configure routes\n# Cloud mode: Create admin DM room + schedule welcome message\n# ============================================================\nif [ \"${HICLAW_RUNTIME}\" != \"aliyun\" ]; then\n COOKIE_FILE=\"/tmp/higress-session-cookie\"\n\n log \"Waiting for Higress Console to be fully ready and initializing admin...\"\n INIT_DONE=false\n for i in $(seq 1 90); do\n INIT_RESULT=$(curl -s -X POST http://127.0.0.1:8001/system/init \\\n -H 'Content-Type: application/json' \\\n -d '{\"adminUser\":{\"name\":\"'\"${HICLAW_ADMIN_USER}\"'\",\"password\":\"'\"${HICLAW_ADMIN_PASSWORD}\"'\",\"displayName\":\"'\"${HICLAW_ADMIN_USER}\"'\"}}' 2>/dev/null) || true\n if echo \"${INIT_RESULT}\" | grep -qE '\"success\":true|already.?init' 2>/dev/null; then\n INIT_DONE=true\n break\n fi\n if echo \"${INIT_RESULT}\" | grep -q '\"name\"' 2>/dev/null; then\n INIT_DONE=true\n break\n fi\n sleep 2\n done\n\n if [ \"${INIT_DONE}\" != \"true\" ]; then\n log \"ERROR: Higress Console did not become ready within 180s\"\n exit 1\n fi\n log \"Higress Console init done\"\n\n log \"Logging into Higress Console...\"\n LOGIN_OK=false\n for i in $(seq 1 10); do\n HTTP_CODE=$(curl -s -o /dev/null -w '%{http_code}' -X POST http://127.0.0.1:8001/session/login \\\n -H 'Content-Type: application/json' \\\n -c \"${COOKIE_FILE}\" \\\n -d '{\"username\":\"'\"${HICLAW_ADMIN_USER}\"'\",\"password\":\"'\"${HICLAW_ADMIN_PASSWORD}\"'\"}' 2>/dev/null) || true\n if { [ \"${HTTP_CODE}\" = \"200\" ] || [ \"${HTTP_CODE}\" = \"201\" ]; } && [ -f \"${COOKIE_FILE}\" ] && [ -s \"${COOKIE_FILE}\" ]; then\n LOGIN_OK=true\n break\n fi\n log \"Login attempt $i (HTTP ${HTTP_CODE}), retrying in 3s...\"\n sleep 3\n done\n\n if [ \"${LOGIN_OK}\" != \"true\" ]; then\n log \"ERROR: Could not login to Higress Console after retries\"\n exit 1\n fi\n log \"Higress Console login successful\"\n\n VERIFY_CODE=$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8001/v1/consumers -b \"${COOKIE_FILE}\" 2>/dev/null) || true\n if [ \"${VERIFY_CODE}\" = \"200\" ]; then\n log \"Console session verified (cookie valid)\"\n else\n log \"WARNING: Console session may be invalid (verify returned HTTP ${VERIFY_CODE})\"\n rm -f \"${COOKIE_FILE}\"\n for i in $(seq 1 5); do\n curl -s -o /dev/null -w '%{http_code}' -X POST http://127.0.0.1:8001/session/login \\\n -H 'Content-Type: application/json' \\\n -c \"${COOKIE_FILE}\" \\\n -d '{\"username\":\"'\"${HICLAW_ADMIN_USER}\"'\",\"password\":\"'\"${HICLAW_ADMIN_PASSWORD}\"'\"}' 2>/dev/null\n VERIFY2=$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8001/v1/consumers -b \"${COOKIE_FILE}\" 2>/dev/null) || true\n if [ \"${VERIFY2}\" = \"200\" ]; then\n log \"Re-login successful, session verified\"\n break\n fi\n sleep 2\n done\n fi\n\n export HIGRESS_COOKIE_FILE=\"${COOKIE_FILE}\"\n\n # Configure Higress routes, consumers, MCP servers\n /opt/hiclaw/scripts/init/setup-higress.sh\nfi\n\n# ============================================================\n# Create admin DM room, persist to state.json, send welcome message\n# Runs in both local and cloud modes (idempotent)\n# ============================================================\nMANAGER_FULL_ID=\"@manager:${MATRIX_DOMAIN}\"\nADMIN_FULL_ID=\"@${HICLAW_ADMIN_USER}:${MATRIX_DOMAIN}\"\n\nlog \"Logging in as admin to create DM room...\"\n_ADMIN_LOGIN=$(curl -sf -X POST \"${HICLAW_MATRIX_SERVER}/_matrix/client/v3/login\" \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"type\": \"m.login.password\",\n \"identifier\": {\"type\": \"m.id.user\", \"user\": \"'\"${HICLAW_ADMIN_USER}\"'\"},\n \"password\": \"'\"${HICLAW_ADMIN_PASSWORD}\"'\"\n }' 2>&1) || true\n\nADMIN_MATRIX_TOKEN=$(echo \"${_ADMIN_LOGIN}\" | jq -r '.access_token // empty' 2>/dev/null)\nif [ -z \"${ADMIN_MATRIX_TOKEN}\" ]; then\n log \"WARNING: Failed to login as admin, skipping DM room creation\"\nelse\n # Search for existing DM room with Manager (idempotent)\n DM_ROOM_ID=\"\"\n _JOINED_ROOMS=$(curl -sf \"${HICLAW_MATRIX_SERVER}/_matrix/client/v3/joined_rooms\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" 2>/dev/null \\\n | jq -r '.joined_rooms[]' 2>/dev/null) || true\n for _rid in ${_JOINED_ROOMS}; do\n _members=$(curl -sf \"${HICLAW_MATRIX_SERVER}/_matrix/client/v3/rooms/${_rid}/members\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" 2>/dev/null \\\n | jq -r '.chunk[].state_key' 2>/dev/null) || continue\n _count=$(echo \"${_members}\" | wc -l | xargs)\n if [ \"${_count}\" = \"2\" ] && echo \"${_members}\" | grep -q \"@manager:\"; then\n DM_ROOM_ID=\"${_rid}\"\n break\n fi\n done\n\n if [ -n \"${DM_ROOM_ID}\" ]; then\n log \"Existing DM room found: ${DM_ROOM_ID}\"\n else\n log \"Creating DM room with Manager...\"\n _RAW=$(curl -s -w '\\nHTTP_CODE:%{http_code}' -X POST \"${HICLAW_MATRIX_SERVER}/_matrix/client/v3/createRoom\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d \"{\\\"is_direct\\\":true,\\\"invite\\\":[\\\"${MANAGER_FULL_ID}\\\"],\\\"preset\\\":\\\"trusted_private_chat\\\"}\" 2>&1) || true\n _HTTP_CODE=$(echo \"${_RAW}\" | tail -1 | sed 's/HTTP_CODE://')\n _CREATE_RESP=$(echo \"${_RAW}\" | sed '$d')\n DM_ROOM_ID=$(echo \"${_CREATE_RESP}\" | jq -r '.room_id // empty' 2>/dev/null)\n if [ -n \"${DM_ROOM_ID}\" ]; then\n log \"DM room created: ${DM_ROOM_ID}\"\n else\n log \"WARNING: Failed to create DM room (HTTP ${_HTTP_CODE}): ${_CREATE_RESP}\"\n fi\n fi\n\n # Persist admin DM room ID to state.json\n if [ -n \"${DM_ROOM_ID}\" ]; then\n STATE_SCRIPT=\"/opt/hiclaw/agent/skills/task-management/scripts/manage-state.sh\"\n if [ -f \"${STATE_SCRIPT}\" ]; then\n bash \"${STATE_SCRIPT}\" --action init 2>/dev/null || true\n bash \"${STATE_SCRIPT}\" --action set-admin-dm --room-id \"${DM_ROOM_ID}\" 2>/dev/null || true\n log \"Admin DM room persisted to state.json: ${DM_ROOM_ID}\"\n fi\n fi\n\n # Schedule welcome message in background (only on first boot)\n if [ -n \"${DM_ROOM_ID}\" ] && [ ! -f \"/root/manager-workspace/soul-configured\" ]; then\n log \"Scheduling welcome message (background, waiting for OpenClaw to start)...\"\n # Double-fork so the welcome-message process is reparented to PID 1\n # and does not become a zombie after `exec openclaw` replaces this shell.\n (\n (\n _HICLAW_LANGUAGE=\"${HICLAW_LANGUAGE:-zh}\"\n _HICLAW_TIMEZONE=\"${TZ:-Asia/Shanghai}\"\n _wait=0\n _ready=false\n while [ \"${_wait}\" -lt 300 ]; do\n if curl -sf http://127.0.0.1:18799/ > /dev/null 2>&1; then\n _ready=true\n break\n fi\n sleep 3\n _wait=$((_wait + 3))\n done\n if [ \"${_ready}\" != \"true\" ]; then\n echo \"[manager] WARNING: OpenClaw gateway not ready within 300s, skipping welcome message\"\n exit 0\n fi\n # Ensure Manager has joined the DM room before sending the welcome\n # message. Without this, there is a race between OpenClaw's Matrix\n # auto-join and the message send — the message may land before Manager\n # joins, so OpenClaw's /sync never picks it up.\n _join_ok=false\n for _join_attempt in 1 2 3; do\n if curl -sf -X POST \"${HICLAW_MATRIX_SERVER}/_matrix/client/v3/rooms/${DM_ROOM_ID}/join\" \\\n -H \"Authorization: Bearer ${MANAGER_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d '{}' > /dev/null 2>&1; then\n echo \"[manager] Manager joined DM room before welcome message\"\n _join_ok=true\n break\n fi\n sleep 2\n done\n if [ \"${_join_ok}\" != \"true\" ]; then\n echo \"[manager] WARNING: Manager join request failed after 3 attempts (may already be joined)\"\n fi\n _welcome_msg=\"This is an automated message from the HiClaw setup. This is a fresh installation.\n\n--- Installation Context ---\nUser Language: ${_HICLAW_LANGUAGE} (zh = Chinese, en = English)\nUser Timezone: ${_HICLAW_TIMEZONE} (IANA timezone identifier)\n---\n\nYou are an AI agent that manages a team of worker agents. Your identity and personality have not been configured yet — the human admin is about to meet you for the first time.\n\nPlease begin the onboarding conversation:\n\n1. Greet the admin warmly and briefly describe what you can do (coordinate workers, manage tasks, run multi-agent projects)\n2. The user has selected \\\"${_HICLAW_LANGUAGE}\\\" as their preferred language during installation. Use this language for your greeting and all subsequent communication.\n3. The user's timezone is ${_HICLAW_TIMEZONE}. Based on this timezone, you may infer their likely region and suggest additional language options.\n4. Ask them: a) What would they like to call you? b) Communication style preference? c) Any behavior guidelines? d) Confirm default language\n5. After they reply, write their preferences to ~/SOUL.md\n6. Confirm what you wrote, and ask if they would like to adjust anything\n7. Once confirmed, run: touch ~/soul-configured\n\nThe human admin will start chatting shortly.\"\n _txn_id=\"welcome-$(date +%s)\"\n _payload=$(jq -nc --arg body \"${_welcome_msg}\" '{\"msgtype\":\"m.text\",\"body\":$body}')\n _raw=$(curl -s -w '\\nHTTP_CODE:%{http_code}' -X PUT \"${HICLAW_MATRIX_SERVER}/_matrix/client/v3/rooms/${DM_ROOM_ID}/send/m.room.message/${_txn_id}\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d \"${_payload}\" 2>&1) || true\n _http_code=$(echo \"${_raw}\" | tail -1 | sed 's/HTTP_CODE://')\n _send_resp=$(echo \"${_raw}\" | sed '$d')\n if echo \"${_send_resp}\" | jq -e '.event_id' > /dev/null 2>&1; then\n echo \"[manager] Welcome message sent to DM room\"\n else\n echo \"[manager] WARNING: Failed to send welcome message (HTTP ${_http_code}): ${_send_resp}\"\n fi\n ) & )\n log \"Welcome message background process started\"\n fi\nfi\n\n# ============================================================\n# Generate Manager Agent openclaw.json from template\n# ============================================================\nlog \"Generating Manager openclaw.json...\"\nexport MANAGER_MATRIX_TOKEN=\"${MANAGER_TOKEN}\"\nexport MANAGER_GATEWAY_KEY=\"${HICLAW_MANAGER_GATEWAY_KEY}\"\n# Resolve model parameters based on model name\nMODEL_NAME=\"${HICLAW_DEFAULT_MODEL:-qwen3.5-plus}\"\ncase \"${MODEL_NAME}\" in\n gpt-5.3-codex|gpt-5-mini|gpt-5-nano)\n export MODEL_CONTEXT_WINDOW=400000 MODEL_MAX_TOKENS=128000 ;;\n claude-opus-4-6)\n export MODEL_CONTEXT_WINDOW=1000000 MODEL_MAX_TOKENS=128000 ;;\n claude-sonnet-4-6)\n export MODEL_CONTEXT_WINDOW=1000000 MODEL_MAX_TOKENS=64000 ;;\n claude-haiku-4-5)\n export MODEL_CONTEXT_WINDOW=200000 MODEL_MAX_TOKENS=64000 ;;\n qwen3.5-plus)\n export MODEL_CONTEXT_WINDOW=200000 MODEL_MAX_TOKENS=64000 ;;\n deepseek-chat|deepseek-reasoner|kimi-k2.5)\n export MODEL_CONTEXT_WINDOW=256000 MODEL_MAX_TOKENS=128000 ;;\n glm-5|MiniMax-M2.7|MiniMax-M2.7-highspeed|MiniMax-M2.5)\n export MODEL_CONTEXT_WINDOW=200000 MODEL_MAX_TOKENS=128000 ;;\n *)\n export MODEL_CONTEXT_WINDOW=150000 MODEL_MAX_TOKENS=128000 ;;\nesac\nexport MODEL_REASONING=true\n\n# Override with user-supplied custom model parameters from env (set during install)\n[ -n \"${HICLAW_MODEL_CONTEXT_WINDOW:-}\" ] && export MODEL_CONTEXT_WINDOW=\"${HICLAW_MODEL_CONTEXT_WINDOW}\"\n[ -n \"${HICLAW_MODEL_MAX_TOKENS:-}\" ] && export MODEL_MAX_TOKENS=\"${HICLAW_MODEL_MAX_TOKENS}\"\n[ -n \"${HICLAW_MODEL_REASONING:-}\" ] && export MODEL_REASONING=\"${HICLAW_MODEL_REASONING}\"\n\n# E2EE: convert HICLAW_MATRIX_E2EE to JSON boolean for template substitution\nif [ \"${HICLAW_MATRIX_E2EE:-0}\" = \"1\" ] || [ \"${HICLAW_MATRIX_E2EE:-}\" = \"true\" ]; then\n export MATRIX_E2EE_ENABLED=true\nelse\n export MATRIX_E2EE_ENABLED=false\nfi\nlog \"Matrix E2EE: ${MATRIX_E2EE_ENABLED}\"\n\n# Resolve input modalities: only vision-capable models get \"image\"\ncase \"${MODEL_NAME}\" in\n gpt-5.4|gpt-5.3-codex|gpt-5-mini|gpt-5-nano|claude-opus-4-6|claude-sonnet-4-6|claude-haiku-4-5|qwen3.5-plus|kimi-k2.5)\n export MODEL_INPUT='[\"text\", \"image\"]' ;;\n *)\n export MODEL_INPUT='[\"text\"]' ;;\nesac\n# Override with user-supplied vision setting from env\nif [ \"${HICLAW_MODEL_VISION:-}\" = \"true\" ]; then\n export MODEL_INPUT='[\"text\", \"image\"]'\nelif [ \"${HICLAW_MODEL_VISION:-}\" = \"false\" ]; then\n export MODEL_INPUT='[\"text\"]'\nfi\n\nlog \"Model: ${MODEL_NAME} (context=${MODEL_CONTEXT_WINDOW}, maxTokens=${MODEL_MAX_TOKENS}, reasoning=${MODEL_REASONING}, input=${MODEL_INPUT})\"\n\nif [ -f /root/manager-workspace/openclaw.json ]; then\n log \"Manager openclaw.json already exists, updating dynamic fields only (preserving user customizations)...\"\n # Merge known models into existing config (add missing, preserve user-added)\n # Use known-models.json (valid JSON) instead of template (contains ${VAR} placeholders)\n KNOWN_MODELS=$(cat /opt/hiclaw/configs/known-models.json 2>/dev/null || echo '[]')\n jq --arg token \"${MANAGER_TOKEN}\" \\\n --arg key \"${HICLAW_MANAGER_GATEWAY_KEY}\" \\\n --arg model \"${MODEL_NAME}\" \\\n --arg emb_model \"${HICLAW_EMBEDDING_MODEL}\" \\\n --arg aigw_domain \"${AI_GATEWAY_DOMAIN}\" \\\n --argjson e2ee \"${MATRIX_E2EE_ENABLED}\" \\\n --argjson known_models \"${KNOWN_MODELS}\" \\\n --argjson ctx \"${MODEL_CONTEXT_WINDOW}\" \\\n --argjson max \"${MODEL_MAX_TOKENS}\" \\\n --argjson reasoning \"${MODEL_REASONING}\" \\\n --argjson input \"${MODEL_INPUT}\" \\\n '\n # Merge known models: add any model id not already present\n .models.providers[\"hiclaw-gateway\"].models as $existing\n | ($existing | map(.id)) as $existing_ids\n | ($known_models | map(select(.id as $id | $existing_ids | index($id) | not))) as $new\n | .models.providers[\"hiclaw-gateway\"].models = ($existing + $new)\n # Ensure the user-chosen default model is in the list (custom model support)\n | if (.models.providers[\"hiclaw-gateway\"].models | map(.id) | index($model) | not) then\n .models.providers[\"hiclaw-gateway\"].models += [{\"id\": $model, \"name\": $model, \"reasoning\": $reasoning, \"contextWindow\": $ctx, \"maxTokens\": $max, \"input\": $input}]\n else . end\n # Rebuild model aliases from the full models list\n | (.models.providers[\"hiclaw-gateway\"].models | map({ (\"hiclaw-gateway/\" + .id): { \"alias\": .id } }) | add // {}) as $aliases\n | .agents.defaults.models = ((.agents.defaults.models // {}) + $aliases)\n | .channels.matrix.accessToken = $token | .models.providers[\"hiclaw-gateway\"].apiKey = $key\n | ((.hooks.token // \"\") as $ht | if $ht == $key or $ht == ($key + \"-hooks\" | @base64) then del(.hooks) else . end)\n | .agents.defaults.model.primary = (\"hiclaw-gateway/\" + $model)\n | .commands.restart = true\n | .gateway.controlUi.dangerouslyDisableDeviceAuth = true\n | .channels.matrix.encryption = $e2ee\n # Ensure memorySearch config exists (embedding model for memory) — skip if embedding model is empty\n | if $emb_model != \"\" then .agents.defaults.memorySearch //= {\"provider\":\"openai\",\"model\":$emb_model,\"remote\":{\"baseUrl\":(\"http://\" + $aigw_domain + \":8080/v1\"),\"apiKey\":$key}} else . end\n ' \\\n /root/manager-workspace/openclaw.json > /tmp/openclaw.json.tmp && \\\n mv /tmp/openclaw.json.tmp /root/manager-workspace/openclaw.json\n # Verify the token was written correctly\n _written_token=$(jq -r '.channels.matrix.accessToken' /root/manager-workspace/openclaw.json 2>/dev/null)\n if [ -z \"${_written_token}\" ] || [ \"${_written_token}\" = \"null\" ]; then\n log \"ERROR: Matrix token was not written correctly to openclaw.json (got: ${_written_token})\"\n else\n log \"Matrix token written to openclaw.json (prefix: ${_written_token:0:10}...)\"\n fi\nelse\n log \"Manager openclaw.json not found, generating from template...\"\n envsubst < /opt/hiclaw/configs/manager-openclaw.json.tmpl > /root/manager-workspace/openclaw.json\n # Post-envsubst injection: memorySearch + custom model (single jq pass when possible)\n if ! jq -e --arg model \"${MODEL_NAME}\" '.models.providers[\"hiclaw-gateway\"].models | map(.id) | index($model)' /root/manager-workspace/openclaw.json > /dev/null 2>&1; then\n log \"Custom model '${MODEL_NAME}' not in built-in list, injecting into config...\"\n jq --arg emb_model \"${HICLAW_EMBEDDING_MODEL}\" \\\n --arg aigw_domain \"${AI_GATEWAY_DOMAIN}\" \\\n --arg key \"${HICLAW_MANAGER_GATEWAY_KEY}\" \\\n --arg model \"${MODEL_NAME}\" \\\n --argjson ctx \"${MODEL_CONTEXT_WINDOW}\" \\\n --argjson max \"${MODEL_MAX_TOKENS}\" \\\n --argjson reasoning \"${MODEL_REASONING}\" \\\n --argjson input \"${MODEL_INPUT}\" \\\n '\n (if $emb_model != \"\" then .agents.defaults.memorySearch = {\"provider\":\"openai\",\"model\":$emb_model,\"remote\":{\"baseUrl\":(\"http://\" + $aigw_domain + \":8080/v1\"),\"apiKey\":$key}} else . end)\n | .models.providers[\"hiclaw-gateway\"].models += [{\"id\": $model, \"name\": $model, \"reasoning\": $reasoning, \"contextWindow\": $ctx, \"maxTokens\": $max, \"input\": $input}]\n | .agents.defaults.models += {(\"hiclaw-gateway/\" + $model): {\"alias\": $model}}\n ' /root/manager-workspace/openclaw.json > /tmp/openclaw.json.tmp && \\\n mv /tmp/openclaw.json.tmp /root/manager-workspace/openclaw.json\n elif [ -n \"${HICLAW_EMBEDDING_MODEL}\" ]; then\n jq --arg emb_model \"${HICLAW_EMBEDDING_MODEL}\" \\\n --arg aigw_domain \"${AI_GATEWAY_DOMAIN}\" \\\n --arg key \"${HICLAW_MANAGER_GATEWAY_KEY}\" \\\n '.agents.defaults.memorySearch = {\"provider\":\"openai\",\"model\":$emb_model,\"remote\":{\"baseUrl\":(\"http://\" + $aigw_domain + \":8080/v1\"),\"apiKey\":$key}}' \\\n /root/manager-workspace/openclaw.json > /tmp/openclaw.json.tmp && \\\n mv /tmp/openclaw.json.tmp /root/manager-workspace/openclaw.json\n fi\n _written_token=$(jq -r '.channels.matrix.accessToken' /root/manager-workspace/openclaw.json 2>/dev/null)\n log \"Matrix token written from template (prefix: ${_written_token:0:10}...)\"\nfi\n\n# Cloud mode: overlay cloud-specific settings onto generated config\nif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n log \"Applying cloud overlay to openclaw.json...\"\n jq --arg homeserver \"${HICLAW_MATRIX_SERVER}\" \\\n --arg gateway \"${HICLAW_AI_GATEWAY_URL}/v1\" \\\n --arg key \"${HICLAW_MANAGER_GATEWAY_KEY}\" \\\n '.channels.matrix.homeserver = $homeserver\n | .models.providers[\"hiclaw-gateway\"].baseUrl = $gateway\n | .models.providers[\"hiclaw-gateway\"].apiKey = $key\n | ((.hooks.token // \"\") as $ht | if $ht == $key or $ht == ($key + \"-hooks\" | @base64) then del(.hooks) else . end)\n | .commands.restart = false\n | if .agents.defaults.memorySearch then .agents.defaults.memorySearch.remote.baseUrl = $gateway | .agents.defaults.memorySearch.remote.apiKey = $key else . end' \\\n /root/manager-workspace/openclaw.json > /tmp/openclaw-cloud.json && \\\n mv /tmp/openclaw-cloud.json /root/manager-workspace/openclaw.json\n log \"Cloud overlay applied\"\nfi\n\n# ============================================================\n# Optional: enable openclaw-cms-plugin observability\n# Config is applied at runtime so secrets stay out of image layers.\n# ============================================================\nCMS_TRACES_ENABLED=\"$(echo \"${HICLAW_CMS_TRACES_ENABLED:-false}\" | tr '[:upper:]' '[:lower:]')\"\nif [ \"${CMS_TRACES_ENABLED}\" = \"true\" ]; then\n CMS_PLUGIN_NAME=\"openclaw-cms-plugin\"\n CMS_PLUGIN_DIR=\"${OPENCLAW_CMS_PLUGIN_DIR:-/opt/openclaw/extensions/openclaw-cms-plugin}\"\n CMS_PLUGIN_MANIFEST=\"${CMS_PLUGIN_DIR}/openclaw.plugin.json\"\n DIAG_PLUGIN_NAME=\"diagnostics-otel\"\n DIAG_PLUGIN_DIR=\"/opt/openclaw/extensions/diagnostics-otel\"\n CMS_LICENSE_KEY=\"${HICLAW_CMS_LICENSE_KEY:-}\"\n CMS_PROJECT=\"${HICLAW_CMS_PROJECT:-}\"\n CMS_METRICS_ENABLED=\"${HICLAW_CMS_METRICS_ENABLED:-false}\"\n\n if [ ! -f \"${CMS_PLUGIN_MANIFEST}\" ]; then\n log \"WARNING: ${CMS_PLUGIN_NAME} manifest not found at ${CMS_PLUGIN_MANIFEST}, skipping plugin config\"\n else\n _missing=0\n [ -z \"${HICLAW_CMS_ENDPOINT:-}\" ] && log \"WARNING: HICLAW_CMS_ENDPOINT is required when HICLAW_CMS_TRACES_ENABLED=true\" && _missing=1\n [ -z \"${CMS_LICENSE_KEY:-}\" ] && log \"WARNING: HICLAW_CMS_LICENSE_KEY is required when HICLAW_CMS_TRACES_ENABLED=true\" && _missing=1\n [ -z \"${HICLAW_CMS_WORKSPACE:-}\" ] && log \"WARNING: HICLAW_CMS_WORKSPACE is required when HICLAW_CMS_TRACES_ENABLED=true\" && _missing=1\n\n if [ \"${_missing}\" = \"0\" ]; then\n CMS_SERVICE_NAME=\"${HICLAW_CMS_SERVICE_NAME:-hiclaw-manager}\"\n CMS_ENABLE_METRICS=\"${CMS_METRICS_ENABLED}\"\n DIAG_AVAILABLE=\"0\"\n _metrics_lc=\"$(echo \"${CMS_ENABLE_METRICS}\" | tr '[:upper:]' '[:lower:]')\"\n if [ \"${_metrics_lc}\" = \"true\" ]; then\n if [ -f \"${DIAG_PLUGIN_DIR}/package.json\" ]; then\n DIAG_AVAILABLE=\"1\"\n if [ ! -d \"${DIAG_PLUGIN_DIR}/node_modules\" ]; then\n log \"diagnostics-otel dependencies missing, installing...\"\n if (cd \"${DIAG_PLUGIN_DIR}\" && npm install --omit=dev --ignore-scripts >/tmp/hiclaw-diag-install.log 2>&1); then\n log \"diagnostics-otel dependencies installed\"\n else\n log \"WARNING: diagnostics-otel npm install failed, metrics plugin may not load\"\n fi\n else\n log \"diagnostics-otel dependencies already present\"\n fi\n else\n log \"WARNING: diagnostics-otel package.json not found at ${DIAG_PLUGIN_DIR}, metrics plugin may not load\"\n fi\n fi\n\n log \"Applying ${CMS_PLUGIN_NAME} config to openclaw.json...\"\n jq --arg pluginName \"${CMS_PLUGIN_NAME}\" \\\n --arg pluginDir \"${CMS_PLUGIN_DIR}\" \\\n --arg endpoint \"${HICLAW_CMS_ENDPOINT}\" \\\n --arg licenseKey \"${CMS_LICENSE_KEY}\" \\\n --arg armsProject \"${CMS_PROJECT}\" \\\n --arg cmsWorkspace \"${HICLAW_CMS_WORKSPACE}\" \\\n --arg serviceName \"${CMS_SERVICE_NAME}\" \\\n --arg diagPluginName \"${DIAG_PLUGIN_NAME}\" \\\n --arg diagPluginDir \"${DIAG_PLUGIN_DIR}\" \\\n --arg metricsRaw \"${CMS_ENABLE_METRICS}\" \\\n --arg diagAvailableRaw \"${DIAG_AVAILABLE}\" \\\n '\n .plugins = (.plugins // {})\n | .plugins.load = (.plugins.load // {})\n | .plugins.entries = (.plugins.entries // {})\n | if (.plugins.allow | type) != \"array\" then .plugins.allow = [] else . end\n | if (.plugins.allow | index($pluginName)) == null then .plugins.allow += [$pluginName] else . end\n | if (.plugins.load.paths | type) != \"array\" then .plugins.load.paths = [] else . end\n | if (.plugins.load.paths | index($pluginDir)) == null then .plugins.load.paths += [$pluginDir] else . end\n | .plugins.entries[$pluginName] = {\n \"enabled\": true,\n \"config\": {\n \"endpoint\": $endpoint,\n \"headers\": {\n \"x-arms-license-key\": $licenseKey,\n \"x-arms-project\": $armsProject,\n \"x-cms-workspace\": $cmsWorkspace\n },\n \"serviceName\": $serviceName\n }\n }\n\n # diagnostics-otel metrics (optional)\n | ($metricsRaw | ascii_downcase) as $m\n | ($diagAvailableRaw == \"1\") as $diagAvailable\n | (($m == \"true\") and $diagAvailable) as $metricsEnabled\n | if $metricsEnabled then\n (if (.plugins.allow | index($diagPluginName)) == null then .plugins.allow += [$diagPluginName] else . end)\n | (if (.plugins.load.paths | index($diagPluginDir)) == null then .plugins.load.paths += [$diagPluginDir] else . end)\n | .plugins.entries[$diagPluginName].enabled = true\n | .diagnostics = (.diagnostics // {})\n | .diagnostics.otel = (.diagnostics.otel // {})\n | .diagnostics.enabled = true\n | .diagnostics.otel.enabled = true\n | .diagnostics.otel.endpoint = $endpoint\n | .diagnostics.otel.protocol = (.diagnostics.otel.protocol // \"http/protobuf\")\n | .diagnostics.otel.headers = {\n \"x-arms-license-key\": $licenseKey,\n \"x-arms-project\": $armsProject,\n \"x-cms-workspace\": $cmsWorkspace\n }\n | .diagnostics.otel.serviceName = $serviceName\n | .diagnostics.otel.metrics = true\n | .diagnostics.otel.traces = (.diagnostics.otel.traces // false)\n | .diagnostics.otel.logs = (.diagnostics.otel.logs // false)\n else\n .\n end\n ' /root/manager-workspace/openclaw.json > /tmp/openclaw-cms.json && \\\n mv /tmp/openclaw-cms.json /root/manager-workspace/openclaw.json\n log \"${CMS_PLUGIN_NAME} config applied (metrics=${CMS_ENABLE_METRICS}, service=${CMS_SERVICE_NAME})\"\n else\n log \"Skipping ${CMS_PLUGIN_NAME} config due to missing required env vars\"\n fi\n fi\nfi\n\n# ============================================================\n# Detect container runtime (for Worker creation)\n# ============================================================\nsource /opt/hiclaw/scripts/lib/container-api.sh\nif container_api_available; then\n log \"Container runtime socket detected at ${CONTAINER_SOCKET} — direct Worker creation enabled\"\n export HICLAW_CONTAINER_RUNTIME=\"socket\"\nelif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n log \"Cloud mode — Workers created via SAE API\"\n export HICLAW_CONTAINER_RUNTIME=\"cloud\"\nelse\n log \"No container runtime found — Worker creation will output install commands\"\n export HICLAW_CONTAINER_RUNTIME=\"none\"\nfi\n\n# ============================================================\n# Upgrade Worker openclaw.json: merge known models + E2EE flag into existing configs\n# Existing workers in MinIO may have old single-model configs or missing encryption field.\n# Merge template models so they can hot-switch without restart.\n# ============================================================\nREGISTRY_FILE=\"/root/manager-workspace/workers-registry.json\"\nif [ -f \"${REGISTRY_FILE}\" ]; then\n # Use known-models.json (valid JSON) instead of template (contains ${VAR} placeholders)\n KNOWN_MODELS_FILE=\"/opt/hiclaw/configs/known-models.json\"\n if [ -f \"${KNOWN_MODELS_FILE}\" ]; then\n _KNOWN_MODELS=$(cat \"${KNOWN_MODELS_FILE}\")\n for _wname in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_wname}\" ] && continue\n _minio_path=\"${HICLAW_STORAGE_PREFIX}/agents/${_wname}/openclaw.json\"\n _tmp_in=\"/tmp/openclaw-${_wname}-models-upgrade-in.json\"\n if mc cp \"${_minio_path}\" \"${_tmp_in}\" 2>/dev/null; then\n _tmp_out=\"/tmp/openclaw-${_wname}-models-upgrade-out.json\"\n # Idempotent merge: add missing known models, rebuild aliases, set e2ee.\n # Always runs — jq deduplicates by model id, so re-runs are safe.\n jq --argjson known_models \"${_KNOWN_MODELS}\" \\\n --argjson e2ee \"${MATRIX_E2EE_ENABLED}\" '\n .models.providers[\"hiclaw-gateway\"].models as $existing\n | ($existing | map(.id)) as $existing_ids\n | ($known_models | map(select(.id as $id | $existing_ids | index($id) | not))) as $new\n | .models.providers[\"hiclaw-gateway\"].models = ($existing + $new)\n | (.models.providers[\"hiclaw-gateway\"].models | map({ (\"hiclaw-gateway/\" + .id): { \"alias\": .id } }) | add // {}) as $aliases\n | .agents.defaults.models = ((.agents.defaults.models // {}) + $aliases)\n | .channels.matrix.encryption = $e2ee\n ' \"${_tmp_in}\" > \"${_tmp_out}\" 2>/dev/null\n if ! diff -q \"${_tmp_in}\" \"${_tmp_out}\" > /dev/null 2>&1; then\n if mc cp \"${_tmp_out}\" \"${_minio_path}\" 2>/dev/null; then\n _new_count=$(jq '.models.providers[\"hiclaw-gateway\"].models | length' \"${_tmp_out}\" 2>/dev/null)\n log \"Worker ${_wname}: upgraded openclaw.json (models: ${_new_count}, e2ee: ${MATRIX_E2EE_ENABLED})\"\n fi\n fi\n rm -f \"${_tmp_in}\" \"${_tmp_out}\"\n fi\n done\n fi\nfi\n\n# ============================================================\n# Ensure Worker Matrix password files exist in MinIO (E2EE fix)\n# Workers need to re-login on restart to get a fresh device_id.\n# Older workers created before this fix won't have the password file.\n# ============================================================\nif [ -f \"${REGISTRY_FILE}\" ]; then\n for _wname in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_wname}\" ] && continue\n _creds_file=\"/data/worker-creds/${_wname}.env\"\n if [ -f \"${_creds_file}\" ]; then\n # Check if password file already exists in MinIO\n if ! mc stat \"${HICLAW_STORAGE_PREFIX}/agents/${_wname}/credentials/matrix/password\" > /dev/null 2>&1; then\n source \"${_creds_file}\"\n if [ -n \"${WORKER_PASSWORD}\" ]; then\n _tmp_pw=\"/tmp/matrix-pw-${_wname}\"\n echo -n \"${WORKER_PASSWORD}\" > \"${_tmp_pw}\"\n mc cp \"${_tmp_pw}\" \"${HICLAW_STORAGE_PREFIX}/agents/${_wname}/credentials/matrix/password\" 2>/dev/null \\\n && log \"Worker ${_wname}: wrote Matrix password to MinIO (E2EE re-login fix)\" \\\n || log \"Worker ${_wname}: WARNING: failed to write Matrix password to MinIO\"\n rm -f \"${_tmp_pw}\"\n fi\n fi\n fi\n done\nfi\n\n# ============================================================\n# Recreate Worker containers as needed after Manager restart.\n# Workers are on hiclaw-net; Docker DNS resolves *-local.hiclaw.io via\n# the Manager's network aliases, so IP changes don't require worker recreation.\n# Only recreate stopped/missing workers.\n# ============================================================\nif container_api_available; then\n REGISTRY_FILE=\"/root/manager-workspace/workers-registry.json\"\n if [ -f \"${REGISTRY_FILE}\" ]; then\n for _worker_name in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_worker_name}\" ] && continue\n\n # Skip remote workers — they are not Manager-managed containers.\n _deployment=$(jq -r --arg w \"${_worker_name}\" '.workers[$w].deployment // \"local\"' \"${REGISTRY_FILE}\" 2>/dev/null)\n if [ \"${_deployment}\" = \"remote\" ]; then\n log \"Worker ${_worker_name} is remote, skipping container recreate\"\n continue\n fi\n\n _status=$(container_status_worker \"${_worker_name}\")\n if [ \"${_status}\" = \"running\" ]; then\n log \"Worker running: ${_worker_name}, skipping\"\n continue\n fi\n # Container missing or stopped — recreate.\n log \"Worker container ${_status}: ${_worker_name}, recreating...\"\n _creds_file=\"/data/worker-creds/${_worker_name}.env\"\n if [ -f \"${_creds_file}\" ]; then\n source \"${_creds_file}\"\n _runtime=$(jq -r --arg w \"${_worker_name}\" '.workers[$w].runtime // \"openclaw\"' \"${REGISTRY_FILE}\" 2>/dev/null)\n _recreated=false\n for _attempt in 1 2 3; do\n if [ \"${_runtime}\" = \"copaw\" ]; then\n container_create_copaw_worker \"${_worker_name}\" \"${_worker_name}\" \"${WORKER_MINIO_PASSWORD}\" 2>&1 && _recreated=true && break\n else\n container_create_worker \"${_worker_name}\" \"${_worker_name}\" \"${WORKER_MINIO_PASSWORD}\" 2>&1 && _recreated=true && break\n fi\n log \" Attempt ${_attempt}/3 failed for ${_worker_name}, retrying in $((5 * _attempt))s...\"\n sleep $((5 * _attempt))\n done\n if [ \"${_recreated}\" = true ]; then\n log \" Recreated ${_runtime} worker: ${_worker_name}\"\n else\n log \" ERROR: Failed to recreate ${_worker_name} after 3 attempts\"\n fi\n else\n log \" WARNING: No credentials found for ${_worker_name} (${_creds_file} missing), skipping\"\n fi\n done\n fi\nfi\n\n# ============================================================\n# Notify workers of builtin updates if upgrade happened\n# Builtin files (AGENTS.md, skills) are already synced by upgrade-builtins.sh\n#\n# Cooldown: skip notification if the last successful notify was within\n# NOTIFY_COOLDOWN_SECS (default 3600s / 1 hour). This prevents repeated\n# notifications when the Manager crash-loops and re-runs upgrade-builtins\n# on every restart (e.g. IMAGE_VERSION=latest always triggers upgrade).\n# ============================================================\nNOTIFY_COOLDOWN_SECS=\"${HICLAW_NOTIFY_COOLDOWN_SECS:-3600}\"\nNOTIFY_TS_FILE=\"/root/manager-workspace/.last-worker-notify-ts\"\n\nif [ -f /root/manager-workspace/.upgrade-pending-worker-notify ]; then\n _now=$(date +%s)\n _last_notify=$(cat \"${NOTIFY_TS_FILE}\" 2>/dev/null || echo \"0\")\n _elapsed=$(( _now - _last_notify ))\n\n if [ \"${_elapsed}\" -lt \"${NOTIFY_COOLDOWN_SECS}\" ]; then\n log \"Skipping worker builtin notification (last notify ${_elapsed}s ago, cooldown ${NOTIFY_COOLDOWN_SECS}s)\"\n rm -f /root/manager-workspace/.upgrade-pending-worker-notify\n else\n log \"Notifying workers about builtin updates...\"\n REGISTRY_FILE=\"/root/manager-workspace/workers-registry.json\"\n _notify_ok=false\n if [ -f \"${REGISTRY_FILE}\" ]; then\n for _worker_name in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_worker_name}\" ] && continue\n _room_id=$(jq -r --arg w \"${_worker_name}\" '.workers[$w].room_id // empty' \"${REGISTRY_FILE}\" 2>/dev/null)\n if [ -n \"${_room_id}\" ]; then\n _worker_id=\"@${_worker_name}:${MATRIX_DOMAIN}\"\n _txn_id=\"upgrade-$(date +%s%N)\"\n _msg=\"@${_worker_name}:${MATRIX_DOMAIN} Manager upgraded builtin files (AGENTS.md, skills). Please use your file-sync skill to sync the latest config.\"\n _raw=$(curl -s -w '\\nHTTP_CODE:%{http_code}' -X PUT \\\n \"${HICLAW_MATRIX_SERVER}/_matrix/client/v3/rooms/${_room_id}/send/m.room.message/${_txn_id}\" \\\n -H \"Authorization: Bearer ${MANAGER_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d \"{\\\"msgtype\\\":\\\"m.text\\\",\\\"body\\\":\\\"${_msg}\\\",\\\"m.mentions\\\":{\\\"user_ids\\\":[\\\"${_worker_id}\\\"]}}\" \\\n 2>&1) || true\n _http_code=$(echo \"${_raw}\" | tail -1 | sed 's/HTTP_CODE://')\n _notify_resp=$(echo \"${_raw}\" | sed '$d')\n if echo \"${_notify_resp}\" | jq -e '.event_id' > /dev/null 2>&1; then\n log \" Notified ${_worker_name}\"; _notify_ok=true\n else\n log \" WARNING: Failed to notify ${_worker_name} (HTTP ${_http_code}): ${_notify_resp}\"\n fi\n fi\n done\n fi\n # Record timestamp only if at least one notification succeeded\n if [ \"${_notify_ok}\" = true ]; then\n echo \"${_now}\" > \"${NOTIFY_TS_FILE}\"\n fi\n rm -f /root/manager-workspace/.upgrade-pending-worker-notify\n fi\nfi\n\n# ============================================================\n# Start OpenClaw Manager Agent\n# ============================================================\nlog \"Starting Manager Agent (OpenClaw)...\"\n\n# HOME is already set to /root/manager-workspace via docker run -e HOME=...\nexport OPENCLAW_CONFIG_PATH=\"/root/manager-workspace/openclaw.json\"\n\n# Symlink to default OpenClaw config path so CLI commands find the config\nmkdir -p \"${HOME}/.openclaw\"\nln -sf \"/root/manager-workspace/openclaw.json\" \"${HOME}/.openclaw/openclaw.json\"\n\n# Ensure host credential symlinks exist under HOME\nif [ -d \"/host-share\" ]; then\n [ -f \"/host-share/.gitconfig\" ] && ln -sf \"/host-share/.gitconfig\" \"${HOME}/.gitconfig\"\nfi\n\nlog \"HOME=${HOME} (manager-workspace, host-mounted)\"\ncd \"${HOME}\"\n\n# Clean orphaned session write locks (e.g. from SIGKILL or crash before exit handlers)\n# Prevents \"session file locked (timeout 10000ms)\" when PID was reused\nfind \"${HOME}/.openclaw/agents\" -name \"*.jsonl.lock\" -delete 2>/dev/null || true\nlog \"Cleaned up any orphaned session write locks\"\n\n# Clean Matrix crypto storage (SQLite WAL may be corrupted after unclean shutdown)\n# Crypto state is re-negotiated on startup; losing it only means re-establishing E2EE sessions\nrm -rf \"${HOME}/.openclaw/matrix\" 2>/dev/null || true\nlog \"Cleaned Matrix crypto storage (will re-establish E2EE sessions)\"\n\n# ── Render agent doc templates ────────────────────────────────────────────\n# Replace ${VAR} placeholders with actual values so the AI agent reads\n# plain text and never needs to resolve environment variables.\nexport MANAGER_MATRIX_TOKEN MANAGER_TOKEN HIGRESS_COOKIE_FILE\nRENDER=/opt/hiclaw/scripts/lib/render-skills.sh\nlog \"Rendering agent doc templates...\"\n# Manager-owned docs (workspace)\nbash \"$RENDER\" /root/manager-workspace/skills\nbash \"$RENDER\" /root/manager-workspace/skills-alpha\nbash \"$RENDER\" /root/manager-workspace AGENTS.md TOOLS.md HEARTBEAT.md SOUL.md\n# Worker templates (workspace + image) — rendered before push to MinIO\n# so Workers (including remote pip-install) receive plain text\nbash \"$RENDER\" /root/manager-workspace/worker-skills\nbash \"$RENDER\" /root/manager-workspace/worker-agent\nbash \"$RENDER\" /root/manager-workspace/copaw-worker-agent\nbash \"$RENDER\" /opt/hiclaw/agent/worker-skills\nbash \"$RENDER\" /opt/hiclaw/agent/worker-agent\nbash \"$RENDER\" /opt/hiclaw/agent/copaw-worker-agent\nlog \"Agent doc templates rendered\"\n\n# Cloud mode: start background file sync (workspace ↔ OSS) and initial push\nif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n log \"Syncing initial workspace to OSS...\"\n ensure_mc_credentials\n mc mirror /root/manager-workspace/ \"${HICLAW_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" 2>/dev/null || true\n\n # Local → OSS: change-triggered sync\n (\n while true; do\n CHANGED=$(find /root/manager-workspace/ -type f -newermt \"15 seconds ago\" 2>/dev/null | head -1)\n if [ -n \"${CHANGED}\" ]; then\n ensure_mc_credentials 2>/dev/null || true\n mc mirror /root/manager-workspace/ \"${HICLAW_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" --exclude \".npm/**\" \\\n --exclude \".local/**\" --exclude \".mc/**\" 2>/dev/null || true\n fi\n sleep 10\n done\n ) &\n log \"Local→OSS sync started (PID: $!)\"\n\n # OSS → Local: periodic pull (shared data, agent configs)\n (\n while true; do\n sleep 300\n ensure_mc_credentials 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/shared/\" /root/hiclaw-fs/shared/ --overwrite --newer-than \"5m\" 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/agents/\" /root/hiclaw-fs/agents/ --overwrite --newer-than \"5m\" 2>/dev/null || true\n done\n ) &\n log \"OSS→Local sync started (every 5m, PID: $!)\"\nfi\n\n# Launch OpenClaw\nexec openclaw gateway run --verbose --force\n"}, "8087c37f2479f39430977b1ffda476e812c7adb8": {"t": 1785764766.833295, "n": 49218, "h": "2c906170a39e1e2a", "c": "#!/bin/bash\n# start-manager-agent.sh - Initialize and start the Manager Agent\n# Supports both local (supervisord) and cloud (SAE single-process) deployments.\n# In local mode this is the last supervisord component to start (priority 800).\n# In cloud mode (HICLAW_RUNTIME=aliyun) this is the container entrypoint.\n\nsource /opt/hiclaw/scripts/lib/hiclaw-env.sh\n\n# ============================================================\n# Set timezone from TZ env var\n# ============================================================\nif [ -n \"${TZ}\" ] && [ -f \"/usr/share/zoneinfo/${TZ}\" ]; then\n ln -sf \"/usr/share/zoneinfo/${TZ}\" /etc/localtime\n echo \"${TZ}\" > /etc/timezone\n log \"Timezone set to ${TZ}\"\nfi\n\nMATRIX_DOMAIN=\"${HICLAW_MATRIX_DOMAIN:-matrix-local.hiclaw.io:8080}\"\nAI_GATEWAY_DOMAIN=\"${HICLAW_AI_GATEWAY_DOMAIN:-aigw-local.hiclaw.io}\"\n\n# ============================================================\n# Cloud mode: validate required environment variables + initial credentials\n# ============================================================\nif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n : \"${HICLAW_MATRIX_URL:?HICLAW_MATRIX_URL is required}\"\n : \"${HICLAW_MATRIX_DOMAIN:?HICLAW_MATRIX_DOMAIN is required}\"\n : \"${HICLAW_AI_GATEWAY_URL:?HICLAW_AI_GATEWAY_URL is required}\"\n : \"${HICLAW_MANAGER_GATEWAY_KEY:?HICLAW_MANAGER_GATEWAY_KEY is required}\"\n : \"${HICLAW_MANAGER_PASSWORD:?HICLAW_MANAGER_PASSWORD is required (cloud containers are stateless, password must be injected)}\"\n : \"${HICLAW_REGISTRATION_TOKEN:?HICLAW_REGISTRATION_TOKEN is required}\"\n : \"${HICLAW_ADMIN_USER:?HICLAW_ADMIN_USER is required}\"\n : \"${HICLAW_ADMIN_PASSWORD:?HICLAW_ADMIN_PASSWORD is required}\"\n log \"Cloud mode: validating environment... OK\"\n log \" Matrix: ${HICLAW_MATRIX_SERVER}, AI Gateway: ${HICLAW_AI_GATEWAY_URL}, OSS: ${HICLAW_STORAGE_BUCKET}\"\n ensure_mc_credentials || { log \"FATAL: Initial STS credential fetch failed\"; exit 1; }\nfi\n\n# ============================================================\n# Local mode: host symlinks, /etc/hosts, wait for local services\n# ============================================================\nif [ \"${HICLAW_RUNTIME}\" != \"aliyun\" ]; then\n # Create symlink for host directory access\n if [ -d \"/host-share\" ]; then\n ORIGINAL_HOST_HOME=\"${HOST_ORIGINAL_HOME:-$HOME}\"\n if [ ! -e \"${ORIGINAL_HOST_HOME}\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/root\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/data\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/host-share\" ]; then\n mkdir -p \"$(dirname \"${ORIGINAL_HOST_HOME}\")\"\n ln -sfn /host-share \"${ORIGINAL_HOST_HOME}\"\n log \"Created symlink: ${ORIGINAL_HOST_HOME} -> /host-share\"\n else\n ln -sfn /host-share /root/host-home\n log \"Created fallback symlink: /root/host-home -> /host-share\"\n fi\n fi\n\n # Add local domains to /etc/hosts\n HOSTS_DOMAINS=\"${MATRIX_DOMAIN%%:*} ${HICLAW_MATRIX_CLIENT_DOMAIN:-matrix-client-local.hiclaw.io} ${AI_GATEWAY_DOMAIN} ${HICLAW_FS_DOMAIN:-fs-local.hiclaw.io}\"\n if ! grep -q \"${AI_GATEWAY_DOMAIN}\" /etc/hosts 2>/dev/null; then\n echo \"127.0.0.1 ${HOSTS_DOMAINS}\" >> /etc/hosts\n log \"Added local domains to /etc/hosts\"\n fi\n\n # Wait for local infrastructure\n waitForService \"Higress Gateway\" \"127.0.0.1\" 8080 180\n waitForService \"Higress Console\" \"127.0.0.1\" 8001 180\n waitForService \"Tuwunel\" \"127.0.0.1\" 6167 120\n waitForHTTP \"Tuwunel Matrix API\" \"${HICLAW_MATRIX_SERVER}/_tuwunel/server_version\" 120\n waitForService \"MinIO\" \"127.0.0.1\" 9000 120\nelse\n # Cloud mode: wait for external Tuwunel\n log \"Waiting for Tuwunel Matrix server at ${HICLAW_MATRIX_SERVER}...\"\n _retry=0\n while [ \"${_retry}\" -lt 30 ]; do\n if curl -sf \"${HICLAW_MATRIX_SERVER}/_matrix/client/versions\" > /dev/null 2>&1; then\n log \"Tuwunel is ready\"\n break\n fi\n _retry=$((_retry + 1))\n log \" Waiting for Tuwunel (attempt ${_retry}/30)...\"\n sleep 5\n done\n if [ \"${_retry}\" -ge 30 ]; then\n log \"ERROR: Tuwunel not reachable at ${HICLAW_MATRIX_SERVER}\"\n exit 1\n fi\nfi\n\n# ============================================================\n# Auto-generate secrets if not provided via environment\n# Persisted to /data so they survive container restart\n# ============================================================\nSECRETS_FILE=\"/data/hiclaw-secrets.env\"\nif [ -f \"${SECRETS_FILE}\" ]; then\n source \"${SECRETS_FILE}\"\n log \"Loaded persisted secrets from ${SECRETS_FILE}\"\nfi\n\nif [ -z \"${HICLAW_MANAGER_GATEWAY_KEY}\" ]; then\n export HICLAW_MANAGER_GATEWAY_KEY=\"$(generateKey 32)\"\n log \"Auto-generated HICLAW_MANAGER_GATEWAY_KEY\"\nfi\nif [ -z \"${HICLAW_MANAGER_PASSWORD}\" ]; then\n export HICLAW_MANAGER_PASSWORD=\"$(generateKey 16)\"\n log \"Auto-generated HICLAW_MANAGER_PASSWORD\"\nfi\n\n# Persist secrets so they survive supervisord restart\nmkdir -p /data\ncat > \"${SECRETS_FILE}\" <<EOF\nexport HICLAW_MANAGER_GATEWAY_KEY=\"${HICLAW_MANAGER_GATEWAY_KEY}\"\nexport HICLAW_MANAGER_PASSWORD=\"${HICLAW_MANAGER_PASSWORD}\"\nEOF\nchmod 600 \"${SECRETS_FILE}\"\n\n# Cloud mode: pull workspace from OSS before initialization\nif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n HICLAW_FS=\"/root/hiclaw-fs\"\n mkdir -p \"${HICLAW_FS}/shared\" \"${HICLAW_FS}/agents\"\n log \"Pulling workspace from OSS...\"\n ensure_mc_credentials\n mc mirror \"${HICLAW_STORAGE_PREFIX}/manager/\" /root/manager-workspace/ --overwrite 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/shared/\" \"${HICLAW_FS}/shared/\" --overwrite 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/agents/\" \"${HICLAW_FS}/agents/\" --overwrite 2>/dev/null || true\n # Symlink hiclaw-fs into workspace for agent access\n ln -sfn \"${HICLAW_FS}\" /root/manager-workspace/hiclaw-fs\nfi\n\n# ============================================================\n# Initialize / upgrade Manager workspace\n# First boot: full init via upgrade-builtins.sh\n# Subsequent boots: compare image version; upgrade only if changed\n# ============================================================\nmkdir -p /root/manager-workspace\n\nIMAGE_VERSION=$(cat /opt/hiclaw/agent/.builtin-version 2>/dev/null || echo \"unknown\")\nINSTALLED_VERSION=$(cat /root/manager-workspace/.builtin-version 2>/dev/null || echo \"\")\n\nif [ ! -f /root/manager-workspace/.initialized ]; then\n log \"First boot: initializing manager workspace...\"\n bash /opt/hiclaw/scripts/init/upgrade-builtins.sh\n touch /root/manager-workspace/.initialized\n log \"Manager workspace initialized (version: ${IMAGE_VERSION})\"\nelif [ \"${IMAGE_VERSION}\" != \"${INSTALLED_VERSION}\" ] || [ \"${IMAGE_VERSION}\" = \"latest\" ]; then\n log \"Upgrade detected: ${INSTALLED_VERSION} -> ${IMAGE_VERSION}${IMAGE_VERSION:+ (latest: always upgrade)}\"\n bash /opt/hiclaw/scripts/init/upgrade-builtins.sh\n log \"Manager workspace upgraded to version: ${IMAGE_VERSION}\"\nelse\n log \"Workspace up to date (version: ${IMAGE_VERSION})\"\nfi\n\n# Local mode: wait for mc mirror initialization (shared + worker data in /root/hiclaw-fs/)\nif [ \"${HICLAW_RUNTIME}\" != \"aliyun\" ]; then\n log \"Waiting for MinIO storage initialization...\"\n _minio_wait=0\n while [ ! -f /root/hiclaw-fs/.initialized ]; do\n sleep 2\n _minio_wait=$(( _minio_wait + 1 ))\n if [ \"${_minio_wait}\" -ge 60 ]; then\n log \"ERROR: MinIO storage initialization timed out after 120s\"\n exit 1\n fi\n done\n log \"MinIO storage initialized\"\nfi\n\n# ============================================================\n# Register Matrix users via Registration API (single-step, no UIAA)\n# ============================================================\nlog \"Registering human admin Matrix account...\"\ncurl -sf -X POST ${HICLAW_MATRIX_SERVER}/_matrix/client/v3/register \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"username\": \"'\"${HICLAW_ADMIN_USER}\"'\",\n \"password\": \"'\"${HICLAW_ADMIN_PASSWORD}\"'\",\n \"auth\": {\n \"type\": \"m.login.registration_token\",\n \"token\": \"'\"${HICLAW_REGISTRATION_TOKEN}\"'\"\n }\n }' > /dev/null 2>&1 || log \"Admin account may already exist\"\n\nlog \"Registering Manager Agent Matrix account...\"\ncurl -sf -X POST ${HICLAW_MATRIX_SERVER}/_matrix/client/v3/register \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"username\": \"manager\",\n \"password\": \"'\"${HICLAW_MANAGER_PASSWORD}\"'\",\n \"auth\": {\n \"type\": \"m.login.registration_token\",\n \"token\": \"'\"${HICLAW_REGISTRATION_TOKEN}\"'\"\n }\n }' > /dev/null 2>&1 || log \"Manager account may already exist\"\n\n# Get Manager Agent's Matrix access token\nlog \"Obtaining Manager Matrix access token...\"\n_LOGIN_RESPONSE=$(curl -sf -X POST ${HICLAW_MATRIX_SERVER}/_matrix/client/v3/login \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"type\": \"m.login.password\",\n \"identifier\": {\"type\": \"m.id.user\", \"user\": \"manager\"},\n \"password\": \"'\"${HICLAW_MANAGER_PASSWORD}\"'\"\n }' 2>&1)\n_LOGIN_EXIT=$?\nlog \"Matrix login HTTP exit code: ${_LOGIN_EXIT}\"\nlog \"Matrix login response: ${_LOGIN_RESPONSE}\"\n\nMANAGER_TOKEN=$(echo \"${_LOGIN_RESPONSE}\" | jq -r '.access_token' 2>/dev/null)\n\nif [ -z \"${MANAGER_TOKEN}\" ] || [ \"${MANAGER_TOKEN}\" = \"null\" ]; then\n log \"ERROR: Failed to obtain Manager Matrix token (exit=${_LOGIN_EXIT})\"\n log \"ERROR: Login response was: ${_LOGIN_RESPONSE}\"\n exit 1\nfi\nlog \"Manager Matrix token obtained (token prefix: ${MANAGER_TOKEN:0:10}...)\"\n\n# ============================================================\n# Local mode: Initialize Higress Console + configure routes\n# Cloud mode: Create admin DM room + schedule welcome message\n# ============================================================\nif [ \"${HICLAW_RUNTIME}\" != \"aliyun\" ]; then\n COOKIE_FILE=\"/tmp/higress-session-cookie\"\n\n log \"Waiting for Higress Console to be fully ready and initializing admin...\"\n INIT_DONE=false\n for i in $(seq 1 90); do\n INIT_RESULT=$(curl -s -X POST http://127.0.0.1:8001/system/init \\\n -H 'Content-Type: application/json' \\\n -d '{\"adminUser\":{\"name\":\"'\"${HICLAW_ADMIN_USER}\"'\",\"password\":\"'\"${HICLAW_ADMIN_PASSWORD}\"'\",\"displayName\":\"'\"${HICLAW_ADMIN_USER}\"'\"}}' 2>/dev/null) || true\n if echo \"${INIT_RESULT}\" | grep -qE '\"success\":true|already.?init' 2>/dev/null; then\n INIT_DONE=true\n break\n fi\n if echo \"${INIT_RESULT}\" | grep -q '\"name\"' 2>/dev/null; then\n INIT_DONE=true\n break\n fi\n sleep 2\n done\n\n if [ \"${INIT_DONE}\" != \"true\" ]; then\n log \"ERROR: Higress Console did not become ready within 180s\"\n exit 1\n fi\n log \"Higress Console init done\"\n\n log \"Logging into Higress Console...\"\n LOGIN_OK=false\n for i in $(seq 1 10); do\n HTTP_CODE=$(curl -s -o /dev/null -w '%{http_code}' -X POST http://127.0.0.1:8001/session/login \\\n -H 'Content-Type: application/json' \\\n -c \"${COOKIE_FILE}\" \\\n -d '{\"username\":\"'\"${HICLAW_ADMIN_USER}\"'\",\"password\":\"'\"${HICLAW_ADMIN_PASSWORD}\"'\"}' 2>/dev/null) || true\n if { [ \"${HTTP_CODE}\" = \"200\" ] || [ \"${HTTP_CODE}\" = \"201\" ]; } && [ -f \"${COOKIE_FILE}\" ] && [ -s \"${COOKIE_FILE}\" ]; then\n LOGIN_OK=true\n break\n fi\n log \"Login attempt $i (HTTP ${HTTP_CODE}), retrying in 3s...\"\n sleep 3\n done\n\n if [ \"${LOGIN_OK}\" != \"true\" ]; then\n log \"ERROR: Could not login to Higress Console after retries\"\n exit 1\n fi\n log \"Higress Console login successful\"\n\n VERIFY_CODE=$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8001/v1/consumers -b \"${COOKIE_FILE}\" 2>/dev/null) || true\n if [ \"${VERIFY_CODE}\" = \"200\" ]; then\n log \"Console session verified (cookie valid)\"\n else\n log \"WARNING: Console session may be invalid (verify returned HTTP ${VERIFY_CODE})\"\n rm -f \"${COOKIE_FILE}\"\n for i in $(seq 1 5); do\n curl -s -o /dev/null -w '%{http_code}' -X POST http://127.0.0.1:8001/session/login \\\n -H 'Content-Type: application/json' \\\n -c \"${COOKIE_FILE}\" \\\n -d '{\"username\":\"'\"${HICLAW_ADMIN_USER}\"'\",\"password\":\"'\"${HICLAW_ADMIN_PASSWORD}\"'\"}' 2>/dev/null\n VERIFY2=$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8001/v1/consumers -b \"${COOKIE_FILE}\" 2>/dev/null) || true\n if [ \"${VERIFY2}\" = \"200\" ]; then\n log \"Re-login successful, session verified\"\n break\n fi\n sleep 2\n done\n fi\n\n export HIGRESS_COOKIE_FILE=\"${COOKIE_FILE}\"\n\n # Configure Higress routes, consumers, MCP servers\n /opt/hiclaw/scripts/init/setup-higress.sh\nfi\n\n# ============================================================\n# Create admin DM room, persist to state.json, send welcome message\n# Runs in both local and cloud modes (idempotent)\n# ============================================================\nMANAGER_FULL_ID=\"@manager:${MATRIX_DOMAIN}\"\nADMIN_FULL_ID=\"@${HICLAW_ADMIN_USER}:${MATRIX_DOMAIN}\"\n\nlog \"Logging in as admin to create DM room...\"\n_ADMIN_LOGIN=$(curl -sf -X POST \"${HICLAW_MATRIX_SERVER}/_matrix/client/v3/login\" \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"type\": \"m.login.password\",\n \"identifier\": {\"type\": \"m.id.user\", \"user\": \"'\"${HICLAW_ADMIN_USER}\"'\"},\n \"password\": \"'\"${HICLAW_ADMIN_PASSWORD}\"'\"\n }' 2>&1) || true\n\nADMIN_MATRIX_TOKEN=$(echo \"${_ADMIN_LOGIN}\" | jq -r '.access_token // empty' 2>/dev/null)\nif [ -z \"${ADMIN_MATRIX_TOKEN}\" ]; then\n log \"WARNING: Failed to login as admin, skipping DM room creation\"\nelse\n # Search for existing DM room with Manager (idempotent)\n DM_ROOM_ID=\"\"\n _JOINED_ROOMS=$(curl -sf \"${HICLAW_MATRIX_SERVER}/_matrix/client/v3/joined_rooms\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" 2>/dev/null \\\n | jq -r '.joined_rooms[]' 2>/dev/null) || true\n for _rid in ${_JOINED_ROOMS}; do\n _members=$(curl -sf \"${HICLAW_MATRIX_SERVER}/_matrix/client/v3/rooms/${_rid}/members\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" 2>/dev/null \\\n | jq -r '.chunk[].state_key' 2>/dev/null) || continue\n _count=$(echo \"${_members}\" | wc -l | xargs)\n if [ \"${_count}\" = \"2\" ] && echo \"${_members}\" | grep -q \"@manager:\"; then\n DM_ROOM_ID=\"${_rid}\"\n break\n fi\n done\n\n if [ -n \"${DM_ROOM_ID}\" ]; then\n log \"Existing DM room found: ${DM_ROOM_ID}\"\n else\n log \"Creating DM room with Manager...\"\n _RAW=$(curl -s -w '\\nHTTP_CODE:%{http_code}' -X POST \"${HICLAW_MATRIX_SERVER}/_matrix/client/v3/createRoom\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d \"{\\\"is_direct\\\":true,\\\"invite\\\":[\\\"${MANAGER_FULL_ID}\\\"],\\\"preset\\\":\\\"trusted_private_chat\\\"}\" 2>&1) || true\n _HTTP_CODE=$(echo \"${_RAW}\" | tail -1 | sed 's/HTTP_CODE://')\n _CREATE_RESP=$(echo \"${_RAW}\" | sed '$d')\n DM_ROOM_ID=$(echo \"${_CREATE_RESP}\" | jq -r '.room_id // empty' 2>/dev/null)\n if [ -n \"${DM_ROOM_ID}\" ]; then\n log \"DM room created: ${DM_ROOM_ID}\"\n else\n log \"WARNING: Failed to create DM room (HTTP ${_HTTP_CODE}): ${_CREATE_RESP}\"\n fi\n fi\n\n # Persist admin DM room ID to state.json\n if [ -n \"${DM_ROOM_ID}\" ]; then\n STATE_SCRIPT=\"/opt/hiclaw/agent/skills/task-management/scripts/manage-state.sh\"\n if [ -f \"${STATE_SCRIPT}\" ]; then\n bash \"${STATE_SCRIPT}\" --action init 2>/dev/null || true\n bash \"${STATE_SCRIPT}\" --action set-admin-dm --room-id \"${DM_ROOM_ID}\" 2>/dev/null || true\n log \"Admin DM room persisted to state.json: ${DM_ROOM_ID}\"\n fi\n fi\n\n # Schedule welcome message in background (only on first boot)\n if [ -n \"${DM_ROOM_ID}\" ] && [ ! -f \"/root/manager-workspace/soul-configured\" ]; then\n log \"Scheduling welcome message (background, waiting for OpenClaw to start)...\"\n # Double-fork so the welcome-message process is reparented to PID 1\n # and does not become a zombie after `exec openclaw` replaces this shell.\n (\n (\n _HICLAW_LANGUAGE=\"${HICLAW_LANGUAGE:-zh}\"\n _HICLAW_TIMEZONE=\"${TZ:-Asia/Shanghai}\"\n _wait=0\n _ready=false\n while [ \"${_wait}\" -lt 300 ]; do\n if curl -sf http://127.0.0.1:18799/ > /dev/null 2>&1; then\n _ready=true\n break\n fi\n sleep 3\n _wait=$((_wait + 3))\n done\n if [ \"${_ready}\" != \"true\" ]; then\n echo \"[manager] WARNING: OpenClaw gateway not ready within 300s, skipping welcome message\"\n exit 0\n fi\n # Ensure Manager has joined the DM room before sending the welcome\n # message. Without this, there is a race between OpenClaw's Matrix\n # auto-join and the message send — the message may land before Manager\n # joins, so OpenClaw's /sync never picks it up.\n _join_ok=false\n for _join_attempt in 1 2 3; do\n if curl -sf -X POST \"${HICLAW_MATRIX_SERVER}/_matrix/client/v3/rooms/${DM_ROOM_ID}/join\" \\\n -H \"Authorization: Bearer ${MANAGER_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d '{}' > /dev/null 2>&1; then\n echo \"[manager] Manager joined DM room before welcome message\"\n _join_ok=true\n break\n fi\n sleep 2\n done\n if [ \"${_join_ok}\" != \"true\" ]; then\n echo \"[manager] WARNING: Manager join request failed after 3 attempts (may already be joined)\"\n fi\n _welcome_msg=\"This is an automated message from the HiClaw setup. This is a fresh installation.\n\n--- Installation Context ---\nUser Language: ${_HICLAW_LANGUAGE} (zh = Chinese, en = English)\nUser Timezone: ${_HICLAW_TIMEZONE} (IANA timezone identifier)\n---\n\nYou are an AI agent that manages a team of worker agents. Your identity and personality have not been configured yet — the human admin is about to meet you for the first time.\n\nPlease begin the onboarding conversation:\n\n1. Greet the admin warmly and briefly describe what you can do (coordinate workers, manage tasks, run multi-agent projects)\n2. The user has selected \\\"${_HICLAW_LANGUAGE}\\\" as their preferred language during installation. Use this language for your greeting and all subsequent communication.\n3. The user's timezone is ${_HICLAW_TIMEZONE}. Based on this timezone, you may infer their likely region and suggest additional language options.\n4. Ask them: a) What would they like to call you? b) Communication style preference? c) Any behavior guidelines? d) Confirm default language\n5. After they reply, write their preferences to ~/SOUL.md\n6. Confirm what you wrote, and ask if they would like to adjust anything\n7. Once confirmed, run: touch ~/soul-configured\n\nThe human admin will start chatting shortly.\"\n _txn_id=\"welcome-$(date +%s)\"\n _payload=$(jq -nc --arg body \"${_welcome_msg}\" '{\"msgtype\":\"m.text\",\"body\":$body}')\n _raw=$(curl -s -w '\\nHTTP_CODE:%{http_code}' -X PUT \"${HICLAW_MATRIX_SERVER}/_matrix/client/v3/rooms/${DM_ROOM_ID}/send/m.room.message/${_txn_id}\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d \"${_payload}\" 2>&1) || true\n _http_code=$(echo \"${_raw}\" | tail -1 | sed 's/HTTP_CODE://')\n _send_resp=$(echo \"${_raw}\" | sed '$d')\n if echo \"${_send_resp}\" | jq -e '.event_id' > /dev/null 2>&1; then\n echo \"[manager] Welcome message sent to DM room\"\n else\n echo \"[manager] WARNING: Failed to send welcome message (HTTP ${_http_code}): ${_send_resp}\"\n fi\n ) & )\n log \"Welcome message background process started\"\n fi\nfi\n\n# ============================================================\n# Generate Manager Agent openclaw.json from template\n# ============================================================\nlog \"Generating Manager openclaw.json...\"\nexport MANAGER_MATRIX_TOKEN=\"${MANAGER_TOKEN}\"\nexport MANAGER_GATEWAY_KEY=\"${HICLAW_MANAGER_GATEWAY_KEY}\"\n# Resolve model parameters based on model name\nMODEL_NAME=\"${HICLAW_DEFAULT_MODEL:-qwen3.5-plus}\"\ncase \"${MODEL_NAME}\" in\n gpt-5.3-codex|gpt-5-mini|gpt-5-nano)\n export MODEL_CONTEXT_WINDOW=400000 MODEL_MAX_TOKENS=128000 ;;\n claude-opus-4-6)\n export MODEL_CONTEXT_WINDOW=1000000 MODEL_MAX_TOKENS=128000 ;;\n claude-sonnet-4-6)\n export MODEL_CONTEXT_WINDOW=1000000 MODEL_MAX_TOKENS=64000 ;;\n claude-haiku-4-5)\n export MODEL_CONTEXT_WINDOW=200000 MODEL_MAX_TOKENS=64000 ;;\n qwen3.5-plus)\n export MODEL_CONTEXT_WINDOW=200000 MODEL_MAX_TOKENS=64000 ;;\n deepseek-chat|deepseek-reasoner|kimi-k2.5)\n export MODEL_CONTEXT_WINDOW=256000 MODEL_MAX_TOKENS=128000 ;;\n glm-5|MiniMax-M2.7|MiniMax-M2.7-highspeed|MiniMax-M2.5)\n export MODEL_CONTEXT_WINDOW=200000 MODEL_MAX_TOKENS=128000 ;;\n *)\n export MODEL_CONTEXT_WINDOW=150000 MODEL_MAX_TOKENS=128000 ;;\nesac\nexport MODEL_REASONING=true\n\n# Override with user-supplied custom model parameters from env (set during install)\n[ -n \"${HICLAW_MODEL_CONTEXT_WINDOW:-}\" ] && export MODEL_CONTEXT_WINDOW=\"${HICLAW_MODEL_CONTEXT_WINDOW}\"\n[ -n \"${HICLAW_MODEL_MAX_TOKENS:-}\" ] && export MODEL_MAX_TOKENS=\"${HICLAW_MODEL_MAX_TOKENS}\"\n[ -n \"${HICLAW_MODEL_REASONING:-}\" ] && export MODEL_REASONING=\"${HICLAW_MODEL_REASONING}\"\n\n# E2EE: convert HICLAW_MATRIX_E2EE to JSON boolean for template substitution\nif [ \"${HICLAW_MATRIX_E2EE:-0}\" = \"1\" ] || [ \"${HICLAW_MATRIX_E2EE:-}\" = \"true\" ]; then\n export MATRIX_E2EE_ENABLED=true\nelse\n export MATRIX_E2EE_ENABLED=false\nfi\nlog \"Matrix E2EE: ${MATRIX_E2EE_ENABLED}\"\n\n# Resolve input modalities: only vision-capable models get \"image\"\ncase \"${MODEL_NAME}\" in\n gpt-5.4|gpt-5.3-codex|gpt-5-mini|gpt-5-nano|claude-opus-4-6|claude-sonnet-4-6|claude-haiku-4-5|qwen3.5-plus|kimi-k2.5)\n export MODEL_INPUT='[\"text\", \"image\"]' ;;\n *)\n export MODEL_INPUT='[\"text\"]' ;;\nesac\n# Override with user-supplied vision setting from env\nif [ \"${HICLAW_MODEL_VISION:-}\" = \"true\" ]; then\n export MODEL_INPUT='[\"text\", \"image\"]'\nelif [ \"${HICLAW_MODEL_VISION:-}\" = \"false\" ]; then\n export MODEL_INPUT='[\"text\"]'\nfi\n\nlog \"Model: ${MODEL_NAME} (context=${MODEL_CONTEXT_WINDOW}, maxTokens=${MODEL_MAX_TOKENS}, reasoning=${MODEL_REASONING}, input=${MODEL_INPUT})\"\n\nif [ -f /root/manager-workspace/openclaw.json ]; then\n log \"Manager openclaw.json already exists, updating dynamic fields only (preserving user customizations)...\"\n # Merge known models into existing config (add missing, preserve user-added)\n # Use known-models.json (valid JSON) instead of template (contains ${VAR} placeholders)\n KNOWN_MODELS=$(cat /opt/hiclaw/configs/known-models.json 2>/dev/null || echo '[]')\n jq --arg token \"${MANAGER_TOKEN}\" \\\n --arg key \"${HICLAW_MANAGER_GATEWAY_KEY}\" \\\n --arg model \"${MODEL_NAME}\" \\\n --argjson e2ee \"${MATRIX_E2EE_ENABLED}\" \\\n --argjson known_models \"${KNOWN_MODELS}\" \\\n --argjson ctx \"${MODEL_CONTEXT_WINDOW}\" \\\n --argjson max \"${MODEL_MAX_TOKENS}\" \\\n --argjson reasoning \"${MODEL_REASONING}\" \\\n --argjson input \"${MODEL_INPUT}\" \\\n '\n # Merge known models: add any model id not already present\n .models.providers[\"hiclaw-gateway\"].models as $existing\n | ($existing | map(.id)) as $existing_ids\n | ($known_models | map(select(.id as $id | $existing_ids | index($id) | not))) as $new\n | .models.providers[\"hiclaw-gateway\"].models = ($existing + $new)\n # Ensure the user-chosen default model is in the list (custom model support)\n | if (.models.providers[\"hiclaw-gateway\"].models | map(.id) | index($model) | not) then\n .models.providers[\"hiclaw-gateway\"].models += [{\"id\": $model, \"name\": $model, \"reasoning\": $reasoning, \"contextWindow\": $ctx, \"maxTokens\": $max, \"input\": $input}]\n else . end\n # Rebuild model aliases from the full models list\n | (.models.providers[\"hiclaw-gateway\"].models | map({ (\"hiclaw-gateway/\" + .id): { \"alias\": .id } }) | add // {}) as $aliases\n | .agents.defaults.models = ((.agents.defaults.models // {}) + $aliases)\n | .channels.matrix.accessToken = $token | .models.providers[\"hiclaw-gateway\"].apiKey = $key\n | ((.hooks.token // \"\") as $ht | if $ht == $key or $ht == ($key + \"-hooks\" | @base64) then del(.hooks) else . end)\n | .agents.defaults.model.primary = (\"hiclaw-gateway/\" + $model)\n | .commands.restart = true\n | .gateway.controlUi.dangerouslyDisableDeviceAuth = true\n | .channels.matrix.encryption = $e2ee\n ' \\\n /root/manager-workspace/openclaw.json > /tmp/openclaw.json.tmp && \\\n mv /tmp/openclaw.json.tmp /root/manager-workspace/openclaw.json\n # Verify the token was written correctly\n _written_token=$(jq -r '.channels.matrix.accessToken' /root/manager-workspace/openclaw.json 2>/dev/null)\n if [ -z \"${_written_token}\" ] || [ \"${_written_token}\" = \"null\" ]; then\n log \"ERROR: Matrix token was not written correctly to openclaw.json (got: ${_written_token})\"\n else\n log \"Matrix token written to openclaw.json (prefix: ${_written_token:0:10}...)\"\n fi\nelse\n log \"Manager openclaw.json not found, generating from template...\"\n envsubst < /opt/hiclaw/configs/manager-openclaw.json.tmpl > /root/manager-workspace/openclaw.json\n # Inject custom model if not in the built-in list\n if ! jq -e --arg model \"${MODEL_NAME}\" '.models.providers[\"hiclaw-gateway\"].models | map(.id) | index($model)' /root/manager-workspace/openclaw.json > /dev/null 2>&1; then\n log \"Custom model '${MODEL_NAME}' not in built-in list, injecting into config...\"\n jq --arg model \"${MODEL_NAME}\" \\\n --argjson ctx \"${MODEL_CONTEXT_WINDOW}\" \\\n --argjson max \"${MODEL_MAX_TOKENS}\" \\\n --argjson reasoning \"${MODEL_REASONING}\" \\\n --argjson input \"${MODEL_INPUT}\" \\\n '\n .models.providers[\"hiclaw-gateway\"].models += [{\"id\": $model, \"name\": $model, \"reasoning\": $reasoning, \"contextWindow\": $ctx, \"maxTokens\": $max, \"input\": $input}]\n | .agents.defaults.models += {(\"hiclaw-gateway/\" + $model): {\"alias\": $model}}\n ' /root/manager-workspace/openclaw.json > /tmp/openclaw.json.tmp && \\\n mv /tmp/openclaw.json.tmp /root/manager-workspace/openclaw.json\n fi\n _written_token=$(jq -r '.channels.matrix.accessToken' /root/manager-workspace/openclaw.json 2>/dev/null)\n log \"Matrix token written from template (prefix: ${_written_token:0:10}...)\"\nfi\n\n# Cloud mode: overlay cloud-specific settings onto generated config\nif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n log \"Applying cloud overlay to openclaw.json...\"\n jq --arg homeserver \"${HICLAW_MATRIX_SERVER}\" \\\n --arg gateway \"${HICLAW_AI_GATEWAY_URL}/v1\" \\\n --arg key \"${HICLAW_MANAGER_GATEWAY_KEY}\" \\\n '.channels.matrix.homeserver = $homeserver\n | .models.providers[\"hiclaw-gateway\"].baseUrl = $gateway\n | .models.providers[\"hiclaw-gateway\"].apiKey = $key\n | ((.hooks.token // \"\") as $ht | if $ht == $key or $ht == ($key + \"-hooks\" | @base64) then del(.hooks) else . end)\n | .commands.restart = false' \\\n /root/manager-workspace/openclaw.json > /tmp/openclaw-cloud.json && \\\n mv /tmp/openclaw-cloud.json /root/manager-workspace/openclaw.json\n log \"Cloud overlay applied\"\nfi\n\n# ============================================================\n# Optional: enable openclaw-cms-plugin observability\n# Config is applied at runtime so secrets stay out of image layers.\n# ============================================================\nCMS_TRACES_ENABLED=\"$(echo \"${HICLAW_CMS_TRACES_ENABLED:-false}\" | tr '[:upper:]' '[:lower:]')\"\nif [ \"${CMS_TRACES_ENABLED}\" = \"true\" ]; then\n CMS_PLUGIN_NAME=\"openclaw-cms-plugin\"\n CMS_PLUGIN_DIR=\"${OPENCLAW_CMS_PLUGIN_DIR:-/opt/openclaw/extensions/openclaw-cms-plugin}\"\n CMS_PLUGIN_MANIFEST=\"${CMS_PLUGIN_DIR}/openclaw.plugin.json\"\n DIAG_PLUGIN_NAME=\"diagnostics-otel\"\n DIAG_PLUGIN_DIR=\"/opt/openclaw/extensions/diagnostics-otel\"\n CMS_LICENSE_KEY=\"${HICLAW_CMS_LICENSE_KEY:-}\"\n CMS_PROJECT=\"${HICLAW_CMS_PROJECT:-}\"\n CMS_METRICS_ENABLED=\"${HICLAW_CMS_METRICS_ENABLED:-false}\"\n\n if [ ! -f \"${CMS_PLUGIN_MANIFEST}\" ]; then\n log \"WARNING: ${CMS_PLUGIN_NAME} manifest not found at ${CMS_PLUGIN_MANIFEST}, skipping plugin config\"\n else\n _missing=0\n [ -z \"${HICLAW_CMS_ENDPOINT:-}\" ] && log \"WARNING: HICLAW_CMS_ENDPOINT is required when HICLAW_CMS_TRACES_ENABLED=true\" && _missing=1\n [ -z \"${CMS_LICENSE_KEY:-}\" ] && log \"WARNING: HICLAW_CMS_LICENSE_KEY is required when HICLAW_CMS_TRACES_ENABLED=true\" && _missing=1\n [ -z \"${HICLAW_CMS_WORKSPACE:-}\" ] && log \"WARNING: HICLAW_CMS_WORKSPACE is required when HICLAW_CMS_TRACES_ENABLED=true\" && _missing=1\n\n if [ \"${_missing}\" = \"0\" ]; then\n CMS_SERVICE_NAME=\"${HICLAW_CMS_SERVICE_NAME:-hiclaw-manager}\"\n CMS_ENABLE_METRICS=\"${CMS_METRICS_ENABLED}\"\n DIAG_AVAILABLE=\"0\"\n _metrics_lc=\"$(echo \"${CMS_ENABLE_METRICS}\" | tr '[:upper:]' '[:lower:]')\"\n if [ \"${_metrics_lc}\" = \"true\" ]; then\n if [ -f \"${DIAG_PLUGIN_DIR}/package.json\" ]; then\n DIAG_AVAILABLE=\"1\"\n if [ ! -d \"${DIAG_PLUGIN_DIR}/node_modules\" ]; then\n log \"diagnostics-otel dependencies missing, installing...\"\n if (cd \"${DIAG_PLUGIN_DIR}\" && npm install --omit=dev --ignore-scripts >/tmp/hiclaw-diag-install.log 2>&1); then\n log \"diagnostics-otel dependencies installed\"\n else\n log \"WARNING: diagnostics-otel npm install failed, metrics plugin may not load\"\n fi\n else\n log \"diagnostics-otel dependencies already present\"\n fi\n else\n log \"WARNING: diagnostics-otel package.json not found at ${DIAG_PLUGIN_DIR}, metrics plugin may not load\"\n fi\n fi\n\n log \"Applying ${CMS_PLUGIN_NAME} config to openclaw.json...\"\n jq --arg pluginName \"${CMS_PLUGIN_NAME}\" \\\n --arg pluginDir \"${CMS_PLUGIN_DIR}\" \\\n --arg endpoint \"${HICLAW_CMS_ENDPOINT}\" \\\n --arg licenseKey \"${CMS_LICENSE_KEY}\" \\\n --arg armsProject \"${CMS_PROJECT}\" \\\n --arg cmsWorkspace \"${HICLAW_CMS_WORKSPACE}\" \\\n --arg serviceName \"${CMS_SERVICE_NAME}\" \\\n --arg diagPluginName \"${DIAG_PLUGIN_NAME}\" \\\n --arg diagPluginDir \"${DIAG_PLUGIN_DIR}\" \\\n --arg metricsRaw \"${CMS_ENABLE_METRICS}\" \\\n --arg diagAvailableRaw \"${DIAG_AVAILABLE}\" \\\n '\n .plugins = (.plugins // {})\n | .plugins.load = (.plugins.load // {})\n | .plugins.entries = (.plugins.entries // {})\n | if (.plugins.allow | type) != \"array\" then .plugins.allow = [] else . end\n | if (.plugins.allow | index($pluginName)) == null then .plugins.allow += [$pluginName] else . end\n | if (.plugins.load.paths | type) != \"array\" then .plugins.load.paths = [] else . end\n | if (.plugins.load.paths | index($pluginDir)) == null then .plugins.load.paths += [$pluginDir] else . end\n | .plugins.entries[$pluginName] = {\n \"enabled\": true,\n \"config\": {\n \"endpoint\": $endpoint,\n \"headers\": {\n \"x-arms-license-key\": $licenseKey,\n \"x-arms-project\": $armsProject,\n \"x-cms-workspace\": $cmsWorkspace\n },\n \"serviceName\": $serviceName\n }\n }\n\n # diagnostics-otel metrics (optional)\n | ($metricsRaw | ascii_downcase) as $m\n | ($diagAvailableRaw == \"1\") as $diagAvailable\n | (($m == \"true\") and $diagAvailable) as $metricsEnabled\n | if $metricsEnabled then\n (if (.plugins.allow | index($diagPluginName)) == null then .plugins.allow += [$diagPluginName] else . end)\n | (if (.plugins.load.paths | index($diagPluginDir)) == null then .plugins.load.paths += [$diagPluginDir] else . end)\n | .plugins.entries[$diagPluginName].enabled = true\n | .diagnostics = (.diagnostics // {})\n | .diagnostics.otel = (.diagnostics.otel // {})\n | .diagnostics.enabled = true\n | .diagnostics.otel.enabled = true\n | .diagnostics.otel.endpoint = $endpoint\n | .diagnostics.otel.protocol = (.diagnostics.otel.protocol // \"http/protobuf\")\n | .diagnostics.otel.headers = {\n \"x-arms-license-key\": $licenseKey,\n \"x-arms-project\": $armsProject,\n \"x-cms-workspace\": $cmsWorkspace\n }\n | .diagnostics.otel.serviceName = $serviceName\n | .diagnostics.otel.metrics = true\n | .diagnostics.otel.traces = (.diagnostics.otel.traces // false)\n | .diagnostics.otel.logs = (.diagnostics.otel.logs // false)\n else\n .\n end\n ' /root/manager-workspace/openclaw.json > /tmp/openclaw-cms.json && \\\n mv /tmp/openclaw-cms.json /root/manager-workspace/openclaw.json\n log \"${CMS_PLUGIN_NAME} config applied (metrics=${CMS_ENABLE_METRICS}, service=${CMS_SERVICE_NAME})\"\n else\n log \"Skipping ${CMS_PLUGIN_NAME} config due to missing required env vars\"\n fi\n fi\nfi\n\n# ============================================================\n# Detect container runtime (for Worker creation)\n# ============================================================\nsource /opt/hiclaw/scripts/lib/container-api.sh\nif container_api_available; then\n log \"Container runtime socket detected at ${CONTAINER_SOCKET} — direct Worker creation enabled\"\n export HICLAW_CONTAINER_RUNTIME=\"socket\"\nelif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n log \"Cloud mode — Workers created via SAE API\"\n export HICLAW_CONTAINER_RUNTIME=\"cloud\"\nelse\n log \"No container runtime found — Worker creation will output install commands\"\n export HICLAW_CONTAINER_RUNTIME=\"none\"\nfi\n\n# ============================================================\n# Upgrade Worker openclaw.json: merge known models + E2EE flag into existing configs\n# Existing workers in MinIO may have old single-model configs or missing encryption field.\n# Merge template models so they can hot-switch without restart.\n# ============================================================\nREGISTRY_FILE=\"/root/manager-workspace/workers-registry.json\"\nif [ -f \"${REGISTRY_FILE}\" ]; then\n # Use known-models.json (valid JSON) instead of template (contains ${VAR} placeholders)\n KNOWN_MODELS_FILE=\"/opt/hiclaw/configs/known-models.json\"\n if [ -f \"${KNOWN_MODELS_FILE}\" ]; then\n _KNOWN_MODELS=$(cat \"${KNOWN_MODELS_FILE}\")\n for _wname in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_wname}\" ] && continue\n _minio_path=\"${HICLAW_STORAGE_PREFIX}/agents/${_wname}/openclaw.json\"\n _tmp_in=\"/tmp/openclaw-${_wname}-models-upgrade-in.json\"\n if mc cp \"${_minio_path}\" \"${_tmp_in}\" 2>/dev/null; then\n _tmp_out=\"/tmp/openclaw-${_wname}-models-upgrade-out.json\"\n # Idempotent merge: add missing known models, rebuild aliases, set e2ee.\n # Always runs — jq deduplicates by model id, so re-runs are safe.\n jq --argjson known_models \"${_KNOWN_MODELS}\" \\\n --argjson e2ee \"${MATRIX_E2EE_ENABLED}\" '\n .models.providers[\"hiclaw-gateway\"].models as $existing\n | ($existing | map(.id)) as $existing_ids\n | ($known_models | map(select(.id as $id | $existing_ids | index($id) | not))) as $new\n | .models.providers[\"hiclaw-gateway\"].models = ($existing + $new)\n | (.models.providers[\"hiclaw-gateway\"].models | map({ (\"hiclaw-gateway/\" + .id): { \"alias\": .id } }) | add // {}) as $aliases\n | .agents.defaults.models = ((.agents.defaults.models // {}) + $aliases)\n | .channels.matrix.encryption = $e2ee\n ' \"${_tmp_in}\" > \"${_tmp_out}\" 2>/dev/null\n if ! diff -q \"${_tmp_in}\" \"${_tmp_out}\" > /dev/null 2>&1; then\n if mc cp \"${_tmp_out}\" \"${_minio_path}\" 2>/dev/null; then\n _new_count=$(jq '.models.providers[\"hiclaw-gateway\"].models | length' \"${_tmp_out}\" 2>/dev/null)\n log \"Worker ${_wname}: upgraded openclaw.json (models: ${_new_count}, e2ee: ${MATRIX_E2EE_ENABLED})\"\n fi\n fi\n rm -f \"${_tmp_in}\" \"${_tmp_out}\"\n fi\n done\n fi\nfi\n\n# ============================================================\n# Ensure Worker Matrix password files exist in MinIO (E2EE fix)\n# Workers need to re-login on restart to get a fresh device_id.\n# Older workers created before this fix won't have the password file.\n# ============================================================\nif [ -f \"${REGISTRY_FILE}\" ]; then\n for _wname in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_wname}\" ] && continue\n _creds_file=\"/data/worker-creds/${_wname}.env\"\n if [ -f \"${_creds_file}\" ]; then\n # Check if password file already exists in MinIO\n if ! mc stat \"${HICLAW_STORAGE_PREFIX}/agents/${_wname}/credentials/matrix/password\" > /dev/null 2>&1; then\n source \"${_creds_file}\"\n if [ -n \"${WORKER_PASSWORD}\" ]; then\n _tmp_pw=\"/tmp/matrix-pw-${_wname}\"\n echo -n \"${WORKER_PASSWORD}\" > \"${_tmp_pw}\"\n mc cp \"${_tmp_pw}\" \"${HICLAW_STORAGE_PREFIX}/agents/${_wname}/credentials/matrix/password\" 2>/dev/null \\\n && log \"Worker ${_wname}: wrote Matrix password to MinIO (E2EE re-login fix)\" \\\n || log \"Worker ${_wname}: WARNING: failed to write Matrix password to MinIO\"\n rm -f \"${_tmp_pw}\"\n fi\n fi\n fi\n done\nfi\n\n# ============================================================\n# Recreate Worker containers as needed after Manager restart.\n# Workers are on hiclaw-net; Docker DNS resolves *-local.hiclaw.io via\n# the Manager's network aliases, so IP changes don't require worker recreation.\n# Only recreate stopped/missing workers.\n# ============================================================\nif container_api_available; then\n REGISTRY_FILE=\"/root/manager-workspace/workers-registry.json\"\n if [ -f \"${REGISTRY_FILE}\" ]; then\n for _worker_name in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_worker_name}\" ] && continue\n\n # Skip remote workers — they are not Manager-managed containers.\n _deployment=$(jq -r --arg w \"${_worker_name}\" '.workers[$w].deployment // \"local\"' \"${REGISTRY_FILE}\" 2>/dev/null)\n if [ \"${_deployment}\" = \"remote\" ]; then\n log \"Worker ${_worker_name} is remote, skipping container recreate\"\n continue\n fi\n\n _status=$(container_status_worker \"${_worker_name}\")\n if [ \"${_status}\" = \"running\" ]; then\n log \"Worker running: ${_worker_name}, skipping\"\n continue\n fi\n # Container missing or stopped — recreate.\n log \"Worker container ${_status}: ${_worker_name}, recreating...\"\n _creds_file=\"/data/worker-creds/${_worker_name}.env\"\n if [ -f \"${_creds_file}\" ]; then\n source \"${_creds_file}\"\n _runtime=$(jq -r --arg w \"${_worker_name}\" '.workers[$w].runtime // \"openclaw\"' \"${REGISTRY_FILE}\" 2>/dev/null)\n _recreated=false\n for _attempt in 1 2 3; do\n if [ \"${_runtime}\" = \"copaw\" ]; then\n container_create_copaw_worker \"${_worker_name}\" \"${_worker_name}\" \"${WORKER_MINIO_PASSWORD}\" 2>&1 && _recreated=true && break\n else\n container_create_worker \"${_worker_name}\" \"${_worker_name}\" \"${WORKER_MINIO_PASSWORD}\" 2>&1 && _recreated=true && break\n fi\n log \" Attempt ${_attempt}/3 failed for ${_worker_name}, retrying in $((5 * _attempt))s...\"\n sleep $((5 * _attempt))\n done\n if [ \"${_recreated}\" = true ]; then\n log \" Recreated ${_runtime} worker: ${_worker_name}\"\n else\n log \" ERROR: Failed to recreate ${_worker_name} after 3 attempts\"\n fi\n else\n log \" WARNING: No credentials found for ${_worker_name} (${_creds_file} missing), skipping\"\n fi\n done\n fi\nfi\n\n# ============================================================\n# Notify workers of builtin updates if upgrade happened\n# Builtin files (AGENTS.md, skills) are already synced by upgrade-builtins.sh\n#\n# Cooldown: skip notification if the last successful notify was within\n# NOTIFY_COOLDOWN_SECS (default 3600s / 1 hour). This prevents repeated\n# notifications when the Manager crash-loops and re-runs upgrade-builtins\n# on every restart (e.g. IMAGE_VERSION=latest always triggers upgrade).\n# ============================================================\nNOTIFY_COOLDOWN_SECS=\"${HICLAW_NOTIFY_COOLDOWN_SECS:-3600}\"\nNOTIFY_TS_FILE=\"/root/manager-workspace/.last-worker-notify-ts\"\n\nif [ -f /root/manager-workspace/.upgrade-pending-worker-notify ]; then\n _now=$(date +%s)\n _last_notify=$(cat \"${NOTIFY_TS_FILE}\" 2>/dev/null || echo \"0\")\n _elapsed=$(( _now - _last_notify ))\n\n if [ \"${_elapsed}\" -lt \"${NOTIFY_COOLDOWN_SECS}\" ]; then\n log \"Skipping worker builtin notification (last notify ${_elapsed}s ago, cooldown ${NOTIFY_COOLDOWN_SECS}s)\"\n rm -f /root/manager-workspace/.upgrade-pending-worker-notify\n else\n log \"Notifying workers about builtin updates...\"\n REGISTRY_FILE=\"/root/manager-workspace/workers-registry.json\"\n _notify_ok=false\n if [ -f \"${REGISTRY_FILE}\" ]; then\n for _worker_name in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_worker_name}\" ] && continue\n _room_id=$(jq -r --arg w \"${_worker_name}\" '.workers[$w].room_id // empty' \"${REGISTRY_FILE}\" 2>/dev/null)\n if [ -n \"${_room_id}\" ]; then\n _worker_id=\"@${_worker_name}:${MATRIX_DOMAIN}\"\n _txn_id=\"upgrade-$(date +%s%N)\"\n _msg=\"@${_worker_name}:${MATRIX_DOMAIN} Manager upgraded builtin files (AGENTS.md, skills). Please use your file-sync skill to sync the latest config.\"\n _raw=$(curl -s -w '\\nHTTP_CODE:%{http_code}' -X PUT \\\n \"${HICLAW_MATRIX_SERVER}/_matrix/client/v3/rooms/${_room_id}/send/m.room.message/${_txn_id}\" \\\n -H \"Authorization: Bearer ${MANAGER_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d \"{\\\"msgtype\\\":\\\"m.text\\\",\\\"body\\\":\\\"${_msg}\\\",\\\"m.mentions\\\":{\\\"user_ids\\\":[\\\"${_worker_id}\\\"]}}\" \\\n 2>&1) || true\n _http_code=$(echo \"${_raw}\" | tail -1 | sed 's/HTTP_CODE://')\n _notify_resp=$(echo \"${_raw}\" | sed '$d')\n if echo \"${_notify_resp}\" | jq -e '.event_id' > /dev/null 2>&1; then\n log \" Notified ${_worker_name}\"; _notify_ok=true\n else\n log \" WARNING: Failed to notify ${_worker_name} (HTTP ${_http_code}): ${_notify_resp}\"\n fi\n fi\n done\n fi\n # Record timestamp only if at least one notification succeeded\n if [ \"${_notify_ok}\" = true ]; then\n echo \"${_now}\" > \"${NOTIFY_TS_FILE}\"\n fi\n rm -f /root/manager-workspace/.upgrade-pending-worker-notify\n fi\nfi\n\n# ============================================================\n# Start OpenClaw Manager Agent\n# ============================================================\nlog \"Starting Manager Agent (OpenClaw)...\"\n\n# HOME is already set to /root/manager-workspace via docker run -e HOME=...\nexport OPENCLAW_CONFIG_PATH=\"/root/manager-workspace/openclaw.json\"\n\n# Symlink to default OpenClaw config path so CLI commands find the config\nmkdir -p \"${HOME}/.openclaw\"\nln -sf \"/root/manager-workspace/openclaw.json\" \"${HOME}/.openclaw/openclaw.json\"\n\n# Ensure host credential symlinks exist under HOME\nif [ -d \"/host-share\" ]; then\n [ -f \"/host-share/.gitconfig\" ] && ln -sf \"/host-share/.gitconfig\" \"${HOME}/.gitconfig\"\nfi\n\nlog \"HOME=${HOME} (manager-workspace, host-mounted)\"\ncd \"${HOME}\"\n\n# Clean orphaned session write locks (e.g. from SIGKILL or crash before exit handlers)\n# Prevents \"session file locked (timeout 10000ms)\" when PID was reused\nfind \"${HOME}/.openclaw/agents\" -name \"*.jsonl.lock\" -delete 2>/dev/null || true\nlog \"Cleaned up any orphaned session write locks\"\n\n# Clean Matrix crypto storage (SQLite WAL may be corrupted after unclean shutdown)\n# Crypto state is re-negotiated on startup; losing it only means re-establishing E2EE sessions\nrm -rf \"${HOME}/.openclaw/matrix\" 2>/dev/null || true\nlog \"Cleaned Matrix crypto storage (will re-establish E2EE sessions)\"\n\n# ── Render agent doc templates ────────────────────────────────────────────\n# Replace ${VAR} placeholders with actual values so the AI agent reads\n# plain text and never needs to resolve environment variables.\nexport MANAGER_MATRIX_TOKEN MANAGER_TOKEN HIGRESS_COOKIE_FILE\nRENDER=/opt/hiclaw/scripts/lib/render-skills.sh\nlog \"Rendering agent doc templates...\"\n# Manager-owned docs (workspace)\nbash \"$RENDER\" /root/manager-workspace/skills\nbash \"$RENDER\" /root/manager-workspace/skills-alpha\nbash \"$RENDER\" /root/manager-workspace AGENTS.md TOOLS.md HEARTBEAT.md SOUL.md\n# Worker templates (workspace + image) — rendered before push to MinIO\n# so Workers (including remote pip-install) receive plain text\nbash \"$RENDER\" /root/manager-workspace/worker-skills\nbash \"$RENDER\" /root/manager-workspace/worker-agent\nbash \"$RENDER\" /root/manager-workspace/copaw-worker-agent\nbash \"$RENDER\" /opt/hiclaw/agent/worker-skills\nbash \"$RENDER\" /opt/hiclaw/agent/worker-agent\nbash \"$RENDER\" /opt/hiclaw/agent/copaw-worker-agent\nlog \"Agent doc templates rendered\"\n\n# Cloud mode: start background file sync (workspace ↔ OSS) and initial push\nif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n log \"Syncing initial workspace to OSS...\"\n ensure_mc_credentials\n mc mirror /root/manager-workspace/ \"${HICLAW_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" 2>/dev/null || true\n\n # Local → OSS: change-triggered sync\n (\n while true; do\n CHANGED=$(find /root/manager-workspace/ -type f -newermt \"15 seconds ago\" 2>/dev/null | head -1)\n if [ -n \"${CHANGED}\" ]; then\n ensure_mc_credentials 2>/dev/null || true\n mc mirror /root/manager-workspace/ \"${HICLAW_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" --exclude \".npm/**\" \\\n --exclude \".local/**\" --exclude \".mc/**\" 2>/dev/null || true\n fi\n sleep 10\n done\n ) &\n log \"Local→OSS sync started (PID: $!)\"\n\n # OSS → Local: periodic pull (shared data, agent configs)\n (\n while true; do\n sleep 300\n ensure_mc_credentials 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/shared/\" /root/hiclaw-fs/shared/ --overwrite --newer-than \"5m\" 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/agents/\" /root/hiclaw-fs/agents/ --overwrite --newer-than \"5m\" 2>/dev/null || true\n done\n ) &\n log \"OSS→Local sync started (every 5m, PID: $!)\"\nfi\n\n# Launch OpenClaw\nexec openclaw gateway run --verbose --force\n"}, "0eb635d510f9cc6782a04f100cfff18aad4e073c": {"t": 1785764768.4421406, "n": 49058, "h": "29e0ba1a70ba4659", "c": "#!/bin/bash\n# start-manager-agent.sh - Initialize and start the Manager Agent\n# Supports both local (supervisord) and cloud (SAE single-process) deployments.\n# In local mode this is the last supervisord component to start (priority 800).\n# In cloud mode (HICLAW_RUNTIME=aliyun) this is the container entrypoint.\n\nsource /opt/hiclaw/scripts/lib/hiclaw-env.sh\n\n# ============================================================\n# Set timezone from TZ env var\n# ============================================================\nif [ -n \"${TZ}\" ] && [ -f \"/usr/share/zoneinfo/${TZ}\" ]; then\n ln -sf \"/usr/share/zoneinfo/${TZ}\" /etc/localtime\n echo \"${TZ}\" > /etc/timezone\n log \"Timezone set to ${TZ}\"\nfi\n\nMATRIX_DOMAIN=\"${HICLAW_MATRIX_DOMAIN:-matrix-local.hiclaw.io:8080}\"\nAI_GATEWAY_DOMAIN=\"${HICLAW_AI_GATEWAY_DOMAIN:-aigw-local.hiclaw.io}\"\n\n# ============================================================\n# Cloud mode: validate required environment variables + initial credentials\n# ============================================================\nif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n : \"${HICLAW_MATRIX_URL:?HICLAW_MATRIX_URL is required}\"\n : \"${HICLAW_MATRIX_DOMAIN:?HICLAW_MATRIX_DOMAIN is required}\"\n : \"${HICLAW_AI_GATEWAY_URL:?HICLAW_AI_GATEWAY_URL is required}\"\n : \"${HICLAW_MANAGER_GATEWAY_KEY:?HICLAW_MANAGER_GATEWAY_KEY is required}\"\n : \"${HICLAW_MANAGER_PASSWORD:?HICLAW_MANAGER_PASSWORD is required (cloud containers are stateless, password must be injected)}\"\n : \"${HICLAW_REGISTRATION_TOKEN:?HICLAW_REGISTRATION_TOKEN is required}\"\n : \"${HICLAW_ADMIN_USER:?HICLAW_ADMIN_USER is required}\"\n : \"${HICLAW_ADMIN_PASSWORD:?HICLAW_ADMIN_PASSWORD is required}\"\n log \"Cloud mode: validating environment... OK\"\n log \" Matrix: ${HICLAW_MATRIX_SERVER}, AI Gateway: ${HICLAW_AI_GATEWAY_URL}, OSS: ${HICLAW_STORAGE_BUCKET}\"\n ensure_mc_credentials || { log \"FATAL: Initial STS credential fetch failed\"; exit 1; }\nfi\n\n# ============================================================\n# Local mode: host symlinks, /etc/hosts, wait for local services\n# ============================================================\nif [ \"${HICLAW_RUNTIME}\" != \"aliyun\" ]; then\n # Create symlink for host directory access\n if [ -d \"/host-share\" ]; then\n ORIGINAL_HOST_HOME=\"${HOST_ORIGINAL_HOME:-$HOME}\"\n if [ ! -e \"${ORIGINAL_HOST_HOME}\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/root\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/data\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/host-share\" ]; then\n mkdir -p \"$(dirname \"${ORIGINAL_HOST_HOME}\")\"\n ln -sfn /host-share \"${ORIGINAL_HOST_HOME}\"\n log \"Created symlink: ${ORIGINAL_HOST_HOME} -> /host-share\"\n else\n ln -sfn /host-share /root/host-home\n log \"Created fallback symlink: /root/host-home -> /host-share\"\n fi\n fi\n\n # Add local domains to /etc/hosts\n HOSTS_DOMAINS=\"${MATRIX_DOMAIN%%:*} ${HICLAW_MATRIX_CLIENT_DOMAIN:-matrix-client-local.hiclaw.io} ${AI_GATEWAY_DOMAIN} ${HICLAW_FS_DOMAIN:-fs-local.hiclaw.io}\"\n if ! grep -q \"${AI_GATEWAY_DOMAIN}\" /etc/hosts 2>/dev/null; then\n echo \"127.0.0.1 ${HOSTS_DOMAINS}\" >> /etc/hosts\n log \"Added local domains to /etc/hosts\"\n fi\n\n # Wait for local infrastructure\n waitForService \"Higress Gateway\" \"127.0.0.1\" 8080 180\n waitForService \"Higress Console\" \"127.0.0.1\" 8001 180\n waitForService \"Tuwunel\" \"127.0.0.1\" 6167 120\n waitForHTTP \"Tuwunel Matrix API\" \"${HICLAW_MATRIX_SERVER}/_tuwunel/server_version\" 120\n waitForService \"MinIO\" \"127.0.0.1\" 9000 120\nelse\n # Cloud mode: wait for external Tuwunel\n log \"Waiting for Tuwunel Matrix server at ${HICLAW_MATRIX_SERVER}...\"\n _retry=0\n while [ \"${_retry}\" -lt 30 ]; do\n if curl -sf \"${HICLAW_MATRIX_SERVER}/_matrix/client/versions\" > /dev/null 2>&1; then\n log \"Tuwunel is ready\"\n break\n fi\n _retry=$((_retry + 1))\n log \" Waiting for Tuwunel (attempt ${_retry}/30)...\"\n sleep 5\n done\n if [ \"${_retry}\" -ge 30 ]; then\n log \"ERROR: Tuwunel not reachable at ${HICLAW_MATRIX_SERVER}\"\n exit 1\n fi\nfi\n\n# ============================================================\n# Auto-generate secrets if not provided via environment\n# Persisted to /data so they survive container restart\n# ============================================================\nSECRETS_FILE=\"/data/hiclaw-secrets.env\"\nif [ -f \"${SECRETS_FILE}\" ]; then\n source \"${SECRETS_FILE}\"\n log \"Loaded persisted secrets from ${SECRETS_FILE}\"\nfi\n\nif [ -z \"${HICLAW_MANAGER_GATEWAY_KEY}\" ]; then\n export HICLAW_MANAGER_GATEWAY_KEY=\"$(generateKey 32)\"\n log \"Auto-generated HICLAW_MANAGER_GATEWAY_KEY\"\nfi\nif [ -z \"${HICLAW_MANAGER_PASSWORD}\" ]; then\n export HICLAW_MANAGER_PASSWORD=\"$(generateKey 16)\"\n log \"Auto-generated HICLAW_MANAGER_PASSWORD\"\nfi\n\n# Persist secrets so they survive supervisord restart\nmkdir -p /data\ncat > \"${SECRETS_FILE}\" <<EOF\nexport HICLAW_MANAGER_GATEWAY_KEY=\"${HICLAW_MANAGER_GATEWAY_KEY}\"\nexport HICLAW_MANAGER_PASSWORD=\"${HICLAW_MANAGER_PASSWORD}\"\nEOF\nchmod 600 \"${SECRETS_FILE}\"\n\n# Cloud mode: pull workspace from OSS before initialization\nif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n HICLAW_FS=\"/root/hiclaw-fs\"\n mkdir -p \"${HICLAW_FS}/shared\" \"${HICLAW_FS}/agents\"\n log \"Pulling workspace from OSS...\"\n ensure_mc_credentials\n mc mirror \"${HICLAW_STORAGE_PREFIX}/manager/\" /root/manager-workspace/ --overwrite 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/shared/\" \"${HICLAW_FS}/shared/\" --overwrite 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/agents/\" \"${HICLAW_FS}/agents/\" --overwrite 2>/dev/null || true\n # Symlink hiclaw-fs into workspace for agent access\n ln -sfn \"${HICLAW_FS}\" /root/manager-workspace/hiclaw-fs\nfi\n\n# ============================================================\n# Initialize / upgrade Manager workspace\n# First boot: full init via upgrade-builtins.sh\n# Subsequent boots: compare image version; upgrade only if changed\n# ============================================================\nmkdir -p /root/manager-workspace\n\nIMAGE_VERSION=$(cat /opt/hiclaw/agent/.builtin-version 2>/dev/null || echo \"unknown\")\nINSTALLED_VERSION=$(cat /root/manager-workspace/.builtin-version 2>/dev/null || echo \"\")\n\nif [ ! -f /root/manager-workspace/.initialized ]; then\n log \"First boot: initializing manager workspace...\"\n bash /opt/hiclaw/scripts/init/upgrade-builtins.sh\n touch /root/manager-workspace/.initialized\n log \"Manager workspace initialized (version: ${IMAGE_VERSION})\"\nelif [ \"${IMAGE_VERSION}\" != \"${INSTALLED_VERSION}\" ] || [ \"${IMAGE_VERSION}\" = \"latest\" ]; then\n log \"Upgrade detected: ${INSTALLED_VERSION} -> ${IMAGE_VERSION}${IMAGE_VERSION:+ (latest: always upgrade)}\"\n bash /opt/hiclaw/scripts/init/upgrade-builtins.sh\n log \"Manager workspace upgraded to version: ${IMAGE_VERSION}\"\nelse\n log \"Workspace up to date (version: ${IMAGE_VERSION})\"\nfi\n\n# Local mode: wait for mc mirror initialization (shared + worker data in /root/hiclaw-fs/)\nif [ \"${HICLAW_RUNTIME}\" != \"aliyun\" ]; then\n log \"Waiting for MinIO storage initialization...\"\n _minio_wait=0\n while [ ! -f /root/hiclaw-fs/.initialized ]; do\n sleep 2\n _minio_wait=$(( _minio_wait + 1 ))\n if [ \"${_minio_wait}\" -ge 60 ]; then\n log \"ERROR: MinIO storage initialization timed out after 120s\"\n exit 1\n fi\n done\n log \"MinIO storage initialized\"\nfi\n\n# ============================================================\n# Register Matrix users via Registration API (single-step, no UIAA)\n# ============================================================\nlog \"Registering human admin Matrix account...\"\ncurl -sf -X POST ${HICLAW_MATRIX_SERVER}/_matrix/client/v3/register \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"username\": \"'\"${HICLAW_ADMIN_USER}\"'\",\n \"password\": \"'\"${HICLAW_ADMIN_PASSWORD}\"'\",\n \"auth\": {\n \"type\": \"m.login.registration_token\",\n \"token\": \"'\"${HICLAW_REGISTRATION_TOKEN}\"'\"\n }\n }' > /dev/null 2>&1 || log \"Admin account may already exist\"\n\nlog \"Registering Manager Agent Matrix account...\"\ncurl -sf -X POST ${HICLAW_MATRIX_SERVER}/_matrix/client/v3/register \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"username\": \"manager\",\n \"password\": \"'\"${HICLAW_MANAGER_PASSWORD}\"'\",\n \"auth\": {\n \"type\": \"m.login.registration_token\",\n \"token\": \"'\"${HICLAW_REGISTRATION_TOKEN}\"'\"\n }\n }' > /dev/null 2>&1 || log \"Manager account may already exist\"\n\n# Get Manager Agent's Matrix access token\nlog \"Obtaining Manager Matrix access token...\"\n_LOGIN_RESPONSE=$(curl -sf -X POST ${HICLAW_MATRIX_SERVER}/_matrix/client/v3/login \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"type\": \"m.login.password\",\n \"identifier\": {\"type\": \"m.id.user\", \"user\": \"manager\"},\n \"password\": \"'\"${HICLAW_MANAGER_PASSWORD}\"'\"\n }' 2>&1)\n_LOGIN_EXIT=$?\nlog \"Matrix login HTTP exit code: ${_LOGIN_EXIT}\"\nlog \"Matrix login response: ${_LOGIN_RESPONSE}\"\n\nMANAGER_TOKEN=$(echo \"${_LOGIN_RESPONSE}\" | jq -r '.access_token' 2>/dev/null)\n\nif [ -z \"${MANAGER_TOKEN}\" ] || [ \"${MANAGER_TOKEN}\" = \"null\" ]; then\n log \"ERROR: Failed to obtain Manager Matrix token (exit=${_LOGIN_EXIT})\"\n log \"ERROR: Login response was: ${_LOGIN_RESPONSE}\"\n exit 1\nfi\nlog \"Manager Matrix token obtained (token prefix: ${MANAGER_TOKEN:0:10}...)\"\n\n# ============================================================\n# Local mode: Initialize Higress Console + configure routes\n# Cloud mode: Create admin DM room + schedule welcome message\n# ============================================================\nif [ \"${HICLAW_RUNTIME}\" != \"aliyun\" ]; then\n COOKIE_FILE=\"/tmp/higress-session-cookie\"\n\n log \"Waiting for Higress Console to be fully ready and initializing admin...\"\n INIT_DONE=false\n for i in $(seq 1 90); do\n INIT_RESULT=$(curl -s -X POST http://127.0.0.1:8001/system/init \\\n -H 'Content-Type: application/json' \\\n -d '{\"adminUser\":{\"name\":\"'\"${HICLAW_ADMIN_USER}\"'\",\"password\":\"'\"${HICLAW_ADMIN_PASSWORD}\"'\",\"displayName\":\"'\"${HICLAW_ADMIN_USER}\"'\"}}' 2>/dev/null) || true\n if echo \"${INIT_RESULT}\" | grep -qE '\"success\":true|already.?init' 2>/dev/null; then\n INIT_DONE=true\n break\n fi\n if echo \"${INIT_RESULT}\" | grep -q '\"name\"' 2>/dev/null; then\n INIT_DONE=true\n break\n fi\n sleep 2\n done\n\n if [ \"${INIT_DONE}\" != \"true\" ]; then\n log \"ERROR: Higress Console did not become ready within 180s\"\n exit 1\n fi\n log \"Higress Console init done\"\n\n log \"Logging into Higress Console...\"\n LOGIN_OK=false\n for i in $(seq 1 10); do\n HTTP_CODE=$(curl -s -o /dev/null -w '%{http_code}' -X POST http://127.0.0.1:8001/session/login \\\n -H 'Content-Type: application/json' \\\n -c \"${COOKIE_FILE}\" \\\n -d '{\"username\":\"'\"${HICLAW_ADMIN_USER}\"'\",\"password\":\"'\"${HICLAW_ADMIN_PASSWORD}\"'\"}' 2>/dev/null) || true\n if { [ \"${HTTP_CODE}\" = \"200\" ] || [ \"${HTTP_CODE}\" = \"201\" ]; } && [ -f \"${COOKIE_FILE}\" ] && [ -s \"${COOKIE_FILE}\" ]; then\n LOGIN_OK=true\n break\n fi\n log \"Login attempt $i (HTTP ${HTTP_CODE}), retrying in 3s...\"\n sleep 3\n done\n\n if [ \"${LOGIN_OK}\" != \"true\" ]; then\n log \"ERROR: Could not login to Higress Console after retries\"\n exit 1\n fi\n log \"Higress Console login successful\"\n\n VERIFY_CODE=$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8001/v1/consumers -b \"${COOKIE_FILE}\" 2>/dev/null) || true\n if [ \"${VERIFY_CODE}\" = \"200\" ]; then\n log \"Console session verified (cookie valid)\"\n else\n log \"WARNING: Console session may be invalid (verify returned HTTP ${VERIFY_CODE})\"\n rm -f \"${COOKIE_FILE}\"\n for i in $(seq 1 5); do\n curl -s -o /dev/null -w '%{http_code}' -X POST http://127.0.0.1:8001/session/login \\\n -H 'Content-Type: application/json' \\\n -c \"${COOKIE_FILE}\" \\\n -d '{\"username\":\"'\"${HICLAW_ADMIN_USER}\"'\",\"password\":\"'\"${HICLAW_ADMIN_PASSWORD}\"'\"}' 2>/dev/null\n VERIFY2=$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8001/v1/consumers -b \"${COOKIE_FILE}\" 2>/dev/null) || true\n if [ \"${VERIFY2}\" = \"200\" ]; then\n log \"Re-login successful, session verified\"\n break\n fi\n sleep 2\n done\n fi\n\n export HIGRESS_COOKIE_FILE=\"${COOKIE_FILE}\"\n\n # Configure Higress routes, consumers, MCP servers\n /opt/hiclaw/scripts/init/setup-higress.sh\nfi\n\n# ============================================================\n# Create admin DM room, persist to state.json, send welcome message\n# Runs in both local and cloud modes (idempotent)\n# ============================================================\nMANAGER_FULL_ID=\"@manager:${MATRIX_DOMAIN}\"\nADMIN_FULL_ID=\"@${HICLAW_ADMIN_USER}:${MATRIX_DOMAIN}\"\n\nlog \"Logging in as admin to create DM room...\"\n_ADMIN_LOGIN=$(curl -sf -X POST \"${HICLAW_MATRIX_SERVER}/_matrix/client/v3/login\" \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"type\": \"m.login.password\",\n \"identifier\": {\"type\": \"m.id.user\", \"user\": \"'\"${HICLAW_ADMIN_USER}\"'\"},\n \"password\": \"'\"${HICLAW_ADMIN_PASSWORD}\"'\"\n }' 2>&1) || true\n\nADMIN_MATRIX_TOKEN=$(echo \"${_ADMIN_LOGIN}\" | jq -r '.access_token // empty' 2>/dev/null)\nif [ -z \"${ADMIN_MATRIX_TOKEN}\" ]; then\n log \"WARNING: Failed to login as admin, skipping DM room creation\"\nelse\n # Search for existing DM room with Manager (idempotent)\n DM_ROOM_ID=\"\"\n _JOINED_ROOMS=$(curl -sf \"${HICLAW_MATRIX_SERVER}/_matrix/client/v3/joined_rooms\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" 2>/dev/null \\\n | jq -r '.joined_rooms[]' 2>/dev/null) || true\n for _rid in ${_JOINED_ROOMS}; do\n _members=$(curl -sf \"${HICLAW_MATRIX_SERVER}/_matrix/client/v3/rooms/${_rid}/members\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" 2>/dev/null \\\n | jq -r '.chunk[].state_key' 2>/dev/null) || continue\n _count=$(echo \"${_members}\" | wc -l | xargs)\n if [ \"${_count}\" = \"2\" ] && echo \"${_members}\" | grep -q \"@manager:\"; then\n DM_ROOM_ID=\"${_rid}\"\n break\n fi\n done\n\n if [ -n \"${DM_ROOM_ID}\" ]; then\n log \"Existing DM room found: ${DM_ROOM_ID}\"\n else\n log \"Creating DM room with Manager...\"\n _RAW=$(curl -s -w '\\nHTTP_CODE:%{http_code}' -X POST \"${HICLAW_MATRIX_SERVER}/_matrix/client/v3/createRoom\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d \"{\\\"is_direct\\\":true,\\\"invite\\\":[\\\"${MANAGER_FULL_ID}\\\"],\\\"preset\\\":\\\"trusted_private_chat\\\"}\" 2>&1) || true\n _HTTP_CODE=$(echo \"${_RAW}\" | tail -1 | sed 's/HTTP_CODE://')\n _CREATE_RESP=$(echo \"${_RAW}\" | sed '$d')\n DM_ROOM_ID=$(echo \"${_CREATE_RESP}\" | jq -r '.room_id // empty' 2>/dev/null)\n if [ -n \"${DM_ROOM_ID}\" ]; then\n log \"DM room created: ${DM_ROOM_ID}\"\n else\n log \"WARNING: Failed to create DM room (HTTP ${_HTTP_CODE}): ${_CREATE_RESP}\"\n fi\n fi\n\n # Persist admin DM room ID to state.json\n if [ -n \"${DM_ROOM_ID}\" ]; then\n STATE_SCRIPT=\"/opt/hiclaw/agent/skills/task-management/scripts/manage-state.sh\"\n if [ -f \"${STATE_SCRIPT}\" ]; then\n bash \"${STATE_SCRIPT}\" --action init 2>/dev/null || true\n bash \"${STATE_SCRIPT}\" --action set-admin-dm --room-id \"${DM_ROOM_ID}\" 2>/dev/null || true\n log \"Admin DM room persisted to state.json: ${DM_ROOM_ID}\"\n fi\n fi\n\n # Schedule welcome message in background (only on first boot)\n if [ -n \"${DM_ROOM_ID}\" ] && [ ! -f \"/root/manager-workspace/soul-configured\" ]; then\n log \"Scheduling welcome message (background, waiting for OpenClaw to start)...\"\n (\n _HICLAW_LANGUAGE=\"${HICLAW_LANGUAGE:-zh}\"\n _HICLAW_TIMEZONE=\"${TZ:-Asia/Shanghai}\"\n _wait=0\n _ready=false\n while [ \"${_wait}\" -lt 300 ]; do\n if curl -sf http://127.0.0.1:18799/ > /dev/null 2>&1; then\n _ready=true\n break\n fi\n sleep 3\n _wait=$((_wait + 3))\n done\n if [ \"${_ready}\" != \"true\" ]; then\n echo \"[manager] WARNING: OpenClaw gateway not ready within 300s, skipping welcome message\"\n exit 0\n fi\n # Ensure Manager has joined the DM room before sending the welcome\n # message. Without this, there is a race between OpenClaw's Matrix\n # auto-join and the message send — the message may land before Manager\n # joins, so OpenClaw's /sync never picks it up.\n _join_ok=false\n for _join_attempt in 1 2 3; do\n if curl -sf -X POST \"${HICLAW_MATRIX_SERVER}/_matrix/client/v3/rooms/${DM_ROOM_ID}/join\" \\\n -H \"Authorization: Bearer ${MANAGER_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d '{}' > /dev/null 2>&1; then\n echo \"[manager] Manager joined DM room before welcome message\"\n _join_ok=true\n break\n fi\n sleep 2\n done\n if [ \"${_join_ok}\" != \"true\" ]; then\n echo \"[manager] WARNING: Manager join request failed after 3 attempts (may already be joined)\"\n fi\n _welcome_msg=\"This is an automated message from the HiClaw setup. This is a fresh installation.\n\n--- Installation Context ---\nUser Language: ${_HICLAW_LANGUAGE} (zh = Chinese, en = English)\nUser Timezone: ${_HICLAW_TIMEZONE} (IANA timezone identifier)\n---\n\nYou are an AI agent that manages a team of worker agents. Your identity and personality have not been configured yet — the human admin is about to meet you for the first time.\n\nPlease begin the onboarding conversation:\n\n1. Greet the admin warmly and briefly describe what you can do (coordinate workers, manage tasks, run multi-agent projects)\n2. The user has selected \\\"${_HICLAW_LANGUAGE}\\\" as their preferred language during installation. Use this language for your greeting and all subsequent communication.\n3. The user's timezone is ${_HICLAW_TIMEZONE}. Based on this timezone, you may infer their likely region and suggest additional language options.\n4. Ask them: a) What would they like to call you? b) Communication style preference? c) Any behavior guidelines? d) Confirm default language\n5. After they reply, write their preferences to ~/SOUL.md\n6. Confirm what you wrote, and ask if they would like to adjust anything\n7. Once confirmed, run: touch ~/soul-configured\n\nThe human admin will start chatting shortly.\"\n _txn_id=\"welcome-$(date +%s)\"\n _payload=$(jq -nc --arg body \"${_welcome_msg}\" '{\"msgtype\":\"m.text\",\"body\":$body}')\n _raw=$(curl -s -w '\\nHTTP_CODE:%{http_code}' -X PUT \"${HICLAW_MATRIX_SERVER}/_matrix/client/v3/rooms/${DM_ROOM_ID}/send/m.room.message/${_txn_id}\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d \"${_payload}\" 2>&1) || true\n _http_code=$(echo \"${_raw}\" | tail -1 | sed 's/HTTP_CODE://')\n _send_resp=$(echo \"${_raw}\" | sed '$d')\n if echo \"${_send_resp}\" | jq -e '.event_id' > /dev/null 2>&1; then\n echo \"[manager] Welcome message sent to DM room\"\n else\n echo \"[manager] WARNING: Failed to send welcome message (HTTP ${_http_code}): ${_send_resp}\"\n fi\n ) &\n log \"Welcome message background process started (PID: $!)\"\n fi\nfi\n\n# ============================================================\n# Generate Manager Agent openclaw.json from template\n# ============================================================\nlog \"Generating Manager openclaw.json...\"\nexport MANAGER_MATRIX_TOKEN=\"${MANAGER_TOKEN}\"\nexport MANAGER_GATEWAY_KEY=\"${HICLAW_MANAGER_GATEWAY_KEY}\"\n# Resolve model parameters based on model name\nMODEL_NAME=\"${HICLAW_DEFAULT_MODEL:-qwen3.5-plus}\"\ncase \"${MODEL_NAME}\" in\n gpt-5.3-codex|gpt-5-mini|gpt-5-nano)\n export MODEL_CONTEXT_WINDOW=400000 MODEL_MAX_TOKENS=128000 ;;\n claude-opus-4-6)\n export MODEL_CONTEXT_WINDOW=1000000 MODEL_MAX_TOKENS=128000 ;;\n claude-sonnet-4-6)\n export MODEL_CONTEXT_WINDOW=1000000 MODEL_MAX_TOKENS=64000 ;;\n claude-haiku-4-5)\n export MODEL_CONTEXT_WINDOW=200000 MODEL_MAX_TOKENS=64000 ;;\n qwen3.5-plus)\n export MODEL_CONTEXT_WINDOW=200000 MODEL_MAX_TOKENS=64000 ;;\n deepseek-chat|deepseek-reasoner|kimi-k2.5)\n export MODEL_CONTEXT_WINDOW=256000 MODEL_MAX_TOKENS=128000 ;;\n glm-5|MiniMax-M2.7|MiniMax-M2.7-highspeed|MiniMax-M2.5)\n export MODEL_CONTEXT_WINDOW=200000 MODEL_MAX_TOKENS=128000 ;;\n *)\n export MODEL_CONTEXT_WINDOW=150000 MODEL_MAX_TOKENS=128000 ;;\nesac\nexport MODEL_REASONING=true\n\n# Override with user-supplied custom model parameters from env (set during install)\n[ -n \"${HICLAW_MODEL_CONTEXT_WINDOW:-}\" ] && export MODEL_CONTEXT_WINDOW=\"${HICLAW_MODEL_CONTEXT_WINDOW}\"\n[ -n \"${HICLAW_MODEL_MAX_TOKENS:-}\" ] && export MODEL_MAX_TOKENS=\"${HICLAW_MODEL_MAX_TOKENS}\"\n[ -n \"${HICLAW_MODEL_REASONING:-}\" ] && export MODEL_REASONING=\"${HICLAW_MODEL_REASONING}\"\n\n# E2EE: convert HICLAW_MATRIX_E2EE to JSON boolean for template substitution\nif [ \"${HICLAW_MATRIX_E2EE:-0}\" = \"1\" ] || [ \"${HICLAW_MATRIX_E2EE:-}\" = \"true\" ]; then\n export MATRIX_E2EE_ENABLED=true\nelse\n export MATRIX_E2EE_ENABLED=false\nfi\nlog \"Matrix E2EE: ${MATRIX_E2EE_ENABLED}\"\n\n# Resolve input modalities: only vision-capable models get \"image\"\ncase \"${MODEL_NAME}\" in\n gpt-5.4|gpt-5.3-codex|gpt-5-mini|gpt-5-nano|claude-opus-4-6|claude-sonnet-4-6|claude-haiku-4-5|qwen3.5-plus|kimi-k2.5)\n export MODEL_INPUT='[\"text\", \"image\"]' ;;\n *)\n export MODEL_INPUT='[\"text\"]' ;;\nesac\n# Override with user-supplied vision setting from env\nif [ \"${HICLAW_MODEL_VISION:-}\" = \"true\" ]; then\n export MODEL_INPUT='[\"text\", \"image\"]'\nelif [ \"${HICLAW_MODEL_VISION:-}\" = \"false\" ]; then\n export MODEL_INPUT='[\"text\"]'\nfi\n\nlog \"Model: ${MODEL_NAME} (context=${MODEL_CONTEXT_WINDOW}, maxTokens=${MODEL_MAX_TOKENS}, reasoning=${MODEL_REASONING}, input=${MODEL_INPUT})\"\n\nif [ -f /root/manager-workspace/openclaw.json ]; then\n log \"Manager openclaw.json already exists, updating dynamic fields only (preserving user customizations)...\"\n # Merge known models into existing config (add missing, preserve user-added)\n # Use known-models.json (valid JSON) instead of template (contains ${VAR} placeholders)\n KNOWN_MODELS=$(cat /opt/hiclaw/configs/known-models.json 2>/dev/null || echo '[]')\n jq --arg token \"${MANAGER_TOKEN}\" \\\n --arg key \"${HICLAW_MANAGER_GATEWAY_KEY}\" \\\n --arg model \"${MODEL_NAME}\" \\\n --argjson e2ee \"${MATRIX_E2EE_ENABLED}\" \\\n --argjson known_models \"${KNOWN_MODELS}\" \\\n --argjson ctx \"${MODEL_CONTEXT_WINDOW}\" \\\n --argjson max \"${MODEL_MAX_TOKENS}\" \\\n --argjson reasoning \"${MODEL_REASONING}\" \\\n --argjson input \"${MODEL_INPUT}\" \\\n '\n # Merge known models: add any model id not already present\n .models.providers[\"hiclaw-gateway\"].models as $existing\n | ($existing | map(.id)) as $existing_ids\n | ($known_models | map(select(.id as $id | $existing_ids | index($id) | not))) as $new\n | .models.providers[\"hiclaw-gateway\"].models = ($existing + $new)\n # Ensure the user-chosen default model is in the list (custom model support)\n | if (.models.providers[\"hiclaw-gateway\"].models | map(.id) | index($model) | not) then\n .models.providers[\"hiclaw-gateway\"].models += [{\"id\": $model, \"name\": $model, \"reasoning\": $reasoning, \"contextWindow\": $ctx, \"maxTokens\": $max, \"input\": $input}]\n else . end\n # Rebuild model aliases from the full models list\n | (.models.providers[\"hiclaw-gateway\"].models | map({ (\"hiclaw-gateway/\" + .id): { \"alias\": .id } }) | add // {}) as $aliases\n | .agents.defaults.models = ((.agents.defaults.models // {}) + $aliases)\n | .channels.matrix.accessToken = $token | .models.providers[\"hiclaw-gateway\"].apiKey = $key\n | ((.hooks.token // \"\") as $ht | if $ht == $key or $ht == ($key + \"-hooks\" | @base64) then del(.hooks) else . end)\n | .agents.defaults.model.primary = (\"hiclaw-gateway/\" + $model)\n | .commands.restart = true\n | .gateway.controlUi.dangerouslyDisableDeviceAuth = true\n | .channels.matrix.encryption = $e2ee\n ' \\\n /root/manager-workspace/openclaw.json > /tmp/openclaw.json.tmp && \\\n mv /tmp/openclaw.json.tmp /root/manager-workspace/openclaw.json\n # Verify the token was written correctly\n _written_token=$(jq -r '.channels.matrix.accessToken' /root/manager-workspace/openclaw.json 2>/dev/null)\n if [ -z \"${_written_token}\" ] || [ \"${_written_token}\" = \"null\" ]; then\n log \"ERROR: Matrix token was not written correctly to openclaw.json (got: ${_written_token})\"\n else\n log \"Matrix token written to openclaw.json (prefix: ${_written_token:0:10}...)\"\n fi\nelse\n log \"Manager openclaw.json not found, generating from template...\"\n envsubst < /opt/hiclaw/configs/manager-openclaw.json.tmpl > /root/manager-workspace/openclaw.json\n # Inject custom model if not in the built-in list\n if ! jq -e --arg model \"${MODEL_NAME}\" '.models.providers[\"hiclaw-gateway\"].models | map(.id) | index($model)' /root/manager-workspace/openclaw.json > /dev/null 2>&1; then\n log \"Custom model '${MODEL_NAME}' not in built-in list, injecting into config...\"\n jq --arg model \"${MODEL_NAME}\" \\\n --argjson ctx \"${MODEL_CONTEXT_WINDOW}\" \\\n --argjson max \"${MODEL_MAX_TOKENS}\" \\\n --argjson reasoning \"${MODEL_REASONING}\" \\\n --argjson input \"${MODEL_INPUT}\" \\\n '\n .models.providers[\"hiclaw-gateway\"].models += [{\"id\": $model, \"name\": $model, \"reasoning\": $reasoning, \"contextWindow\": $ctx, \"maxTokens\": $max, \"input\": $input}]\n | .agents.defaults.models += {(\"hiclaw-gateway/\" + $model): {\"alias\": $model}}\n ' /root/manager-workspace/openclaw.json > /tmp/openclaw.json.tmp && \\\n mv /tmp/openclaw.json.tmp /root/manager-workspace/openclaw.json\n fi\n _written_token=$(jq -r '.channels.matrix.accessToken' /root/manager-workspace/openclaw.json 2>/dev/null)\n log \"Matrix token written from template (prefix: ${_written_token:0:10}...)\"\nfi\n\n# Cloud mode: overlay cloud-specific settings onto generated config\nif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n log \"Applying cloud overlay to openclaw.json...\"\n jq --arg homeserver \"${HICLAW_MATRIX_SERVER}\" \\\n --arg gateway \"${HICLAW_AI_GATEWAY_URL}/v1\" \\\n --arg key \"${HICLAW_MANAGER_GATEWAY_KEY}\" \\\n '.channels.matrix.homeserver = $homeserver\n | .models.providers[\"hiclaw-gateway\"].baseUrl = $gateway\n | .models.providers[\"hiclaw-gateway\"].apiKey = $key\n | ((.hooks.token // \"\") as $ht | if $ht == $key or $ht == ($key + \"-hooks\" | @base64) then del(.hooks) else . end)\n | .commands.restart = false' \\\n /root/manager-workspace/openclaw.json > /tmp/openclaw-cloud.json && \\\n mv /tmp/openclaw-cloud.json /root/manager-workspace/openclaw.json\n log \"Cloud overlay applied\"\nfi\n\n# ============================================================\n# Optional: enable openclaw-cms-plugin observability\n# Config is applied at runtime so secrets stay out of image layers.\n# ============================================================\nCMS_TRACES_ENABLED=\"$(echo \"${HICLAW_CMS_TRACES_ENABLED:-false}\" | tr '[:upper:]' '[:lower:]')\"\nif [ \"${CMS_TRACES_ENABLED}\" = \"true\" ]; then\n CMS_PLUGIN_NAME=\"openclaw-cms-plugin\"\n CMS_PLUGIN_DIR=\"${OPENCLAW_CMS_PLUGIN_DIR:-/opt/openclaw/extensions/openclaw-cms-plugin}\"\n CMS_PLUGIN_MANIFEST=\"${CMS_PLUGIN_DIR}/openclaw.plugin.json\"\n DIAG_PLUGIN_NAME=\"diagnostics-otel\"\n DIAG_PLUGIN_DIR=\"/opt/openclaw/extensions/diagnostics-otel\"\n CMS_LICENSE_KEY=\"${HICLAW_CMS_LICENSE_KEY:-}\"\n CMS_PROJECT=\"${HICLAW_CMS_PROJECT:-}\"\n CMS_METRICS_ENABLED=\"${HICLAW_CMS_METRICS_ENABLED:-false}\"\n\n if [ ! -f \"${CMS_PLUGIN_MANIFEST}\" ]; then\n log \"WARNING: ${CMS_PLUGIN_NAME} manifest not found at ${CMS_PLUGIN_MANIFEST}, skipping plugin config\"\n else\n _missing=0\n [ -z \"${HICLAW_CMS_ENDPOINT:-}\" ] && log \"WARNING: HICLAW_CMS_ENDPOINT is required when HICLAW_CMS_TRACES_ENABLED=true\" && _missing=1\n [ -z \"${CMS_LICENSE_KEY:-}\" ] && log \"WARNING: HICLAW_CMS_LICENSE_KEY is required when HICLAW_CMS_TRACES_ENABLED=true\" && _missing=1\n [ -z \"${HICLAW_CMS_WORKSPACE:-}\" ] && log \"WARNING: HICLAW_CMS_WORKSPACE is required when HICLAW_CMS_TRACES_ENABLED=true\" && _missing=1\n\n if [ \"${_missing}\" = \"0\" ]; then\n CMS_SERVICE_NAME=\"${HICLAW_CMS_SERVICE_NAME:-hiclaw-manager}\"\n CMS_ENABLE_METRICS=\"${CMS_METRICS_ENABLED}\"\n DIAG_AVAILABLE=\"0\"\n _metrics_lc=\"$(echo \"${CMS_ENABLE_METRICS}\" | tr '[:upper:]' '[:lower:]')\"\n if [ \"${_metrics_lc}\" = \"true\" ]; then\n if [ -f \"${DIAG_PLUGIN_DIR}/package.json\" ]; then\n DIAG_AVAILABLE=\"1\"\n if [ ! -d \"${DIAG_PLUGIN_DIR}/node_modules\" ]; then\n log \"diagnostics-otel dependencies missing, installing...\"\n if (cd \"${DIAG_PLUGIN_DIR}\" && npm install --omit=dev --ignore-scripts >/tmp/hiclaw-diag-install.log 2>&1); then\n log \"diagnostics-otel dependencies installed\"\n else\n log \"WARNING: diagnostics-otel npm install failed, metrics plugin may not load\"\n fi\n else\n log \"diagnostics-otel dependencies already present\"\n fi\n else\n log \"WARNING: diagnostics-otel package.json not found at ${DIAG_PLUGIN_DIR}, metrics plugin may not load\"\n fi\n fi\n\n log \"Applying ${CMS_PLUGIN_NAME} config to openclaw.json...\"\n jq --arg pluginName \"${CMS_PLUGIN_NAME}\" \\\n --arg pluginDir \"${CMS_PLUGIN_DIR}\" \\\n --arg endpoint \"${HICLAW_CMS_ENDPOINT}\" \\\n --arg licenseKey \"${CMS_LICENSE_KEY}\" \\\n --arg armsProject \"${CMS_PROJECT}\" \\\n --arg cmsWorkspace \"${HICLAW_CMS_WORKSPACE}\" \\\n --arg serviceName \"${CMS_SERVICE_NAME}\" \\\n --arg diagPluginName \"${DIAG_PLUGIN_NAME}\" \\\n --arg diagPluginDir \"${DIAG_PLUGIN_DIR}\" \\\n --arg metricsRaw \"${CMS_ENABLE_METRICS}\" \\\n --arg diagAvailableRaw \"${DIAG_AVAILABLE}\" \\\n '\n .plugins = (.plugins // {})\n | .plugins.load = (.plugins.load // {})\n | .plugins.entries = (.plugins.entries // {})\n | if (.plugins.allow | type) != \"array\" then .plugins.allow = [] else . end\n | if (.plugins.allow | index($pluginName)) == null then .plugins.allow += [$pluginName] else . end\n | if (.plugins.load.paths | type) != \"array\" then .plugins.load.paths = [] else . end\n | if (.plugins.load.paths | index($pluginDir)) == null then .plugins.load.paths += [$pluginDir] else . end\n | .plugins.entries[$pluginName] = {\n \"enabled\": true,\n \"config\": {\n \"endpoint\": $endpoint,\n \"headers\": {\n \"x-arms-license-key\": $licenseKey,\n \"x-arms-project\": $armsProject,\n \"x-cms-workspace\": $cmsWorkspace\n },\n \"serviceName\": $serviceName\n }\n }\n\n # diagnostics-otel metrics (optional)\n | ($metricsRaw | ascii_downcase) as $m\n | ($diagAvailableRaw == \"1\") as $diagAvailable\n | (($m == \"true\") and $diagAvailable) as $metricsEnabled\n | if $metricsEnabled then\n (if (.plugins.allow | index($diagPluginName)) == null then .plugins.allow += [$diagPluginName] else . end)\n | (if (.plugins.load.paths | index($diagPluginDir)) == null then .plugins.load.paths += [$diagPluginDir] else . end)\n | .plugins.entries[$diagPluginName].enabled = true\n | .diagnostics = (.diagnostics // {})\n | .diagnostics.otel = (.diagnostics.otel // {})\n | .diagnostics.enabled = true\n | .diagnostics.otel.enabled = true\n | .diagnostics.otel.endpoint = $endpoint\n | .diagnostics.otel.protocol = (.diagnostics.otel.protocol // \"http/protobuf\")\n | .diagnostics.otel.headers = {\n \"x-arms-license-key\": $licenseKey,\n \"x-arms-project\": $armsProject,\n \"x-cms-workspace\": $cmsWorkspace\n }\n | .diagnostics.otel.serviceName = $serviceName\n | .diagnostics.otel.metrics = true\n | .diagnostics.otel.traces = (.diagnostics.otel.traces // false)\n | .diagnostics.otel.logs = (.diagnostics.otel.logs // false)\n else\n .\n end\n ' /root/manager-workspace/openclaw.json > /tmp/openclaw-cms.json && \\\n mv /tmp/openclaw-cms.json /root/manager-workspace/openclaw.json\n log \"${CMS_PLUGIN_NAME} config applied (metrics=${CMS_ENABLE_METRICS}, service=${CMS_SERVICE_NAME})\"\n else\n log \"Skipping ${CMS_PLUGIN_NAME} config due to missing required env vars\"\n fi\n fi\nfi\n\n# ============================================================\n# Detect container runtime (for Worker creation)\n# ============================================================\nsource /opt/hiclaw/scripts/lib/container-api.sh\nif container_api_available; then\n log \"Container runtime socket detected at ${CONTAINER_SOCKET} — direct Worker creation enabled\"\n export HICLAW_CONTAINER_RUNTIME=\"socket\"\nelif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n log \"Cloud mode — Workers created via SAE API\"\n export HICLAW_CONTAINER_RUNTIME=\"cloud\"\nelse\n log \"No container runtime found — Worker creation will output install commands\"\n export HICLAW_CONTAINER_RUNTIME=\"none\"\nfi\n\n# ============================================================\n# Upgrade Worker openclaw.json: merge known models + E2EE flag into existing configs\n# Existing workers in MinIO may have old single-model configs or missing encryption field.\n# Merge template models so they can hot-switch without restart.\n# ============================================================\nREGISTRY_FILE=\"/root/manager-workspace/workers-registry.json\"\nif [ -f \"${REGISTRY_FILE}\" ]; then\n # Use known-models.json (valid JSON) instead of template (contains ${VAR} placeholders)\n KNOWN_MODELS_FILE=\"/opt/hiclaw/configs/known-models.json\"\n if [ -f \"${KNOWN_MODELS_FILE}\" ]; then\n _KNOWN_MODELS=$(cat \"${KNOWN_MODELS_FILE}\")\n for _wname in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_wname}\" ] && continue\n _minio_path=\"${HICLAW_STORAGE_PREFIX}/agents/${_wname}/openclaw.json\"\n _tmp_in=\"/tmp/openclaw-${_wname}-models-upgrade-in.json\"\n if mc cp \"${_minio_path}\" \"${_tmp_in}\" 2>/dev/null; then\n _tmp_out=\"/tmp/openclaw-${_wname}-models-upgrade-out.json\"\n # Idempotent merge: add missing known models, rebuild aliases, set e2ee.\n # Always runs — jq deduplicates by model id, so re-runs are safe.\n jq --argjson known_models \"${_KNOWN_MODELS}\" \\\n --argjson e2ee \"${MATRIX_E2EE_ENABLED}\" '\n .models.providers[\"hiclaw-gateway\"].models as $existing\n | ($existing | map(.id)) as $existing_ids\n | ($known_models | map(select(.id as $id | $existing_ids | index($id) | not))) as $new\n | .models.providers[\"hiclaw-gateway\"].models = ($existing + $new)\n | (.models.providers[\"hiclaw-gateway\"].models | map({ (\"hiclaw-gateway/\" + .id): { \"alias\": .id } }) | add // {}) as $aliases\n | .agents.defaults.models = ((.agents.defaults.models // {}) + $aliases)\n | .channels.matrix.encryption = $e2ee\n ' \"${_tmp_in}\" > \"${_tmp_out}\" 2>/dev/null\n if ! diff -q \"${_tmp_in}\" \"${_tmp_out}\" > /dev/null 2>&1; then\n if mc cp \"${_tmp_out}\" \"${_minio_path}\" 2>/dev/null; then\n _new_count=$(jq '.models.providers[\"hiclaw-gateway\"].models | length' \"${_tmp_out}\" 2>/dev/null)\n log \"Worker ${_wname}: upgraded openclaw.json (models: ${_new_count}, e2ee: ${MATRIX_E2EE_ENABLED})\"\n fi\n fi\n rm -f \"${_tmp_in}\" \"${_tmp_out}\"\n fi\n done\n fi\nfi\n\n# ============================================================\n# Ensure Worker Matrix password files exist in MinIO (E2EE fix)\n# Workers need to re-login on restart to get a fresh device_id.\n# Older workers created before this fix won't have the password file.\n# ============================================================\nif [ -f \"${REGISTRY_FILE}\" ]; then\n for _wname in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_wname}\" ] && continue\n _creds_file=\"/data/worker-creds/${_wname}.env\"\n if [ -f \"${_creds_file}\" ]; then\n # Check if password file already exists in MinIO\n if ! mc stat \"${HICLAW_STORAGE_PREFIX}/agents/${_wname}/credentials/matrix/password\" > /dev/null 2>&1; then\n source \"${_creds_file}\"\n if [ -n \"${WORKER_PASSWORD}\" ]; then\n _tmp_pw=\"/tmp/matrix-pw-${_wname}\"\n echo -n \"${WORKER_PASSWORD}\" > \"${_tmp_pw}\"\n mc cp \"${_tmp_pw}\" \"${HICLAW_STORAGE_PREFIX}/agents/${_wname}/credentials/matrix/password\" 2>/dev/null \\\n && log \"Worker ${_wname}: wrote Matrix password to MinIO (E2EE re-login fix)\" \\\n || log \"Worker ${_wname}: WARNING: failed to write Matrix password to MinIO\"\n rm -f \"${_tmp_pw}\"\n fi\n fi\n fi\n done\nfi\n\n# ============================================================\n# Recreate Worker containers as needed after Manager restart.\n# Workers are on hiclaw-net; Docker DNS resolves *-local.hiclaw.io via\n# the Manager's network aliases, so IP changes don't require worker recreation.\n# Only recreate stopped/missing workers.\n# ============================================================\nif container_api_available; then\n REGISTRY_FILE=\"/root/manager-workspace/workers-registry.json\"\n if [ -f \"${REGISTRY_FILE}\" ]; then\n for _worker_name in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_worker_name}\" ] && continue\n\n # Skip remote workers — they are not Manager-managed containers.\n _deployment=$(jq -r --arg w \"${_worker_name}\" '.workers[$w].deployment // \"local\"' \"${REGISTRY_FILE}\" 2>/dev/null)\n if [ \"${_deployment}\" = \"remote\" ]; then\n log \"Worker ${_worker_name} is remote, skipping container recreate\"\n continue\n fi\n\n _status=$(container_status_worker \"${_worker_name}\")\n if [ \"${_status}\" = \"running\" ]; then\n log \"Worker running: ${_worker_name}, skipping\"\n continue\n fi\n # Container missing or stopped — recreate.\n log \"Worker container ${_status}: ${_worker_name}, recreating...\"\n _creds_file=\"/data/worker-creds/${_worker_name}.env\"\n if [ -f \"${_creds_file}\" ]; then\n source \"${_creds_file}\"\n _runtime=$(jq -r --arg w \"${_worker_name}\" '.workers[$w].runtime // \"openclaw\"' \"${REGISTRY_FILE}\" 2>/dev/null)\n _recreated=false\n for _attempt in 1 2 3; do\n if [ \"${_runtime}\" = \"copaw\" ]; then\n container_create_copaw_worker \"${_worker_name}\" \"${_worker_name}\" \"${WORKER_MINIO_PASSWORD}\" 2>&1 && _recreated=true && break\n else\n container_create_worker \"${_worker_name}\" \"${_worker_name}\" \"${WORKER_MINIO_PASSWORD}\" 2>&1 && _recreated=true && break\n fi\n log \" Attempt ${_attempt}/3 failed for ${_worker_name}, retrying in $((5 * _attempt))s...\"\n sleep $((5 * _attempt))\n done\n if [ \"${_recreated}\" = true ]; then\n log \" Recreated ${_runtime} worker: ${_worker_name}\"\n else\n log \" ERROR: Failed to recreate ${_worker_name} after 3 attempts\"\n fi\n else\n log \" WARNING: No credentials found for ${_worker_name} (${_creds_file} missing), skipping\"\n fi\n done\n fi\nfi\n\n# ============================================================\n# Notify workers of builtin updates if upgrade happened\n# Builtin files (AGENTS.md, skills) are already synced by upgrade-builtins.sh\n#\n# Cooldown: skip notification if the last successful notify was within\n# NOTIFY_COOLDOWN_SECS (default 3600s / 1 hour). This prevents repeated\n# notifications when the Manager crash-loops and re-runs upgrade-builtins\n# on every restart (e.g. IMAGE_VERSION=latest always triggers upgrade).\n# ============================================================\nNOTIFY_COOLDOWN_SECS=\"${HICLAW_NOTIFY_COOLDOWN_SECS:-3600}\"\nNOTIFY_TS_FILE=\"/root/manager-workspace/.last-worker-notify-ts\"\n\nif [ -f /root/manager-workspace/.upgrade-pending-worker-notify ]; then\n _now=$(date +%s)\n _last_notify=$(cat \"${NOTIFY_TS_FILE}\" 2>/dev/null || echo \"0\")\n _elapsed=$(( _now - _last_notify ))\n\n if [ \"${_elapsed}\" -lt \"${NOTIFY_COOLDOWN_SECS}\" ]; then\n log \"Skipping worker builtin notification (last notify ${_elapsed}s ago, cooldown ${NOTIFY_COOLDOWN_SECS}s)\"\n rm -f /root/manager-workspace/.upgrade-pending-worker-notify\n else\n log \"Notifying workers about builtin updates...\"\n REGISTRY_FILE=\"/root/manager-workspace/workers-registry.json\"\n _notify_ok=false\n if [ -f \"${REGISTRY_FILE}\" ]; then\n for _worker_name in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_worker_name}\" ] && continue\n _room_id=$(jq -r --arg w \"${_worker_name}\" '.workers[$w].room_id // empty' \"${REGISTRY_FILE}\" 2>/dev/null)\n if [ -n \"${_room_id}\" ]; then\n _worker_id=\"@${_worker_name}:${MATRIX_DOMAIN}\"\n _txn_id=\"upgrade-$(date +%s%N)\"\n _msg=\"@${_worker_name}:${MATRIX_DOMAIN} Manager upgraded builtin files (AGENTS.md, skills). Please use your file-sync skill to sync the latest config.\"\n _raw=$(curl -s -w '\\nHTTP_CODE:%{http_code}' -X PUT \\\n \"${HICLAW_MATRIX_SERVER}/_matrix/client/v3/rooms/${_room_id}/send/m.room.message/${_txn_id}\" \\\n -H \"Authorization: Bearer ${MANAGER_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d \"{\\\"msgtype\\\":\\\"m.text\\\",\\\"body\\\":\\\"${_msg}\\\",\\\"m.mentions\\\":{\\\"user_ids\\\":[\\\"${_worker_id}\\\"]}}\" \\\n 2>&1) || true\n _http_code=$(echo \"${_raw}\" | tail -1 | sed 's/HTTP_CODE://')\n _notify_resp=$(echo \"${_raw}\" | sed '$d')\n if echo \"${_notify_resp}\" | jq -e '.event_id' > /dev/null 2>&1; then\n log \" Notified ${_worker_name}\"; _notify_ok=true\n else\n log \" WARNING: Failed to notify ${_worker_name} (HTTP ${_http_code}): ${_notify_resp}\"\n fi\n fi\n done\n fi\n # Record timestamp only if at least one notification succeeded\n if [ \"${_notify_ok}\" = true ]; then\n echo \"${_now}\" > \"${NOTIFY_TS_FILE}\"\n fi\n rm -f /root/manager-workspace/.upgrade-pending-worker-notify\n fi\nfi\n\n# ============================================================\n# Start OpenClaw Manager Agent\n# ============================================================\nlog \"Starting Manager Agent (OpenClaw)...\"\n\n# HOME is already set to /root/manager-workspace via docker run -e HOME=...\nexport OPENCLAW_CONFIG_PATH=\"/root/manager-workspace/openclaw.json\"\n\n# Symlink to default OpenClaw config path so CLI commands find the config\nmkdir -p \"${HOME}/.openclaw\"\nln -sf \"/root/manager-workspace/openclaw.json\" \"${HOME}/.openclaw/openclaw.json\"\n\n# Ensure host credential symlinks exist under HOME\nif [ -d \"/host-share\" ]; then\n [ -f \"/host-share/.gitconfig\" ] && ln -sf \"/host-share/.gitconfig\" \"${HOME}/.gitconfig\"\nfi\n\nlog \"HOME=${HOME} (manager-workspace, host-mounted)\"\ncd \"${HOME}\"\n\n# Clean orphaned session write locks (e.g. from SIGKILL or crash before exit handlers)\n# Prevents \"session file locked (timeout 10000ms)\" when PID was reused\nfind \"${HOME}/.openclaw/agents\" -name \"*.jsonl.lock\" -delete 2>/dev/null || true\nlog \"Cleaned up any orphaned session write locks\"\n\n# Clean Matrix crypto storage (SQLite WAL may be corrupted after unclean shutdown)\n# Crypto state is re-negotiated on startup; losing it only means re-establishing E2EE sessions\nrm -rf \"${HOME}/.openclaw/matrix\" 2>/dev/null || true\nlog \"Cleaned Matrix crypto storage (will re-establish E2EE sessions)\"\n\n# ── Render agent doc templates ────────────────────────────────────────────\n# Replace ${VAR} placeholders with actual values so the AI agent reads\n# plain text and never needs to resolve environment variables.\nexport MANAGER_MATRIX_TOKEN MANAGER_TOKEN HIGRESS_COOKIE_FILE\nRENDER=/opt/hiclaw/scripts/lib/render-skills.sh\nlog \"Rendering agent doc templates...\"\n# Manager-owned docs (workspace)\nbash \"$RENDER\" /root/manager-workspace/skills\nbash \"$RENDER\" /root/manager-workspace/skills-alpha\nbash \"$RENDER\" /root/manager-workspace AGENTS.md TOOLS.md HEARTBEAT.md SOUL.md\n# Worker templates (workspace + image) — rendered before push to MinIO\n# so Workers (including remote pip-install) receive plain text\nbash \"$RENDER\" /root/manager-workspace/worker-skills\nbash \"$RENDER\" /root/manager-workspace/worker-agent\nbash \"$RENDER\" /root/manager-workspace/copaw-worker-agent\nbash \"$RENDER\" /opt/hiclaw/agent/worker-skills\nbash \"$RENDER\" /opt/hiclaw/agent/worker-agent\nbash \"$RENDER\" /opt/hiclaw/agent/copaw-worker-agent\nlog \"Agent doc templates rendered\"\n\n# Cloud mode: start background file sync (workspace ↔ OSS) and initial push\nif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n log \"Syncing initial workspace to OSS...\"\n ensure_mc_credentials\n mc mirror /root/manager-workspace/ \"${HICLAW_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" 2>/dev/null || true\n\n # Local → OSS: change-triggered sync\n (\n while true; do\n CHANGED=$(find /root/manager-workspace/ -type f -newermt \"15 seconds ago\" 2>/dev/null | head -1)\n if [ -n \"${CHANGED}\" ]; then\n ensure_mc_credentials 2>/dev/null || true\n mc mirror /root/manager-workspace/ \"${HICLAW_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" --exclude \".npm/**\" \\\n --exclude \".local/**\" --exclude \".mc/**\" 2>/dev/null || true\n fi\n sleep 10\n done\n ) &\n log \"Local→OSS sync started (PID: $!)\"\n\n # OSS → Local: periodic pull (shared data, agent configs)\n (\n while true; do\n sleep 300\n ensure_mc_credentials 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/shared/\" /root/hiclaw-fs/shared/ --overwrite --newer-than \"5m\" 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/agents/\" /root/hiclaw-fs/agents/ --overwrite --newer-than \"5m\" 2>/dev/null || true\n done\n ) &\n log \"OSS→Local sync started (every 5m, PID: $!)\"\nfi\n\n# Launch OpenClaw\nexec openclaw gateway run --verbose --force\n"}, "1b5a5d88028c653faaf1a32d07d7e1ff1020db8f": {"t": 1785764770.1584988, "n": 50080, "h": "7bf80a242701a18e", "c": "#!/bin/bash\n# start-manager-agent.sh - Initialize and start the Manager Agent\n# Supports both local (supervisord) and cloud (SAE single-process) deployments.\n# In local mode this is the last supervisord component to start (priority 800).\n# In cloud mode (HICLAW_RUNTIME=aliyun) this is the container entrypoint.\n\nsource /opt/hiclaw/scripts/lib/hiclaw-env.sh\n\n# ============================================================\n# Set timezone from TZ env var\n# ============================================================\nif [ -n \"${TZ}\" ] && [ -f \"/usr/share/zoneinfo/${TZ}\" ]; then\n ln -sf \"/usr/share/zoneinfo/${TZ}\" /etc/localtime\n echo \"${TZ}\" > /etc/timezone\n log \"Timezone set to ${TZ}\"\nfi\n\nMATRIX_DOMAIN=\"${HICLAW_MATRIX_DOMAIN:-matrix-local.hiclaw.io:8080}\"\nAI_GATEWAY_DOMAIN=\"${HICLAW_AI_GATEWAY_DOMAIN:-aigw-local.hiclaw.io}\"\n\n# ============================================================\n# Cloud mode: validate required environment variables + initial credentials\n# ============================================================\nif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n : \"${HICLAW_MATRIX_URL:?HICLAW_MATRIX_URL is required}\"\n : \"${HICLAW_MATRIX_DOMAIN:?HICLAW_MATRIX_DOMAIN is required}\"\n : \"${HICLAW_AI_GATEWAY_URL:?HICLAW_AI_GATEWAY_URL is required}\"\n : \"${HICLAW_MANAGER_GATEWAY_KEY:?HICLAW_MANAGER_GATEWAY_KEY is required}\"\n : \"${HICLAW_MANAGER_PASSWORD:?HICLAW_MANAGER_PASSWORD is required (cloud containers are stateless, password must be injected)}\"\n : \"${HICLAW_REGISTRATION_TOKEN:?HICLAW_REGISTRATION_TOKEN is required}\"\n : \"${HICLAW_ADMIN_USER:?HICLAW_ADMIN_USER is required}\"\n : \"${HICLAW_ADMIN_PASSWORD:?HICLAW_ADMIN_PASSWORD is required}\"\n log \"Cloud mode: validating environment... OK\"\n log \" Matrix: ${HICLAW_MATRIX_SERVER}, AI Gateway: ${HICLAW_AI_GATEWAY_URL}, OSS: ${HICLAW_STORAGE_BUCKET}\"\n ensure_mc_credentials || { log \"FATAL: Initial STS credential fetch failed\"; exit 1; }\nfi\n\n# ============================================================\n# Local mode: host symlinks, /etc/hosts, wait for local services\n# ============================================================\nif [ \"${HICLAW_RUNTIME}\" != \"aliyun\" ]; then\n # Create symlink for host directory access\n if [ -d \"/host-share\" ]; then\n ORIGINAL_HOST_HOME=\"${HOST_ORIGINAL_HOME:-$HOME}\"\n if [ ! -e \"${ORIGINAL_HOST_HOME}\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/root\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/data\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/host-share\" ]; then\n mkdir -p \"$(dirname \"${ORIGINAL_HOST_HOME}\")\"\n ln -sfn /host-share \"${ORIGINAL_HOST_HOME}\"\n log \"Created symlink: ${ORIGINAL_HOST_HOME} -> /host-share\"\n else\n ln -sfn /host-share /root/host-home\n log \"Created fallback symlink: /root/host-home -> /host-share\"\n fi\n fi\n\n # Add local domains to /etc/hosts\n HOSTS_DOMAINS=\"${MATRIX_DOMAIN%%:*} ${HICLAW_MATRIX_CLIENT_DOMAIN:-matrix-client-local.hiclaw.io} ${AI_GATEWAY_DOMAIN} ${HICLAW_FS_DOMAIN:-fs-local.hiclaw.io}\"\n if ! grep -q \"${AI_GATEWAY_DOMAIN}\" /etc/hosts 2>/dev/null; then\n echo \"127.0.0.1 ${HOSTS_DOMAINS}\" >> /etc/hosts\n log \"Added local domains to /etc/hosts\"\n fi\n\n # Wait for local infrastructure\n waitForService \"Higress Gateway\" \"127.0.0.1\" 8080 180\n waitForService \"Higress Console\" \"127.0.0.1\" 8001 180\n waitForService \"Tuwunel\" \"127.0.0.1\" 6167 120\n waitForHTTP \"Tuwunel Matrix API\" \"${HICLAW_MATRIX_SERVER}/_tuwunel/server_version\" 120\n waitForService \"MinIO\" \"127.0.0.1\" 9000 120\nelse\n # Cloud mode: wait for external Tuwunel\n log \"Waiting for Tuwunel Matrix server at ${HICLAW_MATRIX_SERVER}...\"\n _retry=0\n while [ \"${_retry}\" -lt 30 ]; do\n if curl -sf \"${HICLAW_MATRIX_SERVER}/_matrix/client/versions\" > /dev/null 2>&1; then\n log \"Tuwunel is ready\"\n break\n fi\n _retry=$((_retry + 1))\n log \" Waiting for Tuwunel (attempt ${_retry}/30)...\"\n sleep 5\n done\n if [ \"${_retry}\" -ge 30 ]; then\n log \"ERROR: Tuwunel not reachable at ${HICLAW_MATRIX_SERVER}\"\n exit 1\n fi\nfi\n\n# ============================================================\n# Auto-generate secrets if not provided via environment\n# Persisted to /data so they survive container restart\n# ============================================================\nSECRETS_FILE=\"/data/hiclaw-secrets.env\"\nif [ -f \"${SECRETS_FILE}\" ]; then\n source \"${SECRETS_FILE}\"\n log \"Loaded persisted secrets from ${SECRETS_FILE}\"\nfi\n\nif [ -z \"${HICLAW_MANAGER_GATEWAY_KEY}\" ]; then\n export HICLAW_MANAGER_GATEWAY_KEY=\"$(generateKey 32)\"\n log \"Auto-generated HICLAW_MANAGER_GATEWAY_KEY\"\nfi\nif [ -z \"${HICLAW_MANAGER_PASSWORD}\" ]; then\n export HICLAW_MANAGER_PASSWORD=\"$(generateKey 16)\"\n log \"Auto-generated HICLAW_MANAGER_PASSWORD\"\nfi\n\n# Persist secrets so they survive supervisord restart\nmkdir -p /data\ncat > \"${SECRETS_FILE}\" <<EOF\nexport HICLAW_MANAGER_GATEWAY_KEY=\"${HICLAW_MANAGER_GATEWAY_KEY}\"\nexport HICLAW_MANAGER_PASSWORD=\"${HICLAW_MANAGER_PASSWORD}\"\nEOF\nchmod 600 \"${SECRETS_FILE}\"\n\n# Cloud mode: pull workspace from OSS before initialization\nif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n HICLAW_FS=\"/root/hiclaw-fs\"\n mkdir -p \"${HICLAW_FS}/shared\" \"${HICLAW_FS}/agents\"\n log \"Pulling workspace from OSS...\"\n ensure_mc_credentials\n mc mirror \"${HICLAW_STORAGE_PREFIX}/manager/\" /root/manager-workspace/ --overwrite 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/shared/\" \"${HICLAW_FS}/shared/\" --overwrite 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/agents/\" \"${HICLAW_FS}/agents/\" --overwrite 2>/dev/null || true\n # Symlink hiclaw-fs into workspace for agent access\n ln -sfn \"${HICLAW_FS}\" /root/manager-workspace/hiclaw-fs\nfi\n\n# ============================================================\n# Initialize / upgrade Manager workspace\n# First boot: full init via upgrade-builtins.sh\n# Subsequent boots: compare image version; upgrade only if changed\n# ============================================================\nmkdir -p /root/manager-workspace\n\nIMAGE_VERSION=$(cat /opt/hiclaw/agent/.builtin-version 2>/dev/null || echo \"unknown\")\nINSTALLED_VERSION=$(cat /root/manager-workspace/.builtin-version 2>/dev/null || echo \"\")\n\nif [ ! -f /root/manager-workspace/.initialized ]; then\n log \"First boot: initializing manager workspace...\"\n bash /opt/hiclaw/scripts/init/upgrade-builtins.sh\n touch /root/manager-workspace/.initialized\n log \"Manager workspace initialized (version: ${IMAGE_VERSION})\"\nelif [ \"${IMAGE_VERSION}\" != \"${INSTALLED_VERSION}\" ] || [ \"${IMAGE_VERSION}\" = \"latest\" ]; then\n log \"Upgrade detected: ${INSTALLED_VERSION} -> ${IMAGE_VERSION}${IMAGE_VERSION:+ (latest: always upgrade)}\"\n bash /opt/hiclaw/scripts/init/upgrade-builtins.sh\n log \"Manager workspace upgraded to version: ${IMAGE_VERSION}\"\nelse\n log \"Workspace up to date (version: ${IMAGE_VERSION})\"\nfi\n\n# Local mode: wait for mc mirror initialization (shared + worker data in /root/hiclaw-fs/)\nif [ \"${HICLAW_RUNTIME}\" != \"aliyun\" ]; then\n log \"Waiting for MinIO storage initialization...\"\n _minio_wait=0\n while [ ! -f /root/hiclaw-fs/.initialized ]; do\n sleep 2\n _minio_wait=$(( _minio_wait + 1 ))\n if [ \"${_minio_wait}\" -ge 60 ]; then\n log \"ERROR: MinIO storage initialization timed out after 120s\"\n exit 1\n fi\n done\n log \"MinIO storage initialized\"\nfi\n\n# ============================================================\n# Register Matrix users via Registration API (single-step, no UIAA)\n# ============================================================\nlog \"Registering human admin Matrix account...\"\ncurl -sf -X POST ${HICLAW_MATRIX_SERVER}/_matrix/client/v3/register \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"username\": \"'\"${HICLAW_ADMIN_USER}\"'\",\n \"password\": \"'\"${HICLAW_ADMIN_PASSWORD}\"'\",\n \"auth\": {\n \"type\": \"m.login.registration_token\",\n \"token\": \"'\"${HICLAW_REGISTRATION_TOKEN}\"'\"\n }\n }' > /dev/null 2>&1 || log \"Admin account may already exist\"\n\nlog \"Registering Manager Agent Matrix account...\"\ncurl -sf -X POST ${HICLAW_MATRIX_SERVER}/_matrix/client/v3/register \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"username\": \"manager\",\n \"password\": \"'\"${HICLAW_MANAGER_PASSWORD}\"'\",\n \"auth\": {\n \"type\": \"m.login.registration_token\",\n \"token\": \"'\"${HICLAW_REGISTRATION_TOKEN}\"'\"\n }\n }' > /dev/null 2>&1 || log \"Manager account may already exist\"\n\n# Get Manager Agent's Matrix access token\nlog \"Obtaining Manager Matrix access token...\"\n_LOGIN_RESPONSE=$(curl -sf -X POST ${HICLAW_MATRIX_SERVER}/_matrix/client/v3/login \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"type\": \"m.login.password\",\n \"identifier\": {\"type\": \"m.id.user\", \"user\": \"manager\"},\n \"password\": \"'\"${HICLAW_MANAGER_PASSWORD}\"'\"\n }' 2>&1)\n_LOGIN_EXIT=$?\nlog \"Matrix login HTTP exit code: ${_LOGIN_EXIT}\"\nlog \"Matrix login response: ${_LOGIN_RESPONSE}\"\n\nMANAGER_TOKEN=$(echo \"${_LOGIN_RESPONSE}\" | jq -r '.access_token' 2>/dev/null)\n\nif [ -z \"${MANAGER_TOKEN}\" ] || [ \"${MANAGER_TOKEN}\" = \"null\" ]; then\n log \"ERROR: Failed to obtain Manager Matrix token (exit=${_LOGIN_EXIT})\"\n log \"ERROR: Login response was: ${_LOGIN_RESPONSE}\"\n exit 1\nfi\nlog \"Manager Matrix token obtained (token prefix: ${MANAGER_TOKEN:0:10}...)\"\n\n# ============================================================\n# Local mode: Initialize Higress Console + configure routes\n# Cloud mode: Create admin DM room + schedule welcome message\n# ============================================================\nif [ \"${HICLAW_RUNTIME}\" != \"aliyun\" ]; then\n COOKIE_FILE=\"/tmp/higress-session-cookie\"\n\n log \"Waiting for Higress Console to be fully ready and initializing admin...\"\n INIT_DONE=false\n for i in $(seq 1 90); do\n INIT_RESULT=$(curl -s -X POST http://127.0.0.1:8001/system/init \\\n -H 'Content-Type: application/json' \\\n -d '{\"adminUser\":{\"name\":\"'\"${HICLAW_ADMIN_USER}\"'\",\"password\":\"'\"${HICLAW_ADMIN_PASSWORD}\"'\",\"displayName\":\"'\"${HICLAW_ADMIN_USER}\"'\"}}' 2>/dev/null) || true\n if echo \"${INIT_RESULT}\" | grep -qE '\"success\":true|already.?init' 2>/dev/null; then\n INIT_DONE=true\n break\n fi\n if echo \"${INIT_RESULT}\" | grep -q '\"name\"' 2>/dev/null; then\n INIT_DONE=true\n break\n fi\n sleep 2\n done\n\n if [ \"${INIT_DONE}\" != \"true\" ]; then\n log \"ERROR: Higress Console did not become ready within 180s\"\n exit 1\n fi\n log \"Higress Console init done\"\n\n log \"Logging into Higress Console...\"\n LOGIN_OK=false\n for i in $(seq 1 10); do\n HTTP_CODE=$(curl -s -o /dev/null -w '%{http_code}' -X POST http://127.0.0.1:8001/session/login \\\n -H 'Content-Type: application/json' \\\n -c \"${COOKIE_FILE}\" \\\n -d '{\"username\":\"'\"${HICLAW_ADMIN_USER}\"'\",\"password\":\"'\"${HICLAW_ADMIN_PASSWORD}\"'\"}' 2>/dev/null) || true\n if { [ \"${HTTP_CODE}\" = \"200\" ] || [ \"${HTTP_CODE}\" = \"201\" ]; } && [ -f \"${COOKIE_FILE}\" ] && [ -s \"${COOKIE_FILE}\" ]; then\n LOGIN_OK=true\n break\n fi\n log \"Login attempt $i (HTTP ${HTTP_CODE}), retrying in 3s...\"\n sleep 3\n done\n\n if [ \"${LOGIN_OK}\" != \"true\" ]; then\n log \"ERROR: Could not login to Higress Console after retries\"\n exit 1\n fi\n log \"Higress Console login successful\"\n\n VERIFY_CODE=$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8001/v1/consumers -b \"${COOKIE_FILE}\" 2>/dev/null) || true\n if [ \"${VERIFY_CODE}\" = \"200\" ]; then\n log \"Console session verified (cookie valid)\"\n else\n log \"WARNING: Console session may be invalid (verify returned HTTP ${VERIFY_CODE})\"\n rm -f \"${COOKIE_FILE}\"\n for i in $(seq 1 5); do\n curl -s -o /dev/null -w '%{http_code}' -X POST http://127.0.0.1:8001/session/login \\\n -H 'Content-Type: application/json' \\\n -c \"${COOKIE_FILE}\" \\\n -d '{\"username\":\"'\"${HICLAW_ADMIN_USER}\"'\",\"password\":\"'\"${HICLAW_ADMIN_PASSWORD}\"'\"}' 2>/dev/null\n VERIFY2=$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8001/v1/consumers -b \"${COOKIE_FILE}\" 2>/dev/null) || true\n if [ \"${VERIFY2}\" = \"200\" ]; then\n log \"Re-login successful, session verified\"\n break\n fi\n sleep 2\n done\n fi\n\n export HIGRESS_COOKIE_FILE=\"${COOKIE_FILE}\"\n\n # Configure Higress routes, consumers, MCP servers\n /opt/hiclaw/scripts/init/setup-higress.sh\nfi\n\n# ============================================================\n# Create admin DM room, persist to state.json, send welcome message\n# Runs in both local and cloud modes (idempotent)\n# ============================================================\nMANAGER_FULL_ID=\"@manager:${MATRIX_DOMAIN}\"\nADMIN_FULL_ID=\"@${HICLAW_ADMIN_USER}:${MATRIX_DOMAIN}\"\n\nlog \"Logging in as admin to create DM room...\"\n_ADMIN_LOGIN=$(curl -sf -X POST \"${HICLAW_MATRIX_SERVER}/_matrix/client/v3/login\" \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"type\": \"m.login.password\",\n \"identifier\": {\"type\": \"m.id.user\", \"user\": \"'\"${HICLAW_ADMIN_USER}\"'\"},\n \"password\": \"'\"${HICLAW_ADMIN_PASSWORD}\"'\"\n }' 2>&1) || true\n\nADMIN_MATRIX_TOKEN=$(echo \"${_ADMIN_LOGIN}\" | jq -r '.access_token // empty' 2>/dev/null)\nif [ -z \"${ADMIN_MATRIX_TOKEN}\" ]; then\n log \"WARNING: Failed to login as admin, skipping DM room creation\"\nelse\n # Search for existing DM room with Manager (idempotent)\n DM_ROOM_ID=\"\"\n _JOINED_ROOMS=$(curl -sf \"${HICLAW_MATRIX_SERVER}/_matrix/client/v3/joined_rooms\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" 2>/dev/null \\\n | jq -r '.joined_rooms[]' 2>/dev/null) || true\n for _rid in ${_JOINED_ROOMS}; do\n _members=$(curl -sf \"${HICLAW_MATRIX_SERVER}/_matrix/client/v3/rooms/${_rid}/members\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" 2>/dev/null \\\n | jq -r '.chunk[].state_key' 2>/dev/null) || continue\n _count=$(echo \"${_members}\" | wc -l | xargs)\n if [ \"${_count}\" = \"2\" ] && echo \"${_members}\" | grep -q \"@manager:\"; then\n DM_ROOM_ID=\"${_rid}\"\n break\n fi\n done\n\n if [ -n \"${DM_ROOM_ID}\" ]; then\n log \"Existing DM room found: ${DM_ROOM_ID}\"\n else\n log \"Creating DM room with Manager...\"\n _RAW=$(curl -s -w '\\nHTTP_CODE:%{http_code}' -X POST \"${HICLAW_MATRIX_SERVER}/_matrix/client/v3/createRoom\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d \"{\\\"is_direct\\\":true,\\\"invite\\\":[\\\"${MANAGER_FULL_ID}\\\"],\\\"preset\\\":\\\"trusted_private_chat\\\"}\" 2>&1) || true\n _HTTP_CODE=$(echo \"${_RAW}\" | tail -1 | sed 's/HTTP_CODE://')\n _CREATE_RESP=$(echo \"${_RAW}\" | sed '$d')\n DM_ROOM_ID=$(echo \"${_CREATE_RESP}\" | jq -r '.room_id // empty' 2>/dev/null)\n if [ -n \"${DM_ROOM_ID}\" ]; then\n log \"DM room created: ${DM_ROOM_ID}\"\n else\n log \"WARNING: Failed to create DM room (HTTP ${_HTTP_CODE}): ${_CREATE_RESP}\"\n fi\n fi\n\n # Persist admin DM room ID to state.json\n if [ -n \"${DM_ROOM_ID}\" ]; then\n STATE_SCRIPT=\"/opt/hiclaw/agent/skills/task-management/scripts/manage-state.sh\"\n if [ -f \"${STATE_SCRIPT}\" ]; then\n bash \"${STATE_SCRIPT}\" --action init 2>/dev/null || true\n bash \"${STATE_SCRIPT}\" --action set-admin-dm --room-id \"${DM_ROOM_ID}\" 2>/dev/null || true\n log \"Admin DM room persisted to state.json: ${DM_ROOM_ID}\"\n fi\n fi\n\n # Schedule welcome message in background (only on first boot)\n if [ -n \"${DM_ROOM_ID}\" ] && [ ! -f \"/root/manager-workspace/soul-configured\" ]; then\n log \"Scheduling welcome message (background, waiting for OpenClaw to start)...\"\n (\n _HICLAW_LANGUAGE=\"${HICLAW_LANGUAGE:-zh}\"\n _HICLAW_TIMEZONE=\"${TZ:-Asia/Shanghai}\"\n _wait=0\n _ready=false\n while [ \"${_wait}\" -lt 300 ]; do\n if curl -sf http://127.0.0.1:18799/ > /dev/null 2>&1; then\n _ready=true\n break\n fi\n sleep 3\n _wait=$((_wait + 3))\n done\n if [ \"${_ready}\" != \"true\" ]; then\n echo \"[manager] WARNING: OpenClaw gateway not ready within 300s, skipping welcome message\"\n exit 0\n fi\n # Ensure Manager has joined the DM room before sending the welcome\n # message. Without this, there is a race between OpenClaw's Matrix\n # auto-join and the message send — the message may land before Manager\n # joins, so OpenClaw's /sync never picks it up.\n _join_ok=false\n for _join_attempt in 1 2 3; do\n if curl -sf -X POST \"${HICLAW_MATRIX_SERVER}/_matrix/client/v3/rooms/${DM_ROOM_ID}/join\" \\\n -H \"Authorization: Bearer ${MANAGER_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d '{}' > /dev/null 2>&1; then\n echo \"[manager] Manager joined DM room before welcome message\"\n _join_ok=true\n break\n fi\n sleep 2\n done\n if [ \"${_join_ok}\" != \"true\" ]; then\n echo \"[manager] WARNING: Manager join request failed after 3 attempts (may already be joined)\"\n fi\n _welcome_msg=\"This is an automated message from the HiClaw setup. This is a fresh installation.\n\n--- Installation Context ---\nUser Language: ${_HICLAW_LANGUAGE} (zh = Chinese, en = English)\nUser Timezone: ${_HICLAW_TIMEZONE} (IANA timezone identifier)\n---\n\nYou are an AI agent that manages a team of worker agents. Your identity and personality have not been configured yet — the human admin is about to meet you for the first time.\n\nPlease begin the onboarding conversation:\n\n1. Greet the admin warmly and briefly describe what you can do (coordinate workers, manage tasks, run multi-agent projects)\n2. The user has selected \\\"${_HICLAW_LANGUAGE}\\\" as their preferred language during installation. Use this language for your greeting and all subsequent communication.\n3. The user's timezone is ${_HICLAW_TIMEZONE}. Based on this timezone, you may infer their likely region and suggest additional language options.\n4. Ask them: a) What would they like to call you? b) Communication style preference? c) Any behavior guidelines? d) Confirm default language\n5. After they reply, write their preferences to ~/SOUL.md\n6. Confirm what you wrote, and ask if they would like to adjust anything\n7. Once confirmed, run: touch ~/soul-configured\n\nThe human admin will start chatting shortly.\"\n _txn_id=\"welcome-$(date +%s)\"\n _payload=$(jq -nc --arg body \"${_welcome_msg}\" '{\"msgtype\":\"m.text\",\"body\":$body}')\n _raw=$(curl -s -w '\\nHTTP_CODE:%{http_code}' -X PUT \"${HICLAW_MATRIX_SERVER}/_matrix/client/v3/rooms/${DM_ROOM_ID}/send/m.room.message/${_txn_id}\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d \"${_payload}\" 2>&1) || true\n _http_code=$(echo \"${_raw}\" | tail -1 | sed 's/HTTP_CODE://')\n _send_resp=$(echo \"${_raw}\" | sed '$d')\n if echo \"${_send_resp}\" | jq -e '.event_id' > /dev/null 2>&1; then\n echo \"[manager] Welcome message sent to DM room\"\n else\n echo \"[manager] WARNING: Failed to send welcome message (HTTP ${_http_code}): ${_send_resp}\"\n fi\n ) &\n log \"Welcome message background process started (PID: $!)\"\n fi\nfi\n\n# ============================================================\n# Generate Manager Agent openclaw.json from template\n# ============================================================\nlog \"Generating Manager openclaw.json...\"\nexport MANAGER_MATRIX_TOKEN=\"${MANAGER_TOKEN}\"\nexport MANAGER_GATEWAY_KEY=\"${HICLAW_MANAGER_GATEWAY_KEY}\"\n# Resolve model parameters based on model name\nMODEL_NAME=\"${HICLAW_DEFAULT_MODEL:-qwen3.5-plus}\"\ncase \"${MODEL_NAME}\" in\n gpt-5.3-codex|gpt-5-mini|gpt-5-nano)\n export MODEL_CONTEXT_WINDOW=400000 MODEL_MAX_TOKENS=128000 ;;\n claude-opus-4-6)\n export MODEL_CONTEXT_WINDOW=1000000 MODEL_MAX_TOKENS=128000 ;;\n claude-sonnet-4-6)\n export MODEL_CONTEXT_WINDOW=1000000 MODEL_MAX_TOKENS=64000 ;;\n claude-haiku-4-5)\n export MODEL_CONTEXT_WINDOW=200000 MODEL_MAX_TOKENS=64000 ;;\n qwen3.5-plus)\n export MODEL_CONTEXT_WINDOW=200000 MODEL_MAX_TOKENS=64000 ;;\n deepseek-chat|deepseek-reasoner|kimi-k2.5)\n export MODEL_CONTEXT_WINDOW=256000 MODEL_MAX_TOKENS=128000 ;;\n glm-5|MiniMax-M2.7|MiniMax-M2.7-highspeed|MiniMax-M2.5)\n export MODEL_CONTEXT_WINDOW=200000 MODEL_MAX_TOKENS=128000 ;;\n *)\n export MODEL_CONTEXT_WINDOW=150000 MODEL_MAX_TOKENS=128000 ;;\nesac\nexport MODEL_REASONING=true\n\n# Override with user-supplied custom model parameters from env (set during install)\n[ -n \"${HICLAW_MODEL_CONTEXT_WINDOW:-}\" ] && export MODEL_CONTEXT_WINDOW=\"${HICLAW_MODEL_CONTEXT_WINDOW}\"\n[ -n \"${HICLAW_MODEL_MAX_TOKENS:-}\" ] && export MODEL_MAX_TOKENS=\"${HICLAW_MODEL_MAX_TOKENS}\"\n[ -n \"${HICLAW_MODEL_REASONING:-}\" ] && export MODEL_REASONING=\"${HICLAW_MODEL_REASONING}\"\n\n# E2EE: convert HICLAW_MATRIX_E2EE to JSON boolean for template substitution\nif [ \"${HICLAW_MATRIX_E2EE:-0}\" = \"1\" ] || [ \"${HICLAW_MATRIX_E2EE:-}\" = \"true\" ]; then\n export MATRIX_E2EE_ENABLED=true\nelse\n export MATRIX_E2EE_ENABLED=false\nfi\nlog \"Matrix E2EE: ${MATRIX_E2EE_ENABLED}\"\n\n# Resolve input modalities: only vision-capable models get \"image\"\ncase \"${MODEL_NAME}\" in\n gpt-5.4|gpt-5.3-codex|gpt-5-mini|gpt-5-nano|claude-opus-4-6|claude-sonnet-4-6|claude-haiku-4-5|qwen3.5-plus|kimi-k2.5)\n export MODEL_INPUT='[\"text\", \"image\"]' ;;\n *)\n export MODEL_INPUT='[\"text\"]' ;;\nesac\n# Override with user-supplied vision setting from env\nif [ \"${HICLAW_MODEL_VISION:-}\" = \"true\" ]; then\n export MODEL_INPUT='[\"text\", \"image\"]'\nelif [ \"${HICLAW_MODEL_VISION:-}\" = \"false\" ]; then\n export MODEL_INPUT='[\"text\"]'\nfi\n\nlog \"Model: ${MODEL_NAME} (context=${MODEL_CONTEXT_WINDOW}, maxTokens=${MODEL_MAX_TOKENS}, reasoning=${MODEL_REASONING}, input=${MODEL_INPUT})\"\n\nif [ -f /root/manager-workspace/openclaw.json ]; then\n log \"Manager openclaw.json already exists, updating dynamic fields only (preserving user customizations)...\"\n # Merge known models into existing config (add missing, preserve user-added)\n # Use known-models.json (valid JSON) instead of template (contains ${VAR} placeholders)\n KNOWN_MODELS=$(cat /opt/hiclaw/configs/known-models.json 2>/dev/null || echo '[]')\n jq --arg token \"${MANAGER_TOKEN}\" \\\n --arg key \"${HICLAW_MANAGER_GATEWAY_KEY}\" \\\n --arg model \"${MODEL_NAME}\" \\\n --argjson e2ee \"${MATRIX_E2EE_ENABLED}\" \\\n --argjson known_models \"${KNOWN_MODELS}\" \\\n --argjson ctx \"${MODEL_CONTEXT_WINDOW}\" \\\n --argjson max \"${MODEL_MAX_TOKENS}\" \\\n --argjson reasoning \"${MODEL_REASONING}\" \\\n --argjson input \"${MODEL_INPUT}\" \\\n '\n # Merge known models: add any model id not already present\n .models.providers[\"hiclaw-gateway\"].models as $existing\n | ($existing | map(.id)) as $existing_ids\n | ($known_models | map(select(.id as $id | $existing_ids | index($id) | not))) as $new\n | .models.providers[\"hiclaw-gateway\"].models = ($existing + $new)\n # Ensure the user-chosen default model is in the list (custom model support)\n | if (.models.providers[\"hiclaw-gateway\"].models | map(.id) | index($model) | not) then\n .models.providers[\"hiclaw-gateway\"].models += [{\"id\": $model, \"name\": $model, \"reasoning\": $reasoning, \"contextWindow\": $ctx, \"maxTokens\": $max, \"input\": $input}]\n else . end\n # Rebuild model aliases from the full models list\n | (.models.providers[\"hiclaw-gateway\"].models | map({ (\"hiclaw-gateway/\" + .id): { \"alias\": .id } }) | add // {}) as $aliases\n | .agents.defaults.models = ((.agents.defaults.models // {}) + $aliases)\n | .channels.matrix.accessToken = $token | .models.providers[\"hiclaw-gateway\"].apiKey = $key\n | ((.hooks.token // \"\") as $ht | if $ht == $key or $ht == ($key + \"-hooks\" | @base64) then del(.hooks) else . end)\n | .agents.defaults.model.primary = (\"hiclaw-gateway/\" + $model)\n | .commands.restart = true\n | .gateway.controlUi.dangerouslyDisableDeviceAuth = true\n | .channels.matrix.encryption = $e2ee\n ' \\\n /root/manager-workspace/openclaw.json > /tmp/openclaw.json.tmp && \\\n mv /tmp/openclaw.json.tmp /root/manager-workspace/openclaw.json\n # Verify the token was written correctly\n _written_token=$(jq -r '.channels.matrix.accessToken' /root/manager-workspace/openclaw.json 2>/dev/null)\n if [ -z \"${_written_token}\" ] || [ \"${_written_token}\" = \"null\" ]; then\n log \"ERROR: Matrix token was not written correctly to openclaw.json (got: ${_written_token})\"\n else\n log \"Matrix token written to openclaw.json (prefix: ${_written_token:0:10}...)\"\n fi\nelse\n log \"Manager openclaw.json not found, generating from template...\"\n envsubst < /opt/hiclaw/configs/manager-openclaw.json.tmpl > /root/manager-workspace/openclaw.json\n # Inject custom model if not in the built-in list\n if ! jq -e --arg model \"${MODEL_NAME}\" '.models.providers[\"hiclaw-gateway\"].models | map(.id) | index($model)' /root/manager-workspace/openclaw.json > /dev/null 2>&1; then\n log \"Custom model '${MODEL_NAME}' not in built-in list, injecting into config...\"\n jq --arg model \"${MODEL_NAME}\" \\\n --argjson ctx \"${MODEL_CONTEXT_WINDOW}\" \\\n --argjson max \"${MODEL_MAX_TOKENS}\" \\\n --argjson reasoning \"${MODEL_REASONING}\" \\\n --argjson input \"${MODEL_INPUT}\" \\\n '\n .models.providers[\"hiclaw-gateway\"].models += [{\"id\": $model, \"name\": $model, \"reasoning\": $reasoning, \"contextWindow\": $ctx, \"maxTokens\": $max, \"input\": $input}]\n | .agents.defaults.models += {(\"hiclaw-gateway/\" + $model): {\"alias\": $model}}\n ' /root/manager-workspace/openclaw.json > /tmp/openclaw.json.tmp && \\\n mv /tmp/openclaw.json.tmp /root/manager-workspace/openclaw.json\n fi\n _written_token=$(jq -r '.channels.matrix.accessToken' /root/manager-workspace/openclaw.json 2>/dev/null)\n log \"Matrix token written from template (prefix: ${_written_token:0:10}...)\"\nfi\n\n# Cloud mode: overlay cloud-specific settings onto generated config\nif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n log \"Applying cloud overlay to openclaw.json...\"\n jq --arg homeserver \"${HICLAW_MATRIX_SERVER}\" \\\n --arg gateway \"${HICLAW_AI_GATEWAY_URL}/v1\" \\\n --arg key \"${HICLAW_MANAGER_GATEWAY_KEY}\" \\\n '.channels.matrix.homeserver = $homeserver\n | .models.providers[\"hiclaw-gateway\"].baseUrl = $gateway\n | .models.providers[\"hiclaw-gateway\"].apiKey = $key\n | ((.hooks.token // \"\") as $ht | if $ht == $key or $ht == ($key + \"-hooks\" | @base64) then del(.hooks) else . end)\n | .commands.restart = false' \\\n /root/manager-workspace/openclaw.json > /tmp/openclaw-cloud.json && \\\n mv /tmp/openclaw-cloud.json /root/manager-workspace/openclaw.json\n log \"Cloud overlay applied\"\nfi\n\n# ============================================================\n# Optional: enable openclaw-cms-plugin observability\n# Config is applied at runtime so secrets stay out of image layers.\n# ============================================================\nCMS_TRACES_ENABLED=\"$(echo \"${HICLAW_CMS_TRACES_ENABLED:-false}\" | tr '[:upper:]' '[:lower:]')\"\nif [ \"${CMS_TRACES_ENABLED}\" = \"true\" ]; then\n CMS_PLUGIN_NAME=\"openclaw-cms-plugin\"\n CMS_PLUGIN_DIR=\"${OPENCLAW_CMS_PLUGIN_DIR:-/opt/openclaw/extensions/openclaw-cms-plugin}\"\n CMS_PLUGIN_MANIFEST=\"${CMS_PLUGIN_DIR}/openclaw.plugin.json\"\n DIAG_PLUGIN_NAME=\"diagnostics-otel\"\n DIAG_PLUGIN_DIR=\"/opt/openclaw/extensions/diagnostics-otel\"\n CMS_LICENSE_KEY=\"${HICLAW_CMS_LICENSE_KEY:-}\"\n CMS_PROJECT=\"${HICLAW_CMS_PROJECT:-}\"\n CMS_METRICS_ENABLED=\"${HICLAW_CMS_METRICS_ENABLED:-false}\"\n\n if [ ! -f \"${CMS_PLUGIN_MANIFEST}\" ]; then\n log \"WARNING: ${CMS_PLUGIN_NAME} manifest not found at ${CMS_PLUGIN_MANIFEST}, skipping plugin config\"\n else\n _missing=0\n [ -z \"${HICLAW_CMS_ENDPOINT:-}\" ] && log \"WARNING: HICLAW_CMS_ENDPOINT is required when HICLAW_CMS_TRACES_ENABLED=true\" && _missing=1\n [ -z \"${CMS_LICENSE_KEY:-}\" ] && log \"WARNING: HICLAW_CMS_LICENSE_KEY is required when HICLAW_CMS_TRACES_ENABLED=true\" && _missing=1\n [ -z \"${HICLAW_CMS_WORKSPACE:-}\" ] && log \"WARNING: HICLAW_CMS_WORKSPACE is required when HICLAW_CMS_TRACES_ENABLED=true\" && _missing=1\n\n if [ \"${_missing}\" = \"0\" ]; then\n CMS_SERVICE_NAME=\"${HICLAW_CMS_SERVICE_NAME:-hiclaw-manager}\"\n CMS_ENABLE_METRICS=\"${CMS_METRICS_ENABLED}\"\n DIAG_AVAILABLE=\"0\"\n _metrics_lc=\"$(echo \"${CMS_ENABLE_METRICS}\" | tr '[:upper:]' '[:lower:]')\"\n if [ \"${_metrics_lc}\" = \"true\" ]; then\n if [ -f \"${DIAG_PLUGIN_DIR}/package.json\" ]; then\n DIAG_AVAILABLE=\"1\"\n if [ ! -d \"${DIAG_PLUGIN_DIR}/node_modules\" ]; then\n log \"diagnostics-otel dependencies missing, installing...\"\n if (cd \"${DIAG_PLUGIN_DIR}\" && npm install --omit=dev --ignore-scripts >/tmp/hiclaw-diag-install.log 2>&1); then\n log \"diagnostics-otel dependencies installed\"\n else\n log \"WARNING: diagnostics-otel npm install failed, metrics plugin may not load\"\n fi\n else\n log \"diagnostics-otel dependencies already present\"\n fi\n else\n log \"WARNING: diagnostics-otel package.json not found at ${DIAG_PLUGIN_DIR}, metrics plugin may not load\"\n fi\n fi\n\n log \"Applying ${CMS_PLUGIN_NAME} config to openclaw.json...\"\n jq --arg pluginName \"${CMS_PLUGIN_NAME}\" \\\n --arg pluginDir \"${CMS_PLUGIN_DIR}\" \\\n --arg endpoint \"${HICLAW_CMS_ENDPOINT}\" \\\n --arg licenseKey \"${CMS_LICENSE_KEY}\" \\\n --arg armsProject \"${CMS_PROJECT}\" \\\n --arg cmsWorkspace \"${HICLAW_CMS_WORKSPACE}\" \\\n --arg serviceName \"${CMS_SERVICE_NAME}\" \\\n --arg diagPluginName \"${DIAG_PLUGIN_NAME}\" \\\n --arg diagPluginDir \"${DIAG_PLUGIN_DIR}\" \\\n --arg metricsRaw \"${CMS_ENABLE_METRICS}\" \\\n --arg diagAvailableRaw \"${DIAG_AVAILABLE}\" \\\n '\n .plugins = (.plugins // {})\n | .plugins.load = (.plugins.load // {})\n | .plugins.entries = (.plugins.entries // {})\n | if (.plugins.allow | type) != \"array\" then .plugins.allow = [] else . end\n | if (.plugins.allow | index($pluginName)) == null then .plugins.allow += [$pluginName] else . end\n | if (.plugins.load.paths | type) != \"array\" then .plugins.load.paths = [] else . end\n | if (.plugins.load.paths | index($pluginDir)) == null then .plugins.load.paths += [$pluginDir] else . end\n | .plugins.entries[$pluginName] = {\n \"enabled\": true,\n \"config\": {\n \"endpoint\": $endpoint,\n \"headers\": {\n \"x-arms-license-key\": $licenseKey,\n \"x-arms-project\": $armsProject,\n \"x-cms-workspace\": $cmsWorkspace\n },\n \"serviceName\": $serviceName\n }\n }\n\n # diagnostics-otel metrics (optional)\n | ($metricsRaw | ascii_downcase) as $m\n | ($diagAvailableRaw == \"1\") as $diagAvailable\n | (($m == \"true\") and $diagAvailable) as $metricsEnabled\n | if $metricsEnabled then\n (if (.plugins.allow | index($diagPluginName)) == null then .plugins.allow += [$diagPluginName] else . end)\n | (if (.plugins.load.paths | index($diagPluginDir)) == null then .plugins.load.paths += [$diagPluginDir] else . end)\n | .plugins.entries[$diagPluginName].enabled = true\n | .diagnostics = (.diagnostics // {})\n | .diagnostics.otel = (.diagnostics.otel // {})\n | .diagnostics.enabled = true\n | .diagnostics.otel.enabled = true\n | .diagnostics.otel.endpoint = $endpoint\n | .diagnostics.otel.protocol = (.diagnostics.otel.protocol // \"http/protobuf\")\n | .diagnostics.otel.headers = {\n \"x-arms-license-key\": $licenseKey,\n \"x-arms-project\": $armsProject,\n \"x-cms-workspace\": $cmsWorkspace\n }\n | .diagnostics.otel.serviceName = $serviceName\n | .diagnostics.otel.metrics = true\n | .diagnostics.otel.traces = (.diagnostics.otel.traces // false)\n | .diagnostics.otel.logs = (.diagnostics.otel.logs // false)\n else\n .\n end\n ' /root/manager-workspace/openclaw.json > /tmp/openclaw-cms.json && \\\n mv /tmp/openclaw-cms.json /root/manager-workspace/openclaw.json\n log \"${CMS_PLUGIN_NAME} config applied (metrics=${CMS_ENABLE_METRICS}, service=${CMS_SERVICE_NAME})\"\n else\n log \"Skipping ${CMS_PLUGIN_NAME} config due to missing required env vars\"\n fi\n fi\nfi\n\n# ============================================================\n# Detect container runtime (for Worker creation)\n# ============================================================\nsource /opt/hiclaw/scripts/lib/container-api.sh\nif container_api_available; then\n log \"Container runtime socket detected at ${CONTAINER_SOCKET} — direct Worker creation enabled\"\n export HICLAW_CONTAINER_RUNTIME=\"socket\"\nelif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n log \"Cloud mode — Workers created via SAE API\"\n export HICLAW_CONTAINER_RUNTIME=\"cloud\"\nelse\n log \"No container runtime found — Worker creation will output install commands\"\n export HICLAW_CONTAINER_RUNTIME=\"none\"\nfi\n\n# ============================================================\n# Upgrade Worker openclaw.json: merge known models + E2EE flag into existing configs\n# Existing workers in MinIO may have old single-model configs or missing encryption field.\n# Merge template models so they can hot-switch without restart.\n# ============================================================\nREGISTRY_FILE=\"/root/manager-workspace/workers-registry.json\"\nif [ -f \"${REGISTRY_FILE}\" ]; then\n # Use known-models.json (valid JSON) instead of template (contains ${VAR} placeholders)\n KNOWN_MODELS_FILE=\"/opt/hiclaw/configs/known-models.json\"\n if [ -f \"${KNOWN_MODELS_FILE}\" ]; then\n _KNOWN_MODELS=$(cat \"${KNOWN_MODELS_FILE}\")\n for _wname in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_wname}\" ] && continue\n _minio_path=\"${HICLAW_STORAGE_PREFIX}/agents/${_wname}/openclaw.json\"\n _tmp_in=\"/tmp/openclaw-${_wname}-models-upgrade-in.json\"\n if mc cp \"${_minio_path}\" \"${_tmp_in}\" 2>/dev/null; then\n _tmp_out=\"/tmp/openclaw-${_wname}-models-upgrade-out.json\"\n # Idempotent merge: add missing known models, rebuild aliases, set e2ee.\n # Always runs — jq deduplicates by model id, so re-runs are safe.\n jq --argjson known_models \"${_KNOWN_MODELS}\" \\\n --argjson e2ee \"${MATRIX_E2EE_ENABLED}\" '\n .models.providers[\"hiclaw-gateway\"].models as $existing\n | ($existing | map(.id)) as $existing_ids\n | ($known_models | map(select(.id as $id | $existing_ids | index($id) | not))) as $new\n | .models.providers[\"hiclaw-gateway\"].models = ($existing + $new)\n | (.models.providers[\"hiclaw-gateway\"].models | map({ (\"hiclaw-gateway/\" + .id): { \"alias\": .id } }) | add // {}) as $aliases\n | .agents.defaults.models = ((.agents.defaults.models // {}) + $aliases)\n | .channels.matrix.encryption = $e2ee\n ' \"${_tmp_in}\" > \"${_tmp_out}\" 2>/dev/null\n if ! diff -q \"${_tmp_in}\" \"${_tmp_out}\" > /dev/null 2>&1; then\n if mc cp \"${_tmp_out}\" \"${_minio_path}\" 2>/dev/null; then\n _new_count=$(jq '.models.providers[\"hiclaw-gateway\"].models | length' \"${_tmp_out}\" 2>/dev/null)\n log \"Worker ${_wname}: upgraded openclaw.json (models: ${_new_count}, e2ee: ${MATRIX_E2EE_ENABLED})\"\n fi\n fi\n rm -f \"${_tmp_in}\" \"${_tmp_out}\"\n fi\n done\n fi\nfi\n\n# ============================================================\n# Ensure Worker Matrix password files exist in MinIO (E2EE fix)\n# Workers need to re-login on restart to get a fresh device_id.\n# Older workers created before this fix won't have the password file.\n# ============================================================\nif [ -f \"${REGISTRY_FILE}\" ]; then\n for _wname in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_wname}\" ] && continue\n _creds_file=\"/data/worker-creds/${_wname}.env\"\n if [ -f \"${_creds_file}\" ]; then\n # Check if password file already exists in MinIO\n if ! mc stat \"${HICLAW_STORAGE_PREFIX}/agents/${_wname}/credentials/matrix/password\" > /dev/null 2>&1; then\n source \"${_creds_file}\"\n if [ -n \"${WORKER_PASSWORD}\" ]; then\n _tmp_pw=\"/tmp/matrix-pw-${_wname}\"\n echo -n \"${WORKER_PASSWORD}\" > \"${_tmp_pw}\"\n mc cp \"${_tmp_pw}\" \"${HICLAW_STORAGE_PREFIX}/agents/${_wname}/credentials/matrix/password\" 2>/dev/null \\\n && log \"Worker ${_wname}: wrote Matrix password to MinIO (E2EE re-login fix)\" \\\n || log \"Worker ${_wname}: WARNING: failed to write Matrix password to MinIO\"\n rm -f \"${_tmp_pw}\"\n fi\n fi\n fi\n done\nfi\n\n# ============================================================\n# Recreate Worker containers as needed after Manager restart.\n# Manager IP may change on restart; Workers use ExtraHosts pointing to Manager IP,\n# so any worker whose ExtraHosts IP no longer matches must be recreated.\n# ============================================================\nif container_api_available; then\n REGISTRY_FILE=\"/root/manager-workspace/workers-registry.json\"\n _manager_ip=$(container_get_manager_ip)\n if [ -f \"${REGISTRY_FILE}\" ]; then\n for _worker_name in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_worker_name}\" ] && continue\n\n # Skip remote workers — they are not Manager-managed containers.\n _deployment=$(jq -r --arg w \"${_worker_name}\" '.workers[$w].deployment // \"local\"' \"${REGISTRY_FILE}\" 2>/dev/null)\n if [ \"${_deployment}\" = \"remote\" ]; then\n log \"Worker ${_worker_name} is remote, skipping container recreate\"\n continue\n fi\n\n _status=$(container_status_worker \"${_worker_name}\")\n if [ \"${_status}\" = \"running\" ]; then\n # Check if ExtraHosts IP still matches current Manager IP.\n # If ExtraHosts is empty the worker uses real DNS — no recreate needed.\n _inspect=$(_api GET \"/containers/${WORKER_CONTAINER_PREFIX}${_worker_name}/json\" 2>/dev/null)\n _extra_hosts_len=$(echo \"${_inspect}\" | jq -r '.HostConfig.ExtraHosts | length' 2>/dev/null)\n if [ -z \"${_extra_hosts_len}\" ] || [ \"${_extra_hosts_len}\" = \"0\" ]; then\n log \"Worker running (no ExtraHosts, real DNS): ${_worker_name}, skipping\"\n continue\n fi\n _worker_ip=$(echo \"${_inspect}\" | jq -r '.HostConfig.ExtraHosts[0]' 2>/dev/null | cut -d: -f2)\n if [ \"${_worker_ip}\" = \"${_manager_ip}\" ]; then\n log \"Worker running with current Manager IP (${_manager_ip}): ${_worker_name}, skipping\"\n continue\n fi\n log \"Worker running but Manager IP changed (${_worker_ip} -> ${_manager_ip}): ${_worker_name}, recreating...\"\n else\n # Container missing or stopped — always recreate.\n log \"Worker container ${_status}: ${_worker_name}, recreating...\"\n fi\n _creds_file=\"/data/worker-creds/${_worker_name}.env\"\n if [ -f \"${_creds_file}\" ]; then\n source \"${_creds_file}\"\n _runtime=$(jq -r --arg w \"${_worker_name}\" '.workers[$w].runtime // \"openclaw\"' \"${REGISTRY_FILE}\" 2>/dev/null)\n _recreated=false\n for _attempt in 1 2 3; do\n if [ \"${_runtime}\" = \"copaw\" ]; then\n container_create_copaw_worker \"${_worker_name}\" \"${_worker_name}\" \"${WORKER_MINIO_PASSWORD}\" 2>&1 && _recreated=true && break\n else\n container_create_worker \"${_worker_name}\" \"${_worker_name}\" \"${WORKER_MINIO_PASSWORD}\" 2>&1 && _recreated=true && break\n fi\n log \" Attempt ${_attempt}/3 failed for ${_worker_name}, retrying in $((5 * _attempt))s...\"\n sleep $((5 * _attempt))\n done\n if [ \"${_recreated}\" = true ]; then\n log \" Recreated ${_runtime} worker: ${_worker_name}\"\n else\n log \" ERROR: Failed to recreate ${_worker_name} after 3 attempts\"\n fi\n else\n log \" WARNING: No credentials found for ${_worker_name} (${_creds_file} missing), skipping\"\n fi\n done\n fi\nfi\n\n# ============================================================\n# Notify workers of builtin updates if upgrade happened\n# Builtin files (AGENTS.md, skills) are already synced by upgrade-builtins.sh\n#\n# Cooldown: skip notification if the last successful notify was within\n# NOTIFY_COOLDOWN_SECS (default 3600s / 1 hour). This prevents repeated\n# notifications when the Manager crash-loops and re-runs upgrade-builtins\n# on every restart (e.g. IMAGE_VERSION=latest always triggers upgrade).\n# ============================================================\nNOTIFY_COOLDOWN_SECS=\"${HICLAW_NOTIFY_COOLDOWN_SECS:-3600}\"\nNOTIFY_TS_FILE=\"/root/manager-workspace/.last-worker-notify-ts\"\n\nif [ -f /root/manager-workspace/.upgrade-pending-worker-notify ]; then\n _now=$(date +%s)\n _last_notify=$(cat \"${NOTIFY_TS_FILE}\" 2>/dev/null || echo \"0\")\n _elapsed=$(( _now - _last_notify ))\n\n if [ \"${_elapsed}\" -lt \"${NOTIFY_COOLDOWN_SECS}\" ]; then\n log \"Skipping worker builtin notification (last notify ${_elapsed}s ago, cooldown ${NOTIFY_COOLDOWN_SECS}s)\"\n rm -f /root/manager-workspace/.upgrade-pending-worker-notify\n else\n log \"Notifying workers about builtin updates...\"\n REGISTRY_FILE=\"/root/manager-workspace/workers-registry.json\"\n _notify_ok=false\n if [ -f \"${REGISTRY_FILE}\" ]; then\n for _worker_name in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_worker_name}\" ] && continue\n _room_id=$(jq -r --arg w \"${_worker_name}\" '.workers[$w].room_id // empty' \"${REGISTRY_FILE}\" 2>/dev/null)\n if [ -n \"${_room_id}\" ]; then\n _worker_id=\"@${_worker_name}:${MATRIX_DOMAIN}\"\n _txn_id=\"upgrade-$(date +%s%N)\"\n _msg=\"@${_worker_name}:${MATRIX_DOMAIN} Manager upgraded builtin files (AGENTS.md, skills). Please use your file-sync skill to sync the latest config.\"\n _raw=$(curl -s -w '\\nHTTP_CODE:%{http_code}' -X PUT \\\n \"${HICLAW_MATRIX_SERVER}/_matrix/client/v3/rooms/${_room_id}/send/m.room.message/${_txn_id}\" \\\n -H \"Authorization: Bearer ${MANAGER_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d \"{\\\"msgtype\\\":\\\"m.text\\\",\\\"body\\\":\\\"${_msg}\\\",\\\"m.mentions\\\":{\\\"user_ids\\\":[\\\"${_worker_id}\\\"]}}\" \\\n 2>&1) || true\n _http_code=$(echo \"${_raw}\" | tail -1 | sed 's/HTTP_CODE://')\n _notify_resp=$(echo \"${_raw}\" | sed '$d')\n if echo \"${_notify_resp}\" | jq -e '.event_id' > /dev/null 2>&1; then\n log \" Notified ${_worker_name}\"; _notify_ok=true\n else\n log \" WARNING: Failed to notify ${_worker_name} (HTTP ${_http_code}): ${_notify_resp}\"\n fi\n fi\n done\n fi\n # Record timestamp only if at least one notification succeeded\n if [ \"${_notify_ok}\" = true ]; then\n echo \"${_now}\" > \"${NOTIFY_TS_FILE}\"\n fi\n rm -f /root/manager-workspace/.upgrade-pending-worker-notify\n fi\nfi\n\n# ============================================================\n# Start OpenClaw Manager Agent\n# ============================================================\nlog \"Starting Manager Agent (OpenClaw)...\"\n\n# HOME is already set to /root/manager-workspace via docker run -e HOME=...\nexport OPENCLAW_CONFIG_PATH=\"/root/manager-workspace/openclaw.json\"\n\n# Symlink to default OpenClaw config path so CLI commands find the config\nmkdir -p \"${HOME}/.openclaw\"\nln -sf \"/root/manager-workspace/openclaw.json\" \"${HOME}/.openclaw/openclaw.json\"\n\n# Ensure host credential symlinks exist under HOME\nif [ -d \"/host-share\" ]; then\n [ -f \"/host-share/.gitconfig\" ] && ln -sf \"/host-share/.gitconfig\" \"${HOME}/.gitconfig\"\nfi\n\nlog \"HOME=${HOME} (manager-workspace, host-mounted)\"\ncd \"${HOME}\"\n\n# Clean orphaned session write locks (e.g. from SIGKILL or crash before exit handlers)\n# Prevents \"session file locked (timeout 10000ms)\" when PID was reused\nfind \"${HOME}/.openclaw/agents\" -name \"*.jsonl.lock\" -delete 2>/dev/null || true\nlog \"Cleaned up any orphaned session write locks\"\n\n# Clean Matrix crypto storage (SQLite WAL may be corrupted after unclean shutdown)\n# Crypto state is re-negotiated on startup; losing it only means re-establishing E2EE sessions\nrm -rf \"${HOME}/.openclaw/matrix\" 2>/dev/null || true\nlog \"Cleaned Matrix crypto storage (will re-establish E2EE sessions)\"\n\n# ── Render agent doc templates ────────────────────────────────────────────\n# Replace ${VAR} placeholders with actual values so the AI agent reads\n# plain text and never needs to resolve environment variables.\nexport MANAGER_MATRIX_TOKEN MANAGER_TOKEN HIGRESS_COOKIE_FILE\nRENDER=/opt/hiclaw/scripts/lib/render-skills.sh\nlog \"Rendering agent doc templates...\"\n# Manager-owned docs (workspace)\nbash \"$RENDER\" /root/manager-workspace/skills\nbash \"$RENDER\" /root/manager-workspace/skills-alpha\nbash \"$RENDER\" /root/manager-workspace AGENTS.md TOOLS.md HEARTBEAT.md SOUL.md\n# Worker templates (workspace + image) — rendered before push to MinIO\n# so Workers (including remote pip-install) receive plain text\nbash \"$RENDER\" /root/manager-workspace/worker-skills\nbash \"$RENDER\" /root/manager-workspace/worker-agent\nbash \"$RENDER\" /root/manager-workspace/copaw-worker-agent\nbash \"$RENDER\" /opt/hiclaw/agent/worker-skills\nbash \"$RENDER\" /opt/hiclaw/agent/worker-agent\nbash \"$RENDER\" /opt/hiclaw/agent/copaw-worker-agent\nlog \"Agent doc templates rendered\"\n\n# Cloud mode: start background file sync (workspace ↔ OSS) and initial push\nif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n log \"Syncing initial workspace to OSS...\"\n ensure_mc_credentials\n mc mirror /root/manager-workspace/ \"${HICLAW_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" 2>/dev/null || true\n\n # Local → OSS: change-triggered sync\n (\n while true; do\n CHANGED=$(find /root/manager-workspace/ -type f -newermt \"15 seconds ago\" 2>/dev/null | head -1)\n if [ -n \"${CHANGED}\" ]; then\n ensure_mc_credentials 2>/dev/null || true\n mc mirror /root/manager-workspace/ \"${HICLAW_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" --exclude \".npm/**\" \\\n --exclude \".local/**\" --exclude \".mc/**\" 2>/dev/null || true\n fi\n sleep 10\n done\n ) &\n log \"Local→OSS sync started (PID: $!)\"\n\n # OSS → Local: periodic pull (shared data, agent configs)\n (\n while true; do\n sleep 300\n ensure_mc_credentials 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/shared/\" /root/hiclaw-fs/shared/ --overwrite --newer-than \"5m\" 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/agents/\" /root/hiclaw-fs/agents/ --overwrite --newer-than \"5m\" 2>/dev/null || true\n done\n ) &\n log \"OSS→Local sync started (every 5m, PID: $!)\"\nfi\n\n# Launch OpenClaw\nexec openclaw gateway run --verbose --force\n"}, "c6fe49299a05bf81e682ca54ad50c27c54b621fe": {"t": 1785764771.8215184, "n": 43502, "h": "11bcc6d1c9d0dc2b", "c": "#!/bin/bash\n# start-manager-agent.sh - Initialize and start the Manager Agent\n# Supports both local (supervisord) and cloud (SAE single-process) deployments.\n# In local mode this is the last supervisord component to start (priority 800).\n# In cloud mode (HICLAW_RUNTIME=aliyun) this is the container entrypoint.\n\nsource /opt/hiclaw/scripts/lib/hiclaw-env.sh\n\n# ============================================================\n# Set timezone from TZ env var\n# ============================================================\nif [ -n \"${TZ}\" ] && [ -f \"/usr/share/zoneinfo/${TZ}\" ]; then\n ln -sf \"/usr/share/zoneinfo/${TZ}\" /etc/localtime\n echo \"${TZ}\" > /etc/timezone\n log \"Timezone set to ${TZ}\"\nfi\n\nMATRIX_DOMAIN=\"${HICLAW_MATRIX_DOMAIN:-matrix-local.hiclaw.io:8080}\"\nAI_GATEWAY_DOMAIN=\"${HICLAW_AI_GATEWAY_DOMAIN:-aigw-local.hiclaw.io}\"\n\n# ============================================================\n# Cloud mode: validate required environment variables + initial credentials\n# ============================================================\nif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n : \"${HICLAW_MATRIX_URL:?HICLAW_MATRIX_URL is required}\"\n : \"${HICLAW_MATRIX_DOMAIN:?HICLAW_MATRIX_DOMAIN is required}\"\n : \"${HICLAW_AI_GATEWAY_URL:?HICLAW_AI_GATEWAY_URL is required}\"\n : \"${HICLAW_MANAGER_GATEWAY_KEY:?HICLAW_MANAGER_GATEWAY_KEY is required}\"\n : \"${HICLAW_MANAGER_PASSWORD:?HICLAW_MANAGER_PASSWORD is required (cloud containers are stateless, password must be injected)}\"\n : \"${HICLAW_REGISTRATION_TOKEN:?HICLAW_REGISTRATION_TOKEN is required}\"\n : \"${HICLAW_ADMIN_USER:?HICLAW_ADMIN_USER is required}\"\n : \"${HICLAW_ADMIN_PASSWORD:?HICLAW_ADMIN_PASSWORD is required}\"\n log \"Cloud mode: validating environment... OK\"\n log \" Matrix: ${HICLAW_MATRIX_SERVER}, AI Gateway: ${HICLAW_AI_GATEWAY_URL}, OSS: ${HICLAW_STORAGE_BUCKET}\"\n ensure_mc_credentials || { log \"FATAL: Initial STS credential fetch failed\"; exit 1; }\nfi\n\n# ============================================================\n# Local mode: host symlinks, /etc/hosts, wait for local services\n# ============================================================\nif [ \"${HICLAW_RUNTIME}\" != \"aliyun\" ]; then\n # Create symlink for host directory access\n if [ -d \"/host-share\" ]; then\n ORIGINAL_HOST_HOME=\"${HOST_ORIGINAL_HOME:-$HOME}\"\n if [ ! -e \"${ORIGINAL_HOST_HOME}\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/root\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/data\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/host-share\" ]; then\n mkdir -p \"$(dirname \"${ORIGINAL_HOST_HOME}\")\"\n ln -sfn /host-share \"${ORIGINAL_HOST_HOME}\"\n log \"Created symlink: ${ORIGINAL_HOST_HOME} -> /host-share\"\n else\n ln -sfn /host-share /root/host-home\n log \"Created fallback symlink: /root/host-home -> /host-share\"\n fi\n fi\n\n # Add local domains to /etc/hosts\n HOSTS_DOMAINS=\"${MATRIX_DOMAIN%%:*} ${HICLAW_MATRIX_CLIENT_DOMAIN:-matrix-client-local.hiclaw.io} ${AI_GATEWAY_DOMAIN} ${HICLAW_FS_DOMAIN:-fs-local.hiclaw.io}\"\n if ! grep -q \"${AI_GATEWAY_DOMAIN}\" /etc/hosts 2>/dev/null; then\n echo \"127.0.0.1 ${HOSTS_DOMAINS}\" >> /etc/hosts\n log \"Added local domains to /etc/hosts\"\n fi\n\n # Wait for local infrastructure\n waitForService \"Higress Gateway\" \"127.0.0.1\" 8080 180\n waitForService \"Higress Console\" \"127.0.0.1\" 8001 180\n waitForService \"Tuwunel\" \"127.0.0.1\" 6167 120\n waitForHTTP \"Tuwunel Matrix API\" \"${HICLAW_MATRIX_SERVER}/_tuwunel/server_version\" 120\n waitForService \"MinIO\" \"127.0.0.1\" 9000 120\nelse\n # Cloud mode: wait for external Tuwunel\n log \"Waiting for Tuwunel Matrix server at ${HICLAW_MATRIX_SERVER}...\"\n _retry=0\n while [ \"${_retry}\" -lt 30 ]; do\n if curl -sf \"${HICLAW_MATRIX_SERVER}/_matrix/client/versions\" > /dev/null 2>&1; then\n log \"Tuwunel is ready\"\n break\n fi\n _retry=$((_retry + 1))\n log \" Waiting for Tuwunel (attempt ${_retry}/30)...\"\n sleep 5\n done\n if [ \"${_retry}\" -ge 30 ]; then\n log \"ERROR: Tuwunel not reachable at ${HICLAW_MATRIX_SERVER}\"\n exit 1\n fi\nfi\n\n# ============================================================\n# Auto-generate secrets if not provided via environment\n# Persisted to /data so they survive container restart\n# ============================================================\nSECRETS_FILE=\"/data/hiclaw-secrets.env\"\nif [ -f \"${SECRETS_FILE}\" ]; then\n source \"${SECRETS_FILE}\"\n log \"Loaded persisted secrets from ${SECRETS_FILE}\"\nfi\n\nif [ -z \"${HICLAW_MANAGER_GATEWAY_KEY}\" ]; then\n export HICLAW_MANAGER_GATEWAY_KEY=\"$(generateKey 32)\"\n log \"Auto-generated HICLAW_MANAGER_GATEWAY_KEY\"\nfi\nif [ -z \"${HICLAW_MANAGER_PASSWORD}\" ]; then\n export HICLAW_MANAGER_PASSWORD=\"$(generateKey 16)\"\n log \"Auto-generated HICLAW_MANAGER_PASSWORD\"\nfi\n\n# Persist secrets so they survive supervisord restart\nmkdir -p /data\ncat > \"${SECRETS_FILE}\" <<EOF\nexport HICLAW_MANAGER_GATEWAY_KEY=\"${HICLAW_MANAGER_GATEWAY_KEY}\"\nexport HICLAW_MANAGER_PASSWORD=\"${HICLAW_MANAGER_PASSWORD}\"\nEOF\nchmod 600 \"${SECRETS_FILE}\"\n\n# Cloud mode: pull workspace from OSS before initialization\nif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n HICLAW_FS=\"/root/hiclaw-fs\"\n mkdir -p \"${HICLAW_FS}/shared\" \"${HICLAW_FS}/agents\"\n log \"Pulling workspace from OSS...\"\n ensure_mc_credentials\n mc mirror \"${HICLAW_STORAGE_PREFIX}/manager/\" /root/manager-workspace/ --overwrite 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/shared/\" \"${HICLAW_FS}/shared/\" --overwrite 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/agents/\" \"${HICLAW_FS}/agents/\" --overwrite 2>/dev/null || true\n # Symlink hiclaw-fs into workspace for agent access\n ln -sfn \"${HICLAW_FS}\" /root/manager-workspace/hiclaw-fs\nfi\n\n# ============================================================\n# Initialize / upgrade Manager workspace\n# First boot: full init via upgrade-builtins.sh\n# Subsequent boots: compare image version; upgrade only if changed\n# ============================================================\nmkdir -p /root/manager-workspace\n\nIMAGE_VERSION=$(cat /opt/hiclaw/agent/.builtin-version 2>/dev/null || echo \"unknown\")\nINSTALLED_VERSION=$(cat /root/manager-workspace/.builtin-version 2>/dev/null || echo \"\")\n\nif [ ! -f /root/manager-workspace/.initialized ]; then\n log \"First boot: initializing manager workspace...\"\n bash /opt/hiclaw/scripts/init/upgrade-builtins.sh\n touch /root/manager-workspace/.initialized\n log \"Manager workspace initialized (version: ${IMAGE_VERSION})\"\nelif [ \"${IMAGE_VERSION}\" != \"${INSTALLED_VERSION}\" ] || [ \"${IMAGE_VERSION}\" = \"latest\" ]; then\n log \"Upgrade detected: ${INSTALLED_VERSION} -> ${IMAGE_VERSION}${IMAGE_VERSION:+ (latest: always upgrade)}\"\n bash /opt/hiclaw/scripts/init/upgrade-builtins.sh\n log \"Manager workspace upgraded to version: ${IMAGE_VERSION}\"\nelse\n log \"Workspace up to date (version: ${IMAGE_VERSION})\"\nfi\n\n# Local mode: wait for mc mirror initialization (shared + worker data in /root/hiclaw-fs/)\nif [ \"${HICLAW_RUNTIME}\" != \"aliyun\" ]; then\n log \"Waiting for MinIO storage initialization...\"\n _minio_wait=0\n while [ ! -f /root/hiclaw-fs/.initialized ]; do\n sleep 2\n _minio_wait=$(( _minio_wait + 1 ))\n if [ \"${_minio_wait}\" -ge 60 ]; then\n log \"ERROR: MinIO storage initialization timed out after 120s\"\n exit 1\n fi\n done\n log \"MinIO storage initialized\"\nfi\n\n# ============================================================\n# Register Matrix users via Registration API (single-step, no UIAA)\n# ============================================================\nlog \"Registering human admin Matrix account...\"\ncurl -sf -X POST ${HICLAW_MATRIX_SERVER}/_matrix/client/v3/register \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"username\": \"'\"${HICLAW_ADMIN_USER}\"'\",\n \"password\": \"'\"${HICLAW_ADMIN_PASSWORD}\"'\",\n \"auth\": {\n \"type\": \"m.login.registration_token\",\n \"token\": \"'\"${HICLAW_REGISTRATION_TOKEN}\"'\"\n }\n }' > /dev/null 2>&1 || log \"Admin account may already exist\"\n\nlog \"Registering Manager Agent Matrix account...\"\ncurl -sf -X POST ${HICLAW_MATRIX_SERVER}/_matrix/client/v3/register \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"username\": \"manager\",\n \"password\": \"'\"${HICLAW_MANAGER_PASSWORD}\"'\",\n \"auth\": {\n \"type\": \"m.login.registration_token\",\n \"token\": \"'\"${HICLAW_REGISTRATION_TOKEN}\"'\"\n }\n }' > /dev/null 2>&1 || log \"Manager account may already exist\"\n\n# Get Manager Agent's Matrix access token\nlog \"Obtaining Manager Matrix access token...\"\n_LOGIN_RESPONSE=$(curl -sf -X POST ${HICLAW_MATRIX_SERVER}/_matrix/client/v3/login \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"type\": \"m.login.password\",\n \"identifier\": {\"type\": \"m.id.user\", \"user\": \"manager\"},\n \"password\": \"'\"${HICLAW_MANAGER_PASSWORD}\"'\"\n }' 2>&1)\n_LOGIN_EXIT=$?\nlog \"Matrix login HTTP exit code: ${_LOGIN_EXIT}\"\nlog \"Matrix login response: ${_LOGIN_RESPONSE}\"\n\nMANAGER_TOKEN=$(echo \"${_LOGIN_RESPONSE}\" | jq -r '.access_token' 2>/dev/null)\n\nif [ -z \"${MANAGER_TOKEN}\" ] || [ \"${MANAGER_TOKEN}\" = \"null\" ]; then\n log \"ERROR: Failed to obtain Manager Matrix token (exit=${_LOGIN_EXIT})\"\n log \"ERROR: Login response was: ${_LOGIN_RESPONSE}\"\n exit 1\nfi\nlog \"Manager Matrix token obtained (token prefix: ${MANAGER_TOKEN:0:10}...)\"\n\n# ============================================================\n# Local mode: Initialize Higress Console + configure routes\n# Cloud mode: Create admin DM room + schedule welcome message\n# ============================================================\nif [ \"${HICLAW_RUNTIME}\" != \"aliyun\" ]; then\n COOKIE_FILE=\"/tmp/higress-session-cookie\"\n\n log \"Waiting for Higress Console to be fully ready and initializing admin...\"\n INIT_DONE=false\n for i in $(seq 1 90); do\n INIT_RESULT=$(curl -s -X POST http://127.0.0.1:8001/system/init \\\n -H 'Content-Type: application/json' \\\n -d '{\"adminUser\":{\"name\":\"'\"${HICLAW_ADMIN_USER}\"'\",\"password\":\"'\"${HICLAW_ADMIN_PASSWORD}\"'\",\"displayName\":\"'\"${HICLAW_ADMIN_USER}\"'\"}}' 2>/dev/null) || true\n if echo \"${INIT_RESULT}\" | grep -qE '\"success\":true|already.?init' 2>/dev/null; then\n INIT_DONE=true\n break\n fi\n if echo \"${INIT_RESULT}\" | grep -q '\"name\"' 2>/dev/null; then\n INIT_DONE=true\n break\n fi\n sleep 2\n done\n\n if [ \"${INIT_DONE}\" != \"true\" ]; then\n log \"ERROR: Higress Console did not become ready within 180s\"\n exit 1\n fi\n log \"Higress Console init done\"\n\n log \"Logging into Higress Console...\"\n LOGIN_OK=false\n for i in $(seq 1 10); do\n HTTP_CODE=$(curl -s -o /dev/null -w '%{http_code}' -X POST http://127.0.0.1:8001/session/login \\\n -H 'Content-Type: application/json' \\\n -c \"${COOKIE_FILE}\" \\\n -d '{\"username\":\"'\"${HICLAW_ADMIN_USER}\"'\",\"password\":\"'\"${HICLAW_ADMIN_PASSWORD}\"'\"}' 2>/dev/null) || true\n if { [ \"${HTTP_CODE}\" = \"200\" ] || [ \"${HTTP_CODE}\" = \"201\" ]; } && [ -f \"${COOKIE_FILE}\" ] && [ -s \"${COOKIE_FILE}\" ]; then\n LOGIN_OK=true\n break\n fi\n log \"Login attempt $i (HTTP ${HTTP_CODE}), retrying in 3s...\"\n sleep 3\n done\n\n if [ \"${LOGIN_OK}\" != \"true\" ]; then\n log \"ERROR: Could not login to Higress Console after retries\"\n exit 1\n fi\n log \"Higress Console login successful\"\n\n VERIFY_CODE=$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8001/v1/consumers -b \"${COOKIE_FILE}\" 2>/dev/null) || true\n if [ \"${VERIFY_CODE}\" = \"200\" ]; then\n log \"Console session verified (cookie valid)\"\n else\n log \"WARNING: Console session may be invalid (verify returned HTTP ${VERIFY_CODE})\"\n rm -f \"${COOKIE_FILE}\"\n for i in $(seq 1 5); do\n curl -s -o /dev/null -w '%{http_code}' -X POST http://127.0.0.1:8001/session/login \\\n -H 'Content-Type: application/json' \\\n -c \"${COOKIE_FILE}\" \\\n -d '{\"username\":\"'\"${HICLAW_ADMIN_USER}\"'\",\"password\":\"'\"${HICLAW_ADMIN_PASSWORD}\"'\"}' 2>/dev/null\n VERIFY2=$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8001/v1/consumers -b \"${COOKIE_FILE}\" 2>/dev/null) || true\n if [ \"${VERIFY2}\" = \"200\" ]; then\n log \"Re-login successful, session verified\"\n break\n fi\n sleep 2\n done\n fi\n\n export HIGRESS_COOKIE_FILE=\"${COOKIE_FILE}\"\n\n # Configure Higress routes, consumers, MCP servers\n /opt/hiclaw/scripts/init/setup-higress.sh\nfi\n\n# ============================================================\n# Create admin DM room, persist to state.json, send welcome message\n# Runs in both local and cloud modes (idempotent)\n# ============================================================\nMANAGER_FULL_ID=\"@manager:${MATRIX_DOMAIN}\"\nADMIN_FULL_ID=\"@${HICLAW_ADMIN_USER}:${MATRIX_DOMAIN}\"\n\nlog \"Logging in as admin to create DM room...\"\n_ADMIN_LOGIN=$(curl -sf -X POST \"${HICLAW_MATRIX_SERVER}/_matrix/client/v3/login\" \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"type\": \"m.login.password\",\n \"identifier\": {\"type\": \"m.id.user\", \"user\": \"'\"${HICLAW_ADMIN_USER}\"'\"},\n \"password\": \"'\"${HICLAW_ADMIN_PASSWORD}\"'\"\n }' 2>&1) || true\n\nADMIN_MATRIX_TOKEN=$(echo \"${_ADMIN_LOGIN}\" | jq -r '.access_token // empty' 2>/dev/null)\nif [ -z \"${ADMIN_MATRIX_TOKEN}\" ]; then\n log \"WARNING: Failed to login as admin, skipping DM room creation\"\nelse\n # Search for existing DM room with Manager (idempotent)\n DM_ROOM_ID=\"\"\n _JOINED_ROOMS=$(curl -sf \"${HICLAW_MATRIX_SERVER}/_matrix/client/v3/joined_rooms\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" 2>/dev/null \\\n | jq -r '.joined_rooms[]' 2>/dev/null) || true\n for _rid in ${_JOINED_ROOMS}; do\n _members=$(curl -sf \"${HICLAW_MATRIX_SERVER}/_matrix/client/v3/rooms/${_rid}/members\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" 2>/dev/null \\\n | jq -r '.chunk[].state_key' 2>/dev/null) || continue\n _count=$(echo \"${_members}\" | wc -l | xargs)\n if [ \"${_count}\" = \"2\" ] && echo \"${_members}\" | grep -q \"@manager:\"; then\n DM_ROOM_ID=\"${_rid}\"\n break\n fi\n done\n\n if [ -n \"${DM_ROOM_ID}\" ]; then\n log \"Existing DM room found: ${DM_ROOM_ID}\"\n else\n log \"Creating DM room with Manager...\"\n _RAW=$(curl -s -w '\\nHTTP_CODE:%{http_code}' -X POST \"${HICLAW_MATRIX_SERVER}/_matrix/client/v3/createRoom\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d \"{\\\"is_direct\\\":true,\\\"invite\\\":[\\\"${MANAGER_FULL_ID}\\\"],\\\"preset\\\":\\\"trusted_private_chat\\\"}\" 2>&1) || true\n _HTTP_CODE=$(echo \"${_RAW}\" | tail -1 | sed 's/HTTP_CODE://')\n _CREATE_RESP=$(echo \"${_RAW}\" | sed '$d')\n DM_ROOM_ID=$(echo \"${_CREATE_RESP}\" | jq -r '.room_id // empty' 2>/dev/null)\n if [ -n \"${DM_ROOM_ID}\" ]; then\n log \"DM room created: ${DM_ROOM_ID}\"\n else\n log \"WARNING: Failed to create DM room (HTTP ${_HTTP_CODE}): ${_CREATE_RESP}\"\n fi\n fi\n\n # Persist admin DM room ID to state.json\n if [ -n \"${DM_ROOM_ID}\" ]; then\n STATE_SCRIPT=\"/opt/hiclaw/agent/skills/task-management/scripts/manage-state.sh\"\n if [ -f \"${STATE_SCRIPT}\" ]; then\n bash \"${STATE_SCRIPT}\" --action init 2>/dev/null || true\n bash \"${STATE_SCRIPT}\" --action set-admin-dm --room-id \"${DM_ROOM_ID}\" 2>/dev/null || true\n log \"Admin DM room persisted to state.json: ${DM_ROOM_ID}\"\n fi\n fi\n\n # Schedule welcome message in background (only on first boot)\n if [ -n \"${DM_ROOM_ID}\" ] && [ ! -f \"/root/manager-workspace/soul-configured\" ]; then\n log \"Scheduling welcome message (background, waiting for OpenClaw to start)...\"\n (\n _HICLAW_LANGUAGE=\"${HICLAW_LANGUAGE:-zh}\"\n _HICLAW_TIMEZONE=\"${TZ:-Asia/Shanghai}\"\n _wait=0\n _ready=false\n while [ \"${_wait}\" -lt 300 ]; do\n if curl -sf http://127.0.0.1:18799/ > /dev/null 2>&1; then\n _ready=true\n break\n fi\n sleep 3\n _wait=$((_wait + 3))\n done\n if [ \"${_ready}\" != \"true\" ]; then\n echo \"[manager] WARNING: OpenClaw gateway not ready within 300s, skipping welcome message\"\n exit 0\n fi\n # Ensure Manager has joined the DM room before sending the welcome\n # message. Without this, there is a race between OpenClaw's Matrix\n # auto-join and the message send — the message may land before Manager\n # joins, so OpenClaw's /sync never picks it up.\n _join_ok=false\n for _join_attempt in 1 2 3; do\n if curl -sf -X POST \"${HICLAW_MATRIX_SERVER}/_matrix/client/v3/rooms/${DM_ROOM_ID}/join\" \\\n -H \"Authorization: Bearer ${MANAGER_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d '{}' > /dev/null 2>&1; then\n echo \"[manager] Manager joined DM room before welcome message\"\n _join_ok=true\n break\n fi\n sleep 2\n done\n if [ \"${_join_ok}\" != \"true\" ]; then\n echo \"[manager] WARNING: Manager join request failed after 3 attempts (may already be joined)\"\n fi\n _welcome_msg=\"This is an automated message from the HiClaw setup. This is a fresh installation.\n\n--- Installation Context ---\nUser Language: ${_HICLAW_LANGUAGE} (zh = Chinese, en = English)\nUser Timezone: ${_HICLAW_TIMEZONE} (IANA timezone identifier)\n---\n\nYou are an AI agent that manages a team of worker agents. Your identity and personality have not been configured yet — the human admin is about to meet you for the first time.\n\nPlease begin the onboarding conversation:\n\n1. Greet the admin warmly and briefly describe what you can do (coordinate workers, manage tasks, run multi-agent projects)\n2. The user has selected \\\"${_HICLAW_LANGUAGE}\\\" as their preferred language during installation. Use this language for your greeting and all subsequent communication.\n3. The user's timezone is ${_HICLAW_TIMEZONE}. Based on this timezone, you may infer their likely region and suggest additional language options.\n4. Ask them: a) What would they like to call you? b) Communication style preference? c) Any behavior guidelines? d) Confirm default language\n5. After they reply, write their preferences to ~/SOUL.md\n6. Confirm what you wrote, and ask if they would like to adjust anything\n7. Once confirmed, run: touch ~/soul-configured\n\nThe human admin will start chatting shortly.\"\n _txn_id=\"welcome-$(date +%s)\"\n _payload=$(jq -nc --arg body \"${_welcome_msg}\" '{\"msgtype\":\"m.text\",\"body\":$body}')\n _raw=$(curl -s -w '\\nHTTP_CODE:%{http_code}' -X PUT \"${HICLAW_MATRIX_SERVER}/_matrix/client/v3/rooms/${DM_ROOM_ID}/send/m.room.message/${_txn_id}\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d \"${_payload}\" 2>&1) || true\n _http_code=$(echo \"${_raw}\" | tail -1 | sed 's/HTTP_CODE://')\n _send_resp=$(echo \"${_raw}\" | sed '$d')\n if echo \"${_send_resp}\" | jq -e '.event_id' > /dev/null 2>&1; then\n echo \"[manager] Welcome message sent to DM room\"\n else\n echo \"[manager] WARNING: Failed to send welcome message (HTTP ${_http_code}): ${_send_resp}\"\n fi\n ) &\n log \"Welcome message background process started (PID: $!)\"\n fi\nfi\n\n# ============================================================\n# Generate Manager Agent openclaw.json from template\n# ============================================================\nlog \"Generating Manager openclaw.json...\"\nexport MANAGER_MATRIX_TOKEN=\"${MANAGER_TOKEN}\"\nexport MANAGER_GATEWAY_KEY=\"${HICLAW_MANAGER_GATEWAY_KEY}\"\n# Resolve model parameters based on model name\nMODEL_NAME=\"${HICLAW_DEFAULT_MODEL:-qwen3.5-plus}\"\ncase \"${MODEL_NAME}\" in\n gpt-5.3-codex|gpt-5-mini|gpt-5-nano)\n export MODEL_CONTEXT_WINDOW=400000 MODEL_MAX_TOKENS=128000 ;;\n claude-opus-4-6)\n export MODEL_CONTEXT_WINDOW=1000000 MODEL_MAX_TOKENS=128000 ;;\n claude-sonnet-4-6)\n export MODEL_CONTEXT_WINDOW=1000000 MODEL_MAX_TOKENS=64000 ;;\n claude-haiku-4-5)\n export MODEL_CONTEXT_WINDOW=200000 MODEL_MAX_TOKENS=64000 ;;\n qwen3.5-plus)\n export MODEL_CONTEXT_WINDOW=200000 MODEL_MAX_TOKENS=64000 ;;\n deepseek-chat|deepseek-reasoner|kimi-k2.5)\n export MODEL_CONTEXT_WINDOW=256000 MODEL_MAX_TOKENS=128000 ;;\n glm-5|MiniMax-M2.7|MiniMax-M2.7-highspeed|MiniMax-M2.5)\n export MODEL_CONTEXT_WINDOW=200000 MODEL_MAX_TOKENS=128000 ;;\n *)\n export MODEL_CONTEXT_WINDOW=150000 MODEL_MAX_TOKENS=128000 ;;\nesac\nexport MODEL_REASONING=true\n\n# Override with user-supplied custom model parameters from env (set during install)\n[ -n \"${HICLAW_MODEL_CONTEXT_WINDOW:-}\" ] && export MODEL_CONTEXT_WINDOW=\"${HICLAW_MODEL_CONTEXT_WINDOW}\"\n[ -n \"${HICLAW_MODEL_MAX_TOKENS:-}\" ] && export MODEL_MAX_TOKENS=\"${HICLAW_MODEL_MAX_TOKENS}\"\n[ -n \"${HICLAW_MODEL_REASONING:-}\" ] && export MODEL_REASONING=\"${HICLAW_MODEL_REASONING}\"\n\n# E2EE: convert HICLAW_MATRIX_E2EE to JSON boolean for template substitution\nif [ \"${HICLAW_MATRIX_E2EE:-0}\" = \"1\" ] || [ \"${HICLAW_MATRIX_E2EE:-}\" = \"true\" ]; then\n export MATRIX_E2EE_ENABLED=true\nelse\n export MATRIX_E2EE_ENABLED=false\nfi\nlog \"Matrix E2EE: ${MATRIX_E2EE_ENABLED}\"\n\n# Resolve input modalities: only vision-capable models get \"image\"\ncase \"${MODEL_NAME}\" in\n gpt-5.4|gpt-5.3-codex|gpt-5-mini|gpt-5-nano|claude-opus-4-6|claude-sonnet-4-6|claude-haiku-4-5|qwen3.5-plus|kimi-k2.5)\n export MODEL_INPUT='[\"text\", \"image\"]' ;;\n *)\n export MODEL_INPUT='[\"text\"]' ;;\nesac\n# Override with user-supplied vision setting from env\nif [ \"${HICLAW_MODEL_VISION:-}\" = \"true\" ]; then\n export MODEL_INPUT='[\"text\", \"image\"]'\nelif [ \"${HICLAW_MODEL_VISION:-}\" = \"false\" ]; then\n export MODEL_INPUT='[\"text\"]'\nfi\n\nlog \"Model: ${MODEL_NAME} (context=${MODEL_CONTEXT_WINDOW}, maxTokens=${MODEL_MAX_TOKENS}, reasoning=${MODEL_REASONING}, input=${MODEL_INPUT})\"\n\nif [ -f /root/manager-workspace/openclaw.json ]; then\n log \"Manager openclaw.json already exists, updating dynamic fields only (preserving user customizations)...\"\n # Merge known models into existing config (add missing, preserve user-added)\n # Use known-models.json (valid JSON) instead of template (contains ${VAR} placeholders)\n KNOWN_MODELS=$(cat /opt/hiclaw/configs/known-models.json 2>/dev/null || echo '[]')\n jq --arg token \"${MANAGER_TOKEN}\" \\\n --arg key \"${HICLAW_MANAGER_GATEWAY_KEY}\" \\\n --arg model \"${MODEL_NAME}\" \\\n --argjson e2ee \"${MATRIX_E2EE_ENABLED}\" \\\n --argjson known_models \"${KNOWN_MODELS}\" \\\n --argjson ctx \"${MODEL_CONTEXT_WINDOW}\" \\\n --argjson max \"${MODEL_MAX_TOKENS}\" \\\n --argjson reasoning \"${MODEL_REASONING}\" \\\n --argjson input \"${MODEL_INPUT}\" \\\n '\n # Merge known models: add any model id not already present\n .models.providers[\"hiclaw-gateway\"].models as $existing\n | ($existing | map(.id)) as $existing_ids\n | ($known_models | map(select(.id as $id | $existing_ids | index($id) | not))) as $new\n | .models.providers[\"hiclaw-gateway\"].models = ($existing + $new)\n # Ensure the user-chosen default model is in the list (custom model support)\n | if (.models.providers[\"hiclaw-gateway\"].models | map(.id) | index($model) | not) then\n .models.providers[\"hiclaw-gateway\"].models += [{\"id\": $model, \"name\": $model, \"reasoning\": $reasoning, \"contextWindow\": $ctx, \"maxTokens\": $max, \"input\": $input}]\n else . end\n # Rebuild model aliases from the full models list\n | (.models.providers[\"hiclaw-gateway\"].models | map({ (\"hiclaw-gateway/\" + .id): { \"alias\": .id } }) | add // {}) as $aliases\n | .agents.defaults.models = ((.agents.defaults.models // {}) + $aliases)\n | .channels.matrix.accessToken = $token | .models.providers[\"hiclaw-gateway\"].apiKey = $key\n | ((.hooks.token // \"\") as $ht | if $ht == $key or $ht == ($key + \"-hooks\" | @base64) then del(.hooks) else . end)\n | .agents.defaults.model.primary = (\"hiclaw-gateway/\" + $model)\n | .commands.restart = true\n | .gateway.controlUi.dangerouslyDisableDeviceAuth = true\n | .channels.matrix.encryption = $e2ee\n ' \\\n /root/manager-workspace/openclaw.json > /tmp/openclaw.json.tmp && \\\n mv /tmp/openclaw.json.tmp /root/manager-workspace/openclaw.json\n # Verify the token was written correctly\n _written_token=$(jq -r '.channels.matrix.accessToken' /root/manager-workspace/openclaw.json 2>/dev/null)\n if [ -z \"${_written_token}\" ] || [ \"${_written_token}\" = \"null\" ]; then\n log \"ERROR: Matrix token was not written correctly to openclaw.json (got: ${_written_token})\"\n else\n log \"Matrix token written to openclaw.json (prefix: ${_written_token:0:10}...)\"\n fi\nelse\n log \"Manager openclaw.json not found, generating from template...\"\n envsubst < /opt/hiclaw/configs/manager-openclaw.json.tmpl > /root/manager-workspace/openclaw.json\n # Inject custom model if not in the built-in list\n if ! jq -e --arg model \"${MODEL_NAME}\" '.models.providers[\"hiclaw-gateway\"].models | map(.id) | index($model)' /root/manager-workspace/openclaw.json > /dev/null 2>&1; then\n log \"Custom model '${MODEL_NAME}' not in built-in list, injecting into config...\"\n jq --arg model \"${MODEL_NAME}\" \\\n --argjson ctx \"${MODEL_CONTEXT_WINDOW}\" \\\n --argjson max \"${MODEL_MAX_TOKENS}\" \\\n --argjson reasoning \"${MODEL_REASONING}\" \\\n --argjson input \"${MODEL_INPUT}\" \\\n '\n .models.providers[\"hiclaw-gateway\"].models += [{\"id\": $model, \"name\": $model, \"reasoning\": $reasoning, \"contextWindow\": $ctx, \"maxTokens\": $max, \"input\": $input}]\n | .agents.defaults.models += {(\"hiclaw-gateway/\" + $model): {\"alias\": $model}}\n ' /root/manager-workspace/openclaw.json > /tmp/openclaw.json.tmp && \\\n mv /tmp/openclaw.json.tmp /root/manager-workspace/openclaw.json\n fi\n _written_token=$(jq -r '.channels.matrix.accessToken' /root/manager-workspace/openclaw.json 2>/dev/null)\n log \"Matrix token written from template (prefix: ${_written_token:0:10}...)\"\nfi\n\n# Cloud mode: overlay cloud-specific settings onto generated config\nif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n log \"Applying cloud overlay to openclaw.json...\"\n jq --arg homeserver \"${HICLAW_MATRIX_SERVER}\" \\\n --arg gateway \"${HICLAW_AI_GATEWAY_URL}/v1\" \\\n --arg key \"${HICLAW_MANAGER_GATEWAY_KEY}\" \\\n '.channels.matrix.homeserver = $homeserver\n | .models.providers[\"hiclaw-gateway\"].baseUrl = $gateway\n | .models.providers[\"hiclaw-gateway\"].apiKey = $key\n | ((.hooks.token // \"\") as $ht | if $ht == $key or $ht == ($key + \"-hooks\" | @base64) then del(.hooks) else . end)\n | .commands.restart = false' \\\n /root/manager-workspace/openclaw.json > /tmp/openclaw-cloud.json && \\\n mv /tmp/openclaw-cloud.json /root/manager-workspace/openclaw.json\n log \"Cloud overlay applied\"\nfi\n\n# ============================================================\n# Detect container runtime (for Worker creation)\n# ============================================================\nsource /opt/hiclaw/scripts/lib/container-api.sh\nif container_api_available; then\n log \"Container runtime socket detected at ${CONTAINER_SOCKET} — direct Worker creation enabled\"\n export HICLAW_CONTAINER_RUNTIME=\"socket\"\nelif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n log \"Cloud mode — Workers created via SAE API\"\n export HICLAW_CONTAINER_RUNTIME=\"cloud\"\nelse\n log \"No container runtime found — Worker creation will output install commands\"\n export HICLAW_CONTAINER_RUNTIME=\"none\"\nfi\n\n# ============================================================\n# Upgrade Worker openclaw.json: merge known models + E2EE flag into existing configs\n# Existing workers in MinIO may have old single-model configs or missing encryption field.\n# Merge template models so they can hot-switch without restart.\n# ============================================================\nREGISTRY_FILE=\"/root/manager-workspace/workers-registry.json\"\nif [ -f \"${REGISTRY_FILE}\" ]; then\n # Use known-models.json (valid JSON) instead of template (contains ${VAR} placeholders)\n KNOWN_MODELS_FILE=\"/opt/hiclaw/configs/known-models.json\"\n if [ -f \"${KNOWN_MODELS_FILE}\" ]; then\n _KNOWN_MODELS=$(cat \"${KNOWN_MODELS_FILE}\")\n for _wname in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_wname}\" ] && continue\n _minio_path=\"${HICLAW_STORAGE_PREFIX}/agents/${_wname}/openclaw.json\"\n _tmp_in=\"/tmp/openclaw-${_wname}-models-upgrade-in.json\"\n if mc cp \"${_minio_path}\" \"${_tmp_in}\" 2>/dev/null; then\n _tmp_out=\"/tmp/openclaw-${_wname}-models-upgrade-out.json\"\n # Idempotent merge: add missing known models, rebuild aliases, set e2ee.\n # Always runs — jq deduplicates by model id, so re-runs are safe.\n jq --argjson known_models \"${_KNOWN_MODELS}\" \\\n --argjson e2ee \"${MATRIX_E2EE_ENABLED}\" '\n .models.providers[\"hiclaw-gateway\"].models as $existing\n | ($existing | map(.id)) as $existing_ids\n | ($known_models | map(select(.id as $id | $existing_ids | index($id) | not))) as $new\n | .models.providers[\"hiclaw-gateway\"].models = ($existing + $new)\n | (.models.providers[\"hiclaw-gateway\"].models | map({ (\"hiclaw-gateway/\" + .id): { \"alias\": .id } }) | add // {}) as $aliases\n | .agents.defaults.models = ((.agents.defaults.models // {}) + $aliases)\n | .channels.matrix.encryption = $e2ee\n ' \"${_tmp_in}\" > \"${_tmp_out}\" 2>/dev/null\n if ! diff -q \"${_tmp_in}\" \"${_tmp_out}\" > /dev/null 2>&1; then\n if mc cp \"${_tmp_out}\" \"${_minio_path}\" 2>/dev/null; then\n _new_count=$(jq '.models.providers[\"hiclaw-gateway\"].models | length' \"${_tmp_out}\" 2>/dev/null)\n log \"Worker ${_wname}: upgraded openclaw.json (models: ${_new_count}, e2ee: ${MATRIX_E2EE_ENABLED})\"\n fi\n fi\n rm -f \"${_tmp_in}\" \"${_tmp_out}\"\n fi\n done\n fi\nfi\n\n# ============================================================\n# Ensure Worker Matrix password files exist in MinIO (E2EE fix)\n# Workers need to re-login on restart to get a fresh device_id.\n# Older workers created before this fix won't have the password file.\n# ============================================================\nif [ -f \"${REGISTRY_FILE}\" ]; then\n for _wname in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_wname}\" ] && continue\n _creds_file=\"/data/worker-creds/${_wname}.env\"\n if [ -f \"${_creds_file}\" ]; then\n # Check if password file already exists in MinIO\n if ! mc stat \"${HICLAW_STORAGE_PREFIX}/agents/${_wname}/credentials/matrix/password\" > /dev/null 2>&1; then\n source \"${_creds_file}\"\n if [ -n \"${WORKER_PASSWORD}\" ]; then\n _tmp_pw=\"/tmp/matrix-pw-${_wname}\"\n echo -n \"${WORKER_PASSWORD}\" > \"${_tmp_pw}\"\n mc cp \"${_tmp_pw}\" \"${HICLAW_STORAGE_PREFIX}/agents/${_wname}/credentials/matrix/password\" 2>/dev/null \\\n && log \"Worker ${_wname}: wrote Matrix password to MinIO (E2EE re-login fix)\" \\\n || log \"Worker ${_wname}: WARNING: failed to write Matrix password to MinIO\"\n rm -f \"${_tmp_pw}\"\n fi\n fi\n fi\n done\nfi\n\n# ============================================================\n# Recreate Worker containers as needed after Manager restart.\n# Manager IP may change on restart; Workers use ExtraHosts pointing to Manager IP,\n# so any worker whose ExtraHosts IP no longer matches must be recreated.\n# ============================================================\nif container_api_available; then\n REGISTRY_FILE=\"/root/manager-workspace/workers-registry.json\"\n _manager_ip=$(container_get_manager_ip)\n if [ -f \"${REGISTRY_FILE}\" ]; then\n for _worker_name in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_worker_name}\" ] && continue\n\n # Skip remote workers — they are not Manager-managed containers.\n _deployment=$(jq -r --arg w \"${_worker_name}\" '.workers[$w].deployment // \"local\"' \"${REGISTRY_FILE}\" 2>/dev/null)\n if [ \"${_deployment}\" = \"remote\" ]; then\n log \"Worker ${_worker_name} is remote, skipping container recreate\"\n continue\n fi\n\n _status=$(container_status_worker \"${_worker_name}\")\n if [ \"${_status}\" = \"running\" ]; then\n # Check if ExtraHosts IP still matches current Manager IP.\n # If ExtraHosts is empty the worker uses real DNS — no recreate needed.\n _inspect=$(_api GET \"/containers/${WORKER_CONTAINER_PREFIX}${_worker_name}/json\" 2>/dev/null)\n _extra_hosts_len=$(echo \"${_inspect}\" | jq -r '.HostConfig.ExtraHosts | length' 2>/dev/null)\n if [ -z \"${_extra_hosts_len}\" ] || [ \"${_extra_hosts_len}\" = \"0\" ]; then\n log \"Worker running (no ExtraHosts, real DNS): ${_worker_name}, skipping\"\n continue\n fi\n _worker_ip=$(echo \"${_inspect}\" | jq -r '.HostConfig.ExtraHosts[0]' 2>/dev/null | cut -d: -f2)\n if [ \"${_worker_ip}\" = \"${_manager_ip}\" ]; then\n log \"Worker running with current Manager IP (${_manager_ip}): ${_worker_name}, skipping\"\n continue\n fi\n log \"Worker running but Manager IP changed (${_worker_ip} -> ${_manager_ip}): ${_worker_name}, recreating...\"\n else\n # Container missing or stopped — always recreate.\n log \"Worker container ${_status}: ${_worker_name}, recreating...\"\n fi\n _creds_file=\"/data/worker-creds/${_worker_name}.env\"\n if [ -f \"${_creds_file}\" ]; then\n source \"${_creds_file}\"\n _runtime=$(jq -r --arg w \"${_worker_name}\" '.workers[$w].runtime // \"openclaw\"' \"${REGISTRY_FILE}\" 2>/dev/null)\n _recreated=false\n for _attempt in 1 2 3; do\n if [ \"${_runtime}\" = \"copaw\" ]; then\n container_create_copaw_worker \"${_worker_name}\" \"${_worker_name}\" \"${WORKER_MINIO_PASSWORD}\" 2>&1 && _recreated=true && break\n else\n container_create_worker \"${_worker_name}\" \"${_worker_name}\" \"${WORKER_MINIO_PASSWORD}\" 2>&1 && _recreated=true && break\n fi\n log \" Attempt ${_attempt}/3 failed for ${_worker_name}, retrying in $((5 * _attempt))s...\"\n sleep $((5 * _attempt))\n done\n if [ \"${_recreated}\" = true ]; then\n log \" Recreated ${_runtime} worker: ${_worker_name}\"\n else\n log \" ERROR: Failed to recreate ${_worker_name} after 3 attempts\"\n fi\n else\n log \" WARNING: No credentials found for ${_worker_name} (${_creds_file} missing), skipping\"\n fi\n done\n fi\nfi\n\n# ============================================================\n# Notify workers of builtin updates if upgrade happened\n# Builtin files (AGENTS.md, skills) are already synced by upgrade-builtins.sh\n#\n# Cooldown: skip notification if the last successful notify was within\n# NOTIFY_COOLDOWN_SECS (default 3600s / 1 hour). This prevents repeated\n# notifications when the Manager crash-loops and re-runs upgrade-builtins\n# on every restart (e.g. IMAGE_VERSION=latest always triggers upgrade).\n# ============================================================\nNOTIFY_COOLDOWN_SECS=\"${HICLAW_NOTIFY_COOLDOWN_SECS:-3600}\"\nNOTIFY_TS_FILE=\"/root/manager-workspace/.last-worker-notify-ts\"\n\nif [ -f /root/manager-workspace/.upgrade-pending-worker-notify ]; then\n _now=$(date +%s)\n _last_notify=$(cat \"${NOTIFY_TS_FILE}\" 2>/dev/null || echo \"0\")\n _elapsed=$(( _now - _last_notify ))\n\n if [ \"${_elapsed}\" -lt \"${NOTIFY_COOLDOWN_SECS}\" ]; then\n log \"Skipping worker builtin notification (last notify ${_elapsed}s ago, cooldown ${NOTIFY_COOLDOWN_SECS}s)\"\n rm -f /root/manager-workspace/.upgrade-pending-worker-notify\n else\n log \"Notifying workers about builtin updates...\"\n REGISTRY_FILE=\"/root/manager-workspace/workers-registry.json\"\n _notify_ok=false\n if [ -f \"${REGISTRY_FILE}\" ]; then\n for _worker_name in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_worker_name}\" ] && continue\n _room_id=$(jq -r --arg w \"${_worker_name}\" '.workers[$w].room_id // empty' \"${REGISTRY_FILE}\" 2>/dev/null)\n if [ -n \"${_room_id}\" ]; then\n _worker_id=\"@${_worker_name}:${MATRIX_DOMAIN}\"\n _txn_id=\"upgrade-$(date +%s%N)\"\n _msg=\"@${_worker_name}:${MATRIX_DOMAIN} Manager upgraded builtin files (AGENTS.md, skills). Please use your file-sync skill to sync the latest config.\"\n _raw=$(curl -s -w '\\nHTTP_CODE:%{http_code}' -X PUT \\\n \"${HICLAW_MATRIX_SERVER}/_matrix/client/v3/rooms/${_room_id}/send/m.room.message/${_txn_id}\" \\\n -H \"Authorization: Bearer ${MANAGER_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d \"{\\\"msgtype\\\":\\\"m.text\\\",\\\"body\\\":\\\"${_msg}\\\",\\\"m.mentions\\\":{\\\"user_ids\\\":[\\\"${_worker_id}\\\"]}}\" \\\n 2>&1) || true\n _http_code=$(echo \"${_raw}\" | tail -1 | sed 's/HTTP_CODE://')\n _notify_resp=$(echo \"${_raw}\" | sed '$d')\n if echo \"${_notify_resp}\" | jq -e '.event_id' > /dev/null 2>&1; then\n log \" Notified ${_worker_name}\"; _notify_ok=true\n else\n log \" WARNING: Failed to notify ${_worker_name} (HTTP ${_http_code}): ${_notify_resp}\"\n fi\n fi\n done\n fi\n # Record timestamp only if at least one notification succeeded\n if [ \"${_notify_ok}\" = true ]; then\n echo \"${_now}\" > \"${NOTIFY_TS_FILE}\"\n fi\n rm -f /root/manager-workspace/.upgrade-pending-worker-notify\n fi\nfi\n\n# ============================================================\n# Start OpenClaw Manager Agent\n# ============================================================\nlog \"Starting Manager Agent (OpenClaw)...\"\n\n# HOME is already set to /root/manager-workspace via docker run -e HOME=...\nexport OPENCLAW_CONFIG_PATH=\"/root/manager-workspace/openclaw.json\"\n\n# Symlink to default OpenClaw config path so CLI commands find the config\nmkdir -p \"${HOME}/.openclaw\"\nln -sf \"/root/manager-workspace/openclaw.json\" \"${HOME}/.openclaw/openclaw.json\"\n\n# Ensure host credential symlinks exist under HOME\nif [ -d \"/host-share\" ]; then\n [ -f \"/host-share/.gitconfig\" ] && ln -sf \"/host-share/.gitconfig\" \"${HOME}/.gitconfig\"\nfi\n\nlog \"HOME=${HOME} (manager-workspace, host-mounted)\"\ncd \"${HOME}\"\n\n# Clean orphaned session write locks (e.g. from SIGKILL or crash before exit handlers)\n# Prevents \"session file locked (timeout 10000ms)\" when PID was reused\nfind \"${HOME}/.openclaw/agents\" -name \"*.jsonl.lock\" -delete 2>/dev/null || true\nlog \"Cleaned up any orphaned session write locks\"\n\n# Clean Matrix crypto storage (SQLite WAL may be corrupted after unclean shutdown)\n# Crypto state is re-negotiated on startup; losing it only means re-establishing E2EE sessions\nrm -rf \"${HOME}/.openclaw/matrix\" 2>/dev/null || true\nlog \"Cleaned Matrix crypto storage (will re-establish E2EE sessions)\"\n\n# ── Render agent doc templates ────────────────────────────────────────────\n# Replace ${VAR} placeholders with actual values so the AI agent reads\n# plain text and never needs to resolve environment variables.\nexport MANAGER_MATRIX_TOKEN MANAGER_TOKEN HIGRESS_COOKIE_FILE\nRENDER=/opt/hiclaw/scripts/lib/render-skills.sh\nlog \"Rendering agent doc templates...\"\n# Manager-owned docs (workspace)\nbash \"$RENDER\" /root/manager-workspace/skills\nbash \"$RENDER\" /root/manager-workspace/skills-alpha\nbash \"$RENDER\" /root/manager-workspace AGENTS.md TOOLS.md HEARTBEAT.md SOUL.md\n# Worker templates (workspace + image) — rendered before push to MinIO\n# so Workers (including remote pip-install) receive plain text\nbash \"$RENDER\" /root/manager-workspace/worker-skills\nbash \"$RENDER\" /root/manager-workspace/worker-agent\nbash \"$RENDER\" /root/manager-workspace/copaw-worker-agent\nbash \"$RENDER\" /opt/hiclaw/agent/worker-skills\nbash \"$RENDER\" /opt/hiclaw/agent/worker-agent\nbash \"$RENDER\" /opt/hiclaw/agent/copaw-worker-agent\nlog \"Agent doc templates rendered\"\n\n# Cloud mode: start background file sync (workspace ↔ OSS) and initial push\nif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n log \"Syncing initial workspace to OSS...\"\n ensure_mc_credentials\n mc mirror /root/manager-workspace/ \"${HICLAW_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" 2>/dev/null || true\n\n # Local → OSS: change-triggered sync\n (\n while true; do\n CHANGED=$(find /root/manager-workspace/ -type f -newermt \"15 seconds ago\" 2>/dev/null | head -1)\n if [ -n \"${CHANGED}\" ]; then\n ensure_mc_credentials 2>/dev/null || true\n mc mirror /root/manager-workspace/ \"${HICLAW_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" --exclude \".npm/**\" \\\n --exclude \".local/**\" --exclude \".mc/**\" 2>/dev/null || true\n fi\n sleep 10\n done\n ) &\n log \"Local→OSS sync started (PID: $!)\"\n\n # OSS → Local: periodic pull (shared data, agent configs)\n (\n while true; do\n sleep 300\n ensure_mc_credentials 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/shared/\" /root/hiclaw-fs/shared/ --overwrite --newer-than \"5m\" 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/agents/\" /root/hiclaw-fs/agents/ --overwrite --newer-than \"5m\" 2>/dev/null || true\n done\n ) &\n log \"OSS→Local sync started (every 5m, PID: $!)\"\nfi\n\n# Launch OpenClaw\nexec openclaw gateway run --verbose --force\n"}, "0569d1a130064e5172edd444fc6b6b01eb55224b": {"t": 1785764773.8074186, "n": 42470, "h": "d0ba25cd3dd249ac", "c": "#!/bin/bash\n# start-manager-agent.sh - Initialize and start the Manager Agent\n# Supports both local (supervisord) and cloud (SAE single-process) deployments.\n# In local mode this is the last supervisord component to start (priority 800).\n# In cloud mode (HICLAW_RUNTIME=aliyun) this is the container entrypoint.\n\nsource /opt/hiclaw/scripts/lib/hiclaw-env.sh\n\n# ============================================================\n# Set timezone from TZ env var\n# ============================================================\nif [ -n \"${TZ}\" ] && [ -f \"/usr/share/zoneinfo/${TZ}\" ]; then\n ln -sf \"/usr/share/zoneinfo/${TZ}\" /etc/localtime\n echo \"${TZ}\" > /etc/timezone\n log \"Timezone set to ${TZ}\"\nfi\n\nMATRIX_DOMAIN=\"${HICLAW_MATRIX_DOMAIN:-matrix-local.hiclaw.io:8080}\"\nAI_GATEWAY_DOMAIN=\"${HICLAW_AI_GATEWAY_DOMAIN:-aigw-local.hiclaw.io}\"\n\n# ============================================================\n# Cloud mode: validate required environment variables + initial credentials\n# ============================================================\nif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n : \"${HICLAW_MATRIX_URL:?HICLAW_MATRIX_URL is required}\"\n : \"${HICLAW_MATRIX_DOMAIN:?HICLAW_MATRIX_DOMAIN is required}\"\n : \"${HICLAW_AI_GATEWAY_URL:?HICLAW_AI_GATEWAY_URL is required}\"\n : \"${HICLAW_MANAGER_GATEWAY_KEY:?HICLAW_MANAGER_GATEWAY_KEY is required}\"\n : \"${HICLAW_MANAGER_PASSWORD:?HICLAW_MANAGER_PASSWORD is required (cloud containers are stateless, password must be injected)}\"\n : \"${HICLAW_REGISTRATION_TOKEN:?HICLAW_REGISTRATION_TOKEN is required}\"\n : \"${HICLAW_ADMIN_USER:?HICLAW_ADMIN_USER is required}\"\n : \"${HICLAW_ADMIN_PASSWORD:?HICLAW_ADMIN_PASSWORD is required}\"\n log \"Cloud mode: validating environment... OK\"\n log \" Matrix: ${HICLAW_MATRIX_SERVER}, AI Gateway: ${HICLAW_AI_GATEWAY_URL}, OSS: ${HICLAW_STORAGE_BUCKET}\"\n ensure_mc_credentials || { log \"FATAL: Initial STS credential fetch failed\"; exit 1; }\nfi\n\n# ============================================================\n# Local mode: host symlinks, /etc/hosts, wait for local services\n# ============================================================\nif [ \"${HICLAW_RUNTIME}\" != \"aliyun\" ]; then\n # Create symlink for host directory access\n if [ -d \"/host-share\" ]; then\n ORIGINAL_HOST_HOME=\"${HOST_ORIGINAL_HOME:-$HOME}\"\n if [ ! -e \"${ORIGINAL_HOST_HOME}\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/root\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/data\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/host-share\" ]; then\n mkdir -p \"$(dirname \"${ORIGINAL_HOST_HOME}\")\"\n ln -sfn /host-share \"${ORIGINAL_HOST_HOME}\"\n log \"Created symlink: ${ORIGINAL_HOST_HOME} -> /host-share\"\n else\n ln -sfn /host-share /root/host-home\n log \"Created fallback symlink: /root/host-home -> /host-share\"\n fi\n fi\n\n # Add local domains to /etc/hosts\n HOSTS_DOMAINS=\"${MATRIX_DOMAIN%%:*} ${HICLAW_MATRIX_CLIENT_DOMAIN:-matrix-client-local.hiclaw.io} ${AI_GATEWAY_DOMAIN} ${HICLAW_FS_DOMAIN:-fs-local.hiclaw.io}\"\n if ! grep -q \"${AI_GATEWAY_DOMAIN}\" /etc/hosts 2>/dev/null; then\n echo \"127.0.0.1 ${HOSTS_DOMAINS}\" >> /etc/hosts\n log \"Added local domains to /etc/hosts\"\n fi\n\n # Wait for local infrastructure\n waitForService \"Higress Gateway\" \"127.0.0.1\" 8080 180\n waitForService \"Higress Console\" \"127.0.0.1\" 8001 180\n waitForService \"Tuwunel\" \"127.0.0.1\" 6167 120\n waitForHTTP \"Tuwunel Matrix API\" \"${HICLAW_MATRIX_SERVER}/_tuwunel/server_version\" 120\n waitForService \"MinIO\" \"127.0.0.1\" 9000 120\nelse\n # Cloud mode: wait for external Tuwunel\n log \"Waiting for Tuwunel Matrix server at ${HICLAW_MATRIX_SERVER}...\"\n _retry=0\n while [ \"${_retry}\" -lt 30 ]; do\n if curl -sf \"${HICLAW_MATRIX_SERVER}/_matrix/client/versions\" > /dev/null 2>&1; then\n log \"Tuwunel is ready\"\n break\n fi\n _retry=$((_retry + 1))\n log \" Waiting for Tuwunel (attempt ${_retry}/30)...\"\n sleep 5\n done\n if [ \"${_retry}\" -ge 30 ]; then\n log \"ERROR: Tuwunel not reachable at ${HICLAW_MATRIX_SERVER}\"\n exit 1\n fi\nfi\n\n# ============================================================\n# Auto-generate secrets if not provided via environment\n# Persisted to /data so they survive container restart\n# ============================================================\nSECRETS_FILE=\"/data/hiclaw-secrets.env\"\nif [ -f \"${SECRETS_FILE}\" ]; then\n source \"${SECRETS_FILE}\"\n log \"Loaded persisted secrets from ${SECRETS_FILE}\"\nfi\n\nif [ -z \"${HICLAW_MANAGER_GATEWAY_KEY}\" ]; then\n export HICLAW_MANAGER_GATEWAY_KEY=\"$(generateKey 32)\"\n log \"Auto-generated HICLAW_MANAGER_GATEWAY_KEY\"\nfi\nif [ -z \"${HICLAW_MANAGER_PASSWORD}\" ]; then\n export HICLAW_MANAGER_PASSWORD=\"$(generateKey 16)\"\n log \"Auto-generated HICLAW_MANAGER_PASSWORD\"\nfi\n\n# Persist secrets so they survive supervisord restart\nmkdir -p /data\ncat > \"${SECRETS_FILE}\" <<EOF\nexport HICLAW_MANAGER_GATEWAY_KEY=\"${HICLAW_MANAGER_GATEWAY_KEY}\"\nexport HICLAW_MANAGER_PASSWORD=\"${HICLAW_MANAGER_PASSWORD}\"\nEOF\nchmod 600 \"${SECRETS_FILE}\"\n\n# Cloud mode: pull workspace from OSS before initialization\nif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n HICLAW_FS=\"/root/hiclaw-fs\"\n mkdir -p \"${HICLAW_FS}/shared\" \"${HICLAW_FS}/agents\"\n log \"Pulling workspace from OSS...\"\n ensure_mc_credentials\n mc mirror \"${HICLAW_STORAGE_PREFIX}/manager/\" /root/manager-workspace/ --overwrite 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/shared/\" \"${HICLAW_FS}/shared/\" --overwrite 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/agents/\" \"${HICLAW_FS}/agents/\" --overwrite 2>/dev/null || true\n # Symlink hiclaw-fs into workspace for agent access\n ln -sfn \"${HICLAW_FS}\" /root/manager-workspace/hiclaw-fs\nfi\n\n# ============================================================\n# Initialize / upgrade Manager workspace\n# First boot: full init via upgrade-builtins.sh\n# Subsequent boots: compare image version; upgrade only if changed\n# ============================================================\nmkdir -p /root/manager-workspace\n\nIMAGE_VERSION=$(cat /opt/hiclaw/agent/.builtin-version 2>/dev/null || echo \"unknown\")\nINSTALLED_VERSION=$(cat /root/manager-workspace/.builtin-version 2>/dev/null || echo \"\")\n\nif [ ! -f /root/manager-workspace/.initialized ]; then\n log \"First boot: initializing manager workspace...\"\n bash /opt/hiclaw/scripts/init/upgrade-builtins.sh\n touch /root/manager-workspace/.initialized\n log \"Manager workspace initialized (version: ${IMAGE_VERSION})\"\nelif [ \"${IMAGE_VERSION}\" != \"${INSTALLED_VERSION}\" ] || [ \"${IMAGE_VERSION}\" = \"latest\" ]; then\n log \"Upgrade detected: ${INSTALLED_VERSION} -> ${IMAGE_VERSION}${IMAGE_VERSION:+ (latest: always upgrade)}\"\n bash /opt/hiclaw/scripts/init/upgrade-builtins.sh\n log \"Manager workspace upgraded to version: ${IMAGE_VERSION}\"\nelse\n log \"Workspace up to date (version: ${IMAGE_VERSION})\"\nfi\n\n# Local mode: wait for mc mirror initialization (shared + worker data in /root/hiclaw-fs/)\nif [ \"${HICLAW_RUNTIME}\" != \"aliyun\" ]; then\n log \"Waiting for MinIO storage initialization...\"\n _minio_wait=0\n while [ ! -f /root/hiclaw-fs/.initialized ]; do\n sleep 2\n _minio_wait=$(( _minio_wait + 1 ))\n if [ \"${_minio_wait}\" -ge 60 ]; then\n log \"ERROR: MinIO storage initialization timed out after 120s\"\n exit 1\n fi\n done\n log \"MinIO storage initialized\"\nfi\n\n# ============================================================\n# Register Matrix users via Registration API (single-step, no UIAA)\n# ============================================================\nlog \"Registering human admin Matrix account...\"\ncurl -sf -X POST ${HICLAW_MATRIX_SERVER}/_matrix/client/v3/register \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"username\": \"'\"${HICLAW_ADMIN_USER}\"'\",\n \"password\": \"'\"${HICLAW_ADMIN_PASSWORD}\"'\",\n \"auth\": {\n \"type\": \"m.login.registration_token\",\n \"token\": \"'\"${HICLAW_REGISTRATION_TOKEN}\"'\"\n }\n }' > /dev/null 2>&1 || log \"Admin account may already exist\"\n\nlog \"Registering Manager Agent Matrix account...\"\ncurl -sf -X POST ${HICLAW_MATRIX_SERVER}/_matrix/client/v3/register \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"username\": \"manager\",\n \"password\": \"'\"${HICLAW_MANAGER_PASSWORD}\"'\",\n \"auth\": {\n \"type\": \"m.login.registration_token\",\n \"token\": \"'\"${HICLAW_REGISTRATION_TOKEN}\"'\"\n }\n }' > /dev/null 2>&1 || log \"Manager account may already exist\"\n\n# Get Manager Agent's Matrix access token\nlog \"Obtaining Manager Matrix access token...\"\n_LOGIN_RESPONSE=$(curl -sf -X POST ${HICLAW_MATRIX_SERVER}/_matrix/client/v3/login \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"type\": \"m.login.password\",\n \"identifier\": {\"type\": \"m.id.user\", \"user\": \"manager\"},\n \"password\": \"'\"${HICLAW_MANAGER_PASSWORD}\"'\"\n }' 2>&1)\n_LOGIN_EXIT=$?\nlog \"Matrix login HTTP exit code: ${_LOGIN_EXIT}\"\nlog \"Matrix login response: ${_LOGIN_RESPONSE}\"\n\nMANAGER_TOKEN=$(echo \"${_LOGIN_RESPONSE}\" | jq -r '.access_token' 2>/dev/null)\n\nif [ -z \"${MANAGER_TOKEN}\" ] || [ \"${MANAGER_TOKEN}\" = \"null\" ]; then\n log \"ERROR: Failed to obtain Manager Matrix token (exit=${_LOGIN_EXIT})\"\n log \"ERROR: Login response was: ${_LOGIN_RESPONSE}\"\n exit 1\nfi\nlog \"Manager Matrix token obtained (token prefix: ${MANAGER_TOKEN:0:10}...)\"\n\n# ============================================================\n# Local mode: Initialize Higress Console + configure routes\n# Cloud mode: Create admin DM room + schedule welcome message\n# ============================================================\nif [ \"${HICLAW_RUNTIME}\" != \"aliyun\" ]; then\n COOKIE_FILE=\"/tmp/higress-session-cookie\"\n\n log \"Waiting for Higress Console to be fully ready and initializing admin...\"\n INIT_DONE=false\n for i in $(seq 1 90); do\n INIT_RESULT=$(curl -s -X POST http://127.0.0.1:8001/system/init \\\n -H 'Content-Type: application/json' \\\n -d '{\"adminUser\":{\"name\":\"'\"${HICLAW_ADMIN_USER}\"'\",\"password\":\"'\"${HICLAW_ADMIN_PASSWORD}\"'\",\"displayName\":\"'\"${HICLAW_ADMIN_USER}\"'\"}}' 2>/dev/null) || true\n if echo \"${INIT_RESULT}\" | grep -qE '\"success\":true|already.?init' 2>/dev/null; then\n INIT_DONE=true\n break\n fi\n if echo \"${INIT_RESULT}\" | grep -q '\"name\"' 2>/dev/null; then\n INIT_DONE=true\n break\n fi\n sleep 2\n done\n\n if [ \"${INIT_DONE}\" != \"true\" ]; then\n log \"ERROR: Higress Console did not become ready within 180s\"\n exit 1\n fi\n log \"Higress Console init done\"\n\n log \"Logging into Higress Console...\"\n LOGIN_OK=false\n for i in $(seq 1 10); do\n HTTP_CODE=$(curl -s -o /dev/null -w '%{http_code}' -X POST http://127.0.0.1:8001/session/login \\\n -H 'Content-Type: application/json' \\\n -c \"${COOKIE_FILE}\" \\\n -d '{\"username\":\"'\"${HICLAW_ADMIN_USER}\"'\",\"password\":\"'\"${HICLAW_ADMIN_PASSWORD}\"'\"}' 2>/dev/null) || true\n if { [ \"${HTTP_CODE}\" = \"200\" ] || [ \"${HTTP_CODE}\" = \"201\" ]; } && [ -f \"${COOKIE_FILE}\" ] && [ -s \"${COOKIE_FILE}\" ]; then\n LOGIN_OK=true\n break\n fi\n log \"Login attempt $i (HTTP ${HTTP_CODE}), retrying in 3s...\"\n sleep 3\n done\n\n if [ \"${LOGIN_OK}\" != \"true\" ]; then\n log \"ERROR: Could not login to Higress Console after retries\"\n exit 1\n fi\n log \"Higress Console login successful\"\n\n VERIFY_CODE=$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8001/v1/consumers -b \"${COOKIE_FILE}\" 2>/dev/null) || true\n if [ \"${VERIFY_CODE}\" = \"200\" ]; then\n log \"Console session verified (cookie valid)\"\n else\n log \"WARNING: Console session may be invalid (verify returned HTTP ${VERIFY_CODE})\"\n rm -f \"${COOKIE_FILE}\"\n for i in $(seq 1 5); do\n curl -s -o /dev/null -w '%{http_code}' -X POST http://127.0.0.1:8001/session/login \\\n -H 'Content-Type: application/json' \\\n -c \"${COOKIE_FILE}\" \\\n -d '{\"username\":\"'\"${HICLAW_ADMIN_USER}\"'\",\"password\":\"'\"${HICLAW_ADMIN_PASSWORD}\"'\"}' 2>/dev/null\n VERIFY2=$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8001/v1/consumers -b \"${COOKIE_FILE}\" 2>/dev/null) || true\n if [ \"${VERIFY2}\" = \"200\" ]; then\n log \"Re-login successful, session verified\"\n break\n fi\n sleep 2\n done\n fi\n\n export HIGRESS_COOKIE_FILE=\"${COOKIE_FILE}\"\n\n # Configure Higress routes, consumers, MCP servers\n /opt/hiclaw/scripts/init/setup-higress.sh\nfi\n\n# ============================================================\n# Create admin DM room, persist to state.json, send welcome message\n# Runs in both local and cloud modes (idempotent)\n# ============================================================\nMANAGER_FULL_ID=\"@manager:${MATRIX_DOMAIN}\"\nADMIN_FULL_ID=\"@${HICLAW_ADMIN_USER}:${MATRIX_DOMAIN}\"\n\nlog \"Logging in as admin to create DM room...\"\n_ADMIN_LOGIN=$(curl -sf -X POST \"${HICLAW_MATRIX_SERVER}/_matrix/client/v3/login\" \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"type\": \"m.login.password\",\n \"identifier\": {\"type\": \"m.id.user\", \"user\": \"'\"${HICLAW_ADMIN_USER}\"'\"},\n \"password\": \"'\"${HICLAW_ADMIN_PASSWORD}\"'\"\n }' 2>&1) || true\n\nADMIN_MATRIX_TOKEN=$(echo \"${_ADMIN_LOGIN}\" | jq -r '.access_token // empty' 2>/dev/null)\nif [ -z \"${ADMIN_MATRIX_TOKEN}\" ]; then\n log \"WARNING: Failed to login as admin, skipping DM room creation\"\nelse\n # Search for existing DM room with Manager (idempotent)\n DM_ROOM_ID=\"\"\n _JOINED_ROOMS=$(curl -sf \"${HICLAW_MATRIX_SERVER}/_matrix/client/v3/joined_rooms\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" 2>/dev/null \\\n | jq -r '.joined_rooms[]' 2>/dev/null) || true\n for _rid in ${_JOINED_ROOMS}; do\n _members=$(curl -sf \"${HICLAW_MATRIX_SERVER}/_matrix/client/v3/rooms/${_rid}/members\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" 2>/dev/null \\\n | jq -r '.chunk[].state_key' 2>/dev/null) || continue\n _count=$(echo \"${_members}\" | wc -l | xargs)\n if [ \"${_count}\" = \"2\" ] && echo \"${_members}\" | grep -q \"@manager:\"; then\n DM_ROOM_ID=\"${_rid}\"\n break\n fi\n done\n\n if [ -n \"${DM_ROOM_ID}\" ]; then\n log \"Existing DM room found: ${DM_ROOM_ID}\"\n else\n log \"Creating DM room with Manager...\"\n _RAW=$(curl -s -w '\\nHTTP_CODE:%{http_code}' -X POST \"${HICLAW_MATRIX_SERVER}/_matrix/client/v3/createRoom\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d \"{\\\"is_direct\\\":true,\\\"invite\\\":[\\\"${MANAGER_FULL_ID}\\\"],\\\"preset\\\":\\\"trusted_private_chat\\\"}\" 2>&1) || true\n _HTTP_CODE=$(echo \"${_RAW}\" | tail -1 | sed 's/HTTP_CODE://')\n _CREATE_RESP=$(echo \"${_RAW}\" | sed '$d')\n DM_ROOM_ID=$(echo \"${_CREATE_RESP}\" | jq -r '.room_id // empty' 2>/dev/null)\n if [ -n \"${DM_ROOM_ID}\" ]; then\n log \"DM room created: ${DM_ROOM_ID}\"\n else\n log \"WARNING: Failed to create DM room (HTTP ${_HTTP_CODE}): ${_CREATE_RESP}\"\n fi\n fi\n\n # Persist admin DM room ID to state.json\n if [ -n \"${DM_ROOM_ID}\" ]; then\n STATE_SCRIPT=\"/opt/hiclaw/agent/skills/task-management/scripts/manage-state.sh\"\n if [ -f \"${STATE_SCRIPT}\" ]; then\n bash \"${STATE_SCRIPT}\" --action init 2>/dev/null || true\n bash \"${STATE_SCRIPT}\" --action set-admin-dm --room-id \"${DM_ROOM_ID}\" 2>/dev/null || true\n log \"Admin DM room persisted to state.json: ${DM_ROOM_ID}\"\n fi\n fi\n\n # Schedule welcome message in background (only on first boot)\n if [ -n \"${DM_ROOM_ID}\" ] && [ ! -f \"/root/manager-workspace/soul-configured\" ]; then\n log \"Scheduling welcome message (background, waiting for OpenClaw to start)...\"\n (\n _HICLAW_LANGUAGE=\"${HICLAW_LANGUAGE:-zh}\"\n _HICLAW_TIMEZONE=\"${TZ:-Asia/Shanghai}\"\n _wait=0\n _ready=false\n while [ \"${_wait}\" -lt 300 ]; do\n if curl -sf http://127.0.0.1:18799/ > /dev/null 2>&1; then\n _ready=true\n break\n fi\n sleep 3\n _wait=$((_wait + 3))\n done\n if [ \"${_ready}\" != \"true\" ]; then\n echo \"[manager] WARNING: OpenClaw gateway not ready within 300s, skipping welcome message\"\n exit 0\n fi\n _welcome_msg=\"This is an automated message from the HiClaw setup. This is a fresh installation.\n\n--- Installation Context ---\nUser Language: ${_HICLAW_LANGUAGE} (zh = Chinese, en = English)\nUser Timezone: ${_HICLAW_TIMEZONE} (IANA timezone identifier)\n---\n\nYou are an AI agent that manages a team of worker agents. Your identity and personality have not been configured yet — the human admin is about to meet you for the first time.\n\nPlease begin the onboarding conversation:\n\n1. Greet the admin warmly and briefly describe what you can do (coordinate workers, manage tasks, run multi-agent projects)\n2. The user has selected \\\"${_HICLAW_LANGUAGE}\\\" as their preferred language during installation. Use this language for your greeting and all subsequent communication.\n3. The user's timezone is ${_HICLAW_TIMEZONE}. Based on this timezone, you may infer their likely region and suggest additional language options.\n4. Ask them: a) What would they like to call you? b) Communication style preference? c) Any behavior guidelines? d) Confirm default language\n5. After they reply, write their preferences to ~/SOUL.md\n6. Confirm what you wrote, and ask if they would like to adjust anything\n7. Once confirmed, run: touch ~/soul-configured\n\nThe human admin will start chatting shortly.\"\n _txn_id=\"welcome-$(date +%s)\"\n _payload=$(jq -nc --arg body \"${_welcome_msg}\" '{\"msgtype\":\"m.text\",\"body\":$body}')\n _raw=$(curl -s -w '\\nHTTP_CODE:%{http_code}' -X PUT \"${HICLAW_MATRIX_SERVER}/_matrix/client/v3/rooms/${DM_ROOM_ID}/send/m.room.message/${_txn_id}\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d \"${_payload}\" 2>&1) || true\n _http_code=$(echo \"${_raw}\" | tail -1 | sed 's/HTTP_CODE://')\n _send_resp=$(echo \"${_raw}\" | sed '$d')\n if echo \"${_send_resp}\" | jq -e '.event_id' > /dev/null 2>&1; then\n echo \"[manager] Welcome message sent to DM room\"\n else\n echo \"[manager] WARNING: Failed to send welcome message (HTTP ${_http_code}): ${_send_resp}\"\n fi\n ) &\n log \"Welcome message background process started (PID: $!)\"\n fi\nfi\n\n# ============================================================\n# Generate Manager Agent openclaw.json from template\n# ============================================================\nlog \"Generating Manager openclaw.json...\"\nexport MANAGER_MATRIX_TOKEN=\"${MANAGER_TOKEN}\"\nexport MANAGER_GATEWAY_KEY=\"${HICLAW_MANAGER_GATEWAY_KEY}\"\n# Resolve model parameters based on model name\nMODEL_NAME=\"${HICLAW_DEFAULT_MODEL:-qwen3.5-plus}\"\ncase \"${MODEL_NAME}\" in\n gpt-5.3-codex|gpt-5-mini|gpt-5-nano)\n export MODEL_CONTEXT_WINDOW=400000 MODEL_MAX_TOKENS=128000 ;;\n claude-opus-4-6)\n export MODEL_CONTEXT_WINDOW=1000000 MODEL_MAX_TOKENS=128000 ;;\n claude-sonnet-4-6)\n export MODEL_CONTEXT_WINDOW=1000000 MODEL_MAX_TOKENS=64000 ;;\n claude-haiku-4-5)\n export MODEL_CONTEXT_WINDOW=200000 MODEL_MAX_TOKENS=64000 ;;\n qwen3.5-plus)\n export MODEL_CONTEXT_WINDOW=200000 MODEL_MAX_TOKENS=64000 ;;\n deepseek-chat|deepseek-reasoner|kimi-k2.5)\n export MODEL_CONTEXT_WINDOW=256000 MODEL_MAX_TOKENS=128000 ;;\n glm-5|MiniMax-M2.7|MiniMax-M2.7-highspeed|MiniMax-M2.5)\n export MODEL_CONTEXT_WINDOW=200000 MODEL_MAX_TOKENS=128000 ;;\n *)\n export MODEL_CONTEXT_WINDOW=150000 MODEL_MAX_TOKENS=128000 ;;\nesac\nexport MODEL_REASONING=true\n\n# Override with user-supplied custom model parameters from env (set during install)\n[ -n \"${HICLAW_MODEL_CONTEXT_WINDOW:-}\" ] && export MODEL_CONTEXT_WINDOW=\"${HICLAW_MODEL_CONTEXT_WINDOW}\"\n[ -n \"${HICLAW_MODEL_MAX_TOKENS:-}\" ] && export MODEL_MAX_TOKENS=\"${HICLAW_MODEL_MAX_TOKENS}\"\n[ -n \"${HICLAW_MODEL_REASONING:-}\" ] && export MODEL_REASONING=\"${HICLAW_MODEL_REASONING}\"\n\n# E2EE: convert HICLAW_MATRIX_E2EE to JSON boolean for template substitution\nif [ \"${HICLAW_MATRIX_E2EE:-0}\" = \"1\" ] || [ \"${HICLAW_MATRIX_E2EE:-}\" = \"true\" ]; then\n export MATRIX_E2EE_ENABLED=true\nelse\n export MATRIX_E2EE_ENABLED=false\nfi\nlog \"Matrix E2EE: ${MATRIX_E2EE_ENABLED}\"\n\n# Resolve input modalities: only vision-capable models get \"image\"\ncase \"${MODEL_NAME}\" in\n gpt-5.4|gpt-5.3-codex|gpt-5-mini|gpt-5-nano|claude-opus-4-6|claude-sonnet-4-6|claude-haiku-4-5|qwen3.5-plus|kimi-k2.5)\n export MODEL_INPUT='[\"text\", \"image\"]' ;;\n *)\n export MODEL_INPUT='[\"text\"]' ;;\nesac\n# Override with user-supplied vision setting from env\nif [ \"${HICLAW_MODEL_VISION:-}\" = \"true\" ]; then\n export MODEL_INPUT='[\"text\", \"image\"]'\nelif [ \"${HICLAW_MODEL_VISION:-}\" = \"false\" ]; then\n export MODEL_INPUT='[\"text\"]'\nfi\n\nlog \"Model: ${MODEL_NAME} (context=${MODEL_CONTEXT_WINDOW}, maxTokens=${MODEL_MAX_TOKENS}, reasoning=${MODEL_REASONING}, input=${MODEL_INPUT})\"\n\nif [ -f /root/manager-workspace/openclaw.json ]; then\n log \"Manager openclaw.json already exists, updating dynamic fields only (preserving user customizations)...\"\n # Merge known models into existing config (add missing, preserve user-added)\n # Use known-models.json (valid JSON) instead of template (contains ${VAR} placeholders)\n KNOWN_MODELS=$(cat /opt/hiclaw/configs/known-models.json 2>/dev/null || echo '[]')\n jq --arg token \"${MANAGER_TOKEN}\" \\\n --arg key \"${HICLAW_MANAGER_GATEWAY_KEY}\" \\\n --arg model \"${MODEL_NAME}\" \\\n --argjson e2ee \"${MATRIX_E2EE_ENABLED}\" \\\n --argjson known_models \"${KNOWN_MODELS}\" \\\n --argjson ctx \"${MODEL_CONTEXT_WINDOW}\" \\\n --argjson max \"${MODEL_MAX_TOKENS}\" \\\n --argjson reasoning \"${MODEL_REASONING}\" \\\n --argjson input \"${MODEL_INPUT}\" \\\n '\n # Merge known models: add any model id not already present\n .models.providers[\"hiclaw-gateway\"].models as $existing\n | ($existing | map(.id)) as $existing_ids\n | ($known_models | map(select(.id as $id | $existing_ids | index($id) | not))) as $new\n | .models.providers[\"hiclaw-gateway\"].models = ($existing + $new)\n # Ensure the user-chosen default model is in the list (custom model support)\n | if (.models.providers[\"hiclaw-gateway\"].models | map(.id) | index($model) | not) then\n .models.providers[\"hiclaw-gateway\"].models += [{\"id\": $model, \"name\": $model, \"reasoning\": $reasoning, \"contextWindow\": $ctx, \"maxTokens\": $max, \"input\": $input}]\n else . end\n # Rebuild model aliases from the full models list\n | (.models.providers[\"hiclaw-gateway\"].models | map({ (\"hiclaw-gateway/\" + .id): { \"alias\": .id } }) | add // {}) as $aliases\n | .agents.defaults.models = ((.agents.defaults.models // {}) + $aliases)\n | .channels.matrix.accessToken = $token | .models.providers[\"hiclaw-gateway\"].apiKey = $key\n | ((.hooks.token // \"\") as $ht | if $ht == $key or $ht == ($key + \"-hooks\" | @base64) then del(.hooks) else . end)\n | .agents.defaults.model.primary = (\"hiclaw-gateway/\" + $model)\n | .commands.restart = true\n | .gateway.controlUi.dangerouslyDisableDeviceAuth = true\n | .channels.matrix.encryption = $e2ee\n ' \\\n /root/manager-workspace/openclaw.json > /tmp/openclaw.json.tmp && \\\n mv /tmp/openclaw.json.tmp /root/manager-workspace/openclaw.json\n # Verify the token was written correctly\n _written_token=$(jq -r '.channels.matrix.accessToken' /root/manager-workspace/openclaw.json 2>/dev/null)\n if [ -z \"${_written_token}\" ] || [ \"${_written_token}\" = \"null\" ]; then\n log \"ERROR: Matrix token was not written correctly to openclaw.json (got: ${_written_token})\"\n else\n log \"Matrix token written to openclaw.json (prefix: ${_written_token:0:10}...)\"\n fi\nelse\n log \"Manager openclaw.json not found, generating from template...\"\n envsubst < /opt/hiclaw/configs/manager-openclaw.json.tmpl > /root/manager-workspace/openclaw.json\n # Inject custom model if not in the built-in list\n if ! jq -e --arg model \"${MODEL_NAME}\" '.models.providers[\"hiclaw-gateway\"].models | map(.id) | index($model)' /root/manager-workspace/openclaw.json > /dev/null 2>&1; then\n log \"Custom model '${MODEL_NAME}' not in built-in list, injecting into config...\"\n jq --arg model \"${MODEL_NAME}\" \\\n --argjson ctx \"${MODEL_CONTEXT_WINDOW}\" \\\n --argjson max \"${MODEL_MAX_TOKENS}\" \\\n --argjson reasoning \"${MODEL_REASONING}\" \\\n --argjson input \"${MODEL_INPUT}\" \\\n '\n .models.providers[\"hiclaw-gateway\"].models += [{\"id\": $model, \"name\": $model, \"reasoning\": $reasoning, \"contextWindow\": $ctx, \"maxTokens\": $max, \"input\": $input}]\n | .agents.defaults.models += {(\"hiclaw-gateway/\" + $model): {\"alias\": $model}}\n ' /root/manager-workspace/openclaw.json > /tmp/openclaw.json.tmp && \\\n mv /tmp/openclaw.json.tmp /root/manager-workspace/openclaw.json\n fi\n _written_token=$(jq -r '.channels.matrix.accessToken' /root/manager-workspace/openclaw.json 2>/dev/null)\n log \"Matrix token written from template (prefix: ${_written_token:0:10}...)\"\nfi\n\n# Cloud mode: overlay cloud-specific settings onto generated config\nif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n log \"Applying cloud overlay to openclaw.json...\"\n jq --arg homeserver \"${HICLAW_MATRIX_SERVER}\" \\\n --arg gateway \"${HICLAW_AI_GATEWAY_URL}/v1\" \\\n --arg key \"${HICLAW_MANAGER_GATEWAY_KEY}\" \\\n '.channels.matrix.homeserver = $homeserver\n | .models.providers[\"hiclaw-gateway\"].baseUrl = $gateway\n | .models.providers[\"hiclaw-gateway\"].apiKey = $key\n | ((.hooks.token // \"\") as $ht | if $ht == $key or $ht == ($key + \"-hooks\" | @base64) then del(.hooks) else . end)\n | .commands.restart = false' \\\n /root/manager-workspace/openclaw.json > /tmp/openclaw-cloud.json && \\\n mv /tmp/openclaw-cloud.json /root/manager-workspace/openclaw.json\n log \"Cloud overlay applied\"\nfi\n\n# ============================================================\n# Detect container runtime (for Worker creation)\n# ============================================================\nsource /opt/hiclaw/scripts/lib/container-api.sh\nif container_api_available; then\n log \"Container runtime socket detected at ${CONTAINER_SOCKET} — direct Worker creation enabled\"\n export HICLAW_CONTAINER_RUNTIME=\"socket\"\nelif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n log \"Cloud mode — Workers created via SAE API\"\n export HICLAW_CONTAINER_RUNTIME=\"cloud\"\nelse\n log \"No container runtime found — Worker creation will output install commands\"\n export HICLAW_CONTAINER_RUNTIME=\"none\"\nfi\n\n# ============================================================\n# Upgrade Worker openclaw.json: merge known models + E2EE flag into existing configs\n# Existing workers in MinIO may have old single-model configs or missing encryption field.\n# Merge template models so they can hot-switch without restart.\n# ============================================================\nREGISTRY_FILE=\"/root/manager-workspace/workers-registry.json\"\nif [ -f \"${REGISTRY_FILE}\" ]; then\n # Use known-models.json (valid JSON) instead of template (contains ${VAR} placeholders)\n KNOWN_MODELS_FILE=\"/opt/hiclaw/configs/known-models.json\"\n if [ -f \"${KNOWN_MODELS_FILE}\" ]; then\n _KNOWN_MODELS=$(cat \"${KNOWN_MODELS_FILE}\")\n for _wname in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_wname}\" ] && continue\n _minio_path=\"${HICLAW_STORAGE_PREFIX}/agents/${_wname}/openclaw.json\"\n _tmp_in=\"/tmp/openclaw-${_wname}-models-upgrade-in.json\"\n if mc cp \"${_minio_path}\" \"${_tmp_in}\" 2>/dev/null; then\n _tmp_out=\"/tmp/openclaw-${_wname}-models-upgrade-out.json\"\n # Idempotent merge: add missing known models, rebuild aliases, set e2ee.\n # Always runs — jq deduplicates by model id, so re-runs are safe.\n jq --argjson known_models \"${_KNOWN_MODELS}\" \\\n --argjson e2ee \"${MATRIX_E2EE_ENABLED}\" '\n .models.providers[\"hiclaw-gateway\"].models as $existing\n | ($existing | map(.id)) as $existing_ids\n | ($known_models | map(select(.id as $id | $existing_ids | index($id) | not))) as $new\n | .models.providers[\"hiclaw-gateway\"].models = ($existing + $new)\n | (.models.providers[\"hiclaw-gateway\"].models | map({ (\"hiclaw-gateway/\" + .id): { \"alias\": .id } }) | add // {}) as $aliases\n | .agents.defaults.models = ((.agents.defaults.models // {}) + $aliases)\n | .channels.matrix.encryption = $e2ee\n ' \"${_tmp_in}\" > \"${_tmp_out}\" 2>/dev/null\n if ! diff -q \"${_tmp_in}\" \"${_tmp_out}\" > /dev/null 2>&1; then\n if mc cp \"${_tmp_out}\" \"${_minio_path}\" 2>/dev/null; then\n _new_count=$(jq '.models.providers[\"hiclaw-gateway\"].models | length' \"${_tmp_out}\" 2>/dev/null)\n log \"Worker ${_wname}: upgraded openclaw.json (models: ${_new_count}, e2ee: ${MATRIX_E2EE_ENABLED})\"\n fi\n fi\n rm -f \"${_tmp_in}\" \"${_tmp_out}\"\n fi\n done\n fi\nfi\n\n# ============================================================\n# Ensure Worker Matrix password files exist in MinIO (E2EE fix)\n# Workers need to re-login on restart to get a fresh device_id.\n# Older workers created before this fix won't have the password file.\n# ============================================================\nif [ -f \"${REGISTRY_FILE}\" ]; then\n for _wname in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_wname}\" ] && continue\n _creds_file=\"/data/worker-creds/${_wname}.env\"\n if [ -f \"${_creds_file}\" ]; then\n # Check if password file already exists in MinIO\n if ! mc stat \"${HICLAW_STORAGE_PREFIX}/agents/${_wname}/credentials/matrix/password\" > /dev/null 2>&1; then\n source \"${_creds_file}\"\n if [ -n \"${WORKER_PASSWORD}\" ]; then\n _tmp_pw=\"/tmp/matrix-pw-${_wname}\"\n echo -n \"${WORKER_PASSWORD}\" > \"${_tmp_pw}\"\n mc cp \"${_tmp_pw}\" \"${HICLAW_STORAGE_PREFIX}/agents/${_wname}/credentials/matrix/password\" 2>/dev/null \\\n && log \"Worker ${_wname}: wrote Matrix password to MinIO (E2EE re-login fix)\" \\\n || log \"Worker ${_wname}: WARNING: failed to write Matrix password to MinIO\"\n rm -f \"${_tmp_pw}\"\n fi\n fi\n fi\n done\nfi\n\n# ============================================================\n# Recreate Worker containers as needed after Manager restart.\n# Manager IP may change on restart; Workers use ExtraHosts pointing to Manager IP,\n# so any worker whose ExtraHosts IP no longer matches must be recreated.\n# ============================================================\nif container_api_available; then\n REGISTRY_FILE=\"/root/manager-workspace/workers-registry.json\"\n _manager_ip=$(container_get_manager_ip)\n if [ -f \"${REGISTRY_FILE}\" ]; then\n for _worker_name in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_worker_name}\" ] && continue\n\n # Skip remote workers — they are not Manager-managed containers.\n _deployment=$(jq -r --arg w \"${_worker_name}\" '.workers[$w].deployment // \"local\"' \"${REGISTRY_FILE}\" 2>/dev/null)\n if [ \"${_deployment}\" = \"remote\" ]; then\n log \"Worker ${_worker_name} is remote, skipping container recreate\"\n continue\n fi\n\n _status=$(container_status_worker \"${_worker_name}\")\n if [ \"${_status}\" = \"running\" ]; then\n # Check if ExtraHosts IP still matches current Manager IP.\n # If ExtraHosts is empty the worker uses real DNS — no recreate needed.\n _inspect=$(_api GET \"/containers/${WORKER_CONTAINER_PREFIX}${_worker_name}/json\" 2>/dev/null)\n _extra_hosts_len=$(echo \"${_inspect}\" | jq -r '.HostConfig.ExtraHosts | length' 2>/dev/null)\n if [ -z \"${_extra_hosts_len}\" ] || [ \"${_extra_hosts_len}\" = \"0\" ]; then\n log \"Worker running (no ExtraHosts, real DNS): ${_worker_name}, skipping\"\n continue\n fi\n _worker_ip=$(echo \"${_inspect}\" | jq -r '.HostConfig.ExtraHosts[0]' 2>/dev/null | cut -d: -f2)\n if [ \"${_worker_ip}\" = \"${_manager_ip}\" ]; then\n log \"Worker running with current Manager IP (${_manager_ip}): ${_worker_name}, skipping\"\n continue\n fi\n log \"Worker running but Manager IP changed (${_worker_ip} -> ${_manager_ip}): ${_worker_name}, recreating...\"\n else\n # Container missing or stopped — always recreate.\n log \"Worker container ${_status}: ${_worker_name}, recreating...\"\n fi\n _creds_file=\"/data/worker-creds/${_worker_name}.env\"\n if [ -f \"${_creds_file}\" ]; then\n source \"${_creds_file}\"\n _runtime=$(jq -r --arg w \"${_worker_name}\" '.workers[$w].runtime // \"openclaw\"' \"${REGISTRY_FILE}\" 2>/dev/null)\n _recreated=false\n for _attempt in 1 2 3; do\n if [ \"${_runtime}\" = \"copaw\" ]; then\n container_create_copaw_worker \"${_worker_name}\" \"${_worker_name}\" \"${WORKER_MINIO_PASSWORD}\" 2>&1 && _recreated=true && break\n else\n container_create_worker \"${_worker_name}\" \"${_worker_name}\" \"${WORKER_MINIO_PASSWORD}\" 2>&1 && _recreated=true && break\n fi\n log \" Attempt ${_attempt}/3 failed for ${_worker_name}, retrying in $((5 * _attempt))s...\"\n sleep $((5 * _attempt))\n done\n if [ \"${_recreated}\" = true ]; then\n log \" Recreated ${_runtime} worker: ${_worker_name}\"\n else\n log \" ERROR: Failed to recreate ${_worker_name} after 3 attempts\"\n fi\n else\n log \" WARNING: No credentials found for ${_worker_name} (${_creds_file} missing), skipping\"\n fi\n done\n fi\nfi\n\n# ============================================================\n# Notify workers of builtin updates if upgrade happened\n# Builtin files (AGENTS.md, skills) are already synced by upgrade-builtins.sh\n#\n# Cooldown: skip notification if the last successful notify was within\n# NOTIFY_COOLDOWN_SECS (default 3600s / 1 hour). This prevents repeated\n# notifications when the Manager crash-loops and re-runs upgrade-builtins\n# on every restart (e.g. IMAGE_VERSION=latest always triggers upgrade).\n# ============================================================\nNOTIFY_COOLDOWN_SECS=\"${HICLAW_NOTIFY_COOLDOWN_SECS:-3600}\"\nNOTIFY_TS_FILE=\"/root/manager-workspace/.last-worker-notify-ts\"\n\nif [ -f /root/manager-workspace/.upgrade-pending-worker-notify ]; then\n _now=$(date +%s)\n _last_notify=$(cat \"${NOTIFY_TS_FILE}\" 2>/dev/null || echo \"0\")\n _elapsed=$(( _now - _last_notify ))\n\n if [ \"${_elapsed}\" -lt \"${NOTIFY_COOLDOWN_SECS}\" ]; then\n log \"Skipping worker builtin notification (last notify ${_elapsed}s ago, cooldown ${NOTIFY_COOLDOWN_SECS}s)\"\n rm -f /root/manager-workspace/.upgrade-pending-worker-notify\n else\n log \"Notifying workers about builtin updates...\"\n REGISTRY_FILE=\"/root/manager-workspace/workers-registry.json\"\n _notify_ok=false\n if [ -f \"${REGISTRY_FILE}\" ]; then\n for _worker_name in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_worker_name}\" ] && continue\n _room_id=$(jq -r --arg w \"${_worker_name}\" '.workers[$w].room_id // empty' \"${REGISTRY_FILE}\" 2>/dev/null)\n if [ -n \"${_room_id}\" ]; then\n _worker_id=\"@${_worker_name}:${MATRIX_DOMAIN}\"\n _txn_id=\"upgrade-$(date +%s%N)\"\n _msg=\"@${_worker_name}:${MATRIX_DOMAIN} Manager upgraded builtin files (AGENTS.md, skills). Please use your file-sync skill to sync the latest config.\"\n _raw=$(curl -s -w '\\nHTTP_CODE:%{http_code}' -X PUT \\\n \"${HICLAW_MATRIX_SERVER}/_matrix/client/v3/rooms/${_room_id}/send/m.room.message/${_txn_id}\" \\\n -H \"Authorization: Bearer ${MANAGER_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d \"{\\\"msgtype\\\":\\\"m.text\\\",\\\"body\\\":\\\"${_msg}\\\",\\\"m.mentions\\\":{\\\"user_ids\\\":[\\\"${_worker_id}\\\"]}}\" \\\n 2>&1) || true\n _http_code=$(echo \"${_raw}\" | tail -1 | sed 's/HTTP_CODE://')\n _notify_resp=$(echo \"${_raw}\" | sed '$d')\n if echo \"${_notify_resp}\" | jq -e '.event_id' > /dev/null 2>&1; then\n log \" Notified ${_worker_name}\"; _notify_ok=true\n else\n log \" WARNING: Failed to notify ${_worker_name} (HTTP ${_http_code}): ${_notify_resp}\"\n fi\n fi\n done\n fi\n # Record timestamp only if at least one notification succeeded\n if [ \"${_notify_ok}\" = true ]; then\n echo \"${_now}\" > \"${NOTIFY_TS_FILE}\"\n fi\n rm -f /root/manager-workspace/.upgrade-pending-worker-notify\n fi\nfi\n\n# ============================================================\n# Start OpenClaw Manager Agent\n# ============================================================\nlog \"Starting Manager Agent (OpenClaw)...\"\n\n# HOME is already set to /root/manager-workspace via docker run -e HOME=...\nexport OPENCLAW_CONFIG_PATH=\"/root/manager-workspace/openclaw.json\"\n\n# Symlink to default OpenClaw config path so CLI commands find the config\nmkdir -p \"${HOME}/.openclaw\"\nln -sf \"/root/manager-workspace/openclaw.json\" \"${HOME}/.openclaw/openclaw.json\"\n\n# Ensure host credential symlinks exist under HOME\nif [ -d \"/host-share\" ]; then\n [ -f \"/host-share/.gitconfig\" ] && ln -sf \"/host-share/.gitconfig\" \"${HOME}/.gitconfig\"\nfi\n\nlog \"HOME=${HOME} (manager-workspace, host-mounted)\"\ncd \"${HOME}\"\n\n# Clean orphaned session write locks (e.g. from SIGKILL or crash before exit handlers)\n# Prevents \"session file locked (timeout 10000ms)\" when PID was reused\nfind \"${HOME}/.openclaw/agents\" -name \"*.jsonl.lock\" -delete 2>/dev/null || true\nlog \"Cleaned up any orphaned session write locks\"\n\n# Clean Matrix crypto storage (SQLite WAL may be corrupted after unclean shutdown)\n# Crypto state is re-negotiated on startup; losing it only means re-establishing E2EE sessions\nrm -rf \"${HOME}/.openclaw/matrix\" 2>/dev/null || true\nlog \"Cleaned Matrix crypto storage (will re-establish E2EE sessions)\"\n\n# ── Render agent doc templates ────────────────────────────────────────────\n# Replace ${VAR} placeholders with actual values so the AI agent reads\n# plain text and never needs to resolve environment variables.\nexport MANAGER_MATRIX_TOKEN MANAGER_TOKEN HIGRESS_COOKIE_FILE\nRENDER=/opt/hiclaw/scripts/lib/render-skills.sh\nlog \"Rendering agent doc templates...\"\n# Manager-owned docs (workspace)\nbash \"$RENDER\" /root/manager-workspace/skills\nbash \"$RENDER\" /root/manager-workspace/skills-alpha\nbash \"$RENDER\" /root/manager-workspace AGENTS.md TOOLS.md HEARTBEAT.md SOUL.md\n# Worker templates (workspace + image) — rendered before push to MinIO\n# so Workers (including remote pip-install) receive plain text\nbash \"$RENDER\" /root/manager-workspace/worker-skills\nbash \"$RENDER\" /root/manager-workspace/worker-agent\nbash \"$RENDER\" /root/manager-workspace/copaw-worker-agent\nbash \"$RENDER\" /opt/hiclaw/agent/worker-skills\nbash \"$RENDER\" /opt/hiclaw/agent/worker-agent\nbash \"$RENDER\" /opt/hiclaw/agent/copaw-worker-agent\nlog \"Agent doc templates rendered\"\n\n# Cloud mode: start background file sync (workspace ↔ OSS) and initial push\nif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n log \"Syncing initial workspace to OSS...\"\n ensure_mc_credentials\n mc mirror /root/manager-workspace/ \"${HICLAW_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" 2>/dev/null || true\n\n # Local → OSS: change-triggered sync\n (\n while true; do\n CHANGED=$(find /root/manager-workspace/ -type f -newermt \"15 seconds ago\" 2>/dev/null | head -1)\n if [ -n \"${CHANGED}\" ]; then\n ensure_mc_credentials 2>/dev/null || true\n mc mirror /root/manager-workspace/ \"${HICLAW_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" --exclude \".npm/**\" \\\n --exclude \".local/**\" --exclude \".mc/**\" 2>/dev/null || true\n fi\n sleep 10\n done\n ) &\n log \"Local→OSS sync started (PID: $!)\"\n\n # OSS → Local: periodic pull (shared data, agent configs)\n (\n while true; do\n sleep 300\n ensure_mc_credentials 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/shared/\" /root/hiclaw-fs/shared/ --overwrite --newer-than \"5m\" 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/agents/\" /root/hiclaw-fs/agents/ --overwrite --newer-than \"5m\" 2>/dev/null || true\n done\n ) &\n log \"OSS→Local sync started (every 5m, PID: $!)\"\nfi\n\n# Launch OpenClaw\nexec openclaw gateway run --verbose --force\n"}, "1c737723877896a4a5f04edbe9257cb6c0c4dcff": {"t": 1785764775.8942513, "n": 42196, "h": "00a6bb059c41a3ff", "c": "#!/bin/bash\n# start-manager-agent.sh - Initialize and start the Manager Agent\n# Supports both local (supervisord) and cloud (SAE single-process) deployments.\n# In local mode this is the last supervisord component to start (priority 800).\n# In cloud mode (HICLAW_RUNTIME=aliyun) this is the container entrypoint.\n\nsource /opt/hiclaw/scripts/lib/hiclaw-env.sh\n\n# ============================================================\n# Set timezone from TZ env var\n# ============================================================\nif [ -n \"${TZ}\" ] && [ -f \"/usr/share/zoneinfo/${TZ}\" ]; then\n ln -sf \"/usr/share/zoneinfo/${TZ}\" /etc/localtime\n echo \"${TZ}\" > /etc/timezone\n log \"Timezone set to ${TZ}\"\nfi\n\nMATRIX_DOMAIN=\"${HICLAW_MATRIX_DOMAIN:-matrix-local.hiclaw.io:8080}\"\nAI_GATEWAY_DOMAIN=\"${HICLAW_AI_GATEWAY_DOMAIN:-aigw-local.hiclaw.io}\"\n\n# ============================================================\n# Cloud mode: validate required environment variables + initial credentials\n# ============================================================\nif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n : \"${HICLAW_MATRIX_URL:?HICLAW_MATRIX_URL is required}\"\n : \"${HICLAW_MATRIX_DOMAIN:?HICLAW_MATRIX_DOMAIN is required}\"\n : \"${HICLAW_AI_GATEWAY_URL:?HICLAW_AI_GATEWAY_URL is required}\"\n : \"${HICLAW_MANAGER_GATEWAY_KEY:?HICLAW_MANAGER_GATEWAY_KEY is required}\"\n : \"${HICLAW_MANAGER_PASSWORD:?HICLAW_MANAGER_PASSWORD is required (cloud containers are stateless, password must be injected)}\"\n : \"${HICLAW_REGISTRATION_TOKEN:?HICLAW_REGISTRATION_TOKEN is required}\"\n : \"${HICLAW_ADMIN_USER:?HICLAW_ADMIN_USER is required}\"\n : \"${HICLAW_ADMIN_PASSWORD:?HICLAW_ADMIN_PASSWORD is required}\"\n log \"Cloud mode: validating environment... OK\"\n log \" Matrix: ${HICLAW_MATRIX_SERVER}, AI Gateway: ${HICLAW_AI_GATEWAY_URL}, OSS: ${HICLAW_STORAGE_BUCKET}\"\n ensure_mc_credentials || { log \"FATAL: Initial STS credential fetch failed\"; exit 1; }\nfi\n\n# ============================================================\n# Local mode: host symlinks, /etc/hosts, wait for local services\n# ============================================================\nif [ \"${HICLAW_RUNTIME}\" != \"aliyun\" ]; then\n # Create symlink for host directory access\n if [ -d \"/host-share\" ]; then\n ORIGINAL_HOST_HOME=\"${HOST_ORIGINAL_HOME:-$HOME}\"\n if [ ! -e \"${ORIGINAL_HOST_HOME}\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/root\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/data\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/host-share\" ]; then\n mkdir -p \"$(dirname \"${ORIGINAL_HOST_HOME}\")\"\n ln -sfn /host-share \"${ORIGINAL_HOST_HOME}\"\n log \"Created symlink: ${ORIGINAL_HOST_HOME} -> /host-share\"\n else\n ln -sfn /host-share /root/host-home\n log \"Created fallback symlink: /root/host-home -> /host-share\"\n fi\n fi\n\n # Add local domains to /etc/hosts\n HOSTS_DOMAINS=\"${MATRIX_DOMAIN%%:*} ${HICLAW_MATRIX_CLIENT_DOMAIN:-matrix-client-local.hiclaw.io} ${AI_GATEWAY_DOMAIN} ${HICLAW_FS_DOMAIN:-fs-local.hiclaw.io}\"\n if ! grep -q \"${AI_GATEWAY_DOMAIN}\" /etc/hosts 2>/dev/null; then\n echo \"127.0.0.1 ${HOSTS_DOMAINS}\" >> /etc/hosts\n log \"Added local domains to /etc/hosts\"\n fi\n\n # Wait for local infrastructure\n waitForService \"Higress Gateway\" \"127.0.0.1\" 8080 180\n waitForService \"Higress Console\" \"127.0.0.1\" 8001 180\n waitForService \"Tuwunel\" \"127.0.0.1\" 6167 120\n waitForHTTP \"Tuwunel Matrix API\" \"${HICLAW_MATRIX_SERVER}/_tuwunel/server_version\" 120\n waitForService \"MinIO\" \"127.0.0.1\" 9000 120\nelse\n # Cloud mode: wait for external Tuwunel\n log \"Waiting for Tuwunel Matrix server at ${HICLAW_MATRIX_SERVER}...\"\n _retry=0\n while [ \"${_retry}\" -lt 30 ]; do\n if curl -sf \"${HICLAW_MATRIX_SERVER}/_matrix/client/versions\" > /dev/null 2>&1; then\n log \"Tuwunel is ready\"\n break\n fi\n _retry=$((_retry + 1))\n log \" Waiting for Tuwunel (attempt ${_retry}/30)...\"\n sleep 5\n done\n if [ \"${_retry}\" -ge 30 ]; then\n log \"ERROR: Tuwunel not reachable at ${HICLAW_MATRIX_SERVER}\"\n exit 1\n fi\nfi\n\n# ============================================================\n# Auto-generate secrets if not provided via environment\n# Persisted to /data so they survive container restart\n# ============================================================\nSECRETS_FILE=\"/data/hiclaw-secrets.env\"\nif [ -f \"${SECRETS_FILE}\" ]; then\n source \"${SECRETS_FILE}\"\n log \"Loaded persisted secrets from ${SECRETS_FILE}\"\nfi\n\nif [ -z \"${HICLAW_MANAGER_GATEWAY_KEY}\" ]; then\n export HICLAW_MANAGER_GATEWAY_KEY=\"$(generateKey 32)\"\n log \"Auto-generated HICLAW_MANAGER_GATEWAY_KEY\"\nfi\nif [ -z \"${HICLAW_MANAGER_PASSWORD}\" ]; then\n export HICLAW_MANAGER_PASSWORD=\"$(generateKey 16)\"\n log \"Auto-generated HICLAW_MANAGER_PASSWORD\"\nfi\n\n# Persist secrets so they survive supervisord restart\nmkdir -p /data\ncat > \"${SECRETS_FILE}\" <<EOF\nexport HICLAW_MANAGER_GATEWAY_KEY=\"${HICLAW_MANAGER_GATEWAY_KEY}\"\nexport HICLAW_MANAGER_PASSWORD=\"${HICLAW_MANAGER_PASSWORD}\"\nEOF\nchmod 600 \"${SECRETS_FILE}\"\n\n# Cloud mode: pull workspace from OSS before initialization\nif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n HICLAW_FS=\"/root/hiclaw-fs\"\n mkdir -p \"${HICLAW_FS}/shared\" \"${HICLAW_FS}/agents\"\n log \"Pulling workspace from OSS...\"\n ensure_mc_credentials\n mc mirror \"${HICLAW_STORAGE_PREFIX}/manager/\" /root/manager-workspace/ --overwrite 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/shared/\" \"${HICLAW_FS}/shared/\" --overwrite 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/agents/\" \"${HICLAW_FS}/agents/\" --overwrite 2>/dev/null || true\n # Symlink hiclaw-fs into workspace for agent access\n ln -sfn \"${HICLAW_FS}\" /root/manager-workspace/hiclaw-fs\nfi\n\n# ============================================================\n# Initialize / upgrade Manager workspace\n# First boot: full init via upgrade-builtins.sh\n# Subsequent boots: compare image version; upgrade only if changed\n# ============================================================\nmkdir -p /root/manager-workspace\n\nIMAGE_VERSION=$(cat /opt/hiclaw/agent/.builtin-version 2>/dev/null || echo \"unknown\")\nINSTALLED_VERSION=$(cat /root/manager-workspace/.builtin-version 2>/dev/null || echo \"\")\n\nif [ ! -f /root/manager-workspace/.initialized ]; then\n log \"First boot: initializing manager workspace...\"\n bash /opt/hiclaw/scripts/init/upgrade-builtins.sh\n touch /root/manager-workspace/.initialized\n log \"Manager workspace initialized (version: ${IMAGE_VERSION})\"\nelif [ \"${IMAGE_VERSION}\" != \"${INSTALLED_VERSION}\" ] || [ \"${IMAGE_VERSION}\" = \"latest\" ]; then\n log \"Upgrade detected: ${INSTALLED_VERSION} -> ${IMAGE_VERSION}${IMAGE_VERSION:+ (latest: always upgrade)}\"\n bash /opt/hiclaw/scripts/init/upgrade-builtins.sh\n log \"Manager workspace upgraded to version: ${IMAGE_VERSION}\"\nelse\n log \"Workspace up to date (version: ${IMAGE_VERSION})\"\nfi\n\n# Local mode: wait for mc mirror initialization (shared + worker data in /root/hiclaw-fs/)\nif [ \"${HICLAW_RUNTIME}\" != \"aliyun\" ]; then\n log \"Waiting for MinIO storage initialization...\"\n _minio_wait=0\n while [ ! -f /root/hiclaw-fs/.initialized ]; do\n sleep 2\n _minio_wait=$(( _minio_wait + 1 ))\n if [ \"${_minio_wait}\" -ge 60 ]; then\n log \"ERROR: MinIO storage initialization timed out after 120s\"\n exit 1\n fi\n done\n log \"MinIO storage initialized\"\nfi\n\n# ============================================================\n# Register Matrix users via Registration API (single-step, no UIAA)\n# ============================================================\nlog \"Registering human admin Matrix account...\"\ncurl -sf -X POST ${HICLAW_MATRIX_SERVER}/_matrix/client/v3/register \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"username\": \"'\"${HICLAW_ADMIN_USER}\"'\",\n \"password\": \"'\"${HICLAW_ADMIN_PASSWORD}\"'\",\n \"auth\": {\n \"type\": \"m.login.registration_token\",\n \"token\": \"'\"${HICLAW_REGISTRATION_TOKEN}\"'\"\n }\n }' > /dev/null 2>&1 || log \"Admin account may already exist\"\n\nlog \"Registering Manager Agent Matrix account...\"\ncurl -sf -X POST ${HICLAW_MATRIX_SERVER}/_matrix/client/v3/register \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"username\": \"manager\",\n \"password\": \"'\"${HICLAW_MANAGER_PASSWORD}\"'\",\n \"auth\": {\n \"type\": \"m.login.registration_token\",\n \"token\": \"'\"${HICLAW_REGISTRATION_TOKEN}\"'\"\n }\n }' > /dev/null 2>&1 || log \"Manager account may already exist\"\n\n# Get Manager Agent's Matrix access token\nlog \"Obtaining Manager Matrix access token...\"\n_LOGIN_RESPONSE=$(curl -sf -X POST ${HICLAW_MATRIX_SERVER}/_matrix/client/v3/login \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"type\": \"m.login.password\",\n \"identifier\": {\"type\": \"m.id.user\", \"user\": \"manager\"},\n \"password\": \"'\"${HICLAW_MANAGER_PASSWORD}\"'\"\n }' 2>&1)\n_LOGIN_EXIT=$?\nlog \"Matrix login HTTP exit code: ${_LOGIN_EXIT}\"\nlog \"Matrix login response: ${_LOGIN_RESPONSE}\"\n\nMANAGER_TOKEN=$(echo \"${_LOGIN_RESPONSE}\" | jq -r '.access_token' 2>/dev/null)\n\nif [ -z \"${MANAGER_TOKEN}\" ] || [ \"${MANAGER_TOKEN}\" = \"null\" ]; then\n log \"ERROR: Failed to obtain Manager Matrix token (exit=${_LOGIN_EXIT})\"\n log \"ERROR: Login response was: ${_LOGIN_RESPONSE}\"\n exit 1\nfi\nlog \"Manager Matrix token obtained (token prefix: ${MANAGER_TOKEN:0:10}...)\"\n\n# ============================================================\n# Local mode: Initialize Higress Console + configure routes\n# Cloud mode: Create admin DM room + schedule welcome message\n# ============================================================\nif [ \"${HICLAW_RUNTIME}\" != \"aliyun\" ]; then\n COOKIE_FILE=\"/tmp/higress-session-cookie\"\n\n log \"Waiting for Higress Console to be fully ready and initializing admin...\"\n INIT_DONE=false\n for i in $(seq 1 90); do\n INIT_RESULT=$(curl -s -X POST http://127.0.0.1:8001/system/init \\\n -H 'Content-Type: application/json' \\\n -d '{\"adminUser\":{\"name\":\"'\"${HICLAW_ADMIN_USER}\"'\",\"password\":\"'\"${HICLAW_ADMIN_PASSWORD}\"'\",\"displayName\":\"'\"${HICLAW_ADMIN_USER}\"'\"}}' 2>/dev/null) || true\n if echo \"${INIT_RESULT}\" | grep -qE '\"success\":true|already.?init' 2>/dev/null; then\n INIT_DONE=true\n break\n fi\n if echo \"${INIT_RESULT}\" | grep -q '\"name\"' 2>/dev/null; then\n INIT_DONE=true\n break\n fi\n sleep 2\n done\n\n if [ \"${INIT_DONE}\" != \"true\" ]; then\n log \"ERROR: Higress Console did not become ready within 180s\"\n exit 1\n fi\n log \"Higress Console init done\"\n\n log \"Logging into Higress Console...\"\n LOGIN_OK=false\n for i in $(seq 1 10); do\n HTTP_CODE=$(curl -s -o /dev/null -w '%{http_code}' -X POST http://127.0.0.1:8001/session/login \\\n -H 'Content-Type: application/json' \\\n -c \"${COOKIE_FILE}\" \\\n -d '{\"username\":\"'\"${HICLAW_ADMIN_USER}\"'\",\"password\":\"'\"${HICLAW_ADMIN_PASSWORD}\"'\"}' 2>/dev/null) || true\n if { [ \"${HTTP_CODE}\" = \"200\" ] || [ \"${HTTP_CODE}\" = \"201\" ]; } && [ -f \"${COOKIE_FILE}\" ] && [ -s \"${COOKIE_FILE}\" ]; then\n LOGIN_OK=true\n break\n fi\n log \"Login attempt $i (HTTP ${HTTP_CODE}), retrying in 3s...\"\n sleep 3\n done\n\n if [ \"${LOGIN_OK}\" != \"true\" ]; then\n log \"ERROR: Could not login to Higress Console after retries\"\n exit 1\n fi\n log \"Higress Console login successful\"\n\n VERIFY_CODE=$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8001/v1/consumers -b \"${COOKIE_FILE}\" 2>/dev/null) || true\n if [ \"${VERIFY_CODE}\" = \"200\" ]; then\n log \"Console session verified (cookie valid)\"\n else\n log \"WARNING: Console session may be invalid (verify returned HTTP ${VERIFY_CODE})\"\n rm -f \"${COOKIE_FILE}\"\n for i in $(seq 1 5); do\n curl -s -o /dev/null -w '%{http_code}' -X POST http://127.0.0.1:8001/session/login \\\n -H 'Content-Type: application/json' \\\n -c \"${COOKIE_FILE}\" \\\n -d '{\"username\":\"'\"${HICLAW_ADMIN_USER}\"'\",\"password\":\"'\"${HICLAW_ADMIN_PASSWORD}\"'\"}' 2>/dev/null\n VERIFY2=$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8001/v1/consumers -b \"${COOKIE_FILE}\" 2>/dev/null) || true\n if [ \"${VERIFY2}\" = \"200\" ]; then\n log \"Re-login successful, session verified\"\n break\n fi\n sleep 2\n done\n fi\n\n export HIGRESS_COOKIE_FILE=\"${COOKIE_FILE}\"\n\n # Configure Higress routes, consumers, MCP servers\n /opt/hiclaw/scripts/init/setup-higress.sh\nelse\n # Cloud mode: create admin DM room and schedule welcome message\n MANAGER_FULL_ID=\"@manager:${MATRIX_DOMAIN}\"\n ADMIN_FULL_ID=\"@${HICLAW_ADMIN_USER}:${MATRIX_DOMAIN}\"\n\n log \"Logging in as admin to create DM room...\"\n _ADMIN_LOGIN=$(curl -sf -X POST \"${HICLAW_MATRIX_SERVER}/_matrix/client/v3/login\" \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"type\": \"m.login.password\",\n \"identifier\": {\"type\": \"m.id.user\", \"user\": \"'\"${HICLAW_ADMIN_USER}\"'\"},\n \"password\": \"'\"${HICLAW_ADMIN_PASSWORD}\"'\"\n }' 2>&1) || true\n\n ADMIN_MATRIX_TOKEN=$(echo \"${_ADMIN_LOGIN}\" | jq -r '.access_token // empty' 2>/dev/null)\n if [ -z \"${ADMIN_MATRIX_TOKEN}\" ]; then\n log \"WARNING: Failed to login as admin, skipping DM room creation\"\n else\n # Search for existing DM room with Manager (idempotent)\n DM_ROOM_ID=\"\"\n _JOINED_ROOMS=$(curl -sf \"${HICLAW_MATRIX_SERVER}/_matrix/client/v3/joined_rooms\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" 2>/dev/null \\\n | jq -r '.joined_rooms[]' 2>/dev/null) || true\n for _rid in ${_JOINED_ROOMS}; do\n _members=$(curl -sf \"${HICLAW_MATRIX_SERVER}/_matrix/client/v3/rooms/${_rid}/members\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" 2>/dev/null \\\n | jq -r '.chunk[].state_key' 2>/dev/null) || continue\n _count=$(echo \"${_members}\" | wc -l | xargs)\n if [ \"${_count}\" = \"2\" ] && echo \"${_members}\" | grep -q \"@manager:\"; then\n DM_ROOM_ID=\"${_rid}\"\n break\n fi\n done\n\n if [ -n \"${DM_ROOM_ID}\" ]; then\n log \"Existing DM room found: ${DM_ROOM_ID}\"\n else\n log \"Creating DM room with Manager...\"\n _RAW=$(curl -s -w '\\nHTTP_CODE:%{http_code}' -X POST \"${HICLAW_MATRIX_SERVER}/_matrix/client/v3/createRoom\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d \"{\\\"is_direct\\\":true,\\\"invite\\\":[\\\"${MANAGER_FULL_ID}\\\"],\\\"preset\\\":\\\"trusted_private_chat\\\"}\" 2>&1) || true\n _HTTP_CODE=$(echo \"${_RAW}\" | tail -1 | sed 's/HTTP_CODE://')\n _CREATE_RESP=$(echo \"${_RAW}\" | sed '$d')\n DM_ROOM_ID=$(echo \"${_CREATE_RESP}\" | jq -r '.room_id // empty' 2>/dev/null)\n if [ -n \"${DM_ROOM_ID}\" ]; then\n log \"DM room created: ${DM_ROOM_ID}\"\n else\n log \"WARNING: Failed to create DM room (HTTP ${_HTTP_CODE}): ${_CREATE_RESP}\"\n fi\n fi\n\n # Schedule welcome message in background (only on first boot)\n if [ -n \"${DM_ROOM_ID}\" ] && [ ! -f \"/root/manager-workspace/soul-configured\" ]; then\n log \"Scheduling welcome message (background, waiting for OpenClaw to start)...\"\n (\n _HICLAW_LANGUAGE=\"${HICLAW_LANGUAGE:-zh}\"\n _HICLAW_TIMEZONE=\"${TZ:-Asia/Shanghai}\"\n _wait=0\n _ready=false\n while [ \"${_wait}\" -lt 300 ]; do\n if curl -sf http://127.0.0.1:18799/ > /dev/null 2>&1; then\n _ready=true\n break\n fi\n sleep 3\n _wait=$((_wait + 3))\n done\n if [ \"${_ready}\" != \"true\" ]; then\n echo \"[cloud-manager] WARNING: OpenClaw gateway not ready within 300s, skipping welcome message\"\n exit 0\n fi\n _welcome_msg=\"This is an automated message from the HiClaw cloud deployment. This is a fresh installation.\n\n--- Installation Context ---\nUser Language: ${_HICLAW_LANGUAGE} (zh = Chinese, en = English)\nUser Timezone: ${_HICLAW_TIMEZONE} (IANA timezone identifier)\n---\n\nYou are an AI agent that manages a team of worker agents. Your identity and personality have not been configured yet — the human admin is about to meet you for the first time.\n\nPlease begin the onboarding conversation:\n\n1. Greet the admin warmly and briefly describe what you can do (coordinate workers, manage tasks, run multi-agent projects)\n2. The user has selected \\\"${_HICLAW_LANGUAGE}\\\" as their preferred language during installation. Use this language for your greeting and all subsequent communication.\n3. The user's timezone is ${_HICLAW_TIMEZONE}. Based on this timezone, you may infer their likely region and suggest additional language options.\n4. Ask them: a) What would they like to call you? b) Communication style preference? c) Any behavior guidelines? d) Confirm default language\n5. After they reply, write their preferences to ~/SOUL.md\n6. Confirm what you wrote, and ask if they would like to adjust anything\n7. Once confirmed, run: touch ~/soul-configured\n\nThe human admin will start chatting shortly.\"\n _txn_id=\"welcome-cloud-$(date +%s)\"\n _payload=$(jq -nc --arg body \"${_welcome_msg}\" '{\"msgtype\":\"m.text\",\"body\":$body}')\n _raw=$(curl -s -w '\\nHTTP_CODE:%{http_code}' -X PUT \"${HICLAW_MATRIX_SERVER}/_matrix/client/v3/rooms/${DM_ROOM_ID}/send/m.room.message/${_txn_id}\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d \"${_payload}\" 2>&1) || true\n _http_code=$(echo \"${_raw}\" | tail -1 | sed 's/HTTP_CODE://')\n _send_resp=$(echo \"${_raw}\" | sed '$d')\n if echo \"${_send_resp}\" | jq -e '.event_id' > /dev/null 2>&1; then\n echo \"[cloud-manager] Welcome message sent to DM room\"\n else\n echo \"[cloud-manager] WARNING: Failed to send welcome message (HTTP ${_http_code}): ${_send_resp}\"\n fi\n ) &\n log \"Welcome message background process started (PID: $!)\"\n fi\n fi\nfi\n\n# ============================================================\n# Generate Manager Agent openclaw.json from template\n# ============================================================\nlog \"Generating Manager openclaw.json...\"\nexport MANAGER_MATRIX_TOKEN=\"${MANAGER_TOKEN}\"\nexport MANAGER_GATEWAY_KEY=\"${HICLAW_MANAGER_GATEWAY_KEY}\"\n# Resolve model parameters based on model name\nMODEL_NAME=\"${HICLAW_DEFAULT_MODEL:-qwen3.5-plus}\"\ncase \"${MODEL_NAME}\" in\n gpt-5.3-codex|gpt-5-mini|gpt-5-nano)\n export MODEL_CONTEXT_WINDOW=400000 MODEL_MAX_TOKENS=128000 ;;\n claude-opus-4-6)\n export MODEL_CONTEXT_WINDOW=1000000 MODEL_MAX_TOKENS=128000 ;;\n claude-sonnet-4-6)\n export MODEL_CONTEXT_WINDOW=1000000 MODEL_MAX_TOKENS=64000 ;;\n claude-haiku-4-5)\n export MODEL_CONTEXT_WINDOW=200000 MODEL_MAX_TOKENS=64000 ;;\n qwen3.5-plus)\n export MODEL_CONTEXT_WINDOW=200000 MODEL_MAX_TOKENS=64000 ;;\n deepseek-chat|deepseek-reasoner|kimi-k2.5)\n export MODEL_CONTEXT_WINDOW=256000 MODEL_MAX_TOKENS=128000 ;;\n glm-5|MiniMax-M2.7|MiniMax-M2.7-highspeed|MiniMax-M2.5)\n export MODEL_CONTEXT_WINDOW=200000 MODEL_MAX_TOKENS=128000 ;;\n *)\n export MODEL_CONTEXT_WINDOW=150000 MODEL_MAX_TOKENS=128000 ;;\nesac\nexport MODEL_REASONING=true\n\n# Override with user-supplied custom model parameters from env (set during install)\n[ -n \"${HICLAW_MODEL_CONTEXT_WINDOW:-}\" ] && export MODEL_CONTEXT_WINDOW=\"${HICLAW_MODEL_CONTEXT_WINDOW}\"\n[ -n \"${HICLAW_MODEL_MAX_TOKENS:-}\" ] && export MODEL_MAX_TOKENS=\"${HICLAW_MODEL_MAX_TOKENS}\"\n[ -n \"${HICLAW_MODEL_REASONING:-}\" ] && export MODEL_REASONING=\"${HICLAW_MODEL_REASONING}\"\n\n# E2EE: convert HICLAW_MATRIX_E2EE to JSON boolean for template substitution\nif [ \"${HICLAW_MATRIX_E2EE:-0}\" = \"1\" ] || [ \"${HICLAW_MATRIX_E2EE:-}\" = \"true\" ]; then\n export MATRIX_E2EE_ENABLED=true\nelse\n export MATRIX_E2EE_ENABLED=false\nfi\nlog \"Matrix E2EE: ${MATRIX_E2EE_ENABLED}\"\n\n# Resolve input modalities: only vision-capable models get \"image\"\ncase \"${MODEL_NAME}\" in\n gpt-5.4|gpt-5.3-codex|gpt-5-mini|gpt-5-nano|claude-opus-4-6|claude-sonnet-4-6|claude-haiku-4-5|qwen3.5-plus|kimi-k2.5)\n export MODEL_INPUT='[\"text\", \"image\"]' ;;\n *)\n export MODEL_INPUT='[\"text\"]' ;;\nesac\n# Override with user-supplied vision setting from env\nif [ \"${HICLAW_MODEL_VISION:-}\" = \"true\" ]; then\n export MODEL_INPUT='[\"text\", \"image\"]'\nelif [ \"${HICLAW_MODEL_VISION:-}\" = \"false\" ]; then\n export MODEL_INPUT='[\"text\"]'\nfi\n\nlog \"Model: ${MODEL_NAME} (context=${MODEL_CONTEXT_WINDOW}, maxTokens=${MODEL_MAX_TOKENS}, reasoning=${MODEL_REASONING}, input=${MODEL_INPUT})\"\n\nif [ -f /root/manager-workspace/openclaw.json ]; then\n log \"Manager openclaw.json already exists, updating dynamic fields only (preserving user customizations)...\"\n # Merge known models into existing config (add missing, preserve user-added)\n # Use known-models.json (valid JSON) instead of template (contains ${VAR} placeholders)\n KNOWN_MODELS=$(cat /opt/hiclaw/configs/known-models.json 2>/dev/null || echo '[]')\n jq --arg token \"${MANAGER_TOKEN}\" \\\n --arg key \"${HICLAW_MANAGER_GATEWAY_KEY}\" \\\n --arg model \"${MODEL_NAME}\" \\\n --argjson e2ee \"${MATRIX_E2EE_ENABLED}\" \\\n --argjson known_models \"${KNOWN_MODELS}\" \\\n --argjson ctx \"${MODEL_CONTEXT_WINDOW}\" \\\n --argjson max \"${MODEL_MAX_TOKENS}\" \\\n --argjson reasoning \"${MODEL_REASONING}\" \\\n --argjson input \"${MODEL_INPUT}\" \\\n '\n # Merge known models: add any model id not already present\n .models.providers[\"hiclaw-gateway\"].models as $existing\n | ($existing | map(.id)) as $existing_ids\n | ($known_models | map(select(.id as $id | $existing_ids | index($id) | not))) as $new\n | .models.providers[\"hiclaw-gateway\"].models = ($existing + $new)\n # Ensure the user-chosen default model is in the list (custom model support)\n | if (.models.providers[\"hiclaw-gateway\"].models | map(.id) | index($model) | not) then\n .models.providers[\"hiclaw-gateway\"].models += [{\"id\": $model, \"name\": $model, \"reasoning\": $reasoning, \"contextWindow\": $ctx, \"maxTokens\": $max, \"input\": $input}]\n else . end\n # Rebuild model aliases from the full models list\n | (.models.providers[\"hiclaw-gateway\"].models | map({ (\"hiclaw-gateway/\" + .id): { \"alias\": .id } }) | add // {}) as $aliases\n | .agents.defaults.models = ((.agents.defaults.models // {}) + $aliases)\n | .channels.matrix.accessToken = $token | .models.providers[\"hiclaw-gateway\"].apiKey = $key\n | ((.hooks.token // \"\") as $ht | if $ht == $key or $ht == ($key + \"-hooks\" | @base64) then del(.hooks) else . end)\n | .agents.defaults.model.primary = (\"hiclaw-gateway/\" + $model)\n | .commands.restart = true\n | .gateway.controlUi.dangerouslyDisableDeviceAuth = true\n | .channels.matrix.encryption = $e2ee\n ' \\\n /root/manager-workspace/openclaw.json > /tmp/openclaw.json.tmp && \\\n mv /tmp/openclaw.json.tmp /root/manager-workspace/openclaw.json\n # Verify the token was written correctly\n _written_token=$(jq -r '.channels.matrix.accessToken' /root/manager-workspace/openclaw.json 2>/dev/null)\n if [ -z \"${_written_token}\" ] || [ \"${_written_token}\" = \"null\" ]; then\n log \"ERROR: Matrix token was not written correctly to openclaw.json (got: ${_written_token})\"\n else\n log \"Matrix token written to openclaw.json (prefix: ${_written_token:0:10}...)\"\n fi\nelse\n log \"Manager openclaw.json not found, generating from template...\"\n envsubst < /opt/hiclaw/configs/manager-openclaw.json.tmpl > /root/manager-workspace/openclaw.json\n # Inject custom model if not in the built-in list\n if ! jq -e --arg model \"${MODEL_NAME}\" '.models.providers[\"hiclaw-gateway\"].models | map(.id) | index($model)' /root/manager-workspace/openclaw.json > /dev/null 2>&1; then\n log \"Custom model '${MODEL_NAME}' not in built-in list, injecting into config...\"\n jq --arg model \"${MODEL_NAME}\" \\\n --argjson ctx \"${MODEL_CONTEXT_WINDOW}\" \\\n --argjson max \"${MODEL_MAX_TOKENS}\" \\\n --argjson reasoning \"${MODEL_REASONING}\" \\\n --argjson input \"${MODEL_INPUT}\" \\\n '\n .models.providers[\"hiclaw-gateway\"].models += [{\"id\": $model, \"name\": $model, \"reasoning\": $reasoning, \"contextWindow\": $ctx, \"maxTokens\": $max, \"input\": $input}]\n | .agents.defaults.models += {(\"hiclaw-gateway/\" + $model): {\"alias\": $model}}\n ' /root/manager-workspace/openclaw.json > /tmp/openclaw.json.tmp && \\\n mv /tmp/openclaw.json.tmp /root/manager-workspace/openclaw.json\n fi\n _written_token=$(jq -r '.channels.matrix.accessToken' /root/manager-workspace/openclaw.json 2>/dev/null)\n log \"Matrix token written from template (prefix: ${_written_token:0:10}...)\"\nfi\n\n# Cloud mode: overlay cloud-specific settings onto generated config\nif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n log \"Applying cloud overlay to openclaw.json...\"\n jq --arg homeserver \"${HICLAW_MATRIX_SERVER}\" \\\n --arg gateway \"${HICLAW_AI_GATEWAY_URL}/v1\" \\\n --arg key \"${HICLAW_MANAGER_GATEWAY_KEY}\" \\\n '.channels.matrix.homeserver = $homeserver\n | .models.providers[\"hiclaw-gateway\"].baseUrl = $gateway\n | .models.providers[\"hiclaw-gateway\"].apiKey = $key\n | ((.hooks.token // \"\") as $ht | if $ht == $key or $ht == ($key + \"-hooks\" | @base64) then del(.hooks) else . end)\n | .commands.restart = false' \\\n /root/manager-workspace/openclaw.json > /tmp/openclaw-cloud.json && \\\n mv /tmp/openclaw-cloud.json /root/manager-workspace/openclaw.json\n log \"Cloud overlay applied\"\nfi\n\n# ============================================================\n# Detect container runtime (for Worker creation)\n# ============================================================\nsource /opt/hiclaw/scripts/lib/container-api.sh\nif container_api_available; then\n log \"Container runtime socket detected at ${CONTAINER_SOCKET} — direct Worker creation enabled\"\n export HICLAW_CONTAINER_RUNTIME=\"socket\"\nelif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n log \"Cloud mode — Workers created via SAE API\"\n export HICLAW_CONTAINER_RUNTIME=\"cloud\"\nelse\n log \"No container runtime found — Worker creation will output install commands\"\n export HICLAW_CONTAINER_RUNTIME=\"none\"\nfi\n\n# ============================================================\n# Upgrade Worker openclaw.json: merge known models + E2EE flag into existing configs\n# Existing workers in MinIO may have old single-model configs or missing encryption field.\n# Merge template models so they can hot-switch without restart.\n# ============================================================\nREGISTRY_FILE=\"/root/manager-workspace/workers-registry.json\"\nif [ -f \"${REGISTRY_FILE}\" ]; then\n # Use known-models.json (valid JSON) instead of template (contains ${VAR} placeholders)\n KNOWN_MODELS_FILE=\"/opt/hiclaw/configs/known-models.json\"\n if [ -f \"${KNOWN_MODELS_FILE}\" ]; then\n _KNOWN_MODELS=$(cat \"${KNOWN_MODELS_FILE}\")\n for _wname in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_wname}\" ] && continue\n _minio_path=\"${HICLAW_STORAGE_PREFIX}/agents/${_wname}/openclaw.json\"\n _tmp_in=\"/tmp/openclaw-${_wname}-models-upgrade-in.json\"\n if mc cp \"${_minio_path}\" \"${_tmp_in}\" 2>/dev/null; then\n _tmp_out=\"/tmp/openclaw-${_wname}-models-upgrade-out.json\"\n # Idempotent merge: add missing known models, rebuild aliases, set e2ee.\n # Always runs — jq deduplicates by model id, so re-runs are safe.\n jq --argjson known_models \"${_KNOWN_MODELS}\" \\\n --argjson e2ee \"${MATRIX_E2EE_ENABLED}\" '\n .models.providers[\"hiclaw-gateway\"].models as $existing\n | ($existing | map(.id)) as $existing_ids\n | ($known_models | map(select(.id as $id | $existing_ids | index($id) | not))) as $new\n | .models.providers[\"hiclaw-gateway\"].models = ($existing + $new)\n | (.models.providers[\"hiclaw-gateway\"].models | map({ (\"hiclaw-gateway/\" + .id): { \"alias\": .id } }) | add // {}) as $aliases\n | .agents.defaults.models = ((.agents.defaults.models // {}) + $aliases)\n | .channels.matrix.encryption = $e2ee\n ' \"${_tmp_in}\" > \"${_tmp_out}\" 2>/dev/null\n if ! diff -q \"${_tmp_in}\" \"${_tmp_out}\" > /dev/null 2>&1; then\n if mc cp \"${_tmp_out}\" \"${_minio_path}\" 2>/dev/null; then\n _new_count=$(jq '.models.providers[\"hiclaw-gateway\"].models | length' \"${_tmp_out}\" 2>/dev/null)\n log \"Worker ${_wname}: upgraded openclaw.json (models: ${_new_count}, e2ee: ${MATRIX_E2EE_ENABLED})\"\n fi\n fi\n rm -f \"${_tmp_in}\" \"${_tmp_out}\"\n fi\n done\n fi\nfi\n\n# ============================================================\n# Ensure Worker Matrix password files exist in MinIO (E2EE fix)\n# Workers need to re-login on restart to get a fresh device_id.\n# Older workers created before this fix won't have the password file.\n# ============================================================\nif [ -f \"${REGISTRY_FILE}\" ]; then\n for _wname in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_wname}\" ] && continue\n _creds_file=\"/data/worker-creds/${_wname}.env\"\n if [ -f \"${_creds_file}\" ]; then\n # Check if password file already exists in MinIO\n if ! mc stat \"${HICLAW_STORAGE_PREFIX}/agents/${_wname}/credentials/matrix/password\" > /dev/null 2>&1; then\n source \"${_creds_file}\"\n if [ -n \"${WORKER_PASSWORD}\" ]; then\n _tmp_pw=\"/tmp/matrix-pw-${_wname}\"\n echo -n \"${WORKER_PASSWORD}\" > \"${_tmp_pw}\"\n mc cp \"${_tmp_pw}\" \"${HICLAW_STORAGE_PREFIX}/agents/${_wname}/credentials/matrix/password\" 2>/dev/null \\\n && log \"Worker ${_wname}: wrote Matrix password to MinIO (E2EE re-login fix)\" \\\n || log \"Worker ${_wname}: WARNING: failed to write Matrix password to MinIO\"\n rm -f \"${_tmp_pw}\"\n fi\n fi\n fi\n done\nfi\n\n# ============================================================\n# Recreate Worker containers as needed after Manager restart.\n# Manager IP may change on restart; Workers use ExtraHosts pointing to Manager IP,\n# so any worker whose ExtraHosts IP no longer matches must be recreated.\n# ============================================================\nif container_api_available; then\n REGISTRY_FILE=\"/root/manager-workspace/workers-registry.json\"\n _manager_ip=$(container_get_manager_ip)\n if [ -f \"${REGISTRY_FILE}\" ]; then\n for _worker_name in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_worker_name}\" ] && continue\n\n # Skip remote workers — they are not Manager-managed containers.\n _deployment=$(jq -r --arg w \"${_worker_name}\" '.workers[$w].deployment // \"local\"' \"${REGISTRY_FILE}\" 2>/dev/null)\n if [ \"${_deployment}\" = \"remote\" ]; then\n log \"Worker ${_worker_name} is remote, skipping container recreate\"\n continue\n fi\n\n _status=$(container_status_worker \"${_worker_name}\")\n if [ \"${_status}\" = \"running\" ]; then\n # Check if ExtraHosts IP still matches current Manager IP.\n # If ExtraHosts is empty the worker uses real DNS — no recreate needed.\n _inspect=$(_api GET \"/containers/${WORKER_CONTAINER_PREFIX}${_worker_name}/json\" 2>/dev/null)\n _extra_hosts_len=$(echo \"${_inspect}\" | jq -r '.HostConfig.ExtraHosts | length' 2>/dev/null)\n if [ -z \"${_extra_hosts_len}\" ] || [ \"${_extra_hosts_len}\" = \"0\" ]; then\n log \"Worker running (no ExtraHosts, real DNS): ${_worker_name}, skipping\"\n continue\n fi\n _worker_ip=$(echo \"${_inspect}\" | jq -r '.HostConfig.ExtraHosts[0]' 2>/dev/null | cut -d: -f2)\n if [ \"${_worker_ip}\" = \"${_manager_ip}\" ]; then\n log \"Worker running with current Manager IP (${_manager_ip}): ${_worker_name}, skipping\"\n continue\n fi\n log \"Worker running but Manager IP changed (${_worker_ip} -> ${_manager_ip}): ${_worker_name}, recreating...\"\n else\n # Container missing or stopped — always recreate.\n log \"Worker container ${_status}: ${_worker_name}, recreating...\"\n fi\n _creds_file=\"/data/worker-creds/${_worker_name}.env\"\n if [ -f \"${_creds_file}\" ]; then\n source \"${_creds_file}\"\n _runtime=$(jq -r --arg w \"${_worker_name}\" '.workers[$w].runtime // \"openclaw\"' \"${REGISTRY_FILE}\" 2>/dev/null)\n _recreated=false\n for _attempt in 1 2 3; do\n if [ \"${_runtime}\" = \"copaw\" ]; then\n container_create_copaw_worker \"${_worker_name}\" \"${_worker_name}\" \"${WORKER_MINIO_PASSWORD}\" 2>&1 && _recreated=true && break\n else\n container_create_worker \"${_worker_name}\" \"${_worker_name}\" \"${WORKER_MINIO_PASSWORD}\" 2>&1 && _recreated=true && break\n fi\n log \" Attempt ${_attempt}/3 failed for ${_worker_name}, retrying in $((5 * _attempt))s...\"\n sleep $((5 * _attempt))\n done\n if [ \"${_recreated}\" = true ]; then\n log \" Recreated ${_runtime} worker: ${_worker_name}\"\n else\n log \" ERROR: Failed to recreate ${_worker_name} after 3 attempts\"\n fi\n else\n log \" WARNING: No credentials found for ${_worker_name} (${_creds_file} missing), skipping\"\n fi\n done\n fi\nfi\n\n# ============================================================\n# Notify workers of builtin updates if upgrade happened\n# Builtin files (AGENTS.md, skills) are already synced by upgrade-builtins.sh\n#\n# Cooldown: skip notification if the last successful notify was within\n# NOTIFY_COOLDOWN_SECS (default 3600s / 1 hour). This prevents repeated\n# notifications when the Manager crash-loops and re-runs upgrade-builtins\n# on every restart (e.g. IMAGE_VERSION=latest always triggers upgrade).\n# ============================================================\nNOTIFY_COOLDOWN_SECS=\"${HICLAW_NOTIFY_COOLDOWN_SECS:-3600}\"\nNOTIFY_TS_FILE=\"/root/manager-workspace/.last-worker-notify-ts\"\n\nif [ -f /root/manager-workspace/.upgrade-pending-worker-notify ]; then\n _now=$(date +%s)\n _last_notify=$(cat \"${NOTIFY_TS_FILE}\" 2>/dev/null || echo \"0\")\n _elapsed=$(( _now - _last_notify ))\n\n if [ \"${_elapsed}\" -lt \"${NOTIFY_COOLDOWN_SECS}\" ]; then\n log \"Skipping worker builtin notification (last notify ${_elapsed}s ago, cooldown ${NOTIFY_COOLDOWN_SECS}s)\"\n rm -f /root/manager-workspace/.upgrade-pending-worker-notify\n else\n log \"Notifying workers about builtin updates...\"\n REGISTRY_FILE=\"/root/manager-workspace/workers-registry.json\"\n _notify_ok=false\n if [ -f \"${REGISTRY_FILE}\" ]; then\n for _worker_name in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_worker_name}\" ] && continue\n _room_id=$(jq -r --arg w \"${_worker_name}\" '.workers[$w].room_id // empty' \"${REGISTRY_FILE}\" 2>/dev/null)\n if [ -n \"${_room_id}\" ]; then\n _worker_id=\"@${_worker_name}:${MATRIX_DOMAIN}\"\n _txn_id=\"upgrade-$(date +%s%N)\"\n _msg=\"@${_worker_name}:${MATRIX_DOMAIN} Manager upgraded builtin files (AGENTS.md, skills). Please use your file-sync skill to sync the latest config.\"\n _raw=$(curl -s -w '\\nHTTP_CODE:%{http_code}' -X PUT \\\n \"${HICLAW_MATRIX_SERVER}/_matrix/client/v3/rooms/${_room_id}/send/m.room.message/${_txn_id}\" \\\n -H \"Authorization: Bearer ${MANAGER_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d \"{\\\"msgtype\\\":\\\"m.text\\\",\\\"body\\\":\\\"${_msg}\\\",\\\"m.mentions\\\":{\\\"user_ids\\\":[\\\"${_worker_id}\\\"]}}\" \\\n 2>&1) || true\n _http_code=$(echo \"${_raw}\" | tail -1 | sed 's/HTTP_CODE://')\n _notify_resp=$(echo \"${_raw}\" | sed '$d')\n if echo \"${_notify_resp}\" | jq -e '.event_id' > /dev/null 2>&1; then\n log \" Notified ${_worker_name}\"; _notify_ok=true\n else\n log \" WARNING: Failed to notify ${_worker_name} (HTTP ${_http_code}): ${_notify_resp}\"\n fi\n fi\n done\n fi\n # Record timestamp only if at least one notification succeeded\n if [ \"${_notify_ok}\" = true ]; then\n echo \"${_now}\" > \"${NOTIFY_TS_FILE}\"\n fi\n rm -f /root/manager-workspace/.upgrade-pending-worker-notify\n fi\nfi\n\n# ============================================================\n# Start OpenClaw Manager Agent\n# ============================================================\nlog \"Starting Manager Agent (OpenClaw)...\"\n\n# HOME is already set to /root/manager-workspace via docker run -e HOME=...\nexport OPENCLAW_CONFIG_PATH=\"/root/manager-workspace/openclaw.json\"\n\n# Symlink to default OpenClaw config path so CLI commands find the config\nmkdir -p \"${HOME}/.openclaw\"\nln -sf \"/root/manager-workspace/openclaw.json\" \"${HOME}/.openclaw/openclaw.json\"\n\n# Ensure host credential symlinks exist under HOME\nif [ -d \"/host-share\" ]; then\n [ -f \"/host-share/.gitconfig\" ] && ln -sf \"/host-share/.gitconfig\" \"${HOME}/.gitconfig\"\nfi\n\nlog \"HOME=${HOME} (manager-workspace, host-mounted)\"\ncd \"${HOME}\"\n\n# Clean orphaned session write locks (e.g. from SIGKILL or crash before exit handlers)\n# Prevents \"session file locked (timeout 10000ms)\" when PID was reused\nfind \"${HOME}/.openclaw/agents\" -name \"*.jsonl.lock\" -delete 2>/dev/null || true\nlog \"Cleaned up any orphaned session write locks\"\n\n# Clean Matrix crypto storage (SQLite WAL may be corrupted after unclean shutdown)\n# Crypto state is re-negotiated on startup; losing it only means re-establishing E2EE sessions\nrm -rf \"${HOME}/.openclaw/matrix\" 2>/dev/null || true\nlog \"Cleaned Matrix crypto storage (will re-establish E2EE sessions)\"\n\n# ── Render agent doc templates ────────────────────────────────────────────\n# Replace ${VAR} placeholders with actual values so the AI agent reads\n# plain text and never needs to resolve environment variables.\nexport MANAGER_MATRIX_TOKEN MANAGER_TOKEN HIGRESS_COOKIE_FILE\nRENDER=/opt/hiclaw/scripts/lib/render-skills.sh\nlog \"Rendering agent doc templates...\"\n# Manager-owned docs (workspace)\nbash \"$RENDER\" /root/manager-workspace/skills\nbash \"$RENDER\" /root/manager-workspace/skills-alpha\nbash \"$RENDER\" /root/manager-workspace AGENTS.md TOOLS.md HEARTBEAT.md SOUL.md\n# Worker templates (workspace + image) — rendered before push to MinIO\n# so Workers (including remote pip-install) receive plain text\nbash \"$RENDER\" /root/manager-workspace/worker-skills\nbash \"$RENDER\" /root/manager-workspace/worker-agent\nbash \"$RENDER\" /root/manager-workspace/copaw-worker-agent\nbash \"$RENDER\" /opt/hiclaw/agent/worker-skills\nbash \"$RENDER\" /opt/hiclaw/agent/worker-agent\nbash \"$RENDER\" /opt/hiclaw/agent/copaw-worker-agent\nlog \"Agent doc templates rendered\"\n\n# Cloud mode: start background file sync (workspace ↔ OSS) and initial push\nif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n log \"Syncing initial workspace to OSS...\"\n ensure_mc_credentials\n mc mirror /root/manager-workspace/ \"${HICLAW_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" 2>/dev/null || true\n\n # Local → OSS: change-triggered sync\n (\n while true; do\n CHANGED=$(find /root/manager-workspace/ -type f -newermt \"15 seconds ago\" 2>/dev/null | head -1)\n if [ -n \"${CHANGED}\" ]; then\n ensure_mc_credentials 2>/dev/null || true\n mc mirror /root/manager-workspace/ \"${HICLAW_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" --exclude \".npm/**\" \\\n --exclude \".local/**\" --exclude \".mc/**\" 2>/dev/null || true\n fi\n sleep 10\n done\n ) &\n log \"Local→OSS sync started (PID: $!)\"\n\n # OSS → Local: periodic pull (shared data, agent configs)\n (\n while true; do\n sleep 300\n ensure_mc_credentials 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/shared/\" /root/hiclaw-fs/shared/ --overwrite --newer-than \"5m\" 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/agents/\" /root/hiclaw-fs/agents/ --overwrite --newer-than \"5m\" 2>/dev/null || true\n done\n ) &\n log \"OSS→Local sync started (every 5m, PID: $!)\"\nfi\n\n# Launch OpenClaw\nexec openclaw gateway run --verbose --force\n"}, "f058051bcc26a957c68513ecc3e0e55931276ce0": {"t": 1785764777.6827917, "n": 42139, "h": "ba3ccb4c74147e92", "c": "#!/bin/bash\n# start-manager-agent.sh - Initialize and start the Manager Agent\n# Supports both local (supervisord) and cloud (SAE single-process) deployments.\n# In local mode this is the last supervisord component to start (priority 800).\n# In cloud mode (HICLAW_RUNTIME=aliyun) this is the container entrypoint.\n\nsource /opt/hiclaw/scripts/lib/hiclaw-env.sh\n\n# ============================================================\n# Set timezone from TZ env var\n# ============================================================\nif [ -n \"${TZ}\" ] && [ -f \"/usr/share/zoneinfo/${TZ}\" ]; then\n ln -sf \"/usr/share/zoneinfo/${TZ}\" /etc/localtime\n echo \"${TZ}\" > /etc/timezone\n log \"Timezone set to ${TZ}\"\nfi\n\nMATRIX_DOMAIN=\"${HICLAW_MATRIX_DOMAIN:-matrix-local.hiclaw.io:8080}\"\nAI_GATEWAY_DOMAIN=\"${HICLAW_AI_GATEWAY_DOMAIN:-aigw-local.hiclaw.io}\"\n\n# ============================================================\n# Cloud mode: validate required environment variables + initial credentials\n# ============================================================\nif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n : \"${HICLAW_MATRIX_URL:?HICLAW_MATRIX_URL is required}\"\n : \"${HICLAW_MATRIX_DOMAIN:?HICLAW_MATRIX_DOMAIN is required}\"\n : \"${HICLAW_AI_GATEWAY_URL:?HICLAW_AI_GATEWAY_URL is required}\"\n : \"${HICLAW_MANAGER_GATEWAY_KEY:?HICLAW_MANAGER_GATEWAY_KEY is required}\"\n : \"${HICLAW_MANAGER_PASSWORD:?HICLAW_MANAGER_PASSWORD is required (cloud containers are stateless, password must be injected)}\"\n : \"${HICLAW_REGISTRATION_TOKEN:?HICLAW_REGISTRATION_TOKEN is required}\"\n : \"${HICLAW_ADMIN_USER:?HICLAW_ADMIN_USER is required}\"\n : \"${HICLAW_ADMIN_PASSWORD:?HICLAW_ADMIN_PASSWORD is required}\"\n log \"Cloud mode: validating environment... OK\"\n log \" Matrix: ${HICLAW_MATRIX_SERVER}, AI Gateway: ${HICLAW_AI_GATEWAY_URL}, OSS: ${HICLAW_STORAGE_BUCKET}\"\n ensure_mc_credentials || { log \"FATAL: Initial STS credential fetch failed\"; exit 1; }\nfi\n\n# ============================================================\n# Local mode: host symlinks, /etc/hosts, wait for local services\n# ============================================================\nif [ \"${HICLAW_RUNTIME}\" != \"aliyun\" ]; then\n # Create symlink for host directory access\n if [ -d \"/host-share\" ]; then\n ORIGINAL_HOST_HOME=\"${HOST_ORIGINAL_HOME:-$HOME}\"\n if [ ! -e \"${ORIGINAL_HOST_HOME}\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/root\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/data\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/host-share\" ]; then\n mkdir -p \"$(dirname \"${ORIGINAL_HOST_HOME}\")\"\n ln -sfn /host-share \"${ORIGINAL_HOST_HOME}\"\n log \"Created symlink: ${ORIGINAL_HOST_HOME} -> /host-share\"\n else\n ln -sfn /host-share /root/host-home\n log \"Created fallback symlink: /root/host-home -> /host-share\"\n fi\n fi\n\n # Add local domains to /etc/hosts\n HOSTS_DOMAINS=\"${MATRIX_DOMAIN%%:*} ${HICLAW_MATRIX_CLIENT_DOMAIN:-matrix-client-local.hiclaw.io} ${AI_GATEWAY_DOMAIN} ${HICLAW_FS_DOMAIN:-fs-local.hiclaw.io}\"\n if ! grep -q \"${AI_GATEWAY_DOMAIN}\" /etc/hosts 2>/dev/null; then\n echo \"127.0.0.1 ${HOSTS_DOMAINS}\" >> /etc/hosts\n log \"Added local domains to /etc/hosts\"\n fi\n\n # Wait for local infrastructure\n waitForService \"Higress Gateway\" \"127.0.0.1\" 8080 180\n waitForService \"Higress Console\" \"127.0.0.1\" 8001 180\n waitForService \"Tuwunel\" \"127.0.0.1\" 6167 120\n waitForHTTP \"Tuwunel Matrix API\" \"${HICLAW_MATRIX_SERVER}/_tuwunel/server_version\" 120\n waitForService \"MinIO\" \"127.0.0.1\" 9000 120\nelse\n # Cloud mode: wait for external Tuwunel\n log \"Waiting for Tuwunel Matrix server at ${HICLAW_MATRIX_SERVER}...\"\n _retry=0\n while [ \"${_retry}\" -lt 30 ]; do\n if curl -sf \"${HICLAW_MATRIX_SERVER}/_matrix/client/versions\" > /dev/null 2>&1; then\n log \"Tuwunel is ready\"\n break\n fi\n _retry=$((_retry + 1))\n log \" Waiting for Tuwunel (attempt ${_retry}/30)...\"\n sleep 5\n done\n if [ \"${_retry}\" -ge 30 ]; then\n log \"ERROR: Tuwunel not reachable at ${HICLAW_MATRIX_SERVER}\"\n exit 1\n fi\nfi\n\n# ============================================================\n# Auto-generate secrets if not provided via environment\n# Persisted to /data so they survive container restart\n# ============================================================\nSECRETS_FILE=\"/data/hiclaw-secrets.env\"\nif [ -f \"${SECRETS_FILE}\" ]; then\n source \"${SECRETS_FILE}\"\n log \"Loaded persisted secrets from ${SECRETS_FILE}\"\nfi\n\nif [ -z \"${HICLAW_MANAGER_GATEWAY_KEY}\" ]; then\n export HICLAW_MANAGER_GATEWAY_KEY=\"$(generateKey 32)\"\n log \"Auto-generated HICLAW_MANAGER_GATEWAY_KEY\"\nfi\nif [ -z \"${HICLAW_MANAGER_PASSWORD}\" ]; then\n export HICLAW_MANAGER_PASSWORD=\"$(generateKey 16)\"\n log \"Auto-generated HICLAW_MANAGER_PASSWORD\"\nfi\n\n# Persist secrets so they survive supervisord restart\nmkdir -p /data\ncat > \"${SECRETS_FILE}\" <<EOF\nexport HICLAW_MANAGER_GATEWAY_KEY=\"${HICLAW_MANAGER_GATEWAY_KEY}\"\nexport HICLAW_MANAGER_PASSWORD=\"${HICLAW_MANAGER_PASSWORD}\"\nEOF\nchmod 600 \"${SECRETS_FILE}\"\n\n# Cloud mode: pull workspace from OSS before initialization\nif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n HICLAW_FS=\"/root/hiclaw-fs\"\n mkdir -p \"${HICLAW_FS}/shared\" \"${HICLAW_FS}/agents\"\n log \"Pulling workspace from OSS...\"\n ensure_mc_credentials\n mc mirror \"${HICLAW_STORAGE_PREFIX}/manager/\" /root/manager-workspace/ --overwrite 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/shared/\" \"${HICLAW_FS}/shared/\" --overwrite 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/agents/\" \"${HICLAW_FS}/agents/\" --overwrite 2>/dev/null || true\n # Symlink hiclaw-fs into workspace for agent access\n ln -sfn \"${HICLAW_FS}\" /root/manager-workspace/hiclaw-fs\nfi\n\n# ============================================================\n# Initialize / upgrade Manager workspace\n# First boot: full init via upgrade-builtins.sh\n# Subsequent boots: compare image version; upgrade only if changed\n# ============================================================\nmkdir -p /root/manager-workspace\n\nIMAGE_VERSION=$(cat /opt/hiclaw/agent/.builtin-version 2>/dev/null || echo \"unknown\")\nINSTALLED_VERSION=$(cat /root/manager-workspace/.builtin-version 2>/dev/null || echo \"\")\n\nif [ ! -f /root/manager-workspace/.initialized ]; then\n log \"First boot: initializing manager workspace...\"\n bash /opt/hiclaw/scripts/init/upgrade-builtins.sh\n touch /root/manager-workspace/.initialized\n log \"Manager workspace initialized (version: ${IMAGE_VERSION})\"\nelif [ \"${IMAGE_VERSION}\" != \"${INSTALLED_VERSION}\" ] || [ \"${IMAGE_VERSION}\" = \"latest\" ]; then\n log \"Upgrade detected: ${INSTALLED_VERSION} -> ${IMAGE_VERSION}${IMAGE_VERSION:+ (latest: always upgrade)}\"\n bash /opt/hiclaw/scripts/init/upgrade-builtins.sh\n log \"Manager workspace upgraded to version: ${IMAGE_VERSION}\"\nelse\n log \"Workspace up to date (version: ${IMAGE_VERSION})\"\nfi\n\n# Local mode: wait for mc mirror initialization (shared + worker data in /root/hiclaw-fs/)\nif [ \"${HICLAW_RUNTIME}\" != \"aliyun\" ]; then\n log \"Waiting for MinIO storage initialization...\"\n _minio_wait=0\n while [ ! -f /root/hiclaw-fs/.initialized ]; do\n sleep 2\n _minio_wait=$(( _minio_wait + 1 ))\n if [ \"${_minio_wait}\" -ge 60 ]; then\n log \"ERROR: MinIO storage initialization timed out after 120s\"\n exit 1\n fi\n done\n log \"MinIO storage initialized\"\nfi\n\n# ============================================================\n# Register Matrix users via Registration API (single-step, no UIAA)\n# ============================================================\nlog \"Registering human admin Matrix account...\"\ncurl -sf -X POST ${HICLAW_MATRIX_SERVER}/_matrix/client/v3/register \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"username\": \"'\"${HICLAW_ADMIN_USER}\"'\",\n \"password\": \"'\"${HICLAW_ADMIN_PASSWORD}\"'\",\n \"auth\": {\n \"type\": \"m.login.registration_token\",\n \"token\": \"'\"${HICLAW_REGISTRATION_TOKEN}\"'\"\n }\n }' > /dev/null 2>&1 || log \"Admin account may already exist\"\n\nlog \"Registering Manager Agent Matrix account...\"\ncurl -sf -X POST ${HICLAW_MATRIX_SERVER}/_matrix/client/v3/register \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"username\": \"manager\",\n \"password\": \"'\"${HICLAW_MANAGER_PASSWORD}\"'\",\n \"auth\": {\n \"type\": \"m.login.registration_token\",\n \"token\": \"'\"${HICLAW_REGISTRATION_TOKEN}\"'\"\n }\n }' > /dev/null 2>&1 || log \"Manager account may already exist\"\n\n# Get Manager Agent's Matrix access token\nlog \"Obtaining Manager Matrix access token...\"\n_LOGIN_RESPONSE=$(curl -sf -X POST ${HICLAW_MATRIX_SERVER}/_matrix/client/v3/login \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"type\": \"m.login.password\",\n \"identifier\": {\"type\": \"m.id.user\", \"user\": \"manager\"},\n \"password\": \"'\"${HICLAW_MANAGER_PASSWORD}\"'\"\n }' 2>&1)\n_LOGIN_EXIT=$?\nlog \"Matrix login HTTP exit code: ${_LOGIN_EXIT}\"\nlog \"Matrix login response: ${_LOGIN_RESPONSE}\"\n\nMANAGER_TOKEN=$(echo \"${_LOGIN_RESPONSE}\" | jq -r '.access_token' 2>/dev/null)\n\nif [ -z \"${MANAGER_TOKEN}\" ] || [ \"${MANAGER_TOKEN}\" = \"null\" ]; then\n log \"ERROR: Failed to obtain Manager Matrix token (exit=${_LOGIN_EXIT})\"\n log \"ERROR: Login response was: ${_LOGIN_RESPONSE}\"\n exit 1\nfi\nlog \"Manager Matrix token obtained (token prefix: ${MANAGER_TOKEN:0:10}...)\"\n\n# ============================================================\n# Local mode: Initialize Higress Console + configure routes\n# Cloud mode: Create admin DM room + schedule welcome message\n# ============================================================\nif [ \"${HICLAW_RUNTIME}\" != \"aliyun\" ]; then\n COOKIE_FILE=\"/tmp/higress-session-cookie\"\n\n log \"Waiting for Higress Console to be fully ready and initializing admin...\"\n INIT_DONE=false\n for i in $(seq 1 90); do\n INIT_RESULT=$(curl -s -X POST http://127.0.0.1:8001/system/init \\\n -H 'Content-Type: application/json' \\\n -d '{\"adminUser\":{\"name\":\"'\"${HICLAW_ADMIN_USER}\"'\",\"password\":\"'\"${HICLAW_ADMIN_PASSWORD}\"'\",\"displayName\":\"'\"${HICLAW_ADMIN_USER}\"'\"}}' 2>/dev/null) || true\n if echo \"${INIT_RESULT}\" | grep -qE '\"success\":true|already.?init' 2>/dev/null; then\n INIT_DONE=true\n break\n fi\n if echo \"${INIT_RESULT}\" | grep -q '\"name\"' 2>/dev/null; then\n INIT_DONE=true\n break\n fi\n sleep 2\n done\n\n if [ \"${INIT_DONE}\" != \"true\" ]; then\n log \"ERROR: Higress Console did not become ready within 180s\"\n exit 1\n fi\n log \"Higress Console init done\"\n\n log \"Logging into Higress Console...\"\n LOGIN_OK=false\n for i in $(seq 1 10); do\n HTTP_CODE=$(curl -s -o /dev/null -w '%{http_code}' -X POST http://127.0.0.1:8001/session/login \\\n -H 'Content-Type: application/json' \\\n -c \"${COOKIE_FILE}\" \\\n -d '{\"username\":\"'\"${HICLAW_ADMIN_USER}\"'\",\"password\":\"'\"${HICLAW_ADMIN_PASSWORD}\"'\"}' 2>/dev/null) || true\n if { [ \"${HTTP_CODE}\" = \"200\" ] || [ \"${HTTP_CODE}\" = \"201\" ]; } && [ -f \"${COOKIE_FILE}\" ] && [ -s \"${COOKIE_FILE}\" ]; then\n LOGIN_OK=true\n break\n fi\n log \"Login attempt $i (HTTP ${HTTP_CODE}), retrying in 3s...\"\n sleep 3\n done\n\n if [ \"${LOGIN_OK}\" != \"true\" ]; then\n log \"ERROR: Could not login to Higress Console after retries\"\n exit 1\n fi\n log \"Higress Console login successful\"\n\n VERIFY_CODE=$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8001/v1/consumers -b \"${COOKIE_FILE}\" 2>/dev/null) || true\n if [ \"${VERIFY_CODE}\" = \"200\" ]; then\n log \"Console session verified (cookie valid)\"\n else\n log \"WARNING: Console session may be invalid (verify returned HTTP ${VERIFY_CODE})\"\n rm -f \"${COOKIE_FILE}\"\n for i in $(seq 1 5); do\n curl -s -o /dev/null -w '%{http_code}' -X POST http://127.0.0.1:8001/session/login \\\n -H 'Content-Type: application/json' \\\n -c \"${COOKIE_FILE}\" \\\n -d '{\"username\":\"'\"${HICLAW_ADMIN_USER}\"'\",\"password\":\"'\"${HICLAW_ADMIN_PASSWORD}\"'\"}' 2>/dev/null\n VERIFY2=$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8001/v1/consumers -b \"${COOKIE_FILE}\" 2>/dev/null) || true\n if [ \"${VERIFY2}\" = \"200\" ]; then\n log \"Re-login successful, session verified\"\n break\n fi\n sleep 2\n done\n fi\n\n export HIGRESS_COOKIE_FILE=\"${COOKIE_FILE}\"\n\n # Configure Higress routes, consumers, MCP servers\n /opt/hiclaw/scripts/init/setup-higress.sh\nelse\n # Cloud mode: create admin DM room and schedule welcome message\n MANAGER_FULL_ID=\"@manager:${MATRIX_DOMAIN}\"\n ADMIN_FULL_ID=\"@${HICLAW_ADMIN_USER}:${MATRIX_DOMAIN}\"\n\n log \"Logging in as admin to create DM room...\"\n _ADMIN_LOGIN=$(curl -sf -X POST \"${HICLAW_MATRIX_SERVER}/_matrix/client/v3/login\" \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"type\": \"m.login.password\",\n \"identifier\": {\"type\": \"m.id.user\", \"user\": \"'\"${HICLAW_ADMIN_USER}\"'\"},\n \"password\": \"'\"${HICLAW_ADMIN_PASSWORD}\"'\"\n }' 2>&1) || true\n\n ADMIN_MATRIX_TOKEN=$(echo \"${_ADMIN_LOGIN}\" | jq -r '.access_token // empty' 2>/dev/null)\n if [ -z \"${ADMIN_MATRIX_TOKEN}\" ]; then\n log \"WARNING: Failed to login as admin, skipping DM room creation\"\n else\n # Search for existing DM room with Manager (idempotent)\n DM_ROOM_ID=\"\"\n _JOINED_ROOMS=$(curl -sf \"${HICLAW_MATRIX_SERVER}/_matrix/client/v3/joined_rooms\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" 2>/dev/null \\\n | jq -r '.joined_rooms[]' 2>/dev/null) || true\n for _rid in ${_JOINED_ROOMS}; do\n _members=$(curl -sf \"${HICLAW_MATRIX_SERVER}/_matrix/client/v3/rooms/${_rid}/members\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" 2>/dev/null \\\n | jq -r '.chunk[].state_key' 2>/dev/null) || continue\n _count=$(echo \"${_members}\" | wc -l | xargs)\n if [ \"${_count}\" = \"2\" ] && echo \"${_members}\" | grep -q \"@manager:\"; then\n DM_ROOM_ID=\"${_rid}\"\n break\n fi\n done\n\n if [ -n \"${DM_ROOM_ID}\" ]; then\n log \"Existing DM room found: ${DM_ROOM_ID}\"\n else\n log \"Creating DM room with Manager...\"\n _RAW=$(curl -s -w '\\nHTTP_CODE:%{http_code}' -X POST \"${HICLAW_MATRIX_SERVER}/_matrix/client/v3/createRoom\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d \"{\\\"is_direct\\\":true,\\\"invite\\\":[\\\"${MANAGER_FULL_ID}\\\"],\\\"preset\\\":\\\"trusted_private_chat\\\"}\" 2>&1) || true\n _HTTP_CODE=$(echo \"${_RAW}\" | tail -1 | sed 's/HTTP_CODE://')\n _CREATE_RESP=$(echo \"${_RAW}\" | sed '$d')\n DM_ROOM_ID=$(echo \"${_CREATE_RESP}\" | jq -r '.room_id // empty' 2>/dev/null)\n if [ -n \"${DM_ROOM_ID}\" ]; then\n log \"DM room created: ${DM_ROOM_ID}\"\n else\n log \"WARNING: Failed to create DM room (HTTP ${_HTTP_CODE}): ${_CREATE_RESP}\"\n fi\n fi\n\n # Schedule welcome message in background (only on first boot)\n if [ -n \"${DM_ROOM_ID}\" ] && [ ! -f \"/root/manager-workspace/soul-configured\" ]; then\n log \"Scheduling welcome message (background, waiting for OpenClaw to start)...\"\n (\n _HICLAW_LANGUAGE=\"${HICLAW_LANGUAGE:-zh}\"\n _HICLAW_TIMEZONE=\"${TZ:-Asia/Shanghai}\"\n _wait=0\n _ready=false\n while [ \"${_wait}\" -lt 300 ]; do\n if curl -sf http://127.0.0.1:18799/ > /dev/null 2>&1; then\n _ready=true\n break\n fi\n sleep 3\n _wait=$((_wait + 3))\n done\n if [ \"${_ready}\" != \"true\" ]; then\n echo \"[cloud-manager] WARNING: OpenClaw gateway not ready within 300s, skipping welcome message\"\n exit 0\n fi\n _welcome_msg=\"This is an automated message from the HiClaw cloud deployment. This is a fresh installation.\n\n--- Installation Context ---\nUser Language: ${_HICLAW_LANGUAGE} (zh = Chinese, en = English)\nUser Timezone: ${_HICLAW_TIMEZONE} (IANA timezone identifier)\n---\n\nYou are an AI agent that manages a team of worker agents. Your identity and personality have not been configured yet — the human admin is about to meet you for the first time.\n\nPlease begin the onboarding conversation:\n\n1. Greet the admin warmly and briefly describe what you can do (coordinate workers, manage tasks, run multi-agent projects)\n2. The user has selected \\\"${_HICLAW_LANGUAGE}\\\" as their preferred language during installation. Use this language for your greeting and all subsequent communication.\n3. The user's timezone is ${_HICLAW_TIMEZONE}. Based on this timezone, you may infer their likely region and suggest additional language options.\n4. Ask them: a) What would they like to call you? b) Communication style preference? c) Any behavior guidelines? d) Confirm default language\n5. After they reply, write their preferences to ~/SOUL.md\n6. Confirm what you wrote, and ask if they would like to adjust anything\n7. Once confirmed, run: touch ~/soul-configured\n\nThe human admin will start chatting shortly.\"\n _txn_id=\"welcome-cloud-$(date +%s)\"\n _payload=$(jq -nc --arg body \"${_welcome_msg}\" '{\"msgtype\":\"m.text\",\"body\":$body}')\n _raw=$(curl -s -w '\\nHTTP_CODE:%{http_code}' -X PUT \"${HICLAW_MATRIX_SERVER}/_matrix/client/v3/rooms/${DM_ROOM_ID}/send/m.room.message/${_txn_id}\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d \"${_payload}\" 2>&1) || true\n _http_code=$(echo \"${_raw}\" | tail -1 | sed 's/HTTP_CODE://')\n _send_resp=$(echo \"${_raw}\" | sed '$d')\n if echo \"${_send_resp}\" | jq -e '.event_id' > /dev/null 2>&1; then\n echo \"[cloud-manager] Welcome message sent to DM room\"\n else\n echo \"[cloud-manager] WARNING: Failed to send welcome message (HTTP ${_http_code}): ${_send_resp}\"\n fi\n ) &\n log \"Welcome message background process started (PID: $!)\"\n fi\n fi\nfi\n\n# ============================================================\n# Generate Manager Agent openclaw.json from template\n# ============================================================\nlog \"Generating Manager openclaw.json...\"\nexport MANAGER_MATRIX_TOKEN=\"${MANAGER_TOKEN}\"\nexport MANAGER_GATEWAY_KEY=\"${HICLAW_MANAGER_GATEWAY_KEY}\"\nexport MANAGER_HOOKS_TOKEN=$(echo -n \"${HICLAW_MANAGER_GATEWAY_KEY}-hooks\" | base64 -w 0)\n\n# Resolve model parameters based on model name\nMODEL_NAME=\"${HICLAW_DEFAULT_MODEL:-qwen3.5-plus}\"\ncase \"${MODEL_NAME}\" in\n gpt-5.3-codex|gpt-5-mini|gpt-5-nano)\n export MODEL_CONTEXT_WINDOW=400000 MODEL_MAX_TOKENS=128000 ;;\n claude-opus-4-6)\n export MODEL_CONTEXT_WINDOW=1000000 MODEL_MAX_TOKENS=128000 ;;\n claude-sonnet-4-6)\n export MODEL_CONTEXT_WINDOW=1000000 MODEL_MAX_TOKENS=64000 ;;\n claude-haiku-4-5)\n export MODEL_CONTEXT_WINDOW=200000 MODEL_MAX_TOKENS=64000 ;;\n qwen3.5-plus)\n export MODEL_CONTEXT_WINDOW=200000 MODEL_MAX_TOKENS=64000 ;;\n deepseek-chat|deepseek-reasoner|kimi-k2.5)\n export MODEL_CONTEXT_WINDOW=256000 MODEL_MAX_TOKENS=128000 ;;\n glm-5|MiniMax-M2.7|MiniMax-M2.7-highspeed|MiniMax-M2.5)\n export MODEL_CONTEXT_WINDOW=200000 MODEL_MAX_TOKENS=128000 ;;\n *)\n export MODEL_CONTEXT_WINDOW=150000 MODEL_MAX_TOKENS=128000 ;;\nesac\nexport MODEL_REASONING=true\n\n# Override with user-supplied custom model parameters from env (set during install)\n[ -n \"${HICLAW_MODEL_CONTEXT_WINDOW:-}\" ] && export MODEL_CONTEXT_WINDOW=\"${HICLAW_MODEL_CONTEXT_WINDOW}\"\n[ -n \"${HICLAW_MODEL_MAX_TOKENS:-}\" ] && export MODEL_MAX_TOKENS=\"${HICLAW_MODEL_MAX_TOKENS}\"\n[ -n \"${HICLAW_MODEL_REASONING:-}\" ] && export MODEL_REASONING=\"${HICLAW_MODEL_REASONING}\"\n\n# E2EE: convert HICLAW_MATRIX_E2EE to JSON boolean for template substitution\nif [ \"${HICLAW_MATRIX_E2EE:-0}\" = \"1\" ] || [ \"${HICLAW_MATRIX_E2EE:-}\" = \"true\" ]; then\n export MATRIX_E2EE_ENABLED=true\nelse\n export MATRIX_E2EE_ENABLED=false\nfi\nlog \"Matrix E2EE: ${MATRIX_E2EE_ENABLED}\"\n\n# Resolve input modalities: only vision-capable models get \"image\"\ncase \"${MODEL_NAME}\" in\n gpt-5.4|gpt-5.3-codex|gpt-5-mini|gpt-5-nano|claude-opus-4-6|claude-sonnet-4-6|claude-haiku-4-5|qwen3.5-plus|kimi-k2.5)\n export MODEL_INPUT='[\"text\", \"image\"]' ;;\n *)\n export MODEL_INPUT='[\"text\"]' ;;\nesac\n# Override with user-supplied vision setting from env\nif [ \"${HICLAW_MODEL_VISION:-}\" = \"true\" ]; then\n export MODEL_INPUT='[\"text\", \"image\"]'\nelif [ \"${HICLAW_MODEL_VISION:-}\" = \"false\" ]; then\n export MODEL_INPUT='[\"text\"]'\nfi\n\nlog \"Model: ${MODEL_NAME} (context=${MODEL_CONTEXT_WINDOW}, maxTokens=${MODEL_MAX_TOKENS}, reasoning=${MODEL_REASONING}, input=${MODEL_INPUT})\"\n\nif [ -f /root/manager-workspace/openclaw.json ]; then\n log \"Manager openclaw.json already exists, updating dynamic fields only (preserving user customizations)...\"\n # Merge known models into existing config (add missing, preserve user-added)\n # Use known-models.json (valid JSON) instead of template (contains ${VAR} placeholders)\n KNOWN_MODELS=$(cat /opt/hiclaw/configs/known-models.json 2>/dev/null || echo '[]')\n jq --arg token \"${MANAGER_TOKEN}\" \\\n --arg key \"${HICLAW_MANAGER_GATEWAY_KEY}\" \\\n --arg model \"${MODEL_NAME}\" \\\n --argjson e2ee \"${MATRIX_E2EE_ENABLED}\" \\\n --argjson known_models \"${KNOWN_MODELS}\" \\\n --argjson ctx \"${MODEL_CONTEXT_WINDOW}\" \\\n --argjson max \"${MODEL_MAX_TOKENS}\" \\\n --argjson reasoning \"${MODEL_REASONING}\" \\\n --argjson input \"${MODEL_INPUT}\" \\\n '\n # Merge known models: add any model id not already present\n .models.providers[\"hiclaw-gateway\"].models as $existing\n | ($existing | map(.id)) as $existing_ids\n | ($known_models | map(select(.id as $id | $existing_ids | index($id) | not))) as $new\n | .models.providers[\"hiclaw-gateway\"].models = ($existing + $new)\n # Ensure the user-chosen default model is in the list (custom model support)\n | if (.models.providers[\"hiclaw-gateway\"].models | map(.id) | index($model) | not) then\n .models.providers[\"hiclaw-gateway\"].models += [{\"id\": $model, \"name\": $model, \"reasoning\": $reasoning, \"contextWindow\": $ctx, \"maxTokens\": $max, \"input\": $input}]\n else . end\n # Rebuild model aliases from the full models list\n | (.models.providers[\"hiclaw-gateway\"].models | map({ (\"hiclaw-gateway/\" + .id): { \"alias\": .id } }) | add // {}) as $aliases\n | .agents.defaults.models = ((.agents.defaults.models // {}) + $aliases)\n | .channels.matrix.accessToken = $token | .hooks.token = ($key + \"-hooks\" | @base64) | .models.providers[\"hiclaw-gateway\"].apiKey = $key\n | .agents.defaults.model.primary = (\"hiclaw-gateway/\" + $model)\n | .commands.restart = true\n | .gateway.controlUi.dangerouslyDisableDeviceAuth = true\n | .channels.matrix.encryption = $e2ee\n ' \\\n /root/manager-workspace/openclaw.json > /tmp/openclaw.json.tmp && \\\n mv /tmp/openclaw.json.tmp /root/manager-workspace/openclaw.json\n # Verify the token was written correctly\n _written_token=$(jq -r '.channels.matrix.accessToken' /root/manager-workspace/openclaw.json 2>/dev/null)\n if [ -z \"${_written_token}\" ] || [ \"${_written_token}\" = \"null\" ]; then\n log \"ERROR: Matrix token was not written correctly to openclaw.json (got: ${_written_token})\"\n else\n log \"Matrix token written to openclaw.json (prefix: ${_written_token:0:10}...)\"\n fi\nelse\n log \"Manager openclaw.json not found, generating from template...\"\n envsubst < /opt/hiclaw/configs/manager-openclaw.json.tmpl > /root/manager-workspace/openclaw.json\n # Inject custom model if not in the built-in list\n if ! jq -e --arg model \"${MODEL_NAME}\" '.models.providers[\"hiclaw-gateway\"].models | map(.id) | index($model)' /root/manager-workspace/openclaw.json > /dev/null 2>&1; then\n log \"Custom model '${MODEL_NAME}' not in built-in list, injecting into config...\"\n jq --arg model \"${MODEL_NAME}\" \\\n --argjson ctx \"${MODEL_CONTEXT_WINDOW}\" \\\n --argjson max \"${MODEL_MAX_TOKENS}\" \\\n --argjson reasoning \"${MODEL_REASONING}\" \\\n --argjson input \"${MODEL_INPUT}\" \\\n '\n .models.providers[\"hiclaw-gateway\"].models += [{\"id\": $model, \"name\": $model, \"reasoning\": $reasoning, \"contextWindow\": $ctx, \"maxTokens\": $max, \"input\": $input}]\n | .agents.defaults.models += {(\"hiclaw-gateway/\" + $model): {\"alias\": $model}}\n ' /root/manager-workspace/openclaw.json > /tmp/openclaw.json.tmp && \\\n mv /tmp/openclaw.json.tmp /root/manager-workspace/openclaw.json\n fi\n _written_token=$(jq -r '.channels.matrix.accessToken' /root/manager-workspace/openclaw.json 2>/dev/null)\n log \"Matrix token written from template (prefix: ${_written_token:0:10}...)\"\nfi\n\n# Cloud mode: overlay cloud-specific settings onto generated config\nif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n log \"Applying cloud overlay to openclaw.json...\"\n jq --arg homeserver \"${HICLAW_MATRIX_SERVER}\" \\\n --arg gateway \"${HICLAW_AI_GATEWAY_URL}/v1\" \\\n --arg key \"${HICLAW_MANAGER_GATEWAY_KEY}\" \\\n '.channels.matrix.homeserver = $homeserver\n | .models.providers[\"hiclaw-gateway\"].baseUrl = $gateway\n | .models.providers[\"hiclaw-gateway\"].apiKey = $key\n | .hooks.token = ($key + \"-hooks\" | @base64)\n | .commands.restart = false' \\\n /root/manager-workspace/openclaw.json > /tmp/openclaw-cloud.json && \\\n mv /tmp/openclaw-cloud.json /root/manager-workspace/openclaw.json\n log \"Cloud overlay applied\"\nfi\n\n# ============================================================\n# Detect container runtime (for Worker creation)\n# ============================================================\nsource /opt/hiclaw/scripts/lib/container-api.sh\nif container_api_available; then\n log \"Container runtime socket detected at ${CONTAINER_SOCKET} — direct Worker creation enabled\"\n export HICLAW_CONTAINER_RUNTIME=\"socket\"\nelif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n log \"Cloud mode — Workers created via SAE API\"\n export HICLAW_CONTAINER_RUNTIME=\"cloud\"\nelse\n log \"No container runtime found — Worker creation will output install commands\"\n export HICLAW_CONTAINER_RUNTIME=\"none\"\nfi\n\n# ============================================================\n# Upgrade Worker openclaw.json: merge known models + E2EE flag into existing configs\n# Existing workers in MinIO may have old single-model configs or missing encryption field.\n# Merge template models so they can hot-switch without restart.\n# ============================================================\nREGISTRY_FILE=\"/root/manager-workspace/workers-registry.json\"\nif [ -f \"${REGISTRY_FILE}\" ]; then\n # Use known-models.json (valid JSON) instead of template (contains ${VAR} placeholders)\n KNOWN_MODELS_FILE=\"/opt/hiclaw/configs/known-models.json\"\n if [ -f \"${KNOWN_MODELS_FILE}\" ]; then\n _KNOWN_MODELS=$(cat \"${KNOWN_MODELS_FILE}\")\n for _wname in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_wname}\" ] && continue\n _minio_path=\"${HICLAW_STORAGE_PREFIX}/agents/${_wname}/openclaw.json\"\n _tmp_in=\"/tmp/openclaw-${_wname}-models-upgrade-in.json\"\n if mc cp \"${_minio_path}\" \"${_tmp_in}\" 2>/dev/null; then\n _tmp_out=\"/tmp/openclaw-${_wname}-models-upgrade-out.json\"\n # Idempotent merge: add missing known models, rebuild aliases, set e2ee.\n # Always runs — jq deduplicates by model id, so re-runs are safe.\n jq --argjson known_models \"${_KNOWN_MODELS}\" \\\n --argjson e2ee \"${MATRIX_E2EE_ENABLED}\" '\n .models.providers[\"hiclaw-gateway\"].models as $existing\n | ($existing | map(.id)) as $existing_ids\n | ($known_models | map(select(.id as $id | $existing_ids | index($id) | not))) as $new\n | .models.providers[\"hiclaw-gateway\"].models = ($existing + $new)\n | (.models.providers[\"hiclaw-gateway\"].models | map({ (\"hiclaw-gateway/\" + .id): { \"alias\": .id } }) | add // {}) as $aliases\n | .agents.defaults.models = ((.agents.defaults.models // {}) + $aliases)\n | .channels.matrix.encryption = $e2ee\n ' \"${_tmp_in}\" > \"${_tmp_out}\" 2>/dev/null\n if ! diff -q \"${_tmp_in}\" \"${_tmp_out}\" > /dev/null 2>&1; then\n if mc cp \"${_tmp_out}\" \"${_minio_path}\" 2>/dev/null; then\n _new_count=$(jq '.models.providers[\"hiclaw-gateway\"].models | length' \"${_tmp_out}\" 2>/dev/null)\n log \"Worker ${_wname}: upgraded openclaw.json (models: ${_new_count}, e2ee: ${MATRIX_E2EE_ENABLED})\"\n fi\n fi\n rm -f \"${_tmp_in}\" \"${_tmp_out}\"\n fi\n done\n fi\nfi\n\n# ============================================================\n# Ensure Worker Matrix password files exist in MinIO (E2EE fix)\n# Workers need to re-login on restart to get a fresh device_id.\n# Older workers created before this fix won't have the password file.\n# ============================================================\nif [ -f \"${REGISTRY_FILE}\" ]; then\n for _wname in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_wname}\" ] && continue\n _creds_file=\"/data/worker-creds/${_wname}.env\"\n if [ -f \"${_creds_file}\" ]; then\n # Check if password file already exists in MinIO\n if ! mc stat \"${HICLAW_STORAGE_PREFIX}/agents/${_wname}/credentials/matrix/password\" > /dev/null 2>&1; then\n source \"${_creds_file}\"\n if [ -n \"${WORKER_PASSWORD}\" ]; then\n _tmp_pw=\"/tmp/matrix-pw-${_wname}\"\n echo -n \"${WORKER_PASSWORD}\" > \"${_tmp_pw}\"\n mc cp \"${_tmp_pw}\" \"${HICLAW_STORAGE_PREFIX}/agents/${_wname}/credentials/matrix/password\" 2>/dev/null \\\n && log \"Worker ${_wname}: wrote Matrix password to MinIO (E2EE re-login fix)\" \\\n || log \"Worker ${_wname}: WARNING: failed to write Matrix password to MinIO\"\n rm -f \"${_tmp_pw}\"\n fi\n fi\n fi\n done\nfi\n\n# ============================================================\n# Recreate Worker containers as needed after Manager restart.\n# Manager IP may change on restart; Workers use ExtraHosts pointing to Manager IP,\n# so any worker whose ExtraHosts IP no longer matches must be recreated.\n# ============================================================\nif container_api_available; then\n REGISTRY_FILE=\"/root/manager-workspace/workers-registry.json\"\n _manager_ip=$(container_get_manager_ip)\n if [ -f \"${REGISTRY_FILE}\" ]; then\n for _worker_name in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_worker_name}\" ] && continue\n\n # Skip remote workers — they are not Manager-managed containers.\n _deployment=$(jq -r --arg w \"${_worker_name}\" '.workers[$w].deployment // \"local\"' \"${REGISTRY_FILE}\" 2>/dev/null)\n if [ \"${_deployment}\" = \"remote\" ]; then\n log \"Worker ${_worker_name} is remote, skipping container recreate\"\n continue\n fi\n\n _status=$(container_status_worker \"${_worker_name}\")\n if [ \"${_status}\" = \"running\" ]; then\n # Check if ExtraHosts IP still matches current Manager IP.\n # If ExtraHosts is empty the worker uses real DNS — no recreate needed.\n _inspect=$(_api GET \"/containers/${WORKER_CONTAINER_PREFIX}${_worker_name}/json\" 2>/dev/null)\n _extra_hosts_len=$(echo \"${_inspect}\" | jq -r '.HostConfig.ExtraHosts | length' 2>/dev/null)\n if [ -z \"${_extra_hosts_len}\" ] || [ \"${_extra_hosts_len}\" = \"0\" ]; then\n log \"Worker running (no ExtraHosts, real DNS): ${_worker_name}, skipping\"\n continue\n fi\n _worker_ip=$(echo \"${_inspect}\" | jq -r '.HostConfig.ExtraHosts[0]' 2>/dev/null | cut -d: -f2)\n if [ \"${_worker_ip}\" = \"${_manager_ip}\" ]; then\n log \"Worker running with current Manager IP (${_manager_ip}): ${_worker_name}, skipping\"\n continue\n fi\n log \"Worker running but Manager IP changed (${_worker_ip} -> ${_manager_ip}): ${_worker_name}, recreating...\"\n else\n # Container missing or stopped — always recreate.\n log \"Worker container ${_status}: ${_worker_name}, recreating...\"\n fi\n _creds_file=\"/data/worker-creds/${_worker_name}.env\"\n if [ -f \"${_creds_file}\" ]; then\n source \"${_creds_file}\"\n _runtime=$(jq -r --arg w \"${_worker_name}\" '.workers[$w].runtime // \"openclaw\"' \"${REGISTRY_FILE}\" 2>/dev/null)\n _recreated=false\n for _attempt in 1 2 3; do\n if [ \"${_runtime}\" = \"copaw\" ]; then\n container_create_copaw_worker \"${_worker_name}\" \"${_worker_name}\" \"${WORKER_MINIO_PASSWORD}\" 2>&1 && _recreated=true && break\n else\n container_create_worker \"${_worker_name}\" \"${_worker_name}\" \"${WORKER_MINIO_PASSWORD}\" 2>&1 && _recreated=true && break\n fi\n log \" Attempt ${_attempt}/3 failed for ${_worker_name}, retrying in $((5 * _attempt))s...\"\n sleep $((5 * _attempt))\n done\n if [ \"${_recreated}\" = true ]; then\n log \" Recreated ${_runtime} worker: ${_worker_name}\"\n else\n log \" ERROR: Failed to recreate ${_worker_name} after 3 attempts\"\n fi\n else\n log \" WARNING: No credentials found for ${_worker_name} (${_creds_file} missing), skipping\"\n fi\n done\n fi\nfi\n\n# ============================================================\n# Notify workers of builtin updates if upgrade happened\n# Builtin files (AGENTS.md, skills) are already synced by upgrade-builtins.sh\n#\n# Cooldown: skip notification if the last successful notify was within\n# NOTIFY_COOLDOWN_SECS (default 3600s / 1 hour). This prevents repeated\n# notifications when the Manager crash-loops and re-runs upgrade-builtins\n# on every restart (e.g. IMAGE_VERSION=latest always triggers upgrade).\n# ============================================================\nNOTIFY_COOLDOWN_SECS=\"${HICLAW_NOTIFY_COOLDOWN_SECS:-3600}\"\nNOTIFY_TS_FILE=\"/root/manager-workspace/.last-worker-notify-ts\"\n\nif [ -f /root/manager-workspace/.upgrade-pending-worker-notify ]; then\n _now=$(date +%s)\n _last_notify=$(cat \"${NOTIFY_TS_FILE}\" 2>/dev/null || echo \"0\")\n _elapsed=$(( _now - _last_notify ))\n\n if [ \"${_elapsed}\" -lt \"${NOTIFY_COOLDOWN_SECS}\" ]; then\n log \"Skipping worker builtin notification (last notify ${_elapsed}s ago, cooldown ${NOTIFY_COOLDOWN_SECS}s)\"\n rm -f /root/manager-workspace/.upgrade-pending-worker-notify\n else\n log \"Notifying workers about builtin updates...\"\n REGISTRY_FILE=\"/root/manager-workspace/workers-registry.json\"\n _notify_ok=false\n if [ -f \"${REGISTRY_FILE}\" ]; then\n for _worker_name in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_worker_name}\" ] && continue\n _room_id=$(jq -r --arg w \"${_worker_name}\" '.workers[$w].room_id // empty' \"${REGISTRY_FILE}\" 2>/dev/null)\n if [ -n \"${_room_id}\" ]; then\n _worker_id=\"@${_worker_name}:${MATRIX_DOMAIN}\"\n _txn_id=\"upgrade-$(date +%s%N)\"\n _msg=\"@${_worker_name}:${MATRIX_DOMAIN} Manager upgraded builtin files (AGENTS.md, skills). Please use your file-sync skill to sync the latest config.\"\n _raw=$(curl -s -w '\\nHTTP_CODE:%{http_code}' -X PUT \\\n \"${HICLAW_MATRIX_SERVER}/_matrix/client/v3/rooms/${_room_id}/send/m.room.message/${_txn_id}\" \\\n -H \"Authorization: Bearer ${MANAGER_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d \"{\\\"msgtype\\\":\\\"m.text\\\",\\\"body\\\":\\\"${_msg}\\\",\\\"m.mentions\\\":{\\\"user_ids\\\":[\\\"${_worker_id}\\\"]}}\" \\\n 2>&1) || true\n _http_code=$(echo \"${_raw}\" | tail -1 | sed 's/HTTP_CODE://')\n _notify_resp=$(echo \"${_raw}\" | sed '$d')\n if echo \"${_notify_resp}\" | jq -e '.event_id' > /dev/null 2>&1; then\n log \" Notified ${_worker_name}\"; _notify_ok=true\n else\n log \" WARNING: Failed to notify ${_worker_name} (HTTP ${_http_code}): ${_notify_resp}\"\n fi\n fi\n done\n fi\n # Record timestamp only if at least one notification succeeded\n if [ \"${_notify_ok}\" = true ]; then\n echo \"${_now}\" > \"${NOTIFY_TS_FILE}\"\n fi\n rm -f /root/manager-workspace/.upgrade-pending-worker-notify\n fi\nfi\n\n# ============================================================\n# Start OpenClaw Manager Agent\n# ============================================================\nlog \"Starting Manager Agent (OpenClaw)...\"\n\n# HOME is already set to /root/manager-workspace via docker run -e HOME=...\nexport OPENCLAW_CONFIG_PATH=\"/root/manager-workspace/openclaw.json\"\n\n# Symlink to default OpenClaw config path so CLI commands find the config\nmkdir -p \"${HOME}/.openclaw\"\nln -sf \"/root/manager-workspace/openclaw.json\" \"${HOME}/.openclaw/openclaw.json\"\n\n# Ensure host credential symlinks exist under HOME\nif [ -d \"/host-share\" ]; then\n [ -f \"/host-share/.gitconfig\" ] && ln -sf \"/host-share/.gitconfig\" \"${HOME}/.gitconfig\"\nfi\n\nlog \"HOME=${HOME} (manager-workspace, host-mounted)\"\ncd \"${HOME}\"\n\n# Clean orphaned session write locks (e.g. from SIGKILL or crash before exit handlers)\n# Prevents \"session file locked (timeout 10000ms)\" when PID was reused\nfind \"${HOME}/.openclaw/agents\" -name \"*.jsonl.lock\" -delete 2>/dev/null || true\nlog \"Cleaned up any orphaned session write locks\"\n\n# Clean Matrix crypto storage (SQLite WAL may be corrupted after unclean shutdown)\n# Crypto state is re-negotiated on startup; losing it only means re-establishing E2EE sessions\nrm -rf \"${HOME}/.openclaw/matrix\" 2>/dev/null || true\nlog \"Cleaned Matrix crypto storage (will re-establish E2EE sessions)\"\n\n# ── Render agent doc templates ────────────────────────────────────────────\n# Replace ${VAR} placeholders with actual values so the AI agent reads\n# plain text and never needs to resolve environment variables.\nexport MANAGER_MATRIX_TOKEN MANAGER_TOKEN HIGRESS_COOKIE_FILE\nRENDER=/opt/hiclaw/scripts/lib/render-skills.sh\nlog \"Rendering agent doc templates...\"\n# Manager-owned docs (workspace)\nbash \"$RENDER\" /root/manager-workspace/skills\nbash \"$RENDER\" /root/manager-workspace/skills-alpha\nbash \"$RENDER\" /root/manager-workspace AGENTS.md TOOLS.md HEARTBEAT.md SOUL.md\n# Worker templates (workspace + image) — rendered before push to MinIO\n# so Workers (including remote pip-install) receive plain text\nbash \"$RENDER\" /root/manager-workspace/worker-skills\nbash \"$RENDER\" /root/manager-workspace/worker-agent\nbash \"$RENDER\" /root/manager-workspace/copaw-worker-agent\nbash \"$RENDER\" /opt/hiclaw/agent/worker-skills\nbash \"$RENDER\" /opt/hiclaw/agent/worker-agent\nbash \"$RENDER\" /opt/hiclaw/agent/copaw-worker-agent\nlog \"Agent doc templates rendered\"\n\n# Cloud mode: start background file sync (workspace ↔ OSS) and initial push\nif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n log \"Syncing initial workspace to OSS...\"\n ensure_mc_credentials\n mc mirror /root/manager-workspace/ \"${HICLAW_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" 2>/dev/null || true\n\n # Local → OSS: change-triggered sync\n (\n while true; do\n CHANGED=$(find /root/manager-workspace/ -type f -newermt \"15 seconds ago\" 2>/dev/null | head -1)\n if [ -n \"${CHANGED}\" ]; then\n ensure_mc_credentials 2>/dev/null || true\n mc mirror /root/manager-workspace/ \"${HICLAW_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" --exclude \".npm/**\" \\\n --exclude \".local/**\" --exclude \".mc/**\" 2>/dev/null || true\n fi\n sleep 10\n done\n ) &\n log \"Local→OSS sync started (PID: $!)\"\n\n # OSS → Local: periodic pull (shared data, agent configs)\n (\n while true; do\n sleep 300\n ensure_mc_credentials 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/shared/\" /root/hiclaw-fs/shared/ --overwrite --newer-than \"5m\" 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/agents/\" /root/hiclaw-fs/agents/ --overwrite --newer-than \"5m\" 2>/dev/null || true\n done\n ) &\n log \"OSS→Local sync started (every 5m, PID: $!)\"\nfi\n\n# Launch OpenClaw\nexec openclaw gateway run --verbose --force\n"}, "3f8603a6ef15f127c258963be816fb72721bc5b0": {"t": 1785764779.2768333, "n": 42103, "h": "dfcbcd0ef5808193", "c": "#!/bin/bash\n# start-manager-agent.sh - Initialize and start the Manager Agent\n# Supports both local (supervisord) and cloud (SAE single-process) deployments.\n# In local mode this is the last supervisord component to start (priority 800).\n# In cloud mode (HICLAW_RUNTIME=aliyun) this is the container entrypoint.\n\nsource /opt/hiclaw/scripts/lib/hiclaw-env.sh\n\n# ============================================================\n# Set timezone from TZ env var\n# ============================================================\nif [ -n \"${TZ}\" ] && [ -f \"/usr/share/zoneinfo/${TZ}\" ]; then\n ln -sf \"/usr/share/zoneinfo/${TZ}\" /etc/localtime\n echo \"${TZ}\" > /etc/timezone\n log \"Timezone set to ${TZ}\"\nfi\n\nMATRIX_DOMAIN=\"${HICLAW_MATRIX_DOMAIN:-matrix-local.hiclaw.io:8080}\"\nAI_GATEWAY_DOMAIN=\"${HICLAW_AI_GATEWAY_DOMAIN:-aigw-local.hiclaw.io}\"\n\n# ============================================================\n# Cloud mode: validate required environment variables + initial credentials\n# ============================================================\nif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n : \"${HICLAW_MATRIX_URL:?HICLAW_MATRIX_URL is required}\"\n : \"${HICLAW_MATRIX_DOMAIN:?HICLAW_MATRIX_DOMAIN is required}\"\n : \"${HICLAW_AI_GATEWAY_URL:?HICLAW_AI_GATEWAY_URL is required}\"\n : \"${HICLAW_MANAGER_GATEWAY_KEY:?HICLAW_MANAGER_GATEWAY_KEY is required}\"\n : \"${HICLAW_MANAGER_PASSWORD:?HICLAW_MANAGER_PASSWORD is required (cloud containers are stateless, password must be injected)}\"\n : \"${HICLAW_REGISTRATION_TOKEN:?HICLAW_REGISTRATION_TOKEN is required}\"\n : \"${HICLAW_ADMIN_USER:?HICLAW_ADMIN_USER is required}\"\n : \"${HICLAW_ADMIN_PASSWORD:?HICLAW_ADMIN_PASSWORD is required}\"\n log \"Cloud mode: validating environment... OK\"\n log \" Matrix: ${HICLAW_MATRIX_SERVER}, AI Gateway: ${HICLAW_AI_GATEWAY_URL}, OSS: ${HICLAW_STORAGE_BUCKET}\"\n ensure_mc_credentials || { log \"FATAL: Initial STS credential fetch failed\"; exit 1; }\nfi\n\n# ============================================================\n# Local mode: host symlinks, /etc/hosts, wait for local services\n# ============================================================\nif [ \"${HICLAW_RUNTIME}\" != \"aliyun\" ]; then\n # Create symlink for host directory access\n if [ -d \"/host-share\" ]; then\n ORIGINAL_HOST_HOME=\"${HOST_ORIGINAL_HOME:-$HOME}\"\n if [ ! -e \"${ORIGINAL_HOST_HOME}\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/root\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/data\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/host-share\" ]; then\n mkdir -p \"$(dirname \"${ORIGINAL_HOST_HOME}\")\"\n ln -sfn /host-share \"${ORIGINAL_HOST_HOME}\"\n log \"Created symlink: ${ORIGINAL_HOST_HOME} -> /host-share\"\n else\n ln -sfn /host-share /root/host-home\n log \"Created fallback symlink: /root/host-home -> /host-share\"\n fi\n fi\n\n # Add local domains to /etc/hosts\n HOSTS_DOMAINS=\"${MATRIX_DOMAIN%%:*} ${HICLAW_MATRIX_CLIENT_DOMAIN:-matrix-client-local.hiclaw.io} ${AI_GATEWAY_DOMAIN} ${HICLAW_FS_DOMAIN:-fs-local.hiclaw.io}\"\n if ! grep -q \"${AI_GATEWAY_DOMAIN}\" /etc/hosts 2>/dev/null; then\n echo \"127.0.0.1 ${HOSTS_DOMAINS}\" >> /etc/hosts\n log \"Added local domains to /etc/hosts\"\n fi\n\n # Wait for local infrastructure\n waitForService \"Higress Gateway\" \"127.0.0.1\" 8080 180\n waitForService \"Higress Console\" \"127.0.0.1\" 8001 180\n waitForService \"Tuwunel\" \"127.0.0.1\" 6167 120\n waitForHTTP \"Tuwunel Matrix API\" \"${HICLAW_MATRIX_SERVER}/_tuwunel/server_version\" 120\n waitForService \"MinIO\" \"127.0.0.1\" 9000 120\nelse\n # Cloud mode: wait for external Tuwunel\n log \"Waiting for Tuwunel Matrix server at ${HICLAW_MATRIX_SERVER}...\"\n _retry=0\n while [ \"${_retry}\" -lt 30 ]; do\n if curl -sf \"${HICLAW_MATRIX_SERVER}/_matrix/client/versions\" > /dev/null 2>&1; then\n log \"Tuwunel is ready\"\n break\n fi\n _retry=$((_retry + 1))\n log \" Waiting for Tuwunel (attempt ${_retry}/30)...\"\n sleep 5\n done\n if [ \"${_retry}\" -ge 30 ]; then\n log \"ERROR: Tuwunel not reachable at ${HICLAW_MATRIX_SERVER}\"\n exit 1\n fi\nfi\n\n# ============================================================\n# Auto-generate secrets if not provided via environment\n# Persisted to /data so they survive container restart\n# ============================================================\nSECRETS_FILE=\"/data/hiclaw-secrets.env\"\nif [ -f \"${SECRETS_FILE}\" ]; then\n source \"${SECRETS_FILE}\"\n log \"Loaded persisted secrets from ${SECRETS_FILE}\"\nfi\n\nif [ -z \"${HICLAW_MANAGER_GATEWAY_KEY}\" ]; then\n export HICLAW_MANAGER_GATEWAY_KEY=\"$(generateKey 32)\"\n log \"Auto-generated HICLAW_MANAGER_GATEWAY_KEY\"\nfi\nif [ -z \"${HICLAW_MANAGER_PASSWORD}\" ]; then\n export HICLAW_MANAGER_PASSWORD=\"$(generateKey 16)\"\n log \"Auto-generated HICLAW_MANAGER_PASSWORD\"\nfi\n\n# Persist secrets so they survive supervisord restart\nmkdir -p /data\ncat > \"${SECRETS_FILE}\" <<EOF\nexport HICLAW_MANAGER_GATEWAY_KEY=\"${HICLAW_MANAGER_GATEWAY_KEY}\"\nexport HICLAW_MANAGER_PASSWORD=\"${HICLAW_MANAGER_PASSWORD}\"\nEOF\nchmod 600 \"${SECRETS_FILE}\"\n\n# Cloud mode: pull workspace from OSS before initialization\nif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n HICLAW_FS=\"/root/hiclaw-fs\"\n mkdir -p \"${HICLAW_FS}/shared\" \"${HICLAW_FS}/agents\"\n log \"Pulling workspace from OSS...\"\n ensure_mc_credentials\n mc mirror \"${HICLAW_STORAGE_PREFIX}/manager/\" /root/manager-workspace/ --overwrite 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/shared/\" \"${HICLAW_FS}/shared/\" --overwrite 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/agents/\" \"${HICLAW_FS}/agents/\" --overwrite 2>/dev/null || true\n # Symlink hiclaw-fs into workspace for agent access\n ln -sfn \"${HICLAW_FS}\" /root/manager-workspace/hiclaw-fs\nfi\n\n# ============================================================\n# Initialize / upgrade Manager workspace\n# First boot: full init via upgrade-builtins.sh\n# Subsequent boots: compare image version; upgrade only if changed\n# ============================================================\nmkdir -p /root/manager-workspace\n\nIMAGE_VERSION=$(cat /opt/hiclaw/agent/.builtin-version 2>/dev/null || echo \"unknown\")\nINSTALLED_VERSION=$(cat /root/manager-workspace/.builtin-version 2>/dev/null || echo \"\")\n\nif [ ! -f /root/manager-workspace/.initialized ]; then\n log \"First boot: initializing manager workspace...\"\n bash /opt/hiclaw/scripts/init/upgrade-builtins.sh\n touch /root/manager-workspace/.initialized\n log \"Manager workspace initialized (version: ${IMAGE_VERSION})\"\nelif [ \"${IMAGE_VERSION}\" != \"${INSTALLED_VERSION}\" ] || [ \"${IMAGE_VERSION}\" = \"latest\" ]; then\n log \"Upgrade detected: ${INSTALLED_VERSION} -> ${IMAGE_VERSION}${IMAGE_VERSION:+ (latest: always upgrade)}\"\n bash /opt/hiclaw/scripts/init/upgrade-builtins.sh\n log \"Manager workspace upgraded to version: ${IMAGE_VERSION}\"\nelse\n log \"Workspace up to date (version: ${IMAGE_VERSION})\"\nfi\n\n# Local mode: wait for mc mirror initialization (shared + worker data in /root/hiclaw-fs/)\nif [ \"${HICLAW_RUNTIME}\" != \"aliyun\" ]; then\n log \"Waiting for MinIO storage initialization...\"\n _minio_wait=0\n while [ ! -f /root/hiclaw-fs/.initialized ]; do\n sleep 2\n _minio_wait=$(( _minio_wait + 1 ))\n if [ \"${_minio_wait}\" -ge 60 ]; then\n log \"ERROR: MinIO storage initialization timed out after 120s\"\n exit 1\n fi\n done\n log \"MinIO storage initialized\"\nfi\n\n# ============================================================\n# Register Matrix users via Registration API (single-step, no UIAA)\n# ============================================================\nlog \"Registering human admin Matrix account...\"\ncurl -sf -X POST ${HICLAW_MATRIX_SERVER}/_matrix/client/v3/register \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"username\": \"'\"${HICLAW_ADMIN_USER}\"'\",\n \"password\": \"'\"${HICLAW_ADMIN_PASSWORD}\"'\",\n \"auth\": {\n \"type\": \"m.login.registration_token\",\n \"token\": \"'\"${HICLAW_REGISTRATION_TOKEN}\"'\"\n }\n }' > /dev/null 2>&1 || log \"Admin account may already exist\"\n\nlog \"Registering Manager Agent Matrix account...\"\ncurl -sf -X POST ${HICLAW_MATRIX_SERVER}/_matrix/client/v3/register \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"username\": \"manager\",\n \"password\": \"'\"${HICLAW_MANAGER_PASSWORD}\"'\",\n \"auth\": {\n \"type\": \"m.login.registration_token\",\n \"token\": \"'\"${HICLAW_REGISTRATION_TOKEN}\"'\"\n }\n }' > /dev/null 2>&1 || log \"Manager account may already exist\"\n\n# Get Manager Agent's Matrix access token\nlog \"Obtaining Manager Matrix access token...\"\n_LOGIN_RESPONSE=$(curl -sf -X POST ${HICLAW_MATRIX_SERVER}/_matrix/client/v3/login \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"type\": \"m.login.password\",\n \"identifier\": {\"type\": \"m.id.user\", \"user\": \"manager\"},\n \"password\": \"'\"${HICLAW_MANAGER_PASSWORD}\"'\"\n }' 2>&1)\n_LOGIN_EXIT=$?\nlog \"Matrix login HTTP exit code: ${_LOGIN_EXIT}\"\nlog \"Matrix login response: ${_LOGIN_RESPONSE}\"\n\nMANAGER_TOKEN=$(echo \"${_LOGIN_RESPONSE}\" | jq -r '.access_token' 2>/dev/null)\n\nif [ -z \"${MANAGER_TOKEN}\" ] || [ \"${MANAGER_TOKEN}\" = \"null\" ]; then\n log \"ERROR: Failed to obtain Manager Matrix token (exit=${_LOGIN_EXIT})\"\n log \"ERROR: Login response was: ${_LOGIN_RESPONSE}\"\n exit 1\nfi\nlog \"Manager Matrix token obtained (token prefix: ${MANAGER_TOKEN:0:10}...)\"\n\n# ============================================================\n# Local mode: Initialize Higress Console + configure routes\n# Cloud mode: Create admin DM room + schedule welcome message\n# ============================================================\nif [ \"${HICLAW_RUNTIME}\" != \"aliyun\" ]; then\n COOKIE_FILE=\"/tmp/higress-session-cookie\"\n\n log \"Waiting for Higress Console to be fully ready and initializing admin...\"\n INIT_DONE=false\n for i in $(seq 1 90); do\n INIT_RESULT=$(curl -s -X POST http://127.0.0.1:8001/system/init \\\n -H 'Content-Type: application/json' \\\n -d '{\"adminUser\":{\"name\":\"'\"${HICLAW_ADMIN_USER}\"'\",\"password\":\"'\"${HICLAW_ADMIN_PASSWORD}\"'\",\"displayName\":\"'\"${HICLAW_ADMIN_USER}\"'\"}}' 2>/dev/null) || true\n if echo \"${INIT_RESULT}\" | grep -qE '\"success\":true|already.?init' 2>/dev/null; then\n INIT_DONE=true\n break\n fi\n if echo \"${INIT_RESULT}\" | grep -q '\"name\"' 2>/dev/null; then\n INIT_DONE=true\n break\n fi\n sleep 2\n done\n\n if [ \"${INIT_DONE}\" != \"true\" ]; then\n log \"ERROR: Higress Console did not become ready within 180s\"\n exit 1\n fi\n log \"Higress Console init done\"\n\n log \"Logging into Higress Console...\"\n LOGIN_OK=false\n for i in $(seq 1 10); do\n HTTP_CODE=$(curl -s -o /dev/null -w '%{http_code}' -X POST http://127.0.0.1:8001/session/login \\\n -H 'Content-Type: application/json' \\\n -c \"${COOKIE_FILE}\" \\\n -d '{\"username\":\"'\"${HICLAW_ADMIN_USER}\"'\",\"password\":\"'\"${HICLAW_ADMIN_PASSWORD}\"'\"}' 2>/dev/null) || true\n if { [ \"${HTTP_CODE}\" = \"200\" ] || [ \"${HTTP_CODE}\" = \"201\" ]; } && [ -f \"${COOKIE_FILE}\" ] && [ -s \"${COOKIE_FILE}\" ]; then\n LOGIN_OK=true\n break\n fi\n log \"Login attempt $i (HTTP ${HTTP_CODE}), retrying in 3s...\"\n sleep 3\n done\n\n if [ \"${LOGIN_OK}\" != \"true\" ]; then\n log \"ERROR: Could not login to Higress Console after retries\"\n exit 1\n fi\n log \"Higress Console login successful\"\n\n VERIFY_CODE=$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8001/v1/consumers -b \"${COOKIE_FILE}\" 2>/dev/null) || true\n if [ \"${VERIFY_CODE}\" = \"200\" ]; then\n log \"Console session verified (cookie valid)\"\n else\n log \"WARNING: Console session may be invalid (verify returned HTTP ${VERIFY_CODE})\"\n rm -f \"${COOKIE_FILE}\"\n for i in $(seq 1 5); do\n curl -s -o /dev/null -w '%{http_code}' -X POST http://127.0.0.1:8001/session/login \\\n -H 'Content-Type: application/json' \\\n -c \"${COOKIE_FILE}\" \\\n -d '{\"username\":\"'\"${HICLAW_ADMIN_USER}\"'\",\"password\":\"'\"${HICLAW_ADMIN_PASSWORD}\"'\"}' 2>/dev/null\n VERIFY2=$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8001/v1/consumers -b \"${COOKIE_FILE}\" 2>/dev/null) || true\n if [ \"${VERIFY2}\" = \"200\" ]; then\n log \"Re-login successful, session verified\"\n break\n fi\n sleep 2\n done\n fi\n\n export HIGRESS_COOKIE_FILE=\"${COOKIE_FILE}\"\n\n # Configure Higress routes, consumers, MCP servers\n /opt/hiclaw/scripts/init/setup-higress.sh\nelse\n # Cloud mode: create admin DM room and schedule welcome message\n MANAGER_FULL_ID=\"@manager:${MATRIX_DOMAIN}\"\n ADMIN_FULL_ID=\"@${HICLAW_ADMIN_USER}:${MATRIX_DOMAIN}\"\n\n log \"Logging in as admin to create DM room...\"\n _ADMIN_LOGIN=$(curl -sf -X POST \"${HICLAW_MATRIX_SERVER}/_matrix/client/v3/login\" \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"type\": \"m.login.password\",\n \"identifier\": {\"type\": \"m.id.user\", \"user\": \"'\"${HICLAW_ADMIN_USER}\"'\"},\n \"password\": \"'\"${HICLAW_ADMIN_PASSWORD}\"'\"\n }' 2>&1) || true\n\n ADMIN_MATRIX_TOKEN=$(echo \"${_ADMIN_LOGIN}\" | jq -r '.access_token // empty' 2>/dev/null)\n if [ -z \"${ADMIN_MATRIX_TOKEN}\" ]; then\n log \"WARNING: Failed to login as admin, skipping DM room creation\"\n else\n # Search for existing DM room with Manager (idempotent)\n DM_ROOM_ID=\"\"\n _JOINED_ROOMS=$(curl -sf \"${HICLAW_MATRIX_SERVER}/_matrix/client/v3/joined_rooms\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" 2>/dev/null \\\n | jq -r '.joined_rooms[]' 2>/dev/null) || true\n for _rid in ${_JOINED_ROOMS}; do\n _members=$(curl -sf \"${HICLAW_MATRIX_SERVER}/_matrix/client/v3/rooms/${_rid}/members\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" 2>/dev/null \\\n | jq -r '.chunk[].state_key' 2>/dev/null) || continue\n _count=$(echo \"${_members}\" | wc -l | xargs)\n if [ \"${_count}\" = \"2\" ] && echo \"${_members}\" | grep -q \"@manager:\"; then\n DM_ROOM_ID=\"${_rid}\"\n break\n fi\n done\n\n if [ -n \"${DM_ROOM_ID}\" ]; then\n log \"Existing DM room found: ${DM_ROOM_ID}\"\n else\n log \"Creating DM room with Manager...\"\n _RAW=$(curl -s -w '\\nHTTP_CODE:%{http_code}' -X POST \"${HICLAW_MATRIX_SERVER}/_matrix/client/v3/createRoom\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d \"{\\\"is_direct\\\":true,\\\"invite\\\":[\\\"${MANAGER_FULL_ID}\\\"],\\\"preset\\\":\\\"trusted_private_chat\\\"}\" 2>&1) || true\n _HTTP_CODE=$(echo \"${_RAW}\" | tail -1 | sed 's/HTTP_CODE://')\n _CREATE_RESP=$(echo \"${_RAW}\" | sed '$d')\n DM_ROOM_ID=$(echo \"${_CREATE_RESP}\" | jq -r '.room_id // empty' 2>/dev/null)\n if [ -n \"${DM_ROOM_ID}\" ]; then\n log \"DM room created: ${DM_ROOM_ID}\"\n else\n log \"WARNING: Failed to create DM room (HTTP ${_HTTP_CODE}): ${_CREATE_RESP}\"\n fi\n fi\n\n # Schedule welcome message in background (only on first boot)\n if [ -n \"${DM_ROOM_ID}\" ] && [ ! -f \"/root/manager-workspace/soul-configured\" ]; then\n log \"Scheduling welcome message (background, waiting for OpenClaw to start)...\"\n (\n _HICLAW_LANGUAGE=\"${HICLAW_LANGUAGE:-zh}\"\n _HICLAW_TIMEZONE=\"${TZ:-Asia/Shanghai}\"\n _wait=0\n _ready=false\n while [ \"${_wait}\" -lt 300 ]; do\n if curl -sf http://127.0.0.1:18799/ > /dev/null 2>&1; then\n _ready=true\n break\n fi\n sleep 3\n _wait=$((_wait + 3))\n done\n if [ \"${_ready}\" != \"true\" ]; then\n echo \"[cloud-manager] WARNING: OpenClaw gateway not ready within 300s, skipping welcome message\"\n exit 0\n fi\n _welcome_msg=\"This is an automated message from the HiClaw cloud deployment. This is a fresh installation.\n\n--- Installation Context ---\nUser Language: ${_HICLAW_LANGUAGE} (zh = Chinese, en = English)\nUser Timezone: ${_HICLAW_TIMEZONE} (IANA timezone identifier)\n---\n\nYou are an AI agent that manages a team of worker agents. Your identity and personality have not been configured yet — the human admin is about to meet you for the first time.\n\nPlease begin the onboarding conversation:\n\n1. Greet the admin warmly and briefly describe what you can do (coordinate workers, manage tasks, run multi-agent projects)\n2. The user has selected \\\"${_HICLAW_LANGUAGE}\\\" as their preferred language during installation. Use this language for your greeting and all subsequent communication.\n3. The user's timezone is ${_HICLAW_TIMEZONE}. Based on this timezone, you may infer their likely region and suggest additional language options.\n4. Ask them: a) What would they like to call you? b) Communication style preference? c) Any behavior guidelines? d) Confirm default language\n5. After they reply, write their preferences to ~/SOUL.md\n6. Confirm what you wrote, and ask if they would like to adjust anything\n7. Once confirmed, run: touch ~/soul-configured\n\nThe human admin will start chatting shortly.\"\n _txn_id=\"welcome-cloud-$(date +%s)\"\n _payload=$(jq -nc --arg body \"${_welcome_msg}\" '{\"msgtype\":\"m.text\",\"body\":$body}')\n _raw=$(curl -s -w '\\nHTTP_CODE:%{http_code}' -X PUT \"${HICLAW_MATRIX_SERVER}/_matrix/client/v3/rooms/${DM_ROOM_ID}/send/m.room.message/${_txn_id}\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d \"${_payload}\" 2>&1) || true\n _http_code=$(echo \"${_raw}\" | tail -1 | sed 's/HTTP_CODE://')\n _send_resp=$(echo \"${_raw}\" | sed '$d')\n if echo \"${_send_resp}\" | jq -e '.event_id' > /dev/null 2>&1; then\n echo \"[cloud-manager] Welcome message sent to DM room\"\n else\n echo \"[cloud-manager] WARNING: Failed to send welcome message (HTTP ${_http_code}): ${_send_resp}\"\n fi\n ) &\n log \"Welcome message background process started (PID: $!)\"\n fi\n fi\nfi\n\n# ============================================================\n# Generate Manager Agent openclaw.json from template\n# ============================================================\nlog \"Generating Manager openclaw.json...\"\nexport MANAGER_MATRIX_TOKEN=\"${MANAGER_TOKEN}\"\nexport MANAGER_GATEWAY_KEY=\"${HICLAW_MANAGER_GATEWAY_KEY}\"\nexport MANAGER_HOOKS_TOKEN=$(echo -n \"${HICLAW_MANAGER_GATEWAY_KEY}-hooks\" | base64 -w 0)\n\n# Resolve model parameters based on model name\nMODEL_NAME=\"${HICLAW_DEFAULT_MODEL:-qwen3.5-plus}\"\ncase \"${MODEL_NAME}\" in\n gpt-5.3-codex|gpt-5-mini|gpt-5-nano)\n export MODEL_CONTEXT_WINDOW=400000 MODEL_MAX_TOKENS=128000 ;;\n claude-opus-4-6)\n export MODEL_CONTEXT_WINDOW=1000000 MODEL_MAX_TOKENS=128000 ;;\n claude-sonnet-4-6)\n export MODEL_CONTEXT_WINDOW=1000000 MODEL_MAX_TOKENS=64000 ;;\n claude-haiku-4-5)\n export MODEL_CONTEXT_WINDOW=200000 MODEL_MAX_TOKENS=64000 ;;\n qwen3.5-plus)\n export MODEL_CONTEXT_WINDOW=200000 MODEL_MAX_TOKENS=64000 ;;\n deepseek-chat|deepseek-reasoner|kimi-k2.5)\n export MODEL_CONTEXT_WINDOW=256000 MODEL_MAX_TOKENS=128000 ;;\n glm-5|MiniMax-M2.5)\n export MODEL_CONTEXT_WINDOW=200000 MODEL_MAX_TOKENS=128000 ;;\n *)\n export MODEL_CONTEXT_WINDOW=150000 MODEL_MAX_TOKENS=128000 ;;\nesac\nexport MODEL_REASONING=true\n\n# Override with user-supplied custom model parameters from env (set during install)\n[ -n \"${HICLAW_MODEL_CONTEXT_WINDOW:-}\" ] && export MODEL_CONTEXT_WINDOW=\"${HICLAW_MODEL_CONTEXT_WINDOW}\"\n[ -n \"${HICLAW_MODEL_MAX_TOKENS:-}\" ] && export MODEL_MAX_TOKENS=\"${HICLAW_MODEL_MAX_TOKENS}\"\n[ -n \"${HICLAW_MODEL_REASONING:-}\" ] && export MODEL_REASONING=\"${HICLAW_MODEL_REASONING}\"\n\n# E2EE: convert HICLAW_MATRIX_E2EE to JSON boolean for template substitution\nif [ \"${HICLAW_MATRIX_E2EE:-0}\" = \"1\" ] || [ \"${HICLAW_MATRIX_E2EE:-}\" = \"true\" ]; then\n export MATRIX_E2EE_ENABLED=true\nelse\n export MATRIX_E2EE_ENABLED=false\nfi\nlog \"Matrix E2EE: ${MATRIX_E2EE_ENABLED}\"\n\n# Resolve input modalities: only vision-capable models get \"image\"\ncase \"${MODEL_NAME}\" in\n gpt-5.4|gpt-5.3-codex|gpt-5-mini|gpt-5-nano|claude-opus-4-6|claude-sonnet-4-6|claude-haiku-4-5|qwen3.5-plus|kimi-k2.5)\n export MODEL_INPUT='[\"text\", \"image\"]' ;;\n *)\n export MODEL_INPUT='[\"text\"]' ;;\nesac\n# Override with user-supplied vision setting from env\nif [ \"${HICLAW_MODEL_VISION:-}\" = \"true\" ]; then\n export MODEL_INPUT='[\"text\", \"image\"]'\nelif [ \"${HICLAW_MODEL_VISION:-}\" = \"false\" ]; then\n export MODEL_INPUT='[\"text\"]'\nfi\n\nlog \"Model: ${MODEL_NAME} (context=${MODEL_CONTEXT_WINDOW}, maxTokens=${MODEL_MAX_TOKENS}, reasoning=${MODEL_REASONING}, input=${MODEL_INPUT})\"\n\nif [ -f /root/manager-workspace/openclaw.json ]; then\n log \"Manager openclaw.json already exists, updating dynamic fields only (preserving user customizations)...\"\n # Merge known models into existing config (add missing, preserve user-added)\n # Use known-models.json (valid JSON) instead of template (contains ${VAR} placeholders)\n KNOWN_MODELS=$(cat /opt/hiclaw/configs/known-models.json 2>/dev/null || echo '[]')\n jq --arg token \"${MANAGER_TOKEN}\" \\\n --arg key \"${HICLAW_MANAGER_GATEWAY_KEY}\" \\\n --arg model \"${MODEL_NAME}\" \\\n --argjson e2ee \"${MATRIX_E2EE_ENABLED}\" \\\n --argjson known_models \"${KNOWN_MODELS}\" \\\n --argjson ctx \"${MODEL_CONTEXT_WINDOW}\" \\\n --argjson max \"${MODEL_MAX_TOKENS}\" \\\n --argjson reasoning \"${MODEL_REASONING}\" \\\n --argjson input \"${MODEL_INPUT}\" \\\n '\n # Merge known models: add any model id not already present\n .models.providers[\"hiclaw-gateway\"].models as $existing\n | ($existing | map(.id)) as $existing_ids\n | ($known_models | map(select(.id as $id | $existing_ids | index($id) | not))) as $new\n | .models.providers[\"hiclaw-gateway\"].models = ($existing + $new)\n # Ensure the user-chosen default model is in the list (custom model support)\n | if (.models.providers[\"hiclaw-gateway\"].models | map(.id) | index($model) | not) then\n .models.providers[\"hiclaw-gateway\"].models += [{\"id\": $model, \"name\": $model, \"reasoning\": $reasoning, \"contextWindow\": $ctx, \"maxTokens\": $max, \"input\": $input}]\n else . end\n # Rebuild model aliases from the full models list\n | (.models.providers[\"hiclaw-gateway\"].models | map({ (\"hiclaw-gateway/\" + .id): { \"alias\": .id } }) | add // {}) as $aliases\n | .agents.defaults.models = ((.agents.defaults.models // {}) + $aliases)\n | .channels.matrix.accessToken = $token | .hooks.token = ($key + \"-hooks\" | @base64) | .models.providers[\"hiclaw-gateway\"].apiKey = $key\n | .agents.defaults.model.primary = (\"hiclaw-gateway/\" + $model)\n | .commands.restart = true\n | .gateway.controlUi.dangerouslyDisableDeviceAuth = true\n | .channels.matrix.encryption = $e2ee\n ' \\\n /root/manager-workspace/openclaw.json > /tmp/openclaw.json.tmp && \\\n mv /tmp/openclaw.json.tmp /root/manager-workspace/openclaw.json\n # Verify the token was written correctly\n _written_token=$(jq -r '.channels.matrix.accessToken' /root/manager-workspace/openclaw.json 2>/dev/null)\n if [ -z \"${_written_token}\" ] || [ \"${_written_token}\" = \"null\" ]; then\n log \"ERROR: Matrix token was not written correctly to openclaw.json (got: ${_written_token})\"\n else\n log \"Matrix token written to openclaw.json (prefix: ${_written_token:0:10}...)\"\n fi\nelse\n log \"Manager openclaw.json not found, generating from template...\"\n envsubst < /opt/hiclaw/configs/manager-openclaw.json.tmpl > /root/manager-workspace/openclaw.json\n # Inject custom model if not in the built-in list\n if ! jq -e --arg model \"${MODEL_NAME}\" '.models.providers[\"hiclaw-gateway\"].models | map(.id) | index($model)' /root/manager-workspace/openclaw.json > /dev/null 2>&1; then\n log \"Custom model '${MODEL_NAME}' not in built-in list, injecting into config...\"\n jq --arg model \"${MODEL_NAME}\" \\\n --argjson ctx \"${MODEL_CONTEXT_WINDOW}\" \\\n --argjson max \"${MODEL_MAX_TOKENS}\" \\\n --argjson reasoning \"${MODEL_REASONING}\" \\\n --argjson input \"${MODEL_INPUT}\" \\\n '\n .models.providers[\"hiclaw-gateway\"].models += [{\"id\": $model, \"name\": $model, \"reasoning\": $reasoning, \"contextWindow\": $ctx, \"maxTokens\": $max, \"input\": $input}]\n | .agents.defaults.models += {(\"hiclaw-gateway/\" + $model): {\"alias\": $model}}\n ' /root/manager-workspace/openclaw.json > /tmp/openclaw.json.tmp && \\\n mv /tmp/openclaw.json.tmp /root/manager-workspace/openclaw.json\n fi\n _written_token=$(jq -r '.channels.matrix.accessToken' /root/manager-workspace/openclaw.json 2>/dev/null)\n log \"Matrix token written from template (prefix: ${_written_token:0:10}...)\"\nfi\n\n# Cloud mode: overlay cloud-specific settings onto generated config\nif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n log \"Applying cloud overlay to openclaw.json...\"\n jq --arg homeserver \"${HICLAW_MATRIX_SERVER}\" \\\n --arg gateway \"${HICLAW_AI_GATEWAY_URL}/v1\" \\\n --arg key \"${HICLAW_MANAGER_GATEWAY_KEY}\" \\\n '.channels.matrix.homeserver = $homeserver\n | .models.providers[\"hiclaw-gateway\"].baseUrl = $gateway\n | .models.providers[\"hiclaw-gateway\"].apiKey = $key\n | .hooks.token = ($key + \"-hooks\" | @base64)\n | .commands.restart = false' \\\n /root/manager-workspace/openclaw.json > /tmp/openclaw-cloud.json && \\\n mv /tmp/openclaw-cloud.json /root/manager-workspace/openclaw.json\n log \"Cloud overlay applied\"\nfi\n\n# ============================================================\n# Detect container runtime (for Worker creation)\n# ============================================================\nsource /opt/hiclaw/scripts/lib/container-api.sh\nif container_api_available; then\n log \"Container runtime socket detected at ${CONTAINER_SOCKET} — direct Worker creation enabled\"\n export HICLAW_CONTAINER_RUNTIME=\"socket\"\nelif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n log \"Cloud mode — Workers created via SAE API\"\n export HICLAW_CONTAINER_RUNTIME=\"cloud\"\nelse\n log \"No container runtime found — Worker creation will output install commands\"\n export HICLAW_CONTAINER_RUNTIME=\"none\"\nfi\n\n# ============================================================\n# Upgrade Worker openclaw.json: merge known models + E2EE flag into existing configs\n# Existing workers in MinIO may have old single-model configs or missing encryption field.\n# Merge template models so they can hot-switch without restart.\n# ============================================================\nREGISTRY_FILE=\"/root/manager-workspace/workers-registry.json\"\nif [ -f \"${REGISTRY_FILE}\" ]; then\n # Use known-models.json (valid JSON) instead of template (contains ${VAR} placeholders)\n KNOWN_MODELS_FILE=\"/opt/hiclaw/configs/known-models.json\"\n if [ -f \"${KNOWN_MODELS_FILE}\" ]; then\n _KNOWN_MODELS=$(cat \"${KNOWN_MODELS_FILE}\")\n for _wname in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_wname}\" ] && continue\n _minio_path=\"${HICLAW_STORAGE_PREFIX}/agents/${_wname}/openclaw.json\"\n _tmp_in=\"/tmp/openclaw-${_wname}-models-upgrade-in.json\"\n if mc cp \"${_minio_path}\" \"${_tmp_in}\" 2>/dev/null; then\n _tmp_out=\"/tmp/openclaw-${_wname}-models-upgrade-out.json\"\n # Idempotent merge: add missing known models, rebuild aliases, set e2ee.\n # Always runs — jq deduplicates by model id, so re-runs are safe.\n jq --argjson known_models \"${_KNOWN_MODELS}\" \\\n --argjson e2ee \"${MATRIX_E2EE_ENABLED}\" '\n .models.providers[\"hiclaw-gateway\"].models as $existing\n | ($existing | map(.id)) as $existing_ids\n | ($known_models | map(select(.id as $id | $existing_ids | index($id) | not))) as $new\n | .models.providers[\"hiclaw-gateway\"].models = ($existing + $new)\n | (.models.providers[\"hiclaw-gateway\"].models | map({ (\"hiclaw-gateway/\" + .id): { \"alias\": .id } }) | add // {}) as $aliases\n | .agents.defaults.models = ((.agents.defaults.models // {}) + $aliases)\n | .channels.matrix.encryption = $e2ee\n ' \"${_tmp_in}\" > \"${_tmp_out}\" 2>/dev/null\n if ! diff -q \"${_tmp_in}\" \"${_tmp_out}\" > /dev/null 2>&1; then\n if mc cp \"${_tmp_out}\" \"${_minio_path}\" 2>/dev/null; then\n _new_count=$(jq '.models.providers[\"hiclaw-gateway\"].models | length' \"${_tmp_out}\" 2>/dev/null)\n log \"Worker ${_wname}: upgraded openclaw.json (models: ${_new_count}, e2ee: ${MATRIX_E2EE_ENABLED})\"\n fi\n fi\n rm -f \"${_tmp_in}\" \"${_tmp_out}\"\n fi\n done\n fi\nfi\n\n# ============================================================\n# Ensure Worker Matrix password files exist in MinIO (E2EE fix)\n# Workers need to re-login on restart to get a fresh device_id.\n# Older workers created before this fix won't have the password file.\n# ============================================================\nif [ -f \"${REGISTRY_FILE}\" ]; then\n for _wname in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_wname}\" ] && continue\n _creds_file=\"/data/worker-creds/${_wname}.env\"\n if [ -f \"${_creds_file}\" ]; then\n # Check if password file already exists in MinIO\n if ! mc stat \"${HICLAW_STORAGE_PREFIX}/agents/${_wname}/credentials/matrix/password\" > /dev/null 2>&1; then\n source \"${_creds_file}\"\n if [ -n \"${WORKER_PASSWORD}\" ]; then\n _tmp_pw=\"/tmp/matrix-pw-${_wname}\"\n echo -n \"${WORKER_PASSWORD}\" > \"${_tmp_pw}\"\n mc cp \"${_tmp_pw}\" \"${HICLAW_STORAGE_PREFIX}/agents/${_wname}/credentials/matrix/password\" 2>/dev/null \\\n && log \"Worker ${_wname}: wrote Matrix password to MinIO (E2EE re-login fix)\" \\\n || log \"Worker ${_wname}: WARNING: failed to write Matrix password to MinIO\"\n rm -f \"${_tmp_pw}\"\n fi\n fi\n fi\n done\nfi\n\n# ============================================================\n# Recreate Worker containers as needed after Manager restart.\n# Manager IP may change on restart; Workers use ExtraHosts pointing to Manager IP,\n# so any worker whose ExtraHosts IP no longer matches must be recreated.\n# ============================================================\nif container_api_available; then\n REGISTRY_FILE=\"/root/manager-workspace/workers-registry.json\"\n _manager_ip=$(container_get_manager_ip)\n if [ -f \"${REGISTRY_FILE}\" ]; then\n for _worker_name in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_worker_name}\" ] && continue\n\n # Skip remote workers — they are not Manager-managed containers.\n _deployment=$(jq -r --arg w \"${_worker_name}\" '.workers[$w].deployment // \"local\"' \"${REGISTRY_FILE}\" 2>/dev/null)\n if [ \"${_deployment}\" = \"remote\" ]; then\n log \"Worker ${_worker_name} is remote, skipping container recreate\"\n continue\n fi\n\n _status=$(container_status_worker \"${_worker_name}\")\n if [ \"${_status}\" = \"running\" ]; then\n # Check if ExtraHosts IP still matches current Manager IP.\n # If ExtraHosts is empty the worker uses real DNS — no recreate needed.\n _inspect=$(_api GET \"/containers/${WORKER_CONTAINER_PREFIX}${_worker_name}/json\" 2>/dev/null)\n _extra_hosts_len=$(echo \"${_inspect}\" | jq -r '.HostConfig.ExtraHosts | length' 2>/dev/null)\n if [ -z \"${_extra_hosts_len}\" ] || [ \"${_extra_hosts_len}\" = \"0\" ]; then\n log \"Worker running (no ExtraHosts, real DNS): ${_worker_name}, skipping\"\n continue\n fi\n _worker_ip=$(echo \"${_inspect}\" | jq -r '.HostConfig.ExtraHosts[0]' 2>/dev/null | cut -d: -f2)\n if [ \"${_worker_ip}\" = \"${_manager_ip}\" ]; then\n log \"Worker running with current Manager IP (${_manager_ip}): ${_worker_name}, skipping\"\n continue\n fi\n log \"Worker running but Manager IP changed (${_worker_ip} -> ${_manager_ip}): ${_worker_name}, recreating...\"\n else\n # Container missing or stopped — always recreate.\n log \"Worker container ${_status}: ${_worker_name}, recreating...\"\n fi\n _creds_file=\"/data/worker-creds/${_worker_name}.env\"\n if [ -f \"${_creds_file}\" ]; then\n source \"${_creds_file}\"\n _runtime=$(jq -r --arg w \"${_worker_name}\" '.workers[$w].runtime // \"openclaw\"' \"${REGISTRY_FILE}\" 2>/dev/null)\n _recreated=false\n for _attempt in 1 2 3; do\n if [ \"${_runtime}\" = \"copaw\" ]; then\n container_create_copaw_worker \"${_worker_name}\" \"${_worker_name}\" \"${WORKER_MINIO_PASSWORD}\" 2>&1 && _recreated=true && break\n else\n container_create_worker \"${_worker_name}\" \"${_worker_name}\" \"${WORKER_MINIO_PASSWORD}\" 2>&1 && _recreated=true && break\n fi\n log \" Attempt ${_attempt}/3 failed for ${_worker_name}, retrying in $((5 * _attempt))s...\"\n sleep $((5 * _attempt))\n done\n if [ \"${_recreated}\" = true ]; then\n log \" Recreated ${_runtime} worker: ${_worker_name}\"\n else\n log \" ERROR: Failed to recreate ${_worker_name} after 3 attempts\"\n fi\n else\n log \" WARNING: No credentials found for ${_worker_name} (${_creds_file} missing), skipping\"\n fi\n done\n fi\nfi\n\n# ============================================================\n# Notify workers of builtin updates if upgrade happened\n# Builtin files (AGENTS.md, skills) are already synced by upgrade-builtins.sh\n#\n# Cooldown: skip notification if the last successful notify was within\n# NOTIFY_COOLDOWN_SECS (default 3600s / 1 hour). This prevents repeated\n# notifications when the Manager crash-loops and re-runs upgrade-builtins\n# on every restart (e.g. IMAGE_VERSION=latest always triggers upgrade).\n# ============================================================\nNOTIFY_COOLDOWN_SECS=\"${HICLAW_NOTIFY_COOLDOWN_SECS:-3600}\"\nNOTIFY_TS_FILE=\"/root/manager-workspace/.last-worker-notify-ts\"\n\nif [ -f /root/manager-workspace/.upgrade-pending-worker-notify ]; then\n _now=$(date +%s)\n _last_notify=$(cat \"${NOTIFY_TS_FILE}\" 2>/dev/null || echo \"0\")\n _elapsed=$(( _now - _last_notify ))\n\n if [ \"${_elapsed}\" -lt \"${NOTIFY_COOLDOWN_SECS}\" ]; then\n log \"Skipping worker builtin notification (last notify ${_elapsed}s ago, cooldown ${NOTIFY_COOLDOWN_SECS}s)\"\n rm -f /root/manager-workspace/.upgrade-pending-worker-notify\n else\n log \"Notifying workers about builtin updates...\"\n REGISTRY_FILE=\"/root/manager-workspace/workers-registry.json\"\n _notify_ok=false\n if [ -f \"${REGISTRY_FILE}\" ]; then\n for _worker_name in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_worker_name}\" ] && continue\n _room_id=$(jq -r --arg w \"${_worker_name}\" '.workers[$w].room_id // empty' \"${REGISTRY_FILE}\" 2>/dev/null)\n if [ -n \"${_room_id}\" ]; then\n _worker_id=\"@${_worker_name}:${MATRIX_DOMAIN}\"\n _txn_id=\"upgrade-$(date +%s%N)\"\n _msg=\"@${_worker_name}:${MATRIX_DOMAIN} Manager upgraded builtin files (AGENTS.md, skills). Please use your file-sync skill to sync the latest config.\"\n _raw=$(curl -s -w '\\nHTTP_CODE:%{http_code}' -X PUT \\\n \"${HICLAW_MATRIX_SERVER}/_matrix/client/v3/rooms/${_room_id}/send/m.room.message/${_txn_id}\" \\\n -H \"Authorization: Bearer ${MANAGER_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d \"{\\\"msgtype\\\":\\\"m.text\\\",\\\"body\\\":\\\"${_msg}\\\",\\\"m.mentions\\\":{\\\"user_ids\\\":[\\\"${_worker_id}\\\"]}}\" \\\n 2>&1) || true\n _http_code=$(echo \"${_raw}\" | tail -1 | sed 's/HTTP_CODE://')\n _notify_resp=$(echo \"${_raw}\" | sed '$d')\n if echo \"${_notify_resp}\" | jq -e '.event_id' > /dev/null 2>&1; then\n log \" Notified ${_worker_name}\"; _notify_ok=true\n else\n log \" WARNING: Failed to notify ${_worker_name} (HTTP ${_http_code}): ${_notify_resp}\"\n fi\n fi\n done\n fi\n # Record timestamp only if at least one notification succeeded\n if [ \"${_notify_ok}\" = true ]; then\n echo \"${_now}\" > \"${NOTIFY_TS_FILE}\"\n fi\n rm -f /root/manager-workspace/.upgrade-pending-worker-notify\n fi\nfi\n\n# ============================================================\n# Start OpenClaw Manager Agent\n# ============================================================\nlog \"Starting Manager Agent (OpenClaw)...\"\n\n# HOME is already set to /root/manager-workspace via docker run -e HOME=...\nexport OPENCLAW_CONFIG_PATH=\"/root/manager-workspace/openclaw.json\"\n\n# Symlink to default OpenClaw config path so CLI commands find the config\nmkdir -p \"${HOME}/.openclaw\"\nln -sf \"/root/manager-workspace/openclaw.json\" \"${HOME}/.openclaw/openclaw.json\"\n\n# Ensure host credential symlinks exist under HOME\nif [ -d \"/host-share\" ]; then\n [ -f \"/host-share/.gitconfig\" ] && ln -sf \"/host-share/.gitconfig\" \"${HOME}/.gitconfig\"\nfi\n\nlog \"HOME=${HOME} (manager-workspace, host-mounted)\"\ncd \"${HOME}\"\n\n# Clean orphaned session write locks (e.g. from SIGKILL or crash before exit handlers)\n# Prevents \"session file locked (timeout 10000ms)\" when PID was reused\nfind \"${HOME}/.openclaw/agents\" -name \"*.jsonl.lock\" -delete 2>/dev/null || true\nlog \"Cleaned up any orphaned session write locks\"\n\n# Clean Matrix crypto storage (SQLite WAL may be corrupted after unclean shutdown)\n# Crypto state is re-negotiated on startup; losing it only means re-establishing E2EE sessions\nrm -rf \"${HOME}/.openclaw/matrix\" 2>/dev/null || true\nlog \"Cleaned Matrix crypto storage (will re-establish E2EE sessions)\"\n\n# ── Render agent doc templates ────────────────────────────────────────────\n# Replace ${VAR} placeholders with actual values so the AI agent reads\n# plain text and never needs to resolve environment variables.\nexport MANAGER_MATRIX_TOKEN MANAGER_TOKEN HIGRESS_COOKIE_FILE\nRENDER=/opt/hiclaw/scripts/lib/render-skills.sh\nlog \"Rendering agent doc templates...\"\n# Manager-owned docs (workspace)\nbash \"$RENDER\" /root/manager-workspace/skills\nbash \"$RENDER\" /root/manager-workspace/skills-alpha\nbash \"$RENDER\" /root/manager-workspace AGENTS.md TOOLS.md HEARTBEAT.md SOUL.md\n# Worker templates (workspace + image) — rendered before push to MinIO\n# so Workers (including remote pip-install) receive plain text\nbash \"$RENDER\" /root/manager-workspace/worker-skills\nbash \"$RENDER\" /root/manager-workspace/worker-agent\nbash \"$RENDER\" /root/manager-workspace/copaw-worker-agent\nbash \"$RENDER\" /opt/hiclaw/agent/worker-skills\nbash \"$RENDER\" /opt/hiclaw/agent/worker-agent\nbash \"$RENDER\" /opt/hiclaw/agent/copaw-worker-agent\nlog \"Agent doc templates rendered\"\n\n# Cloud mode: start background file sync (workspace ↔ OSS) and initial push\nif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n log \"Syncing initial workspace to OSS...\"\n ensure_mc_credentials\n mc mirror /root/manager-workspace/ \"${HICLAW_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" 2>/dev/null || true\n\n # Local → OSS: change-triggered sync\n (\n while true; do\n CHANGED=$(find /root/manager-workspace/ -type f -newermt \"15 seconds ago\" 2>/dev/null | head -1)\n if [ -n \"${CHANGED}\" ]; then\n ensure_mc_credentials 2>/dev/null || true\n mc mirror /root/manager-workspace/ \"${HICLAW_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" --exclude \".npm/**\" \\\n --exclude \".local/**\" --exclude \".mc/**\" 2>/dev/null || true\n fi\n sleep 10\n done\n ) &\n log \"Local→OSS sync started (PID: $!)\"\n\n # OSS → Local: periodic pull (shared data, agent configs)\n (\n while true; do\n sleep 300\n ensure_mc_credentials 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/shared/\" /root/hiclaw-fs/shared/ --overwrite --newer-than \"5m\" 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/agents/\" /root/hiclaw-fs/agents/ --overwrite --newer-than \"5m\" 2>/dev/null || true\n done\n ) &\n log \"OSS→Local sync started (every 5m, PID: $!)\"\nfi\n\n# Launch OpenClaw\nexec openclaw gateway run --verbose --force\n"}, "d8ac9e817b65145c10f6d2e64ba11288231c7624": {"t": 1785764781.2057545, "n": 41881, "h": "cc5fbfa949c586be", "c": "#!/bin/bash\n# start-manager-agent.sh - Initialize and start the Manager Agent\n# Supports both local (supervisord) and cloud (SAE single-process) deployments.\n# In local mode this is the last supervisord component to start (priority 800).\n# In cloud mode (HICLAW_RUNTIME=aliyun) this is the container entrypoint.\n\nsource /opt/hiclaw/scripts/lib/hiclaw-env.sh\n\n# ============================================================\n# Set timezone from TZ env var\n# ============================================================\nif [ -n \"${TZ}\" ] && [ -f \"/usr/share/zoneinfo/${TZ}\" ]; then\n ln -sf \"/usr/share/zoneinfo/${TZ}\" /etc/localtime\n echo \"${TZ}\" > /etc/timezone\n log \"Timezone set to ${TZ}\"\nfi\n\nMATRIX_DOMAIN=\"${HICLAW_MATRIX_DOMAIN:-matrix-local.hiclaw.io:8080}\"\nAI_GATEWAY_DOMAIN=\"${HICLAW_AI_GATEWAY_DOMAIN:-aigw-local.hiclaw.io}\"\n\n# ============================================================\n# Cloud mode: validate required environment variables + initial credentials\n# ============================================================\nif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n : \"${HICLAW_MATRIX_URL:?HICLAW_MATRIX_URL is required}\"\n : \"${HICLAW_MATRIX_DOMAIN:?HICLAW_MATRIX_DOMAIN is required}\"\n : \"${HICLAW_AI_GATEWAY_URL:?HICLAW_AI_GATEWAY_URL is required}\"\n : \"${HICLAW_MANAGER_GATEWAY_KEY:?HICLAW_MANAGER_GATEWAY_KEY is required}\"\n : \"${HICLAW_MANAGER_PASSWORD:?HICLAW_MANAGER_PASSWORD is required (cloud containers are stateless, password must be injected)}\"\n : \"${HICLAW_REGISTRATION_TOKEN:?HICLAW_REGISTRATION_TOKEN is required}\"\n : \"${HICLAW_ADMIN_USER:?HICLAW_ADMIN_USER is required}\"\n : \"${HICLAW_ADMIN_PASSWORD:?HICLAW_ADMIN_PASSWORD is required}\"\n log \"Cloud mode: validating environment... OK\"\n log \" Matrix: ${HICLAW_MATRIX_SERVER}, AI Gateway: ${HICLAW_AI_GATEWAY_URL}, OSS: ${HICLAW_STORAGE_BUCKET}\"\n ensure_mc_credentials || { log \"FATAL: Initial STS credential fetch failed\"; exit 1; }\nfi\n\n# ============================================================\n# Local mode: host symlinks, /etc/hosts, wait for local services\n# ============================================================\nif [ \"${HICLAW_RUNTIME}\" != \"aliyun\" ]; then\n # Create symlink for host directory access\n if [ -d \"/host-share\" ]; then\n ORIGINAL_HOST_HOME=\"${HOST_ORIGINAL_HOME:-$HOME}\"\n if [ ! -e \"${ORIGINAL_HOST_HOME}\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/root\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/data\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/host-share\" ]; then\n mkdir -p \"$(dirname \"${ORIGINAL_HOST_HOME}\")\"\n ln -sfn /host-share \"${ORIGINAL_HOST_HOME}\"\n log \"Created symlink: ${ORIGINAL_HOST_HOME} -> /host-share\"\n else\n ln -sfn /host-share /root/host-home\n log \"Created fallback symlink: /root/host-home -> /host-share\"\n fi\n fi\n\n # Add local domains to /etc/hosts\n HOSTS_DOMAINS=\"${MATRIX_DOMAIN%%:*} ${HICLAW_MATRIX_CLIENT_DOMAIN:-matrix-client-local.hiclaw.io} ${AI_GATEWAY_DOMAIN} ${HICLAW_FS_DOMAIN:-fs-local.hiclaw.io}\"\n if ! grep -q \"${AI_GATEWAY_DOMAIN}\" /etc/hosts 2>/dev/null; then\n echo \"127.0.0.1 ${HOSTS_DOMAINS}\" >> /etc/hosts\n log \"Added local domains to /etc/hosts\"\n fi\n\n # Wait for local infrastructure\n waitForService \"Higress Gateway\" \"127.0.0.1\" 8080 180\n waitForService \"Higress Console\" \"127.0.0.1\" 8001 180\n waitForService \"Tuwunel\" \"127.0.0.1\" 6167 120\n waitForHTTP \"Tuwunel Matrix API\" \"${HICLAW_MATRIX_SERVER}/_tuwunel/server_version\" 120\n waitForService \"MinIO\" \"127.0.0.1\" 9000 120\nelse\n # Cloud mode: wait for external Tuwunel\n log \"Waiting for Tuwunel Matrix server at ${HICLAW_MATRIX_SERVER}...\"\n _retry=0\n while [ \"${_retry}\" -lt 30 ]; do\n if curl -sf \"${HICLAW_MATRIX_SERVER}/_matrix/client/versions\" > /dev/null 2>&1; then\n log \"Tuwunel is ready\"\n break\n fi\n _retry=$((_retry + 1))\n log \" Waiting for Tuwunel (attempt ${_retry}/30)...\"\n sleep 5\n done\n if [ \"${_retry}\" -ge 30 ]; then\n log \"ERROR: Tuwunel not reachable at ${HICLAW_MATRIX_SERVER}\"\n exit 1\n fi\nfi\n\n# ============================================================\n# Auto-generate secrets if not provided via environment\n# Persisted to /data so they survive container restart\n# ============================================================\nSECRETS_FILE=\"/data/hiclaw-secrets.env\"\nif [ -f \"${SECRETS_FILE}\" ]; then\n source \"${SECRETS_FILE}\"\n log \"Loaded persisted secrets from ${SECRETS_FILE}\"\nfi\n\nif [ -z \"${HICLAW_MANAGER_GATEWAY_KEY}\" ]; then\n export HICLAW_MANAGER_GATEWAY_KEY=\"$(generateKey 32)\"\n log \"Auto-generated HICLAW_MANAGER_GATEWAY_KEY\"\nfi\nif [ -z \"${HICLAW_MANAGER_PASSWORD}\" ]; then\n export HICLAW_MANAGER_PASSWORD=\"$(generateKey 16)\"\n log \"Auto-generated HICLAW_MANAGER_PASSWORD\"\nfi\n\n# Persist secrets so they survive supervisord restart\nmkdir -p /data\ncat > \"${SECRETS_FILE}\" <<EOF\nexport HICLAW_MANAGER_GATEWAY_KEY=\"${HICLAW_MANAGER_GATEWAY_KEY}\"\nexport HICLAW_MANAGER_PASSWORD=\"${HICLAW_MANAGER_PASSWORD}\"\nEOF\nchmod 600 \"${SECRETS_FILE}\"\n\n# Cloud mode: pull workspace from OSS before initialization\nif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n HICLAW_FS=\"/root/hiclaw-fs\"\n mkdir -p \"${HICLAW_FS}/shared\" \"${HICLAW_FS}/agents\"\n log \"Pulling workspace from OSS...\"\n ensure_mc_credentials\n mc mirror \"${HICLAW_STORAGE_PREFIX}/manager/\" /root/manager-workspace/ --overwrite 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/shared/\" \"${HICLAW_FS}/shared/\" --overwrite 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/agents/\" \"${HICLAW_FS}/agents/\" --overwrite 2>/dev/null || true\n # Symlink hiclaw-fs into workspace for agent access\n ln -sfn \"${HICLAW_FS}\" /root/manager-workspace/hiclaw-fs\nfi\n\n# ============================================================\n# Initialize / upgrade Manager workspace\n# First boot: full init via upgrade-builtins.sh\n# Subsequent boots: compare image version; upgrade only if changed\n# ============================================================\nmkdir -p /root/manager-workspace\n\nIMAGE_VERSION=$(cat /opt/hiclaw/agent/.builtin-version 2>/dev/null || echo \"unknown\")\nINSTALLED_VERSION=$(cat /root/manager-workspace/.builtin-version 2>/dev/null || echo \"\")\n\nif [ ! -f /root/manager-workspace/.initialized ]; then\n log \"First boot: initializing manager workspace...\"\n bash /opt/hiclaw/scripts/init/upgrade-builtins.sh\n touch /root/manager-workspace/.initialized\n log \"Manager workspace initialized (version: ${IMAGE_VERSION})\"\nelif [ \"${IMAGE_VERSION}\" != \"${INSTALLED_VERSION}\" ] || [ \"${IMAGE_VERSION}\" = \"latest\" ]; then\n log \"Upgrade detected: ${INSTALLED_VERSION} -> ${IMAGE_VERSION}${IMAGE_VERSION:+ (latest: always upgrade)}\"\n bash /opt/hiclaw/scripts/init/upgrade-builtins.sh\n log \"Manager workspace upgraded to version: ${IMAGE_VERSION}\"\nelse\n log \"Workspace up to date (version: ${IMAGE_VERSION})\"\nfi\n\n# Local mode: wait for mc mirror initialization (shared + worker data in /root/hiclaw-fs/)\nif [ \"${HICLAW_RUNTIME}\" != \"aliyun\" ]; then\n log \"Waiting for MinIO storage initialization...\"\n while [ ! -f /root/hiclaw-fs/.initialized ]; do sleep 2; done\n log \"MinIO storage initialized\"\nfi\n\n# ============================================================\n# Register Matrix users via Registration API (single-step, no UIAA)\n# ============================================================\nlog \"Registering human admin Matrix account...\"\ncurl -sf -X POST ${HICLAW_MATRIX_SERVER}/_matrix/client/v3/register \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"username\": \"'\"${HICLAW_ADMIN_USER}\"'\",\n \"password\": \"'\"${HICLAW_ADMIN_PASSWORD}\"'\",\n \"auth\": {\n \"type\": \"m.login.registration_token\",\n \"token\": \"'\"${HICLAW_REGISTRATION_TOKEN}\"'\"\n }\n }' > /dev/null 2>&1 || log \"Admin account may already exist\"\n\nlog \"Registering Manager Agent Matrix account...\"\ncurl -sf -X POST ${HICLAW_MATRIX_SERVER}/_matrix/client/v3/register \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"username\": \"manager\",\n \"password\": \"'\"${HICLAW_MANAGER_PASSWORD}\"'\",\n \"auth\": {\n \"type\": \"m.login.registration_token\",\n \"token\": \"'\"${HICLAW_REGISTRATION_TOKEN}\"'\"\n }\n }' > /dev/null 2>&1 || log \"Manager account may already exist\"\n\n# Get Manager Agent's Matrix access token\nlog \"Obtaining Manager Matrix access token...\"\n_LOGIN_RESPONSE=$(curl -sf -X POST ${HICLAW_MATRIX_SERVER}/_matrix/client/v3/login \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"type\": \"m.login.password\",\n \"identifier\": {\"type\": \"m.id.user\", \"user\": \"manager\"},\n \"password\": \"'\"${HICLAW_MANAGER_PASSWORD}\"'\"\n }' 2>&1)\n_LOGIN_EXIT=$?\nlog \"Matrix login HTTP exit code: ${_LOGIN_EXIT}\"\nlog \"Matrix login response: ${_LOGIN_RESPONSE}\"\n\nMANAGER_TOKEN=$(echo \"${_LOGIN_RESPONSE}\" | jq -r '.access_token' 2>/dev/null)\n\nif [ -z \"${MANAGER_TOKEN}\" ] || [ \"${MANAGER_TOKEN}\" = \"null\" ]; then\n log \"ERROR: Failed to obtain Manager Matrix token (exit=${_LOGIN_EXIT})\"\n log \"ERROR: Login response was: ${_LOGIN_RESPONSE}\"\n exit 1\nfi\nlog \"Manager Matrix token obtained (token prefix: ${MANAGER_TOKEN:0:10}...)\"\n\n# ============================================================\n# Local mode: Initialize Higress Console + configure routes\n# Cloud mode: Create admin DM room + schedule welcome message\n# ============================================================\nif [ \"${HICLAW_RUNTIME}\" != \"aliyun\" ]; then\n COOKIE_FILE=\"/tmp/higress-session-cookie\"\n\n log \"Waiting for Higress Console to be fully ready and initializing admin...\"\n INIT_DONE=false\n for i in $(seq 1 90); do\n INIT_RESULT=$(curl -s -X POST http://127.0.0.1:8001/system/init \\\n -H 'Content-Type: application/json' \\\n -d '{\"adminUser\":{\"name\":\"'\"${HICLAW_ADMIN_USER}\"'\",\"password\":\"'\"${HICLAW_ADMIN_PASSWORD}\"'\",\"displayName\":\"'\"${HICLAW_ADMIN_USER}\"'\"}}' 2>/dev/null) || true\n if echo \"${INIT_RESULT}\" | grep -qE '\"success\":true|already.?init' 2>/dev/null; then\n INIT_DONE=true\n break\n fi\n if echo \"${INIT_RESULT}\" | grep -q '\"name\"' 2>/dev/null; then\n INIT_DONE=true\n break\n fi\n sleep 2\n done\n\n if [ \"${INIT_DONE}\" != \"true\" ]; then\n log \"ERROR: Higress Console did not become ready within 180s\"\n exit 1\n fi\n log \"Higress Console init done\"\n\n log \"Logging into Higress Console...\"\n LOGIN_OK=false\n for i in $(seq 1 10); do\n HTTP_CODE=$(curl -s -o /dev/null -w '%{http_code}' -X POST http://127.0.0.1:8001/session/login \\\n -H 'Content-Type: application/json' \\\n -c \"${COOKIE_FILE}\" \\\n -d '{\"username\":\"'\"${HICLAW_ADMIN_USER}\"'\",\"password\":\"'\"${HICLAW_ADMIN_PASSWORD}\"'\"}' 2>/dev/null) || true\n if { [ \"${HTTP_CODE}\" = \"200\" ] || [ \"${HTTP_CODE}\" = \"201\" ]; } && [ -f \"${COOKIE_FILE}\" ] && [ -s \"${COOKIE_FILE}\" ]; then\n LOGIN_OK=true\n break\n fi\n log \"Login attempt $i (HTTP ${HTTP_CODE}), retrying in 3s...\"\n sleep 3\n done\n\n if [ \"${LOGIN_OK}\" != \"true\" ]; then\n log \"ERROR: Could not login to Higress Console after retries\"\n exit 1\n fi\n log \"Higress Console login successful\"\n\n VERIFY_CODE=$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8001/v1/consumers -b \"${COOKIE_FILE}\" 2>/dev/null) || true\n if [ \"${VERIFY_CODE}\" = \"200\" ]; then\n log \"Console session verified (cookie valid)\"\n else\n log \"WARNING: Console session may be invalid (verify returned HTTP ${VERIFY_CODE})\"\n rm -f \"${COOKIE_FILE}\"\n for i in $(seq 1 5); do\n curl -s -o /dev/null -w '%{http_code}' -X POST http://127.0.0.1:8001/session/login \\\n -H 'Content-Type: application/json' \\\n -c \"${COOKIE_FILE}\" \\\n -d '{\"username\":\"'\"${HICLAW_ADMIN_USER}\"'\",\"password\":\"'\"${HICLAW_ADMIN_PASSWORD}\"'\"}' 2>/dev/null\n VERIFY2=$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8001/v1/consumers -b \"${COOKIE_FILE}\" 2>/dev/null) || true\n if [ \"${VERIFY2}\" = \"200\" ]; then\n log \"Re-login successful, session verified\"\n break\n fi\n sleep 2\n done\n fi\n\n export HIGRESS_COOKIE_FILE=\"${COOKIE_FILE}\"\n\n # Configure Higress routes, consumers, MCP servers\n /opt/hiclaw/scripts/init/setup-higress.sh\nelse\n # Cloud mode: create admin DM room and schedule welcome message\n MANAGER_FULL_ID=\"@manager:${MATRIX_DOMAIN}\"\n ADMIN_FULL_ID=\"@${HICLAW_ADMIN_USER}:${MATRIX_DOMAIN}\"\n\n log \"Logging in as admin to create DM room...\"\n _ADMIN_LOGIN=$(curl -sf -X POST \"${HICLAW_MATRIX_SERVER}/_matrix/client/v3/login\" \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"type\": \"m.login.password\",\n \"identifier\": {\"type\": \"m.id.user\", \"user\": \"'\"${HICLAW_ADMIN_USER}\"'\"},\n \"password\": \"'\"${HICLAW_ADMIN_PASSWORD}\"'\"\n }' 2>&1) || true\n\n ADMIN_MATRIX_TOKEN=$(echo \"${_ADMIN_LOGIN}\" | jq -r '.access_token // empty' 2>/dev/null)\n if [ -z \"${ADMIN_MATRIX_TOKEN}\" ]; then\n log \"WARNING: Failed to login as admin, skipping DM room creation\"\n else\n # Search for existing DM room with Manager (idempotent)\n DM_ROOM_ID=\"\"\n _JOINED_ROOMS=$(curl -sf \"${HICLAW_MATRIX_SERVER}/_matrix/client/v3/joined_rooms\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" 2>/dev/null \\\n | jq -r '.joined_rooms[]' 2>/dev/null) || true\n for _rid in ${_JOINED_ROOMS}; do\n _members=$(curl -sf \"${HICLAW_MATRIX_SERVER}/_matrix/client/v3/rooms/${_rid}/members\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" 2>/dev/null \\\n | jq -r '.chunk[].state_key' 2>/dev/null) || continue\n _count=$(echo \"${_members}\" | wc -l | xargs)\n if [ \"${_count}\" = \"2\" ] && echo \"${_members}\" | grep -q \"@manager:\"; then\n DM_ROOM_ID=\"${_rid}\"\n break\n fi\n done\n\n if [ -n \"${DM_ROOM_ID}\" ]; then\n log \"Existing DM room found: ${DM_ROOM_ID}\"\n else\n log \"Creating DM room with Manager...\"\n _RAW=$(curl -s -w '\\nHTTP_CODE:%{http_code}' -X POST \"${HICLAW_MATRIX_SERVER}/_matrix/client/v3/createRoom\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d \"{\\\"is_direct\\\":true,\\\"invite\\\":[\\\"${MANAGER_FULL_ID}\\\"],\\\"preset\\\":\\\"trusted_private_chat\\\"}\" 2>&1) || true\n _HTTP_CODE=$(echo \"${_RAW}\" | tail -1 | sed 's/HTTP_CODE://')\n _CREATE_RESP=$(echo \"${_RAW}\" | sed '$d')\n DM_ROOM_ID=$(echo \"${_CREATE_RESP}\" | jq -r '.room_id // empty' 2>/dev/null)\n if [ -n \"${DM_ROOM_ID}\" ]; then\n log \"DM room created: ${DM_ROOM_ID}\"\n else\n log \"WARNING: Failed to create DM room (HTTP ${_HTTP_CODE}): ${_CREATE_RESP}\"\n fi\n fi\n\n # Schedule welcome message in background (only on first boot)\n if [ -n \"${DM_ROOM_ID}\" ] && [ ! -f \"/root/manager-workspace/soul-configured\" ]; then\n log \"Scheduling welcome message (background, waiting for OpenClaw to start)...\"\n (\n _HICLAW_LANGUAGE=\"${HICLAW_LANGUAGE:-zh}\"\n _HICLAW_TIMEZONE=\"${TZ:-Asia/Shanghai}\"\n _wait=0\n _ready=false\n while [ \"${_wait}\" -lt 300 ]; do\n if curl -sf http://127.0.0.1:18799/ > /dev/null 2>&1; then\n _ready=true\n break\n fi\n sleep 3\n _wait=$((_wait + 3))\n done\n if [ \"${_ready}\" != \"true\" ]; then\n echo \"[cloud-manager] WARNING: OpenClaw gateway not ready within 300s, skipping welcome message\"\n exit 0\n fi\n _welcome_msg=\"This is an automated message from the HiClaw cloud deployment. This is a fresh installation.\n\n--- Installation Context ---\nUser Language: ${_HICLAW_LANGUAGE} (zh = Chinese, en = English)\nUser Timezone: ${_HICLAW_TIMEZONE} (IANA timezone identifier)\n---\n\nYou are an AI agent that manages a team of worker agents. Your identity and personality have not been configured yet — the human admin is about to meet you for the first time.\n\nPlease begin the onboarding conversation:\n\n1. Greet the admin warmly and briefly describe what you can do (coordinate workers, manage tasks, run multi-agent projects)\n2. The user has selected \\\"${_HICLAW_LANGUAGE}\\\" as their preferred language during installation. Use this language for your greeting and all subsequent communication.\n3. The user's timezone is ${_HICLAW_TIMEZONE}. Based on this timezone, you may infer their likely region and suggest additional language options.\n4. Ask them: a) What would they like to call you? b) Communication style preference? c) Any behavior guidelines? d) Confirm default language\n5. After they reply, write their preferences to ~/SOUL.md\n6. Confirm what you wrote, and ask if they would like to adjust anything\n7. Once confirmed, run: touch ~/soul-configured\n\nThe human admin will start chatting shortly.\"\n _txn_id=\"welcome-cloud-$(date +%s)\"\n _payload=$(jq -nc --arg body \"${_welcome_msg}\" '{\"msgtype\":\"m.text\",\"body\":$body}')\n _raw=$(curl -s -w '\\nHTTP_CODE:%{http_code}' -X PUT \"${HICLAW_MATRIX_SERVER}/_matrix/client/v3/rooms/${DM_ROOM_ID}/send/m.room.message/${_txn_id}\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d \"${_payload}\" 2>&1) || true\n _http_code=$(echo \"${_raw}\" | tail -1 | sed 's/HTTP_CODE://')\n _send_resp=$(echo \"${_raw}\" | sed '$d')\n if echo \"${_send_resp}\" | jq -e '.event_id' > /dev/null 2>&1; then\n echo \"[cloud-manager] Welcome message sent to DM room\"\n else\n echo \"[cloud-manager] WARNING: Failed to send welcome message (HTTP ${_http_code}): ${_send_resp}\"\n fi\n ) &\n log \"Welcome message background process started (PID: $!)\"\n fi\n fi\nfi\n\n# ============================================================\n# Generate Manager Agent openclaw.json from template\n# ============================================================\nlog \"Generating Manager openclaw.json...\"\nexport MANAGER_MATRIX_TOKEN=\"${MANAGER_TOKEN}\"\nexport MANAGER_GATEWAY_KEY=\"${HICLAW_MANAGER_GATEWAY_KEY}\"\nexport MANAGER_HOOKS_TOKEN=$(echo -n \"${HICLAW_MANAGER_GATEWAY_KEY}-hooks\" | base64 -w 0)\n\n# Resolve model parameters based on model name\nMODEL_NAME=\"${HICLAW_DEFAULT_MODEL:-qwen3.5-plus}\"\ncase \"${MODEL_NAME}\" in\n gpt-5.3-codex|gpt-5-mini|gpt-5-nano)\n export MODEL_CONTEXT_WINDOW=400000 MODEL_MAX_TOKENS=128000 ;;\n claude-opus-4-6)\n export MODEL_CONTEXT_WINDOW=1000000 MODEL_MAX_TOKENS=128000 ;;\n claude-sonnet-4-6)\n export MODEL_CONTEXT_WINDOW=1000000 MODEL_MAX_TOKENS=64000 ;;\n claude-haiku-4-5)\n export MODEL_CONTEXT_WINDOW=200000 MODEL_MAX_TOKENS=64000 ;;\n qwen3.5-plus)\n export MODEL_CONTEXT_WINDOW=200000 MODEL_MAX_TOKENS=64000 ;;\n deepseek-chat|deepseek-reasoner|kimi-k2.5)\n export MODEL_CONTEXT_WINDOW=256000 MODEL_MAX_TOKENS=128000 ;;\n glm-5|MiniMax-M2.5)\n export MODEL_CONTEXT_WINDOW=200000 MODEL_MAX_TOKENS=128000 ;;\n *)\n export MODEL_CONTEXT_WINDOW=150000 MODEL_MAX_TOKENS=128000 ;;\nesac\nexport MODEL_REASONING=true\n\n# Override with user-supplied custom model parameters from env (set during install)\n[ -n \"${HICLAW_MODEL_CONTEXT_WINDOW:-}\" ] && export MODEL_CONTEXT_WINDOW=\"${HICLAW_MODEL_CONTEXT_WINDOW}\"\n[ -n \"${HICLAW_MODEL_MAX_TOKENS:-}\" ] && export MODEL_MAX_TOKENS=\"${HICLAW_MODEL_MAX_TOKENS}\"\n[ -n \"${HICLAW_MODEL_REASONING:-}\" ] && export MODEL_REASONING=\"${HICLAW_MODEL_REASONING}\"\n\n# E2EE: convert HICLAW_MATRIX_E2EE to JSON boolean for template substitution\nif [ \"${HICLAW_MATRIX_E2EE:-0}\" = \"1\" ] || [ \"${HICLAW_MATRIX_E2EE:-}\" = \"true\" ]; then\n export MATRIX_E2EE_ENABLED=true\nelse\n export MATRIX_E2EE_ENABLED=false\nfi\nlog \"Matrix E2EE: ${MATRIX_E2EE_ENABLED}\"\n\n# Resolve input modalities: only vision-capable models get \"image\"\ncase \"${MODEL_NAME}\" in\n gpt-5.4|gpt-5.3-codex|gpt-5-mini|gpt-5-nano|claude-opus-4-6|claude-sonnet-4-6|claude-haiku-4-5|qwen3.5-plus|kimi-k2.5)\n export MODEL_INPUT='[\"text\", \"image\"]' ;;\n *)\n export MODEL_INPUT='[\"text\"]' ;;\nesac\n# Override with user-supplied vision setting from env\nif [ \"${HICLAW_MODEL_VISION:-}\" = \"true\" ]; then\n export MODEL_INPUT='[\"text\", \"image\"]'\nelif [ \"${HICLAW_MODEL_VISION:-}\" = \"false\" ]; then\n export MODEL_INPUT='[\"text\"]'\nfi\n\nlog \"Model: ${MODEL_NAME} (context=${MODEL_CONTEXT_WINDOW}, maxTokens=${MODEL_MAX_TOKENS}, reasoning=${MODEL_REASONING}, input=${MODEL_INPUT})\"\n\nif [ -f /root/manager-workspace/openclaw.json ]; then\n log \"Manager openclaw.json already exists, updating dynamic fields only (preserving user customizations)...\"\n # Merge known models into existing config (add missing, preserve user-added)\n # Use known-models.json (valid JSON) instead of template (contains ${VAR} placeholders)\n KNOWN_MODELS=$(cat /opt/hiclaw/configs/known-models.json 2>/dev/null || echo '[]')\n jq --arg token \"${MANAGER_TOKEN}\" \\\n --arg key \"${HICLAW_MANAGER_GATEWAY_KEY}\" \\\n --arg model \"${MODEL_NAME}\" \\\n --argjson e2ee \"${MATRIX_E2EE_ENABLED}\" \\\n --argjson known_models \"${KNOWN_MODELS}\" \\\n --argjson ctx \"${MODEL_CONTEXT_WINDOW}\" \\\n --argjson max \"${MODEL_MAX_TOKENS}\" \\\n --argjson reasoning \"${MODEL_REASONING}\" \\\n --argjson input \"${MODEL_INPUT}\" \\\n '\n # Merge known models: add any model id not already present\n .models.providers[\"hiclaw-gateway\"].models as $existing\n | ($existing | map(.id)) as $existing_ids\n | ($known_models | map(select(.id as $id | $existing_ids | index($id) | not))) as $new\n | .models.providers[\"hiclaw-gateway\"].models = ($existing + $new)\n # Ensure the user-chosen default model is in the list (custom model support)\n | if (.models.providers[\"hiclaw-gateway\"].models | map(.id) | index($model) | not) then\n .models.providers[\"hiclaw-gateway\"].models += [{\"id\": $model, \"name\": $model, \"reasoning\": $reasoning, \"contextWindow\": $ctx, \"maxTokens\": $max, \"input\": $input}]\n else . end\n # Rebuild model aliases from the full models list\n | (.models.providers[\"hiclaw-gateway\"].models | map({ (\"hiclaw-gateway/\" + .id): { \"alias\": .id } }) | add // {}) as $aliases\n | .agents.defaults.models = ((.agents.defaults.models // {}) + $aliases)\n | .channels.matrix.accessToken = $token | .hooks.token = ($key + \"-hooks\" | @base64) | .models.providers[\"hiclaw-gateway\"].apiKey = $key\n | .agents.defaults.model.primary = (\"hiclaw-gateway/\" + $model)\n | .commands.restart = true\n | .gateway.controlUi.dangerouslyDisableDeviceAuth = true\n | .channels.matrix.encryption = $e2ee\n ' \\\n /root/manager-workspace/openclaw.json > /tmp/openclaw.json.tmp && \\\n mv /tmp/openclaw.json.tmp /root/manager-workspace/openclaw.json\n # Verify the token was written correctly\n _written_token=$(jq -r '.channels.matrix.accessToken' /root/manager-workspace/openclaw.json 2>/dev/null)\n if [ -z \"${_written_token}\" ] || [ \"${_written_token}\" = \"null\" ]; then\n log \"ERROR: Matrix token was not written correctly to openclaw.json (got: ${_written_token})\"\n else\n log \"Matrix token written to openclaw.json (prefix: ${_written_token:0:10}...)\"\n fi\nelse\n log \"Manager openclaw.json not found, generating from template...\"\n envsubst < /opt/hiclaw/configs/manager-openclaw.json.tmpl > /root/manager-workspace/openclaw.json\n # Inject custom model if not in the built-in list\n if ! jq -e --arg model \"${MODEL_NAME}\" '.models.providers[\"hiclaw-gateway\"].models | map(.id) | index($model)' /root/manager-workspace/openclaw.json > /dev/null 2>&1; then\n log \"Custom model '${MODEL_NAME}' not in built-in list, injecting into config...\"\n jq --arg model \"${MODEL_NAME}\" \\\n --argjson ctx \"${MODEL_CONTEXT_WINDOW}\" \\\n --argjson max \"${MODEL_MAX_TOKENS}\" \\\n --argjson reasoning \"${MODEL_REASONING}\" \\\n --argjson input \"${MODEL_INPUT}\" \\\n '\n .models.providers[\"hiclaw-gateway\"].models += [{\"id\": $model, \"name\": $model, \"reasoning\": $reasoning, \"contextWindow\": $ctx, \"maxTokens\": $max, \"input\": $input}]\n | .agents.defaults.models += {(\"hiclaw-gateway/\" + $model): {\"alias\": $model}}\n ' /root/manager-workspace/openclaw.json > /tmp/openclaw.json.tmp && \\\n mv /tmp/openclaw.json.tmp /root/manager-workspace/openclaw.json\n fi\n _written_token=$(jq -r '.channels.matrix.accessToken' /root/manager-workspace/openclaw.json 2>/dev/null)\n log \"Matrix token written from template (prefix: ${_written_token:0:10}...)\"\nfi\n\n# Cloud mode: overlay cloud-specific settings onto generated config\nif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n log \"Applying cloud overlay to openclaw.json...\"\n jq --arg homeserver \"${HICLAW_MATRIX_SERVER}\" \\\n --arg gateway \"${HICLAW_AI_GATEWAY_URL}/v1\" \\\n --arg key \"${HICLAW_MANAGER_GATEWAY_KEY}\" \\\n '.channels.matrix.homeserver = $homeserver\n | .models.providers[\"hiclaw-gateway\"].baseUrl = $gateway\n | .models.providers[\"hiclaw-gateway\"].apiKey = $key\n | .hooks.token = ($key + \"-hooks\" | @base64)\n | .commands.restart = false' \\\n /root/manager-workspace/openclaw.json > /tmp/openclaw-cloud.json && \\\n mv /tmp/openclaw-cloud.json /root/manager-workspace/openclaw.json\n log \"Cloud overlay applied\"\nfi\n\n# ============================================================\n# Detect container runtime (for Worker creation)\n# ============================================================\nsource /opt/hiclaw/scripts/lib/container-api.sh\nif container_api_available; then\n log \"Container runtime socket detected at ${CONTAINER_SOCKET} — direct Worker creation enabled\"\n export HICLAW_CONTAINER_RUNTIME=\"socket\"\nelif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n log \"Cloud mode — Workers created via SAE API\"\n export HICLAW_CONTAINER_RUNTIME=\"cloud\"\nelse\n log \"No container runtime found — Worker creation will output install commands\"\n export HICLAW_CONTAINER_RUNTIME=\"none\"\nfi\n\n# ============================================================\n# Upgrade Worker openclaw.json: merge known models + E2EE flag into existing configs\n# Existing workers in MinIO may have old single-model configs or missing encryption field.\n# Merge template models so they can hot-switch without restart.\n# ============================================================\nREGISTRY_FILE=\"/root/manager-workspace/workers-registry.json\"\nif [ -f \"${REGISTRY_FILE}\" ]; then\n # Use known-models.json (valid JSON) instead of template (contains ${VAR} placeholders)\n KNOWN_MODELS_FILE=\"/opt/hiclaw/configs/known-models.json\"\n if [ -f \"${KNOWN_MODELS_FILE}\" ]; then\n _KNOWN_MODELS=$(cat \"${KNOWN_MODELS_FILE}\")\n for _wname in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_wname}\" ] && continue\n _minio_path=\"${HICLAW_STORAGE_PREFIX}/agents/${_wname}/openclaw.json\"\n _tmp_in=\"/tmp/openclaw-${_wname}-models-upgrade-in.json\"\n if mc cp \"${_minio_path}\" \"${_tmp_in}\" 2>/dev/null; then\n _tmp_out=\"/tmp/openclaw-${_wname}-models-upgrade-out.json\"\n # Idempotent merge: add missing known models, rebuild aliases, set e2ee.\n # Always runs — jq deduplicates by model id, so re-runs are safe.\n jq --argjson known_models \"${_KNOWN_MODELS}\" \\\n --argjson e2ee \"${MATRIX_E2EE_ENABLED}\" '\n .models.providers[\"hiclaw-gateway\"].models as $existing\n | ($existing | map(.id)) as $existing_ids\n | ($known_models | map(select(.id as $id | $existing_ids | index($id) | not))) as $new\n | .models.providers[\"hiclaw-gateway\"].models = ($existing + $new)\n | (.models.providers[\"hiclaw-gateway\"].models | map({ (\"hiclaw-gateway/\" + .id): { \"alias\": .id } }) | add // {}) as $aliases\n | .agents.defaults.models = ((.agents.defaults.models // {}) + $aliases)\n | .channels.matrix.encryption = $e2ee\n ' \"${_tmp_in}\" > \"${_tmp_out}\" 2>/dev/null\n if ! diff -q \"${_tmp_in}\" \"${_tmp_out}\" > /dev/null 2>&1; then\n if mc cp \"${_tmp_out}\" \"${_minio_path}\" 2>/dev/null; then\n _new_count=$(jq '.models.providers[\"hiclaw-gateway\"].models | length' \"${_tmp_out}\" 2>/dev/null)\n log \"Worker ${_wname}: upgraded openclaw.json (models: ${_new_count}, e2ee: ${MATRIX_E2EE_ENABLED})\"\n fi\n fi\n rm -f \"${_tmp_in}\" \"${_tmp_out}\"\n fi\n done\n fi\nfi\n\n# ============================================================\n# Ensure Worker Matrix password files exist in MinIO (E2EE fix)\n# Workers need to re-login on restart to get a fresh device_id.\n# Older workers created before this fix won't have the password file.\n# ============================================================\nif [ -f \"${REGISTRY_FILE}\" ]; then\n for _wname in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_wname}\" ] && continue\n _creds_file=\"/data/worker-creds/${_wname}.env\"\n if [ -f \"${_creds_file}\" ]; then\n # Check if password file already exists in MinIO\n if ! mc stat \"${HICLAW_STORAGE_PREFIX}/agents/${_wname}/credentials/matrix/password\" > /dev/null 2>&1; then\n source \"${_creds_file}\"\n if [ -n \"${WORKER_PASSWORD}\" ]; then\n _tmp_pw=\"/tmp/matrix-pw-${_wname}\"\n echo -n \"${WORKER_PASSWORD}\" > \"${_tmp_pw}\"\n mc cp \"${_tmp_pw}\" \"${HICLAW_STORAGE_PREFIX}/agents/${_wname}/credentials/matrix/password\" 2>/dev/null \\\n && log \"Worker ${_wname}: wrote Matrix password to MinIO (E2EE re-login fix)\" \\\n || log \"Worker ${_wname}: WARNING: failed to write Matrix password to MinIO\"\n rm -f \"${_tmp_pw}\"\n fi\n fi\n fi\n done\nfi\n\n# ============================================================\n# Recreate Worker containers as needed after Manager restart.\n# Manager IP may change on restart; Workers use ExtraHosts pointing to Manager IP,\n# so any worker whose ExtraHosts IP no longer matches must be recreated.\n# ============================================================\nif container_api_available; then\n REGISTRY_FILE=\"/root/manager-workspace/workers-registry.json\"\n _manager_ip=$(container_get_manager_ip)\n if [ -f \"${REGISTRY_FILE}\" ]; then\n for _worker_name in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_worker_name}\" ] && continue\n\n # Skip remote workers — they are not Manager-managed containers.\n _deployment=$(jq -r --arg w \"${_worker_name}\" '.workers[$w].deployment // \"local\"' \"${REGISTRY_FILE}\" 2>/dev/null)\n if [ \"${_deployment}\" = \"remote\" ]; then\n log \"Worker ${_worker_name} is remote, skipping container recreate\"\n continue\n fi\n\n _status=$(container_status_worker \"${_worker_name}\")\n if [ \"${_status}\" = \"running\" ]; then\n # Check if ExtraHosts IP still matches current Manager IP.\n # If ExtraHosts is empty the worker uses real DNS — no recreate needed.\n _inspect=$(_api GET \"/containers/${WORKER_CONTAINER_PREFIX}${_worker_name}/json\" 2>/dev/null)\n _extra_hosts_len=$(echo \"${_inspect}\" | jq -r '.HostConfig.ExtraHosts | length' 2>/dev/null)\n if [ -z \"${_extra_hosts_len}\" ] || [ \"${_extra_hosts_len}\" = \"0\" ]; then\n log \"Worker running (no ExtraHosts, real DNS): ${_worker_name}, skipping\"\n continue\n fi\n _worker_ip=$(echo \"${_inspect}\" | jq -r '.HostConfig.ExtraHosts[0]' 2>/dev/null | cut -d: -f2)\n if [ \"${_worker_ip}\" = \"${_manager_ip}\" ]; then\n log \"Worker running with current Manager IP (${_manager_ip}): ${_worker_name}, skipping\"\n continue\n fi\n log \"Worker running but Manager IP changed (${_worker_ip} -> ${_manager_ip}): ${_worker_name}, recreating...\"\n else\n # Container missing or stopped — always recreate.\n log \"Worker container ${_status}: ${_worker_name}, recreating...\"\n fi\n _creds_file=\"/data/worker-creds/${_worker_name}.env\"\n if [ -f \"${_creds_file}\" ]; then\n source \"${_creds_file}\"\n _runtime=$(jq -r --arg w \"${_worker_name}\" '.workers[$w].runtime // \"openclaw\"' \"${REGISTRY_FILE}\" 2>/dev/null)\n _recreated=false\n for _attempt in 1 2 3; do\n if [ \"${_runtime}\" = \"copaw\" ]; then\n container_create_copaw_worker \"${_worker_name}\" \"${_worker_name}\" \"${WORKER_MINIO_PASSWORD}\" 2>&1 && _recreated=true && break\n else\n container_create_worker \"${_worker_name}\" \"${_worker_name}\" \"${WORKER_MINIO_PASSWORD}\" 2>&1 && _recreated=true && break\n fi\n log \" Attempt ${_attempt}/3 failed for ${_worker_name}, retrying in $((5 * _attempt))s...\"\n sleep $((5 * _attempt))\n done\n if [ \"${_recreated}\" = true ]; then\n log \" Recreated ${_runtime} worker: ${_worker_name}\"\n else\n log \" ERROR: Failed to recreate ${_worker_name} after 3 attempts\"\n fi\n else\n log \" WARNING: No credentials found for ${_worker_name} (${_creds_file} missing), skipping\"\n fi\n done\n fi\nfi\n\n# ============================================================\n# Notify workers of builtin updates if upgrade happened\n# Builtin files (AGENTS.md, skills) are already synced by upgrade-builtins.sh\n#\n# Cooldown: skip notification if the last successful notify was within\n# NOTIFY_COOLDOWN_SECS (default 3600s / 1 hour). This prevents repeated\n# notifications when the Manager crash-loops and re-runs upgrade-builtins\n# on every restart (e.g. IMAGE_VERSION=latest always triggers upgrade).\n# ============================================================\nNOTIFY_COOLDOWN_SECS=\"${HICLAW_NOTIFY_COOLDOWN_SECS:-3600}\"\nNOTIFY_TS_FILE=\"/root/manager-workspace/.last-worker-notify-ts\"\n\nif [ -f /root/manager-workspace/.upgrade-pending-worker-notify ]; then\n _now=$(date +%s)\n _last_notify=$(cat \"${NOTIFY_TS_FILE}\" 2>/dev/null || echo \"0\")\n _elapsed=$(( _now - _last_notify ))\n\n if [ \"${_elapsed}\" -lt \"${NOTIFY_COOLDOWN_SECS}\" ]; then\n log \"Skipping worker builtin notification (last notify ${_elapsed}s ago, cooldown ${NOTIFY_COOLDOWN_SECS}s)\"\n rm -f /root/manager-workspace/.upgrade-pending-worker-notify\n else\n log \"Notifying workers about builtin updates...\"\n REGISTRY_FILE=\"/root/manager-workspace/workers-registry.json\"\n _notify_ok=false\n if [ -f \"${REGISTRY_FILE}\" ]; then\n for _worker_name in $(jq -r '.workers | keys[]' \"${REGISTRY_FILE}\" 2>/dev/null); do\n [ -z \"${_worker_name}\" ] && continue\n _room_id=$(jq -r --arg w \"${_worker_name}\" '.workers[$w].room_id // empty' \"${REGISTRY_FILE}\" 2>/dev/null)\n if [ -n \"${_room_id}\" ]; then\n _worker_id=\"@${_worker_name}:${MATRIX_DOMAIN}\"\n _txn_id=\"upgrade-$(date +%s%N)\"\n _msg=\"@${_worker_name}:${MATRIX_DOMAIN} Manager upgraded builtin files (AGENTS.md, skills). Please use your file-sync skill to sync the latest config.\"\n _raw=$(curl -s -w '\\nHTTP_CODE:%{http_code}' -X PUT \\\n \"${HICLAW_MATRIX_SERVER}/_matrix/client/v3/rooms/${_room_id}/send/m.room.message/${_txn_id}\" \\\n -H \"Authorization: Bearer ${MANAGER_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d \"{\\\"msgtype\\\":\\\"m.text\\\",\\\"body\\\":\\\"${_msg}\\\",\\\"m.mentions\\\":{\\\"user_ids\\\":[\\\"${_worker_id}\\\"]}}\" \\\n 2>&1) || true\n _http_code=$(echo \"${_raw}\" | tail -1 | sed 's/HTTP_CODE://')\n _notify_resp=$(echo \"${_raw}\" | sed '$d')\n if echo \"${_notify_resp}\" | jq -e '.event_id' > /dev/null 2>&1; then\n log \" Notified ${_worker_name}\"; _notify_ok=true\n else\n log \" WARNING: Failed to notify ${_worker_name} (HTTP ${_http_code}): ${_notify_resp}\"\n fi\n fi\n done\n fi\n # Record timestamp only if at least one notification succeeded\n if [ \"${_notify_ok}\" = true ]; then\n echo \"${_now}\" > \"${NOTIFY_TS_FILE}\"\n fi\n rm -f /root/manager-workspace/.upgrade-pending-worker-notify\n fi\nfi\n\n# ============================================================\n# Start OpenClaw Manager Agent\n# ============================================================\nlog \"Starting Manager Agent (OpenClaw)...\"\n\n# HOME is already set to /root/manager-workspace via docker run -e HOME=...\nexport OPENCLAW_CONFIG_PATH=\"/root/manager-workspace/openclaw.json\"\n\n# Symlink to default OpenClaw config path so CLI commands find the config\nmkdir -p \"${HOME}/.openclaw\"\nln -sf \"/root/manager-workspace/openclaw.json\" \"${HOME}/.openclaw/openclaw.json\"\n\n# Ensure host credential symlinks exist under HOME\nif [ -d \"/host-share\" ]; then\n [ -f \"/host-share/.gitconfig\" ] && ln -sf \"/host-share/.gitconfig\" \"${HOME}/.gitconfig\"\nfi\n\nlog \"HOME=${HOME} (manager-workspace, host-mounted)\"\ncd \"${HOME}\"\n\n# Clean orphaned session write locks (e.g. from SIGKILL or crash before exit handlers)\n# Prevents \"session file locked (timeout 10000ms)\" when PID was reused\nfind \"${HOME}/.openclaw/agents\" -name \"*.jsonl.lock\" -delete 2>/dev/null || true\nlog \"Cleaned up any orphaned session write locks\"\n\n# Clean Matrix crypto storage (SQLite WAL may be corrupted after unclean shutdown)\n# Crypto state is re-negotiated on startup; losing it only means re-establishing E2EE sessions\nrm -rf \"${HOME}/.openclaw/matrix\" 2>/dev/null || true\nlog \"Cleaned Matrix crypto storage (will re-establish E2EE sessions)\"\n\n# ── Render agent doc templates ────────────────────────────────────────────\n# Replace ${VAR} placeholders with actual values so the AI agent reads\n# plain text and never needs to resolve environment variables.\nexport MANAGER_MATRIX_TOKEN MANAGER_TOKEN HIGRESS_COOKIE_FILE\nRENDER=/opt/hiclaw/scripts/lib/render-skills.sh\nlog \"Rendering agent doc templates...\"\n# Manager-owned docs (workspace)\nbash \"$RENDER\" /root/manager-workspace/skills\nbash \"$RENDER\" /root/manager-workspace/skills-alpha\nbash \"$RENDER\" /root/manager-workspace AGENTS.md TOOLS.md HEARTBEAT.md SOUL.md\n# Worker templates (workspace + image) — rendered before push to MinIO\n# so Workers (including remote pip-install) receive plain text\nbash \"$RENDER\" /root/manager-workspace/worker-skills\nbash \"$RENDER\" /root/manager-workspace/worker-agent\nbash \"$RENDER\" /root/manager-workspace/copaw-worker-agent\nbash \"$RENDER\" /opt/hiclaw/agent/worker-skills\nbash \"$RENDER\" /opt/hiclaw/agent/worker-agent\nbash \"$RENDER\" /opt/hiclaw/agent/copaw-worker-agent\nlog \"Agent doc templates rendered\"\n\n# Cloud mode: start background file sync (workspace ↔ OSS) and initial push\nif [ \"${HICLAW_RUNTIME}\" = \"aliyun\" ]; then\n log \"Syncing initial workspace to OSS...\"\n ensure_mc_credentials\n mc mirror /root/manager-workspace/ \"${HICLAW_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" 2>/dev/null || true\n\n # Local → OSS: change-triggered sync\n (\n while true; do\n CHANGED=$(find /root/manager-workspace/ -type f -newermt \"15 seconds ago\" 2>/dev/null | head -1)\n if [ -n \"${CHANGED}\" ]; then\n ensure_mc_credentials 2>/dev/null || true\n mc mirror /root/manager-workspace/ \"${HICLAW_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" --exclude \".npm/**\" \\\n --exclude \".local/**\" --exclude \".mc/**\" 2>/dev/null || true\n fi\n sleep 10\n done\n ) &\n log \"Local→OSS sync started (PID: $!)\"\n\n # OSS → Local: periodic pull (shared data, agent configs)\n (\n while true; do\n sleep 300\n ensure_mc_credentials 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/shared/\" /root/hiclaw-fs/shared/ --overwrite --newer-than \"5m\" 2>/dev/null || true\n mc mirror \"${HICLAW_STORAGE_PREFIX}/agents/\" /root/hiclaw-fs/agents/ --overwrite --newer-than \"5m\" 2>/dev/null || true\n done\n ) &\n log \"OSS→Local sync started (every 5m, PID: $!)\"\nfi\n\n# Launch OpenClaw\nexec openclaw gateway run --verbose --force\n"}, "1145f796b3de8b9e07c298add26d0d825c48dc39": {"t": 1786067819.577584, "n": 64818, "h": "854fc78a9c23ede8", "c": "#!/bin/bash\n# start-manager-agent.sh - Initialize and start the Manager Agent\n# Supports local (supervisord), cloud (SAE), and K8s (Helm) deployments.\n# In local mode this is the last supervisord component to start (priority 800).\n# In cloud/k8s mode (AGENTTEAMS_RUNTIME=aliyun|k8s) this is the container entrypoint.\n#\n# Runtime selection:\n# AGENTTEAMS_MANAGER_RUNTIME=openclaw (default) - OpenClaw gateway mode\n# AGENTTEAMS_MANAGER_RUNTIME=copaw - CoPaw workspace mode\n# (hermes runtime is supported for Workers only; Managers run openclaw or copaw.)\n\nsource /opt/agentteams/scripts/lib/agentteams-env.sh\n\n# ============================================================\n# Runtime selection\n# ============================================================\nMANAGER_RUNTIME=\"${AGENTTEAMS_MANAGER_RUNTIME:-openclaw}\"\ncase \"${MANAGER_RUNTIME}\" in\n copaw)\n log \"Manager runtime: CoPaw (Python workspace)\"\n ;;\n *)\n log \"Manager runtime: OpenClaw (Node.js gateway)\"\n MANAGER_RUNTIME=\"openclaw\"\n ;;\nesac\n\n# ============================================================\n# Set timezone from TZ env var\n# ============================================================\nif [ -n \"${TZ}\" ] && [ -f \"/usr/share/zoneinfo/${TZ}\" ]; then\n ln -sf \"/usr/share/zoneinfo/${TZ}\" /etc/localtime\n echo \"${TZ}\" > /etc/timezone\n log \"Timezone set to ${TZ}\"\nfi\n\nexport MATRIX_DOMAIN=\"${AGENTTEAMS_MATRIX_DOMAIN:-matrix-local.agentteams.io:8080}\"\nAI_GATEWAY_DOMAIN=\"${AGENTTEAMS_AI_GATEWAY_DOMAIN:-aigw-local.agentteams.io}\"\n\n# ============================================================\n# YOLO mode promotion\n# ============================================================\n# In embedded mode the controller does not propagate AGENTTEAMS_YOLO to the\n# manager container, but installer / test scripts touch a marker file at\n# `${WORKSPACE}/yolo-mode` instead. Promote that marker to the env var so the\n# agent's documented YOLO check (`AGENTTEAMS_YOLO=1`) reliably detects it without\n# depending on filesystem lookups during a turn.\nif [ -z \"${AGENTTEAMS_YOLO:-}\" ] && [ -f /root/manager-workspace/yolo-mode ]; then\n export AGENTTEAMS_YOLO=1\n log \"YOLO mode marker detected at /root/manager-workspace/yolo-mode; AGENTTEAMS_YOLO=1 exported\"\nfi\n\n# ============================================================\n# Cloud/K8s mode: validate required environment variables + initial credentials\n# ============================================================\nif [ \"${AGENTTEAMS_RUNTIME}\" = \"aliyun\" ] || [ \"${AGENTTEAMS_RUNTIME}\" = \"k8s\" ]; then\n : \"${AGENTTEAMS_MATRIX_URL:?AGENTTEAMS_MATRIX_URL is required}\"\n : \"${AGENTTEAMS_MATRIX_DOMAIN:?AGENTTEAMS_MATRIX_DOMAIN is required}\"\n : \"${AGENTTEAMS_AI_GATEWAY_URL:?AGENTTEAMS_AI_GATEWAY_URL is required}\"\n if [ \"${AGENTTEAMS_RUNTIME}\" = \"aliyun\" ]; then\n : \"${AGENTTEAMS_MANAGER_GATEWAY_KEY:?AGENTTEAMS_MANAGER_GATEWAY_KEY is required}\"\n : \"${AGENTTEAMS_MANAGER_PASSWORD:?AGENTTEAMS_MANAGER_PASSWORD is required (cloud containers are stateless, password must be injected)}\"\n fi\n if [ \"${AGENTTEAMS_RUNTIME}\" = \"k8s\" ]; then\n # K8s mode: controller handles initialization (admin registration, Higress setup).\n # Manager only needs credentials injected by the ManagerReconciler.\n : \"${AGENTTEAMS_MANAGER_GATEWAY_KEY:?AGENTTEAMS_MANAGER_GATEWAY_KEY is required (injected by controller)}\"\n # AGENTTEAMS_MANAGER_PASSWORD is optional: not needed in AppService mode\n # (token obtained via AS login), only required in legacy password mode.\n else\n # Cloud (aliyun) mode: Manager still does its own initialization\n : \"${AGENTTEAMS_REGISTRATION_TOKEN:?AGENTTEAMS_REGISTRATION_TOKEN is required}\"\n : \"${AGENTTEAMS_ADMIN_USER:?AGENTTEAMS_ADMIN_USER is required}\"\n : \"${AGENTTEAMS_ADMIN_PASSWORD:?AGENTTEAMS_ADMIN_PASSWORD is required}\"\n fi\n log \"${AGENTTEAMS_RUNTIME} mode: validating environment... OK\"\n log \" Matrix: ${AGENTTEAMS_MATRIX_URL}, AI Gateway: ${AGENTTEAMS_AI_GATEWAY_URL}, Storage: ${AGENTTEAMS_FS_BUCKET}\"\n if [ \"${AGENTTEAMS_RUNTIME}\" = \"aliyun\" ]; then\n ensure_mc_credentials || { log \"FATAL: Initial STS credential fetch failed\"; exit 1; }\n fi\nfi\n\n# ============================================================\n# Local mode: host symlinks, /etc/hosts, wait for local services\n# ============================================================\nif [ \"${AGENTTEAMS_RUNTIME}\" != \"aliyun\" ] && [ \"${AGENTTEAMS_RUNTIME}\" != \"k8s\" ]; then\n # Create symlink for host directory access\n if [ -d \"/host-share\" ]; then\n ORIGINAL_HOST_HOME=\"${HOST_ORIGINAL_HOME:-$HOME}\"\n if [ ! -e \"${ORIGINAL_HOST_HOME}\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/root\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/data\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/host-share\" ]; then\n mkdir -p \"$(dirname \"${ORIGINAL_HOST_HOME}\")\"\n ln -sfn /host-share \"${ORIGINAL_HOST_HOME}\"\n log \"Created symlink: ${ORIGINAL_HOST_HOME} -> /host-share\"\n else\n ln -sfn /host-share /root/host-home\n log \"Created fallback symlink: /root/host-home -> /host-share\"\n fi\n fi\n\n # Add local domains to /etc/hosts\n HOSTS_DOMAINS=\"${MATRIX_DOMAIN%%:*} ${AGENTTEAMS_MATRIX_CLIENT_DOMAIN:-matrix-client-local.agentteams.io} ${AI_GATEWAY_DOMAIN} ${AGENTTEAMS_FS_DOMAIN:-fs-local.agentteams.io}\"\n if ! grep -q \"${AI_GATEWAY_DOMAIN}\" /etc/hosts 2>/dev/null; then\n echo \"127.0.0.1 ${HOSTS_DOMAINS}\" >> /etc/hosts\n log \"Added local domains to /etc/hosts\"\n fi\n\n # Wait for local infrastructure\n waitForService \"Higress Gateway\" \"127.0.0.1\" 8080 180\n waitForService \"Higress Console\" \"127.0.0.1\" 8001 180\n waitForService \"Tuwunel\" \"127.0.0.1\" 6167 120\n waitForHTTP \"Tuwunel Matrix API\" \"${AGENTTEAMS_MATRIX_URL}/_tuwunel/server_version\" 120\n waitForService \"MinIO\" \"127.0.0.1\" 9000 120\nelse\n # Cloud/K8s mode: wait for external Tuwunel\n log \"Waiting for Tuwunel Matrix server at ${AGENTTEAMS_MATRIX_URL}...\"\n _retry=0\n while [ \"${_retry}\" -lt 30 ]; do\n if curl -sf \"${AGENTTEAMS_MATRIX_URL}/_matrix/client/versions\" > /dev/null 2>&1; then\n log \"Tuwunel is ready\"\n break\n fi\n _retry=$((_retry + 1))\n log \" Waiting for Tuwunel (attempt ${_retry}/30)...\"\n sleep 5\n done\n if [ \"${_retry}\" -ge 30 ]; then\n log \"ERROR: Tuwunel not reachable at ${AGENTTEAMS_MATRIX_URL}\"\n exit 1\n fi\nfi\n\n# ============================================================\n# Auto-generate secrets if not provided via environment\n# Persisted to /data so they survive container restart\n# ============================================================\nSECRETS_FILE=\"/data/agentteams-secrets.env\"\nif [ -f \"${SECRETS_FILE}\" ]; then\n source \"${SECRETS_FILE}\"\n log \"Loaded persisted secrets from ${SECRETS_FILE}\"\nfi\n\nif [ -z \"${AGENTTEAMS_MANAGER_GATEWAY_KEY}\" ]; then\n export AGENTTEAMS_MANAGER_GATEWAY_KEY=\"$(generateKey 32)\"\n log \"Auto-generated AGENTTEAMS_MANAGER_GATEWAY_KEY\"\nfi\nif [ -z \"${AGENTTEAMS_MANAGER_PASSWORD}\" ]; then\n export AGENTTEAMS_MANAGER_PASSWORD=\"$(generateKey 16)\"\n log \"Auto-generated AGENTTEAMS_MANAGER_PASSWORD\"\nfi\n\n# Persist secrets so they survive supervisord restart\nmkdir -p /data\ncat > \"${SECRETS_FILE}\" <<EOF\nexport AGENTTEAMS_MANAGER_GATEWAY_KEY=\"${AGENTTEAMS_MANAGER_GATEWAY_KEY}\"\nexport AGENTTEAMS_MANAGER_PASSWORD=\"${AGENTTEAMS_MANAGER_PASSWORD}\"\nEOF\nchmod 600 \"${SECRETS_FILE}\"\n\n# Cloud mode: pull workspace from OSS before initialization\nif [ \"${AGENTTEAMS_RUNTIME}\" = \"aliyun\" ]; then\n AGENTTEAMS_FS=\"/root/agentteams-fs\"\n mkdir -p \"${AGENTTEAMS_FS}/shared\" \"${AGENTTEAMS_FS}/agents\"\n log \"Pulling workspace from OSS...\"\n ensure_mc_credentials\n mc mirror \"${AGENTTEAMS_STORAGE_PREFIX}/manager/\" /root/manager-workspace/ --overwrite 2>/dev/null || true\n mc mirror \"${AGENTTEAMS_STORAGE_PREFIX}/shared/\" \"${AGENTTEAMS_FS}/shared/\" --overwrite 2>/dev/null || true\n mc mirror \"${AGENTTEAMS_STORAGE_PREFIX}/agents/\" \"${AGENTTEAMS_FS}/agents/\" --overwrite 2>/dev/null || true\n # Keep the canonical filesystem link in the Manager workspace.\n ln -sfn \"${AGENTTEAMS_FS}\" /root/manager-workspace/agentteams-fs\n ln -sfn \"${AGENTTEAMS_FS}\" /root/manager-workspace/agentteams-fs\nfi\n\n# K8s mode: sync workspace from cluster-internal MinIO\nif [ \"${AGENTTEAMS_RUNTIME}\" = \"k8s\" ]; then\n AGENTTEAMS_FS=\"/root/agentteams-fs\"\n mkdir -p \"${AGENTTEAMS_FS}/shared\" \"${AGENTTEAMS_FS}/agents\" \"${AGENTTEAMS_FS}/agentteams-config\"\n log \"Configuring mc alias for cluster MinIO...\"\n mc alias set agentteams \"${AGENTTEAMS_FS_ENDPOINT}\" \"${AGENTTEAMS_FS_ACCESS_KEY}\" \"${AGENTTEAMS_FS_SECRET_KEY}\"\n log \"Syncing workspace from MinIO...\"\n mc mirror \"${AGENTTEAMS_STORAGE_PREFIX}/manager/\" /root/manager-workspace/ --overwrite 2>/dev/null || true\n mc mirror \"${AGENTTEAMS_STORAGE_PREFIX}/\" \"${AGENTTEAMS_FS}/\" --overwrite 2>/dev/null || true\n ln -sfn \"${AGENTTEAMS_FS}\" /root/manager-workspace/agentteams-fs\n ln -sfn \"${AGENTTEAMS_FS}\" /root/manager-workspace/agentteams-fs\n touch \"${AGENTTEAMS_FS}/.initialized\"\nfi\n\n# ============================================================\n# Initialize / upgrade Manager workspace\n# First boot: full init via upgrade-builtins.sh\n# Subsequent boots: compare image version; upgrade only if changed\n# ============================================================\nmkdir -p /root/manager-workspace\n\nIMAGE_VERSION=$(cat /opt/agentteams/agent/.builtin-version 2>/dev/null || echo \"unknown\")\nINSTALLED_VERSION=$(cat /root/manager-workspace/.builtin-version 2>/dev/null || echo \"\")\n\nif [ ! -f /root/manager-workspace/.initialized ]; then\n log \"First boot: initializing manager workspace...\"\n bash /opt/agentteams/scripts/init/upgrade-builtins.sh\n touch /root/manager-workspace/.initialized\n log \"Manager workspace initialized (version: ${IMAGE_VERSION})\"\nelif [ \"${IMAGE_VERSION}\" != \"${INSTALLED_VERSION}\" ] || [ \"${IMAGE_VERSION}\" = \"latest\" ]; then\n log \"Upgrade detected: ${INSTALLED_VERSION} -> ${IMAGE_VERSION}${IMAGE_VERSION:+ (latest: always upgrade)}\"\n bash /opt/agentteams/scripts/init/upgrade-builtins.sh\n log \"Manager workspace upgraded to version: ${IMAGE_VERSION}\"\nelse\n log \"Workspace up to date (version: ${IMAGE_VERSION})\"\nfi\n\n# Local mode: wait for mc mirror initialization (shared + worker data in /root/agentteams-fs/)\nif [ \"${AGENTTEAMS_RUNTIME}\" != \"aliyun\" ] && [ \"${AGENTTEAMS_RUNTIME}\" != \"k8s\" ]; then\n log \"Waiting for MinIO storage initialization...\"\n _minio_wait=0\n while [ ! -f /root/agentteams-fs/.initialized ]; do\n sleep 2\n _minio_wait=$(( _minio_wait + 1 ))\n if [ \"${_minio_wait}\" -ge 60 ]; then\n log \"ERROR: MinIO storage initialization timed out after 120s\"\n exit 1\n fi\n done\n log \"MinIO storage initialized\"\nfi\n\n# ============================================================\n# Obtain Manager Matrix access token.\n#\n# Priority:\n# 1. Pre-injected token (AGENTTEAMS_MANAGER_MATRIX_TOKEN) — set by the\n# controller in both AppService mode (where no password exists) and\n# legacy mode (to avoid a redundant login round-trip).\n# 2. Password-based login — fallback for containers started without\n# controller involvement (manual docker run, legacy installs).\n# ============================================================\nif [ -n \"${AGENTTEAMS_MANAGER_MATRIX_TOKEN:-}\" ]; then\n MANAGER_TOKEN=\"${AGENTTEAMS_MANAGER_MATRIX_TOKEN}\"\n log \"Manager Matrix token pre-injected by controller (token prefix: ${MANAGER_TOKEN:0:10}...)\"\nelif [ \"${AGENTTEAMS_RUNTIME}\" = \"k8s\" ]; then\n log \"K8s mode: obtaining Manager Matrix token via password login...\"\n _LOGIN_RESPONSE=$(curl -s -X POST ${AGENTTEAMS_MATRIX_URL}/_matrix/client/v3/login \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"type\": \"m.login.password\",\n \"identifier\": {\"type\": \"m.id.user\", \"user\": \"manager\"},\n \"password\": \"'\"${AGENTTEAMS_MANAGER_PASSWORD}\"'\"\n }' 2>&1)\n MANAGER_TOKEN=$(echo \"${_LOGIN_RESPONSE}\" | jq -r '.access_token' 2>/dev/null)\n if [ -z \"${MANAGER_TOKEN}\" ] || [ \"${MANAGER_TOKEN}\" = \"null\" ]; then\n log \"ERROR: Failed to obtain Manager Matrix token\"\n log \"ERROR: Login response was: ${_LOGIN_RESPONSE}\"\n exit 1\n fi\n log \"Manager Matrix token obtained (token prefix: ${MANAGER_TOKEN:0:10}...)\"\nelse\nlog \"Registering human admin Matrix account...\"\ncurl -sf -X POST ${AGENTTEAMS_MATRIX_URL}/_matrix/client/v3/register \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"username\": \"'\"${AGENTTEAMS_ADMIN_USER}\"'\",\n \"password\": \"'\"${AGENTTEAMS_ADMIN_PASSWORD}\"'\",\n \"auth\": {\n \"type\": \"m.login.registration_token\",\n \"token\": \"'\"${AGENTTEAMS_REGISTRATION_TOKEN}\"'\"\n }\n }' > /dev/null 2>&1 || log \"Admin account may already exist\"\n\nlog \"Registering Manager Agent Matrix account...\"\ncurl -sf -X POST ${AGENTTEAMS_MATRIX_URL}/_matrix/client/v3/register \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"username\": \"manager\",\n \"password\": \"'\"${AGENTTEAMS_MANAGER_PASSWORD}\"'\",\n \"auth\": {\n \"type\": \"m.login.registration_token\",\n \"token\": \"'\"${AGENTTEAMS_REGISTRATION_TOKEN}\"'\"\n }\n }' > /dev/null 2>&1 || log \"Manager account may already exist\"\n\n# Get Manager Agent's Matrix access token\nlog \"Obtaining Manager Matrix access token...\"\n_LOGIN_RESPONSE=$(curl -s -X POST ${AGENTTEAMS_MATRIX_URL}/_matrix/client/v3/login \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"type\": \"m.login.password\",\n \"identifier\": {\"type\": \"m.id.user\", \"user\": \"manager\"},\n \"password\": \"'\"${AGENTTEAMS_MANAGER_PASSWORD}\"'\"\n }' 2>&1)\n_LOGIN_EXIT=$?\nlog \"Matrix login HTTP exit code: ${_LOGIN_EXIT}\"\nlog \"Matrix login response: ${_LOGIN_RESPONSE}\"\n\nMANAGER_TOKEN=$(echo \"${_LOGIN_RESPONSE}\" | jq -r '.access_token' 2>/dev/null)\n\nif [ -z \"${MANAGER_TOKEN}\" ] || [ \"${MANAGER_TOKEN}\" = \"null\" ]; then\n log \"ERROR: Failed to obtain Manager Matrix token (exit=${_LOGIN_EXIT})\"\n log \"ERROR: Login response was: ${_LOGIN_RESPONSE}\"\n exit 1\nfi\nlog \"Manager Matrix token obtained (token prefix: ${MANAGER_TOKEN:0:10}...)\"\nfi\n\n# ============================================================\n# Higress Console initialization\n# Docker mode: full setup-higress.sh (internal Higress at localhost:8001)\n# K8s mode: skip — controller Initializer handles Higress setup\n# Cloud (aliyun) mode: skip entirely (Higress managed externally)\n# ============================================================\n_HIGRESS_CONSOLE_URL=\"\"\n_HIGRESS_USER=\"${AGENTTEAMS_ADMIN_USER}\"\n_HIGRESS_PASS=\"${AGENTTEAMS_ADMIN_PASSWORD}\"\nif [ \"${AGENTTEAMS_RUNTIME}\" = \"k8s\" ]; then\n log \"K8s mode: skipping Higress initialization (handled by controller)\"\nelif [ \"${AGENTTEAMS_RUNTIME}\" != \"aliyun\" ]; then\n _HIGRESS_CONSOLE_URL=\"http://127.0.0.1:8001\"\nfi\n\nif [ -n \"${_HIGRESS_CONSOLE_URL}\" ]; then\n COOKIE_FILE=\"/tmp/higress-session-cookie\"\n\n log \"Waiting for Higress Console (${_HIGRESS_CONSOLE_URL}) to be fully ready and initializing admin...\"\n INIT_DONE=false\n for i in $(seq 1 90); do\n INIT_RESULT=$(curl -s -X POST \"${_HIGRESS_CONSOLE_URL}/system/init\" \\\n -H 'Content-Type: application/json' \\\n -d '{\"adminUser\":{\"name\":\"'\"${_HIGRESS_USER}\"'\",\"password\":\"'\"${_HIGRESS_PASS}\"'\",\"displayName\":\"'\"${_HIGRESS_USER}\"'\"}}' 2>/dev/null) || true\n if echo \"${INIT_RESULT}\" | grep -qE '\"success\":true|already.?init' 2>/dev/null; then\n INIT_DONE=true\n break\n fi\n if echo \"${INIT_RESULT}\" | grep -q '\"name\"' 2>/dev/null; then\n INIT_DONE=true\n break\n fi\n sleep 2\n done\n\n if [ \"${INIT_DONE}\" != \"true\" ]; then\n log \"ERROR: Higress Console did not become ready within 180s\"\n exit 1\n fi\n log \"Higress Console init done\"\n\n log \"Logging into Higress Console...\"\n LOGIN_OK=false\n for i in $(seq 1 10); do\n HTTP_CODE=$(curl -s -o /dev/null -w '%{http_code}' -X POST \"${_HIGRESS_CONSOLE_URL}/session/login\" \\\n -H 'Content-Type: application/json' \\\n -c \"${COOKIE_FILE}\" \\\n -d '{\"username\":\"'\"${_HIGRESS_USER}\"'\",\"password\":\"'\"${_HIGRESS_PASS}\"'\"}' 2>/dev/null) || true\n if { [ \"${HTTP_CODE}\" = \"200\" ] || [ \"${HTTP_CODE}\" = \"201\" ]; } && [ -f \"${COOKIE_FILE}\" ] && [ -s \"${COOKIE_FILE}\" ]; then\n LOGIN_OK=true\n break\n fi\n log \"Login attempt $i (HTTP ${HTTP_CODE}), retrying in 3s...\"\n sleep 3\n done\n\n if [ \"${LOGIN_OK}\" != \"true\" ]; then\n log \"ERROR: Could not login to Higress Console after retries\"\n exit 1\n fi\n log \"Higress Console login successful\"\n\n VERIFY_CODE=$(curl -s -o /dev/null -w '%{http_code}' \"${_HIGRESS_CONSOLE_URL}/v1/consumers\" -b \"${COOKIE_FILE}\" 2>/dev/null) || true\n if [ \"${VERIFY_CODE}\" = \"200\" ]; then\n log \"Console session verified (cookie valid)\"\n else\n log \"WARNING: Console session may be invalid (verify returned HTTP ${VERIFY_CODE})\"\n rm -f \"${COOKIE_FILE}\"\n for i in $(seq 1 5); do\n curl -s -o /dev/null -w '%{http_code}' -X POST \"${_HIGRESS_CONSOLE_URL}/session/login\" \\\n -H 'Content-Type: application/json' \\\n -c \"${COOKIE_FILE}\" \\\n -d '{\"username\":\"'\"${_HIGRESS_USER}\"'\",\"password\":\"'\"${_HIGRESS_PASS}\"'\"}' 2>/dev/null\n VERIFY2=$(curl -s -o /dev/null -w '%{http_code}' \"${_HIGRESS_CONSOLE_URL}/v1/consumers\" -b \"${COOKIE_FILE}\" 2>/dev/null) || true\n if [ \"${VERIFY2}\" = \"200\" ]; then\n log \"Re-login successful, session verified\"\n break\n fi\n sleep 2\n done\n fi\n\n export HIGRESS_COOKIE_FILE=\"${COOKIE_FILE}\"\n export HIGRESS_CONSOLE_URL=\"${_HIGRESS_CONSOLE_URL}\"\n\n if [ \"${AGENTTEAMS_RUNTIME}\" = \"k8s\" ]; then\n # K8s mode: lightweight Higress config — only what's needed for LLM access\n source /opt/agentteams/scripts/lib/base.sh\n _k8s_higress_api() {\n local method=\"$1\" path=\"$2\" desc=\"$3\"; shift 3; local body=\"$*\"\n local tmpfile; tmpfile=$(mktemp)\n local http_code\n http_code=$(curl -s -o \"${tmpfile}\" -w '%{http_code}' -X \"${method}\" \"${_HIGRESS_CONSOLE_URL}${path}\" \\\n -b \"${COOKIE_FILE}\" -H 'Content-Type: application/json' -d \"${body}\" 2>/dev/null) || true\n local response; response=$(cat \"${tmpfile}\" 2>/dev/null); rm -f \"${tmpfile}\"\n if echo \"${response}\" | grep -q '\"success\":true' 2>/dev/null; then\n log \"${desc} ... OK\"\n elif [ \"${http_code}\" = \"409\" ]; then\n log \"${desc} ... already exists, skipping\"\n elif [ \"${http_code}\" = \"200\" ] || [ \"${http_code}\" = \"201\" ] || [ \"${http_code}\" = \"204\" ]; then\n log \"${desc} ... OK (HTTP ${http_code})\"\n else\n log \"WARNING: ${desc} ... (HTTP ${http_code}): ${response}\"\n fi\n }\n\n # 1. Service Sources (DNS type → K8s Service FQDN)\n # Extract host:port from URLs for Higress service source registration\n _TUWUNEL_HOST=$(echo \"${AGENTTEAMS_MATRIX_URL}\" | sed 's|^http[s]*://||')\n _TUWUNEL_DOMAIN=$(echo \"${_TUWUNEL_HOST}\" | cut -d: -f1)\n _TUWUNEL_PORT=$(echo \"${_TUWUNEL_HOST}\" | cut -d: -f2)\n _k8s_higress_api POST /v1/service-sources \"Registering Tuwunel service source\" \\\n '{\"type\":\"dns\",\"name\":\"tuwunel\",\"domain\":\"'\"${_TUWUNEL_DOMAIN}\"'\",\"port\":'\"${_TUWUNEL_PORT}\"'}'\n\n if [ -n \"${AGENTTEAMS_ELEMENT_WEB_URL:-}\" ]; then\n _ELEMENT_HOST=$(echo \"${AGENTTEAMS_ELEMENT_WEB_URL}\" | sed 's|^http[s]*://||')\n _ELEMENT_DOMAIN=$(echo \"${_ELEMENT_HOST}\" | cut -d: -f1)\n _ELEMENT_PORT=$(echo \"${_ELEMENT_HOST}\" | cut -d: -f2)\n _k8s_higress_api POST /v1/service-sources \"Registering Element Web service source\" \\\n '{\"type\":\"dns\",\"name\":\"element-web\",\"domain\":\"'\"${_ELEMENT_DOMAIN}\"'\",\"port\":'\"${_ELEMENT_PORT}\"'}'\n fi\n\n # 2. Manager Consumer (key-auth)\n _k8s_higress_api POST /v1/consumers \"Creating Manager consumer\" \\\n '{\"name\":\"manager\",\"credentials\":[{\"type\":\"key-auth\",\"source\":\"BEARER\",\"values\":[\"'\"${AGENTTEAMS_MANAGER_GATEWAY_KEY}\"'\"]}]}'\n\n # 3. LLM Provider\n _LLM_PROVIDER=\"${AGENTTEAMS_LLM_PROVIDER:-qwen}\"\n _LLM_API_URL=\"${AGENTTEAMS_LLM_API_URL:-}\"\n if [ -z \"${_LLM_API_URL}\" ]; then\n case \"${_LLM_PROVIDER}\" in\n qwen) _LLM_API_URL=\"https://dashscope.aliyuncs.com/compatible-mode/v1\" ;;\n esac\n fi\n # 4. LLM Provider type-specific config\n case \"${_LLM_PROVIDER}\" in\n qwen)\n _k8s_higress_api POST /v1/ai/providers \"Creating LLM provider (qwen)\" \\\n '{\"type\":\"qwen\",\"name\":\"qwen\",\"tokens\":[\"'\"${AGENTTEAMS_LLM_API_KEY}\"'\"],\"protocol\":\"openai/v1\",\"tokenFailoverConfig\":{\"enabled\":false},\"rawConfigs\":{\"qwenEnableSearch\":false,\"qwenEnableCompatible\":true,\"qwenFileIds\":[],\"agentteamsMode\":true}}'\n ;;\n *)\n _BODY='{\"name\":\"'\"${_LLM_PROVIDER}\"'\",\"type\":\"openai\",\"tokens\":[\"'\"${AGENTTEAMS_LLM_API_KEY}\"'\"],\"modelMapping\":{},\"protocol\":\"openai/v1\",\"rawConfigs\":{\"agentteamsMode\":true}}'\n _k8s_higress_api POST /v1/ai/providers \"Creating LLM provider (${_LLM_PROVIDER})\" \"${_BODY}\"\n ;;\n esac\n\n # 5. AI Route (bind provider + consumer auth, /v1 prefix to avoid clash with Element Web catch-all)\n _k8s_higress_api POST /v1/ai/routes \"Creating AI Gateway route\" \\\n '{\"name\":\"default-ai-route\",\"domains\":[],\"pathPredicate\":{\"matchType\":\"PRE\",\"matchValue\":\"/v1\",\"caseSensitive\":false},\"upstreams\":[{\"provider\":\"'\"${_LLM_PROVIDER}\"'\",\"weight\":100,\"modelMapping\":{}}],\"authConfig\":{\"enabled\":true,\"allowedCredentialTypes\":[\"key-auth\"],\"allowedConsumers\":[\"manager\"]}}'\n\n # 6. Matrix Homeserver Route (/_matrix/* → Tuwunel, no auth)\n _k8s_higress_api POST /v1/routes \"Creating Matrix Homeserver route\" \\\n '{\"name\":\"matrix-homeserver\",\"domains\":[],\"path\":{\"matchType\":\"PRE\",\"matchValue\":\"/_matrix\"},\"services\":[{\"name\":\"tuwunel.dns\",\"port\":'\"${_TUWUNEL_PORT}\"',\"weight\":100}]}'\n\n # 7. Element Web Route (/ catch-all → Element Web, no auth)\n if [ -n \"${AGENTTEAMS_ELEMENT_WEB_URL:-}\" ]; then\n _k8s_higress_api POST /v1/routes \"Creating Element Web route\" \\\n '{\"name\":\"element-web\",\"domains\":[],\"path\":{\"matchType\":\"PRE\",\"matchValue\":\"/\"},\"services\":[{\"name\":\"element-web.dns\",\"port\":'\"${_ELEMENT_PORT}\"',\"weight\":100}]}'\n fi\n\n # 8. Remove Higress default landing page (Exact match on / takes precedence over Element Web catch-all)\n _k8s_higress_api DELETE /v1/routes/default \"Removing Higress default landing route\"\n\n log \"K8s Higress lightweight setup complete\"\n\n # Wait for AI plugin activation (~45s for first config)\n log \"Waiting for AI Gateway plugin activation (45s)...\"\n sleep 45\n else\n # Docker mode: full setup with all routes, domains, MCP servers\n /opt/agentteams/scripts/init/setup-higress.sh\n fi\nfi\n\n# ============================================================\n# Create admin DM room, persist to state.json, send welcome message\n# K8s mode: skip — controller ProvisionManager creates the Admin DM\n# room (Step 4 in service/provisioner.go) AND reconcileManagerWelcome\n# delivers the first-boot onboarding prompt once OpenClaw inside this\n# container has joined the room. In k8s mode the manager intentionally\n# does NOT have the admin password (only AGENTTEAMS_ADMIN_USER), so it\n# could not log in as admin to send the welcome itself anyway. The\n# Manager Agent discovers its admin DM room on first heartbeat via\n# state.json / `manage-state.sh --action set-admin-dm` (see HEARTBEAT.md\n# Step 1) — it does not need it to be pre-injected by this script.\n# Runs in both local and cloud modes (idempotent)\n# ============================================================\nif [ \"${AGENTTEAMS_RUNTIME}\" = \"k8s\" ]; then\n log \"K8s mode: skipping admin DM room creation and welcome message (both handled by agentteams-controller)\"\nelse\nMANAGER_FULL_ID=\"@manager:${MATRIX_DOMAIN}\"\nADMIN_FULL_ID=\"@${AGENTTEAMS_ADMIN_USER}:${MATRIX_DOMAIN}\"\n\nlog \"Logging in as admin to create DM room...\"\n_ADMIN_LOGIN=$(curl -sf -X POST \"${AGENTTEAMS_MATRIX_URL}/_matrix/client/v3/login\" \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"type\": \"m.login.password\",\n \"identifier\": {\"type\": \"m.id.user\", \"user\": \"'\"${AGENTTEAMS_ADMIN_USER}\"'\"},\n \"password\": \"'\"${AGENTTEAMS_ADMIN_PASSWORD}\"'\"\n }' 2>&1) || true\n\nADMIN_MATRIX_TOKEN=$(echo \"${_ADMIN_LOGIN}\" | jq -r '.access_token // empty' 2>/dev/null)\nif [ -z \"${ADMIN_MATRIX_TOKEN}\" ]; then\n log \"WARNING: Failed to login as admin, skipping DM room creation\"\nelse\n # Search for existing DM room with Manager (idempotent)\n DM_ROOM_ID=\"\"\n _JOINED_ROOMS=$(curl -sf \"${AGENTTEAMS_MATRIX_URL}/_matrix/client/v3/joined_rooms\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" 2>/dev/null \\\n | jq -r '.joined_rooms[]' 2>/dev/null) || true\n for _rid in ${_JOINED_ROOMS}; do\n _members=$(curl -sf \"${AGENTTEAMS_MATRIX_URL}/_matrix/client/v3/rooms/${_rid}/members\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" 2>/dev/null \\\n | jq -r '.chunk[].state_key' 2>/dev/null) || continue\n _count=$(echo \"${_members}\" | wc -l | xargs)\n if [ \"${_count}\" = \"2\" ] && echo \"${_members}\" | grep -q \"@manager:\"; then\n DM_ROOM_ID=\"${_rid}\"\n break\n fi\n done\n\n if [ -n \"${DM_ROOM_ID}\" ]; then\n log \"Existing DM room found: ${DM_ROOM_ID}\"\n else\n log \"Creating DM room with Manager...\"\n _RAW=$(curl -s -w '\\nHTTP_CODE:%{http_code}' -X POST \"${AGENTTEAMS_MATRIX_URL}/_matrix/client/v3/createRoom\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d \"{\\\"is_direct\\\":true,\\\"invite\\\":[\\\"${MANAGER_FULL_ID}\\\"],\\\"preset\\\":\\\"trusted_private_chat\\\"}\" 2>&1) || true\n _HTTP_CODE=$(echo \"${_RAW}\" | tail -1 | sed 's/HTTP_CODE://')\n _CREATE_RESP=$(echo \"${_RAW}\" | sed '$d')\n DM_ROOM_ID=$(echo \"${_CREATE_RESP}\" | jq -r '.room_id // empty' 2>/dev/null)\n if [ -n \"${DM_ROOM_ID}\" ]; then\n log \"DM room created: ${DM_ROOM_ID}\"\n else\n log \"WARNING: Failed to create DM room (HTTP ${_HTTP_CODE}): ${_CREATE_RESP}\"\n fi\n fi\n\n # Persist admin DM room ID to state.json\n if [ -n \"${DM_ROOM_ID}\" ]; then\n STATE_SCRIPT=\"/opt/agentteams/agent/skills/task-management/scripts/manage-state.sh\"\n if [ -f \"${STATE_SCRIPT}\" ]; then\n bash \"${STATE_SCRIPT}\" --action init 2>/dev/null || true\n bash \"${STATE_SCRIPT}\" --action set-admin-dm --room-id \"${DM_ROOM_ID}\" 2>/dev/null || true\n log \"Admin DM room persisted to state.json: ${DM_ROOM_ID}\"\n fi\n fi\n\n # Schedule welcome message in background (only on first boot)\n if [ -n \"${DM_ROOM_ID}\" ] && [ ! -f \"/root/manager-workspace/soul-configured\" ]; then\n log \"Scheduling welcome message (background, waiting for OpenClaw to start)...\"\n (\n _AGENTTEAMS_LANGUAGE=\"${AGENTTEAMS_LANGUAGE:-zh}\"\n _AGENTTEAMS_TIMEZONE=\"${TZ:-Asia/Shanghai}\"\n _wait=0\n _ready=false\n while [ \"${_wait}\" -lt 300 ]; do\n if curl -sf http://127.0.0.1:18799/ > /dev/null 2>&1; then\n _ready=true\n break\n fi\n sleep 3\n _wait=$((_wait + 3))\n done\n if [ \"${_ready}\" != \"true\" ]; then\n echo \"[manager] WARNING: OpenClaw gateway not ready within 300s, skipping welcome message\"\n exit 0\n fi\n # Ensure Manager has joined the DM room before sending the welcome\n # message. Without this, there is a race between OpenClaw's Matrix\n # auto-join and the message send — the message may land before Manager\n # joins, so OpenClaw's /sync never picks it up.\n _join_ok=false\n for _join_attempt in 1 2 3; do\n if curl -sf -X POST \"${AGENTTEAMS_MATRIX_URL}/_matrix/client/v3/rooms/${DM_ROOM_ID}/join\" \\\n -H \"Authorization: Bearer ${MANAGER_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d '{}' > /dev/null 2>&1; then\n echo \"[manager] Manager joined DM room before welcome message\"\n _join_ok=true\n break\n fi\n sleep 2\n done\n if [ \"${_join_ok}\" != \"true\" ]; then\n echo \"[manager] WARNING: Manager join request failed after 3 attempts (may already be joined)\"\n fi\n _welcome_msg=\"This is an automated message from the AgentTeams setup. This is a fresh installation.\n\n--- Installation Context ---\nUser Language: ${_AGENTTEAMS_LANGUAGE} (zh = Chinese, en = English)\nUser Timezone: ${_AGENTTEAMS_TIMEZONE} (IANA timezone identifier)\n---\n\nYou are an AI agent that manages a team of worker agents. Your identity and personality have not been configured yet — the human admin is about to meet you for the first time.\n\nPlease begin the onboarding conversation:\n\n1. Greet the admin warmly and briefly describe what you can do (coordinate workers, manage tasks, run multi-agent projects)\n2. The user has selected \\\"${_AGENTTEAMS_LANGUAGE}\\\" as their preferred language during installation. Use this language for your greeting and all subsequent communication.\n3. The user's timezone is ${_AGENTTEAMS_TIMEZONE}. Based on this timezone, you may infer their likely region and suggest additional language options.\n4. Ask them: a) What would they like to call you? b) Communication style preference? c) Any behavior guidelines? d) Confirm default language\n5. After they reply, write their preferences to ~/SOUL.md\n6. Confirm what you wrote, and ask if they would like to adjust anything\n7. Once confirmed, run: touch ~/soul-configured\n\nThe human admin will start chatting shortly.\"\n _txn_id=\"welcome-$(date +%s)\"\n _payload=$(jq -nc --arg body \"${_welcome_msg}\" '{\"msgtype\":\"m.text\",\"body\":$body}')\n _raw=$(curl -s -w '\\nHTTP_CODE:%{http_code}' -X PUT \"${AGENTTEAMS_MATRIX_URL}/_matrix/client/v3/rooms/${DM_ROOM_ID}/send/m.room.message/${_txn_id}\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d \"${_payload}\" 2>&1) || true\n _http_code=$(echo \"${_raw}\" | tail -1 | sed 's/HTTP_CODE://')\n _send_resp=$(echo \"${_raw}\" | sed '$d')\n if echo \"${_send_resp}\" | jq -e '.event_id' > /dev/null 2>&1; then\n echo \"[manager] Welcome message sent to DM room\"\n else\n echo \"[manager] WARNING: Failed to send welcome message (HTTP ${_http_code}): ${_send_resp}\"\n fi\n ) &\n log \"Welcome message background process started (PID: $!)\"\n fi\nfi\nfi # end K8s mode skip for admin DM room\n\n# ============================================================\n# Generate Manager Agent openclaw.json from template\n# ============================================================\nlog \"Generating Manager openclaw.json...\"\nexport MANAGER_MATRIX_TOKEN=\"${MANAGER_TOKEN}\"\nexport MANAGER_GATEWAY_KEY=\"${AGENTTEAMS_MANAGER_GATEWAY_KEY}\"\n# Resolve model parameters based on model name\nMODEL_NAME=\"${AGENTTEAMS_DEFAULT_MODEL:-qwen3.6-plus}\"\ncase \"${MODEL_NAME}\" in\n gpt-5.3-codex|gpt-5-mini|gpt-5-nano)\n export MODEL_CONTEXT_WINDOW=400000 MODEL_MAX_TOKENS=128000 ;;\n claude-opus-4-6)\n export MODEL_CONTEXT_WINDOW=1000000 MODEL_MAX_TOKENS=128000 ;;\n claude-sonnet-4-6)\n export MODEL_CONTEXT_WINDOW=1000000 MODEL_MAX_TOKENS=64000 ;;\n claude-haiku-4-5)\n export MODEL_CONTEXT_WINDOW=200000 MODEL_MAX_TOKENS=64000 ;;\n qwen3.6-plus|qwen3.5-plus)\n export MODEL_CONTEXT_WINDOW=200000 MODEL_MAX_TOKENS=64000 ;;\n deepseek-chat|deepseek-reasoner|kimi-k2.5)\n export MODEL_CONTEXT_WINDOW=256000 MODEL_MAX_TOKENS=128000 ;;\n glm-5|MiniMax-M2.7|MiniMax-M2.7-highspeed|MiniMax-M2.5)\n export MODEL_CONTEXT_WINDOW=200000 MODEL_MAX_TOKENS=128000 ;;\n *)\n export MODEL_CONTEXT_WINDOW=150000 MODEL_MAX_TOKENS=128000 ;;\nesac\nexport MODEL_REASONING=true\n\n# Override with user-supplied custom model parameters from env (set during install)\n[ -n \"${AGENTTEAMS_MODEL_CONTEXT_WINDOW:-}\" ] && export MODEL_CONTEXT_WINDOW=\"${AGENTTEAMS_MODEL_CONTEXT_WINDOW}\"\n[ -n \"${AGENTTEAMS_MODEL_MAX_TOKENS:-}\" ] && export MODEL_MAX_TOKENS=\"${AGENTTEAMS_MODEL_MAX_TOKENS}\"\n[ -n \"${AGENTTEAMS_MODEL_REASONING:-}\" ] && export MODEL_REASONING=\"${AGENTTEAMS_MODEL_REASONING}\"\n\n# E2EE: convert AGENTTEAMS_MATRIX_E2EE to JSON boolean for template substitution\nif [ \"${AGENTTEAMS_MATRIX_E2EE:-0}\" = \"1\" ] || [ \"${AGENTTEAMS_MATRIX_E2EE:-}\" = \"true\" ]; then\n export MATRIX_E2EE_ENABLED=true\nelse\n export MATRIX_E2EE_ENABLED=false\nfi\nlog \"Matrix E2EE: ${MATRIX_E2EE_ENABLED}\"\n\n# Resolve input modalities: only vision-capable models get \"image\"\ncase \"${MODEL_NAME}\" in\n gpt-5.4|gpt-5.3-codex|gpt-5-mini|gpt-5-nano|claude-opus-4-6|claude-sonnet-4-6|claude-haiku-4-5|qwen3.6-plus|qwen3.5-plus|kimi-k2.5)\n export MODEL_INPUT='[\"text\", \"image\"]' ;;\n *)\n export MODEL_INPUT='[\"text\"]' ;;\nesac\n# Override with user-supplied vision setting from env\nif [ \"${AGENTTEAMS_MODEL_VISION:-}\" = \"true\" ]; then\n export MODEL_INPUT='[\"text\", \"image\"]'\nelif [ \"${AGENTTEAMS_MODEL_VISION:-}\" = \"false\" ]; then\n export MODEL_INPUT='[\"text\"]'\nfi\n\nlog \"Model: ${MODEL_NAME} (context=${MODEL_CONTEXT_WINDOW}, maxTokens=${MODEL_MAX_TOKENS}, reasoning=${MODEL_REASONING}, input=${MODEL_INPUT})\"\n\nif [ -f /root/manager-workspace/openclaw.json ]; then\n log \"Manager openclaw.json already exists, updating dynamic fields only (preserving user customizations)...\"\n # Merge known models into existing config (add missing, preserve user-added)\n # Use known-models.json (valid JSON) instead of template (contains ${VAR} placeholders)\n KNOWN_MODELS=$(cat /opt/agentteams/configs/known-models.json 2>/dev/null || echo '[]')\n jq --arg token \"${MANAGER_TOKEN}\" \\\n --arg key \"${AGENTTEAMS_MANAGER_GATEWAY_KEY}\" \\\n --arg model \"${MODEL_NAME}\" \\\n --arg emb_model \"${AGENTTEAMS_EMBEDDING_MODEL}\" \\\n --arg aigw_domain \"${AI_GATEWAY_DOMAIN}\" \\\n --arg matrix_user_id \"@manager:${MATRIX_DOMAIN}\" \\\n --argjson e2ee \"${MATRIX_E2EE_ENABLED}\" \\\n --argjson known_models \"${KNOWN_MODELS}\" \\\n --argjson ctx \"${MODEL_CONTEXT_WINDOW}\" \\\n --argjson max \"${MODEL_MAX_TOKENS}\" \\\n --argjson reasoning \"${MODEL_REASONING}\" \\\n --argjson input \"${MODEL_INPUT}\" \\\n '\n # Merge known models: add any model id not already present\n .models.providers[\"agentteams-gateway\"].models as $existing\n | ($existing | map(.id)) as $existing_ids\n | ($known_models | map(select(.id as $id | $existing_ids | index($id) | not))) as $new\n | .models.providers[\"agentteams-gateway\"].models = ($existing + $new)\n # Ensure the user-chosen default model is in the list (custom model support)\n | if (.models.providers[\"agentteams-gateway\"].models | map(.id) | index($model) | not) then\n .models.providers[\"agentteams-gateway\"].models += [{\"id\": $model, \"name\": $model, \"reasoning\": $reasoning, \"contextWindow\": $ctx, \"maxTokens\": $max, \"input\": $input}]\n else . end\n # Rebuild model aliases from the full models list\n | (.models.providers[\"agentteams-gateway\"].models | map({ (\"agentteams-gateway/\" + .id): { \"alias\": .id } }) | add // {}) as $aliases\n | .agents.defaults.models = ((.agents.defaults.models // {}) + $aliases)\n | .channels.matrix.accessToken = $token | .channels.matrix.userId = $matrix_user_id | .models.providers[\"agentteams-gateway\"].apiKey = $key\n | ((.hooks.token // \"\") as $ht | if $ht == $key or $ht == ($key + \"-hooks\" | @base64) then del(.hooks) else . end)\n | .agents.defaults.model.primary = (\"agentteams-gateway/\" + $model)\n | .commands.restart = true\n | .gateway.port = 18799\n | .gateway.bind = \"lan\"\n | .gateway.controlUi = ((.gateway.controlUi // {}) + {\"dangerouslyDisableDeviceAuth\": true, \"allowInsecureAuth\": true, \"allowedOrigins\": [\"*\"]})\n | .channels.matrix.encryption = $e2ee\n | .channels.matrix.network = ((.channels.matrix.network // {}) + {\"dangerouslyAllowPrivateNetwork\": true})\n | .channels.matrix.autoJoin = \"always\"\n # OpenClaw YOLO defaults: host exec without approval prompts (see openclaw docs tools/exec-approvals)\n | .tools = (.tools // {})\n | .tools.exec = ((.tools.exec // {}) + {\"host\":\"gateway\",\"security\":\"full\",\"ask\":\"off\"})\n | .tools.elevated = (.tools.elevated // {})\n | .tools.elevated.enabled = true\n | .tools.elevated.allowFrom |= ((. // {}) | .matrix = [\"*\"])\n | .agents.defaults.elevatedDefault = \"full\"\n # Ensure memorySearch config exists (embedding model for memory) — skip if embedding model is empty\n | if $emb_model != \"\" then .agents.defaults.memorySearch //= {\"provider\":\"openai\",\"model\":$emb_model,\"remote\":{\"baseUrl\":(\"http://\" + $aigw_domain + \":8080/v1\"),\"apiKey\":$key}} else . end\n ' \\\n /root/manager-workspace/openclaw.json > /tmp/openclaw.json.tmp && \\\n mv /tmp/openclaw.json.tmp /root/manager-workspace/openclaw.json\n # Disable openclaw's observe-recovery mechanism which compares config against\n # a lastKnownGood baseline in config-health.json. When meta is missing from the\n # current file but present in the baseline, observe-recovery restores from .bak,\n # undoing user customizations (plugins, channels, etc).\n # Clearing config-health.json removes the baseline so observe-recovery won't\n # interfere, while preserving .bak as a backup.\n rm -f /root/manager-workspace/.openclaw/logs/config-health.json\n # Verify the token was written correctly\n _written_token=$(jq -r '.channels.matrix.accessToken' /root/manager-workspace/openclaw.json 2>/dev/null)\n if [ -z \"${_written_token}\" ] || [ \"${_written_token}\" = \"null\" ]; then\n log \"ERROR: Matrix token was not written correctly to openclaw.json (got: ${_written_token})\"\n else\n log \"Matrix token written to openclaw.json (prefix: ${_written_token:0:10}...)\"\n fi\nelse\n log \"Manager openclaw.json not found, generating from template...\"\n envsubst < /opt/agentteams/configs/manager-openclaw.json.tmpl > /root/manager-workspace/openclaw.json\n # Post-envsubst injection: memorySearch + custom model (single jq pass when possible)\n if ! jq -e --arg model \"${MODEL_NAME}\" '.models.providers[\"agentteams-gateway\"].models | map(.id) | index($model)' /root/manager-workspace/openclaw.json > /dev/null 2>&1; then\n log \"Custom model '${MODEL_NAME}' not in built-in list, injecting into config...\"\n jq --arg emb_model \"${AGENTTEAMS_EMBEDDING_MODEL}\" \\\n --arg aigw_domain \"${AI_GATEWAY_DOMAIN}\" \\\n --arg key \"${AGENTTEAMS_MANAGER_GATEWAY_KEY}\" \\\n --arg model \"${MODEL_NAME}\" \\\n --argjson ctx \"${MODEL_CONTEXT_WINDOW}\" \\\n --argjson max \"${MODEL_MAX_TOKENS}\" \\\n --argjson reasoning \"${MODEL_REASONING}\" \\\n --argjson input \"${MODEL_INPUT}\" \\\n '\n (if $emb_model != \"\" then .agents.defaults.memorySearch = {\"provider\":\"openai\",\"model\":$emb_model,\"remote\":{\"baseUrl\":(\"http://\" + $aigw_domain + \":8080/v1\"),\"apiKey\":$key}} else . end)\n | .models.providers[\"agentteams-gateway\"].models += [{\"id\": $model, \"name\": $model, \"reasoning\": $reasoning, \"contextWindow\": $ctx, \"maxTokens\": $max, \"input\": $input}]\n | .agents.defaults.models += {(\"agentteams-gateway/\" + $model): {\"alias\": $model}}\n ' /root/manager-workspace/openclaw.json > /tmp/openclaw.json.tmp && \\\n mv /tmp/openclaw.json.tmp /root/manager-workspace/openclaw.json\n elif [ -n \"${AGENTTEAMS_EMBEDDING_MODEL}\" ]; then\n jq --arg emb_model \"${AGENTTEAMS_EMBEDDING_MODEL}\" \\\n --arg aigw_domain \"${AI_GATEWAY_DOMAIN}\" \\\n --arg key \"${AGENTTEAMS_MANAGER_GATEWAY_KEY}\" \\\n '.agents.defaults.memorySearch = {\"provider\":\"openai\",\"model\":$emb_model,\"remote\":{\"baseUrl\":(\"http://\" + $aigw_domain + \":8080/v1\"),\"apiKey\":$key}}' \\\n /root/manager-workspace/openclaw.json > /tmp/openclaw.json.tmp && \\\n mv /tmp/openclaw.json.tmp /root/manager-workspace/openclaw.json\n fi\n _written_token=$(jq -r '.channels.matrix.accessToken' /root/manager-workspace/openclaw.json 2>/dev/null)\n log \"Matrix token written from template (prefix: ${_written_token:0:10}...)\"\nfi\n\n# Cloud/K8s mode: overlay cloud-specific settings onto generated config\nif [ \"${AGENTTEAMS_RUNTIME}\" = \"aliyun\" ] || [ \"${AGENTTEAMS_RUNTIME}\" = \"k8s\" ]; then\n log \"Applying cloud/k8s overlay to openclaw.json...\"\n jq --arg homeserver \"${AGENTTEAMS_MATRIX_URL}\" \\\n --arg gateway \"${AGENTTEAMS_AI_GATEWAY_URL}/v1\" \\\n --arg key \"${AGENTTEAMS_MANAGER_GATEWAY_KEY}\" \\\n '.channels.matrix.homeserver = $homeserver\n | .models.providers[\"agentteams-gateway\"].baseUrl = $gateway\n | .models.providers[\"agentteams-gateway\"].apiKey = $key\n | ((.hooks.token // \"\") as $ht | if $ht == $key or $ht == ($key + \"-hooks\" | @base64) then del(.hooks) else . end)\n | .commands.restart = false\n | if .agents.defaults.memorySearch then .agents.defaults.memorySearch.remote.baseUrl = $gateway | .agents.defaults.memorySearch.remote.apiKey = $key else . end' \\\n /root/manager-workspace/openclaw.json > /tmp/openclaw-cloud.json && \\\n mv /tmp/openclaw-cloud.json /root/manager-workspace/openclaw.json\n log \"Cloud/K8s overlay applied\"\nfi\n\n# ============================================================\n# Optional: enable openclaw-cms-plugin observability\n# Config is applied at runtime so secrets stay out of image layers.\n# ============================================================\nCMS_TRACES_ENABLED=\"$(echo \"${AGENTTEAMS_CMS_TRACES_ENABLED:-false}\" | tr '[:upper:]' '[:lower:]')\"\nif [ \"${CMS_TRACES_ENABLED}\" = \"true\" ]; then\n CMS_PLUGIN_NAME=\"openclaw-cms-plugin\"\n CMS_PLUGIN_DIR=\"${OPENCLAW_CMS_PLUGIN_DIR:-/opt/openclaw/extensions/openclaw-cms-plugin}\"\n CMS_PLUGIN_MANIFEST=\"${CMS_PLUGIN_DIR}/openclaw.plugin.json\"\n DIAG_PLUGIN_NAME=\"diagnostics-otel\"\n DIAG_PLUGIN_DIR=\"/opt/openclaw/extensions/diagnostics-otel\"\n CMS_LICENSE_KEY=\"${AGENTTEAMS_CMS_LICENSE_KEY:-}\"\n CMS_PROJECT=\"${AGENTTEAMS_CMS_PROJECT:-}\"\n CMS_METRICS_ENABLED=\"${AGENTTEAMS_CMS_METRICS_ENABLED:-false}\"\n\n if [ ! -f \"${CMS_PLUGIN_MANIFEST}\" ]; then\n log \"WARNING: ${CMS_PLUGIN_NAME} manifest not found at ${CMS_PLUGIN_MANIFEST}, skipping plugin config\"\n else\n _missing=0\n [ -z \"${AGENTTEAMS_CMS_ENDPOINT:-}\" ] && log \"WARNING: AGENTTEAMS_CMS_ENDPOINT is required when AGENTTEAMS_CMS_TRACES_ENABLED=true\" && _missing=1\n [ -z \"${CMS_LICENSE_KEY:-}\" ] && log \"WARNING: AGENTTEAMS_CMS_LICENSE_KEY is required when AGENTTEAMS_CMS_TRACES_ENABLED=true\" && _missing=1\n [ -z \"${AGENTTEAMS_CMS_WORKSPACE:-}\" ] && log \"WARNING: AGENTTEAMS_CMS_WORKSPACE is required when AGENTTEAMS_CMS_TRACES_ENABLED=true\" && _missing=1\n\n if [ \"${_missing}\" = \"0\" ]; then\n CMS_SERVICE_NAME=\"${AGENTTEAMS_CMS_SERVICE_NAME:-agentteams-manager}\"\n CMS_ENABLE_METRICS=\"${CMS_METRICS_ENABLED}\"\n DIAG_AVAILABLE=\"0\"\n _metrics_lc=\"$(echo \"${CMS_ENABLE_METRICS}\" | tr '[:upper:]' '[:lower:]')\"\n if [ \"${_metrics_lc}\" = \"true\" ]; then\n if [ -f \"${DIAG_PLUGIN_DIR}/package.json\" ]; then\n DIAG_AVAILABLE=\"1\"\n if [ ! -d \"${DIAG_PLUGIN_DIR}/node_modules\" ]; then\n log \"diagnostics-otel dependencies missing, installing...\"\n if (cd \"${DIAG_PLUGIN_DIR}\" && npm install --omit=dev --ignore-scripts >/tmp/agentteams-diag-install.log 2>&1); then\n log \"diagnostics-otel dependencies installed\"\n else\n log \"WARNING: diagnostics-otel npm install failed, metrics plugin may not load\"\n fi\n else\n log \"diagnostics-otel dependencies already present\"\n fi\n else\n log \"WARNING: diagnostics-otel package.json not found at ${DIAG_PLUGIN_DIR}, metrics plugin may not load\"\n fi\n fi\n\n log \"Applying ${CMS_PLUGIN_NAME} config to openclaw.json...\"\n jq --arg pluginName \"${CMS_PLUGIN_NAME}\" \\\n --arg pluginDir \"${CMS_PLUGIN_DIR}\" \\\n --arg endpoint \"${AGENTTEAMS_CMS_ENDPOINT}\" \\\n --arg licenseKey \"${CMS_LICENSE_KEY}\" \\\n --arg armsProject \"${CMS_PROJECT}\" \\\n --arg cmsWorkspace \"${AGENTTEAMS_CMS_WORKSPACE}\" \\\n --arg serviceName \"${CMS_SERVICE_NAME}\" \\\n --arg diagPluginName \"${DIAG_PLUGIN_NAME}\" \\\n --arg diagPluginDir \"${DIAG_PLUGIN_DIR}\" \\\n --arg metricsRaw \"${CMS_ENABLE_METRICS}\" \\\n --arg diagAvailableRaw \"${DIAG_AVAILABLE}\" \\\n '\n .plugins = (.plugins // {})\n | .plugins.load = (.plugins.load // {})\n | .plugins.entries = (.plugins.entries // {})\n | if (.plugins.allow | type) != \"array\" then .plugins.allow = [] else . end\n | if (.plugins.allow | index($pluginName)) == null then .plugins.allow += [$pluginName] else . end\n | if (.plugins.load.paths | type) != \"array\" then .plugins.load.paths = [] else . end\n | if (.plugins.load.paths | index($pluginDir)) == null then .plugins.load.paths += [$pluginDir] else . end\n | .plugins.entries[$pluginName] = {\n \"enabled\": true,\n \"config\": {\n \"endpoint\": $endpoint,\n \"headers\": {\n \"x-arms-license-key\": $licenseKey,\n \"x-arms-project\": $armsProject,\n \"x-cms-workspace\": $cmsWorkspace\n },\n \"serviceName\": $serviceName\n }\n }\n\n # diagnostics-otel metrics (optional)\n | ($metricsRaw | ascii_downcase) as $m\n | ($diagAvailableRaw == \"1\") as $diagAvailable\n | (($m == \"true\") and $diagAvailable) as $metricsEnabled\n | if $metricsEnabled then\n (if (.plugins.allow | index($diagPluginName)) == null then .plugins.allow += [$diagPluginName] else . end)\n | (if (.plugins.load.paths | index($diagPluginDir)) == null then .plugins.load.paths += [$diagPluginDir] else . end)\n | .plugins.entries[$diagPluginName].enabled = true\n | .diagnostics = (.diagnostics // {})\n | .diagnostics.otel = (.diagnostics.otel // {})\n | .diagnostics.enabled = true\n | .diagnostics.otel.enabled = true\n | .diagnostics.otel.endpoint = $endpoint\n | .diagnostics.otel.protocol = (.diagnostics.otel.protocol // \"http/protobuf\")\n | .diagnostics.otel.headers = {\n \"x-arms-license-key\": $licenseKey,\n \"x-arms-project\": $armsProject,\n \"x-cms-workspace\": $cmsWorkspace\n }\n | .diagnostics.otel.serviceName = $serviceName\n | .diagnostics.otel.metrics = true\n | .diagnostics.otel.traces = (.diagnostics.otel.traces // false)\n | .diagnostics.otel.logs = (.diagnostics.otel.logs // false)\n else\n .\n end\n ' /root/manager-workspace/openclaw.json > /tmp/openclaw-cms.json && \\\n mv /tmp/openclaw-cms.json /root/manager-workspace/openclaw.json\n log \"${CMS_PLUGIN_NAME} config applied (metrics=${CMS_ENABLE_METRICS}, service=${CMS_SERVICE_NAME})\"\n else\n log \"Skipping ${CMS_PLUGIN_NAME} config due to missing required env vars\"\n fi\n fi\nfi\n\n# ============================================================\n# Detect container runtime (for Worker creation)\n# ============================================================\nsource /opt/agentteams/scripts/lib/container-api.sh\nif container_api_available; then\n log \"Container runtime socket detected at ${CONTAINER_SOCKET} — direct Worker creation enabled\"\n export AGENTTEAMS_CONTAINER_RUNTIME=\"socket\"\nelif [ \"${AGENTTEAMS_RUNTIME}\" = \"aliyun\" ] || [ \"${AGENTTEAMS_RUNTIME}\" = \"k8s\" ]; then\n log \"Cloud/K8s mode — Workers created via controller API\"\n export AGENTTEAMS_CONTAINER_RUNTIME=\"cloud\"\nelse\n log \"No container runtime found — Worker creation will output install commands\"\n export AGENTTEAMS_CONTAINER_RUNTIME=\"none\"\nfi\n\n# ============================================================\n# Recreate Worker containers as needed after Manager restart.\n# Workers are on agentteams-net; Docker DNS resolves *-local.agentteams.io via\n# the Manager's network aliases, so IP changes don't require worker recreation.\n# Only recreate stopped/missing workers.\n# ============================================================\nif container_api_available; then\n _workers_json=$(agt get workers -o json 2>/dev/null || echo '{\"workers\":[]}')\n for _worker_name in $(echo \"${_workers_json}\" | jq -r '.workers[].name'); do\n [ -z \"${_worker_name}\" ] && continue\n\n _status=$(container_status_worker \"${_worker_name}\")\n if [ \"${_status}\" = \"running\" ]; then\n log \"Worker running: ${_worker_name}, skipping\"\n continue\n fi\n # Container missing or stopped — recreate.\n log \"Worker container ${_status}: ${_worker_name}, recreating...\"\n _creds_file=\"/data/worker-creds/${_worker_name}.env\"\n if [ -f \"${_creds_file}\" ]; then\n source \"${_creds_file}\"\n _runtime=$(echo \"${_workers_json}\" | jq -r --arg w \"${_worker_name}\" '.workers[] | select(.name == $w) | .runtime // \"openclaw\"')\n _recreated=false\n for _attempt in 1 2 3; do\n _env_map=\"\"\n _create_body=\"\"\n _env_map=$(jq -cn \\\n --arg name \"${_worker_name}\" \\\n --arg fak \"${_worker_name}\" \\\n --arg fsk \"${WORKER_MINIO_PASSWORD:-}\" \\\n --arg fs_domain \"${AGENTTEAMS_FS_DOMAIN:-fs-local.agentteams.io}\" \\\n --arg controller_url \"${AGENTTEAMS_CONTROLLER_URL:-}\" \\\n '{\n \"AGENTTEAMS_WORKER_NAME\": $name,\n \"AGENTTEAMS_FS_ENDPOINT\": (\"http://\" + ($fs_domain | split(\":\")[0]) + \":9000\"),\n \"AGENTTEAMS_FS_ACCESS_KEY\": $fak,\n \"AGENTTEAMS_FS_SECRET_KEY\": $fsk\n }\n | if $controller_url != \"\" then . + {\"AGENTTEAMS_CONTROLLER_URL\": $controller_url} else . end')\n _create_body=$(jq -cn --arg name \"${_worker_name}\" --arg runtime \"${_runtime}\" --argjson env \"${_env_map}\" '{name: $name, runtime: $runtime, env: $env}')\n worker_backend_create \"${_create_body}\" > /dev/null 2>&1 && _recreated=true && break\n log \" Attempt ${_attempt}/3 failed for ${_worker_name}, retrying in $((5 * _attempt))s...\"\n sleep $((5 * _attempt))\n done\n if [ \"${_recreated}\" = true ]; then\n log \" Recreated ${_runtime} worker: ${_worker_name}\"\n else\n log \" ERROR: Failed to recreate ${_worker_name} after 3 attempts\"\n fi\n else\n log \" WARNING: No credentials found for ${_worker_name} (${_creds_file} missing), skipping\"\n fi\n done\nfi\n\n# ============================================================\n# Notify workers of builtin updates if upgrade happened\n# Builtin files (AGENTS.md, skills) are already synced by upgrade-builtins.sh\n#\n# Cooldown: skip notification if the last successful notify was within\n# NOTIFY_COOLDOWN_SECS (default 3600s / 1 hour). This prevents repeated\n# notifications when the Manager crash-loops and re-runs upgrade-builtins\n# on every restart (e.g. IMAGE_VERSION=latest always triggers upgrade).\n# ============================================================\nNOTIFY_COOLDOWN_SECS=\"${AGENTTEAMS_NOTIFY_COOLDOWN_SECS:-3600}\"\nNOTIFY_TS_FILE=\"/root/manager-workspace/.last-worker-notify-ts\"\n\nif [ -f /root/manager-workspace/.upgrade-pending-worker-notify ]; then\n _now=$(date +%s)\n _last_notify=$(cat \"${NOTIFY_TS_FILE}\" 2>/dev/null || echo \"0\")\n _elapsed=$(( _now - _last_notify ))\n\n if [ \"${_elapsed}\" -lt \"${NOTIFY_COOLDOWN_SECS}\" ]; then\n log \"Skipping worker builtin notification (last notify ${_elapsed}s ago, cooldown ${NOTIFY_COOLDOWN_SECS}s)\"\n rm -f /root/manager-workspace/.upgrade-pending-worker-notify\n else\n log \"Notifying workers about builtin updates...\"\n _notify_ok=false\n _workers_json=$(agt get workers -o json 2>/dev/null || echo '{\"workers\":[]}')\n for _worker_name in $(echo \"${_workers_json}\" | jq -r '.workers[].name'); do\n [ -z \"${_worker_name}\" ] && continue\n _room_id=$(echo \"${_workers_json}\" | jq -r --arg w \"${_worker_name}\" '.workers[] | select(.name == $w) | .roomID // empty')\n if [ -n \"${_room_id}\" ]; then\n _worker_id=\"@${_worker_name}:${MATRIX_DOMAIN}\"\n _txn_id=\"upgrade-$(date +%s%N)\"\n _msg=\"@${_worker_name}:${MATRIX_DOMAIN} Manager upgraded builtin files (AGENTS.md, skills). Please use your file-sync skill to sync the latest config.\"\n _raw=$(curl -s -w '\\nHTTP_CODE:%{http_code}' -X PUT \\\n \"${AGENTTEAMS_MATRIX_URL}/_matrix/client/v3/rooms/${_room_id}/send/m.room.message/${_txn_id}\" \\\n -H \"Authorization: Bearer ${MANAGER_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d \"{\\\"msgtype\\\":\\\"m.text\\\",\\\"body\\\":\\\"${_msg}\\\",\\\"m.mentions\\\":{\\\"user_ids\\\":[\\\"${_worker_id}\\\"]}}\" \\\n 2>&1) || true\n _http_code=$(echo \"${_raw}\" | tail -1 | sed 's/HTTP_CODE://')\n _notify_resp=$(echo \"${_raw}\" | sed '$d')\n if echo \"${_notify_resp}\" | jq -e '.event_id' > /dev/null 2>&1; then\n log \" Notified ${_worker_name}\"; _notify_ok=true\n else\n log \" WARNING: Failed to notify ${_worker_name} (HTTP ${_http_code}): ${_notify_resp}\"\n fi\n fi\n done\n # Record timestamp only if at least one notification succeeded\n if [ \"${_notify_ok}\" = true ]; then\n echo \"${_now}\" > \"${NOTIFY_TS_FILE}\"\n fi\n rm -f /root/manager-workspace/.upgrade-pending-worker-notify\n fi\nfi\n\n# ============================================================\n# Start Manager Agent\n# ============================================================\nlog \"Starting Manager Agent (${MANAGER_RUNTIME})...\"\n\n# HOME is already set to /root/manager-workspace via docker run -e HOME=...\ncd \"${HOME}\"\n\n# Ensure host credential symlinks exist under HOME\nif [ -d \"/host-share\" ]; then\n [ -f \"/host-share/.gitconfig\" ] && ln -sf \"/host-share/.gitconfig\" \"${HOME}/.gitconfig\"\nfi\n\nlog \"HOME=${HOME} (manager-workspace, host-mounted)\"\n\n# ── Render agent doc templates ────────────────────────────────────────────\n# Replace ${VAR} placeholders with actual values so the AI agent reads\n# plain text and never needs to resolve environment variables.\nexport MANAGER_MATRIX_TOKEN MANAGER_TOKEN HIGRESS_COOKIE_FILE\nRENDER=/opt/agentteams/scripts/lib/render-skills.sh\nlog \"Rendering agent doc templates...\"\n# Manager-owned docs (workspace)\nbash \"$RENDER\" /root/manager-workspace/skills\nbash \"$RENDER\" /root/manager-workspace/skills-alpha\nbash \"$RENDER\" /root/manager-workspace AGENTS.md TOOLS.md HEARTBEAT.md SOUL.md\n# Worker templates (workspace + image) — rendered before push to MinIO\n# so Workers (including remote pip-install) receive plain text\nbash \"$RENDER\" /root/manager-workspace/worker-skills\nbash \"$RENDER\" /root/manager-workspace/worker-agent\nbash \"$RENDER\" /root/manager-workspace/copaw-worker-agent\nbash \"$RENDER\" /root/manager-workspace/hermes-worker-agent\nbash \"$RENDER\" /opt/agentteams/agent/worker-skills\nbash \"$RENDER\" /opt/agentteams/agent/worker-agent\nbash \"$RENDER\" /opt/agentteams/agent/copaw-worker-agent\nbash \"$RENDER\" /opt/agentteams/agent/hermes-worker-agent\nlog \"Agent doc templates rendered\"\n\n# Cloud mode: start background file sync (workspace ↔ OSS) and initial push\nif [ \"${AGENTTEAMS_RUNTIME}\" = \"aliyun\" ]; then\n log \"Syncing initial workspace to OSS...\"\n ensure_mc_credentials\n mc mirror /root/manager-workspace/ \"${AGENTTEAMS_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" 2>/dev/null || true\n\n # Local → OSS: change-triggered sync\n (\n while true; do\n CHANGED=$(find /root/manager-workspace/ -type f -newermt \"15 seconds ago\" 2>/dev/null | head -1)\n if [ -n \"${CHANGED}\" ]; then\n ensure_mc_credentials 2>/dev/null || true\n mc mirror /root/manager-workspace/ \"${AGENTTEAMS_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" --exclude \".npm/**\" \\\n --exclude \".local/**\" --exclude \".mc/**\" 2>/dev/null || true\n fi\n sleep 10\n done\n ) &\n log \"Local→OSS sync started (PID: $!)\"\n\n # OSS → Local: periodic pull (shared data, agent configs, manager openclaw.json)\n (\n while true; do\n sleep 60\n ensure_mc_credentials 2>/dev/null || true\n mc mirror \"${AGENTTEAMS_STORAGE_PREFIX}/shared/\" /root/agentteams-fs/shared/ --overwrite --newer-than \"1m\" 2>/dev/null || true\n mc mirror \"${AGENTTEAMS_STORAGE_PREFIX}/agents/\" /root/agentteams-fs/agents/ --overwrite --newer-than \"1m\" 2>/dev/null || true\n mc cp \"${AGENTTEAMS_STORAGE_PREFIX}/manager/openclaw.json\" /root/manager-workspace/openclaw.json 2>/dev/null || true\n done\n ) &\n log \"OSS→Local sync started (every 60s, PID: $!)\"\nfi\n\n# K8s mode: start background file sync (workspace ↔ MinIO)\nif [ \"${AGENTTEAMS_RUNTIME}\" = \"k8s\" ]; then\n log \"Syncing initial workspace to MinIO...\"\n mc mirror /root/manager-workspace/ \"${AGENTTEAMS_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" 2>/dev/null || true\n\n # Local → MinIO: change-triggered sync\n (\n while true; do\n CHANGED=$(find /root/manager-workspace/ -type f -newermt \"15 seconds ago\" 2>/dev/null | head -1)\n if [ -n \"${CHANGED}\" ]; then\n mc mirror /root/manager-workspace/ \"${AGENTTEAMS_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" --exclude \".npm/**\" \\\n --exclude \".local/**\" --exclude \".mc/**\" 2>/dev/null || true\n fi\n sleep 10\n done\n ) &\n log \"Local→MinIO sync started (PID: $!)\"\n\n # MinIO → Local: periodic pull (shared data, agent configs, manager openclaw.json)\n (\n while true; do\n sleep 60\n mc mirror \"${AGENTTEAMS_STORAGE_PREFIX}/shared/\" /root/agentteams-fs/shared/ --overwrite --newer-than \"1m\" 2>/dev/null || true\n mc mirror \"${AGENTTEAMS_STORAGE_PREFIX}/agents/\" /root/agentteams-fs/agents/ --overwrite --newer-than \"1m\" 2>/dev/null || true\n mc mirror \"${AGENTTEAMS_STORAGE_PREFIX}/agentteams-config/\" /root/agentteams-fs/agentteams-config/ --overwrite --newer-than \"1m\" 2>/dev/null || true\n mc cp \"${AGENTTEAMS_STORAGE_PREFIX}/manager/openclaw.json\" /root/manager-workspace/openclaw.json 2>/dev/null || true\n done\n ) &\n log \"MinIO→Local sync started (every 60s, PID: $!)\"\nfi\n\n# ============================================================\n# Auto-generate Manager mcporter config for pre-configured MCP servers\n# If AGENTTEAMS_GITHUB_TOKEN was set at install time, setup-higress.sh already\n# configured GitHub MCP on Higress. Run setup-mcp-server.sh now so that\n# config/mcporter.json exists when the Agent starts — no need to ask user for PAT.\n# ============================================================\nif [ -n \"${AGENTTEAMS_GITHUB_TOKEN}\" ] && [ \"${AGENTTEAMS_RUNTIME}\" != \"aliyun\" ] && [ \"${AGENTTEAMS_RUNTIME}\" != \"k8s\" ]; then\n if [ ! -f \"${HOME}/config/mcporter.json\" ]; then\n log \"Auto-generating Manager mcporter config for GitHub MCP (AGENTTEAMS_GITHUB_TOKEN set)...\"\n bash /opt/agentteams/agent/skills/mcp-server-management/scripts/setup-mcp-server.sh \\\n github \"${AGENTTEAMS_GITHUB_TOKEN}\" 2>&1 | while IFS= read -r line; do log \" [setup-mcp] ${line}\"; done || \\\n log \"WARNING: setup-mcp-server.sh failed — Agent may need to configure GitHub MCP manually\"\n else\n log \"Manager mcporter config already exists, skipping auto-generate\"\n fi\nfi\n\n# ============================================================\n# Runtime-specific startup\n# ============================================================\nif [ \"${MANAGER_RUNTIME}\" = \"copaw\" ]; then\n # Delegate to CoPaw startup script\n exec /opt/agentteams/scripts/init/start-copaw-manager.sh\nelse\n # ── OpenClaw Runtime ─────────────────────────────────────────────────────\n log \"Starting OpenClaw Manager...\"\n\n export OPENCLAW_CONFIG_PATH=\"/root/manager-workspace/openclaw.json\"\n\n # Symlink to default OpenClaw config path so CLI commands find the config\n mkdir -p \"${HOME}/.openclaw\"\n ln -sf \"/root/manager-workspace/openclaw.json\" \"${HOME}/.openclaw/openclaw.json\"\n\n # Clean orphaned session write locks (e.g. from SIGKILL or crash before exit handlers)\n # Prevents \"session file locked (timeout 10000ms)\" when PID was reused\n find \"${HOME}/.openclaw/agents\" -name \"*.jsonl.lock\" -delete 2>/dev/null || true\n log \"Cleaned up any orphaned session write locks\"\n\n # Clean Matrix crypto storage (SQLite WAL may be corrupted after unclean shutdown)\n # Crypto state is re-negotiated on startup; losing it only means re-establishing E2EE sessions\n rm -rf \"${HOME}/.openclaw/matrix\" 2>/dev/null || true\n log \"Cleaned Matrix crypto storage (will re-establish E2EE sessions)\"\n\n # Launch OpenClaw\n # Disable full-process respawn so the CLI uses its internal restart loop.\n # Without this, config reload spawns a detached child and exits, then\n # supervisord restarts the CLI — resulting in two gateway processes.\n export OPENCLAW_NO_RESPAWN=1\n\n # Optional matrix-plugin trace logging — when AGENTTEAMS_MATRIX_DEBUG=1 is set\n # in the manager environment (propagated by install / supervisord), turn on\n # OPENCLAW_MATRIX_DEBUG so the matrix plugin emits structured INFO-level\n # lifecycle traces (sync.state transitions, room.invite/join, message\n # handler arrival + filter outcomes). Useful for diagnosing \"worker never\n # joined the room\" / \"manager never replied\" hangs without rebuilding the\n # image.\n if [ \"${AGENTTEAMS_MATRIX_DEBUG:-}\" = \"1\" ] && [ -z \"${OPENCLAW_MATRIX_DEBUG:-}\" ]; then\n export OPENCLAW_MATRIX_DEBUG=1\n log \"AGENTTEAMS_MATRIX_DEBUG=1 detected; OPENCLAW_MATRIX_DEBUG=1 exported for matrix plugin tracing\"\n fi\n\n exec openclaw gateway run --verbose --force\nfi\n"}, "cf9bc5ea7e37e5d96dddbaf5cd7f2c6b4ea40207": {"t": 1786068468.2394617, "n": 64921, "h": "9e69523f67d18268", "c": "#!/bin/bash\n# start-manager-agent.sh - Initialize and start the Manager Agent\n# Supports local (supervisord), cloud (SAE), and K8s (Helm) deployments.\n# In local mode this is the last supervisord component to start (priority 800).\n# In cloud/k8s mode (AGENTTEAMS_RUNTIME=aliyun|k8s) this is the container entrypoint.\n#\n# Runtime selection:\n# AGENTTEAMS_MANAGER_RUNTIME=qwenpaw - QwenPaw 2.0 workspace mode (default)\n# AGENTTEAMS_MANAGER_RUNTIME=copaw - Legacy alias, auto-routed to QwenPaw\n# AGENTTEAMS_MANAGER_RUNTIME=openclaw - OpenClaw gateway mode\n\nsource /opt/agentteams/scripts/lib/agentteams-env.sh\n\n# ============================================================\n# Runtime selection\n# ============================================================\nMANAGER_RUNTIME=\"${AGENTTEAMS_MANAGER_RUNTIME:-qwenpaw}\"\ncase \"${MANAGER_RUNTIME}\" in\n copaw|qwenpaw)\n log \"Manager runtime: QwenPaw (Python workspace, runtime=${MANAGER_RUNTIME})\"\n ;;\n *)\n log \"Manager runtime: OpenClaw (Node.js gateway)\"\n MANAGER_RUNTIME=\"openclaw\"\n ;;\nesac\n\n# ============================================================\n# Set timezone from TZ env var\n# ============================================================\nif [ -n \"${TZ}\" ] && [ -f \"/usr/share/zoneinfo/${TZ}\" ]; then\n ln -sf \"/usr/share/zoneinfo/${TZ}\" /etc/localtime\n echo \"${TZ}\" > /etc/timezone\n log \"Timezone set to ${TZ}\"\nfi\n\nexport MATRIX_DOMAIN=\"${AGENTTEAMS_MATRIX_DOMAIN:-matrix-local.agentteams.io:8080}\"\nAI_GATEWAY_DOMAIN=\"${AGENTTEAMS_AI_GATEWAY_DOMAIN:-aigw-local.agentteams.io}\"\n\n# ============================================================\n# YOLO mode promotion\n# ============================================================\n# In embedded mode the controller does not propagate AGENTTEAMS_YOLO to the\n# manager container, but installer / test scripts touch a marker file at\n# `${WORKSPACE}/yolo-mode` instead. Promote that marker to the env var so the\n# agent's documented YOLO check (`AGENTTEAMS_YOLO=1`) reliably detects it without\n# depending on filesystem lookups during a turn.\nif [ -z \"${AGENTTEAMS_YOLO:-}\" ] && [ -f /root/manager-workspace/yolo-mode ]; then\n export AGENTTEAMS_YOLO=1\n log \"YOLO mode marker detected at /root/manager-workspace/yolo-mode; AGENTTEAMS_YOLO=1 exported\"\nfi\n\n# ============================================================\n# Cloud/K8s mode: validate required environment variables + initial credentials\n# ============================================================\nif [ \"${AGENTTEAMS_RUNTIME}\" = \"aliyun\" ] || [ \"${AGENTTEAMS_RUNTIME}\" = \"k8s\" ]; then\n : \"${AGENTTEAMS_MATRIX_URL:?AGENTTEAMS_MATRIX_URL is required}\"\n : \"${AGENTTEAMS_MATRIX_DOMAIN:?AGENTTEAMS_MATRIX_DOMAIN is required}\"\n : \"${AGENTTEAMS_AI_GATEWAY_URL:?AGENTTEAMS_AI_GATEWAY_URL is required}\"\n if [ \"${AGENTTEAMS_RUNTIME}\" = \"aliyun\" ]; then\n : \"${AGENTTEAMS_MANAGER_GATEWAY_KEY:?AGENTTEAMS_MANAGER_GATEWAY_KEY is required}\"\n : \"${AGENTTEAMS_MANAGER_PASSWORD:?AGENTTEAMS_MANAGER_PASSWORD is required (cloud containers are stateless, password must be injected)}\"\n fi\n if [ \"${AGENTTEAMS_RUNTIME}\" = \"k8s\" ]; then\n # K8s mode: controller handles initialization (admin registration, Higress setup).\n # Manager only needs credentials injected by the ManagerReconciler.\n : \"${AGENTTEAMS_MANAGER_GATEWAY_KEY:?AGENTTEAMS_MANAGER_GATEWAY_KEY is required (injected by controller)}\"\n # AGENTTEAMS_MANAGER_PASSWORD is optional: not needed in AppService mode\n # (token obtained via AS login), only required in legacy password mode.\n else\n # Cloud (aliyun) mode: Manager still does its own initialization\n : \"${AGENTTEAMS_REGISTRATION_TOKEN:?AGENTTEAMS_REGISTRATION_TOKEN is required}\"\n : \"${AGENTTEAMS_ADMIN_USER:?AGENTTEAMS_ADMIN_USER is required}\"\n : \"${AGENTTEAMS_ADMIN_PASSWORD:?AGENTTEAMS_ADMIN_PASSWORD is required}\"\n fi\n log \"${AGENTTEAMS_RUNTIME} mode: validating environment... OK\"\n log \" Matrix: ${AGENTTEAMS_MATRIX_URL}, AI Gateway: ${AGENTTEAMS_AI_GATEWAY_URL}, Storage: ${AGENTTEAMS_FS_BUCKET}\"\n if [ \"${AGENTTEAMS_RUNTIME}\" = \"aliyun\" ]; then\n ensure_mc_credentials || { log \"FATAL: Initial STS credential fetch failed\"; exit 1; }\n fi\nfi\n\n# ============================================================\n# Local mode: host symlinks, /etc/hosts, wait for local services\n# ============================================================\nif [ \"${AGENTTEAMS_RUNTIME}\" != \"aliyun\" ] && [ \"${AGENTTEAMS_RUNTIME}\" != \"k8s\" ]; then\n # Create symlink for host directory access\n if [ -d \"/host-share\" ]; then\n ORIGINAL_HOST_HOME=\"${HOST_ORIGINAL_HOME:-$HOME}\"\n if [ ! -e \"${ORIGINAL_HOST_HOME}\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/root\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/data\" ] && [ \"${ORIGINAL_HOST_HOME}\" != \"/host-share\" ]; then\n mkdir -p \"$(dirname \"${ORIGINAL_HOST_HOME}\")\"\n ln -sfn /host-share \"${ORIGINAL_HOST_HOME}\"\n log \"Created symlink: ${ORIGINAL_HOST_HOME} -> /host-share\"\n else\n ln -sfn /host-share /root/host-home\n log \"Created fallback symlink: /root/host-home -> /host-share\"\n fi\n fi\n\n # Add local domains to /etc/hosts\n HOSTS_DOMAINS=\"${MATRIX_DOMAIN%%:*} ${AGENTTEAMS_MATRIX_CLIENT_DOMAIN:-matrix-client-local.agentteams.io} ${AI_GATEWAY_DOMAIN} ${AGENTTEAMS_FS_DOMAIN:-fs-local.agentteams.io}\"\n if ! grep -q \"${AI_GATEWAY_DOMAIN}\" /etc/hosts 2>/dev/null; then\n echo \"127.0.0.1 ${HOSTS_DOMAINS}\" >> /etc/hosts\n log \"Added local domains to /etc/hosts\"\n fi\n\n # Wait for local infrastructure\n waitForService \"Higress Gateway\" \"127.0.0.1\" 8080 180\n waitForService \"Higress Console\" \"127.0.0.1\" 8001 180\n waitForService \"Tuwunel\" \"127.0.0.1\" 6167 120\n waitForHTTP \"Tuwunel Matrix API\" \"${AGENTTEAMS_MATRIX_URL}/_tuwunel/server_version\" 120\n waitForService \"MinIO\" \"127.0.0.1\" 9000 120\nelse\n # Cloud/K8s mode: wait for external Tuwunel\n log \"Waiting for Tuwunel Matrix server at ${AGENTTEAMS_MATRIX_URL}...\"\n _retry=0\n while [ \"${_retry}\" -lt 30 ]; do\n if curl -sf \"${AGENTTEAMS_MATRIX_URL}/_matrix/client/versions\" > /dev/null 2>&1; then\n log \"Tuwunel is ready\"\n break\n fi\n _retry=$((_retry + 1))\n log \" Waiting for Tuwunel (attempt ${_retry}/30)...\"\n sleep 5\n done\n if [ \"${_retry}\" -ge 30 ]; then\n log \"ERROR: Tuwunel not reachable at ${AGENTTEAMS_MATRIX_URL}\"\n exit 1\n fi\nfi\n\n# ============================================================\n# Auto-generate secrets if not provided via environment\n# Persisted to /data so they survive container restart\n# ============================================================\nSECRETS_FILE=\"/data/agentteams-secrets.env\"\nif [ -f \"${SECRETS_FILE}\" ]; then\n source \"${SECRETS_FILE}\"\n log \"Loaded persisted secrets from ${SECRETS_FILE}\"\nfi\n\nif [ -z \"${AGENTTEAMS_MANAGER_GATEWAY_KEY}\" ]; then\n export AGENTTEAMS_MANAGER_GATEWAY_KEY=\"$(generateKey 32)\"\n log \"Auto-generated AGENTTEAMS_MANAGER_GATEWAY_KEY\"\nfi\nif [ -z \"${AGENTTEAMS_MANAGER_PASSWORD}\" ]; then\n export AGENTTEAMS_MANAGER_PASSWORD=\"$(generateKey 16)\"\n log \"Auto-generated AGENTTEAMS_MANAGER_PASSWORD\"\nfi\n\n# Persist secrets so they survive supervisord restart\nmkdir -p /data\ncat > \"${SECRETS_FILE}\" <<EOF\nexport AGENTTEAMS_MANAGER_GATEWAY_KEY=\"${AGENTTEAMS_MANAGER_GATEWAY_KEY}\"\nexport AGENTTEAMS_MANAGER_PASSWORD=\"${AGENTTEAMS_MANAGER_PASSWORD}\"\nEOF\nchmod 600 \"${SECRETS_FILE}\"\n\n# Cloud mode: pull workspace from OSS before initialization\nif [ \"${AGENTTEAMS_RUNTIME}\" = \"aliyun\" ]; then\n AGENTTEAMS_FS=\"/root/agentteams-fs\"\n mkdir -p \"${AGENTTEAMS_FS}/shared\" \"${AGENTTEAMS_FS}/agents\"\n log \"Pulling workspace from OSS...\"\n ensure_mc_credentials\n mc mirror \"${AGENTTEAMS_STORAGE_PREFIX}/manager/\" /root/manager-workspace/ --overwrite 2>/dev/null || true\n mc mirror \"${AGENTTEAMS_STORAGE_PREFIX}/shared/\" \"${AGENTTEAMS_FS}/shared/\" --overwrite 2>/dev/null || true\n mc mirror \"${AGENTTEAMS_STORAGE_PREFIX}/agents/\" \"${AGENTTEAMS_FS}/agents/\" --overwrite 2>/dev/null || true\n # Keep the canonical filesystem link in the Manager workspace.\n ln -sfn \"${AGENTTEAMS_FS}\" /root/manager-workspace/agentteams-fs\n ln -sfn \"${AGENTTEAMS_FS}\" /root/manager-workspace/agentteams-fs\nfi\n\n# K8s mode: sync workspace from cluster-internal MinIO\nif [ \"${AGENTTEAMS_RUNTIME}\" = \"k8s\" ]; then\n AGENTTEAMS_FS=\"/root/agentteams-fs\"\n mkdir -p \"${AGENTTEAMS_FS}/shared\" \"${AGENTTEAMS_FS}/agents\" \"${AGENTTEAMS_FS}/agentteams-config\"\n log \"Configuring mc alias for cluster MinIO...\"\n mc alias set agentteams \"${AGENTTEAMS_FS_ENDPOINT}\" \"${AGENTTEAMS_FS_ACCESS_KEY}\" \"${AGENTTEAMS_FS_SECRET_KEY}\"\n log \"Syncing workspace from MinIO...\"\n mc mirror \"${AGENTTEAMS_STORAGE_PREFIX}/manager/\" /root/manager-workspace/ --overwrite 2>/dev/null || true\n mc mirror \"${AGENTTEAMS_STORAGE_PREFIX}/\" \"${AGENTTEAMS_FS}/\" --overwrite 2>/dev/null || true\n ln -sfn \"${AGENTTEAMS_FS}\" /root/manager-workspace/agentteams-fs\n ln -sfn \"${AGENTTEAMS_FS}\" /root/manager-workspace/agentteams-fs\n touch \"${AGENTTEAMS_FS}/.initialized\"\nfi\n\n# ============================================================\n# Initialize / upgrade Manager workspace\n# First boot: full init via upgrade-builtins.sh\n# Subsequent boots: compare image version; upgrade only if changed\n# ============================================================\nmkdir -p /root/manager-workspace\n\nIMAGE_VERSION=$(cat /opt/agentteams/agent/.builtin-version 2>/dev/null || echo \"unknown\")\nINSTALLED_VERSION=$(cat /root/manager-workspace/.builtin-version 2>/dev/null || echo \"\")\n\nif [ ! -f /root/manager-workspace/.initialized ]; then\n log \"First boot: initializing manager workspace...\"\n bash /opt/agentteams/scripts/init/upgrade-builtins.sh\n touch /root/manager-workspace/.initialized\n log \"Manager workspace initialized (version: ${IMAGE_VERSION})\"\nelif [ \"${IMAGE_VERSION}\" != \"${INSTALLED_VERSION}\" ] || [ \"${IMAGE_VERSION}\" = \"latest\" ]; then\n log \"Upgrade detected: ${INSTALLED_VERSION} -> ${IMAGE_VERSION}${IMAGE_VERSION:+ (latest: always upgrade)}\"\n bash /opt/agentteams/scripts/init/upgrade-builtins.sh\n log \"Manager workspace upgraded to version: ${IMAGE_VERSION}\"\nelse\n log \"Workspace up to date (version: ${IMAGE_VERSION})\"\nfi\n\n# Local mode: wait for mc mirror initialization (shared + worker data in /root/agentteams-fs/)\nif [ \"${AGENTTEAMS_RUNTIME}\" != \"aliyun\" ] && [ \"${AGENTTEAMS_RUNTIME}\" != \"k8s\" ]; then\n log \"Waiting for MinIO storage initialization...\"\n _minio_wait=0\n while [ ! -f /root/agentteams-fs/.initialized ]; do\n sleep 2\n _minio_wait=$(( _minio_wait + 1 ))\n if [ \"${_minio_wait}\" -ge 60 ]; then\n log \"ERROR: MinIO storage initialization timed out after 120s\"\n exit 1\n fi\n done\n log \"MinIO storage initialized\"\nfi\n\n# ============================================================\n# Obtain Manager Matrix access token.\n#\n# Priority:\n# 1. Pre-injected token (AGENTTEAMS_MANAGER_MATRIX_TOKEN) — set by the\n# controller in both AppService mode (where no password exists) and\n# legacy mode (to avoid a redundant login round-trip).\n# 2. Password-based login — fallback for containers started without\n# controller involvement (manual docker run, legacy installs).\n# ============================================================\nif [ -n \"${AGENTTEAMS_MANAGER_MATRIX_TOKEN:-}\" ]; then\n MANAGER_TOKEN=\"${AGENTTEAMS_MANAGER_MATRIX_TOKEN}\"\n log \"Manager Matrix token pre-injected by controller (token prefix: ${MANAGER_TOKEN:0:10}...)\"\nelif [ \"${AGENTTEAMS_RUNTIME}\" = \"k8s\" ]; then\n log \"K8s mode: obtaining Manager Matrix token via password login...\"\n _LOGIN_RESPONSE=$(curl -s -X POST ${AGENTTEAMS_MATRIX_URL}/_matrix/client/v3/login \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"type\": \"m.login.password\",\n \"identifier\": {\"type\": \"m.id.user\", \"user\": \"manager\"},\n \"password\": \"'\"${AGENTTEAMS_MANAGER_PASSWORD}\"'\"\n }' 2>&1)\n MANAGER_TOKEN=$(echo \"${_LOGIN_RESPONSE}\" | jq -r '.access_token' 2>/dev/null)\n if [ -z \"${MANAGER_TOKEN}\" ] || [ \"${MANAGER_TOKEN}\" = \"null\" ]; then\n log \"ERROR: Failed to obtain Manager Matrix token\"\n log \"ERROR: Login response was: ${_LOGIN_RESPONSE}\"\n exit 1\n fi\n log \"Manager Matrix token obtained (token prefix: ${MANAGER_TOKEN:0:10}...)\"\nelse\nlog \"Registering human admin Matrix account...\"\ncurl -sf -X POST ${AGENTTEAMS_MATRIX_URL}/_matrix/client/v3/register \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"username\": \"'\"${AGENTTEAMS_ADMIN_USER}\"'\",\n \"password\": \"'\"${AGENTTEAMS_ADMIN_PASSWORD}\"'\",\n \"auth\": {\n \"type\": \"m.login.registration_token\",\n \"token\": \"'\"${AGENTTEAMS_REGISTRATION_TOKEN}\"'\"\n }\n }' > /dev/null 2>&1 || log \"Admin account may already exist\"\n\nlog \"Registering Manager Agent Matrix account...\"\ncurl -sf -X POST ${AGENTTEAMS_MATRIX_URL}/_matrix/client/v3/register \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"username\": \"manager\",\n \"password\": \"'\"${AGENTTEAMS_MANAGER_PASSWORD}\"'\",\n \"auth\": {\n \"type\": \"m.login.registration_token\",\n \"token\": \"'\"${AGENTTEAMS_REGISTRATION_TOKEN}\"'\"\n }\n }' > /dev/null 2>&1 || log \"Manager account may already exist\"\n\n# Get Manager Agent's Matrix access token\nlog \"Obtaining Manager Matrix access token...\"\n_LOGIN_RESPONSE=$(curl -s -X POST ${AGENTTEAMS_MATRIX_URL}/_matrix/client/v3/login \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"type\": \"m.login.password\",\n \"identifier\": {\"type\": \"m.id.user\", \"user\": \"manager\"},\n \"password\": \"'\"${AGENTTEAMS_MANAGER_PASSWORD}\"'\"\n }' 2>&1)\n_LOGIN_EXIT=$?\nlog \"Matrix login HTTP exit code: ${_LOGIN_EXIT}\"\nlog \"Matrix login response: ${_LOGIN_RESPONSE}\"\n\nMANAGER_TOKEN=$(echo \"${_LOGIN_RESPONSE}\" | jq -r '.access_token' 2>/dev/null)\n\nif [ -z \"${MANAGER_TOKEN}\" ] || [ \"${MANAGER_TOKEN}\" = \"null\" ]; then\n log \"ERROR: Failed to obtain Manager Matrix token (exit=${_LOGIN_EXIT})\"\n log \"ERROR: Login response was: ${_LOGIN_RESPONSE}\"\n exit 1\nfi\nlog \"Manager Matrix token obtained (token prefix: ${MANAGER_TOKEN:0:10}...)\"\nfi\n\n# ============================================================\n# Higress Console initialization\n# Docker mode: full setup-higress.sh (internal Higress at localhost:8001)\n# K8s mode: skip — controller Initializer handles Higress setup\n# Cloud (aliyun) mode: skip entirely (Higress managed externally)\n# ============================================================\n_HIGRESS_CONSOLE_URL=\"\"\n_HIGRESS_USER=\"${AGENTTEAMS_ADMIN_USER}\"\n_HIGRESS_PASS=\"${AGENTTEAMS_ADMIN_PASSWORD}\"\nif [ \"${AGENTTEAMS_RUNTIME}\" = \"k8s\" ]; then\n log \"K8s mode: skipping Higress initialization (handled by controller)\"\nelif [ \"${AGENTTEAMS_RUNTIME}\" != \"aliyun\" ]; then\n _HIGRESS_CONSOLE_URL=\"http://127.0.0.1:8001\"\nfi\n\nif [ -n \"${_HIGRESS_CONSOLE_URL}\" ]; then\n COOKIE_FILE=\"/tmp/higress-session-cookie\"\n\n log \"Waiting for Higress Console (${_HIGRESS_CONSOLE_URL}) to be fully ready and initializing admin...\"\n INIT_DONE=false\n for i in $(seq 1 90); do\n INIT_RESULT=$(curl -s -X POST \"${_HIGRESS_CONSOLE_URL}/system/init\" \\\n -H 'Content-Type: application/json' \\\n -d '{\"adminUser\":{\"name\":\"'\"${_HIGRESS_USER}\"'\",\"password\":\"'\"${_HIGRESS_PASS}\"'\",\"displayName\":\"'\"${_HIGRESS_USER}\"'\"}}' 2>/dev/null) || true\n if echo \"${INIT_RESULT}\" | grep -qE '\"success\":true|already.?init' 2>/dev/null; then\n INIT_DONE=true\n break\n fi\n if echo \"${INIT_RESULT}\" | grep -q '\"name\"' 2>/dev/null; then\n INIT_DONE=true\n break\n fi\n sleep 2\n done\n\n if [ \"${INIT_DONE}\" != \"true\" ]; then\n log \"ERROR: Higress Console did not become ready within 180s\"\n exit 1\n fi\n log \"Higress Console init done\"\n\n log \"Logging into Higress Console...\"\n LOGIN_OK=false\n for i in $(seq 1 10); do\n HTTP_CODE=$(curl -s -o /dev/null -w '%{http_code}' -X POST \"${_HIGRESS_CONSOLE_URL}/session/login\" \\\n -H 'Content-Type: application/json' \\\n -c \"${COOKIE_FILE}\" \\\n -d '{\"username\":\"'\"${_HIGRESS_USER}\"'\",\"password\":\"'\"${_HIGRESS_PASS}\"'\"}' 2>/dev/null) || true\n if { [ \"${HTTP_CODE}\" = \"200\" ] || [ \"${HTTP_CODE}\" = \"201\" ]; } && [ -f \"${COOKIE_FILE}\" ] && [ -s \"${COOKIE_FILE}\" ]; then\n LOGIN_OK=true\n break\n fi\n log \"Login attempt $i (HTTP ${HTTP_CODE}), retrying in 3s...\"\n sleep 3\n done\n\n if [ \"${LOGIN_OK}\" != \"true\" ]; then\n log \"ERROR: Could not login to Higress Console after retries\"\n exit 1\n fi\n log \"Higress Console login successful\"\n\n VERIFY_CODE=$(curl -s -o /dev/null -w '%{http_code}' \"${_HIGRESS_CONSOLE_URL}/v1/consumers\" -b \"${COOKIE_FILE}\" 2>/dev/null) || true\n if [ \"${VERIFY_CODE}\" = \"200\" ]; then\n log \"Console session verified (cookie valid)\"\n else\n log \"WARNING: Console session may be invalid (verify returned HTTP ${VERIFY_CODE})\"\n rm -f \"${COOKIE_FILE}\"\n for i in $(seq 1 5); do\n curl -s -o /dev/null -w '%{http_code}' -X POST \"${_HIGRESS_CONSOLE_URL}/session/login\" \\\n -H 'Content-Type: application/json' \\\n -c \"${COOKIE_FILE}\" \\\n -d '{\"username\":\"'\"${_HIGRESS_USER}\"'\",\"password\":\"'\"${_HIGRESS_PASS}\"'\"}' 2>/dev/null\n VERIFY2=$(curl -s -o /dev/null -w '%{http_code}' \"${_HIGRESS_CONSOLE_URL}/v1/consumers\" -b \"${COOKIE_FILE}\" 2>/dev/null) || true\n if [ \"${VERIFY2}\" = \"200\" ]; then\n log \"Re-login successful, session verified\"\n break\n fi\n sleep 2\n done\n fi\n\n export HIGRESS_COOKIE_FILE=\"${COOKIE_FILE}\"\n export HIGRESS_CONSOLE_URL=\"${_HIGRESS_CONSOLE_URL}\"\n\n if [ \"${AGENTTEAMS_RUNTIME}\" = \"k8s\" ]; then\n # K8s mode: lightweight Higress config — only what's needed for LLM access\n source /opt/agentteams/scripts/lib/base.sh\n _k8s_higress_api() {\n local method=\"$1\" path=\"$2\" desc=\"$3\"; shift 3; local body=\"$*\"\n local tmpfile; tmpfile=$(mktemp)\n local http_code\n http_code=$(curl -s -o \"${tmpfile}\" -w '%{http_code}' -X \"${method}\" \"${_HIGRESS_CONSOLE_URL}${path}\" \\\n -b \"${COOKIE_FILE}\" -H 'Content-Type: application/json' -d \"${body}\" 2>/dev/null) || true\n local response; response=$(cat \"${tmpfile}\" 2>/dev/null); rm -f \"${tmpfile}\"\n if echo \"${response}\" | grep -q '\"success\":true' 2>/dev/null; then\n log \"${desc} ... OK\"\n elif [ \"${http_code}\" = \"409\" ]; then\n log \"${desc} ... already exists, skipping\"\n elif [ \"${http_code}\" = \"200\" ] || [ \"${http_code}\" = \"201\" ] || [ \"${http_code}\" = \"204\" ]; then\n log \"${desc} ... OK (HTTP ${http_code})\"\n else\n log \"WARNING: ${desc} ... (HTTP ${http_code}): ${response}\"\n fi\n }\n\n # 1. Service Sources (DNS type → K8s Service FQDN)\n # Extract host:port from URLs for Higress service source registration\n _TUWUNEL_HOST=$(echo \"${AGENTTEAMS_MATRIX_URL}\" | sed 's|^http[s]*://||')\n _TUWUNEL_DOMAIN=$(echo \"${_TUWUNEL_HOST}\" | cut -d: -f1)\n _TUWUNEL_PORT=$(echo \"${_TUWUNEL_HOST}\" | cut -d: -f2)\n _k8s_higress_api POST /v1/service-sources \"Registering Tuwunel service source\" \\\n '{\"type\":\"dns\",\"name\":\"tuwunel\",\"domain\":\"'\"${_TUWUNEL_DOMAIN}\"'\",\"port\":'\"${_TUWUNEL_PORT}\"'}'\n\n if [ -n \"${AGENTTEAMS_ELEMENT_WEB_URL:-}\" ]; then\n _ELEMENT_HOST=$(echo \"${AGENTTEAMS_ELEMENT_WEB_URL}\" | sed 's|^http[s]*://||')\n _ELEMENT_DOMAIN=$(echo \"${_ELEMENT_HOST}\" | cut -d: -f1)\n _ELEMENT_PORT=$(echo \"${_ELEMENT_HOST}\" | cut -d: -f2)\n _k8s_higress_api POST /v1/service-sources \"Registering Element Web service source\" \\\n '{\"type\":\"dns\",\"name\":\"element-web\",\"domain\":\"'\"${_ELEMENT_DOMAIN}\"'\",\"port\":'\"${_ELEMENT_PORT}\"'}'\n fi\n\n # 2. Manager Consumer (key-auth)\n _k8s_higress_api POST /v1/consumers \"Creating Manager consumer\" \\\n '{\"name\":\"manager\",\"credentials\":[{\"type\":\"key-auth\",\"source\":\"BEARER\",\"values\":[\"'\"${AGENTTEAMS_MANAGER_GATEWAY_KEY}\"'\"]}]}'\n\n # 3. LLM Provider\n _LLM_PROVIDER=\"${AGENTTEAMS_LLM_PROVIDER:-qwen}\"\n _LLM_API_URL=\"${AGENTTEAMS_LLM_API_URL:-}\"\n if [ -z \"${_LLM_API_URL}\" ]; then\n case \"${_LLM_PROVIDER}\" in\n qwen) _LLM_API_URL=\"https://dashscope.aliyuncs.com/compatible-mode/v1\" ;;\n esac\n fi\n # 4. LLM Provider type-specific config\n case \"${_LLM_PROVIDER}\" in\n qwen)\n _k8s_higress_api POST /v1/ai/providers \"Creating LLM provider (qwen)\" \\\n '{\"type\":\"qwen\",\"name\":\"qwen\",\"tokens\":[\"'\"${AGENTTEAMS_LLM_API_KEY}\"'\"],\"protocol\":\"openai/v1\",\"tokenFailoverConfig\":{\"enabled\":false},\"rawConfigs\":{\"qwenEnableSearch\":false,\"qwenEnableCompatible\":true,\"qwenFileIds\":[],\"agentteamsMode\":true}}'\n ;;\n *)\n _BODY='{\"name\":\"'\"${_LLM_PROVIDER}\"'\",\"type\":\"openai\",\"tokens\":[\"'\"${AGENTTEAMS_LLM_API_KEY}\"'\"],\"modelMapping\":{},\"protocol\":\"openai/v1\",\"rawConfigs\":{\"agentteamsMode\":true}}'\n _k8s_higress_api POST /v1/ai/providers \"Creating LLM provider (${_LLM_PROVIDER})\" \"${_BODY}\"\n ;;\n esac\n\n # 5. AI Route (bind provider + consumer auth, /v1 prefix to avoid clash with Element Web catch-all)\n _k8s_higress_api POST /v1/ai/routes \"Creating AI Gateway route\" \\\n '{\"name\":\"default-ai-route\",\"domains\":[],\"pathPredicate\":{\"matchType\":\"PRE\",\"matchValue\":\"/v1\",\"caseSensitive\":false},\"upstreams\":[{\"provider\":\"'\"${_LLM_PROVIDER}\"'\",\"weight\":100,\"modelMapping\":{}}],\"authConfig\":{\"enabled\":true,\"allowedCredentialTypes\":[\"key-auth\"],\"allowedConsumers\":[\"manager\"]}}'\n\n # 6. Matrix Homeserver Route (/_matrix/* → Tuwunel, no auth)\n _k8s_higress_api POST /v1/routes \"Creating Matrix Homeserver route\" \\\n '{\"name\":\"matrix-homeserver\",\"domains\":[],\"path\":{\"matchType\":\"PRE\",\"matchValue\":\"/_matrix\"},\"services\":[{\"name\":\"tuwunel.dns\",\"port\":'\"${_TUWUNEL_PORT}\"',\"weight\":100}]}'\n\n # 7. Element Web Route (/ catch-all → Element Web, no auth)\n if [ -n \"${AGENTTEAMS_ELEMENT_WEB_URL:-}\" ]; then\n _k8s_higress_api POST /v1/routes \"Creating Element Web route\" \\\n '{\"name\":\"element-web\",\"domains\":[],\"path\":{\"matchType\":\"PRE\",\"matchValue\":\"/\"},\"services\":[{\"name\":\"element-web.dns\",\"port\":'\"${_ELEMENT_PORT}\"',\"weight\":100}]}'\n fi\n\n # 8. Remove Higress default landing page (Exact match on / takes precedence over Element Web catch-all)\n _k8s_higress_api DELETE /v1/routes/default \"Removing Higress default landing route\"\n\n log \"K8s Higress lightweight setup complete\"\n\n # Wait for AI plugin activation (~45s for first config)\n log \"Waiting for AI Gateway plugin activation (45s)...\"\n sleep 45\n else\n # Docker mode: full setup with all routes, domains, MCP servers\n /opt/agentteams/scripts/init/setup-higress.sh\n fi\nfi\n\n# ============================================================\n# Create admin DM room, persist to state.json, send welcome message\n# K8s mode: skip — controller ProvisionManager creates the Admin DM\n# room (Step 4 in service/provisioner.go) AND reconcileManagerWelcome\n# delivers the first-boot onboarding prompt once OpenClaw inside this\n# container has joined the room. In k8s mode the manager intentionally\n# does NOT have the admin password (only AGENTTEAMS_ADMIN_USER), so it\n# could not log in as admin to send the welcome itself anyway. The\n# Manager Agent discovers its admin DM room on first heartbeat via\n# state.json / `manage-state.sh --action set-admin-dm` (see HEARTBEAT.md\n# Step 1) — it does not need it to be pre-injected by this script.\n# Runs in both local and cloud modes (idempotent)\n# ============================================================\nif [ \"${AGENTTEAMS_RUNTIME}\" = \"k8s\" ]; then\n log \"K8s mode: skipping admin DM room creation and welcome message (both handled by agentteams-controller)\"\nelse\nMANAGER_FULL_ID=\"@manager:${MATRIX_DOMAIN}\"\nADMIN_FULL_ID=\"@${AGENTTEAMS_ADMIN_USER}:${MATRIX_DOMAIN}\"\n\nlog \"Logging in as admin to create DM room...\"\n_ADMIN_LOGIN=$(curl -sf -X POST \"${AGENTTEAMS_MATRIX_URL}/_matrix/client/v3/login\" \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"type\": \"m.login.password\",\n \"identifier\": {\"type\": \"m.id.user\", \"user\": \"'\"${AGENTTEAMS_ADMIN_USER}\"'\"},\n \"password\": \"'\"${AGENTTEAMS_ADMIN_PASSWORD}\"'\"\n }' 2>&1) || true\n\nADMIN_MATRIX_TOKEN=$(echo \"${_ADMIN_LOGIN}\" | jq -r '.access_token // empty' 2>/dev/null)\nif [ -z \"${ADMIN_MATRIX_TOKEN}\" ]; then\n log \"WARNING: Failed to login as admin, skipping DM room creation\"\nelse\n # Search for existing DM room with Manager (idempotent)\n DM_ROOM_ID=\"\"\n _JOINED_ROOMS=$(curl -sf \"${AGENTTEAMS_MATRIX_URL}/_matrix/client/v3/joined_rooms\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" 2>/dev/null \\\n | jq -r '.joined_rooms[]' 2>/dev/null) || true\n for _rid in ${_JOINED_ROOMS}; do\n _members=$(curl -sf \"${AGENTTEAMS_MATRIX_URL}/_matrix/client/v3/rooms/${_rid}/members\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" 2>/dev/null \\\n | jq -r '.chunk[].state_key' 2>/dev/null) || continue\n _count=$(echo \"${_members}\" | wc -l | xargs)\n if [ \"${_count}\" = \"2\" ] && echo \"${_members}\" | grep -q \"@manager:\"; then\n DM_ROOM_ID=\"${_rid}\"\n break\n fi\n done\n\n if [ -n \"${DM_ROOM_ID}\" ]; then\n log \"Existing DM room found: ${DM_ROOM_ID}\"\n else\n log \"Creating DM room with Manager...\"\n _RAW=$(curl -s -w '\\nHTTP_CODE:%{http_code}' -X POST \"${AGENTTEAMS_MATRIX_URL}/_matrix/client/v3/createRoom\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d \"{\\\"is_direct\\\":true,\\\"invite\\\":[\\\"${MANAGER_FULL_ID}\\\"],\\\"preset\\\":\\\"trusted_private_chat\\\"}\" 2>&1) || true\n _HTTP_CODE=$(echo \"${_RAW}\" | tail -1 | sed 's/HTTP_CODE://')\n _CREATE_RESP=$(echo \"${_RAW}\" | sed '$d')\n DM_ROOM_ID=$(echo \"${_CREATE_RESP}\" | jq -r '.room_id // empty' 2>/dev/null)\n if [ -n \"${DM_ROOM_ID}\" ]; then\n log \"DM room created: ${DM_ROOM_ID}\"\n else\n log \"WARNING: Failed to create DM room (HTTP ${_HTTP_CODE}): ${_CREATE_RESP}\"\n fi\n fi\n\n # Persist admin DM room ID to state.json\n if [ -n \"${DM_ROOM_ID}\" ]; then\n STATE_SCRIPT=\"/opt/agentteams/agent/skills/task-management/scripts/manage-state.sh\"\n if [ -f \"${STATE_SCRIPT}\" ]; then\n bash \"${STATE_SCRIPT}\" --action init 2>/dev/null || true\n bash \"${STATE_SCRIPT}\" --action set-admin-dm --room-id \"${DM_ROOM_ID}\" 2>/dev/null || true\n log \"Admin DM room persisted to state.json: ${DM_ROOM_ID}\"\n fi\n fi\n\n # Schedule welcome message in background (only on first boot)\n if [ -n \"${DM_ROOM_ID}\" ] && [ ! -f \"/root/manager-workspace/soul-configured\" ]; then\n log \"Scheduling welcome message (background, waiting for OpenClaw to start)...\"\n (\n _AGENTTEAMS_LANGUAGE=\"${AGENTTEAMS_LANGUAGE:-zh}\"\n _AGENTTEAMS_TIMEZONE=\"${TZ:-Asia/Shanghai}\"\n _wait=0\n _ready=false\n while [ \"${_wait}\" -lt 300 ]; do\n if curl -sf http://127.0.0.1:18799/ > /dev/null 2>&1; then\n _ready=true\n break\n fi\n sleep 3\n _wait=$((_wait + 3))\n done\n if [ \"${_ready}\" != \"true\" ]; then\n echo \"[manager] WARNING: OpenClaw gateway not ready within 300s, skipping welcome message\"\n exit 0\n fi\n # Ensure Manager has joined the DM room before sending the welcome\n # message. Without this, there is a race between OpenClaw's Matrix\n # auto-join and the message send — the message may land before Manager\n # joins, so OpenClaw's /sync never picks it up.\n _join_ok=false\n for _join_attempt in 1 2 3; do\n if curl -sf -X POST \"${AGENTTEAMS_MATRIX_URL}/_matrix/client/v3/rooms/${DM_ROOM_ID}/join\" \\\n -H \"Authorization: Bearer ${MANAGER_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d '{}' > /dev/null 2>&1; then\n echo \"[manager] Manager joined DM room before welcome message\"\n _join_ok=true\n break\n fi\n sleep 2\n done\n if [ \"${_join_ok}\" != \"true\" ]; then\n echo \"[manager] WARNING: Manager join request failed after 3 attempts (may already be joined)\"\n fi\n _welcome_msg=\"This is an automated message from the AgentTeams setup. This is a fresh installation.\n\n--- Installation Context ---\nUser Language: ${_AGENTTEAMS_LANGUAGE} (zh = Chinese, en = English)\nUser Timezone: ${_AGENTTEAMS_TIMEZONE} (IANA timezone identifier)\n---\n\nYou are an AI agent that manages a team of worker agents. Your identity and personality have not been configured yet — the human admin is about to meet you for the first time.\n\nPlease begin the onboarding conversation:\n\n1. Greet the admin warmly and briefly describe what you can do (coordinate workers, manage tasks, run multi-agent projects)\n2. The user has selected \\\"${_AGENTTEAMS_LANGUAGE}\\\" as their preferred language during installation. Use this language for your greeting and all subsequent communication.\n3. The user's timezone is ${_AGENTTEAMS_TIMEZONE}. Based on this timezone, you may infer their likely region and suggest additional language options.\n4. Ask them: a) What would they like to call you? b) Communication style preference? c) Any behavior guidelines? d) Confirm default language\n5. After they reply, write their preferences to ~/SOUL.md\n6. Confirm what you wrote, and ask if they would like to adjust anything\n7. Once confirmed, run: touch ~/soul-configured\n\nThe human admin will start chatting shortly.\"\n _txn_id=\"welcome-$(date +%s)\"\n _payload=$(jq -nc --arg body \"${_welcome_msg}\" '{\"msgtype\":\"m.text\",\"body\":$body}')\n _raw=$(curl -s -w '\\nHTTP_CODE:%{http_code}' -X PUT \"${AGENTTEAMS_MATRIX_URL}/_matrix/client/v3/rooms/${DM_ROOM_ID}/send/m.room.message/${_txn_id}\" \\\n -H \"Authorization: Bearer ${ADMIN_MATRIX_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d \"${_payload}\" 2>&1) || true\n _http_code=$(echo \"${_raw}\" | tail -1 | sed 's/HTTP_CODE://')\n _send_resp=$(echo \"${_raw}\" | sed '$d')\n if echo \"${_send_resp}\" | jq -e '.event_id' > /dev/null 2>&1; then\n echo \"[manager] Welcome message sent to DM room\"\n else\n echo \"[manager] WARNING: Failed to send welcome message (HTTP ${_http_code}): ${_send_resp}\"\n fi\n ) &\n log \"Welcome message background process started (PID: $!)\"\n fi\nfi\nfi # end K8s mode skip for admin DM room\n\n# ============================================================\n# Generate Manager Agent openclaw.json from template\n# ============================================================\nlog \"Generating Manager openclaw.json...\"\nexport MANAGER_MATRIX_TOKEN=\"${MANAGER_TOKEN}\"\nexport MANAGER_GATEWAY_KEY=\"${AGENTTEAMS_MANAGER_GATEWAY_KEY}\"\n# Resolve model parameters based on model name\nMODEL_NAME=\"${AGENTTEAMS_DEFAULT_MODEL:-qwen3.6-plus}\"\ncase \"${MODEL_NAME}\" in\n gpt-5.3-codex|gpt-5-mini|gpt-5-nano)\n export MODEL_CONTEXT_WINDOW=400000 MODEL_MAX_TOKENS=128000 ;;\n claude-opus-4-6)\n export MODEL_CONTEXT_WINDOW=1000000 MODEL_MAX_TOKENS=128000 ;;\n claude-sonnet-4-6)\n export MODEL_CONTEXT_WINDOW=1000000 MODEL_MAX_TOKENS=64000 ;;\n claude-haiku-4-5)\n export MODEL_CONTEXT_WINDOW=200000 MODEL_MAX_TOKENS=64000 ;;\n qwen3.6-plus|qwen3.5-plus)\n export MODEL_CONTEXT_WINDOW=200000 MODEL_MAX_TOKENS=64000 ;;\n deepseek-chat|deepseek-reasoner|kimi-k2.5)\n export MODEL_CONTEXT_WINDOW=256000 MODEL_MAX_TOKENS=128000 ;;\n glm-5|MiniMax-M2.7|MiniMax-M2.7-highspeed|MiniMax-M2.5)\n export MODEL_CONTEXT_WINDOW=200000 MODEL_MAX_TOKENS=128000 ;;\n *)\n export MODEL_CONTEXT_WINDOW=150000 MODEL_MAX_TOKENS=128000 ;;\nesac\nexport MODEL_REASONING=true\n\n# Override with user-supplied custom model parameters from env (set during install)\n[ -n \"${AGENTTEAMS_MODEL_CONTEXT_WINDOW:-}\" ] && export MODEL_CONTEXT_WINDOW=\"${AGENTTEAMS_MODEL_CONTEXT_WINDOW}\"\n[ -n \"${AGENTTEAMS_MODEL_MAX_TOKENS:-}\" ] && export MODEL_MAX_TOKENS=\"${AGENTTEAMS_MODEL_MAX_TOKENS}\"\n[ -n \"${AGENTTEAMS_MODEL_REASONING:-}\" ] && export MODEL_REASONING=\"${AGENTTEAMS_MODEL_REASONING}\"\n\n# E2EE: convert AGENTTEAMS_MATRIX_E2EE to JSON boolean for template substitution\nif [ \"${AGENTTEAMS_MATRIX_E2EE:-0}\" = \"1\" ] || [ \"${AGENTTEAMS_MATRIX_E2EE:-}\" = \"true\" ]; then\n export MATRIX_E2EE_ENABLED=true\nelse\n export MATRIX_E2EE_ENABLED=false\nfi\nlog \"Matrix E2EE: ${MATRIX_E2EE_ENABLED}\"\n\n# Resolve input modalities: only vision-capable models get \"image\"\ncase \"${MODEL_NAME}\" in\n gpt-5.4|gpt-5.3-codex|gpt-5-mini|gpt-5-nano|claude-opus-4-6|claude-sonnet-4-6|claude-haiku-4-5|qwen3.6-plus|qwen3.5-plus|kimi-k2.5)\n export MODEL_INPUT='[\"text\", \"image\"]' ;;\n *)\n export MODEL_INPUT='[\"text\"]' ;;\nesac\n# Override with user-supplied vision setting from env\nif [ \"${AGENTTEAMS_MODEL_VISION:-}\" = \"true\" ]; then\n export MODEL_INPUT='[\"text\", \"image\"]'\nelif [ \"${AGENTTEAMS_MODEL_VISION:-}\" = \"false\" ]; then\n export MODEL_INPUT='[\"text\"]'\nfi\n\nlog \"Model: ${MODEL_NAME} (context=${MODEL_CONTEXT_WINDOW}, maxTokens=${MODEL_MAX_TOKENS}, reasoning=${MODEL_REASONING}, input=${MODEL_INPUT})\"\n\nif [ -f /root/manager-workspace/openclaw.json ]; then\n log \"Manager openclaw.json already exists, updating dynamic fields only (preserving user customizations)...\"\n # Merge known models into existing config (add missing, preserve user-added)\n # Use known-models.json (valid JSON) instead of template (contains ${VAR} placeholders)\n KNOWN_MODELS=$(cat /opt/agentteams/configs/known-models.json 2>/dev/null || echo '[]')\n jq --arg token \"${MANAGER_TOKEN}\" \\\n --arg key \"${AGENTTEAMS_MANAGER_GATEWAY_KEY}\" \\\n --arg model \"${MODEL_NAME}\" \\\n --arg emb_model \"${AGENTTEAMS_EMBEDDING_MODEL}\" \\\n --arg aigw_domain \"${AI_GATEWAY_DOMAIN}\" \\\n --arg matrix_user_id \"@manager:${MATRIX_DOMAIN}\" \\\n --argjson e2ee \"${MATRIX_E2EE_ENABLED}\" \\\n --argjson known_models \"${KNOWN_MODELS}\" \\\n --argjson ctx \"${MODEL_CONTEXT_WINDOW}\" \\\n --argjson max \"${MODEL_MAX_TOKENS}\" \\\n --argjson reasoning \"${MODEL_REASONING}\" \\\n --argjson input \"${MODEL_INPUT}\" \\\n '\n # Merge known models: add any model id not already present\n .models.providers[\"agentteams-gateway\"].models as $existing\n | ($existing | map(.id)) as $existing_ids\n | ($known_models | map(select(.id as $id | $existing_ids | index($id) | not))) as $new\n | .models.providers[\"agentteams-gateway\"].models = ($existing + $new)\n # Ensure the user-chosen default model is in the list (custom model support)\n | if (.models.providers[\"agentteams-gateway\"].models | map(.id) | index($model) | not) then\n .models.providers[\"agentteams-gateway\"].models += [{\"id\": $model, \"name\": $model, \"reasoning\": $reasoning, \"contextWindow\": $ctx, \"maxTokens\": $max, \"input\": $input}]\n else . end\n # Rebuild model aliases from the full models list\n | (.models.providers[\"agentteams-gateway\"].models | map({ (\"agentteams-gateway/\" + .id): { \"alias\": .id } }) | add // {}) as $aliases\n | .agents.defaults.models = ((.agents.defaults.models // {}) + $aliases)\n | .channels.matrix.accessToken = $token | .channels.matrix.userId = $matrix_user_id | .models.providers[\"agentteams-gateway\"].apiKey = $key\n | ((.hooks.token // \"\") as $ht | if $ht == $key or $ht == ($key + \"-hooks\" | @base64) then del(.hooks) else . end)\n | .agents.defaults.model.primary = (\"agentteams-gateway/\" + $model)\n | .commands.restart = true\n | .gateway.port = 18799\n | .gateway.bind = \"lan\"\n | .gateway.controlUi = ((.gateway.controlUi // {}) + {\"dangerouslyDisableDeviceAuth\": true, \"allowInsecureAuth\": true, \"allowedOrigins\": [\"*\"]})\n | .channels.matrix.encryption = $e2ee\n | .channels.matrix.network = ((.channels.matrix.network // {}) + {\"dangerouslyAllowPrivateNetwork\": true})\n | .channels.matrix.autoJoin = \"always\"\n # OpenClaw YOLO defaults: host exec without approval prompts (see openclaw docs tools/exec-approvals)\n | .tools = (.tools // {})\n | .tools.exec = ((.tools.exec // {}) + {\"host\":\"gateway\",\"security\":\"full\",\"ask\":\"off\"})\n | .tools.elevated = (.tools.elevated // {})\n | .tools.elevated.enabled = true\n | .tools.elevated.allowFrom |= ((. // {}) | .matrix = [\"*\"])\n | .agents.defaults.elevatedDefault = \"full\"\n # Ensure memorySearch config exists (embedding model for memory) — skip if embedding model is empty\n | if $emb_model != \"\" then .agents.defaults.memorySearch //= {\"provider\":\"openai\",\"model\":$emb_model,\"remote\":{\"baseUrl\":(\"http://\" + $aigw_domain + \":8080/v1\"),\"apiKey\":$key}} else . end\n ' \\\n /root/manager-workspace/openclaw.json > /tmp/openclaw.json.tmp && \\\n mv /tmp/openclaw.json.tmp /root/manager-workspace/openclaw.json\n # Disable openclaw's observe-recovery mechanism which compares config against\n # a lastKnownGood baseline in config-health.json. When meta is missing from the\n # current file but present in the baseline, observe-recovery restores from .bak,\n # undoing user customizations (plugins, channels, etc).\n # Clearing config-health.json removes the baseline so observe-recovery won't\n # interfere, while preserving .bak as a backup.\n rm -f /root/manager-workspace/.openclaw/logs/config-health.json\n # Verify the token was written correctly\n _written_token=$(jq -r '.channels.matrix.accessToken' /root/manager-workspace/openclaw.json 2>/dev/null)\n if [ -z \"${_written_token}\" ] || [ \"${_written_token}\" = \"null\" ]; then\n log \"ERROR: Matrix token was not written correctly to openclaw.json (got: ${_written_token})\"\n else\n log \"Matrix token written to openclaw.json (prefix: ${_written_token:0:10}...)\"\n fi\nelse\n log \"Manager openclaw.json not found, generating from template...\"\n envsubst < /opt/agentteams/configs/manager-openclaw.json.tmpl > /root/manager-workspace/openclaw.json\n # Post-envsubst injection: memorySearch + custom model (single jq pass when possible)\n if ! jq -e --arg model \"${MODEL_NAME}\" '.models.providers[\"agentteams-gateway\"].models | map(.id) | index($model)' /root/manager-workspace/openclaw.json > /dev/null 2>&1; then\n log \"Custom model '${MODEL_NAME}' not in built-in list, injecting into config...\"\n jq --arg emb_model \"${AGENTTEAMS_EMBEDDING_MODEL}\" \\\n --arg aigw_domain \"${AI_GATEWAY_DOMAIN}\" \\\n --arg key \"${AGENTTEAMS_MANAGER_GATEWAY_KEY}\" \\\n --arg model \"${MODEL_NAME}\" \\\n --argjson ctx \"${MODEL_CONTEXT_WINDOW}\" \\\n --argjson max \"${MODEL_MAX_TOKENS}\" \\\n --argjson reasoning \"${MODEL_REASONING}\" \\\n --argjson input \"${MODEL_INPUT}\" \\\n '\n (if $emb_model != \"\" then .agents.defaults.memorySearch = {\"provider\":\"openai\",\"model\":$emb_model,\"remote\":{\"baseUrl\":(\"http://\" + $aigw_domain + \":8080/v1\"),\"apiKey\":$key}} else . end)\n | .models.providers[\"agentteams-gateway\"].models += [{\"id\": $model, \"name\": $model, \"reasoning\": $reasoning, \"contextWindow\": $ctx, \"maxTokens\": $max, \"input\": $input}]\n | .agents.defaults.models += {(\"agentteams-gateway/\" + $model): {\"alias\": $model}}\n ' /root/manager-workspace/openclaw.json > /tmp/openclaw.json.tmp && \\\n mv /tmp/openclaw.json.tmp /root/manager-workspace/openclaw.json\n elif [ -n \"${AGENTTEAMS_EMBEDDING_MODEL}\" ]; then\n jq --arg emb_model \"${AGENTTEAMS_EMBEDDING_MODEL}\" \\\n --arg aigw_domain \"${AI_GATEWAY_DOMAIN}\" \\\n --arg key \"${AGENTTEAMS_MANAGER_GATEWAY_KEY}\" \\\n '.agents.defaults.memorySearch = {\"provider\":\"openai\",\"model\":$emb_model,\"remote\":{\"baseUrl\":(\"http://\" + $aigw_domain + \":8080/v1\"),\"apiKey\":$key}}' \\\n /root/manager-workspace/openclaw.json > /tmp/openclaw.json.tmp && \\\n mv /tmp/openclaw.json.tmp /root/manager-workspace/openclaw.json\n fi\n _written_token=$(jq -r '.channels.matrix.accessToken' /root/manager-workspace/openclaw.json 2>/dev/null)\n log \"Matrix token written from template (prefix: ${_written_token:0:10}...)\"\nfi\n\n# Cloud/K8s mode: overlay cloud-specific settings onto generated config\nif [ \"${AGENTTEAMS_RUNTIME}\" = \"aliyun\" ] || [ \"${AGENTTEAMS_RUNTIME}\" = \"k8s\" ]; then\n log \"Applying cloud/k8s overlay to openclaw.json...\"\n jq --arg homeserver \"${AGENTTEAMS_MATRIX_URL}\" \\\n --arg gateway \"${AGENTTEAMS_AI_GATEWAY_URL}/v1\" \\\n --arg key \"${AGENTTEAMS_MANAGER_GATEWAY_KEY}\" \\\n '.channels.matrix.homeserver = $homeserver\n | .models.providers[\"agentteams-gateway\"].baseUrl = $gateway\n | .models.providers[\"agentteams-gateway\"].apiKey = $key\n | ((.hooks.token // \"\") as $ht | if $ht == $key or $ht == ($key + \"-hooks\" | @base64) then del(.hooks) else . end)\n | .commands.restart = false\n | if .agents.defaults.memorySearch then .agents.defaults.memorySearch.remote.baseUrl = $gateway | .agents.defaults.memorySearch.remote.apiKey = $key else . end' \\\n /root/manager-workspace/openclaw.json > /tmp/openclaw-cloud.json && \\\n mv /tmp/openclaw-cloud.json /root/manager-workspace/openclaw.json\n log \"Cloud/K8s overlay applied\"\nfi\n\n# ============================================================\n# Optional: enable openclaw-cms-plugin observability\n# Config is applied at runtime so secrets stay out of image layers.\n# ============================================================\nCMS_TRACES_ENABLED=\"$(echo \"${AGENTTEAMS_CMS_TRACES_ENABLED:-false}\" | tr '[:upper:]' '[:lower:]')\"\nif [ \"${CMS_TRACES_ENABLED}\" = \"true\" ]; then\n CMS_PLUGIN_NAME=\"openclaw-cms-plugin\"\n CMS_PLUGIN_DIR=\"${OPENCLAW_CMS_PLUGIN_DIR:-/opt/openclaw/extensions/openclaw-cms-plugin}\"\n CMS_PLUGIN_MANIFEST=\"${CMS_PLUGIN_DIR}/openclaw.plugin.json\"\n DIAG_PLUGIN_NAME=\"diagnostics-otel\"\n DIAG_PLUGIN_DIR=\"/opt/openclaw/extensions/diagnostics-otel\"\n CMS_LICENSE_KEY=\"${AGENTTEAMS_CMS_LICENSE_KEY:-}\"\n CMS_PROJECT=\"${AGENTTEAMS_CMS_PROJECT:-}\"\n CMS_METRICS_ENABLED=\"${AGENTTEAMS_CMS_METRICS_ENABLED:-false}\"\n\n if [ ! -f \"${CMS_PLUGIN_MANIFEST}\" ]; then\n log \"WARNING: ${CMS_PLUGIN_NAME} manifest not found at ${CMS_PLUGIN_MANIFEST}, skipping plugin config\"\n else\n _missing=0\n [ -z \"${AGENTTEAMS_CMS_ENDPOINT:-}\" ] && log \"WARNING: AGENTTEAMS_CMS_ENDPOINT is required when AGENTTEAMS_CMS_TRACES_ENABLED=true\" && _missing=1\n [ -z \"${CMS_LICENSE_KEY:-}\" ] && log \"WARNING: AGENTTEAMS_CMS_LICENSE_KEY is required when AGENTTEAMS_CMS_TRACES_ENABLED=true\" && _missing=1\n [ -z \"${AGENTTEAMS_CMS_WORKSPACE:-}\" ] && log \"WARNING: AGENTTEAMS_CMS_WORKSPACE is required when AGENTTEAMS_CMS_TRACES_ENABLED=true\" && _missing=1\n\n if [ \"${_missing}\" = \"0\" ]; then\n CMS_SERVICE_NAME=\"${AGENTTEAMS_CMS_SERVICE_NAME:-agentteams-manager}\"\n CMS_ENABLE_METRICS=\"${CMS_METRICS_ENABLED}\"\n DIAG_AVAILABLE=\"0\"\n _metrics_lc=\"$(echo \"${CMS_ENABLE_METRICS}\" | tr '[:upper:]' '[:lower:]')\"\n if [ \"${_metrics_lc}\" = \"true\" ]; then\n if [ -f \"${DIAG_PLUGIN_DIR}/package.json\" ]; then\n DIAG_AVAILABLE=\"1\"\n if [ ! -d \"${DIAG_PLUGIN_DIR}/node_modules\" ]; then\n log \"diagnostics-otel dependencies missing, installing...\"\n if (cd \"${DIAG_PLUGIN_DIR}\" && npm install --omit=dev --ignore-scripts >/tmp/agentteams-diag-install.log 2>&1); then\n log \"diagnostics-otel dependencies installed\"\n else\n log \"WARNING: diagnostics-otel npm install failed, metrics plugin may not load\"\n fi\n else\n log \"diagnostics-otel dependencies already present\"\n fi\n else\n log \"WARNING: diagnostics-otel package.json not found at ${DIAG_PLUGIN_DIR}, metrics plugin may not load\"\n fi\n fi\n\n log \"Applying ${CMS_PLUGIN_NAME} config to openclaw.json...\"\n jq --arg pluginName \"${CMS_PLUGIN_NAME}\" \\\n --arg pluginDir \"${CMS_PLUGIN_DIR}\" \\\n --arg endpoint \"${AGENTTEAMS_CMS_ENDPOINT}\" \\\n --arg licenseKey \"${CMS_LICENSE_KEY}\" \\\n --arg armsProject \"${CMS_PROJECT}\" \\\n --arg cmsWorkspace \"${AGENTTEAMS_CMS_WORKSPACE}\" \\\n --arg serviceName \"${CMS_SERVICE_NAME}\" \\\n --arg diagPluginName \"${DIAG_PLUGIN_NAME}\" \\\n --arg diagPluginDir \"${DIAG_PLUGIN_DIR}\" \\\n --arg metricsRaw \"${CMS_ENABLE_METRICS}\" \\\n --arg diagAvailableRaw \"${DIAG_AVAILABLE}\" \\\n '\n .plugins = (.plugins // {})\n | .plugins.load = (.plugins.load // {})\n | .plugins.entries = (.plugins.entries // {})\n | if (.plugins.allow | type) != \"array\" then .plugins.allow = [] else . end\n | if (.plugins.allow | index($pluginName)) == null then .plugins.allow += [$pluginName] else . end\n | if (.plugins.load.paths | type) != \"array\" then .plugins.load.paths = [] else . end\n | if (.plugins.load.paths | index($pluginDir)) == null then .plugins.load.paths += [$pluginDir] else . end\n | .plugins.entries[$pluginName] = {\n \"enabled\": true,\n \"config\": {\n \"endpoint\": $endpoint,\n \"headers\": {\n \"x-arms-license-key\": $licenseKey,\n \"x-arms-project\": $armsProject,\n \"x-cms-workspace\": $cmsWorkspace\n },\n \"serviceName\": $serviceName\n }\n }\n\n # diagnostics-otel metrics (optional)\n | ($metricsRaw | ascii_downcase) as $m\n | ($diagAvailableRaw == \"1\") as $diagAvailable\n | (($m == \"true\") and $diagAvailable) as $metricsEnabled\n | if $metricsEnabled then\n (if (.plugins.allow | index($diagPluginName)) == null then .plugins.allow += [$diagPluginName] else . end)\n | (if (.plugins.load.paths | index($diagPluginDir)) == null then .plugins.load.paths += [$diagPluginDir] else . end)\n | .plugins.entries[$diagPluginName].enabled = true\n | .diagnostics = (.diagnostics // {})\n | .diagnostics.otel = (.diagnostics.otel // {})\n | .diagnostics.enabled = true\n | .diagnostics.otel.enabled = true\n | .diagnostics.otel.endpoint = $endpoint\n | .diagnostics.otel.protocol = (.diagnostics.otel.protocol // \"http/protobuf\")\n | .diagnostics.otel.headers = {\n \"x-arms-license-key\": $licenseKey,\n \"x-arms-project\": $armsProject,\n \"x-cms-workspace\": $cmsWorkspace\n }\n | .diagnostics.otel.serviceName = $serviceName\n | .diagnostics.otel.metrics = true\n | .diagnostics.otel.traces = (.diagnostics.otel.traces // false)\n | .diagnostics.otel.logs = (.diagnostics.otel.logs // false)\n else\n .\n end\n ' /root/manager-workspace/openclaw.json > /tmp/openclaw-cms.json && \\\n mv /tmp/openclaw-cms.json /root/manager-workspace/openclaw.json\n log \"${CMS_PLUGIN_NAME} config applied (metrics=${CMS_ENABLE_METRICS}, service=${CMS_SERVICE_NAME})\"\n else\n log \"Skipping ${CMS_PLUGIN_NAME} config due to missing required env vars\"\n fi\n fi\nfi\n\n# ============================================================\n# Detect container runtime (for Worker creation)\n# ============================================================\nsource /opt/agentteams/scripts/lib/container-api.sh\nif container_api_available; then\n log \"Container runtime socket detected at ${CONTAINER_SOCKET} — direct Worker creation enabled\"\n export AGENTTEAMS_CONTAINER_RUNTIME=\"socket\"\nelif [ \"${AGENTTEAMS_RUNTIME}\" = \"aliyun\" ] || [ \"${AGENTTEAMS_RUNTIME}\" = \"k8s\" ]; then\n log \"Cloud/K8s mode — Workers created via controller API\"\n export AGENTTEAMS_CONTAINER_RUNTIME=\"cloud\"\nelse\n log \"No container runtime found — Worker creation will output install commands\"\n export AGENTTEAMS_CONTAINER_RUNTIME=\"none\"\nfi\n\n# ============================================================\n# Recreate Worker containers as needed after Manager restart.\n# Workers are on agentteams-net; Docker DNS resolves *-local.agentteams.io via\n# the Manager's network aliases, so IP changes don't require worker recreation.\n# Only recreate stopped/missing workers.\n# ============================================================\nif container_api_available; then\n _workers_json=$(agt get workers -o json 2>/dev/null || echo '{\"workers\":[]}')\n for _worker_name in $(echo \"${_workers_json}\" | jq -r '.workers[].name'); do\n [ -z \"${_worker_name}\" ] && continue\n\n _status=$(container_status_worker \"${_worker_name}\")\n if [ \"${_status}\" = \"running\" ]; then\n log \"Worker running: ${_worker_name}, skipping\"\n continue\n fi\n # Container missing or stopped — recreate.\n log \"Worker container ${_status}: ${_worker_name}, recreating...\"\n _creds_file=\"/data/worker-creds/${_worker_name}.env\"\n if [ -f \"${_creds_file}\" ]; then\n source \"${_creds_file}\"\n _runtime=$(echo \"${_workers_json}\" | jq -r --arg w \"${_worker_name}\" '.workers[] | select(.name == $w) | .runtime // \"openclaw\"')\n _recreated=false\n for _attempt in 1 2 3; do\n _env_map=\"\"\n _create_body=\"\"\n _env_map=$(jq -cn \\\n --arg name \"${_worker_name}\" \\\n --arg fak \"${_worker_name}\" \\\n --arg fsk \"${WORKER_MINIO_PASSWORD:-}\" \\\n --arg fs_domain \"${AGENTTEAMS_FS_DOMAIN:-fs-local.agentteams.io}\" \\\n --arg controller_url \"${AGENTTEAMS_CONTROLLER_URL:-}\" \\\n '{\n \"AGENTTEAMS_WORKER_NAME\": $name,\n \"AGENTTEAMS_FS_ENDPOINT\": (\"http://\" + ($fs_domain | split(\":\")[0]) + \":9000\"),\n \"AGENTTEAMS_FS_ACCESS_KEY\": $fak,\n \"AGENTTEAMS_FS_SECRET_KEY\": $fsk\n }\n | if $controller_url != \"\" then . + {\"AGENTTEAMS_CONTROLLER_URL\": $controller_url} else . end')\n _create_body=$(jq -cn --arg name \"${_worker_name}\" --arg runtime \"${_runtime}\" --argjson env \"${_env_map}\" '{name: $name, runtime: $runtime, env: $env}')\n worker_backend_create \"${_create_body}\" > /dev/null 2>&1 && _recreated=true && break\n log \" Attempt ${_attempt}/3 failed for ${_worker_name}, retrying in $((5 * _attempt))s...\"\n sleep $((5 * _attempt))\n done\n if [ \"${_recreated}\" = true ]; then\n log \" Recreated ${_runtime} worker: ${_worker_name}\"\n else\n log \" ERROR: Failed to recreate ${_worker_name} after 3 attempts\"\n fi\n else\n log \" WARNING: No credentials found for ${_worker_name} (${_creds_file} missing), skipping\"\n fi\n done\nfi\n\n# ============================================================\n# Notify workers of builtin updates if upgrade happened\n# Builtin files (AGENTS.md, skills) are already synced by upgrade-builtins.sh\n#\n# Cooldown: skip notification if the last successful notify was within\n# NOTIFY_COOLDOWN_SECS (default 3600s / 1 hour). This prevents repeated\n# notifications when the Manager crash-loops and re-runs upgrade-builtins\n# on every restart (e.g. IMAGE_VERSION=latest always triggers upgrade).\n# ============================================================\nNOTIFY_COOLDOWN_SECS=\"${AGENTTEAMS_NOTIFY_COOLDOWN_SECS:-3600}\"\nNOTIFY_TS_FILE=\"/root/manager-workspace/.last-worker-notify-ts\"\n\nif [ -f /root/manager-workspace/.upgrade-pending-worker-notify ]; then\n _now=$(date +%s)\n _last_notify=$(cat \"${NOTIFY_TS_FILE}\" 2>/dev/null || echo \"0\")\n _elapsed=$(( _now - _last_notify ))\n\n if [ \"${_elapsed}\" -lt \"${NOTIFY_COOLDOWN_SECS}\" ]; then\n log \"Skipping worker builtin notification (last notify ${_elapsed}s ago, cooldown ${NOTIFY_COOLDOWN_SECS}s)\"\n rm -f /root/manager-workspace/.upgrade-pending-worker-notify\n else\n log \"Notifying workers about builtin updates...\"\n _notify_ok=false\n _workers_json=$(agt get workers -o json 2>/dev/null || echo '{\"workers\":[]}')\n for _worker_name in $(echo \"${_workers_json}\" | jq -r '.workers[].name'); do\n [ -z \"${_worker_name}\" ] && continue\n _room_id=$(echo \"${_workers_json}\" | jq -r --arg w \"${_worker_name}\" '.workers[] | select(.name == $w) | .roomID // empty')\n if [ -n \"${_room_id}\" ]; then\n _worker_id=\"@${_worker_name}:${MATRIX_DOMAIN}\"\n _txn_id=\"upgrade-$(date +%s%N)\"\n _msg=\"@${_worker_name}:${MATRIX_DOMAIN} Manager upgraded builtin files (AGENTS.md, skills). Please use your file-sync skill to sync the latest config.\"\n _raw=$(curl -s -w '\\nHTTP_CODE:%{http_code}' -X PUT \\\n \"${AGENTTEAMS_MATRIX_URL}/_matrix/client/v3/rooms/${_room_id}/send/m.room.message/${_txn_id}\" \\\n -H \"Authorization: Bearer ${MANAGER_TOKEN}\" \\\n -H 'Content-Type: application/json' \\\n -d \"{\\\"msgtype\\\":\\\"m.text\\\",\\\"body\\\":\\\"${_msg}\\\",\\\"m.mentions\\\":{\\\"user_ids\\\":[\\\"${_worker_id}\\\"]}}\" \\\n 2>&1) || true\n _http_code=$(echo \"${_raw}\" | tail -1 | sed 's/HTTP_CODE://')\n _notify_resp=$(echo \"${_raw}\" | sed '$d')\n if echo \"${_notify_resp}\" | jq -e '.event_id' > /dev/null 2>&1; then\n log \" Notified ${_worker_name}\"; _notify_ok=true\n else\n log \" WARNING: Failed to notify ${_worker_name} (HTTP ${_http_code}): ${_notify_resp}\"\n fi\n fi\n done\n # Record timestamp only if at least one notification succeeded\n if [ \"${_notify_ok}\" = true ]; then\n echo \"${_now}\" > \"${NOTIFY_TS_FILE}\"\n fi\n rm -f /root/manager-workspace/.upgrade-pending-worker-notify\n fi\nfi\n\n# ============================================================\n# Start Manager Agent\n# ============================================================\nlog \"Starting Manager Agent (${MANAGER_RUNTIME})...\"\n\n# HOME is already set to /root/manager-workspace via docker run -e HOME=...\ncd \"${HOME}\"\n\n# Ensure host credential symlinks exist under HOME\nif [ -d \"/host-share\" ]; then\n [ -f \"/host-share/.gitconfig\" ] && ln -sf \"/host-share/.gitconfig\" \"${HOME}/.gitconfig\"\nfi\n\nlog \"HOME=${HOME} (manager-workspace, host-mounted)\"\n\n# ── Render agent doc templates ────────────────────────────────────────────\n# Replace ${VAR} placeholders with actual values so the AI agent reads\n# plain text and never needs to resolve environment variables.\nexport MANAGER_MATRIX_TOKEN MANAGER_TOKEN HIGRESS_COOKIE_FILE\nRENDER=/opt/agentteams/scripts/lib/render-skills.sh\nlog \"Rendering agent doc templates...\"\n# Manager-owned docs (workspace)\nbash \"$RENDER\" /root/manager-workspace/skills\nbash \"$RENDER\" /root/manager-workspace/skills-alpha\nbash \"$RENDER\" /root/manager-workspace AGENTS.md TOOLS.md HEARTBEAT.md SOUL.md\n# Worker templates (workspace + image) — rendered before push to MinIO\n# so Workers (including remote pip-install) receive plain text\nbash \"$RENDER\" /root/manager-workspace/worker-skills\nbash \"$RENDER\" /root/manager-workspace/worker-agent\nbash \"$RENDER\" /root/manager-workspace/copaw-worker-agent\nbash \"$RENDER\" /root/manager-workspace/hermes-worker-agent\nbash \"$RENDER\" /opt/agentteams/agent/worker-skills\nbash \"$RENDER\" /opt/agentteams/agent/worker-agent\nbash \"$RENDER\" /opt/agentteams/agent/copaw-worker-agent\nbash \"$RENDER\" /opt/agentteams/agent/hermes-worker-agent\nlog \"Agent doc templates rendered\"\n\n# Cloud mode: start background file sync (workspace ↔ OSS) and initial push\nif [ \"${AGENTTEAMS_RUNTIME}\" = \"aliyun\" ]; then\n log \"Syncing initial workspace to OSS...\"\n ensure_mc_credentials\n mc mirror /root/manager-workspace/ \"${AGENTTEAMS_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" 2>/dev/null || true\n\n # Local → OSS: change-triggered sync\n (\n while true; do\n CHANGED=$(find /root/manager-workspace/ -type f -newermt \"15 seconds ago\" 2>/dev/null | head -1)\n if [ -n \"${CHANGED}\" ]; then\n ensure_mc_credentials 2>/dev/null || true\n mc mirror /root/manager-workspace/ \"${AGENTTEAMS_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" --exclude \".npm/**\" \\\n --exclude \".local/**\" --exclude \".mc/**\" 2>/dev/null || true\n fi\n sleep 10\n done\n ) &\n log \"Local→OSS sync started (PID: $!)\"\n\n # OSS → Local: periodic pull (shared data, agent configs, manager openclaw.json)\n (\n while true; do\n sleep 60\n ensure_mc_credentials 2>/dev/null || true\n mc mirror \"${AGENTTEAMS_STORAGE_PREFIX}/shared/\" /root/agentteams-fs/shared/ --overwrite --newer-than \"1m\" 2>/dev/null || true\n mc mirror \"${AGENTTEAMS_STORAGE_PREFIX}/agents/\" /root/agentteams-fs/agents/ --overwrite --newer-than \"1m\" 2>/dev/null || true\n mc cp \"${AGENTTEAMS_STORAGE_PREFIX}/manager/openclaw.json\" /root/manager-workspace/openclaw.json 2>/dev/null || true\n done\n ) &\n log \"OSS→Local sync started (every 60s, PID: $!)\"\nfi\n\n# K8s mode: start background file sync (workspace ↔ MinIO)\nif [ \"${AGENTTEAMS_RUNTIME}\" = \"k8s\" ]; then\n log \"Syncing initial workspace to MinIO...\"\n mc mirror /root/manager-workspace/ \"${AGENTTEAMS_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" 2>/dev/null || true\n\n # Local → MinIO: change-triggered sync\n (\n while true; do\n CHANGED=$(find /root/manager-workspace/ -type f -newermt \"15 seconds ago\" 2>/dev/null | head -1)\n if [ -n \"${CHANGED}\" ]; then\n mc mirror /root/manager-workspace/ \"${AGENTTEAMS_STORAGE_PREFIX}/manager/\" --overwrite \\\n --exclude \".openclaw/**\" --exclude \".cache/**\" --exclude \".npm/**\" \\\n --exclude \".local/**\" --exclude \".mc/**\" 2>/dev/null || true\n fi\n sleep 10\n done\n ) &\n log \"Local→MinIO sync started (PID: $!)\"\n\n # MinIO → Local: periodic pull (shared data, agent configs, manager openclaw.json)\n (\n while true; do\n sleep 60\n mc mirror \"${AGENTTEAMS_STORAGE_PREFIX}/shared/\" /root/agentteams-fs/shared/ --overwrite --newer-than \"1m\" 2>/dev/null || true\n mc mirror \"${AGENTTEAMS_STORAGE_PREFIX}/agents/\" /root/agentteams-fs/agents/ --overwrite --newer-than \"1m\" 2>/dev/null || true\n mc mirror \"${AGENTTEAMS_STORAGE_PREFIX}/agentteams-config/\" /root/agentteams-fs/agentteams-config/ --overwrite --newer-than \"1m\" 2>/dev/null || true\n mc cp \"${AGENTTEAMS_STORAGE_PREFIX}/manager/openclaw.json\" /root/manager-workspace/openclaw.json 2>/dev/null || true\n done\n ) &\n log \"MinIO→Local sync started (every 60s, PID: $!)\"\nfi\n\n# ============================================================\n# Auto-generate Manager mcporter config for pre-configured MCP servers\n# If AGENTTEAMS_GITHUB_TOKEN was set at install time, setup-higress.sh already\n# configured GitHub MCP on Higress. Run setup-mcp-server.sh now so that\n# config/mcporter.json exists when the Agent starts — no need to ask user for PAT.\n# ============================================================\nif [ -n \"${AGENTTEAMS_GITHUB_TOKEN}\" ] && [ \"${AGENTTEAMS_RUNTIME}\" != \"aliyun\" ] && [ \"${AGENTTEAMS_RUNTIME}\" != \"k8s\" ]; then\n if [ ! -f \"${HOME}/config/mcporter.json\" ]; then\n log \"Auto-generating Manager mcporter config for GitHub MCP (AGENTTEAMS_GITHUB_TOKEN set)...\"\n bash /opt/agentteams/agent/skills/mcp-server-management/scripts/setup-mcp-server.sh \\\n github \"${AGENTTEAMS_GITHUB_TOKEN}\" 2>&1 | while IFS= read -r line; do log \" [setup-mcp] ${line}\"; done || \\\n log \"WARNING: setup-mcp-server.sh failed — Agent may need to configure GitHub MCP manually\"\n else\n log \"Manager mcporter config already exists, skipping auto-generate\"\n fi\nfi\n\n# ============================================================\n# Runtime-specific startup\n# ============================================================\nif [ \"${MANAGER_RUNTIME}\" = \"copaw\" ] || [ \"${MANAGER_RUNTIME}\" = \"qwenpaw\" ]; then\n # Delegate to QwenPaw startup script\n exec /opt/agentteams/scripts/init/start-qwenpaw-manager.sh\nelse\n # ── OpenClaw Runtime ─────────────────────────────────────────────────────\n log \"Starting OpenClaw Manager...\"\n\n export OPENCLAW_CONFIG_PATH=\"/root/manager-workspace/openclaw.json\"\n\n # Symlink to default OpenClaw config path so CLI commands find the config\n mkdir -p \"${HOME}/.openclaw\"\n ln -sf \"/root/manager-workspace/openclaw.json\" \"${HOME}/.openclaw/openclaw.json\"\n\n # Clean orphaned session write locks (e.g. from SIGKILL or crash before exit handlers)\n # Prevents \"session file locked (timeout 10000ms)\" when PID was reused\n find \"${HOME}/.openclaw/agents\" -name \"*.jsonl.lock\" -delete 2>/dev/null || true\n log \"Cleaned up any orphaned session write locks\"\n\n # Clean Matrix crypto storage (SQLite WAL may be corrupted after unclean shutdown)\n # Crypto state is re-negotiated on startup; losing it only means re-establishing E2EE sessions\n rm -rf \"${HOME}/.openclaw/matrix\" 2>/dev/null || true\n log \"Cleaned Matrix crypto storage (will re-establish E2EE sessions)\"\n\n # Launch OpenClaw\n # Disable full-process respawn so the CLI uses its internal restart loop.\n # Without this, config reload spawns a detached child and exits, then\n # supervisord restarts the CLI — resulting in two gateway processes.\n export OPENCLAW_NO_RESPAWN=1\n\n # Optional matrix-plugin trace logging — when AGENTTEAMS_MATRIX_DEBUG=1 is set\n # in the manager environment (propagated by install / supervisord), turn on\n # OPENCLAW_MATRIX_DEBUG so the matrix plugin emits structured INFO-level\n # lifecycle traces (sync.state transitions, room.invite/join, message\n # handler arrival + filter outcomes). Useful for diagnosing \"worker never\n # joined the room\" / \"manager never replied\" hangs without rebuilding the\n # image.\n if [ \"${AGENTTEAMS_MATRIX_DEBUG:-}\" = \"1\" ] && [ -z \"${OPENCLAW_MATRIX_DEBUG:-}\" ]; then\n export OPENCLAW_MATRIX_DEBUG=1\n log \"AGENTTEAMS_MATRIX_DEBUG=1 detected; OPENCLAW_MATRIX_DEBUG=1 exported for matrix plugin tracing\"\n fi\n\n exec openclaw gateway run --verbose --force\nfi\n"}}}}